-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathinput.lua
328 lines (298 loc) · 12.8 KB
/
input.lua
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
--
-- Flow: Formspec input processor
--
-- Copyright © 2022-2025 by luk3yx
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Lesser General Public License as published by
-- the Free Software Foundation, either version 2.1 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
--
local ceil, floor, max = math.ceil, math.floor, math.max
local function chain_cb(f1, f2)
return function(...)
f1(...)
f2(...)
end
end
local function range_check_transformer(items_length)
return function(value)
local num = tonumber(value)
if num and num == num then
num = floor(num)
if num >= 1 and num <= items_length then
return num
end
end
end
end
local function simple_transformer(func)
return function() return func end
end
-- Functions that transform field values into the easiest to use type
local C1_CHARS = "\194[\128-\159]"
local field_value_transformers = {
field = simple_transformer(function(value)
-- Remove control characters and newlines
return value:gsub("[%z\1-\8\10-\31\127]", ""):gsub(C1_CHARS, "")
end),
checkbox = simple_transformer(core.is_yes),
-- Scrollbars do have min/max values but scrollbars are only really used by
-- ScrollableVBox which doesn't need the extra checks
scrollbar = simple_transformer(function(value)
return core.explode_scrollbar_event(value).value
end),
}
-- Field value transformers that depend on some property of the element
function field_value_transformers.tabheader(node)
return range_check_transformer(node.captions and #node.captions or 0)
end
function field_value_transformers.dropdown(node, _, formspec_version)
local items = node.items or {}
if node.index_event and not node._index_event_hack then
return range_check_transformer(#items)
end
-- MT will start sanitising formspec fields on its own at some point
-- (https://github.com/minetest/minetest/pull/14878), however it may strip
-- escape sequences from dropdowns as well. Since we know what the actual
-- value of the dropdown is anyway, we can just enable index_event for new
-- clients and keep the same behaviour
if (formspec_version and formspec_version >= 4) or
(core.global_exists("fs51") and
fs51.monkey_patching_enabled) then
node.index_event = true
-- Detect reuse of the same Dropdown element (this is unsupported and
-- will break in other ways)
node._index_event_hack = true
return function(value)
return items[tonumber(value)]
end
elseif node._index_event_hack then
node.index_event = nil
end
-- Make sure that the value sent by the client is in the list of items
return function(value)
if table.indexof(items, value) > 0 then
return value
end
end
end
function field_value_transformers.table(node, tablecolumn_count)
-- Figure out how many rows the table has
local cells = node.cells and #node.cells or 0
local rows = ceil(cells / tablecolumn_count)
return function(value)
local row = floor(core.explode_table_event(value).row)
-- Tables and textlists can have values of 0 (nothing selected) but I
-- don't think the client can un-select a row so it should be safe to
-- ignore any 0 sent by the client to guarantee that the row will be
-- valid if the default value is valid
if row >= 1 and row <= rows then
return row
end
end
end
function field_value_transformers.textlist(node)
local rows = node.listelems and #node.listelems or 0
return function(value)
local index = floor(core.explode_textlist_event(value).index)
if index >= 1 and index <= rows then
return index
end
end
end
local function default_field_value_transformer(value)
-- Remove control characters (but preserve newlines)
-- Pattern by https://github.com/appgurueu
return value:gsub("[%z\1-\8\11-\31\127]", ""):gsub(C1_CHARS, "")
end
local default_value_fields = {
field = "default",
pwdfield = "default",
textarea = "default",
checkbox = "selected",
dropdown = "selected_idx",
table = "selected_idx",
textlist = "selected_idx",
scrollbar = "value",
tabheader = "current_tab",
}
local sensible_defaults = {
default = "", selected = false, selected_idx = 1, value = 0,
}
local button_types = {
button = true, image_button = true, item_image_button = true,
button_exit = true, image_button_exit = true
}
-- Removes on_event from a formspec_ast tree and returns a callbacks table
local function parse_callbacks(tree, ctx_form, auto_name_id,
replace_backgrounds, formspec_version)
local callbacks, on_key_enters
local btn_callbacks = {}
local saved_fields = {}
local tablecolumn_count = 1
for node in formspec_ast.walk(tree) do
if node.type == "container" then
if node.bgcolor then
local padding = node.padding or 0
table.insert(node, 1, {
type = "box", color = node.bgcolor,
x = -padding, y = -padding,
w = node.w + padding * 2, h = node.h + padding * 2,
})
end
if node.bgimg then
local padding = node.padding or 0
table.insert(node, 1, {
type = node.bgimg_middle and "background9" or "background",
texture_name = node.bgimg, middle_x = node.bgimg_middle,
x = -padding, y = -padding,
w = node.w + padding * 2, h = node.h + padding * 2,
})
end
-- The on_quit callback is undocumented and not recommended, it
-- only gets called when the client tells the server that it's
-- closing the form and not when another form is shown.
if node.on_quit then
callbacks = callbacks or {}
if callbacks.quit then
-- HACK
callbacks.quit = chain_cb(callbacks.quit, node.on_quit)
else
callbacks.quit = node.on_quit
end
end
replace_backgrounds = replace_backgrounds or node._enable_bgimg_hack
elseif node.type == "tablecolumns" and node.tablecolumns then
-- Store the amount of columns for input validation
tablecolumn_count = max(#node.tablecolumns, 1)
elseif replace_backgrounds then
if (node.type == "background" or node.type == "background9") and
not node.auto_clip then
node.type = "image"
end
elseif node.type == "scroll_container" then
-- Work around a Minetest bug with scroll containers not scrolling
-- backgrounds.
replace_backgrounds = true
end
local node_name = node.name
if node_name and node_name ~= "" then
local value_field = default_value_fields[node.type]
if value_field then
-- Update ctx.form if there is no current value, otherwise
-- change the node's value to the saved one.
local value = ctx_form[node_name]
if node.type == "dropdown" and (not node.index_event or
node._index_event_hack) then
-- Special case for dropdowns without index_event
local items = node.items or {}
if value == nil then
ctx_form[node_name] = items[node.selected_idx or 1]
else
local idx = table.indexof(items, value)
if idx > 0 then
node.selected_idx = idx
end
end
node.selected_idx = node.selected_idx or 1
elseif value == nil then
-- If ctx.form[node_name] doesn't exist, then check whether
-- a default value is specified.
local default_value = node[value_field]
local sensible_default = sensible_defaults[value_field]
if default_value == nil then
-- If the element doesn't have a default set, set it to
-- the sensible default value and update ctx.form in
-- case the client doesn't send the field value back.
node[value_field] = sensible_default
ctx_form[node_name] = sensible_default
else
-- Update ctx.form to the default value
ctx_form[node_name] = default_value
end
else
-- Set the node's value to the one saved in ctx.form
node[value_field] = value
end
-- Add the corresponding value transformer transformer to
-- saved_fields
local get_transformer = field_value_transformers[node.type]
saved_fields[node_name] = get_transformer and
get_transformer(node, tablecolumn_count,
formspec_version) or
default_field_value_transformer
elseif node.type == "hypertext" then
-- Experimental (may be broken in the future): Allow accessing
-- hypertext fields with "ctx.form.hypertext_name" as this is
-- the most straightforward way of doing it.
saved_fields[node_name] = default_field_value_transformer
end
end
-- Add the on_event callback (if any) to the callbacks table
if node.on_event then
local is_btn = button_types[node.type]
if not node_name then
-- Flow internal field names start with "_#" to avoid
-- conflicts with user-provided fields.
node_name = ("_#%x"):format(auto_name_id)
node.name = node_name
auto_name_id = auto_name_id + 1
elseif btn_callbacks[node_name] or
(is_btn and saved_fields[node_name]) or
(callbacks and callbacks[node_name]) then
core.log("warning", ("[flow] Multiple callbacks have " ..
"been registered for elements with the same name (%q), " ..
"this will not work properly."):format(node_name))
-- Preserve previous behaviour
btn_callbacks[node_name] = nil
if callbacks then
callbacks[node_name] = nil
end
is_btn = is_btn and not saved_fields[node_name]
end
-- Put buttons into a separate callback table so that malicious
-- clients can't send multiple button presses in one submission
if is_btn then
btn_callbacks[node_name] = node.on_event
else
callbacks = callbacks or {}
callbacks[node_name] = node.on_event
end
node.on_event = nil
end
local is_field = node.type == "field" or node.type == "pwdfield"
if node.on_key_enter and node_name then
if is_field then
on_key_enters = on_key_enters or {}
if on_key_enters[node_name] then
error(("Multiple on_key_enter callbacks registered for " ..
"elements with the same name (%q)"):format(node_name))
end
on_key_enters[node_name] = node.on_key_enter
else
core.log("warning",
"[flow] on_key_enter only works with Field and Pwdfield")
end
node.on_key_enter = nil
elseif is_field and node.close_on_enter then
core.log("warning", ("[flow] Field %q has close_on_enter " ..
"enabled but no on_key_enter callback."):format(node_name))
end
-- Call _after_positioned (used internally for ScrollableVBox)
if node._after_positioned then
node:_after_positioned()
node._after_positioned = nil
end
end
return callbacks, btn_callbacks, saved_fields, auto_name_id, on_key_enters
end
return parse_callbacks