+
+
|
@@ -27,45 +27,89 @@
- {{#each permissions as |permission|}}
+ {{#each spacePermissions as |permission|}}
- {{permission.fullname}} {{if (eq permission.userId session.user.id) '(you)'}} |
- {{input type="checkbox" id=(concat 'space-role-view-' permission.userId) checked=permission.spaceView}}
+ {{#if (eq permission.who "role")}}
+
+ people
+
+ {{permission.name}}
+ ({{permission.members}})
+
+ {{else}}
+ {{#if (eq permission.whoId constants.EveryoneUserId)}}
+
+ language
+
+ {{permission.name}}
+ {{else}}
+
+ person
+
+ {{permission.name}}
+ {{#if (eq permission.whoId session.user.id)}}
+ (you)
+ {{/if}}
+
+ {{/if}}
+ {{/if}}
|
- {{input type="checkbox" id=(concat 'space-role-manage-' permission.userId) checked=permission.spaceManage}}
+ {{input type="checkbox" id=(concat 'space-role-view-' permission.whoId) checked=permission.spaceView}}
|
- {{input type="checkbox" id=(concat 'space-role-owner-' permission.userId) checked=permission.spaceOwner}}
+ {{input type="checkbox" id=(concat 'space-role-manage-' permission.whoId) checked=permission.spaceManage}}
|
- {{input type="checkbox" id=(concat 'doc-role-add-' permission.userId) checked=permission.documentAdd}}
+ {{input type="checkbox" id=(concat 'space-role-owner-' permission.whoId) checked=permission.spaceOwner}}
|
- {{input type="checkbox" id=(concat 'doc-role-edit-' permission.userId) checked=permission.documentEdit}}
+ {{input type="checkbox" id=(concat 'doc-role-add-' permission.whoId) checked=permission.documentAdd}}
|
- {{input type="checkbox" id=(concat 'doc-role-delete-' permission.userId) checked=permission.documentDelete}}
+ {{input type="checkbox" id=(concat 'doc-role-edit-' permission.whoId) checked=permission.documentEdit}}
|
- {{input type="checkbox" id=(concat 'doc-role-move-' permission.userId) checked=permission.documentMove}}
+ {{input type="checkbox" id=(concat 'doc-role-delete-' permission.whoId) checked=permission.documentDelete}}
|
- {{input type="checkbox" id=(concat 'doc-role-copy-' permission.userId) checked=permission.documentCopy}}
+ {{input type="checkbox" id=(concat 'doc-role-move-' permission.whoId) checked=permission.documentMove}}
|
- {{input type="checkbox" id=(concat 'doc-role-template-' permission.userId) checked=permission.documentTemplate}}
+ {{input type="checkbox" id=(concat 'doc-role-copy-' permission.whoId) checked=permission.documentCopy}}
|
- {{input type="checkbox" id=(concat 'doc-role-approve-' permission.userId) checked=permission.documentApprove}}
+ {{input type="checkbox" id=(concat 'doc-role-template-' permission.whoId) checked=permission.documentTemplate}}
+ |
+
+ {{input type="checkbox" id=(concat 'doc-role-approve-' permission.whoId) checked=permission.documentApprove}}
|
{{/each}}
-
+
+
+
+
+ {{focus-input id="user-search" type="text" class="form-control mousetrap" placeholder="Search users..." value=searchText key-up=(action 'onSearch')}}
+ firstname, lastname, email
+
+ {{#each filteredUsers as |user|}}
+
+
{{user.fullname}}
+
+
+
+
+ {{/each}}
+
+
+
+
+
diff --git a/gui/package.json b/gui/package.json
index c83201a6..b72cb1f9 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -1,6 +1,6 @@
{
"name": "documize",
- "version": "1.57.3",
+ "version": "1.58.0",
"description": "The Document IDE",
"private": true,
"repository": "",
diff --git a/meta.json b/meta.json
index 1eb719c5..6f6f0693 100644
--- a/meta.json
+++ b/meta.json
@@ -1,16 +1,16 @@
{
"community":
{
- "version": "1.57.3",
+ "version": "1.58.0",
"major": 1,
- "minor": 57,
- "patch": 3
+ "minor": 58,
+ "patch": 0
},
"enterprise":
{
- "version": "1.59.3",
+ "version": "1.60.0",
"major": 1,
- "minor": 59,
- "patch": 3
+ "minor": 60,
+ "patch": 0
}
}
\ No newline at end of file
diff --git a/model/audit/audit.go b/model/audit/audit.go
index d754e448..5bf46131 100644
--- a/model/audit/audit.go
+++ b/model/audit/audit.go
@@ -74,4 +74,9 @@ const (
EventTypeCategoryUpdate EventType = "updated-category"
EventTypeCategoryLink EventType = "linked-category"
EventTypeCategoryUnlink EventType = "unlinked-category"
+ EventTypeGroupAdd EventType = "added-group"
+ EventTypeGroupDelete EventType = "removed-group"
+ EventTypeGroupUpdate EventType = "updated-group"
+ EventTypeGroupJoin EventType = "joined-group"
+ EventTypeGroupLeave EventType = "left-group"
)
diff --git a/model/group/group.go b/model/group/group.go
new file mode 100644
index 00000000..a6c14631
--- /dev/null
+++ b/model/group/group.go
@@ -0,0 +1,67 @@
+// Copyright 2018 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package group
+
+import "github.com/documize/community/model"
+
+// Group defines a user group.
+type Group struct {
+ model.BaseEntity
+ OrgID string `json:"orgId"`
+ Name string `json:"name"`
+ Purpose string `json:"purpose"`
+ Members int `json:"members"` // read-only info
+}
+
+// Member defines user membership of a user group.
+type Member struct {
+ ID uint64 `json:"id"`
+ OrgID string `json:"orgId"`
+ RoleID string `json:"roleId"`
+ UserID string `json:"userId"`
+ Firstname string `json:"firstname"` //read-only info
+ Lastname string `json:"lastname"` //read-only info
+}
+
+// Record details user membership of a user group.
+type Record struct {
+ ID uint64 `json:"id"`
+ OrgID string `json:"orgId"`
+ RoleID string `json:"roleId"`
+ UserID string `json:"userId"`
+ Name string `json:"name"`
+ Purpose string `json:"purpose"`
+}
+
+// UserHasGroupMembership returns true if user belongs to specified group.
+func UserHasGroupMembership(r []Record, groupID, userID string) bool {
+ for i := range r {
+ if r[i].RoleID == groupID && r[i].UserID == userID {
+ return true
+ }
+ }
+
+ return false
+}
+
+// FilterGroupRecords returns only those records matching group ID.
+func FilterGroupRecords(r []Record, groupID string) (m []Record) {
+ m = []Record{}
+
+ for i := range r {
+ if r[i].RoleID == groupID {
+ m = append(m, r[i])
+ }
+ }
+
+ return
+}
diff --git a/model/permission/category.go b/model/permission/category.go
new file mode 100644
index 00000000..a257bbcf
--- /dev/null
+++ b/model/permission/category.go
@@ -0,0 +1,75 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package permission
+
+// CategoryRecord represents space permissions for a user on a category.
+// This data structure is made from database permission records for the category,
+// and it is designed to be sent to HTTP clients (web, mobile).
+type CategoryRecord struct {
+ OrgID string `json:"orgId"`
+ CategoryID string `json:"categoryId"`
+ WhoID string `json:"whoId"`
+ Who WhoType `json:"who"`
+ CategoryView bool `json:"categoryView"`
+ Name string `json:"name"` // read-only, user or group name
+}
+
+// DecodeUserCategoryPermissions returns a flat, usable permission summary record
+// from multiple user permission records for a given category.
+func DecodeUserCategoryPermissions(perm []Permission) (r CategoryRecord) {
+ r = CategoryRecord{}
+
+ if len(perm) > 0 {
+ r.OrgID = perm[0].OrgID
+ r.WhoID = perm[0].WhoID
+ r.Who = perm[0].Who
+ r.CategoryID = perm[0].RefID
+ }
+
+ for _, p := range perm {
+ switch p.Action {
+ case CategoryView:
+ r.CategoryView = true
+ }
+ }
+
+ return
+}
+
+// EncodeUserCategoryPermissions returns multiple user permission records
+// for a given document, using flat permission summary record.
+func EncodeUserCategoryPermissions(r CategoryRecord) (perm []Permission) {
+ if r.CategoryView {
+ perm = append(perm, EncodeCategoryRecord(r, CategoryView))
+ }
+
+ return
+}
+
+// HasAnyCategoryPermission returns true if user has at least one permission.
+func HasAnyCategoryPermission(p CategoryRecord) bool {
+ return p.CategoryView
+}
+
+// EncodeCategoryRecord creates standard permission record representing user permissions for a category.
+func EncodeCategoryRecord(r CategoryRecord, a Action) (p Permission) {
+ p = Permission{}
+ p.OrgID = r.OrgID
+ p.WhoID = r.WhoID
+ p.Who = r.Who
+ p.Location = LocationDocument
+ p.RefID = r.CategoryID
+ p.Action = a
+ p.Scope = ScopeRow
+
+ return
+}
diff --git a/model/permission/common.go b/model/permission/common.go
index 2b868d8c..29248c3e 100644
--- a/model/permission/common.go
+++ b/model/permission/common.go
@@ -15,17 +15,50 @@ import "time"
// Permission represents a permission for a space and is persisted to the database.
type Permission struct {
- ID uint64 `json:"id"`
- OrgID string `json:"orgId"`
- Who string `json:"who"` // user, role
- WhoID string `json:"whoId"` // either a user or role ID
- Action Action `json:"action"` // view, edit, delete
- Scope string `json:"scope"` // object, table
- Location string `json:"location"` // table name
- RefID string `json:"refId"` // id of row in table / blank when scope=table
- Created time.Time `json:"created"`
+ ID uint64 `json:"id"`
+ OrgID string `json:"orgId"`
+ Who WhoType `json:"who"` // user, role
+ WhoID string `json:"whoId"` // either a user or role ID
+ Action Action `json:"action"` // view, edit, delete
+ Scope ScopeType `json:"scope"` // object, table
+ Location LocationType `json:"location"` // table name
+ RefID string `json:"refId"` // id of row in table / blank when scope=table
+ Created time.Time `json:"created"`
}
+// WhoType tell us if permission record represents user or group
+type WhoType string
+
+const (
+ // GroupPermission means permission is assigned to a group
+ GroupPermission WhoType = "role"
+
+ // UserPermission means permission is assigned to a user
+ UserPermission WhoType = "user"
+)
+
+// LocationType tells us the entity being permissioned
+type LocationType string
+
+const (
+ // LocationSpace means space is being permissioned
+ LocationSpace LocationType = "space"
+
+ // LocationCategory means category is being permissioned
+ LocationCategory LocationType = "category"
+
+ // LocationDocument means document is being permissioned
+ LocationDocument LocationType = "document"
+)
+
+// ScopeType details at what level data is being protected, e.g. table, row
+type ScopeType string
+
+const (
+ // ScopeRow identifies row in table is being protected
+ ScopeRow ScopeType = "object"
+)
+
// Action details type of action
type Action string
diff --git a/model/permission/document.go b/model/permission/document.go
index 2083bca2..6b1e12fd 100644
--- a/model/permission/document.go
+++ b/model/permission/document.go
@@ -15,11 +15,12 @@ package permission
// This data structure is made from database permission records for the document,
// and it is designed to be sent to HTTP clients (web, mobile).
type DocumentRecord struct {
- OrgID string `json:"orgId"`
- DocumentID string `json:"documentId"`
- UserID string `json:"userId"`
- DocumentRoleEdit bool `json:"documentRoleEdit"`
- DocumentRoleApprove bool `json:"documentRoleApprove"`
+ OrgID string `json:"orgId"`
+ DocumentID string `json:"documentId"`
+ WhoID string `json:"whoId"`
+ Who WhoType `json:"who"`
+ DocumentRoleEdit bool `json:"documentRoleEdit"`
+ DocumentRoleApprove bool `json:"documentRoleApprove"`
}
// DecodeUserDocumentPermissions returns a flat, usable permission summary record
@@ -29,7 +30,8 @@ func DecodeUserDocumentPermissions(perm []Permission) (r DocumentRecord) {
if len(perm) > 0 {
r.OrgID = perm[0].OrgID
- r.UserID = perm[0].WhoID
+ r.WhoID = perm[0].WhoID
+ r.Who = perm[0].Who
r.DocumentID = perm[0].RefID
}
@@ -67,12 +69,12 @@ func HasAnyDocumentPermission(p DocumentRecord) bool {
func EncodeDocumentRecord(r DocumentRecord, a Action) (p Permission) {
p = Permission{}
p.OrgID = r.OrgID
- p.Who = "user"
- p.WhoID = r.UserID
- p.Location = "document"
+ p.WhoID = r.WhoID
+ p.Who = r.Who
+ p.Location = LocationDocument
p.RefID = r.DocumentID
p.Action = a
- p.Scope = "object" // default to row level permission
+ p.Scope = ScopeRow
return
}
diff --git a/model/permission/space.go b/model/permission/space.go
index 6d02ef0f..144e4989 100644
--- a/model/permission/space.go
+++ b/model/permission/space.go
@@ -15,19 +15,21 @@ package permission
// This data structure is made from database permission records for the space,
// and it is designed to be sent to HTTP clients (web, mobile).
type Record struct {
- OrgID string `json:"orgId"`
- SpaceID string `json:"folderId"`
- UserID string `json:"userId"`
- SpaceView bool `json:"spaceView"`
- SpaceManage bool `json:"spaceManage"`
- SpaceOwner bool `json:"spaceOwner"`
- DocumentAdd bool `json:"documentAdd"`
- DocumentEdit bool `json:"documentEdit"`
- DocumentDelete bool `json:"documentDelete"`
- DocumentMove bool `json:"documentMove"`
- DocumentCopy bool `json:"documentCopy"`
- DocumentTemplate bool `json:"documentTemplate"`
- DocumentApprove bool `json:"documentApprove"`
+ OrgID string `json:"orgId"`
+ SpaceID string `json:"folderId"`
+ WhoID string `json:"whoId"`
+ Who WhoType `json:"who"`
+ SpaceView bool `json:"spaceView"`
+ SpaceManage bool `json:"spaceManage"`
+ SpaceOwner bool `json:"spaceOwner"`
+ DocumentAdd bool `json:"documentAdd"`
+ DocumentEdit bool `json:"documentEdit"`
+ DocumentDelete bool `json:"documentDelete"`
+ DocumentMove bool `json:"documentMove"`
+ DocumentCopy bool `json:"documentCopy"`
+ DocumentTemplate bool `json:"documentTemplate"`
+ DocumentApprove bool `json:"documentApprove"`
+ Name string `json:"name"` // read-only, user or group name
}
// DecodeUserPermissions returns a flat, usable permission summary record
@@ -37,7 +39,8 @@ func DecodeUserPermissions(perm []Permission) (r Record) {
if len(perm) > 0 {
r.OrgID = perm[0].OrgID
- r.UserID = perm[0].WhoID
+ r.WhoID = perm[0].WhoID
+ r.Who = perm[0].Who
r.SpaceID = perm[0].RefID
}
@@ -118,22 +121,24 @@ func HasAnyPermission(p Record) bool {
func EncodeRecord(r Record, a Action) (p Permission) {
p = Permission{}
p.OrgID = r.OrgID
- p.Who = "user"
- p.WhoID = r.UserID
- p.Location = "space"
+ p.Who = r.Who
+ p.WhoID = r.WhoID
+ p.Location = LocationSpace
p.RefID = r.SpaceID
p.Action = a
- p.Scope = "object" // default to row level permission
+ p.Scope = ScopeRow
return
}
// CategoryViewRequestModel represents who should be allowed to see a category.
type CategoryViewRequestModel struct {
- OrgID string `json:"orgId"`
- SpaceID string `json:"folderId"`
- CategoryID string `json:"categoryID"`
- UserID string `json:"userId"`
+ OrgID string `json:"orgId"`
+ SpaceID string `json:"folderId"`
+ CategoryID string `json:"categoryID"`
+ WhoID string `json:"whoId"`
+ Who WhoType `json:"who"`
+ // UserID string `json:"userId"`
}
// SpaceRequestModel details which users have what permissions on a given space.
diff --git a/model/user/user.go b/model/user/user.go
index e3e7f3e6..15406cf5 100644
--- a/model/user/user.go
+++ b/model/user/user.go
@@ -16,6 +16,7 @@ import (
"github.com/documize/community/model"
"github.com/documize/community/model/account"
+ "github.com/documize/community/model/group"
)
// User defines a login.
@@ -34,6 +35,7 @@ type User struct {
Salt string `json:"-"`
Reset string `json:"-"`
Accounts []account.Account `json:"accounts"`
+ Groups []group.Record `json:"groups"`
}
// ProtectSecrets blanks sensitive data.
@@ -69,3 +71,11 @@ func Exists(users []User, userID string) bool {
return false
}
+
+const (
+ // EveryoneUserID provides a shortcut to state "all authenticated users".
+ EveryoneUserID string = "0"
+
+ // EveryoneUserName provides the descriptor for this type of user/group.
+ EveryoneUserName string = "Everyone"
+)
diff --git a/server/routing/routes.go b/server/routing/routes.go
index e917f5f8..b59815b8 100644
--- a/server/routing/routes.go
+++ b/server/routing/routes.go
@@ -23,6 +23,7 @@ import (
"github.com/documize/community/domain/category"
"github.com/documize/community/domain/conversion"
"github.com/documize/community/domain/document"
+ "github.com/documize/community/domain/group"
"github.com/documize/community/domain/link"
"github.com/documize/community/domain/meta"
"github.com/documize/community/domain/organization"
@@ -53,6 +54,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
page := page.Handler{Runtime: rt, Store: s, Indexer: indexer}
space := space.Handler{Runtime: rt, Store: s}
block := block.Handler{Runtime: rt, Store: s}
+ group := group.Handler{Runtime: rt, Store: s}
section := section.Handler{Runtime: rt, Store: s}
setting := setting.Handler{Runtime: rt, Store: s}
category := category.Handler{Runtime: rt, Store: s}
@@ -65,9 +67,18 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
organization := organization.Handler{Runtime: rt, Store: s}
//**************************************************
- // Non-secure routes
+ // Non-secure public info routes
//**************************************************
+
Add(rt, RoutePrefixPublic, "meta", []string{"GET", "OPTIONS"}, nil, meta.Meta)
+ Add(rt, RoutePrefixPublic, "version", []string{"GET", "OPTIONS"}, nil, func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte(rt.Product.Version))
+ })
+
+ //**************************************************
+ // Non-secure public service routes
+ //**************************************************
+
Add(rt, RoutePrefixPublic, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, keycloak.Authenticate)
Add(rt, RoutePrefixPublic, "authenticate", []string{"POST", "OPTIONS"}, nil, auth.Login)
Add(rt, RoutePrefixPublic, "validate", []string{"GET", "OPTIONS"}, nil, auth.ValidateToken)
@@ -75,12 +86,9 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPublic, "reset/{token}", []string{"POST", "OPTIONS"}, nil, user.ResetPassword)
Add(rt, RoutePrefixPublic, "share/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.AcceptInvitation)
Add(rt, RoutePrefixPublic, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, attachment.Download)
- Add(rt, RoutePrefixPublic, "version", []string{"GET", "OPTIONS"}, nil, func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(rt.Product.Version))
- })
//**************************************************
- // Secure routes
+ // Secured private routes (require authentication)
//**************************************************
Add(rt, RoutePrefixPrivate, "import/folder/{folderID}", []string{"POST", "OPTIONS"}, nil, conversion.UploadConvert)
@@ -89,10 +97,6 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, document.Get)
Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, document.Update)
Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, document.Delete)
- Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetDocumentPermissions)
- Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetDocumentPermissions)
- Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserDocumentPermissions)
-
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/level", []string{"POST", "OPTIONS"}, nil, page.ChangePageLevel)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/sequence", []string{"POST", "OPTIONS"}, nil, page.ChangePageSequence)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions", []string{"GET", "OPTIONS"}, nil, page.GetRevisions)
@@ -117,9 +121,6 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove)
- Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetSpacePermissions)
- Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserSpacePermissions)
- Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetSpacePermissions)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite)
Add(rt, RoutePrefixPrivate, "space/manage", []string{"GET", "OPTIONS"}, nil, space.GetAll)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
@@ -129,11 +130,8 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}/summary", []string{"GET", "OPTIONS"}, nil, category.GetSummary)
Add(rt, RoutePrefixPrivate, "category/document/{documentID}", []string{"GET", "OPTIONS"}, nil, category.GetDocumentCategoryMembership)
- Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"PUT", "OPTIONS"}, nil, permission.SetCategoryPermissions)
- Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryPermissions)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, []string{"filter", "all"}, category.GetAll)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.Get)
- Add(rt, RoutePrefixPrivate, "category/{categoryID}/user", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryViewers)
Add(rt, RoutePrefixPrivate, "category/member/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.GetSpaceCategoryMembers)
Add(rt, RoutePrefixPrivate, "category/member", []string{"POST", "OPTIONS"}, nil, category.SetDocumentCategoryMembership)
Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"PUT", "OPTIONS"}, nil, category.Update)
@@ -148,6 +146,8 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, user.Update)
Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, user.Delete)
Add(rt, RoutePrefixPrivate, "users/sync", []string{"GET", "OPTIONS"}, nil, keycloak.Sync)
+ Add(rt, RoutePrefixPrivate, "users/match", []string{"POST", "OPTIONS"}, nil, user.MatchUsers)
+ Add(rt, RoutePrefixPrivate, "users/import", []string{"POST", "OPTIONS"}, nil, user.BulkImport)
Add(rt, RoutePrefixPrivate, "search", []string{"POST", "OPTIONS"}, nil, document.SearchDocuments)
@@ -180,6 +180,24 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, pin.UpdatePinSequence)
Add(rt, RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, pin.DeleteUserPin)
+ Add(rt, RoutePrefixPrivate, "group/{groupID}/members", []string{"GET", "OPTIONS"}, nil, group.GetGroupMembers)
+ Add(rt, RoutePrefixPrivate, "group", []string{"POST", "OPTIONS"}, nil, group.Add)
+ Add(rt, RoutePrefixPrivate, "group", []string{"GET", "OPTIONS"}, nil, group.Groups)
+ Add(rt, RoutePrefixPrivate, "group/{groupID}", []string{"PUT", "OPTIONS"}, nil, group.Update)
+ Add(rt, RoutePrefixPrivate, "group/{groupID}", []string{"DELETE", "OPTIONS"}, nil, group.Delete)
+ Add(rt, RoutePrefixPrivate, "group/{groupID}/join/{userID}", []string{"POST", "OPTIONS"}, nil, group.JoinGroup)
+ Add(rt, RoutePrefixPrivate, "group/{groupID}/leave/{userID}", []string{"DELETE", "OPTIONS"}, nil, group.LeaveGroup)
+
+ Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetDocumentPermissions)
+ Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetDocumentPermissions)
+ Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserDocumentPermissions)
+ Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetSpacePermissions)
+ Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserSpacePermissions)
+ Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetSpacePermissions)
+ Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"PUT", "OPTIONS"}, nil, permission.SetCategoryPermissions)
+ Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryPermissions)
+ Add(rt, RoutePrefixPrivate, "category/{categoryID}/user", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryViewers)
+
// fetch methods exist to speed up UI rendering by returning data in bulk
Add(rt, RoutePrefixPrivate, "fetch/category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.FetchSpaceData)
Add(rt, RoutePrefixPrivate, "fetch/document/{documentID}", []string{"GET", "OPTIONS"}, nil, document.FetchDocumentData)
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
new file mode 100644
index 00000000..5f5c12af
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Alexandre Cesaro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
new file mode 100644
index 00000000..98ddf829
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
@@ -0,0 +1,16 @@
+# quotedprintable
+
+## Introduction
+
+Package quotedprintable implements quoted-printable and message header encoding
+as specified by RFC 2045 and RFC 2047.
+
+It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes
+the new functions of package `mime` concerning RFC 2047.
+
+This code has minor changes with the standard library code in order to work
+with Go 1.0 and newer.
+
+## Documentation
+
+https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
new file mode 100644
index 00000000..cfd02617
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
@@ -0,0 +1,279 @@
+package quotedprintable
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+// A WordEncoder is a RFC 2047 encoded-word encoder.
+type WordEncoder byte
+
+const (
+ // BEncoding represents Base64 encoding scheme as defined by RFC 2045.
+ BEncoding = WordEncoder('b')
+ // QEncoding represents the Q-encoding scheme as defined by RFC 2047.
+ QEncoding = WordEncoder('q')
+)
+
+var (
+ errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
+)
+
+// Encode returns the encoded-word form of s. If s is ASCII without special
+// characters, it is returned unchanged. The provided charset is the IANA
+// charset name of s. It is case insensitive.
+func (e WordEncoder) Encode(charset, s string) string {
+ if !needsEncoding(s) {
+ return s
+ }
+ return e.encodeWord(charset, s)
+}
+
+func needsEncoding(s string) bool {
+ for _, b := range s {
+ if (b < ' ' || b > '~') && b != '\t' {
+ return true
+ }
+ }
+ return false
+}
+
+// encodeWord encodes a string into an encoded-word.
+func (e WordEncoder) encodeWord(charset, s string) string {
+ buf := getBuffer()
+ defer putBuffer(buf)
+
+ buf.WriteString("=?")
+ buf.WriteString(charset)
+ buf.WriteByte('?')
+ buf.WriteByte(byte(e))
+ buf.WriteByte('?')
+
+ if e == BEncoding {
+ w := base64.NewEncoder(base64.StdEncoding, buf)
+ io.WriteString(w, s)
+ w.Close()
+ } else {
+ enc := make([]byte, 3)
+ for i := 0; i < len(s); i++ {
+ b := s[i]
+ switch {
+ case b == ' ':
+ buf.WriteByte('_')
+ case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
+ buf.WriteByte(b)
+ default:
+ enc[0] = '='
+ enc[1] = upperhex[b>>4]
+ enc[2] = upperhex[b&0x0f]
+ buf.Write(enc)
+ }
+ }
+ }
+ buf.WriteString("?=")
+ return buf.String()
+}
+
+const upperhex = "0123456789ABCDEF"
+
+// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
+type WordDecoder struct {
+ // CharsetReader, if non-nil, defines a function to generate
+ // charset-conversion readers, converting from the provided
+ // charset into UTF-8.
+ // Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
+ // are handled by default.
+ // One of the the CharsetReader's result values must be non-nil.
+ CharsetReader func(charset string, input io.Reader) (io.Reader, error)
+}
+
+// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word,
+// word is returned unchanged.
+func (d *WordDecoder) Decode(word string) (string, error) {
+ fields := strings.Split(word, "?") // TODO: remove allocation?
+ if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
+ return "", errInvalidWord
+ }
+
+ content, err := decode(fields[2][0], fields[3])
+ if err != nil {
+ return "", err
+ }
+
+ buf := getBuffer()
+ defer putBuffer(buf)
+
+ if err := d.convert(buf, fields[1], content); err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+}
+
+// DecodeHeader decodes all encoded-words of the given string. It returns an
+// error if and only if CharsetReader of d returns an error.
+func (d *WordDecoder) DecodeHeader(header string) (string, error) {
+ // If there is no encoded-word, returns before creating a buffer.
+ i := strings.Index(header, "=?")
+ if i == -1 {
+ return header, nil
+ }
+
+ buf := getBuffer()
+ defer putBuffer(buf)
+
+ buf.WriteString(header[:i])
+ header = header[i:]
+
+ betweenWords := false
+ for {
+ start := strings.Index(header, "=?")
+ if start == -1 {
+ break
+ }
+ cur := start + len("=?")
+
+ i := strings.Index(header[cur:], "?")
+ if i == -1 {
+ break
+ }
+ charset := header[cur : cur+i]
+ cur += i + len("?")
+
+ if len(header) < cur+len("Q??=") {
+ break
+ }
+ encoding := header[cur]
+ cur++
+
+ if header[cur] != '?' {
+ break
+ }
+ cur++
+
+ j := strings.Index(header[cur:], "?=")
+ if j == -1 {
+ break
+ }
+ text := header[cur : cur+j]
+ end := cur + j + len("?=")
+
+ content, err := decode(encoding, text)
+ if err != nil {
+ betweenWords = false
+ buf.WriteString(header[:start+2])
+ header = header[start+2:]
+ continue
+ }
+
+ // Write characters before the encoded-word. White-space and newline
+ // characters separating two encoded-words must be deleted.
+ if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
+ buf.WriteString(header[:start])
+ }
+
+ if err := d.convert(buf, charset, content); err != nil {
+ return "", err
+ }
+
+ header = header[end:]
+ betweenWords = true
+ }
+
+ if len(header) > 0 {
+ buf.WriteString(header)
+ }
+
+ return buf.String(), nil
+}
+
+func decode(encoding byte, text string) ([]byte, error) {
+ switch encoding {
+ case 'B', 'b':
+ return base64.StdEncoding.DecodeString(text)
+ case 'Q', 'q':
+ return qDecode(text)
+ }
+ return nil, errInvalidWord
+}
+
+func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error {
+ switch {
+ case strings.EqualFold("utf-8", charset):
+ buf.Write(content)
+ case strings.EqualFold("iso-8859-1", charset):
+ for _, c := range content {
+ buf.WriteRune(rune(c))
+ }
+ case strings.EqualFold("us-ascii", charset):
+ for _, c := range content {
+ if c >= utf8.RuneSelf {
+ buf.WriteRune(unicode.ReplacementChar)
+ } else {
+ buf.WriteByte(c)
+ }
+ }
+ default:
+ if d.CharsetReader == nil {
+ return fmt.Errorf("mime: unhandled charset %q", charset)
+ }
+ r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
+ if err != nil {
+ return err
+ }
+ if _, err = buf.ReadFrom(r); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
+// one byte of non-whitespace.
+func hasNonWhitespace(s string) bool {
+ for _, b := range s {
+ switch b {
+ // Encoded-words can only be separated by linear white spaces which does
+ // not include vertical tabs (\v).
+ case ' ', '\t', '\n', '\r':
+ default:
+ return true
+ }
+ }
+ return false
+}
+
+// qDecode decodes a Q encoded string.
+func qDecode(s string) ([]byte, error) {
+ dec := make([]byte, len(s))
+ n := 0
+ for i := 0; i < len(s); i++ {
+ switch c := s[i]; {
+ case c == '_':
+ dec[n] = ' '
+ case c == '=':
+ if i+2 >= len(s) {
+ return nil, errInvalidWord
+ }
+ b, err := readHexByte(s[i+1], s[i+2])
+ if err != nil {
+ return nil, err
+ }
+ dec[n] = b
+ i += 2
+ case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
+ dec[n] = c
+ default:
+ return nil, errInvalidWord
+ }
+ n++
+ }
+
+ return dec[:n], nil
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
new file mode 100644
index 00000000..24283c52
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
@@ -0,0 +1,26 @@
+// +build go1.3
+
+package quotedprintable
+
+import (
+ "bytes"
+ "sync"
+)
+
+var bufPool = sync.Pool{
+ New: func() interface{} {
+ return new(bytes.Buffer)
+ },
+}
+
+func getBuffer() *bytes.Buffer {
+ return bufPool.Get().(*bytes.Buffer)
+}
+
+func putBuffer(buf *bytes.Buffer) {
+ if buf.Len() > 1024 {
+ return
+ }
+ buf.Reset()
+ bufPool.Put(buf)
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
new file mode 100644
index 00000000..d335b4ab
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
@@ -0,0 +1,24 @@
+// +build !go1.3
+
+package quotedprintable
+
+import "bytes"
+
+var ch = make(chan *bytes.Buffer, 32)
+
+func getBuffer() *bytes.Buffer {
+ select {
+ case buf := <-ch:
+ return buf
+ default:
+ }
+ return new(bytes.Buffer)
+}
+
+func putBuffer(buf *bytes.Buffer) {
+ buf.Reset()
+ select {
+ case ch <- buf:
+ default:
+ }
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
new file mode 100644
index 00000000..955edca2
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
@@ -0,0 +1,121 @@
+// Package quotedprintable implements quoted-printable encoding as specified by
+// RFC 2045.
+package quotedprintable
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+)
+
+// Reader is a quoted-printable decoder.
+type Reader struct {
+ br *bufio.Reader
+ rerr error // last read error
+ line []byte // to be consumed before more of br
+}
+
+// NewReader returns a quoted-printable reader, decoding from r.
+func NewReader(r io.Reader) *Reader {
+ return &Reader{
+ br: bufio.NewReader(r),
+ }
+}
+
+func fromHex(b byte) (byte, error) {
+ switch {
+ case b >= '0' && b <= '9':
+ return b - '0', nil
+ case b >= 'A' && b <= 'F':
+ return b - 'A' + 10, nil
+ // Accept badly encoded bytes.
+ case b >= 'a' && b <= 'f':
+ return b - 'a' + 10, nil
+ }
+ return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
+}
+
+func readHexByte(a, b byte) (byte, error) {
+ var hb, lb byte
+ var err error
+ if hb, err = fromHex(a); err != nil {
+ return 0, err
+ }
+ if lb, err = fromHex(b); err != nil {
+ return 0, err
+ }
+ return hb<<4 | lb, nil
+}
+
+func isQPDiscardWhitespace(r rune) bool {
+ switch r {
+ case '\n', '\r', ' ', '\t':
+ return true
+ }
+ return false
+}
+
+var (
+ crlf = []byte("\r\n")
+ lf = []byte("\n")
+ softSuffix = []byte("=")
+)
+
+// Read reads and decodes quoted-printable data from the underlying reader.
+func (r *Reader) Read(p []byte) (n int, err error) {
+ // Deviations from RFC 2045:
+ // 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
+ // 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
+ // with other broken QP encoders & decoders.
+ for len(p) > 0 {
+ if len(r.line) == 0 {
+ if r.rerr != nil {
+ return n, r.rerr
+ }
+ r.line, r.rerr = r.br.ReadSlice('\n')
+
+ // Does the line end in CRLF instead of just LF?
+ hasLF := bytes.HasSuffix(r.line, lf)
+ hasCR := bytes.HasSuffix(r.line, crlf)
+ wholeLine := r.line
+ r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
+ if bytes.HasSuffix(r.line, softSuffix) {
+ rightStripped := wholeLine[len(r.line):]
+ r.line = r.line[:len(r.line)-1]
+ if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
+ r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
+ }
+ } else if hasLF {
+ if hasCR {
+ r.line = append(r.line, '\r', '\n')
+ } else {
+ r.line = append(r.line, '\n')
+ }
+ }
+ continue
+ }
+ b := r.line[0]
+
+ switch {
+ case b == '=':
+ if len(r.line[1:]) < 2 {
+ return n, io.ErrUnexpectedEOF
+ }
+ b, err = readHexByte(r.line[1], r.line[2])
+ if err != nil {
+ return n, err
+ }
+ r.line = r.line[2:] // 2 of the 3; other 1 is done below
+ case b == '\t' || b == '\r' || b == '\n':
+ break
+ case b < ' ' || b > '~':
+ return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
+ }
+ p[0] = b
+ p = p[1:]
+ r.line = r.line[1:]
+ n++
+ }
+ return n, nil
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
new file mode 100644
index 00000000..43359d51
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
@@ -0,0 +1,166 @@
+package quotedprintable
+
+import "io"
+
+const lineMaxLen = 76
+
+// A Writer is a quoted-printable writer that implements io.WriteCloser.
+type Writer struct {
+ // Binary mode treats the writer's input as pure binary and processes end of
+ // line bytes as binary data.
+ Binary bool
+
+ w io.Writer
+ i int
+ line [78]byte
+ cr bool
+}
+
+// NewWriter returns a new Writer that writes to w.
+func NewWriter(w io.Writer) *Writer {
+ return &Writer{w: w}
+}
+
+// Write encodes p using quoted-printable encoding and writes it to the
+// underlying io.Writer. It limits line length to 76 characters. The encoded
+// bytes are not necessarily flushed until the Writer is closed.
+func (w *Writer) Write(p []byte) (n int, err error) {
+ for i, b := range p {
+ switch {
+ // Simple writes are done in batch.
+ case b >= '!' && b <= '~' && b != '=':
+ continue
+ case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
+ continue
+ }
+
+ if i > n {
+ if err := w.write(p[n:i]); err != nil {
+ return n, err
+ }
+ n = i
+ }
+
+ if err := w.encode(b); err != nil {
+ return n, err
+ }
+ n++
+ }
+
+ if n == len(p) {
+ return n, nil
+ }
+
+ if err := w.write(p[n:]); err != nil {
+ return n, err
+ }
+
+ return len(p), nil
+}
+
+// Close closes the Writer, flushing any unwritten data to the underlying
+// io.Writer, but does not close the underlying io.Writer.
+func (w *Writer) Close() error {
+ if err := w.checkLastByte(); err != nil {
+ return err
+ }
+
+ return w.flush()
+}
+
+// write limits text encoded in quoted-printable to 76 characters per line.
+func (w *Writer) write(p []byte) error {
+ for _, b := range p {
+ if b == '\n' || b == '\r' {
+ // If the previous byte was \r, the CRLF has already been inserted.
+ if w.cr && b == '\n' {
+ w.cr = false
+ continue
+ }
+
+ if b == '\r' {
+ w.cr = true
+ }
+
+ if err := w.checkLastByte(); err != nil {
+ return err
+ }
+ if err := w.insertCRLF(); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if w.i == lineMaxLen-1 {
+ if err := w.insertSoftLineBreak(); err != nil {
+ return err
+ }
+ }
+
+ w.line[w.i] = b
+ w.i++
+ w.cr = false
+ }
+
+ return nil
+}
+
+func (w *Writer) encode(b byte) error {
+ if lineMaxLen-1-w.i < 3 {
+ if err := w.insertSoftLineBreak(); err != nil {
+ return err
+ }
+ }
+
+ w.line[w.i] = '='
+ w.line[w.i+1] = upperhex[b>>4]
+ w.line[w.i+2] = upperhex[b&0x0f]
+ w.i += 3
+
+ return nil
+}
+
+// checkLastByte encodes the last buffered byte if it is a space or a tab.
+func (w *Writer) checkLastByte() error {
+ if w.i == 0 {
+ return nil
+ }
+
+ b := w.line[w.i-1]
+ if isWhitespace(b) {
+ w.i--
+ if err := w.encode(b); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (w *Writer) insertSoftLineBreak() error {
+ w.line[w.i] = '='
+ w.i++
+
+ return w.insertCRLF()
+}
+
+func (w *Writer) insertCRLF() error {
+ w.line[w.i] = '\r'
+ w.line[w.i+1] = '\n'
+ w.i += 2
+
+ return w.flush()
+}
+
+func (w *Writer) flush() error {
+ if _, err := w.w.Write(w.line[:w.i]); err != nil {
+ return err
+ }
+
+ w.i = 0
+ return nil
+}
+
+func isWhitespace(b byte) bool {
+ return b == ' ' || b == '\t'
+}