mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' into PLT-1429
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
131
CHANGELOG.md
131
CHANGELOG.md
@@ -1,5 +1,136 @@
|
||||
# Mattermost Changelog
|
||||
|
||||
## Release v1.4.0
|
||||
|
||||
Expected Release date: 2016-01-16
|
||||
|
||||
### Release Highlights
|
||||
|
||||
#### Data Center Support
|
||||
|
||||
- Deployment guides on Red Hat Enterprise Linux 6 and 7 now available
|
||||
- Legal disclosure and support links (terms of service, privacy policy, help, about, and support email) now configurable
|
||||
- Over a dozen new configuration options in System Console
|
||||
|
||||
#### Mobile Experience
|
||||
|
||||
- iOS reference app [now available from iTunes](https://itunes.apple.com/us/app/mattermost/id984966508?ls=1&mt=8), compiled from [open source repo](https://github.com/mattermost/ios)
|
||||
- Date headers now show when scrolling on mobile, so you can quickly see when messages were sent
|
||||
- Added "rapid scroll" support for jumping quickily to bottom of channels on mobile
|
||||
|
||||
### New Features
|
||||
|
||||
Mobile Experience
|
||||
- Date headers now show when scrolling on mobile, so you can quickly see when messages were sent
|
||||
- Added "rapid scroll" support for jumping quickily to bottom of channels on mobile
|
||||
|
||||
Authentication
|
||||
|
||||
- Accounts can now switch between email and GitLab SSO sign-in options
|
||||
- New ability to customize session token length
|
||||
|
||||
System Console
|
||||
|
||||
- Added **Legal and Support Settings** so System Administrators can change the default Terms of Service, Privacy Policy, and Help links
|
||||
- Under **Service Settings** added options to customize expiry of web, mobile and SSO session tokens, expiry of caches in memory, and an EnableDeveloper option to turn on Developer Mode which alerts users to any console errors that occur
|
||||
|
||||
### Improvements
|
||||
|
||||
Performance and Testing
|
||||
|
||||
- Added logging for email and push notifications events in DEBUG mode
|
||||
|
||||
Integrations
|
||||
|
||||
- Added support to allow optional parameters in the `Content-Type` of incoming webhook requests
|
||||
|
||||
Files and Images
|
||||
|
||||
- Animated GIFs autoplay in the image previewer
|
||||
|
||||
Notifications and Email
|
||||
|
||||
- Changed email notifications to display the server's local timezone instead of UTC
|
||||
|
||||
User Interface
|
||||
|
||||
- Updated the "About Mattermost" dialog formatting
|
||||
- Going to domain/teamname now goes to the last channel of your previous session, instead of Town Square
|
||||
- Various improvements to mobile UI, including a floating date indicator and the ability to quickly scroll to the bottom of the channel
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fixed issue where usernames containing a "." did not get mention notifications
|
||||
- Fixed issue where System Console did not save the "Send push notifications" setting
|
||||
- Fixed issue with Font Display cancel button not working in Account Settings menu
|
||||
- Fixed incorrect default for "Team Name Display" settings
|
||||
- Fixed issue where various media files appeared broken in the media player on some browsers
|
||||
- Fixed cross-contamination issue when multiple accounts log into the same team on the same browser
|
||||
- Fixed issue where color pickers did not update when a theme was pasted in
|
||||
- Increased the maximum number of channels
|
||||
|
||||
### Compatibility
|
||||
|
||||
#### Config.json Changes from v1.3 to v1.4
|
||||
|
||||
Multiple settings were added to `config.json`. Below is a list of the changes and their new default values in a fresh install.
|
||||
|
||||
The following options can be modified in the System Console:
|
||||
|
||||
- Under `ServiceSettings` in `config.json`:
|
||||
- Added: `"EnableDeveloper": false` to set whether developer mode is enabled, which alerts users to any console errors that occur
|
||||
- Added: `"SessionLengthWebInDays" : 30` to set the number of days before web sessions expire and users will need to log in again
|
||||
- Added: `"SessionLengthMobileInDays" : 30` to set the number of days before native mobile sessions expire
|
||||
- Added: `"SessionLengthSSOInDays" : 30` to set the number of days before SSO sessions expire
|
||||
- Added: `"SessionCacheInMinutes" : 10` to set the number of minutes to cache a session in memory
|
||||
- Added `SupportSettings` section to `config.json`:
|
||||
- Added: `"TermsOfServiceLink": "/static/help/terms.html"` to allow System Administrators to set the terms of service link
|
||||
- Added: `"PrivacyPolicyLink": "/static/help/privacy.html"` to allow System Administrators to set the privacy policy link
|
||||
- Added: `"AboutLink": "/static/help/about.html"` to allow System Administrators to set the about page link
|
||||
- Added: `"HelpLink": "/static/help/help.html"` to allow System Administrators to set the help page link
|
||||
- Added: `"ReportAProblemLink": "/static/help/report_problem.html"` to allow System Administrators to set the home page for the support website
|
||||
- Added: `"SupportEmail":"feedback@mattermost.com"` to allow System Administrators to set an email address for feedback and support requests
|
||||
|
||||
The following options are not present in the System Console, and can be modified manually in the `config.json` file:
|
||||
|
||||
- Under `FileSettings` in `config.json`:
|
||||
- Added: `"AmazonS3Endpoint": ""` to set an endpoint URL for an Amazon S3 instance
|
||||
- Added: `"AmazonS3BucketEndpoint": ""` to set an endpoint URL for Amazon S3 buckets
|
||||
- Added: `"AmazonS3LocationConstraint": false` to set whether the S3 region is location constrained
|
||||
- Added: `"AmazonS3LowercaseBucket": false` to set whether bucket names are fully lowercase or not
|
||||
|
||||
#### Known Issues
|
||||
|
||||
- When navigating to a page with new messages as well as message containing inline images added via markdown, the channel may move up and down while loading the inline images
|
||||
- Microsoft Edge does not yet support drag and drop
|
||||
- No scroll bar in center channel
|
||||
- Pasting images into text box fails to upload on Firefox, Safari, and IE11
|
||||
- Public links for attachments attempt to download the file on IE, Edge, and Safari
|
||||
- Importing from Slack breaks @mentions and fails to load in certain cases with comments on files
|
||||
- System Console > TEAMS > Statistics > Newly Created Users shows all of the users are created "just now"
|
||||
- Favicon does not always become red when @mentions and direct messages are received on an inactive browser tab
|
||||
- Searching for a phrase in quotations returns more than just the phrase on Mattermost installations with a Postgres database
|
||||
- Deleted/Archived channels are not removed from the "More" menu of the person that deleted/archived the channel until after refresh
|
||||
- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks
|
||||
- Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator
|
||||
- Hashtags less than three characters long are not searchable
|
||||
- After deactivating a team member, the person remains in the channel counter
|
||||
- Certain symbols (<,>,-,+,=,%,^,#,*,|) directly before or after a hashtag cause the message to not show up in a hashtag search
|
||||
- Security tab > Active Sessions reports iOS devices as "unknown"
|
||||
- Getting a permalink for the second message or later consecutively sent in a group by the same author displaces the copy link popover or causes an error
|
||||
|
||||
#### Contributors
|
||||
|
||||
Many thanks to our external contributors. In no particular order:
|
||||
|
||||
- [npcode](https://github.com/npcode)
|
||||
- [hjf288](https://github.com/hjf288)
|
||||
- [apskim](https://github.com/apskim)
|
||||
- [ejm2172](https://github.com/ejm2172)
|
||||
- [hvnsweeting](https://github.com/hvnsweeting)
|
||||
- [benburkert](https://github.com/benburkert)
|
||||
- [erikthered](https://github.com/erikthered)
|
||||
|
||||
## Release v1.3.0
|
||||
|
||||
Release date: 2015-12-16
|
||||
|
||||
5
Godeps/Godeps.json
generated
5
Godeps/Godeps.json
generated
@@ -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",
|
||||
|
||||
4
Godeps/_workspace/src/code.google.com/p/log4go/.hgtags
generated
vendored
4
Godeps/_workspace/src/code.google.com/p/log4go/.hgtags
generated
vendored
@@ -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
|
||||
534
Godeps/_workspace/src/code.google.com/p/log4go/log4go_test.go
generated
vendored
534
Godeps/_workspace/src/code.google.com/p/log4go/log4go_test.go
generated
vendored
@@ -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
|
||||
45
Godeps/_workspace/src/code.google.com/p/log4go/termlog.go
generated
vendored
45
Godeps/_workspace/src/code.google.com/p/log4go/termlog.go
generated
vendored
@@ -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)
|
||||
}
|
||||
2
Godeps/_workspace/src/github.com/alecthomas/log4go/.gitignore
generated
vendored
Normal file
2
Godeps/_workspace/src/github.com/alecthomas/log4go/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.sw[op]
|
||||
.DS_Store
|
||||
@@ -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 {
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
package log4go
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -37,6 +37,7 @@ type FileLogWriter struct {
|
||||
|
||||
// Keep old logfiles (.001, .002, etc)
|
||||
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
|
||||
@@ -64,6 +66,7 @@ func NewFileLogWriter(fname string, rotate bool) *FileLogWriter {
|
||||
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 := ""
|
||||
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(".%03d", 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
49
Godeps/_workspace/src/github.com/alecthomas/log4go/termlog.go
generated
vendored
Normal file
49
Godeps/_workspace/src/github.com/alecthomas/log4go/termlog.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
4
Makefile
4
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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
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"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultExtraMemberLimit = 100
|
||||
)
|
||||
|
||||
func InitChannel(r *mux.Router) {
|
||||
l4g.Debug("Initializing channel api routes")
|
||||
|
||||
@@ -27,6 +32,7 @@ func InitChannel(r *mux.Router) {
|
||||
sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST")
|
||||
sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
|
||||
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
|
||||
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
|
||||
sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(join)).Methods("POST")
|
||||
sr.Handle("/{id:[A-Za-z0-9]+}/leave", ApiUserRequired(leave)).Methods("POST")
|
||||
sr.Handle("/{id:[A-Za-z0-9]+}/delete", ApiUserRequired(deleteChannel)).Methods("POST")
|
||||
@@ -730,10 +736,19 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
params := mux.Vars(r)
|
||||
id := params["id"]
|
||||
|
||||
var memberLimit int
|
||||
if memberLimitString, ok := params["member_limit"]; !ok {
|
||||
memberLimit = defaultExtraMemberLimit
|
||||
} else if memberLimitInt64, err := strconv.ParseInt(memberLimitString, 10, 0); err != nil {
|
||||
c.Err = model.NewAppError("getChannelExtraInfo", "Failed to parse member limit", err.Error())
|
||||
return
|
||||
} else {
|
||||
memberLimit = int(memberLimitInt64)
|
||||
}
|
||||
|
||||
sc := Srv.Store.Channel().Get(id)
|
||||
var channel *model.Channel
|
||||
if cresult := <-sc; cresult.Err != nil {
|
||||
@@ -743,13 +758,13 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
channel = cresult.Data.(*model.Channel)
|
||||
}
|
||||
|
||||
extraEtag := channel.ExtraEtag()
|
||||
extraEtag := channel.ExtraEtag(memberLimit)
|
||||
if HandleEtag(extraEtag, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
scm := Srv.Store.Channel().GetMember(id, c.Session.UserId)
|
||||
ecm := Srv.Store.Channel().GetExtraMembers(id, 100)
|
||||
ecm := Srv.Store.Channel().GetExtraMembers(id, memberLimit)
|
||||
ccm := Srv.Store.Channel().GetMemberCount(id)
|
||||
|
||||
if cmresult := <-scm; cmresult.Err != nil {
|
||||
|
||||
@@ -189,7 +189,7 @@ func BenchmarkGetChannelExtraInfo(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range channels {
|
||||
Client.Must(Client.GetChannelExtraInfo(channels[j].Id, ""))
|
||||
Client.Must(Client.GetChannelExtraInfo(channels[j].Id, -1, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,7 +674,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id, ""))
|
||||
rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id, -1, ""))
|
||||
data := rget.Data.(*model.ChannelExtra)
|
||||
if data.Id != channel1.Id {
|
||||
t.Fatal("couldnt't get extra info")
|
||||
@@ -690,7 +690,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
|
||||
currentEtag := rget.Etag
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache_result.Data.(*model.ChannelExtra) != nil {
|
||||
t.Log(cache_result.Data)
|
||||
@@ -708,7 +708,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
Client2.LoginByEmail(team.Name, user2.Email, "pwd")
|
||||
Client2.Must(Client2.JoinChannel(channel1.Id))
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache_result.Data.(*model.ChannelExtra) == nil {
|
||||
t.Log(cache_result.Data)
|
||||
@@ -717,7 +717,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
currentEtag = cache_result.Etag
|
||||
}
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache_result.Data.(*model.ChannelExtra) != nil {
|
||||
t.Log(cache_result.Data)
|
||||
@@ -728,7 +728,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
|
||||
Client2.Must(Client2.LeaveChannel(channel1.Id))
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache_result.Data.(*model.ChannelExtra) == nil {
|
||||
t.Log(cache_result.Data)
|
||||
@@ -737,7 +737,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
currentEtag = cache_result.Etag
|
||||
}
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache_result.Data.(*model.ChannelExtra) != nil {
|
||||
t.Log(cache_result.Data)
|
||||
@@ -745,6 +745,42 @@ func TestGetChannelExtraInfo(t *testing.T) {
|
||||
} else {
|
||||
currentEtag = cache_result.Etag
|
||||
}
|
||||
|
||||
Client2.Must(Client2.JoinChannel(channel1.Id))
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 2, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil {
|
||||
t.Fatal("response should not be empty")
|
||||
} else if len(extra.Members) != 2 {
|
||||
t.Fatal("should've returned 2 members")
|
||||
} else if extra.MemberCount != 2 {
|
||||
t.Fatal("should've returned member count of 2")
|
||||
} else {
|
||||
currentEtag = cache_result.Etag
|
||||
}
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil {
|
||||
t.Fatal("response should not be empty")
|
||||
} else if len(extra.Members) != 1 {
|
||||
t.Fatal("should've returned only 1 member")
|
||||
} else if extra.MemberCount != 2 {
|
||||
t.Fatal("should've returned member count of 2")
|
||||
} else {
|
||||
currentEtag = cache_result.Etag
|
||||
}
|
||||
|
||||
if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache_result.Data.(*model.ChannelExtra) != nil {
|
||||
t.Log(cache_result.Data)
|
||||
t.Fatal("response should be empty")
|
||||
} else {
|
||||
currentEtag = cache_result.Etag
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddChannelMember(t *testing.T) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
22
api/file.go
22
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 {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
|
||||
100
api/license.go
Normal file
100
api/license.go
Normal file
@@ -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)))
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
29
api/user.go
29
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,20 +141,30 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool {
|
||||
shouldVerifyHash := true
|
||||
func CheckUserDomain(user *model.User, domains string) bool {
|
||||
if len(domains) == 0 {
|
||||
return 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))))
|
||||
domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
|
||||
|
||||
matched := false
|
||||
for _, d := range domains {
|
||||
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 {
|
||||
matched := CheckUserDomain(user, team.AllowedDomains)
|
||||
|
||||
if matched {
|
||||
shouldVerifyHash = false
|
||||
} else {
|
||||
@@ -1794,7 +1809,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string
|
||||
props["team"] = teamName
|
||||
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
|
||||
|
||||
redirectUri := c.GetSiteURL() + "/" + service + "/complete"
|
||||
redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
|
||||
|
||||
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:**
|
||||
|
||||
43
doc/developer/tests/test-search.md
Normal file
43
doc/developer/tests/test-search.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Search Testing
|
||||
|
||||
### Basic Search Testing
|
||||
|
||||
This post is used by the core team to test search. It should be returned for the test cases for search, with proper highlighting in the search results.
|
||||
|
||||
**Basic word search:** Hello world!
|
||||
**Emoji search:** :strawberry:
|
||||
**Accent search:** Crème friache
|
||||
**Non-latin search:**
|
||||
您好吗
|
||||
您好
|
||||
**Email search:** person@domain.org
|
||||
**Link search:** www.dropbox.com
|
||||
**Markdown search:**
|
||||
##### Hello
|
||||
```
|
||||
Hello
|
||||
```
|
||||
`Hello`
|
||||
|
||||
|
||||
### Hashtags:
|
||||
|
||||
Click on the linked hashtags below, and confirm that the search results match the linked hashtags. Confirm that the highlighting in the search results is correct.
|
||||
|
||||
#### Basic Hashtags
|
||||
|
||||
#hello #world
|
||||
|
||||
#### Hashtags containing punctuation:
|
||||
|
||||
*Note: Make a separate post containing only the word “hashtag”, and confirm the hashtags below do not return the separate post.*
|
||||
|
||||
#hashtag #hashtag-dash #hashtag_underscore #hashtag.dot
|
||||
|
||||
#### Punctuation following a hashtag:
|
||||
|
||||
#colon: #slash/ #backslash\ #percent% #dollar$ #semicolon; #ampersand& #bracket( #bracket) #lessthan< #greaterthan> #dash- #plus+ #equals= #caret^ #hashtag# #asterisk* #verticalbar| #invertedquestion¿ #atsign@ #quote” #apostrophe' #curlybracket{ #curlybracket} #squarebracket[ #squarebracket]
|
||||
|
||||
#### Markdown surrounding a hashtag:
|
||||
|
||||
*#markdown-hashtag*
|
||||
@@ -23,8 +23,23 @@ Set this key to enable embedding of YouTube video previews based on hyperlinks a
|
||||
```"EnableTesting": false```
|
||||
"true": `/loadtest` slash command is enabled to load test accounts and test data.
|
||||
|
||||
```"EnableDeveloper": false```
|
||||
"true": Users are alerted to any console errors that occur.
|
||||
|
||||
```"EnableSecurityFixAlert": true```
|
||||
”true”: System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.
|
||||
"true": System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.
|
||||
|
||||
```"SessionLengthWebInDays" : 30```
|
||||
Set the number of days before web sessions expire and users will need to log in again.
|
||||
|
||||
```"SessionLengthMobileInDays" : 30```
|
||||
Set the number of days before native mobile sessions expire.
|
||||
|
||||
```"SessionLengthSSOInDays" : 30```
|
||||
Set the number of days before SSO sessions expire.
|
||||
|
||||
```"SessionCacheInMinutes" : 10```
|
||||
Set the number of minutes to cache a session in memory.
|
||||
|
||||
#### Webhooks
|
||||
|
||||
@@ -108,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.
|
||||
|
||||
@@ -198,7 +215,7 @@ The width to which profile pictures are resized after being uploaded via Account
|
||||
The height to which profile pictures are resized after being uploaded via Account Settings.
|
||||
|
||||
```"EnablePublicLink": true```
|
||||
”true”: Allow users to share public links to files and images when previewing; “false”: The Get Public Link option is hidden from the image preview user interface.
|
||||
"true": Allow users to share public links to files and images when previewing; “false”: The Get Public Link option is hidden from the image preview user interface.
|
||||
|
||||
```"PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip"```
|
||||
32-character (to be randomly generated via Admin Console) salt added to signing of public image links.
|
||||
@@ -211,7 +228,7 @@ Settings to configure the console and log file output, detail level, format and
|
||||
#### Console Settings
|
||||
|
||||
```"EnableConsole": true```
|
||||
“true”: Output log messages to the console based on **ConsoleLevel** option. The server writes messages to the standard output stream (stdout).
|
||||
"true": Output log messages to the console based on **ConsoleLevel** option. The server writes messages to the standard output stream (stdout).
|
||||
|
||||
```"ConsoleLevel": "DEBUG"```
|
||||
Level of detail at which log events are written to the console when **EnableConsole**=true. ”ERROR”: Outputs only error messages; “INFO”: Outputs error messages and information around startup and initialization; “DEBUG”: Prints high detail for developers debugging issues.
|
||||
@@ -219,7 +236,7 @@ Level of detail at which log events are written to the console when **EnableCons
|
||||
#### Log File Settings
|
||||
|
||||
```"EnableFile": true```
|
||||
”true”: Log files are written to files specified in **FileLocation**.
|
||||
"true": Log files are written to files specified in **FileLocation**.
|
||||
|
||||
```"FileLevel": "INFO"```
|
||||
Level of detail at which log events are written to log files when **EnableFile**=true. “ERROR”: Outputs only error messages; “INFO”: Outputs error messages and information around startup and initialization; “DEBUG”: Prints high detail for developers debugging issues.
|
||||
@@ -243,7 +260,7 @@ Directory to which log files are written. If blank, log files write to ./logs/ma
|
||||
Settings to enable API rate limiting and configure requests per second, user sessions and variables for rate limiting. Changing properties in this section will require a server restart before taking effect.
|
||||
|
||||
```"EnableRateLimiter": true```
|
||||
”true”: APIs are throttled at the rate specified by **PerSec**.
|
||||
"true": APIs are throttled at the rate specified by **PerSec**.
|
||||
|
||||
```"PerSec": 10```
|
||||
Throttle API at this number of requests per second if **EnableRateLimiter**=true.
|
||||
@@ -262,17 +279,17 @@ Vary rate limiting by HTTP header field specified (e.g. when configuring Ngnix s
|
||||
Settings to configure the name and email privacy of users on your system.
|
||||
|
||||
```"ShowEmailAddress": true```
|
||||
“true”: Show email address of all users; "false": Hide email address of users from other users in the user interface, including team owners and team administrators. This is designed for managing teams where users choose to keep their contact information private.
|
||||
"true": Show email address of all users; "false": Hide email address of users from other users in the user interface, including team owners and team administrators. This is designed for managing teams where users choose to keep their contact information private.
|
||||
|
||||
```"ShowFullName": true```
|
||||
”true”: Show full name of all users; “false”: hide full name of users from other users including team owner and team administrators.
|
||||
"true": Show full name of all users; “false”: hide full name of users from other users including team owner and team administrators.
|
||||
|
||||
### GitLab Settings
|
||||
|
||||
Settings to configure account and team creation using GitLab OAuth.
|
||||
|
||||
```"Enable": false```
|
||||
“true”: Allow team creation and account signup using GitLab OAuth. To configure, input the **Secret** and **Id** credentials.
|
||||
"true": Allow team creation and account signup using GitLab OAuth. To configure, input the **Secret** and **Id** credentials.
|
||||
|
||||
```"Secret": ""```
|
||||
Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs `https://<your-mattermost-url>/login/gitlab/complete` (example: `https://example.com:8065/login/gitlab/complete`) and `https://<your-mattermost-url>/signup/gitlab/complete`.
|
||||
@@ -289,50 +306,68 @@ Enter `https://<your-gitlab-url>/oauth/authorize` (example: `https://example.com
|
||||
```"UserApiEndpoint": ""```
|
||||
Enter `https://<your-gitlab-url>/oauth/authorize` (example: `https://example.com:3000/api/v3/user`). Use HTTP or HTTPS depending on how your server is configured.
|
||||
|
||||
### Support Settings
|
||||
|
||||
```"TermsOfServiceLink": "/static/help/terms.html"```
|
||||
Set the link for the terms of service.
|
||||
|
||||
```"PrivacyPolicyLink": "/static/help/privacy.html"```
|
||||
Set the link for the privacy policy.
|
||||
|
||||
```"AboutLink": "/static/help/about.html"```
|
||||
Set the link for the about page.
|
||||
|
||||
```"HelpLink": "/static/help/help.html"```
|
||||
Set the link for the help page.
|
||||
|
||||
```"ReportAProblemLink": "/static/help/report_problem.html"```
|
||||
Set the link for the support website.
|
||||
|
||||
`"SupportEmail":"feedback@mattermost.com"`
|
||||
Set an email for feedback or support requests.
|
||||
|
||||
### LDAP Settings (Enterprise)
|
||||
|
||||
Settings used to enable and configure LDAP authentication with Mattermost. Available in the Enterprise version of Mattermost.
|
||||
|
||||
```"Enable Login With LDAP": "false"```
|
||||
When true, Mattermost allows login using LDAP.
|
||||
"true": Mattermost allows login using LDAP.
|
||||
|
||||
```“LDAP Server”: “”```
|
||||
```"LDAP Server": ""```
|
||||
The domain or IP address of the LDAP server.
|
||||
|
||||
```“LDAP Port”: “389”```
|
||||
```"LDAP Port": "389"```
|
||||
The port Mattermost will use to connect to the LDAP server. Default is 389.
|
||||
|
||||
```”BaseDN”: ””```
|
||||
```"BaseDN": ""```
|
||||
The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.
|
||||
|
||||
```”Bind Username”: ””```
|
||||
```"Bind Username": ""```
|
||||
The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should be a read only account with access limited to the portion of the LDAP tree specified in the BaseDN field.
|
||||
|
||||
```”Bind Password”: ””```
|
||||
```"Bind Password": ""```
|
||||
Password of the user given in “Bind Username”.
|
||||
|
||||
```”First Name Attribute”: ””```
|
||||
```"First Name Attribute": ""```
|
||||
The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.
|
||||
|
||||
```”Last Name Attribute”: ””```
|
||||
```"Last Name Attribute": ""```
|
||||
The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.
|
||||
|
||||
```”Email Attribute”: ””```
|
||||
```"Email Attribute": ""```
|
||||
The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.
|
||||
|
||||
```”Username Attribute”: ””```
|
||||
```"Username Attribute": ""```
|
||||
The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.
|
||||
|
||||
```”ID Attribute”: ””```
|
||||
|
||||
```"ID Attribute": ""```
|
||||
The attribute in the LDAP server that will be used as a unique identifier in Mattermost.
|
||||
|
||||
This is the attribute that will be used to create Mattermost accounts. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one.
|
||||
|
||||
This is also the value used to log in to Mattermost in the “LDAP Username” field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\username to sign in to other services with LDAP, you may choose to put domain\username in this field to maintain consistency between sites.
|
||||
|
||||
```”Query Timeout (seconds)”: ”60”```
|
||||
|
||||
```"Query Timeout (seconds)": "60"```
|
||||
The timeout value for queries to the LDAP server. Increase this value if you are getting timeout errors caused by a slow LDAP server.
|
||||
|
||||
## Config.json Settings Not in System Console
|
||||
@@ -342,13 +377,25 @@ System Console allows an IT Admin to update settings defined in `config.json`. H
|
||||
### Service Settings
|
||||
|
||||
```"EnableOAuthServiceProvider": false```
|
||||
”true”: Allow Mattermost to function as an OAuth provider, allowing 3rd party apps access to your user store for authentication.
|
||||
"true": Allow Mattermost to function as an OAuth provider, allowing 3rd party apps access to your user store for authentication.
|
||||
|
||||
### File Settings
|
||||
|
||||
```"InitialFont": "luximbi.ttf"```
|
||||
Font used in auto-generated profile pics with colored backgrounds.
|
||||
|
||||
```"AmazonS3Endpoint": ""```
|
||||
Set an endpoint URL for an Amazon S3 instance.
|
||||
|
||||
```"AmazonS3BucketEndpoint": ""```
|
||||
Set an endpoint URL for Amazon S3 buckets.
|
||||
|
||||
```"AmazonS3LocationConstraint": false```
|
||||
Set whether the S3 region is location constrained.
|
||||
|
||||
```Added: "AmazonS3LowercaseBucket": false```
|
||||
Set whether bucket names are fully lowercase or not.
|
||||
|
||||
### GitLab Settings
|
||||
|
||||
```"Scope": ""```
|
||||
|
||||
@@ -43,7 +43,7 @@ The following instructions apply to updating installations of Mattermost v0.7-Be
|
||||
Mattermost is designed to be upgraded sequentially through major version releases. If you skip versions when upgrading GitLab, you may find a `panic: The database schema version of 1.1.0 cannot be upgraded. You must not skip a version` error in your `/var/log/gitlab/mattermost/current` directory. If so:
|
||||
|
||||
1. Run `platform -version` to check your version of Mattermost
|
||||
2. If your version of the Mattermost binary doesn't match the version listed in the database error message, downgrade the version of the Mattermost binary you are using by [following the manual upgrade steps for Mattermost](/var/log/gitlab/mattermost/current) and using the database schema version listed in the error messages for the version you select in Step 1) iv).
|
||||
2. If your version of the Mattermost binary doesn't match the version listed in the database error message, downgrade the version of the Mattermost binary you are using by [following the manual upgrade steps for Mattermost](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md#upgrading-mattermost-to-next-major-release) and using the database schema version listed in the error messages for the version you select in Step 1) iv).
|
||||
3. Once Mattermost is working again, you can use the same upgrade procedure to upgrade Mattermost version by version to your current GitLab version. After this is done, GitLab automation should continue to work for future upgrades, so long as you don't skip versions.
|
||||
|
||||
| GitLab Version | Mattermost Version |
|
||||
|
||||
49
docker/1.4/Dockerfile
Normal file
49
docker/1.4/Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
# See License.txt for license information.
|
||||
FROM ubuntu:14.04
|
||||
|
||||
#
|
||||
# Install SQL
|
||||
#
|
||||
|
||||
ENV MYSQL_ROOT_PASSWORD=mostest
|
||||
ENV MYSQL_USER=mmuser
|
||||
ENV MYSQL_PASSWORD=mostest
|
||||
ENV MYSQL_DATABASE=mattermost_test
|
||||
|
||||
RUN groupadd -r mysql && useradd -r -g mysql mysql
|
||||
|
||||
RUN apt-key adv --keyserver pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5
|
||||
|
||||
ENV MYSQL_MAJOR 5.6
|
||||
ENV MYSQL_VERSION 5.6.25
|
||||
|
||||
RUN echo "deb http://repo.mysql.com/apt/debian/ wheezy mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list
|
||||
|
||||
RUN apt-get update \
|
||||
&& export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install perl wget mysql-server \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql
|
||||
|
||||
RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf
|
||||
|
||||
VOLUME /var/lib/mysql
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
WORKDIR /mattermost
|
||||
|
||||
# Copy over files
|
||||
ADD https://github.com/mattermost/platform/releases/download/v1.4.0-rc2/mattermost.tar.gz /
|
||||
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
|
||||
ADD config_docker.json /
|
||||
ADD docker-entry.sh /
|
||||
|
||||
RUN chmod +x /docker-entry.sh
|
||||
ENTRYPOINT /docker-entry.sh
|
||||
|
||||
# Create default storage directory
|
||||
RUN mkdir /mattermost-data/
|
||||
|
||||
# Ports
|
||||
EXPOSE 80
|
||||
BIN
docker/1.4/Dockerrun.aws.zip
Normal file
BIN
docker/1.4/Dockerrun.aws.zip
Normal file
Binary file not shown.
14
docker/1.4/Dockerrun.aws/.ebextensions/01_files.config
Normal file
14
docker/1.4/Dockerrun.aws/.ebextensions/01_files.config
Normal file
@@ -0,0 +1,14 @@
|
||||
files:
|
||||
"/etc/nginx/conf.d/proxy.conf":
|
||||
mode: "000755"
|
||||
owner: root
|
||||
group: root
|
||||
content: |
|
||||
client_max_body_size 50M;
|
||||
"/opt/elasticbeanstalk/hooks/appdeploy/post/init.sh":
|
||||
mode: "000755"
|
||||
owner: root
|
||||
group: root
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
gpasswd -a ec2-user docker
|
||||
13
docker/1.4/Dockerrun.aws/Dockerrun.aws.json
Executable file
13
docker/1.4/Dockerrun.aws/Dockerrun.aws.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"AWSEBDockerrunVersion": "1",
|
||||
"Image": {
|
||||
"Name": "mattermost/platform:1.4",
|
||||
"Update": "true"
|
||||
},
|
||||
"Ports": [
|
||||
{
|
||||
"ContainerPort": "80"
|
||||
}
|
||||
],
|
||||
"Logging": "/var/log/"
|
||||
}
|
||||
23
docker/1.4/README.md
Normal file
23
docker/1.4/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
Mattermost
|
||||
==========
|
||||
|
||||
http:/mattermost.org
|
||||
|
||||
Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search.
|
||||
|
||||
Installing Mattermost
|
||||
=====================
|
||||
|
||||
To run an instance of the latest version of mattermost on your local machine you can run:
|
||||
|
||||
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
|
||||
|
||||
To update this image to the latest version you can run:
|
||||
|
||||
`docker pull mattermost/platform`
|
||||
|
||||
To run an instance of the latest code from the master branch on GitHub you can run:
|
||||
|
||||
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev`
|
||||
|
||||
Any questions, please visit http://forum.mattermost.org
|
||||
111
docker/1.4/config_docker.json
Normal file
111
docker/1.4/config_docker.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"ServiceSettings": {
|
||||
"ListenAddress": ":80",
|
||||
"MaximumLoginAttempts": 10,
|
||||
"SegmentDeveloperKey": "",
|
||||
"GoogleDeveloperKey": "",
|
||||
"EnableOAuthServiceProvider": false,
|
||||
"EnableIncomingWebhooks": false,
|
||||
"EnableOutgoingWebhooks": false,
|
||||
"EnablePostUsernameOverride": false,
|
||||
"EnablePostIconOverride": false,
|
||||
"EnableTesting": false,
|
||||
"EnableDeveloper": false,
|
||||
"EnableSecurityFixAlert": true,
|
||||
"SessionLengthWebInDays" : 30,
|
||||
"SessionLengthMobileInDays" : 30,
|
||||
"SessionLengthSSOInDays" : 30,
|
||||
"SessionCacheInMinutes" : 10
|
||||
},
|
||||
"TeamSettings": {
|
||||
"SiteName": "Mattermost",
|
||||
"MaxUsersPerTeam": 50,
|
||||
"EnableTeamCreation": true,
|
||||
"EnableUserCreation": true,
|
||||
"RestrictCreationToDomains": "",
|
||||
"RestrictTeamNames": true,
|
||||
"EnableTeamListing": false
|
||||
},
|
||||
"SqlSettings": {
|
||||
"DriverName": "mysql",
|
||||
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
|
||||
"DataSourceReplicas": [],
|
||||
"MaxIdleConns": 10,
|
||||
"MaxOpenConns": 10,
|
||||
"Trace": false,
|
||||
"AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg"
|
||||
},
|
||||
"LogSettings": {
|
||||
"EnableConsole": false,
|
||||
"ConsoleLevel": "INFO",
|
||||
"EnableFile": true,
|
||||
"FileLevel": "INFO",
|
||||
"FileFormat": "",
|
||||
"FileLocation": ""
|
||||
},
|
||||
"FileSettings": {
|
||||
"DriverName": "local",
|
||||
"Directory": "/mattermost/data/",
|
||||
"EnablePublicLink": true,
|
||||
"PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip",
|
||||
"ThumbnailWidth": 120,
|
||||
"ThumbnailHeight": 100,
|
||||
"PreviewWidth": 1024,
|
||||
"PreviewHeight": 0,
|
||||
"ProfileWidth": 128,
|
||||
"ProfileHeight": 128,
|
||||
"InitialFont": "luximbi.ttf",
|
||||
"AmazonS3AccessKeyId": "",
|
||||
"AmazonS3SecretAccessKey": "",
|
||||
"AmazonS3Bucket": "",
|
||||
"AmazonS3Region": "",
|
||||
"AmazonS3Endpoint": "",
|
||||
"AmazonS3BucketEndpoint": "",
|
||||
"AmazonS3LocationConstraint": false,
|
||||
"AmazonS3LowercaseBucket": false
|
||||
},
|
||||
"EmailSettings": {
|
||||
"EnableSignUpWithEmail": true,
|
||||
"SendEmailNotifications": false,
|
||||
"RequireEmailVerification": false,
|
||||
"FeedbackName": "",
|
||||
"FeedbackEmail": "",
|
||||
"SMTPUsername": "",
|
||||
"SMTPPassword": "",
|
||||
"SMTPServer": "",
|
||||
"SMTPPort": "",
|
||||
"ConnectionSecurity": "",
|
||||
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
|
||||
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
|
||||
"SendPushNotifications": false,
|
||||
"PushNotificationServer": ""
|
||||
},
|
||||
"RateLimitSettings": {
|
||||
"EnableRateLimiter": true,
|
||||
"PerSec": 10,
|
||||
"MemoryStoreSize": 10000,
|
||||
"VaryByRemoteAddr": true,
|
||||
"VaryByHeader": ""
|
||||
},
|
||||
"PrivacySettings": {
|
||||
"ShowEmailAddress": true,
|
||||
"ShowFullName": true
|
||||
},
|
||||
"SupportSettings": {
|
||||
"TermsOfServiceLink": "/static/help/terms.html",
|
||||
"PrivacyPolicyLink": "/static/help/privacy.html",
|
||||
"AboutLink": "/static/help/about.html",
|
||||
"HelpLink": "/static/help/help.html",
|
||||
"ReportAProblemLink": "/static/help/report_problem.html",
|
||||
"SupportEmail": "feedback@mattermost.com"
|
||||
},
|
||||
"GitLabSettings": {
|
||||
"Enable": false,
|
||||
"Secret": "",
|
||||
"Id": "",
|
||||
"Scope": "",
|
||||
"AuthEndpoint": "",
|
||||
"TokenEndpoint": "",
|
||||
"UserApiEndpoint": ""
|
||||
}
|
||||
}
|
||||
111
docker/1.4/docker-entry.sh
Executable file
111
docker/1.4/docker-entry.sh
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
# See License.txt for license information.
|
||||
|
||||
mkdir -p web/static/js
|
||||
|
||||
echo "127.0.0.1 dockerhost" >> /etc/hosts
|
||||
/etc/init.d/networking restart
|
||||
|
||||
echo configuring mysql
|
||||
|
||||
# SQL!!!
|
||||
set -e
|
||||
|
||||
get_option () {
|
||||
local section=$1
|
||||
local option=$2
|
||||
local default=$3
|
||||
ret=$(my_print_defaults $section | grep '^--'${option}'=' | cut -d= -f2-)
|
||||
[ -z $ret ] && ret=$default
|
||||
echo $ret
|
||||
}
|
||||
|
||||
|
||||
# Get config
|
||||
DATADIR="$("mysqld" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"
|
||||
SOCKET=$(get_option mysqld socket "$DATADIR/mysql.sock")
|
||||
PIDFILE=$(get_option mysqld pid-file "/var/run/mysqld/mysqld.pid")
|
||||
|
||||
if [ ! -d "$DATADIR/mysql" ]; then
|
||||
if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then
|
||||
echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'
|
||||
echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DATADIR"
|
||||
chown -R mysql:mysql "$DATADIR"
|
||||
|
||||
echo 'Running mysql_install_db'
|
||||
mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf
|
||||
echo 'Finished mysql_install_db'
|
||||
|
||||
mysqld --user=mysql --datadir="$DATADIR" --skip-networking &
|
||||
for i in $(seq 30 -1 0); do
|
||||
[ -S "$SOCKET" ] && break
|
||||
echo 'MySQL init process in progress...'
|
||||
sleep 1
|
||||
done
|
||||
if [ $i = 0 ]; then
|
||||
echo >&2 'MySQL init process failed.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# These statements _must_ be on individual lines, and _must_ end with
|
||||
# semicolons (no line breaks or comments are permitted).
|
||||
# TODO proper SQL escaping on ALL the things D:
|
||||
|
||||
tempSqlFile=$(mktemp /tmp/mysql-first-time.XXXXXX.sql)
|
||||
cat > "$tempSqlFile" <<-EOSQL
|
||||
-- What's done in this file shouldn't be replicated
|
||||
-- or products like mysql-fabric won't work
|
||||
SET @@SESSION.SQL_LOG_BIN=0;
|
||||
|
||||
DELETE FROM mysql.user ;
|
||||
CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
|
||||
GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
|
||||
DROP DATABASE IF EXISTS test ;
|
||||
EOSQL
|
||||
|
||||
if [ "$MYSQL_DATABASE" ]; then
|
||||
echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile"
|
||||
fi
|
||||
|
||||
if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
|
||||
echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" >> "$tempSqlFile"
|
||||
|
||||
if [ "$MYSQL_DATABASE" ]; then
|
||||
echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" >> "$tempSqlFile"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile"
|
||||
|
||||
mysql -uroot < "$tempSqlFile"
|
||||
|
||||
rm -f "$tempSqlFile"
|
||||
kill $(cat $PIDFILE)
|
||||
for i in $(seq 30 -1 0); do
|
||||
[ -f "$PIDFILE" ] || break
|
||||
echo 'MySQL init process in progress...'
|
||||
sleep 1
|
||||
done
|
||||
if [ $i = 0 ]; then
|
||||
echo >&2 'MySQL hangs during init process.'
|
||||
exit 1
|
||||
fi
|
||||
echo 'MySQL init process done. Ready for start up.'
|
||||
fi
|
||||
|
||||
chown -R mysql:mysql "$DATADIR"
|
||||
|
||||
mysqld &
|
||||
|
||||
sleep 5
|
||||
|
||||
# ------------------------
|
||||
|
||||
echo starting platform
|
||||
cd /mattermost/bin
|
||||
./platform -config=/config_docker.json
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package manualtesting
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -57,8 +57,8 @@ func (o *Channel) Etag() string {
|
||||
return Etag(o.Id, o.UpdateAt)
|
||||
}
|
||||
|
||||
func (o *Channel) ExtraEtag() string {
|
||||
return Etag(o.Id, o.ExtraUpdateAt)
|
||||
func (o *Channel) ExtraEtag(memberLimit int) string {
|
||||
return Etag(o.Id, o.ExtraUpdateAt, memberLimit)
|
||||
}
|
||||
|
||||
func (o *Channel) IsValid() *AppError {
|
||||
|
||||
@@ -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"
|
||||
@@ -636,8 +636,8 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/channels/"+id+"/extra_info", "", etag); err != nil {
|
||||
func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/channels/"+id+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
|
||||
85
model/license.go
Normal file
85
model/license.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
34
model/license_test.go
Normal file
34
model/license_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
// It should be maitained in chronological order with most current
|
||||
// release at the front of the list.
|
||||
var versions = []string{
|
||||
"1.4.0",
|
||||
"1.3.0",
|
||||
"1.2.1",
|
||||
"1.2.0",
|
||||
|
||||
@@ -603,7 +603,14 @@ func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChann
|
||||
result := StoreResult{}
|
||||
|
||||
var members []model.ExtraMember
|
||||
_, err := s.GetReplica().Select(&members, "SELECT Id, Nickname, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = :ChannelId LIMIT :Limit", map[string]interface{}{"ChannelId": channelId, "Limit": limit})
|
||||
var err error
|
||||
|
||||
if limit != -1 {
|
||||
_, err = s.GetReplica().Select(&members, "SELECT Id, Nickname, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = :ChannelId LIMIT :Limit", map[string]interface{}{"ChannelId": channelId, "Limit": limit})
|
||||
} else {
|
||||
_, err = s.GetReplica().Select(&members, "SELECT Id, Nickname, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlChannelStore.GetExtraMembers", "We couldn't get the extra info for channel members", "channel_id="+channelId+", "+err.Error())
|
||||
} else {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
157
utils/license.go
Normal file
157
utils/license.go
Normal file
@@ -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
|
||||
}
|
||||
50
utils/license_test.go
Normal file
50
utils/license_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]} />;
|
||||
|
||||
@@ -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='#'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -581,12 +581,12 @@ export default class EmailSettings extends React.Component {
|
||||
className='form-control'
|
||||
id='PushNotificationServer'
|
||||
ref='PushNotificationServer'
|
||||
placeholder='E.g.: "https://push.mattermost.com"'
|
||||
placeholder='E.g.: "https://push-test.mattermost.com"'
|
||||
defaultValue={this.props.config.EmailSettings.PushNotificationServer}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendPushNotifications}
|
||||
/>
|
||||
<p className='help-text'>{'Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use https://push.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'}</p>
|
||||
<p className='help-text'>{'Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use https://push-test.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
237
web/react/components/admin_console/license_settings.jsx
Normal file
237
web/react/components/admin_console/license_settings.jsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -20,9 +20,14 @@ export default class ChannelInviteModal extends React.Component {
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleInvite = this.handleInvite.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
// the state gets populated when the modal is shown
|
||||
this.state = {};
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!this.props.show && !nextProps.show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
@@ -34,13 +39,25 @@ export default class ChannelInviteModal extends React.Component {
|
||||
return false;
|
||||
}
|
||||
getStateFromStores() {
|
||||
function getId(user) {
|
||||
return user.id;
|
||||
}
|
||||
var users = UserStore.getActiveOnlyProfiles();
|
||||
var memberIds = ChannelStore.getCurrentExtraInfo().members.map(getId);
|
||||
const users = UserStore.getActiveOnlyProfiles();
|
||||
|
||||
var loading = $.isEmptyObject(users);
|
||||
if ($.isEmptyObject(users)) {
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
// make sure we have all members of this channel before rendering
|
||||
const extraInfo = ChannelStore.getCurrentExtraInfo();
|
||||
if (extraInfo.member_count !== extraInfo.members.length) {
|
||||
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
|
||||
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const memberIds = extraInfo.members.map((user) => user.id);
|
||||
|
||||
var nonmembers = [];
|
||||
for (var id in users) {
|
||||
@@ -55,7 +72,7 @@ export default class ChannelInviteModal extends React.Component {
|
||||
|
||||
return {
|
||||
nonmembers,
|
||||
loading
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
onShow() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import MemberList from './member_list.jsx';
|
||||
import ChannelInviteModal from './channel_invite_modal.jsx';
|
||||
|
||||
@@ -21,9 +22,10 @@ export default class ChannelMembersModal extends React.Component {
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
state.showInviteModal = false;
|
||||
this.state = state;
|
||||
// the rest of the state gets populated when the modal is shown
|
||||
this.state = {
|
||||
showInviteModal: false
|
||||
};
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
@@ -37,8 +39,18 @@ export default class ChannelMembersModal extends React.Component {
|
||||
return false;
|
||||
}
|
||||
getStateFromStores() {
|
||||
const extraInfo = ChannelStore.getCurrentExtraInfo();
|
||||
|
||||
if (extraInfo.member_count !== extraInfo.members.length) {
|
||||
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
|
||||
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const users = UserStore.getActiveOnlyProfiles();
|
||||
const memberList = ChannelStore.getCurrentExtraInfo().members;
|
||||
const memberList = extraInfo.members;
|
||||
|
||||
const nonmemberList = [];
|
||||
for (const id in users) {
|
||||
@@ -71,14 +83,14 @@ export default class ChannelMembersModal extends React.Component {
|
||||
|
||||
return {
|
||||
nonmemberList,
|
||||
memberList
|
||||
memberList,
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
onShow() {
|
||||
if ($(window).width() > 768) {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
|
||||
}
|
||||
this.onChange();
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show && !prevProps.show) {
|
||||
@@ -89,6 +101,8 @@ export default class ChannelMembersModal extends React.Component {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
|
||||
this.onChange();
|
||||
} else if (this.props.show && !nextProps.show) {
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onChange);
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
@@ -154,6 +168,21 @@ export default class ChannelMembersModal extends React.Component {
|
||||
isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.state.loading) {
|
||||
content = (<LoadingScreen />);
|
||||
} else {
|
||||
content = (
|
||||
<div className='team-member-list'>
|
||||
<MemberList
|
||||
memberList={this.state.memberList}
|
||||
isAdmin={isAdmin}
|
||||
handleRemove={this.handleRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
@@ -178,13 +207,7 @@ export default class ChannelMembersModal extends React.Component {
|
||||
ref='modalBody'
|
||||
style={{maxHeight}}
|
||||
>
|
||||
<div className='team-member-list'>
|
||||
<MemberList
|
||||
memberList={this.state.memberList}
|
||||
isAdmin={isAdmin}
|
||||
handleRemove={this.handleRemove}
|
||||
/>
|
||||
</div>
|
||||
{content}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
|
||||
@@ -32,7 +32,6 @@ export default class CreateComment extends React.Component {
|
||||
this.handleUploadStart = this.handleUploadStart.bind(this);
|
||||
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
|
||||
this.handleUploadError = this.handleUploadError.bind(this);
|
||||
this.handleTextDrop = this.handleTextDrop.bind(this);
|
||||
this.removePreview = this.removePreview.bind(this);
|
||||
this.getFileCount = this.getFileCount.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
@@ -239,11 +238,6 @@ export default class CreateComment extends React.Component {
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
|
||||
}
|
||||
}
|
||||
handleTextDrop(text) {
|
||||
const newText = this.state.messageText + text;
|
||||
this.handleUserInput(newText);
|
||||
Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.textbox.refs.message), newText.length);
|
||||
}
|
||||
removePreview(id) {
|
||||
let previews = this.state.previews;
|
||||
let uploadsInProgress = this.state.uploadsInProgress;
|
||||
@@ -344,7 +338,6 @@ export default class CreateComment extends React.Component {
|
||||
onUploadStart={this.handleUploadStart}
|
||||
onFileUpload={this.handleFileUploadComplete}
|
||||
onUploadError={this.handleUploadError}
|
||||
onTextDrop={this.handleTextDrop}
|
||||
postType='comment'
|
||||
channelId={this.props.channelId}
|
||||
/>
|
||||
|
||||
@@ -40,7 +40,6 @@ export default class CreatePost extends React.Component {
|
||||
this.handleUploadStart = this.handleUploadStart.bind(this);
|
||||
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
|
||||
this.handleUploadError = this.handleUploadError.bind(this);
|
||||
this.handleTextDrop = this.handleTextDrop.bind(this);
|
||||
this.removePreview = this.removePreview.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
@@ -276,11 +275,6 @@ export default class CreatePost extends React.Component {
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
|
||||
}
|
||||
}
|
||||
handleTextDrop(text) {
|
||||
const newText = this.state.messageText + text;
|
||||
this.handleUserInput(newText);
|
||||
Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.textbox.refs.message), newText.length);
|
||||
}
|
||||
removePreview(id) {
|
||||
const previews = Object.assign([], this.state.previews);
|
||||
const uploadsInProgress = this.state.uploadsInProgress;
|
||||
@@ -457,7 +451,6 @@ export default class CreatePost extends React.Component {
|
||||
onUploadStart={this.handleUploadStart}
|
||||
onFileUpload={this.handleFileUploadComplete}
|
||||
onUploadError={this.handleUploadError}
|
||||
onTextDrop={this.handleTextDrop}
|
||||
postType='post'
|
||||
channelId=''
|
||||
/>
|
||||
|
||||
@@ -266,7 +266,7 @@ export default class FileAttachment extends React.Component {
|
||||
href={fileUrl}
|
||||
download={filenameString}
|
||||
data-toggle='tooltip'
|
||||
title={'Download ' + filenameString}
|
||||
title={'Download \"' + filenameString + '\"'}
|
||||
className='post-image__name'
|
||||
>
|
||||
{trimmedFilename}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// 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';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
|
||||
export default class FileUpload extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -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) {
|
||||
@@ -109,8 +100,6 @@ export default class FileUpload extends React.Component {
|
||||
|
||||
if (typeof files !== 'string' && files.length) {
|
||||
this.uploadFiles(files);
|
||||
} else {
|
||||
this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,11 +109,19 @@ export default class FileUpload extends React.Component {
|
||||
|
||||
if (this.props.postType === 'post') {
|
||||
$('.row.main').dragster({
|
||||
enter() {
|
||||
enter(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.center-file-overlay').removeClass('hidden');
|
||||
}
|
||||
},
|
||||
leave() {
|
||||
leave(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.center-file-overlay').addClass('hidden');
|
||||
}
|
||||
},
|
||||
drop(dragsterEvent, e) {
|
||||
$('.center-file-overlay').addClass('hidden');
|
||||
@@ -133,11 +130,19 @@ export default class FileUpload extends React.Component {
|
||||
});
|
||||
} else if (this.props.postType === 'comment') {
|
||||
$('.post-right__container').dragster({
|
||||
enter() {
|
||||
enter(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.right-file-overlay').removeClass('hidden');
|
||||
}
|
||||
},
|
||||
leave() {
|
||||
leave(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.right-file-overlay').addClass('hidden');
|
||||
}
|
||||
},
|
||||
drop(dragsterEvent, e) {
|
||||
$('.right-file-overlay').addClass('hidden');
|
||||
@@ -191,7 +196,7 @@ export default class FileUpload extends React.Component {
|
||||
var channelId = self.props.channelId || ChannelStore.getCurrentId();
|
||||
|
||||
// generate a unique id that can be used by other components to refer back to this file upload
|
||||
var clientId = utils.generateId();
|
||||
var clientId = Utils.generateId();
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('channel_id', channelId);
|
||||
@@ -213,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);
|
||||
}
|
||||
@@ -229,6 +234,18 @@ export default class FileUpload extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let target;
|
||||
if (this.props.postType === 'post') {
|
||||
target = $('.row.main');
|
||||
} else {
|
||||
target = $('.post-right__container');
|
||||
}
|
||||
|
||||
// jquery-dragster doesn't provide a function to unregister itself so do it manually
|
||||
target.off('dragenter dragleave dragover drop dragster:enter dragster:leave dragster:over dragster:drop');
|
||||
}
|
||||
|
||||
cancelUpload(clientId) {
|
||||
var requests = this.state.requests;
|
||||
var request = requests[clientId];
|
||||
@@ -237,11 +254,23 @@ export default class FileUpload extends React.Component {
|
||||
request.abort();
|
||||
|
||||
delete requests[clientId];
|
||||
this.setState({requests: requests});
|
||||
this.setState({requests});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let multiple = true;
|
||||
if (Utils.isMobileApp()) {
|
||||
// iOS WebViews don't upload videos properly in multiple mode
|
||||
multiple = false;
|
||||
}
|
||||
|
||||
let accept = '';
|
||||
if (Utils.isIosChrome()) {
|
||||
// iOS Chrome can't upload videos at all
|
||||
accept = 'image/*';
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref='input'
|
||||
@@ -254,7 +283,8 @@ export default class FileUpload extends React.Component {
|
||||
ref='fileInput'
|
||||
type='file'
|
||||
onChange={this.handleChange}
|
||||
multiple='true'
|
||||
multiple={multiple}
|
||||
accept={accept}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -223,13 +223,13 @@ export default class PostInfo extends React.Component {
|
||||
/>
|
||||
</li>
|
||||
<li className='col col__reply'>
|
||||
{comments}
|
||||
<div
|
||||
className='dropdown'
|
||||
ref='dotMenu'
|
||||
>
|
||||
{dropdown}
|
||||
</div>
|
||||
{comments}
|
||||
<Overlay
|
||||
show={this.state.show}
|
||||
target={() => ReactDOM.findDOMNode(this.refs.dotMenu)}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class PostsView extends React.Component {
|
||||
this.updateScrolling = this.updateScrolling.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
|
||||
|
||||
this.jumpToPostNode = null;
|
||||
this.wasAtBottom = true;
|
||||
@@ -339,6 +340,10 @@ export default class PostsView extends React.Component {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
});
|
||||
}
|
||||
scrollToBottomAnimated() {
|
||||
var postList = $(this.refs.postlist);
|
||||
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
|
||||
}
|
||||
componentDidMount() {
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
@@ -458,7 +463,7 @@ export default class PostsView extends React.Component {
|
||||
<ScrollToBottomArrows
|
||||
isScrolling={this.state.isScrolling}
|
||||
atBottom={this.wasAtBottom}
|
||||
onClick={this.scrollToBottom}
|
||||
onClick={this.scrollToBottomAnimated}
|
||||
/>
|
||||
<div
|
||||
ref='postlist'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -168,7 +168,7 @@ export function getMoreChannels(force) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getChannelExtraInfo(id) {
|
||||
export function getChannelExtraInfo(id, memberLimit) {
|
||||
let channelId;
|
||||
if (id) {
|
||||
channelId = id;
|
||||
@@ -185,6 +185,7 @@ export function getChannelExtraInfo(id) {
|
||||
|
||||
client.getChannelExtraInfo(
|
||||
channelId,
|
||||
memberLimit,
|
||||
(data, textStatus, xhr) => {
|
||||
callTracker['getChannelExtraInfo_' + channelId] = 0;
|
||||
|
||||
|
||||
@@ -824,10 +824,17 @@ export function getChannelCounts(success, error) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getChannelExtraInfo(id, success, error) {
|
||||
export function getChannelExtraInfo(id, memberLimit, success, error) {
|
||||
let url = '/api/v1/channels/' + id + '/extra_info';
|
||||
|
||||
if (memberLimit) {
|
||||
url += '/' + memberLimit;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/v1/channels/' + id + '/extra_info',
|
||||
url,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success,
|
||||
error: function onError(xhr, status, err) {
|
||||
@@ -1399,3 +1406,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');
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
@@ -299,6 +304,10 @@ export default {
|
||||
id: 'onlineIndicator',
|
||||
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
|
||||
};
|
||||
|
||||
@@ -74,6 +74,21 @@ export function isSafari() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isIosChrome() {
|
||||
// https://developer.chrome.com/multidevice/user-agent
|
||||
return navigator.userAgent.indexOf('CriOS') !== -1;
|
||||
}
|
||||
|
||||
export function isMobileApp() {
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
// the mobile app has different user agents for the native api calls and the shim, so handle them both
|
||||
const isApi = userAgent.indexOf('Mattermost') !== -1;
|
||||
const isShim = userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') === -1 && userAgent.indexOf('Chrome') === -1;
|
||||
|
||||
return isApi || isShim;
|
||||
}
|
||||
|
||||
export function isInRole(roles, inRole) {
|
||||
var parts = roles.split(' ');
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
@@ -186,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;
|
||||
@@ -209,7 +234,7 @@ export function displayTime(ticks) {
|
||||
}
|
||||
}
|
||||
|
||||
return hours + ':' + minutes + ampm;
|
||||
return hours + ':' + minutes + ampm + timezone;
|
||||
}
|
||||
|
||||
export function displayDateTime(ticks) {
|
||||
@@ -557,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);
|
||||
}
|
||||
|
||||
@@ -602,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);
|
||||
@@ -1276,3 +1305,22 @@ export function fillArray(value, length) {
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Checks if a data transfer contains files not text, folders, etc..
|
||||
// Slightly modified from http://stackoverflow.com/questions/6848043/how-do-i-detect-a-file-is-being-dragged-rather-than-a-draggable-element-on-my-pa
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,9 @@
|
||||
.banner__content {
|
||||
width: 80%;
|
||||
}
|
||||
&.warning {
|
||||
background: #e60000;
|
||||
}
|
||||
}
|
||||
.popover {
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -286,8 +286,10 @@ body.ios {
|
||||
z-index: 50;
|
||||
@include opacity(0);
|
||||
@include single-transition(all, 0.3s);
|
||||
display: none;
|
||||
|
||||
&.scrolling {
|
||||
display: block;
|
||||
@include opacity(1);
|
||||
}
|
||||
}
|
||||
@@ -417,12 +419,6 @@ body.ios {
|
||||
background-color: beige;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.6em;
|
||||
@@ -601,6 +597,7 @@ body.ios {
|
||||
right: 0;
|
||||
top: 30px;
|
||||
width: 65px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.permalink-popover {
|
||||
@@ -634,8 +631,7 @@ body.ios {
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-right: 5px;
|
||||
top: -1px;
|
||||
|
||||
.dropdown-menu {
|
||||
@@ -671,20 +667,17 @@ body.ios {
|
||||
@include legacy-pie-clearfix;
|
||||
width: calc(100% - 75px);
|
||||
|
||||
p {
|
||||
margin: 0 0 0.4em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0.6em;
|
||||
padding: 5px 0 0 20px;
|
||||
}
|
||||
|
||||
ul + p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 0.4em;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user