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