1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +02:00
documize/domain/document/endpoint.go

1015 lines
27 KiB
Go
Raw Normal View History

// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package document
import (
"database/sql"
"encoding/json"
"io/ioutil"
"net/http"
2018-01-10 16:07:17 +00:00
"sort"
"strings"
"github.com/documize/community/core/env"
"github.com/documize/community/core/request"
"github.com/documize/community/core/response"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/core/stringutil"
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain"
"github.com/documize/community/domain/organization"
2017-09-18 17:53:42 +01:00
"github.com/documize/community/domain/permission"
2017-08-01 10:39:07 +01:00
indexer "github.com/documize/community/domain/search"
"github.com/documize/community/domain/store"
"github.com/documize/community/model/activity"
"github.com/documize/community/model/attachment"
"github.com/documize/community/model/audit"
"github.com/documize/community/model/category"
"github.com/documize/community/model/doc"
"github.com/documize/community/model/link"
"github.com/documize/community/model/page"
pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/search"
"github.com/documize/community/model/space"
"github.com/documize/community/model/user"
"github.com/documize/community/model/workflow"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime *env.Runtime
Store *store.Store
2017-08-01 10:39:07 +01:00
Indexer indexer.Indexer
}
2018-03-19 12:24:58 +00:00
// Get is an endpoint that returns the document-level information for a
// given documentID.
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
2018-03-19 12:24:58 +00:00
method := "document.Get"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
document, err := h.Store.Document.Get(ctx, id)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, id)
return
}
if err != nil {
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
2018-09-19 16:03:29 +01:00
if !permission.CanViewSpaceDocument(ctx, *h.Store, document.SpaceID) {
response.WriteForbiddenError(w)
return
}
2018-03-15 17:11:53 +00:00
// draft mode does not record document views
if document.Lifecycle == workflow.LifecycleLive {
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
2018-03-15 17:11:53 +00:00
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: document.SpaceID,
2018-03-15 17:11:53 +00:00
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
2018-03-15 17:11:53 +00:00
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
2018-09-20 17:23:26 +01:00
return
2018-03-15 17:11:53 +00:00
}
2017-08-02 15:58:39 +01:00
ctx.Transaction.Commit()
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentView)
response.WriteJSON(w, document)
}
// DocumentLinks is an endpoint returning the links for a document.
func (h *Handler) DocumentLinks(w http.ResponseWriter, r *http.Request) {
2018-03-19 12:24:58 +00:00
method := "document.DocumentLinks"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
l, err := h.Store.Link.GetDocumentOutboundLinks(ctx, id)
if len(l) == 0 {
l = []link.Link{}
}
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, l)
}
// BySpace is an endpoint that returns the documents for given space.
func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
method := "document.BySpace"
ctx := domain.GetRequestContext(r)
spaceID := request.Query(r, "space")
if len(spaceID) == 0 {
response.WriteMissingDataError(w, method, "space")
return
}
2017-09-18 17:53:42 +01:00
if !permission.CanViewSpace(ctx, *h.Store, spaceID) {
response.WriteForbiddenError(w)
return
}
// Can user view drafts?
2018-03-15 17:11:53 +00:00
viewDrafts := permission.CanViewDrafts(ctx, *h.Store, spaceID)
2018-03-19 12:24:58 +00:00
// Get complete list of documents regardless of category permission
// and versioning.
documents, err := h.Store.Document.GetBySpace(ctx, spaceID)
2018-01-10 16:07:17 +00:00
if err != nil {
2017-08-03 10:00:24 +01:00
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
2018-03-19 12:24:58 +00:00
// Remove documents that cannot be seen due to lack of
// category view/access permission.
cats, err := h.Store.Category.GetBySpace(ctx, spaceID)
members, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
2018-03-16 18:27:33 +00:00
filtered := FilterCategoryProtected(documents, cats, members, viewDrafts)
2017-08-03 10:00:24 +01:00
2018-03-19 12:24:58 +00:00
// Keep the latest version when faced with multiple versions.
filtered = FilterLastVersion(filtered)
// Sort document list by ID.
sort.Sort(doc.ByID(filtered))
// Attach category membership to each document.
// Put category names into map for easier retrieval.
catNames := make(map[string]string)
for i := range cats {
catNames[cats[i].RefID] = cats[i].Name
}
// Loop the smaller list which is categories assigned to documents.
for i := range members {
// Get name of category
cn := catNames[members[i].CategoryID]
// Find document that is assigned this category.
j := sort.Search(len(filtered), func(k int) bool { return filtered[k].RefID <= members[i].DocumentID })
// Attach category name to document
if j < len(filtered) && filtered[j].RefID == members[i].DocumentID {
filtered[j].Category = append(filtered[j].Category, cn)
}
}
// Sort document list by title.
sort.Sort(doc.ByName(filtered))
response.WriteJSON(w, filtered)
}
2018-03-19 12:24:58 +00:00
// Update updates an existing document using the format described
// in NewDocumentModel() encoded as JSON in the request.
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
2018-03-19 12:24:58 +00:00
method := "document.Update"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
2017-09-18 17:53:42 +01:00
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
d := doc.Document{}
err = json.Unmarshal(body, &d)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
d.RefID = documentID
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
2018-03-19 12:24:58 +00:00
// If space changed for document, remove document categories.
2017-09-26 20:13:44 +01:00
oldDoc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
ctx.Transaction.Rollback()
2017-09-26 20:13:44 +01:00
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
2018-09-19 16:03:29 +01:00
if oldDoc.SpaceID != d.SpaceID {
2017-09-26 20:13:44 +01:00
h.Store.Category.RemoveDocumentCategories(ctx, d.RefID)
2018-09-19 16:03:29 +01:00
err = h.Store.Document.MoveActivity(ctx, documentID, oldDoc.SpaceID, d.SpaceID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
2017-09-26 20:13:44 +01:00
}
err = h.Store.Document.Update(ctx, d)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
2018-03-19 12:24:58 +00:00
// If document part of versioned document group
// then document name must be applied to all documents
// in the group.
if len(d.GroupID) > 0 {
err = h.Store.Document.UpdateGroup(ctx, d)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
// Detect change in document status/lifecycle.
if d.Lifecycle != oldDoc.Lifecycle {
// Record document being marked as archived.
if d.Lifecycle == workflow.LifecycleArchived {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: d.SpaceID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeArchived})
}
2018-03-16 18:27:33 +00:00
// Record document being marked as draft.
if d.Lifecycle == workflow.LifecycleDraft {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: d.SpaceID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDraft})
}
// Record document being marked as live.
if d.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: d.SpaceID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypePublished})
}
2018-03-16 18:27:33 +00:00
}
ctx.Transaction.Commit()
h.Store.Space.SetStats(ctx, d.SpaceID)
if oldDoc.SpaceID != d.SpaceID {
h.Store.Space.SetStats(ctx, oldDoc.SpaceID)
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentUpdate)
2018-03-19 12:24:58 +00:00
// Live document indexed for search.
if d.Lifecycle == workflow.LifecycleLive {
2018-03-15 17:11:53 +00:00
a, _ := h.Store.Attachment.GetAttachments(ctx, documentID)
go h.Indexer.IndexDocument(ctx, d, a)
2018-03-19 15:04:02 +00:00
pages, _ := h.Store.Page.GetPages(ctx, d.RefID)
for i := range pages {
go h.Indexer.IndexContent(ctx, pages[i])
}
} else {
go h.Indexer.DeleteDocument(ctx, d.RefID)
}
2017-08-01 10:39:07 +01:00
response.WriteEmpty(w)
}
// Delete is an endpoint that deletes a document specified by documentID.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
2018-03-19 12:24:58 +00:00
method := "document.Delete"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
// If locked document then no can do
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted action on locked document")
return
}
// If approval workflow then only approvers can delete page
if doc.Protection == workflow.ProtectionReview {
2018-09-19 16:03:29 +01:00
approvers, err := permission.GetUsersWithDocumentPermission(ctx, *h.Store, doc.SpaceID, doc.RefID, pm.DocumentApprove)
if err != nil {
response.WriteForbiddenError(w)
h.Runtime.Log.Error(method, err)
return
}
if !user.Exists(approvers, ctx.UserID) {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted action on document when not approver")
return
}
}
// permission check
if !permission.CanDeleteDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
_, err = h.Store.Document.Delete(ctx, documentID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
_, err = h.Store.Pin.DeletePinnedDocument(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
return
}
h.Store.Link.MarkOrphanDocumentLink(ctx, documentID)
h.Store.Link.DeleteSourceDocumentLinks(ctx, documentID)
2018-03-15 17:11:53 +00:00
// Draft actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: doc.SpaceID,
2018-03-15 17:11:53 +00:00
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
}
ctx.Transaction.Commit()
h.Store.Space.SetStats(ctx, doc.SpaceID)
h.Store.Audit.Record(ctx, audit.EventTypeDocumentDelete)
2017-08-15 19:41:44 +01:00
go h.Indexer.DeleteDocument(ctx, documentID)
2017-08-01 10:39:07 +01:00
response.WriteEmpty(w)
}
2017-08-01 10:39:07 +01:00
2018-03-19 12:24:58 +00:00
// SearchDocuments endpoint takes a list of keywords and returns a list of
// document references matching those keywords.
2017-08-01 10:39:07 +01:00
func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
2018-03-19 12:24:58 +00:00
method := "document.SearchDocuments"
2017-08-01 10:39:07 +01:00
ctx := domain.GetRequestContext(r)
2017-08-15 14:15:31 +01:00
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
2017-08-01 10:39:07 +01:00
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
2017-08-15 14:15:31 +01:00
h.Runtime.Log.Error(method, err)
return
}
// Get search criteria.
2017-08-15 14:15:31 +01:00
options := search.QueryOptions{}
err = json.Unmarshal(body, &options)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
2017-08-01 10:39:07 +01:00
return
}
options.Keywords = strings.TrimSpace(options.Keywords)
// Get documents for search criteria.
2017-08-15 14:15:31 +01:00
results, err := h.Store.Search.Documents(ctx, options)
2017-08-01 10:39:07 +01:00
if err != nil {
2017-08-03 10:00:24 +01:00
h.Runtime.Log.Error(method, err)
2017-08-01 10:39:07 +01:00
}
// Generate slugs for search URL.
2017-08-01 10:39:07 +01:00
for key, result := range results {
2018-03-30 17:03:18 +01:00
results[key].DocumentSlug = stringutil.MakeSlug(result.Document)
results[key].SpaceSlug = stringutil.MakeSlug(result.Space)
2017-08-01 10:39:07 +01:00
}
// Remove documents that cannot be seen due to lack of
// category view/access permission.
cats, err := h.Store.Category.GetByOrg(ctx, ctx.UserID)
members, err := h.Store.Category.GetOrgCategoryMembership(ctx, ctx.UserID)
filtered := indexer.FilterCategoryProtected(results, cats, members)
// Record user search history.
2018-04-07 14:30:31 +01:00
if !options.SkipLog {
if len(filtered) > 0 {
go h.recordSearchActivity(ctx, filtered, options.Keywords)
2018-04-07 14:30:31 +01:00
} else {
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: "",
2018-04-07 14:30:31 +01:00
DocumentID: "",
Metadata: options.Keywords,
SourceType: activity.SourceTypeSearch,
ActivityType: activity.TypeSearched})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
}
}
2017-08-01 10:39:07 +01:00
h.Store.Audit.Record(ctx, audit.EventTypeSearch)
response.WriteJSON(w, filtered)
2017-08-01 10:39:07 +01:00
}
// Record search request once per document.
// But only if document is partof shared space at the time of the search.
func (h *Handler) recordSearchActivity(ctx domain.RequestContext, q []search.QueryResult, keywords string) {
2018-03-30 17:03:18 +01:00
method := "recordSearchActivity"
var err error
prev := make(map[string]bool)
2018-03-30 17:03:18 +01:00
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
for i := range q {
// Empty space ID usually signals private document
// hence search activity should not be visible to others.
if len(q[i].SpaceID) == 0 {
continue
}
sp, err := h.Store.Space.Get(ctx, q[i].SpaceID)
if err != nil || len(sp.RefID) == 0 || sp.Type == space.ScopePrivate {
continue
}
if _, isExisting := prev[q[i].DocumentID]; !isExisting {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: q[i].SpaceID,
DocumentID: q[i].DocumentID,
Metadata: keywords,
SourceType: activity.SourceTypeSearch,
ActivityType: activity.TypeSearched})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
prev[q[i].DocumentID] = true
2018-03-30 17:03:18 +01:00
}
}
ctx.Transaction.Commit()
}
// FetchDocumentData returns all document data in single API call.
func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
method := "document.FetchDocumentData"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
document, err := h.Store.Document.Get(ctx, id)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, id)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
2018-09-19 16:03:29 +01:00
if !permission.CanViewSpaceDocument(ctx, *h.Store, document.SpaceID) {
response.WriteForbiddenError(w)
return
}
// Don't serve archived document.
2018-03-15 17:11:53 +00:00
if document.Lifecycle == workflow.LifecycleArchived {
response.WriteForbiddenError(w)
return
}
// Check if draft document can been seen by user.
if document.Lifecycle == workflow.LifecycleDraft && !permission.CanViewDrafts(ctx, *h.Store, document.SpaceID) {
response.WriteForbiddenError(w)
return
}
// If document has been assigned one or more categories,
// we check to see if user can view this document.
cat, err := h.Store.Category.GetDocumentCategoryMembership(ctx, document.RefID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
perm, err := h.Store.Permission.GetUserCategoryPermissions(ctx, ctx.UserID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
see := []category.Category{}
for _, c := range cat {
for _, p := range perm {
if p.RefID == c.RefID {
see = append(see, c)
break
}
}
}
// User cannot view document if document has categories assigned
// but user cannot see any of them.
if len(cat) > 0 && len(see) == 0 {
response.WriteForbiddenError(w)
return
}
// permissions
2018-09-19 16:03:29 +01:00
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, document.SpaceID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
2018-09-19 16:03:29 +01:00
h.Runtime.Log.Error(method, err)
return
}
if len(perms) == 0 {
perms = []pm.Permission{}
}
record := pm.DecodeUserPermissions(perms)
2017-12-26 13:25:10 +00:00
roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, document.RefID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
2018-09-19 16:03:29 +01:00
h.Runtime.Log.Error(method, err)
2017-12-26 13:25:10 +00:00
return
}
if len(roles) == 0 {
roles = []pm.Permission{}
}
rolesRecord := pm.DecodeUserDocumentPermissions(roles)
// links
l, err := h.Store.Link.GetDocumentOutboundLinks(ctx, id)
if len(l) == 0 {
l = []link.Link{}
}
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// spaces
sp, err := h.Store.Space.GetViewable(ctx)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(sp) == 0 {
sp = []space.Space{}
}
2018-03-19 12:24:58 +00:00
// Get version information for this document.
v := []doc.Version{}
if len(document.GroupID) > 0 {
// Get versions.
vt, err := h.Store.Document.GetVersions(ctx, document.GroupID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// What about draft document versions?
if record.DocumentLifecycle {
// We can see and manage document lifecycle so take all versions.
v = vt
} else {
// Only send back LIVE content because user cannot drafts.
for i := range vt {
if vt[i].Lifecycle == workflow.LifecycleLive {
v = append(v, vt[i])
}
}
}
2018-03-19 12:24:58 +00:00
}
// Attachments.
a, err := h.Store.Attachment.GetAttachments(ctx, id)
if err != nil && err != sql.ErrNoRows {
h.Runtime.Log.Error("get attachment", err)
response.WriteServerError(w, method, err)
return
}
if len(a) == 0 {
a = []attachment.Attachment{}
}
2018-03-19 12:24:58 +00:00
// Prepare response.
data := BulkDocumentData{}
data.Document = document
data.Permissions = record
2017-12-26 13:25:10 +00:00
data.Roles = rolesRecord
data.Links = l
data.Spaces = sp
2018-03-19 12:24:58 +00:00
data.Versions = v
data.Attachments = a
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
2018-03-15 17:11:53 +00:00
if document.Lifecycle == workflow.LifecycleLive {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
2018-09-19 16:03:29 +01:00
SpaceID: document.SpaceID,
2018-03-15 17:11:53 +00:00
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
2018-03-15 17:11:53 +00:00
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeDocumentView)
response.WriteJSON(w, data)
}
// BulkDocumentData represents all data associated for a single document.
// Used by FetchDocumentData() bulk data load call.
type BulkDocumentData struct {
Document doc.Document `json:"document"`
Permissions pm.Record `json:"permissions"`
Roles pm.DocumentRecord `json:"roles"`
Spaces []space.Space `json:"folders"`
Links []link.Link `json:"links"`
Versions []doc.Version `json:"versions"`
Attachments []attachment.Attachment `json:"attachments"`
}
2018-07-28 11:43:45 -04:00
// Export returns content as self-enclosed HTML file.
func (h *Handler) Export(w http.ResponseWriter, r *http.Request) {
method := "document.Export"
ctx := domain.GetRequestContext(r)
2018-07-28 15:30:33 -04:00
// Deduce ORG if anon user.
if len(ctx.OrgID) == 0 {
ctx.Subdomain = organization.GetSubdomainFromHost(r)
org, err := h.Store.Organization.GetOrganizationByDomain(ctx.Subdomain)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ctx.OrgID = org.RefID
}
2018-07-28 11:43:45 -04:00
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
spec := exportSpec{}
err = json.Unmarshal(body, &spec)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
export, err := BuildExport(ctx, *h.Store, spec)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
2018-07-28 15:30:33 -04:00
w.Write([]byte(export))
2018-07-28 11:43:45 -04:00
}
// Duplicate makes a copy of a document.
// Name of new document is required.
func (h *Handler) Duplicate(w http.ResponseWriter, r *http.Request) {
method := "document.Duplicate"
ctx := domain.GetRequestContext(r)
// Holds old to new ref ID values.
pageRefMap := make(map[string]string)
// Parse payload
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
m := doc.DuplicateModel{}
err = json.Unmarshal(body, &m)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
// Check permissions
if !permission.CanViewDocument(ctx, *h.Store, m.DocumentID) {
response.WriteForbiddenError(w)
return
}
if !permission.CanUploadDocument(ctx, *h.Store, m.SpaceID) {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Get document to be duplicated.
d, err := h.Store.Document.Get(ctx, m.DocumentID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Assign new ID and remove versioning info.
d.RefID = uniqueid.Generate()
d.GroupID = ""
d.Name = m.Name
// Fetch doc attachments, links.
da, err := h.Store.Attachment.GetAttachmentsWithData(ctx, m.DocumentID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
dl, err := h.Store.Link.GetDocumentOutboundLinks(ctx, m.DocumentID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Fetch published and unpublished sections.
pages, err := h.Store.Page.GetPages(ctx, m.DocumentID)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pages) == 0 {
pages = []page.Page{}
}
unpublished, err := h.Store.Page.GetUnpublishedPages(ctx, m.DocumentID)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(unpublished) == 0 {
unpublished = []page.Page{}
}
pages = append(pages, unpublished...)
meta, err := h.Store.Page.GetDocumentPageMeta(ctx, m.DocumentID, false)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(meta) == 0 {
meta = []page.Meta{}
}
// Duplicate the complete document starting with the document.
err = h.Store.Document.Add(ctx, d)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Attachments
for i := range da {
da[i].RefID = uniqueid.Generate()
da[i].DocumentID = d.RefID
err = h.Store.Attachment.Add(ctx, da[i])
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
// Sections
for j := range pages {
// Create mapping between old and new section IDs.
pageRefMap[pages[j].RefID] = uniqueid.Generate()
// Get meta for section
sm := page.Meta{}
for k := range meta {
if meta[k].SectionID == pages[j].RefID {
sm = meta[k]
break
}
}
// Get attachments for section.
sa, err := h.Store.Attachment.GetSectionAttachments(ctx, pages[j].RefID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
pages[j].RefID = pageRefMap[pages[j].RefID]
pages[j].DocumentID = d.RefID
sm.DocumentID = d.RefID
sm.SectionID = pages[j].RefID
err = h.Store.Page.Add(ctx, page.NewPage{Page: pages[j], Meta: sm})
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Now add any section attachments.
for n := range sa {
sa[n].RefID = uniqueid.Generate()
sa[n].DocumentID = d.RefID
sa[n].SectionID = pages[j].RefID
err = h.Store.Attachment.Add(ctx, sa[n])
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
}
// Links
for l := range dl {
// Update common meta for all links.
dl[l].RefID = uniqueid.Generate()
dl[l].SourceDocumentID = d.RefID
// Remap section ID.
if len(dl[l].SourceSectionID) > 0 && len(pageRefMap[dl[l].SourceSectionID]) > 0 {
dl[l].SourceSectionID = pageRefMap[dl[l].SourceSectionID]
}
err = h.Store.Link.Add(ctx, dl[l])
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
// Record activity and finish.
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: d.SpaceID,
DocumentID: d.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeCreated})
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeDocumentAdd)
// Update search index if published.
if d.Lifecycle == workflow.LifecycleLive {
a, _ := h.Store.Attachment.GetAttachments(ctx, d.RefID)
go h.Indexer.IndexDocument(ctx, d, a)
pages, _ := h.Store.Page.GetPages(ctx, d.RefID)
for i := range pages {
go h.Indexer.IndexContent(ctx, pages[i])
}
} else {
go h.Indexer.DeleteDocument(ctx, d.RefID)
}
response.WriteEmpty(w)
}