WebMCP is a web development framework based on the Lua programming language (read more about Lua here).
WebMCP has been developed on Linux and FreeBSD. Using it with Mac OS X is untested as of yet; Microsoft Windows is not supported. Beside the operating system, the only mandatory dependencies for WebMCP are the programming language Lua version 5.2 or 5.3, the Moonbridge Network Server for Lua Applications version 1.0.1 or higher, PostgreSQL version 8.2 or higher, and a C compiler.
Please read the following instructions carefully to avoid problems during installation.
A WebMCP application may consist of several (sub-)applications. Each application sharing the same base directory also shares the database models but may provide different views and actions. The views and actions of an application are found within the "app/application_name/" directory, relative to the application base path. When starting WebMCP, the application's base path as well as the desired application name must be provided.
In addition to selection of an application, a config file must be chosen when starting the application. This enables to run an application in different contexts, e.g. you may have one configuration file for development purposes and one for productive use. Configuration files are found in the "config/" directory, relative to the application base path.
WebMCP uses the Moonbridge Network Server to handle HTTP requests. The Moonbridge Network Server listens to a TCP port and passes control to WebMCP (by invoking bin/mcp.lua in the framework's base directory), eventually resulting in a call of request.handler(...) for each request. However, before any request is processed, WebMCP will initialize the environment. This initialization includes tasks such as
For each request, it is also possible to execute filters. Filters can be used to
Filters and initializers are created by adding files in the application's directory structure. The filename determines the execution order (lexicographical order). It is a common idiom to start the filename of a filter or initializer with a two digit number to be easily able to change the execution order when desired. Filters and initializers are wrapping requests, i.e. part of them is executed before and part after the remaining request handling.
When an initializer or filter calls execute.inner(), execution of the initializer or filter is suspended and the remaining initializers and/or filters or the requested view or action are executed. Afterwards, the interrupted filters and initializers are resumed in reverse order (from where they called execute.inner()). Most often, execute.inner() is the last line in an initializer or filter, resulting in all code to be executed prior to request handling (and nothing to be executed afterwards).
The Moonbridge server creates forks (i.e. clones) of the application server process (i.e. the whole Lua engine including all libraries and variables) in order to handle concurrent requests. Certain initializations may be performed before forking, other initializations must be performed after forking. For this purpose, WebMCP allows an application to provide so-called "pre-fork" and "post-fork" initializers. The application's configuration files as well as its pre-fork initializers are executed before forking. The application's post-fork initializers are executed after forking. In particular, any libraries that open file or network handles during initialization must not be loaded before the server process is forked. Opening database connections must be performed after forking as well. WebMCP follows the following execution order (directory structure is explained further down):
As a minimum configuration, the used configuration file or pre-fork initializer should at least contain a listen{...} call, e.g.:
listen{ { proto = "tcp", host = "::", port = 8080 }, { proto = "tcp", host = "0.0.0.0", port = 8080 } } execute.inner() -- only use this line if done in pre-fork initializer
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}. Note that atom.date{...} is a shortcut for atom.date:new{...}. 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). The database connection is usually configured in the config file (e.g. in config/devel.lua):
config.db = { engine="postgresql", dbname="webmcp_demo" }
In addition to configuring the database, it must be opened within a post-fork initializer (e.g. in app/_postfork/01_database.lua):
_G.db = assert(mondelefant.connect(config.db)) function mondelefant.class_prototype:get_db_conn() return db end execute.inner()
The parameters for mondelefant.connect are directly passed to PostgreSQL's client library libpq. See PostgreSQL's documentation 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, which is stored in a relational database (PostgreSQL), in an object oriented way. Methods provided by the corresponding classes can be used to alter stored objects or execute any other associated program code. Models are usually defined in a file with a lowercase filename ending with ".lua" in the models/ directory of the application. The corresponding model name (i.e. class name) must be written in CamelCase, e.g. "models/my_model.lua" should define a model class named "MyModel". The simplest model is created by calling mondelefant.new_class() and subsequently setting the table attribute of the returned class.
-- filename: model/customer_receipt.lua CustomerReceipt = mondelefant.new_class() CustomerReceipt.table = "custreceipt"
Methods such as :add_reference(...) can be used to further modify or extend the class.
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 of a layout (see slot.select(...), slot.put(...), and slot.put_into(...) or by calling helper functions for the user interface (those functions beginning with "ui."). Views are stored in files with the file path "app/application_name/module_name/view_name.lua". When their corresponding URL, e.g. "http://hostname:port/module_name/view_name.html", is requested, the code in that file gets executed (after calling appropriate filters). After the execution of the view has finished (and after all filters have finished their execution too), the slot data will be inserted into placeholder sections in the selected layout file. The layout file defaults to "app/application_name/_layout/default.html" but may be changed using the slot.set_layout(...) function.
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 string (via Lua's return statement, where true can also be used instead of "ok", and false instead of "error"). Depending on the status string 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 string returned by the action. See the routing parameter to the ui.form{...} function for further details.
Templates for HTML documents to be returned by views are stored at the path "app/application_name/_layout/layout_name.html", relative to the application base path. The default layout name is "default". For system errors, the layout name "system_error" is used. A sample layout is given as follows:
<!DOCTYPE HTML> <html> <head> <title><!-- WEBMCP SLOTNODIV title --></title> <link rel="stylesheet" type="text/css" media="screen" href="__BASEURL__/static/style.css"/> </head> <body> <!-- WEBMCP SLOT content --> </body> </html>
The following elements in a layout file get replaced automatically:
To translate certain strings in your application, simply use the underscore function. A language can be selected with locale.set{lang = "code"} where code is a language code. The translations for strings are expected to be contained in files "locale/translations.language_code.lua". These files should return a table that maps strings to their corresponding translation:
return{ ["Are you sure?"] = "Bist Du sicher?"; ["User '#{name}' created"] = "Benutzer '#{name}' created"; }
Such translation files can be automatically created with the langtool.lua program, found in the framework's bin/ directory.
To avoid accidental programming errors, WebMCP forbids setting of global variables by default. This is overridden by using the prefix "_G." (see reference) when setting the variable, e.g. _G.myvar = 7, or by setting the variable in a file with the same name of the global varaible (but suffixed with ".lua") in the env/ directory of the framework or application. Note, however, that the lifetime of global variables is not predictable as it depends on process recycling of the Moonbridge webserver (one fork will handle more than one request) and because there may be multiple forks of the Lua machine which all have their own global variable space (there is usually more than one fork).
If an application needs to store request related data, the global table app should be used (e.g. app.myvar = true instead of _G.myvar = true). The app table gets automatically initialized (i.e. emptied) for each request.
Global variables are still useful when providing access to libraries, for example. WebMCP automatically loads certain libraries and functions though an autoloader mechanism. On read-accessing any unknown variable, WebMCP will search the framework's and application's env/ directories for an appropriate file (e.g. "my_func.lua if you invoke "my_func()") or a matching directory (e.g. "my_module/ if you access "my_module.my_func()). In case of an existing directory in env/, an empty table with autoloading capabilities is automatically created as global variable with the name of the directory. The autoloading mechanism allows directories to contain further files which can be used to initialize variables within that table when accessed. Directories can also contain a special file called "__init.lua" that always gets executed when the table is accessed for the first time. The env/ root directory can also contain a file ("env/__init__.lua") which gets executed before any configuration is loaded.
A good place to store utility functions is a global table called util. This table will be automatically accessible if you create a env/util/ directory in your WebMCP application. To provide a function util.myfunc(...) simply create a file env/util/myfunc.lua, with the following function definition:
-- filename: env/util/myfunc.lua function util.myfunc() slot.put_into("hello", "Hello World!") end
Summarizing information from the previous section, we get the following directory structure for a WebMCP application:
If the moonbridge executable is within your system's search path for binaries (i.e. if optional step 2 of the installation instructions has been carried out), you can directly call the mcp.lua script (found in framework/bin/mcp.lua). Pass the following arguments to mcp.lua:
Alternatively, the moonbridge binary may be called manually (e.g. by invoking ../moonbridge/moonbridge). In this case, the following five arguments are required:
Note that the demo application will require a database to be set up prior to starting. Execute the following shell commands first:
createdb webmcp_demo psql -v ON_ERROR_STOP=1 -f demo-app/db/schema.sql webmcp_demo
<db_class>.document_column
class_prototype.document_column = nil
<db_class>.primary_key
class_prototype.primary_key = "id"
<db_class>.schema
class_prototype.schema = nil
<db_class>.table
class_prototype.table = nil
db_class = -- same class returned <db_class>:add_reference{ mode = mode, -- "11", "1m", "m1", or "mm" (one/many to one/many) to = to, -- referenced class (model), optionally as string or function returning the value (avoids autoload) this_key = this_key, -- name of key in this class (model) that_key = that_key, -- name of key in the other class (model) ("to" argument) ref = ref, -- name of reference in this class, referring to the other class back_ref = back_ref, -- name of reference in other class, referring to this class default_order = default_order, -- expression as passed to "assemble_command" used for sorting selector_generator = selector_generator, -- alternative function used as selector generator (use only, when you know what you are doing) connected_by_table = connected_by_table, -- connecting table used for many to many relations connected_by_this_key = connected_by_this_key, -- key in connecting table referring to "this_key" of this class (model) connected_by_that_key = connected_by_that_key -- key in connecting table referring to "that_key" in other class (model) ("to" argument) }
function class_prototype:add_reference(args) local selector_generator = args.selector_generator local mode = args.mode local to = args.to local this_key = args.this_key local that_key = args.that_key local connected_by_table = args.connected_by_table -- TODO: split to table and schema local connected_by_this_key = args.connected_by_this_key local connected_by_that_key = args.connected_by_that_key local ref = args.ref local back_ref = args.back_ref local default_order = args.default_order local model local function get_model() if not model then if type(to) == "string" then model = _G for path_element in string.gmatch(to, "[^.]+") do model = model[path_element] end elseif type(to) == "function" then model = to() else model = to end end if not model or model == _G then error("Could not get model for reference.") end return model end self.references[ref] = { mode = mode, this_key = this_key, that_key = connected_by_table and "mm_ref_" or that_key, ref = ref, back_ref = back_ref, selector_generator = selector_generator or function(list, options) -- TODO: support tuple keys local options = options or {} local model = get_model() -- TODO: too many records cause PostgreSQL command stack overflow local ids_used = {} local ids = { sep = ", " } for i, object in ipairs(list) do local id = object[this_key] if id ~= nil then if not ids_used[id] then ids[#ids+1] = {"?", id} ids_used[id] = true end end end if #ids == 0 then return model:new_selector():empty_list_mode() end local selector = model:new_selector() if connected_by_table then selector:join( connected_by_table, nil, { '$."$" = $."$"', {connected_by_table}, {connected_by_that_key}, {model:get_qualified_table()}, {that_key} } ) selector:add_field( { '$."$"', {connected_by_table}, {connected_by_this_key} }, 'mm_ref_' ) selector:add_where{ '$."$" IN ($)', {connected_by_table}, {connected_by_this_key}, ids } else selector:add_where{'$."$" IN ($)', {model:get_qualified_table()}, {that_key}, ids} end if options.order == nil and default_order then selector:add_order_by(default_order) elseif options.order then selector:add_order_by(options.order) end return selector end } if mode == "m1" or mode == "11" then self.foreign_keys[this_key] = ref end return self end
db_list = -- database result being an empty list
<db_class>:create_list()
function class_prototype:create_list() local list = self:get_db_conn():create_list() list._class = self return list end
columns = -- list of columns
<db_class>:get_columns()
function class_prototype:get_columns() if self._columns then return self._columns end local selector = self:get_db_conn():new_selector() selector:set_class(self) selector:from(self:get_qualified_table()) selector:add_field("*") selector:add_where("FALSE") local db_result = selector:exec() local connection = db_result._connection local columns = {} for idx, info in ipairs(db_result._column_info) do local key = info.field_name local value = { name = key, type = connection.type_mappings[info.type] } columns[key] = value table.insert(columns, value) end self._columns = columns return columns end
db_handle = -- database connection handle used by this class
<db_class>:get_db_conn()
function class_prototype:get_db_conn() error( "Method mondelefant class(_prototype):get_db_conn() " .. "has to be implemented." ) end
reference_name = -- reference name <db_class>:get_foreign_key_reference_name( foreign_key -- foreign key )
-- implemented in mondelefant_native.c as -- static int mondelefant_class_get_reference(lua_State *L)
list = -- list of column names of primary key
<db_class>:get_primary_key_list()
function class_prototype:get_primary_key_list() local primary_key = self.primary_key if type(primary_key) == "string" then return {primary_key} else return primary_key end end
string = -- string of form '"schemaname"."tablename"' or '"tablename"'
<db_class>:get_qualified_table()
function class_prototype:get_qualified_table() if not self.table then error "Table unknown." end if self.schema then return '"' .. self.schema .. '"."' .. self.table .. '"' else return '"' .. self.table .. '"' end end --]]-- --[[-- string = -- single quoted string of form "'schemaname.tablename'" or "'tablename'" <db_class>:get_qualified_table_literal() This method returns a string with an SQL literal representing the given table. It causes ambiguities when the table name contains a dot (".") character. --]]-- function class_prototype:get_qualified_table_literal() if not self.table then error "Table unknown." end if self.schema then return self.schema .. '.' .. self.table else return self.table end end
reference_data = -- table with reference information <db_class>:get_reference( name -- reference name )
-- implemented in mondelefant_native.c as -- static int mondelefant_class_get_reference(lua_State *L)
db_object = -- database object (instance of model)
<db_class>:new()
function class_prototype:new() local object = self:get_db_conn():create_object() object._class = self object._new = true return object end
selector = -- new selector for selecting objects of this class <db_class>:new_selector( db_conn -- optional(!) database connection handle, defaults to result of :get_db_conn() )
function class_prototype:new_selector(db_conn) local selector = (db_conn or self:get_db_conn()):new_selector() selector:set_class(self) selector:from(self:get_qualified_table()) selector:add_field(self:get_qualified_table() .. ".*") return selector end
<db_error>.code -- hierarchical error code (separated by dots) in camel case
-- implemented in mondelefant_native.c as -- static const char *mondelefant_translate_errcode(const char *pgcode)
<db_error>.message -- string which summarizes the error
-- implemented in mondelefant_native.c
<db_error>:escalate()
-- implemented in mondelefant_native.c as -- static int mondelefant_errorobject_escalate(lua_State *L)
bool = -- true or false <db_error>:is_kind_of( error_code -- error code as used by this library )
-- implemented in mondelefant_native.c as -- static int mondelefant_errorobject_is_kind_of(lua_State *L)
<db_handle>.fd -- file descriptor of underlaying database connection
-- set/unset in mondelefant_native.c in -- static int mondelefant_connect(lua_State *L) and -- static int mondelefant_close(lua_State *L)
sql_string = <db_handle>:assemble_command{ template, -- template string arg1, -- value to be inserted arg2, -- another value to be inserted key1 = named_arg3, -- named value key2 = named_arg4, -- another named value ... }
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_assemble_command(lua_State *L)
<db_handle>:close()
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_close(lua_State *L)
db_list = -- database result being an empty list (or filled list) <db_handle>:create_list( tbl -- optional table to be converted to a filled list )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_create_list(lua_State *L)
db_object = -- database result being an empty object (row)
<db_handle>:create_object()
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_create_object(lua_State *L)
status = -- status string
<db_handle>:get_transaction_status()
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_get_transaction_status(lua_State *L)
status = -- true, if database connection has no malfunction
<db_handle>:is_ok()
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_is_ok(lua_State *L)
selector = -- new selector
<db_handle>:new_selector()
function connection_prototype:new_selector() return init_selector(setmetatable({}, selector_metatable), self) end
result1, -- result of first command result2, -- result of second command ... = <db_handle>:query( command1, -- first command (to be processed by "assemble_command" method) mode1, -- mode for first command: "list", "object" or "opt_object" command2, -- second command (to be processed by "assemble_command" method) mode2, -- mode for second command: "list", "object" or "opt_object" .. )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_query(lua_State *L)
quoted_encoded_data = -- encoded and quoted data (as Lua string) <db_handle>:quote_binary( raw_data -- data (as Lua string) to encode and quote )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_quote_binary(lua_State *L)
quoted_encoded_string = -- encoded and quoted string <db_handle>:quote_string( unencoded_string -- string to encode and quote )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_quote_string(lua_State *L)
db_error, -- error object, or nil in case of success result1, -- result of first command result2, -- result of second command ... = <db_handle>:try_query( command1, -- first command (to be processed by "assemble_command" method) mode1, -- mode for first command: "list", "object" or "opt_object" command2, -- second command (to be processed by "assemble_command" method) mode2, -- mode for second command: "list", "object" or "opt_object" .. )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_try_query(lua_State *L)
db_error, -- error object, or nil in case of success or timeout channel, -- notification channel name, or nil in case of timeout or no pending notify payload, -- notification payload string pid = -- process ID of notifying server process <db_handle>:try_wait( timeout -- number of seconds to wait, 0 = do not block, nil = wait infinitely )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_try_wait(lua_State *L)
channel, -- notification channel name, or nil in case of timeout or no pending notify payload, -- notification payload string pid = -- process ID of notifying server process <db_handle>:wait( timeout -- number of seconds to wait, 0 = do not block, nil = wait infinitely )
-- implemented in mondelefant_native.c as -- static int mondelefant_conn_wait(lua_State *L)
db_selector = <db_list>:get_reference_selector( ref_name, -- name of reference (e.g. "children") options, -- table options passed to the reference loader (e.g. { order = ... }) ref_alias, -- optional alias for the reference (e.g. "ordered_children") back_ref_alias -- back reference name (e.g. "parent") )
function class_prototype.list:get_reference_selector( ref_name, options, ref_alias, back_ref_alias ) local ref_info = self._class.references[ref_name] if not ref_info then error('Reference with name "' .. ref_name .. '" not found.') end local selector = ref_info.selector_generator(self, options or {}) local mode = ref_info.mode if mode == "mm" or mode == "1m" then mode = "m1" elseif mode == "m1" then mode = "1m" end local ref_alias = ref_alias if ref_alias == false then ref_alias = nil elseif ref_alias == nil then ref_alias = ref_name end local back_ref_alias if back_ref_alias == false then back_ref_alias = nil elseif back_ref_alias == nil then back_ref_alias = ref_info.back_ref end selector:attach( mode, self, ref_info.that_key, ref_info.this_key, back_ref_alias or ref_info.back_ref, ref_alias or ref_name ) return selector end
db_list_or_object = <db_list>:load( ref_name, -- name of reference (e.g. "children") options, -- table options passed to the reference loader (e.g. { order = ... }) ref_alias, -- optional alias for the reference (e.g. "ordered_children") back_ref_alias -- back reference name (e.g. "parent") )
function class_prototype.list.load(...) return class_prototype.list.get_reference_selector(...):exec() end
<db_object>._col -- proxy table mapping column names to their values
-- implemented in mondelefant_native.c as -- static const struct luaL_Reg mondelefant_columns_mt_functions[]
<db_object>:destroy()
function class_prototype.object:destroy() local db_error = self:try_destroy() if db_error then db_error:escalate() end return self end
db_object = <db_object>:get_reference_selector( ref_name, -- name of reference (e.g. "children") options, -- table options passed to the reference loader (e.g. { order = ... }) ref_alias, -- optional alias for the reference (e.g. "ordered_children") back_ref_alias -- back reference name (e.g. "parent") )
function class_prototype.object:get_reference_selector(...) local list = self._class:create_list() list[1] = self return list:get_reference_selector(...) end
db_list_or_object = <db_object>:load( ref_name, -- name of reference (e.g. "children") options, -- table options passed to the reference loader (e.g. { order = ... }) ref_alias, -- optional alias for the reference (e.g. "ordered_children") back_ref_alias -- back reference name (e.g. "parent") )
function class_prototype.object.load(...) return class_prototype.object.get_reference_selector(...):exec() end
<db_object>:save()
function class_prototype.object:save() local db_error = self:try_save() if db_error then db_error:escalate() end return self end
db_error = -- database error object, or nil in case of success
<db_object>:try_destroy()
function class_prototype.object:try_destroy() if not self._class then error("Cannot destroy object: No class information available.") end local primary_key = self._class:get_primary_key_list() local primary_key_compare = {sep = " AND "} if primary_key.json_doc then primary_key_compare[1] = { '("$"->>?)::$ = ?', {primary_key.json_doc}, primary_key.key, {primary_key.type}, self._col[primary_key.json_doc][primary_key.key] } else for idx, value in ipairs(primary_key) do primary_key_compare[idx] = { "$ = ?", {'"' .. value .. '"'}, self[value] } end end return self._connection:try_query{ 'DELETE FROM $ WHERE $', {self._class:get_qualified_table()}, primary_key_compare } end
db_error = -- database error object, or nil in case of success
<db_object>:try_save()
function class_prototype.object:try_save()
if not self._class then
error("Cannot save object: No class information available.")
end
local primary_key = self._class:get_primary_key_list()
if self._new then
local fields = {sep = ", "}
local values = {sep = ", "}
for key in pairs(self._dirty or {}) do
add(fields, '"' .. key .. '"')
add(values, {'?', self._col[key]})
end
local returning = { sep = ", " }
if primary_key.json_doc then
returning[1] = {
'("$"->>?)::$ AS "json_key"',
{primary_key.json_doc}, primary_key.key, {primary_key.type}
}
else
for idx, value in ipairs(primary_key) do
returning[idx] = '"' .. value .. '"'
end
end
local db_error, db_result
if self._upsert then
local upsert_keys = {sep = ", "}
if primary_key.json_doc then
upsert_keys[1] = {
'("$"->>?)::$',
{primary_key.json_doc}, primary_key.key, {primary_key.type}
}
else
for idx, value in ipairs(primary_key) do
upsert_keys[idx] = '"' .. value .. '"'
end
end
if #fields == 0 then
db_error, db_result = self._connection:try_query(
{
'INSERT INTO $ DEFAULT VALUES ON CONFLICT ($) DO NOTHING $',
{self._class:get_qualified_table()},
upsert_keys,
returning
},
"object"
)
else
local upsert_sets = {sep = ", "}
for key in pairs(self._dirty) do
add(upsert_sets, {'"$" = ?', {key}, self._col[key]})
end
db_error, db_result = self._connection:try_query(
{
'INSERT INTO $ ($) VALUES ($) ON CONFLICT ($) DO UPDATE SET $ RETURNING $',
{self._class:get_qualified_table()},
fields,
values,
upsert_keys,
upsert_sets,
returning
},
"object"
)
end
else
if #fields == 0 then
db_error, db_result = self._connection:try_query(
{
'INSERT INTO $ DEFAULT VALUES RETURNING $',
{self._class:get_qualified_table()},
returning
},
"object"
)
else
db_error, db_result = self._connection:try_query(
{
'INSERT INTO $ ($) VALUES ($) RETURNING $',
{self._class:get_qualified_table()},
fields,
values,
returning
},
"object"
)
end
end
if db_error then
return db_error
end
if primary_key.json_doc then
self._col[primary_key.json_doc][primary_key.key] = db_result.json_key
else
for idx, value in ipairs(primary_key) do
self[value] = db_result[value]
end
end
if not self._upsert then
self._new = false
end
else
local update_sets = {sep = ", "}
for key, mutability_state in pairs(self._dirty or {}) do
if
mutability_state == true or (
verify_mutability_state and
verify_mutability_state(self._col[key], mutability_state)
)
then
add(update_sets, {'"$" = ?', {key}, self._col[key]})
self._dirty[key] = true -- always dirty in case of later error
end
end
if #update_sets >= 1 then
local primary_key_compare = {sep = " AND "}
if primary_key.json_doc then
primary_key_compare[1] = {
'("$"->>?)::$ = ?',
{primary_key.json_doc}, primary_key.key, {primary_key.type},
self._col[primary_key.json_doc][primary_key.key]
}
else
for idx, value in ipairs(primary_key) do
primary_key_compare[idx] = {
"$ = ?",
{'"' .. value .. '"'},
self[value]
}
end
end
local db_error = self._connection:try_query{
'UPDATE $ SET $ WHERE $',
{self._class:get_qualified_table()},
update_sets,
primary_key_compare
}
if db_error then
return db_error
end
end
end
for key in pairs(self._dirty or {}) do
if save_mutability_state then
self._dirty[key] =
save_mutability_state and save_mutability_state(self._col[key]) or nil
end
end
return nil
end
<db_object>:upsert_mode()
function class_prototype.object:upsert_mode() if not self._new then error("Upsert mode requires a new object and cannot be used on objects returned from a database query.") end self._upsert = true return self end
db_selector = -- same selector returned <db_selector>:add_combine( expression -- expression as passed to "assemble_command" )
function selector_prototype:add_combine(expression) add(self._combine, expression) return self end
db_selector = -- same selector returned <db_selector>:add_distinct_on( expression -- expression as passed to "assemble_command" )
function selector_prototype:add_distinct_on(expression) if self._distinct then error("Can not combine DISTINCT with DISTINCT ON.") end add(self._distinct_on, expression) return self end
db_selector = -- same selector returned <db_selector>:add_field( expression, -- expression as passed to "assemble_command" alias, -- optional alias expression as passed to "assemble_command" option_list -- optional list of options (may contain strings "distinct" or "grouped") )
function selector_prototype:add_field(expression, alias, options) if alias then add(self._fields, {'$ AS "$"', {expression}, {alias}}) else add(self._fields, expression) end if options then for i, option in ipairs(options) do if option == "distinct" then if alias then self:add_distinct_on('"' .. alias .. '"') else self:add_distinct_on(expression) end elseif option == "grouped" then if alias then self:add_group_by('"' .. alias .. '"') else self:add_group_by(expression) end else error("Unknown option '" .. option .. "' to add_field method.") end end end return self end
db_selector = -- same selector returned <db_selector>:add_from( expression, -- expression as passed to "assemble_command" alias, -- optional alias expression as passed to "assemble_command" condition -- optional condition expression as passed to "assemble_command" )
function selector_prototype:add_from(expression, alias, condition) local first = (#self._from == 0) if not first then if condition then add(self._from, "INNER JOIN") else add(self._from, "CROSS JOIN") end end if getmetatable(expression) == selector_metatable then if alias then add(self._from, {'($) AS "$"', {expression}, {alias}}) else add(self._from, {'($) AS "subquery"', {expression}}) end else if alias then add(self._from, {'$ AS "$"', {expression}, {alias}}) else add(self._from, expression) end end if condition then if first then self:add_where(condition) else add(self._from, "ON") add(self._from, condition) end end return self end
db_selector = -- same selector returned <db_selector>:add_group_by( expression -- expression as passed to "assemble_command" )
function selector_prototype:add_group_by(expression) add(self._group_by, expression) return self end
db_selector = -- same selector returned <db_selector>:add_having( expression -- expression as passed to "assemble_command" )
function selector_prototype:add_having(expression) add(self._having, expression) return self end
db_selector = -- same selector returned <db_selector>:add_order_by( expression -- expression as passed to "assemble_command" )
function selector_prototype:add_order_by(expression) add(self._order_by, expression) return self end
db_selector = -- same selector returned <db_selector>:add_where( expression -- expression as passed to "assemble_command" )
function selector_prototype:add_where(expression) add(self._where, expression) return self end
db_selector = <db_selector>:add_with( expression = expression, selector = selector )
function selector_prototype:add_with(expression, selector) add(self._with, {"$ AS ($)", {expression}, {selector}}) return self end
db_selector = -- same selector returned <db_selector>:attach( mode, -- attachment type: "11" one to one, "1m" one to many, "m1" many to one data2, -- other database result list or object, the results of this selector shall be attached with field1, -- field name(s) in result list or object of this selector used for attaching field2, -- field name(s) in "data2" used for attaching ref1, -- name of reference field in the results of this selector after attaching ref2 -- name of reference field in "data2" after attaching )
function selector_prototype:attach(mode, data2, field1, field2, ref1, ref2) self._attach = { mode = mode, data2 = data2, field1 = field1, field2 = field2, ref1 = ref1, ref2 = ref2 } return self end
count = -- number of rows returned
<db_selector>:count()
function selector_prototype:count() if not self._count then local count_selector = self:get_db_conn():new_selector() count_selector:add_field('count(1)') count_selector:add_from(self) count_selector:single_object_mode() self._count = count_selector:exec().count end return self._count end
db_selector = -- same selector returned
<db_selector>:empty_list_mode()
function selector_prototype:empty_list_mode() self._mode = "empty_list" return self end
db_selector = -- same selector returned <db_selector>:except( expression -- expression or selector without ORDER BY, LIMIT, FOR UPDATE or FOR SHARE )
function selector_prototype:except(expression) self:add_combine{"EXCEPT $", {expression}} return self end
db_selector = -- same selector returned <db_selector>:except_all( expression -- expression or selector without ORDER BY, LIMIT, FOR UPDATE or FOR SHARE )
function selector_prototype:except_all(expression) self:add_combine{"EXCEPT ALL $", {expression}} return self end
result = -- database result list or object
<db_selector>:exec()
function selector_prototype:exec() local db_error, result = self:try_exec() if db_error then db_error:escalate() else return result end end
db_selector = -- same selector returned
<db_selector>:for_share()
function selector_prototype:for_share() self._read_lock.all = true return self end
db_selector = -- same selector returned <db_selector>:for_share_of( expression -- expression as passed to "assemble_command" )
function selector_prototype:for_share_of(expression) add(self._read_lock, expression) return self end
db_selector = -- same selector returned
<db_selector>:for_update()
function selector_prototype:for_update() self._write_lock.all = true return self end
db_selector = -- same selector returned <db_selector>:for_update_of( expression -- expression as passed to "assemble_command" )
function selector_prototype:for_update_of(expression) add(self._write_lock, expression) return self end
db_selector = -- same selector returned <db_selector>:from( expression, -- expression as passed to "assemble_command" alias, -- optional alias expression as passed to "assemble_command" condition -- optional condition expression as passed to "assemble_command" )
function selector_prototype:from(expression, alias, condition) if #self._from > 0 then error("From-clause already existing (hint: try join).") end return self:join(expression, alias, condition) end
db_handle = -- handle of database connection
<db_selector>:get_db_conn()
function selector_prototype:get_db_conn() return self._db_conn end
db_selector = -- same selector returned <db_selector>:intersect( expression -- expression or selector without ORDER BY, LIMIT, FOR UPDATE or FOR SHARE )
function selector_prototype:intersect(expression) self:add_combine{"INTERSECT $", {expression}} return self end
db_selector = -- same selector returned <db_selector>:intersect_all( expression -- expression or selector without ORDER BY, LIMIT, FOR UPDATE or FOR SHARE )
function selector_prototype:intersect_all(expression) self:add_combine{"INTERSECT ALL $", {expression}} return self end
db_selector = -- same selector returned <db_selector>:join( expression, -- expression as passed to "assemble_command" alias, -- optional alias expression as passed to "assemble_command" condition -- optional condition expression as passed to "assemble_command" )
function selector_prototype:join(...) -- NOTE: alias for add_from
return self:add_from(...)
end
db_selector = -- same selector returned <db_selector>:left_join( expression, -- expression as passed to "assemble_command" alias, -- optional alias expression as passed to "assemble_command" condition -- optional condition expression as passed to "assemble_command" )
function selector_prototype:left_join(expression, alias, condition) local first = (#self._from == 0) if not first then add(self._from, "LEFT OUTER JOIN") end if alias then add(self._from, {'$ AS "$"', {expression}, {alias}}) else add(self._from, expression) end if condition then if first then self:add_where(condition) else add(self._from, "ON") add(self._from, condition) end end return self end
db_selector = -- same selector returned <db_selector>:limit( count -- integer used as LIMIT )
function selector_prototype:limit(count) if type(count) ~= "number" or count % 1 ~= 0 then error("LIMIT must be an integer.") end self._limit = count return self end
db_selector = -- same selector returned <db_selector>:offset( count -- integer used as OFFSET )
function selector_prototype:offset(count) if type(count) ~= "number" or count % 1 ~= 0 then error("OFFSET must be an integer.") end self._offset = count return self end
db_selector = -- same selector returned
<db_selector>:optional_object_mode()
function selector_prototype:optional_object_mode() self._mode = "opt_object" return self end
db_selector = -- same selector returned
<db_selector>:reset_fields()
function selector_prototype:reset_fields() for idx in ipairs(self._fields) do self._fields[idx] = nil end return self end
db_selector = -- same selector returned <db_selector>:set_class( class -- database class (model) )
function selector_prototype:set_class(class) self._class = class return self end
db_selector = -- same selector returned
<db_selector>:set_distinct()
function selector_prototype:set_distinct() if #self._distinct_on > 0 then error("Can not combine DISTINCT with DISTINCT ON.") end self._distinct = true return self end
db_selector = -- same selector returned
<db_selector>:single_object_mode()
function selector_prototype:single_object_mode() self._mode = "object" return self end
db_error, -- database error object, or nil in case of success result = -- database result list or object <db_selector>:try_exec()
function selector_prototype:try_exec() if self._mode == "empty_list" then if self._class then return nil, self._class:create_list() else return nil, self._db_conn:create_list() end end local db_error, db_result = self._db_conn:try_query(self, self._mode) if db_error then return db_error elseif db_result then if self._class then set_class(db_result, self._class) end if self._attach then attach( self._attach.mode, db_result, self._attach.data2, self._attach.field1, self._attach.field2, self._attach.ref1, self._attach.ref2 ) end return nil, db_result else return nil end end
db_selector = -- same selector returned <db_selector>:union( expression -- expression or selector without ORDER BY, LIMIT, FOR UPDATE or FOR SHARE )
function selector_prototype:union(expression) self:add_combine{"UNION $", {expression}} return self end
db_selector = -- same selector returned <db_selector>:union_all( expression -- expression or selector without ORDER BY, LIMIT, FOR UPDATE or FOR SHARE )
function selector_prototype:union_all(expression) self:add_combine{"UNION ALL $", {expression}} return self end
WEBMCP_APP_NAME
-- set in mcp.lua
WEBMCP_BASE_PATH
-- set in mcp.lua
WEBMCP_CONFIG_NAMES
-- configuration names are provided as 4th, 5th, etc. command line argument
WEBMCP_CONFIG_NAMES = {select(4, ...)}
WEBMCP_FRAMEWORK_PATH
-- set in mcp.lua
WEBMCP_MODE
if _MOONBRIDGE_VERSION then WEBMCP_MODE = "listen" else WEBMCP_MODE = "interactive" end
WEBMCP_VERSION
WEBMCP_VERSION = "2.2.0"
translated_string = -- translated string _( string_to_translate, -- string to translate { placeholder_name1 = text1, -- replace all occurrences of "#{placeholder_name1}" with the string text1 placeholder_name2 = text2, -- replace all occurrences of "#{placeholder_name2}" with the string text2 ... } )
function _G._(text, replacements) local text = locale._get_translation_table()[text] or text if replacements then return ( string.gsub( text, "#{(.-)}", function (placeholder) return replacements[placeholder] end ) ) else return text end end
_G
local _G = _G
local allowed_globals = {}
local protected_environment = setmetatable(
{}, -- proxy environment used all chunks loaded through loadcached(...)
{
__index = _G,
__newindex = function(self, key, value)
if allowed_globals[key] then
_G[key] = value
else
if type(key) == "string" and string.match(key, "^[A-Za-z_][A-Za-z_0-9]*$") then
error('Attempt to set global variable "' .. key .. '" (Hint: missing local statement? Use _G.' .. key .. '=<value> to really set global variable.)', 2)
else
error('Attempt to set global variable', 2)
end
end
end
}
)
app -- table to store an application state
-- Initialized in request.initialize(...).
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 local jd = date.ymd_to_jd(year, month, day) local year2, month2, day2 = date.jd_to_ymd(jd) if year == year2 and month == month2 and day == day2 then return date:_create{ jd = date.ymd_to_jd(year, month, day), year = year2, month = month2, day = day2 } else return date.invalid end 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
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
int = -- a number or atom.number.invalid (atom.not_a_number) atom.number:load( string -- a string representing a number )
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
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
atom.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 }
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
url = -- normalized URL or nil auth.openid._normalize_url( url -- unnormalized URL )
function auth.openid._normalize_url(url) local url = string.match(url, "^(.-)??$") -- remove "?" at end local proto, host, path = string.match( url, "([A-Za-z]+)://([0-9A-Za-z.:_-]+)/?([0-9A-Za-z%%/._~-]*)$" ) if not proto then return nil end proto = string.lower(proto) host = string.lower(host) local port = string.match(host, ":(.*)") if port then if string.find(port, "^[0-9]+$") then port = tonumber(port) host = string.match(host, "^(.-):") if port < 1 or port > 65535 then return nil end else return nil end end if proto == "http" then if port == 80 then port = nil end elseif proto == "https" then if port == 443 then port = nil end else return nil end if string.find(host, "^%.") or string.find(host, "%.$") or string.find(host, "%.%.") then return nil end for part in string.gmatch(host, "[^.]+") do if not string.find(part, "[A-Za-z]") then return nil end end local path_parts = {} for part in string.gmatch(path, "[^/]+") do if part == "." then -- do nothing elseif part == ".." then path_parts[#path_parts] = nil else local fail = false local part = string.gsub( part, "%%([0-9A-Fa-f]?[0-9A-Fa-f]?)", function (hex) if #hex ~= 2 then fail = true return end local char = string.char(tonumber("0x" .. hex)) if string.find(char, "[0-9A-Za-z._~-]") then return char else return "%" .. string.upper(hex) end end ) if fail then return nil end path_parts[#path_parts+1] = part end end if string.find(path, "/$") then path_parts[#path_parts+1] = "" end path = table.concat(path_parts, "/") if port then host = host .. ":" .. tostring(port) end return proto .. "://" .. host .. "/" .. path end
discovery_data, -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier" errmsg, -- error message in case of failure errcode = -- error code in case of failure (TODO: not implemented yet) auth.openid.discover{ user_supplied_identifier = user_supplied_identifier, -- string given by user https_as_default = https_as_default, -- default to https curl_options = curl_options -- options passed to "curl" binary, when performing discovery }
-- helper function local function decode_entities(str) local str = str str = string.gsub(value, "<", '<') str = string.gsub(value, ">", '>') str = string.gsub(value, """, '"') str = string.gsub(value, "&", '&') return str end -- helper function local function get_tag_value( str, -- HTML document or document snippet match_tag, -- tag name match_key, -- attribute key to match match_value, -- attribute value to match result_key -- attribute key of value to return ) -- NOTE: The following parameters are case insensitive local match_tag = string.lower(match_tag) local match_key = string.lower(match_key) local match_value = string.lower(match_value) local result_key = string.lower(result_key) for tag, attributes in string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>") do local tag = string.lower(tag) if tag == match_tag then local matching = false for key, value in string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"') do local key = string.lower(key) local value = decode_entities(value) if key == match_key then -- NOTE: match_key must only match one key of space seperated list for value in string.gmatch(value, "[^ ]+") do if string.lower(value) == match_value then matching = true break end end end if key == result_key then result_value = value end end if matching then return result_value end end end return nil end -- helper function local function tag_contents(str, match_tag) local pos = 0 local tagpos, closing, tag local function next_tag() local prefix tagpos, prefix, tag, pos = string.match( str, "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()", pos ) closing = (prefix == "/") end return function() repeat next_tag() if not tagpos then return nil end local stripped_tag if string.find(tag, ":") then stripped_tag = string.match(tag, ":([^:]*)$") else stripped_tag = tag end until stripped_tag == match_tag and not closing local content_start = pos local used_tag = tag local counter = 0 while true do repeat next_tag() if not tagpos then return nil end until tag == used_tag if closing then if counter > 0 then counter = counter - 1 else return string.sub(str, content_start, tagpos-1) end else counter = counter + 1 end end local content = string.sub(rest, 1, startpos-1) str = string.sub(rest, endpos+1) return content end end local function strip(str) local str = str string.gsub(str, "^[ \t\r\n]+", "") string.gsub(str, "[ \t\r\n]+$", "") return str end function auth.openid.discover(args) local url = string.match(args.user_supplied_identifier, "[^#]*") -- NOTE: XRIs are not supported if string.find(url, "^[Xx][Rr][Ii]://") or string.find(url, "^[=@+$!(]") then return nil, "XRI identifiers are not supported." end -- Prepend http:// or https://, if neccessary: if not string.find(url, "://") then if args.default_to_https then url = "https://" .. url else url = "http://" .. url end end -- Either an xrds_document or an html_document will be fetched local xrds_document, html_document -- Repeat max 10 times to avoid endless redirection loops local redirects = 0 while true do local status, headers, body = auth.openid._curl(url, args.curl_options) if not status then return nil, "Error while locating XRDS or HTML file for discovery." end -- Check, if we are redirected: local location = string.match( headers, "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)" ) if location then -- If we are redirected too often, then return an error. if redirects >= 10 then return nil, "Too many redirects." end -- Otherwise follow the redirection by changing the variable "url" -- and by incrementing the redirect counter. url = location redirects = redirects + 1 else -- Check, if there is an X-XRDS-Location header -- pointing to an XRDS document: local xrds_location = string.match( headers, "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)" ) -- If there is no X-XRDS-Location header, there might be an -- http-equiv meta tag serving the same purpose: if not xrds_location and status == 200 then xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content") end if xrds_location then -- If we know the XRDS-Location, we can fetch the XRDS document -- from that location: local status, headers, body = auth.openid._curl(xrds_location, args.curl_options) if not status then return nil, "XRDS document could not be loaded." end if status ~= 200 then return nil, "XRDS document not found where expected." end xrds_document = body break elseif -- If the Content-Type header is set accordingly, then we already -- should have received an XRDS document: string.find( headers, "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n" ) then if status ~= 200 then return nil, "XRDS document announced but not found." end xrds_document = body break else -- Otherwise we should have received an HTML document: if status ~= 200 then return nil, "No XRDS or HTML document found for discovery." end html_document = body break; end end end local claimed_identifier -- OpenID identifier the user claims to own local op_endpoint -- OpenID provider endpoint URL local op_local_identifier -- optional user identifier, local to the OpenID provider if xrds_document then -- If we got an XRDS document, we look for a matching <Service> entry: for content in tag_contents(xrds_document, "Service") do local service_uri, service_localid for content in tag_contents(content, "URI") do if not string.find(content, "[<>]") then service_uri = strip(content) break end end for content in tag_contents(content, "LocalID") do if not string.find(content, "[<>]") then service_localid = strip(content) break end end for content in tag_contents(content, "Type") do if not string.find(content, "[<>]") then local content = strip(content) if content == "http://specs.openid.net/auth/2.0/server" then -- The user entered a provider identifier, thus claimed_identifier -- and op_local_identifier will be set to nil. op_endpoint = service_uri break elseif content == "http://specs.openid.net/auth/2.0/signon" then -- The user entered his/her own identifier. claimed_identifier = url op_endpoint = service_uri op_local_identifier = service_localid break end end end end elseif html_document then -- If we got an HTML document, we look for matching <link .../> tags: claimed_identifier = url op_endpoint = get_tag_value( html_document, "link", "rel", "openid2.provider", "href" ) op_local_identifier = get_tag_value( html_document, "link", "rel", "openid2.local_id", "href" ) else error("Assertion failed") -- should not happen end if not op_endpoint then return nil, "No OpenID endpoint found." end if claimed_identifier then claimed_identifier = auth.openid._normalize_url(claimed_identifier) if not claimed_identifier then return nil, "Claimed identifier could not be normalized." end end return { claimed_identifier = claimed_identifier, op_endpoint = op_endpoint, op_local_identifier = op_local_identifier } end
success, -- boolean indicating success or failure errmsg, -- error message in case of failure errcode = -- error code in case of failure (TODO: not implemented yet) auth.openid.initiate{ user_supplied_identifier = user_supplied_identifier, -- string given by user https_as_default = https_as_default, -- default to https curl_options = curl_options, -- additional options passed to "curl" binary, when performing discovery return_to_module = return_to_module, -- module of the verifying view, the user shall return to after authentication return_to_view = return_to_view, -- verifying view, the user shall return to after authentication realm = realm -- URL the user should authenticate for, defaults to application base }
function auth.openid.initiate(args) local dd, errmsg, errcode = auth.openid.discover(args) if not dd then return nil, errmsg, errcode end -- TODO: Use request.redirect once it supports external URLs request.set_status("303 See Other") request.add_header( "Location: " .. encode.url{ external = dd.op_endpoint, params = { ["openid.ns"] = "http://specs.openid.net/auth/2.0", ["openid.mode"] = "checkid_setup", ["openid.claimed_id"] = dd.claimed_identifier or "http://specs.openid.net/auth/2.0/identifier_select", ["openid.identity"] = dd.op_local_identifier or dd.claimed_identifier or "http://specs.openid.net/auth/2.0/identifier_select", ["openid.return_to"] = encode.url{ base = request.get_absolute_baseurl(), module = args.return_to_module, view = args.return_to_view }, ["openid.realm"] = args.realm or request.get_absolute_baseurl() } } ) error("Not implemented") -- TODO --cgi.send_data() --exit() end
claimed_identifier, -- identifier owned by the user errmsg, -- error message in case of failure errcode = -- error code in case of failure (TODO: not implemented yet) auth.openid.verify{ force_https = force_https, -- only allow https curl_options = curl_options -- options passed to "curl" binary, when performing discovery }
function auth.openid.verify(args) local args = args or {} if request.get_param{name="openid.ns"} ~= "http://specs.openid.net/auth/2.0" then return nil, "No indirect OpenID 2.0 message received." end local mode = request.get_param{name="openid.mode"} if mode == "id_res" then local return_to_url = request.get_param{name="openid.return_to"} if not return_to_url then return nil, "No return_to URL received in answer." end if return_to_url ~= encode.url{ base = request.get_absolute_baseurl(), module = request.get_module(), view = request.get_view() } then return nil, "return_to URL not matching." end local discovery_args = table.new(args) local claimed_identifier = request.get_param{name="openid.claimed_id"} if not claimed_identifier then return nil, "No claimed identifier received." end local cropped_identifier = string.match(claimed_identifier, "[^#]*") local normalized_identifier = auth.openid._normalize_url( cropped_identifier ) if not normalized_identifier then return nil, "Claimed identifier could not be normalized." end if normalized_identifier ~= cropped_identifier then return nil, "Claimed identifier was not normalized." end discovery_args.user_supplied_identifier = cropped_identifier local dd, errmsg, errcode = auth.openid.discover(discovery_args) if not dd then return nil, errmsg, errcode end if not dd.claimed_identifier then return nil, "Identifier is an OpenID Provider." end if dd.claimed_identifier ~= cropped_identifier then return nil, "Claimed identifier does not match." end local nonce = request.get_param{name="openid.response_nonce"} if not nonce then return nil, "Did not receive a response nonce." end local year, month, day, hour, minute, second = string.match( nonce, "^([0-9][0-9][0-9][0-9])%-([0-9][0-9])%-([0-9][0-9])T([0-9][0-9]):([0-9][0-9]):([0-9][0-9])Z" ) if not year then return nil, "Response nonce did not contain a parsable date/time." end local ts = atom.timestamp{ year = tonumber(year), month = tonumber(month), day = tonumber(day), hour = tonumber(hour), minute = tonumber(minute), second = tonumber(second) } -- NOTE: 50 hours margin allows us to ignore time zone issues here: if math.abs(ts - atom.timestamp:get_current()) > 3600 * 50 then return nil, "Response nonce contains wrong time or local time is wrong." end local params = {} for key, value in pairs(cgi.params) do local trimmed_key = string.match(key, "^openid%.(.+)") if trimmed_key then params[key] = value end end params["openid.mode"] = "check_authentication" local options = table.new(args.curl_options) for key, value in pairs(params) do options[#options+1] = "--data-urlencode" options[#options+1] = key .. "=" .. value end local status, headers, body = auth.openid._curl(dd.op_endpoint, options) if status ~= 200 then return nil, "Authorization could not be verified." end local result = {} for key, value in string.gmatch(body, "([^\n:]+):([^\n]*)") do result[key] = value end if result.ns ~= "http://specs.openid.net/auth/2.0" then return nil, "No OpenID 2.0 message replied." end if result.is_valid == "true" then return claimed_identifier else return nil, "Signature invalid." end elseif mode == "cancel" then return nil, "Authorization failed according to OpenID provider." else return nil, "Unexpected OpenID mode." end end
auth.openid.xrds_document{ return_to_module = return_to_module, return_to_view = return_to_view }
function auth.openid.xrds_document(args) slot.set_layout(nil, "application/xrds+xml") slot.put_into("data", '<?xml version="1.0" encoding="UTF-8"?>\n', '<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">\n', ' <XRD>\n', ' <Service>\n', ' <Type>http://specs.openid.net/auth/2.0/return_to</Type>\n', ' <URI>', encode.url{ base = request.get_absolute_baseurl(), module = args.return_to_module, view = args.return_to_view }, '</URI>\n', ' </Service>\n', ' </XRD>\n', '</xrds:XRDS>\n' ) end
auth.openid.xrds_header{
... -- arguments as used for encode.url{...}, pointing to an XRDS document as explained below
}
function auth.openid.xrds_header(args) request.add_header("X-XRDS-Location: " .. encode.url(args)) 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) request.configure(function() charset._current = charset_ident end) end
config -- table to store application configuration
_G.config = {}
path = -- string containing a path to an action encode.action_file_path{ module = module, -- module name action = action -- action name }
-- TODO: remove deprecated function
function encode.action_file_path(args)
return (encode.file_path(
WEBMCP_BASE_PATH,
'app',
WEBMCP_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.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, table.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 encode.json( value -- nil, false, true, a number, a string, or json.array{...} or json.object{...} )
function encode.json(obj) local str = json.export(obj) str = string.gsub(str, "</", "<\\/") str = string.gsub(str, "<!%[CDATA%[", "\\u003c![CDATA[") str = string.gsub(str, "]]>", "]]\\u003e") return str 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 anchor = anchor -- anchor in URL }
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 anchor = args.anchor 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 if not (external and string.find(external, "%?")) then add("?") elseif external and not string.find(external, "&$") then add("&") end if id and id_as_param then add("_webmcp_id=", encode.url_part(id), "&") end for key, value in pairs(params) do -- TODO: better way to detect arrays? if string.match(key, "%[%]$") then for idx, entry in ipairs(value) do add(encode.url_part(key), "=", encode.url_part(entry), "&") end else add(encode.url_part(key), "=", encode.url_part(value), "&") end end result[#result] = nil -- remove last '&' or '?' end local string_result = table.concat(result) if anchor ~= nil then string_result = string.match(string_result, "^[^#]*") if anchor then string_result = string_result .. "#" .. encode.url_part(anchor) end end return string_result end
path = -- string containing a path to a view encode.view_file_path{ module = module, -- module name view = view -- view name }
-- TODO: remove deprecated function
function encode.view_file_path(args)
return (encode.file_path(
WEBMCP_BASE_PATH, 'app', WEBMCP_APP_NAME, args.module, args.view .. '.lua'
))
end
action_status = -- status code returned by action (a string), or, if "test_existence" == true, a boolean 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 test_existence = test_existence -- do not execute action but only check if it exists }
function execute.action(args) local module = args.module local action = args.action local test = args.test_existence if not test then trace.enter_action{ module = module, action = action } end local action_status = execute.file_path{ file_path = encode.file_path( WEBMCP_BASE_PATH, 'app', WEBMCP_APP_NAME, module, '_action', action .. '.lua' ), id = args.id, params = args.params, test_existence = test } if not test then trace.execution_return{ status = action_status } end return action_status end
return_value = -- return value of executed chunk execute.chunk{ file_path = file_path, -- path to a Lua source or byte-code file app = app, -- app name to use or the current will be used module = module, -- module where chunk is located chunk = chunk -- name of chunk (filename without .lua extension) id = id, -- id to be returned by param.get_id(...) during execution params = params -- parameters to be returned by param.get(...) during execution }
local function pack_return_values(...) local storage = {...} storage.n = select("#", ...) return storage end local function unpack_return_values(storage) return table.unpack(storage, 1, storage.n) end function execute.chunk(args) local file_path = args.file_path local app = args.app or WEBMCP_APP_NAME local module = args.module local chunk = args.chunk local id = args.id local params = args.params file_path = file_path or encode.file_path( WEBMCP_BASE_PATH, 'app', app, module, chunk .. '.lua' ) local func = assert(loadcached(file_path)) if id or params then param.exchange(id, params) end local result = pack_return_values(func()) if id or params then param.restore() end return unpack_return_values(result) end
output, -- collected data from stdout if process exited successfully errmsg = -- error message if execution failed or if process didn't exit successfully execute.command{ command = { filename, arg1, arg2, ... }, -- command and arguments stdin_data = stdin_data, -- optional data to be sent to process via stdin stdout_result_handler = stdout_result_handler, -- callback receiving: stdout data, success boolean, optional error message stderr_line_handler = stderr_line_handler, -- callback for processing stderr line-wise exit_handler = exit_handler, -- callback when process exited signal_handler = signal_handler, -- callback when process terminated due to signal timeout_handler = timeout_handler, -- callback when process gets killed due to timeout abort_handler = abort_handler, -- callback when process gets killed due to request by poll function abortable = abortable, -- set to true if process shall be terminated if poll function requests termination poll = poll, -- alternative poll command with moonbridge_io.poll(...) semantics db = db, -- database handle for waiting for notifies db_notify_handler = db_notify_handler -- handler for database notifications which may return true to kill process }
function execute.command(args) local moonbridge_io = require("moonbridge_io") local poll = args.poll or moonbridge_io.poll local stdout_chunks, stderr_chunks = {}, {} local function return_error(errmsg) if args.stdout_result_handler then args.stdout_result_handler(table.concat(stdout_chunks), false, errmsg) end return nil, errmsg end if args.abortable then local pollready, pollmsg, pollterm = poll(nil, nil, 0, true) if pollterm then if args.abort_handler then args.abort_handler(pollmsg) end return_error(pollmsg) end end local start = moonbridge_io.timeref() local process, errmsg = moonbridge_io.exec(table.unpack(args.command)) if not process then return nil, errmsg end local read_fds = { [process] = true, [process.stdout] = true, [process.stderr] = true } local write_fds = { [process.stdin] = true } if args.db then read_fds[args.db.fd] = true end local function write(...) if write_fds[process.stdin] then local buffered = process.stdin:flush_nb(...) if not buffered or buffered == 0 then process.stdin:close() write_fds[process.stdin] = nil end end end write(args.stdin_data or "") local status while not status or read_fds[process.stdout] or read_fds[process.stderr] or write_fds[process.stdin] do local timeout = args.timeout and args.timeout-moonbridge_io.timeref(start) local pollready, pollmsg, pollterm = poll(read_fds, write_fds, timeout, args.abortable) if not pollready then if not status then process:kill():wait() end if pollterm then if args.abort_handler then args.abort_handler(pollmsg) end else if args.timeout_handler then args.timeout_handler() end end return return_error(pollmsg) end if not status then status = process:wait_nb() if status then read_fds[process] = nil end end if args.db then local channel, payload, pid = db:wait(0) if channel then if args.db_notify_handler(channel, payload, pid) then process:kill():wait() return return_error("Database event received") end end end if read_fds[process.stdout] then local chunk, status = process.stdout:read_nb() if not chunk or status == "eof" then process.stdout:close() read_fds[process.stdout] = nil end if chunk and chunk ~= "" then stdout_chunks[#stdout_chunks+1] = chunk end end if read_fds[process.stderr] then local chunk, status = process.stderr:read_nb() if not chunk or status == "eof" then process.stderr:close() read_fds[process.stderr] = nil end if chunk and args.stderr_line_handler then while true do local chunk1, chunk2 = string.match(chunk, "(.-)\n(.*)") if not chunk1 then break end stderr_chunks[#stderr_chunks+1] = chunk1 args.stderr_line_handler(table.concat(stderr_chunks)) stderr_chunks = {} chunk = chunk2 end if chunk ~= "" then stderr_chunks[#stderr_chunks+1] = chunk end if status == "eof" then local line = table.concat(stderr_chunks) if #line > 0 then args.stderr_line_handler(line) end end end end write() end if status < 0 then if args.signal_handler then args.signal_handler(-status) end return return_error("Command terminated by signal " .. -status) elseif status > 0 then if args.exit_handler then args.exit_handler(status) end return return_error("Command returned exit code " .. status) elseif args.stdout_result_handler then args.stdout_result_handler(table.concat(stdout_chunks), true) return true else return table.concat(stdout_chunks) end 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( WEBMCP_BASE_PATH, '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 test_existence = test_existence -- do not execute view or action but only check if it exists }
function execute.file_path(args) local file_path = args.file_path local test = args.test_existence if test then if loadcached(file_path) then return true else return false end end local id = args.id local params = args.params local func = assert(loadcached(file_path)) 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 result execute.multi_wrapped( execute._create_sorted_execution_list( function(add_by_path) add_by_path("_filter") add_by_path("_filter_action") add_by_path(WEBMCP_APP_NAME, "_filter") add_by_path(WEBMCP_APP_NAME, "_filter_action") add_by_path(WEBMCP_APP_NAME, args.module, "_filter") add_by_path(WEBMCP_APP_NAME, args.module, "_filter_action") end, function(full_path, relative_path) trace.enter_filter{ path = relative_path } execute.file_path{ file_path = full_path } trace.execution_return() end ), 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) execute.multi_wrapped( execute._create_sorted_execution_list( function(add_by_path) add_by_path("_filter") add_by_path("_filter_view") add_by_path(WEBMCP_APP_NAME, "_filter") add_by_path(WEBMCP_APP_NAME, "_filter_view") add_by_path(WEBMCP_APP_NAME, args.module, "_filter") add_by_path(WEBMCP_APP_NAME, args.module, "_filter_view") end, function(full_path, relative_path) trace.enter_filter{ path = relative_path } execute.file_path{ file_path = full_path } trace.execution_return() end ), function() execute.view(args) end ) end
execute.finalizers()
function execute.finalizers() for i = #execute._finalizers, 1, -1 do execute._finalizers[i]() execute._finalizers[i] = nil 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.postfork_initializers()
function execute.postfork_initializers() execute._initializers("_postfork") end
execute.prefork_initializers()
function execute.prefork_initializers() execute._initializers("_prefork") end
view_exists = -- boolean returned if "test_existence" is set to true, otherwise no value returned 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 test_existence = test_existence -- do not execute view but only check if it exists }
function execute.view(args) local module = args.module local view = args.view local test = args.test_existence if not test then trace.enter_view{ module = module, view = view } end local result = execute.file_path{ file_path = encode.file_path( WEBMCP_BASE_PATH, 'app', WEBMCP_APP_NAME, module, view .. '.lua' ), id = args.id, params = args.params, test_existence = test } if not test then trace.execution_return() end if test then return result end 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
passhash = -- encrypted password extos.crypt{ key = key, -- password to be one-way encrypted salt = salt -- salt to be used for encryption, optionally starting with "$N$", where N is a digit }
-- implemented in extos.c as -- static int extos_crypt(lua_State *L)
filestat_table, -- table with information on the file, nil on error errmsg = -- error message if file information could not be determined extos.fstat( file_handle -- Lua file handle (e.g. as returned by io.open(...)) )
-- implemented in extos.c as -- static int extos_stat(lua_State *L)
seconds = extos.hires_time()
-- implemented in extos.c as -- static int extos_hires_time(lua_State *L)
directory_entries, -- table of directory entries errmsg = -- error message if directory could not be read extos.listdir( path -- path name )
-- implemented in extos.c as -- static int extos_listdir(lua_State *L)
filestat_table, -- table with information on the file, false if file does not exist, nil on error errmsg = -- error message if file information could not be read or file does not exist extos.lstat( filename -- path to a file on the file system )
-- implemented in extos.c as -- static int extos_stat(lua_State *L)
seconds = extos.monotonic_hires_time()
-- implemented in extos.c as -- static int extos_monotonic_hires_time(lua_State *L)
data_out, -- string containing stdout data, or nil in case of error data_err, -- string containing error or stderr data status = -- exit code, or negative code in case of abnormal termination extos.pfilter( data_in, -- string containing stdin data filename, -- executable arg1, -- first (non-zero) argument to executable arg2, -- second argument to executable ... )
-- implemented in extos.c as -- static int extos_pfilter(lua_State *L)
filestat_table, -- table with information on the file, false if file does not exist, nil on error errmsg = -- error message if file information could not be read or file does not exist extos.stat( filename -- path to a file on the file system )
-- implemented in extos.c as -- static int extos_stat(lua_State *L)
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 = -- a string format.file_path_element( value, -- part of an encoded file path, see encode.file_path_element(...) options -- options as supported by format.string(...) )
function format.file_path_element(value, options) local options = options or {} if value == nil then return options.nil_as or "" else return format.string( string.gsub( tostring(value), "%%(%x%x)", function (hexcode) return string.char(tonumber("0x" .. hexcode)) end ), options ) 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 truncate_mode = "codepoints", -- performe truncating by counting UTF-8 codepoints ("codepoints") or Unicode grapheme clusters ("graphmeclusters") -- (currently only "codepoints" are supported and this option may be omitted) truncate_at = truncate_at, -- truncate string after the given number of UTF-8 codepoints (or Unicode grapheme clusters) truncate_suffix = truncate_suffix, -- string to append, if string was truncated (use boolean true for Unicode ellipsis) truncate_count_suffix = truncate_count_suffix -- if true, then the total length (including suffix) may not exceed the given length } )
local function codepoint_count(str) return #string.gsub(str, '[\128-\255][\128-\191]?[\128-\191]?[\128-\191]?', 'x') end local function codepoint_truncate(str, length) local byte_pos = 1 local count = 0 while count < length do b1, b2, b3, b4 = string.byte(str, byte_pos, byte_pos+3) if not b2 then break end b3 = b3 or 0 b4 = b4 or 0 if b1 >= 128 and b2 >= 128 and b2 <= 191 then if b3 >= 128 and b3 <= 191 then if b4 >= 128 and b4 <= 191 then byte_pos = byte_pos + 4 count = count + 1 elseif count + 1 < length and b4 < 128 then byte_pos = byte_pos + 4 count = count + 2 else byte_pos = byte_pos + 3 count = count + 1 end elseif count + 1 < length and b3 < 128 then if count + 2 < length and b4 < 128 then byte_pos = byte_pos + 4 count = count + 3 else byte_pos = byte_pos + 3 count = count + 2 end else byte_pos = byte_pos + 2 count = count + 1 end elseif count + 1 < length and b2 < 128 then if count + 2 < length and b3 < 128 then if count + 3 < length and b4 < 128 then byte_pos = byte_pos + 4 count = count + 4 else byte_pos = byte_pos + 3 count = count + 3 end else byte_pos = byte_pos + 2 count = count + 2 end else byte_pos = byte_pos + 1 count = count + 1 end end return string.sub(str, 1, byte_pos-1) end function format.string(str, options) local options = options or {} if str == nil then return options.nil_as or "" elseif options.truncate_at then str = tostring(str) -- TODO: Unicode grapheme cluster boundary detetion is not implemented -- (Unicode codepoints are used instead) local truncate_suffix = options.truncate_suffix if truncate_suffix == true then truncate_suffix = '\226\128\166' elseif not truncate_suffix then truncate_suffix = '' end if options.truncate_count_suffix and truncate_suffix then local suffix_length = codepoint_count(truncate_suffix) if codepoint_count(str) > options.truncate_at then return ( codepoint_truncate(str, options.truncate_at - suffix_length) .. truncate_suffix ) else return str end else if codepoint_count(str) > options.truncate_at then return codepoint_truncate(str, options.truncate_at) .. truncate_suffix else return str end end 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 hide_seconds = hide_seconds -- set to TRUE to hide seconds } )
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 if am_pm and hour == 0 then 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 hide_seconds = hide_seconds -- set to TRUE to hide seconds } )
function format.timestamp(value, options) if value == nil then return options and options.nil_as or "" end return format.date(value, options) .. " " .. format.time(value, options) end
ary = -- a new JSON array json.array{ value1, -- first value to be set in JSON array value2, -- second value to be set in JSON array ... }
-- implemented in json.c as -- static int json_object(lua_State *L)
str = -- encoded JSON document as string json.export( document, -- JSON value (object, array, string, number, boolean, null), or a Lua table indentation -- optional indentation string for pretty printing, or true for two spaces )
-- implemented in json.c as -- static int json_export(lua_State *L)
value = -- value that has been read, or nil if path does not exist json.get( document, -- JSON value as returned by json.import(...), json.object{...}, etc., or a Lua table key1, -- first path element (e.g. a string key to descent into an object) key2, -- second path element (e.g. an integer key to descent into an array) ..., last_key -- last path element )
-- implemented in json.c as -- static int json_get(lua_State *L)
value, -- parsed value/document, or nil in case of error errmsg = -- error message if document could not be parsed json.import( str -- string to be parsed )
-- implemented in json.c as -- static int json_import(lua_State *L)
json.null
-- implemented as lightuserdata pointing to -- field "nullmark" of json_lightuserdata struct -- in json.c
obj = -- a new JSON object json.object{ key1 = value1, -- key value pair to be set in JSON object key2 = value2, -- another key value pair to be set in JSON object ... }
-- implemented as in json.c as -- static int json_object(lua_State *L)
document = -- first argument is returned for convenience json.set( document, -- JSON value as returned by json.import(...), json.object{...}, etc., or a Lua table value, -- new value to be set within the document key1, -- first path element (e.g. a string key to descent into an object) key2, -- second path element (e.g. an integer key to descent into an array) ... last_key -- last path element )
-- implemented in json.c as -- static int json_set(lua_State *L)
type_str = -- "object", "array", "string", "number", "boolean", "null", string "nil", or nil json.type( document, -- JSON value as returned by json.import(...), json.object{...}, etc., or a Lua table key1, -- first path element (e.g. a string key to descent into an object) key2, -- second path element (e.g. an integer key to descent into an array) ..., last_key -- last path element )
-- implemented in json.c as -- static int json_type(lua_State *L)
listen{ { proto = proto, -- "local", "tcp", "interval", or "main" path = path, -- path to unix domain socket if proto == "local" port = port, -- TCP port number host = host, -- "::" for all IPv6 interfaces, "0.0.0.0" for all IPv4 interfaces name = name, -- optional name for main handlers or interval handlers (may be useful for log output) handler = handler, -- handler if proto == "interval" or proto == "main" delay = delay, -- delay between invocations of interval handler strict = strict -- set to true to substract runtime of interval handler from delay }, { ... -- second listener }, ... -- more listeners -- the following options are all optional and have default values: pre_fork = pre_fork, -- desired number of spare (idle) processes min_fork = min_fork, -- minimum number of processes max_fork = max_fork, -- maximum number of processes (hard limit) fork_delay = fork_delay, -- delay (seconds) between creation of spare processes fork_error_delay = fork_error_delay, -- delay (seconds) before retry of failed process creation exit_delay = exit_delay, -- delay (seconds) between destruction of excessive spare processes idle_timeout = idle_timeout, -- idle time (seconds) after a fork gets terminated (0 for no timeout) memory_limit = memory_limit, -- maximum memory consumption (bytes) before process gets terminated min_requests_per_fork = min_requests_per_fork, -- minimum count of requests handled before fork is terminated max_requests_per_fork = max_requests_per_fork, -- maximum count of requests handled before fork is terminated http_options = { static_headers = static_headers, -- string or table of static headers to be returned with every request request_header_size_limit = request_header_size_limit, -- maximum size of request headers sent by client request_body_size_limit = request_body_size_limit, -- maximum size of request body sent by client idle_timeout = idle_timeout, -- maximum time until receiving the first byte of the request header stall_timeout = stall_timeout, -- maximum time a client connection may be stalled request_header_timeout = request_header_timeout, -- maximum time until receiving the remaining bytes of the request header response_timeout = response_timeout, -- time in which request body and response must be sent maximum_input_chunk_size = maximum_input_chunk_size, -- tweaks behavior of request-body parser minimum_output_chunk_size = minimum_output_chunk_size -- chunk size for chunked-transfer-encoding } }
-- prepare for interactive or listen mode if WEBMCP_MODE == "interactive" then function listen() -- overwrite Moonbridge's listen function -- ignore listen function calls for interactive mode end trace.disable() -- avoids memory leakage when scripts are running endlessly else local moonbridge_listen = listen local http = require("moonbridge_http") function listen(args) -- overwrite Moonbridge's listen function assert(args, "No argument passed to listen function") local min_requests_per_fork = args.min_requests_per_fork or 50 local max_requests_per_fork = args.max_requests_per_fork or 200 local main_handlers = {} local interval_handlers = {} for j, listener in ipairs(args) do if listener.proto == "main" then local name = listener.name or "Unnamed main thread #" .. #main_handlers+1 if main_handlers[name] ~= nil then error('Main thread handler with duplicate name "' .. name .. '"') end main_handlers[name] = listener.handler listener.name = name elseif listener.proto == "interval" then local name = listener.name or "Unnamed interval #" .. #interval_handlers+1 if interval_handlers[name] ~= nil then error('Interval handler with duplicate name "' .. name .. '"') end interval_handlers[name] = listener.handler listener.name = name end end local request_count = 0 local function inner_handler(http_request) request_count = request_count + 1 if request_count >= max_requests_per_fork then http_request:close_after_finish() end request.initialize() return request.handler(http_request) end local outer_handler = http.generate_handler(inner_handler, args.http_options) args.prepare = postfork_init args.connect = function(socket) if socket.main then request.initialize() main_handlers[socket.main](moonbridge_io.poll) io.stderr:write('Main handler "' .. socket.main .. '" terminated.\n') return false elseif socket.interval then request_count = request_count + 1 request.initialize() interval_handlers[socket.interval]() else local success = outer_handler(socket) if not success then return false end end return request_count < min_requests_per_fork end args.finish = execute.finalizers moonbridge_listen(args) end end
lua_func, -- compiled Lua function, nil if the file does not exist errmsg = -- error message (only for non-existing file, other errors are thrown) loadcached( filename -- path to a Lua source or byte-code file )
do local cache = {} function loadcached(filename) local cached_func = cache[filename] if cached_func then return cached_func end local stat, errmsg = extos.stat(filename) if stat == nil then error(errmsg) elseif stat == false then return nil, 'File "' .. filename .. '" does not exist' elseif stat.isdir then error('File "' .. filename .. '" is a directory') elseif not stat.isreg then error('File "' .. filename .. '" is not a regular file') end local func, compile_error = loadfile(filename, nil, protected_environment) if func then cache[filename] = func return func end error(compile_error, 0) end 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 = table.new(locale._current_data) 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 locale.set(locale_options) block() locale._current_data = 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) request.configure(function() 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) end
mondelefant.attach( mode, -- attachment type: "11" one to one, "1m" one to many, "m1" many to one data1, -- first database result list or object data2, -- second database result list or object key1, -- field name(s) in first result list or object used for attaching key2, -- field name(s) in second result list or object used for attaching ref1, -- name of reference field to be set in first database result list or object ref2 -- name of reference field to be set in second database result list or object )
function attach(mode, data1, data2, key1, key2, ref1, ref2) local many1, many2 if mode == "11" then many1 = false many2 = false elseif mode == "1m" then many1 = false many2 = true elseif mode == "m1" then many1 = true many2 = false elseif mode == "mm" then many1 = true many2 = true else error("Unknown mode specified for 'mondelefant.attach'.") end local list1, list2 if data1._type == "object" then list1 = { data1 } elseif data1._type == "list" then list1 = data1 else error("First result data given to 'mondelefant.attach' is invalid.") end if data2._type == "object" then list2 = { data2 } elseif data2._type == "list" then list2 = data2 else error("Second result data given to 'mondelefant.attach' is invalid.") end local hash1 = {} local hash2 = {} if ref2 then for i, row in ipairs(list1) do local key = attach_key(row, key1) local list = hash1[key] if not list then list = {}; hash1[key] = list end list[#list + 1] = row end end if ref1 then for i, row in ipairs(list2) do local key = attach_key(row, key2) local list = hash2[key] if not list then list = {}; hash2[key] = list end list[#list + 1] = row end for i, row in ipairs(list1) do local key = attach_key(row, key1) local matching_rows = hash2[key] if many2 then local list = data2._connection:create_list(matching_rows) list._class = data2._class row._ref[ref1] = list elseif matching_rows and #matching_rows == 1 then row._ref[ref1] = matching_rows[1] else row._ref[ref1] = false end end end if ref2 then for i, row in ipairs(list2) do local key = attach_key(row, key2) local matching_rows = hash1[key] if many1 then local list = data1._connection:create_list(matching_rows) list._class = data1._class row._ref[ref2] = list elseif matching_rows and #matching_rows == 1 then row._ref[ref2] = matching_rows[1] else row._ref[ref2] = false end end end end
db_handle, -- database handle, or nil in case of error errmsg, -- error message db_error = -- error object mondelefant.connect{ conninfo = conninfo, -- string passed directly to PostgreSQL's libpq host = host, -- hostname or directory with leading slash where Unix-domain socket resides hostaddr = hostaddr, -- IPv4, or IPv6 address if supported port = port, -- TCP port or socket file name extension dbname = dbname, -- name of database to connect with user = user, -- login name password = password, -- password connect_timeout = connect_timeout, -- seconds to wait for connection to be established. Zero or nil means infinite ... }
-- implemented in mondelefant_native.c as -- static int mondelefant_connect(lua_State *L)
db_class = -- new database class (model)
mondelefant.new_class()
-- implemented in mondelefant_native.c as -- static int mondelefant_new_class(lua_State *L)
db_list_or_object = -- first argument is returned mondelefant.set_class( db_list_or_object, -- database result list or object db_class -- database class (model) )
-- implemented in mondelefant_native.c as -- static int mondelefant_set_class(lua_State *L)
hash = -- SHA3-224 digest (in hex notation) of input string moonhash.sha3_224( data -- input string )
-- Implemented in moonhash.c and moonhash_sha3.c
hash = -- SHA3-256 digest (in hex notation) of input string moonhash.sha3_256( data -- input string )
-- Implemented in moonhash.c and moonhash_sha3.c
hash = -- SHA3-384 digest (in hex notation) of input string moonhash.sha3_384( data -- input string )
-- Implemented in moonhash.c and moonhash_sha3.c
hash = -- SHA3-512 digest (in hex notation) of input string moonhash.sha3_512( data -- input string )
-- Implemented in moonhash.c and moonhash_sha3.c
hash = -- SHAKE128 digest of input string moonhash.shake128( input_data, -- input string output_length, -- number of characters (not bytes or bits) in output, defaults to 32 output_alphabet -- characters for encoding, defaults to "0123456789abcdef" for hex encoding )
-- Implemented in moonhash.c and moonhash_sha3.c
hash = -- SHAKE256 digest of input string moonhash.shake256( input_data, -- input string output_length, -- number of characters (not bytes or bits) in output, defaults to 64 output_alphabet -- characters for encoding, defaults to "0123456789abcdef" for hex encoding )
-- Implemented in moonhash.c and moonhash_sha3.c
net.configure_mail{ command = command, -- table with binary name and static command line arguments envelope_from_option = envelope_from_option -- command line option to select "envelope from", e.g. "-f" }
function net.configure_mail(args) local mail_config = { command = table.new(args.command), envelope_from_option = envelope_from_option } request.configure(function() net._mail_config = mail_config end) end
success = -- true, if mail has been sent successfully, otherwise false 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 = table.new(net._mail_config.command) if envelope_from and net._mail_config.envelope_from_option and string.find(envelope_from, "^[0-9A-Za-z%.-_@0-9A-Za-z%.-_]+$") then command[#command+1] = net._mail_config.envelope_from_option command[#command+1] = envelope_from end local stdout, errmsg, status = extos.pfilter(mail, table.unpack(command)) if not status then error("Error while calling sendmail: " .. errmsg) end if status == 0 then return true else return false end 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 = request.get_param{ name = key } local format_info = request.get_param{ name = 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
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 local str = request.get_id_string() if str then return param._get_parser(nil, param_type)(str) else return nil end end 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 = request.get_param{ name = key .. "__format" } local parser = param._get_parser(format_info, param_type) local raw_values = request.get_param{ name = key .. "[]", multiple = true } 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( 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
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
request.add_error_handler( function(errobj, stacktrace) ... end )
function request.add_error_handler(func) request.configure(function() local handlers = request._error_handlers handlers[#handlers+1] = func end) end
request.add_header( key, -- name of header, e.g. "Date" value -- value, e.g. "Mon, 1 Jan 2001 01:00:00 GMT" )
function request.add_header(key, value) if value == nil then error("Function request.add_header(...) requires two arguments") end request.configure(function() local headers = request._response_headers headers[#headers+1] = {key, value} local lower_key = string.lower(key) if lower_key == "cache-control" then request._cache_manual = true end end) end
request.allow_caching()
function request.allow_caching() request.configure(function() request._cache = true end) end
request.configure(
func -- function which is performing the configuration
)
function request.configure(func) if not request._in_progress then local initializers = request._initializers initializers[#initializers+1] = func end func() end
route =
request.default_router(
path -- URL path without leading slash
)
function request.default_router(path)
if not path then
return nil
end
if path == "" then
return {module = "index", view = "index"}
end
local static = string.match(path, "^static/([-./0-9A-Z_a-z]*)$")
if static then
-- Note: sanitizer is in request.handler(...)
return {static = static}
end
local module, action, view, id, suffix
module = string.match(path, "^([^/]+)/$")
if module then
return {module = module, view = "index"}
end
module, action = string.match(path, "^([^/]+)/([^/.]+)$")
if module then
return {module = module, action = action}
end
module, view, suffix = string.match(path, "^([^/]+)/([^/.]+)%.([^/]+)$")
if module then
return {module = module, view = view, suffix = suffix}
end
module, view, id, suffix = string.match(path, "^([^/]+)/([^/]+)/([^/.]+)%.([^/]+)$")
if module then
return {module = module, view = view, id = id, suffix = suffix}
end
return nil
end
request.for_each(
func -- function to be called on every request
)
function request.for_each(func) local initializers = request._initializers initializers[#initializers+1] = func func() end
request.force_absolute_baseurl()
function request.force_absolute_baseurl() request.configure(function() request._force_absolute_baseurl = true end) 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
baseurl = request.get_absolute_baseurl()
function request.get_absolute_baseurl() if request._absolute_baseurl then return request._absolute_baseurl else return request._relative_baseurl end end
action_name = request.get_action()
function request.get_action() if request._forward_processed then return nil else return request._route.action end end
value = -- cookie value, or nil if not set request.get_cookie{ name = name -- name of cookie }
function request.get_cookie(args) return request._http_request.cookies[args.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
value = -- value of header as string containing comma (and space) separated values request.get_header( key -- name of header, e.g. "Authorization" )
function request.get_header(key) local http_request = request._http_request local values = http_request.headers[key] if #values == 0 then return nil elseif #values == 1 then return values[1] else return http_request.headers_csv_string[key] end end
id_string = request.get_id_string()
function request.get_id_string() return request._route.id end
module_name = request.get_module()
function request.get_module() if request._forward_processed then return request._forward.module or request._route.module or 'index' else return request._route.module or 'index' end end
params = request.get_param_strings{ method = method, -- "GET", "POST", or nil to query both (POST has precedence) include_internal = include_internal -- set to true to include also parameters starting with "_webmcp_" prefix }
local function merge_params(tbl, params_list, include)
for key, values in pairs(params_list) do
if not include and string.match(key, "^_webmcp_") then
-- do nothing
elseif string.match(key, "%[%]$") then
tbl[key] = table.new(values)
else
tbl[key] = values[1]
end
end
end
function request.get_param_strings(args)
local method = nil
local include = false
if args then
method = args.method
include = args.include_internal
end
local t = {}
if not method then
merge_params(t, request._http_request.get_params_list, include)
merge_params(t, request._http_request.post_params_list, include)
elseif method == "GET" then
merge_params(t, request._http_request.get_params_list, include)
elseif method == "POST" then
merge_params(t, request._http_request.post_params_list, include)
else
error("Invalid method passed to request.get_param_strings{...}")
end
return t
end
value = -- value of GET/POST parameter, or value list if multiple == true request.get_param{ method = method, -- "GET", "POST", or nil to query both (POST has precedence) name = name, -- field name index = index, -- defaults to 1 to get first occurrence, only applicable if multiple == false multiple = multiple, -- boolean to indicate whether to return a single value or a value list meta = meta -- set to true to get metadata (table with "file_name" and "content_type") }
function request.get_param(args) local param_list if args.metadata then if args.method == "GET" then error("HTTP GET parameters do not have metadata") elseif args.method == "POST" or not args.method then param_list = request._http_request.post_metadata_list[args.name] else error("Unknown HTTP method selected") end else if args.method == "GET" then param_list = request._http_request.get_params_list[args.name] elseif args.method == "POST" then param_list = request._http_request.post_params_list[args.name] elseif not args.method then param_list = request._http_request.post_params_list[args.name] if not param_list[args.index or 1] then param_list = request._http_request.get_params_list[args.name] end else error("Unknown HTTP method selected") end end if args.multiple then return param_list else return param_list[args.index or 1] end end
path = request.get_path()
function request.get_path() return request._http_request.path 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
view_name = request.get_view()
function request.get_view() if request._forward_processed then return request._forward.view or 'index' else if request._route.view then local suffix = request._route.suffix or "html" if suffix == "html" then return request._route.view else return request._route.view .. "." .. suffix end elseif not request._route.action then return 'index' else return nil end end end
success = -- false if an error occurred, true otherwise request.handler( http_request -- HTTP request object )
function request.handler(http_request) request._http_request = http_request local path = http_request.path if path then local relative_baseurl_elements = {} for match in string.gmatch(path, "/") do relative_baseurl_elements[#relative_baseurl_elements+1] = "../" end if #relative_baseurl_elements > 0 then request._relative_baseurl = table.concat(relative_baseurl_elements) else request._relative_baseurl = "./" end else request._relative_baseurl = nil end local success, error_info = xpcall( function() local function require_method(errmsg, ...) for i = 1, select("#", ...) do if http_request.method == select(i, ...) then return end end request.set_status("405 Method Not Allowed") request.add_header("Allow", table.concat({...}, ", ")) error(errmsg) end request._route = request.router() do local post_id = http_request.post_params["_webmcp_id"] if post_id then request._route.id = post_id end end local options_handler = loadcached(encode.file_path( WEBMCP_BASE_PATH, 'app', WEBMCP_APP_NAME, 'http_options.lua' )) if options_handler then options_handler() end if http_request.method == "OPTIONS" then return end if not request._route then request._route = {} if request.get_404_route() then request.set_status("404 Not Found") request.forward(request.get_404_route()) else error("Could not route request URL") end end if request._route.static then local subpath = request._route.static local errmsg for element in string.gmatch(subpath, "[^/]+") do if element == "." or element == ".." then errmsg = "Illegal path" break end end local fstat, f if not errmsg then local filename = WEBMCP_BASE_PATH .. "static/" .. subpath fstat, errmsg = extos.stat(filename) if fstat then if fstat.isdir then errmsg = "Is a directory" elseif not fstat.isreg then errmsg = "Not a regular file" else f, errmsg = io.open(filename, "r") end end end if not f then if request.get_404_route() then request.set_status("404 Not Found") request.forward(request.get_404_route()) else error('Could not open static file "' .. subpath .. '": ' .. errmsg) end else require_method( "Invalid HTTP method for static file", "HEAD", "GET", "POST" ) local d = assert(f:read("*a")) f:close() slot.put_into("data", d) local filename_extension = string.match(subpath, "%.([^.]+)$") slot.set_layout(nil, request._mime_types[filename_extension] or "application/octet-stream") request.allow_caching() return end end -- restore slots if coming from http redirect do local tempstore_value = http_request.get_params["_tempstore"] if tempstore_value then trace.restore_slots{} local blob = tempstore.pop(tempstore_value) if blob then slot.restore_all(blob) end end end if request.get_action() then trace.request{ module = request.get_module(), action = request.get_action() } if not execute.action{ module = request.get_module(), action = request.get_action(), test_existence = true } and request.get_404_route() then request.set_status("404 Not Found") request.forward(request.get_404_route()) else require_method( "Invalid HTTP method for action (POST request required)", "POST" ) local action_status = execute.filtered_action{ module = request.get_module(), action = request.get_action(), } if not (request.is_rerouted() or slot.layout_is_set()) then local routing_mode, routing_module, routing_view, routing_anchor routing_mode = http_request.post_params["_webmcp_routing." .. action_status .. ".mode"] routing_module = http_request.post_params["_webmcp_routing." .. action_status .. ".module"] routing_view = http_request.post_params["_webmcp_routing." .. action_status .. ".view"] routing_anchor = http_request.post_params["_webmcp_routing." .. action_status .. ".anchor"] if not (routing_mode or routing_module or routing_view) then action_status = "default" routing_mode = http_request.post_params["_webmcp_routing.default.mode"] routing_module = http_request.post_params["_webmcp_routing.default.module"] routing_view = http_request.post_params["_webmcp_routing.default.view"] routing_anchor = http_request.post_params["_webmcp_routing.default.anchor"] end assert(routing_module, "Routing information has no module.") assert(routing_view, "Routing information has no view.") if routing_mode == "redirect" then local routing_params = {} for key, value in pairs(request.get_param_strings{ method="POST", include_internal=true }) do local status, stripped_key = string.match( key, "^_webmcp_routing%.([^%.]*)%.params%.(.*)$" ) if status == action_status then routing_params[stripped_key] = value end end request.redirect{ module = routing_module, view = routing_view, id = http_request.post_params["_webmcp_routing." .. action_status .. ".id"], params = routing_params, anchor = routing_anchor } elseif routing_mode == "forward" then request.forward{ module = routing_module, view = routing_view } else error("Missing or unknown routing mode in request parameters.") end end end else -- no action trace.request{ module = request.get_module(), view = request.get_view() } if not execute.view{ module = request.get_module(), view = request.get_view(), test_existence = true } and request.get_404_route() then request.set_status("404 Not Found") request.forward(request.get_404_route()) end end if not (request.get_redirect_data() or slot.layout_is_set()) then request.process_forward() local view = request.get_view() if string.find(view, "^_") then error("Tried to call a private view (prefixed with underscore).") end require_method( "Invalid HTTP method", "HEAD", "GET", "POST" ) execute.filtered_view{ module = request.get_module(), view = view, } end end, function(errobj) return { errobj = errobj, stacktrace = string.gsub( debug.traceback('', 2), "^\r?\n?stack traceback:\r?\n?", "" ) } end ) if not success then trace.error{} end slot.select('trace', trace.render) -- render trace information local redirect_data = request.get_redirect_data() -- log error and switch to error layout, unless success if not success then local errobj = error_info.errobj local stacktrace = error_info.stacktrace if not request._status then request._status = "500 Internal Server Error" end http_request:close_after_finish() slot.set_layout('system_error') slot.select('system_error', function() if getmetatable(errobj) == mondelefant.errorobject_metatable then slot.put( "<p>Database error of class <b>", encode.html(errobj.code), "</b> occured:<br/><b>", encode.html(errobj.message), "</b></p>" ) else slot.put("<p><b>", encode.html(tostring(errobj)), "</b></p>") end slot.put("<p>Stack trace follows:<br/>") slot.put(encode.html_newlines(encode.html(stacktrace))) slot.put("</p>") end) for i, error_handler in ipairs(request._error_handlers) do error_handler(error_info.errobj, error_info.stacktrace) end elseif redirect_data then if redirect_data.include_tempstore == true or ( redirect_data.include_tempstore ~= false and not redirect_data.external ) then redirect_data = table.new(redirect_data) redirect_data.params = table.new(redirect_data.params) local slot_dump = slot.dump_all() if slot_dump ~= "" then redirect_data.params._tempstore = tempstore.save(slot_dump) end end http_request:send_status("303 See Other") for i, header in ipairs(request._response_headers) do http_request:send_header(header[1], header[2]) end http_request:send_header("Location", encode.url(redirect_data)) http_request:finish() end if not success or not redirect_data then http_request:send_status(request._status or "200 OK") for i, header in ipairs(request._response_headers) do http_request:send_header(header[1], header[2]) end if not request._cache_manual then local cache_time = request._cache_time if request._cache and cache_time and cache_time > 0 then http_request:send_header("Cache-Control", "max-age=" .. cache_time) else http_request:send_header("Cache-Control", "no-cache") end end http_request:send_header("Content-Type", slot.get_content_type()) http_request:send_data(slot.render_layout()) http_request:finish() end return success end
request.initialize()
function request.initialize() _G.app = {} -- may be filled and modified by request initializers do request._in_progress = true -- NOTE: must be set to true before initializer functions are called for i, func in ipairs(request._initializers) do func() end end end
bool = -- true, if a request is currently in progress (i.e. being answered)
request.is_in_progress()
function request.is_in_progress() return request._in_progress 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 request._http_request.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{ 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 anchor = anchor, -- anchor in URL include_tempstore = include_tempstore -- set to true to include slot data via _tempstore param (defaults to true unless external is set) }
function request.redirect(args)
args = table.new(args)
if type(args.external) ~= "string" and type(args.static) ~= "string" then
if type(args.module) ~= "string" then
error("No module string passed to request.redirect{...}.")
end
if type(args.view) ~= "string" then
error("No view string passed to request.redirect{...}.")
end
if args.params ~= nil and type(args.params) ~= "table" then
error("Params array passed to request.redirect{...} is not a table.")
end
if args.anchor ~= nil and type(args.anchor) ~= "string" then
error("Anchor passed to request.redirect{...} must be a string or nil.")
end
end
if request.is_rerouted() then
error("Tried to redirect after another forward or redirect.")
end
request._redirect = args
if args.module and args.view then -- TODO: support for external redirects
trace.redirect{ module = args.module, view = args.view }
end
end
request.register_mime_type( filename_extension, mime_type )
function request.register_mime_type(filename_extension, mime_type) request.configure(function() request._mime_types[filename_extension] = mime_type end) end
route = request.router()
function request.router() return (request.default_router(request.get_path())) end
request.set_404_route{ module = module, -- module name view = view -- view name }
function request.set_404_route(tbl)
request.configure(function()
request._404_route = tbl -- TODO: clone?
end)
end
request.set_absolute_baseurl(
url -- Base URL of the application
)
function request.set_absolute_baseurl(url) request.configure(function() if string.find(url, "/$") then request._absolute_baseurl = url else request._absolute_baseurl = url .. "/" end end) end
request.set_cache_time(
seconds -- duration in seconds
)
function request.set_cache_time(seconds) request.configure(function() request._cache_time = seconds end) end
request.set_cookie{ name = name, -- name of cookie value = value, -- value of cookie domain = domain, -- optional domain domain where cookie is transmitted path = path, -- optional path where cookie is transmitted, defaults to application base secure = secure -- optional boolean, indicating if cookie should only be transmitted over HTTPS }
function request.set_cookie(args) local args = table.new(args) if not args.path then args.path = string.match( request.get_absolute_baseurl(), "://[^/]*(.*)" ) if args.path == nil then args.path = "/" end end if args.secure == nil then if string.find( string.lower(request.get_absolute_baseurl()), "^https://" ) then args.secure = true else args.secure = false end end assert(string.find(args.name, "^[0-9A-Za-z%%._~-]+$"), "Illegal cookie name") assert(string.find(args.value, "^[0-9A-Za-z%%._~-]+$"), "Illegal cookie value") local parts = {args.name .. "=" .. args.value} if args.domain then assert( string.find(args.path, "^[0-9A-Za-z%%/._~-]+$"), "Illegal cookie domain" ) parts[#parts+1] = "domain=" .. args.domain end if args.path then assert( string.find(args.path, "^[0-9A-Za-z%%/._~-]+$"), "Illegal cookie path" ) parts[#parts+1] = "path=" .. args.path end if args.secure then parts[#parts+1] = "secure" end request.add_header("Set-Cookie", table.concat(parts, "; ")) end
request.set_csrf_secret(
secret -- secret random string
)
function request.set_csrf_secret(secret) if request.get_action() and request._http_request.post_params["_webmcp_csrf_secret"] ~= secret then error("Cross-Site Request Forgery attempt detected"); end request._csrf_secret = secret end
request.set_perm_param( name, -- name of parameter value -- value of parameter )
function request.set_perm_param(name, value) request.configure(function() request._perm_params[name] = value end) 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
bool = -- set to true if layout has been set explicitly during processing request
slot.layout_is_set()
function slot.layout_is_set() return slot._layout_set 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
slot.put_into( 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, select("#", ...) do t[#t + 1] = select(i, ...) end end
output = -- document/data to be sent to the web browser slot.render_layout( layout_ident -- if set, selects layout to be used; otherwise layout set by slot.set_layout(...) is used )
function slot.render_layout(layout_ident) local layout_ident = layout_ident or slot._current_layout if layout_ident then local layout_file = assert(io.open( encode.file_path( WEBMCP_BASE_PATH, 'app', WEBMCP_APP_NAME, '_layout', layout_ident .. '.html' ), 'r' )) local layout = assert(layout_file:read("*a")) assert(layout_file:close()) -- 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{
except = except -- Reset all slots, except slots named in this list
}
local data_metatable = {} function data_metatable:__index(key) self[key] = { string_fragments = {}, state_table = {} } return self[key] end function slot.reset_all(args) local saved if args and args.except then saved = {} for i, key in ipairs(args.except) do saved[key] = slot._data[key] end end slot._data = setmetatable({}, data_metatable) if saved then for key, value in pairs(saved) do slot._data[key] = value end end 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) request.configure(function() slot._current_layout = layout_ident slot._content_type = content_type end) if request.is_in_progress() then slot._layout_set = true end 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
table.insert( t, -- table index, -- optional index value -- value )
if _VERSION == "Lua 5.2" then local old_insert = table.insert function table.insert(...) if select("#", ...) == 2 then local t, value = ... t[#t+1] = value return end return old_insert(...) end 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) local 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( WEBMCP_BASE_PATH, '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( WEBMCP_BASE_PATH, 'tmp', "tempstore-" .. key .. ".tmp" ) local file = assert(io.open(filename, "w")) file:write(blob) io.close(file) return key end
trace.debug( value1, -- value to be converted to a string and included in the debug output value2, -- another value to be converted to a string and included in the debug output ... )
function trace.debug(...) if not trace._disabled then local values = {} for i = 1, select("#", ...) do values[i] = tostring((select(i, ...))) end trace._new_entry{ type = "debug", message = table.concat(values, " ") } end end
trace.debug_table(
message -- message to be inserted into the trace log
)
function trace.debug_table(table) trace._new_entry{ type = "debug_table", message = table } end
trace.debug_trace(
message -- optional message to add
)
function trace.debug_trace(message) trace._new_entry{ type = "traceback", message = tostring(debug.traceback(message or "", 2)) } end
trace.disable()
function trace.disable() trace._disabled = true end
trace.enter_action{ module = module, action = action }
function trace.enter_action(args) if not trace._disabled then 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 end
trace.enter_config{ name = name }
function trace.enter_config(args) if not trace._disabled then 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 end
trace.enter_filter{ path = path }
function trace.enter_filter(args) if not trace._disabled then 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 end
trace.enter_view{ module = module, view = view }
function trace.enter_view(args) if not trace._disabled then 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 end
trace.error{ }
function trace.error(args) if not trace._disabled then trace._new_entry { type = "error" } while #trace._stack > 1 do trace._close_section() end end end
trace.execution_return{
status = status -- optional status
}
function trace.execution_return(args) if not trace._disabled then 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 end
trace.forward{ module = module, view = view }
function trace.forward(args) if not trace._disabled then 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 end
disabled = -- boolean indicating if trace system is disabled
trace.is_disabled()
function trace.is_disabled() return trace._disabled end
trace.redirect{ module = module, view = view }
function trace.redirect(args) if not trace._disabled then 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 end
trace.render()
function trace.render()
if not trace._disabled then
-- TODO: check if all sections are closed?
trace._render_sub_tree(trace._tree)
end
end
trace.request{ module = module, view = view, action = action }
function trace.request(args) if not trace._disabled then 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 end
trace.restore_slots{ }
function trace.restore_slots(args) if not trace._disabled then trace._new_entry{ type = "restore_slots" } end end
trace.sql{ command = command, -- executed SQL command as string execution_time = execution_time, -- execution time of the statement in seconds error_position = error_position -- optional position in bytes where an error occurred }
function trace.sql(args) if not trace._disabled then local command = args.command local execution_time = args.execution_time local error_position = args.error_position if type(command) ~= "string" then error("No command string passed to trace.sql{...}.") end if type(execution_time) ~= "number" then error("No execution time number 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, execution_time = execution_time, error_position = error_position } for i, entry in ipairs(trace._stack) do entry.db_time = entry.db_time + execution_time end end end
ui.anchor{ name = name, -- name of anchor attr = attr, -- table of HTML attributes content = content -- string to be HTML encoded, or function to be executed }
function ui.anchor(args) local attr = table.new(args.attr) attr.name = args.name return ui.tag{ tag = "a", attr = attr, content = args.content } 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 legend 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.file{
... -- generic ui.field.* arguments, as described for ui.autofield{...}
}
function ui.field.file(args)
ui.form_element(args, nil, function(args)
if args.readonly then
-- nothing
else
if not slot.get_state_table().form_file_upload then
error('Parameter "file_upload" of ui.form{...} must be set to true to allow file uploads.')
end
local attr = table.new(args.attr)
attr.type = "file"
attr.name = args.html_name
ui.tag{ tag = "input", attr = attr }
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 selected_record = selected_record -- id of (or reference to) record which is selected (optional, overrides "value" argument when not nil) disabled_records = disabled_records -- table with ids of (or references to) records that should be disabled (stored as table keys mapped to true) }
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 local one_selected = false for idx, record in ipairs(foreign_records) do local key = record[args.foreign_id] local selected = false if not one_selected then if args.selected_record == nil then if args.value == key then selected = true end else if args.selected_record == record or args.selected_record == key then selected = true end end one_selected = selected end local disabled = false if args.disabled_records then if args.disabled_records[record] or args.disabled_records[key] then disabled = true end end ui.tag{ tag = "option", attr = { value = key, disabled = disabled and "disabled" or nil, selected = selected 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.filters{ selector = selector, -- selector to be modified label = label, -- text to be displayed when filters are collapsed { name = name1, -- name of first filter (used as GET param) label = label1, -- label of first filter { name = name1a, -- name of first option of first filter label = label1a, -- label of first option of first filter selector_modifier = function(selector) ... end }, { name = name1b, -- name of second option of first filter label = label1b, -- label of second option of first filter selector_modifier = function(selector) ... end }, ... }, { name = name2, -- name of second filter (used as GET param) label = label2, -- label of second filter { ... }, { ... }, ... }, ... content = function() ... -- inner code where filter is to be applied end }
function ui.filters(args) local el_id = ui.create_unique_id() ui.container{ attr = { class = "ui_filter" }, content = function() ui.container{ attr = { class = "ui_filter_closed_head" }, content = function() ui.tag{ tag = "span", content = function() local current_options = {} for idx, filter in ipairs(args) do local filter_name = filter.name or "filter" local current_option = atom.string:load(request.get_param{name=filter_name}) if not current_option then current_option = param.get(filter_name) end if not current_option or #current_option == 0 then current_option = filter[1].name end for idx, option in ipairs(filter) do if current_option == option.name then current_options[#current_options+1] = encode.html(filter.label) .. ": " .. encode.html(option.label) end end end slot.put(table.concat(current_options, "; ")) end } slot.put(" (") ui.link{ attr = { onclick = "this.parentNode.style.display='none'; document.getElementById('" .. el_id .. "_head').style.display='block'; return(false);" }, text = args.label, external = "#" } slot.put(")") end } ui.container{ attr = { id = el_id .. "_head", style = "display: none;" }, content = function() for idx, filter in ipairs(args) do local filter_name = filter.name or "filter" local current_option = atom.string:load(request.get_param{name=filter_name}) if not current_option then current_option = param.get(filter_name) end if not current_option or #current_option == 0 then current_option = filter[1].name end local id = request.get_id_string() local params = request.get_param_strings() ui.container{ attr = { class = "ui_filter_head" }, content = function() slot.put(filter.label or "Filter", ": ") for idx, option in ipairs(filter) do params[filter_name] = option.name local attr = {} if current_option == option.name then attr.class = "active" option.selector_modifier(args.selector) end ui.link{ attr = attr, module = request.get_module(), view = request.get_view(), id = id, params = params, text = option.label } end end } end end } end } ui.container{ attr = { class = "ui_filter_content" }, content = function() args.content() 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) file_upload = file_upload, -- must be set to true, if form contains file upload element 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 anchor = anchor -- optional anchor for URL }, 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 }
local function prepare_routing_params(params, routing, default_module) local routing_default_given = false if routing then for status, settings in pairs(routing) do if status == "default" then routing_default_given = true end local module = settings.module or default_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 params["_webmcp_routing." .. status .. ".anchor"] = settings.anchor 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 return params 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 local old_file_upload = slot_state.form_file_upload 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) prepare_routing_params(params, args.routing, args.module) params._webmcp_csrf_secret = request.get_csrf_secret() local attr = table.new(args.attr) if attr.enctype=="multipart/form-data" or args.file_upload then slot_state.form_file_upload = true if attr.enctype == nil then attr.enctype = "multipart/form-data" end end 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_file_upload = old_file_upload 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 attr = attr, -- for views: table of HTML attributes a_attr = a_attr, -- for actions: table of HTML attributes for the "a" tag form_attr = form_attr, -- for actions: table of HTML attributes for the "form" tag 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{...} anchor = anchor, -- for views: anchor in destination URL text = text, -- link text content = content -- link content (overrides link text, except for submit buttons for action calls without JavaScript) }
function ui.link(args)
local args = args or {}
local content = args.content or args.text
assert(content, "ui.link{...} needs a text.")
local function wrapped_content()
if args.image then
ui.image(args.image)
end
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() }; return false;"
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,
anchor = args.anchor
}
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 -- alternative function to output field data per record -- (ignoring name, format, etc. when set) -- (receives record as first and integer index as second argument) }, { ... }, ... } }
-- 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, field_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, record_idx) 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 container2_attr = container2_attr, -- extra HTML attributes for the container (div) of the real element (in checkbox case only) 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 = args.container2_attr or { class = "ui_checkbox_div" }, label_for = attr.id, label_attr = args.label_attr or { 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 (will be modified) anchor = anchor, -- optional name of anchor in document to jump to per_page = per_page, -- items per page, defaults to 10 container_attr = container_attr, -- html attr for the container element 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 = 1 if count > 0 then page_count = math.floor((count - 1) / per_page) + 1 end local current_page = atom.integer:load(request.get_param{name=name}) or 1 if current_page > page_count then current_page = page_count end selector:limit(per_page) selector:offset((current_page - 1) * per_page) local id = request.get_id_string() local params = request.get_param_strings() 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, anchor = args.anchor, text = tostring(page) } end end end ui.container{ attr = args.container_attr or { 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 }
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 args.external then attr.src = encode.url{ external = args.external } elseif args.static then attr.src = encode.url{ static = args.static } end if noscript then ui.tag{ tag = "noscript", attr = attr, content = noscript } end if attr.src then ui.tag{ tag = "script", attr = attr, content = "" } elseif script then local script_string if type(script) == "function" then script_string = slot.use_temporary(script) else script_string = script end if string.find(script_string, "]]>") then error('Script contains character sequence "]]>" and is thus rejected to avoid ambiguity. If this sequence occurs as part of program code, please add additional space characters. If this sequence occurs inside a string literal, please encode one of this characters using the \\uNNNN unicode escape sequence.') end ui.tag{ tag = "script", attr = attr, content = function() slot.put("/* <![CDATA[ */") slot.put(script_string) slot.put("/* ]]> */") end } 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(args.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-2019 Public Software Group e. V., Berlin