A registry row saved without a reference resolved to nothing and was left out of /sitemap.xml without a word. The definitions form now refuses it on the row (data.<i>.reference, item.reference_required). The admin smoke test also covers the CR-01 row refusals.
163 lines
5.8 KiB
Go
163 lines
5.8 KiB
Go
// Package controllers holds the sitemap plugin's admin controllers.
|
|
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
|
"git.golem15.com/golem15/sm-sitemap-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/signpost"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
// DefinitionsControllerID is the cabana controller id of the sitemap
|
|
// definition form.
|
|
DefinitionsControllerID = "golem15.sitemap.definitions"
|
|
// PermissionAccessDefinitions is Definitions::$requiredPermissions.
|
|
PermissionAccessDefinitions = "golem15.sitemap.access_definitions"
|
|
)
|
|
|
|
// typeURLLabel is the label of the built-in url item type.
|
|
const typeURLLabel = "golem15.sitemap::lang.item.url"
|
|
|
|
// The phrase keys of the item row refusals.
|
|
const (
|
|
errURLScheme = "golem15.sitemap::lang.errors.url_scheme"
|
|
errURLPattern = "golem15.sitemap::lang.errors.url_pattern"
|
|
errReference = "golem15.sitemap::lang.item.reference_required"
|
|
)
|
|
|
|
// definitionsAdminController edits the one sitemap definition of the
|
|
// application (theme default): a singleton form whose items are a repeater
|
|
// with a type dropdown (url plus the registry types) and a reference
|
|
// dropdown that depends on it.
|
|
type definitionsAdminController struct {
|
|
app func() *backpack.App
|
|
}
|
|
|
|
func (definitionsAdminController) ID() string { return DefinitionsControllerID }
|
|
|
|
func (definitionsAdminController) ModelName() string { return `Golem15\Sitemap\Models\Definition` }
|
|
|
|
func (definitionsAdminController) ConfigDir() string { return "controllers/definitions" }
|
|
|
|
func (definitionsAdminController) RequiredPermissions() []string {
|
|
return []string{PermissionAccessDefinitions}
|
|
}
|
|
|
|
func (definitionsAdminController) NewRecord() any { return &models.Definition{} }
|
|
|
|
// The singleton record is the definition with the fixed theme key, created
|
|
// with no items when there is none (PHP index(): firstOrCreate, then the
|
|
// update form).
|
|
func (definitionsAdminController) SingletonRecord(_ context.Context, db *gorm.DB) (any, error) {
|
|
if db == nil {
|
|
return nil, errors.New("golem15.sitemap: database is not configured")
|
|
}
|
|
var defs []models.Definition
|
|
if err := db.Where("theme = ?", models.DefinitionTheme).Order("id ASC").Limit(1).Find(&defs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if len(defs) > 0 {
|
|
return &defs[0], nil
|
|
}
|
|
theme := models.DefinitionTheme
|
|
def := &models.Definition{Theme: &theme}
|
|
def.SetItems(nil)
|
|
if err := db.Create(def).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return def, nil
|
|
}
|
|
|
|
func (c definitionsAdminController) registry() *signpost.Registry {
|
|
if c.app == nil {
|
|
return nil
|
|
}
|
|
app := c.app()
|
|
if app == nil {
|
|
return nil
|
|
}
|
|
reg, _ := app.Lookup[*signpost.Registry]()
|
|
return reg
|
|
}
|
|
|
|
// DropdownOptions serves the item type dropdown: url, then every registry
|
|
// type in registry order with its label (PHP getTypeOptions).
|
|
func (c definitionsAdminController) DropdownOptions(string) []pact.Option {
|
|
out := []pact.Option{{Value: models.ItemTypeURL, Label: typeURLLabel}}
|
|
for _, t := range c.registry().Types() {
|
|
out = append(out, pact.Option{Value: t.Key, Label: t.Label})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DependentOptions serves the reference dropdown of an item row: the
|
|
// references of the row's type through signpost.ReferenceOptions; url and a
|
|
// type without references answer Applies false.
|
|
func (c definitionsAdminController) DependentOptions(ctx context.Context, _ string, values map[string]any) (pact.DependentOptionSet, error) {
|
|
typ, _ := values["type"].(string)
|
|
return signpost.ReferenceOptions(ctx, c.registry(), typ)
|
|
}
|
|
|
|
// FormBeforeCreate checks the item rows (see checkItems).
|
|
func (c definitionsAdminController) FormBeforeCreate(_ context.Context, model any) error {
|
|
return c.checkItems(model)
|
|
}
|
|
|
|
// FormBeforeUpdate checks the item rows (see checkItems).
|
|
func (c definitionsAdminController) FormBeforeUpdate(_ context.Context, model any) error {
|
|
return c.checkItems(model)
|
|
}
|
|
|
|
// checkItems refuses, per row, a url outside classes.AllowedItemURL
|
|
// (data.<i>.url), a URL pattern that is not a path
|
|
// (signpost.ValidItemPattern, data.<i>.urlPattern) and a missing reference
|
|
// for a registry type that takes one (data.<i>.reference). The PHP plugin
|
|
// checked none of them: a javascript: URL would reach <loc>, which
|
|
// sitemap.xsl renders as a link, and a row without its reference resolves to
|
|
// nothing and is left out of the sitemap without a word.
|
|
func (c definitionsAdminController) checkItems(model any) error {
|
|
def, ok := model.(*models.Definition)
|
|
if !ok || def == nil {
|
|
return nil
|
|
}
|
|
reg := c.registry()
|
|
details := map[string]any{}
|
|
for i, item := range def.Items() {
|
|
if item.Type != models.ItemTypeURL {
|
|
if t, known := reg.Type(item.Type); known && t.References != nil && strings.TrimSpace(models.Text(item.Reference)) == "" {
|
|
details[fmt.Sprintf("data.%d.reference", i)] = []string{errReference}
|
|
}
|
|
}
|
|
if !classes.AllowedItemURL(models.Text(item.URL)) {
|
|
details[fmt.Sprintf("data.%d.url", i)] = []string{errURLScheme}
|
|
}
|
|
if p := models.Text(item.URLPattern); p != "" && !signpost.ValidItemPattern(p) {
|
|
details[fmt.Sprintf("data.%d.urlPattern", i)] = []string{errURLPattern}
|
|
}
|
|
}
|
|
if len(details) > 0 {
|
|
return &cabana.ValidationError{Details: details}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
_ pact.AdminController = definitionsAdminController{}
|
|
_ pact.AdminPermissioned = definitionsAdminController{}
|
|
_ pact.AdminRecordSource = definitionsAdminController{}
|
|
_ pact.AdminSingleton = definitionsAdminController{}
|
|
_ pact.DropdownOptionsProvider = definitionsAdminController{}
|
|
_ pact.DependentOptionsProvider = definitionsAdminController{}
|
|
_ pact.FormBeforeCreate = definitionsAdminController{}
|
|
_ pact.FormBeforeUpdate = definitionsAdminController{}
|
|
)
|