2013-07-17 22:55:27 +02:00
local util = require ( ' util.utils ' ) -- required for string:split()
local urlcode = require ( ' util.urlcode ' )
local confDefaults = require ( ' conf_defaults ' )
local s = require ( ' util.settings ' )
local ResponseClass = require ( ' rest.response ' )
2013-07-05 17:26:39 +02:00
2013-07-08 13:34:27 +02:00
local M = { }
M.__index = M
2013-07-05 17:26:39 +02:00
2013-07-17 22:55:27 +02:00
local GLOBAL_API_FUNCTION_NAME = ' _global '
2013-07-09 01:49:56 +02:00
2013-07-10 00:32:43 +02:00
--NOTE: requestedApi* contain what was extracted from the request data
-- regarding the other variables: either both resolvedApiFunction and realApiFunctionName
-- are nil and resolutionError is not, or exactly the other way around
M.requestedApiModule = nil
M.requestedApiFunction = nil
M.resolvedApiFunction = nil --will contain function address, or nil
M.realApiFunctionName = nil --will contain requested name, or global name, or nil
M.resolutionError = nil --non-nil means function could not be resolved
2013-07-08 13:34:27 +02:00
local function kvTableFromUrlEncodedString ( encodedText )
local args = { }
if ( encodedText ~= nil ) then
2013-09-20 23:38:20 +02:00
urlcode.parsequeryNoRegex ( encodedText , args )
2013-07-08 13:34:27 +02:00
end
return args
2013-07-10 00:32:43 +02:00
2013-07-08 13:34:27 +02:00
end
2013-07-05 17:26:39 +02:00
2013-07-08 13:34:27 +02:00
local function kvTableFromArray ( argArray )
local args = { }
2013-09-20 23:38:20 +02:00
2013-07-17 08:06:04 +02:00
if not argArray then return args end
2013-09-20 23:38:20 +02:00
2013-07-08 13:34:27 +02:00
for _ , v in ipairs ( argArray ) do
2013-07-05 17:26:39 +02:00
local split = v : find ( " = " )
if split ~= nil then
2013-07-29 13:48:56 +02:00
args [ v : sub ( 1 , split - 1 ) ] = urlcode.unescape ( v : sub ( split + 1 ) )
2013-07-08 13:34:27 +02:00
else
args [ v ] = true
2013-07-05 17:26:39 +02:00
end
end
2013-09-20 23:38:20 +02:00
2013-07-08 13:34:27 +02:00
return args
end
2013-07-17 22:55:27 +02:00
--- Create an array from the given '/'-separated path.
-- Empty path elements are not ignored (e.g. '/a//b' yields { [1] = '', [2] = 'a', [3] = '', [4] = 'b' }).
-- @param pathText The path to split.
-- @return An array with the path elements.
2013-07-11 10:30:59 +02:00
local function arrayFromPath ( pathText )
2013-07-17 22:55:27 +02:00
return pathText and pathText : split ( ' / ' ) or { }
2013-07-17 08:06:04 +02:00
end
2013-07-17 22:55:27 +02:00
--- Resolve the given module name.
-- Modules are searched for in the 'rest.api' path, with their name prefixed by 'api_'.
-- e.g. if modname is 'test', then the generated 'require()' path will be 'rest.api.api_test'.
-- Furthermore, the module must have the table key 'isApi' set to true.
-- @param modname The basename of the module to resolve.
-- @return Either a module object, or nil on error
-- @return An message on error, or nil otherwise
-- @see resolveApiFunction
2013-07-10 00:32:43 +02:00
local function resolveApiModule ( modname )
if modname == nil then return nil , " missing module name " end
2013-07-17 22:55:27 +02:00
if string.find ( modname , ' _ ' ) == 1 then return nil , " module names starting with '_' are preserved for internal use " end
2013-09-20 23:38:20 +02:00
2013-07-17 22:55:27 +02:00
local reqModName = ' rest.api.api_ ' .. modname
2013-07-10 00:32:43 +02:00
local ok , modObj
2013-09-20 23:38:20 +02:00
2013-07-17 22:55:27 +02:00
if confDefaults.DEBUG_PCALLS then ok , modObj = true , require ( reqModName )
2013-07-10 00:32:43 +02:00
else ok , modObj = pcall ( require , reqModName )
end
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
if ok == false then return nil , " API module does not exist " end
if modObj == nil then return nil , " API module could not be found " end
if modObj.isApi ~= true then return nil , " module is not part of the CGI API " end
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
return modObj
end
2013-07-17 22:55:27 +02:00
--- Resolves a module/function name pair with appropiate access for the given request method.
2013-07-29 12:18:39 +02:00
-- First, the function name suffixed with the request method is looked up, if not found the plain
2013-07-17 22:55:27 +02:00
-- function name is looked up.
2013-07-29 12:18:39 +02:00
-- The returned table contains a 'func' key if resolution was successful.
-- A key 'accessType' will also be included indicating valid access methods (GET, POST or ANY), except of course when the function does not exist at all.
-- If present, a key 'blankArg' will also be included.
-- Finally, the key 'notfound' will be set to true if no function (even of invalid access type) could be found.
--
-- @tparam string modname Basename of the module to resolve funcname in.
-- @tparam string funcname Basename of the function to resolve.
-- @tparam string requestMethod Method by which the request was received.
-- @treturn table A table with resultData.
2013-07-17 22:55:27 +02:00
-- @see resolveApiModule
local function resolveApiFunction ( modname , funcname , requestMethod )
2013-07-17 08:06:04 +02:00
local resultData = { }
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
if funcname and string.find ( funcname , " _ " ) == 1 then return nil , " function names starting with '_' are preserved for internal use " end
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
local mod , msg = resolveApiModule ( modname )
2013-09-20 23:38:20 +02:00
2013-07-11 10:30:59 +02:00
if mod == nil then
2013-07-29 13:48:56 +02:00
-- error is indicated by leaving out 'func' key and adding 'notfound'=true
resultData.notfound = true
resultData.msg = msg
return resultData
2013-07-11 10:30:59 +02:00
end
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
if ( funcname == nil or funcname == ' ' ) then funcname = GLOBAL_API_FUNCTION_NAME end --treat empty function name as nil
2013-07-17 22:55:27 +02:00
local rqType = requestMethod == ' POST ' and ' POST ' or ' GET '
local fGeneric = mod [ funcname ]
local fWithMethod = mod [ funcname .. ' _ ' .. rqType ]
2013-07-10 00:32:43 +02:00
local funcNumber = tonumber ( funcname )
2013-09-20 23:38:20 +02:00
2013-07-17 22:55:27 +02:00
if ( type ( fWithMethod ) == ' function ' ) then
resultData.func = fWithMethod
2013-07-29 12:18:39 +02:00
resultData.accessType = rqType
2013-09-20 23:38:20 +02:00
2013-07-17 22:55:27 +02:00
elseif ( type ( fGeneric ) == ' function ' ) then
resultData.func = fGeneric
2013-07-29 12:18:39 +02:00
resultData.accessType = ' ANY '
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
elseif funcNumber ~= nil then
2013-07-17 22:55:27 +02:00
resultData.func = mod [ GLOBAL_API_FUNCTION_NAME .. ' _ ' .. rqType ]
2013-07-29 12:18:39 +02:00
resultData.accessType = rqType
2013-09-20 23:38:20 +02:00
2013-07-29 12:18:39 +02:00
if not resultData.func then
resultData.func = mod [ GLOBAL_API_FUNCTION_NAME ]
resultData.accessType = ' ANY '
end
2013-09-20 23:38:20 +02:00
2013-07-17 08:06:04 +02:00
resultData.blankArg = funcNumber
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
else
2013-07-29 12:18:39 +02:00
local otherRqType = rqType == ' POST ' and ' GET ' or ' POST '
local fWithOtherMethod = mod [ funcname .. ' _ ' .. otherRqType ]
if ( type ( fWithOtherMethod ) == ' function ' ) then
-- error is indicated by leaving out 'func' key
resultData.accessType = otherRqType
else
-- error is indicated by leaving out 'func' key and adding 'notfound'=true
resultData.notfound = true
end
2013-07-10 00:32:43 +02:00
end
2013-09-20 23:38:20 +02:00
2013-07-17 08:06:04 +02:00
return resultData
2013-07-10 00:32:43 +02:00
end
2013-07-08 13:34:27 +02:00
setmetatable ( M , {
__call = function ( cls , ... )
return cls.new ( ... )
end
} )
2013-07-10 00:32:43 +02:00
--This function initializes itself using various environment variables, the arg array and the given postData
--NOTE: if debugging is enabled, commandline arguments 'm' and 'f' override requested module and function
2013-08-21 22:49:17 +02:00
function M . new ( environment , postData , debugEnabled )
2013-07-08 13:34:27 +02:00
local self = setmetatable ( { } , M )
2013-09-20 23:38:20 +02:00
2013-07-08 13:34:27 +02:00
--NOTE: is it correct to assume that absence of REQUEST_METHOD indicates command line invocation?
2013-08-21 22:49:17 +02:00
self.requestMethod = environment [ ' REQUEST_METHOD ' ]
if type ( self.requestMethod ) == ' string ' and self.requestMethod : len ( ) > 0 then
self.remoteHost = environment [ ' REMOTE_HOST ' ]
self.remotePort = environment [ ' REMOTE_PORT ' ]
self.userAgent = environment [ ' HTTP_USER_AGENT ' ]
2013-07-08 13:34:27 +02:00
else
2013-07-17 22:55:27 +02:00
self.requestMethod = ' CMDLINE '
2013-07-08 13:34:27 +02:00
end
2013-09-20 23:38:20 +02:00
2013-07-08 13:34:27 +02:00
self.cmdLineArgs = kvTableFromArray ( arg )
2013-08-21 22:49:17 +02:00
self.getArgs = kvTableFromUrlEncodedString ( environment [ ' QUERY_STRING ' ] )
2013-07-08 13:34:27 +02:00
self.postArgs = kvTableFromUrlEncodedString ( postData )
2013-08-21 22:49:17 +02:00
self.pathArgs = arrayFromPath ( environment [ ' PATH_INFO ' ] )
2013-09-20 23:38:20 +02:00
2013-08-20 22:38:16 +02:00
-- override path arguments with command line parameter and allow to emulate GET/POST if debugging is enabled *and* if the autowifi special command wasn't mentioned
if debugEnabled and self.requestMethod == ' CMDLINE ' and self : get ( ' autowifi ' ) == nil then
2013-07-17 22:55:27 +02:00
self.pathArgs = arrayFromPath ( self.cmdLineArgs [ ' p ' ] )
2013-09-20 23:38:20 +02:00
2013-07-29 13:48:56 +02:00
if self.cmdLineArgs [ ' r ' ] == ' GET ' or self.cmdLineArgs [ ' r ' ] == nil then
2013-07-17 22:55:27 +02:00
self.requestMethod = ' GET '
self.getArgs = self.cmdLineArgs
self.getArgs . p , self.getArgs . r = nil , nil
elseif self.cmdLineArgs [ ' r ' ] == ' POST ' then
self.requestMethod = ' POST '
self.postArgs = self.cmdLineArgs
self.postArgs . p , self.postArgs . r = nil , nil
end
2013-07-10 00:32:43 +02:00
end
2013-07-17 08:06:04 +02:00
table.remove ( self.pathArgs , 1 ) --drop the first 'empty' field caused by the opening slash of the query string
2013-09-20 23:38:20 +02:00
2013-07-11 10:30:59 +02:00
if # self.pathArgs >= 1 then self.requestedApiModule = self.pathArgs [ 1 ] end
if # self.pathArgs >= 2 then self.requestedApiFunction = self.pathArgs [ 2 ] end
2013-09-20 23:38:20 +02:00
2013-07-17 22:55:27 +02:00
if self.requestedApiModule == ' ' then self.requestedApiModule = nil end
if self.requestedApiFunction == ' ' then self.requestedApiFunction = nil end
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
-- Perform module/function resolution
2013-07-29 12:18:39 +02:00
local rData = resolveApiFunction ( self : getRequestedApiModule ( ) , self : getRequestedApiFunction ( ) , self.requestMethod )
2013-08-01 16:29:17 +02:00
local modFuncInfo = ( self : getRequestedApiModule ( ) or " <> " ) .. " / " .. ( self : getRequestedApiFunction ( ) or " <> " )
2013-09-20 23:38:20 +02:00
2013-07-29 12:18:39 +02:00
if rData.func ~= nil then --function (possibly the global one) could be resolved
2013-07-17 08:06:04 +02:00
self.resolvedApiFunction = rData.func
if rData.blankArg ~= nil then --apparently it was the global one, and we received a 'blank argument'
self : setBlankArgument ( rData.blankArg )
2013-07-10 00:32:43 +02:00
self.realApiFunctionName = GLOBAL_API_FUNCTION_NAME
else --resolved without blank argument but still potentially the global function, hence the _or_ construction
2013-07-11 10:30:59 +02:00
if self : getRequestedApiFunction ( ) ~= nil then
self.realApiFunctionName = self : getRequestedApiFunction ( )
if # self.pathArgs >= 3 then self : setBlankArgument ( self.pathArgs [ 3 ] ) end --aha, we have both a function and a blank argument
else
self.realApiFunctionName = GLOBAL_API_FUNCTION_NAME
end
2013-07-10 00:32:43 +02:00
end
2013-07-29 12:18:39 +02:00
elseif rData.notfound == true then
2013-07-29 13:48:56 +02:00
self.resolutionError = " module/function ' " .. modFuncInfo .. " ' does not exist "
2013-07-10 00:32:43 +02:00
else
2013-07-29 13:48:56 +02:00
self.resolutionError = " module/function ' " .. modFuncInfo .. " ' can only be accessed with the " .. rData.accessType .. " method "
2013-07-08 13:34:27 +02:00
end
2013-09-20 23:38:20 +02:00
2013-07-08 13:34:27 +02:00
return self
end
2013-07-17 08:06:04 +02:00
function M : getRequestMethod ( ) return self.requestMethod end --returns either GET or POST or CMDLINE
function M : getRequestedApiModule ( ) return self.requestedApiModule end
function M : getRequestedApiFunction ( ) return self.requestedApiFunction end
function M : getRealApiFunctionName ( ) return self.realApiFunctionName end
function M : getBlankArgument ( ) return self.blankArgument end
function M : setBlankArgument ( arg ) self.blankArgument = arg end
2013-07-08 13:34:27 +02:00
function M : getRemoteHost ( ) return self.remoteHost or " " end
function M : getRemotePort ( ) return self.remotePort or 0 end
function M : getUserAgent ( ) return self.userAgent or " " end
function M : get ( key )
2013-07-17 22:55:27 +02:00
if self.requestMethod == ' GET ' then
2013-07-08 13:34:27 +02:00
return self.getArgs [ key ]
2013-07-17 22:55:27 +02:00
elseif self.requestMethod == ' POST ' then
2013-07-08 13:34:27 +02:00
return self.postArgs [ key ]
2013-07-17 22:55:27 +02:00
elseif self.requestMethod == ' CMDLINE ' then
2013-07-08 13:34:27 +02:00
return self.cmdLineArgs [ key ]
else
return nil
end
2013-07-05 17:26:39 +02:00
end
2013-07-08 13:34:27 +02:00
function M : getAll ( )
2013-07-17 22:55:27 +02:00
if self.requestMethod == ' GET ' then
2013-07-08 13:34:27 +02:00
return self.getArgs
2013-07-17 22:55:27 +02:00
elseif self.requestMethod == ' POST ' then
2013-07-08 13:34:27 +02:00
return self.postArgs
2013-07-17 22:55:27 +02:00
elseif self.requestMethod == ' CMDLINE ' then
2013-07-08 13:34:27 +02:00
return self.cmdLineArgs
else
return nil
end
end
2013-07-11 10:30:59 +02:00
function M : getPathData ( )
return self.pathArgs
end
2013-07-09 01:49:56 +02:00
--returns either a response object+nil, or response object+errmsg
function M : handle ( )
2013-07-10 00:32:43 +02:00
local modname = self : getRequestedApiModule ( )
local resp = ResponseClass.new ( self )
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
if ( self.resolvedApiFunction ~= nil ) then --we found a function (possible the global function)
--invoke the function
2013-07-09 01:49:56 +02:00
local ok , r
2013-07-17 22:55:27 +02:00
if confDefaults.DEBUG_PCALLS then ok , r = true , self.resolvedApiFunction ( self , resp )
2013-07-10 00:32:43 +02:00
else ok , r = pcall ( self.resolvedApiFunction , self , resp )
2013-07-09 01:49:56 +02:00
end
2013-09-20 23:38:20 +02:00
2013-07-10 00:32:43 +02:00
--handle the result
2013-07-09 01:49:56 +02:00
if ok == true then
2013-07-10 00:32:43 +02:00
return resp , nil
2013-07-09 01:49:56 +02:00
else
2013-07-10 00:32:43 +02:00
resp : setError ( " call to function ' " .. modname .. " / " .. self.realApiFunctionName .. " ' failed " )
return resp , ( " calling function ' " .. self.realApiFunctionName .. " ' in API module ' " .. modname .. " ' somehow failed (' " .. r .. " ') " )
2013-07-09 01:49:56 +02:00
end
else
2013-07-29 12:18:39 +02:00
resp : setError ( " cannot call function or module ' " .. ( modname or " <empty> " ) .. " / " .. ( self : getRequestedApiFunction ( ) or " <empty> " ) .. " ' (' " .. self.resolutionError .. " ') " )
return resp , ( " cannot call requested API function (' " .. self.resolutionError .. " ') " )
2013-07-09 01:49:56 +02:00
end
2013-09-20 23:38:20 +02:00
2013-07-09 01:49:56 +02:00
return resp
end
2013-07-08 13:34:27 +02:00
return M