package models import ( "strings" "time" "gorm.io/gorm" ) // The match kinds of a URL rule (D-02). const ( MatchExact = "exact" MatchPattern = "pattern" ) // The phrase keys of the rule refusals. const ( errRuleURLInvalid = "golem15.seo::lang.rules.url_invalid" errRuleURLTaken = "golem15.seo::lang.rules.url_taken" ) // Rule is a per-URL SEO rule on golem15_seo_rules (D-02): the SEO of an // address with or without a record behind it, which wins over the record's // SEO tab. URL is a path with an optional query ("/", "/blog?tag=*"); Match // is exact or pattern, where * matches any text. Empty values inherit. type Rule struct { ID uint `gorm:"column:id;primaryKey"` URL string `gorm:"column:url"` Match string `gorm:"column:match"` Title string `gorm:"column:title"` Description string `gorm:"column:description"` Canonical string `gorm:"column:canonical"` Robots string `gorm:"column:robots"` OGTitle string `gorm:"column:og_title"` OGDescription string `gorm:"column:og_description"` OGImage string `gorm:"column:og_image"` Note string `gorm:"column:note"` CreatedAt *time.Time `gorm:"column:created_at"` UpdatedAt *time.Time `gorm:"column:updated_at"` // Overrides is the list column of the properties the rule sets, filled // by the rules controller for the list only. Overrides string `gorm:"-:all;column:overrides"` } func (Rule) TableName() string { return "golem15_seo_rules" } // MorphName names a rule in history and attachments. func (Rule) MorphName() string { return `Golem15\SEO\Models\Rule` } // Fillable lists the form's columns. func (Rule) Fillable() []string { return []string{"url", "match", "title", "description", "canonical", "robots", "og_title", "og_description", "og_image", "note"} } // Rules requires the address; its form is checked in Problems. func (Rule) Rules() map[string]string { return map[string]string{"url": "required"} } // RuleProperties are the value columns of a rule with their phrase keys, in // form order. var RuleProperties = []struct{ Column, Label string }{ {"title", "golem15.seo::lang.field.title"}, {"description", "golem15.seo::lang.field.description"}, {"canonical", "golem15.seo::lang.field.canonical"}, {"robots", "golem15.seo::lang.field.robots"}, {"og_title", "golem15.seo::lang.field.og_title"}, {"og_description", "golem15.seo::lang.field.og_description"}, {"og_image", "golem15.seo::lang.field.og_image"}, } // Value returns the rule's value of a property column. func (r Rule) Value(column string) string { switch column { case "title": return r.Title case "description": return r.Description case "canonical": return r.Canonical case "robots": return r.Robots case "og_title": return r.OGTitle case "og_description": return r.OGDescription case "og_image": return r.OGImage } return "" } // NormalizeMatch returns exact unless m is pattern. func NormalizeMatch(m string) string { if strings.TrimSpace(m) == MatchPattern { return MatchPattern } return MatchExact } // Problems refuses an address that does not start with "/" or carries * in // an exact rule, and an address and match another rule already has (db is // the save transaction; nil skips the uniqueness check). func (r Rule) Problems(db *gorm.DB) map[string][]Problem { out := map[string][]Problem{} url := strings.TrimSpace(r.URL) match := NormalizeMatch(r.Match) if !strings.HasPrefix(url, "/") || strings.HasPrefix(url, "//") || strings.ContainsAny(url, " \t\r\n") || (match == MatchExact && strings.Contains(url, "*")) { out["url"] = []Problem{{Key: errRuleURLInvalid}} return out } if db != nil { var count int64 q := db.Model(&Rule{}).Where("url = ? AND match = ?", url, match) if r.ID != 0 { q = q.Where("id <> ?", r.ID) } if err := q.Count(&count).Error; err == nil && count > 0 { out["url"] = []Problem{{Key: errRuleURLTaken}} } } return out } func init() { Register(Rule{}) } // FilterMatch is the rules list's match filter scope. const FilterMatch = "FilterMatch" // FilterScopes lists the rules list's filter scopes. func (Rule) FilterScopes() []string { return []string{FilterMatch} } // FilterScope narrows the rules list to one match kind; an unknown value // matches nothing. func (Rule) FilterScope(name string, db *gorm.DB, value any) *gorm.DB { if db == nil || name != FilterMatch { return db } s, _ := value.(string) if s != MatchExact && s != MatchPattern { return db.Where("1 = 0") } return db.Where("golem15_seo_rules.match = ?", s) }