Add unit tests and correct bugs detected by the tests

This commit is contained in:
DanyLE 2023-01-25 17:01:01 +01:00
parent d4bb12c6b5
commit 7711526a7c
29 changed files with 1814 additions and 1024 deletions

View File

@ -22,16 +22,14 @@ silk_DATA = silkmvc/router.lua.tpl \
silkmvc/Template.lua \ silkmvc/Template.lua \
silkmvc/Logger.lua \ silkmvc/Logger.lua \
silkmvc/BaseObject.lua \ silkmvc/BaseObject.lua \
silkmvc/DBHelper.lua \
silkmvc/api.lua silkmvc/api.lua
coredir = $(libdir)/lua/silk/core coredir = $(libdir)/lua/silk/core
core_DATA = silkmvc/core/OOP.lua \ core_DATA = silkmvc/core/OOP.lua \
silkmvc/core/std.lua \ silkmvc/core/std.lua \
silkmvc/core/extra_mime.lua \ silkmvc/core/mimes.lua \
silkmvc/core/utils.lua \ silkmvc/core/utils.lua \
silkmvc/core/api.lua \ silkmvc/core/hook.lua \
silkmvc/core/cif.lua \
silkmvc/core/sqlite.lua silkmvc/core/sqlite.lua
# lua libraris & modules # lua libraris & modules

View File

@ -208,7 +208,7 @@ static int l_base64_decode(lua_State *L)
// decode data to a byte array // decode data to a byte array
lua_new_slice(L, len); lua_new_slice(L, len);
slice_t *vec = NULL; slice_t *vec = NULL;
vec = lua_check_slice(L, 2); vec = lua_check_slice(L, -1);
len = Base64decode((char *)vec->data, s); len = Base64decode((char *)vec->data, s);
vec->len = len; vec->len = len;
// lua_pushstring(L,dst); // lua_pushstring(L,dst);

View File

@ -14,15 +14,43 @@
#define SLICE "slice" #define SLICE "slice"
typedef struct { typedef struct {
size_t magic;
size_t len; size_t len;
uint8_t* data; uint8_t* data;
} slice_t; } 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) void lua_new_slice(lua_State*L, int n)
{ {
size_t nbytes = sizeof(slice_t) + n * 1U; size_t nbytes = sizeof(slice_t) + n * 1U;
slice_t *a = (slice_t *)lua_newuserdata(L, nbytes); slice_t *a = (slice_t *)lua_newuserdata(L, nbytes);
a->data = &((char *)a)[sizeof(slice_t)]; a->data = &((char *)a)[sizeof(slice_t)];
a->magic = lua_slice_magic();
luaL_getmetatable(L, SLICE); luaL_getmetatable(L, SLICE);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);

View File

@ -1,4 +1,5 @@
#include "lua/lualib.h" #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) 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); slice_t *a = (slice_t *)lua_newuserdata(L, nbytes);
a->len = n; a->len = n;
a->data = ptr; a->data = ptr;
a->magic = lua_slice_magic();
luaL_getmetatable(L, SLICE); luaL_getmetatable(L, SLICE);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);
} }
@ -78,6 +80,14 @@ static int l_slice_write(lua_State *L)
return 1; 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) static int l_slice_index(lua_State *L)
{ {
if(lua_isnumber(L,2)) if(lua_isnumber(L,2))
@ -91,10 +101,18 @@ static int l_slice_index(lua_State *L)
{ {
lua_pushcfunction(L, l_get_slice_size); 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); 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 else
{ {
lua_pushnil(L); lua_pushnil(L);
@ -120,6 +138,16 @@ static int l_slice_to_string(lua_State *L)
return 1; 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[] = { static const struct luaL_Reg slicemetalib[] = {
{"unew", l_new_lightslice}, {"unew", l_new_lightslice},
{"new", l_new_slice}, {"new", l_new_slice},

View File

@ -83,6 +83,9 @@ static int l_db_query(lua_State *L)
int cols = sqlite3_column_count(statement); int cols = sqlite3_column_count(statement);
int result = 0; int result = 0;
int cnt = 1; int cnt = 1;
uint8_t* data = NULL;
size_t len = 0;
slice_t* vec = NULL;
// new table for data // new table for data
lua_newtable(L); lua_newtable(L);
while ((result = sqlite3_step(statement)) == SQLITE_ROW) while ((result = sqlite3_step(statement)) == SQLITE_ROW)
@ -91,10 +94,37 @@ static int l_db_query(lua_State *L)
lua_newtable(L); lua_newtable(L);
for (int col = 0; col < cols; col++) 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); const char *name = sqlite3_column_name(statement, col);
lua_pushstring(L, name); 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);
} }
lua_settable(L, -3); lua_settable(L, -3);

View File

@ -41,11 +41,12 @@ function BaseController:switchLayout(name)
if self.main then if self.main then
self.registry.layout = name self.registry.layout = name
else 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
end end
function BaseController:setSession(key, value) function BaseController:setSession(key, value)
SESSION[key] = value SESSION[key] = value
end end
function BaseController:getSession(key) function BaseController:getSession(key)

View File

@ -30,9 +30,7 @@ function BaseModel:find(cond)
end end
function BaseModel:get(id) function BaseModel:get(id)
local data, order = self:find({exp = {["="] = {id = id}}}) return self.db:get(self.name, id)
if not data or #order == 0 then return false end
return data[1]
end end
function BaseModel:findAll() function BaseModel:findAll()
@ -46,6 +44,7 @@ function BaseModel:query(sql)
end end
function BaseModel:select(sel, sql_cnd) 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 return nil
end end

View File

@ -4,31 +4,34 @@ function BaseObject:subclass(name, args)
_G[name].class = name _G[name].class = name
end end
function BaseObject:log(msg, level) function BaseObject:log(level,msg,...)
level = level or "INFO"
if self.registry.logger then if self.registry.logger then
self.registry.logger:log(msg,level) self.registry.logger:log(level, msg,...)
end end
end end
function BaseObject:debug(msg) function BaseObject:debug(msg,...)
self:log(msg, "DEBUG") 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 end
function BaseObject:print() function BaseObject:print()
print(self.class) self:debug(self.class)
end end
function BaseObject:error(msg, trace) function BaseObject:error(msg,...)
html() html()
--local line = debug.getinfo(1).currentline --local line = debug.getinfo(1).currentline
echo(msg) local emsg = string.format(msg or "ERROR",...)
self:log(msg,"ERROR") echo(emsg)
if trace then self:log(Logger.ERROR, msg,...)
debug.traceback=nil error(emsg)
error(msg)
else
error(msg)
end
return false return false
end end

View File

@ -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

View File

@ -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() function Logger:initialize()
end if not self.level then
self.level = Logger.INFO
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)
end end
end end
function Logger:info(msg) function Logger:log(verb,msg,...)
self:log(msg, "INFO") local level = verb
if level > self.level then return end
if level > Logger.DEBUG then
level = Logger.DEBUG
end
Logger.handles[level](msg,...)
end end
function Logger:debug(msg) function Logger:info(msg,...)
self:log(msg, "DEBUG") self:log(Logger.INFO, msg,...)
end end
function Logger:debug(msg,...)
function Logger:error(msg) self:log(Logger.DEBUG, msg,...)
self:log(msg, "ERROR") end
function Logger:error(msg,...)
self:log(Logger.ERROR, msg,...)
end
function Logger:warn(msg,...)
self:log(Logger.WARN, msg,...)
end end

View File

@ -7,6 +7,7 @@ end
function Router:initialize() function Router:initialize()
self.routes = {} self.routes = {}
self.remaps = {} self.remaps = {}
self.path = CONTROLLER_ROOT
end end
--function Router:setArgs(args) --function Router:setArgs(args)
@ -25,7 +26,7 @@ function Router:infer(url)
-- if user dont provide the url, try to infer it -- if user dont provide the url, try to infer it
-- from the REQUEST -- from the REQUEST
url = url or REQUEST.r or "" url = url or REQUEST.r or ""
url = std.trim(url, "/") url = ulib.trim(url, "/")
local args = explode(url, "/") local args = explode(url, "/")
local data = { local data = {
name = "index", name = "index",
@ -79,7 +80,7 @@ function Router:infer(url)
end end
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 return data
end end
@ -90,7 +91,7 @@ function Router:delegate()
data.controller.main = true data.controller.main = true
views.__main__ = self:call(data) views.__main__ = self:call(data)
if not views.__main__ then 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 return
end end
-- get all visible routes -- get all visible routes
@ -106,8 +107,8 @@ function Router:delegate()
table.insert( view_args, k ) table.insert( view_args, k )
table.insert( view_argv, v ) table.insert( view_argv, v )
end end
local script_path = VIEW_ROOT .. DIR_SEP .. self.registry.layout .. DIR_SEP .. "layout.ls"
local fn, e = loadscript(VIEW_ROOT .. DIR_SEP .. self.registry.layout .. DIR_SEP .. "layout.ls", view_args) local fn, e = loadscript(script_path, view_args)
html() html()
if fn then if fn then
local r, o = pcall(fn, table.unpack(view_argv)) local r, o = pcall(fn, table.unpack(view_argv))
@ -116,7 +117,7 @@ function Router:delegate()
end end
else else
e = e or "" 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
end end
@ -125,9 +126,8 @@ function Router:dependencies(url)
return {} return {}
end end
local list = {} local list = {}
--self:log("comparing "..url)
for k, v in pairs(self.routes[self.registry.layout]) do 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 if v.visibility == "ALL" then
list[k] = v.url list[k] = v.url
elseif v.visibility.routes then elseif v.visibility.routes then

View File

@ -1,19 +1,21 @@
require("OOP") require("silk.core.hook")
ulib = require("ulib") require("silk.core.OOP")
require(BASE_FRW.."silk.BaseObject") require("silk.core.sqlite")
require(BASE_FRW.."silk.DBHelper")
require(BASE_FRW.."silk.Router") require("silk.BaseObject")
require(BASE_FRW.."silk.BaseController") require("silk.Router")
require(BASE_FRW.."silk.BaseModel") require("silk.BaseController")
require(BASE_FRW.."silk.Logger") require("silk.BaseModel")
require(BASE_FRW.."silk.Template") require("silk.Logger")
require("silk.Template")
DIR_SEP = "/"
-- mime type allows -- mime type allows
-- this will bypass the default server security -- this will bypass the default server security
-- the default list is from the server setting -- the default list is from the server setting
POLICY = {} POLICY = {}
POLICY.mimes = { POLICY.mimes = {
["application/javascript"] = true,
["image/bmp"] = true, ["image/bmp"] = true,
["image/jpeg"] = true, ["image/jpeg"] = true,
["image/png"] = true, ["image/png"] = true,
@ -38,20 +40,12 @@ POLICY.mimes = {
["application/vnd.ms-fontobject"] = true, ["application/vnd.ms-fontobject"] = true,
["application/x-font-woff"] = true, ["application/x-font-woff"] = true,
["application/x-font-otf"] = true, ["application/x-font-otf"] = true,
["audio/mpeg"] = true, ["audio/mpeg"] = true
} }
HEADER_FLAG = false
function html() function html()
if not HEADER_FLAG then if not RESPONSE_HEADER.sent then
std.chtml(SESSION) std.html()
HEADER_FLAG = true
end end
end end
function import(module)
return require(BASE_FRW.."silk.api."..module)
end

View File

@ -4,6 +4,7 @@ function Object:prototype(o)
o = o or {} -- create table if user does not provide one o = o or {} -- create table if user does not provide one
setmetatable(o, self) setmetatable(o, self)
self.__index = self self.__index = self
self.__tostring = o:tostring()
return o return o
end end
@ -13,8 +14,12 @@ function Object:new(o)
return obj return obj
end end
function Object:tostring()
return ""
end
function Object:print() function Object:print()
print('an Object') print(self:tostring())
end end
function Object:initialize() function Object:initialize()
@ -28,13 +33,7 @@ function Object:inherit(o)
return self:prototype(o) return self:prototype(o)
end end
function Object:extends(o) function Object:extends(o)
return self:inherit(o) return self:inherit(o)
end end
Test = Object:inherit{dummy = 0}
function Test:toWeb()
wio.t(self.dummy)
end

View File

@ -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')

View File

@ -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

View File

@ -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

210
silkmvc/core/hook.lua Normal file
View File

@ -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

115
silkmvc/core/mimes.lua Normal file
View File

@ -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

View File

@ -1,148 +1,193 @@
sqlite = modules.sqlite() sqlite = require("sqlitedb")
if sqlite == nil then return 0 end if sqlite == nil then
require("OOP") return 0
-- create class
DBModel = Object:inherit{db=nil, name=''}
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
end end
function DBModel:insert(m) 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
-- create class
DBModel = Object:inherit{
db = nil,
}
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(name, m)
local keys = {} local keys = {}
local values = {} local values = {}
for k,v in pairs(m) do for k, v in pairs(m) do
if k ~= "id" then if k ~= "id" then
table.insert(keys,k) table.insert(keys, k)
if type(v) == "number" then if type(v) == "number" then
table.insert(values, v) table.insert(values, v)
elseif type(v) == "boolean" then elseif type(v) == "boolean" then
table.insert( values, v and 1 or 0 ) table.insert(values, v and 1 or 0)
else else
local t = "\""..v:gsub('"', '""').."\"" local t = "\"" .. v:gsub('"', '""') .. "\""
table.insert(values,t) table.insert(values, t)
end end
end end
end end
local sql = "INSERT INTO "..self.name.." ("..table.concat(keys,',')..') VALUES (' local sql = "INSERT INTO " .. name .. " (" .. table.concat(keys, ',') .. ') VALUES ('
sql = sql..table.concat(values,',')..');' sql = sql .. table.concat(values, ',') .. ');'
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
function DBModel:get(id) function DBModel:get(name, id)
return sqlite.select(self.db, self.name, "*","id="..id)[1] 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 end
function DBModel:getAll() function DBModel:getAll(name)
--local sql = "SELECT * FROM "..self.name local data = self:query( "SELECT * FROM " .. name)
--return sqlite.select(self.db, self.name, "1=1") if not data then
local data = sqlite.select(self.db, self.name, "*", "1=1") return nil
if data == nil then return nil end end
local a = {} local a = {}
for n in pairs(data) do table.insert(a, n) end for n in pairs(data) do
table.insert(a, n)
end
table.sort(a) table.sort(a)
return data, a return data, a
end end
function DBModel:find(cond) function DBModel:find(name, cond)
local cnd = "1=1" local cnd = "1=1"
local sel = "*" local sel = "*"
if cond.exp then if cond.exp then
cnd = self:gencond(cond.exp) cnd = self:gencond(cond.exp)
end end
if cond.order then if cond.order then
cnd = cnd.." ORDER BY " cnd = cnd .. " ORDER BY "
local l = {} local l = {}
local i = 1 local i = 1
for k,v in pairs(cond.order) do for k, v in pairs(cond.order) do
l[i] = k.." "..v l[i] = k .. " " .. v
i = i+1 i = i + 1
end end
cnd = cnd..table.concat(l, ",") cnd = cnd .. table.concat(l, ",")
end end
if cond.limit then if cond.limit then
cnd = cnd.." LIMIT "..cond.limit cnd = cnd .. " LIMIT " .. cond.limit
end end
if cond.fields then if cond.fields then
sel = table.concat(cond.fields, ",") sel = table.concat(cond.fields, ",")
--print(sel) -- 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 end
--print(cnd)
local data = sqlite.select(self.db, self.name, sel, cnd)
if data == nil then return nil end
local a = {} local a = {}
for n in pairs(data) do table.insert(a, n) end for n in pairs(data) do
table.insert(a, n)
end
table.sort(a) table.sort(a)
return data, a return data, a
end end
function DBModel:query(sql) 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 end
function DBModel:update(m) 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:update(name, m)
local id = m['id'] local id = m['id']
if id ~= nil then if id ~= nil then
local lst = {} local lst = {}
for k,v in pairs(m) do for k, v in pairs(m) do
if(type(v)== "number") then if (type(v) == "number") then
table.insert(lst,k.."="..v) table.insert(lst, k .. "=" .. v)
elseif type(v) == "boolean" then elseif type(v) == "boolean" then
table.insert( lst, k.."="..(v and 1 or 0) ) table.insert(lst, k .. "=" .. (v and 1 or 0))
else else
table.insert(lst,k.."=\""..v:gsub('"', '""').."\"") table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"")
end end
end end
local sql = "UPDATE "..self.name.." SET "..table.concat(lst,",").." WHERE id="..id..";" local sql = "UPDATE " .. name .. " SET " .. table.concat(lst, ",") .. " WHERE id=" .. id .. ";"
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
return false return false
end end
function DBModel:available() function DBModel:available(name)
return sqlite.hasTable(self.db, self.name) == 1 local records = self:query(string.format("SELECT * FROM sqlite_master WHERE type='table' and name='%s'", name))
return #records == 1
end end
function DBModel:deleteByID(id) function DBModel:deleteByID(name, id)
local sql = "DELETE FROM "..self.name.." WHERE id="..id..";" local sql = "DELETE FROM " .. name .. " WHERE id=" .. id .. ";"
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
function DBModel:gencond(o) function DBModel:gencond(o)
for k,v in pairs(o) do for k, v in pairs(o) do
if k == "and" or k == "or" then if k == "and" or k == "or" then
local cnd = {} local cnd = {}
local i = 1 local i = 1
for k1,v1 in pairs(v) do for k1, v1 in pairs(v) do
cnd[i] = self:gencond(v1) cnd[i] = self:gencond(v1)
i = i + 1 i = i + 1
end end
return " ("..table.concat(cnd, " "..k.." ")..") " return " (" .. table.concat(cnd, " " .. k .. " ") .. ") "
else else
for k1,v1 in pairs(v) do for k1, v1 in pairs(v) do
local t = type(v1) local t = type(v1)
if(t == "string") then if (t == "string") then
return " ("..k1.." "..k..' "'..v1:gsub('"','""')..'") ' return " (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') .. '") '
end end
return " ("..k1.." "..k.." "..v1..") " return " (" .. k1 .. " " .. k .. " " .. v1 .. ") "
end end
end end
end end
end end
function DBModel:delete(cond) function DBModel:delete(name, cond)
local sql = "DELETE FROM "..self.name.." WHERE "..self:gencond(cond)..";" local sql = "DELETE FROM " .. name .. " WHERE " .. self:gencond(cond) .. ";"
return sqlite.query(self.db, sql) == 1 return self:exec(sql)
end end
function DBModel:lastInsertID() function DBModel:lastInsertID()
return sqlite.lastInsertID(self.db) return sqlite.last_insert_id(self.db)
end end
function DBModel:close() function DBModel:close()

View File

@ -1,22 +1,5 @@
bytes = modules.bytes() std = {}
array = modules.array() require("silk.core.mimes")
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
RESPONSE_HEADER = { RESPONSE_HEADER = {
status = 200, status = 200,
@ -25,147 +8,190 @@ RESPONSE_HEADER = {
sent = false 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) function std.status(code)
RESPONSE_HEADER.status=code RESPONSE_HEADER.status = code
end
function std.custom_header(k,v)
std.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
end end
function 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 RESPONSE_HEADER.header[k] = v
end end
function std.cjson(ck) function std.header_flush()
for k,v in pairs(ck) do -- send out status
std.setCookie(k.."="..v.."; Path=/") 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 end
std.header("Content-Type","application/json; charset=utf-8") -- send out cookie
std.header_flush() for key, val in ipairs(RESPONSE_HEADER.cookie) do
end echo("Set-Cookie: ", val, "\r\n")
function std.chtml(ck)
for k,v in pairs(ck) do
std.setCookie(k.."="..v.."; Path=/")
end end
std.header("Content-Type","text/html; charset=utf-8") echo("\r\n")
std.header_flush() RESPONSE_HEADER.sent = true
end RESPONSE_HEADER.header = {}
function std.t(s) RESPONSE_HEADER.cookie = {}
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 end
function std.setCookie(v) function std.setCookie(...)
RESPONSE_HEADER.cookie[#RESPONSE_HEADER.cookie] = v local args = table.pack(...)
cookie = table.concat(args,";")
RESPONSE_HEADER.cookie[#RESPONSE_HEADER.cookie + 1] = cookie
end end
function std.error(status, msg) function std.error(status, msg)
std._error(HTTP_REQUEST.id, status, msg) std.status(status)
std.header("Content-Type", "text/html")
std.header_flush()
echo(string.format("<HTML><HEAD><TITLE>%s</TITLE></HEAD><BODY><h2>%s</h2></BODY></HTML>",msg, msg))
end end
--_upload
--_route
function std.unknow(s) function std.unknow(s)
std.error(404, "Unknown request") std.error(404, "Unknown request")
end end
--_redirect function std.f(path)
--[[ function std.redirect(s) fcgio:send_file(path)
std._redirect(HTTP_REQUEST.id,s) end
end ]]
function std.html() function std.html()
std.header("Content-Type","text/html; charset=utf-8") std.header("Content-Type", "text/html; charset=utf-8")
std.header_flush() std.header_flush()
end end
function std.text() function std.text()
std.header("Content-Type","text/plain; charset=utf-8") std.header("Content-Type", "text/plain; charset=utf-8")
std.header_flush() std.header_flush()
end end
function std.json() function std.json()
std.header("Content-Type","application/json; charset=utf-8") std.header("Content-Type", "application/json; charset=utf-8")
std.header_flush() std.header_flush()
end end
function std.jpeg() function std.jpeg()
std.header("Content-Type","image/jpeg") std.header("Content-Type", "image/jpeg")
std.header_flush() std.header_flush()
end end
function std.octstream(s) function std.octstream(s)
std.header("Content-Type","application/octet-stream") std.header("Content-Type", "application/octet-stream")
std.header("Content-Disposition",'attachment; filename="'..s..'"') std.header("Content-Disposition", 'attachment; filename="' .. s .. '"')
std.header_flush() std.header_flush()
end 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 -- TODO provide web socket support
local proxy = {} -- use coroutine to read socket message
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
std.ws = {} std.ws = {}
function std.ws.header() function std.ws.header()
local h = std.ws_header(HTTP_REQUEST.id) local h = std.ws_header(HTTP_REQUEST.id)
if(h) then if (h) then
return h --std.readOnly(h) return h -- std.readOnly(h)
else else
return nil return nil
end end
end end
function std.ws.read(h) function std.ws.read(h)
return std.ws_read(HTTP_REQUEST.id,h) return std.ws_read(HTTP_REQUEST.id, h)
end end
function std.ws.swrite(s) function std.ws.swrite(s)
std.ws_t(HTTP_REQUEST.id,s) std.ws_t(HTTP_REQUEST.id, s)
end end
function std.ws.fwrite(s) function std.ws.fwrite(s)
std.ws_f(HTTP_REQUEST.id,s) std.ws_f(HTTP_REQUEST.id, s)
end end
function std.ws.write_bytes(arr) function std.ws.write_bytes(arr)
std.ws_b(HTTP_REQUEST.id,arr) std.ws_b(HTTP_REQUEST.id, arr)
end end
function std.ws.enable() 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 end
function std.ws.close(code) function std.ws.close(code)
std.ws_close(HTTP_REQUEST.id,code) std.ws_close(HTTP_REQUEST.id, code)
end 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.TEXT = 1
std.ws.BIN = 2 std.ws.BIN = 2
std.ws.CLOSE = 8 std.ws.CLOSE = 8

View File

@ -5,7 +5,9 @@ function utils.is_array(table)
local count = 0 local count = 0
for k, v in pairs(table) do for k, v in pairs(table) do
if type(k) == "number" then if type(k) == "number" then
if k > max then max = k end if k > max then
max = k
end
count = count + 1 count = count + 1
else else
return false return false
@ -20,7 +22,7 @@ end
function utils.escape(s) function utils.escape(s)
local replacements = { local replacements = {
["\\"] = "\\\\" , ["\\"] = "\\\\",
['"'] = '\\"', ['"'] = '\\"',
["\n"] = "\\n", ["\n"] = "\\n",
["\t"] = "\\t", ["\t"] = "\\t",
@ -29,7 +31,7 @@ function utils.escape(s)
["\r"] = "\\r", ["\r"] = "\\r",
["%"] = "%%" ["%"] = "%%"
} }
return (s:gsub( "[\\'\"\n\t\b\f\r%%]", replacements )) return (s:gsub("[\\'\"\n\t\b\f\r%%]", replacements))
end end
function utils.escape_pattern(s) function utils.escape_pattern(s)
@ -37,7 +39,7 @@ function utils.escape_pattern(s)
end end
function utils.unescape_pattern(s) function utils.unescape_pattern(s)
return s:gsub( "[%%]", "%%%%") return s:gsub("[%%]", "%%%%")
end end
function utils.hex_to_char(x) function utils.hex_to_char(x)
@ -45,28 +47,34 @@ function utils.hex_to_char(x)
end end
function utils.decodeURI(url) function utils.decodeURI(url)
return url:gsub("%%(%x%x)", utils.hex_to_char) return url:gsub("%%(%x%x)", utils.hex_to_char):gsub('+', ' ')
end end
function utils.unescape(s) function utils.unescape(s)
local str = "" local str = ""
local escape = false local escape = false
local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} local esc_map = {
for c in s:gmatch"." do b = '\b',
f = '\f',
n = '\n',
r = '\r',
t = '\t'
}
for c in s:gsub("%%%%", "%%"):gmatch "." do
if c ~= '\\' then if c ~= '\\' then
if escape then if escape then
if esc_map[c] then if esc_map[c] then
str = str..esc_map[c] str = str .. esc_map[c]
else else
str = str..c str = str .. c
end end
else else
str = str..c str = str .. c
end end
escape = false escape = false
else else
if escape then if escape then
str = str..c str = str .. c
escape = false escape = false
else else
escape = true escape = true
@ -77,8 +85,68 @@ function utils.unescape(s)
end end
function utils.file_exists(name) function utils.file_exists(name)
local f=io.open(name,"r") local f = io.open(name, "r")
if f~=nil then io.close(f) return true else return false end 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 end
function utils.url_parser(uri) function utils.url_parser(uri)
@ -89,8 +157,14 @@ function utils.url_parser(uri)
obj.port = uri:gsub(pattern, "%3") obj.port = uri:gsub(pattern, "%3")
obj.query = uri:gsub(pattern, "%4") obj.query = uri:gsub(pattern, "%4")
if obj.port == "" then obj.port = 80 else obj.port = tonumber(obj.port) end if obj.port == "" then
if obj.query == "" then obj.query="/" end obj.port = 80
else
obj.port = tonumber(obj.port)
end
if obj.query == "" then
obj.query = "/"
end
return obj return obj
end end
@ -102,52 +176,57 @@ function JSON.encode(obj)
-- encode object -- encode object
if utils.is_array(obj) == false then if utils.is_array(obj) == false then
local lst = {} local lst = {}
for k,v in pairs(obj) do for k, v in pairs(obj) do
table.insert(lst,'"'..k..'":'..JSON.encode(v)) table.insert(lst, '"' .. k .. '":' .. JSON.encode(v))
end end
return "{"..table.concat(lst,",").."}" return "{" .. table.concat(lst, ",") .. "}"
else else
local lst = {} local lst = {}
local a = {} local a = {}
for n in pairs(obj) do table.insert(a, n) end for n in pairs(obj) do
table.sort(a) table.insert(a, n)
for i,v in pairs(a) do
table.insert(lst,JSON.encode(obj[v]))
end end
return "["..table.concat(lst,",").."]" table.sort(a)
for i, v in pairs(a) do
table.insert(lst, JSON.encode(obj[v]))
end
return "[" .. table.concat(lst, ",") .. "]"
end end
elseif t == 'string' then elseif t == 'string' then
--print('"'..utils.escape(obj)..'"') -- print('"'..utils.escape(obj)..'"')
return '"'..utils.escape(obj)..'"' return '"' .. utils.escape(obj) .. '"'
elseif t == 'boolean' or t == 'number' then elseif t == 'boolean' or t == 'number' then
return tostring(obj) return tostring(obj)
elseif obj == nil then elseif obj == nil then
return "null" return "null"
else else
return '"'..tostring(obj)..'"' return '"' .. tostring(obj) .. '"'
end end
end end
function explode(str, div) -- credit: http://richard.warburton.it function explode(str, div) -- credit: http://richard.warburton.it
if (div=='') then return false end if (div == '') then
local pos,arr = 0,{} return false
end
local pos, arr = 0, {}
-- for each divider found -- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do for st, sp in function()
table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider 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 pos = sp + 1 -- Jump past current divider
end end
table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider table.insert(arr, string.sub(str, pos)) -- Attach chars right of last divider
return arr return arr
end end
function implode(arr, div) function implode(arr, div)
return table.concat(arr,div) return table.concat(arr, div)
end end
function firstToUpper(str) function firstToUpper(str)
return (str:gsub("^%l", string.upper)) return (str:gsub("^%l", string.upper))
end end
local charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" local charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
function utils.generate_salt(length) function utils.generate_salt(length)
@ -159,3 +238,12 @@ function utils.generate_salt(length)
end end
return table.concat(ret) return table.concat(ret)
end end
function utils.ext(path)
return path:match("%.([^%.]*)$")
end
function utils.basename(str)
local name = string.gsub(ulib.trim(str, "/"), "(.*/)(.*)", "%2")
return name
end

View File

@ -2,26 +2,26 @@
-- the rewrite rule for the framework -- the rewrite rule for the framework
-- should be something like this -- should be something like this
-- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&<query> -- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&<query>
-- require needed library
BASE_FRW = ""
require(BASE_FRW.."api")
-- some global variables -- some global variables
DIR_SEP = "/"
WWW_ROOT = "/opt/www/htdocs/apps" WWW_ROOT = "/opt/www/htdocs/apps"
HTTP_ROOT = "https://apps.localhost:9195/" HTTP_ROOT = "https://apps.localhost:9195/"
-- class path: path.to.class
BASE_FRW = ""
-- class path: path.to.class -- class path: path.to.class
CONTROLLER_ROOT = BASE_FRW.."apps.controllers" CONTROLLER_ROOT = BASE_FRW.."apps.controllers"
MODEL_ROOT = BASE_FRW.."apps.models" MODEL_ROOT = BASE_FRW.."apps.models"
-- file path: path/to/file -- file path: path/to/file
VIEW_ROOT = WWW_ROOT..DIR_SEP.."views" 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 -- registry object store global variables
local REGISTRY = {} local REGISTRY = {}
-- set logging level -- 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.db = DBHelper:new{db="iosapps"}
REGISTRY.layout = 'default' REGISTRY.layout = 'default'
@ -31,7 +31,7 @@ REGISTRY.router = router
router:setPath(CONTROLLER_ROOT) router:setPath(CONTROLLER_ROOT)
--router:route('edit', 'post/edit', "ALL" ) --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 -- each layout may have different dependencies
local default_routes_dependencies = { local default_routes_dependencies = {
edit = { edit = {

3
test/ad.ls Normal file
View File

@ -0,0 +1,3 @@
<?lua
echo(ad)
?>

3
test/detail.ls Normal file
View File

@ -0,0 +1,3 @@
<?lua
echo("Post ID:", data.id, "\n", "Content:", data.content,"\n")
?>

4
test/layout.ls Normal file
View File

@ -0,0 +1,4 @@
<?lua
__main__:render()
ad:render()
?>

46
test/lunit.lua Normal file
View File

@ -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

37
test/request.json Normal file
View File

@ -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"
}

396
test/test_core.lua Normal file
View File

@ -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\n<HTML><HEAD><TITLE>No page found</TITLE></HEAD><BODY><h2>No page found</h2></BODY></HTML>")
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()

270
test/test_silk.lua Normal file
View File

@ -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()