From 7711526a7c5f541e1d3f01a7b908ade3be96f6b9 Mon Sep 17 00:00:00 2001 From: DanyLE Date: Wed, 25 Jan 2023 17:01:01 +0100 Subject: [PATCH] Add unit tests and correct bugs detected by the tests --- Makefile.am | 6 +- modules/enc.c | 2 +- modules/lua/lualib.h | 28 +++ modules/slice.c | 30 ++- modules/sqlitedb.c | 34 +++- silkmvc/BaseController.lua | 3 +- silkmvc/BaseModel.lua | 7 +- silkmvc/BaseObject.lua | 33 +-- silkmvc/DBHelper.lua | 144 ------------- silkmvc/Logger.lua | 51 +++-- silkmvc/Router.lua | 16 +- silkmvc/api.lua | 86 ++++---- silkmvc/core/OOP.lua | 33 ++- silkmvc/core/api.lua | 260 ----------------------- silkmvc/core/cif.lua | 63 ------ silkmvc/core/extra_mime.lua | 79 ------- silkmvc/core/hook.lua | 210 +++++++++++++++++++ silkmvc/core/mimes.lua | 115 +++++++++++ silkmvc/core/sqlite.lua | 299 +++++++++++++++------------ silkmvc/core/std.lua | 272 ++++++++++++++----------- silkmvc/core/utils.lua | 292 ++++++++++++++++---------- silkmvc/router.lua.tpl | 16 +- test/ad.ls | 3 + test/detail.ls | 3 + test/layout.ls | 4 + test/lunit.lua | 46 +++++ test/request.json | 37 ++++ test/test_core.lua | 396 ++++++++++++++++++++++++++++++++++++ test/test_silk.lua | 270 ++++++++++++++++++++++++ 29 files changed, 1814 insertions(+), 1024 deletions(-) delete mode 100644 silkmvc/DBHelper.lua delete mode 100644 silkmvc/core/api.lua delete mode 100644 silkmvc/core/cif.lua delete mode 100644 silkmvc/core/extra_mime.lua create mode 100644 silkmvc/core/hook.lua create mode 100644 silkmvc/core/mimes.lua create mode 100644 test/ad.ls create mode 100644 test/detail.ls create mode 100644 test/layout.ls create mode 100644 test/lunit.lua create mode 100644 test/request.json create mode 100644 test/test_core.lua create mode 100644 test/test_silk.lua diff --git a/Makefile.am b/Makefile.am index bd58518..8b0abd0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,16 +22,14 @@ silk_DATA = silkmvc/router.lua.tpl \ silkmvc/Template.lua \ silkmvc/Logger.lua \ silkmvc/BaseObject.lua \ - silkmvc/DBHelper.lua \ silkmvc/api.lua coredir = $(libdir)/lua/silk/core core_DATA = silkmvc/core/OOP.lua \ silkmvc/core/std.lua \ - silkmvc/core/extra_mime.lua \ + silkmvc/core/mimes.lua \ silkmvc/core/utils.lua \ - silkmvc/core/api.lua \ - silkmvc/core/cif.lua \ + silkmvc/core/hook.lua \ silkmvc/core/sqlite.lua # lua libraris & modules diff --git a/modules/enc.c b/modules/enc.c index af365fc..ef5c0c5 100644 --- a/modules/enc.c +++ b/modules/enc.c @@ -208,7 +208,7 @@ static int l_base64_decode(lua_State *L) // decode data to a byte array lua_new_slice(L, len); slice_t *vec = NULL; - vec = lua_check_slice(L, 2); + vec = lua_check_slice(L, -1); len = Base64decode((char *)vec->data, s); vec->len = len; // lua_pushstring(L,dst); diff --git a/modules/lua/lualib.h b/modules/lua/lualib.h index a1e9529..ffacbc6 100644 --- a/modules/lua/lualib.h +++ b/modules/lua/lualib.h @@ -14,15 +14,43 @@ #define SLICE "slice" typedef struct { + size_t magic; size_t len; uint8_t* data; } slice_t; + +#ifndef LUA_SLICE_MAGIC +/** + * @brief Send data to the server via fastCGI protocol + * This function is defined by the luad fcgi server + * + * @param fd the socket fd + * @param id the request id + * @param ptr data pointer + * @param size data size + * @return int + */ +int fcgi_send_slice(int fd, uint16_t id, uint8_t* ptr, size_t size); + +/** + * @brief Get the magic number of the slice + * This value is defined by the luad fastCGI server + * + * @return size_t + */ +size_t lua_slice_magic(); +#else +#define lua_slice_magic() (LUA_SLICE_MAGIC) +#define fcgi_send_slice(fd,id,ptr,size) (-1) +#endif + void lua_new_slice(lua_State*L, int n) { size_t nbytes = sizeof(slice_t) + n * 1U; slice_t *a = (slice_t *)lua_newuserdata(L, nbytes); a->data = &((char *)a)[sizeof(slice_t)]; + a->magic = lua_slice_magic(); luaL_getmetatable(L, SLICE); lua_setmetatable(L, -2); diff --git a/modules/slice.c b/modules/slice.c index 93a67be..94ef2b8 100644 --- a/modules/slice.c +++ b/modules/slice.c @@ -1,4 +1,5 @@ #include "lua/lualib.h" +static int l_slice_send_to(lua_State* L); void lua_new_light_slice(lua_State *L, int n, char *ptr) { @@ -6,6 +7,7 @@ void lua_new_light_slice(lua_State *L, int n, char *ptr) slice_t *a = (slice_t *)lua_newuserdata(L, nbytes); a->len = n; a->data = ptr; + a->magic = lua_slice_magic(); luaL_getmetatable(L, SLICE); lua_setmetatable(L, -2); } @@ -78,6 +80,14 @@ static int l_slice_write(lua_State *L) return 1; } +static int l_slice_ptr(lua_State *L) +{ + slice_t *a = lua_check_slice(L, 1); + lua_pushnumber(L, (size_t)a); + return 1; +} + + static int l_slice_index(lua_State *L) { if(lua_isnumber(L,2)) @@ -91,10 +101,18 @@ static int l_slice_index(lua_State *L) { lua_pushcfunction(L, l_get_slice_size); } - else if(strcmp(string, "write") == 0) + else if(strcmp(string, "fileout") == 0) { lua_pushcfunction(L, l_slice_write); } + else if(strcmp(string,"out") == 0) + { + lua_pushcfunction(L, l_slice_send_to); + } + else if(strcmp(string,"ptr") == 0) + { + lua_pushcfunction(L, l_slice_ptr); + } else { lua_pushnil(L); @@ -120,6 +138,16 @@ static int l_slice_to_string(lua_State *L) return 1; } +static int l_slice_send_to(lua_State* L) +{ + slice_t *a = lua_check_slice(L, 1); + int fd = (int) luaL_checknumber(L, 2); + uint16_t id = (uint16_t) luaL_checknumber(L, 3); + + lua_pushboolean(L, fcgi_send_slice(fd, id, a->data, a->len) == 0); + return 1; +} + static const struct luaL_Reg slicemetalib[] = { {"unew", l_new_lightslice}, {"new", l_new_slice}, diff --git a/modules/sqlitedb.c b/modules/sqlitedb.c index d89e4d8..dbe8f2f 100644 --- a/modules/sqlitedb.c +++ b/modules/sqlitedb.c @@ -83,6 +83,9 @@ static int l_db_query(lua_State *L) int cols = sqlite3_column_count(statement); int result = 0; int cnt = 1; + uint8_t* data = NULL; + size_t len = 0; + slice_t* vec = NULL; // new table for data lua_newtable(L); while ((result = sqlite3_step(statement)) == SQLITE_ROW) @@ -91,10 +94,37 @@ static int l_db_query(lua_State *L) lua_newtable(L); for (int col = 0; col < cols; col++) { - const char *value = (const char *)sqlite3_column_text(statement, col); const char *name = sqlite3_column_name(statement, col); lua_pushstring(L, name); - lua_pushstring(L, value); + int type = sqlite3_column_type(statement,col); + switch (type) + { + case SQLITE_INTEGER: + lua_pushnumber(L, sqlite3_column_int64(statement,col)); + break; + + case SQLITE_FLOAT: + lua_pushnumber(L, sqlite3_column_double(statement,col)); + break; + case SQLITE_BLOB: + data = (uint8_t*)sqlite3_column_blob(statement, col); + len = sqlite3_column_bytes(statement, col); + if(len > 0) + { + lua_new_slice(L, len); + vec = lua_check_slice(L, -1); + (void)memcpy(vec->data, data, len); + } + else + { + lua_pushnil(L); + } + break; + default: + lua_pushstring(L, (const char *)sqlite3_column_text(statement, col)); + break; + } + lua_settable(L, -3); } lua_settable(L, -3); diff --git a/silkmvc/BaseController.lua b/silkmvc/BaseController.lua index 29644ad..352a2f4 100644 --- a/silkmvc/BaseController.lua +++ b/silkmvc/BaseController.lua @@ -41,11 +41,12 @@ function BaseController:switchLayout(name) if self.main then self.registry.layout = name else - self:log("Cannot switch layout since the controller "..self.class.." is not the main controller") + self:warn("Cannot switch layout since the controller "..self.class.." is not the main controller") end end function BaseController:setSession(key, value) + SESSION[key] = value end function BaseController:getSession(key) diff --git a/silkmvc/BaseModel.lua b/silkmvc/BaseModel.lua index f26b304..e1d096b 100644 --- a/silkmvc/BaseModel.lua +++ b/silkmvc/BaseModel.lua @@ -30,9 +30,7 @@ function BaseModel:find(cond) end function BaseModel:get(id) - local data, order = self:find({exp = {["="] = {id = id}}}) - if not data or #order == 0 then return false end - return data[1] + return self.db:get(self.name, id) end function BaseModel:findAll() @@ -46,6 +44,7 @@ function BaseModel:query(sql) end function BaseModel:select(sel, sql_cnd) - if self.db then return self.db:select(self.name, sel, sql_cnd) end + local sql = string.format("SELECT %s FROM %s WHERE %s;", sel, self.name, sql_cnd) + if self.db then return self.db:query(sql) end return nil end diff --git a/silkmvc/BaseObject.lua b/silkmvc/BaseObject.lua index efc5935..f1dd233 100644 --- a/silkmvc/BaseObject.lua +++ b/silkmvc/BaseObject.lua @@ -4,31 +4,34 @@ function BaseObject:subclass(name, args) _G[name].class = name end -function BaseObject:log(msg, level) - level = level or "INFO" +function BaseObject:log(level,msg,...) if self.registry.logger then - self.registry.logger:log(msg,level) + self.registry.logger:log(level, msg,...) end end -function BaseObject:debug(msg) - self:log(msg, "DEBUG") +function BaseObject:debug(msg,...) + self:log(Logger.DEBUG, msg,...) +end + +function BaseObject:info(msg,...) + self:log(Logger.INFO, msg,...) +end + +function BaseObject:warn(msg,...) + self:log(Logger.WARN, msg,...) end function BaseObject:print() - print(self.class) + self:debug(self.class) end -function BaseObject:error(msg, trace) +function BaseObject:error(msg,...) html() --local line = debug.getinfo(1).currentline - echo(msg) - self:log(msg,"ERROR") - if trace then - debug.traceback=nil - error(msg) - else - error(msg) - end + local emsg = string.format(msg or "ERROR",...) + echo(emsg) + self:log(Logger.ERROR, msg,...) + error(emsg) return false end \ No newline at end of file diff --git a/silkmvc/DBHelper.lua b/silkmvc/DBHelper.lua deleted file mode 100644 index 5ee9c3f..0000000 --- a/silkmvc/DBHelper.lua +++ /dev/null @@ -1,144 +0,0 @@ -sqlite = modules.sqlite() - -if sqlite == nil then return 0 end --- create class -BaseObject:subclass("DBHelper", {db = {}}) - -function DBHelper:createTable(tbl, m) - if self:available(tbl) then return true end - local sql = "CREATE TABLE " .. tbl .. "(id INTEGER PRIMARY KEY" - for k, v in pairs(m) do - if k ~= "id" then sql = sql .. "," .. k .. " " .. v end - end - sql = sql .. ");" - return sqlite.query(self.db, sql) == 1 -end - -function DBHelper:insert(tbl, m) - local keys = {} - local values = {} - for k, v in pairs(m) do - if k ~= "id" then - table.insert(keys, k) - if type(v) == "number" then - table.insert(values, v) - else - local t = "\"" .. v:gsub('"', '""') .. "\"" - table.insert(values, t) - end - end - end - local sql = "INSERT INTO " .. tbl .. " (" .. table.concat(keys, ',') .. - ') VALUES (' - sql = sql .. table.concat(values, ',') .. ');' - return sqlite.query(self.db, sql) == 1 -end - -function DBHelper:get(tbl, id) - return sqlite.select(self.db, tbl, "*", "id=" .. id)[1] -end - -function DBHelper:getAll(tbl) - local data = sqlite.select(self.db, tbl, "*", "1=1") - if data == nil then return nil end - local a = {} - for n in pairs(data) do table.insert(a, n) end - table.sort(a) - return data, a -end - -function DBHelper:find(tbl, cond) - local cnd = "1=1" - local sel = "*" - if cond.exp then cnd = self:gencond(cond.exp) end - if cond.order then - cnd = cnd .. " ORDER BY " - local l = {} - local i = 1 - for k, v in pairs(cond.order) do - l[i] = k .. " " .. v - i = i + 1 - end - cnd = cnd .. table.concat(l, ",") - end - if cond.limit then cnd = cnd .. " LIMIT " .. cond.limit end - if cond.fields then - sel = table.concat(cond.fields, ",") - -- print(sel) - end - local data = sqlite.select(self.db, tbl, sel, cnd) - if data == nil then return nil end - local a = {} - for n in pairs(data) do table.insert(a, n) end - table.sort(a) - return data, a -end - -function DBHelper:select(tbl, sel, cnd) - local data = sqlite.select(self.db, tbl, sel, cnd) - if data == nil then return nil end - local a = {} - for n in pairs(data) do table.insert(a, n) end - table.sort(a) - return data, a -end - -function DBHelper:query(sql) return sqlite.query(self.db, sql) == 1 end - -function DBHelper:update(tbl, m) - local id = m['id'] - if id ~= nil then - local lst = {} - for k, v in pairs(m) do - if (type(v) == "number") then - table.insert(lst, k .. "=" .. v) - else - table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"") - end - end - local sql = "UPDATE " .. tbl .. " SET " .. table.concat(lst, ",") .. - " WHERE id=" .. id .. ";" - return sqlite.query(self.db, sql) == 1 - end - return false -end - -function DBHelper:available(tbl) return sqlite.hasTable(self.db, tbl) == 1 end -function DBHelper:deleteByID(tbl, id) - local sql = "DELETE FROM " .. tbl .. " WHERE id=" .. id .. ";" - return sqlite.query(self.db, sql) == 1 -end -function DBHelper:gencond(o) - for k, v in pairs(o) do - if k == "and" or k == "or" then - local cnd = {} - local i = 1 - for k1, v1 in pairs(v) do - cnd[i] = self:gencond(v1) - i = i + 1 - end - return " (" .. table.concat(cnd, " " .. k .. " ") .. ") " - else - for k1, v1 in pairs(v) do - local t = type(v1) - if (t == "string") then - return - " (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') .. - '") ' - end - return " (" .. k1 .. " " .. k .. " " .. v1 .. ") " - end - end - end -end -function DBHelper:delete(tbl, cond) - local sql = "DELETE FROM " .. tbl .. " WHERE " .. self:gencond(cond) .. ";" - return sqlite.query(self.db, sql) == 1 -end - -function DBHelper:lastInsertID() return sqlite.lastInsertID(self.db) end - -function DBHelper:close() if self.db then sqlite.dbclose(self.db) end end -function DBHelper:open() - if self.db ~= nil then self.db = sqlite.getdb(self.db) end -end diff --git a/silkmvc/Logger.lua b/silkmvc/Logger.lua index 4cdcc5b..705d19b 100644 --- a/silkmvc/Logger.lua +++ b/silkmvc/Logger.lua @@ -1,30 +1,43 @@ -Logger = Object:extends{levels = {}} +Logger = Object:extends{} + +Logger.ERROR = 1 +Logger.WARN = 2 +Logger.INFO = 3 +Logger.DEBUG = 4 +Logger.handles = { + [1] = LOG_ERROR, + [2] = LOG_WARN, + [3] = LOG_INFO, + [4] = LOG_DEBUG +} function Logger:initialize() -end - -function Logger:log(msg,level) - if self.levels[level] and ulib.exists(LOG_ROOT) then - local path = LOG_ROOT..DIR_SEP..level..'.txt' - local f = io.open(path, 'a') - local text = '['..level.."]: "..msg - if f then - f:write(text..'\n') - f:close() - end - print(text) + if not self.level then + self.level = Logger.INFO end end -function Logger:info(msg) - self:log(msg, "INFO") +function Logger:log(verb,msg,...) + local level = verb + if level > self.level then return end + if level > Logger.DEBUG then + level = Logger.DEBUG + end + Logger.handles[level](msg,...) end -function Logger:debug(msg) - self:log(msg, "DEBUG") +function Logger:info(msg,...) + self:log(Logger.INFO, msg,...) end +function Logger:debug(msg,...) + self:log(Logger.DEBUG, msg,...) +end -function Logger:error(msg) - self:log(msg, "ERROR") +function Logger:error(msg,...) + self:log(Logger.ERROR, msg,...) +end + +function Logger:warn(msg,...) + self:log(Logger.WARN, msg,...) end \ No newline at end of file diff --git a/silkmvc/Router.lua b/silkmvc/Router.lua index 855bbe0..c73f225 100644 --- a/silkmvc/Router.lua +++ b/silkmvc/Router.lua @@ -7,6 +7,7 @@ end function Router:initialize() self.routes = {} self.remaps = {} + self.path = CONTROLLER_ROOT end --function Router:setArgs(args) @@ -25,7 +26,7 @@ function Router:infer(url) -- if user dont provide the url, try to infer it -- from the REQUEST url = url or REQUEST.r or "" - url = std.trim(url, "/") + url = ulib.trim(url, "/") local args = explode(url, "/") local data = { name = "index", @@ -79,7 +80,7 @@ function Router:infer(url) end end - self:log("Controller: " .. data.controller.class .. ", action: "..data.action..", args: ".. JSON.encode(data.args)) + self:info("Controller: " .. data.controller.class .. ", action: "..data.action..", args: ".. JSON.encode(data.args)) return data end @@ -90,7 +91,7 @@ function Router:delegate() data.controller.main = true views.__main__ = self:call(data) if not views.__main__ then - --self:error("No view available for this action") + self:info("No view available for action: %s:%s", data.controller.class, data.action) return end -- get all visible routes @@ -106,8 +107,8 @@ function Router:delegate() table.insert( view_args, k ) table.insert( view_argv, v ) end - - local fn, e = loadscript(VIEW_ROOT .. DIR_SEP .. self.registry.layout .. DIR_SEP .. "layout.ls", view_args) + local script_path = VIEW_ROOT .. DIR_SEP .. self.registry.layout .. DIR_SEP .. "layout.ls" + local fn, e = loadscript(script_path, view_args) html() if fn then local r, o = pcall(fn, table.unpack(view_argv)) @@ -116,7 +117,7 @@ function Router:delegate() end else e = e or "" - self:error("The index page is not found for layout: " .. self.registry.layout..": "..e) + self:error("The index page is not found for layout (%s: %s): %s" ,self.registry.layout, script_path,e) end end @@ -125,9 +126,8 @@ function Router:dependencies(url) return {} end local list = {} - --self:log("comparing "..url) for k, v in pairs(self.routes[self.registry.layout]) do - v.url = std.trim(v.url, "/") + v.url = ulib.trim(v.url, "/") if v.visibility == "ALL" then list[k] = v.url elseif v.visibility.routes then diff --git a/silkmvc/api.lua b/silkmvc/api.lua index 6dc685d..2bfa3f1 100644 --- a/silkmvc/api.lua +++ b/silkmvc/api.lua @@ -1,57 +1,51 @@ -require("OOP") -ulib = require("ulib") -require(BASE_FRW.."silk.BaseObject") -require(BASE_FRW.."silk.DBHelper") -require(BASE_FRW.."silk.Router") -require(BASE_FRW.."silk.BaseController") -require(BASE_FRW.."silk.BaseModel") -require(BASE_FRW.."silk.Logger") -require(BASE_FRW.."silk.Template") +require("silk.core.hook") +require("silk.core.OOP") +require("silk.core.sqlite") + +require("silk.BaseObject") +require("silk.Router") +require("silk.BaseController") +require("silk.BaseModel") +require("silk.Logger") +require("silk.Template") + +DIR_SEP = "/" -- mime type allows -- this will bypass the default server security -- the default list is from the server setting POLICY = {} POLICY.mimes = { - ["application/javascript"] = true, - ["image/bmp"] = true, - ["image/jpeg"] = true, - ["image/png"] = true, - ["text/css"] = true, - ["text/markdown"] = true, - ["text/csv"] = true, - ["application/pdf"] = true, - ["image/gif"] = true, - ["text/html"] = true, - ["application/json"] = true, - ["application/javascript"] = true, - ["image/x-portable-pixmap"] = true, - ["application/x-rar-compressed"] = true, - ["image/tiff"] = true, - ["application/x-tar"] = true, - ["text/plain"] = true, - ["application/x-font-ttf"] = true, - ["application/xhtml+xml"] = true, - ["application/xml"] = true, - ["application/zip"] = true, - ["image/svg+xml"] = true, - ["application/vnd.ms-fontobject"] = true, - ["application/x-font-woff"] = true, - ["application/x-font-otf"] = true, - ["audio/mpeg"] = true, + ["image/bmp"] = true, + ["image/jpeg"] = true, + ["image/png"] = true, + ["text/css"] = true, + ["text/markdown"] = true, + ["text/csv"] = true, + ["application/pdf"] = true, + ["image/gif"] = true, + ["text/html"] = true, + ["application/json"] = true, + ["application/javascript"] = true, + ["image/x-portable-pixmap"] = true, + ["application/x-rar-compressed"] = true, + ["image/tiff"] = true, + ["application/x-tar"] = true, + ["text/plain"] = true, + ["application/x-font-ttf"] = true, + ["application/xhtml+xml"] = true, + ["application/xml"] = true, + ["application/zip"] = true, + ["image/svg+xml"] = true, + ["application/vnd.ms-fontobject"] = true, + ["application/x-font-woff"] = true, + ["application/x-font-otf"] = true, + ["audio/mpeg"] = true } - -HEADER_FLAG = false - function html() - if not HEADER_FLAG then - std.chtml(SESSION) - HEADER_FLAG = true - end + if not RESPONSE_HEADER.sent then + std.html() + end end - -function import(module) - return require(BASE_FRW.."silk.api."..module) -end \ No newline at end of file diff --git a/silkmvc/core/OOP.lua b/silkmvc/core/OOP.lua index 182dacc..5cfd26c 100644 --- a/silkmvc/core/OOP.lua +++ b/silkmvc/core/OOP.lua @@ -1,40 +1,39 @@ Object = {} function Object:prototype(o) - o = o or {} -- create table if user does not provide one - setmetatable(o, self) - self.__index = self - return o + o = o or {} -- create table if user does not provide one + setmetatable(o, self) + self.__index = self + self.__tostring = o:tostring() + return o end function Object:new(o) - local obj = self:prototype(o) - obj:initialize() - return obj + local obj = self:prototype(o) + obj:initialize() + return obj +end + +function Object:tostring() + return "" end function Object:print() - print('an Object') + print(self:tostring()) end function Object:initialize() end function Object:asJSON() - return '{}' + return '{}' end function Object:inherit(o) - return self:prototype(o) + return self:prototype(o) end - function Object:extends(o) - return self:inherit(o) + return self:inherit(o) end -Test = Object:inherit{dummy = 0} - -function Test:toWeb() - wio.t(self.dummy) -end \ No newline at end of file diff --git a/silkmvc/core/api.lua b/silkmvc/core/api.lua deleted file mode 100644 index d5e36d6..0000000 --- a/silkmvc/core/api.lua +++ /dev/null @@ -1,260 +0,0 @@ -math.randomseed(os.clock()) -package.cpath = __api__.apiroot..'/?.so' -require("antd") -std = modules.std() -local read_header =function() - local l - repeat - l = std.antd_recv(HTTP_REQUEST.id) - if l and l ~= '\r' then - if l == "HTTP_REQUEST" or l == "request" or l == "COOKIE" or l == "REQUEST_HEADER" or l == "REQUEST_DATA" then - coroutine.yield(l, "LUA_TABLE") - else - local l1 = std.antd_recv(HTTP_REQUEST.id) - if l1 ~= '\r' then - coroutine.yield(l, l1) - end - l = l1 - end - end - until not l or l == '\r' -end - - -local read_headers = function() - local co = coroutine.create(function () read_header() end) - return function () -- iterator - local code, k, v = coroutine.resume(co) - return k,v - end -end - -local parse_headers =function() - local lut = { - HTTP_REQUEST = HTTP_REQUEST - } - local curr_tbl = "HTTP_REQUEST" - for k,v in read_headers() do - if v == "LUA_TABLE" then - if not lut[k] then - lut[k] = {} - end - curr_tbl = k - else - lut[curr_tbl][k] = v - end - end - HTTP_REQUEST.request = lut.request - HTTP_REQUEST.request.COOKIE = lut.COOKIE - HTTP_REQUEST.request.REQUEST_HEADER = lut.REQUEST_HEADER - HTTP_REQUEST.request.REQUEST_DATA = lut.REQUEST_DATA -end - --- parsing the header -parse_headers() --- root dir -__ROOT__ = HTTP_REQUEST.request.SERVER_WWW_ROOT --- set require path -package.path = __ROOT__ .. '/?.lua;'..__api__.apiroot..'/?.lua' -require("std") -require("utils") -require("extra_mime") -ulib = require("ulib") --- set session -SESSION = {} - -REQUEST = HTTP_REQUEST.request.REQUEST_DATA -REQUEST.method = HTTP_REQUEST.request.METHOD -if HTTP_REQUEST.request.COOKIE then - SESSION = HTTP_REQUEST.request.COOKIE -end -HEADER = HTTP_REQUEST.request.REQUEST_HEADER -HEADER.mobile = false - -if HEADER["User-Agent"] and HEADER["User-Agent"]:match("Mobi") then - HEADER.mobile = true -end - -function LOG_INFO(fmt,...) - ulib.syslog(5,string.format(fmt or "LOG",...)) -end - -function LOG_ERROR(fmt,...) - ulib.syslog(3,string.format(fmt or "ERROR",...)) -end - -function has_module(m) - if utils.file_exists(__ROOT__..'/'..m) then - if m:find("%.ls$") then - return true, true, __ROOT__..'/'..m - else - return true, false, m:gsub(".lua$","") - end - elseif utils.file_exists(__ROOT__..'/'..string.gsub(m,'%.','/')..'.lua') then - return true, false, m - elseif utils.file_exists(__ROOT__..'/'..string.gsub(m,'%.','/')..'.ls') then - return true, true, __ROOT__..'/'..string.gsub(m,'%.','/')..'.ls' - end - return false, false, nil -end - -function echo(m) - if m then std.t(m) else std.t("Undefined value") end -end - -function loadscript(file, args) - local f = io.open(file, "rb") - local content = "" - if f then - local html = "" - local pro = "local fn = function(...)" - local s,e, mt - local mtbegin = true -- find begin of scrit, 0 end of scrit - local i = 1 - if args then - pro = "local fn = function("..table.concat( args, ",")..")" - end - for line in io.lines(file) do - line = std.trim(line, " ") - if(line ~= "") then - if(mtbegin) then - mt = "^%s*<%?lua" - else - mt = "%?>%s*$" - end - s,e = line:find(mt) - if(s) then - if mtbegin then - if html ~= "" then - pro= pro.."echo(\""..utils.escape(html).."\")\n" - html = "" - end - local b,f = line:find("%?>%s*$") - if b then - pro = pro..line:sub(e+1,b-1).."\n" - else - pro = pro..line:sub(e+1).."\n" - mtbegin = not mtbegin - end - else - pro = pro..line:sub(0,s-1).."\n" - mtbegin = not mtbegin - end - else -- no match - if mtbegin then - -- detect if we have inline lua with format - local b,f = line:find("<%?=") - if b then - local tmp = line - pro= pro.."echo(" - while(b) do - -- find the close - local x,y = tmp:find("%?>") - if x then - pro = pro.."\""..utils.escape(html..tmp:sub(0,b-1):gsub("%%","%%%%")).."\".." - pro = pro..tmp:sub(f+1,x-1)..".." - html = "" - tmp = tmp:sub(y+1) - b,f = tmp:find("<%?=") - else - error("Syntax error near line "..i) - end - end - pro = pro.."\""..utils.escape(tmp:gsub("%%","%%%%")).."\")\n" - else - html = html..std.trim(line," "):gsub("%%","%%%%").."\n" - end - else - if line ~= "" then pro = pro..line.."\n" end - end - end - end - i = i+ 1 - end - f:close() - if(html ~= "") then - pro = pro.."echo(\""..utils.escape(html).."\")\n" - end - pro = pro.."\nend \n return fn" - local r,e = load(pro) - if r then return r(), e else return nil,e end - end -end - --- decode post data if any -local decode_request_data = function() - if (not REQUEST.method) - or (REQUEST.method ~= "POST" - and REQUEST.method ~= "PUT" - and REQUEST.method ~= "PATCH") - or (not REQUEST.HAS_RAW_BODY) then - return 0 - end - local ctype = HEADER['Content-Type'] - local clen = HEADER['Content-Length'] or -1 - if clen then - clen = tonumber(clen) - end - if not ctype or clen == -1 then - LOG_ERROR("Invalid content type %s or content length %d", ctype, clen) - return 400, "Bad Request, missing content description" - end - local raw_data, len = std.antd_recv(HTTP_REQUEST.id, clen) - if len ~= clen then - LOG_ERROR("Unable to read all data: read %d expected %d", len, clen) - return 400, "Bad Request, missing content data" - end - if ctype:find("application/json") then - REQUEST.json = bytes.__tostring(raw_data) - else - REQUEST[ctype] = raw_data - end - REQUEST.HAS_RAW_BODY = nil - return 0 -end - --- set compression level -local accept_encoding = HEADER["Accept-Encoding"] -if accept_encoding then - if accept_encoding:find("gzip") then - std.antd_set_zlevel(HTTP_REQUEST.id, "gzip") - elseif accept_encoding:find("deflate") then - std.antd_set_zlevel(HTTP_REQUEST.id, "deflate") - end -end - -local code, error = decode_request_data() - -if code ~= 0 then - LOG_ERROR(error) - std.error(code, error) - return -end - --- LOG_INFO(JSON.encode(REQUEST)) - --- OOP support ---require("OOP") --- load sqlite helper ---require("sqlite") --- enable extra mime - --- run the file - - -local m, s, p = has_module(HTTP_REQUEST.request.RESOURCE_PATH) -if m then - -- run the correct module - if s then - local r,e = loadscript(p) - if r then r() else unknow(e) end - else - LOG_INFO("RUNNING MODULE %s", p) - require(p) - end -else - unknow("Resource not found for request "..HTTP_REQUEST.request.RESOURCE_PATH) -end - - ---require('router') diff --git a/silkmvc/core/cif.lua b/silkmvc/core/cif.lua deleted file mode 100644 index 7e0ebf3..0000000 --- a/silkmvc/core/cif.lua +++ /dev/null @@ -1,63 +0,0 @@ -FFI = require("ffi") -FFI.type = {} -FFI.type.VOID = 0 -FFI.type.UINT8 = 1 -FFI.type.SINT8 = 2 -FFI.type.UINT16 = 3 -FFI.type.SINT16 = 4 -FFI.type.UINT32 = 5 -FFI.type.SINT32 = 6 -FFI.type.UINT64 = 7 -FFI.type.SINT64 = 8 -FFI.type.FLOAT = 9 -FFI.type.DOUBLE = 10 -FFI.type.UCHAR = 11 -FFI.type.SCHAR = 12 -FFI.type.USHORT = 13 -FFI.type.SSHORT = 14 -FFI.type.UINT = 15 -FFI.type.SINT = 16 -FFI.type.ULONG = 17 -FFI.type.SLONG = 18 -FFI.type.LONGDOUBLE = 19 -FFI.type.POINTER = 20 -FFI.cache = {} - -FFI.load = function(path) - if FFI.cache[path] then - return FFI.cache[path] - else - print("Loading: "..path) - local lib = FFI.dlopen(path) - if lib then - FFI.cache[path] = {ref = lib, fn= {}} - end - return FFI.cache[path] - end -end - -FFI.unload = function(path) - local lib = FFI.cache[path] - if lib then - FFI.dlclose(lib.ref) - FFI.cache[path] = false - end -end - -FFI.unloadAll = function() - for k,v in pairs(FFI.cache) do - FFI.dlclose(v.ref) - end - FFI.cache = {} -end - -FFI.lookup = function(lib, name) - local fn = lib.fn[name] - if fn then return fn end - fn = FFI.dlsym(lib.ref, name) - if fn then - lib.fn[name] = fn - return fn - end - return nil -end \ No newline at end of file diff --git a/silkmvc/core/extra_mime.lua b/silkmvc/core/extra_mime.lua deleted file mode 100644 index da97300..0000000 --- a/silkmvc/core/extra_mime.lua +++ /dev/null @@ -1,79 +0,0 @@ -function std.extra_mime(name) - local ext = std.ext(name) - local mpath = __ROOT__.."/".."mimes.json" - local xmimes = {} - if utils.file_exists(mpath) then - local f = io.open(mpath, "r") - if f then - xmimes = JSON.decodeString(f:read("*all")) - f:close() - end - end - if(name:find("Makefile$")) then return "text/makefile",false - elseif ext == "php" then return "text/php",false - elseif ext == "c" or ext == "h" then return "text/c",false - elseif ext == "cpp" or ext == "hpp" then return "text/cpp",false - elseif ext == "md" then return "text/markdown",false - elseif ext == "lua" then return "text/lua",false - elseif ext == "yml" then return "application/x-yaml", false - elseif xmimes[ext] then return xmimes[ext].mime, xmimes[ext].binary - --elseif ext == "pgm" then return "image/x-portable-graymap", true - else - return "application/octet-stream",true - end -end - -function std.mimeOf(name) - local mime = std.mime(name) - if mime ~= "application/octet-stream" then - return mime - else - return std.extra_mime(name) - end -end - ---[[ function std.isBinary(name) - local mime = std.mime(name) - if mime ~= "application/octet-stream" then - return std.is_bin(name) - else - local xmime,bin = std.extra_mime(name) - return bin - end -end ]] - -function std.sendFile(m) - local mime = std.mimeOf(m) - local finfo = ulib.file_stat(m) - local len = tostring(math.floor(finfo.size)) - local len1 = tostring(math.floor(finfo.size - 1)) - if mime == "audio/mpeg" then - std.status(200) - std.header("Pragma", "public") - std.header("Expires", "0") - std.header("Content-Type", mime) - std.header("Content-Length", len) - std.header("Content-Disposition", "inline; filename=" .. std.basename(m)) - std.header("Content-Range:", "bytes 0-" .. len1 .. "/" .. len) - std.header("Accept-Ranges", "bytes") - std.header("X-Pad", "avoid browser bug") - std.header("Content-Transfer-Encoding", "binary") - std.header("Cache-Control", "no-cache, no-store") - std.header("Connection", "Keep-Alive") - std.header_flush() - std.f(m) - else - if HEADER['If-Modified-Since'] and HEADER['If-Modified-Since'] == finfo.ctime then - std.status(304) - std.header_flush() - else - std.status(200) - std.header("Content-Type", mime) - --std.header("Content-Length", len) - std.header("Cache-Control", "no-cache") - std.header("Last-Modified", finfo.ctime) - std.header_flush() - std.f(m) - end - end -end diff --git a/silkmvc/core/hook.lua b/silkmvc/core/hook.lua new file mode 100644 index 0000000..bad7f4b --- /dev/null +++ b/silkmvc/core/hook.lua @@ -0,0 +1,210 @@ +math.randomseed(os.time()) +-- define some legacy global variables to provide backward support +-- for some old web application +__api__ = { + apiroot = string.format("%s/lua", _SERVER["LIB_DIR"]), + tmpdir = _SERVER["TMP_DIR"], + dbpath = _SERVER["DB_DIR"] +} +-- root dir +__ROOT__ = _SERVER["DOCUMENT_ROOT"] +-- set require path +package.cpath = __api__.apiroot .. '/?.so' +package.path = string.format("%s/?.lua;%s/?.lua",__api__.apiroot,__ROOT__) + +ulib = require("ulib") +slice = require("slice") +require("silk.core.utils") +require("silk.core.std") + + +-- global helper functions for lua page script +function has_module(m) + if utils.file_exists(__ROOT__ .. '/' .. m) then + if m:find("%.ls$") then + return true, true, __ROOT__ .. '/' .. m + else + return true, false, m:gsub(".lua$", "") + end + elseif utils.file_exists(__ROOT__ .. '/' .. string.gsub(m, '%.', '/') .. '.lua') then + return true, false, m + elseif utils.file_exists(__ROOT__ .. '/' .. string.gsub(m, '%.', '/') .. '.ls') then + return true, true, __ROOT__ .. '/' .. string.gsub(m, '%.', '/') .. '.ls' + end + return false, false, nil +end + +--- Send data to client +function echo(...) + fcgio:echo(...) +end + +--- luad lua page script +function loadscript(file, args) + local f = io.open(file, "rb") + local content = "" + if f then + local html = "" + local pro = "local fn = function(...)" + local s, e, mt + local mtbegin = true -- find begin of scrit, 0 end of scrit + local i = 1 + if args then + pro = "local fn = function(" .. table.concat(args, ",") .. ")" + end + for line in io.lines(file) do + line = ulib.trim(line, " ") + if (line ~= "") then + if (mtbegin) then + mt = "^%s*<%?lua" + else + mt = "%?>%s*$" + end + s, e = line:find(mt) + if (s) then + if mtbegin then + if html ~= "" then + pro = pro .. "echo(\"" .. utils.escape(html) .. "\")\n" + html = "" + end + local b, f = line:find("%?>%s*$") + if b then + pro = pro .. line:sub(e + 1, b - 1) .. "\n" + else + pro = pro .. line:sub(e + 1) .. "\n" + mtbegin = not mtbegin + end + else + pro = pro .. line:sub(0, s - 1) .. "\n" + mtbegin = not mtbegin + end + else -- no match + if mtbegin then + -- detect if we have inline lua with format + local b, f = line:find("<%?=") + if b then + local tmp = line + pro = pro .. "echo(" + while (b) do + -- find the close + local x, y = tmp:find("%?>") + if x then + pro = pro .. "\"" .. utils.escape(html .. tmp:sub(0, b - 1):gsub("%%", "%%%%")) .. + "\".." + pro = pro .. tmp:sub(f + 1, x - 1) .. ".." + html = "" + tmp = tmp:sub(y + 1) + b, f = tmp:find("<%?=") + else + error("Syntax error near line " .. i) + end + end + pro = pro .. "\"" .. utils.escape(tmp:gsub("%%", "%%%%")) .. "\")\n" + else + html = html .. ulib.trim(line, " "):gsub("%%", "%%%%") .. "\n" + end + else + if line ~= "" then + pro = pro .. line .. "\n" + end + end + end + end + i = i + 1 + end + f:close() + if (html ~= "") then + pro = pro .. "echo(\"" .. utils.escape(html) .. "\")\n" + end + pro = pro .. "\nend \n return fn" + local r, e = load(pro) + if r then + return r(), e + else + return nil, e + end + end +end + +-- logging helpers +function LOG_INFO(fmt, ...) + fcgio:log_info(string.format(fmt or "LOG", ...)) +end + +function LOG_ERROR(fmt, ...) + fcgio:log_error(string.format(fmt or "ERROR", ...)) +end + +function LOG_DEBUG(fmt, ...) + fcgio:log_debug(string.format(fmt or "ERROR", ...)) +end + +function LOG_WARN(fmt, ...) + fcgio:log_warn(string.format(fmt or "ERROR", ...)) +end + +-- decode post data if any +local decode_request_data = function() + -- decode POST request data + if _SERVER["RAW_DATA"] then + if REQUEST.method == "POST" and HEADER["Content-Type"] == "application/x-www-form-urlencoded" then + for k, v in pairs(utils.parse_query(tostring(_SERVER["RAW_DATA"]))) do + REQUEST[k] = v + end + else + local ctype = HEADER['Content-Type'] + local clen = HEADER['Content-Length'] or -1 + if clen then + clen = tonumber(clen) + end + if not ctype or clen == -1 then + LOG_ERROR("Invalid content type %s or content length %d", ctype, clen) + return 400, "Bad Request, missing content description" + end + if ctype:find("application/json") then + REQUEST.json = tostring(_SERVER["RAW_DATA"]) + else + REQUEST[ctype] = _SERVER["RAW_DATA"] + end + end + end + return 0 +end + +-- define old fashion global request object +HEADER = {} +setmetatable(HEADER, { + __index = function(o, data) + local key = "HTTP_" .. string.upper(data:gsub("-", "_")) + return _SERVER[key] + end +}) +HEADER.mobile = false +if HEADER["User-Agent"] and HEADER["User-Agent"]:match("Mobi") then + HEADER.mobile = true +end + +REQUEST = {} +-- decode GET request +if _SERVER["QUERY_STRING"] then + REQUEST = utils.parse_query(_SERVER["QUERY_STRING"]) +end +REQUEST.method = _SERVER["REQUEST_METHOD"] + +-- set session +SESSION = {} +if HEADER["Cookie"] then + for key, val in HEADER["Cookie"]:gmatch("([^;=]+)=*([^;]*)") do + SESSION[ulib.trim(key," ")] = ulib.trim(val, " ") + end +end + +local code, error = decode_request_data() + +if code ~= 0 then + LOG_ERROR(error) + std.error(code, error) + return false +end + +return true diff --git a/silkmvc/core/mimes.lua b/silkmvc/core/mimes.lua new file mode 100644 index 0000000..798aaf8 --- /dev/null +++ b/silkmvc/core/mimes.lua @@ -0,0 +1,115 @@ +local default_mimes = { + ["bmp"] = "image/bmp", + ["jpg"] = "image/jpeg", + ["jpeg"] = "image/jpeg", + ["css"] = "text/css", + ["md"] = "text/markdown", + ["csv"] = "text/csv", + ["pdf"] = "application/pdf", + ["gif"] = "image/gif", + ["html"] = "text/html", + ["htm"] = "text/html", + ["chtml"] = "text/html", + ["json"] = "application/json", + ["js"] = "application/javascript", + ["png"] = "image/png", + ["ppm"] = "image/x-portable-pixmap", + ["rar"] = "application/x-rar-compressed", + ["tiff"] = "image/tiff", + ["tar"] = "application/x-tar", + ["txt"] = "text/plain", + ["ttf"] = "application/x-font-ttf", + ["xhtml"] = "application/xhtml+xml", + ["xml"] = "application/xml", + ["zip"] = "application/zip", + ["svg"] = "image/svg+xml", + ["eot"] = "application/vnd.ms-fontobject", + ["woff"] = "application/x-font-woff", + ["woff2"] = "application/x-font-woff", + ["otf"] = "application/x-font-otf", + ["mp3"] = "audio/mpeg", + ["mpeg"] = "audio/mpeg" +} + +setmetatable(default_mimes, { + __index = function(this, key) + return "application/octet-stream" + end +}) +function std.mime(ext) + return default_mimes[ext] +end +function std.extra_mime(name) + local ext = utils.ext(name) + local mpath = __ROOT__ .. "/" .. "mimes.json" + local xmimes = {} + if utils.file_exists(mpath) then + xmimes = JSON.decodeFile(mpath) + end + if (name:find("Makefile$")) then + return "text/makefile", false + elseif ext == "php" then + return "text/php", false + elseif ext == "c" or ext == "h" then + return "text/c", false + elseif ext == "cpp" or ext == "hpp" then + return "text/cpp", false + elseif ext == "md" then + return "text/markdown", false + elseif ext == "lua" then + return "text/lua", false + elseif ext == "yml" then + return "application/x-yaml", false + elseif xmimes[ext] then + return xmimes[ext].mime, xmimes[ext].binary + -- elseif ext == "pgm" then return "image/x-portable-graymap", true + else + return "application/octet-stream", true + end +end + +function std.mimeOf(name) + local mime = std.mime(utils.ext(name)) + if mime ~= "application/octet-stream" then + return mime + else + return std.extra_mime(name) + end +end + + +function std.sendFile(m) + local mime = std.mimeOf(m) + local finfo = ulib.file_stat(m) + local len = tostring(math.floor(finfo.size)) + local len1 = tostring(math.floor(finfo.size - 1)) + if mime == "audio/mpeg" then + std.status(200) + std.header("Pragma", "public") + std.header("Expires", "0") + std.header("Content-Type", mime) + std.header("Content-Length", len) + std.header("Content-Disposition", "inline; filename=" .. utils.basename(m)) + std.header("Content-Range:", "bytes 0-" .. len1 .. "/" .. len) + std.header("Accept-Ranges", "bytes") + std.header("X-Pad", "avoid browser bug") + std.header("Content-Transfer-Encoding", "binary") + std.header("Cache-Control", "no-cache, no-store") + std.header("Connection", "Keep-Alive") + std.header_flush() + std.f(m) + else + if HEADER['If-Modified-Since'] and HEADER['If-Modified-Since'] == finfo.ctime then + std.status(304) + std.header_flush() + else + std.status(200) + std.header("Content-Type", mime) + -- std.header("Content-Length", len) + std.header("Cache-Control", "no-cache") + std.header("Last-Modified", finfo.ctime) + std.header_flush() + std.f(m) + end + end +end diff --git a/silkmvc/core/sqlite.lua b/silkmvc/core/sqlite.lua index 7d2d38e..c0408c6 100644 --- a/silkmvc/core/sqlite.lua +++ b/silkmvc/core/sqlite.lua @@ -1,157 +1,202 @@ -sqlite = modules.sqlite() +sqlite = require("sqlitedb") + +if sqlite == nil then + return 0 +end + +require("silk.core.OOP") + +sqlite.getdb = function(name) + if name:find("%.db$") then + return sqlite.db(name) + elseif name:find("/") then + LOG_ERROR("Invalid database name %s", name) + return nil + else + return sqlite.db(__api__.dbpath .. "/" .. name .. ".db") + end +end -if sqlite == nil then return 0 end -require("OOP") -- create class -DBModel = Object:inherit{db=nil, name=''} +DBModel = Object:inherit{ + db = nil, +} -function DBModel:createTable(m) - if self:available() then return true end - local sql = "CREATE TABLE "..self.name.."(id INTEGER PRIMARY KEY" - for k, v in pairs(m) do - if k ~= "id" then - sql = sql..","..k.." "..v - end - end - sql = sql..");" - return sqlite.query(self.db,sql) == 1 +function DBModel:createTable(name, m) + if self:available() then + return true + end + local sql = "CREATE TABLE " .. name .. "(id INTEGER PRIMARY KEY" + for k, v in pairs(m) do + if k ~= "id" then + sql = sql .. "," .. k .. " " .. v + end + end + sql = sql .. ");" + return self:exec(sql) end -function DBModel:insert(m) - local keys = {} - local values = {} - for k,v in pairs(m) do - if k ~= "id" then - table.insert(keys,k) - if type(v) == "number" then - table.insert(values, v) - elseif type(v) == "boolean" then - table.insert( values, v and 1 or 0 ) - else - local t = "\""..v:gsub('"', '""').."\"" - table.insert(values,t) - end - end - end - local sql = "INSERT INTO "..self.name.." ("..table.concat(keys,',')..') VALUES (' - sql = sql..table.concat(values,',')..');' - return sqlite.query(self.db, sql) == 1 +function DBModel:insert(name, m) + local keys = {} + local values = {} + for k, v in pairs(m) do + if k ~= "id" then + table.insert(keys, k) + if type(v) == "number" then + table.insert(values, v) + elseif type(v) == "boolean" then + table.insert(values, v and 1 or 0) + else + local t = "\"" .. v:gsub('"', '""') .. "\"" + table.insert(values, t) + end + end + end + local sql = "INSERT INTO " .. name .. " (" .. table.concat(keys, ',') .. ') VALUES (' + sql = sql .. table.concat(values, ',') .. ');' + return self:exec(sql) end -function DBModel:get(id) - return sqlite.select(self.db, self.name, "*","id="..id)[1] +function DBModel:get(name, id) + local records = self:query( string.format("SELECT * FROM %s WHERE id=%d", name, id)) + if records and #records == 1 then + return records[1] + end + return nil end -function DBModel:getAll() - --local sql = "SELECT * FROM "..self.name - --return sqlite.select(self.db, self.name, "1=1") - local data = sqlite.select(self.db, self.name, "*", "1=1") - if data == nil then return nil end - local a = {} - for n in pairs(data) do table.insert(a, n) end - table.sort(a) - return data, a +function DBModel:getAll(name) + local data = self:query( "SELECT * FROM " .. name) + if not data then + return nil + end + local a = {} + for n in pairs(data) do + table.insert(a, n) + end + table.sort(a) + return data, a end -function DBModel:find(cond) - local cnd = "1=1" - local sel = "*" - if cond.exp then - cnd = self:gencond(cond.exp) - end - if cond.order then - cnd = cnd.." ORDER BY " - local l = {} - local i = 1 - for k,v in pairs(cond.order) do - l[i] = k.." "..v - i = i+1 - end - cnd = cnd..table.concat(l, ",") - end - if cond.limit then - cnd = cnd.." LIMIT "..cond.limit - end - if cond.fields then - sel = table.concat(cond.fields, ",") - --print(sel) - end - --print(cnd) - local data = sqlite.select(self.db, self.name, sel, cnd) - if data == nil then return nil end - local a = {} - for n in pairs(data) do table.insert(a, n) end - table.sort(a) - return data, a +function DBModel:find(name, cond) + local cnd = "1=1" + local sel = "*" + if cond.exp then + cnd = self:gencond(cond.exp) + end + if cond.order then + cnd = cnd .. " ORDER BY " + local l = {} + local i = 1 + for k, v in pairs(cond.order) do + l[i] = k .. " " .. v + i = i + 1 + end + cnd = cnd .. table.concat(l, ",") + end + if cond.limit then + cnd = cnd .. " LIMIT " .. cond.limit + end + if cond.fields then + sel = table.concat(cond.fields, ",") + -- print(sel) + end + -- print(cnd) + local data = self:query( string.format("SELECT %s FROM %s WHERE %s", sel, name, cnd)) + if data == nil then + return nil + end + local a = {} + for n in pairs(data) do + table.insert(a, n) + end + table.sort(a) + return data, a end function DBModel:query(sql) - return sqlite.query(self.db, sql) == 1 + local data, error = sqlite.query(self.db, sql) + --LOG_DEBUG(sql) + if not data then + LOG_ERROR("Error querying recorda SQL[%s]: %s", sql, error or "") + return nil + end + return data end -function DBModel:update(m) - local id = m['id'] - if id ~= nil then - local lst = {} - for k,v in pairs(m) do - if(type(v)== "number") then - table.insert(lst,k.."="..v) - elseif type(v) == "boolean" then - table.insert( lst, k.."="..(v and 1 or 0) ) - else - table.insert(lst,k.."=\""..v:gsub('"', '""').."\"") - end - end - local sql = "UPDATE "..self.name.." SET "..table.concat(lst,",").." WHERE id="..id..";" - return sqlite.query(self.db, sql) == 1 - end - return false +function DBModel:exec(sql) + --LOG_DEBUG(sql) + local ret, err = sqlite.exec(self.db, sql) + if not ret then + LOG_ERROR("Error execute [%s]: %s", sql, err or "") + end + return ret == true end -function DBModel:available() - return sqlite.hasTable(self.db, self.name) == 1 +function DBModel:update(name, m) + local id = m['id'] + if id ~= nil then + local lst = {} + for k, v in pairs(m) do + if (type(v) == "number") then + table.insert(lst, k .. "=" .. v) + elseif type(v) == "boolean" then + table.insert(lst, k .. "=" .. (v and 1 or 0)) + else + table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"") + end + end + local sql = "UPDATE " .. name .. " SET " .. table.concat(lst, ",") .. " WHERE id=" .. id .. ";" + return self:exec(sql) + end + return false end -function DBModel:deleteByID(id) - local sql = "DELETE FROM "..self.name.." WHERE id="..id..";" - return sqlite.query(self.db, sql) == 1 + +function DBModel:available(name) + local records = self:query(string.format("SELECT * FROM sqlite_master WHERE type='table' and name='%s'", name)) + return #records == 1 +end +function DBModel:deleteByID(name, id) + local sql = "DELETE FROM " .. name .. " WHERE id=" .. id .. ";" + return self:exec(sql) end function DBModel:gencond(o) - for k,v in pairs(o) do - if k == "and" or k == "or" then - local cnd = {} - local i = 1 - for k1,v1 in pairs(v) do - cnd[i] = self:gencond(v1) - i = i + 1 - end - return " ("..table.concat(cnd, " "..k.." ")..") " - else - for k1,v1 in pairs(v) do - local t = type(v1) - if(t == "string") then - return " ("..k1.." "..k..' "'..v1:gsub('"','""')..'") ' - end - return " ("..k1.." "..k.." "..v1..") " - end - end - end + for k, v in pairs(o) do + if k == "and" or k == "or" then + local cnd = {} + local i = 1 + for k1, v1 in pairs(v) do + cnd[i] = self:gencond(v1) + i = i + 1 + end + return " (" .. table.concat(cnd, " " .. k .. " ") .. ") " + else + for k1, v1 in pairs(v) do + local t = type(v1) + if (t == "string") then + return " (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') .. '") ' + end + return " (" .. k1 .. " " .. k .. " " .. v1 .. ") " + end + end + end end -function DBModel:delete(cond) - local sql = "DELETE FROM "..self.name.." WHERE "..self:gencond(cond)..";" - return sqlite.query(self.db, sql) == 1 +function DBModel:delete(name, cond) + local sql = "DELETE FROM " .. name .. " WHERE " .. self:gencond(cond) .. ";" + return self:exec(sql) end function DBModel:lastInsertID() - return sqlite.lastInsertID(self.db) + return sqlite.last_insert_id(self.db) end function DBModel:close() - if self.db then - sqlite.dbclose(self.db) - end + if self.db then + sqlite.dbclose(self.db) + end end function DBModel:open() - if self.db ~= nil then - self.db = sqlite.getdb(self.db) - end -end \ No newline at end of file + if self.db ~= nil then + self.db = sqlite.getdb(self.db) + end +end diff --git a/silkmvc/core/std.lua b/silkmvc/core/std.lua index 746d7a3..17d9a4a 100644 --- a/silkmvc/core/std.lua +++ b/silkmvc/core/std.lua @@ -1,171 +1,197 @@ -bytes = modules.bytes() -array = modules.array() - -modules.sqlite = function() - if not sqlite then - sqlite = require("sqlitedb") - sqlite.getdb = function(name) - if name:find("%.db$") then - return sqlite._getdb(name) - elseif name:find("/") then - LOG_ERROR("Invalid database name %s", name) - return nil - else - return sqlite._getdb(__api__.dbpath.."/"..name..".db") - end - end - end - return sqlite -end +std = {} +require("silk.core.mimes") RESPONSE_HEADER = { - status = 200, - header = {}, - cookie = {}, - sent = false + status = 200, + header = {}, + cookie = {}, + sent = false } +local http_status = { + [100] = "Continue", + [101] = "Switching Protocols", + [102] = "Processing", + [103] = "Early Hints", + + [200] = "OK", + [201] = "Created", + [202] = "Accepted", + [203] = "Non-Authoritative Information", + [204] = "No Content", + [205] = "Reset Content", + [206] = "Partial Content", + [207] = "Multi-Status", + [208] = "Already Reported", + [226] = "IM Used", + + [300] = "Multiple Choices", + [301] = "Moved Permanently", + [302] = "Found", + [303] = "See Other", + [304] = "Not Modified", + [305] = "Use Proxy", + [306] = "Switch Proxy", + [307] = "Temporary Redirect", + [308] = "Permanent Redirect", + + [400] = "Bad Request", + [401] = "Unauthorized", + [402] = "Payment Required", + [403] = "Forbidden", + [404] = "Not Found", + [405] = "Method Not Allowed", + [406] = "Not Acceptable", + [407] = "Proxy Authentication Required", + [408] = "Request Timeout", + [409] = "Conflict", + [410] = "Gone", + [411] = "Length Required", + [412] = "Precondition Failed", + [413] = "Payload Too Large", + [414] = "URI Too Long", + [415] = "Unsupported Media Type", + [416] = "Range Not Satisfiable", + [417] = "Expectation Failed", + [421] = "Misdirected Request", + [422] = "Unprocessable Entity", + [423] = "Locked", + [424] = "Failed Dependency", + [425] = "Too Early", + [426] = "Upgrade Required", + [428] = "Precondition Required", + [429] = "Too Many Requests", + [431] = "Request Header Fields Too Large", + [451] = "Unavailable For Legal Reasons", + + [500] = "Internal Server Error", + [501] = "Not Implemented", + [502] = "Bad Gateway", + [503] = "Service Unavailable", + [504] = "Gateway Timeout", + [505] = "HTTP Version Not Supported", + [506] = "Variant Also Negotiates", + [507] = "Insufficient Storage", + [508] = "Loop Detected", + [510] = "Not Extended", + [511] = "Network Authentication Required" +} +setmetatable(http_status, { + __index = function(this, key) + return "Unofficial Status" + end +}) + function std.status(code) - RESPONSE_HEADER.status=code + RESPONSE_HEADER.status = code end -function std.custom_header(k,v) - std.header(k,v) + +function std.custom_header(k, v) + std.header(k, v) end + +function std.header(k, v) + RESPONSE_HEADER.header[k] = v +end + function std.header_flush() - std._send_header(HTTP_REQUEST.id,RESPONSE_HEADER.status, RESPONSE_HEADER.header, RESPONSE_HEADER.cookie) - RESPONSE_HEADER.sent = true + -- send out status + echo("Status: ", RESPONSE_HEADER.status, " ", http_status[RESPONSE_HEADER.status], "\r\n") + -- send out header + for key, val in pairs(RESPONSE_HEADER.header) do + echo(key, ": ", val, "\r\n") + end + -- send out cookie + for key, val in ipairs(RESPONSE_HEADER.cookie) do + echo("Set-Cookie: ", val, "\r\n") + end + echo("\r\n") + RESPONSE_HEADER.sent = true + RESPONSE_HEADER.header = {} + RESPONSE_HEADER.cookie = {} end -function std.header(k,v) - RESPONSE_HEADER.header[k] = v -end - -function std.cjson(ck) - for k,v in pairs(ck) do - std.setCookie(k.."="..v.."; Path=/") - end - std.header("Content-Type","application/json; charset=utf-8") - std.header_flush() -end -function std.chtml(ck) - for k,v in pairs(ck) do - std.setCookie(k.."="..v.."; Path=/") - end - std.header("Content-Type","text/html; charset=utf-8") - std.header_flush() -end -function std.t(s) - if RESPONSE_HEADER.sent == false then - std.header_flush() - end - std._t(HTTP_REQUEST.id,s) -end -function std.b(s) - if RESPONSE_HEADER.sent == false then - std.header_flush() - end - std._b(HTTP_REQUEST.id,s) -end -function std.f(v) - std._f(HTTP_REQUEST.id,v) - --ulib.send_file(v, HTTP_REQUEST.socket) -end - -function std.setCookie(v) - RESPONSE_HEADER.cookie[#RESPONSE_HEADER.cookie] = v +function std.setCookie(...) + local args = table.pack(...) + cookie = table.concat(args,";") + RESPONSE_HEADER.cookie[#RESPONSE_HEADER.cookie + 1] = cookie end function std.error(status, msg) - std._error(HTTP_REQUEST.id, status, msg) -end ---_upload ---_route -function std.unknow(s) - std.error(404, "Unknown request") + std.status(status) + std.header("Content-Type", "text/html") + std.header_flush() + echo(string.format("%s

%s

",msg, msg)) end ---_redirect ---[[ function std.redirect(s) - std._redirect(HTTP_REQUEST.id,s) -end ]] +function std.unknow(s) + std.error(404, "Unknown request") +end + +function std.f(path) + fcgio:send_file(path) +end function std.html() - std.header("Content-Type","text/html; charset=utf-8") - std.header_flush() + std.header("Content-Type", "text/html; charset=utf-8") + std.header_flush() end + function std.text() - std.header("Content-Type","text/plain; charset=utf-8") - std.header_flush() + std.header("Content-Type", "text/plain; charset=utf-8") + std.header_flush() end function std.json() - std.header("Content-Type","application/json; charset=utf-8") - std.header_flush() + std.header("Content-Type", "application/json; charset=utf-8") + std.header_flush() end + function std.jpeg() - std.header("Content-Type","image/jpeg") - std.header_flush() + std.header("Content-Type", "image/jpeg") + std.header_flush() end + function std.octstream(s) - std.header("Content-Type","application/octet-stream") - std.header("Content-Disposition",'attachment; filename="'..s..'"') - std.header_flush() + std.header("Content-Type", "application/octet-stream") + std.header("Content-Disposition", 'attachment; filename="' .. s .. '"') + std.header_flush() end ---[[ function std.textstream() - std._textstream(HTTP_REQUEST.id) -end ]] +function std.is_file(f) + return ulib.is_dir(f) == false +end -function std.readOnly(t) -- bugging - local proxy = {} - local mt = { -- create metatable - __index = t, - __newindex = function (t,k,v) - error("attempt to update a read-only table", 2) - end - } - setmetatable(proxy, mt) - return proxy - end - - --- web socket +-- TODO provide web socket support +-- use coroutine to read socket message std.ws = {} function std.ws.header() - local h = std.ws_header(HTTP_REQUEST.id) - if(h) then - return h --std.readOnly(h) - else - return nil - end + local h = std.ws_header(HTTP_REQUEST.id) + if (h) then + return h -- std.readOnly(h) + else + return nil + end end function std.ws.read(h) - return std.ws_read(HTTP_REQUEST.id,h) + return std.ws_read(HTTP_REQUEST.id, h) end function std.ws.swrite(s) - std.ws_t(HTTP_REQUEST.id,s) + std.ws_t(HTTP_REQUEST.id, s) end function std.ws.fwrite(s) - std.ws_f(HTTP_REQUEST.id,s) + std.ws_f(HTTP_REQUEST.id, s) end function std.ws.write_bytes(arr) - std.ws_b(HTTP_REQUEST.id,arr) + std.ws_b(HTTP_REQUEST.id, arr) end function std.ws.enable() - return HTTP_REQUEST ~= nil and HTTP_REQUEST.request["__web_socket__"] == "1" + return HTTP_REQUEST ~= nil and HTTP_REQUEST.request["__web_socket__"] == "1" end function std.ws.close(code) - std.ws_close(HTTP_REQUEST.id,code) + std.ws_close(HTTP_REQUEST.id, code) end -function std.basename(str) - local name = string.gsub(std.trim(str,"/"), "(.*/)(.*)", "%2") - return name -end -function std.is_file(f) - return std.is_dir(f) == false -end - std.ws.TEXT = 1 std.ws.BIN = 2 std.ws.CLOSE = 8 diff --git a/silkmvc/core/utils.lua b/silkmvc/core/utils.lua index 8f933c8..85453a3 100644 --- a/silkmvc/core/utils.lua +++ b/silkmvc/core/utils.lua @@ -5,7 +5,9 @@ function utils.is_array(table) local count = 0 for k, v in pairs(table) do if type(k) == "number" then - if k > max then max = k end + if k > max then + max = k + end count = count + 1 else return false @@ -19,143 +21,229 @@ function utils.is_array(table) end function utils.escape(s) - local replacements = { - ["\\"] = "\\\\" , - ['"'] = '\\"', - ["\n"] = "\\n", - ["\t"] = "\\t", - ["\b"] = "\\b", - ["\f"] = "\\f", - ["\r"] = "\\r", - ["%"] = "%%" - } - return (s:gsub( "[\\'\"\n\t\b\f\r%%]", replacements )) + local replacements = { + ["\\"] = "\\\\", + ['"'] = '\\"', + ["\n"] = "\\n", + ["\t"] = "\\t", + ["\b"] = "\\b", + ["\f"] = "\\f", + ["\r"] = "\\r", + ["%"] = "%%" + } + return (s:gsub("[\\'\"\n\t\b\f\r%%]", replacements)) end function utils.escape_pattern(s) - return s:gsub("[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1") + return s:gsub("[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1") end function utils.unescape_pattern(s) - return s:gsub( "[%%]", "%%%%") + return s:gsub("[%%]", "%%%%") end function utils.hex_to_char(x) - return string.char(tonumber(x, 16)) + return string.char(tonumber(x, 16)) end - + function utils.decodeURI(url) - return url:gsub("%%(%x%x)", utils.hex_to_char) + return url:gsub("%%(%x%x)", utils.hex_to_char):gsub('+', ' ') end function utils.unescape(s) - local str = "" - local escape = false - local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} - for c in s:gmatch"." do - if c ~= '\\' then - if escape then - if esc_map[c] then - str = str..esc_map[c] - else - str = str..c - end - else - str = str..c - end - escape = false - else - if escape then - str = str..c - escape = false - else - escape = true - end - end - end - return str + local str = "" + local escape = false + local esc_map = { + b = '\b', + f = '\f', + n = '\n', + r = '\r', + t = '\t' + } + for c in s:gsub("%%%%", "%%"):gmatch "." do + if c ~= '\\' then + if escape then + if esc_map[c] then + str = str .. esc_map[c] + else + str = str .. c + end + else + str = str .. c + end + escape = false + else + if escape then + str = str .. c + escape = false + else + escape = true + end + end + end + return str end function utils.file_exists(name) - local f=io.open(name,"r") - if f~=nil then io.close(f) return true else return false end + local f = io.open(name, "r") + if f ~= nil then + io.close(f) + return true + else + return false + end +end + +function utils.parse_query(str, sep) + if not sep then + sep = '&' + end + + local values = {} + for key, val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do + local key = utils.decodeURI(key) + local keys = {} + key = key:gsub('%[([^%]]*)%]', function(v) + -- extract keys between balanced brackets + if string.find(v, "^-?%d+$") then + v = tonumber(v) + else + v = utils.decodeURI(v) + end + table.insert(keys, v) + return "=" + end) + key = key:gsub('=+.*$', "") + key = key:gsub('%s', "_") -- remove spaces in parameter name + val = val:gsub('^=+', "") + + if not values[key] then + values[key] = {} + end + if #keys > 0 and type(values[key]) ~= 'table' then + values[key] = {} + elseif #keys == 0 and type(values[key]) == 'table' then + values[key] = utils.decodeURI(val) + elseif type(values[key]) == 'string' then + values[key] = {values[key]} + table.insert(values[key], utils.decodeURI(val)) + end + + local t = values[key] + for i, k in ipairs(keys) do + if type(t) ~= 'table' then + t = {} + end + if k == "" then + k = #t + 1 + end + if not t[k] then + t[k] = {} + end + if i == #keys then + t[k] = val + end + t = t[k] + end + end + return values end function utils.url_parser(uri) - local pattern = "^(https?)://([%.%w]+):?(%d*)(/?[^#]*)#?.*$" - local obj = {} - obj.protocol = uri:gsub(pattern, "%1") - obj.hostname = uri:gsub(pattern, "%2") - obj.port = uri:gsub(pattern, "%3") - obj.query = uri:gsub(pattern, "%4") - - if obj.port == "" then obj.port = 80 else obj.port = tonumber(obj.port) end - if obj.query == "" then obj.query="/" end - return obj + local pattern = "^(https?)://([%.%w]+):?(%d*)(/?[^#]*)#?.*$" + local obj = {} + obj.protocol = uri:gsub(pattern, "%1") + obj.hostname = uri:gsub(pattern, "%2") + obj.port = uri:gsub(pattern, "%3") + obj.query = uri:gsub(pattern, "%4") + + if obj.port == "" then + obj.port = 80 + else + obj.port = tonumber(obj.port) + end + if obj.query == "" then + obj.query = "/" + end + return obj end JSON = require("json") function JSON.encode(obj) - local t = type(obj) - if t == 'table' then - -- encode object - if utils.is_array(obj) == false then - local lst = {} - for k,v in pairs(obj) do - table.insert(lst,'"'..k..'":'..JSON.encode(v)) - end - return "{"..table.concat(lst,",").."}" - else - local lst = {} - local a = {} - for n in pairs(obj) do table.insert(a, n) end - table.sort(a) - for i,v in pairs(a) do - table.insert(lst,JSON.encode(obj[v])) - end - return "["..table.concat(lst,",").."]" - end - elseif t == 'string' then - --print('"'..utils.escape(obj)..'"') - return '"'..utils.escape(obj)..'"' - elseif t == 'boolean' or t == 'number' then - return tostring(obj) - elseif obj == nil then - return "null" - else - return '"'..tostring(obj)..'"' - end + local t = type(obj) + if t == 'table' then + -- encode object + if utils.is_array(obj) == false then + local lst = {} + for k, v in pairs(obj) do + table.insert(lst, '"' .. k .. '":' .. JSON.encode(v)) + end + return "{" .. table.concat(lst, ",") .. "}" + else + local lst = {} + local a = {} + for n in pairs(obj) do + table.insert(a, n) + end + table.sort(a) + for i, v in pairs(a) do + table.insert(lst, JSON.encode(obj[v])) + end + return "[" .. table.concat(lst, ",") .. "]" + end + elseif t == 'string' then + -- print('"'..utils.escape(obj)..'"') + return '"' .. utils.escape(obj) .. '"' + elseif t == 'boolean' or t == 'number' then + return tostring(obj) + elseif obj == nil then + return "null" + else + return '"' .. tostring(obj) .. '"' + end end function explode(str, div) -- credit: http://richard.warburton.it - if (div=='') then return false end - local pos,arr = 0,{} - -- for each divider found - for st,sp in function() return string.find(str,div,pos,true) end do - table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider - pos = sp + 1 -- Jump past current divider - end - table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider - return arr - end + if (div == '') then + return false + end + local pos, arr = 0, {} + -- for each divider found + for st, sp in function() + return string.find(str, div, pos, true) + end do + table.insert(arr, string.sub(str, pos, st - 1)) -- Attach chars left of current divider + pos = sp + 1 -- Jump past current divider + end + table.insert(arr, string.sub(str, pos)) -- Attach chars right of last divider + return arr +end function implode(arr, div) - return table.concat(arr,div) + return table.concat(arr, div) end function firstToUpper(str) return (str:gsub("^%l", string.upper)) end - local charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" function utils.generate_salt(length) - local ret = {} - local r - for i = 1, length do - r = math.random(1, #charset) - table.insert(ret, charset:sub(r, r)) - end - return table.concat(ret) + local ret = {} + local r + for i = 1, length do + r = math.random(1, #charset) + table.insert(ret, charset:sub(r, r)) + end + return table.concat(ret) +end + +function utils.ext(path) + return path:match("%.([^%.]*)$") +end + +function utils.basename(str) + local name = string.gsub(ulib.trim(str, "/"), "(.*/)(.*)", "%2") + return name end \ No newline at end of file diff --git a/silkmvc/router.lua.tpl b/silkmvc/router.lua.tpl index 5eb7f2f..2f7cf1f 100644 --- a/silkmvc/router.lua.tpl +++ b/silkmvc/router.lua.tpl @@ -2,26 +2,26 @@ -- the rewrite rule for the framework -- should be something like this -- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>& + +-- require needed library +BASE_FRW = "" +require(BASE_FRW.."api") + -- some global variables -DIR_SEP = "/" WWW_ROOT = "/opt/www/htdocs/apps" HTTP_ROOT = "https://apps.localhost:9195/" --- class path: path.to.class -BASE_FRW = "" + -- class path: path.to.class CONTROLLER_ROOT = BASE_FRW.."apps.controllers" MODEL_ROOT = BASE_FRW.."apps.models" -- file path: path/to/file VIEW_ROOT = WWW_ROOT..DIR_SEP.."views" -LOG_ROOT = WWW_ROOT..DIR_SEP.."logs" --- require needed library -require(BASE_FRW.."silk.api") -- registry object store global variables local REGISTRY = {} -- set logging level -REGISTRY.logger = Logger:new{ levels = {INFO = true, ERROR = true, DEBUG = true}} +REGISTRY.logger = Logger:new{ level = Logger.INFO } REGISTRY.db = DBHelper:new{db="iosapps"} REGISTRY.layout = 'default' @@ -31,7 +31,7 @@ REGISTRY.router = router router:setPath(CONTROLLER_ROOT) --router:route('edit', 'post/edit', "ALL" ) --- example of depedencies to the current main route +-- example of dependencies to the current main route -- each layout may have different dependencies local default_routes_dependencies = { edit = { diff --git a/test/ad.ls b/test/ad.ls new file mode 100644 index 0000000..2217eea --- /dev/null +++ b/test/ad.ls @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/detail.ls b/test/detail.ls new file mode 100644 index 0000000..1bbdba8 --- /dev/null +++ b/test/detail.ls @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/layout.ls b/test/layout.ls new file mode 100644 index 0000000..3c21d41 --- /dev/null +++ b/test/layout.ls @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test/lunit.lua b/test/lunit.lua new file mode 100644 index 0000000..092ac5e --- /dev/null +++ b/test/lunit.lua @@ -0,0 +1,46 @@ +TESTS = {} + +function test(description, fn) + TESTS[#TESTS + 1] = {description = description, test = fn } +end + + +function run() + local report = { + ok = 0, + fail= 0, + total = #TESTS + } + for l,ts in ipairs(TESTS) do + io.write(string.format("Executing: %s...",ts.description)) + local status,err = pcall(ts.test) + if status then + io.write("\27[32mOK\27[0m\n") + report.ok = report.ok + 1 + else + io.write("\27[31mFAIL\27[0m\n") + print(err) + report.fail = report.fail + 1 + end + end + print("----------------------------") + print(string.format("Total tests: %d", report.total)) + print(string.format("Tests passed: %d", report.ok)) + print(string.format("Tests failed: %d", report.fail)) + TESTS = {} +end + +function assert(b, e,...) + if not b then + error(string.format(e,...)) + print(debug.traceback()) + end +end + +function expect(v1,v2) + assert(v1 == v2, "Expect: [%s] get: [%s]", tostring(v2), tostring(v1)) +end + +function unexpect(v1,v2) + assert(v1 ~= v2, "Unexpect value", tostring(v2)) +end \ No newline at end of file diff --git a/test/request.json b/test/request.json new file mode 100644 index 0000000..26c2c78 --- /dev/null +++ b/test/request.json @@ -0,0 +1,37 @@ +{ + "PATH_INFO": "luacgi/lua/test.lua", + "REDIRECT_STATUS": "200", + "SCRIPT_NAME": "test.lua", + "HTTP_ACCEPT_ENCODING": "gzip, deflate", + "DOCUMENT_ROOT": "/tmp/www", + "REMOTE_ADDR": "192.168.1.44", + "REQUEST_URI": "/luacgi/lua/test.lua?r=1&id=3&name=John", + "SERVER_PROTOCOL": "HTTP/1.1", + "SERVER_NAME": "Antd", + "PATH_TRANSLATED": "/opt/www/htdocs/lua/test.lua", + "RAW_DATA": "firstname=Dany&lastname=LE&form_submitted=1", + "CONTENT_TYPE": "application/x-www-form-urlencoded", + "HTTP_ORIGIN": "http://192.168.1.27", + "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "REMOTE_HOST": "192.168.1.44", + "HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9", + "HTTP_CONTENT_LENGTH": "43", + "HTTP_CONNECTION": "keep-alive", + "HTTP_COOKIE": "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1", + "LIB_DIR": "/tmp/lib", + "REQUEST_METHOD": "POST", + "HTTP_REFERER": "http://192.168.1.27/php/sign.html", + "SERVER_SOFTWARE": "Antd", + "QUERY_STRING": "r=post/id/1&id=3&name=John", + "TMP_DIR": "/tmp", + "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", + "HTTP_CONTENT_TYPE": "application/x-www-form-urlencoded", + "HTTP_UPGRADE_INSECURE_REQUESTS": "1", + "CONTENT_LENGTH": "43", + "GATEWAY_INTERFACE": "CGI/1.1", + "HTTP_HOST": "192.168.1.27", + "SERVER_PORT": "80", + "SCRIPT_FILENAME": "/opt/www/htdocs/lua/test.lua", + "DB_DIR": "/tmp", + "HTTP_CACHE_CONTROL": "max-age=0" +} \ No newline at end of file diff --git a/test/test_core.lua b/test/test_core.lua new file mode 100644 index 0000000..d73365a --- /dev/null +++ b/test/test_core.lua @@ -0,0 +1,396 @@ +--- the binary shall be compiled with +--- make CFLAGS=-DLUA_SLICE_MAGIC=0x8AD73B9F +--- otherwise the tests unable to load C modules + +require("lunit") + +package.cpath = "/tmp/lib/lua/?.so" +package.path = "" + +test("IO setup", function() + fcgio = { OUTPUT = "", + LOG = { + INFO = "", + ERROR = "", + DEBUG = "", + WARN = "" + } + } + function fcgio:flush() + fcgio.OUTPUT = "" + end + function fcgio:echo(...) + local args = table.pack(...) + for i=1,args.n do + -- do something with args[i], careful, it might be nil! + fcgio.OUTPUT = fcgio.OUTPUT..tostring(args[i]) + end + end + function fcgio:log_info(fmt,...) + fcgio.LOG.INFO = string.format(fmt,...) + io.stderr:write("INFO: ", fcgio.LOG.INFO) + io.stderr:write("\n") + end + function fcgio:log_error(fmt,...) + fcgio.LOG.ERROR = string.format(fmt,...) + io.stderr:write("ERROR: ",fcgio.LOG.ERROR) + io.stderr:write("\n") + end + function fcgio:log_debug(fmt,...) + fcgio.LOG.DEBUG = string.format(fmt,...) + io.stderr:write("DEBUG: ", fcgio.LOG.DEBUG) + io.stderr:write("\n") + end + function fcgio:log_warn(fmt,...) + fcgio.LOG.WARN = string.format(fmt,...) + io.stderr:write("WARN: ", fcgio.LOG.WARN) + io.stderr:write("\n") + end + function fcgio:send_file(path) + local f = io.open(path, "rb") + local content = f:read("*all") + f:close() + fcgio.OUTPUT = fcgio.OUTPUT..content + end +end) + +test("Setup request", function() + local json = require("json") + _SERVER = json.decodeFile("request.json") + assert(_SERVER ~= nil, "Global _SERVER object not found") +end) + +test("SEVER PATH", function() + expect(_SERVER["LIB_DIR"], "/tmp/lib") + expect(_SERVER["TMP_DIR"], "/tmp") + expect(_SERVER["DB_DIR"], "/tmp") + expect(_SERVER["DOCUMENT_ROOT"], "/tmp/www") +end) + +test("Import the hook", function() + package.path = "../silkmvc/?.lua" + local ret = require("core.hook") + expect(ret, true) +end) + +test("Lua path", function() + expect(package.cpath, "/tmp/lib/lua/?.so") + expect(package.path, "/tmp/lib/lua/?.lua;/tmp/www/?.lua") + unexpect(ulib, nil) + unexpect(utils, nil) + unexpect(std, nil) +end) + +test("HTTP Headers", function() + expect(HEADER["mobile"], false) + for k,v in pairs(_SERVER) do + if k:match("^HTTP_.*") then + local key = (k:gsub("HTTP_",""):gsub("_","-")):lower() + expect(HEADER[key],v) + end + end +end) + +test("HTTP request", function() + expect(REQUEST.method, "POST") + expect(REQUEST.r, "post/id/1") + expect(REQUEST.id, "3") + expect(REQUEST.name, "John") + expect(REQUEST.firstname, "Dany") + expect(REQUEST.lastname, "LE") + expect(REQUEST.form_submitted, "1") +end) + +test('HTTP COOKIE', function() + unexpect(SESSION, nil) + expect(SESSION.PHPSESSID, "298zf09hf012fh2") + expect(SESSION.csrftoken, "u32t4o3tb3gg43") + expect(SESSION._gat, "1") +end) + +test("Echo", function() + echo("Hello ", "World: ", 10, true) + expect(fcgio.OUTPUT, "Hello World: 10true") +end) + +test("STD response", function() + std.status(500) + expect(RESPONSE_HEADER.status, 500) + std.header("Content-Type", "text/html") + expect(RESPONSE_HEADER.header["Content-Type"], "text/html") +end) + +test("STD Error", function() + fcgio:flush() + std.error(404, "No page found") + expect(fcgio.OUTPUT, "Status: 404 Not Found\r\nContent-Type: text/html\r\n\r\nNo page found

No page found

") +end) + +test("STD header with cookie", function() + RESPONSE_HEADER.sent = false + fcgio:flush() + + std.status(200) + std.header("Content-Type", "text/html") + std.setCookie("sessionid=12345;user=dany; path=/") + std.setCookie("date=now", "_gcat=1") + --print(JSON.encode(RESPONSE_HEADER)) + std.header_flush() + echo("hello") + expect(fcgio.OUTPUT, "Status: 200 OK\r\nContent-Type: text/html\r\nSet-Cookie: sessionid=12345;user=dany; path=/\r\nSet-Cookie: date=now;_gcat=1\r\n\r\nhello") +end) +--- mimes test +test("STD Mime", function() + expect(std.mimeOf("request.json"), "application/json") + expect(std.mimeOf("test.exe"), "application/octet-stream") +end) + +test("STD send file", function() + RESPONSE_HEADER.sent = false + fcgio:flush() + std.sendFile("request.json") + print(fcgio.OUTPUT) +end) + +test("utils.is_array", function() + local tb = { name = "Dany", test = true} + expect(utils.is_array(tb), false) + local arr = {[1] = "Dany", [2] = true} + expect(utils.is_array(arr), true) +end) + +test("utils.escape and utils.unescape", function() + local before = 'this is a escape string \\ " % \n \t \r' + local escaped = utils.escape(before) + expect(escaped, 'this is a escape string \\\\ \\" %% \\n \\t \\r') + expect(utils.unescape(escaped), before) +end) + +test("utils.decodeURI", function() + local uri = "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + local decoded = utils.decodeURI(uri) + expect(decoded, "https://mozilla.org/?x=шеллы") +end) + +test("utils.file_exists", function() + expect(utils.file_exists("request.json"), true) + expect(utils.file_exists("test1.json"), false) +end) + +test("utils.parse_query", function() + local query = "r=1&id=3&name=John&desc=some%20thing&enc=this+is+encode" + local tb = utils.parse_query(query) + expect(tb.r, "1") + expect(tb.id, "3") + expect(tb.desc, "some thing") + expect(tb.enc, "this is encode") + expect(tb.name, "John") +end) + +test("utils.url_parser", function() + local uri = "https://mozilla.org:9000/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + local obj = utils.url_parser(uri) + expect(obj.query, "/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B") + expect(obj.hostname, "mozilla.org") + expect(obj.protocol, "https") + expect(obj.port, 9000) +end) + +test("utils explode/implode", function() + local str = "this is a test" + tbl = explode(str, " ") + expect(tbl[1], "this") + expect(tbl[2], "is") + expect(tbl[3], "a") + expect(tbl[4], "test") + local str1 = implode(tbl, "|") + expect(str1, "this|is|a|test") +end) + +test("utils firstToUpper", function() + local str = "this is a test" + expect(firstToUpper(str), "This is a test") +end) + +test("utils.ext", function() + expect(utils.ext("foo.bar"), "bar") + expect(utils.ext("foo.bar.baz"), "baz") + expect(utils.ext("foo"), nil) +end) + +test("utils.basename", function() + expect(utils.basename("path/to/foo.bar"), "foo.bar") +end) +--- Test for sqlite database +test("sqlite.getdb", function() + require("silk.core.sqlite") + local path = "/tmp/test.db" + local db = sqlite.getdb("/tmp/test.db") + sqlite.dbclose(db) + expect(ulib.exists(path), true) + + db = sqlite.getdb("system") + sqlite.dbclose(db) + expect(ulib.exists("/tmp/system.db"), true) + + ulib.delete("/tmp/secret.db") + expect(ulib.exists("/tmp/secret.db"), false) + DB = DBModel:new{db="secret"} + DB:open() + + expect(ulib.exists("/tmp/secret.db"), true) + unexpect(DB.db, nil) + unexpect(DB.db,"secret") +end) + +test("DBModel:createTable", function() + ret = DB:createTable("test", { + first_name = "TEXT NOT NULL", + last_name = "TEXT NOT NULL", + age = "INTEGER" + }) + expect(ret, true) +end) + +test("DBModel:available", function() + expect(DB:available("test"), true) +end) + +test("DBModel:insert", function() + local data = { + first_name = "Dany", + last_name = "LE", + age = 30, + phone = "Unknown" + } + expect(DB:insert("test",data), false) + data.phone = nil + expect(DB:insert("test",data), true) + data = { + first_name = "Lisa", + last_name = "LE", + age = 5 + } + expect(DB:insert("test",data), true) +end) + +test("DBModel:lastInsertID", function() + local id = DB:lastInsertID() + expect(id, 2) +end) + +test("DBModel:get", function() + local record = DB:get("test", 2) + expect(record.id, 2) + expect(record.first_name, "Lisa") + expect(record.last_name, "LE") + expect(record.age, 5) +end) + +test("DBModel:getAll", function() + local records = DB:getAll("test") + expect(#records, 2) + + expect(records[1].id, 1) + expect(records[1].first_name, "Dany") + expect(records[1].last_name, "LE") + expect(records[1].age, 30) + + expect(records[2].id, 2) + expect(records[2].first_name, "Lisa") + expect(records[2].last_name, "LE") + expect(records[2].age, 5) +end) + +test("DBModel:find", function() + local cond = { + exp = { + ["and"] = { + { + ["="] = { + first_name = "Dany" + } + }, + { + ["="] = { + age = 25 + } + } + } + } + } + local records = DB:find("test", cond) + expect(#records, 0) + + cond.exp["and"][2]["="].age = 30 + records = DB:find("test", cond) + expect(#records, 1) + + cond = { + exp = { + ["="] = { + last_name = "LE" + } + }, + order = { + id = "DESC" + } + } + records = DB:find("test", cond) + expect(#records, 2) + expect(records[1].id, 2) + expect(records[1].first_name, "Lisa") + expect(records[1].last_name, "LE") + expect(records[1].age, 5) +end) + +test("DBModel:update", function() + local data = { + id = 1, + first_name = "Dany Xuan-Sang", + age = 35, + } + expect(DB:update("test", data), true) + local record = DB:get("test", 1) + unexpect(record, nil) + expect(record.age , 35) + expect(record.first_name, "Dany Xuan-Sang") +end) + +test("DBModel:deleteByID", function() + expect(DB:deleteByID("test", 1), true) + local record = DB:get("test", 1) + expect(record, nil) +end) + +test("DBModel:delete", function() + local cond = { + ["="] = { + last_name = "LE" + } + } + expect(DB:delete("test", cond), true) + local records = DB:getAll("test") + expect(#records, 0) +end) + +--- test enc module +test("Base64 encode/decode", function() + enc = require("enc") + local string = "this is the test" + local encode = enc.b64encode(string) + expect(encode,"dGhpcyBpcyB0aGUgdGVzdA==") + local buf = enc.b64decode(encode) + unexpect(buf,nil) + expect(tostring(buf), string) +end) + +test("md5 encode", function() + expect(enc.md5("this is a test"), "54b0c58c7ce9f2a8b551351102ee0938") +end) + +test("sha1 encode", function() + expect(enc.sha1("this is a test"), "fa26be19de6bff93f70bc2308434e4a440bbad02") +end) +--- run all unit tests +run() \ No newline at end of file diff --git a/test/test_silk.lua b/test/test_silk.lua new file mode 100644 index 0000000..e79a52a --- /dev/null +++ b/test/test_silk.lua @@ -0,0 +1,270 @@ +--- the binary shall be compiled with +--- make CFLAGS=-DLUA_SLICE_MAGIC=0x8AD73B9F +--- otherwise the tests unable to load C modules + +require("lunit") + +package.cpath = "/tmp/lib/lua/?.so" +package.path = "/tmp/lib/lua/?.lua" + +test("IO setup", function() + fcgio = { OUTPUT = "", + LOG = { + INFO = "", + ERROR = "", + DEBUG = "", + WARN = "" + } + } + function fcgio:flush() + fcgio.OUTPUT = "" + fcgio.LOG.INFO = "" + fcgio.LOG.DEBUG = "" + fcgio.LOG.WARN = "" + fcgio.LOG.ERROR = "" + RESPONSE_HEADER.sent = false + end + function fcgio:echo(...) + local args = table.pack(...) + for i=1,args.n do + -- do something with args[i], careful, it might be nil! + fcgio.OUTPUT = fcgio.OUTPUT..tostring(args[i]) + end + end + function fcgio:log_info(fmt,...) + fcgio.LOG.INFO = string.format(fmt,...) + io.stderr:write("INFO: ", fcgio.LOG.INFO) + io.stderr:write("\n") + end + function fcgio:log_error(fmt,...) + fcgio.LOG.ERROR = string.format(fmt,...) + io.stderr:write("ERROR: ",fcgio.LOG.ERROR) + io.stderr:write("\n") + end + function fcgio:log_debug(fmt,...) + fcgio.LOG.DEBUG = string.format(fmt,...) + io.stderr:write("DEBUG: ", fcgio.LOG.DEBUG) + io.stderr:write("\n") + end + function fcgio:log_warn(fmt,...) + fcgio.LOG.WARN = string.format(fmt,...) + io.stderr:write("WARN: ", fcgio.LOG.WARN) + io.stderr:write("\n") + end + function fcgio:send_file(path) + local f = io.open(path, "rb") + local content = f:read("*all") + f:close() + fcgio.OUTPUT = fcgio.OUTPUT..content + end +end) + +test("Setup request", function() + local json = require("json") + _SERVER = json.decodeFile("request.json") + assert(_SERVER ~= nil, "Global _SERVER object not found") +end) + +test("SEVER PATH", function() + expect(_SERVER["LIB_DIR"], "/tmp/lib") + expect(_SERVER["TMP_DIR"], "/tmp") + expect(_SERVER["DB_DIR"], "/tmp") + expect(_SERVER["DOCUMENT_ROOT"], "/tmp/www") +end) + +test("Import the api", function() + local ret = require("silk.api") +end) + +test("Lua path", function() + expect(package.cpath, "/tmp/lib/lua/?.so") + expect(package.path, "/tmp/lib/lua/?.lua;/tmp/www/?.lua") + unexpect(ulib, nil) + unexpect(utils, nil) + unexpect(std, nil) +end) + +test("Logger", function() + local logger = Logger:new{ level = Logger.ERROR} + logger:info("Info message") + logger:debug("Debug message") + logger:warn("Warning message") + logger:error("Error message") + expect(fcgio.LOG.INFO, "") + expect(fcgio.LOG.DEBUG, "") + expect(fcgio.LOG.WARN, "") + expect(fcgio.LOG.ERROR, "Error message") + logger.level = Logger.INFO + + logger:info("Info message") + logger:debug("Debug message") + logger:warn("Warning message") + logger:error("Error message") + expect(fcgio.LOG.ERROR, "Error message") + expect(fcgio.LOG.DEBUG, "") + expect(fcgio.LOG.WARN, "Warning message") + expect(fcgio.LOG.INFO, "Info message") +end) + +test("BaseObject", function() + fcgio:flush() + local obj = BaseObject:new{ registry = { + logger = Logger:new{level = Logger.DEBUG} + } } + obj:info('Info message') + expect(fcgio.LOG.INFO, "Info message") + obj:debug("Debug message") + expect(fcgio.LOG.DEBUG, "Debug message") + obj:warn("Warning message") + expect(fcgio.LOG.WARN, "Warning message") + obj:print() + expect(fcgio.LOG.DEBUG, "BaseObject") + --obj:error("Error message") +end) + +test("Silk define env", function() + DIR_SEP = "/" + BASE_FRW = "" + WWW_ROOT = "/tmp/www" + HTTP_ROOT = "https://apps.localhost:9195/" + CONTROLLER_ROOT = "" + -- class path: path.to.class + MODEL_ROOT = BASE_FRW + -- file path: path/to/file + VIEW_ROOT = WWW_ROOT + ulib.delete(WWW_ROOT) + expect(ulib.mkdir(WWW_ROOT), true) + expect(ulib.mkdir(WWW_ROOT.."/post"), true) + expect(ulib.send_file("request.json", WWW_ROOT.."/rq.json"), true) + expect(ulib.send_file("layout.ls", WWW_ROOT.."/layout.ls"), true) + expect(ulib.send_file("detail.ls", WWW_ROOT.."/post/detail.ls"), true) + expect(ulib.send_file("ad.ls", WWW_ROOT.."/post/ad.ls"), true) +end) + +test("Define model", function() + BaseModel:subclass("NewsModel",{ + registry = {}, + name = "news", + fields = { + content = "TEXT" + } + }) + local REGISTRY = {} + ulib.delete("/tmp/news.db") + -- set logging level + REGISTRY.logger = Logger:new{ level = Logger.INFO } + REGISTRY.layout = '/' + REGISTRY.db = DBModel:new {db = "news"} + REGISTRY.db:open() + local model = NewsModel:new{registry = REGISTRY} + -- insert data + expect(model:create({content = "Hello HELL"}), true) + expect(model:create({content = "Goodbye"}), true) + local records = model:findAll() + expect(#records, 2) + expect(model:update({id =1, content = "Hello World"}), true) + expect(model:delete({ ["="] = {id = 2} }), true) + records = model:findAll() + expect(#records, 1) + local record = model:get(1) + unexpect(record, nil) + expect(record.content, "Hello World") + records = model:select("id as ID, content", "1=1") + unexpect(records, nil) + expect(#records, 1) + expect(records[1].ID,1) + REGISTRY.db:close() + +end) + +test("Define controller", function() + BaseController:subclass("PostController",{ + registry = {}, + models = {"news"} + }) + function PostController:id(n) + local record = self.news:get(n) + self.template:set("data", record) + --self.template:set("id", n) + self.template:setView("detail") + return true + end + + function PostController:ad() + self.template:set("ad", "AD HERE") + return true + end +end) + +test("Router infer controller", function() + local router = Router:new{} + local action = router:infer() + expect(action.controller.class, "PostController") + expect(action.action, "id") + expect(action.args[1], "1") +end) + +test("Router infer asset", function() + fcgio:flush() + local router = Router:new{registry = { + fileaccess = true + }} + local action = router:infer("/rq.json") + expect(action.controller.class, "AssetController") + expect(action.action, "get") + expect(action.args[1], "rq.json") + local ret = router:call(action) + expect(ret, false) + io.stderr:write(fcgio.OUTPUT) + io.stderr:write("\n") +end) + +test("Router fetch views with dependencies", function() + fcgio:flush() + local REGISTRY = {} + REGISTRY.db = DBModel:new {db = "news"} + REGISTRY.db:open() + -- set logging level + REGISTRY.logger = Logger:new{ level = Logger.INFO } + REGISTRY.layout = '/' + local default_routes_dependencies = { + ad = { + url = "post/ad", + visibility = { + shown = true, + routes = { + ["post/id"] = true + } + } + } + } + local router = Router:new{registry = REGISTRY} + router:route('/', default_routes_dependencies ) + router:delegate() + REGISTRY.db:close() + expect(fcgio.OUTPUT, "Status: 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\nPost ID:1.0\nContent:Hello World\nAD HERE") +end) + +test("Controller action not found", function() + fcgio:flush() + REQUEST.r = "/post/all" + local router = Router:new{registry = {}} + local s,e = pcall(router.delegate, router) + expect(s, false) + expect(fcgio.OUTPUT,"Status: 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n#action all is not found in controller PostController") +end) + +test("Controller not found", function() + fcgio:flush() + REQUEST.r = "/user/dany" + local REGISTRY = {} + -- set logging level + --REGISTRY.logger = Logger:new{ level = Logger.INFO } + REGISTRY.layout = '/' + local router = Router:new{registry = REGISTRY} + local s,e = pcall(router.delegate, router) + expect(s, false) + print(fcgio.OUTPUT) +end) +-- run all test +run() \ No newline at end of file