1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-24 07:39:43 +02:00
documize/domain/page/endpoint.go
sauls8t 0997655e0a Fix order of DB transaction commits
Some DB commit commands were out of sequence and so have been fixed to be consist across the board. Specially, audit log entries have their own DB TX and so should be  executed outside of any other commit cycle.
2018-02-04 15:43:57 +00:00

1402 lines
35 KiB
Go

// 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 page
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"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/uniqueid"
"github.com/documize/community/domain"
"github.com/documize/community/domain/link"
"github.com/documize/community/domain/permission"
indexer "github.com/documize/community/domain/search"
"github.com/documize/community/domain/section/provider"
"github.com/documize/community/model/activity"
"github.com/documize/community/model/audit"
dm "github.com/documize/community/model/doc"
"github.com/documize/community/model/page"
pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/user"
"github.com/documize/community/model/workflow"
htmldiff "github.com/documize/html-diff"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime *env.Runtime
Store *domain.Store
Indexer indexer.Indexer
}
// Add inserts new section into document.
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "page.add"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
// check param
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
// read 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
}
model := new(page.NewPage)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
if model.Page.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
return
}
if model.Meta.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
return
}
// Check protection and approval process
document, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
// Protect locked
if document.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted write to locked document")
return
}
// Check edit permission
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// if document review process then we must mark page as pending
if document.Protection == workflow.ProtectionReview {
if model.Page.RelativeID == "" {
model.Page.Status = workflow.ChangePendingNew
} else {
model.Page.Status = workflow.ChangePending
}
} else {
model.Page.RelativeID = ""
model.Page.Status = workflow.ChangePublished
}
pageID := uniqueid.Generate()
model.Page.RefID = pageID
model.Meta.PageID = pageID
model.Meta.OrgID = ctx.OrgID // required for Render call below
model.Meta.UserID = ctx.UserID // required for Render call below
model.Page.SetDefaults()
model.Meta.SetDefaults()
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
output, ok := provider.Render(model.Page.ContentType, provider.NewContext(model.Meta.OrgID, model.Meta.UserID, ctx), model.Meta.Config, model.Meta.RawBody)
if !ok {
h.Runtime.Log.Info("provider.Render could not find: " + model.Page.ContentType)
}
model.Page.Body = output
err = h.Store.Page.Add(ctx, *model)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(model.Page.BlockID) > 0 {
h.Store.Block.IncrementUsage(ctx, model.Page.BlockID)
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionAdd)
np, _ := h.Store.Page.Get(ctx, pageID)
go h.Indexer.IndexContent(ctx, np)
response.WriteJSON(w, np)
}
// GetPage gets specified page for document.
func (h *Handler) GetPage(w http.ResponseWriter, r *http.Request) {
method := "page.GetPage"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
page, err := h.Store.Page.Get(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, documentID)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if page.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
return
}
response.WriteJSON(w, page)
}
// GetPages gets all pages for document.
func (h *Handler) GetPages(w http.ResponseWriter, r *http.Request) {
method := "page.GetPages"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
var pages []page.Page
var err error
content := request.Query(r, "content")
if len(content) > 0 {
pages, err = h.Store.Page.GetPagesWithoutContent(ctx, documentID)
} else {
pages, err = h.Store.Page.GetPages(ctx, documentID)
}
if len(pages) == 0 {
pages = []page.Page{}
}
page.Numberize(pages)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
}
response.WriteJSON(w, pages)
}
// GetMeta gets page meta data for specified document page.
func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
method := "page.meta"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, pageID)
h.Runtime.Log.Info(method + " no record")
meta = page.Meta{}
response.WriteJSON(w, meta)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if meta.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, meta)
}
// Update will persist changed page and note the fact
// that this is a new revision. If the page is the first in a document
// then the corresponding document title will also be changed.
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "page.update"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
// Check params
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
// Read payload
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "Bad request body")
h.Runtime.Log.Error(method, err)
return
}
model := new(page.NewPage)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
if model.Page.RefID != pageID || model.Page.DocumentID != documentID {
response.WriteBadRequestError(w, method, err.Error())
return
}
// Check protection and approval process
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted write to locked document")
return
}
// Check edit permission
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
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
}
model.Page.SetDefaults()
model.Meta.SetDefaults()
oldPageMeta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
output, ok := provider.Render(model.Page.ContentType, provider.NewContext(model.Meta.OrgID, oldPageMeta.UserID, ctx), model.Meta.Config, model.Meta.RawBody)
if !ok {
h.Runtime.Log.Info("provider.Render could not find: " + model.Page.ContentType)
}
model.Page.Body = output
refID := uniqueid.Generate()
skipRevision := false
skipRevision, err = strconv.ParseBool(request.Query(r, "r"))
// We don't track revisions for non-published pages
if model.Page.Status != workflow.ChangePublished {
skipRevision = true
}
err = h.Store.Page.Update(ctx, model.Page, refID, ctx.UserID, skipRevision)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Page.UpdateMeta(ctx, model.Meta, true) // change the UserID to the current one
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeEdited})
h.Store.Audit.Record(ctx, audit.EventTypeSectionUpdate)
// find any content links in the HTML
links := link.GetContentLinks(model.Page.Body)
// get a copy of previously saved links
previousLinks, _ := h.Store.Link.GetPageLinks(ctx, model.Page.DocumentID, model.Page.RefID)
// delete previous content links for this page
_, _ = h.Store.Link.DeleteSourcePageLinks(ctx, model.Page.RefID)
// save latest content links for this page
for _, link := range links {
link.Orphan = false
link.OrgID = ctx.OrgID
link.UserID = ctx.UserID
link.SourceDocumentID = model.Page.DocumentID
link.SourcePageID = model.Page.RefID
if link.LinkType == "document" {
link.TargetID = ""
}
// We check if there was a previously saved version of this link.
// If we find one, we carry forward the orphan flag.
for _, p := range previousLinks {
if link.TargetID == p.TargetID && link.LinkType == p.LinkType {
link.Orphan = p.Orphan
break
}
}
// save
err := h.Store.Link.Add(ctx, link)
if err != nil {
h.Runtime.Log.Error(fmt.Sprintf("Unable to insert content links for page %s", model.Page.RefID), err)
}
}
ctx.Transaction.Commit()
go h.Indexer.IndexContent(ctx, model.Page)
updatedPage, err := h.Store.Page.Get(ctx, pageID)
response.WriteJSON(w, updatedPage)
}
// Delete a page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "page.delete"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// you can delete your own pending page
ownPending := false
if (p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew) && p.UserID == ctx.UserID {
ownPending = true
}
// if not own page then check permission
if !ownPending {
ok, _ := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted delete section on locked document")
return
}
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(p.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, p.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: pageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
go h.Indexer.DeleteContent(ctx, pageID)
h.Store.Link.DeleteSourcePageLinks(ctx, pageID)
h.Store.Link.MarkOrphanPageLink(ctx, pageID)
h.Store.Page.DeletePageRevisions(ctx, pageID)
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
// Re-level all pages in document
h.LevelizeDocument(ctx, documentID)
response.WriteEmpty(w)
}
// DeletePages batch deletes pages.
func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
method := "page.delete.pages"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "Bad body")
return
}
model := new([]page.LevelRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, "JSON marshal")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
for _, page := range *model {
pageData, err := h.Store.Page.Get(ctx, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ownPending := false
if (pageData.Status == workflow.ChangePending || pageData.Status == workflow.ChangePendingNew) && pageData.UserID == ctx.UserID {
ownPending = true
}
// if not own page then check permission
if !ownPending {
ok, _ := h.workflowPermitsChange(doc, ctx)
if !ok {
ctx.Transaction.Rollback()
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted delete section on locked document")
return
}
}
if len(pageData.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, pageData.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
go h.Indexer.DeleteContent(ctx, page.PageID)
h.Store.Link.DeleteSourcePageLinks(ctx, page.PageID)
h.Store.Link.MarkOrphanPageLink(ctx, page.PageID)
h.Store.Page.DeletePageRevisions(ctx, page.PageID)
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: page.PageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
// Re-level all pages in document
h.LevelizeDocument(ctx, documentID)
response.WriteEmpty(w)
}
//**************************************************
// Table of Contents
//**************************************************
// ChangePageSequence will swap page sequence for a given number of pages.
func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
method := "page.sequence"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
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)
h.Runtime.Log.Error(method, err)
return
}
ok, err := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted to chaneg page sequence on protected document")
return
}
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
}
model := new([]page.SequenceRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
for _, p := range *model {
err = h.Store.Page.UpdateSequence(ctx, documentID, p.PageID, p.Sequence)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence)
response.WriteEmpty(w)
}
// ChangePageLevel handles page indent/outdent changes.
func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
method := "page.level"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
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)
h.Runtime.Log.Error(method, err)
return
}
ok, err := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted to chaneg page level on protected document")
return
}
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
}
model := new([]page.LevelRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
for _, p := range *model {
err = h.Store.Page.UpdateLevel(ctx, documentID, p.PageID, p.Level)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence)
response.WriteEmpty(w)
}
//**************************************************
// Copy Move Page
//**************************************************
// Copy copies page to either same or different document.
func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
method := "page.targets"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
targetID := request.Param(r, "targetID")
if len(targetID) == 0 {
response.WriteMissingDataError(w, method, "targetID")
return
}
// permission
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// fetch data
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// workflow check
if doc.Protection == workflow.ProtectionLock || doc.Protection == workflow.ProtectionReview {
response.WriteForbiddenError(w)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, documentID)
h.Runtime.Log.Error(method, err)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
pageMeta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, documentID)
h.Runtime.Log.Error(method, err)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
newPageID := uniqueid.Generate()
p.RefID = newPageID
p.Level = 1
p.Sequence = 0
p.DocumentID = targetID
p.UserID = ctx.UserID
pageMeta.DocumentID = targetID
pageMeta.PageID = newPageID
pageMeta.UserID = ctx.UserID
model := new(page.NewPage)
model.Meta = pageMeta
model.Page = p
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Page.Add(ctx, *model)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(model.Page.BlockID) > 0 {
h.Store.Block.IncrementUsage(ctx, model.Page.BlockID)
}
// Log action against target document
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: targetID,
PageID: newPageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionCopy)
np, _ := h.Store.Page.Get(ctx, pageID)
response.WriteJSON(w, np)
}
//**************************************************
// Revisions
//**************************************************
// GetDocumentRevisions returns all changes for a document.
func (h *Handler) GetDocumentRevisions(w http.ResponseWriter, r *http.Request) {
method := "page.document.revisions"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
revisions, _ := h.Store.Page.GetDocumentRevisions(ctx, documentID)
if len(revisions) == 0 {
revisions = []page.Revision{}
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentRevisions)
response.WriteJSON(w, revisions)
}
// GetRevisions returns all changes for a given page.
func (h *Handler) GetRevisions(w http.ResponseWriter, r *http.Request) {
method := "page.revisions"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
revisions, _ := h.Store.Page.GetPageRevisions(ctx, pageID)
if len(revisions) == 0 {
revisions = []page.Revision{}
}
response.WriteJSON(w, revisions)
}
// GetDiff returns HTML diff between two revisions of a given page.
func (h *Handler) GetDiff(w http.ResponseWriter, r *http.Request) {
method := "page.diff"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
revisionID := request.Param(r, "revisionID")
if len(revisionID) == 0 {
response.WriteMissingDataError(w, method, "revisionID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, pageID)
h.Runtime.Log.Error(method, err)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
revision, _ := h.Store.Page.GetPageRevision(ctx, revisionID)
latestHTML := p.Body
previousHTML := revision.Body
var result []byte
var cfg = &htmldiff.Config{
Granularity: 5,
InsertedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: palegreen;"}},
DeletedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: lightpink; text-decoration: line-through;"}},
ReplacedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: lightskyblue;"}},
CleanTags: []string{"documize"},
}
res, err := cfg.HTMLdiff([]string{latestHTML, previousHTML})
// res, err := cfg.HTMLdiff([]string{previousHTML, latestHTML})
if err != nil {
response.WriteServerError(w, method, err)
return
}
result = []byte(res[0])
w.Write(result)
}
// Rollback rolls back to a specific page revision.
func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
method := "page.rollback"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
revisionID := request.Param(r, "revisionID")
if len(revisionID) == 0 {
response.WriteMissingDataError(w, method, "revisionID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ok, err := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted to chaneg page sequence on protected document")
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
revision, err := h.Store.Page.GetPageRevision(ctx, revisionID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// roll back page
p.Body = revision.Body
refID := uniqueid.Generate()
err = h.Store.Page.Update(ctx, p, refID, ctx.UserID, false)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// roll back page meta
meta.Config = revision.Config
meta.RawBody = revision.RawBody
err = h.Store.Page.UpdateMeta(ctx, meta, false)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: p.DocumentID,
PageID: p.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeReverted})
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionRollback)
response.WriteJSON(w, p)
}
//**************************************************
// Bulk data fetching (reduce network traffic)
//**************************************************
// FetchPages returns all page data for given document: page, meta data, pending changes.
func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
method := "page.FetchPages"
ctx := domain.GetRequestContext(r)
model := []page.BulkRequest{}
// check params
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)
h.Runtime.Log.Error(method, err)
return
}
// published pages and new pages awaiting approval
pages, err := h.Store.Page.GetPages(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pages) == 0 {
pages = []page.Page{}
}
// unpublished pages
unpublished, err := h.Store.Page.GetUnpublishedPages(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(unpublished) == 0 {
unpublished = []page.Page{}
}
// meta for all pages
meta, err := h.Store.Page.GetDocumentPageMeta(ctx, documentID, false)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(meta) == 0 {
meta = []page.Meta{}
}
// permissions
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, doc.LabelID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(perms) == 0 {
perms = []pm.Permission{}
}
permissions := pm.DecodeUserPermissions(perms)
roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, doc.RefID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(roles) == 0 {
roles = []pm.Permission{}
}
docRoles := pm.DecodeUserDocumentPermissions(roles)
// check document view permissions
if !permissions.SpaceView && !permissions.SpaceManage && !permissions.SpaceOwner {
response.WriteForbiddenError(w)
return
}
// process published pages
for _, p := range pages {
// only send back pages that user can see
process := false
forcePending := false
if process == false && p.Status == workflow.ChangePublished {
process = true
}
if process == false && p.Status == workflow.ChangePendingNew && p.RelativeID == "" && p.UserID == ctx.UserID {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && p.UserID == ctx.UserID {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && (permissions.DocumentApprove || docRoles.DocumentRoleApprove) {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process {
d := page.BulkRequest{}
d.ID = fmt.Sprintf("container-%s", p.RefID)
d.Page = p
for _, m := range meta {
if p.RefID == m.PageID {
d.Meta = m
break
}
}
d.Pending = []page.PendingPage{}
// process pending pages
for _, up := range unpublished {
if up.RelativeID == p.RefID {
ud := page.PendingPage{}
ud.Page = up
for _, m := range meta {
if up.RefID == m.PageID {
ud.Meta = m
break
}
}
owner, err := h.Store.User.Get(ctx, up.UserID)
if err == nil {
ud.Owner = owner.Fullname()
}
d.Pending = append(d.Pending, ud)
}
}
// Handle situation where we need approval, and user has created new page
if forcePending && len(d.Pending) == 0 && doc.Protection == workflow.ProtectionReview {
ud := page.PendingPage{}
ud.Page = d.Page
ud.Meta = d.Meta
owner, err := h.Store.User.Get(ctx, d.Page.UserID)
if err == nil {
ud.Owner = owner.Fullname()
}
d.Pending = append(d.Pending, ud)
}
model = append(model, d)
}
}
// Attach numbers to pages, 1.1, 2.1.1 etc.
t := []page.Page{}
for _, i := range model {
t = append(t, i.Page)
}
page.Numberize(t)
for i, j := range t {
model[i].Page = j
// pending pages get same numbering
for k := range model[i].Pending {
model[i].Pending[k].Page.Numbering = j.Numbering
}
}
// deliver payload
response.WriteJSON(w, model)
}
func (h *Handler) workflowPermitsChange(doc dm.Document, ctx domain.RequestContext) (ok bool, err error) {
if doc.Protection == workflow.ProtectionNone {
if !permission.CanChangeDocument(ctx, *h.Store, doc.RefID) {
h.Runtime.Log.Info("attempted forbidden action on document")
return false, nil
}
return true, nil
}
// If locked document then no can do
if doc.Protection == workflow.ProtectionLock {
h.Runtime.Log.Info("attempted action on locked document")
return false, err
}
// If approval workflow then only approvers can delete page
if doc.Protection == workflow.ProtectionReview {
approvers, err := permission.GetDocumentApprovers(ctx, *h.Store, doc.LabelID, doc.RefID)
if err != nil {
h.Runtime.Log.Error("workflowAllowsChange", err)
return false, err
}
if user.Exists(approvers, ctx.UserID) {
h.Runtime.Log.Info("attempted action on document when not approver")
return true, nil
}
return false, nil
}
return true, nil
}