- generator in Definition::addItemToSet order: 50000 URL cap, lastmod from the record or the updates manifest (regular file, 5 MiB cap, fail safe), BeforeAddItem (skip) and AddItem (extra XML) festival events, tier or item priority, per-alternate expansion of list type children - priority tiers from golem15.sitemap.priority_tiers, compiled at boot, defaults equal to the PHP getPriorityForUrl rules - frontend_url rewrite of loc, xhtml:link href and the stylesheet href; GET /sitemap.xsl serves the PHP stylesheet byte for byte - golem15_sitemap_settings singleton and Settings model - sitemap:import (PHP ImportCommand) and sitemap:import-winter with the read-only Winter export SQL - urlto and full goldens generated from PHP; TestSitemapImportSmoke
256 lines
6.4 KiB
Go
256 lines
6.4 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ItemTypeURL is the built-in item type of an explicit URL. Every other
|
|
// item type is a menu item type of the framework's registry (signpost).
|
|
const ItemTypeURL = "url"
|
|
|
|
// DefinitionItem is one sitemap definition item, the PHP
|
|
// Golem15\Sitemap\Classes\DefinitionItem. Type is "url" or a registry menu
|
|
// item type key; URL is the address of a url item; Reference is the record a
|
|
// registry item points at; URLPattern overrides the type's URL pattern;
|
|
// Nesting asks a registry item for its sub-items; Priority and Changefreq
|
|
// are written to the sitemap as they are stored.
|
|
//
|
|
// Text fields are pointers so a stored JSON null stays null on the next
|
|
// save. Decoding is tolerant of what PHP stored: numbers and booleans are
|
|
// accepted for text fields, strings and numbers for Nesting, and unknown keys
|
|
// (cmsPage, code, exists, fillable and any other) are ignored. Encoding
|
|
// writes the PHP toArray key set: changefreq, priority, nesting, type, url,
|
|
// reference, cmsPage (always null, CMS pages are not supported) and
|
|
// urlPattern.
|
|
type DefinitionItem struct {
|
|
Type string
|
|
URL *string
|
|
Reference *string
|
|
URLPattern *string
|
|
Nesting bool
|
|
Priority *string
|
|
Changefreq *string
|
|
}
|
|
|
|
// Text returns the value of a text field, "" for null.
|
|
func Text(p *string) string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
return *p
|
|
}
|
|
|
|
// UnmarshalJSON decodes one stored item tolerantly. A missing type is the
|
|
// PHP default "url"; a null type is "".
|
|
func (d *DefinitionItem) UnmarshalJSON(raw []byte) error {
|
|
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
// PHP initFromArray kept a default item for a null entry.
|
|
*d = DefinitionItem{Type: ItemTypeURL}
|
|
return nil
|
|
}
|
|
var fields map[string]json.RawMessage
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.UseNumber()
|
|
if err := dec.Decode(&fields); err != nil {
|
|
return fmt.Errorf("sitemap definition item: %w", err)
|
|
}
|
|
item := DefinitionItem{Type: ItemTypeURL}
|
|
if v, ok := fields["type"]; ok {
|
|
s, err := DecodeText(v)
|
|
if err != nil {
|
|
return fmt.Errorf("sitemap definition item type: %w", err)
|
|
}
|
|
item.Type = Text(s)
|
|
}
|
|
for key, dest := range map[string]**string{
|
|
"url": &item.URL,
|
|
"reference": &item.Reference,
|
|
"urlPattern": &item.URLPattern,
|
|
"priority": &item.Priority,
|
|
"changefreq": &item.Changefreq,
|
|
} {
|
|
v, ok := fields[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
s, err := DecodeText(v)
|
|
if err != nil {
|
|
return fmt.Errorf("sitemap definition item %s: %w", key, err)
|
|
}
|
|
*dest = s
|
|
}
|
|
if v, ok := fields["nesting"]; ok {
|
|
b, err := decodeTruthy(v)
|
|
if err != nil {
|
|
return fmt.Errorf("sitemap definition item nesting: %w", err)
|
|
}
|
|
item.Nesting = b
|
|
}
|
|
*d = item
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON writes the PHP toArray key set in its order.
|
|
func (d DefinitionItem) MarshalJSON() ([]byte, error) {
|
|
var b bytes.Buffer
|
|
b.WriteByte('{')
|
|
write := func(key string, v any, last bool) error {
|
|
kb, _ := json.Marshal(key)
|
|
b.Write(kb)
|
|
b.WriteByte(':')
|
|
vb, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Write(vb)
|
|
if !last {
|
|
b.WriteByte(',')
|
|
}
|
|
return nil
|
|
}
|
|
for _, f := range []struct {
|
|
key string
|
|
v any
|
|
}{
|
|
{"changefreq", d.Changefreq},
|
|
{"priority", d.Priority},
|
|
{"nesting", d.Nesting},
|
|
{"type", d.Type},
|
|
{"url", d.URL},
|
|
{"reference", d.Reference},
|
|
{"cmsPage", nil},
|
|
} {
|
|
if err := write(f.key, f.v, false); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := write("urlPattern", d.URLPattern, true); err != nil {
|
|
return nil, err
|
|
}
|
|
b.WriteByte('}')
|
|
return b.Bytes(), nil
|
|
}
|
|
|
|
// DefinitionItems is the stored item list of a definition. It decodes a
|
|
// JSON array, and a JSON object (a PHP array whose keys were not sequential)
|
|
// in key order.
|
|
type DefinitionItems []DefinitionItem
|
|
|
|
// UnmarshalJSON accepts a JSON array or object of items; null is no items.
|
|
func (l *DefinitionItems) UnmarshalJSON(raw []byte) error {
|
|
raw = bytes.TrimSpace(raw)
|
|
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
|
*l = nil
|
|
return nil
|
|
}
|
|
if raw[0] == '[' {
|
|
var items []DefinitionItem
|
|
if err := json.Unmarshal(raw, &items); err != nil {
|
|
return err
|
|
}
|
|
*l = items
|
|
return nil
|
|
}
|
|
if raw[0] != '{' {
|
|
return fmt.Errorf("sitemap definition data must be a list of items")
|
|
}
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
if _, err := dec.Token(); err != nil {
|
|
return err
|
|
}
|
|
var items []DefinitionItem
|
|
for dec.More() {
|
|
if _, err := dec.Token(); err != nil {
|
|
return err
|
|
}
|
|
var item DefinitionItem
|
|
if err := dec.Decode(&item); err != nil {
|
|
return err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
*l = items
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON writes a JSON array; no items is [].
|
|
func (l DefinitionItems) MarshalJSON() ([]byte, error) {
|
|
if l == nil {
|
|
return []byte("[]"), nil
|
|
}
|
|
return json.Marshal([]DefinitionItem(l))
|
|
}
|
|
|
|
// DecodeText decodes a stored text value: a string as it is, a number in
|
|
// its PHP string form, a boolean as PHP casts it ("1" or ""), null as nil.
|
|
func DecodeText(raw json.RawMessage) (*string, error) {
|
|
var v any
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.UseNumber()
|
|
if err := dec.Decode(&v); err != nil {
|
|
return nil, err
|
|
}
|
|
var s string
|
|
switch t := v.(type) {
|
|
case nil:
|
|
return nil, nil
|
|
case string:
|
|
s = t
|
|
case json.Number:
|
|
s = phpNumberString(t)
|
|
case bool:
|
|
if t {
|
|
s = "1"
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("must be text, got %T", v)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// decodeTruthy decodes a stored flag with PHP truthiness: false, 0, "",
|
|
// "0" and null are false.
|
|
func decodeTruthy(raw json.RawMessage) (bool, error) {
|
|
var v any
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.UseNumber()
|
|
if err := dec.Decode(&v); err != nil {
|
|
return false, err
|
|
}
|
|
switch t := v.(type) {
|
|
case nil:
|
|
return false, nil
|
|
case bool:
|
|
return t, nil
|
|
case string:
|
|
return t != "" && t != "0", nil
|
|
case json.Number:
|
|
f, err := t.Float64()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return f != 0, nil
|
|
}
|
|
return false, fmt.Errorf("must be a flag, got %T", v)
|
|
}
|
|
|
|
// phpNumberString is PHP's string cast of a json_decode'd number: an
|
|
// integer as written, a float in its shortest round-trip form (1.0 is "1").
|
|
func phpNumberString(n json.Number) string {
|
|
text := n.String()
|
|
if !strings.ContainsAny(text, ".eE") {
|
|
if i, err := strconv.ParseInt(text, 10, 64); err == nil {
|
|
return strconv.FormatInt(i, 10)
|
|
}
|
|
}
|
|
f, err := n.Float64()
|
|
if err != nil {
|
|
return text
|
|
}
|
|
return strconv.FormatFloat(f, 'f', -1, 64)
|
|
}
|