package service import ( "context" "reflect" "github.com/cortezaproject/corteza/server/compose/service/event" "github.com/cortezaproject/corteza/server/compose/types" "github.com/cortezaproject/corteza/server/pkg/actionlog" "github.com/cortezaproject/corteza/server/pkg/errors" "github.com/cortezaproject/corteza/server/pkg/eventbus" "github.com/cortezaproject/corteza/server/pkg/handle" "github.com/cortezaproject/corteza/server/pkg/label" "github.com/cortezaproject/corteza/server/pkg/locale" "github.com/cortezaproject/corteza/server/store" systemTypes "github.com/cortezaproject/corteza/server/system/types" ) type ( page struct { actionlog actionlog.Recorder ac pageAccessController eventbus eventDispatcher store store.Storer locale ResourceTranslationsManagerService pageSettings *pageSettings } pageSettings struct { hideNew bool hideEdit bool hideSubmit bool hideDelete bool hideClone bool hideBack bool } pageAccessController interface { CanManageResourceTranslations(ctx context.Context) bool CanSearchPagesOnNamespace(context.Context, *types.Namespace) bool CanReadNamespace(context.Context, *types.Namespace) bool CanCreatePageOnNamespace(context.Context, *types.Namespace) bool CanReadPage(context.Context, *types.Page) bool CanUpdatePage(context.Context, *types.Page) bool CanDeletePage(context.Context, *types.Page) bool } pageUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Page) (pageChanges, error) pageChanges uint8 ) const ( pageUnchanged pageChanges = 0 pageChanged pageChanges = 1 pageLabelsChanged pageChanges = 2 ) func Page() *page { return &page{ actionlog: DefaultActionlog, ac: DefaultAccessControl, eventbus: eventbus.Service(), store: DefaultStore, locale: DefaultResourceTranslation, pageSettings: &pageSettings{}, } } func (svc page) FindByID(ctx context.Context, namespaceID, pageID uint64) (p *types.Page, err error) { return svc.lookup(ctx, namespaceID, func(aProps *pageActionProps) (*types.Page, error) { if pageID == 0 { return nil, PageErrInvalidID() } aProps.page.ID = pageID return store.LookupComposePageByID(ctx, svc.store, pageID) }) } func (svc page) FindByHandle(ctx context.Context, namespaceID uint64, h string) (c *types.Page, err error) { return svc.lookup(ctx, namespaceID, func(aProps *pageActionProps) (*types.Page, error) { if !handle.IsValid(h) { return nil, PageErrInvalidHandle() } aProps.page.Handle = h return store.LookupComposePageByNamespaceIDHandle(ctx, svc.store, namespaceID, h) }) } func (svc page) FindByPageID(ctx context.Context, namespaceID, pageID uint64) (p *types.Page, err error) { return svc.lookup(ctx, namespaceID, func(aProps *pageActionProps) (*types.Page, error) { if pageID == 0 { return nil, PageErrInvalidID() } aProps.page.ID = pageID return store.LookupComposePageByID(ctx, svc.store, pageID) }) } func checkPage(ctx context.Context, ac pageAccessController) func(res *types.Page) (bool, error) { return func(res *types.Page) (bool, error) { if !ac.CanReadPage(ctx, res) { return false, nil } return true, nil } } // search fn() orchestrates pages search, namespace preload and check func (svc page) search(ctx context.Context, filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error) { var ( aProps = &pageActionProps{filter: &filter} ns *types.Namespace ) // For each fetched item, store backend will check if it is valid or not filter.Check = checkPage(ctx, svc.ac) err = func() error { ns, err = loadNamespace(ctx, svc.store, filter.NamespaceID) if err != nil { return err } aProps.setNamespace(ns) if !svc.ac.CanSearchPagesOnNamespace(ctx, ns) { return PageErrNotAllowedToSearch() } if len(filter.Labels) > 0 { filter.LabeledIDs, err = label.Search( ctx, svc.store, types.Page{}.LabelResourceKind(), filter.Labels, ) if err != nil { return err } // labels specified but no labeled resources found if len(filter.LabeledIDs) == 0 { return nil } } if set, f, err = store.SearchComposePages(ctx, svc.store, filter); err != nil { return err } if err = label.Load(ctx, svc.store, toLabeledPages(set)...); err != nil { return err } set.Walk(func(p *types.Page) error { preparePageConfig(svc.pageSettings, p) return nil }) // i18n tag := locale.GetAcceptLanguageFromContext(ctx) set.Walk(func(p *types.Page) error { p.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, p.ResourceTranslation())) return nil }) return nil }() return set, f, svc.recordAction(ctx, aProps, PageActionSearch, err) } func (svc page) FindBySelfID(ctx context.Context, namespaceID, parentID uint64) (pp types.PageSet, f types.PageFilter, err error) { return svc.search(ctx, types.PageFilter{ NamespaceID: namespaceID, ParentID: parentID, // This will enable parentID=0 query Root: true, Check: checkPage(ctx, svc.ac), }) } func (svc page) Find(ctx context.Context, filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error) { return svc.search(ctx, filter) } func (svc page) Tree(ctx context.Context, namespaceID uint64) (tree types.PageSet, err error) { var ( pages types.PageSet filter = types.PageFilter{ NamespaceID: namespaceID, Check: checkPage(ctx, svc.ac), } ) if err = filter.Sort.Set("weight ASC"); err != nil { return } if pages, _, err = svc.search(ctx, filter); err != nil { return } // safe to ignore errors _ = pages.Walk(func(p *types.Page) error { if p.SelfID == 0 { tree = append(tree, p) } else if c := pages.FindByID(p.SelfID); c != nil { if c.Children == nil { c.Children = types.PageSet{} } c.Children = append(c.Children, p) } else { // Move orphans to root p.SelfID = 0 tree = append(tree, p) } return nil }) return tree, nil } // Reorder pages func (svc page) Reorder(ctx context.Context, namespaceID, parentID uint64, pageIDs []uint64) (err error) { var ( aProps = &pageActionProps{page: &types.Page{ID: parentID}} ns *types.Namespace p *types.Page ) err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error { if ns, err = loadNamespace(ctx, s, namespaceID); err != nil { return err } if parentID == 0 { // Reordering on root mode -- check if user can create pages. if !svc.ac.CanCreatePageOnNamespace(ctx, ns) { return PageErrNotAllowedToUpdate() } } else { // Validate permissions on parent page if p, err = store.LookupComposePageByID(ctx, s, parentID); errors.IsNotFound(err) { return PageErrNotFound() } else if err != nil { return err } aProps.setPage(p) if !svc.ac.CanUpdatePage(ctx, p) { return PageErrNotAllowedToUpdate() } } return store.ReorderComposePages(ctx, s, namespaceID, parentID, pageIDs) }) return svc.recordAction(ctx, aProps, PageActionReorder, err) } func (svc page) Create(ctx context.Context, new *types.Page) (*types.Page, error) { var ( ns *types.Namespace aProps = &pageActionProps{page: new} ) new.ID = 0 err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { if !handle.IsValid(new.Handle) { return PageErrInvalidID() } if ns, err = loadNamespace(ctx, s, new.NamespaceID); err != nil { return err } if !svc.ac.CanCreatePageOnNamespace(ctx, ns) { return PageErrNotAllowedToCreate() } aProps.setNamespace(ns) if err = svc.eventbus.WaitFor(ctx, event.PageBeforeCreate(new, nil, ns, nil)); err != nil { return err } if err = svc.uniqueCheck(ctx, new); err != nil { return err } new.ID = nextID() new.CreatedAt = *now() new.UpdatedAt = nil new.DeletedAt = nil // Ensure page-block IDs for i := range new.Blocks { new.Blocks[i].BlockID = uint64(i) + 1 } aProps.setChanged(new) if err = store.CreateComposePage(ctx, s, new); err != nil { return err } if err = updateTranslations(ctx, svc.ac, svc.locale, new.EncodeTranslations()...); err != nil { return } preparePageConfig(svc.pageSettings, new) if err = label.Create(ctx, s, new); err != nil { return } _ = svc.eventbus.WaitFor(ctx, event.PageAfterCreate(new, nil, ns, nil)) return err }) return new, svc.recordAction(ctx, aProps, PageActionCreate, err) } func (svc page) Update(ctx context.Context, upd *types.Page) (c *types.Page, err error) { err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { ns, res, err := loadPageCombo(ctx, s, upd.NamespaceID, upd.ID) if err != nil { return } c, err = svc.updater(ctx, svc.store, ns, res, PageActionUpdate, svc.handleUpdate(ctx, upd)) return }) return } func (svc page) DeleteByID(ctx context.Context, namespaceID, pageID uint64, strategy types.PageChildrenDeleteStrategy) error { var ( validChildren, pp types.PageSet ns *types.Namespace res *types.Page skipUndeleted = func(p *types.Page) (bool, error) { return p.DeletedAt == nil, nil } ) return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { if strategy == types.PageChildrenOnDeleteForce { // simply delete the page and ignore the subpages ns, res, err = loadPageCombo(ctx, s, namespaceID, pageID) if err != nil { return } } else { // Load all pages in the namespace and // try to figure out the family tree pp, _, err = store.SearchComposePages(ctx, s, types.PageFilter{ NamespaceID: namespaceID, }) if res = pp.FindByID(pageID); res == nil { return PageErrNotFound() } validChildren, _ = pp.FindByParent(res.ID).Filter(skipUndeleted) switch strategy { case types.PageChildrenOnDeleteAbort: // Abort if there are any valid (undeleted) children if len(validChildren) > 0 { return PageErrDeleteAbortedForPageWithSubpages() } case types.PageChildrenOnDeleteRebase: // update all our children to point to our parent err = validChildren.Walk(func(child *types.Page) (err error) { updChild := child.Clone() updChild.SelfID = res.SelfID _, err = svc.updater(ctx, s, ns, child, PageActionUpdate, svc.handleUpdate(ctx, updChild)) return err }) if err != nil { return } case types.PageChildrenOnDeleteCascade: // update all our children to point to our parent err = pp.RecursiveWalk(res, func(child *types.Page, _ *types.Page) (err error) { if child.DeletedAt != nil { // skip the ones that are already deleted return nil } _, err = svc.updater(ctx, s, ns, child, PageActionDelete, svc.handleDelete) return err }) if err != nil { return } default: return PageErrUnknownDeleteStrategy() } if ns, err = loadNamespace(ctx, s, namespaceID); err != nil { return } } _, err = svc.updater(ctx, svc.store, ns, res, PageActionDelete, svc.handleDelete) return }) } func (svc page) UndeleteByID(ctx context.Context, namespaceID, pageID uint64) error { return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { ns, res, err := loadPageCombo(ctx, s, namespaceID, pageID) if err != nil { return } _, err = svc.updater(ctx, svc.store, ns, res, PageActionUpdate, svc.handleUndelete) return }) } func (svc page) UpdateIcon(ctx context.Context, namespaceID, pageID uint64, icon *types.PageConfigIcon) (out *types.PageConfigIcon, err error) { err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { ns, p, err := loadPageCombo(ctx, s, namespaceID, pageID) if err != nil { return } p.Config.NavItem.Icon = icon p, err = svc.updater(ctx, svc.store, ns, p, PageActionUpdate, svc.handleUpdate(ctx, p)) out = p.Config.NavItem.Icon return }) return } func (svc page) updater(ctx context.Context, s store.Storer, ns *types.Namespace, res *types.Page, action func(...*pageActionProps) *pageAction, fn pageUpdateHandler) (*types.Page, error) { var ( changes pageChanges old *types.Page aProps = &pageActionProps{page: res} err error ) err = store.Tx(ctx, s, func(ctx context.Context, s store.Storer) (err error) { if err = label.Load(ctx, svc.store, res); err != nil { return err } // Get max blockID for later use blockID := uint64(0) for _, b := range res.Blocks { if b.BlockID > blockID { blockID = b.BlockID } } old = res.Clone() aProps.setNamespace(ns) aProps.setChanged(res) if res.DeletedAt == nil { err = svc.eventbus.WaitFor(ctx, event.PageBeforeUpdate(old, res, ns, nil)) } else { err = svc.eventbus.WaitFor(ctx, event.PageBeforeDelete(old, res, ns, nil)) } if err != nil { return } if changes, err = fn(ctx, ns, res); err != nil { return err } if changes&pageChanged > 0 { if err = store.UpdateComposePage(ctx, s, res); err != nil { return err } } if err = updateTranslations(ctx, svc.ac, svc.locale, res.EncodeTranslations()...); err != nil { return } preparePageConfig(svc.pageSettings, res) if changes&pageLabelsChanged > 0 { if err = label.Update(ctx, s, res); err != nil { return } } if res.DeletedAt == nil { err = svc.eventbus.WaitFor(ctx, event.PageAfterUpdate(res, res, ns, nil)) } else { err = svc.eventbus.WaitFor(ctx, event.PageAfterDelete(nil, res, ns, nil)) } return err }) return res, svc.recordAction(ctx, aProps, action, err) } // lookup fn() orchestrates page lookup, namespace preload and check func (svc page) lookup(ctx context.Context, namespaceID uint64, lookup func(*pageActionProps) (*types.Page, error)) (p *types.Page, err error) { var aProps = &pageActionProps{page: &types.Page{NamespaceID: namespaceID}} err = func() error { if ns, err := loadNamespace(ctx, svc.store, namespaceID); err != nil { return err } else { aProps.setNamespace(ns) } if p, err = lookup(aProps); errors.IsNotFound(err) { return PageErrNotFound() } else if err != nil { return err } preparePageConfig(svc.pageSettings, p) p.DecodeTranslations(svc.locale.Locale().ResourceTranslations(locale.GetAcceptLanguageFromContext(ctx), p.ResourceTranslation())) aProps.setPage(p) if !svc.ac.CanReadPage(ctx, p) { return PageErrNotAllowedToRead() } if err = label.Load(ctx, svc.store, p); err != nil { return err } return nil }() return p, svc.recordAction(ctx, aProps, PageActionLookup, err) } func (svc page) uniqueCheck(ctx context.Context, p *types.Page) (err error) { if p.Handle != "" { if e, _ := store.LookupComposePageByNamespaceIDHandle(ctx, svc.store, p.NamespaceID, p.Handle); e != nil && e.ID != p.ID { return PageErrHandleNotUnique() } } if p.ModuleID > 0 { if e, _ := store.LookupComposePageByNamespaceIDModuleID(ctx, svc.store, p.NamespaceID, p.ModuleID); e != nil && e.ID != p.ID { return PageErrModuleNotFound() } } return nil } func (svc page) handleUpdate(ctx context.Context, upd *types.Page) pageUpdateHandler { return func(ctx context.Context, ns *types.Namespace, res *types.Page) (changes pageChanges, err error) { if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) { return pageUnchanged, PageErrStaleData() } if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) { return pageUnchanged, PageErrInvalidHandle() } if err := svc.uniqueCheck(ctx, upd); err != nil { return pageUnchanged, err } if !svc.ac.CanUpdatePage(ctx, res) { return pageUnchanged, PageErrNotAllowedToUpdate() } // Get max blockID for later use blockID := uint64(0) for _, b := range res.Blocks { if b.BlockID > blockID { blockID = b.BlockID } } if res.ID != upd.ID { res.ID = upd.ID changes |= pageChanged } if res.SelfID != upd.SelfID { res.SelfID = upd.SelfID changes |= pageChanged } if !reflect.DeepEqual(res.Config, upd.Config) { res.Config = upd.Config changes |= pageChanged } if !reflect.DeepEqual(res.Blocks, upd.Blocks) { res.Blocks = upd.Blocks changes |= pageChanged } // Assure blockIDs for i, b := range res.Blocks { if b.BlockID == 0 { blockID++ b.BlockID = blockID res.Blocks[i] = b changes |= pageChanged } } if res.Title != upd.Title { res.Title = upd.Title changes |= pageChanged } if res.Handle != upd.Handle { res.Handle = upd.Handle changes |= pageChanged } if res.Description != upd.Description { res.Description = upd.Description changes |= pageChanged } if res.Visible != upd.Visible { res.Visible = upd.Visible changes |= pageChanged } if res.Weight != upd.Weight { res.Weight = upd.Weight changes |= pageChanged } if upd.Labels != nil { if label.Changed(res.Labels, upd.Labels) { changes |= pageLabelsChanged res.Labels = upd.Labels } } if changes&pageChanged > 0 { res.UpdatedAt = now() } return } } func (svc page) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Page) (pageChanges, error) { if !svc.ac.CanDeletePage(ctx, m) { return pageUnchanged, PageErrNotAllowedToDelete() } if m.DeletedAt != nil { // page already deleted return pageUnchanged, nil } m.DeletedAt = now() return pageChanged, nil } func (svc page) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Page) (pageChanges, error) { if !svc.ac.CanDeletePage(ctx, m) { return pageUnchanged, PageErrNotAllowedToUndelete() } if m.DeletedAt == nil { // page not deleted return pageUnchanged, nil } m.DeletedAt = nil return pageChanged, nil } func preparePageConfig(ss *pageSettings, p *types.Page) { if p.ModuleID == 0 { p.Config.Buttons = nil return } p.Config.Buttons = &types.PageButtonConfig{} if ss == nil { return } p.Config.Buttons.New.Enabled = !ss.hideNew p.Config.Buttons.Edit.Enabled = !ss.hideEdit p.Config.Buttons.Submit.Enabled = !ss.hideSubmit p.Config.Buttons.Delete.Enabled = !ss.hideDelete p.Config.Buttons.Clone.Enabled = !ss.hideClone p.Config.Buttons.Back.Enabled = !ss.hideBack } func (svc *page) UpdateConfig(ss *systemTypes.AppSettings) { a := ss.Compose.UI.RecordToolbar svc.pageSettings = &pageSettings{ hideNew: a.HideNew, hideEdit: a.HideEdit, hideSubmit: a.HideSubmit, hideDelete: a.HideDelete, hideClone: a.HideClone, hideBack: a.HideBack, } } func loadPageCombo(ctx context.Context, s interface { store.ComposePages store.ComposeNamespaces }, namespaceID, pageID uint64) (ns *types.Namespace, c *types.Page, err error) { ns, err = loadNamespace(ctx, s, namespaceID) if err != nil { return } c, err = loadPage(ctx, s, namespaceID, pageID) return } func loadPage(ctx context.Context, s store.ComposePages, namespaceID, pageID uint64) (res *types.Page, err error) { if pageID == 0 || namespaceID == 0 { return nil, PageErrInvalidID() } if res, err = store.LookupComposePageByID(ctx, s, pageID); errors.IsNotFound(err) { err = PageErrNotFound() } if err == nil && namespaceID != res.NamespaceID { // Make sure chart belongs to the right namespace return nil, PageErrNotFound() } if err == nil && namespaceID != res.NamespaceID { // Make sure page belongs to the right namespace return nil, PageErrNotFound() } preparePageConfig(nil, res) return } // toLabeledPages converts to []label.LabeledResource // // This function is auto-generated. func toLabeledPages(set []*types.Page) []label.LabeledResource { if len(set) == 0 { return nil } ll := make([]label.LabeledResource, len(set)) for i := range set { ll[i] = set[i] } return ll }