package classes import ( "fmt" "regexp" "strings" "time" "git.golem15.com/golem15/sm-sitemap-plugin/models" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" "gorm.io/gorm/clause" ) // The phrase keys of the content type refusals (UI-SPEC C26). const ( errSlug = "golem15.sitemap::lang.errors.slug" errSlugTaken = "golem15.sitemap::lang.errors.slug_taken" ) var typeFieldUnsafe = regexp.MustCompile(`[^a-z0-9_]`) // TypeFieldBase is the prefix of a content type's settings fields: // type__, where every character of the key outside [a-z0-9_] becomes // "_". func TypeFieldBase(key string) string { return "type_" + typeFieldUnsafe.ReplaceAllString(strings.ToLower(key), "_") + "_" } // SaveTypeSettings stores submitted content type settings: values holds // TypeFieldBase(key)+"include" (a switch), +"slug" and +"title" (a locale // map) per type; a type or value not sent keeps what is stored. A slug // outside [a-z0-9-] is refused with errors.slug, one that another type (or // the image and News sitemaps) uses with errors.slug_taken; an empty slug // is the type's default. Refusals are by field name; nothing is written // while there are any. func SaveTypeSettings(db *gorm.DB, types []pact.MenuItemType, values map[string]any) (map[string][]string, error) { rows, err := LoadTypeSettings(db) if err != nil { return nil, err } refusals := map[string][]string{} next := make([]models.TypeSetting, 0, len(types)) slugOwner := map[string]string{} for _, typ := range types { base := TypeFieldBase(typ.Key) cfg := EffectiveType(typ, rows) row, ok := rows[typ.Key] if !ok { row = models.TypeSetting{TypeKey: typ.Key, Include: true} } if raw, sent := values[base+"include"]; sent { row.Include = truthy(raw) } slug := cfg.Slug if raw, sent := values[base+"slug"]; sent { slug = strings.TrimSpace(stringOf(raw)) if slug == "" { slug = DefaultSlug(typ.Key) } else if !ValidSlug(slug) { refusals[base+"slug"] = append(refusals[base+"slug"], errSlug) } } if ReservedSlugs[slug] { refusals[base+"slug"] = append(refusals[base+"slug"], errSlugTaken) } else if owner, taken := slugOwner[slug]; taken && owner != typ.Key { refusals[base+"slug"] = append(refusals[base+"slug"], errSlugTaken) } slugOwner[slug] = typ.Key if slug == DefaultSlug(typ.Key) { row.Slug = "" } else { row.Slug = slug } if raw, sent := values[base+"title"]; sent { row.Titles = lagoon.Jsonable[map[string]string]{Data: titlesOf(raw), Valid: true} } next = append(next, row) } if len(refusals) > 0 { return refusals, nil } now := time.Now().UTC() for _, row := range next { row.UpdatedAt = now if !row.Titles.Valid { row.Titles = lagoon.Jsonable[map[string]string]{Data: map[string]string{}, Valid: true} } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "type_key"}}, DoUpdates: clause.AssignmentColumns([]string{"include", "slug", "titles", "updated_at"}), }).Create(&row).Error; err != nil { return nil, err } } return nil, nil } // titlesOf reads a submitted translatable value (a map of locale to text); // blank texts are dropped. func titlesOf(v any) map[string]string { out := map[string]string{} switch m := v.(type) { case map[string]string: for k, s := range m { if strings.TrimSpace(s) != "" { out[k] = strings.TrimSpace(s) } } case map[string]any: for k, s := range m { if text, ok := s.(string); ok && strings.TrimSpace(text) != "" { out[k] = strings.TrimSpace(text) } } } return out } // truthy reads a submitted switch value. func truthy(v any) bool { switch b := v.(type) { case bool: return b case string: return b == "1" || strings.EqualFold(b, "true") || strings.EqualFold(b, "on") case float64: return b != 0 case fmt.Stringer: return b.String() == "1" } return false } // stringOf reads a submitted text value. func stringOf(v any) string { switch s := v.(type) { case string: return s case nil: return "" } return fmt.Sprint(v) }