WebMCP is a completely new web development framework, and has not been extensively tested yet. The API might change at any time, but in future releases there will be a list of all changes, which break downward compatibility.
WebMCP has been developed on Linux and FreeBSD. Using it with Mac OS X is completely untested; Microsoft Windows is not supported. Beside the operating system, the only mandatory dependencies for WebMCP are the programming language Lua version 5.1, PostgreSQL version 8.2 or higher, a C compiler, and a Webserver like Lighttpd or Apache.
After downloading the tar.gz package, unpack it, enter the unpacked directory and type make. If you use Mac OS X or if you experience problems during compilation, you need to edit the Makefile.options file prior to compilation. The framework itself will be available in the framework/ directory, while a demo application is available in the demo-app/ directory. The framework.precompiled/ and demo-app.precompiled/ directories will contain a version with all Lua files being byte-code pre-compiled, which can be used instead. You may copy these directories (with cp -L to follow links) to any other place you like. Use the files doc/lighttpd.sample.conf or doc/apache.sample.conf to setup your webserver appropriatly. Don't forget to setup a database, and make the tmp/ directory of the application writable for the web server process. Good luck and have fun!
Lua itself has only very few built-in data types. The atom library gives support for extra data types. Currently the following extra data types are provided:
In addition the following pseudo-types are existent, corresponding to Lua's base types:
Both atom.integer and atom.number refer to Lua's base type “number”.
New values of atom data types are created by either calling atom.type:load(string_representation) or by calling atom.type{...}, e.g. atom.date{year=1970, month=1, day=1}. You can dump any atom value as a string by calling atom.dump(value) and later reload it with atom.type:load(string).
The library “mondelefant” shipping with WebMCP can be used to access PostgreSQL databases. It also serves as an Object-Relational Mapper (ORM). Opening a connection to a database is usually done in a config file in the following way:
db = assert( mondelefant.connect{ engine='postgresql', dbname='webmcp_demo' } ) at_exit(function() db:close() end) function mondelefant.class_prototype:get_db_conn() return db end function db:sql_tracer(command) return function(error_info) local error_info = error_info or {} trace.sql{ command = command, error_position = error_info.position } end end
Overwriting the sql_tracer method of the database handle is optional, but helpful for debugging. The parameters for mondelefant.connect are directly passed to PostgreSQL's client library libpq. See PostgreSQL's documentation on PQconnect for information about supported parameters.
To define a model to be used within a WebMCP application, create a file named with the name of the model and .lua as extension in the model/ directory of your application. The most basic definition of a model (named “movie” in this example) is:
Movie = mondelefant.new_class() Movie.table = 'movie'
Note: Model classes are always written CamelCase, while the name of the file in model/ is written lower_case.
To select objects from the database, the mondelefant library provides a selector framework:
local s = Movie:new_selector() s:add_where{ 'id = ?', param.get_id() } s:single_object_mode() -- return single object instead of list local movie = s:exec()
A short form of the above query would be:
local movie = Movie:new_selector():add_where{ 'id = ?', param.get_id() }:single_object_mode():exec()
For more examples about how to use the model system, please take a look at the demo application.
As opposed to other web application frameworks, WebMCP does not use a Model-View-Controller (MVC) concept, but a Model-View-Action (MVA) concept.
The models in MVA are like the models in MVC; they are used to access data stored in a relational database (PostgreSQL) in an object oriented way. They can also be used to provide methods for working with objects representing the database entries.
The views in the MVA concept are different from the views in the MVC concept. As WebMCP has no controllers, the views are responsible for processing the GET/POST parameters from the webbrowser, fetching the data to be displayed, and creating the output by directly writing HTML to slots in a layout or by calling helper functions for the user interface.
Actions are similar to views, but supposed to change data in the database, hence only callable by HTTP POST requests. They are also responsible for processing the POST parameters from the webbrowser. They can modify the database, but instead of rendering a page to be displayed, they just return a status code. Depending on the status code there will be an internal forward or an HTTP 303 redirect to a view. When calling an action via a POST request, additional POST parameters, which are usually added by hidden form fields, determine the view to be displayed for each status code returned by the action.
app -- table to store an application state
app = {}
at_exit(
func -- function to be called before the process is ending
)
do local exit_handlers = {} function at_exit(func) table.insert(exit_handlers, func) end function exit(code) for i = #exit_handlers, 1, -1 do exit_handlers[i]() end os.exit(code) end end
bool = -- true, false, or nil atom.boolean:load( string -- string to be interpreted as boolean )
function boolean:load(str)
if str == nil or str == "" then
return nil
elseif type(str) ~= "string" then
error("String expected")
elseif string.find(str, "^[TtYy1]") then
return true
elseif string.find(str, "^[FfNn0]") then
return false
else
return nil -- we don't have an undefined bool
end
end
atom.date.invalid
date.invalid = date:_create{ jd = not_a_number, year = not_a_number, month = not_a_number, day = not_a_number, invalid = true }
year, -- year month, -- month from 1 to 12 day = -- day from 1 to 31 atom.date.jd_to_ymd( jd, -- days from January 1st 1970 )
function date.jd_to_ymd(jd) assert(is_integer(jd), "Invalid julian date specified.") local calc_jd = jd + offset assert(is_integer(calc_jd), "Julian date is out of range.") local n400 = math.floor(calc_jd / c400) local r400 = calc_jd % c400 local n100 = math.floor(r400 / c100) local r100 = r400 % c100 if n100 == 4 then n100, r100 = 3, c100 end local n4 = math.floor(r100 / c4) local r4 = r100 % c4 local n1 = math.floor(r4 / c1) local r1 = r4 % c1 if n1 == 4 then n1, r1 = 3, c1 end local year = 1 + 400 * n400 + 100 * n100 + 4 * n4 + n1 local month = 1 + math.floor(r1 / 31) local month_offset = get_month_offset(year, month) if month < 12 then local next_month_offset = get_month_offset(year, month + 1) if r1 >= next_month_offset then month = month + 1 month_offset = next_month_offset end end local day = 1 + r1 - month_offset return year, month, day end
jd = -- days from January 1st 1970 atom.date.ymd_to_jd( year, -- year month, -- month from 1 to 12 day -- day from 1 to 31 )
local offset = 0 function date.ymd_to_jd(year, month, day) assert(is_integer(year), "Invalid year specified.") assert(is_integer(month), "Invalid month specified.") assert(is_integer(day), "Invalid day specified.") local calc_year = year - 1 local n400 = math.floor(calc_year / 400) local r400 = calc_year % 400 local n100 = math.floor(r400 / 100) local r100 = r400 % 100 local n4 = math.floor(r100 / 4) local n1 = r100 % 4 local jd = ( c400 * n400 + c100 * n100 + c4 * n4 + c1 * n1 + get_month_offset(year, month) + (day - 1) ) return jd - offset end offset = date.ymd_to_jd(1970, 1, 1)
atom.date:get_current()
function date:get_current() local now = os.date("*t") return date{ year = now.year, month = now.month, day = now.day } end
date = -- date represented by the string atom.date:load( string -- string representing a date )
function date:load(str) if str == nil or str == "" then return nil elseif type(str) ~= "string" then error("String expected") else local year, month, day = string.match( str, "^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$" ) if year then return date{ year = tonumber(year), month = tonumber(month), day = tonumber(day) } else return date.invalid end end end
d = -- date based on the given data atom.date:new{ jd = jd, -- days since January 1st 1970 year = year, -- year month = month, -- month from 1 to 12 day = day, -- day from 1 to 31 iso_weekyear = iso_weekyear, -- year according to ISO 8601 iso_week = iso_week, -- week number according to ISO 8601 iso_weekday = iso_weekday, -- day of week from 1 for monday to 7 for sunday us_weekyear = us_weekyear, -- year us_week = us_week, -- week number according to US style counting us_weekday = us_weekday -- day of week from 1 for sunday to 7 for saturday }
function date:new(args) local args = args if type(args) == "number" then args = { jd = args } end if type(args) == "table" then local year, month, day = args.year, args.month, args.day local jd = args.jd local iso_weekyear = args.iso_weekyear local iso_week = args.iso_week local iso_weekday = args.iso_weekday local us_week = args.us_week local us_weekday = args.us_weekday if type(year) == "number" and type(month) == "number" and type(day) == "number" then if is_integer(year) and year >= 1 and year <= 9999 and is_integer(month) and month >= 1 and month <= 12 and is_integer(day) and day >= 1 and day <= 31 then return date:_create{ jd = date.ymd_to_jd(year, month, day), year = year, month = month, day = day } else return date.invalid end elseif type(jd) == "number" then if is_integer(jd) and jd >= -719162 and jd <= 2932896 then local year, month, day = date.jd_to_ymd(jd) return date:_create{ jd = jd, year = year, month = month, day = day } else return date.invalid end elseif type(year) == "number" and not iso_weekyear and type(iso_week) == "number" and type(iso_weekday) == "number" then if is_integer(year) and is_integer(iso_week) and iso_week >= 0 and iso_week <= 53 and is_integer(iso_weekday) and iso_weekday >= 1 and iso_weekday <= 7 then local jan4 = date{ year = year, month = 1, day = 4 } local reference = jan4 - jan4.iso_weekday - 7 -- Sun. of week -1 return date(reference + 7 * iso_week + iso_weekday) else return date.invalid end elseif type(iso_weekyear) == "number" and not year and type(iso_week) == "number" and type(iso_weekday) == "number" then if is_integer(iso_weekyear) and is_integer(iso_week) and iso_week >= 0 and iso_week <= 53 and is_integer(iso_weekday) and iso_weekday >= 1 and iso_weekday <= 7 then local guessed = date{ year = iso_weekyear, iso_week = iso_week, iso_weekday = iso_weekday } if guessed.invalid or guessed.iso_weekyear == iso_weekyear then return guessed else local year if iso_week <= 1 then year = iso_weekyear - 1 elseif iso_week >= 52 then year = iso_weekyear + 1 else error("Internal error in ISO week computation occured.") end return date{ year = year, iso_week = iso_week, iso_weekday = iso_weekday } end else return date.invalid end elseif type(year) == "number" and type(us_week) == "number" and type(us_weekday) == "number" then if is_integer(year) and is_integer(us_week) and us_week >= 0 and us_week <= 54 and is_integer(us_weekday) and us_weekday >= 1 and us_weekday <= 7 then local jan1 = date{ year = year, month = 1, day = 1 } local reference = jan1 - jan1.us_weekday - 7 -- Sat. of week -1 return date(reference + 7 * us_week + us_weekday) else return date.invalid end end end error("Illegal arguments passed to date constructor.") end
string = -- string representation to be passed to a load function atom.dump( value -- value to be dumped )
function dump(obj) if obj == nil then return "" else return tostring(obj) end end
atom.fraction.invalid
fraction.invalid = fraction:_create{ numerator = not_a_number, denominator = not_a_number, invalid = true }
frac = -- fraction represented by the given string atom.fraction:load( string -- string representation of a fraction )
function fraction:load(str) if str == nil or str == "" then return nil elseif type(str) ~= "string" then error("String expected") else local sign, int = string.match(str, "^(%-?)([0-9]+)$") if sign == "" then return fraction:new(tonumber(int)) elseif sign == "-" then return fraction:new(- tonumber(int)) end local sign, n, d = string.match(str, "^(%-?)([0-9]+)/([0-9]+)$") if sign == "" then return fraction:new(tonumber(n), tonumber(d)) elseif sign == "-" then return fraction:new(- tonumber(n), tonumber(d)) end return fraction.invalid end end
frac = -- fraction atom.fraction:new( numerator, -- numerator denominator -- denominator )
function fraction:new(numerator, denominator) if not ( (numerator == nil or type(numerator) == "number") and (denominator == nil or type(denominator) == "number") ) then error("Invalid arguments passed to fraction constructor.") elseif (not is_integer(numerator)) or (denominator and (not is_integer(denominator))) then return fraction.invalid elseif denominator then if denominator == 0 then return fraction.invalid elseif numerator == 0 then return fraction:_create{ numerator = 0, denominator = 1, float = 0 } else local d = gcd(math.abs(numerator), math.abs(denominator)) if denominator < 0 then d = -d end local numerator2, denominator2 = numerator / d, denominator / d return fraction:_create{ numerator = numerator2, denominator = denominator2, float = numerator2 / denominator2 } end else return fraction:_create{ numerator = numerator, denominator = 1, float = numerator } end end
i = -- the greatest common divisor (GCD) of all given natural numbers atom.gcd( a, -- a natural number b, -- another natural number ... -- optionally more natural numbers )
function gcd(a, b, ...) if a % 1 ~= 0 or a <= 0 then return 0 / 0 end if b == nil then return a else if b % 1 ~= 0 or b <= 0 then return 0 / 0 end if ... == nil then local k = 0 local t while a % 2 == 0 and b % 2 == 0 do a = a / 2; b = b / 2; k = k + 1 end if a % 2 == 0 then t = a else t = -b end while t ~= 0 do while t % 2 == 0 do t = t / 2 end if t > 0 then a = t else b = -t end t = a - b end return a * 2 ^ k else return gcd(gcd(a, b), ...) end end end
bool = -- true, if 'value' is of type 't' atom.has_type( value, -- any value t -- atom time, e.g. atom.date, or lua type, e.g. "string" )
function has_type(value, t) if t == nil then error("No type passed to has_type(...) function.") end local lua_type = type(value) return lua_type == t or getmetatable(value) == t or (lua_type == "boolean" and t == _M.boolean) or (lua_type == "string" and t == _M.string) or ( lua_type == "number" and (t == _M.number or ( t == _M.integer and ( not (value <= 0 or value >= 0) or ( value % 1 == 0 and (value + 1) - value == 1 and value - (value - 1) == 1 ) ) )) ) end
atom.integer.invalid
integer.invalid = not_a_number --// ------------ -- number -- ------------ number = create_new_type("number") --[[-- int = -- a number or atom.number.invalid (atom.not_a_number) atom.number:load( string -- a string representing a number ) This method returns a number represented by the given string. If the string doesn't represent a valid number, then not-a-number is returned. --]]-- function number:load(str) if str == nil or str == "" then return nil elseif type(str) ~= "string" then error("String expected") else return tonumber(str) or not_a_number end end
int = -- an integer or atom.integer.invalid (atom.not_a_number) atom.integer:load( string -- a string representing an integer )
function integer:load(str) if str == nil or str == "" then return nil elseif type(str) ~= "string" then error("String expected") else local num = tonumber(str) if is_integer(num) then return num else return not_a_number end end end
bool = -- true, if value is an integer within resolution atom.is_integer( value -- value to be tested )
function is_integer(i) return type(i) == "number" and i % 1 == 0 and (i + 1) - i == 1 and i - (i - 1) == 1 end
bool = -- true, if 'value' is of type 't' atom.is_valid( value, -- any value t -- atom time, e.g. atom.date, or lua type, e.g. "string" )
function is_valid(value, t) local lua_type = type(value) if lua_type == "table" then local mt = getmetatable(value) if t then return t == mt and not value.invalid else return (getmetatable(mt) == type_mt) and not value.invalid end elseif lua_type == "boolean" then return not t or t == "boolean" or t == _M.boolean elseif lua_type == "string" then return not t or t == "string" or t == _M.string elseif lua_type == "number" then if t == _M.integer then return value % 1 == 0 and (value + 1) - value == 1 and value - (value - 1) == 1 else return (not t or t == "number" or t == _M.number) and (value <= 0 or value >= 0) end else return false end end
i = --the least common multiple (LCD) of all given natural numbers atom.lcm( a, -- a natural number b, -- another natural number ... -- optionally more natural numbers )
function lcm(a, b, ...) if a % 1 ~= 0 or a <= 0 then return 0 / 0 end if b == nil then return a else if b % 1 ~= 0 or b <= 0 then return 0 / 0 end if ... == nil then return a * b / gcd(a, b) else return lcm(lcm(a, b), ...) end end end
atom.not_a_number
not_a_number = 0 / 0
atom.number.invalid
number.invalid = not_a_number
string = -- the same string atom.string:load( string -- a string )
function _M.string:load(str) if str == nil then return nil elseif type(str) ~= "string" then error("String expected") else return str end end
t = -- current time of day
atom.time:get_current()
function time:get_current() local now = os.date("*t") return time{ hour = now.hour, minute = now.min, second = now.sec } end
t = -- time represented by the string atom.time:load( string -- string representing a time of day )
function time:load(str) if str == nil or str == "" then return nil elseif type(str) ~= "string" then error("String expected") else local hour, minute, second = string.match( str, "^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$" ) if hour then return time{ hour = tonumber(hour), minute = tonumber(minute), second = tonumber(second) } else return time.invalid end end end
t = -- time based on given data atom.time:new{ dsec = dsec, -- seconds since 00:00:00 hour = hour, -- hour from 0 to 23 minute = minute, -- minute from 0 to 59 second = second -- second from 0 to 59 }
function time:new(args) local args = args if type(args) == "number" then args = { dsec = args } end if type(args) == "table" then if not args.second then args.second = 0 if not args.minute then args.minute = 0 end end if type(args.hour) == "number" and type(args.minute) == "number" and type(args.second) == "number" then if is_integer(args.hour) and args.hour >= 0 and args.hour <= 23 and is_integer(args.minute) and args.minute >= 0 and args.minute <= 59 and is_integer(args.second) and args.second >= 0 and args.second <= 59 then return time:_create{ dsec = time.hms_to_dsec(args.hour, args.minute, args.second), hour = args.hour, minute = args.minute, second = args.second } else return time.invalid end elseif type(args.dsec) == "number" then if is_integer(args.dsec) and args.dsec >= 0 and args.dsec <= 86399 then local hour, minute, second = time.dsec_to_hms(args.dsec) return time:_create{ dsec = args.dsec, hour = hour, minute = minute, second = second } else return time.invalid end end end error("Invalid arguments passed to time constructor.") end
year, -- year month, -- month from 1 to 12 day, -- day from 1 to 31 hour, -- hour from 0 to 23 minute, -- minute from 0 to 59 second = -- second from 0 to 59 atom.timestamp.tsec_to_ymdhms( tsec -- seconds since January 1st 1970 00:00 )
function timestamp.tsec_to_ymdhms(tsec) local jd = math.floor(tsec / 86400) local dsec = tsec % 86400 local year, month, day = date.jd_to_ymd(jd) local hour = math.floor(dsec / 3600) local minute = math.floor((dsec % 3600) / 60) local second = dsec % 60 return year, month, day, hour, minute, second end
tsec = -- seconds since January 1st 1970 00:00 atom.timestamp.ymdhms_to_tsec( year, -- year month, -- month from 1 to 12 day, -- day from 1 to 31 hour, -- hour from 0 to 23 minute, -- minute from 0 to 59 second -- second from 0 to 59 )
function timestamp.ymdhms_to_tsec(year, month, day, hour, minute, second) return 86400 * date.ymd_to_jd(year, month, day) + 3600 * hour + 60 * minute + second end
ts = -- current date/time as timestamp
atom.timestamp:get_current()
function timestamp:get_current() local now = os.date("*t") return timestamp{ year = now.year, month = now.month, day = now.day, hour = now.hour, minute = now.min, second = now.sec } end
ts = -- timestamp represented by the string atom.timestamp:load( string -- string representing a timestamp )
function timestamp:load(str) if str == nil or str == "" then return nil elseif type(str) ~= "string" then error("String expected") else local year, month, day, hour, minute, second = string.match( str, "^([0-9][0-9][0-9][0-9])%-([0-9][0-9])%-([0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$" ) if year then return timestamp{ year = tonumber(year), month = tonumber(month), day = tonumber(day), hour = tonumber(hour), minute = tonumber(minute), second = tonumber(second) } else return timestamp.invalid end end end function timestamp:__tostring() if self.invalid then return "invalid_timestamp" else return string.format( "%04i-%02i-%02i %02i:%02i:%02i", self.year, self.month, self.day, self.hour, self.minute, self.second ) end end function timestamp.getters:date() return date{ year = self.year, month = self.month, day = self.day } end function timestamp.getters:time() return time{ hour = self.hour, minute = self.minute, second = self.second } end function timestamp.__eq(value1, value2) if value1.invalid or value2.invalid then return false else return value1.tsec == value2.tsec end end function timestamp.__lt(value1, value2) if value1.invalid or value2.invalid then return false else return value1.tsec < value2.tsec end end function timestamp.__le(value1, value2) if value1.invalid or value2.invalid then return false else return value1.tsec <= value2.tsec end end function timestamp.__add(value1, value2) if getmetatable(value1) == timestamp then if getmetatable(value2) == timestamp then error("Can not add two timestamps.") elseif type(value2) == "number" then return timestamp(value1.tsec + value2) else error("Right operand of '+' operator has wrong type.") end elseif type(value1) == "number" then if getmetatable(value2) == timestamp then return timestamp(value1 + value2.tsec) else error("Assertion failed") end else error("Left operand of '+' operator has wrong type.") end end function timestamp.__sub(value1, value2) if not getmetatable(value1) == timestamp then error("Left operand of '-' operator has wrong type.") end if getmetatable(value2) == timestamp then return value1.tsec - value2.tsec -- TODO: transform to interval elseif type(value2) == "number" then return timestamp(value1.tsec - value2) else error("Right operand of '-' operator has wrong type.") end end ---------- -- time -- ---------- time = create_new_type("time") function time.hms_to_dsec(hour, minute, second) return 3600 * hour + 60 * minute + second end function time.dsec_to_hms(dsec) local hour = math.floor(dsec / 3600) local minute = math.floor((dsec % 3600) / 60) local second = dsec % 60 return hour, minute, second end --[[-- atom.time.invalid Value representing an invalid time of day. --]]-- time.invalid = time:_create{ dsec = not_a_number, hour = not_a_number, minute = not_a_number, second = not_a_number, invalid = true }
ts = -- timestamp based on given data atom.timestamp:new{ tsec = tsec, -- seconds since January 1st 1970 00:00 year = year, -- year month = month, -- month from 1 to 12 day = day, -- day from 1 to 31 hour = hour, -- hour from 0 to 23 minute = minute, -- minute from 0 to 59 second = second -- second from 0 to 59 }
function timestamp:new(args) local args = args if type(args) == "number" then args = { tsec = args } end if type(args) == "table" then if not args.second then args.second = 0 if not args.minute then args.minute = 0 if not args.hour then args.hour = 0 end end end if type(args.year) == "number" and type(args.month) == "number" and type(args.day) == "number" and type(args.hour) == "number" and type(args.minute) == "number" and type(args.second) == "number" then if is_integer(args.year) and args.year >= 1 and args.year <= 9999 and is_integer(args.month) and args.month >= 1 and args.month <= 12 and is_integer(args.day) and args.day >= 1 and args.day <= 31 and is_integer(args.hour) and args.hour >= 0 and args.hour <= 23 and is_integer(args.minute) and args.minute >= 0 and args.minute <= 59 and is_integer(args.second) and args.second >= 0 and args.second <= 59 then return timestamp:_create{ tsec = timestamp.ymdhms_to_tsec( args.year, args.month, args.day, args.hour, args.minute, args.second ), year = args.year, month = args.month, day = args.day, hour = args.hour, minute = args.minute, second = args.second } else return timestamp.invalid end elseif type(args.tsec) == "number" then if is_integer(args.tsec) and args.tsec >= -62135596800 and args.tsec <= 253402300799 then local year, month, day, hour, minute, second = timestamp.tsec_to_ymdhms(args.tsec) return timestamp:_create{ tsec = args.tsec, year = year, month = month, day = day, hour = hour, minute = minute, second = second } else return timestamp.invalid end end end error("Invalid arguments passed to timestamp constructor.") end
current_charset = -- currently selected character set to be used
charset.get()
function charset.get() return charset._current end
charset_data = -- table containing information about the current charset
charset.get_data()
function charset.get_data() return charset.data[ string.gsub(string.lower(charset._current), "%-", "_") ] end
charset.set(
charset_ident -- identifier of a charset, i.e. "UTF-8"
)
function charset.set(charset_ident) charset._current = charset_ident end
config -- table to store application configuration
config = {}
path = -- string containing a path to an action encode.action_file_path{ module = module, -- module name action = action -- action name }
function encode.action_file_path(args) return (encode.file_path( request.get_app_basepath(), 'app', request.get_app_name(), args.module, '_action', args.action .. '.lua' )) end
path = -- string containing a (file) path encode.concat_file_path( element1, -- first part of the path element2, -- second part of the path ... -- more parts of the path )
function encode.concat_file_path(...) return ( string.gsub( table.concat({...}, "/"), "/+", "/" ) ) end
path = -- string containing a (file) path encode.encode_file_path( base_path, element1, -- next part of the path element2, -- next part of the path ... )
function encode.file_path(base, ...) -- base argument is not encoded
local raw_elements = {...}
local encoded_elements = {}
for i = 1, #raw_elements do
encoded_elements[i] = encode.file_path_element(raw_elements[i])
end
return encode.concat_file_path(base, unpack(encoded_elements))
end
encoded_path_element = -- string which can't contain evil stuff like "/" encode.file_path_element( path_element -- string to be encoded )
function encode.file_path_element(path_element) return ( string.gsub( string.gsub( path_element, "[^0-9A-Za-z_%.-]", function(char) return string.format("%%%02x", string.byte(char)) end ), "^%.", string.format("%%%%%02x", string.byte(".")) ) ) end
string = -- string to be used as __format information encode.format_info( format, -- name of format function params -- arguments for format function )
function encode.format_info(format, params) return format .. encode.format_options(params) end
string = -- part of string to be used as __format information encode.format_options( params -- arguments for format function )
function encode.format_options(params) local params = params or {} local result_parts = {} for key, value in pairs(params) do if type(key) == "string" then if string.find(key, "^[A-Za-z][A-Za-z0-9_]*$") then table.insert(result_parts, "-") table.insert(result_parts, key) table.insert(result_parts, "-") local t = type(value) if t == "string" then value = string.gsub(value, "\\", "\\\\") value = string.gsub(value, "'", "\\'") table.insert(result_parts, "'") table.insert(result_parts, value) table.insert(result_parts, "'") elseif t == "number" then table.insert(result_parts, tostring(value)) elseif t == "boolean" then table.insert(result_parts, value and "true" or "false") else error("Format parameter table contained value of unsupported type " .. t .. ".") end else error('Format parameter table contained invalid key "' .. key .. '".') end end end return table.concat(result_parts) end
result = -- encoded string encode.html( str -- original string )
function encode.html(text)
-- TODO: perhaps filter evil control characters?
return (
string.gsub(
text, '[<>&"]',
function(char)
if char == '<' then
return "<"
elseif char == '>' then
return ">"
elseif char == '&' then
return "&"
elseif char == '"' then
return """
end
end
)
)
end
text_with_br_tags = -- text with <br/> tags encode.html_newlines( text_with_lf_control_characters -- text with LF control characters )
function encode.html_newlines(text) return (string.gsub(text, '\n', '<br/>')) end
json_string = -- JavaScript code representing the given datum (with quotes, if needed) encode.json( obj -- true, false, nil or a number or string )
-- TODO: check if numeric representations are JSON compatible function encode.json(obj) if obj == nil then return "null"; elseif atom.has_type(obj, atom.boolean) then return tostring(obj) elseif atom.has_type(obj, atom.number) then return tostring(obj) elseif type(obj) == "table" then local parts = {} local first = true if #obj > 0 then parts[#parts+1] = "[" for idx, value in ipairs(obj) do if first then first = false else parts[#parts+1] = "," end parts[#parts+1] = tostring(value) end parts[#parts+1] = "]" else parts[#parts+1] = "{" for key, value in pairs(obj) do if first then first = false else parts[#parts+1] = "," end parts[#parts+1] = encode.json(key) parts[#parts+1] = ":" parts[#parts+1] = encode.json(value) end parts[#parts+1] = "}" end return table.concat(parts) else return '"' .. string.gsub(atom.dump(obj), ".", function (char) if char == '\r' then return '\\r' end if char == '\n' then return '\\n' end if char == '\\' then return '\\\\' end if char == '"' then return '\\"' end if char == '/' then return '\\/' end -- allowed according to RFC4627, needed for </script> local byte = string.byte(char) if byte < 32 then return string.format("\\u%04x", byte) end end ) .. '"' end end
url_encoded_string = -- URL-encoded string encode.url_part( obj -- any native datatype or atom )
function encode.url_part(obj) return ( string.gsub( atom.dump(obj), "[^0-9A-Za-z_%.~-]", function (char) return string.format("%%%02x", string.byte(char)) end ) ) end
url_string = -- a string containing an URL encode.url{ external = external, -- external URL (instead of specifying base, module, etc. below) base = base, -- optional string containing a base URL of a WebMCP application static = static, -- an URL relative to the static file directory module = module, -- a module name of the WebMCP application view = view, -- a view name of the WebMCP application action = action, -- an action name of the WebMCP application id = id, -- optional id to be passed to the view or action to select a particular data record params = params -- optional parameters to be passed to the view or action }
function encode.url(args) local external = args.external local base = args.base or request.get_relative_baseurl() local static = args.static local module = args.module local view = args.view local action = args.action local id = args.id local params = args.params or {} local result = {} local id_as_param = false local function add(...) for i = 1, math.huge do local v = select(i, ...) if v == nil then break end result[#result+1] = v end end if external then add(external) else add(base) if not string.find(base, "/$") then add("/") end if static then add("static/") add(static) elseif module or view or action or id then assert(module, "Module not specified.") add(encode.url_part(module), "/") if view and not action then local view_base, view_suffix = string.match( view, "^([^.]*)(.*)$" ) add(encode.url_part(view_base)) if args.id then add("/", encode.url_part(id)) end if view_suffix == "" then add(".html") else add(view_suffix) -- view_suffix includes dot as first character end elseif action and not view then add(encode.url_part(action)) id_as_param = true elseif view and action then error("Both a view and an action was specified.") end end do local new_params = request.get_perm_params() for key, value in pairs(params) do new_params[key] = value end params = new_params end end if next(params) ~= nil or (id and id_as_param) then add("?") if id and id_as_param then add("_webmcp_id=", encode.url_part(id), "&") end for key, value in pairs(params) do add(encode.url_part(key), "=", encode.url_part(value), "&") end result[#result] = nil -- remove last '&' or '?' end return table.concat(result) end
path = -- string containing a path to a view encode.view_file_path{ module = module, -- module name view = view -- view name }
function encode.view_file_path(args) return (encode.file_path( request.get_app_basepath(), 'app', request.get_app_name(), args.module, args.view .. '.lua' )) end
action_status = -- status code returned by the action (a string) execute.action{ module = module, -- module name of the action to be executed action = action, -- name of the action to be executed id = id, -- id to be returned by param.get_id(...) during execution params = params -- parameters to be returned by param.get(...) during execution }
function execute.action(args) local module = args.module local action = args.action trace.enter_action{ module = module, action = action } local action_status = execute.file_path{ file_path = encode.file_path( request.get_app_basepath(), 'app', request.get_app_name(), module, '_action', action .. '.lua' ), id = args.id, params = args.params } trace.execution_return{ status = action_status } return action_status end
execute.config(
name -- name of the configuration to be loaded
)
function execute.config(name) trace.enter_config{ name = name } execute.file_path{ file_path = encode.file_path( request.get_app_basepath(), 'config', name .. '.lua' ) } trace.execution_return() end
status_code = -- status code returned by the executed lua file (a string) execute.file_path{ file_path = file_path, -- path to a lua source or byte-code file id = id, -- id to be returned by param.get_id(...) during execution params = params -- parameters to be returned by param.get(...) during execution }
function execute.file_path(args) local file_path = args.file_path local id = args.id local params = args.params local func, load_errmsg = loadfile(file_path) if not func then error('Could not load file "' .. file_path .. '": ' .. load_errmsg) end if id or params then param.exchange(id, params) end local result = func() if result == nil or result == true then result = 'ok' elseif result == false then result = 'error' elseif type(result) ~= "string" then error("Unexpected type of result: " .. type(result)) end if id or params then param.restore() end return result end
action_status = -- status code returned by the action (a string) execute.filtered_action{ module = module, -- module name of the action to be executed action = action, -- name of the action to be executed id = id, -- id to be returned by param.get_id(...) during execution params = params -- parameters to be returned by param.get(...) during execution }
function execute.filtered_action(args) local filters = {} local function add_by_path(...) execute._add_filters_by_path(filters, ...) end add_by_path("_filter") add_by_path("_filter_action") add_by_path(request.get_app_name(), "_filter") add_by_path(request.get_app_name(), "_filter_action") add_by_path(request.get_app_name(), args.module, "_filter") add_by_path(request.get_app_name(), args.module, "_filter_action") table.sort(filters) for idx, filter_name in ipairs(filters) do filters[idx] = filters[filter_name] filters[filter_name] = nil end local result execute.multi_wrapped( filters, function() result = execute.action(args) end ) return result end
execute.filtered_view{ module = module, -- module name of the view to be executed view = view -- name of the view to be executed }
function execute.filtered_view(args) local filters = {} local function add_by_path(...) execute._add_filters_by_path(filters, ...) end add_by_path("_filter") add_by_path("_filter_view") add_by_path(request.get_app_name(), "_filter") add_by_path(request.get_app_name(), "_filter_view") add_by_path(request.get_app_name(), args.module, "_filter") add_by_path(request.get_app_name(), args.module, "_filter_view") table.sort(filters) for idx, filter_name in ipairs(filters) do filters[idx] = filters[filter_name] filters[filter_name] = nil end execute.multi_wrapped( filters, function() execute.view(args) end ) end
execute.inner()
function execute.inner() local stack = execute._wrap_stack local pos = #stack if pos == 0 then error("Unexpected call of execute.inner().") end local inner_func = stack[pos] if not inner_func then error("Repeated call of execute.inner().") end stack[pos] = false inner_func() end
execute.multi_wrapped( wrapper_funcs, -- multiple wrapper functions (i.e. filters) inner_func -- inner function (i.e. an action or view) )
function execute.multi_wrapped(wrapper_funcs, inner_func) local function wrapped_execution(pos) local wrapper_func = wrapper_funcs[pos] if wrapper_func then return execute.wrapped( wrapper_func, function() wrapped_execution(pos+1) end ) else return inner_func() end end return wrapped_execution(1) end
execute.view{ module = module, -- module name of the view to be executed view = view, -- name of the view to be executed id = id, -- id to be returned by param.get_id(...) during execution params = params -- parameters to be returned by param.get(...) during execution }
function execute.view(args) local module = args.module local view = args.view trace.enter_view{ module = module, view = view } execute.file_path{ file_path = encode.file_path( request.get_app_basepath(), 'app', request.get_app_name(), module, view .. '.lua' ), id = args.id, params = args.params } trace.execution_return() end
execute.wrapped( wrapper_func, -- function with an execute.inner() call inside inner_func -- function which is executed when execute.inner() is called )
function execute.wrapped(wrapper_func, inner_func) if type(wrapper_func) ~= "function" or type(inner_func) ~= "function" then error("Two functions need to be passed to execute.wrapped(...).") end local stack = execute._wrap_stack local pos = #stack + 1 stack[pos] = inner_func wrapper_func() -- if stack[pos] then -- error("Wrapper function did not call execute.inner().") -- end stack[pos] = nil end
text = -- human text representation of the boolean format.boolean( value, -- true, false or nil { true_as = true_text, -- text representing true false_as = false_text, -- text representing false nil_as = nil_text -- text representing nil } )
function format.boolean(value, options) local options = options or {} local true_text = options.true_as or "Yes" -- TODO: localization? local false_text = options.false_as or "No" -- TODO: localization? local nil_text = options.nil_as or "" if value == nil then return nil_text elseif value == false then return false_text elseif value == true then return true_text else error("Value passed to format.boolean(...) is neither a boolean nor nil.") end end
text = format.currency( value, { nil_as = nil_text, -- text to be returned for a nil value digits = digits, -- number of digits before the decimal point currency_precision = currency_precision, -- number of digits after decimal point currency_prefix = currency_prefix, -- prefix string, i.e. "$ " currency_decimal_point = currency_decimal_point, -- string to be used as decimal point currency_suffix = currency_suffix, -- suffix string, i.e. " EUR" hide_unit = hide_unit, -- hide the currency unit, if true decimal_point = decimal_point -- used instead of 'currency_decimal_point', if 'hide_unit' is true } )
function format.currency(value, options) local options = table.new(options) local prefix local suffix if options.hide_unit then prefix = "" suffix = "" options.decimal_point = options.decimal_point or locale.get("decimal_point") options.precision = options.currency_precision or locale.get("currency_precision") or 2 elseif options.currency_prefix or options.currency_suffix or options.currency_precision or options.currency_decimal_point then prefix = options.currency_prefix or '' suffix = options.currency_suffix or '' options.decimal_point = options.currency_decimal_point options.precision = options.currency_precision or 2 else prefix = locale.get("currency_prefix") or '' suffix = locale.get("currency_suffix") or '' options.decimal_point = locale.get("currency_decimal_point") options.precision = locale.get("currency_precision") or 2 end if value == nil then return options.nil_as or '' end return prefix .. format.decimal(value, options) .. suffix end
text = -- text with the value formatted as a date, according to the locale settings format.date( value, -- a date, a timestamp or nil { nil_as = nil_text -- text to be returned for a nil value } )
function format.date(value, options) local options = options or {} if value == nil then return options.nil_as or "" end if not ( atom.has_type(value, atom.date) or atom.has_type(value, atom.timestamp) ) then error("Value passed to format.date(...) is neither a date, a timestamp, nor nil.") end if value.invalid then return "invalid" end local result = locale.get("date_format") or "YYYY-MM-DD" result = string.gsub(result, "YYYY", function() return format.decimal(value.year, { digits = 4 }) end) result = string.gsub(result, "YY", function() return format.decimal(value.year % 100, { digits = 2 }) end) result = string.gsub(result, "Y", function() return format.decimal(value.year) end) result = string.gsub(result, "MM", function() return format.decimal(value.month, { digits = 2 }) end) result = string.gsub(result, "M", function() return format.decimal(value.month) end) result = string.gsub(result, "DD", function() return format.decimal(value.day, { digits = 2 }) end) result = string.gsub(result, "D", function() return format.decimal(value.day) end) return result end
text = -- text with the value formatted as decimal number format.decimal( value, -- a number, a fraction or nil { nil_as = nil_text, -- text to be returned for a nil value digits = digits, -- digits before decimal point precision = precision, -- digits after decimal point decimal_shift = decimal_shift -- divide the value by 10^decimal_shift (setting true uses precision) } )
function format.decimal(value, options) -- TODO: more features local options = options or {} local special_chars = charset.get_data().special_chars local f if value == nil then return options.nil_as or "" elseif atom.has_type(value, atom.number) then f = value elseif atom.has_type(value, atom.fraction) then f = value.float else error("Value passed to format.decimal(...) is neither a number nor a fraction nor nil.") end local digits = options.digits local precision = options.precision or 0 local decimal_shift = options.decimal_shift or 0 if decimal_shift == true then decimal_shift = precision end f = f / 10 ^ decimal_shift local negative local absolute if f < 0 then absolute = -f negative = true else absolute = f negative = false end absolute = absolute + 0.5 / 10 ^ precision local int = math.floor(absolute) if not atom.is_integer(int) then if f > 0 then return "+" .. special_chars.inf_sign elseif f < 0 then return minus_sign .. special_chars.inf_sign else return "NaN" end end local int_str = tostring(int) if digits then while #int_str < digits do int_str = "0" .. int_str end end if precision > 0 then local decimal_point = options.decimal_point or locale.get('decimal_point') or '.' local frac_str = tostring(math.floor((absolute - int) * 10 ^ precision)) while #frac_str < precision do frac_str = "0" .. frac_str end assert(#frac_str == precision, "Assertion failed in format.float(...).") -- should not happen if negative then return special_chars.minus_sign .. int_str .. decimal_point .. frac_str elseif options.show_plus then return "+" .. int_str .. decimal_point .. frac_str else return int_str .. decimal_point .. frac_str end else if negative then return special_chars.minus_sign .. int elseif options.show_plus then return "+" .. int_str else return int_str end end end
text = -- text with the value formatted as a percentage format.percentage( value, -- a number, a fraction or nil { nil_as = nil_text -- text to be returned for a nil value digits = digits, -- digits before decimal point (of the percentage value) precision = precision, -- digits after decimal point (of the percentage value) decimal_shift = decimal_shift -- divide the value by 10^decimal_shift (setting true uses precision + 2) } )
function format.percentage(value, options) local options = table.new(options) local f if value == nil then return options.nil_as or "" elseif atom.has_type(value, atom.number) then f = value elseif atom.has_type(value, atom.fraction) then f = value.float else error("Value passed to format.percentage(...) is neither a number nor a fraction nor nil.") end options.precision = options.precision or 0 if options.decimal_shift == true then options.decimal_shift = options.precision + 2 end local suffix = options.hide_unit and "" or " %" return format.decimal(f * 100, options) .. suffix end
text = -- a string format.string( value, -- any value where tostring(value) gives a reasonable result { nil_as = nil_text -- text to be returned for a nil value } )
function format.string(str, options) local options = options or {} if str == nil then return options.nil_as or "" else return tostring(str) end end
text = -- text with the value formatted as a time, according to the locale settings format.time( value, -- a time, a timestamp or nil { nil_as = nil_text -- text to be returned for a nil value } )
function format.time(value, options) local options = options or {} if value == nil then return options.nil_as or "" end if not ( atom.has_type(value, atom.time) or atom.has_type(value, atom.timestamp) ) then error("Value passed to format.time(...) is neither a time, a timestamp, nor nil.") end if value.invalid then return "invalid" end local result = locale.get("time_format") or "HH:MM{:SS}" if options.hide_seconds then result = string.gsub(result, "{[^{|}]*}", "") else result = string.gsub(result, "{([^|]*)}", "%1") end local am_pm local hour = value.hour result = string.gsub(result, "{([^{}]*)|([^{}]*)}", function(am, pm) if hour > 12 then am_pm = pm else am_pm = am end return "{|}" end) if am_pm and hour > 12 then hour = hour - 12 end result = string.gsub(result, "HH", function() return format.decimal(hour, { digits = 2 }) end) result = string.gsub(result, "MM", function() return format.decimal(value.minute, { digits = 2 }) end) result = string.gsub(result, "SS", function() return format.decimal(value.second, { digits = 2 }) end) if am_pm then result = string.gsub(result, "{|}", am_pm) end return result end
text = -- text with the given timestamp value formatted according to the locale settings format.timestamp( value, -- a timestamp or nil { nil_as = nil_text -- text to be returned for a nil value } )
function format.timestamp(value, options) if value == nil then return options.nil_as or "" end return format.date(value, options) .. " " .. format.time(value, options) end
locale.do_with( locale_options, -- table with locale information (as if passed to locale.set(...)) function() ... -- code to be executed with the given locale settings end )
function locale.do_with(locale_options, block) local old_data = {} for key, value in pairs(locale._current_data) do old_data[key] = value end locale.set(locale_options) block() old_data.reset = true locale.set(old_data) end
locale_setting = -- setting for the given localization category (could be of any type, depending on the category) locale.get( category -- string selecting a localization category, e.g. "lang" or "time", etc... )
function locale.get(category) return locale._current_data[category] end
locale.set(
locale_options -- table with locale categories as keys and their settings as values
)
function locale.set(locale_options) if locale_options.reset then locale._current_data = {} end for key, value in pairs(locale_options) do if key ~= "reset" then locale._current_data[key] = value end end end
net.send_mail{ envelope_from = envelope_from, -- envelope from address, not part of mail headers from = from, -- From header address or table with 'name' and 'address' fields sender = sender, -- Sender header address or table with 'name' and 'address' fields reply_to = reply_to, -- Reply-To header address or table with 'name' and 'address' fields to = to, -- To header address or table with 'name' and 'address' fields cc = cc, -- Cc header address or table with 'name' and 'address' fields bcc = bcc, -- Bcc header address or table with 'name' and 'address' fields subject = subject, -- subject of e-mail multipart = multipart_type, -- "alternative", "mixed", "related", or nil content_type = content_type, -- only for multipart == nil, defaults to "text/plain" binary = binary, -- allow full 8-bit content content = content or { -- content as lua-string, or table in case of multipart { multipart = multipart_type, ..., content = content or { {...}, ... } }, { ... }, ... } }
function net.send_mail(args)
local mail
if type(args) == "string" then
mail = args
else
mail = encode.mime.mail(args)
end
local envelope_from = args.envelope_from
local command
if
envelope_from and
string.find(envelope_from, "^[0-9A-Za-z%.-_@0-9A-Za-z%.-_]+$")
then
command =
"/usr/sbin/sendmail -t -i -f " ..
envelope_from ..
" > /dev/null 2> /dev/null"
else
command = "/usr/sbin/sendmail -t -i > /dev/null 2> /dev/null"
end
trace.debug(command)
-- TODO: use pfilter
local sendmail = assert(io.popen(command, "w"))
sendmail:write(mail)
sendmail:close()
end
param.exchange( id, params )
function param.exchange(id, params) table.insert(param._saved, param._exchanged) param._exchanged = { id = id, params = params } end
value = -- value of the parameter casted to the chosen param_type param.get( key, -- name of the parameter param_type -- desired type of the returned value )
function param.get(key, param_type) local param_type = param_type or atom.string if param._exchanged then local value = param._exchanged.params[key] if value ~= nil and not atom.has_type(value, param_type) then error("Parameter has unexpected type.") end return value else local str = cgi.params[key] local format_info = cgi.params[key .. "__format"] if not str then if not format_info then return nil end str = "" end return param._get_parser(format_info, param_type)(str) end end
params = -- table with all non-list parameters
param.get_all_cgi()
function param.get_all_cgi()
local result = {}
for key, value in pairs(cgi.params) do -- TODO: exchanged params too?
if
(not string.match(key, "^_webmcp_")) and
(not string.match(key, "%[%]$"))
then
result[key] = value
end
end
return result
end
value = -- value of the id casted to the chosen param_type param.get_id( param_type -- desired type of the returned value )
function param.get_id(param_type) local param_type = param_type or atom.integer if param._exchanged then local value = param._exchanged.id if value ~= nil and not atom.has_type(value, param_type) then error("Parameter has unexpected type.") end return value else return param.get("_webmcp_id", param_type) end end
value = -- id string or nil
param.get_id_cgi()
function param.get_id_cgi() return cgi.params._webmcp_id end
values = -- list of values casted to the chosen param_type param.get_list( key, -- name of the parameter without "[]" suffix param_type, -- desired type of the returned values )
function param.get_list(key, param_type) local param_type = param_type or atom.string if param._exchanged then local values = param._exchanged.params[key] or {} if type(values) ~= "table" then error("Parameter has unexpected type.") end for idx, value in ipairs(values) do if not atom.has_type(value, param_type) then error("Element of parameter list has unexpected type.") end end return values else local format_info = cgi.params[key .. "__format"] local parser = param._get_parser(format_info, param_type) local raw_values = cgi.params[key .. "[]"] local values = {} if raw_values then for idx, value in ipairs(raw_values) do values[idx] = parser(raw_values[idx]) end end return values end end
for index, -- index variable counting up from 1 prefix -- prefix string with index in square brackets to be used as a prefix for a key passed to param.get or param.get_list in param.iterate( prefix -- prefix to be followed by an index in square brackets and another key ) do ... end
function param.iterate(prefix) local length = param.get(prefix .. "[len]", atom.integer) or 0 if not atom.is_integer(length) then error("List length is not a valid integer or nil.") end local index = 0 return function() index = index + 1 if index <= length then return index, prefix .. "[" .. index .. "]" end end end
param.restore()
function param.restore() local saved = param._saved local previous = saved[#saved] saved[#saved] = nil if previous == nil then error("Tried to restore id and params without having exchanged it before.") end param._exchanged = previous end
param.update_relationship{ param_name = param_name, -- name of request GET/POST request parameters containing primary keys for model B id = id, -- value of the primary key for model A connecting_model = connecting_model, -- model used for creating/deleting entries referencing both model A and B own_reference = own_reference, -- field name for foreign key in the connecting model referencing model A foreign_reference = foreign_reference -- field name for foreign key in the connecting model referencing model B }
function param.update_relationship(args) local param_name = args.param_name local id = args.id local connecting_model = args.connecting_model local own_reference = args.own_reference local foreign_reference = args.foreign_reference local selected_ids = param.get_list(param_name, atom.integer) -- TODO: support other types than integer too local db = connecting_model:get_db_conn() local table = connecting_model:get_qualified_table() if #selected_ids == 0 then db:query{ 'DELETE FROM ' .. table .. ' WHERE "' .. own_reference .. '" = ?', args.id } else local selected_ids_sql = { sep = ", " } for idx, value in ipairs(selected_ids) do selected_ids_sql[idx] = {"?::int8", value} end db:query{ 'DELETE FROM ' .. table .. ' WHERE "' .. own_reference .. '" = ?' .. ' AND NOT "' .. foreign_reference .. '" IN ($)', args.id, selected_ids_sql } -- TODO: use VALUES SQL command, instead of this dirty array trick db:query{ 'INSERT INTO ' .. table .. ' ("' .. own_reference .. '", "' .. foreign_reference .. '")' .. ' SELECT ?, "subquery"."foreign" FROM (' .. 'SELECT (ARRAY[$])[i] AS "foreign"' .. ' FROM generate_series(1, ?) AS "dummy"("i")' .. ' EXCEPT SELECT "' .. foreign_reference .. '" AS "foreign"' .. ' FROM ' .. table .. ' WHERE "' .. own_reference .. '" = ?' .. ') AS "subquery"', args.id, selected_ids_sql, #selected_ids, args.id } end end
param.update( record, -- database record to be updated key_and_field_name1, -- name of CGI parameter and record field key_and_field_name2, -- another name of a CGI parameter and record field { key3, -- name of CGI parameter field_name3 -- name of record field } )
function param.update(record, mapping_info, ...)
if not mapping_info then
return
end
assert(record, "No record given for param.update(...).")
assert(record._class, "Record passed to param.update(...) has no _class attribute.")
local key, field_name
if type(mapping_info) == "string" then
key = mapping_info
field_name = mapping_info
else
key = mapping_info[1]
field_name = mapping_info[2]
end
assert(key, "No key given in parameter of param.update(...).")
assert(field_name, "No field name given in parameter of param.update(...).")
local column_info = record._class:get_columns()[field_name]
if not column_info then
error('Type of column "' .. field_name .. '" is unknown.')
end
local new_value = param.get(key, column_info.type)
if new_value ~= record[field_name] then
record[field_name] = new_value
end
return param.update(record, ...) -- recursivly process following arguments
end
request.force_absolute_baseurl()
function request.force_absolute_baseurl() request._force_absolute_baseurl = true end
request.forward{ module = module, -- module name view = view -- view name }
function request.forward(args) if request.is_rerouted() then error("Tried to forward after another forward or redirect.") end request._forward = args trace.forward { module = args.module, view = args.view } end
route_info = -- table with 'module' and 'view' field
request.get_404_route()
function request.get_404_route() return request._404_route end
action_name = request.get_action()
function request.get_action() if request._forward_processed then return nil else return cgi.params._webmcp_action end end
path = -- path to directory of application with trailing slash
request.get_app_basepath()
function request.get_app_basepath() return request._app_basepath end
app_name = request.get_app_name()
function request.get_app_name() return os.getenv("WEBMCP_APP_NAME") or 'main' end
config_name = request.get_config_name()
function request.get_config_name() return os.getenv("WEBMCP_CONFIG_NAME") end
secret = -- secret string, previously set with request.set_csrf_secret(...)
request.get_csrf_secret()
function request.get_csrf_secret(secret) return request._csrf_secret end
slot_idents = -- list of names of slots to be returned as JSON data
request.get_json_request_slots()
function request.get_json_request_slots(slot_idents) local slot_idents = cgi.params["_webmcp_json_slots[]"] if slot_idents and not request._json_requests_allowed then error("JSON requests have not been allowed using request.set_allowed_json_request_slots(...).") end return slot_idents end
module_name = request.get_module()
function request.get_module() if request._forward_processed then return request._forward.module or cgi.params._webmcp_module or 'index' else return cgi.params._webmcp_module or 'index' end end
perm_params = -- table containing permanent parameters
request.get_perm_params()
function request.get_perm_params()
-- NOTICE: it's important to return a copy here
return table.new(request._perm_params)
end
redirect_data = request.get_redirect_data()
function request.get_redirect_data() return request._redirect end
baseurl = request.get_relative_baseurl()
function request.get_relative_baseurl() if request._force_absolute_baseurl then return (request.get_absolute_baseurl()) else return request._relative_baseurl end end
status_string = request.get_status()
function request.get_status() return request._status end
view_name = request.get_view()
function request.get_view() if request._forward_processed then return request._forward.view or 'index' else if cgi.params._webmcp_view then local suffix = cgi.params._webmcp_suffix or "html" if suffix == "html" then return cgi.params._webmcp_view else return cgi.params._webmcp_view .. "." .. suffix end elseif not cgi.params._webmcp_action then return 'index' else return nil end end end
bool = -- true, if the current request is a POST request
request.is_post()
function request.is_post() if request._forward_processed then return false else return cgi.method == "POST" end end
request.process_forward()
function request.process_forward() if request._forward then request._forward_processed = true trace.request{ module = request.get_module(), view = request.get_view() } end end
request.redirect{ module = module, -- module name view = view, -- view name id = id, -- optional id for view params = params -- optional view parameters }
function request.redirect(args) -- TODO: support redirects to external URLs too -- (needs fixes in the trace system as well) local module = args.module local view = args.view local id = args.id local params = args.params or {} if type(module) ~= "string" then error("No module string passed to request.redirect{...}.") end if type(view) ~= "string" then error("No view string passed to request.redirect{...}.") end if type(params) ~= "table" then error("Params array passed to request.redirect{...} is not a table.") end if request.is_rerouted() then error("Tried to redirect after another forward or redirect.") end request._redirect = { module = module, view = view, id = id, params = params } trace.redirect{ module = args.module, view = args.view } end
request.set_404_route{ module = module, -- module name view = view -- view name }
function request.set_404_route(tbl) request._404_route = tbl end
request.set_absolute_baseurl(
url -- Base URL of the application
)
function request.set_absolute_baseurl(url) if string.find(url, "/$") then request._absolute_baseurl = url else request._absolute_baseurl = url .. "/" end end
request.set_allowed_json_request_slots(
slot_idents -- list of names of slots which can be requested in JSON format
)
function request.set_allowed_json_request_slots(slot_idents) local hash = {} for idx, slot_ident in ipairs(slot_idents) do hash[slot_ident] = true end if cgi.params["_webmcp_json_slots[]"] then for idx, slot_ident in ipairs(cgi.params["_webmcp_json_slots[]"]) do if not hash[slot_ident] then error('Requesting slot "' .. slot_ident .. '" is forbidden.') end end end request._json_requests_allowed = true end
request.set_csrf_secret(
secret -- secret random string
)
function request.set_csrf_secret(secret) if request.get_action() and cgi.params._webmcp_csrf_secret ~= secret then error("Cross-Site Request Forgery attempt detected"); end request._csrf_secret = secret end
request.set_perm_param( key, -- name of parameter value -- value of parameter )
function request.set_perm_param(key, value) request._perm_params[key] = value end
request.set_status(
str -- string containing a HTTP status code, e.g. "404 Not Found"
)
function request.set_status(str) if str then local t = type(str) if type(str) == "number" then str = tostring(str) elseif type(str) ~= "string" then error("request.set_status(...) must be called with a string as parameter.") end request._status = str else request._status = nil end end
bool = -- true, if request.forard{...} or request.redirect{...} has been called before.
request_is_rerouted()
function request.is_rerouted() if (request._forward and not request._forward_processed) or request._redirect then return true else return false end end
blob = -- string for later usage with slot.restore_all(...)
slot.dump_all()
local function encode(str) return ( string.gsub( str, "[=;%[%]]", function(char) if char == "=" then return "[eq]" elseif char == ";" then return "[s]" elseif char == "[" then return "[o]" elseif char == "]" then return "[c]" else end end ) ) end function slot.dump_all() local blob_parts = {} for key in pairs(slot._data) do if type(key) == "string" then local value = slot.get_content(key) if value ~= "" then blob_parts[#blob_parts + 1] = encode(key) .. "=" .. encode(value) end end end return table.concat(blob_parts, ";") end
content =
slot.get_content(
slot_ident -- name of the slot
)
function slot.get_content(slot_ident) local slot_data = slot._data[slot_ident] if #slot_data.string_fragments > 1 then local str = table.concat(slot_data.string_fragments) slot_data.string_fragments = { str } return str else return slot_data.string_fragments[1] or "" end end
content_type = -- content-type as selected with slot.set_layout(...)
slot.get_content_type()
function slot.get_content_type() return slot._content_type or 'text/html; charset=UTF-8' end
state_table = -- table for saving a slot's state
slot.get_state_table()
function slot.get_state_table() return slot.get_state_table_of(slot._active_slot) end
state_table = -- table for saving the slot's state slot.get_state_table_of( slot_ident -- name of a slot )
function slot.get_state_table_of(slot_ident) return slot._data[slot_ident].state_table end
slot.put( slot_ident -- name of a slot string1, -- string to be written into the named slot string2, -- another string to be written into the named slot ... )
function slot.put_into(slot_ident, ...) local t = slot._data[slot_ident].string_fragments for i = 1, math.huge do local v = select(i, ...) if v == nil then break end t[#t + 1] = v end end
slot.put( string1, -- string to be written into the active slot string2, -- another string to be written into the active slot ... )
function slot.put(...) return slot.put_into(slot._active_slot, ...) end
output = -- document/data to be sent to the web browser
slot.render_layout()
function slot.render_layout() if slot._current_layout then local layout_file = assert(io.open( encode.file_path( request.get_app_basepath(), 'app', request.get_app_name(), '_layout', slot._current_layout .. '.html' ), 'r' )) local layout = layout_file:read("*a") io.close(layout_file) -- render layout layout = string.gsub(layout, "__BASEURL__/?", request.get_relative_baseurl()) -- TODO: find a better placeholder than __BASEURL__ ? layout = string.gsub(layout, '<!%-%- *WEBMCP +SLOT +([^ ]+) *%-%->', function(slot_ident) if #slot.get_content(slot_ident) > 0 then return '<div class="slot_' .. slot_ident .. '" id="slot_' .. slot_ident .. '">' .. slot.get_content(slot_ident).. '</div>' else return '' end end ) layout = string.gsub(layout, '<!%-%- *WEBMCP +SLOTNODIV +([^ ]+) *%-%->', function(slot_ident) if #slot.get_content(slot_ident) > 0 then return slot.get_content(slot_ident) else return '' end end ) return layout else return slot.get_content("data") end end
slot.reset(
slot_ident -- name of a slot to be emptied
)
function slot.reset(slot_ident) slot._data[slot_ident] = nil end
slot.reset_all()
function slot.reset_all() slot._data = setmetatable({}, slot._data_metatable) end
slot.restore_all(
blob -- string as returned by slot.dump_all()
)
local function decode(str) return ( string.gsub( str, "%[[a-z]+%]", function(char) if char == "[eq]" then return "=" elseif char == "[s]" then return ";" elseif char == "[o]" then return "[" elseif char == "[c]" then return "]" else end end ) ) end function slot.restore_all(blob) slot.reset_all() for encoded_key, encoded_value in string.gmatch(blob, "([^=;]*)=([^=;]*)") do local key, value = decode(encoded_key), decode(encoded_value) slot._data[key].string_fragments = { value } end end
slot.select( slot_ident, -- name of a slot function() ... -- code to be executed using the named slot end )
function slot.select(slot_ident, block) local old_slot = slot._active_slot slot._active_slot = slot_ident block() slot._active_slot = old_slot end
slot.set_layout( layout_ident, -- name of layout or nil for binary data in slot named "data" content_type -- content-type to be sent to the browser, or nil for default )
function slot.set_layout(layout_ident, content_type) slot._current_layout = layout_ident slot._content_type = content_type end
slot_content = slot.use_temporary( function() ... end )
function slot.use_temporary(block)
local old_slot = slot._active_slot
local temp_slot_reference = {} -- just a unique reference
slot._active_slot = temp_slot_reference
block()
slot._active_slot = old_slot
local result = slot.get_content(temp_slot_reference)
slot.reset(temp_slot_reference)
return result
end
cloned_table = -- newly generated table table.new( table_or_nil -- keys of a given table will be copied to the new table )
function table.new(tbl) new_tbl = {} if tbl then for key, value in pairs(tbl) do new_tbl[key] = value end end return new_tbl end
blob = -- loaded string tempstore.pop( key -- key as returned by tempstore.save(...) )
function tempstore.pop(key) local filename = encode.file_path( request.get_app_basepath(), 'tmp', "tempstore-" .. key .. ".tmp" ) local file = io.open(filename, "r") if not file then return nil end local blob = file:read("*a") io.close(file) os.remove(filename) return blob end
key = -- key to be used with tempstore.pop(...) to regain the stored string tempstore.save( blob -- string to be stored )
function tempstore.save(blob) local key = multirand.string(26, "123456789bcdfghjklmnpqrstvwxyz"); local filename = encode.file_path( request.get_app_basepath(), 'tmp', "tempstore-" .. key .. ".tmp" ) local file = assert(io.open(filename, "w")) file:write(blob) io.close(file) return key end
timestamp.invalid
timestamp.invalid = timestamp:_create{ tsec = not_a_number, year = not_a_number, month = not_a_number, day = not_a_number, hour = not_a_number, minute = not_a_number, second = not_a_number, invalid = true }
trace.enter_action{ module = module, action = action }
function trace.enter_action(args) local module = args.module local action = args.action if type(module) ~= "string" then error("No module string passed to trace.enter_action{...}.") end if type(action) ~= "string" then error("No action string passed to trace.enter_action{...}.") end trace._open_section{ type = "action", module = module, action = action } end
trace.enter_config{ name = name }
function trace.enter_config(args) local name = args.name if type(name) ~= "string" then error("No name string passed to trace.enter_config{...}.") end trace._open_section{ type = "config", name = name } end
trace.enter_filter{ path = path }
function trace.enter_filter(args) local path = args.path if type(path) ~= "string" then error("No path string passed to trace.enter_filter{...}.") end trace._open_section{ type = "filter", path = path } end
trace.enter_view{ module = module, view = view }
function trace.enter_view(args) local module = args.module local view = args.view if type(module) ~= "string" then error("No module passed to trace.enter_view{...}.") end if type(view) ~= "string" then error("No view passed to trace.enter_view{...}.") end trace._open_section{ type = "view", module = module, view = view } end
trace.error{ }
function trace.error(args)
trace._new_entry { type = "error" }
local closed_section = trace._close_section()
closed_section.hard_error = true -- TODO: not used, maybe remove
trace._stack = { trace._tree }
end
trace.exectime{ real = real, -- physical time in seconds cpu = cpu -- CPU time in seconds }
function trace.exectime(args) local real = args.real local cpu = args.cpu if type(real) ~= "number" then error("Called trace.exectime{...} without numeric 'real' argument.") end if type(cpu) ~= "number" then error("Called trace.exectime{...} without numeric 'cpu' argument.") end trace._new_entry{ type = "exectime", real = args.real, cpu = args.cpu } end
trace.execution_return{
status = status -- optional status
}
function trace.execution_return(args) local status if args then status = args.status end if status and type(status) ~= "string" then error("Status passed to trace.execution_return{...} is not a string.") end local closed_section = trace._close_section() closed_section.status = status end
trace.forward{ module = module, view = view }
function trace.forward(args) local module = args.module local view = args.view if type(module) ~= "string" then error("No module string passed to trace.forward{...}.") end if type(view) ~= "string" then error("No view string passed to trace.forward{...}.") end trace._new_entry{ type = "forward", module = module, view = view } end
trace.redirect{ module = module, view = view }
function trace.redirect(args) local module = args.module local view = args.view if type(module) ~= "string" then error("No module string passed to trace.redirect{...}.") end if type(view) ~= "string" then error("No view string passed to trace.redirect{...}.") end trace._new_entry{ type = "redirect", module = module, view = view } end
trace.render()
function trace.render()
-- TODO: check if all sections are closed?
trace._render_sub_tree(trace._tree)
end
trace.request{ module = module, view = view, action = action }
function trace.request(args) local module = args.module local view = args.view local action = args.action if type(module) ~= "string" then error("No module string passed to trace.request{...}.") end if view and action then error("Both view and action passed to trace.request{...}.") end if not (view or action) then error("Neither view nor action passed to trace.request{...}.") end if view and type(view) ~= "string" then error("No view string passed to trace.request{...}.") end if action and type(action) ~= "string" then error("No action string passed to trace.request{...}.") end trace._new_entry{ type = "request", module = args.module, view = args.view, action = args.action } end
trace.restore_slots{ }
function trace.restore_slots(args) trace._new_entry{ type = "restore_slots" } end
trace.sql{ command = command, -- executed SQL command as string error_position = error_position -- optional position in bytes where an error occurred }
-- TODO: automatic use of this function?
function trace.sql(args)
local command = args.command
local error_position = args.error_position
if type(command) ~= "string" then
error("No command string passed to trace.sql{...}.")
end
if error_position and type(error_position) ~= "number" then
error("error_position must be a number.")
end
trace._new_entry{
type = "sql",
command = command,
error_position = error_position
}
end
trace_debug(
message -- message to be inserted into the trace log
)
function trace.debug(message) trace._new_entry{ type = "debug", message = tostring(message) } end
ui.autofield{ name = name, -- field name (also used by default as HTML name) html_name = html_name, -- explicit HTML name to be used instead of 'name' value = nihil.lift(value), -- initial value, nil causes automatic lookup of value, use nihil.lift(nil) for nil container_attr = container_attr, -- extra HTML attributes for the container (div) enclosing field and label attr = attr, -- extra HTML attributes for the field label = label, -- text to be used as label for the input field label_attr = label_attr, -- extra HTML attributes for the label readonly = readonly_flag -- set to true, to force read-only mode record = record, -- record to be used, defaults to record given to ui.form{...} ... -- extra arguments for applicable ui.field.* helpers }
function ui.autofield(args) local args = table.new(args) assert(args.name, "ui.autofield{...} needs a field 'name'.") if not args.record then local slot_state = slot.get_state_table() if not slot_state then error("ui.autofield{...} was called without an explicit record to be used, and is also not called inside a form.") elseif not slot_state.form_record then error("ui.autofield{...} was called without an explicit record to be used, and the form does not have a record assigned either.") else args.record = slot_state.form_record end end local class = args.record._class assert(class, "Used ui.autofield{...} on a record with no class information stored in the '_class' attribute.") local fields, field_info, ui_field_type, ui_field_options fields = class.fields if fields then field_info = fields[args.name] end if field_info then ui_field_type = field_info.ui_field_type ui_field_options = table.new(field_info.ui_field_options) end if not ui_field_type then ui_field_type = "text" end if not ui_field_options then ui_field_options = {} end local ui_field_func = ui.field[ui_field_type] if not ui_field_func then error(string.format("Did not find ui.field helper of type %q.", ui_field_type)) end for key, value in pairs(ui_field_options) do if args[key] == nil then args[key] = value end end return ui_field_func(args) end
ui.container{ auto_args = auto_args, attr = attr, -- HTML attributes for the surrounding div or fieldset label = label, -- text to be used as label label_for = label_for, -- DOM id of element to which the label should refer label_attr = label_attr, -- extra HTML attributes for a label tag legend = legend, -- text to be used as legend legend_attr = legend_attr, -- HTML attributes for a legend tag content_first = content_first, -- set to true to place label or legend after the content content = function() ... -- end }
function ui.container(args) local attr, label, label_attr, legend, legend_attr, content local auto_args = args.auto_args if auto_args then attr = auto_args.container_attr label = auto_args.label label_attr = auto_args.label_attr legend = auto_args.legend legend_attr = auto_args.legend_attr if label and auto_args.attr and auto_args.attr.id then label_attr = table.new(label_attr) label_attr["for"] = auto_args.attr.id end else attr = args.attr label = args.label label_attr = args.label_attr or {} legend = args.legend legend_attr = args.legend_attr content = content if args.label_for then label_attr["for"] = args.label_for end end local content = args.content if label and not legend then return ui.tag { tag = "div", attr = attr, content = function() if not args.content_first then ui.tag{ tag = "label", attr = label_attr, content = label } slot.put(" ") end if type(content) == "function" then content() elseif content then slot.put(encode.html(content)) end if args.content_first then slot.put(" ") ui.tag{ tag = "label", attr = label_attr, content = label } end end } elseif legend and not label then return ui.tag { tag = "fieldset", attr = attr, content = function() if not args.content_first then ui.tag{ tag = "legend", attr = legend_attr, content = legend } slot.put(" ") end if type(content) == "function" then content() elseif content then slot.put(encode.html(content)) end if args.content_first then slot.put(" ") ui.tag{ tag = "legend", attr = legend_attr, content = legend } end end } elseif fieldset and label then error("ui.container{...} may either get a label or a legend.") else return ui.tag{ tag = "div", attr = attr, content = content } end end
unique_id = -- unique string to be used as an id in the DOM tree
ui.create_unique_id()
function ui.create_unique_id() return "unique_" .. multirand.string(32, "bcdfghjklmnpqrstvwxyz") end
ui.field.boolean{ ... -- generic ui.field.* arguments, as described for ui.autofield{...} style = style, -- "radio" or "checkbox", nil_allowed = nil_allowed -- set to true, if nil is allowed as third value }
function ui.field.boolean(args) local style = args.style if not style then if args.nil_allowed then style = "radio" else style = "checkbox" end end local extra_args = { fetch_value = true } if not args.readonly and args.style == "radio" then extra_args.disable_label_for_id = true end ui.form_element(args, extra_args, function(args) local value = args.value if value ~= true and value ~= false and value ~= nil then error("Boolean value must be true, false or nil.") end if value == nil then if args.nil_allowed then value = args.default else value = args.default or false end end if args.readonly then ui.tag{ tag = args.tag, attr = args.attr, content = format.boolean(value, args.format_options) } elseif style == "radio" then local attr = table.new(args.attr) attr.type = "radio" attr.name = args.html_name attr.id = ui.create_unique_id() attr.value = "1" if value == true then attr.checked = "checked" else attr.checked = nil end ui.container{ attr = { class = "ui_radio_div" }, label = args.true_as or "Yes", -- TODO: localize label_for = attr.id, label_attr = { class = "ui_radio_label" }, content_first = true, content = function() ui.tag{ tag = "input", attr = attr } end } attr.id = ui.create_unique_id() attr.value = "0" if value == false then attr.checked = "1" else attr.checked = nil end ui.container{ attr = { class = "ui_radio_div" }, label = args.false_as or "No", -- TODO: localize label_for = attr.id, label_attr = { class = "ui_radio_label" }, content_first = true, content = function() ui.tag{ tag = "input", attr = attr } end } if args.nil_allowed then attr.id = ui.create_unique_id() attr.value = "" if value == nil then attr.checked = "1" else attr.checked = nil end ui.container{ attr = { class = "ui_radio_div" }, label = args.nil_as or "N/A", -- TODO: localize label_for = attr.id, label_attr = { class = "ui_radio_label" }, content_first = true, content = function() ui.tag{ tag = "input", attr = attr } end } end ui.hidden_field{ name = args.html_name .. "__format", value = "boolean" } elseif style == "checkbox" then if args.nil_allowed then error("Checkboxes do not support nil values.") end local attr = table.new(args.attr) attr.type = "checkbox" attr.name = args.html_name attr.value = "1" if value then attr.checked = "checked" else attr.checked = nil end ui.tag{ tag = "input", attr = attr } ui.hidden_field{ name = args.html_name .. "__format", value = encode.format_info( "boolean", { true_as = "1", false_as = "" } ) } else error("'style' attribute for ui.field.boolean{...} must be set to \"radio\", \"checkbox\" or nil.") end end) end
ui.field.date{
... -- generic ui.field.* arguments, as described for ui.autofield{...}
}
function ui.field.date(args) ui.form_element(args, {fetch_value = true}, function(args) local value_string = format.date(args.value, args.format_options) if args.readonly then ui.tag{ tag = args.tag, attr = args.attr, content = value_string } else local fallback_data = slot.use_temporary(function() local attr = table.new(args.attr) attr.type = "text" attr.name = args.html_name attr.value = value_string attr.class = attr.class or "ui_field_date" ui.tag{ tag = "input", attr = attr } ui.hidden_field{ name = args.html_name .. "__format", value = encode.format_info("date", args.format_options) } end) local user_field_id, hidden_field_id local helper_data = slot.use_temporary(function() local attr = table.new(args.attr) user_field_id = attr.id or ui.create_unique_id() hidden_field_id = ui.create_unique_id() attr.id = user_field_id attr.type = "text" attr.class = attr.class or "ui_field_date" ui.tag{ tag = "input", attr = attr } local attr = table.new(args.attr) attr.id = hidden_field_id attr.type = "hidden" attr.name = args.html_name attr.value = atom.dump(args.value) -- extra safety for JS failure ui.tag{ tag = "input", attr = { id = hidden_field_id, type = "hidden", name = args.html_name } } end) -- TODO: localization ui.script{ noscript = fallback_data, type = "text/javascript", content = function() slot.put( "if (gregor_addGui == null) document.write(", encode.json(fallback_data), "); else { document.write(", encode.json(helper_data), "); gregor_addGui({element_id: ", encode.json(user_field_id), ", month_names: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], weekday_names: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], week_numbers: 'left', format: 'DD.MM.YYYY', selected: " ) if (args.value) then slot.put( "{year: ", tostring(args.value.year), ", month: ", tostring(args.value.month), ", day: ", tostring(args.value.day), "}" ) else slot.put("null") end slot.put( ", select_callback: function(date) { document.getElementById(", encode.json(hidden_field_id), ").value = (date == null) ? '' : date.iso_string; } } ) }" ) end } end end) end
ui.field.hidden{
... -- generic ui.field.* arguments, as described for ui.autofield{...}
}
function ui.field.hidden(args) ui.form_element(args, {fetch_value = true}, function(args) if not args.readonly then ui.hidden_field{ attr = args.attr, name = args.html_name, value = args.value } end end) end
ui.field.integer{ ... -- generic ui.field.* arguments, as described for ui.autofield{...} format_options = format_options -- format options for format.decimal }
function ui.field.integer(args) ui.form_element(args, {fetch_value = true}, function(args) local value_string = format.decimal(args.value, args.format_options) if args.readonly then ui.tag{ tag = args.tag, attr = args.attr, content = value_string } else local attr = table.new(args.attr) attr.type = "text" attr.name = args.html_name attr.value = value_string ui.tag{ tag = "input", attr = attr } ui.hidden_field{ name = args.html_name .. "__format", value = encode.format_info("decimal", args.format_options) } end end) end
ui.field.password{
... -- generic ui.field.* arguments, as described for ui.autofield{...}
}
function ui.field.password(args)
ui.form_element(args, {fetch_value = true}, function(args)
local value_string = atom.dump(args.value)
if args.readonly then
-- nothing
else
local attr = table.new(args.attr)
attr.type = "password"
attr.name = args.html_name
attr.value = value_string
ui.tag{ tag = "input", attr = attr }
end
end)
end
ui.field.select{ ... -- generic ui.field.* arguments, as described for ui.autofield{...} foreign_records = foreign_records, -- list of records to be chosen from, or function returning such a list foreign_id = foreign_id, -- name of id field in foreign records foreign_name = foreign_name, -- name of field to be used as name in foreign records format_options = format_options -- format options for format.string }
function ui.field.select(args) ui.form_element(args, {fetch_value = true}, function(args) local foreign_records = args.foreign_records if type(foreign_records) == "function" then foreign_records = foreign_records(args.record) end if args.readonly then local name for idx, record in ipairs(foreign_records) do if record[args.foreign_id] == args.value then name = record[args.foreign_name] break end end ui.tag{ tag = args.tag, attr = args.attr, content = format.string(name, args.format_options) } else local attr = table.new(args.attr) attr.name = args.html_name ui.tag{ tag = "select", attr = attr, content = function() if args.nil_as then ui.tag{ tag = "option", attr = { value = "" }, content = format.string( args.nil_as, args.format_options ) } end for idx, record in ipairs(foreign_records) do local key = record[args.foreign_id] ui.tag{ tag = "option", attr = { value = key, selected = ((key == args.value) and "selected" or nil) }, content = format.string( record[args.foreign_name], args.format_options ) } end end } end end) end
ui.field.text{ ... -- generic ui.field.* arguments, as described for ui.autofield{...} format_options = format_options -- format options for format.string }
function ui.field.text(args) ui.form_element(args, {fetch_value = true}, function(args) local value_string = format.string(args.value, args.format_options) if args.readonly then ui.tag{ tag = args.tag, attr = args.attr, content = value_string } else local attr = table.new(args.attr) attr.name = args.html_name if args.multiline then ui.tag { tag = "textarea", attr = attr, content = value_string } else attr.type = "text" attr.value = value_string ui.tag{ tag = "input", attr = attr } end end end) end
ui.form_element( args, -- external arguments { -- options for this function call fetch_value = fetch_value_flag, -- true causes automatic determination of args.value, if nil fetch_record = fetch_record_flag, -- true causes automatic determination of args.record, if nil disable_label_for_id = disable_label_for_id_flag, -- true suppresses automatic setting of args.attr.id for a HTML label_for reference }, function(args) ... -- program code end )
-- TODO: better documentation
function ui.form_element(args, extra_args, func)
local args = table.new(args)
if extra_args then
for key, value in pairs(extra_args) do
args[key] = value
end
end
local slot_state = slot.get_state_table()
args.html_name = args.html_name or args.name
if args.fetch_value then
if args.value == nil then
if not args.record and slot_state then
args.record = slot_state.form_record
end
if args.record then
args.value = args.record[args.name]
end
else
args.value = nihil.lower(args.value)
end
elseif args.fetch_record then
if not args.record and slot_state then
args.record = slot_state.form_record
end
end
if
args.html_name and
not args.readonly and
slot_state.form_readonly == false
then
args.readonly = false
local prefix
if args.html_name_prefix == nil then
prefix = slot_state.html_name_prefix
else
prefix = args.html_name_prefix
end
if prefix then
args.html_name = prefix .. args.html_name
end
else
args.readonly = true
end
if args.label then
if not args.disable_label_for_id then
if not args.attr then
args.attr = { id = ui.create_unique_id() }
elseif not args.attr.id then
args.attr.id = ui.create_unique_id()
end
end
if not args.label_attr then
args.label_attr = { class = "ui_field_label" }
elseif not args.label_attr.class then
args.label_attr.class = "ui_field_label"
end
end
ui.container{
auto_args = args,
content = function() return func(args) end
}
end
ui.form{ record = record, -- optional record to be used read_only = read_only, -- set to true, if form should be read-only (no submit button) external = external, -- external URL to be used as HTML form action module = module, -- module name to be used for HTML form action view = view, -- view name to be used for HTML form action action = action, -- action name to be used for HTML form action routing = { default = { -- default routing for called action mode = mode, -- "forward" or "redirect" module = module, -- optional module name, defaults to current module view = view, -- view name id = id, -- optional id to be passed to the view params = params -- optional params to be passed to the view }, ok = { ... }, -- routing when "ok" is returned by the called action error = { ... }, -- routing when "error" is returned by the called action ... = { ... } -- routing when "..." is returned by the called action } content = function() ... -- code creating the contents of the form end }
function ui.form(args) local args = args or {} local slot_state = slot.get_state_table() local old_record = slot_state.form_record local old_readonly = slot_state.form_readonly slot_state.form_record = args.record if args.readonly then slot_state.form_readonly = true ui.container{ attr = args.attr, content = args.content } else slot_state.form_readonly = false local params = table.new(args.params) local routing_default_given = false if args.routing then for status, settings in pairs(args.routing) do if status == "default" then routing_default_given = true end local module = settings.module or args.module or request.get_module() assert(settings.mode, "No mode specified in routing entry.") assert(settings.view, "No view specified in routing entry.") params["_webmcp_routing." .. status .. ".mode"] = settings.mode params["_webmcp_routing." .. status .. ".module"] = module params["_webmcp_routing." .. status .. ".view"] = settings.view params["_webmcp_routing." .. status .. ".id"] = settings.id if settings.params then for key, value in pairs(settings.params) do params["_webmcp_routing." .. status .. ".params." .. key] = value end end end end if not routing_default_given then params["_webmcp_routing.default.mode"] = "forward" params["_webmcp_routing.default.module"] = request.get_module() params["_webmcp_routing.default.view"] = request.get_view() end params._webmcp_csrf_secret = request.get_csrf_secret() local attr = table.new(args.attr) attr.action = encode.url{ external = args.external, module = args.module or request.get_module(), view = args.view, action = args.action, } attr.method = args.method and string.upper(args.method) or "POST" if slot_state.form_opened then error("Cannot open a non-readonly form inside a non-readonly form.") end slot_state.form_opened = true ui.tag { tag = "form", attr = attr, content = function() if args.id then ui.hidden_field{ name = "_webmcp_id", value = args.id } end for key, value in pairs(params) do ui.hidden_field{ name = key, value = value } end if args.content then args.content() end end } slot_state.form_opened = false end slot_state.form_readonly = old_readonly slot_state.form_record = old_record end
ui.heading{ level = level, -- level from 1 to 6, defaults to 1 attr = attr, -- extra HTML attributes content = content -- string or function for content }
function ui.heading(args) return ui.tag{ tag = "h" .. (args.level or 1), attr = args.attr, content = args.content } end
ui.hidden_field{ name = name, -- HTML name value = value, -- value attr = attr -- extra HTML attributes }
function ui.hidden_field(args) local args = args or {} local attr = table.new(args.attr) attr.type = "hidden" attr.name = args.name attr.value = atom.dump(args.value) return ui.tag{ tag = "input", attr = attr } end
ui.link{ external = external, -- external URL static = static, -- URL relative to the static file directory module = module, -- module name view = view, -- view name action = action, -- action name id = id, -- optional id to be passed to the view or action to select a particular data record params = params, -- optional parameters to be passed to the view or action routing = routing, -- optional routing information for action links, as described for ui.form{...} text = text, -- link text content = content -- alternative name for 'text' option, preferred for functions }
function ui.link(args) local args = args or {} local content = args.text or args.content -- TODO: decide which argument name to use assert(content, "ui.link{...} needs a text.") local function wrapped_content() -- TODO: icon/image if type(content) == "function" then content() else slot.put(encode.html(content)) end end if args.action then local form_attr = table.new(args.form_attr) local form_id if form_attr.id then form_id = form_attr.id else form_id = ui.create_unique_id() end local quoted_form_id = encode.json(form_id) form_attr.id = form_id local a_attr = table.new(args.attr) a_attr.href = "#" a_attr.onclick = "var f = document.getElementById(" .. quoted_form_id .. "); if (! f.onsubmit || f.onsubmit() != false) { f.submit() };" ui.form{ external = args.external, module = args.module or request.get_module(), action = args.action, id = args.id, params = args.params, routing = args.routing, attr = form_attr, content = function() ui.submit{ text = args.text, attr = args.submit_attr } end } ui.script{ type = "text/javascript", script = ( "document.getElementById(" .. quoted_form_id .. ").style.display = 'none'; document.write(" .. encode.json( slot.use_temporary( function() ui.tag{ tag = "a", attr = a_attr, content = wrapped_content } end ) ) .. ");" ) } else -- TODO: support content function local a_attr = table.new(args.attr) a_attr.href = encode.url{ external = args.external, static = args.static, module = args.module or request.get_module(), view = args.view, id = args.id, params = args.params, } return ui.tag{ tag = "a", attr = a_attr, content = wrapped_content } end end
ui.list{ label = list_label, -- optional label for the whole list style = style, -- "table", "ulli" or "div" prefix = prefix, -- prefix for HTML field names records = records, -- array of records to be displayed as rows in the list columns = { { label = column_label, -- label for the column label_attr = label_attr, -- table with HTML attributes for the heading cell or div field_attr = field_attr, -- table with HTML attributes for the data cell or div name = name, -- name of the field in each record html_name = html_name, -- optional html-name for writable fields (defaults to name) ui_field_type = ui_field_type, -- name of the ui.field.* function to use ...., -- other options for the given ui.field.* functions format = format, -- name of the format function to be used (if not using ui_field_type) format_options = format_options, -- options to be passed to the format function content = content -- function to output field data per record (ignoring name, format, ...) }, { ... }, ... } }
-- TODO: documentation of the prefix option -- TODO: check short descriptions of fields in documentation -- TODO: field_attr is used for the OUTER html tag's attributes, while attr is used for the INNER html tag's attributes (produced by ui.field.*), is that okay? -- TODO: use field information of record class, if no columns are given -- TODO: callback to set row attr's for a specific row function ui.list(args) local args = args or {} local label = args.label local list_type = args.style or "table" local prefix = args.prefix local records = assert(args.records, "ui.list{...} needs records.") local columns = assert(args.columns, "ui.list{...} needs column definitions.") local outer_attr = table.new(args.attr) local header_existent = false for idx, column in ipairs(columns) do if column.label then header_existent = true break end end local slot_state = slot.get_state_table() local outer_tag, head_tag, head_tag2, label_tag, body_tag, row_tag if list_type == "table" then outer_tag = "table" head_tag = "thead" head_tag2 = "tr" label_tag = "th" body_tag = "tbody" row_tag = "tr" field_tag = "td" elseif list_type == "ulli" then outer_tag = "div" head_tag = "div" label_tag = "div" body_tag = "ul" row_tag = "li" field_tag = "td" elseif list_type == "div" then outer_tag = "div" head_tag = "div" label_tag = "div" body_tag = "div" row_tag = "div" field_tag = "div" else error("Unknown list type specified for ui.list{...}.") end outer_attr.class = outer_attr.class or "ui_list" ui.container{ auto_args = args, content = function() ui.tag{ tag = outer_tag, attr = outer_attr, content = function() if header_existent then ui.tag{ tag = head_tag, attr = { class = "ui_list_head" }, content = function() local function header_content() for idx, column in ipairs(columns) do if column.ui_field_type ~= "hidden" then local label_attr = table.new(column.label_attr) label_attr.class = label_attr.class or { class = "ui_list_label" } ui.tag{ tag = label_tag, attr = label_attr, content = column.label or "" } end end end if head_tag2 then ui.tag{ tag = head_tag2, content = header_content } else header_content() end end } end ui.tag{ tag = body_tag, attr = { class = "ui_list_body" }, content = function() for record_idx, record in ipairs(records) do local row_class if record_idx % 2 == 0 then row_class = "ui_list_row ui_list_even" else row_class = "ui_list_row ui_list_odd" end ui.tag{ tag = row_tag, attr = { class = row_class }, content = function() local old_html_name_prefix, old_form_record if prefix then old_html_name_prefix = slot_state.html_name_prefix old_form_record = slot_state.form_record slot_state.html_name_prefix = prefix .. "[" .. record_idx .. "]" slot_state.form_record = record end local first_column = true for column_idx, column in ipairs(columns) do if column.ui_field_type ~= "hidden" then local field_attr = table.new(column.field_attr) field_attr.class = field_attr.class or { class = "ui_list_field" } local field_content if column.content then field_content = function() return column.content(record) end elseif column.name then if column.ui_field_type then local ui_field_func = ui.field[column.ui_field_type] if not ui_field_func then error('Unknown ui_field_type "' .. column.ui_field_type .. '".') end local ui_field_options = table.new(column) ui_field_options.record = record ui_field_options.label = nil if not prefix and ui_field_options.readonly == nil then ui_field_options.readonly = true end field_content = function() return ui.field[column.ui_field_type](ui_field_options) end elseif column.format then local formatter = format[column.format] if not formatter then error('Unknown format "' .. column.format .. '".') end field_content = formatter( record[column.name], column.format_options ) else field_content = function() return ui.autofield{ record = record, name = column.name, html_name = column.html_name } end end else error("Each column needs either a 'content' or a 'name'.") end local extended_field_content if first_column then first_column = false extended_field_content = function() for column_idx, column in ipairs(columns) do if column.ui_field_type == "hidden" then local ui_field_options = table.new(column) ui_field_options.record = record ui_field_options.label = nil if not prefix and ui_field_options.readonly == nil then ui_field_options.readonly = true end ui.field.hidden(ui_field_options) end end field_content() end else extended_field_content = field_content end ui.tag{ tag = field_tag, attr = field_attr, content = extended_field_content } end end if prefix then slot_state.html_name_prefix = old_html_name_prefix slot_state.form_record = old_form_record end end } end end } end } end } if prefix then -- ui.field.hidden is used instead of ui.hidden_field to suppress output in case of read-only mode. ui.field.hidden{ html_name = prefix .. "[len]", value = #records } end end
ui.multiselect{ name = name, -- HTML name ('html_name' is NOT a valid argument for this function) container_attr = container_attr, -- extra HTML attributes for the container (div) enclosing field and label attr = attr, -- extra HTML attributes for the field label = label, -- text to be used as label for the input field label_attr = label_attr, -- extra HTML attributes for the label readonly = readonly_flag -- set to true, to force read-only mode foreign_records = foreign_records, -- list of records to be chosen from, or function returning such a list foreign_id = foreign_id, -- name of id field in foreign records foreign_name = foreign_name, -- name of field to be used as name in foreign records selected_ids = selected_ids, -- list of ids of currently selected foreign records connecting_records = connecting_records, -- list of connection entries, determining which foreign records are currently selected own_id = own_id, -- TODO documentation needed own_reference = own_reference, -- name of foreign key field in connecting records, which references the main record foreign_reference = foreign_reference, -- name of foreign key field in connecting records, which references foreign records format_options = format_options -- format options for format.string }
function ui.multiselect(args) local style = args.style or "checkbox" local extra_args = { fetch_record = true } if not args.readonly and args.style == "checkbox" then extra_args.disable_label_for_id = true end ui.form_element(args, extra_args, function(args) local foreign_records = args.foreign_records if type(foreign_records) == "function" then foreign_records = foreign_records(args.record) end local connecting_records = args.connecting_records if type(connecting_records) == "function" then connecting_records = connecting_records(args.record) end local select_hash = {} if args.selected_ids then for idx, selected_id in ipairs(args.selected_ids) do select_hash[selected_id] = true end elseif args.own_reference then for idx, connecting_record in ipairs(args.connecting_records) do if connecting_record[args.own_reference] == args.record[args.own_id] then select_hash[connecting_record[args.foreign_reference]] = true end end else for idx, connecting_record in ipairs(args.connecting_records) do select_hash[connecting_record[args.foreign_reference]] = true end end local attr = table.new(args.attr) if not attr.class then attr.class = "ui_multi_selection" end if args.readonly then ui.tag{ tag = "ul", attr = attr, content = function() for idx, record in ipairs(foreign_records) do if select_hash[record[args.foreign_id]] then ui.tag{ tag = "li", content = format.string( record[args.foreign_name], args.format_options ) } end end end } elseif style == "select" then attr.name = args.name attr.multiple = "multiple" ui.tag{ tag = "select", attr = attr, content = function() if args.nil_as then ui.tag{ tag = "option", attr = { value = "" }, content = format.string( args.nil_as, args.format_options ) } end for idx, record in ipairs(foreign_records) do local key = record[args.foreign_id] local selected = select_hash[key] ui.tag{ tag = "option", attr = { value = key, selected = (selected and "selected" or nil) }, content = format.string( record[args.foreign_name], args.format_options ) } end end } elseif style == "checkbox" then attr.type = "checkbox" attr.name = args.name for idx, record in ipairs(foreign_records) do local key = record[args.foreign_id] local selected = select_hash[key] attr.id = ui.create_unique_id() attr.value = key attr.checked = selected and "checked" or nil ui.container{ label = format.string( record[args.foreign_name], args.format_options ), attr = { class = "ui_checkbox_div" }, label_for = attr.id, label_attr = { class = "ui_checkbox_label" }, content_first = true, content = function() ui.tag{ tag = "input", attr = attr } end } end else error("'style' attribute for ui.multiselect{...} must be set to \"select\", \"checkbox\" or nil.") end end) end
ui.paginate{ selector = selector, -- a selector for items from the database per_page = per_page, -- items per page, defaults to 10 name = name, -- name of the CGI get variable, defaults to "page" page = page, -- directly specify a page, and ignore 'name' parameter content = function() ... -- code block which should be encapsulated with page selection links end }
function ui.paginate(args) local selector = args.selector local per_page = args.per_page or 10 local name = args.name or 'page' local content = args.content local count_selector = selector:get_db_conn():new_selector() count_selector:add_field('count(1)') count_selector:add_from(selector) count_selector:single_object_mode() local count = count_selector:exec().count local page_count = math.floor((count - 1) / per_page) + 1 local current_page = atom.integer:load(cgi.params[name]) or 1 selector:limit(per_page) selector:offset((current_page - 1) * per_page) local id = param.get_id_cgi() local params = param.get_all_cgi() local function pagination_elements() if page_count > 1 then for page = 1, page_count do if page > 1 then slot.put(" ") end params[name] = page local attr = {} if current_page == page then attr.class = "active" end ui.link{ attr = attr, module = request.get_module(), view = request.get_view(), id = id, params = params, text = tostring(page) } end end end ui.container{ attr = { class = 'ui_paginate' }, content = function() ui.container{ attr = { class = 'ui_paginate_head ui_paginate_select' }, content = pagination_elements } ui.container{ attr = { class = 'ui_paginate_content' }, content = content } ui.container{ attr = { class = 'ui_paginate_foot ui_paginate_select' }, content = pagination_elements } end } end
ui.script{ noscript_attr = noscript_attr, -- HTML attributes for noscript tag noscript = noscript, -- string or function for noscript content attr = attr, -- extra HTML attributes for script tag type = type, -- type of script, defaults to "text/javascript" script = script, -- string or function for script content }
-- TODO: CDATA or SGML comment? function ui.script(args) local args = args or {} local noscript_attr = args.noscript_attr local noscript = args.noscript local attr = table.new(args.attr) attr.type = attr.type or args.type or "text/javascript" local script = args.script if script and type(script) ~= "function" then -- disable HTML entity escaping script = function() slot.put(args.script) end end if noscript then ui.tag{ tag = "noscript", attr = attr, content = noscript } end if script then ui.tag{ tag = "script", attr = attr, content = script } end end
ui.submit{ name = name, -- optional HTML name value = value, -- HTML value text = value -- text on button }
function ui.submit(args) if slot.get_state_table().form_readonly == false then local args = args or {} local attr = table.new(attr) attr.type = "submit" attr.name = args.name attr.value = args.value or args.text return ui.tag{ tag = "input", attr = attr } end end
ui.tag{ tag = tag, -- HTML tag, e.g. "a" for <a>...</a> attr = attr, -- table of HTML attributes, e.g. { class = "hide" } content = content -- string to be HTML encoded, or function to be executed }
function ui.tag(args) local tag, attr, content tag = args.tag attr = args.attr or {} content = args.content if type(attr.class) == "table" then attr = table.new(attr) attr.class = table.concat(attr.class, " ") end if not tag and next(attr) then tag = "span" end if tag then slot.put('<', tag) for key, value in pairs(attr) do slot.put(' ', key, '="', encode.html(value), '"') end end if content then if tag then slot.put('>') end if type(content) == "function" then content() else slot.put(encode.html(content)) end if tag then slot.put('</', tag, '>') end else if tag then slot.put(' />') end end end
Copyright (c) 2009 Public Software Group e. V., Berlin