feat!: remove the PHP-parity sitemap, tiers, definitions screen and XSL
BREAKING CHANGE: /sitemap.xsl is gone (404), the definitions screen and the Phase 17 settings tile are replaced by the settings singleton, and priority, change frequency, priority tiers and the update manifest are no longer written or read. The config keys golem15.sitemap.cache_seconds and golem15.sitemap.priority_tiers are removed; site_url, lastmod_ignore and rebuild_delay_seconds are new. - sitemap:import and sitemap:import-winter write the additional addresses (and site_url, default_locale) instead of the definition - the golem15_sitemap_definitions table stays in the database, unread
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Package controllers holds the sitemap plugin's admin controllers: the
|
||||
// settings singleton.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
@@ -10,7 +12,6 @@ import (
|
||||
// a database handle or registry of its own.
|
||||
func AdminControllers(app func() *backpack.App) []pact.AdminController {
|
||||
return []pact.AdminController{
|
||||
definitionsAdminController{app: app},
|
||||
settingsAdminController{app: app},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
// 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{}
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
# ===================================
|
||||
# Form Behavior Config
|
||||
# ===================================
|
||||
|
||||
# Record name
|
||||
name: golem15.sitemap::lang.definition.title
|
||||
|
||||
# Model Form Field configuration
|
||||
form: ~/plugins/golem15/sitemap/models/definition/fields.yaml
|
||||
|
||||
# Model Class name
|
||||
modelClass: Golem15\Sitemap\Models\Definition
|
||||
|
||||
# Default redirect location
|
||||
defaultRedirect: system/settings
|
||||
|
||||
# Update page
|
||||
update:
|
||||
redirect: golem15/sitemap/definitions
|
||||
redirectClose: system/settings
|
||||
|
||||
messages:
|
||||
update: golem15.sitemap::lang.definition.title
|
||||
saved: backend::lang.messages.form.saved
|
||||
@@ -1,39 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
)
|
||||
|
||||
func ptr(s string) *string { return &s }
|
||||
|
||||
// TestCheckItems pins the CR-01 save check: per row, a url outside
|
||||
// classes.AllowedItemURL and a URL pattern that is not a path are refused
|
||||
// under data.<i>.url and data.<i>.urlPattern.
|
||||
func TestCheckItems(t *testing.T) {
|
||||
c := definitionsAdminController{}
|
||||
def := &models.Definition{}
|
||||
def.SetItems([]models.DefinitionItem{
|
||||
{Type: models.ItemTypeURL, URL: ptr("/about")},
|
||||
{Type: models.ItemTypeURL, URL: ptr("javascript://%0aalert(1)%0a//x")},
|
||||
{Type: "acme-post", URLPattern: ptr("//evil.test/:slug")},
|
||||
{Type: "acme-post", URLPattern: ptr("/blog/:slug")},
|
||||
})
|
||||
err := c.checkItems(def)
|
||||
var verr *cabana.ValidationError
|
||||
if !errors.As(err, &verr) {
|
||||
t.Fatalf("checkItems = %v, want a validation error", err)
|
||||
}
|
||||
if len(verr.Details) != 2 || verr.Details["data.1.url"] == nil || verr.Details["data.2.urlPattern"] == nil {
|
||||
t.Fatalf("details = %v", verr.Details)
|
||||
}
|
||||
|
||||
ok := &models.Definition{}
|
||||
ok.SetItems([]models.DefinitionItem{{Type: models.ItemTypeURL, URL: ptr("https://example.test/")}})
|
||||
if err := c.checkItems(ok); err != nil {
|
||||
t.Fatalf("a valid definition was refused: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,15 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SettingsControllerID is the cabana controller id of the sitemap settings
|
||||
// singleton (UI-SPEC C26), linked from the sitemap settings tile.
|
||||
const SettingsControllerID = "golem15.sitemap.settings"
|
||||
const (
|
||||
// SettingsControllerID is the cabana controller id of the sitemap
|
||||
// settings singleton (UI-SPEC C26), linked from the sitemap settings
|
||||
// tile.
|
||||
SettingsControllerID = "golem15.sitemap.settings"
|
||||
// PermissionAccessDefinitions guards the sitemap settings. The code is
|
||||
// the Phase 17 (and WinterCMS) one, so existing roles keep their access.
|
||||
PermissionAccessDefinitions = "golem15.sitemap.access_definitions"
|
||||
)
|
||||
|
||||
// The reasons the files partial counts as "left out", in display order
|
||||
// (P33-11). A deleted or unpublished address is a hole, not left out.
|
||||
|
||||
Reference in New Issue
Block a user