Files
summercms/modules/cabana/action_extensions.go
Jakub Zych 7340241f22 feat(19.6.1-01): Administrators screen and cross-controller record actions
- pact.HasAdminActionExtensions / AdminActionExtension: plugins add record
  actions to any admin controller by ID, merged at boot before permission
  validation (unknown target, no preview, duplicate name stop the boot)
- internal summercms.cabana pseudo-plugin, appended only when some plugin
  registers admin controllers: Administrators list and preview, settings
  link under System, backend.manage_users and backend.manage_roles with no
  role grants
- backend::lang user, user.role and permissions copy (en, pl)
- smoke tests: compile-time seam and settings, Postgres list/preview/action
2026-10-22 10:59:23 +02:00

98 lines
3.7 KiB
Go

package cabana
import (
"fmt"
"strings"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
)
// compileActionExtensions merges the record actions plugins add to admin
// controllers through pact.HasAdminActionExtensions (D-14) into the compiled
// controllers of reg, in plugin slice (activation) order. A contributed
// action is appended to the target form's offered record actions and to its
// RecordActions map, so the show response, the record action route and the
// permission validation of compileContributions treat it like the owner's.
// It must run after every controller is compiled and before
// compileContributions, which validates the actions' permissions. Every
// failure stops boot: an unknown controller ID, a target without a form
// preview block (record actions are offered on the preview screen), a name
// that is not an identifier, a reserved name, a name the target already
// registers, an action without Run or without a label.
func compileActionExtensions(reg *Registry, plugins []party.Plugin) error {
if reg == nil {
return fmt.Errorf("cabana: registry is nil")
}
for _, plugin := range plugins {
if plugin == nil {
continue
}
src, ok := plugin.(pact.HasAdminActionExtensions)
if !ok || src == nil {
continue
}
pluginID := plugin.ID()
for _, ext := range src.AdminActionExtensions() {
if len(ext.RecordActions) == 0 {
continue
}
cc, ok := reg.byID[ext.Controller]
if !ok || cc == nil {
return fmt.Errorf("cabana: plugin %s adds record actions to unknown admin controller %q", pluginID, ext.Controller)
}
if cc.Form == nil || cc.Form.preview == nil {
return fmt.Errorf("cabana: plugin %s adds record actions to admin controller %s, which has no preview block (record actions are offered on the preview screen)", pluginID, ext.Controller)
}
for _, action := range ext.RecordActions {
if err := checkContributedAction(cc, pluginID, ext.Controller, action); err != nil {
return err
}
// A new slice and map: the compiled backing array may be
// shared with another schema value.
names := make([]string, 0, len(cc.Form.recordActions)+1)
names = append(names, cc.Form.recordActions...)
cc.Form.recordActions = append(names, action.Name)
actions := make(map[string]pact.AdminRecordAction, len(cc.RecordActions)+1)
for name, registered := range cc.RecordActions {
actions[name] = registered
}
actions[action.Name] = action
cc.RecordActions = actions
if cc.contributed == nil {
cc.contributed = map[string]string{}
}
cc.contributed[action.Name] = pluginID
}
}
}
return nil
}
// checkContributedAction applies compileRecordActions' checks to one
// contributed record action, plus a label and a name the target does not
// register yet (the owner's or an earlier contribution).
func checkContributedAction(cc *CompiledController, pluginID, controller string, action pact.AdminRecordAction) error {
prefix := fmt.Sprintf("cabana: plugin %s adds record action %q to admin controller %s", pluginID, action.Name, controller)
if !identifier(action.Name) {
return fmt.Errorf("%s: the name is not an identifier", prefix)
}
if builtinToolbarActions[action.Name] {
return fmt.Errorf("%s: the name is reserved (create, delete)", prefix)
}
if action.Run == nil {
return fmt.Errorf("%s: it has no Run function", prefix)
}
if strings.TrimSpace(action.Label) == "" {
return fmt.Errorf("%s: it needs a label", prefix)
}
if _, taken := cc.RecordActions[action.Name]; taken {
owner := cc.PluginID
if contributor, ok := cc.contributed[action.Name]; ok {
owner = contributor
}
return fmt.Errorf("%s: plugin %s already registers a record action with that name", prefix, owner)
}
return nil
}