Files
sm-sitemap-plugin/models/legacy.go
Jakub Zych 84cb27a810 feat!: remove the PHP-parity sitemap, tiers, definitions screen and XSL
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
2026-11-04 11:12:51 +01:00

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)
}