Files
sm-seo-plugin/models/rule.go

145 lines
4.5 KiB
Go

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)
}