- POST /_seo/api/v1/notfound with the golem15.seo.notfound_token bearer (constant-time, off when unset), URL and referer caps, a row cap with an hourly warning, throttle bucket seo-notfound - golem15.seo.notfound list by hits with Resolved rows, a preview with the resolved hint, a prompted 301 and a 410 record action, a bulk 410 that refuses a selection with a covered address - every /_seo/api/v1 answer carries X-Robots-Tag: noindex
66 lines
2.2 KiB
Go
66 lines
2.2 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// NotFound is one address of the 404 log on golem15_seo_notfound (D-20):
|
|
// the path (with its query) that answered 404, how often, when first and
|
|
// last, and the referer of the last hit. The frontend reports each 404
|
|
// through the authenticated report endpoint.
|
|
type NotFound struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
URL string `gorm:"column:url"`
|
|
Hits int64 `gorm:"column:hits"`
|
|
FirstSeen *time.Time `gorm:"column:first_seen"`
|
|
LastHit *time.Time `gorm:"column:last_hit"`
|
|
Referer string `gorm:"column:referer"`
|
|
}
|
|
|
|
func (NotFound) TableName() string { return "golem15_seo_notfound" }
|
|
|
|
// MorphName names a 404 log entry in history.
|
|
func (NotFound) MorphName() string { return `Golem15\SEO\Models\NotFound` }
|
|
|
|
// Fillable is empty: entries come from the report endpoint only, and the
|
|
// preview edits nothing.
|
|
func (NotFound) Fillable() []string { return []string{} }
|
|
|
|
// Rules is empty: nothing is edited.
|
|
func (NotFound) Rules() map[string]string { return map[string]string{} }
|
|
|
|
// The 404 log's status filter scope and its values.
|
|
const (
|
|
FilterNotFoundStatus = "FilterNotFoundStatus"
|
|
NotFoundOpen = "open"
|
|
NotFoundResolved = "resolved"
|
|
)
|
|
|
|
// FilterScopes lists the 404 log's filter scopes.
|
|
func (NotFound) FilterScopes() []string { return []string{FilterNotFoundStatus} }
|
|
|
|
// coveredSQL is true for an entry an exact redirect or 410 answers now
|
|
// (the list filter; the Resolved badge also counts patterns and regex).
|
|
const coveredSQL = `EXISTS (SELECT 1 FROM golem15_seo_redirects AS r WHERE r.match = 'exact' AND (r.source = golem15_seo_notfound.url OR (r.ignore_case AND lower(r.source) = lower(golem15_seo_notfound.url))))`
|
|
|
|
// FilterScope narrows the 404 log to open or resolved entries; an unknown
|
|
// value matches nothing.
|
|
func (NotFound) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
if db == nil || name != FilterNotFoundStatus {
|
|
return db
|
|
}
|
|
switch s, _ := value.(string); s {
|
|
case NotFoundOpen:
|
|
return db.Where("NOT " + coveredSQL)
|
|
case NotFoundResolved:
|
|
return db.Where(coveredSQL)
|
|
}
|
|
return db.Where("1 = 0")
|
|
}
|
|
|
|
func init() {
|
|
Register(NotFound{})
|
|
}
|