288 lines
7.0 KiB
Lua
288 lines
7.0 KiB
Lua
local getmetatable = getmetatable
|
|
local tonumber = tonumber
|
|
local rawget = rawget
|
|
local insert = table.insert
|
|
local unpack = table.unpack -- luacheck: ignore table
|
|
local ipairs = ipairs
|
|
local pairs = pairs
|
|
local type = type
|
|
local ngx = ngx
|
|
local req = ngx.req
|
|
local log = ngx.log
|
|
local re_match = ngx.re.match
|
|
local re_gmatch = ngx.re.gmatch
|
|
local re_gsub = ngx.re.gsub
|
|
local get_method = req.get_method
|
|
|
|
|
|
local NOTICE = ngx.NOTICE
|
|
|
|
local ENC_LEFT_SQUARE_BRACKET = "%5B"
|
|
local ENC_RIGHT_SQUARE_BRACKET = "%5D"
|
|
|
|
|
|
local multipart_mt = {}
|
|
|
|
|
|
function multipart_mt:__tostring()
|
|
return self.data
|
|
end
|
|
|
|
|
|
function multipart_mt:__index(name)
|
|
local json = rawget(self, "json")
|
|
if json then
|
|
return json[name]
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
|
|
-- Extracts keys from a string representing a nested table
|
|
-- e.g. [foo][bar][21].key => ["foo", "bar", 21, "key"].
|
|
-- is_map is meant to label patterns that use the bracket map syntax.
|
|
local function extract_param_keys(keys_string)
|
|
local is_map = false
|
|
|
|
-- iterate through keys (split by dots or square brackets)
|
|
local iterator, err = re_gmatch(keys_string, [=[\.([^\[\.]+)|\[([^\]]*)\]]=], "jos")
|
|
if not iterator then
|
|
return nil, err
|
|
end
|
|
|
|
local keys = {}
|
|
for captures, it_err in iterator do
|
|
if it_err then
|
|
log(NOTICE, it_err)
|
|
|
|
else
|
|
local key_name
|
|
if captures[1] then
|
|
-- The first capture: `\.([^\[\.]+)` matches dot-separated keys
|
|
key_name = captures[1]
|
|
|
|
else
|
|
-- The second capture: \[([^\]]*)\] matches bracket-separated keys
|
|
key_name = captures[2]
|
|
|
|
-- If a bracket-separated key is non-empty and non-numeric, set
|
|
-- is_map to true: foo[test] is a map, foo[] and bar[42] are arrays
|
|
local map_key_found = key_name ~= "" and tonumber(key_name) == nil
|
|
if map_key_found then
|
|
is_map = true
|
|
end
|
|
end
|
|
|
|
insert(keys, key_name)
|
|
end
|
|
end
|
|
|
|
return keys, nil, is_map
|
|
end
|
|
|
|
|
|
-- Extracts the parameter name and keys from a string
|
|
-- e.g. myparam[foo][bar][21].key => myparam, ["foo", "bar", 21, "key"]
|
|
local function get_param_name_and_keys(name_and_keys)
|
|
-- key delimiter must appear after the first character
|
|
-- e.g. for `[5][foo][bar].key`, `[5]` is the parameter name
|
|
local first_key_delimiter = name_and_keys:find('[%[%.]', 2)
|
|
if not first_key_delimiter then
|
|
return nil, "keys not found"
|
|
end
|
|
|
|
local param_name = name_and_keys:sub(1, first_key_delimiter - 1)
|
|
local keys_string = name_and_keys:sub(first_key_delimiter)
|
|
|
|
local keys, err, is_map = extract_param_keys(keys_string)
|
|
if not keys then
|
|
return nil, err
|
|
end
|
|
|
|
return param_name, nil, keys, is_map
|
|
end
|
|
|
|
|
|
-- Nests the provided path into container
|
|
-- e.g. nest_path({}, {"foo", "bar", 21, "key"}, 42) => { foo = { bar = { [21] = { key = 42 } } } }
|
|
local function nest_path(container, path, value)
|
|
container = container or {}
|
|
|
|
if type(path) ~= "table" then
|
|
return nil, "path must be a table"
|
|
end
|
|
|
|
for i = 1, #path do
|
|
local segment = path[i]
|
|
|
|
local arr_index = tonumber(segment)
|
|
-- if it looks like: foo[] or bar[42], it's an array
|
|
local isarray = segment == "" or arr_index ~= nil
|
|
|
|
if isarray then
|
|
if i == #path then
|
|
|
|
if arr_index then
|
|
insert(container, arr_index, value)
|
|
return container[arr_index]
|
|
end
|
|
|
|
if type(value) == "table" and getmetatable(value) ~= multipart_mt then
|
|
for j, v in ipairs(value) do
|
|
insert(container, j, v)
|
|
end
|
|
|
|
return container
|
|
end
|
|
|
|
container[#container + 1] = value
|
|
return container
|
|
|
|
else
|
|
local position = arr_index or 1
|
|
if not container[position] then
|
|
container[position] = {}
|
|
container = container[position]
|
|
end
|
|
end
|
|
|
|
else -- it's a map
|
|
if i == #path then
|
|
container[segment] = value
|
|
return container[segment]
|
|
|
|
elseif not container[segment] then
|
|
container[segment] = {}
|
|
container = container[segment]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Decodes a complex argument (map, array or mixed), into a nested table
|
|
-- e.g. foo[bar][21].key, 42 => { foo = { bar = { [21] = { key = 42 } } } }
|
|
local function decode_map_array_arg(name, value, container)
|
|
local param_name, err, keys, is_map = get_param_name_and_keys(name)
|
|
if not param_name or not keys or #keys == 0 then
|
|
return nil, err or "not a map or array"
|
|
end
|
|
|
|
-- the meaning of square brackets varies depending on the http method.
|
|
-- It is considered a map when a non numeric value exists between brackets
|
|
-- if the method is POST, PUT, or PATCH, otherwise it is interpreted as LHS
|
|
-- brackets used for search capabilities (only in EE).
|
|
if is_map then
|
|
local method = get_method()
|
|
if method ~= "POST" and method ~= "PUT" and method ~= "PATCH" then
|
|
return nil, "map not supported for this method"
|
|
end
|
|
end
|
|
|
|
local path = {param_name, unpack(keys)}
|
|
return nest_path(container, path, value)
|
|
end
|
|
|
|
|
|
local function decode_complex_arg(name, value, container)
|
|
container = container or {}
|
|
|
|
if type(name) ~= "string" then
|
|
container[name] = value
|
|
return container[name]
|
|
end
|
|
|
|
local decoded = decode_map_array_arg(name, value, container)
|
|
if not decoded then
|
|
container[name] = value
|
|
return container[name]
|
|
end
|
|
|
|
return decoded
|
|
end
|
|
|
|
|
|
local function decode_arg(raw_name, value)
|
|
if type(raw_name) ~= "string" or re_match(raw_name, [[^\.+|\.$]], "jos") then
|
|
return { name = value }
|
|
end
|
|
|
|
-- unescape `[` and `]` characters when the array / map syntax is detected
|
|
local array_map_pattern = ENC_LEFT_SQUARE_BRACKET .. "(.*?)" .. ENC_RIGHT_SQUARE_BRACKET
|
|
local name = re_gsub(raw_name, array_map_pattern, "[$1]", "josi")
|
|
|
|
-- treat test[foo.bar] as a single match instead of splitting on the dot
|
|
local iterator, err = re_gmatch(name, [=[([^.](?:\[[^\]]*\])*)+]=], "jos")
|
|
if not iterator then
|
|
if err then
|
|
log(NOTICE, err)
|
|
end
|
|
|
|
return decode_complex_arg(name, value)
|
|
end
|
|
|
|
local names = {}
|
|
local count = 0
|
|
|
|
while true do
|
|
local captures, err = iterator()
|
|
if captures then
|
|
count = count + 1
|
|
names[count] = captures[0]
|
|
|
|
elseif err then
|
|
log(NOTICE, err)
|
|
break
|
|
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
if count == 0 then
|
|
return decode_complex_arg(name, value)
|
|
end
|
|
|
|
local container = {}
|
|
local bucket = container
|
|
|
|
for i = 1, count do
|
|
if i == count then
|
|
decode_complex_arg(names[i], value, bucket)
|
|
return container
|
|
|
|
else
|
|
bucket = decode_complex_arg(names[i], {}, bucket)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function decode(args)
|
|
local i = 0
|
|
local r = {}
|
|
|
|
if type(args) ~= "table" then
|
|
return r
|
|
end
|
|
|
|
for name, value in pairs(args) do
|
|
i = i + 1
|
|
r[i] = decode_arg(name, value)
|
|
end
|
|
|
|
return r
|
|
end
|
|
|
|
|
|
return {
|
|
decode = decode,
|
|
multipart_mt = multipart_mt,
|
|
_decode_arg = decode_arg,
|
|
_extract_param_keys = extract_param_keys,
|
|
_get_param_name_and_keys = get_param_name_and_keys,
|
|
_nest_path = nest_path,
|
|
_decode_map_array_arg = decode_map_array_arg,
|
|
}
|