// 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..url), a URL pattern that is not a path // (signpost.ValidItemPattern, data..urlPattern) and a missing reference // for a registry type that takes one (data..reference). The PHP plugin // checked none of them: a javascript: URL would reach , 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{} )