// 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 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.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}) h.Store.Audit.Record(ctx, audit.EventTypeSectionAdd) ctx.Transaction.Commit() 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 { 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 { response.WriteServerError(w, method, err) ctx.Transaction.Rollback() 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 { response.WriteServerError(w, method, err) ctx.Transaction.Rollback() 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 } ok, err := h.workflowPermitsChange(doc, ctx) if !ok { response.WriteForbiddenError(w) h.Runtime.Log.Info("attempted delete section on locked document") return } // If locked document then no can do if doc.Protection == workflow.ProtectionLock { response.WriteForbiddenError(w) h.Runtime.Log.Info("attempted delete section on locked document") return } // 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 { response.WriteServerError(w, method, err) h.Runtime.Log.Error(method, err) return } if !user.Exists(approvers, ctx.UserID) { response.WriteForbiddenError(w) h.Runtime.Log.Info("attempted delete document section when not approver") 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 } 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 } ok, err := 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 } 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 } 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 } } h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence) ctx.Transaction.Commit() 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 } } h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence) ctx.Transaction.Commit() 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}) h.Store.Audit.Record(ctx, audit.EventTypeSectionCopy) ctx.Transaction.Commit() 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 { response.WriteServerError(w, method, err) return } meta, err := h.Store.Page.GetPageMeta(ctx, pageID) if err != nil { response.WriteServerError(w, method, err) h.Runtime.Log.Error(method, err) return } revision, err := h.Store.Page.GetPageRevision(ctx, revisionID) if err != nil { 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}) h.Store.Audit.Record(ctx, audit.EventTypeSectionRollback) ctx.Transaction.Commit() 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 } // 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 }