Only the admin seoimage field refused values that are not a path or an http(s) URL; SetURLSEO, SaveRule and SaveSettings stored javascript:, data: or protocol-relative values, which /resolve then served as og:image. All three now refuse them with image.url_invalid.
123 lines
3.4 KiB
Go
123 lines
3.4 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ErrNoRecord is returned for an id that names no row.
|
|
var ErrNoRecord = errors.New("golem15.seo: record not found")
|
|
|
|
// LoadRule returns rule id; ErrNoRecord when there is none.
|
|
func LoadRule(db *gorm.DB, id uint) (models.Rule, error) {
|
|
var rows []models.Rule
|
|
if err := db.Where("id = ?", id).Limit(1).Find(&rows).Error; err != nil {
|
|
return models.Rule{}, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return models.Rule{}, ErrNoRecord
|
|
}
|
|
return rows[0], nil
|
|
}
|
|
|
|
// SaveRule validates and stores r inside tx and records it in history: the
|
|
// address is trimmed and the match normalised, robots values are brought
|
|
// to the C5 grammar, and a canonical and the social image must be a path
|
|
// or an http(s) URL. Field problems are refused with a *ProblemsError.
|
|
func (s *Service) SaveRule(ctx context.Context, tx *gorm.DB, r *models.Rule) error {
|
|
if r == nil {
|
|
return errors.New("golem15.seo: no rule")
|
|
}
|
|
r.URL = strings.TrimSpace(r.URL)
|
|
r.Match = models.NormalizeMatch(r.Match)
|
|
problems := r.Problems(tx)
|
|
if v := strings.TrimSpace(r.Robots); v != "" {
|
|
normal, err := NormalizeRobots(v)
|
|
if err != nil {
|
|
problems["robots"] = append(problems["robots"], models.Problem{Key: "golem15.seo::lang.robots.invalid"})
|
|
}
|
|
r.Robots = normal
|
|
}
|
|
if v := strings.TrimSpace(r.Canonical); v != "" && !ValidURLRef(v) {
|
|
problems["canonical"] = append(problems["canonical"], models.Problem{Key: "golem15.seo::lang.api.canonical_invalid"})
|
|
}
|
|
r.OGImage = strings.TrimSpace(r.OGImage)
|
|
if !ValidImageURL(r.OGImage) {
|
|
problems["og_image"] = append(problems["og_image"], models.Problem{Key: errImageURL})
|
|
}
|
|
if len(problems) > 0 {
|
|
return &ProblemsError{Problems: problems}
|
|
}
|
|
now := time.Now().UTC()
|
|
var before map[string]any
|
|
if r.ID == 0 {
|
|
r.CreatedAt, r.UpdatedAt = &now, &now
|
|
if err := tx.Create(r).Error; err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
old, err := LoadRule(tx, r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
before = RuleValues(old)
|
|
r.CreatedAt, r.UpdatedAt = old.CreatedAt, &now
|
|
if err := tx.Save(r).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return s.Record(ctx, tx, Change{
|
|
Kind: models.SubjectRule,
|
|
SubjectID: strconv.FormatUint(uint64(r.ID), 10),
|
|
What: s.Phrase("golem15.seo::lang.history.what_rule", map[string]string{"url": r.URL}),
|
|
Before: before,
|
|
After: RuleValues(*r),
|
|
})
|
|
}
|
|
|
|
// DeleteRule deletes r inside tx and records it in history.
|
|
func (s *Service) DeleteRule(ctx context.Context, tx *gorm.DB, r models.Rule) error {
|
|
if err := tx.Delete(&models.Rule{}, r.ID).Error; err != nil {
|
|
return err
|
|
}
|
|
return s.Record(ctx, tx, Change{
|
|
Kind: models.SubjectRule,
|
|
SubjectID: strconv.FormatUint(uint64(r.ID), 10),
|
|
What: s.Phrase("golem15.seo::lang.history.what_rule", map[string]string{"url": r.URL}),
|
|
Before: RuleValues(r),
|
|
})
|
|
}
|
|
|
|
// SetRuleValue writes one value column of a rule; an unknown column is
|
|
// ignored.
|
|
func SetRuleValue(r *models.Rule, column, value string) {
|
|
switch column {
|
|
case "url":
|
|
r.URL = value
|
|
case "match":
|
|
r.Match = value
|
|
case "note":
|
|
r.Note = value
|
|
case "title":
|
|
r.Title = value
|
|
case "description":
|
|
r.Description = value
|
|
case "canonical":
|
|
r.Canonical = value
|
|
case "robots":
|
|
r.Robots = value
|
|
case "og_title":
|
|
r.OGTitle = value
|
|
case "og_description":
|
|
r.OGDescription = value
|
|
case "og_image":
|
|
r.OGImage = value
|
|
}
|
|
}
|