~samwhited/xmpp

xmpp/form/fields.go -rw-r--r-- 7.1 KiB
c9743d9fSam Whited docs: update changelog to include deprecations 8 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// Copyright 2017 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package form

import (
	"encoding/xml"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/jid"
)

// FieldType is the type of fields in a dataform. For more information see the
// constants defined in this package.
type FieldType string

const (
	// TypeBoolean enables an entity to gather or provide an either-or choice
	// between two options.
	TypeBoolean FieldType = "boolean"

	// TypeFixed is intended for data description (e.g., human-readable text such
	// as "section" headers) rather than data gathering or provision.
	TypeFixed FieldType = "fixed"

	// TypeHidden is for fields that are not shown to the form-submitting entity,
	// but instead are returned with the form.
	TypeHidden FieldType = "hidden"

	// TypeJIDMulti enables an entity to gather or provide multiple JIDs.
	TypeJIDMulti FieldType = "jid-multi"

	// TypeJID enables an entity to gather or provide a single JID.
	TypeJID FieldType = "jid-single"

	// TypeListMulti enables an entity to gather or provide one or more options
	// from among many.
	TypeListMulti FieldType = "list-multi"

	// TypeList enables an entity to gather or provide one option from among many.
	TypeList FieldType = "list-single"

	// TypeTextMulti enables an entity to gather or provide multiple lines of
	// text.
	TypeTextMulti FieldType = "text-multi"

	// TypeTextPrivate enables an entity to gather or provide a single line or
	// word of text, which shall be obscured in an interface (e.g., with multiple
	// instances of the asterisk character).
	TypeTextPrivate FieldType = "text-private"

	// TypeText enables an entity to gather or provide a single line or word of
	// text, which may be shown in an interface.
	TypeText FieldType = "text-single"
)

type fieldOpt struct {
	Label string `xml:"label,attr"`
	Value string `xml:"value"`
}

// FieldData represents values from a single field in a data form.
// The Var field can then be passed to the Get and Set functions on the form to
// modify the field.
type FieldData struct {
	Type     FieldType
	Var      string
	Label    string
	Desc     string
	Required bool

	// Raw is the value of the field as it came over the wire with no type
	// information.
	// Generally speaking, Get methods on form should be used along with the field
	// data's Var value to fetch fields and Raw should be ignored.
	// Raw is mostly provided to access fixed type fields that do not have a
	// variable name (and therefore cannot be referenced or set).
	Raw []string
}

type field struct {
	typ      FieldType
	varName  string
	label    string
	desc     string
	value    []string
	option   []fieldOpt
	required bool
}

func (f *field) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	s := struct {
		Type     FieldType  `xml:"type,attr"`
		Label    string     `xml:"label,attr"`
		Var      string     `xml:"var,attr"`
		Desc     string     `xml:"desc"`
		Required *string    `xml:"required"`
		Value    []string   `xml:"value"`
		Option   []fieldOpt `xml:"option"`
	}{}

	err := d.DecodeElement(&s, &start)
	f.typ = s.Type
	f.label = s.Label
	f.varName = s.Var
	f.desc = s.Desc
	f.required = s.Required != nil
	f.value = s.Value
	f.option = s.Option
	return err
}

func (f *field) TokenReader() xml.TokenReader {
	attr := []xml.Attr{{
		Name:  xml.Name{Local: "type"},
		Value: string(f.typ),
	}}
	if f.varName != "" {
		attr = append(attr, xml.Attr{
			Name:  xml.Name{Local: "var"},
			Value: f.varName,
		})
	}
	if f.label != "" {
		attr = append(attr, xml.Attr{
			Name:  xml.Name{Local: "label"},
			Value: f.label,
		})
	}
	var child []xml.TokenReader
	if f.desc != "" {
		child = append(child, xmlstream.Wrap(
			xmlstream.Token(xml.CharData(f.desc)),
			xml.StartElement{Name: xml.Name{Local: "desc"}},
		))
	}
	if f.required {
		child = append(child, xmlstream.Wrap(
			nil,
			xml.StartElement{Name: xml.Name{Local: "required"}},
		))
	}
	var firstVal bool
	for _, val := range f.value {
		if val == "" {
			continue
		}
		// Some list types are only allowed to have a single value.
		if firstVal && f.typ != "list-multi" && f.typ != "jid-multi" && f.typ != "text-multi" {
			break
		}
		switch f.typ {
		case TypeBoolean:
			if val != "true" && val != "false" && val != "0" && val != "1" {
				continue
			}
		case TypeJID, TypeJIDMulti:
			_, err := jid.Parse(val)
			if err != nil {
				continue
			}
		}
		child = append(child, xmlstream.Wrap(
			xmlstream.Token(xml.CharData(val)),
			xml.StartElement{Name: xml.Name{Local: "value"}},
		))
		firstVal = true
	}
	if f.typ == "list-single" || f.typ == "list-multi" {
		for _, opt := range f.option {
			child = append(child, xmlstream.Wrap(
				xmlstream.Wrap(
					xmlstream.Token(xml.CharData(opt.Value)),
					xml.StartElement{Name: xml.Name{Local: "value"}},
				),
				xml.StartElement{
					Name: xml.Name{Local: "option"},
					Attr: []xml.Attr{{Name: xml.Name{Local: "label"}, Value: opt.Label}},
				},
			))
		}
	}

	return xmlstream.Wrap(
		xmlstream.MultiReader(child...),
		xml.StartElement{
			Name: xml.Name{Local: "field"},
			Attr: attr,
		},
	)
}

func newField(typ FieldType, id string, o ...Option) func(data *Data) {
	return func(data *Data) {
		f := field{
			typ:     typ,
			varName: id,
		}
		getFieldOpts(&f, o...)
		data.fields = append(data.fields, f)
	}
}

// Boolean fields enable an entity to gather or provide an either-or choice
// between two options.
func Boolean(id string, o ...Option) Field {
	return newField(TypeBoolean, id, o...)
}

// Fixed is intended for data description (e.g., human-readable text such as
// "section" headers) rather than data gathering or provision.
func Fixed(o ...Option) Field {
	return newField(TypeFixed, "", o...)
}

// Hidden fields are not shown by the form-submitting entity, but instead are
// returned, generally unmodified, with the form.
func Hidden(id string, o ...Option) Field {
	return newField(TypeHidden, id, o...)
}

// JIDMulti enables an entity to gather or provide multiple Jabber IDs.
func JIDMulti(id string, o ...Option) Field {
	return newField(TypeJIDMulti, id, o...)
}

// JID enables an entity to gather or provide a Jabber ID.
func JID(id string, o ...Option) Field {
	return newField(TypeJID, id, o...)
}

// ListMulti enables an entity to gather or provide one or more entries from a
// list.
func ListMulti(id string, o ...Option) Field {
	return newField(TypeListMulti, id, o...)
}

// List enables an entity to gather or provide a single entry from a list.
func List(id string, o ...Option) Field {
	return newField(TypeList, id, o...)
}

// TextMulti enables an entity to gather or provide multiple lines of text.
func TextMulti(id string, o ...Option) Field {
	return newField(TypeTextMulti, id, o...)
}

// TextPrivate enables an entity to gather or provide a line of text that should
// be obscured in the submitting entities interface (eg. with multiple
// asterisks).
func TextPrivate(id string, o ...Option) Field {
	return newField(TypeTextPrivate, id, o...)
}

// Text enables an entity to gather or provide a line of text.
func Text(id string, o ...Option) Field {
	return newField(TypeText, id, o...)
}