diff --git a/core/database/check.go b/core/database/check.go index 3d84361c..517c41d0 100644 --- a/core/database/check.go +++ b/core/database/check.go @@ -92,7 +92,7 @@ func Check(runtime *env.Runtime) bool { } { // check the MySQL character set and collation - if charset != "utf8" { + if charset != "utf8" && charset != "utf8mb4" { runtime.Log.Error("MySQL character set not utf8:", errors.New(charset)) web.SiteInfo.Issue = "MySQL character set not utf8: " + charset runtime.Flags.SiteMode = env.SiteModeBadDB diff --git a/core/database/readme.md b/core/database/readme.md new file mode 100644 index 00000000..8fc73d58 --- /dev/null +++ b/core/database/readme.md @@ -0,0 +1,40 @@ + +## TODO + +1. Remove audit table +2. Remove document.layout field + + +## MYSQL + +https://stackoverflow.com/questions/37307146/difference-between-utf8mb4-unicode-ci-and-utf8mb4-unicode-520-ci-collations-in-m + +https://mathiasbynens.be/notes/mysql-utf8mb4 + +https://medium.com/@adamhooper/in-mysql-never-use-utf8-use-utf8mb4-11761243e434 + +ALTER DATABASE documize CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; + +ALTER TABLE account CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE attachment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE block CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE config CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE document CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE feedback CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE label CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE labelrole CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE link CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE organization CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE page CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE pagemeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE participant CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE pin CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE revision CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE search CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE share CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE useraction CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE useractivity CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE userconfig CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +ALTER TABLE userevent CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; + diff --git a/core/database/to-remove.md b/core/database/to-remove.md deleted file mode 100644 index 2650cb3a..00000000 --- a/core/database/to-remove.md +++ /dev/null @@ -1,5 +0,0 @@ -TODO ------ - -1. Remove audit table -2. Remove document.layout field diff --git a/domain/context.go b/domain/context.go index e2689592..4c734b8f 100644 --- a/domain/context.go +++ b/domain/context.go @@ -44,7 +44,6 @@ type RequestContext struct { //GetAppURL returns full HTTP url for the app func (c *RequestContext) GetAppURL(endpoint string) string { scheme := "http://" - if c.SSL { scheme = "https://" } @@ -58,17 +57,12 @@ type key string const DocumizeContextKey key = "documize context key" // GetRequestContext returns RequestContext from context.Context -func GetRequestContext(r *http.Request) RequestContext { - return r.Context().Value(DocumizeContextKey).(RequestContext) +func GetRequestContext(r *http.Request) (ctx RequestContext) { + c := r.Context() + if c != nil && c.Value(DocumizeContextKey) != nil { + ctx = c.Value(DocumizeContextKey).(RequestContext) + return + } + + return RequestContext{} } - -// // Scope provides data persistence methods with runtime and request context. -// type Scope struct { -// Runtime *env.Runtime -// Context RequestContext -// } - -// // NewScope returns request scoped user context and store context for persistence logic. -// func NewScope(rt *env.Runtime, r *http.Request) Scope { -// return Scope{Runtime: rt, Context: GetRequestContext(r)} -// } diff --git a/domain/search/mysql/store.go b/domain/search/mysql/store.go index 979113f6..337e418b 100644 --- a/domain/search/mysql/store.go +++ b/domain/search/mysql/store.go @@ -278,7 +278,7 @@ func (s Scope) Documents(ctx domain.RequestContext, keywords string) (results [] keywords = strings.Replace(keywords, " ", "", -1) } - keywords = strings.TrimSpace(keywords) + keywords = strings.ToLower(strings.TrimSpace(keywords)) if len(keywords) > 0 { keywordQuery = "AND MATCH(pagetitle,body) AGAINST('" + keywords + "' in boolean mode)" diff --git a/domain/space/endpoint.go b/domain/space/endpoint.go index 98e271f8..006ca0a5 100644 --- a/domain/space/endpoint.go +++ b/domain/space/endpoint.go @@ -65,15 +65,15 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - var space = space.Space{} - err = json.Unmarshal(body, &space) + var sp = space.Space{} + err = json.Unmarshal(body, &sp) if err != nil { response.WriteServerError(w, method, err) h.Runtime.Log.Error(method, err) return } - if len(space.Name) == 0 { + if len(sp.Name) == 0 { response.WriteMissingDataError(w, method, "name") return } @@ -85,10 +85,12 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - space.RefID = uniqueid.Generate() - space.OrgID = ctx.OrgID + sp.RefID = uniqueid.Generate() + sp.OrgID = ctx.OrgID + sp.Type = space.ScopePrivate + sp.UserID = ctx.UserID - err = h.Store.Space.Add(ctx, space) + err = h.Store.Space.Add(ctx, sp) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) @@ -96,13 +98,29 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - h.Store.Audit.Record(ctx, audit.EventTypeSpaceAdd) + role := space.Role{} + role.LabelID = sp.RefID + role.OrgID = sp.OrgID + role.UserID = ctx.UserID + role.CanEdit = true + role.CanView = true + role.RefID = uniqueid.Generate() + + err = h.Store.Space.AddRole(ctx, role) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } ctx.Transaction.Commit() - space, _ = h.Store.Space.Get(ctx, space.RefID) + h.Store.Audit.Record(ctx, audit.EventTypeSpaceAdd) - response.WriteJSON(w, space) + sp, _ = h.Store.Space.Get(ctx, sp.RefID) + + response.WriteJSON(w, sp) } // Get returns the requested space. diff --git a/domain/space/mysql/store.go b/domain/space/mysql/store.go index bb51ddd6..eb38bae9 100644 --- a/domain/space/mysql/store.go +++ b/domain/space/mysql/store.go @@ -72,7 +72,7 @@ func (s Scope) Get(ctx domain.RequestContext, id string) (sp space.Space, err er return } -// PublicSpaces returns folders that anyone can see. +// PublicSpaces returns spaces that anyone can see. func (s Scope) PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space.Space, err error) { sql := "SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1" @@ -86,18 +86,18 @@ func (s Scope) PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space return } -// GetAll returns folders that the user can see. -// Also handles which folders can be seen by anonymous users. +// GetAll returns spaces that the user can see. +// Also handles which spaces can be seen by anonymous users. func (s Scope) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) { sql := ` -(SELECT id,refid,label as name,orgid,userid,type,created,revised from label WHERE orgid=? AND type=2 AND userid=?) -UNION ALL -(SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1 AND refid in - (SELECT labelid from labelrole WHERE orgid=? AND userid='' AND (canedit=1 OR canview=1))) -UNION ALL -(SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=3 AND refid in - (SELECT labelid from labelrole WHERE orgid=? AND userid=? AND (canedit=1 OR canview=1))) -ORDER BY name` + (SELECT id,refid,label as name,orgid,userid,type,created,revised from label WHERE orgid=? AND type=2 AND userid=?) + UNION ALL + (SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1 AND refid in + (SELECT labelid from labelrole WHERE orgid=? AND userid='' AND (canedit=1 OR canview=1))) + UNION ALL + (SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=3 AND refid in + (SELECT labelid from labelrole WHERE orgid=? AND userid=? AND (canedit=1 OR canview=1))) + ORDER BY name` err = s.Runtime.Db.Select(&sp, sql, ctx.OrgID, @@ -156,22 +156,22 @@ func (s Scope) ChangeOwner(ctx domain.RequestContext, currentOwner, newOwner str return } -// Viewers returns the list of people who can see shared folders. +// Viewers returns the list of people who can see shared spaces. func (s Scope) Viewers(ctx domain.RequestContext) (v []space.Viewer, err error) { sql := ` -SELECT a.userid, - COALESCE(u.firstname, '') as firstname, - COALESCE(u.lastname, '') as lastname, - COALESCE(u.email, '') as email, - a.labelid, - b.label as name, - b.type -FROM labelrole a -LEFT JOIN label b ON b.refid=a.labelid -LEFT JOIN user u ON u.refid=a.userid -WHERE a.orgid=? AND b.type != 2 -GROUP BY a.labelid,a.userid -ORDER BY u.firstname,u.lastname` + SELECT a.userid, + COALESCE(u.firstname, '') as firstname, + COALESCE(u.lastname, '') as lastname, + COALESCE(u.email, '') as email, + a.labelid, + b.label as name, + b.type + FROM labelrole a + LEFT JOIN label b ON b.refid=a.labelid + LEFT JOIN user u ON u.refid=a.userid + WHERE a.orgid=? AND b.type != 2 + GROUP BY a.labelid,a.userid + ORDER BY u.firstname,u.lastname` err = s.Runtime.Db.Select(&v, sql, ctx.OrgID) diff --git a/edition/boot/runtime.go b/edition/boot/runtime.go index 71f4e31e..a4643500 100644 --- a/edition/boot/runtime.go +++ b/edition/boot/runtime.go @@ -84,7 +84,7 @@ func InitRuntime(r *env.Runtime, s *domain.Store) bool { } var stdParams = map[string]string{ - "charset": "utf8", + "charset": "utf8mb4", "parseTime": "True", "maxAllowedPacket": "4194304", // 4194304 // 16777216 = 16MB } diff --git a/edition/logging/logger.go b/edition/logging/logger.go index bb434e42..c83fdc17 100644 --- a/edition/logging/logger.go +++ b/edition/logging/logger.go @@ -13,8 +13,11 @@ package logging import ( + "bytes" + "fmt" "log" "os" + "runtime" "github.com/documize/community/core/env" "github.com/jmoiron/sqlx" @@ -35,6 +38,14 @@ func (l Logger) Info(message string) { func (l Logger) Error(message string, err error) { l.log.Println(message) l.log.Println(err) + + stack := make([]byte, 4096) + runtime.Stack(stack, false) + if idx := bytes.IndexByte(stack, 0); idx > 0 && idx < len(stack) { + stack = stack[:idx] // remove trailing nulls from stack dump + } + + l.log.Println(fmt.Sprintf("%s", stack)) } // SetDB associates database connection with given logger. diff --git a/gui/app/components/folder/sidebar-permissions.js b/gui/app/components/folder/sidebar-permissions.js index cd7b2cba..a8e64403 100644 --- a/gui/app/components/folder/sidebar-permissions.js +++ b/gui/app/components/folder/sidebar-permissions.js @@ -78,11 +78,11 @@ export default Ember.Component.extend(NotifierMixin, { this.set('permissions', folderPermissions.sortBy('fullname')); }); - }); + }); }, getDefaultInvitationMessage() { - return "Hey there, I am sharing the " + this.get('folder.name') + " (in " + this.get("appMeta.title") + ") with you so we can both access the same documents."; + return "Hey there, I am sharing the " + this.get('folder.name') + " space (in " + this.get("appMeta.title") + ") with you so we can both access the same documents."; }, actions: { @@ -90,7 +90,7 @@ export default Ember.Component.extend(NotifierMixin, { let message = this.getDefaultInvitationMessage(); let folder = this.get('folder'); let permissions = this.get('permissions'); - + this.get('permissions').forEach((permission, index) => { // eslint-disable-line no-unused-vars Ember.set(permission, 'canView', $("#canView-" + permission.userId).prop('checked')); Ember.set(permission, 'canEdit', $("#canEdit-" + permission.userId).prop('checked')); diff --git a/gui/app/components/folder/sidebar-share.js b/gui/app/components/folder/sidebar-share.js index 7f0cd7dd..2c8b69c3 100644 --- a/gui/app/components/folder/sidebar-share.js +++ b/gui/app/components/folder/sidebar-share.js @@ -23,7 +23,7 @@ export default Ember.Component.extend(NotifierMixin, { inviteMessage: '', getDefaultInvitationMessage() { - return "Hey there, I am sharing the " + this.folder.get('name') + " (in " + this.get("appMeta.title") + ") with you so we can both access the same documents."; + return "Hey there, I am sharing the " + this.folder.get('name') + " space (in " + this.get("appMeta.title") + ") with you so we can both access the same documents."; }, willRender() { diff --git a/gui/app/templates/components/folder/sidebar-permissions.hbs b/gui/app/templates/components/folder/sidebar-permissions.hbs index c8ced3a8..41ca16e6 100644 --- a/gui/app/templates/components/folder/sidebar-permissions.hbs +++ b/gui/app/templates/components/folder/sidebar-permissions.hbs @@ -5,7 +5,7 @@   - View + View  Edit diff --git a/server/middleware.go b/server/middleware.go index 5ce265fe..42e7a22c 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -68,9 +68,11 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http method := "middleware.auth" // Let certain requests pass straight through - authenticated := preAuthorizeStaticAssets(m.Runtime, r) - - if !authenticated { + authenticated, ctx := m.preAuthorizeStaticAssets(m.Runtime, r) + if authenticated { + ctx2 := context.WithValue(r.Context(), domain.DocumizeContextKey, ctx) + r = r.WithContext(ctx2) + } else { token := auth.FindJWT(r) rc, _, tokenErr := auth.DecodeJWT(m.Runtime, token) @@ -195,7 +197,9 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http // Certain assets/URL do not require authentication. // Just stops the log files being clogged up with failed auth errors. -func preAuthorizeStaticAssets(rt *env.Runtime, r *http.Request) bool { +func (m *middleware) preAuthorizeStaticAssets(rt *env.Runtime, r *http.Request) (auth bool, ctx domain.RequestContext) { + ctx = domain.RequestContext{} + if strings.ToLower(r.URL.Path) == "/" || strings.ToLower(r.URL.Path) == "/validate" || strings.ToLower(r.URL.Path) == "/favicon.ico" || @@ -204,8 +208,27 @@ func preAuthorizeStaticAssets(rt *env.Runtime, r *http.Request) bool { strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") || ((rt.Flags.SiteMode == env.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) { - return true + return true, ctx } - return false + if strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") || + ((rt.Flags.SiteMode == env.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) { + + dom := organization.GetRequestSubdomain(r) + dom = m.Store.Organization.CheckDomain(ctx, dom) + + org, _ := m.Store.Organization.GetOrganizationByDomain(dom) + ctx.Subdomain = organization.GetSubdomainFromHost(r) + ctx.AllowAnonymousAccess = org.AllowAnonymousAccess + ctx.OrgName = org.Title + ctx.Administrator = false + ctx.Editor = false + ctx.Global = false + ctx.AppURL = r.Host + ctx.SSL = r.TLS != nil + + return true, ctx + } + + return false, ctx }