summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Godeps/Godeps.json5
-rw-r--r--Godeps/_workspace/src/code.google.com/p/log4go/.hgtags4
-rw-r--r--Godeps/_workspace/src/code.google.com/p/log4go/log4go_test.go534
-rw-r--r--Godeps/_workspace/src/code.google.com/p/log4go/termlog.go45
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/.gitignore2
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/LICENSE (renamed from Godeps/_workspace/src/code.google.com/p/log4go/LICENSE)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/README (renamed from Godeps/_workspace/src/code.google.com/p/log4go/README)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/config.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/config.go)4
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/examples/ConsoleLogWriter_Manual.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/examples/ConsoleLogWriter_Manual.go)1
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/examples/FileLogWriter_Manual.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/examples/FileLogWriter_Manual.go)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/examples/SimpleNetLogServer.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/examples/SimpleNetLogServer.go)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/examples/SocketLogWriter_Manual.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/examples/SocketLogWriter_Manual.go)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/examples/XMLConfigurationExample.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/examples/XMLConfigurationExample.go)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/examples/example.xml (renamed from Godeps/_workspace/src/code.google.com/p/log4go/examples/example.xml)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/filelog.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/filelog.go)53
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/log4go.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/log4go.go)32
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/pattlog.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/pattlog.go)8
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/socklog.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/socklog.go)0
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/termlog.go49
-rw-r--r--Godeps/_workspace/src/github.com/alecthomas/log4go/wrapper.go (renamed from Godeps/_workspace/src/code.google.com/p/log4go/wrapper.go)10
-rw-r--r--Makefile4
-rw-r--r--api/admin.go2
-rw-r--r--api/api.go3
-rw-r--r--api/channel.go2
-rw-r--r--api/command.go2
-rw-r--r--api/context.go3
-rw-r--r--api/file.go22
-rw-r--r--api/import.go2
-rw-r--r--api/license.go100
-rw-r--r--api/oauth.go2
-rw-r--r--api/post.go15
-rw-r--r--api/preference.go2
-rw-r--r--api/server.go2
-rw-r--r--api/slackimport.go2
-rw-r--r--api/team.go2
-rw-r--r--api/user.go35
-rw-r--r--api/web_conn.go2
-rw-r--r--api/web_hub.go2
-rw-r--r--api/web_socket.go2
-rw-r--r--api/web_team_hub.go2
-rw-r--r--api/webhook.go2
-rw-r--r--config/config.json31
-rw-r--r--doc/developer/tests/test-attachments.md27
-rw-r--r--doc/developer/tests/test-markdown-basics.md14
-rw-r--r--doc/install/Configuration-Settings.md4
-rw-r--r--manualtesting/manual_testing.go2
-rw-r--r--manualtesting/test_autolink.go2
-rw-r--r--mattermost.go6
-rw-r--r--model/client.go2
-rw-r--r--model/license.go85
-rw-r--r--model/license_test.go34
-rw-r--r--store/sql_preference_store.go2
-rw-r--r--store/sql_session_store.go2
-rw-r--r--store/sql_store.go2
-rw-r--r--store/store.go2
-rw-r--r--utils/config.go6
-rw-r--r--utils/license.go157
-rw-r--r--utils/license_test.go50
-rw-r--r--utils/mail.go2
-rw-r--r--web/react/components/about_build_modal.jsx21
-rw-r--r--web/react/components/admin_console/admin_controller.jsx3
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx32
-rw-r--r--web/react/components/admin_console/email_settings.jsx2
-rw-r--r--web/react/components/admin_console/ldap_settings.jsx32
-rw-r--r--web/react/components/admin_console/license_settings.jsx237
-rw-r--r--web/react/components/admin_console/team_analytics.jsx44
-rw-r--r--web/react/components/channel_header.jsx2
-rw-r--r--web/react/components/file_upload.jsx27
-rw-r--r--web/react/components/invite_member_modal.jsx10
-rw-r--r--web/react/components/sidebar.jsx2
-rw-r--r--web/react/components/time_since.jsx3
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx1
-rw-r--r--web/react/utils/client.jsx35
-rw-r--r--web/react/utils/constants.jsx16
-rw-r--r--web/react/utils/utils.jsx37
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss5
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss4
-rw-r--r--web/templates/head.html1
-rw-r--r--web/web.go4
80 files changed, 1168 insertions, 739 deletions
diff --git a/.gitignore b/.gitignore
index dab6b8373..5d6fc98e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@ web/static/js/bundle*.js
web/static/js/bundle*.js.map
web/static/js/libs*.js
+config/active.dat
+
# Build Targets
.prepare
.prepare-go
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index bd2392f90..d9c4a61bd 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -3,9 +3,8 @@
"GoVersion": "go1.5.1",
"Deps": [
{
- "ImportPath": "code.google.com/p/log4go",
- "Comment": "go.weekly.2012-02-22-1",
- "Rev": "c3294304d93f48a37d3bed1d382882a9c2989f99"
+ "ImportPath": "github.com/alecthomas/log4go",
+ "Rev": "8e9057c3b25c409a34c0b9737cdc82cbcafeabce"
},
{
"ImportPath": "github.com/braintree/manners",
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/.hgtags b/Godeps/_workspace/src/code.google.com/p/log4go/.hgtags
deleted file mode 100644
index 72a2eea2c..000000000
--- a/Godeps/_workspace/src/code.google.com/p/log4go/.hgtags
+++ /dev/null
@@ -1,4 +0,0 @@
-4fbe6aadba231e838a449d340e43bdaab0bf85bd go.weekly.2012-02-07
-56168fd53249d639c25c74ced881fffb20d27be9 go.weekly.2012-02-22
-56168fd53249d639c25c74ced881fffb20d27be9 go.weekly.2012-02-22
-5c22fbd77d91f54d76cdbdee05318699754c44cc go.weekly.2012-02-22
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/log4go_test.go b/Godeps/_workspace/src/code.google.com/p/log4go/log4go_test.go
deleted file mode 100644
index 90c629977..000000000
--- a/Godeps/_workspace/src/code.google.com/p/log4go/log4go_test.go
+++ /dev/null
@@ -1,534 +0,0 @@
-// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
-
-package log4go
-
-import (
- "crypto/md5"
- "encoding/hex"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "runtime"
- "testing"
- "time"
-)
-
-const testLogFile = "_logtest.log"
-
-var now time.Time = time.Unix(0, 1234567890123456789).In(time.UTC)
-
-func newLogRecord(lvl level, src string, msg string) *LogRecord {
- return &LogRecord{
- Level: lvl,
- Source: src,
- Created: now,
- Message: msg,
- }
-}
-
-func TestELog(t *testing.T) {
- fmt.Printf("Testing %s\n", L4G_VERSION)
- lr := newLogRecord(CRITICAL, "source", "message")
- if lr.Level != CRITICAL {
- t.Errorf("Incorrect level: %d should be %d", lr.Level, CRITICAL)
- }
- if lr.Source != "source" {
- t.Errorf("Incorrect source: %s should be %s", lr.Source, "source")
- }
- if lr.Message != "message" {
- t.Errorf("Incorrect message: %s should be %s", lr.Source, "message")
- }
-}
-
-var formatTests = []struct {
- Test string
- Record *LogRecord
- Formats map[string]string
-}{
- {
- Test: "Standard formats",
- Record: &LogRecord{
- Level: ERROR,
- Source: "source",
- Message: "message",
- Created: now,
- },
- Formats: map[string]string{
- // TODO(kevlar): How can I do this so it'll work outside of PST?
- FORMAT_DEFAULT: "[2009/02/13 23:31:30 UTC] [EROR] (source) message\n",
- FORMAT_SHORT: "[23:31 02/13/09] [EROR] message\n",
- FORMAT_ABBREV: "[EROR] message\n",
- },
- },
-}
-
-func TestFormatLogRecord(t *testing.T) {
- for _, test := range formatTests {
- name := test.Test
- for fmt, want := range test.Formats {
- if got := FormatLogRecord(fmt, test.Record); got != want {
- t.Errorf("%s - %s:", name, fmt)
- t.Errorf(" got %q", got)
- t.Errorf(" want %q", want)
- }
- }
- }
-}
-
-var logRecordWriteTests = []struct {
- Test string
- Record *LogRecord
- Console string
-}{
- {
- Test: "Normal message",
- Record: &LogRecord{
- Level: CRITICAL,
- Source: "source",
- Message: "message",
- Created: now,
- },
- Console: "[02/13/09 23:31:30] [CRIT] message\n",
- },
-}
-
-func TestConsoleLogWriter(t *testing.T) {
- console := make(ConsoleLogWriter)
-
- r, w := io.Pipe()
- go console.run(w)
- defer console.Close()
-
- buf := make([]byte, 1024)
-
- for _, test := range logRecordWriteTests {
- name := test.Test
-
- console.LogWrite(test.Record)
- n, _ := r.Read(buf)
-
- if got, want := string(buf[:n]), test.Console; got != want {
- t.Errorf("%s: got %q", name, got)
- t.Errorf("%s: want %q", name, want)
- }
- }
-}
-
-func TestFileLogWriter(t *testing.T) {
- defer func(buflen int) {
- LogBufferLength = buflen
- }(LogBufferLength)
- LogBufferLength = 0
-
- w := NewFileLogWriter(testLogFile, false)
- if w == nil {
- t.Fatalf("Invalid return: w should not be nil")
- }
- defer os.Remove(testLogFile)
-
- w.LogWrite(newLogRecord(CRITICAL, "source", "message"))
- w.Close()
- runtime.Gosched()
-
- if contents, err := ioutil.ReadFile(testLogFile); err != nil {
- t.Errorf("read(%q): %s", testLogFile, err)
- } else if len(contents) != 50 {
- t.Errorf("malformed filelog: %q (%d bytes)", string(contents), len(contents))
- }
-}
-
-func TestXMLLogWriter(t *testing.T) {
- defer func(buflen int) {
- LogBufferLength = buflen
- }(LogBufferLength)
- LogBufferLength = 0
-
- w := NewXMLLogWriter(testLogFile, false)
- if w == nil {
- t.Fatalf("Invalid return: w should not be nil")
- }
- defer os.Remove(testLogFile)
-
- w.LogWrite(newLogRecord(CRITICAL, "source", "message"))
- w.Close()
- runtime.Gosched()
-
- if contents, err := ioutil.ReadFile(testLogFile); err != nil {
- t.Errorf("read(%q): %s", testLogFile, err)
- } else if len(contents) != 185 {
- t.Errorf("malformed xmllog: %q (%d bytes)", string(contents), len(contents))
- }
-}
-
-func TestLogger(t *testing.T) {
- sl := NewDefaultLogger(WARNING)
- if sl == nil {
- t.Fatalf("NewDefaultLogger should never return nil")
- }
- if lw, exist := sl["stdout"]; lw == nil || exist != true {
- t.Fatalf("NewDefaultLogger produced invalid logger (DNE or nil)")
- }
- if sl["stdout"].Level != WARNING {
- t.Fatalf("NewDefaultLogger produced invalid logger (incorrect level)")
- }
- if len(sl) != 1 {
- t.Fatalf("NewDefaultLogger produced invalid logger (incorrect map count)")
- }
-
- //func (l *Logger) AddFilter(name string, level int, writer LogWriter) {}
- l := make(Logger)
- l.AddFilter("stdout", DEBUG, NewConsoleLogWriter())
- if lw, exist := l["stdout"]; lw == nil || exist != true {
- t.Fatalf("AddFilter produced invalid logger (DNE or nil)")
- }
- if l["stdout"].Level != DEBUG {
- t.Fatalf("AddFilter produced invalid logger (incorrect level)")
- }
- if len(l) != 1 {
- t.Fatalf("AddFilter produced invalid logger (incorrect map count)")
- }
-
- //func (l *Logger) Warn(format string, args ...interface{}) error {}
- if err := l.Warn("%s %d %#v", "Warning:", 1, []int{}); err.Error() != "Warning: 1 []int{}" {
- t.Errorf("Warn returned invalid error: %s", err)
- }
-
- //func (l *Logger) Error(format string, args ...interface{}) error {}
- if err := l.Error("%s %d %#v", "Error:", 10, []string{}); err.Error() != "Error: 10 []string{}" {
- t.Errorf("Error returned invalid error: %s", err)
- }
-
- //func (l *Logger) Critical(format string, args ...interface{}) error {}
- if err := l.Critical("%s %d %#v", "Critical:", 100, []int64{}); err.Error() != "Critical: 100 []int64{}" {
- t.Errorf("Critical returned invalid error: %s", err)
- }
-
- // Already tested or basically untestable
- //func (l *Logger) Log(level int, source, message string) {}
- //func (l *Logger) Logf(level int, format string, args ...interface{}) {}
- //func (l *Logger) intLogf(level int, format string, args ...interface{}) string {}
- //func (l *Logger) Finest(format string, args ...interface{}) {}
- //func (l *Logger) Fine(format string, args ...interface{}) {}
- //func (l *Logger) Debug(format string, args ...interface{}) {}
- //func (l *Logger) Trace(format string, args ...interface{}) {}
- //func (l *Logger) Info(format string, args ...interface{}) {}
-}
-
-func TestLogOutput(t *testing.T) {
- const (
- expected = "fdf3e51e444da56b4cb400f30bc47424"
- )
-
- // Unbuffered output
- defer func(buflen int) {
- LogBufferLength = buflen
- }(LogBufferLength)
- LogBufferLength = 0
-
- l := make(Logger)
-
- // Delete and open the output log without a timestamp (for a constant md5sum)
- l.AddFilter("file", FINEST, NewFileLogWriter(testLogFile, false).SetFormat("[%L] %M"))
- defer os.Remove(testLogFile)
-
- // Send some log messages
- l.Log(CRITICAL, "testsrc1", fmt.Sprintf("This message is level %d", int(CRITICAL)))
- l.Logf(ERROR, "This message is level %v", ERROR)
- l.Logf(WARNING, "This message is level %s", WARNING)
- l.Logc(INFO, func() string { return "This message is level INFO" })
- l.Trace("This message is level %d", int(TRACE))
- l.Debug("This message is level %s", DEBUG)
- l.Fine(func() string { return fmt.Sprintf("This message is level %v", FINE) })
- l.Finest("This message is level %v", FINEST)
- l.Finest(FINEST, "is also this message's level")
-
- l.Close()
-
- contents, err := ioutil.ReadFile(testLogFile)
- if err != nil {
- t.Fatalf("Could not read output log: %s", err)
- }
-
- sum := md5.New()
- sum.Write(contents)
- if sumstr := hex.EncodeToString(sum.Sum(nil)); sumstr != expected {
- t.Errorf("--- Log Contents:\n%s---", string(contents))
- t.Fatalf("Checksum does not match: %s (expecting %s)", sumstr, expected)
- }
-}
-
-func TestCountMallocs(t *testing.T) {
- const N = 1
- var m runtime.MemStats
- getMallocs := func() uint64 {
- runtime.ReadMemStats(&m)
- return m.Mallocs
- }
-
- // Console logger
- sl := NewDefaultLogger(INFO)
- mallocs := 0 - getMallocs()
- for i := 0; i < N; i++ {
- sl.Log(WARNING, "here", "This is a WARNING message")
- }
- mallocs += getMallocs()
- fmt.Printf("mallocs per sl.Log((WARNING, \"here\", \"This is a log message\"): %d\n", mallocs/N)
-
- // Console logger formatted
- mallocs = 0 - getMallocs()
- for i := 0; i < N; i++ {
- sl.Logf(WARNING, "%s is a log message with level %d", "This", WARNING)
- }
- mallocs += getMallocs()
- fmt.Printf("mallocs per sl.Logf(WARNING, \"%%s is a log message with level %%d\", \"This\", WARNING): %d\n", mallocs/N)
-
- // Console logger (not logged)
- sl = NewDefaultLogger(INFO)
- mallocs = 0 - getMallocs()
- for i := 0; i < N; i++ {
- sl.Log(DEBUG, "here", "This is a DEBUG log message")
- }
- mallocs += getMallocs()
- fmt.Printf("mallocs per unlogged sl.Log((WARNING, \"here\", \"This is a log message\"): %d\n", mallocs/N)
-
- // Console logger formatted (not logged)
- mallocs = 0 - getMallocs()
- for i := 0; i < N; i++ {
- sl.Logf(DEBUG, "%s is a log message with level %d", "This", DEBUG)
- }
- mallocs += getMallocs()
- fmt.Printf("mallocs per unlogged sl.Logf(WARNING, \"%%s is a log message with level %%d\", \"This\", WARNING): %d\n", mallocs/N)
-}
-
-func TestXMLConfig(t *testing.T) {
- const (
- configfile = "example.xml"
- )
-
- fd, err := os.Create(configfile)
- if err != nil {
- t.Fatalf("Could not open %s for writing: %s", configfile, err)
- }
-
- fmt.Fprintln(fd, "<logging>")
- fmt.Fprintln(fd, " <filter enabled=\"true\">")
- fmt.Fprintln(fd, " <tag>stdout</tag>")
- fmt.Fprintln(fd, " <type>console</type>")
- fmt.Fprintln(fd, " <!-- level is (:?FINEST|FINE|DEBUG|TRACE|INFO|WARNING|ERROR) -->")
- fmt.Fprintln(fd, " <level>DEBUG</level>")
- fmt.Fprintln(fd, " </filter>")
- fmt.Fprintln(fd, " <filter enabled=\"true\">")
- fmt.Fprintln(fd, " <tag>file</tag>")
- fmt.Fprintln(fd, " <type>file</type>")
- fmt.Fprintln(fd, " <level>FINEST</level>")
- fmt.Fprintln(fd, " <property name=\"filename\">test.log</property>")
- fmt.Fprintln(fd, " <!--")
- fmt.Fprintln(fd, " %T - Time (15:04:05 MST)")
- fmt.Fprintln(fd, " %t - Time (15:04)")
- fmt.Fprintln(fd, " %D - Date (2006/01/02)")
- fmt.Fprintln(fd, " %d - Date (01/02/06)")
- fmt.Fprintln(fd, " %L - Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT)")
- fmt.Fprintln(fd, " %S - Source")
- fmt.Fprintln(fd, " %M - Message")
- fmt.Fprintln(fd, " It ignores unknown format strings (and removes them)")
- fmt.Fprintln(fd, " Recommended: \"[%D %T] [%L] (%S) %M\"")
- fmt.Fprintln(fd, " -->")
- fmt.Fprintln(fd, " <property name=\"format\">[%D %T] [%L] (%S) %M</property>")
- fmt.Fprintln(fd, " <property name=\"rotate\">false</property> <!-- true enables log rotation, otherwise append -->")
- fmt.Fprintln(fd, " <property name=\"maxsize\">0M</property> <!-- \\d+[KMG]? Suffixes are in terms of 2**10 -->")
- fmt.Fprintln(fd, " <property name=\"maxlines\">0K</property> <!-- \\d+[KMG]? Suffixes are in terms of thousands -->")
- fmt.Fprintln(fd, " <property name=\"daily\">true</property> <!-- Automatically rotates when a log message is written after midnight -->")
- fmt.Fprintln(fd, " </filter>")
- fmt.Fprintln(fd, " <filter enabled=\"true\">")
- fmt.Fprintln(fd, " <tag>xmllog</tag>")
- fmt.Fprintln(fd, " <type>xml</type>")
- fmt.Fprintln(fd, " <level>TRACE</level>")
- fmt.Fprintln(fd, " <property name=\"filename\">trace.xml</property>")
- fmt.Fprintln(fd, " <property name=\"rotate\">true</property> <!-- true enables log rotation, otherwise append -->")
- fmt.Fprintln(fd, " <property name=\"maxsize\">100M</property> <!-- \\d+[KMG]? Suffixes are in terms of 2**10 -->")
- fmt.Fprintln(fd, " <property name=\"maxrecords\">6K</property> <!-- \\d+[KMG]? Suffixes are in terms of thousands -->")
- fmt.Fprintln(fd, " <property name=\"daily\">false</property> <!-- Automatically rotates when a log message is written after midnight -->")
- fmt.Fprintln(fd, " </filter>")
- fmt.Fprintln(fd, " <filter enabled=\"false\"><!-- enabled=false means this logger won't actually be created -->")
- fmt.Fprintln(fd, " <tag>donotopen</tag>")
- fmt.Fprintln(fd, " <type>socket</type>")
- fmt.Fprintln(fd, " <level>FINEST</level>")
- fmt.Fprintln(fd, " <property name=\"endpoint\">192.168.1.255:12124</property> <!-- recommend UDP broadcast -->")
- fmt.Fprintln(fd, " <property name=\"protocol\">udp</property> <!-- tcp or udp -->")
- fmt.Fprintln(fd, " </filter>")
- fmt.Fprintln(fd, "</logging>")
- fd.Close()
-
- log := make(Logger)
- log.LoadConfiguration(configfile)
- defer os.Remove("trace.xml")
- defer os.Remove("test.log")
- defer log.Close()
-
- // Make sure we got all loggers
- if len(log) != 3 {
- t.Fatalf("XMLConfig: Expected 3 filters, found %d", len(log))
- }
-
- // Make sure they're the right keys
- if _, ok := log["stdout"]; !ok {
- t.Errorf("XMLConfig: Expected stdout logger")
- }
- if _, ok := log["file"]; !ok {
- t.Fatalf("XMLConfig: Expected file logger")
- }
- if _, ok := log["xmllog"]; !ok {
- t.Fatalf("XMLConfig: Expected xmllog logger")
- }
-
- // Make sure they're the right type
- if _, ok := log["stdout"].LogWriter.(ConsoleLogWriter); !ok {
- t.Fatalf("XMLConfig: Expected stdout to be ConsoleLogWriter, found %T", log["stdout"].LogWriter)
- }
- if _, ok := log["file"].LogWriter.(*FileLogWriter); !ok {
- t.Fatalf("XMLConfig: Expected file to be *FileLogWriter, found %T", log["file"].LogWriter)
- }
- if _, ok := log["xmllog"].LogWriter.(*FileLogWriter); !ok {
- t.Fatalf("XMLConfig: Expected xmllog to be *FileLogWriter, found %T", log["xmllog"].LogWriter)
- }
-
- // Make sure levels are set
- if lvl := log["stdout"].Level; lvl != DEBUG {
- t.Errorf("XMLConfig: Expected stdout to be set to level %d, found %d", DEBUG, lvl)
- }
- if lvl := log["file"].Level; lvl != FINEST {
- t.Errorf("XMLConfig: Expected file to be set to level %d, found %d", FINEST, lvl)
- }
- if lvl := log["xmllog"].Level; lvl != TRACE {
- t.Errorf("XMLConfig: Expected xmllog to be set to level %d, found %d", TRACE, lvl)
- }
-
- // Make sure the w is open and points to the right file
- if fname := log["file"].LogWriter.(*FileLogWriter).file.Name(); fname != "test.log" {
- t.Errorf("XMLConfig: Expected file to have opened %s, found %s", "test.log", fname)
- }
-
- // Make sure the XLW is open and points to the right file
- if fname := log["xmllog"].LogWriter.(*FileLogWriter).file.Name(); fname != "trace.xml" {
- t.Errorf("XMLConfig: Expected xmllog to have opened %s, found %s", "trace.xml", fname)
- }
-
- // Move XML log file
- os.Rename(configfile, "examples/"+configfile) // Keep this so that an example with the documentation is available
-}
-
-func BenchmarkFormatLogRecord(b *testing.B) {
- const updateEvery = 1
- rec := &LogRecord{
- Level: CRITICAL,
- Created: now,
- Source: "source",
- Message: "message",
- }
- for i := 0; i < b.N; i++ {
- rec.Created = rec.Created.Add(1 * time.Second / updateEvery)
- if i%2 == 0 {
- FormatLogRecord(FORMAT_DEFAULT, rec)
- } else {
- FormatLogRecord(FORMAT_SHORT, rec)
- }
- }
-}
-
-func BenchmarkConsoleLog(b *testing.B) {
- /* This doesn't seem to work on OS X
- sink, err := os.Open(os.DevNull)
- if err != nil {
- panic(err)
- }
- if err := syscall.Dup2(int(sink.Fd()), syscall.Stdout); err != nil {
- panic(err)
- }
- */
-
- stdout = ioutil.Discard
- sl := NewDefaultLogger(INFO)
- for i := 0; i < b.N; i++ {
- sl.Log(WARNING, "here", "This is a log message")
- }
-}
-
-func BenchmarkConsoleNotLogged(b *testing.B) {
- sl := NewDefaultLogger(INFO)
- for i := 0; i < b.N; i++ {
- sl.Log(DEBUG, "here", "This is a log message")
- }
-}
-
-func BenchmarkConsoleUtilLog(b *testing.B) {
- sl := NewDefaultLogger(INFO)
- for i := 0; i < b.N; i++ {
- sl.Info("%s is a log message", "This")
- }
-}
-
-func BenchmarkConsoleUtilNotLog(b *testing.B) {
- sl := NewDefaultLogger(INFO)
- for i := 0; i < b.N; i++ {
- sl.Debug("%s is a log message", "This")
- }
-}
-
-func BenchmarkFileLog(b *testing.B) {
- sl := make(Logger)
- b.StopTimer()
- sl.AddFilter("file", INFO, NewFileLogWriter("benchlog.log", false))
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- sl.Log(WARNING, "here", "This is a log message")
- }
- b.StopTimer()
- os.Remove("benchlog.log")
-}
-
-func BenchmarkFileNotLogged(b *testing.B) {
- sl := make(Logger)
- b.StopTimer()
- sl.AddFilter("file", INFO, NewFileLogWriter("benchlog.log", false))
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- sl.Log(DEBUG, "here", "This is a log message")
- }
- b.StopTimer()
- os.Remove("benchlog.log")
-}
-
-func BenchmarkFileUtilLog(b *testing.B) {
- sl := make(Logger)
- b.StopTimer()
- sl.AddFilter("file", INFO, NewFileLogWriter("benchlog.log", false))
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- sl.Info("%s is a log message", "This")
- }
- b.StopTimer()
- os.Remove("benchlog.log")
-}
-
-func BenchmarkFileUtilNotLog(b *testing.B) {
- sl := make(Logger)
- b.StopTimer()
- sl.AddFilter("file", INFO, NewFileLogWriter("benchlog.log", false))
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- sl.Debug("%s is a log message", "This")
- }
- b.StopTimer()
- os.Remove("benchlog.log")
-}
-
-// Benchmark results (darwin amd64 6g)
-//elog.BenchmarkConsoleLog 100000 22819 ns/op
-//elog.BenchmarkConsoleNotLogged 2000000 879 ns/op
-//elog.BenchmarkConsoleUtilLog 50000 34380 ns/op
-//elog.BenchmarkConsoleUtilNotLog 1000000 1339 ns/op
-//elog.BenchmarkFileLog 100000 26497 ns/op
-//elog.BenchmarkFileNotLogged 2000000 821 ns/op
-//elog.BenchmarkFileUtilLog 50000 33945 ns/op
-//elog.BenchmarkFileUtilNotLog 1000000 1258 ns/op
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/termlog.go b/Godeps/_workspace/src/code.google.com/p/log4go/termlog.go
deleted file mode 100644
index 1ed2e4e0d..000000000
--- a/Godeps/_workspace/src/code.google.com/p/log4go/termlog.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
-
-package log4go
-
-import (
- "io"
- "os"
- "fmt"
-)
-
-var stdout io.Writer = os.Stdout
-
-// This is the standard writer that prints to standard output.
-type ConsoleLogWriter chan *LogRecord
-
-// This creates a new ConsoleLogWriter
-func NewConsoleLogWriter() ConsoleLogWriter {
- records := make(ConsoleLogWriter, LogBufferLength)
- go records.run(stdout)
- return records
-}
-
-func (w ConsoleLogWriter) run(out io.Writer) {
- var timestr string
- var timestrAt int64
-
- for rec := range w {
- if at := rec.Created.UnixNano() / 1e9; at != timestrAt {
- timestr, timestrAt = rec.Created.Format("01/02/06 15:04:05"), at
- }
- fmt.Fprint(out, "[", timestr, "] [", levelStrings[rec.Level], "] ", rec.Message, "\n")
- }
-}
-
-// This is the ConsoleLogWriter's output method. This will block if the output
-// buffer is full.
-func (w ConsoleLogWriter) LogWrite(rec *LogRecord) {
- w <- rec
-}
-
-// Close stops the logger from sending messages to standard output. Attempts to
-// send log messages to this logger after a Close have undefined behavior.
-func (w ConsoleLogWriter) Close() {
- close(w)
-}
diff --git a/Godeps/_workspace/src/github.com/alecthomas/log4go/.gitignore b/Godeps/_workspace/src/github.com/alecthomas/log4go/.gitignore
new file mode 100644
index 000000000..f6207cd8a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/.gitignore
@@ -0,0 +1,2 @@
+*.sw[op]
+.DS_Store
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/LICENSE b/Godeps/_workspace/src/github.com/alecthomas/log4go/LICENSE
index 7093402bf..7093402bf 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/LICENSE
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/LICENSE
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/README b/Godeps/_workspace/src/github.com/alecthomas/log4go/README
index 16d80ecb7..16d80ecb7 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/README
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/README
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/config.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/config.go
index f048b69f5..577c3eb2f 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/config.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/config.go
@@ -53,7 +53,7 @@ func (log Logger) LoadConfiguration(filename string) {
for _, xmlfilt := range xc.Filter {
var filt LogWriter
- var lvl level
+ var lvl Level
bad, good, enabled := false, true, false
// Check required children
@@ -131,7 +131,7 @@ func (log Logger) LoadConfiguration(filename string) {
}
}
-func xmlToConsoleLogWriter(filename string, props []xmlProperty, enabled bool) (ConsoleLogWriter, bool) {
+func xmlToConsoleLogWriter(filename string, props []xmlProperty, enabled bool) (*ConsoleLogWriter, bool) {
// Parse properties
for _, prop := range props {
switch prop.Name {
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/examples/ConsoleLogWriter_Manual.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/ConsoleLogWriter_Manual.go
index 394ca8380..698dd332d 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/examples/ConsoleLogWriter_Manual.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/ConsoleLogWriter_Manual.go
@@ -8,6 +8,7 @@ import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
+ defer log.Close()
log.AddFilter("stdout", l4g.DEBUG, l4g.NewConsoleLogWriter())
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
}
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/examples/FileLogWriter_Manual.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/FileLogWriter_Manual.go
index efd596aa6..efd596aa6 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/examples/FileLogWriter_Manual.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/FileLogWriter_Manual.go
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/examples/SimpleNetLogServer.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/SimpleNetLogServer.go
index 83c80ad12..83c80ad12 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/examples/SimpleNetLogServer.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/SimpleNetLogServer.go
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/examples/SocketLogWriter_Manual.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/SocketLogWriter_Manual.go
index 400b698ca..400b698ca 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/examples/SocketLogWriter_Manual.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/SocketLogWriter_Manual.go
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/examples/XMLConfigurationExample.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/XMLConfigurationExample.go
index 164c2add4..164c2add4 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/examples/XMLConfigurationExample.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/XMLConfigurationExample.go
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/examples/example.xml b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/example.xml
index e791278ce..e791278ce 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/examples/example.xml
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/examples/example.xml
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/filelog.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/filelog.go
index 9cbd815d9..ee0ab0c04 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/filelog.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/filelog.go
@@ -3,8 +3,8 @@
package log4go
import (
- "os"
"fmt"
+ "os"
"time"
)
@@ -36,7 +36,8 @@ type FileLogWriter struct {
daily_opendate int
// Keep old logfiles (.001, .002, etc)
- rotate bool
+ rotate bool
+ maxbackup int
}
// This is the FileLogWriter's output method
@@ -46,6 +47,7 @@ func (w *FileLogWriter) LogWrite(rec *LogRecord) {
func (w *FileLogWriter) Close() {
close(w.rec)
+ w.file.Sync()
}
// NewFileLogWriter creates a new LogWriter which writes to the given file and
@@ -59,11 +61,12 @@ func (w *FileLogWriter) Close() {
// [%D %T] [%L] (%S) %M
func NewFileLogWriter(fname string, rotate bool) *FileLogWriter {
w := &FileLogWriter{
- rec: make(chan *LogRecord, LogBufferLength),
- rot: make(chan bool),
- filename: fname,
- format: "[%D %T] [%L] (%S) %M",
- rotate: rotate,
+ rec: make(chan *LogRecord, LogBufferLength),
+ rot: make(chan bool),
+ filename: fname,
+ format: "[%D %T] [%L] (%S) %M",
+ rotate: rotate,
+ maxbackup: 999,
}
// open the file for the first time
@@ -138,15 +141,30 @@ func (w *FileLogWriter) intRotate() error {
// Find the next available number
num := 1
fname := ""
- for ; err == nil && num <= 999; num++ {
- fname = w.filename + fmt.Sprintf(".%03d", num)
- _, err = os.Lstat(fname)
- }
- // return error if the last file checked still existed
- if err == nil {
- return fmt.Errorf("Rotate: Cannot find free log number to rename %s\n", w.filename)
+ if w.daily && time.Now().Day() != w.daily_opendate {
+ yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
+
+ for ; err == nil && num <= 999; num++ {
+ fname = w.filename + fmt.Sprintf(".%s.%03d", yesterday, num)
+ _, err = os.Lstat(fname)
+ }
+ // return error if the last file checked still existed
+ if err == nil {
+ return fmt.Errorf("Rotate: Cannot find free log number to rename %s\n", w.filename)
+ }
+ } else {
+ num = w.maxbackup - 1
+ for ; num >= 1; num-- {
+ fname = w.filename + fmt.Sprintf(".%d", num)
+ nfname := w.filename + fmt.Sprintf(".%d", num+1)
+ _, err = os.Lstat(fname)
+ if err == nil {
+ os.Rename(fname, nfname)
+ }
+ }
}
+ w.file.Close()
// Rename the file to its newfound home
err = os.Rename(w.filename, fname)
if err != nil {
@@ -217,6 +235,13 @@ func (w *FileLogWriter) SetRotateDaily(daily bool) *FileLogWriter {
return w
}
+// Set max backup files. Must be called before the first log message
+// is written.
+func (w *FileLogWriter) SetRotateMaxBackup(maxbackup int) *FileLogWriter {
+ w.maxbackup = maxbackup
+ return w
+}
+
// SetRotate changes whether or not the old logs are kept. (chainable) Must be
// called before the first log message is written. If rotate is false, the
// files are overwritten; otherwise, they are rotated to another file before the
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/log4go.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/log4go.go
index ab4e857f5..822e890cc 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/log4go.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/log4go.go
@@ -47,11 +47,11 @@ package log4go
import (
"errors"
- "os"
"fmt"
- "time"
- "strings"
+ "os"
"runtime"
+ "strings"
+ "time"
)
// Version information
@@ -65,10 +65,10 @@ const (
/****** Constants ******/
// These are the integer logging levels used by the logger
-type level int
+type Level int
const (
- FINEST level = iota
+ FINEST Level = iota
FINE
DEBUG
TRACE
@@ -83,7 +83,7 @@ var (
levelStrings = [...]string{"FNST", "FINE", "DEBG", "TRAC", "INFO", "WARN", "EROR", "CRIT"}
)
-func (l level) String() string {
+func (l Level) String() string {
if l < 0 || int(l) > len(levelStrings) {
return "UNKNOWN"
}
@@ -101,7 +101,7 @@ var (
// A LogRecord contains all of the pertinent information for each message
type LogRecord struct {
- Level level // The log level
+ Level Level // The log level
Created time.Time // The time at which the log message was created (nanoseconds)
Source string // The message source
Message string // The log message
@@ -124,7 +124,7 @@ type LogWriter interface {
// A Filter represents the log level below which no log records are written to
// the associated LogWriter.
type Filter struct {
- Level level
+ Level Level
LogWriter
}
@@ -144,7 +144,7 @@ func NewLogger() Logger {
// or above lvl to standard output.
//
// DEPRECATED: use NewDefaultLogger instead.
-func NewConsoleLogger(lvl level) Logger {
+func NewConsoleLogger(lvl Level) Logger {
os.Stderr.WriteString("warning: use of deprecated NewConsoleLogger\n")
return Logger{
"stdout": &Filter{lvl, NewConsoleLogWriter()},
@@ -153,7 +153,7 @@ func NewConsoleLogger(lvl level) Logger {
// Create a new logger with a "stdout" filter configured to send log messages at
// or above lvl to standard output.
-func NewDefaultLogger(lvl level) Logger {
+func NewDefaultLogger(lvl Level) Logger {
return Logger{
"stdout": &Filter{lvl, NewConsoleLogWriter()},
}
@@ -174,14 +174,14 @@ func (log Logger) Close() {
// Add a new LogWriter to the Logger which will only log messages at lvl or
// higher. This function should not be called from multiple goroutines.
// Returns the logger for chaining.
-func (log Logger) AddFilter(name string, lvl level, writer LogWriter) Logger {
+func (log Logger) AddFilter(name string, lvl Level, writer LogWriter) Logger {
log[name] = &Filter{lvl, writer}
return log
}
/******* Logging *******/
// Send a formatted log message internally
-func (log Logger) intLogf(lvl level, format string, args ...interface{}) {
+func (log Logger) intLogf(lvl Level, format string, args ...interface{}) {
skip := true
// Determine if any logging will be done
@@ -225,7 +225,7 @@ func (log Logger) intLogf(lvl level, format string, args ...interface{}) {
}
// Send a closure log message internally
-func (log Logger) intLogc(lvl level, closure func() string) {
+func (log Logger) intLogc(lvl Level, closure func() string) {
skip := true
// Determine if any logging will be done
@@ -264,7 +264,7 @@ func (log Logger) intLogc(lvl level, closure func() string) {
}
// Send a log message with manual level, source, and message.
-func (log Logger) Log(lvl level, source, message string) {
+func (log Logger) Log(lvl Level, source, message string) {
skip := true
// Determine if any logging will be done
@@ -297,13 +297,13 @@ func (log Logger) Log(lvl level, source, message string) {
// Logf logs a formatted log message at the given log level, using the caller as
// its source.
-func (log Logger) Logf(lvl level, format string, args ...interface{}) {
+func (log Logger) Logf(lvl Level, format string, args ...interface{}) {
log.intLogf(lvl, format, args...)
}
// Logc logs a string returned by the closure at the given log level, using the caller as
// its source. If no log message would be written, the closure is never called.
-func (log Logger) Logc(lvl level, closure func() string) {
+func (log Logger) Logc(lvl Level, closure func() string) {
log.intLogc(lvl, closure)
}
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/pattlog.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/pattlog.go
index 8224302b3..82b4e36b1 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/pattlog.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/pattlog.go
@@ -3,9 +3,10 @@
package log4go
import (
- "fmt"
"bytes"
+ "fmt"
"io"
+ "strings"
)
const (
@@ -51,7 +52,7 @@ func FormatLogRecord(format string, rec *LogRecord) string {
updated := &formatCacheType{
LastUpdateSeconds: secs,
shortTime: fmt.Sprintf("%02d:%02d", hour, minute),
- shortDate: fmt.Sprintf("%02d/%02d/%02d", month, day, year%100),
+ shortDate: fmt.Sprintf("%02d/%02d/%02d", day, month, year%100),
longTime: fmt.Sprintf("%02d:%02d:%02d %s", hour, minute, second, zone),
longDate: fmt.Sprintf("%04d/%02d/%02d", year, month, day),
}
@@ -78,6 +79,9 @@ func FormatLogRecord(format string, rec *LogRecord) string {
out.WriteString(levelStrings[rec.Level])
case 'S':
out.WriteString(rec.Source)
+ case 's':
+ slice := strings.Split(rec.Source, "/")
+ out.WriteString(slice[len(slice)-1])
case 'M':
out.WriteString(rec.Message)
}
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/socklog.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/socklog.go
index 1d224a99d..1d224a99d 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/socklog.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/socklog.go
diff --git a/Godeps/_workspace/src/github.com/alecthomas/log4go/termlog.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/termlog.go
new file mode 100644
index 000000000..8a941e269
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/termlog.go
@@ -0,0 +1,49 @@
+// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
+
+package log4go
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "time"
+)
+
+var stdout io.Writer = os.Stdout
+
+// This is the standard writer that prints to standard output.
+type ConsoleLogWriter struct {
+ format string
+ w chan *LogRecord
+}
+
+// This creates a new ConsoleLogWriter
+func NewConsoleLogWriter() *ConsoleLogWriter {
+ consoleWriter := &ConsoleLogWriter{
+ format: "[%T %D] [%L] (%S) %M",
+ w: make(chan *LogRecord, LogBufferLength),
+ }
+ go consoleWriter.run(stdout)
+ return consoleWriter
+}
+func (c *ConsoleLogWriter) SetFormat(format string) {
+ c.format = format
+}
+func (c *ConsoleLogWriter) run(out io.Writer) {
+ for rec := range c.w {
+ fmt.Fprint(out, FormatLogRecord(c.format, rec))
+ }
+}
+
+// This is the ConsoleLogWriter's output method. This will block if the output
+// buffer is full.
+func (c *ConsoleLogWriter) LogWrite(rec *LogRecord) {
+ c.w <- rec
+}
+
+// Close stops the logger from sending messages to standard output. Attempts to
+// send log messages to this logger after a Close have undefined behavior.
+func (c *ConsoleLogWriter) Close() {
+ close(c.w)
+ time.Sleep(50 * time.Millisecond) // Try to give console I/O time to complete
+}
diff --git a/Godeps/_workspace/src/code.google.com/p/log4go/wrapper.go b/Godeps/_workspace/src/github.com/alecthomas/log4go/wrapper.go
index 10ecd88e6..2ae222b0c 100644
--- a/Godeps/_workspace/src/code.google.com/p/log4go/wrapper.go
+++ b/Godeps/_workspace/src/github.com/alecthomas/log4go/wrapper.go
@@ -4,8 +4,8 @@ package log4go
import (
"errors"
- "os"
"fmt"
+ "os"
"strings"
)
@@ -23,7 +23,7 @@ func LoadConfiguration(filename string) {
}
// Wrapper for (*Logger).AddFilter
-func AddFilter(name string, lvl level, writer LogWriter) {
+func AddFilter(name string, lvl Level, writer LogWriter) {
Global.AddFilter(name, lvl, writer)
}
@@ -88,19 +88,19 @@ func Stdoutf(format string, args ...interface{}) {
// Send a log message manually
// Wrapper for (*Logger).Log
-func Log(lvl level, source, message string) {
+func Log(lvl Level, source, message string) {
Global.Log(lvl, source, message)
}
// Send a formatted log message easily
// Wrapper for (*Logger).Logf
-func Logf(lvl level, format string, args ...interface{}) {
+func Logf(lvl Level, format string, args ...interface{}) {
Global.intLogf(lvl, format, args...)
}
// Send a closure log message
// Wrapper for (*Logger).Logc
-func Logc(lvl level, closure func() string) {
+func Logc(lvl Level, closure func() string) {
Global.intLogc(lvl, closure)
}
diff --git a/Makefile b/Makefile
index 9fd74b959..3f5cd7f83 100644
--- a/Makefile
+++ b/Makefile
@@ -266,6 +266,9 @@ run: start-docker .prepare-go .prepare-jsx
jq -s '.[0] * .[1]' ./config/config.json $(ENTERPRISE_DIR)/config/enterprise-config-additions.json > config.json.tmp; \
mv config.json.tmp ./config/config.json; \
sed -e '/\/\/ENTERPRISE_IMPORTS/ {' -e 'r $(ENTERPRISE_DIR)/imports' -e 'd' -e '}' -i'.bak' mattermost.go; \
+ sed -i'.bak' 's|_BUILD_ENTERPRISE_READY_|true|g' ./model/version.go; \
+ else \
+ sed -i'.bak' 's|_BUILD_ENTERPRISE_READY_|false|g' ./model/version.go; \
fi
@echo Starting go web server
@@ -299,6 +302,7 @@ stop:
@if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
mv ./config/config.json.bak ./config/config.json 2> /dev/null || true; \
mv ./mattermost.go.bak ./mattermost.go 2> /dev/null || true; \
+ mv ./model/version.go.bak ./model/version.go 2> /dev/null || true; \
fi
setup-mac:
diff --git a/api/admin.go b/api/admin.go
index 8e0a03e4b..885a95d95 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -9,7 +9,7 @@ import (
"os"
"strings"
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
diff --git a/api/api.go b/api/api.go
index 6c7eda0a2..f29063fe1 100644
--- a/api/api.go
+++ b/api/api.go
@@ -5,7 +5,7 @@ package api
import (
"bytes"
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"html/template"
@@ -46,6 +46,7 @@ func InitApi() {
InitOAuth(r)
InitWebhook(r)
InitPreference(r)
+ InitLicense(r)
templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir)
diff --git a/api/channel.go b/api/channel.go
index 674293e19..706baa004 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -4,8 +4,8 @@
package api
import (
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"net/http"
diff --git a/api/command.go b/api/command.go
index db57f0bae..00293cf16 100644
--- a/api/command.go
+++ b/api/command.go
@@ -11,7 +11,7 @@ import (
"strings"
"time"
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
diff --git a/api/context.go b/api/context.go
index b39f03a7d..e8ec6576d 100644
--- a/api/context.go
+++ b/api/context.go
@@ -11,7 +11,7 @@ import (
"strconv"
"strings"
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -35,6 +35,7 @@ type Page struct {
TemplateName string
Props map[string]string
ClientCfg map[string]string
+ ClientLicense map[string]string
User *model.User
Team *model.Team
Channel *model.Channel
diff --git a/api/file.go b/api/file.go
index 67ebc14b7..46e81691e 100644
--- a/api/file.go
+++ b/api/file.go
@@ -5,8 +5,8 @@ package api
import (
"bytes"
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/disintegration/imaging"
"github.com/goamz/goamz/aws"
"github.com/goamz/goamz/s3"
@@ -541,12 +541,8 @@ func writeFile(f []byte, path string) *model.AppError {
return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error())
}
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
- if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
- return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error())
- }
-
- if err := ioutil.WriteFile(utils.Cfg.FileSettings.Directory+path, f, 0644); err != nil {
- return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error())
+ if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
+ return err
}
} else {
return model.NewAppError("writeFile", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
@@ -555,6 +551,18 @@ func writeFile(f []byte, path string) *model.AppError {
return nil
}
+func writeFileLocally(f []byte, path string) *model.AppError {
+ if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
+ return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error())
+ }
+
+ if err := ioutil.WriteFile(path, f, 0644); err != nil {
+ return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error())
+ }
+
+ return nil
+}
+
func readFile(path string) ([]byte, *model.AppError) {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
diff --git a/api/import.go b/api/import.go
index 81de78975..5c8f99348 100644
--- a/api/import.go
+++ b/api/import.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
)
diff --git a/api/license.go b/api/license.go
new file mode 100644
index 000000000..5b3809651
--- /dev/null
+++ b/api/license.go
@@ -0,0 +1,100 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "bytes"
+ l4g "github.com/alecthomas/log4go"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "io"
+ "net/http"
+ "strings"
+)
+
+func InitLicense(r *mux.Router) {
+ l4g.Debug("Initializing license api routes")
+
+ sr := r.PathPrefix("/license").Subrouter()
+ sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
+ sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST")
+}
+
+func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.LogAudit("attempt")
+ err := r.ParseMultipartForm(model.MAX_FILE_SIZE)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ m := r.MultipartForm
+
+ fileArray, ok := m.File["license"]
+ if !ok {
+ c.Err = model.NewAppError("addLicense", "No file under 'license' in request", "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ if len(fileArray) <= 0 {
+ c.Err = model.NewAppError("addLicense", "Empty array under 'license' in request", "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ fileData := fileArray[0]
+
+ file, err := fileData.Open()
+ defer file.Close()
+ if err != nil {
+ c.Err = model.NewAppError("addLicense", "Could not open license file", err.Error())
+ return
+ }
+
+ buf := bytes.NewBuffer(nil)
+ io.Copy(buf, file)
+
+ data := buf.Bytes()
+
+ var license *model.License
+ if success, licenseStr := utils.ValidateLicense(data); success {
+ license = model.LicenseFromJson(strings.NewReader(licenseStr))
+
+ if ok := utils.SetLicense(license); !ok {
+ c.LogAudit("failed - expired or non-started license")
+ c.Err = model.NewAppError("addLicense", "License is either expired or has not yet started.", "")
+ return
+ }
+
+ if err := writeFileLocally(data, utils.LicenseLocation()); err != nil {
+ c.LogAudit("failed - could not save license file")
+ c.Err = model.NewAppError("addLicense", "License did not save properly.", "path="+utils.LicenseLocation())
+ utils.RemoveLicense()
+ return
+ }
+ } else {
+ c.LogAudit("failed - invalid license")
+ c.Err = model.NewAppError("addLicense", "Invalid license file.", "")
+ return
+ }
+
+ c.LogAudit("success")
+ w.Write([]byte(license.ToJson()))
+}
+
+func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.LogAudit("")
+
+ if ok := utils.RemoveLicense(); !ok {
+ c.LogAudit("failed - could not remove license file")
+ c.Err = model.NewAppError("removeLicense", "License did not remove properly.", "")
+ return
+ }
+
+ rdata := map[string]string{}
+ rdata["status"] = "ok"
+ w.Write([]byte(model.MapToJson(rdata)))
+}
diff --git a/api/oauth.go b/api/oauth.go
index 5753db8bd..eb5e0e496 100644
--- a/api/oauth.go
+++ b/api/oauth.go
@@ -4,8 +4,8 @@
package api
import (
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
diff --git a/api/post.go b/api/post.go
index 958479427..ae4d3cc50 100644
--- a/api/post.go
+++ b/api/post.go
@@ -4,8 +4,8 @@
package api
import (
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
@@ -632,15 +632,22 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
alreadySeen := make(map[string]string)
for _, session := range sessions {
- if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && strings.HasPrefix(session.DeviceId, "apple:") {
+ if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" &&
+ (strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) {
alreadySeen[session.DeviceId] = session.DeviceId
msg := model.PushNotification{}
- msg.Platform = model.PUSH_NOTIFY_APPLE
msg.Badge = 1
- msg.DeviceId = strings.TrimPrefix(session.DeviceId, "apple:")
msg.ServerId = utils.CfgDiagnosticId
+ if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") {
+ msg.Platform = model.PUSH_NOTIFY_APPLE
+ msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":")
+ } else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") {
+ msg.Platform = model.PUSH_NOTIFY_ANDROID
+ msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")
+ }
+
if channel.Type == model.CHANNEL_DIRECT {
msg.Message = senderName + " sent you a direct message"
} else {
diff --git a/api/preference.go b/api/preference.go
index e9c74aafe..f5c96f1dd 100644
--- a/api/preference.go
+++ b/api/preference.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"net/http"
diff --git a/api/server.go b/api/server.go
index 2bab62fac..33428009f 100644
--- a/api/server.go
+++ b/api/server.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/braintree/manners"
"github.com/gorilla/mux"
"github.com/mattermost/platform/store"
diff --git a/api/slackimport.go b/api/slackimport.go
index cab4c6184..e0a0ff036 100644
--- a/api/slackimport.go
+++ b/api/slackimport.go
@@ -6,8 +6,8 @@ package api
import (
"archive/zip"
"bytes"
- l4g "code.google.com/p/log4go"
"encoding/json"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"io"
"mime/multipart"
diff --git a/api/team.go b/api/team.go
index fbcb301a9..e2dd8807e 100644
--- a/api/team.go
+++ b/api/team.go
@@ -5,8 +5,8 @@ package api
import (
"bytes"
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
diff --git a/api/user.go b/api/user.go
index 00d637ec2..a6b4fb654 100644
--- a/api/user.go
+++ b/api/user.go
@@ -5,9 +5,9 @@ package api
import (
"bytes"
- l4g "code.google.com/p/log4go"
b64 "encoding/base64"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/disintegration/imaging"
"github.com/golang/freetype"
"github.com/gorilla/mux"
@@ -122,6 +122,11 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
user.EmailVerified = true
}
+ if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) {
+ c.Err = model.NewAppError("createUser", "The email you provided does not belong to an accepted domain. Please contact your administrator or sign up with a different email.", "")
+ return
+ }
+
ruser, err := CreateUser(team, user)
if err != nil {
c.Err = err
@@ -136,19 +141,29 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
+func CheckUserDomain(user *model.User, domains string) bool {
+ if len(domains) == 0 {
+ return true
+ }
+
+ domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
+
+ matched := false
+ for _, d := range domainArray {
+ if strings.HasSuffix(user.Email, "@"+d) {
+ matched = true
+ break
+ }
+ }
+
+ return matched
+}
+
func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool {
shouldVerifyHash := true
if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil {
- domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1))))
-
- matched := false
- for _, d := range domains {
- if strings.HasSuffix(user.Email, "@"+d) {
- matched = true
- break
- }
- }
+ matched := CheckUserDomain(user, team.AllowedDomains)
if matched {
shouldVerifyHash = false
diff --git a/api/web_conn.go b/api/web_conn.go
index 50a003ace..2b0e29038 100644
--- a/api/web_conn.go
+++ b/api/web_conn.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/websocket"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
diff --git a/api/web_hub.go b/api/web_hub.go
index f80488824..4361d1035 100644
--- a/api/web_hub.go
+++ b/api/web_hub.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
)
diff --git a/api/web_socket.go b/api/web_socket.go
index 298e44b44..995e2a677 100644
--- a/api/web_socket.go
+++ b/api/web_socket.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/mattermost/platform/model"
diff --git a/api/web_team_hub.go b/api/web_team_hub.go
index 2c2386317..bb9ed9526 100644
--- a/api/web_team_hub.go
+++ b/api/web_team_hub.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
)
diff --git a/api/webhook.go b/api/webhook.go
index 34c308879..a9a88b7b8 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -4,7 +4,7 @@
package api
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
diff --git a/config/config.json b/config/config.json
index c43db1e50..907b66828 100644
--- a/config/config.json
+++ b/config/config.json
@@ -12,10 +12,10 @@
"EnableTesting": false,
"EnableDeveloper": false,
"EnableSecurityFixAlert": true,
- "SessionLengthWebInDays" : 30,
- "SessionLengthMobileInDays" : 30,
- "SessionLengthSSOInDays" : 30,
- "SessionCacheInMinutes" : 10
+ "SessionLengthWebInDays": 30,
+ "SessionLengthMobileInDays": 30,
+ "SessionLengthSSOInDays": 30,
+ "SessionCacheInMinutes": 10
},
"TeamSettings": {
"SiteName": "Mattermost",
@@ -107,5 +107,28 @@
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
+ },
+ "GoogleSettings": {
+ "Enable": false,
+ "Secret": "",
+ "Id": "",
+ "Scope": "",
+ "AuthEndpoint": "",
+ "TokenEndpoint": "",
+ "UserApiEndpoint": ""
+ },
+ "LdapSettings": {
+ "Enable": false,
+ "LdapServer": null,
+ "LdapPort": 389,
+ "BaseDN": null,
+ "BindUsername": null,
+ "BindPassword": null,
+ "FirstNameAttribute": null,
+ "LastNameAttribute": null,
+ "EmailAttribute": null,
+ "UsernameAttribute": null,
+ "IdAttribute": null,
+ "QueryTimeout": 60
}
} \ No newline at end of file
diff --git a/doc/developer/tests/test-attachments.md b/doc/developer/tests/test-attachments.md
index 71cc496ce..e2fda0eb6 100644
--- a/doc/developer/tests/test-attachments.md
+++ b/doc/developer/tests/test-attachments.md
@@ -29,7 +29,7 @@ Expected: Scaled thumbnail & preview window
**GIF**
`Images/GIF.gif`
-Expected: Scaled thumbnail & preview window. Click to play GIF.
+Expected: Scaled thumbnail & preview window. GIF should auto-play in the preview window.
[Permalink](https://pre-release.mattermost.com/core/pl/j49fowdkstr57g3ed9bgpfoo5w)
**TIFF**
@@ -72,37 +72,37 @@ Expected: Generic Word thumbnail & preview window.
**MP4**
`Videos/MP4.mp4`
-Expected: Generic video thumbnail & playable preview window
+Expected: Generic video thumbnail & playable preview window. View Permalink.
[Permalink](https://pre-release.mattermost.com/core/pl/5dx5qx9t9brqfnhohccxjynx7c)
**AVI**
`Videos/AVI.avi`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/qwn9eiy7j3rkjyruxhcugpogdw)
**MKV**
`Videos/MKV.mkv`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/tszyjkr1cidhxjgiusa4mde3ja)
**MOV**
`Videos/MOV.mov`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/ienzppz5i3f7tbt5jiujn8uuir)
**MPG**
`Videos/MPG.mpg`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/skggdq1hfpritc6c88bi481p5a)
**Webm**
`Videos/WEBM.webm`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/7h8tysuxgfgsxeht3sbn7e4h6y)
**WMV**
`Videos/WMV.wmv`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/kaom7j7uyjra7bzhrre6qwdrbw)
### Audio
@@ -112,7 +112,6 @@ Expected: View Permalink. Expected depends on the operating system, browser and
Expected: Generic audio thumbnail & playable preview window
[Permalink](https://pre-release.mattermost.com/core/pl/if4gn8dbrjgx8fmqmkukzefyme)
-
**M4A**
`Audio/M4a.m4a`
Expected: Generic audio thumbnail & playable preview window
@@ -120,25 +119,25 @@ Expected: Generic audio thumbnail & playable preview window
**AAC**
`Audio/AAC.aac`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/3naoy5pr5tydbk1m6yo1ast9ny)
**FLAC**
`Audio/FLAC.flac`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/kf4cmy44dfya5efmse7rg43eih)
**OGG**
`Audio/OGG.ogg`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/dezrcpbxapyexe77rjuzkrp63r)
**WAV**
`Audio/WAV.wav`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/pdkxx1udepdnbmi9j8kyas5xbh)
**WMA**
`Audio/WMA.wma`
-Expected: View Permalink. Expected depends on the operating system, browser and plugins.
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/756wrmdd57dcig3m4emypp6i1h)
diff --git a/doc/developer/tests/test-markdown-basics.md b/doc/developer/tests/test-markdown-basics.md
index 7a46adeb0..cea9a0867 100644
--- a/doc/developer/tests/test-markdown-basics.md
+++ b/doc/developer/tests/test-markdown-basics.md
@@ -18,6 +18,18 @@ Normal Text_
_Normal Text
_Normal Text*
+### Carriage Return
+
+**The following text should render as:**
+Line #1 followed by Line #2
+Line #2 followed by one blank line
+
+Line #3 followed by one blank line
+
+
+Line #4 following one blank line
+
+
### Code Blocks
```
@@ -119,7 +131,7 @@ Text below line
**The following markdown should render within the block quote:**
> #### Heading 4
-> _Italics_, *Italics*, **Bold**, ***Bold-italics***, **Bold-italics_**, ~~Strikethrough~~
+> _Italics_, *Italics*, **Bold**, ***Bold-italics***, **_Bold-italics_**, ~~Strikethrough~~
> :) :-) ;) :-O :bamboo: :gift_heart: :dolls:
**The following text should render in two block quotes separated by one line of text:**
diff --git a/doc/install/Configuration-Settings.md b/doc/install/Configuration-Settings.md
index 962be0eb7..31d4551f6 100644
--- a/doc/install/Configuration-Settings.md
+++ b/doc/install/Configuration-Settings.md
@@ -123,11 +123,13 @@ Settings to configure email signup, notifications, security, and SMTP options.
#### Notifications
```"SendEmailNotifications": false```
-"true": Enables sending of email notifications. “false”: Disables email notifications for developers who may want to skip email setup for faster development.
+"true": Enables sending of email notifications. “false”: Disables email notifications for developers who may want to skip email setup for faster development. Setting this to true removes the **Preview Mode: Email notifications have not been configured** banner (requires logging out and logging back in after setting is changed)
+
```"RequireEmailVerification": false```
"true": Require email verification after account creation prior to allowing login; “false”: Users do not need to verify their email address prior to login. Developers may set this field to false so skip sending verification emails for faster development.
+
```"FeedbackName": ""```
Name displayed on email account used when sending notification emails from Mattermost system.
diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go
index b3d01a5a6..befc835fb 100644
--- a/manualtesting/manual_testing.go
+++ b/manualtesting/manual_testing.go
@@ -4,7 +4,7 @@
package manualtesting
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
diff --git a/manualtesting/test_autolink.go b/manualtesting/test_autolink.go
index a13ec7a75..16d2d713a 100644
--- a/manualtesting/test_autolink.go
+++ b/manualtesting/test_autolink.go
@@ -4,7 +4,7 @@
package manualtesting
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
)
diff --git a/mattermost.go b/mattermost.go
index 3d8ab736f..d96a24313 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -17,7 +17,7 @@ import (
"syscall"
"time"
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/manualtesting"
"github.com/mattermost/platform/model"
@@ -31,8 +31,6 @@ import (
_ "github.com/go-ldap/ldap"
)
-//ENTERPRISE_IMPORTS
-
var flagCmdCreateTeam bool
var flagCmdCreateUser bool
var flagCmdAssignRole bool
@@ -67,6 +65,8 @@ func main() {
api.InitApi()
web.InitWeb()
+ utils.LoadLicense()
+
if flagRunCmds {
runCmds()
} else {
diff --git a/model/client.go b/model/client.go
index 14746f8ae..75b93c971 100644
--- a/model/client.go
+++ b/model/client.go
@@ -5,8 +5,8 @@ package model
import (
"bytes"
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"io/ioutil"
"net/http"
"net/url"
diff --git a/model/license.go b/model/license.go
new file mode 100644
index 000000000..a271b46b7
--- /dev/null
+++ b/model/license.go
@@ -0,0 +1,85 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type License struct {
+ Id string `json:"id"`
+ IssuedAt int64 `json:"issued_at"`
+ StartsAt int64 `json:"starts_at"`
+ ExpiresAt int64 `json:"expires_at"`
+ Customer *Customer `json:"customer"`
+ Features *Features `json:"features"`
+}
+
+type Customer struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Company string `json:"company"`
+ PhoneNumber string `json:"phone_number"`
+}
+
+type Features struct {
+ Users *int `json:"users"`
+ LDAP *bool `json:"ldap"`
+ GoogleSSO *bool `json:"google_sso"`
+}
+
+func (f *Features) SetDefaults() {
+ if f.Users == nil {
+ f.Users = new(int)
+ *f.Users = 0
+ }
+
+ if f.LDAP == nil {
+ f.LDAP = new(bool)
+ *f.LDAP = true
+ }
+
+ if f.GoogleSSO == nil {
+ f.GoogleSSO = new(bool)
+ *f.GoogleSSO = true
+ }
+}
+
+func (l *License) IsExpired() bool {
+ now := GetMillis()
+ if l.ExpiresAt < now {
+ return true
+ }
+ return false
+}
+
+func (l *License) IsStarted() bool {
+ now := GetMillis()
+ if l.StartsAt < now {
+ return true
+ }
+ return false
+}
+
+func (l *License) ToJson() string {
+ b, err := json.Marshal(l)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func LicenseFromJson(data io.Reader) *License {
+ decoder := json.NewDecoder(data)
+ var o License
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/license_test.go b/model/license_test.go
new file mode 100644
index 000000000..25c74a2e3
--- /dev/null
+++ b/model/license_test.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "testing"
+)
+
+func TestLicenseExpired(t *testing.T) {
+ l1 := License{}
+ l1.ExpiresAt = GetMillis() - 1000
+ if !l1.IsExpired() {
+ t.Fatal("license should be expired")
+ }
+
+ l1.ExpiresAt = GetMillis() + 10000
+ if l1.IsExpired() {
+ t.Fatal("license should not be expired")
+ }
+}
+
+func TestLicenseStarted(t *testing.T) {
+ l1 := License{}
+ l1.StartsAt = GetMillis() - 1000
+ if !l1.IsStarted() {
+ t.Fatal("license should be started")
+ }
+
+ l1.StartsAt = GetMillis() + 10000
+ if l1.IsStarted() {
+ t.Fatal("license should not be started")
+ }
+}
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index 307761150..6302d2f4f 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -4,7 +4,7 @@
package store
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/go-gorp/gorp"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index 86604b4fe..6b0a31443 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -4,7 +4,7 @@
package store
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
)
diff --git a/store/sql_store.go b/store/sql_store.go
index d17a3e8c3..d0471fa1e 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -4,7 +4,6 @@
package store
import (
- l4g "code.google.com/p/log4go"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
@@ -16,6 +15,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"io"
sqltrace "log"
"math/rand"
diff --git a/store/store.go b/store/store.go
index 8e03c8ee7..179cfecd7 100644
--- a/store/store.go
+++ b/store/store.go
@@ -4,7 +4,7 @@
package store
import (
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"time"
)
diff --git a/utils/config.go b/utils/config.go
index 18bd15241..12d03b5de 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -11,7 +11,7 @@ import (
"path/filepath"
"strconv"
- l4g "code.google.com/p/log4go"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
)
@@ -77,7 +77,9 @@ func configureLog(s *model.LogSettings) {
level = l4g.ERROR
}
- l4g.AddFilter("stdout", level, l4g.NewConsoleLogWriter())
+ lw := l4g.NewConsoleLogWriter()
+ lw.SetFormat("[%D %T] [%L] %M")
+ l4g.AddFilter("stdout", level, lw)
}
if s.EnableFile {
diff --git a/utils/license.go b/utils/license.go
new file mode 100644
index 000000000..7594e33af
--- /dev/null
+++ b/utils/license.go
@@ -0,0 +1,157 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rsa"
+ "crypto/sha512"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "io"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+
+ "github.com/mattermost/platform/model"
+)
+
+const (
+ LICENSE_FILENAME = "active.dat"
+)
+
+var IsLicensed bool = false
+var License *model.License = &model.License{}
+var ClientLicense map[string]string = make(map[string]string)
+
+// test public key
+var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn
+0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5
+VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6
+PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN
+K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ
+NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR
+1wIDAQAB
+-----END PUBLIC KEY-----`)
+
+func LoadLicense() {
+ file, err := os.Open(LicenseLocation())
+ if err != nil {
+ l4g.Warn("Unable to open/find license file")
+ return
+ }
+ defer file.Close()
+
+ buf := bytes.NewBuffer(nil)
+ io.Copy(buf, file)
+
+ if success, licenseStr := ValidateLicense(buf.Bytes()); success {
+ license := model.LicenseFromJson(strings.NewReader(licenseStr))
+ SetLicense(license)
+ }
+
+ l4g.Warn("No valid enterprise license found")
+}
+
+func SetLicense(license *model.License) bool {
+ license.Features.SetDefaults()
+
+ if !license.IsExpired() && license.IsStarted() {
+ License = license
+ IsLicensed = true
+ ClientLicense = getClientLicense(license)
+ return true
+ }
+
+ return false
+}
+
+func LicenseLocation() string {
+ return filepath.Dir(CfgFileName) + "/" + LICENSE_FILENAME
+}
+
+func RemoveLicense() bool {
+ License = &model.License{}
+ IsLicensed = false
+ ClientLicense = getClientLicense(License)
+
+ if err := os.Remove(LicenseLocation()); err != nil {
+ l4g.Error("Unable to remove license file, err=%v", err.Error())
+ return false
+ }
+
+ return true
+}
+
+func ValidateLicense(signed []byte) (bool, string) {
+ decoded := make([]byte, base64.StdEncoding.DecodedLen(len(signed)))
+
+ _, err := base64.StdEncoding.Decode(decoded, signed)
+ if err != nil {
+ l4g.Error("Encountered error decoding license, err=%v", err.Error())
+ return false, ""
+ }
+
+ if len(decoded) <= 256 {
+ l4g.Error("Signed license not long enough")
+ return false, ""
+ }
+
+ // remove null terminator
+ if decoded[len(decoded)-1] == byte(0) {
+ decoded = decoded[:len(decoded)-1]
+ }
+
+ plaintext := decoded[:len(decoded)-256]
+ signature := decoded[len(decoded)-256:]
+
+ block, _ := pem.Decode(publicKey)
+
+ public, err := x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ l4g.Error("Encountered error signing license, err=%v", err.Error())
+ return false, ""
+ }
+
+ rsaPublic := public.(*rsa.PublicKey)
+
+ h := sha512.New()
+ h.Write(plaintext)
+ d := h.Sum(nil)
+
+ err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature)
+ if err != nil {
+ l4g.Error("Invalid signature, err=%v", err.Error())
+ return false, ""
+ }
+
+ return true, string(plaintext)
+}
+
+func getClientLicense(l *model.License) map[string]string {
+ props := make(map[string]string)
+
+ props["IsLicensed"] = strconv.FormatBool(IsLicensed)
+
+ if IsLicensed {
+ props["Users"] = strconv.Itoa(*l.Features.Users)
+ props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
+ props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO)
+ props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)
+ props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10)
+ props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10)
+ props["Name"] = l.Customer.Name
+ props["Email"] = l.Customer.Email
+ props["Company"] = l.Customer.Company
+ props["PhoneNumber"] = l.Customer.PhoneNumber
+ }
+
+ return props
+}
diff --git a/utils/license_test.go b/utils/license_test.go
new file mode 100644
index 000000000..826107032
--- /dev/null
+++ b/utils/license_test.go
@@ -0,0 +1,50 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestSetLicense(t *testing.T) {
+ l1 := &model.License{}
+ l1.Features = &model.Features{}
+ l1.Customer = &model.Customer{}
+ l1.StartsAt = model.GetMillis() - 1000
+ l1.ExpiresAt = model.GetMillis() + 100000
+ if ok := SetLicense(l1); !ok {
+ t.Fatal("license should have worked")
+ }
+
+ l2 := &model.License{}
+ l2.Features = &model.Features{}
+ l2.Customer = &model.Customer{}
+ l2.StartsAt = model.GetMillis() - 1000
+ l2.ExpiresAt = model.GetMillis() - 100
+ if ok := SetLicense(l2); ok {
+ t.Fatal("license should have failed")
+ }
+
+ l3 := &model.License{}
+ l3.Features = &model.Features{}
+ l3.Customer = &model.Customer{}
+ l3.StartsAt = model.GetMillis() + 10000
+ l3.ExpiresAt = model.GetMillis() + 100000
+ if ok := SetLicense(l3); ok {
+ t.Fatal("license should have failed")
+ }
+}
+
+func TestValidateLicense(t *testing.T) {
+ b1 := []byte("junk")
+ if ok, _ := ValidateLicense(b1); ok {
+ t.Fatal("should have failed - bad license")
+ }
+
+ b2 := []byte("junkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunk")
+ if ok, _ := ValidateLicense(b2); ok {
+ t.Fatal("should have failed - bad license")
+ }
+}
diff --git a/utils/mail.go b/utils/mail.go
index 6625060de..2f2c10b61 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -4,10 +4,10 @@
package utils
import (
- l4g "code.google.com/p/log4go"
"crypto/tls"
"encoding/base64"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"net"
"net/mail"
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx
index 3143bec22..f70027498 100644
--- a/web/react/components/about_build_modal.jsx
+++ b/web/react/components/about_build_modal.jsx
@@ -15,6 +15,19 @@ export default class AboutBuildModal extends React.Component {
render() {
const config = global.window.mm_config;
+ const license = global.window.mm_license;
+
+ let title = 'Team Edition';
+ let licensee;
+ if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') {
+ title = 'Enterprise Edition';
+ licensee = (
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Licensed by:'}</div>
+ <div className='col-sm-9'>{license.Company}</div>
+ </div>
+ );
+ }
return (
<Modal
@@ -22,9 +35,15 @@ export default class AboutBuildModal extends React.Component {
onHide={this.doHide}
>
<Modal.Header closeButton={true}>
- <Modal.Title>{`Mattermost ${config.Version}`}</Modal.Title>
+ <Modal.Title>{'About Mattermost'}</Modal.Title>
</Modal.Header>
<Modal.Body>
+ <h4>{`Mattermost ${title}`}</h4>
+ {licensee}
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Version:'}</div>
+ <div className='col-sm-9'>{config.Version}</div>
+ </div>
<div className='row form-group'>
<div className='col-sm-3 info__label'>{'Build Number:'}</div>
<div className='col-sm-9'>{config.BuildNumber}</div>
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 32b2e9bb7..0f85c238d 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -22,6 +22,7 @@ import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
import TeamUsersTab from './team_users.jsx';
import TeamAnalyticsTab from './team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx';
+import LicenseSettingsTab from './license_settings.jsx';
export default class AdminController extends React.Component {
constructor(props) {
@@ -154,6 +155,8 @@ export default class AdminController extends React.Component {
tab = <LegalAndSupportSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'ldap_settings') {
tab = <LdapSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'license') {
+ tab = <LicenseSettingsTab />;
} else if (this.state.selected === 'team_users') {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 1279f4d22..5a5eaa055 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -155,6 +155,36 @@ export default class AdminSidebar extends React.Component {
}
}
+ let ldapSettings;
+ let licenseSettings;
+ if (global.window.mm_config.BuildEnterpriseReady === 'true') {
+ if (global.window.mm_license.IsLicensed === 'true') {
+ ldapSettings = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('ldap_settings')}
+ onClick={this.handleClick.bind(this, 'ldap_settings', null)}
+ >
+ {'LDAP Settings'}
+ </a>
+ </li>
+ );
+ }
+
+ licenseSettings = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('license')}
+ onClick={this.handleClick.bind(this, 'license', null)}
+ >
+ {'Edition and License'}
+ </a>
+ </li>
+ );
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -252,6 +282,7 @@ export default class AdminSidebar extends React.Component {
{'GitLab Settings'}
</a>
</li>
+ {ldapSettings}
<li>
<a
href='#'
@@ -300,6 +331,7 @@ export default class AdminSidebar extends React.Component {
</li>
</ul>
<ul className='nav nav__sub-menu padded'>
+ {licenseSettings}
<li>
<a
href='#'
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 193fd4147..c568c5a77 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -254,7 +254,7 @@ export default class EmailSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.'}</p>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.\nSetting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/ldap_settings.jsx b/web/react/components/admin_console/ldap_settings.jsx
index 6e3da2f72..1447f3bd7 100644
--- a/web/react/components/admin_console/ldap_settings.jsx
+++ b/web/react/components/admin_console/ldap_settings.jsx
@@ -90,14 +90,41 @@ export default class LdapSettings extends React.Component {
saveClass = 'btn btn-primary';
}
- return (
- <div className='wrapper--fixed'>
+ const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
+
+ let bannerContent;
+ if (licenseEnabled) {
+ bannerContent = (
<div className='banner'>
<div className='banner__content'>
<h4 className='banner__heading'>{'Note:'}</h4>
<p>{'If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.'}</p>
</div>
</div>
+ );
+ } else {
+ bannerContent = (
+ <div className='banner warning'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>
+ {'LDAP is an enterprise feature. Your current license does not support LDAP. Click '}
+ <a
+ href='http://mattermost.com'
+ target='_blank'
+ >
+ {'here'}
+ </a>
+ {' for information and pricing on enterprise licenses.'}
+ </p>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ {bannerContent}
<h3>{'LDAP Settings'}</h3>
<form
className='form-horizontal'
@@ -119,6 +146,7 @@ export default class LdapSettings extends React.Component {
ref='Enable'
defaultChecked={this.props.config.LdapSettings.Enable}
onChange={this.handleEnable}
+ disabled={!licenseEnabled}
/>
{'true'}
</label>
diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx
new file mode 100644
index 000000000..ba953f3bd
--- /dev/null
+++ b/web/react/components/admin_console/license_settings.jsx
@@ -0,0 +1,237 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from '../../utils/utils.jsx';
+import * as Client from '../../utils/client.jsx';
+
+export default class LicenseSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleRemove = this.handleRemove.bind(this);
+
+ this.state = {
+ fileSelected: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
+ if (element.prop('files').length > 0) {
+ this.setState({fileSelected: true});
+ }
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
+ if (element.prop('files').length === 0) {
+ return;
+ }
+ const file = element.prop('files')[0];
+
+ $('#upload-button').button('loading');
+
+ const formData = new FormData();
+ formData.append('license', file, file.name);
+
+ Client.uploadLicenseFile(formData,
+ () => {
+ Utils.clearFileInput(element[0]);
+ $('#upload-button').button('reset');
+ this.setState({serverError: null});
+ window.location.reload(true);
+ },
+ (error) => {
+ Utils.clearFileInput(element[0]);
+ $('#upload-button').button('reset');
+ this.setState({serverError: error.message});
+ }
+ );
+ }
+
+ handleRemove(e) {
+ e.preventDefault();
+
+ $('#remove-button').button('loading');
+
+ Client.removeLicenseFile(
+ () => {
+ $('#remove-button').button('reset');
+ this.setState({serverError: null});
+ window.location.reload(true);
+ },
+ (error) => {
+ $('#remove-button').button('reset');
+ this.setState({serverError: error.message});
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var btnClass = 'btn';
+ if (this.state.fileSelected) {
+ btnClass = 'btn btn-primary';
+ }
+
+ let edition;
+ let licenseType;
+ let licenseKey;
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ edition = 'Mattermost Enterprise Edition. Designed for enterprise-scale communication.';
+ licenseType = (
+ <div>
+ <p>
+ {'This compiled release of Mattermost platform is provided under a '}
+ <a
+ href='http://mattermost.com'
+ target='_blank'
+ >
+ {'commercial license'}
+ </a>
+ {' from Mattermost, Inc. based on your subscription level and is subject to the '}
+ <a
+ href={global.window.mm_config.TermsOfServiceLink}
+ target='_blank'
+ >
+ {'Terms of Service.'}
+ </a>
+ </p>
+ <p>{'Your subscription details are as follows:'}</p>
+ {'Name: ' + global.window.mm_license.Name}
+ <br/>
+ {'Company or organization name: ' + global.window.mm_license.Company}
+ <br/>
+ {'Number of users: ' + global.window.mm_license.Users}
+ <br/>
+ {`License issued: ${Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10))} ${Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true)}`}
+ <br/>
+ {'Start date of license: ' + Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10))}
+ <br/>
+ {'Expiry date of license: ' + Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10))}
+ <br/>
+ {'LDAP: ' + global.window.mm_license.LDAP}
+ <br/>
+ </div>
+ );
+
+ licenseKey = (
+ <div className='col-sm-8'>
+ <button
+ className='btn btn-danger'
+ onClick={this.handleRemove}
+ id='remove-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Removing License...'}
+ >
+ {'Remove Enterprise License and Downgrade Server'}
+ </button>
+ <br/>
+ <br/>
+ <p className='help-text'>
+ {'If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, '}
+ <a
+ href='http://mattermost.com'
+ target='_blank'
+ >
+ {'disable all Enterprise Edition features on this server'}
+ </a>
+ {'. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.'}
+ </p>
+ </div>
+ );
+ } else {
+ edition = 'Mattermost Team Edition. Designed for teams from 5 to 50 users.';
+
+ licenseType = (
+ <span>
+ <p>{'This compiled release of Mattermost platform is offered under an MIT license.'}</p>
+ <p>{'See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.'}</p>
+ </span>
+ );
+
+ licenseKey = (
+ <div className='col-sm-8'>
+ <input
+ className='pull-left'
+ ref='fileInput'
+ type='file'
+ accept='.mattermost-license'
+ onChange={this.handleChange}
+ />
+ <button
+ className={btnClass + ' pull-left'}
+ disabled={!this.state.fileSelected}
+ onClick={this.handleSubmit}
+ id='upload-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Uploading License...'}
+ >
+ {'Upload'}
+ </button>
+ <br/>
+ <br/>
+ <br/>
+ {serverError}
+ <p className='help-text'>
+ {'Upload a license key for Mattermost Enterprise Edition to upgrade this server. '}
+ <a
+ href='http://mattermost.com'
+ target='_blank'
+ >
+ {'Visit us online'}
+ </a>
+ {' to learn more about the benefits of Enterprise Edition or to purchase a key.'}
+ </p>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Edition and License'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ {'Edition: '}
+ </label>
+ <div className='col-sm-8'>
+ {edition}
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ {'License: '}
+ </label>
+ <div className='col-sm-8'>
+ {licenseType}
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ {'License Key: '}
+ </label>
+ {licenseKey}
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
index e28699d3c..fe7230946 100644
--- a/web/react/components/admin_console/team_analytics.jsx
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -3,8 +3,12 @@
import * as Client from '../../utils/client.jsx';
import * as Utils from '../../utils/utils.jsx';
+import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
export default class TeamAnalytics extends React.Component {
constructor(props) {
super(props);
@@ -314,9 +318,25 @@ export default class TeamAnalytics extends React.Component {
<tbody>
{
this.state.recent_active_users.map((user) => {
+ const tooltip = (
+ <Tooltip id={'recent-user-email-tooltip-' + user.id}>
+ {user.email}
+ </Tooltip>
+ );
+
return (
- <tr key={user.id}>
- <td>{user.email}</td>
+ <tr key={'recent-user-table-entry-' + user.id}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {user.username}
+ </time>
+ </OverlayTrigger>
+ </td>
<td>{Utils.displayDateTime(user.last_activity_at)}</td>
</tr>
);
@@ -347,9 +367,25 @@ export default class TeamAnalytics extends React.Component {
<tbody>
{
this.state.newly_created_users.map((user) => {
+ const tooltip = (
+ <Tooltip id={'new-user-email-tooltip-' + user.id}>
+ {user.email}
+ </Tooltip>
+ );
+
return (
- <tr key={user.id}>
- <td>{user.email}</td>
+ <tr key={'new-user-table-entry-' + user.id}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {user.username}
+ </time>
+ </OverlayTrigger>
+ </td>
<td>{Utils.displayDateTime(user.create_at)}</td>
</tr>
);
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 59ceb038e..f64834775 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -379,7 +379,7 @@ export default class ChannelHeader extends React.Component {
<th>
<div className='dropdown channel-header__links'>
<OverlayTrigger
- delayShow={400}
+ delayShow={Constants.OVERLAY_TIME_DELAY}
placement='bottom'
overlay={recentMentionsTooltip}
>
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 6337afabc..fef253c52 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as client from '../utils/client.jsx';
+import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -26,7 +26,7 @@ export default class FileUpload extends React.Component {
for (var j = 0; j < data.client_ids.length; j++) {
delete requests[data.client_ids[j]];
}
- this.setState({requests: requests});
+ this.setState({requests});
}
fileUploadFail(clientId, err) {
@@ -52,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
- let clientId = Utils.generateId();
+ const clientId = Utils.generateId();
// prepare data to be uploaded
var formData = new FormData();
@@ -60,14 +60,14 @@ export default class FileUpload extends React.Component {
formData.append('files', files[i], files[i].name);
formData.append('client_ids', clientId);
- var request = client.uploadFile(formData,
+ var request = Client.uploadFile(formData,
this.fileUploadSuccess.bind(this, channelId),
this.fileUploadFail.bind(this, clientId)
);
var requests = this.state.requests;
requests[clientId] = request;
- this.setState({requests: requests});
+ this.setState({requests});
this.props.onUploadStart([clientId], channelId);
@@ -90,16 +90,7 @@ export default class FileUpload extends React.Component {
this.uploadFiles(element.prop('files'));
- // clear file input for all modern browsers
- try {
- element[0].value = '';
- if (element.value) {
- element[0].type = 'text';
- element[0].type = 'file';
- }
- } catch (e) {
- // Do nothing
- }
+ Utils.clearFileInput(element[0]);
}
handleDrop(e) {
@@ -227,14 +218,14 @@ export default class FileUpload extends React.Component {
formData.append('files', file, name);
formData.append('client_ids', clientId);
- var request = client.uploadFile(formData,
+ var request = Client.uploadFile(formData,
self.fileUploadSuccess.bind(self, channelId),
self.fileUploadFail.bind(self, clientId)
);
var requests = self.state.requests;
requests[clientId] = request;
- self.setState({requests: requests});
+ self.setState({requests});
self.props.onUploadStart([clientId], channelId);
}
@@ -263,7 +254,7 @@ export default class FileUpload extends React.Component {
request.abort();
delete requests[clientId];
- this.setState({requests: requests});
+ this.setState({requests});
}
}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 56bc00a7e..7e1627555 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -8,6 +8,7 @@ import * as Client from '../utils/client.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import ConfirmModal from './confirm_modal.jsx';
@@ -304,6 +305,11 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
+ var defaultChannelName = '';
+ if (ChannelStore.getByName(Constants.DEFAULT_CHANNEL)) {
+ defaultChannelName = ChannelStore.getByName(Constants.DEFAULT_CHANNEL).display_name;
+ }
+
if (this.state.emailEnabled && this.state.userCreationEnabled) {
content = (
<div>
@@ -312,10 +318,10 @@ export default class InviteMemberModal extends React.Component {
type='button'
className='btn btn-default'
onClick={this.addInviteFields}
- >Add another</button>
+ >{'Add another'}</button>
<br/>
<br/>
- <span>People invited automatically join Town Square channel.</span>
+ <span>{'People invited automatically join the '}<strong>{defaultChannelName}</strong>{' channel.'}</span>
</div>
);
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 18c360cb8..eaeb7bb91 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -372,7 +372,7 @@ export default class Sidebar extends React.Component {
if (channel.status === 'online') {
statusIcon = Constants.ONLINE_ICON_SVG;
} else if (channel.status === 'away') {
- statusIcon = Constants.ONLINE_ICON_SVG;
+ statusIcon = Constants.AWAY_ICON_SVG;
} else {
statusIcon = Constants.OFFLINE_ICON_SVG;
}
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
index cffff6ee7..32947bd60 100644
--- a/web/react/components/time_since.jsx
+++ b/web/react/components/time_since.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import Constants from '../utils/constants.jsx';
import * as Utils from '../utils/utils.jsx';
var Tooltip = ReactBootstrap.Tooltip;
@@ -30,7 +31,7 @@ export default class TimeSince extends React.Component {
return (
<OverlayTrigger
- delayShow={400}
+ delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
overlay={tooltip}
>
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index 3df9dfedf..45b05f19b 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -55,6 +55,7 @@ export default class ImportThemeModal extends React.Component {
theme.sidebarHeaderBg = colors[1];
theme.sidebarHeaderTextColor = colors[5];
theme.onlineIndicator = colors[6];
+ theme.awayIndicator = '#E0B333';
theme.mentionBj = colors[7];
theme.mentionColor = '#ffffff';
theme.centerChannelBg = '#ffffff';
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 96d1ef720..d60fea872 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -1392,3 +1392,38 @@ export function regenOutgoingHookToken(data, success, error) {
}
});
}
+
+export function uploadLicenseFile(formData, success, error) {
+ $.ajax({
+ url: '/api/v1/license/add',
+ type: 'POST',
+ data: formData,
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('uploadLicenseFile', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_license_upload');
+}
+
+export function removeLicenseFile(success, error) {
+ $.ajax({
+ url: '/api/v1/license/remove',
+ type: 'POST',
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('removeLicenseFile', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_license_upload');
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 0298ce533..d0f34293f 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -163,8 +163,9 @@ export default {
OPEN_TEAM: 'O',
MAX_POST_LEN: 4000,
EMOJI_SIZE: 16,
- ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
- OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>",
+ ONLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-243 245 12 12'style='enable-background:new -243 245 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <path class='online--icon' d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'/> <ellipse class='online--icon' cx='-238.5' cy='248' rx='2.5' ry='2.5'/> </g> <path class='online--icon' d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'/> <g> <g> <path class='online--icon' d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'/> </g> </g> </svg>",
+ AWAY_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <ellipse class='away--icon' cx='-294.6' cy='394' rx='2.5' ry='2.5'/> <path class='away--icon' d='M-293.8,399.4c0-0.4,0.1-0.7,0.2-1c-0.3,0.1-0.6,0.2-1,0.2c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0.7,0,1.4-0.1,2-0.3 C-293.3,401.5-293.8,400.5-293.8,399.4z'/> </g> <path class='away--icon' d='M-287,400c0,0.1-0.1,0.1-0.1,0.1l-4.9,0c-0.1,0-0.1-0.1-0.1-0.1v-1.6c0-0.1,0.1-0.1,0.1-0.1l4.9,0c0.1,0,0.1,0.1,0.1,0.1 V400z'/> </svg>",
+ OFFLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <g> <ellipse class='offline--icon' cx='-294.5' cy='394' rx='2.5' ry='2.5'/> <path class='offline--icon' d='M-294.3,399.7c0-0.4,0.1-0.8,0.2-1.2c-0.1,0-0.2,0-0.4,0c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4h0.1h0.1c0.3,0,0.7,0,1-0.1C-293.9,401.6-294.3,400.7-294.3,399.7z'/> </g> </g> <g> <path class='offline--icon' d='M-288.9,399.4l1.8-1.8c0.1-0.1,0.1-0.3,0-0.3l-0.7-0.7c-0.1-0.1-0.3-0.1-0.3,0l-1.8,1.8l-1.8-1.8c-0.1-0.1-0.3-0.1-0.3,0 l-0.7,0.7c-0.1,0.1-0.1,0.3,0,0.3l1.8,1.8l-1.8,1.8c-0.1,0.1-0.1,0.3,0,0.3l0.7,0.7c0.1,0.1,0.3,0.1,0.3,0l1.8-1.8l1.8,1.8 c0.1,0.1,0.3,0.1,0.3,0l0.7-0.7c0.1-0.1,0.1-0.3,0-0.3L-288.9,399.4z'/> </g> </svg>",
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
UPDATE_TYPING_MS: 5000,
@@ -180,6 +181,7 @@ export default {
sidebarHeaderBg: '#2f81b7',
sidebarHeaderTextColor: '#FFFFFF',
onlineIndicator: '#7DBE00',
+ awayIndicator: '#DCBD4E',
mentionBj: '#136197',
mentionColor: '#bfcde8',
centerChannelBg: '#f2f4f8',
@@ -203,6 +205,7 @@ export default {
sidebarHeaderBg: '#2389d7',
sidebarHeaderTextColor: '#ffffff',
onlineIndicator: '#7DBE00',
+ awayIndicator: '#DCBD4E',
mentionBj: '#2389d7',
mentionColor: '#ffffff',
centerChannelBg: '#ffffff',
@@ -226,6 +229,7 @@ export default {
sidebarHeaderBg: '#1B2C3E',
sidebarHeaderTextColor: '#FFFFFF',
onlineIndicator: '#55C5B2',
+ awayIndicator: '#A9A14C',
mentionBj: '#B74A4A',
mentionColor: '#FFFFFF',
centerChannelBg: '#2F3E4E',
@@ -249,6 +253,7 @@ export default {
sidebarHeaderBg: '#1f1f1f',
sidebarHeaderTextColor: '#FFFFFF',
onlineIndicator: '#0177e7',
+ awayIndicator: '#A9A14C',
mentionBj: '#0177e7',
mentionColor: '#FFFFFF',
centerChannelBg: '#1F1F1F',
@@ -300,6 +305,10 @@ export default {
uiName: 'Online Indicator'
},
{
+ id: 'awayIndicator',
+ uiName: 'Away Indicator'
+ },
+ {
id: 'mentionBj',
uiName: 'Mention Jewel BG'
},
@@ -443,5 +452,6 @@ export default {
label: 'embed_preview',
description: 'Show preview snippet of links below message'
}
- }
+ },
+ OVERLAY_TIME_DELAY: 400
};
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 2ddd0e5e3..24042321f 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -201,11 +201,21 @@ export function displayDate(ticks) {
return monthNames[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
}
-export function displayTime(ticks) {
+export function displayTime(ticks, utc) {
const d = new Date(ticks);
- let hours = d.getHours();
- let minutes = d.getMinutes();
+ let hours;
+ let minutes;
let ampm = '';
+ let timezone = '';
+
+ if (utc) {
+ hours = d.getUTCHours();
+ minutes = d.getUTCMinutes();
+ timezone = ' UTC';
+ } else {
+ hours = d.getHours();
+ minutes = d.getMinutes();
+ }
if (minutes <= 9) {
minutes = '0' + minutes;
@@ -224,7 +234,7 @@ export function displayTime(ticks) {
}
}
- return hours + ':' + minutes + ampm;
+ return hours + ':' + minutes + ampm + timezone;
}
export function displayDateTime(ticks) {
@@ -572,7 +582,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
- changeCss('.sidebar--left .status path', 'fill:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .status .offline--icon, .sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText, 1);
changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
@@ -617,6 +627,10 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1);
}
+ if (theme.awayIndicator) {
+ changeCss('.sidebar--left .status .away--icon', 'fill:' + theme.awayIndicator, 1);
+ }
+
if (theme.mentionBj) {
changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1);
changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj, 1);
@@ -1297,3 +1311,16 @@ export function fillArray(value, length) {
export function isFileTransfer(files) {
return files.types != null && (files.types.indexOf ? files.types.indexOf('Files') !== -1 : files.types.contains('application/x-moz-file'));
}
+
+export function clearFileInput(elm) {
+ // clear file input for all modern browsers
+ try {
+ elm.value = '';
+ if (elm.value) {
+ elm.type = 'text';
+ elm.type = 'file';
+ }
+ } catch (e) {
+ // Do nothing
+ }
+}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index abba9de02..b28c7d984 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -174,6 +174,9 @@
.banner__content {
width: 80%;
}
+ &.warning {
+ background: #e60000;
+ }
}
.popover {
border-radius: 3px;
@@ -223,4 +226,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index e99e21257..6f969ed47 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -42,9 +42,9 @@
margin-right: 6px;
width: 12px;
display: inline-block;
- i, path {
+ i, path, ellipse {
@include opacity(0.5);
- &.online--icon {
+ &.online--icon, &.away--icon {
@include opacity(1);
}
}
diff --git a/web/templates/head.html b/web/templates/head.html
index 08d8726ea..689c69d3c 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -46,6 +46,7 @@
<script>
window.mm_config = {{ .ClientCfg }};
+ window.mm_license = {{ .ClientLicense }};
window.mm_team = {{ .Team }};
window.mm_user = {{ .User }};
window.mm_channel = {{ .Channel }};
diff --git a/web/web.go b/web/web.go
index c0fbb9808..016e0c147 100644
--- a/web/web.go
+++ b/web/web.go
@@ -4,8 +4,8 @@
package web
import (
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
@@ -32,7 +32,7 @@ func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage {
props := make(map[string]string)
props["Title"] = title
- return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg}
+ return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg, ClientLicense: utils.ClientLicense}
}
func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) {