aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/certmanager.lua4
-rw-r--r--net/adns.lua79
-rw-r--r--net/dns.lua2
-rw-r--r--plugins/mod_limits.lua96
-rw-r--r--plugins/mod_s2s/mod_s2s.lua2
-rw-r--r--plugins/mod_s2s/s2sout.lib.lua10
-rw-r--r--plugins/mod_server_contact_info.lua49
-rwxr-xr-xprosody4
-rw-r--r--prosody.cfg.lua.dist90
-rwxr-xr-xprosodyctl2
-rw-r--r--util/dataforms.lua40
11 files changed, 285 insertions, 93 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua
index 2e237595..288836ce 100644
--- a/core/certmanager.lua
+++ b/core/certmanager.lua
@@ -58,6 +58,7 @@ local key_try = { "", "/%s.key", "/%s/privkey.pem", "/%s.pem", };
local function find_cert(user_certs, name)
local certs = resolve_path(config_path, user_certs or global_certificates);
+ log("debug", "Searching %s for a key and certificate for %s...", certs, name);
for i = 1, #crt_try do
local crt_path = certs .. crt_try[i]:format(name);
local key_path = certs .. key_try[i]:format(name);
@@ -66,13 +67,16 @@ local function find_cert(user_certs, name)
if key_path:sub(-4) == ".crt" then
key_path = key_path:sub(1, -4) .. "key";
if stat(key_path, "mode") == "file" then
+ log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name);
return { certificate = crt_path, key = key_path };
end
elseif stat(key_path, "mode") == "file" then
+ log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name);
return { certificate = crt_path, key = key_path };
end
end
end
+ log("debug", "No certificate/key found for %s", name);
end
local function find_host_cert(host)
diff --git a/net/adns.lua b/net/adns.lua
index 0b7247ed..f1196a6c 100644
--- a/net/adns.lua
+++ b/net/adns.lua
@@ -7,7 +7,7 @@
--
local server = require "net.server";
-local dns = require "net.dns";
+local new_resolver = require "net.dns".resolver;
local log = require "util.logger".init("adns");
@@ -17,35 +17,11 @@ local function dummy_send(sock, data, i, j) return (j-i)+1; end
local _ENV = nil;
-local function lookup(handler, qname, qtype, qclass)
- return coroutine.wrap(function (peek)
- if peek then
- log("debug", "Records for %s already cached, using those...", qname);
- handler(peek);
- return;
- end
- log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
- local ok, err = dns.query(qname, qtype, qclass);
- if ok then
- coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply
- log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
- end
- if ok then
- ok, err = pcall(handler, dns.peek(qname, qtype, qclass));
- else
- log("error", "Error sending DNS query: %s", err);
- ok, err = pcall(handler, nil, err);
- end
- if not ok then
- log("error", "Error in DNS response handler: %s", tostring(err));
- end
- end)(dns.peek(qname, qtype, qclass));
-end
+local async_resolver_methods = {};
+local async_resolver_mt = { __index = async_resolver_methods };
-local function cancel(handle, call_handler, reason)
- log("warn", "Cancelling DNS lookup for %s", tostring(handle[3]));
- dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler);
-end
+local query_methods = {};
+local query_mt = { __index = query_methods };
local function new_async_socket(sock, resolver)
local peername = "<unknown>";
@@ -54,7 +30,7 @@ local function new_async_socket(sock, resolver)
local err;
function listener.onincoming(conn, data)
if data then
- dns.feed(handler, data);
+ resolver:feed(handler, data);
end
end
function listener.ondisconnect(conn, err)
@@ -85,10 +61,47 @@ local function new_async_socket(sock, resolver)
return handler;
end
-dns.socket_wrapper_set(new_async_socket);
+function async_resolver_methods:lookup(handler, qname, qtype, qclass)
+ local resolver = self._resolver;
+ return coroutine.wrap(function (peek)
+ if peek then
+ log("debug", "Records for %s already cached, using those...", qname);
+ handler(peek);
+ return;
+ end
+ log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
+ local ok, err = resolver:query(qname, qtype, qclass);
+ if ok then
+ coroutine.yield(setmetatable({ resolver, qclass or "IN", qtype or "A", qname, coroutine.running()}, query_mt)); -- Wait for reply
+ log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
+ end
+ if ok then
+ ok, err = pcall(handler, resolver:peek(qname, qtype, qclass));
+ else
+ log("error", "Error sending DNS query: %s", err);
+ ok, err = pcall(handler, nil, err);
+ end
+ if not ok then
+ log("error", "Error in DNS response handler: %s", tostring(err));
+ end
+ end)(resolver:peek(qname, qtype, qclass));
+end
+
+function query_methods:cancel(call_handler, reason)
+ log("warn", "Cancelling DNS lookup for %s", tostring(self[4]));
+ self[1].cancel(self[2], self[3], self[4], self[5], call_handler);
+end
+
+local function new_async_resolver()
+ local resolver = new_resolver();
+ resolver:socket_wrapper_set(new_async_socket);
+ return setmetatable({ _resolver = resolver}, async_resolver_mt);
+end
return {
- lookup = lookup;
- cancel = cancel;
+ lookup = function (...)
+ return new_async_resolver():lookup(...);
+ end;
+ resolver = new_async_resolver;
new_async_socket = new_async_socket;
};
diff --git a/net/dns.lua b/net/dns.lua
index 188bdf43..5ba3db0e 100644
--- a/net/dns.lua
+++ b/net/dns.lua
@@ -504,7 +504,7 @@ function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr
rr.ttl = 0x10000*self:word() + self:word();
rr.rdlength = self:word();
- rr.tod = self.time + math.min(rr.ttl, 1);
+ rr.tod = self.time + math.max(rr.ttl, 1);
local remember = self.offset;
local rr_parser = self[dns.type[rr.type]];
diff --git a/plugins/mod_limits.lua b/plugins/mod_limits.lua
new file mode 100644
index 00000000..2a6ee8a2
--- /dev/null
+++ b/plugins/mod_limits.lua
@@ -0,0 +1,96 @@
+-- Because we deal we pre-authed sessions and streams we can't be host-specific
+module:set_global();
+
+local filters = require "util.filters";
+local throttle = require "util.throttle";
+local timer = require "util.timer";
+
+local limits_cfg = module:get_option("limits", {});
+local limits_resolution = module:get_option_number("limits_resolution", 1);
+
+local default_bytes_per_second = 3000;
+local default_burst = 2;
+
+local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future.
+local function parse_rate(rate, sess_type)
+ local quantity, unit, exp;
+ if rate then
+ quantity, unit = rate:match("^(%d+) ?([^/]+)/s$");
+ exp = quantity and rate_units[unit:sub(1,1):lower()];
+ end
+ if not exp then
+ module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second);
+ return default_bytes_per_second;
+ end
+ return quantity*(10^exp);
+end
+
+local function parse_burst(burst, sess_type)
+ if type(burst) == "string" then
+ burst = burst:match("^(%d+) ?s$");
+ end
+ local n_burst = tonumber(burst);
+ if not n_burst then
+ module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst);
+ end
+ return n_burst or default_burst;
+end
+
+-- Process config option into limits table:
+-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } }
+local limits = {};
+
+for sess_type, sess_limits in pairs(limits_cfg) do
+ limits[sess_type] = {
+ bytes_per_second = parse_rate(sess_limits.rate, sess_type);
+ burst_seconds = parse_burst(sess_limits.burst, sess_type);
+ };
+end
+
+local default_filter_set = {};
+
+function default_filter_set.bytes_in(bytes, session)
+ local throttle = session.throttle;
+ if throttle then
+ local ok, balance, outstanding = throttle:poll(#bytes, true);
+ if not ok then
+ session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding);
+ session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
+ local outstanding_data = bytes:sub(-outstanding);
+ bytes = bytes:sub(1, #bytes-outstanding);
+ timer.add_task(limits_resolution, function ()
+ if not session.conn then return; end
+ if throttle:peek(#outstanding_data) then
+ session.log("debug", "Resuming paused session");
+ session.conn:resume();
+ end
+ -- Handle what we can of the outstanding data
+ session.data(outstanding_data);
+ end);
+ end
+ end
+ return bytes;
+end
+
+local type_filters = {
+ c2s = default_filter_set;
+ s2sin = default_filter_set;
+ s2sout = default_filter_set;
+};
+
+local function filter_hook(session)
+ local session_type = session.type:match("^[^_]+");
+ local filter_set, opts = type_filters[session_type], limits[session_type];
+ if opts then
+ session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds);
+ filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000);
+ end
+end
+
+function module.load()
+ filters.add_filter_hook(filter_hook);
+end
+
+function module.unload()
+ filters.remove_filter_hook(filter_hook);
+end
diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua
index b46b7e2a..bfd8f9af 100644
--- a/plugins/mod_s2s/mod_s2s.lua
+++ b/plugins/mod_s2s/mod_s2s.lua
@@ -180,6 +180,7 @@ end
-- Stream is authorised, and ready for normal stanzas
function mark_connected(session)
+
local sendq = session.sendq;
local from, to = session.from_host, session.to_host;
@@ -211,6 +212,7 @@ function mark_connected(session)
session.sendq = nil;
end
+ session.resolver = nil;
session.ip_hosts = nil;
session.srv_hosts = nil;
end
diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua
index 61d6086e..cd8553e1 100644
--- a/plugins/mod_s2s/s2sout.lib.lua
+++ b/plugins/mod_s2s/s2sout.lib.lua
@@ -49,6 +49,8 @@ function s2sout.initiate_connection(host_session)
initialize_filters(host_session);
host_session.version = 1;
+ host_session.resolver = adns.resolver();
+
-- Kick the connection attempting machine into life
if not s2sout.attempt_connection(host_session) then
-- Intentionally not returning here, the
@@ -84,9 +86,7 @@ function s2sout.attempt_connection(host_session, err)
if not err then -- This is our first attempt
log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
host_session.connecting = true;
- local handle;
- handle = adns.lookup(function (answer)
- handle = nil;
+ host_session.resolver:lookup(function (answer)
local srv_hosts = { answer = answer };
host_session.srv_hosts = srv_hosts;
host_session.srv_choice = 0;
@@ -168,7 +168,7 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
local have_other_result = not(has_ipv4) or not(has_ipv6) or false;
if has_ipv4 then
- handle4 = adns.lookup(function (reply, err)
+ handle4 = host_session.resolver:lookup(function (reply, err)
handle4 = nil;
if reply and reply[#reply] and reply[#reply].a then
@@ -206,7 +206,7 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
end
if has_ipv6 then
- handle6 = adns.lookup(function (reply, err)
+ handle6 = host_session.resolver:lookup(function (reply, err)
handle6 = nil;
if reply and reply[#reply] and reply[#reply].aaaa then
diff --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua
new file mode 100644
index 00000000..7ee8a08f
--- /dev/null
+++ b/plugins/mod_server_contact_info.lua
@@ -0,0 +1,49 @@
+-- XEP-0157: Contact Addresses for XMPP Services for Prosody
+--
+-- Copyright (C) 2011-2016 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+
+local t_insert = table.insert;
+local array = require "util.array";
+local df_new = require "util.dataforms".new;
+
+-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo
+local valid_types = {
+ abuse = true;
+ admin = true;
+ feedback = true;
+ sales = true;
+ security = true;
+ support = true;
+}
+
+local contact_config = module:get_option("contact_info");
+if not contact_config or not next(contact_config) then -- we'll use admins from the config as default
+ local admins = module:get_option_inherited_set("admins", {});
+ if admins:empty() then
+ module:log("error", "No contact_info or admins set in config");
+ return -- Nothing to attach, so we'll just skip it.
+ end
+ module:log("info", "No contact_info in config, using admins as fallback");
+ contact_config = {
+ admin = array.collect( admins / function(admin) return "xmpp:" .. admin; end);
+ };
+end
+
+local form_layout = {
+ { value = "http://jabber.org/network/serverinfo"; type = "hidden"; name = "FORM_TYPE"; };
+};
+
+local form_values = {};
+
+for t in pairs(valid_types) do
+ local addresses = contact_config[t];
+ if addresses then
+ t_insert(form_layout, { name = t .. "-addresses", type = "list-multi" });
+ form_values[t .. "-addresses"] = addresses;
+ end
+end
+
+module:add_extension(df_new(form_layout):form(form_values, "result"));
diff --git a/prosody b/prosody
index 1895cf54..9a15af1c 100755
--- a/prosody
+++ b/prosody
@@ -20,8 +20,8 @@ CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR");
local function is_relative(path)
local path_sep = package.config:sub(1,1);
- return ((path_sep == "/" and path:sub(1,1) ~= "/")
- or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
+ return ((path_sep == "/" and path:sub(1,1) ~= "/")
+ or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
end
-- Tell Lua where to find our libraries
diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist
index c42e56d0..bd897f74 100644
--- a/prosody.cfg.lua.dist
+++ b/prosody.cfg.lua.dist
@@ -4,7 +4,8 @@
-- website at https://prosody.im/doc/configure
--
-- Tip: You can check that the syntax of this file is correct
--- when you have finished by running: prosodyctl check config
+-- when you have finished by running this command:
+-- prosodyctl check config
-- If there are any errors, it will let you know what and where
-- they are, otherwise it will keep quiet.
--
@@ -26,9 +27,14 @@ admins = { }
-- For more information see: https://prosody.im/doc/libevent
--use_libevent = true
+-- Prosody will always look in its source directory for modules, but
+-- this option allows you to specify additional locations where Prosody
+-- will look for modules first. For community modules, see https://modules.prosody.im/
+--plugin_paths = {}
+
-- This is the list of modules Prosody will load on startup.
-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
--- Documentation on modules can be found at: https://prosody.im/doc/modules
+-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules
modules_enabled = {
-- Generally required
@@ -39,20 +45,19 @@ modules_enabled = {
"disco"; -- Service discovery
-- Not essential, but recommended
+ "carbons"; -- Keep multiple clients in sync
+ "pep"; -- Enables users to publish their mood, activity, playing music and more
"private"; -- Private XML storage (for room bookmarks, etc.)
+ "blocklist"; -- Allow users to block communications with other users
"vcard"; -- Allow users to set vCards
- -- These are commented by default as they have a performance impact
- --"blocklist"; -- Allow users to block communications with other users
- --"compression"; -- Stream compression
-
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
- "pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
+ --"mam"; -- Store messages in an archive and allow users to access it
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
@@ -60,15 +65,19 @@ modules_enabled = {
-- HTTP modules
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
+ --"websockets"; -- XMPP over WebSockets
--"http_files"; -- Serve static files from a directory over HTTP
-- Other specific functionality
+ --"limits"; -- Enable bandwidth limiting for XMPP connections
--"groups"; -- Shared roster support
+ --"server_contact_info"; -- Publish contact information for this service
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
+ --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
}
-- These modules are auto-loaded, but should you want
@@ -84,18 +93,18 @@ modules_disabled = {
-- For more information see https://prosody.im/doc/creating_accounts
allow_registration = false
--- These are the SSL/TLS-related settings. If you don't want
--- to use SSL/TLS, you may comment or remove this
-ssl = {
- key = "certs/localhost.key";
- certificate = "certs/localhost.crt";
-}
-
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
c2s_require_encryption = true
+-- Force servers to use encrypted connections? This option will
+-- prevent servers from authenticating unless they are using encryption.
+-- Note that this is different from authentication
+
+s2s_require_encryption = true
+
+
-- Force certificate authentication for server-to-server connections?
-- This provides ideal security, but requires servers you communicate
-- with to support encryption AND present valid, trusted certificates.
@@ -104,11 +113,12 @@ c2s_require_encryption = true
s2s_secure_auth = false
--- Many servers don't support encryption or have invalid or self-signed
--- certificates. You can list domains here that will not be required to
--- authenticate using certificates. They will be authenticated using DNS.
+-- Some servers have invalid or self-signed certificates. You can list
+-- remote domains here that will not be required to authenticate using
+-- certificates. They will be authenticated using DNS instead, even
+-- when s2s_secure_auth is enabled.
---s2s_insecure_domains = { "gmail.com" }
+--s2s_insecure_domains = { "insecure.example" }
-- Even if you leave s2s_secure_auth disabled, you can still require valid
-- certificates for some domains by specifying a list here.
@@ -122,7 +132,7 @@ s2s_secure_auth = false
-- server please see https://prosody.im/doc/modules/mod_auth_internal_hashed
-- for information about using the hashed backend.
-authentication = "internal_plain"
+authentication = "internal_hashed"
-- Select the storage backend to use. By default Prosody uses flat files
-- in its configured data directory, but it also supports more backends
@@ -136,6 +146,18 @@ authentication = "internal_plain"
--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
+
+-- Archiving configuration
+-- If mod_mam is enabled, Prosody will store a copy of every message. This
+-- is used to synchronize conversations between multiple clients, even if
+-- they are offline. This setting controls how long Prosody will keep
+-- messages in the archive before removing them.
+
+archive_expires_after = "1w" -- Remove archived messages after 1 week
+
+-- You can also configure messages to be stored in-memory only. For more
+-- archiving options, see https://prosody.im/doc/modules/mod_mam
+
-- Logging configuration
-- For advanced logging see https://prosody.im/doc/logging
log = {
@@ -145,23 +167,28 @@ log = {
-- "*console"; -- Log to the console, useful for debugging with daemonize=false
}
+-- Uncomment to enable statistics
+-- For more info see https://prosody.im/doc/statistics
+-- statistics = "internal"
+
+-- Certificates
+-- Every virtual host and component needs a certificate so that clients and
+-- servers can securely verify its identity. Prosody will automatically load
+-- certificates/keys from the directory specified here.
+-- For more information, including how to use 'prosodyctl' to auto-import certificates
+-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates
+
+-- Location of directory to find certificates in (relative to main config file):
+certificates = "certs"
+
----------- Virtual hosts -----------
-- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
-- Settings under each VirtualHost entry apply *only* to that host.
VirtualHost "localhost"
-VirtualHost "example.com"
- enabled = false -- Remove this line to enable this host
-
- -- Assign this host a certificate for TLS, otherwise it would use the one
- -- set in the global section (if any).
- -- Note that old-style SSL on port 5223 only supports one certificate, and will always
- -- use the global one.
- ssl = {
- key = "certs/example.com.key";
- certificate = "certs/example.com.crt";
- }
+--VirtualHost "example.com"
+-- certificate = "/path/to/example.crt"
------ Components ------
-- You can specify components to add hosts that provide special services,
@@ -171,9 +198,6 @@ VirtualHost "example.com"
---Set up a MUC (multi-user chat) room server on conference.example.com:
--Component "conference.example.com" "muc"
--- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
---Component "proxy.example.com" "proxy65"
-
---Set up an external component (default component port is 5347)
--
-- External components allow adding various services, such as gateways/
diff --git a/prosodyctl b/prosodyctl
index bb35c641..94d3a4e5 100755
--- a/prosodyctl
+++ b/prosodyctl
@@ -1030,7 +1030,7 @@ function commands.check(arg)
suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
end
end
- if not suggested_global_modules:empty() then
+ if suggested_global_modules and not suggested_global_modules:empty() then
print(" Consider moving these modules into modules_enabled in the global section:")
print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
end
diff --git a/util/dataforms.lua b/util/dataforms.lua
index 756f35a7..469ce976 100644
--- a/util/dataforms.lua
+++ b/util/dataforms.lua
@@ -68,33 +68,37 @@ function form_t.form(layout, data, formtype)
form:tag("value"):text(line):up();
end
elseif field_type == "list-single" then
- local has_default = false;
- for _, val in ipairs(field.options or value) do
- if type(val) == "table" then
- form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
- if value == val.value or val.default and (not has_default) then
- form:tag("value"):text(val.value):up();
- has_default = true;
+ if formtype ~= "result" then
+ local has_default = false;
+ for _, val in ipairs(field.options or value) do
+ if type(val) == "table" then
+ form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+ if value == val.value or val.default and (not has_default) then
+ form:tag("value"):text(val.value):up();
+ has_default = true;
+ end
+ else
+ form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
end
- else
- form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
end
end
- if field.options and value then
+ if (field.options or formtype == "result") and value then
form:tag("value"):text(value):up();
end
elseif field_type == "list-multi" then
- for _, val in ipairs(field.options or value) do
- if type(val) == "table" then
- form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
- if not field.options and val.default then
- form:tag("value"):text(val.value):up();
+ if formtype ~= "result" then
+ for _, val in ipairs(field.options or value) do
+ if type(val) == "table" then
+ form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+ if not field.options and val.default then
+ form:tag("value"):text(val.value):up();
+ end
+ else
+ form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
end
- else
- form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
end
end
- if field.options and value then
+ if (field.options or formtype == "result") and value then
for _, val in ipairs(value) do
form:tag("value"):text(val):up();
end