310 lines
6.7 KiB
Go
310 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"html"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// The printer's admin pages are small, tolerant HTML (XHTML 1.0 with unclosed inputs). Only what the module
|
|
// reads is taken out of them: the title, the links, and the forms with their fields.
|
|
|
|
// Link is an anchor of a page.
|
|
type Link struct {
|
|
Href string `json:"href"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// Field is a named control of a form. A password field's value is never kept (ADR 0467 clause 4).
|
|
type Field struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
Label string `json:"label"`
|
|
Options []string `json:"options,omitempty"`
|
|
id string
|
|
}
|
|
|
|
// Form is a form of a page with its named fields.
|
|
type Form struct {
|
|
Action string `json:"action"`
|
|
Method string `json:"method"`
|
|
Fields []Field `json:"fields"`
|
|
}
|
|
|
|
// Page is what a page of the admin site holds.
|
|
type Page struct {
|
|
Title string `json:"title"`
|
|
Links []Link `json:"links"`
|
|
Forms []Form `json:"forms"`
|
|
}
|
|
|
|
var attrRE = regexp.MustCompile(`([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?`)
|
|
|
|
// attrs reads a tag's attributes, names in lower case and values unescaped.
|
|
func attrs(s string) map[string]string {
|
|
out := map[string]string{}
|
|
for _, m := range attrRE.FindAllStringSubmatch(s, -1) {
|
|
v := m[2] + m[3] + m[4]
|
|
out[strings.ToLower(m[1])] = html.UnescapeString(v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func squash(s string) string { return strings.Join(strings.Fields(html.UnescapeString(s)), " ") }
|
|
|
|
// latin1 turns a body that is not UTF-8 into UTF-8, reading each byte as the character of the same number:
|
|
// the printer's pages say iso-8859-1.
|
|
func latin1(b []byte) string {
|
|
if utf8.Valid(b) {
|
|
return string(b)
|
|
}
|
|
r := make([]rune, len(b))
|
|
for i, c := range b {
|
|
r[i] = rune(c)
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
// tagEnd is the index of the '>' that closes the tag starting at s[0], skipping quoted values, or -1.
|
|
func tagEnd(s string) int {
|
|
var q byte
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
switch {
|
|
case q != 0:
|
|
if c == q {
|
|
q = 0
|
|
}
|
|
case c == '"' || c == '\'':
|
|
q = c
|
|
case c == '>':
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// ParsePage reads a page. Unknown markup is skipped, and what is not closed is closed at the end.
|
|
func ParsePage(body []byte) Page {
|
|
s := latin1(body)
|
|
var p Page
|
|
var (
|
|
inTitle bool
|
|
title strings.Builder
|
|
link *Link
|
|
formOpen bool
|
|
form Form
|
|
labelFor string
|
|
inLabel bool
|
|
label strings.Builder
|
|
labelIDs = map[string]string{}
|
|
wrapped []int // fields inside a label that names none, by index in form.Fields
|
|
selIdx = -1
|
|
inOption bool
|
|
optValue string
|
|
optHasV bool
|
|
optSel bool
|
|
optText strings.Builder
|
|
inTextA bool
|
|
textA strings.Builder
|
|
textAIdx = -1
|
|
)
|
|
finishOption := func() {
|
|
if !inOption || selIdx < 0 {
|
|
inOption = false
|
|
return
|
|
}
|
|
v := optValue
|
|
if !optHasV {
|
|
v = squash(optText.String())
|
|
}
|
|
f := &form.Fields[selIdx]
|
|
f.Options = append(f.Options, v)
|
|
if optSel {
|
|
f.Value = v
|
|
}
|
|
inOption = false
|
|
}
|
|
closeForm := func() {
|
|
finishOption()
|
|
selIdx = -1
|
|
if formOpen {
|
|
p.Forms = append(p.Forms, form)
|
|
}
|
|
formOpen = false
|
|
form = Form{}
|
|
}
|
|
addField := func(f Field) {
|
|
form.Fields = append(form.Fields, f)
|
|
if inLabel && labelFor == "" {
|
|
wrapped = append(wrapped, len(form.Fields)-1)
|
|
}
|
|
}
|
|
text := func(t string) {
|
|
switch {
|
|
case inTitle:
|
|
title.WriteString(t)
|
|
case inOption:
|
|
optText.WriteString(t)
|
|
case inTextA:
|
|
textA.WriteString(t)
|
|
}
|
|
if link != nil {
|
|
link.Text += t
|
|
}
|
|
if inLabel {
|
|
label.WriteString(t)
|
|
}
|
|
}
|
|
i := 0
|
|
for i < len(s) {
|
|
lt := strings.IndexByte(s[i:], '<')
|
|
if lt < 0 {
|
|
text(s[i:])
|
|
break
|
|
}
|
|
text(s[i : i+lt])
|
|
i += lt
|
|
switch {
|
|
case strings.HasPrefix(s[i:], "<!--"):
|
|
end := strings.Index(s[i:], "-->")
|
|
if end < 0 {
|
|
i = len(s)
|
|
} else {
|
|
i += end + 3
|
|
}
|
|
continue
|
|
case strings.HasPrefix(s[i:], "<!") || strings.HasPrefix(s[i:], "<?"):
|
|
end := strings.IndexByte(s[i:], '>')
|
|
if end < 0 {
|
|
i = len(s)
|
|
} else {
|
|
i += end + 1
|
|
}
|
|
continue
|
|
}
|
|
end := tagEnd(s[i:])
|
|
if end < 0 {
|
|
break
|
|
}
|
|
raw := s[i+1 : i+end]
|
|
i += end + 1
|
|
closing := strings.HasPrefix(raw, "/")
|
|
raw = strings.TrimSuffix(strings.TrimPrefix(raw, "/"), "/")
|
|
name := raw
|
|
rest := ""
|
|
if sp := strings.IndexAny(raw, " \t\r\n"); sp >= 0 {
|
|
name, rest = raw[:sp], raw[sp:]
|
|
}
|
|
name = strings.ToLower(name)
|
|
if !closing && (name == "script" || name == "style") {
|
|
if e := strings.Index(strings.ToLower(s[i:]), "</"+name); e >= 0 {
|
|
i += e
|
|
} else {
|
|
i = len(s)
|
|
}
|
|
continue
|
|
}
|
|
a := map[string]string{}
|
|
if !closing {
|
|
a = attrs(rest)
|
|
}
|
|
switch name {
|
|
case "title":
|
|
inTitle = !closing
|
|
case "a":
|
|
if closing {
|
|
if link != nil {
|
|
link.Text = squash(link.Text)
|
|
p.Links = append(p.Links, *link)
|
|
}
|
|
link = nil
|
|
} else if href, ok := a["href"]; ok {
|
|
link = &Link{Href: href}
|
|
}
|
|
case "form":
|
|
closeForm()
|
|
if !closing {
|
|
formOpen = true
|
|
form = Form{Action: a["action"], Method: strings.ToLower(a["method"])}
|
|
if form.Method == "" {
|
|
form.Method = "get"
|
|
}
|
|
}
|
|
case "label":
|
|
if closing {
|
|
t := squash(label.String())
|
|
if labelFor != "" {
|
|
labelIDs[labelFor] = t
|
|
}
|
|
for _, k := range wrapped {
|
|
form.Fields[k].Label = t
|
|
}
|
|
inLabel, labelFor, wrapped = false, "", nil
|
|
label.Reset()
|
|
} else {
|
|
inLabel, labelFor = true, a["for"]
|
|
label.Reset()
|
|
}
|
|
case "input":
|
|
if closing || a["name"] == "" {
|
|
break
|
|
}
|
|
typ := strings.ToLower(a["type"])
|
|
if typ == "" {
|
|
typ = "text"
|
|
}
|
|
if typ == "submit" || typ == "button" || typ == "image" || typ == "reset" {
|
|
break
|
|
}
|
|
f := Field{Name: a["name"], Type: typ, Value: a["value"], id: a["id"]}
|
|
if typ == "password" {
|
|
f.Value = ""
|
|
}
|
|
addField(f)
|
|
case "select":
|
|
finishOption()
|
|
selIdx = -1
|
|
if !closing && a["name"] != "" {
|
|
addField(Field{Name: a["name"], Type: "select", id: a["id"]})
|
|
selIdx = len(form.Fields) - 1
|
|
}
|
|
case "option":
|
|
finishOption()
|
|
if !closing && selIdx >= 0 {
|
|
inOption = true
|
|
optValue, optHasV = a["value"], false
|
|
if _, ok := a["value"]; ok {
|
|
optHasV = true
|
|
}
|
|
_, optSel = a["selected"]
|
|
optText.Reset()
|
|
}
|
|
case "textarea":
|
|
if closing {
|
|
if inTextA && textAIdx >= 0 {
|
|
form.Fields[textAIdx].Value = html.UnescapeString(textA.String())
|
|
}
|
|
inTextA = false
|
|
} else if a["name"] != "" {
|
|
addField(Field{Name: a["name"], Type: "textarea", id: a["id"]})
|
|
inTextA, textAIdx = true, len(form.Fields)-1
|
|
textA.Reset()
|
|
}
|
|
}
|
|
}
|
|
closeForm()
|
|
p.Title = squash(title.String())
|
|
for fi := range p.Forms {
|
|
for k := range p.Forms[fi].Fields {
|
|
f := &p.Forms[fi].Fields[k]
|
|
if f.Label == "" && f.id != "" {
|
|
f.Label = labelIDs[f.id]
|
|
}
|
|
}
|
|
}
|
|
return p
|
|
}
|