feat: URL rules and the settings singleton
This commit is contained in:
144
models/rule.go
Normal file
144
models/rule.go
Normal file
@@ -0,0 +1,144 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user