Files
kong--kong/kong/api/arguments_decoder.lua
2026-07-13 12:32:21 +08:00

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,
}