BREAKING CHANGE: /sitemap.xsl is gone (404), the definitions screen and the Phase 17 settings tile are replaced by the settings singleton, and priority, change frequency, priority tiers and the update manifest are no longer written or read. The config keys golem15.sitemap.cache_seconds and golem15.sitemap.priority_tiers are removed; site_url, lastmod_ignore and rebuild_delay_seconds are new. - sitemap:import and sitemap:import-winter write the additional addresses (and site_url, default_locale) instead of the definition - the golem15_sitemap_definitions table stays in the database, unread
178 lines
4.6 KiB
Go
178 lines
4.6 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Text returns *p, "" for nil.
|
|
func Text(p *string) string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
return *p
|
|
}
|
|
|
|
// LegacyItemURL is the type of a Phase 17 (and WinterCMS) sitemap
|
|
// definition item that is an explicit address.
|
|
const LegacyItemURL = "url"
|
|
|
|
// LegacyItem is one item of a Phase 17 or WinterCMS sitemap definition, as
|
|
// golem15_sitemap_definitions.data and the WinterCMS export store it. Only
|
|
// the fields the per-type format still reads are kept: the type (a missing
|
|
// type is url, as in PHP), the address of a url item and the reference of a
|
|
// registry item.
|
|
type LegacyItem struct {
|
|
Type string
|
|
URL string
|
|
Reference string
|
|
CMSPage string
|
|
}
|
|
|
|
// LegacyEntries splits a JSON array, or a JSON object in key order (a PHP
|
|
// array with keys), into its values with their PHP labels (index or key).
|
|
// ok is false for any other value.
|
|
func LegacyEntries(raw []byte) ([]json.RawMessage, []string, bool) {
|
|
raw = bytes.TrimSpace(raw)
|
|
if len(raw) == 0 {
|
|
return nil, nil, false
|
|
}
|
|
switch raw[0] {
|
|
case '[':
|
|
var list []json.RawMessage
|
|
if err := json.Unmarshal(raw, &list); err != nil {
|
|
return nil, nil, false
|
|
}
|
|
labels := make([]string, len(list))
|
|
for i := range list {
|
|
labels[i] = strconv.Itoa(i)
|
|
}
|
|
return list, labels, true
|
|
case '{':
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
if _, err := dec.Token(); err != nil {
|
|
return nil, nil, false
|
|
}
|
|
var list []json.RawMessage
|
|
var labels []string
|
|
for dec.More() {
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
return nil, nil, false
|
|
}
|
|
key, _ := tok.(string)
|
|
var v json.RawMessage
|
|
if err := dec.Decode(&v); err != nil {
|
|
return nil, nil, false
|
|
}
|
|
list = append(list, v)
|
|
labels = append(labels, key)
|
|
}
|
|
return list, labels, true
|
|
}
|
|
return nil, nil, false
|
|
}
|
|
|
|
// ParseLegacyItem decodes one stored item tolerantly: text fields may be
|
|
// strings, numbers, booleans or null; a missing type is url; unknown keys
|
|
// are ignored.
|
|
func ParseLegacyItem(raw json.RawMessage) (LegacyItem, error) {
|
|
var fields map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &fields); err != nil {
|
|
return LegacyItem{}, fmt.Errorf("an item must be an object: %w", err)
|
|
}
|
|
item := LegacyItem{Type: LegacyItemURL}
|
|
for key, dest := range map[string]*string{"type": &item.Type, "url": &item.URL, "reference": &item.Reference, "cmsPage": &item.CMSPage} {
|
|
v, ok := fields[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
text, err := DecodeText(v)
|
|
if err != nil {
|
|
return LegacyItem{}, fmt.Errorf("%s %w", key, err)
|
|
}
|
|
*dest = strings.TrimSpace(Text(text))
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
// ParseLegacyItems decodes the data of a definition: a JSON array or a PHP
|
|
// keyed object of items, itself possibly stored as a JSON string. Empty or
|
|
// null data has no items.
|
|
func ParseLegacyItems(data []byte) ([]LegacyItem, []string, error) {
|
|
data = LegacyJSON(data)
|
|
if len(data) == 0 || bytes.Equal(data, []byte("null")) {
|
|
return nil, nil, nil
|
|
}
|
|
raws, labels, ok := LegacyEntries(data)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("the definition data is not a list of items")
|
|
}
|
|
items := make([]LegacyItem, 0, len(raws))
|
|
for i, raw := range raws {
|
|
item, err := ParseLegacyItem(raw)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("definition item %s: %w", labels[i], err)
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, labels, nil
|
|
}
|
|
|
|
// LegacyJSON is a JSON value that may be held as text (a TEXT column
|
|
// selected as it is) or as JSON: a string is decoded once more.
|
|
func LegacyJSON(raw []byte) []byte {
|
|
raw = bytes.TrimSpace(raw)
|
|
if len(raw) > 0 && raw[0] == '"' {
|
|
var text string
|
|
if json.Unmarshal(raw, &text) == nil {
|
|
return []byte(strings.TrimSpace(text))
|
|
}
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// DecodeText decodes a stored text value with PHP's string conversion:
|
|
// null is nil, a number its PHP text, true "1" and false "".
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|