Merge branch 'master' into plugins-2
Этот коммит содержится в:
@@ -154,7 +154,11 @@ func getUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.HandleEtag(etag, "Get User", w, r) {
|
||||
return
|
||||
} else {
|
||||
c.App.SanitizeProfile(user, c.IsSystemAdmin())
|
||||
if c.Session.UserId == user.Id {
|
||||
user.Sanitize(map[string]bool{})
|
||||
} else {
|
||||
c.App.SanitizeProfile(user, c.IsSystemAdmin())
|
||||
}
|
||||
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
||||
w.Write([]byte(user.ToJson()))
|
||||
return
|
||||
|
||||
@@ -411,7 +411,7 @@ func TestGetUserByUsername(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.PrivacySettings.ShowEmailAddress = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.PrivacySettings.ShowFullName = false })
|
||||
|
||||
ruser, resp = Client.GetUserByUsername(user.Username, "")
|
||||
ruser, resp = Client.GetUserByUsername(th.BasicUser2.Username, "")
|
||||
CheckNoError(t, resp)
|
||||
|
||||
if ruser.Email != "" {
|
||||
@@ -424,6 +424,12 @@ func TestGetUserByUsername(t *testing.T) {
|
||||
t.Fatal("last name should be blank")
|
||||
}
|
||||
|
||||
ruser, resp = Client.GetUserByUsername(th.BasicUser.Username, "")
|
||||
CheckNoError(t, resp)
|
||||
if len(ruser.NotifyProps) == 0 {
|
||||
t.Fatal("notify props should be sent")
|
||||
}
|
||||
|
||||
Client.Logout()
|
||||
_, resp = Client.GetUserByUsername(user.Username, "")
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
|
||||
@@ -297,13 +297,14 @@ func (a *App) trackConfig() {
|
||||
})
|
||||
|
||||
a.SendDiagnostic(TRACK_CONFIG_SQL, map[string]interface{}{
|
||||
"driver_name": *cfg.SqlSettings.DriverName,
|
||||
"trace": cfg.SqlSettings.Trace,
|
||||
"max_idle_conns": *cfg.SqlSettings.MaxIdleConns,
|
||||
"max_open_conns": *cfg.SqlSettings.MaxOpenConns,
|
||||
"data_source_replicas": len(cfg.SqlSettings.DataSourceReplicas),
|
||||
"data_source_search_replicas": len(cfg.SqlSettings.DataSourceSearchReplicas),
|
||||
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
||||
"driver_name": *cfg.SqlSettings.DriverName,
|
||||
"trace": cfg.SqlSettings.Trace,
|
||||
"max_idle_conns": *cfg.SqlSettings.MaxIdleConns,
|
||||
"conn_max_lifetime_milliseconds": *cfg.SqlSettings.ConnMaxLifetimeMilliseconds,
|
||||
"max_open_conns": *cfg.SqlSettings.MaxOpenConns,
|
||||
"data_source_replicas": len(cfg.SqlSettings.DataSourceReplicas),
|
||||
"data_source_search_replicas": len(cfg.SqlSettings.DataSourceSearchReplicas),
|
||||
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
||||
})
|
||||
|
||||
a.SendDiagnostic(TRACK_CONFIG_LOG, map[string]interface{}{
|
||||
|
||||
@@ -1062,10 +1062,24 @@ func (a *App) ImportUserTeams(user *model.User, data *[]UserTeamImportData) *mod
|
||||
}
|
||||
|
||||
var roles string
|
||||
isSchemeUser := true
|
||||
isSchemeAdmin := false
|
||||
|
||||
if tdata.Roles == nil {
|
||||
roles = model.TEAM_USER_ROLE_ID
|
||||
isSchemeUser = true
|
||||
} else {
|
||||
roles = *tdata.Roles
|
||||
rawRoles := *tdata.Roles
|
||||
explicitRoles := []string{}
|
||||
for _, role := range strings.Fields(rawRoles) {
|
||||
if role == model.TEAM_USER_ROLE_ID {
|
||||
isSchemeUser = true
|
||||
} else if role == model.TEAM_ADMIN_ROLE_ID {
|
||||
isSchemeAdmin = true
|
||||
} else {
|
||||
explicitRoles = append(explicitRoles, role)
|
||||
}
|
||||
}
|
||||
roles = strings.Join(explicitRoles, " ")
|
||||
}
|
||||
|
||||
var member *model.TeamMember
|
||||
@@ -1073,12 +1087,16 @@ func (a *App) ImportUserTeams(user *model.User, data *[]UserTeamImportData) *mod
|
||||
return err
|
||||
}
|
||||
|
||||
if member.Roles != roles {
|
||||
if member.ExplicitRoles != roles {
|
||||
if _, err := a.UpdateTeamMemberRoles(team.Id, user.Id, roles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if member.SchemeAdmin != isSchemeAdmin || member.SchemeUser != isSchemeUser {
|
||||
a.UpdateTeamMemberSchemeRoles(team.Id, user.Id, isSchemeUser, isSchemeAdmin)
|
||||
}
|
||||
|
||||
if defaultChannel, err := a.GetChannelByName(model.DEFAULT_CHANNEL, team.Id); err != nil {
|
||||
return err
|
||||
} else if _, err = a.addUserToChannel(user, defaultChannel, member); err != nil {
|
||||
@@ -1108,10 +1126,24 @@ func (a *App) ImportUserChannels(user *model.User, team *model.Team, teamMember
|
||||
}
|
||||
|
||||
var roles string
|
||||
isSchemeUser := true
|
||||
isSchemeAdmin := false
|
||||
|
||||
if cdata.Roles == nil {
|
||||
roles = model.CHANNEL_USER_ROLE_ID
|
||||
isSchemeUser = true
|
||||
} else {
|
||||
roles = *cdata.Roles
|
||||
rawRoles := *cdata.Roles
|
||||
explicitRoles := []string{}
|
||||
for _, role := range strings.Fields(rawRoles) {
|
||||
if role == model.CHANNEL_USER_ROLE_ID {
|
||||
isSchemeUser = true
|
||||
} else if role == model.CHANNEL_ADMIN_ROLE_ID {
|
||||
isSchemeAdmin = true
|
||||
} else {
|
||||
explicitRoles = append(explicitRoles, role)
|
||||
}
|
||||
}
|
||||
roles = strings.Join(explicitRoles, " ")
|
||||
}
|
||||
|
||||
var member *model.ChannelMember
|
||||
@@ -1123,12 +1155,16 @@ func (a *App) ImportUserChannels(user *model.User, team *model.Team, teamMember
|
||||
}
|
||||
}
|
||||
|
||||
if member.Roles != roles {
|
||||
if member.ExplicitRoles != roles {
|
||||
if _, err := a.UpdateChannelMemberRoles(channel.Id, user.Id, roles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if member.SchemeAdmin != isSchemeAdmin || member.SchemeUser != isSchemeUser {
|
||||
a.UpdateChannelMemberSchemeRoles(channel.Id, user.Id, isSchemeUser, isSchemeAdmin)
|
||||
}
|
||||
|
||||
if cdata.NotifyProps != nil {
|
||||
notifyProps := member.NotifyProps
|
||||
|
||||
@@ -1200,7 +1236,7 @@ func validateUserImportData(data *UserImportData) *model.AppError {
|
||||
}
|
||||
|
||||
if data.Password != nil && len(*data.Password) == 0 {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.pasword_length.error", nil, "", http.StatusBadRequest)
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if data.Password != nil && len(*data.Password) > model.USER_PASSWORD_MAX_LENGTH {
|
||||
|
||||
@@ -2598,6 +2598,126 @@ func TestImportImportUser(t *testing.T) {
|
||||
checkNotifyProp(t, user, model.CHANNEL_MENTIONS_NOTIFY_PROP, "false")
|
||||
checkNotifyProp(t, user, model.COMMENTS_NOTIFY_PROP, model.COMMENTS_NOTIFY_ANY)
|
||||
checkNotifyProp(t, user, model.MENTION_KEYS_NOTIFY_PROP, "misc")
|
||||
|
||||
// Test importing a user with roles set to a team and a channel which are affected by an override scheme.
|
||||
// The import subsystem should translate `channel_admin/channel_user/team_admin/team_user`
|
||||
// to the appropriate scheme-managed-role booleans.
|
||||
|
||||
// Mark the phase 2 permissions migration as completed.
|
||||
<-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
|
||||
|
||||
defer func() {
|
||||
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
|
||||
}()
|
||||
|
||||
teamSchemeData := &SchemeImportData{
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr(model.NewId()),
|
||||
Scope: ptrStr("team"),
|
||||
DefaultTeamUserRole: &RoleImportData{
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr(model.NewId()),
|
||||
},
|
||||
DefaultTeamAdminRole: &RoleImportData{
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr(model.NewId()),
|
||||
},
|
||||
DefaultChannelUserRole: &RoleImportData{
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr(model.NewId()),
|
||||
},
|
||||
DefaultChannelAdminRole: &RoleImportData{
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr(model.NewId()),
|
||||
},
|
||||
Description: ptrStr("description"),
|
||||
}
|
||||
|
||||
if err := th.App.ImportScheme(teamSchemeData, false); err != nil {
|
||||
t.Fatalf("Should have succeeded.")
|
||||
}
|
||||
|
||||
var teamScheme *model.Scheme
|
||||
if res := <-th.App.Srv.Store.Scheme().GetByName(*teamSchemeData.Name); res.Err != nil {
|
||||
t.Fatalf("Failed to import scheme: %v", res.Err)
|
||||
} else {
|
||||
teamScheme = res.Data.(*model.Scheme)
|
||||
}
|
||||
|
||||
teamData := &TeamImportData{
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr("Display Name"),
|
||||
Type: ptrStr("O"),
|
||||
Description: ptrStr("The team description."),
|
||||
AllowOpenInvite: ptrBool(true),
|
||||
Scheme: &teamScheme.Name,
|
||||
}
|
||||
if err := th.App.ImportTeam(teamData, false); err != nil {
|
||||
t.Fatalf("Import should have succeeded: %v", err.Error())
|
||||
}
|
||||
team, err = th.App.GetTeamByName(teamName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get team from database.")
|
||||
}
|
||||
|
||||
channelData := &ChannelImportData{
|
||||
Team: &teamName,
|
||||
Name: ptrStr(model.NewId()),
|
||||
DisplayName: ptrStr("Display Name"),
|
||||
Type: ptrStr("O"),
|
||||
Header: ptrStr("Channe Header"),
|
||||
Purpose: ptrStr("Channel Purpose"),
|
||||
}
|
||||
if err := th.App.ImportChannel(channelData, false); err != nil {
|
||||
t.Fatalf("Import should have succeeded.")
|
||||
}
|
||||
channel, err = th.App.GetChannelByName(*channelData.Name, team.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get channel from database: %v", err.Error())
|
||||
}
|
||||
|
||||
// Test with a valid team & valid channel name in apply mode.
|
||||
userData := &UserImportData{
|
||||
Username: &username,
|
||||
Email: ptrStr(model.NewId() + "@example.com"),
|
||||
Teams: &[]UserTeamImportData{
|
||||
{
|
||||
Name: &team.Name,
|
||||
Roles: ptrStr("team_user team_admin"),
|
||||
Channels: &[]UserChannelImportData{
|
||||
{
|
||||
Name: &channel.Name,
|
||||
Roles: ptrStr("channel_admin channel_user"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := th.App.ImportUser(userData, false); err != nil {
|
||||
t.Fatalf("Should have succeeded.")
|
||||
}
|
||||
|
||||
user, err = th.App.GetUserByUsername(*userData.Username)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user from database.")
|
||||
}
|
||||
|
||||
teamMember, err := th.App.GetTeamMember(team.Id, user.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get the team member")
|
||||
}
|
||||
assert.True(t, teamMember.SchemeAdmin)
|
||||
assert.True(t, teamMember.SchemeUser)
|
||||
assert.Equal(t, "", teamMember.ExplicitRoles)
|
||||
|
||||
channelMember, err := th.App.GetChannelMember(channel.Id, user.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get the channel member")
|
||||
}
|
||||
assert.True(t, channelMember.SchemeAdmin)
|
||||
assert.True(t, channelMember.SchemeUser)
|
||||
assert.Equal(t, "", channelMember.ExplicitRoles)
|
||||
|
||||
}
|
||||
|
||||
func AssertAllPostsCount(t *testing.T, a *App, initialCount int64, change int64, teamName string) {
|
||||
|
||||
@@ -92,15 +92,23 @@ func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second
|
||||
|
||||
func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Host == "" {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
// golang.org/x/crypto/acme/autocert/autocert.go
|
||||
func handleHTTPRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
http.Error(w, "Use HTTPS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
target := "https://" + stripPort(r.Host) + r.URL.RequestURI()
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
}
|
||||
|
||||
url := r.URL
|
||||
url.Host = r.Host
|
||||
url.Scheme = "https"
|
||||
http.Redirect(w, r, url.String(), http.StatusFound)
|
||||
// golang.org/x/crypto/acme/autocert/autocert.go
|
||||
func stripPort(hostport string) string {
|
||||
host, _, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
return net.JoinHostPort(host, "443")
|
||||
}
|
||||
|
||||
func (a *App) StartServer() error {
|
||||
@@ -182,7 +190,7 @@ func (a *App) StartServer() error {
|
||||
defer redirectListener.Close()
|
||||
|
||||
server := &http.Server{
|
||||
Handler: handler,
|
||||
Handler: http.HandlerFunc(handleHTTPRedirect),
|
||||
ErrorLog: a.Log.StdLog(mlog.String("source", "forwarder_server")),
|
||||
}
|
||||
server.Serve(redirectListener)
|
||||
|
||||
@@ -227,6 +227,9 @@ func (a *App) AttachDeviceId(sessionId string, deviceId string, expiresAt int64)
|
||||
|
||||
func (a *App) UpdateLastActivityAtIfNeeded(session model.Session) {
|
||||
now := model.GetMillis()
|
||||
|
||||
a.UpdateWebConnUserActivity(session, now)
|
||||
|
||||
if now-session.LastActivityAt < model.SESSION_ACTIVITY_TIMEOUT {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,6 +161,22 @@ func (a *App) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.Ap
|
||||
return statusMap, nil
|
||||
}
|
||||
|
||||
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
|
||||
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
|
||||
// while an 'away' device is still connected
|
||||
func (a *App) SetStatusLastActivityAt(userId string, activityAt int64) {
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = a.GetStatus(userId); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
status.LastActivityAt = activityAt
|
||||
|
||||
a.AddStatusCacheSkipClusterSend(status)
|
||||
a.SetStatusAwayIfNeeded(userId, false)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusOnline(userId string, sessionId string, manual bool) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
|
||||
@@ -33,6 +33,7 @@ type WebConn struct {
|
||||
Send chan model.WebSocketMessage
|
||||
sessionToken atomic.Value
|
||||
session atomic.Value
|
||||
LastUserActivityAt int64
|
||||
UserId string
|
||||
T goi18n.TranslateFunc
|
||||
Locale string
|
||||
@@ -52,14 +53,15 @@ func (a *App) NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.Tra
|
||||
}
|
||||
|
||||
wc := &WebConn{
|
||||
App: a,
|
||||
Send: make(chan model.WebSocketMessage, SEND_QUEUE_SIZE),
|
||||
WebSocket: ws,
|
||||
UserId: session.UserId,
|
||||
T: t,
|
||||
Locale: locale,
|
||||
endWritePump: make(chan struct{}, 2),
|
||||
pumpFinished: make(chan struct{}, 1),
|
||||
App: a,
|
||||
Send: make(chan model.WebSocketMessage, SEND_QUEUE_SIZE),
|
||||
WebSocket: ws,
|
||||
LastUserActivityAt: model.GetMillis(),
|
||||
UserId: session.UserId,
|
||||
T: t,
|
||||
Locale: locale,
|
||||
endWritePump: make(chan struct{}, 2),
|
||||
pumpFinished: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
wc.SetSession(&session)
|
||||
|
||||
@@ -23,6 +23,12 @@ const (
|
||||
DEADLOCK_WARN = (BROADCAST_QUEUE_SIZE * 99) / 100 // number of buffered messages before printing stack trace
|
||||
)
|
||||
|
||||
type WebConnActivityMessage struct {
|
||||
UserId string
|
||||
SessionToken string
|
||||
ActivityAt int64
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
// connectionCount should be kept first.
|
||||
// See https://github.com/mattermost/mattermost-server/pull/7281
|
||||
@@ -35,6 +41,7 @@ type Hub struct {
|
||||
stop chan struct{}
|
||||
didStop chan struct{}
|
||||
invalidateUser chan string
|
||||
activity chan *WebConnActivityMessage
|
||||
ExplicitStop bool
|
||||
goroutineId int
|
||||
}
|
||||
@@ -48,6 +55,7 @@ func (a *App) NewWebHub() *Hub {
|
||||
stop: make(chan struct{}),
|
||||
didStop: make(chan struct{}),
|
||||
invalidateUser: make(chan string),
|
||||
activity: make(chan *WebConnActivityMessage),
|
||||
ExplicitStop: false,
|
||||
}
|
||||
}
|
||||
@@ -330,6 +338,13 @@ func (a *App) InvalidateWebConnSessionCacheForUser(userId string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
|
||||
hub := a.GetHubForUserId(session.UserId)
|
||||
if hub != nil {
|
||||
hub.UpdateActivity(session.UserId, session.Token, activityAt)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) Register(webConn *WebConn) {
|
||||
h.register <- webConn
|
||||
|
||||
@@ -355,6 +370,10 @@ func (h *Hub) InvalidateUser(userId string) {
|
||||
h.invalidateUser <- userId
|
||||
}
|
||||
|
||||
func (h *Hub) UpdateActivity(userId, sessionToken string, activityAt int64) {
|
||||
h.activity <- &WebConnActivityMessage{UserId: userId, SessionToken: sessionToken, ActivityAt: activityAt}
|
||||
}
|
||||
|
||||
func getGoroutineId() int {
|
||||
var buf [64]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
@@ -395,15 +414,34 @@ func (h *Hub) Start() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(connections.ForUser(webCon.UserId)) == 0 {
|
||||
conns := connections.ForUser(webCon.UserId)
|
||||
if len(conns) == 0 {
|
||||
h.app.Go(func() {
|
||||
h.app.SetStatusOffline(webCon.UserId, false)
|
||||
})
|
||||
} else {
|
||||
var latestActivity int64 = 0
|
||||
for _, conn := range conns {
|
||||
if conn.LastUserActivityAt > latestActivity {
|
||||
latestActivity = conn.LastUserActivityAt
|
||||
}
|
||||
}
|
||||
if h.app.IsUserAway(latestActivity) {
|
||||
h.app.Go(func() {
|
||||
h.app.SetStatusLastActivityAt(webCon.UserId, latestActivity)
|
||||
})
|
||||
}
|
||||
}
|
||||
case userId := <-h.invalidateUser:
|
||||
for _, webCon := range connections.ForUser(userId) {
|
||||
webCon.InvalidateCache()
|
||||
}
|
||||
case activity := <-h.activity:
|
||||
for _, webCon := range connections.ForUser(activity.UserId) {
|
||||
if webCon.GetSessionToken() == activity.SessionToken {
|
||||
webCon.LastUserActivityAt = activity.ActivityAt
|
||||
}
|
||||
}
|
||||
case msg := <-h.broadcast:
|
||||
candidates := connections.All()
|
||||
if msg.Broadcast.UserId != "" {
|
||||
|
||||
@@ -3,27 +3,15 @@ dist: | check-style test package
|
||||
|
||||
build-linux:
|
||||
@echo Build Linux amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
|
||||
env GOOS=linux GOARCH=amd64 $(GO) install -i $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
|
||||
else
|
||||
env GOOS=linux GOARCH=amd64 $(GO) install -i $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
|
||||
endif
|
||||
|
||||
build-osx:
|
||||
@echo Build OSX amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
|
||||
env GOOS=darwin GOARCH=amd64 $(GO) install -i $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
|
||||
else
|
||||
env GOOS=darwin GOARCH=amd64 $(GO) install -i $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
|
||||
endif
|
||||
|
||||
build-windows:
|
||||
@echo Build Windows amd64
|
||||
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
|
||||
env GOOS=windows GOARCH=amd64 $(GO) install -i $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
|
||||
else
|
||||
env GOOS=windows GOARCH=amd64 $(GO) install -i $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
|
||||
endif
|
||||
|
||||
build: build-linux build-windows build-osx
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"DataSourceReplicas": [],
|
||||
"DataSourceSearchReplicas": [],
|
||||
"MaxIdleConns": 20,
|
||||
"ConnMaxLifetimeMilliseconds": 3600000,
|
||||
"MaxOpenConns": 300,
|
||||
"Trace": false,
|
||||
"AtRestEncryptKey": "",
|
||||
|
||||
762
i18n/en.json
762
i18n/en.json
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -59,7 +59,7 @@ func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, las
|
||||
|
||||
if state == MIGRATION_STATE_IN_PROGRESS {
|
||||
// Check the migration job isn't wedged.
|
||||
if job != nil && job.LastActivityAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS {
|
||||
if job != nil && job.LastActivityAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS && job.CreateAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS {
|
||||
mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", scheduler.Name()), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key))
|
||||
if err := scheduler.App.Jobs.SetJobError(job, nil); err != nil {
|
||||
mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", scheduler.Name()), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||
|
||||
@@ -605,15 +605,16 @@ type SSOSettings struct {
|
||||
}
|
||||
|
||||
type SqlSettings struct {
|
||||
DriverName *string
|
||||
DataSource *string
|
||||
DataSourceReplicas []string
|
||||
DataSourceSearchReplicas []string
|
||||
MaxIdleConns *int
|
||||
MaxOpenConns *int
|
||||
Trace bool
|
||||
AtRestEncryptKey string
|
||||
QueryTimeout *int
|
||||
DriverName *string
|
||||
DataSource *string
|
||||
DataSourceReplicas []string
|
||||
DataSourceSearchReplicas []string
|
||||
MaxIdleConns *int
|
||||
ConnMaxLifetimeMilliseconds *int
|
||||
MaxOpenConns *int
|
||||
Trace bool
|
||||
AtRestEncryptKey string
|
||||
QueryTimeout *int
|
||||
}
|
||||
|
||||
func (s *SqlSettings) SetDefaults() {
|
||||
@@ -637,6 +638,10 @@ func (s *SqlSettings) SetDefaults() {
|
||||
s.MaxOpenConns = NewInt(300)
|
||||
}
|
||||
|
||||
if s.ConnMaxLifetimeMilliseconds == nil {
|
||||
s.ConnMaxLifetimeMilliseconds = NewInt(3600000)
|
||||
}
|
||||
|
||||
if s.QueryTimeout == nil {
|
||||
s.QueryTimeout = NewInt(30)
|
||||
}
|
||||
@@ -2069,6 +2074,10 @@ func (ss *SqlSettings) isValid() *AppError {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *ss.ConnMaxLifetimeMilliseconds < 0 {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *ss.QueryTimeout <= 0 {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.sql_query_timeout.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -1617,6 +1617,8 @@ func (s SqlChannelStore) buildFulltextClause(term string) (fulltextClause, fullt
|
||||
|
||||
// Prepare the FULLTEXT portion of the query.
|
||||
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
|
||||
fulltextTerm = strings.Replace(fulltextTerm, "|", "", -1)
|
||||
|
||||
splitTerm := strings.Fields(fulltextTerm)
|
||||
for i, t := range strings.Fields(fulltextTerm) {
|
||||
if i == len(splitTerm)-1 {
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
const (
|
||||
INDEX_TYPE_FULL_TEXT = "full_text"
|
||||
INDEX_TYPE_DEFAULT = "default"
|
||||
MAX_DB_CONN_LIFETIME = 60
|
||||
DB_PING_ATTEMPTS = 18
|
||||
DB_PING_TIMEOUT_SECS = 10
|
||||
)
|
||||
@@ -218,7 +217,7 @@ func setupConnection(con_type string, dataSource string, settings *model.SqlSett
|
||||
|
||||
db.SetMaxIdleConns(*settings.MaxIdleConns)
|
||||
db.SetMaxOpenConns(*settings.MaxOpenConns)
|
||||
db.SetConnMaxLifetime(time.Duration(MAX_DB_CONN_LIFETIME) * time.Minute)
|
||||
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
||||
|
||||
var dbmap *gorp.DbMap
|
||||
|
||||
|
||||
@@ -73,17 +73,19 @@ func TestGetReplica(t *testing.T) {
|
||||
driverName := model.DATABASE_DRIVER_SQLITE
|
||||
dataSource := ":memory:"
|
||||
maxIdleConns := 1
|
||||
connMaxLifetimeMilliseconds := 3600000
|
||||
maxOpenConns := 1
|
||||
queryTimeout := 5
|
||||
|
||||
settings := model.SqlSettings{
|
||||
DriverName: &driverName,
|
||||
DataSource: &dataSource,
|
||||
MaxIdleConns: &maxIdleConns,
|
||||
MaxOpenConns: &maxOpenConns,
|
||||
QueryTimeout: &queryTimeout,
|
||||
DataSourceReplicas: testCase.DataSourceReplicas,
|
||||
DataSourceSearchReplicas: testCase.DataSourceSearchReplicas,
|
||||
DriverName: &driverName,
|
||||
DataSource: &dataSource,
|
||||
MaxIdleConns: &maxIdleConns,
|
||||
ConnMaxLifetimeMilliseconds: &connMaxLifetimeMilliseconds,
|
||||
MaxOpenConns: &maxOpenConns,
|
||||
QueryTimeout: &queryTimeout,
|
||||
DataSourceReplicas: testCase.DataSourceReplicas,
|
||||
DataSourceSearchReplicas: testCase.DataSourceSearchReplicas,
|
||||
}
|
||||
supplier := sqlstore.NewSqlSupplier(settings, nil)
|
||||
|
||||
@@ -209,17 +211,19 @@ func TestGetAllConns(t *testing.T) {
|
||||
driverName := model.DATABASE_DRIVER_SQLITE
|
||||
dataSource := ":memory:"
|
||||
maxIdleConns := 1
|
||||
connMaxLifetimeMilliseconds := 3600000
|
||||
maxOpenConns := 1
|
||||
queryTimeout := 5
|
||||
|
||||
settings := model.SqlSettings{
|
||||
DriverName: &driverName,
|
||||
DataSource: &dataSource,
|
||||
MaxIdleConns: &maxIdleConns,
|
||||
MaxOpenConns: &maxOpenConns,
|
||||
QueryTimeout: &queryTimeout,
|
||||
DataSourceReplicas: testCase.DataSourceReplicas,
|
||||
DataSourceSearchReplicas: testCase.DataSourceSearchReplicas,
|
||||
DriverName: &driverName,
|
||||
DataSource: &dataSource,
|
||||
MaxIdleConns: &maxIdleConns,
|
||||
ConnMaxLifetimeMilliseconds: &connMaxLifetimeMilliseconds,
|
||||
MaxOpenConns: &maxOpenConns,
|
||||
QueryTimeout: &queryTimeout,
|
||||
DataSourceReplicas: testCase.DataSourceReplicas,
|
||||
DataSourceSearchReplicas: testCase.DataSourceSearchReplicas,
|
||||
}
|
||||
supplier := sqlstore.NewSqlSupplier(settings, nil)
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ func (s SqlUserAccessTokenStore) UpdateTokenDisable(tokenId string) store.StoreC
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
transaction, err := s.GetMaster().Begin()
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.UpdateTokenDisable", "store.sql_user_access_token.update_token_disble.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.UpdateTokenDisable", "store.sql_user_access_token.update_token_disable.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
if extrasResult := s.deleteSessionsAndDisableToken(transaction, tokenId); extrasResult.Err != nil {
|
||||
*result = extrasResult
|
||||
|
||||
@@ -2028,6 +2028,19 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
|
||||
t.Fatal("wrong channel returned")
|
||||
}
|
||||
}
|
||||
|
||||
if result := <-search(o1.TeamId, "town square |"); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
channels := result.Data.(*model.ChannelList)
|
||||
if len(*channels) != 1 {
|
||||
t.Fatal("should return 1 channel")
|
||||
}
|
||||
|
||||
if (*channels)[0].Name != o9.Name {
|
||||
t.Fatal("wrong channel returned")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,17 +76,19 @@ func NewPostgreSQLContainer() (*RunningContainer, *model.SqlSettings, error) {
|
||||
|
||||
func databaseSettings(driver, dataSource string) *model.SqlSettings {
|
||||
settings := &model.SqlSettings{
|
||||
DriverName: &driver,
|
||||
DataSource: &dataSource,
|
||||
DataSourceReplicas: []string{},
|
||||
DataSourceSearchReplicas: []string{},
|
||||
MaxIdleConns: new(int),
|
||||
MaxOpenConns: new(int),
|
||||
Trace: false,
|
||||
AtRestEncryptKey: model.NewRandomString(32),
|
||||
QueryTimeout: new(int),
|
||||
DriverName: &driver,
|
||||
DataSource: &dataSource,
|
||||
DataSourceReplicas: []string{},
|
||||
DataSourceSearchReplicas: []string{},
|
||||
MaxIdleConns: new(int),
|
||||
ConnMaxLifetimeMilliseconds: new(int),
|
||||
MaxOpenConns: new(int),
|
||||
Trace: false,
|
||||
AtRestEncryptKey: model.NewRandomString(32),
|
||||
QueryTimeout: new(int),
|
||||
}
|
||||
*settings.MaxIdleConns = 10
|
||||
*settings.ConnMaxLifetimeMilliseconds = 3600000
|
||||
*settings.MaxOpenConns = 100
|
||||
*settings.QueryTimeout = 10
|
||||
return settings
|
||||
|
||||
@@ -89,14 +89,14 @@ func UpdateAssetsSubpath(subpath string) error {
|
||||
return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
|
||||
}
|
||||
|
||||
// Rewrite the *.css references to `/static/*` (or a previously rewritten subpath).
|
||||
// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
|
||||
err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
|
||||
if filepath.Ext(walkPath) == ".css" {
|
||||
if oldCss, err := ioutil.ReadFile(walkPath); err != nil {
|
||||
if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
|
||||
if old, err := ioutil.ReadFile(walkPath); err != nil {
|
||||
return errors.Wrapf(err, "failed to open %s", walkPath)
|
||||
} else {
|
||||
newCss := strings.Replace(string(oldCss), pathToReplace, newPath, -1)
|
||||
if err = ioutil.WriteFile(walkPath, []byte(newCss), 0); err != nil {
|
||||
new := strings.Replace(string(old), pathToReplace, newPath, -1)
|
||||
if err = ioutil.WriteFile(walkPath, []byte(new), 0); err != nil {
|
||||
return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,52 +33,64 @@ func TestUpdateAssetsSubpath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
Description string
|
||||
RootHTML string
|
||||
MainCSS string
|
||||
Subpath string
|
||||
ExpectedRootHTML string
|
||||
ExpectedMainCSS string
|
||||
Description string
|
||||
RootHTML string
|
||||
MainCSS string
|
||||
ManifestJSON string
|
||||
Subpath string
|
||||
ExpectedRootHTML string
|
||||
ExpectedMainCSS string
|
||||
ExpectedManifestJSON string
|
||||
}{
|
||||
{
|
||||
"no changes required, empty subpath provided",
|
||||
baseRootHtml,
|
||||
baseCss,
|
||||
baseManifestJson,
|
||||
"",
|
||||
baseRootHtml,
|
||||
baseCss,
|
||||
baseManifestJson,
|
||||
},
|
||||
{
|
||||
"no changes required",
|
||||
baseRootHtml,
|
||||
baseCss,
|
||||
baseManifestJson,
|
||||
"/",
|
||||
baseRootHtml,
|
||||
baseCss,
|
||||
baseManifestJson,
|
||||
},
|
||||
{
|
||||
"subpath",
|
||||
baseRootHtml,
|
||||
baseCss,
|
||||
baseManifestJson,
|
||||
"/subpath",
|
||||
subpathRootHtml,
|
||||
subpathCss,
|
||||
subpathManifestJson,
|
||||
},
|
||||
{
|
||||
"new subpath from old",
|
||||
subpathRootHtml,
|
||||
subpathCss,
|
||||
subpathManifestJson,
|
||||
"/nested/subpath",
|
||||
newSubpathRootHtml,
|
||||
newSubpathCss,
|
||||
newSubpathManifestJson,
|
||||
},
|
||||
{
|
||||
"resetting to /",
|
||||
subpathRootHtml,
|
||||
subpathCss,
|
||||
baseManifestJson,
|
||||
"/",
|
||||
resetRootHtml,
|
||||
baseCss,
|
||||
baseManifestJson,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -86,6 +98,7 @@ func TestUpdateAssetsSubpath(t *testing.T) {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "root.html"), []byte(testCase.RootHTML), 0700)
|
||||
ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "main.css"), []byte(testCase.MainCSS), 0700)
|
||||
ioutil.WriteFile(filepath.Join(tempDir, model.CLIENT_DIR, "manifest.json"), []byte(testCase.ManifestJSON), 0700)
|
||||
err := utils.UpdateAssetsSubpath(testCase.Subpath)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -97,6 +110,9 @@ func TestUpdateAssetsSubpath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testCase.ExpectedMainCSS, string(contents))
|
||||
|
||||
contents, err = ioutil.ReadFile(filepath.Join(tempDir, model.CLIENT_DIR, "manifest.json"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testCase.ExpectedManifestJSON, string(contents))
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -190,3 +206,207 @@ const newSubpathRootHtml = `<!DOCTYPE html> <html lang=en> <head> <meta charset=
|
||||
const newSubpathCss = `@font-face{font-family:FontAwesome;src:url(/nested/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/nested/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/nested/subpath/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/nested/subpath/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/nested/subpath/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/nested/subpath/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}`
|
||||
|
||||
const resetRootHtml = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.segment.com/analytics.js/ 'unsafe-eval' 'sha256-VFw7U/t/OI+I9YMja3c2GDwEQbnlOq/L5+GealgesK8='"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style><script>window.publicPath='/static/'</script> <link href="/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>`
|
||||
|
||||
const baseManifestJson = `{
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icon_96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_76x76.png",
|
||||
"sizes": "76x76",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_60x60.png",
|
||||
"sizes": "60x60",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_57x57.png",
|
||||
"sizes": "57x57",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_120x120.png",
|
||||
"sizes": "120x120",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon_192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "Mattermost",
|
||||
"short_name": "Mattermost",
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"start_url": ".",
|
||||
"description": "Mattermost is an open source, self-hosted Slack-alternative",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
`
|
||||
|
||||
const subpathManifestJson = `{
|
||||
"icons": [
|
||||
{
|
||||
"src": "/subpath/static/icon_96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_76x76.png",
|
||||
"sizes": "76x76",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_60x60.png",
|
||||
"sizes": "60x60",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_57x57.png",
|
||||
"sizes": "57x57",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_120x120.png",
|
||||
"sizes": "120x120",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/subpath/static/icon_192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "Mattermost",
|
||||
"short_name": "Mattermost",
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"start_url": ".",
|
||||
"description": "Mattermost is an open source, self-hosted Slack-alternative",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
`
|
||||
|
||||
const newSubpathManifestJson = `{
|
||||
"icons": [
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_76x76.png",
|
||||
"sizes": "76x76",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_60x60.png",
|
||||
"sizes": "60x60",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_57x57.png",
|
||||
"sizes": "57x57",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_120x120.png",
|
||||
"sizes": "120x120",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/nested/subpath/static/icon_192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "Mattermost",
|
||||
"short_name": "Mattermost",
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"start_url": ".",
|
||||
"description": "Mattermost is an open source, self-hosted Slack-alternative",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
`
|
||||
|
||||
Ссылка в новой задаче
Block a user