Rule.Problems had no length check, so a long address reached the (url, match) UNIQUE btree index and failed with a 500. It now refuses an address over models.MaxRuleURL with rules.url_invalid, like redirect sources.
151 lines
4.8 KiB
Go
151 lines
4.8 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
|
|
}
|
|
|
|
// MaxRuleURL caps a URL rule's address in bytes, like MaxRedirectURL: the
|
|
// (url, match) UNIQUE btree index refuses rows over about 2.7 KB with a
|
|
// database error (WR-23).
|
|
const MaxRuleURL = 2048
|
|
|
|
// Problems refuses an address that does not start with "/", is longer than
|
|
// MaxRuleURL 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 len(url) > MaxRuleURL || !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)
|
|
}
|