# HG changeset patch # User jbe/bsw # Date 1256468400 -3600 # Node ID 9fdfb27f8e676922467f3f14af94dd05427bf60c Version 1.0.0 diff -r 000000000000 -r 9fdfb27f8e67 LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,19 @@ +Copyright (c) 2009 Public Software Group e. V., Berlin + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff -r 000000000000 -r 9fdfb27f8e67 Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,53 @@ +include Makefile.options + +all:: + make documentation + make accelerator + make libraries + make symlinks + make precompile + +documentation:: + rm -f doc/autodoc.tmp + lua framework/bin/autodoc.lua framework/cgi-bin/ framework/env/ libraries/ > doc/autodoc.tmp + cat doc/autodoc-header.htmlpart doc/autodoc.tmp doc/autodoc-footer.htmlpart > doc/autodoc.html + rm -f doc/autodoc.tmp + +accelerator:: + cd framework/accelerator; make + +libraries:: + cd libraries/extos; make + cd libraries/mondelefant; make + cd libraries/multirand; make + +symlinks:: + ln -s -f ../../libraries/atom/atom.lua framework/lib/ + ln -s -f ../../libraries/extos/extos.so framework/lib/ + ln -s -f ../../libraries/mondelefant/mondelefant.lua framework/lib/ + ln -s -f ../../libraries/mondelefant/mondelefant_native.so framework/lib/ + ln -s -f ../../libraries/mondelefant/mondelefant_atom_connector.lua framework/lib/ + ln -s -f ../../libraries/multirand/multirand.so framework/lib/ + ln -s -f ../../libraries/rocketcgi/rocketcgi.lua framework/lib/ + ln -s -f ../../libraries/nihil/nihil.lua framework/lib/ + ln -s -f ../../libraries/luatex/luatex.lua framework/lib/ + +precompile:: + rm -Rf framework.precompiled + rm -Rf demo-app.precompiled + sh framework/bin/recursive-luac framework/ framework.precompiled/ + rm -f framework.precompiled/accelerator/Makefile + rm -f framework.precompiled/accelerator/webmcp_accelerator.c + rm -f framework.precompiled/accelerator/webmcp_accelerator.o + framework/bin/recursive-luac demo-app/ demo-app.precompiled/ + +clean:: + rm -f doc/autodoc.tmp doc/autodoc.html + rm -Rf framework.precompiled + rm -Rf demo-app.precompiled + rm -f demo-app/tmp/* + rm -f framework/lib/* + cd libraries/extos; make clean + cd libraries/mondelefant; make clean + cd libraries/multirand; make clean + cd framework/accelerator; make clean diff -r 000000000000 -r 9fdfb27f8e67 Makefile.options --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile.options Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,20 @@ +# C compiler command +CC = cc + +# linker command +LD = ld + +# filename extension for shared libraries +SLIB_EXT = so + +# C compiler flags +CFLAGS = -O2 -Wall -I /usr/include -I /usr/local/include -I /usr/local/include/lua51 -I /usr/include/lua5.1 + +# additional C compiler flags for parts which depend on PostgreSQL +CFLAGS_PGSQL = -I /usr/include/postgresql -I /usr/include/postgresql/server -I /usr/local/include/postgresql -I /usr/local/include/postgresql/server + +# linker flags +LDFLAGS = -shared -L /usr/lib -L /usr/local/lib -L /usr/local/lib/lua51 -L /usr/lib/lua5.1 + +# additional linker flags for parts which depend on PostgreSQL +LDFLAGS_PGSQL = diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/_filter/20_session.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/_filter/20_session.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,19 @@ +if cgi.cookies.session then + app.session = Session:by_ident(cgi.cookies.session) +end +if not app.session then + app.session = Session:new() + cgi.add_header('Set-Cookie: session=' .. app.session.ident .. '; path=/' ) +end + +request.set_csrf_secret(app.session.csrf_secret) + +if app.session.user then + locale.set{ lang = app.session.user.lang or "en" } +end + +if param.get("lang") then + locale.set{ lang = param.get("lang") } +end + +execute.inner() diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/_filter/21_auth.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/_filter/21_auth.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,25 @@ +local auth_needed = not ( + request.get_module() == 'index' + and ( + request.get_view() == 'login' + or request.get_action() == 'login' + ) +) + +-- if not app.session.user_id then +-- trace.debug("DEBUG: AUTHENTICATION BYPASS ENABLED") +-- app.session.user_id = 1 +-- end + +if app.session.user == nil and auth_needed then + trace.debug("Not authenticated yet.") + request.redirect{ module = 'index', view = 'login' } +else + if auth_needed then + trace.debug("Authentication accepted.") + else + trace.debug("No authentication needed.") + end + execute.inner() + trace.debug("End of authentication filter.") +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/_filter_action/23_write_priv.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/_filter_action/23_write_priv.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,7 @@ +if + not (request.get_module() == "index" and request.get_action() == "login") + and not (request.get_module() == "index" and request.get_action() == "logout") +then + app.session.user:require_privilege("write") +end +execute.inner() diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/_filter_view/30_topnav.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/_filter_view/30_topnav.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,83 @@ +-- display navigation only, if user is logged in +if app.session.user_id == nil then + execute.inner() + return +end + +slot.select("topnav", function() + ui.link{ + attr = { class = "nav" }, + text = _"Home", + module = "index", + view = "index" + } + ui.link{ + attr = { class = "nav" }, + text = _"Media", + module = "medium" + } + ui.link{ + attr = { class = "nav" }, + text = _"Media types", + module = "media_type" + } + ui.link{ + attr = { class = "nav" }, + text = _"Genres", + module = "genre" + } + if app.session.user.admin then + ui.link{ + attr = { class = "nav" }, + text = _"Users", + module = "user" + } + end + ui.container{ + attr = { class = "nav lang_chooser" }, + content = function() + for i, lang in ipairs{"en", "de", "es"} do + ui.container{ + content = function() + ui.link{ + content = function() + ui.image{ + static = "lang/" .. lang .. ".png", + attr = { alt = lang } + } + slot.put(lang) + end, + module = "index", + action = "set_lang", + params = { lang = lang }, + routing = { + default = { + mode = "redirect", + module = request.get_module(), + view = request.get_view(), + id = param.get_id_cgi(), + params = param.get_all_cgi() + } + } + } + end + } + end + end + } + + ui.link{ + attr = { class = "nav" }, + text = _"Logout", + module = "index", + action = "logout", + redirect_to = { + ok = { + module = "index", + view = "login" + } + } + } +end) + +execute.inner() diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/_layout/default.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/_layout/default.html Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,58 @@ + + + + WebMCP Demo Application + + + + + +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+
+
TRACE
+ +
+
+
+ +
+
+ +
+
+ +
+ + + \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/_layout/system_error.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/_layout/system_error.html Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,41 @@ + + + + webmcp demo application + + + + +
+
+
+
+
+ System message +
+
+
+
+   +
+
+
+ +
+
+
+
+ + index +
+
+
+
+ +
+ +
close
+
+
+ + \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/genre/_action/update.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/genre/_action/update.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,24 @@ +local genre +local id = param.get_id() +if id then + genre = Genre:by_id(id) +else + genre = Genre:new() +end + +if param.get("delete", atom.boolean) then + local name = genre.name + genre:destroy() + slot.put_into("notice", _("Genre '#{name}' deleted", {name = name})) + return +end + +param.update(genre, "name", "description") + +genre:save() + +if id then + slot.put_into("notice", _("Genre '#{name}' updated", {name = genre.name})) +else + slot.put_into("notice", _("Genre '#{name}' created", {name = genre.name})) +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/genre/index.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/genre/index.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,46 @@ +slot.put_into("title", encode.html(_"Genres")) + +slot.select("actions", function() + if app.session.user.write_priv then + ui.link{ + content = _"Create new genre", + module = "genre", + view = "show" + } + end +end) + + +local selector = Genre:new_selector():add_order_by('"name", "id"') + +slot.select("main", function() + ui.paginate{ + selector = selector, + content = function() + ui.list{ + records = selector:exec(), + columns = { + { + field_attr = { style = "float: right;" }, + label = _"Id", + name = "id" + }, + { + label = _"Name", + name = "name" + }, + { + content = function(record) + ui.link{ + content = _"Show", + module = "genre", + view = "show", + id = record.id + } + end + }, + } + } + end + } +end) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/genre/show.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/genre/show.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,63 @@ +local genre +local id = param.get_id() +if id then + genre = Genre:by_id(id) +end + +if genre then + slot.put_into("title", encode.html(_"Genre")) +else + slot.put_into("title", encode.html(_"New genre")) +end + +slot.select("actions", function() + ui.link{ + content = _"Back", + module = "genre" + } + if genre and app.session.user.write_priv then + ui.link{ + content = _"Delete", + form_attr = { + onsubmit = "return confirm('" .. _'Are you sure?' .. "');" + }, + module = "genre", + action = "update", + id = genre.id, + params = { delete = true }, + routing = { + default = { + mode = "redirect", + module = "genre", + view = "index" + } + } + } + end +end) + +slot.select("main", function() + ui.form{ + attr = { class = "vertical" }, + record = genre, + readonly = not app.session.user.write_priv, + module = "genre", + action = "update", + id = id, + routing = { + default = { + mode = "redirect", + module = "genre", + view = "index" + } + }, + content = function() + if id then + ui.field.integer{ label = _"Id", name = "id", readonly = true } + end + ui.field.text{ label = _"Name", name = "name" } + ui.field.text{ label = _"Description", name = "description", multiline = true } + ui.submit{ text = _"Save" } + end + } +end) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/index/_action/login.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/index/_action/login.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,12 @@ +local user = User:by_ident_and_password(param.get('ident'), param.get('password')) + +if user then + app.session.user = user + app.session:save() + slot.put_into('notice', _'Login successful!') + trace.debug('User authenticated') +else + slot.put_into('error', _'Invalid username or password!') + trace.debug('User NOT authenticated') + return false +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/index/_action/logout.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/index/_action/logout.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,4 @@ +if app.session then + app.session:destroy() + slot.put_into("notice", _"Logout successful") +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/index/_action/set_lang.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/index/_action/set_lang.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,6 @@ +app.session.user.lang = param.get("lang") +app.session.user:save() + +locale.set{ lang = app.session.user.lang } + +slot.put_into("notice", _"Language changed") \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/index/index.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/index/index.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,3 @@ +slot.put_into('title', encode.html(_"webmcp demo application")) + +slot.put_into('main', encode.html(_"Welcome to webmcp demo application")) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/index/login.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/index/login.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,47 @@ +slot.put_into("title", encode.html(_"Password login")) + +slot.select("main", function() + + ui.form{ + attr = { class = "vertical" }, + module = "index", + action = "login", + routing = { + default = { + mode = "redirect", + module = "index", + view = "index" + } + }, + content = function() + + ui.container{ + attr = { class = "lang_chooser" }, + content = function() + for i, lang in ipairs{"en", "de", "es"} do + ui.container{ + content = function() + ui.link{ + content = function() + ui.image{ + static = "lang/" .. lang .. ".png", + attr = { alt = lang } + } + slot.put(lang) + end, + module = "index", + view = "login", + params = { lang = lang } + } + end + } + end + end + } + + ui.field.text{ label = _"Username", name = "ident" } + ui.field.text{ label = _"Password", name = "password" } + ui.submit{ text = _"Login" } + end + } +end) \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/media_type/_action/update.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/media_type/_action/update.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,24 @@ +local media_type +local id = param.get_id() +if id then + media_type = MediaType:by_id(id) +else + media_type = MediaType:new() +end + +if param.get("delete", atom.boolean) then + local name = media_type.name + media_type:destroy() + slot.put_into("notice", _("Media type '#{name}' deleted", {name = name})) + return +end + +param.update(media_type, "name", "description") + +media_type:save() + +if id then + slot.put_into("notice", _("Media type '#{name}' updated", {name = media_type.name})) +else + slot.put_into("notice", _("Media type '#{name}' created", {name = media_type.name})) +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/media_type/index.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/media_type/index.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,46 @@ +slot.put_into("title", encode.html(_"Media types")) + +slot.select("actions", function() + if app.session.user.write_priv then + ui.link{ + content = _"Create new media type", + module = "media_type", + view = "show" + } + end +end) + + +local selector = MediaType:new_selector():add_order_by('"name", "id"') + +slot.select("main", function() + ui.paginate{ + selector = selector, + content = function() + ui.list{ + records = selector:exec(), + columns = { + { + field_attr = { style = "float: right;" }, + label = _"Id", + name = "id" + }, + { + label = _"Name", + name = "name" + }, + { + content = function(record) + ui.link{ + content = _"Show", + module = "media_type", + view = "show", + id = record.id + } + end + }, + } + } + end + } +end) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/media_type/show.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/media_type/show.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,63 @@ +local media_type +local id = param.get_id() +if id then + media_type = MediaType:by_id(id) +end + +if media_type then + slot.put_into("title", encode.html(_"Media type")) +else + slot.put_into("title", encode.html(_"New media type")) +end + +slot.select("actions", function() + ui.link{ + content = _"Back", + module = "media_type" + } + if media_type and app.session.user.write_priv then + ui.link{ + content = _"Delete", + form_attr = { + onsubmit = "return confirm('" .. _'Are you sure?' .. "');" + }, + module = "media_type", + action = "update", + id = media_type.id, + params = { delete = true }, + routing = { + default = { + mode = "redirect", + module = "media_type", + view = "index" + } + } + } + end +end) + +slot.select("main", function() + ui.form{ + attr = { class = "vertical" }, + record = media_type, + readonly = not app.session.user.write_priv, + module = "media_type", + action = "update", + id = id, + routing = { + default = { + mode = "redirect", + module = "media_type", + view = "index" + } + }, + content = function() + if id then + ui.field.integer{ label = _"Id", name = "id", readonly = true } + end + ui.field.text{ label = _"Name", name = "name" } + ui.field.text{ label = _"Description", name = "description", multiline = true } + ui.submit{ text = _"Save" } + end + } +end) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/medium/_action/update.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/medium/_action/update.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,51 @@ +local medium +local id = param.get_id() +if id then + medium = Medium:by_id(id) +else + medium = Medium:new() +end + +if param.get("delete", atom.boolean) then + local name = medium.name + medium:destroy() + slot.put_into("notice", _("Medium '#{name}' deleted", {name = name})) + return +end + +param.update(medium, "media_type_id", "name", "copyprotected") + +medium:save() + +param.update_relationship{ + param_name = "genres", + id = medium.id, + connecting_model = Classification, + own_reference = "medium_id", + foreign_reference = "genre_id" +} + +for index, prefix in param.iterate("tracks") do + local id = param.get(prefix .. "id", atom.integer) + local track + if id then + track = Track:by_id(id) + elseif #param.get(prefix .. "name") > 0 then + track = Track:new() + track.medium_id = medium.id + else + break + end + track.position = param.get(prefix .. "position", atom.integer) + track.name = param.get(prefix .. "name") + track.description = param.get(prefix .. "description") + track.duration = param.get(prefix .. "duration") + track:save() +end + + +if id then + slot.put_into("notice", _("Medium '#{name}' updated", {name = medium.name})) +else + slot.put_into("notice", _("Medium '#{name}' created", {name = medium.name})) +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/medium/index.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/medium/index.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,50 @@ +slot.put_into("title", encode.html(_"Media")) + +slot.select("actions", function() + if app.session.user.write_priv then + ui.link{ + content = _"Create new medium", + module = "medium", + view = "show" + } + end +end) + + +local selector = Medium:new_selector():add_order_by('"name", "id"') + +slot.select("main", function() + ui.paginate{ + selector = selector, + content = function() + ui.list{ + records = selector:exec(), + columns = { + { + field_attr = { style = "float: right;" }, + label = _"Id", + name = "id" + }, + { + label = _"Name", + name = "name" + }, + { + label = _"Copy protected", + name = "copyprotected" + }, + { + content = function(record) + ui.link{ + content = _"Show", + module = "medium", + view = "show", + id = record.id + } + end + }, + } + } + end + } +end) \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/medium/show.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/medium/show.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,117 @@ +local medium +local id = param.get_id() +if id then + medium = Medium:by_id(id) +end + +if medium then + slot.put_into("title", encode.html(_"Medium")) +else + slot.put_into("title", encode.html(_"New medium")) +end + +slot.select("actions", function() + ui.link{ + content = _"Back", + module = "medium" + } + if medium and app.session.user.write_priv then + ui.link{ + content = _"Delete", + form_attr = { + onsubmit = "return confirm(" .. encode.json(_'Are you sure?') .. ");" + }, + module = "medium", + action = "update", + id = medium.id, + params = { delete = true }, + routing = { + default = { + mode = "redirect", + module = "medium", + view = "index" + } + } + } + end +end) + +slot.select("main", function() + ui.form{ + attr = { class = "vertical" }, + record = medium, + readonly = not app.session.user.write_priv, + module = "medium", + action = "update", + id = id, + routing = { + default = { + mode = "redirect", + module = "medium", + view = "index" + } + }, + content = function() + if id then + ui.field.integer{ label = _"Id", name = "id", readonly = true } + end + ui.field.select{ + label = _"Media type", + name = "media_type_id", + foreign_records = MediaType:new_selector():exec(), + foreign_id = "id", + foreign_name = "name" + } + ui.field.text{ label = _"Name", name = "name" } + ui.field.boolean{ label = _"Copy protected", name = "copyprotected" } + + ui.multiselect{ + name = "genres[]", + label = _"Genres", + style = "select", + attr = { size = 5 }, + foreign_records = Genre:new_selector():exec(), + connecting_records = medium and medium.classifications or {}, + own_id = "id", + own_reference = "medium_id", + foreign_reference = "genre_id", + foreign_id = "id", + foreign_name = "name", + } + local tracks = medium and medium.tracks or {} + for i = 1, 5 do + tracks[#tracks+1] = Track:new() + end + ui.list{ + label = _"Tracks", + prefix = "tracks", + records = tracks, + columns = { + { + label = _"Pos", + name = "position", + }, + { + label = _"Name", + name = "name", + }, + { + label = _"Description", + name = "description", + }, + { + label = _"Duration", + name = "duration", + }, + { + content = function() + ui.field.hidden{ name = "id" } + end + } + } + } + + ui.submit{ text = _"Save" } + end + } +end) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/user/_action/update.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/user/_action/update.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,24 @@ +local user +local id = param.get_id() +if id then + user = User:by_id(id) +else + user = User:new() +end + +if param.get("delete", atom.boolean) then + local name = user.name + user:destroy() + slot.put_into("notice", _("User '#{name}' deleted", {name = name})) + return +end + +param.update(user, "ident", "password", "name", "write_priv", "admin") + +user:save() + +if id then + slot.put_into("notice", _("User '#{name}' updated", {name = user.name})) +else + slot.put_into("notice", _("User '#{name}' created", {name = user.name})) +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/user/_filter/25_require_admin.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/user/_filter/25_require_admin.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,3 @@ +app.session.user:require_privilege("admin") + +execute.inner() diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/user/index.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/user/index.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,56 @@ +slot.put_into("title", encode.html(_"Users")) + +slot.select("actions", function() + ui.link{ + content = _"Create new user", + module = "user", + view = "show" + } +end) + + +local selector = User:new_selector():add_order_by('"ident", "id"') + +slot.select("main", function() + ui.paginate{ + selector = selector, + content = function() + ui.list{ + records = selector:exec(), + columns = { + { + field_attr = { style = "float: right;" }, + label = _"Id", + name = "id" + }, + { + label = _"Ident", + name = "ident" + }, + { + label = _"Name", + name = "name" + }, + { + label = _"w", + name = "write_priv" + }, + { + label = _"Admin", + name = "admin" + }, + { + content = function(record) + ui.link{ + content = _"Show", + module = "user", + view = "show", + id = record.id + } + end + }, + } + } + end + } +end) \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 demo-app/app/main/user/show.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/app/main/user/show.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,65 @@ +local user +local id = param.get_id() +if id then + user = User:by_id(id) +end + +if user then + slot.put_into("title", encode.html(_"User")) +else + slot.put_into("title", encode.html(_"New user")) +end + +slot.select("actions", function() + ui.link{ + content = _"Back", + module = "user" + } + if user then + ui.link{ + content = _"Delete", + form_attr = { + onsubmit = "return confirm('" .. _'Are you sure?' .. "');" + }, + module = "user", + action = "update", + id = user.id, + params = { delete = true }, + routing = { + default = { + mode = "redirect", + module = "user", + view = "index" + } + } + } + end +end) + +slot.select("main", function() + ui.form{ + attr = { class = "vertical" }, + record = user, + module = "user", + action = "update", + id = id, + routing = { + default = { + mode = "redirect", + module = "user", + view = "index" + } + }, + content = function() + if id then + ui.field.integer{ label = _"Id", name = "id", readonly = true } + end + ui.field.text{ label = _"Ident", name = "ident" } + ui.field.text{ label = _"Password", name = "password" } + ui.field.text{ label = _"Name", name = "name" } + ui.field.boolean{ label = _"Write Priv", name = "write_priv" } + ui.field.boolean{ label = _"Admin", name = "admin" } + ui.submit{ text = _"Save" } + end + } +end) diff -r 000000000000 -r 9fdfb27f8e67 demo-app/config/demo.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/config/demo.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,47 @@ +-- uncomment the following two lines to use C implementations of chosen +-- functions and to disable garbage collection during the request, to +-- increase speed: +-- +-- require 'webmcp_accelerator' +-- collectgarbage("stop") + +-- open and set default database handle +db = assert(mondelefant.connect{ + engine='postgresql', + dbname='webmcp_demo' +}) +at_exit(function() + db:close() +end) +function mondelefant.class_prototype:get_db_conn() return db end + +-- enable output of SQL commands in trace system +function db:sql_tracer(command) + return function(error_info) + local error_info = error_info or {} + trace.sql{ command = command, error_position = error_info.position } + end +end + +-- 'request.get_relative_baseurl()' should be replaced by the absolute +-- base URL of the application, as otherwise HTTP redirects will not be +-- standard compliant +request.set_absolute_baseurl(request.get_relative_baseurl()) + +-- uncomment the following lines, if you want to use a database driven +-- tempstore (for flash messages): +-- +-- function tempstore.save(blob) +-- return Tempstore:create(blob) +-- end +-- function tempstore.pop(key) +-- return Tempstore:data_by_key(key) +-- end + + +function mondelefant.class_prototype:by_id(id) + return self:new_selector() + :add_where{ "id = ?", id } + :optional_object_mode() + :exec() +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/db/schema.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/db/schema.sql Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,79 @@ +-- only needed for database driven tempstore (see application config) +CREATE TABLE "tempstore" ( + "key" TEXT PRIMARY KEY, + "data" BYTEA NOT NULL ); + +-- Attention: USER is a reserved word in PostgreSQL. We use it anyway in +-- this example. Don't forget the double quotes where neccessary. +CREATE TABLE "user" ( + "id" SERIAL8 PRIMARY KEY, + "ident" TEXT NOT NULL, + "password" TEXT, + "name" TEXT, + "lang" TEXT, + "write_priv" BOOLEAN NOT NULL DEFAULT FALSE, + "admin" BOOLEAN NOT NULL DEFAULT FALSE ); + +CREATE TABLE "session" ( + "ident" TEXT PRIMARY KEY, + "csrf_secret" TEXT NOT NULL, + "expiry" TIMESTAMPTZ NOT NULL DEFAULT NOW() + '24 hours', + "user_id" INT8 REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE ); + +CREATE TABLE "media_type" ( + "id" SERIAL8 PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT ); + +CREATE TABLE "genre" ( + "id" SERIAL8 PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT ); + +CREATE TABLE "medium" ( + "id" SERIAL8 PRIMARY KEY, + "media_type_id" INT8 NOT NULL REFERENCES "media_type" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + "name" TEXT NOT NULL, + "copyprotected" BOOLEAN NOT NULL ); + +CREATE TABLE "classification" ( + PRIMARY KEY ("medium_id", "genre_id"), + "medium_id" INT8 REFERENCES "medium" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + "genre_id" INT8 REFERENCES "genre" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); + +CREATE TABLE "track" ( + "id" SERIAL8 PRIMARY KEY, + "medium_id" INT8 NOT NULL REFERENCES "medium" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + "position" INT8 NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "duration" INTERVAL, + UNIQUE ("medium_id", "position") ); + +INSERT INTO "user" ("ident", "password", "name", "write_priv", "admin") + VALUES ('admin', 'admin', 'Administrator', true, true); + +INSERT INTO "user" ("ident", "password", "name", "write_priv", "admin") + VALUES ('user', 'User', 'User', true, false); + +INSERT INTO "user" ("ident", "password", "name", "write_priv", "admin") + VALUES ('anon', 'anon', 'Anonymous', false, false); + +INSERT INTO "media_type" ("name", "description") VALUES ('CD', ''); +INSERT INTO "media_type" ("name", "description") VALUES ('Tape', ''); + +INSERT INTO "genre" ("name", "description") VALUES ('Klassik', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Gospel', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Jazz', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Traditional', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Latin', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Blues', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Rhythm & blues', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Funk', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Rock', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Pop', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Country', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Electronic', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Ska / Reggea', ''); +INSERT INTO "genre" ("name", "description") VALUES ('Hip hop / Rap', ''); + diff -r 000000000000 -r 9fdfb27f8e67 demo-app/locale/translations.de.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/locale/translations.de.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,59 @@ +#!/usr/bin/env lua +return { +["Admin"] = "Admin"; +["Are you sure?"] = "Bist Du sicher?"; +["Back"] = "Zurück"; +["Copy protected"] = "Kopiergeschützt"; +["Create new genre"] = "Neues Genre anlegen"; +["Create new media type"] = "Neuen Medientyp anlegen"; +["Create new medium"] = "Neues Medium anlegen"; +["Create new user"] = "Neuen Benutzer anlegen"; +["Delete"] = "Löschen"; +["Description"] = "Beschreibung"; +["Duration"] = "Dauer"; +["Genre"] = "Genre"; +["Genre '#{name}' created"] = "Genre '#{name}' angelegt"; +["Genre '#{name}' deleted"] = "Genre '#{name}' gelöscht"; +["Genre '#{name}' updated"] = "Genre '#{name}' aktualisiert"; +["Genres"] = "Genres"; +["Home"] = "Startseite"; +["Id"] = "Id"; +["Ident"] = "Ident"; +["Invalid username or password!"] = "Ungülter Benutzername oder Kennwort"; +["Language changed"] = "Sprache gewechselt"; +["Login"] = "Anmeldung"; +["Login successful!"] = "Anmeldung erfolgreich!"; +["Logout"] = "Abmelden"; +["Logout successful"] = "Anmeldung erfolgreich"; +["Media"] = "Medium"; +["Media type"] = "Medientyp"; +["Media type '#{name}' created"] = "Medientyp '#{name}' angelegt"; +["Media type '#{name}' deleted"] = "Medientyp '#{name}' gelöscht"; +["Media type '#{name}' updated"] = "Medientyp '#{name}' aktualisiert"; +["Media types"] = "Medientypen"; +["Medium"] = "Medium"; +["Medium '#{name}' created"] = "Medium '#{name}' angelegt"; +["Medium '#{name}' deleted"] = "Medium '#{name}' gelöscht"; +["Medium '#{name}' updated"] = "Medium '#{name}' aktualisiert"; +["Name"] = "Name"; +["New genre"] = "Neues Genre"; +["New media type"] = "Neuer Medientyp"; +["New medium"] = "Neues Medium"; +["New user"] = "Neuer Benutzer"; +["Password"] = "Kennwort"; +["Password login"] = "Anmeldung mit Kennwort"; +["Pos"] = "Pos"; +["Save"] = "Speichern"; +["Show"] = "Anzeigen"; +["Tracks"] = "Stücke"; +["User"] = "Benutzer"; +["User '#{name}' created"] = "Benutzer '#{name}' angelegt"; +["User '#{name}' deleted"] = "Benutzer '#{name}' gelöscht"; +["User '#{name}' updated"] = "Benutzer '#{name}' aktualisiert"; +["Username"] = "Benutzername"; +["Users"] = "Benutzer"; +["Welcome to webmcp demo application"] = "Willkommen zur webmcp Demo-Anwendung"; +["Write Priv"] = "Schreibrecht"; +["w"] = "w"; +["webmcp demo application"] = "webmcp Demo-Anwendung"; +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/locale/translations.en.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/locale/translations.en.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,59 @@ +#!/usr/bin/env lua +return { +["Admin"] = false; +["Are you sure?"] = false; +["Back"] = false; +["Copy protected"] = false; +["Create new genre"] = false; +["Create new media type"] = false; +["Create new medium"] = false; +["Create new user"] = false; +["Delete"] = false; +["Description"] = false; +["Duration"] = false; +["Genre"] = false; +["Genre '#{name}' created"] = false; +["Genre '#{name}' deleted"] = false; +["Genre '#{name}' updated"] = false; +["Genres"] = false; +["Home"] = false; +["Id"] = false; +["Ident"] = false; +["Invalid username or password!"] = false; +["Language changed"] = false; +["Login"] = false; +["Login successful!"] = false; +["Logout"] = false; +["Logout successful"] = false; +["Media"] = false; +["Media type"] = false; +["Media type '#{name}' created"] = false; +["Media type '#{name}' deleted"] = false; +["Media type '#{name}' updated"] = false; +["Media types"] = false; +["Medium"] = false; +["Medium '#{name}' created"] = false; +["Medium '#{name}' deleted"] = false; +["Medium '#{name}' updated"] = false; +["Name"] = false; +["New genre"] = false; +["New media type"] = false; +["New medium"] = false; +["New user"] = false; +["Password"] = false; +["Password login"] = false; +["Pos"] = false; +["Save"] = false; +["Show"] = false; +["Tracks"] = false; +["User"] = false; +["User '#{name}' created"] = false; +["User '#{name}' deleted"] = false; +["User '#{name}' updated"] = false; +["Username"] = false; +["Users"] = false; +["Welcome to webmcp demo application"] = false; +["Write Priv"] = false; +["w"] = false; +["webmcp demo application"] = false; +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/locale/translations.es.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/locale/translations.es.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,59 @@ +#!/usr/bin/env lua +return { +["Admin"] = "Admin"; +["Are you sure?"] = "Estás seguro?"; +["Back"] = "Atrás"; +["Copy protected"] = "Protegido anticopia"; +["Create new genre"] = "Crear un nuevo género"; +["Create new media type"] = "Crear un nuevo soporte"; +["Create new medium"] = "Crear un nuevo medio"; +["Create new user"] = "Crear un nuevo usuario"; +["Delete"] = "Borrar"; +["Description"] = "Descripción"; +["Duration"] = "Duración"; +["Genre"] = "Género"; +["Genre '#{name}' created"] = "Género '#{name}' creado"; +["Genre '#{name}' deleted"] = "Género '#{name}' borrado"; +["Genre '#{name}' updated"] = "Género '#{name}' actualizado"; +["Genres"] = "Géneros"; +["Home"] = "Inicio"; +["Id"] = "Id"; +["Ident"] = "Ident"; +["Invalid username or password!"] = "Nombre de usuario o Contraseña Incorrecta"; +["Language changed"] = "Idioma cambiado"; +["Login"] = "Login"; +["Login successful!"] = "Login realizado!"; +["Logout"] = "Logout"; +["Logout successful"] = "Logout realizado"; +["Media"] = "Medios"; +["Media type"] = "Soporte"; +["Media type '#{name}' created"] = "Soporte '#{name}' creado"; +["Media type '#{name}' deleted"] = "Soporte '#{name}' borrado"; +["Media type '#{name}' updated"] = "Soporte '#{name}' actualizado"; +["Media types"] = "Soporte"; +["Medium"] = "Medio"; +["Medium '#{name}' created"] = "Medio '#{name}' creado"; +["Medium '#{name}' deleted"] = "Medio '#{name}' borrado"; +["Medium '#{name}' updated"] = "Medio '#{name}' actualizado"; +["Name"] = "Nombre"; +["New genre"] = "Nuevo género"; +["New media type"] = "Nuevo soporte"; +["New medium"] = "Nuevo medio"; +["New user"] = "Nuevo usuario"; +["Password"] = "Contraseña"; +["Password login"] = "Login con contraseña"; +["Pos"] = "Pos"; +["Save"] = "Guardar"; +["Show"] = "Mostrar"; +["Tracks"] = "Pistas"; +["User"] = "Usuario"; +["User '#{name}' created"] = "Usuario '#{name}' creado"; +["User '#{name}' deleted"] = "Usuario '#{name}' borrado"; +["User '#{name}' updated"] = "Usuario '#{name}' actualizado"; +["Username"] = "Nombre de usuario"; +["Users"] = "Usuarios"; +["Welcome to webmcp demo application"] = "Bienvenido a la aplicación de demostración de webmcp"; +["Write Priv"] = "Permiso de escritura"; +["w"] = "w"; +["webmcp demo application"] = "Aplicación de demostración de webmcp"; +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/classification.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/classification.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,22 @@ +Classification = mondelefant.new_class() +Classification.table = 'classification' + +Classification:add_reference{ + mode = 'm1', -- many (m) Classifications can refer to one (1) Medium + to = "Medium", -- name of referenced model (quoting avoids auto-loading of model here) + this_key = 'medium_id', -- our key in the classification table + that_key = 'id', -- other key in the medium table + ref = 'medium', -- name of reference + back_ref = nil, -- not used for m1 relation! + default_order = nil -- not used for m1 relation! +} + +Classification:add_reference{ + mode = 'm1', -- many (m) Classifications can refer to one (1) Medium + to = "Genre", -- name of referenced model (quoting avoids auto-loading of model here) + this_key = 'genre_id', -- our key in the classification table + that_key = 'id', -- other key in the genre table + ref = 'genre', -- name of reference + back_ref = nil, -- not used for m1 relation! + default_order = nil -- not used for m1 relation! +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/genre.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/genre.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,25 @@ +Genre = mondelefant.new_class() +Genre.table = 'genre' + +Genre:add_reference{ + mode = '1m', -- one (1) Genre is used for many (m) Classifications + to = "Classification", -- name of referenced model (using a string instead of reference avoids auto-loading here) + this_key = 'id', -- own key in genre table + that_key = 'genre_id', -- other key in classification table + ref = 'classifications', -- name of reference + back_ref = 'genre', -- each autoloaded Classification automatically refers back to the Genre + default_order = '"media_id"' -- order Classifications by SQL expression "media_id" +} + +Genre:add_reference{ + mode = 'mm', -- many (m) Genres belong to many (m) Medium entries + to = "Medium", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'id', -- (primary) key of genre table + that_key = 'id', -- (primary) key of medium talbe + connected_by_table = 'classification', -- table connecting genres with media + connected_by_this_key = 'genre_id', -- key in connection table referencing genres + connected_by_that_key = 'medium_id', -- key in connection table referencing media + ref = 'media', -- name of reference + back_ref = nil, -- not used for mm relation! + default_order = '"medium"."name", "medium"."id"' -- mm references need qualified names in SQL order expression! +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/media_type.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/media_type.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,12 @@ +MediaType = mondelefant.new_class() +MediaType.table = 'media_type' + +MediaType:add_reference{ + mode = '1m', -- one (1) MediaType is set for many (m) media + to = "Medium", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'id', -- own key in media_type table + that_key = 'media_type_id', -- other key in medium table + ref = 'media', -- name of reference + back_ref = 'media_type', -- each autoloaded Medium automatically refers back to the MediaType + default_order = '"name", "id"' -- order media by SQL expression "name", "id" +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/medium.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/medium.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,45 @@ +Medium = mondelefant.new_class() +Medium.table = 'medium' + +Medium:add_reference{ + mode = 'm1', -- many (m) Medium entries can refer to one (1) MediaType + to = "MediaType", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'media_type_id', -- own key in medium table + that_key = 'id', -- other key in media_type table + ref = 'media_type', -- name of reference + back_ref = nil, -- not used for m1 relation! + default_order = nil -- not used for m1 relation! +} + +Medium:add_reference{ + mode = '1m', -- one (1) Medium has many (m) Classifications + to = "Classification", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'id', -- own key in medium table + that_key = 'medium_id', -- other key in classification table + ref = 'classifications', -- name of reference + back_ref = 'medium', -- each autoloaded classification automatically refers back to the Medium + default_order = '"genre_id"' -- order classifications by SQL expression "genre_id" +} + +Medium:add_reference{ + mode = 'mm', -- many (m) Media belong to many (m) Genres + to = "Genre", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'id', -- (primary) key of medium table + that_key = 'id', -- (primary) key of genre talbe + connected_by_table = 'classification', -- table connecting media with genres + connected_by_this_key = 'medium_id', -- key in classification table referencing media + connected_by_that_key = 'genre_id', -- key in classification table referencing genres + ref = 'genres', -- name of reference + back_ref = nil, -- not used for mm relation! + default_order = '"genre"."name", "genre"."id"' -- mm references need qualified names in SQL order expression! +} + +Medium:add_reference{ + mode = '1m', -- one (1) Medium has many (m) Tracks + to = "Track", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'id', -- own key in medium table + that_key = 'medium_id', -- other key in track table + ref = 'tracks', -- name of reference + back_ref = 'medium', -- each autoloaded classification automatically refers back to the Medium + default_order = '"position"' -- order tracks by SQL expression "position" +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/session.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/session.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,35 @@ +Session = mondelefant.new_class() +Session.table = 'session' +Session.primary_key = { "ident" } + +Session:add_reference{ + mode = 'm1', -- many (m) sessions refer to one (1) user + to = "User", -- name of referenced model (quoting avoids auto-loading here) + this_key = 'user_id', -- own key in session table + that_key = 'id', -- other key in user table + ref = 'user', -- name of reference + back_ref = nil, -- not used for m1 relation! + default_order = nil -- not used for m1 relation! +} + +local function random_string() + return multirand.string( + 32, + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + ) +end + +function Session:new() + local session = self.prototype.new(self) -- super call + session.ident = random_string() + session.csrf_secret = random_string() + session:save() + return session +end + +function Session:by_ident(ident) + local selector = self:new_selector() + selector:add_where{ 'ident = ?', ident } + selector:optional_object_mode() + return selector:exec() +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/tempstore.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/tempstore.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,25 @@ +Tempstore = mondelefant.new_class() +Tempstore.table = 'tempstore' + +function Tempstore:by_key(key) + local selector = self:new_selector() + selector:add_where{ 'key = ?', key } + selector:optional_object_mode() + return selector:exec() +end + +function Tempstore:data_by_key(key) + local tempstore = Tempstore:by_key(key) + if tempstore then + tempstore:destroy() + return tempstore.data + end +end + +function Tempstore:create(data) + tempstore = Tempstore:new() + tempstore.key = multirand.string(22, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') + tempstore.data = data + tempstore:save() + return tempstore.key +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/track.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/track.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,12 @@ +Track = mondelefant.new_class() +Track.table = 'track' + +Track:add_reference{ + mode = 'm1', -- many (m) Tracks can refer to one (1) Medium + to = "Medium", -- name of referenced model (quoting avoids auto-loading of model here) + this_key = 'medium_id', -- our key in the track table + that_key = 'id', -- other key in the medium table + ref = 'medium', -- name of reference + back_ref = nil, -- not used for m1 relation! + default_order = nil -- not used for m1 relation! +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/model/user.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/model/user.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,33 @@ +User = mondelefant.new_class() +User.table = 'user' + +User:add_reference{ + mode = '1m', -- one (1) user can have many (m) sessions + to = "Session", -- referenced model (quoting avoids auto-loading here) + this_key = 'id', -- own key in user table + that_key = 'user_id', -- other key in session table + ref = 'sessions', -- name of reference + back_ref = 'user', -- each autoloaded Session automatically refers back to the User + default_order = '"ident"' -- order sessions by SQL expression "ident" +} + +function User:by_ident_and_password(ident, password) + local selector = self:new_selector() + selector:add_where{ 'ident = ? AND password = ?', ident, password } + selector:optional_object_mode() + return selector:exec() +end + +function User.object_get:name_with_login() + return self.name .. ' (' .. self.login .. ')' +end + +function User.object:require_privilege(privilege) + if privilege == "admin" then + assert(self.admin, "Administrator privilege required") + elseif privilege == "write" then + assert(self.write_priv, "Write privilege required") + else + error("Unknown privilege passed to require_privilege method of User") + end +end diff -r 000000000000 -r 9fdfb27f8e67 demo-app/static/lang/de.png Binary file demo-app/static/lang/de.png has changed diff -r 000000000000 -r 9fdfb27f8e67 demo-app/static/lang/en.png Binary file demo-app/static/lang/en.png has changed diff -r 000000000000 -r 9fdfb27f8e67 demo-app/static/lang/es.png Binary file demo-app/static/lang/es.png has changed diff -r 000000000000 -r 9fdfb27f8e67 demo-app/static/logo.png Binary file demo-app/static/logo.png has changed diff -r 000000000000 -r 9fdfb27f8e67 demo-app/static/style.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/static/style.css Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,383 @@ +/* + * ********** body *********** + */ + +body { + background-color: #ddd; + font-family: sans-serif; + margin: 0; + padding: 0; + font-size: 10pt; +} + +body, td, th, input, select { + font-size: 10pt; +} + +div { + padding: 0px; + margin: 0px; +} + +/* + * ********** Logo *********** + */ + +.layout_logo { + background: #fff; + border: 1px solid #444; + border-left: none; + border-top: none; + color: #444; + float: left; + height: 2.29em; + margin-bottom: 2ex; + padding-bottom: 0.3ex; + padding-left: 0.3ex; + padding-top: 0.3ex; +} + +.logo { + font-size: 1em; + font-weight: bold; +} + +.logo img { + height: 2.29em; + margin-bottom: -0.7em; +} + +/* + * ********** Top navigation *********** + */ + +.layout_topnav { + float: left; + margin-bottom: 2em; + margin-left: 2ex; +} + +.slot_topnav .nav { + background: #fff; + border: 1px solid #444; + border-top: none; + color: #000; + display: block; + float: left; + font-weight: bold; + height: 2.29em; + margin-right: 1ex; + padding-bottom: 0.3ex; + padding-left: 0.3ex; + padding-right: 1ex; + padding-top: 0.3ex; + text-decoration: none; +} + + + +.vertical .lang_chooser { + margin-left: 25ex; + margin-bottom: 1ex; +} + +.vertical .lang_chooser div { + margin-right: 1ex; +} + +.lang_chooser div { + float: left; + text-align: center; +} + +.lang_chooser a { + color: #000; + font-size: 75%; +} + +.lang_chooser img { + display: block; +} + +.lang_chooser img { + border: 1px solid white; +} + +.lang_chooser a:hover img { + border: 1px solid #444; +} + +.lang_chooser a:hover { + background: #444; + color: #fff; +} + + +.slot_topnav a:hover { + background: #444; + color: #fff; +} + +/* + * ********** Content *********** + */ + +.layout_content { + margin-left: 2ex; + margin-right: 2ex; +} + + +/* + * ********** Messages *********** + */ + +.layout_notice, .layout_error, .layout_warning { + background: #fff; + font-weight: bold; + right: 2ex; + line-height: 1.7em; + position: absolute; + top: 2ex; + width: 60ex; + -moz-opacity:0.7; +} + +.slot_notice, .slot_warning, .slot_error { + padding: 2ex; +} + +.slot_notice { + color: green; + border: 2px solid green; +} + +.slot_warning { + color: orange; + border: 2px solid orange; +} + +.slot_error { + color: red; + border: 2px solid red; +} + + + +/* + * ********** Title, Actions and Main *********** + */ + +.layout_title { + background: #fff; + border: 1px solid #444; + border-bottom: none; + padding: 0.5ex; + font-size: 120%; + font-weight: bold; + float: left; +} + +.layout_title .object_class { + float: left; + font-size: 0.7em; + font-weight: normal; + margin-right: 1ex; +} + +.layout_actions { + background: #444; + border-left: 1px solid #444; + padding-top: 0.5ex; + padding-bottom: 0.5ex; +} + +.slot_actions a { + color: #fff; + text-decoration: none; + padding: 0.5ex; +} + +.slot_actions a:hover { + background: #ccc; + color: #444; +} + +.slot_main { + background: #fff; + border: 1px solid #444; + border-top: none; + padding: 2ex; + padding-bottom: 0px; +} + + + +/* + * ********** ui *********** + */ + + +form.ui_link { + display: inline; + margin: 0px; + padding: 0px; +} + +.vertical { + margin-bottom: 1em; +} + +.vertical:after, .vertical div:after { + clear: left; + content: "."; + display: block; + height: 0; + visibility: hidden; +} + +.vertical label { + float: left; + width: 25ex; + padding-top: 0.7em; + font-size: 80%; + font-weight: bold; + color: #666; + text-align: right; + text-transform: uppercase; +} + +.vertical select, +.vertical input, +.vertical textarea, +.vertical span { + margin-left: 1em; + margin-bottom: 2ex; +} + +.vertical textarea { + border: none; + border: 1px dotted #777; + padding: 0px; +} + +.vertical input, +.vertical select { + border: 1px dotted #777; +} + +.vertical textarea { + width: 80ex; + height: 5em; + font-family: sans-serif; + font-size: 100%; + padding: 0.5ex; +} + +input[type=text] { + background: #fff; + padding: 0.5ex; +} + +input[type=submit] { + margin-left: 25ex; + border: 1px dotted #777; +} + +input[type=submit]:hover { + border: 1px solid #777; +} + +/* ui.list */ + +table.ui_list { + border-collapse: collapse; +} + +ul.ui_list { + padding: 0px; + margin: 0px; +} + +.ui_list li { + clear: left; + list-style-type: none; +} + +.ui_list_title { + font-size: 110%; + font-weight: bold; + margin-bottom: 0.2em; +} + + +li.ui_list_head { + font-weight: bold; + background: #777; + color: #fff; +} + +.ui_list li:hover, +.ui_list tr:hover { + background: #ddd; +} + +.ui_list li.ui_list_head:hover, +.ui_list tr.ui_list_head:hover { + background: #777; +} + +.ui_list a { + text-decoration: none; + color: #000; + border-bottom: 1px dashed #777; +} + +.ui_list a:hover { + border-bottom: 1px solid #000; +} + +.ui_list li div { + float: left; +} + +.ui_list td { + vertical-align: top; +} + +.ui_list li div, +.ui_list td { + padding-top: 0.4ex; + padding-bottom: 0.4ex; + padding-right: 0.4em; +} + +.ui_list li div div { + padding: 0px; +} + +.ui_list li div a { + text-decoration: none; + color: #000; +} + +.ui_list_col_align_right { + text-align: right; +} + +.ui_paginate_select { + text-align: center; +} + +.ui_paginate_select a { + padding-left: 1ex; + padding-right: 1ex; + background-color: #ccc; + color: #000; +} + +.ui_paginate_select a.active, +.ui_paginate_select a:hover { + background-color: #444; + color: #fff; +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/static/trace.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo-app/static/trace.css Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,153 @@ +.layout_trace { + position: absolute; + right: 0; + margin-top: 20px; + border: 1px solid #404040; + font-size: 70%; + padding: 5px; + background: #ffe0c0; +} + +#trace_show { + cursor: pointer; +} + +.trace_list { + margin: 0px; + margin-bottom: 10px; + padding: 0px; + list-style-type: none; +} + +.trace_list .trace_list { + border-top: 1px solid; + margin-bottom: 0px; +} + +.trace_list li { + margin: 3px; + padding: 0px; +} + +.trace_head { + font-weight: bold; + margin: 1px; +} + +.trace_error { + border: 3px solid red; + background-color: black; + color: #c00000; + text-align: center; + text-decoration: blink; +} + +.trace_error_position { + color: red; + text-decoration: blink; + font-weight: bold; +} + +.trace_config { + border: 1px solid #608000; + background-color: #ffffff; + color: #608000; +} + +.trace_config .trace_list { + border-color: #608000; +} + +.trace_request { + border: 1px solid #6000ff; + background-color: #c080ff; + color: #6000ff; +} + +.trace_request .trace_list { + border-color: #6000ff; +} + +.trace_filter { + border: 1px solid #606060; + background-color: #c0c0c0; + color: #606060; +} + +.trace_filter .trace_list { + border-color: #606060; +} + +.trace_view { + border: 1px solid #0000ff; + background-color: #40c0ff; + color: #0000ff; +} + +.trace_view .trace_list { + border-color: #0000ff; +} + +.trace_action_success { + border: 1px solid #006000; + background-color: #80ff80; + color: #006000; +} + +.trace_action_success .trace_list { + border-color: #006000; +} + +.trace_action_softfail { + border: 1px solid #600000; + background-color: #ff6020; + color: #600000; +} + +.trace_action_softfail .trace_list { + border-color: #600000; +} + +.trace_action_status { + font-weight: bold; +} + +.trace_action_neutral { + border: 1px solid #600000; + background-color: #ffc040; + color: #600000; +} + +.trace_action_neutral .trace_list { + border-color: #600000; +} + +.trace_redirect, .trace_forward { + border: 1px solid #404000; + background-color: #c08040; + color: #404000; +} + +.trace_redirect .trace_list, .trace_forward .trace_list { + border-color: #404000; +} + +.trace_exectime { + border: 1px solid black; + background-color: #404040; + color: white; +} + +.trace_exectime .trace_list { + border-color: white; +} + +.trace_close { + border: 1px solid black; + background-color: #605040; + margin: 3px; + padding: 3px; + color: white; + text-align: center; + cursor: pointer; +} diff -r 000000000000 -r 9fdfb27f8e67 demo-app/tmp/.keep diff -r 000000000000 -r 9fdfb27f8e67 doc/apache.sample.conf --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/apache.sample.conf Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,37 @@ +# Apache modules cgi_module, env, rewrite and alias must be loaded before +# Take a look in your main apache configuration! + +RewriteEngine on +# do not rewrite static URLs +RewriteRule ^/webmcp-demo/static/(.*)$ /webmcp-demo/static/$1 +# base URL +RewriteRule ^/webmcp-demo/(\?(.*))?$ /webmcp-demo/webmcp-wrapper.lua?_webmcp_urldepth=0&_webmcp_module=index&_webmcp_view=index&$2 +# module base URLs +RewriteRule ^/webmcp-demo/([^/\?]+)/(\?(.*))?$ /webmcp-demo/webmcp-wrapper.lua?_webmcp_urldepth=1&_webmcp_module=$1&_webmcp_view=index&$3 +# actions +RewriteRule ^/webmcp-demo/([^/\?]+)/([^/\.\?]+)(\?(.*))?$ /webmcp-demo/webmcp-wrapper.lua?_webmcp_urldepth=1&_webmcp_module=$1&_webmcp_action=$2&$4 +# views without numeric id or string ident +RewriteRule ^/webmcp-demo/([^/\?]+)/([^/\.\?]+)\.([^/\?]+)(\?(.*))?$ "/webmcp-demo/webmcp-wrapper.lua?_webmcp_urldepth=1&_webmcp_module=$1&_webmcp_view=$2&_webmcp_suffix=$3&$5 +# views with numeric id or string ident +RewriteRule ^/webmcp-demo/([^/\?]+)/([^/\?]+)/([^/\.\?]+)\.([^/\?]+)(\?(.*))?$ /webmcp-demo/webmcp-wrapper.lua?_webmcp_urldepth=2&_webmcp_module=$1&_webmcp_view=$2&_webmcp_id=$3&_webmcp_suffix=$4&$6 + +# Directly serve static files +Alias /webmcp-demo/static /__INSERT_LOCAL_FILE_PATH_TO_DEMO_APPLICATION_HERE__/static + +# Connect extarnal path to the webmcp cgi interface +ScriptAlias /webmcp-demo/ /__INSERT_LOCAL_FILE_PATH_TO_WEBMCP_FRAMEWORK_HERE__/cgi-bin/ + +# Allow CGI execution for the webmcp CGI interface + + AllowOverride None + Options ExecCGI -MultiViews +SymLinksIfOwnerMatch + Order allow,deny + Allow from all + + +# Configure environment for demo application + + SetEnv WEBMCP_APP_BASEPATH '/__INSERT_LOCAL_FILE_PATH_TO_DEMO_APPLICATION_HERE__' + SetEnv WEBMCP_CONFIG_NAME 'demo' + + diff -r 000000000000 -r 9fdfb27f8e67 doc/autodoc-footer.htmlpart --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/autodoc-footer.htmlpart Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,7 @@ + +
+

+ Copyright (c) 2009 Public Software Group e. V., Berlin +

+ + diff -r 000000000000 -r 9fdfb27f8e67 doc/autodoc-header.htmlpart --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/autodoc-header.htmlpart Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,306 @@ + + + + WebMCP 1.0.0 Documentation + + +

WebMCP 1.0.0 Documentation

+

+ WebMCP is a completely new web development framework, and has not been extensively tested yet. The API might change at any time, but in future releases there will be a list of all changes, which break downward compatibility. +

+

Requirements

+

+ WebMCP has been developed on Linux and FreeBSD. Using it with Mac OS X is completely untested; Microsoft Windows is not supported. Beside the operating system, the only mandatory dependencies for WebMCP are the programming language Lua version 5.1, PostgreSQL version 8.2 or higher, a C compiler, and a Webserver like Lighttpd or Apache. +

+

Installation

+

+ After downloading the tar.gz package, unpack it, enter the unpacked directory and type make. If you use Mac OS X or if you experience problems during compilation, you need to edit the Makefile.options file prior to compilation. The framework itself will be available in the framework/ directory, while a demo application is available in the demo-app/ directory. The framework.precompiled/ and demo-app.precompiled/ directories will contain a version with all Lua files being byte-code pre-compiled, which can be used instead. You may copy these directories (with cp -L to follow links) to any other place you like. Use the files doc/lighttpd.sample.conf or doc/apache.sample.conf to setup your webserver appropriatly. Don't forget to setup a database, and make the tmp/ directory of the application writable for the web server process. Good luck and have fun! +

+

Using the atom library

+

+ Lua itself has only very few built-in data types. The atom library gives support for extra data types. Currently the following extra data types are provided: +

+ +

+ In addition the following pseudo-types are existent, corresponding to Lua's base types: +

+ +

+ Both atom.integer and atom.number refer to Lua's base type “number”. +

+

+ New values of atom data types are created by either calling atom.type:load(string_representation) or by calling atom.type{...}, e.g. atom.date{year=1970, month=1, day=1}. You can dump any atom value as a string by calling atom.dump(value) and later reload it with atom.type:load(string). +

+

Using the Object-Relational Mapper “mondelefant”

+

+ The library “mondelefant” shipping with WebMCP can be used to access PostgreSQL databases. It also serves as an Object-Relational Mapper (ORM). Opening a connection to a database is usually done in a config file in the following way: +

+
+db = assert( mondelefant.connect{ engine='postgresql', dbname='webmcp_demo' } )
+at_exit(function() 
+  db:close()
+end)
+function mondelefant.class_prototype:get_db_conn() return db end
+
+function db:sql_tracer(command)
+  return function(error_info)
+    local error_info = error_info or {}
+    trace.sql{ command = command, error_position = error_info.position }
+  end
+end
+

+ Overwriting the sql_tracer method of the database handle is optional, but helpful for debugging. The parameters for mondelefant.connect are directly passed to PostgreSQL's client library libpq. See PostgreSQL's documentation on PQconnect for information about supported parameters. +

+

+ To define a model to be used within a WebMCP application, create a file named with the name of the model and .lua as extension in the model/ directory of your application. The most basic definition of a model (named “movie” in this example) is: +

+
+Movie = mondelefant.new_class()
+Movie.table = 'movie'
+

+ Note: Model classes are always written CamelCase, while the name of the file in model/ is written lower_case. +

+

+ To select objects from the database, the mondelefant library provides a selector framework: +

+
+local s = Movie:new_selector()
+s:add_where{ 'id = ?', param.get_id() }
+s:single_object_mode()  -- return single object instead of list
+local movie = s:exec()
+

+ A short form of the above query would be: +

+
+local movie = Movie:new_selector():add_where{ 'id = ?', param.get_id() }:single_object_mode():exec()
+

+ For more examples about how to use the model system, please take a look at the demo application. +

+

The Model-View-Action (MVA) concept

+

+ As opposed to other web application frameworks, WebMCP does not use a Model-View-Controller (MVC) concept, but a Model-View-Action (MVA) concept. +

+

Models

+

+ The models in MVA are like the models in MVC; they are used to access data stored in a relational database (PostgreSQL) in an object oriented way. They can also be used to provide methods for working with objects representing the database entries. +

+

Views

+

+ The views in the MVA concept are different from the views in the MVC concept. As WebMCP has no controllers, the views are responsible for processing the GET/POST parameters from the webbrowser, fetching the data to be displayed, and creating the output by directly writing HTML to slots in a layout or by calling helper functions for the user interface. +

+

Actions

+

+ Actions are similar to views, but supposed to change data in the database, hence only callable by HTTP POST requests. They are also responsible for processing the POST parameters from the webbrowser. They can modify the database, but instead of rendering a page to be displayed, they just return a status code. Depending on the status code there will be an internal forward or an HTTP 303 redirect to a view. When calling an action via a POST request, additional POST parameters, which are usually added by hidden form fields, determine the view to be displayed for each status code returned by the action. +

+

Directory structure of a WebMCP application

+ +

Automatically generated reference for the WebMCP environment

+
 
') + end +end \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 framework/env/ui_deprecated/multiselect.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui_deprecated/multiselect.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,50 @@ +function ui_deprecated.multiselect(args) + local record = assert(slot.get_state_table(), "ui_deprecated.multiselect was not called within a form.").form_record + local name = args.name + local relationship = args.relationship + + local foreign_records = relationship.foreign_records + + local selector = relationship.connected_by_model:new_selector() + selector:add_where{ relationship.connected_by_my_id .. ' = ?', record[relationship.my_id] } + local connected_by_records = selector:exec() + + local connections = {} + for i, connected_by_record in ipairs(connected_by_records) do + connections[connected_by_record[relationship.connected_by_foreign_id]] = connected_by_record + end + + local html = {} + + if args.type == "checkboxes" then + for i, foreign_record in ipairs(foreign_records) do + local selected = '' + if connections[foreign_record[relationship.foreign_id]] then + selected = ' checked="1"' + end + html[#html + 1] = ' ' .. convert.to_human(foreign_record[relationship.foreign_name], "string") .. '
\n' + end + + else + html[#html + 1] = '' + + end + + slot.put('
\n') + if args.label then + slot.put('
', encode.html(args.label), '
\n') + end + slot.put('
', + table.concat(html), + '
\n', + '
\n' + ) +end \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 framework/env/ui_deprecated/select.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui_deprecated/select.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,31 @@ +function ui_deprecated.select(args) + local record = assert(slot.get_state_table(), "ui_deprecated.select was not called within a form.").form_record + local value = param.get(args.field) or record[args.field] + local html_options = args.html_options or {} + html_options.name = args.field + + ui_deprecated.tag("div", { html_options = { class="ui_field ui_select" }, content = function() + if args.label then + ui_deprecated.tag("div", { html_options = { class="label" }, content = function() + ui_deprecated.text(args.label) + end }) + end + ui_deprecated.tag("div", { html_options = { class="value" }, content = function() + ui_deprecated.tag("select", { html_options = html_options, content = function() + if args.include_option then + ui_deprecated.tag("option", { html_options = { value = "" }, content = args.include_option }) + end + for i, object in ipairs(args.foreign_records) do + local selected = nil + if tostring(object[args.foreign_id]) == tostring(value) then + selected = "1" + end + ui_deprecated.tag("option", { html_options = { value = object[args.foreign_id], selected = selected }, content = object[args.foreign_name] }) + end + end }) -- /select + end }) -- /div + end }) -- /div +end + + + diff -r 000000000000 -r 9fdfb27f8e67 framework/env/ui_deprecated/submit.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui_deprecated/submit.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,24 @@ +-- +-- Creates a submit buttom for a form +-- +-- label (string) The label of the box +-- +-- Example: +-- +-- ui_deprecated.submit({ +-- label = 'Save' +-- }) +-- + +function ui_deprecated.submit(args) + local args = args or {} + args.label = args.label or 'Submit' + slot.put( + '
', + '
 
', + '
', + '', + '
', + '
' + ) +end diff -r 000000000000 -r 9fdfb27f8e67 framework/env/ui_deprecated/tag.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui_deprecated/tag.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,20 @@ +function ui_deprecated.tag(name, args) + + local html_options = args.html_options or {} + + slot.put('<', name, ui_deprecated._stringify_table(html_options), '>') + + if args.label then + ui_deprecated.label(args.label) + end + + if type(args.content) == 'function' then + args.content() + else + ui_deprecated.text(args.content) + end + + slot.put('') + + +end \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 framework/env/ui_deprecated/text.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui_deprecated/text.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,4 @@ + +function ui_deprecated.text(value) + slot.put(encode.html(convert.to_human(value))) +end \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 framework/lib/.keep diff -r 000000000000 -r 9fdfb27f8e67 libraries/atom/atom.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/atom/atom.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,1529 @@ +#!/usr/bin/env lua + +local _G = _G +local _VERSION = _VERSION +local assert = assert +local error = error +local getfenv = getfenv +local getmetatable = getmetatable +local ipairs = ipairs +local module = module +local next = next +local pairs = pairs +local print = print +local rawequal = rawequal +local rawget = rawget +local rawset = rawset +local require = require +local select = select +local setfenv = setfenv +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local type = type +local unpack = unpack + +local coroutine = coroutine +local io = io +local math = math +local os = os +local string = string +local table = table + +module(...) + + + +--------------------------------------- +-- general functions and definitions -- +--------------------------------------- + +--[[-- +bool = -- true, if value is an integer within resolution +atom.is_integer( + value -- value to be tested +) + +This function returns true if the given object is an integer within resolution. + +--]]-- +function is_integer(i) + return + type(i) == "number" and i % 1 == 0 and + (i + 1) - i == 1 and i - (i - 1) == 1 +end +--//-- + +--[[-- +atom.not_a_number + +Value representing an invalid numeric result. Used for atom.integer.invalid and atom.number.invalid. + +--]]-- +not_a_number = 0 / 0 +--//-- + +do + + local shadow = setmetatable({}, { __mode = "k" }) + + local type_mt = { __index = {} } + + function type_mt:__call(...) + return self:new(...) + end + + function type_mt.__index:_create(data) + local value = setmetatable({}, self) + shadow[value] = data + return value + end + + local function write_prohibited() + error("Modification of an atom is prohibited.") + end + + -- returns a new type as a table, which serves also as metatable + function create_new_type(name) + local t = setmetatable( + { methods = {}, getters = {}, name = name }, + type_mt + ) + function t.__index(self, key) + local data = shadow[self] + local value = data[key] + if value ~= nil then return value end + local method = t.methods[key] + if method then return method end + local getter = t.getters[key] + if getter then return getter(self) end + end + t.__newindex = write_prohibited + return t + 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" + ) + + This function checks, if a value is of a given type. The value may be an invalid value though, e.g. atom.date.invalid. + + --]]-- + 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 + --//-- + + --[[-- + 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" + ) + + This function checks, if a value is valid. It optionally checks, if the value is of a given type. + + --]]-- + 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 + --//-- + +end + +--[[-- +string = -- string representation to be passed to a load function +atom.dump( + value -- value to be dumped +) + +This function returns a string representation of the given value. + +--]]-- +function dump(obj) + if obj == nil then + return "" + else + return tostring(obj) + end +end +--//-- + + + +------------- +-- boolean -- +------------- + +boolean = { name = "boolean" } + +--[[-- +bool = -- true, false, or nil +atom.boolean:load( + string -- string to be interpreted as boolean +) + +This method returns true or false or nil, depending on the input string. + +--]]-- +function boolean:load(str) + if type(str) ~= "string" then + error("String expected") + elseif str == "" then + return nil + 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 +--//-- + + + +------------ +-- string -- +------------ + +_M.string = { name = "string" } + +--[[-- +string = -- the same string +atom.string:load( + string -- a string +) + +This method returns the passed string, or throws an error, if the passed argument is not a string. + +--]]-- +function _M.string:load(str) + if type(str) ~= "string" then + error("String expected") + else + return str + end +end +--//-- + + + +------------- +-- integer -- +------------- + +integer = { name = "integer" } + +--[[-- +int = -- an integer or atom.integer.invalid (atom.not_a_number) +atom.integer:load( + string -- a string representing an integer +) + +This method returns an integer represented by the given string. If the string doesn't represent a valid integer, then not-a-number is returned. + +--]]-- +function integer:load(str) + if type(str) ~= "string" then + error("String expected") + elseif str == "" then + return nil + else + local num = tonumber(str) + if is_integer(num) then return num else return not_a_number end + end +end +--//-- + +--[[-- +atom.integer.invalid + +This represents an invalid integer. + +--]]-- +integer.invalid = not_a_number +--// + + + +------------ +-- number -- +------------ + +number = create_new_type("number") + +--[[-- +int = -- a number or atom.number.invalid (atom.not_a_number) +atom.number:load( + string -- a string representing a number +) + +This method returns a number represented by the given string. If the string doesn't represent a valid number, then not-a-number is returned. + +--]]-- +function number:load(str) + if type(str) ~= "string" then + error("String expected") + elseif str == "" then + return nil + else + return tonumber(str) or not_a_number + end +end +--//-- + +--[[-- +atom.number.invalid + +This represents an invalid number. + +--]]-- +number.invalid = not_a_number +--//-- + + + +-------------- +-- fraction -- +-------------- + +fraction = create_new_type("fraction") + +--[[-- +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 +) + +This function returns the greatest common divisor (GCD) of two or 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 +--//-- + +--[[-- +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 +) + +This function returns the least common multiple (LCD) of two or 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.fraction.invalid + +Value representing an invalid fraction. + +--]]-- +fraction.invalid = fraction:_create{ + numerator = not_a_number, denominator = not_a_number, invalid = true +} +--//-- + +--[[-- +frac = -- fraction +atom.fraction:new( + numerator, -- numerator + denominator -- denominator +) + +This method creates a new fraction. + +--]]-- +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 +--//-- + +--[[-- +frac = -- fraction represented by the given string +atom.fraction:load( + string -- string representation of a fraction +) + +This method returns a fraction represented by the given string. + +--]]-- +function fraction:load(str) + if str == "" then + return nil + 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 +--//-- + +function fraction:__tostring() + if self.invalid then + return "not_a_fraction" + else + return self.numerator .. "/" .. self.denominator + end +end + +function fraction.__eq(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return + value1.numerator == value2.numerator and + value1.denominator == value2.denominator + end +end + +function fraction.__lt(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.float < value2.float + end +end + +function fraction.__le(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.float <= value2.float + end +end + +function fraction:__unm() + return fraction(-self.numerator, self.denominator) +end + +do + + local function extract(value1, value2) + local n1, d1, n2, d2 + if getmetatable(value1) == fraction then + n1 = value1.numerator + d1 = value1.denominator + elseif type(value1) == "number" then + n1 = value1 + d1 = 1 + else + error("Left operand of operator has wrong type.") + end + if getmetatable(value2) == fraction then + n2 = value2.numerator + d2 = value2.denominator + elseif type(value2) == "number" then + n2 = value2 + d2 = 1 + else + error("Right operand of operator has wrong type.") + end + return n1, d1, n2, d2 + end + + function fraction.__add(value1, value2) + local n1, d1, n2, d2 = extract(value1, value2) + return fraction(n1 * d2 + n2 * d1, d1 * d2) + end + + function fraction.__sub(value1, value2) + local n1, d1, n2, d2 = extract(value1, value2) + return fraction(n1 * d2 - n2 * d1, d1 * d2) + end + + function fraction.__mul(value1, value2) + local n1, d1, n2, d2 = extract(value1, value2) + return fraction(n1 * n2, d1 * d2) + end + + function fraction.__div(value1, value2) + local n1, d1, n2, d2 = extract(value1, value2) + return fraction(n1 * d2, d1 * n2) + end + + function fraction.__pow(value1, value2) + local n1, d1, n2, d2 = extract(value1, value2) + local n1_abs = math.abs(n1) + local d1_abs = math.abs(d1) + local n2_abs = math.abs(n2) + local d2_abs = math.abs(d2) + local numerator, denominator + if d2_abs == 1 then + numerator = n1_abs + denominator = d1_abs + else + numerator = 0 + while true do + local t = numerator ^ d2_abs + if t == n1_abs then break end + if not (t < n1_abs) then return value1.float / value2.float end + numerator = numerator + 1 + end + denominator = 1 + while true do + local t = denominator ^ d2_abs + if t == d1_abs then break end + if not (t < d1_abs) then return value1.float / value2.float end + denominator = denominator + 1 + end + end + if n1 < 0 then + if d2_abs % 2 == 1 then + numerator = -numerator + else + return fraction.invalid + end + end + if n2 < 0 then + numerator, denominator = denominator, numerator + end + return fraction(numerator ^ n2_abs, denominator ^ n2_abs) + end + +end + + + +---------- +-- date -- +---------- + +date = create_new_type("date") + +do + local c1 = 365 -- days of a non-leap year + local c4 = 4 * c1 + 1 -- days of a full 4 year cycle + local c100 = 25 * c4 - 1 -- days of a full 100 year cycle + local c400 = 4 * c100 + 1 -- days of a full 400 year cycle + local get_month_offset -- function returning days elapsed within + -- the given year until the given month + -- (exclusive the given month) + do + local normal_month_offsets = {} + local normal_month_lengths = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + } + local sum = 0 + for i = 1, 12 do + normal_month_offsets[i] = sum + sum = sum + normal_month_lengths[i] + end + function get_month_offset(year, month) + if + (((year % 4 == 0) and not (year % 100 == 0)) or (year % 400 == 0)) + and month > 2 + then + return normal_month_offsets[month] + 1 + else + return normal_month_offsets[month] + end + end + 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 + ) + + This function calculates the days from January 1st 1970 for a given year, month and day. + + --]]-- + 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) + --//-- + + --[[-- + 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 + ) + + Given the days from January 1st 1970 this function returns year, month and day. + + --]]-- + 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 + --//-- +end + +--[[-- +atom.date.invalid + +Value representing an invalid date. + +--]]-- +date.invalid = date:_create{ + jd = not_a_number, + year = not_a_number, month = not_a_number, day = not_a_number, + invalid = true +} +--//-- + +--[[-- +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 +} + +This method returns a new date value, based on given data. + +--]]-- +function date:new(args) + local args = args + if type(args) == "number" then args = { jd = args } end + if type(args) == "table" then + local year, month, day = args.year, args.month, args.day + local jd = args.jd + local iso_weekyear = args.iso_weekyear + local iso_week = args.iso_week + local iso_weekday = args.iso_weekday + local us_week = args.us_week + local us_weekday = args.us_weekday + if + type(year) == "number" and + type(month) == "number" and + type(day) == "number" + then + if + is_integer(year) and year >= 1 and year <= 9999 and + is_integer(month) and month >= 1 and month <= 12 and + is_integer(day) and day >= 1 and day <= 31 + then + return date:_create{ + jd = date.ymd_to_jd(year, month, day), + year = year, month = month, day = day + } + else + return date.invalid + end + elseif type(jd) == "number" then + if is_integer(jd) and jd >= -719162 and jd <= 2932896 then + local year, month, day = date.jd_to_ymd(jd) + return date:_create{ + jd = jd, year = year, month = month, day = day + } + else + return date.invalid + end + elseif + type(year) == "number" and not iso_weekyear and + type(iso_week) == "number" and + type(iso_weekday) == "number" + then + if + is_integer(year) and + is_integer(iso_week) and iso_week >= 0 and iso_week <= 53 and + is_integer(iso_weekday) and iso_weekday >= 1 and iso_weekday <= 7 + then + local jan4 = date{ year = year, month = 1, day = 4 } + local reference = jan4 - jan4.iso_weekday - 7 -- Sun. of week -1 + return date(reference + 7 * iso_week + iso_weekday) + else + return date.invalid + end + elseif + type(iso_weekyear) == "number" and not year and + type(iso_week) == "number" and + type(iso_weekday) == "number" + then + if + is_integer(iso_weekyear) and + is_integer(iso_week) and iso_week >= 0 and iso_week <= 53 and + is_integer(iso_weekday) and iso_weekday >= 1 and iso_weekday <= 7 + then + local guessed = date{ + year = iso_weekyear, + iso_week = iso_week, + iso_weekday = iso_weekday + } + if guessed.invalid or guessed.iso_weekyear == iso_weekyear then + return guessed + else + local year + if iso_week <= 1 then + year = iso_weekyear - 1 + elseif iso_week >= 52 then + year = iso_weekyear + 1 + else + error("Internal error in ISO week computation occured.") + end + return date{ + year = year, iso_week = iso_week, iso_weekday = iso_weekday + } + end + else + return date.invalid + end + elseif + type(year) == "number" and + type(us_week) == "number" and + type(us_weekday) == "number" + then + if + is_integer(year) and + is_integer(us_week) and us_week >= 0 and us_week <= 54 and + is_integer(us_weekday) and us_weekday >= 1 and us_weekday <= 7 + then + local jan1 = date{ year = year, month = 1, day = 1 } + local reference = jan1 - jan1.us_weekday - 7 -- Sat. of week -1 + return date(reference + 7 * us_week + us_weekday) + else + return date.invalid + end + end + end + error("Illegal arguments passed to date constructor.") +end +--//-- + +--[[-- +atom.date:get_current() + +This function returns today's date. + +--]]-- +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 +) + +This method returns a date represented by the given string. + +--]]-- +function date:load(str) + if str == "" then + return nil + 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 +--//-- + +function date:__tostring() + if self.invalid then + return "invalid_date" + else + return string.format( + "%04i-%02i-%02i", self.year, self.month, self.day + ) + end +end + +function date.getters:midnight() + return time{ year = self.year, month = self.month, day = self.day } +end + +function date.getters:midday() + return time{ + year = self.year, month = self.month, day = self.day, + hour = 12 + } +end + +function date.getters:iso_weekday() -- 1 = Monday + return (self.jd + 3) % 7 + 1 +end + +function date.getters:us_weekday() -- 1 = Sunday + return (self.jd + 4) % 7 + 1 +end + +function date.getters:iso_weekyear() -- ISO week-numbering year + local year, month, day = self.year, self.month, self.day + local iso_weekday = self.iso_weekday + if month == 1 then + if + (day == 3 and iso_weekday == 7) or + (day == 2 and iso_weekday >= 6) or + (day == 1 and iso_weekday >= 5) + then + return year - 1 + end + elseif month == 12 then + if + (day == 29 and iso_weekday == 1) or + (day == 30 and iso_weekday <= 2) or + (day == 31 and iso_weekday <= 3) + then + return year + 1 + end + end + return year +end + +function date.getters:iso_week() + local jan4 = date{ year = self.iso_weekyear, month = 1, day = 4 } + local reference = jan4.jd - jan4.iso_weekday - 6 -- monday of week 0 + return math.floor((self.jd - reference) / 7) +end + +function date.getters:us_week() + local jan1 = date{ year = self.year, month = 1, day = 1 } + local reference = jan1.jd - jan1.us_weekday - 6 -- sunday of week 0 + return math.floor((self.jd - reference) / 7) +end + +function date.__eq(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.jd == value2.jd + end +end + +function date.__lt(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.jd < value2.jd + end +end + +function date.__le(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.jd <= value2.jd + end +end + +function date.__add(value1, value2) + if getmetatable(value1) == date then + if getmetatable(value2) == date then + error("Can not add two dates.") + elseif type(value2) == "number" then + return date(value1.jd + value2) + else + error("Right operand of '+' operator has wrong type.") + end + elseif type(value1) == "number" then + if getmetatable(value2) == date then + return date(value1 + value2.jd) + else + error("Assertion failed") + end + else + error("Left operand of '+' operator has wrong type.") + end +end + +function date.__sub(value1, value2) + if not getmetatable(value1) == date then + error("Left operand of '-' operator has wrong type.") + end + if getmetatable(value2) == date then + return value1.jd - value2.jd -- TODO: transform to interval + elseif type(value2) == "number" then + return date(value1.jd - value2) + else + error("Right operand of '-' operator has wrong type.") + end +end + + + +--------------- +-- timestamp -- +--------------- + +timestamp = create_new_type("timestamp") + +--[[-- +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 +) + +Given the year, month, day, hour, minute and second, this function returns the number of seconds since January 1st 1970 00:00. + +--]]-- +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 +--//-- + +--[[-- +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 +) + +Given the seconds since January 1st 1970 00:00, this function returns the year, month, day, hour, minute and second. + +--]]-- +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 +--//-- + +--[[-- +timestamp.invalid + +Value representing an invalid timestamp. + +--]]-- +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 +} +--//-- + +--[[-- +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 +} + +This method returns a new timestamp value, based on given data. + +--]]-- +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 +--//-- + +--[[-- +ts = -- current date/time as timestamp +atom.timestamp:get_current() + +This function returns the current date and time as timestamp. + +--]]-- +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 +) + +This method returns a timestamp represented by the given string. + +--]]-- +function timestamp:load(str) + if str == "" then + return nil + 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 +} +--//-- + +--[[-- +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 +} + +This method returns a new time value, based on given data. + +--]]-- +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 +--//-- + +--[[-- +t = -- current time of day +atom.time:get_current() + +This method returns the current time of the day. + +--]]-- +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 +) + +This method returns a time represented by the given string. + +--]]-- +function time:load(str) + if str == "" then + return nil + 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 +--//-- + +function time:__tostring() + if self.invalid then + return "invalid_time" + else + return string.format( + "%02i:%02i:%02i", + self.hour, self.minute, self.second + ) + end +end + +function time.__eq(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.dsec == value2.dsec + end +end + +function time.__lt(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.dsec < value2.dsec + end +end + +function time.__le(value1, value2) + if value1.invalid or value2.invalid then + return false + else + return value1.dsec <= value2.dsec + end +end + +function time.__add(value1, value2) + if getmetatable(value1) == time then + if getmetatable(value2) == time then + error("Can not add two times.") + elseif type(value2) == "number" then + return time((value1.dsec + value2) % 86400) + else + error("Right operand of '+' operator has wrong type.") + end + elseif type(value1) == "number" then + if getmetatable(value2) == time then + return time((value1 + value2.dsec) % 86400) + else + error("Assertion failed") + end + else + error("Left operand of '+' operator has wrong type.") + end +end + +function time.__sub(value1, value2) + if not getmetatable(value1) == time then + error("Left operand of '-' operator has wrong type.") + end + if getmetatable(value2) == time then + return value1.dsec - value2.dsec -- TODO: transform to interval + elseif type(value2) == "number" then + return time((value1.dsec - value2) % 86400) + else + error("Right operand of '-' operator has wrong type.") + end +end + +function time.__concat(value1, value2) + local mt1, mt2 = getmetatable(value1), getmetatable(value2) + if mt1 == date and mt2 == time then + return timestamp{ + year = value1.year, month = value1.month, day = value1.day, + hour = value2.hour, minute = value2.minute, second = value2.second + } + elseif mt1 == time and mt2 == date then + return timestamp{ + year = value2.year, month = value2.month, day = value2.day, + hour = value1.hour, minute = value1.minute, second = value1.second + } + elseif mt1 == time then + error("Right operand of '..' operator has wrong type.") + elseif mt2 == time then + error("Left operand of '..' operator has wrong type.") + else + error("Assertion failed") + end +end + diff -r 000000000000 -r 9fdfb27f8e67 libraries/atom/benchmark.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/atom/benchmark.sh Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,12 @@ +#!/bin/ksh +luac -o atom-compiled.lua atom.lua +luac -s -o atom-stripped.lua atom.lua +echo "100x loading source:" +time for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100; do lua -l atom -e ''; done +echo "100x loading precompiled bytecode:" +time for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100; do lua -l atom-compiled -e ''; done +echo "100x loading stripped bytecode:" +time for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100; do lua -l atom-stripped -e ''; done +echo "100x loading nothing:" +time for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100; do lua -e ''; done +rm atom-compiled.lua atom-stripped.lua diff -r 000000000000 -r 9fdfb27f8e67 libraries/extos/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/extos/Makefile Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,10 @@ +include ../../Makefile.options + +extos.so: extos.o + $(LD) $(LDFLAGS) -lrt -lcrypt -o extos.$(SLIB_EXT) extos.o + +extos.o: extos.c + $(CC) -c $(CFLAGS) -o extos.o extos.c + +clean:: + rm -f extos.so extos.o diff -r 000000000000 -r 9fdfb27f8e67 libraries/extos/extos.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/extos/extos.c Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,351 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXTOS_MAX_ERRLEN 80 +#define EXTOS_EXEC_MAX_ARGS 64 + +static lua_Number extos_monotonic_start_time; + +static int extos_pfilter(lua_State *L) { + int i, result, exit_status, status_pipe_len; + const char *in_buf; + size_t in_len; + size_t in_pos = 0; + const char *filename; + const char *args[EXTOS_EXEC_MAX_ARGS+2]; + int pipe_status[2]; + int pipe_in[2]; + int pipe_out[2]; + int pipe_err[2]; + pid_t child; + char status_buf[1]; + char *out_buf = NULL; + size_t out_len = 1024; + size_t out_pos = 0; + char *err_buf = NULL; + size_t err_len = 1024; + size_t err_pos = 0; + void *old_sigpipe_action; + struct pollfd fds[3]; + int in_closed = 0; + int out_closed = 0; + int err_closed = 0; + void *newptr; + char errmsg[EXTOS_MAX_ERRLEN+1]; + in_buf = luaL_optlstring(L, 1, "", &in_len); + filename = luaL_checkstring(L, 2); + args[0] = filename; + for (i = 0; i < EXTOS_EXEC_MAX_ARGS; i++) { + if (lua_isnoneornil(L, 3+i)) break; + else args[i+1] = luaL_checkstring(L, 3+i); + } + if (!lua_isnoneornil(L, 3+i)) { + return luaL_error(L, "Too many arguments for pfilter call."); + } + args[i+1] = 0; + // status pipe for internal communication + if (pipe(pipe_status) < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A0; + } + // stdin + if (pipe(pipe_in) < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A1; + } + if (in_len) { + do result = fcntl(pipe_in[1], F_SETFL, O_NONBLOCK); while (result < 0 && errno == EINTR); + } else { + do result = close(pipe_in[1]); while (result < 0 && errno == EINTR); + in_closed = 1; + } + if (result < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A2; + } + // stdout + if (pipe(pipe_out) < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A2; + } + do result = fcntl(pipe_out[0], F_SETFL, O_NONBLOCK); while (result < 0 && errno == EINTR); + if (result < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A3; + } + // stderr + if (pipe(pipe_err) < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A3; + } + do result = fcntl(pipe_err[0], F_SETFL, O_NONBLOCK); while (result < 0 && errno == EINTR); + if (result < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A4; + } + // fork + child = fork(); + if (child < 0) { + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + goto extos_pfilter_error_A4; + } + // skip error handling + goto extos_pfilter_success_A; + // error handling + extos_pfilter_error_A4: + do result = close(pipe_err[0]); while (result < 0 && errno == EINTR); + do result = close(pipe_err[1]); while (result < 0 && errno == EINTR); + extos_pfilter_error_A3: + do result = close(pipe_out[0]); while (result < 0 && errno == EINTR); + do result = close(pipe_out[1]); while (result < 0 && errno == EINTR); + extos_pfilter_error_A2: + do result = close(pipe_in[0]); while (result < 0 && errno == EINTR); + do result = close(pipe_in[1]); while (result < 0 && errno == EINTR); + extos_pfilter_error_A1: + do result = close(pipe_status[0]); while (result < 0 && errno == EINTR); + do result = close(pipe_status[1]); while (result < 0 && errno == EINTR); + extos_pfilter_error_A0: + return luaL_error(L, "Unexpected error in pfilter: %s", errmsg); + // end of error handling + extos_pfilter_success_A: + if (child) { // parent + old_sigpipe_action = signal(SIGPIPE, SIG_IGN); + do result = close(pipe_status[1]); while (result < 0 && errno == EINTR); + if (result < 0) goto extos_pfilter_error_B; + do result = close(pipe_in[0]); while (result < 0 && errno == EINTR); + if (result < 0) goto extos_pfilter_error_B; + do result = close(pipe_out[1]); while (result < 0 && errno == EINTR); + if (result < 0) goto extos_pfilter_error_B; + do result = close(pipe_err[1]); while (result < 0 && errno == EINTR); + if (result < 0) goto extos_pfilter_error_B; + out_buf = malloc(out_len * sizeof(char)); + if (!out_buf) goto extos_pfilter_error_B; + err_buf = malloc(err_len * sizeof(char)); + if (!err_buf) goto extos_pfilter_error_B; + while (!in_closed || !out_closed || !err_closed) { + i = 0; + if (!in_closed) { + fds[i].fd = pipe_in[1]; + fds[i].events = POLLOUT; + i++; + } + if (!out_closed) { + fds[i].fd = pipe_out[0]; + fds[i].events = POLLIN; + i++; + } + if (!err_closed) { + fds[i].fd = pipe_err[0]; + fds[i].events = POLLIN; + i++; + } + do result = poll(fds, i, -1); while (result < 0 && errno == EINTR); + if (result < 0) goto extos_pfilter_error_B; + if (!in_closed) { + do result = write(pipe_in[1], in_buf+in_pos, in_len-in_pos); while (result < 0 && errno == EINTR); + if (result < 0) { + if (errno == EPIPE) { + do result = close(pipe_in[1]); while (result < 0 && errno == EINTR); + in_closed = 1; + } else if (errno != EAGAIN) { + goto extos_pfilter_error_B; + } + } else { + in_pos += result; + if (in_pos == in_len) { + do result = close(pipe_in[1]); while (result < 0 && errno == EINTR); + in_closed = 1; + } + } + } + if (!out_closed) { + do result = read(pipe_out[0], out_buf+out_pos, out_len-out_pos); while (result < 0 && errno == EINTR); + if (result < 0) { + if (errno != EAGAIN) goto extos_pfilter_error_B; + } else if (result == 0) { + do result = close(pipe_out[0]); while (result < 0 && errno == EINTR); + out_closed = 1; + } else { + out_pos += result; + if (out_pos == out_len) { + out_len *= 2; + newptr = realloc(out_buf, out_len * sizeof(char)); + if (!newptr) goto extos_pfilter_error_B; + out_buf = newptr; + } + } + } + if (!err_closed) { + do result = read(pipe_err[0], err_buf+err_pos, err_len-err_pos); while (result < 0 && errno == EINTR); + if (result < 0) { + if (errno != EAGAIN) goto extos_pfilter_error_B; + } else if (result == 0) { + do result = close(pipe_err[0]); while (result < 0 && errno == EINTR); + err_closed = 1; + } else { + err_pos += result; + if (err_pos == err_len) { + err_len *= 2; + newptr = realloc(err_buf, err_len * sizeof(char)); + if (!newptr) goto extos_pfilter_error_B; + err_buf = newptr; + } + } + } + } + lua_pushlstring(L, out_buf, out_pos); + free(out_buf); + out_buf = NULL; + lua_pushlstring(L, err_buf, err_pos); + free(err_buf); + err_buf = NULL; + do result = waitpid(child, &exit_status, 0); while (result < 0 && errno == EINTR); + child = 0; + if (result < 0) goto extos_pfilter_error_B; + do status_pipe_len = read(pipe_status[0], status_buf, 1); while (status_pipe_len < 0 && errno == EINTR); + if (status_pipe_len < 0) goto extos_pfilter_error_B; + do result = close(pipe_status[0]); while (result < 0 && errno == EINTR); + signal(SIGPIPE, old_sigpipe_action); + if (status_pipe_len == 0) { + if (WIFEXITED(exit_status)) lua_pushinteger(L, WEXITSTATUS(exit_status)); + else lua_pushinteger(L, -WTERMSIG(exit_status)); + return 3; + } else if (status_buf[0] == 0) { + return luaL_error(L, "Error in pfilter while reopening standard file descriptors in child process."); + } else { + strerror_r(status_buf[0], errmsg, EXTOS_MAX_ERRLEN+1); + lua_pushnil(L); + lua_pushfstring(L, "Could not execute \"%s\": %s", filename, errmsg); + return 2; + } + extos_pfilter_error_B: + signal(SIGPIPE, old_sigpipe_action); + strerror_r(errno, errmsg, EXTOS_MAX_ERRLEN+1); + if (out_buf) free(out_buf); + if (err_buf) free(err_buf); + if (!in_closed) { + do result = close(pipe_in[1]); while (result < 0 && errno == EINTR); + } + if (!out_closed) { + do result = close(pipe_out[0]); while (result < 0 && errno == EINTR); + } + if (!err_closed) { + do result = close(pipe_err[0]); while (result < 0 && errno == EINTR); + } + if (child) do result = waitpid(child, &exit_status, 0); while (result < 0 && errno == EINTR); + return luaL_error(L, "Unexpected error in pfilter: %s", errmsg); + } else { // child + do result = close(pipe_status[0]); while (result < 0 && errno == EINTR); + do result = close(pipe_in[1]); while (result < 0 && errno == EINTR); + do result = close(pipe_out[0]); while (result < 0 && errno == EINTR); + do result = close(0); while (result < 0 && errno == EINTR); + do result = close(1); while (result < 0 && errno == EINTR); + do result = close(2); while (result < 0 && errno == EINTR); + do result = dup(pipe_in[0]); while (result < 0 && errno == EINTR); + if (result != 0) goto extos_pfilter_error_fd_remapping; + do result = dup(pipe_out[1]); while (result < 0 && errno == EINTR); + if (result != 1) goto extos_pfilter_error_fd_remapping; + do result = dup(pipe_err[1]); while (result < 0 && errno == EINTR); + if (result != 2) goto extos_pfilter_error_fd_remapping; + execvp(filename, args); + status_buf[0] = errno; + do result = write(pipe_status[1], status_buf, 1); while (result < 0 && errno == EINTR); + _exit(0); + extos_pfilter_error_fd_remapping: + status_buf[0] = 0; + do result = write(pipe_status[1], status_buf, 1); while (result < 0 && errno == EINTR); + _exit(0); + } +} + +static int extos_listdir(lua_State *L) { + DIR *dir; + int i = 1; + struct dirent entry_buffer; + struct dirent *entry; + dir = opendir(luaL_checkstring(L, 1)); + if (!dir) { + lua_pushnil(L); + lua_pushliteral(L, "Could not list directory."); + return 2; + } + lua_settop(L, 0); + lua_newtable(L); // 1 + while (1) { + readdir_r(dir, &entry_buffer, &entry); + if (!entry) break; + // Linux doesn't have d_namlen + //lua_pushlstring(L, entry->d_name, entry->d_namlen); + lua_pushstring(L, entry->d_name); + lua_rawseti(L, 1, i++); + } + closedir(dir); + return 1; +} + +static int extos_crypt(lua_State *L) { + char *key; + char *salt; + char *result; + key = luaL_checkstring(L, 1); + salt = luaL_checkstring(L, 2); + result = crypt(key, salt); // TODO: Call not thread safe + if (result) lua_pushstring(L, result); + else lua_pushnil(L); + return 1; +} + +static int extos_hires_time(lua_State *L) { + struct timespec tp; + if (clock_gettime(CLOCK_REALTIME, &tp)) { + return luaL_error(L, "Could not access CLOCK_REALTIME."); + } + lua_pushnumber(L, tp.tv_sec + 0.000000001 * tp.tv_nsec); + return 1; +} + +// returns time in seconds since loading the library +static int extos_monotonic_hires_time(lua_State *L) { + struct timespec tp; + if (clock_gettime(CLOCK_MONOTONIC, &tp)) { + return luaL_error(L, "Could not access CLOCK_MONOTONIC."); + } + lua_pushnumber(L, + tp.tv_sec + 0.000000001 * tp.tv_nsec - extos_monotonic_start_time + ); + return 1; +} + +int luaopen_extos(lua_State *L) { + { + struct timespec tp; + if (clock_gettime(CLOCK_MONOTONIC, &tp)) { + return luaL_error(L, "Could not access monotonic hires time."); + } + extos_monotonic_start_time = tp.tv_sec + 0.000000001 * tp.tv_nsec; + } + lua_getglobal(L, "os"); + lua_pushcfunction(L, extos_pfilter); + lua_setfield(L, -2, "pfilter"); + lua_pushcfunction(L, extos_listdir); + lua_setfield(L, -2, "listdir"); + lua_pushcfunction(L, extos_crypt); + lua_setfield(L, -2, "crypt"); + lua_pushcfunction(L, extos_hires_time); + lua_setfield(L, -2, "hires_time"); + lua_pushcfunction(L, extos_monotonic_hires_time); + lua_setfield(L, -2, "monotonic_hires_time"); + return 0; +} diff -r 000000000000 -r 9fdfb27f8e67 libraries/luatex/luatex.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/luatex/luatex.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,148 @@ +#!/usr/bin/env lua + +local _G = _G +local _VERSION = _VERSION +local assert = assert +local collectgarbage = collectgarbage +local dofile = dofile +local error = error +local getfenv = getfenv +local getmetatable = getmetatable +local ipairs = ipairs +local load = load +local loadfile = loadfile +local loadstring = loadstring +local module = module +local next = next +local pairs = pairs +local pcall = pcall +local print = print +local rawequal = rawequal +local rawget = rawget +local rawset = rawset +local require = require +local select = select +local setfenv = setfenv +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local type = type +local unpack = unpack +local xpcall = xpcall + +local coroutine = coroutine +local debug = debug +local io = io +local math = math +local os = os +local package = package +local string = string +local table = table + +require("multirand") +local multirand = multirand + +module(...) + +temp_dir = false -- has to be set to a private directory (/tmp can be unsafe) + +function escape(str) + return ( + string.gsub( + str, + "[\001-\031\127\\#$&~_^%%{}]", + function(char) + local b = string.byte(char) + if (b > 1 and b < 31) or b == 127 then + return " " + elseif + char == "#" or char == "$" or char == "&" or char == "_" or + char == "%" or char == "{" or char == "}" + then + return "\\" .. char + else + return "\\symbol{" .. b .. "}" + end + end + ) + ) +end + +document_methods = {} + +document_mt = { + __index = document_methods, + __call = function(...) return document_methods.write(...) end +} + +function new_document() + return setmetatable({}, document_mt) +end + +function document_methods:write(...) + local i = 1 + while true do + local v = select(i, ...) + if v == nil then + break + end + self[#self+1] = v + i = i + 1 + end +end + +function document_methods:get_latex() + local str = table.concat(self) + for i in ipairs(self) do + self[i] = nil + end + self[1] = str + return str +end + +function document_methods:get_pdf() + -- TODO: proper escaping of shell commands (should not be a real risk) + if not temp_dir then + error("luatex.temp_dir not set") + end + local basename = temp_dir .. "/tmp.luatex_" .. multirand.string(16) + local latex_file = assert(io.open(basename .. ".tex", "w")) + latex_file:write(self:get_latex()) + latex_file:close() + local result = os.execute( + 'latex -output-format=pdf "-output-directory=' .. temp_dir .. '" ' .. + basename .. '< /dev/null > /dev/null 2> /dev/null' + ) + if result ~= 0 then + error('LaTeX failed, see "' .. basename .. '.log" for details.') + end + local pdf_file = assert(io.open(basename .. ".pdf", "r")) + local pdf_data = pdf_file:read("*a") + pdf_file:close() + os.execute('rm -f "' .. basename .. '.*"') + return pdf_data +end + +--[[ + +require("luatex") +luatex.temp_dir = "." + +local tex = luatex.new_document() + +tex "\\documentclass[a4paper,12pt]{article}\n" +tex "\\usepackage{german}\n" +tex "\\usepackage{amsfonts}\n" +tex "\\usepackage{amssymb}\n" +tex "\\usepackage{ulem}\n" +tex "\\pagestyle{headings}\n" +tex "\\begin{document}\n" +tex "\\title{Demo}\n" +tex "\\author{John Doe}\n" +tex "\\date{\\small 25. August 2008}\n" +tex "\\maketitle\n" +tex "\\end{document}\n" + +local pdf = tex:get_pdf() + +--]] diff -r 000000000000 -r 9fdfb27f8e67 libraries/mondelefant/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/mondelefant/Makefile Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,13 @@ +include ../../Makefile.options + +mondelefant_native.so: mondelefant_native.o + $(LD) $(LDFLAGS) $(LDFLAGS_PGSQL) -o mondelefant_native.$(SLIB_EXT) mondelefant_native.o -lpq + +mondelefant_native.o: mondelefant_native.c + $(CC) -c $(CFLAGS) $(CFLAGS_PGSQL) -o mondelefant_native.o mondelefant_native.c + +test:: mondelefant_native.so mondelefant.lua + lua -l mondelefant + +clean:: + rm -f mondelefant_native.so mondelefant_native.o diff -r 000000000000 -r 9fdfb27f8e67 libraries/mondelefant/example.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/mondelefant/example.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,86 @@ +#!/usr/bin/env lua +require("mondelefant") + +-- Standarddatenbankverbindung ist globale Variable 'db' +function mondelefant.class_prototype:get_db_conn() return db end + +-- Verbindung aufbauen +db = assert(mondelefant.connect{engine='postgresql', dbname='test'}) + +Product = mondelefant.new_class{ table = "product" } +ProductVariant = mondelefant.new_class{ table = "product_variant" } + +Product:add_reference{ + mode = "1m", + to = ProductVariant, + this_key = "number", + that_key = "product_number", + ref = "product_variants", + back_ref = "product", + --default_order = '"number"' +} + +ProductVariant:add_reference{ + mode = "m1", + to = Product, + this_key = "product_number", + that_key = "number", + ref = "product", + back_ref = nil, + --default_order = '"id"' +} + +p = Product:new_selector():single_object_mode():add_where{"name=?", "Noodles"}:exec() + + +--[[ +-- Neue Datenbankklasse definieren +Product = mondelefant.new_class{ table = '"product"' } + +-- Methode der Klasse, um sofort eine alphabetische Liste aller Produkte +-- zu bekommen +function Product:get_all_ordered_by_name() + local selector = self:new_selector() + selector:add_order_by('"name"') + selector:add_order_by('"id"') + return selector:exec() +end + +function Product.object_get:name_length(key) + local value = #self.name + self._data.name_length = value + return value +end + +function Product.object_set:quality(value) + if value == "perfect" or value == "good" or value == "trash" then + self._data.quality = value + else + self._data.quality = nil + end + self._dirty.quality = true +end + +-- Methode der Listen, um sie auszugeben +function Product.list:print() + for i, product in ipairs(self) do + print(product.id, product.name, product.name_length) + end +end + +products = Product:get_all_ordered_by_name() +products:print() +products[1].quality = "perfect" +print(products[1].quality) +products[2].quality = "I don't know." +print(products[2].quality) +products[3].name = "Produkt Eins!" +products[3].quality = "perfect" +products[3]:save() + +sel = db:new_selector() +sel:from('"product_variant"') +sel:add_field("*") +sel:attach("m1", products, "product_id", "id", "product", "product_variants") +product_variants = sel:exec() +--]] \ No newline at end of file diff -r 000000000000 -r 9fdfb27f8e67 libraries/mondelefant/mondelefant.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/mondelefant/mondelefant.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,845 @@ +#!/usr/bin/env lua + + +--------------------------- +-- module initialization -- +--------------------------- + +local _G = _G +local _VERSION = _VERSION +local assert = assert +local collectgarbage = collectgarbage +local dofile = dofile +local error = error +local getfenv = getfenv +local getmetatable = getmetatable +local ipairs = ipairs +local load = load +local loadfile = loadfile +local loadstring = loadstring +local next = next +local pairs = pairs +local pcall = pcall +local print = print +local rawequal = rawequal +local rawget = rawget +local rawset = rawset +local select = select +local setfenv = setfenv +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local type = type +local unpack = unpack +local xpcall = xpcall + +local coroutine = coroutine +local io = io +local math = math +local os = os +local string = string +local table = table + +local add = table.insert + +_G[...] = require("mondelefant_native") +module(...) + + + +--------------- +-- selectors -- +--------------- + +selector_metatable = {} +selector_prototype = {} +selector_metatable.__index = selector_prototype + +local function init_selector(self, db_conn) + self._db_conn = db_conn + self._mode = "list" + self._fields = { sep = ", " } + self._distinct = false + self._distinct_on = {sep = ", ", expression} + self._from = { sep = " " } + self._where = { sep = " AND " } + self._group_by = { sep = ", " } + self._having = { sep = " AND " } + self._combine = { sep = " " } + self._order_by = { sep = ", " } + self._limit = nil + self._offset = nil + --[[ + self._lock = nil + self._lock_tables = { sep = ", " } + --]] + self._class = nil + self._attach = nil + return self +end + +function connection_prototype:new_selector() + return init_selector(setmetatable({}, selector_metatable), self) +end + +function selector_prototype:get_db_conn() + return self._db_conn +end + +-- TODO: selector clone? + +function selector_prototype:single_object_mode() + self._mode = "object" + return self +end + +function selector_prototype:optional_object_mode() + self._mode = "opt_object" + return self +end + +function selector_prototype:empty_list_mode() + self._mode = "empty_list" + return self +end + +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 + +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 + +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:condition(condition) + else + add(self._from, "ON") + add(self._from, condition) + end + end + return self +end + +function selector_prototype:add_where(expression) + add(self._where, expression) + return self +end + +function selector_prototype:add_group_by(expression) + add(self._group_by, expression) + return self +end + +function selector_prototype:add_having(expression) + add(self._having, expression) + return self +end + +function selector_prototype:add_combine(expression) + add(self._combine, expression) + return self +end + +function selector_prototype:add_order_by(expression) + add(self._order_by, expression) + return self +end + +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 + +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 + +function selector_prototype:reset_fields() + for idx in ipairs(self._fields) do + self._fields[idx] = nil + end + return self +end + +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 + +function selector_prototype:join(...) -- NOTE: alias for add_from + return self:add_from(...) +end + +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 + +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:condition(condition) + else + add(self._from, "ON") + add(self._from, condition) + end + end + return self +end + +function selector_prototype:union(expression) + self:add_combine{"UNION $", {expression}} + return self +end + +function selector_prototype:union_all(expression) + self:add_combine{"UNION ALL $", {expression}} + return self +end + +function selector_prototype:intersect(expression) + self:add_combine{"INTERSECT $", {expression}} + return self +end + +function selector_prototype:intersect_all(expression) + self:add_combine{"INTERSECT ALL $", {expression}} + return self +end + +function selector_prototype:except(expression) + self:add_combine{"EXCEPT $", {expression}} + return self +end + +function selector_prototype:except_all(expression) + self:add_combine{"EXCEPT ALL $", {expression}} + return self +end + +function selector_prototype:set_class(class) + self._class = class + return self +end + +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 + +-- TODO: many-to-many relations + +function selector_metatable:__tostring() + local parts = {sep = " "} + add(parts, "SELECT") + if self._distinct then + add(parts, "DISTINCT") + elseif #self._distinct_on > 0 then + add(parts, {"DISTINCT ON ($)", self._distinct_on}) + end + add(parts, {"$", self._fields}) + if #self._from > 0 then + add(parts, {"FROM $", self._from}) + end + if #self._mode == "empty_list" then + add(parts, "WHERE FALSE") + elseif #self._where > 0 then + add(parts, {"WHERE $", self._where}) + end + if #self._group_by > 0 then + add(parts, {"GROUP BY $", self._group_by}) + end + if #self._having > 0 then + add(parts, {"HAVING $", self._having}) + end + for i, v in ipairs(self._combine) do + add(parts, v) + end + if #self._order_by > 0 then + add(parts, {"ORDER BY $", self._order_by}) + end + if self._mode == "empty_list" then + add(parts, "LIMIT 0") + elseif self._mode ~= "list" then + add(parts, "LIMIT 1") + elseif self._limit then + add(parts, "LIMIT " .. self._limit) + end + if self._offset then + add(parts, "OFFSET " .. self._offset) + end + return self._db_conn:assemble_command{"$", parts} +end + +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 + +function selector_prototype:exec() + local db_error, result = self:try_exec() + if db_error then + db_error:escalate() + else + return result + end +end + + + +----------------- +-- attachments -- +----------------- + +local function attach_key(row, fields) + local t = type(fields) + if t == "string" then + return tostring(row[fields]) + elseif t == "table" then + local r = {} + for idx, field in ipairs(fields) do + r[idx] = string.format("%q", row[field]) + end + return table.concat(r) + else + error("Field information for 'mondelefant.attach' is neither a string nor a table.") + end +end + +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 + + + +------------------ +-- model system -- +------------------ + +class_prototype.primary_key = "id" + +function class_prototype:get_db_conn() + error( + "Method mondelefant class(_prototype):get_db_conn() " .. + "has to be implemented." + ) +end + +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 + +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 + +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 + +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 + +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 + +function class_prototype:create_list() + local list = self:get_db_conn():create_list() + list._class = self + return list +end + +function class_prototype:new() + local object = self:get_db_conn():create_object() + object._class = self + object._new = true + return object +end + +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() + local primary_key_sql = { sep = ", " } + for idx, value in ipairs(primary_key) do + primary_key_sql[idx] = '"' .. value .. '"' + end + if self._new then + local fields = {sep = ", "} + local values = {sep = ", "} + for key, dummy in pairs(self._dirty or {}) do + add(fields, {'"$"', {key}}) + add(values, {'?', self[key]}) + end + if compat_returning then -- compatibility for PostgreSQL 8.1 + local db_error, db_result1, db_result2 = self._connection:try_query( + { + 'INSERT INTO $ ($) VALUES ($)', + {self._class:get_qualified_table()}, + fields, + values, + primary_key_sql + }, + "list", + { + 'SELECT currval(?)', + self._class.table .. '_id_seq' + }, + "object" + ) + if db_error then + return db_error + end + self.id = db_result2.id + else + local db_error, db_result = self._connection:try_query( + { + 'INSERT INTO $ ($) VALUES ($) RETURNING ($)', + {self._class:get_qualified_table()}, + fields, + values, + primary_key_sql + }, + "object" + ) + if db_error then + return db_error + end + for idx, value in ipairs(primary_key) do + self[value] = db_result[value] + end + end + self._new = false + else + local command_sets = {sep = ", "} + for key, dummy in pairs(self._dirty or {}) do + add(command_sets, {'"$" = ?', {key}, self[key]}) + end + if #command_sets >= 1 then + local primary_key_compare = {sep = " AND "} + for idx, value in ipairs(primary_key) do + primary_key_compare[idx] = { + "$ = ?", + {'"' .. value .. '"'}, + self[value] + } + end + local db_error = self._connection:try_query{ + 'UPDATE $ SET $ WHERE $', + {self._class:get_qualified_table()}, + command_sets, + primary_key_compare + } + if db_error then + return db_error + end + end + end + return nil +end + +function class_prototype.object:save() + local db_error = self:try_save() + if db_error then + db_error:escalate() + end + return self +end + +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 "} + for idx, value in ipairs(primary_key) do + primary_key_compare[idx] = { + "$ = ?", + {'"' .. value .. '"'}, + self[value] + } + end + return self._connection:try_query{ + 'DELETE FROM $ WHERE $', + {self._class:get_qualified_table()}, + primary_key_compare + } +end + +function class_prototype.object:destroy() + local db_error = self:try_destroy() + if db_error then + db_error:escalate() + end + return self +end + +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 + +function class_prototype.list.load(...) + return class_prototype.list.get_reference_selector(...):exec() +end + +function class_prototype.object:get_reference_selector(...) + local list = self._class:create_list() + list[1] = self + return list:get_reference_selector(...) +end + +function class_prototype.object.load(...) + return class_prototype.object.get_reference_selector(...):exec() +end + + +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 = { sep = ", " } + for i, object in ipairs(list) do + local id = object[this_key] + if id ~= nil then + ids[#ids+1] = {"?", id} + 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 ($)', {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 diff -r 000000000000 -r 9fdfb27f8e67 libraries/mondelefant/mondelefant_atom_connector.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/mondelefant/mondelefant_atom_connector.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,189 @@ +#!/usr/bin/env lua + +local _G = _G +local _VERSION = _VERSION +local assert = assert +local collectgarbage = collectgarbage +local dofile = dofile +local error = error +local getfenv = getfenv +local getmetatable = getmetatable +local ipairs = ipairs +local load = load +local loadfile = loadfile +local loadstring = loadstring +local module = module +local next = next +local pairs = pairs +local pcall = pcall +local print = print +local rawequal = rawequal +local rawget = rawget +local rawset = rawset +local require = require +local select = select +local setfenv = setfenv +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local type = type +local unpack = unpack +local xpcall = xpcall + +local coroutine = coroutine +local debug = debug +local io = io +local math = math +local os = os +local package = package +local string = string + +local mondelefant = require("mondelefant") +local atom = require("atom") + +module(...) + + +input_converters = setmetatable({}, { __mode = "k" }) + +input_converters["boolean"] = function(conn, value) + if value then return "TRUE" else return "FALSE" end +end + +input_converters["number"] = function(conn, value) + local str = tostring(value) + if string.find(str, "^[0-9%.e%-]+$") then + return str + else + return "'NaN'" + end +end + +input_converters[atom.fraction] = function(conn, value) + if value.invalid then + return "'NaN'" + else + local n, d = tostring(value.numerator), tostring(value.denominator) + if string.find(n, "^%-?[0-9]+$") and string.find(d, "^%-?[0-9]+$") then + return "(" .. n .. "::numeric / " .. d .. "::numeric)" + else + return "'NaN'" + end + end +end + +input_converters[atom.date] = function(conn, value) + return conn:quote_string(tostring(value)) .. "::date" +end + +input_converters[atom.timestamp] = function(conn, value) + return conn:quote_string(tostring(value)) -- don't define type +end + +input_converters[atom.time] = function(conn, value) + return conn:quote_string(tostring(value)) .. "::time" +end + + +output_converters = setmetatable({}, { __mode = "k" }) + +output_converters.int8 = function(str) return atom.integer:load(str) end +output_converters.int4 = function(str) return atom.integer:load(str) end +output_converters.int2 = function(str) return atom.integer:load(str) end + +output_converters.numeric = function(str) return atom.number:load(str) end +output_converters.float4 = function(str) return atom.number:load(str) end +output_converters.float8 = function(str) return atom.number:load(str) end + +output_converters.bool = function(str) return atom.boolean:load(str) end + +output_converters.date = function(str) return atom.date:load(str) end + +local timestamp_loader_func = function(str) + local hour, minute, second = string.match( + str, + "^([0-9]?[0-9]):([0-9][0-9]):([0-9][0-9])" + ) + if hour then + return atom.timestamp{ + hour = tonumber(hour), + minute = tonumber(minute), + second = tonumber(second) + } + else + return atom.timestamp.invalid + end +end +output_converters.timestamp = timestamp_loader_func +output_converters.timestamptz = timestamp_loader_func + +local time_loader_func = function(str) + 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 atom.time{ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = tonumber(hour), + minute = tonumber(minute), + second = tonumber(second) + } + else + return atom.time.invalid + end +end +output_converters.time = time_loader_func +output_converters.timetz = time_loader_func + +mondelefant.postgresql_connection_prototype.type_mappings = { + int8 = atom.integer, + int4 = atom.integer, + int2 = atom.integer, + bool = atom.boolean, + date = atom.date, + timestamp = atom.timestamp, + time = atom.time, + text = atom.string, + varchar = atom.string, +} + + +function mondelefant.postgresql_connection_prototype.input_converter(conn, value, info) + if value == nil then + return "NULL" + else + local converter = + input_converters[getmetatable(value)] or + input_converters[type(value)] + if converter then + return converter(conn, value) + else + return conn:quote_string(tostring(value)) + end + end +end + +function mondelefant.postgresql_connection_prototype.output_converter(conn, value, info) + if value == nil then + return nil + else + local converter = output_converters[info.type] + if converter then + return converter(value) + else + return value + end + end +end + + +--[[ + +db = assert(mondelefant.connect{engine='postgresql', dbname='test'}) +result = db:query{'SELECT ? + 1', atom.date{ year=1999, month=12, day=31}} +print(result[1][1].year) + +--]] diff -r 000000000000 -r 9fdfb27f8e67 libraries/mondelefant/mondelefant_native.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/mondelefant/mondelefant_native.c Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,1558 @@ +#include +#include +#include +#include +#include +#include + +#define MONDELEFANT_REGKEY "e449ba8d9a53d353_mondelefant" + +#define MONDELEFANT_MODULE_REGKEY (MONDELEFANT_REGKEY "_module") +#define MONDELEFANT_CONN_MT_REGKEY (MONDELEFANT_REGKEY "_connection") +#define MONDELEFANT_CONN_DATA_REGKEY (MONDELEFANT_REGKEY "_connection_data") +#define MONDELEFANT_RESULT_MT_REGKEY (MONDELEFANT_REGKEY "_result") +#define MONDELEFANT_ERROROBJECT_MT_REGKEY (MONDELEFANT_REGKEY "_errorobject") +#define MONDELEFANT_CLASS_MT_REGKEY (MONDELEFANT_REGKEY "_class") +#define MONDELEFANT_CLASS_PROTO_REGKEY (MONDELEFANT_REGKEY "_class_proto") + +#define MONDELEFANT_SERVER_ENCODING_ASCII 0 +#define MONDELEFANT_SERVER_ENCODING_UTF8 1 + +typedef struct { + PGconn *pgconn; + int server_encoding; +} mondelefant_conn_t; + +static size_t utf8_position_to_byte(const char *str, size_t utf8pos) { + size_t bytepos; + for (bytepos = 0; utf8pos > 0; bytepos++) { + uint8_t c; + c = ((const uint8_t *)str)[bytepos]; + if (!c) break; + if (c <= 0x7f || c >= 0xc0) utf8pos--; + } + return bytepos; +} + +#define MONDELEFANT_POSTGRESQL_BINARY_OID ((Oid)17) + +static const char *mondelefant_oid_to_typestr(Oid oid) { + switch (oid) { + case 16: return "bool"; + case 17: return "bytea"; + case 18: return "char"; + case 19: return "name"; + case 20: return "int8"; + case 21: return "int2"; + case 23: return "int4"; + case 25: return "text"; + case 26: return "oid"; + case 27: return "tid"; + case 28: return "xid"; + case 29: return "cid"; + case 600: return "point"; + case 601: return "lseg"; + case 602: return "path"; + case 603: return "box"; + case 604: return "polygon"; + case 628: return "line"; + case 700: return "float4"; + case 701: return "float8"; + case 705: return "unknown"; + case 718: return "circle"; + case 790: return "money"; + case 829: return "macaddr"; + case 869: return "inet"; + case 650: return "cidr"; + case 1042: return "bpchar"; + case 1043: return "varchar"; + case 1082: return "date"; + case 1083: return "time"; + case 1114: return "timestamp"; + case 1184: return "timestamptz"; + case 1186: return "interval"; + case 1266: return "timetz"; + case 1560: return "bit"; + case 1562: return "varbit"; + case 1700: return "numeric"; + default: return NULL; + } +} + +#define mondelefant_errcode_item(incode, outcode) \ + if (!strncmp(pgcode, (incode), strlen(incode))) return outcode; else + +#define MONDELEFANT_ERRCODE_UNKNOWN "unknown" +#define MONDELEFANT_ERRCODE_CONNECTION "ConnectionException" +#define MONDELEFANT_ERRCODE_RESULTCOUNT_LOW "WrongResultSetCount.ResultSetMissing" +#define MONDELEFANT_ERRCODE_RESULTCOUNT_HIGH "WrongResultSetCount.TooManyResults" +#define MONDELEFANT_ERRCODE_QUERY1_NO_ROWS "NoData.OneRowExpected" +#define MONDELEFANT_ERRCODE_QUERY1_MULTIPLE_ROWS "CardinalityViolation.OneRowExpected" + +static const char *mondelefant_translate_errcode(const char *pgcode) { + if (!pgcode) abort(); // should not happen + mondelefant_errcode_item("02", "NoData") + mondelefant_errcode_item("03", "SqlStatementNotYetComplete") + mondelefant_errcode_item("08", "ConnectionException") + mondelefant_errcode_item("09", "TriggeredActionException") + mondelefant_errcode_item("0A", "FeatureNotSupported") + mondelefant_errcode_item("0B", "InvalidTransactionInitiation") + mondelefant_errcode_item("0F", "LocatorException") + mondelefant_errcode_item("0L", "InvalidGrantor") + mondelefant_errcode_item("0P", "InvalidRoleSpecification") + mondelefant_errcode_item("21", "CardinalityViolation") + mondelefant_errcode_item("22", "DataException") + mondelefant_errcode_item("23001", "IntegrityConstraintViolation.RestrictViolation") + mondelefant_errcode_item("23502", "IntegrityConstraintViolation.NotNullViolation") + mondelefant_errcode_item("23503", "IntegrityConstraintViolation.ForeignKeyViolation") + mondelefant_errcode_item("23505", "IntegrityConstraintViolation.UniqueViolation") + mondelefant_errcode_item("23514", "IntegrityConstraintViolation.CheckViolation") + mondelefant_errcode_item("23", "IntegrityConstraintViolation") + mondelefant_errcode_item("24", "InvalidCursorState") + mondelefant_errcode_item("25", "InvalidTransactionState") + mondelefant_errcode_item("26", "InvalidSqlStatementName") + mondelefant_errcode_item("27", "TriggeredDataChangeViolation") + mondelefant_errcode_item("28", "InvalidAuthorizationSpecification") + mondelefant_errcode_item("2B", "DependentPrivilegeDescriptorsStillExist") + mondelefant_errcode_item("2D", "InvalidTransactionTermination") + mondelefant_errcode_item("2F", "SqlRoutineException") + mondelefant_errcode_item("34", "InvalidCursorName") + mondelefant_errcode_item("38", "ExternalRoutineException") + mondelefant_errcode_item("39", "ExternalRoutineInvocationException") + mondelefant_errcode_item("3B", "SavepointException") + mondelefant_errcode_item("3D", "InvalidCatalogName") + mondelefant_errcode_item("3F", "InvalidSchemaName") + mondelefant_errcode_item("40", "TransactionRollback") + mondelefant_errcode_item("42", "SyntaxErrorOrAccessRuleViolation") + mondelefant_errcode_item("44", "WithCheckOptionViolation") + mondelefant_errcode_item("53", "InsufficientResources") + mondelefant_errcode_item("54", "ProgramLimitExceeded") + mondelefant_errcode_item("55", "ObjectNotInPrerequisiteState") + mondelefant_errcode_item("57", "OperatorIntervention") + mondelefant_errcode_item("58", "SystemError") + mondelefant_errcode_item("F0", "ConfigurationFileError") + mondelefant_errcode_item("P0", "PlpgsqlError") + mondelefant_errcode_item("XX", "InternalError") + return "unknown"; +} + +static int mondelefant_check_error_class( + const char *errcode, const char *errclass +) { + size_t i = 0; + while (1) { + if (errclass[i] == 0) { + if (errcode[i] == 0 || errcode[i] == '.') return 1; + else return 0; + } + if (errcode[i] != errclass[i]) return 0; + i++; + } +} + +static void mondelefant_push_first_line(lua_State *L, const char *str) { + char *str2; + size_t i = 0; + if (!str) abort(); // should not happen + str2 = strdup(str); + while (1) { + char c = str2[i]; + if (c == '\n' || c == '\r' || c == 0) { str2[i] = 0; break; } + i++; + }; + lua_pushstring(L, str2); + free(str2); +} + +static int mondelefant_connect(lua_State *L) { + luaL_Buffer buf; + const char *conninfo; + PGconn *pgconn; + mondelefant_conn_t *conn; + lua_settop(L, 1); + lua_getfield(L, 1, "engine"); // 2 + if (!lua_toboolean(L, 2)) { + return luaL_error(L, "No database engine selected."); + } + lua_pushliteral(L, "postgresql"); // 3 + if (!lua_rawequal(L, 2, 3)) { + return luaL_error(L, + "Only database engine 'postgresql' is supported." + ); + } + lua_settop(L, 1); + lua_pushnil(L); // slot for key at stack position 2 + lua_pushnil(L); // slot for value at stack position 3 + luaL_buffinit(L, &buf); + { + int need_seperator = 0; + while (lua_pushvalue(L, 2), lua_next(L, 1)) { + lua_replace(L, 3); + lua_replace(L, 2); + // NOTE: numbers will be converted to strings automatically here, + // but perhaps this will change in future versions of lua + luaL_argcheck(L, + lua_isstring(L, 2) && lua_isstring(L, 3), 1, "non-string contained" + ); + lua_pushvalue(L, 2); + lua_pushliteral(L, "engine"); + if (!lua_rawequal(L, -2, -1)) { + const char *value; + size_t value_len; + size_t value_pos = 0; + lua_pop(L, 1); + if (need_seperator) luaL_addchar(&buf, ' '); + luaL_addvalue(&buf); + luaL_addchar(&buf, '='); + luaL_addchar(&buf, '\''); + value = lua_tolstring(L, 3, &value_len); + do { + char c; + c = value[value_pos++]; + if (c == '\'') luaL_addchar(&buf, '\\'); + luaL_addchar(&buf, c); + } while (value_pos < value_len); + luaL_addchar(&buf, '\''); + need_seperator = 1; + } else { + lua_pop(L, 1); + } + } + } + luaL_pushresult(&buf); + lua_replace(L, 2); + lua_settop(L, 2); + conninfo = lua_tostring(L, 2); + pgconn = PQconnectdb(conninfo); + if (!pgconn) { + return luaL_error(L, + "Error in libpq while creating 'PGconn' structure." + ); + } + if (PQstatus(pgconn) != CONNECTION_OK) { + const char *errmsg; + lua_pushnil(L); + errmsg = PQerrorMessage(pgconn); + if (errmsg) { + mondelefant_push_first_line(L, errmsg); + } else { + lua_pushliteral(L, + "Error while connecting to database, but no error message given." + ); + } + lua_pushliteral(L, MONDELEFANT_ERRCODE_CONNECTION); + PQfinish(pgconn); + return 3; + } + lua_settop(L, 0); + conn = lua_newuserdata(L, sizeof(*conn)); // 1 + conn->pgconn = pgconn; + { + const char *charset; + charset = PQparameterStatus(pgconn, "server_encoding"); + if (charset && !strcmp(charset, "UTF8")) { + conn->server_encoding = MONDELEFANT_SERVER_ENCODING_UTF8; + } else { + conn->server_encoding = MONDELEFANT_SERVER_ENCODING_ASCII; + } + } + + luaL_getmetatable(L, MONDELEFANT_CONN_MT_REGKEY); // 2 + lua_setmetatable(L, 1); + + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CONN_DATA_REGKEY); // 2 + lua_pushvalue(L, 1); // 3 + lua_newtable(L); // 4 + lua_settable(L, 2); + lua_settop(L, 1); + + lua_pushliteral(L, "postgresql"); + lua_setfield(L, 1, "engine"); + return 1; +} + +static mondelefant_conn_t *mondelefant_get_conn(lua_State *L, int index) { + mondelefant_conn_t *conn; + conn = luaL_checkudata(L, index, MONDELEFANT_CONN_MT_REGKEY); + if (!conn->pgconn) { + luaL_error(L, "PostgreSQL connection has been closed."); + return NULL; + } + return conn; +} + +static int mondelefant_conn_index(lua_State *L) { + lua_settop(L, 2); + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CONN_DATA_REGKEY); // 3 + lua_pushvalue(L, 1); // 4 + lua_gettable(L, 3); // 4 + lua_remove(L, 3); // connection specific data-table at stack position 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + if (!lua_isnil(L, 4)) return 1; + lua_settop(L, 3); + lua_getfield(L, 3, "prototype"); // 4 + if (lua_toboolean(L, 4)) { + lua_pushvalue(L, 2); // 5 + lua_gettable(L, 4); // 5 + if (!lua_isnil(L, 5)) return 1; + } + lua_settop(L, 2); + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_MODULE_REGKEY); // 3 + lua_getfield(L, 3, "postgresql_connection_prototype"); // 4 + if (lua_toboolean(L, 4)) { + lua_pushvalue(L, 2); // 5 + lua_gettable(L, 4); // 5 + if (!lua_isnil(L, 5)) return 1; + } + lua_settop(L, 3); + lua_getfield(L, 3, "connection_prototype"); // 4 + if (lua_toboolean(L, 4)) { + lua_pushvalue(L, 2); // 5 + lua_gettable(L, 4); // 5 + if (!lua_isnil(L, 5)) return 1; + } + return 0; +} + +static int mondelefant_conn_newindex(lua_State *L) { + lua_settop(L, 3); + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CONN_DATA_REGKEY); // 4 + lua_pushvalue(L, 1); // 5 + lua_gettable(L, 4); // 5 + lua_remove(L, 4); // connection specific data-table at stack position 4 + lua_pushvalue(L, 2); + lua_pushvalue(L, 3); + lua_settable(L, 4); + return 0; +} + +static int mondelefant_conn_free(lua_State *L) { + mondelefant_conn_t *conn; + conn = luaL_checkudata(L, 1, MONDELEFANT_CONN_MT_REGKEY); + if (conn->pgconn) PQfinish(conn->pgconn); + conn->pgconn = NULL; + return 0; +} + +static int mondelefant_conn_close(lua_State *L) { + mondelefant_conn_t *conn; + lua_settop(L, 1); + conn = mondelefant_get_conn(L, 1); + PQfinish(conn->pgconn); + conn->pgconn = NULL; + return 0; +} + +static int mondelefant_conn_is_ok(lua_State *L) { + mondelefant_conn_t *conn; + lua_settop(L, 1); + conn = mondelefant_get_conn(L, 1); + lua_pushboolean(L, PQstatus(conn->pgconn) == CONNECTION_OK); + return 1; +} + +static int mondelefant_conn_get_transaction_status(lua_State *L) { + mondelefant_conn_t *conn; + lua_settop(L, 1); + conn = mondelefant_get_conn(L, 1); + switch (PQtransactionStatus(conn->pgconn)) { + case PQTRANS_IDLE: + lua_pushliteral(L, "idle"); + break; + case PQTRANS_ACTIVE: + lua_pushliteral(L, "active"); + break; + case PQTRANS_INTRANS: + lua_pushliteral(L, "intrans"); + break; + case PQTRANS_INERROR: + lua_pushliteral(L, "inerror"); + break; + default: + lua_pushliteral(L, "unknown"); + } + return 1; +} + +static int mondelefant_conn_create_list(lua_State *L) { + lua_settop(L, 2); + luaL_checkudata(L, 1, MONDELEFANT_CONN_MT_REGKEY); + if (!lua_toboolean(L, 2)) { + lua_newtable(L); + lua_replace(L, 2); // new result at stack position 2 + } + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_RESULT_MT_REGKEY); // 3 + lua_setmetatable(L, 2); + lua_pushvalue(L, 1); // 3 + lua_setfield(L, 2, "_connection"); + lua_pushliteral(L, "list"); // 3 + lua_setfield(L, 2, "_type"); + return 1; +} + +static int mondelefant_conn_create_object(lua_State *L) { + lua_settop(L, 2); + luaL_checkudata(L, 1, MONDELEFANT_CONN_MT_REGKEY); + if (!lua_toboolean(L, 2)) { + lua_newtable(L); + lua_replace(L, 2); // new result at stack position 2 + } + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_RESULT_MT_REGKEY); // 3 + lua_setmetatable(L, 2); + lua_pushvalue(L, 1); // 3 + lua_setfield(L, 2, "_connection"); + lua_pushliteral(L, "object"); // 3 + lua_setfield(L, 2, "_type"); // "object" or "list" + lua_newtable(L); // 3 + lua_setfield(L, 2, "_data"); + lua_newtable(L); // 3 + lua_setfield(L, 2, "_dirty"); + lua_newtable(L); // 3 + lua_setfield(L, 2, "_ref"); // nil=no info, false=nil, else table + return 1; +} + +static int mondelefant_conn_quote_string(lua_State *L) { + mondelefant_conn_t *conn; + const char *input; + size_t input_len; + char *output; + size_t output_len; + lua_settop(L, 2); + conn = mondelefant_get_conn(L, 1); + input = luaL_checklstring(L, 2, &input_len); + if (input_len > (SIZE_MAX / sizeof(char) - 3) / 2) { + return luaL_error(L, "String to be escaped is too long."); + } + output = malloc((2 * input_len + 3) * sizeof(char)); + if (!output) { + return luaL_error(L, "Could not allocate memory for string quoting."); + } + output[0] = '\''; + output_len = PQescapeStringConn( + conn->pgconn, output + 1, input, input_len, NULL + ); + output[output_len + 1] = '\''; + output[output_len + 2] = 0; + lua_pushlstring(L, output, output_len + 2); + free(output); + return 1; +} + +static int mondelefant_conn_quote_binary(lua_State *L) { + mondelefant_conn_t *conn; + const char *input; + size_t input_len; + char *output; + size_t output_len; + luaL_Buffer buf; + lua_settop(L, 2); + conn = mondelefant_get_conn(L, 1); + input = luaL_checklstring(L, 2, &input_len); + output = (char *)PQescapeByteaConn( + conn->pgconn, (const unsigned char *)input, input_len, &output_len + ); + if (!output) { + return luaL_error(L, "Could not allocate memory for binary quoting."); + } + luaL_buffinit(L, &buf); + luaL_addchar(&buf, '\''); + luaL_addlstring(&buf, output, output_len - 1); + luaL_addchar(&buf, '\''); + luaL_pushresult(&buf); + PQfreemem(output); + return 1; +} + +static int mondelefant_conn_assemble_command(lua_State *L) { + mondelefant_conn_t *conn; + int paramidx = 2; + const char *template; + size_t template_pos = 0; + luaL_Buffer buf; + lua_settop(L, 2); + conn = mondelefant_get_conn(L, 1); + if (lua_isstring(L, 2)) { + lua_tostring(L, 2); + return 1; + } + // extra feature for objects with __tostring meta-method: + if (luaL_callmeta(L, 2, "__tostring")) return 1; + luaL_checktype(L, 2, LUA_TTABLE); + lua_rawgeti(L, 2, 1); // 3 + luaL_argcheck(L, + lua_isstring(L, 3), + 2, + "First entry of SQL command structure is not a string." + ); + template = lua_tostring(L, 3); + lua_pushliteral(L, "input_converter"); // 4 + lua_gettable(L, 1); // input_converter at stack position 4 + lua_pushnil(L); // free space at stack position 5 + lua_pushnil(L); // free space at stack position 6 + luaL_buffinit(L, &buf); + while (1) { + char c; + c = template[template_pos++]; + if (!c) break; + if (c == '?' || c == '$') { + if (template[template_pos] == c) { + template_pos++; + luaL_addchar(&buf, c); + } else { + luaL_Buffer keybuf; + int subcmd; + subcmd = (c == '$'); + luaL_buffinit(L, &keybuf); + while (1) { + c = template[template_pos]; + if ( + (c < 'A' || c > 'Z') && + (c < 'a' || c > 'z') && + (c < '0' || c > '9') && + (c != '_') + ) break; + luaL_addchar(&keybuf, c); + template_pos++; + } + luaL_pushresult(&keybuf); + if (lua_objlen(L, -1)) { + lua_pushvalue(L, -1); // save key on stack + lua_gettable(L, 2); // fetch value + } else { + lua_pop(L, 1); + lua_pushnil(L); // put nil on key position + lua_rawgeti(L, 2, paramidx++); // fetch value + } + // stack: ..., , key, pre-value + if (subcmd) { + size_t i; + size_t count; + lua_replace(L, 5); // sub-structure at stack position 5 + lua_pop(L, 1); // drop stored key + // stack: ..., + luaL_argcheck(L, + !lua_isnil(L, 5), + 2, + "SQL sub-structure not found." + ); + luaL_argcheck(L, + lua_type(L, 5) == LUA_TTABLE, + 2, + "SQL sub-structure must be a table." + ); + // stack: ..., + lua_getfield(L, 5, "sep"); + lua_replace(L, 6); // seperator at stack position 6 + if (lua_isnil(L, 6)) { + lua_pushstring(L, ", "); + lua_replace(L, 6); + } else { + luaL_argcheck(L, + lua_isstring(L, 6), + 2, + "Seperator of SQL sub-structure has to be a string." + ); + } + count = lua_objlen(L, 5); + for (i = 0; i < count; i++) { + if (i) { + lua_pushvalue(L, 6); + luaL_addvalue(&buf); + } + lua_pushcfunction(L, mondelefant_conn_assemble_command); + lua_pushvalue(L, 1); + lua_rawgeti(L, 5, i+1); + lua_call(L, 2, 1); + luaL_addvalue(&buf); + } + } else { + if (lua_toboolean(L, 4)) { + // call input_converter with connection handle, value and info + lua_pushvalue(L, 4); + lua_pushvalue(L, 1); + lua_pushvalue(L, -3); + lua_newtable(L); + lua_pushvalue(L, -6); + lua_setfield(L, -2, "field_name"); + lua_call(L, 3, 1); + // stack: ..., , key, pre-value, final-value + lua_remove(L, -2); + lua_remove(L, -2); + // stack: ..., , final-value + if (!lua_isstring(L, -1)) { + return luaL_error(L, "input_converter returned non-string."); + } + } else { + lua_remove(L, -2); + // stack: ..., , pre-value + if (lua_isnil(L, -1)) { + lua_pushliteral(L, "NULL"); + } else if (lua_type(L, -1) == LUA_TBOOLEAN) { + lua_pushstring(L, lua_toboolean(L, -1) ? "TRUE" : "FALSE"); + } else if (lua_isstring(L, -1)) { + // NOTE: In this version of lua a number will be converted + lua_tostring(L, -1); + lua_pushcfunction(L, mondelefant_conn_quote_string); + lua_pushvalue(L, 1); + lua_pushvalue(L, -3); + lua_call(L, 2, 1); + } else { + return luaL_error(L, + "Unable to convert SQL value due to unknown type " + "or missing input_converter." + ); + } + // stack: ..., , pre-value, final-value + lua_remove(L, -2); + // stack: ..., , final-value + } + luaL_addvalue(&buf); + } + } + } else { + luaL_addchar(&buf, c); + } + } + luaL_pushresult(&buf); + return 1; +} + +#define MONDELEFANT_MAX_COMMAND_COUNT 64 +#define MONDELEFANT_MAX_COLUMN_COUNT 1024 +#define MONDELEFANT_QUERY_MODE_LIST 1 +#define MONDELEFANT_QUERY_MODE_OBJECT 2 +#define MONDELEFANT_QUERY_MODE_OPT_OBJECT 3 + +static int mondelefant_conn_try_query(lua_State *L) { + mondelefant_conn_t *conn; + int command_count; + int command_idx; + int modes[MONDELEFANT_MAX_COMMAND_COUNT]; + luaL_Buffer buf; + int sent_success; + PGresult *res; + int rows, cols, row, col; + conn = mondelefant_get_conn(L, 1); + command_count = lua_gettop(L) / 2; + lua_pushnil(L); // needed, if last mode was omitted + if (command_count > MONDELEFANT_MAX_COMMAND_COUNT) { + return luaL_error(L, "Exceeded maximum command count in one query."); + } + luaL_buffinit(L, &buf); + for (command_idx = 0; command_idx < command_count; command_idx++) { + int mode; + int mode_idx; // stack index of mode string + if (command_idx) luaL_addchar(&buf, ' '); + lua_pushcfunction(L, mondelefant_conn_assemble_command); + lua_pushvalue(L, 1); + lua_pushvalue(L, 2 + 2 * command_idx); + lua_call(L, 2, 1); + luaL_addvalue(&buf); + luaL_addchar(&buf, ';'); + mode_idx = 3 + 2 * command_idx; + if (lua_isnil(L, mode_idx)) { + mode = MONDELEFANT_QUERY_MODE_LIST; + } else { + const char *modestr; + modestr = luaL_checkstring(L, mode_idx); + if (!strcmp(modestr, "list")) { + mode = MONDELEFANT_QUERY_MODE_LIST; + } else if (!strcmp(modestr, "object")) { + mode = MONDELEFANT_QUERY_MODE_OBJECT; + } else if (!strcmp(modestr, "opt_object")) { + mode = MONDELEFANT_QUERY_MODE_OPT_OBJECT; + } else { + return luaL_error(L, "Unknown query mode specified."); + } + } + modes[command_idx] = mode; + } + luaL_pushresult(&buf); // stack position unknown + lua_replace(L, 2); // SQL command string to stack position 2 + lua_settop(L, 2); + lua_getfield(L, 1, "sql_tracer"); // tracer at stack position 3 + if (lua_toboolean(L, 3)) { + lua_pushvalue(L, 1); // 4 + lua_pushvalue(L, 2); // 5 + lua_call(L, 2, 1); // trace callback at stack position 3 + } + sent_success = PQsendQuery(conn->pgconn, lua_tostring(L, 2)); + lua_newtable(L); // results in table at stack position 4 + for (command_idx = 0; ; command_idx++) { + int mode; + char binary[MONDELEFANT_MAX_COLUMN_COUNT]; + ExecStatusType pgstatus; + mode = modes[command_idx]; + if (sent_success) { + res = PQgetResult(conn->pgconn); + if (command_idx >= command_count && !res) break; + if (res) { + pgstatus = PQresultStatus(res); + rows = PQntuples(res); + cols = PQnfields(res); + } + } + if ( + !sent_success || command_idx >= command_count || !res || + (pgstatus != PGRES_TUPLES_OK && pgstatus != PGRES_COMMAND_OK) || + (rows < 1 && mode == MONDELEFANT_QUERY_MODE_OBJECT) || + (rows > 1 && mode != MONDELEFANT_QUERY_MODE_LIST) + ) { + const char *command; + command = lua_tostring(L, 2); + lua_newtable(L); // 5 + lua_getfield(L, + LUA_REGISTRYINDEX, + MONDELEFANT_ERROROBJECT_MT_REGKEY + ); + lua_setmetatable(L, 5); + lua_pushvalue(L, 1); + lua_setfield(L, 5, "connection"); + lua_pushinteger(L, command_idx + 1); + lua_setfield(L, 5, "command_number"); + lua_pushvalue(L, 2); + lua_setfield(L, 5, "sql_command"); + if (!res) { + lua_pushliteral(L, MONDELEFANT_ERRCODE_RESULTCOUNT_LOW); + lua_setfield(L, 5, "code"); + lua_pushliteral(L, "Received too few database result sets."); + lua_setfield(L, 5, "message"); + } else if (command_idx >= command_count) { + lua_pushliteral(L, MONDELEFANT_ERRCODE_RESULTCOUNT_HIGH); + lua_setfield(L, 5, "code"); + lua_pushliteral(L, "Received too many database result sets."); + lua_setfield(L, 5, "message"); + } else if ( + pgstatus != PGRES_TUPLES_OK && pgstatus != PGRES_COMMAND_OK + ) { + const char *sqlstate; + const char *errmsg; + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_SEVERITY)); + lua_setfield(L, 5, "pg_severity"); + sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE); + if (sqlstate) { + lua_pushstring(L, sqlstate); + lua_setfield(L, 5, "pg_sqlstate"); + lua_pushstring(L, mondelefant_translate_errcode(sqlstate)); + lua_setfield(L, 5, "code"); + } else { + lua_pushliteral(L, MONDELEFANT_ERRCODE_UNKNOWN); + lua_setfield(L, 5, "code"); + } + errmsg = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); + if (errmsg) { + mondelefant_push_first_line(L, errmsg); + lua_setfield(L, 5, "message"); + lua_pushstring(L, errmsg); + lua_setfield(L, 5, "pg_message_primary"); + } else { + lua_pushliteral(L, + "Error while fetching result, but no error message given." + ); + lua_setfield(L, 5, "message"); + } + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_MESSAGE_DETAIL)); + lua_setfield(L, 5, "pg_message_detail"); + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_MESSAGE_HINT)); + lua_setfield(L, 5, "pg_message_hint"); + // NOTE: "position" and "pg_internal_position" are recalculated to + // byte offsets, as Lua 5.1 is not Unicode aware. + { + char *tmp; + tmp = PQresultErrorField(res, PG_DIAG_STATEMENT_POSITION); + if (tmp) { + int pos; + pos = atoi(tmp) - 1; + if (conn->server_encoding == MONDELEFANT_SERVER_ENCODING_UTF8) { + pos = utf8_position_to_byte(command, pos); + } + lua_pushinteger(L, pos + 1); + lua_setfield(L, 5, "position"); + } + } + { + const char *internal_query; + internal_query = PQresultErrorField(res, PG_DIAG_INTERNAL_QUERY); + lua_pushstring(L, internal_query); + lua_setfield(L, 5, "pg_internal_query"); + char *tmp; + tmp = PQresultErrorField(res, PG_DIAG_INTERNAL_POSITION); + if (tmp) { + int pos; + pos = atoi(tmp) - 1; + if (conn->server_encoding == MONDELEFANT_SERVER_ENCODING_UTF8) { + pos = utf8_position_to_byte(internal_query, pos); + } + lua_pushinteger(L, pos + 1); + lua_setfield(L, 5, "pg_internal_position"); + } + } + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_CONTEXT)); + lua_setfield(L, 5, "pg_context"); + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_SOURCE_FILE)); + lua_setfield(L, 5, "pg_source_file"); + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_SOURCE_LINE)); + lua_setfield(L, 5, "pg_source_line"); + lua_pushstring(L, PQresultErrorField(res, PG_DIAG_SOURCE_FUNCTION)); + lua_setfield(L, 5, "pg_source_function"); + } else if (rows < 1 && mode == MONDELEFANT_QUERY_MODE_OBJECT) { + lua_pushliteral(L, MONDELEFANT_ERRCODE_QUERY1_NO_ROWS); + lua_setfield(L, 5, "code"); + lua_pushliteral(L, "Expected one row, but got empty set."); + lua_setfield(L, 5, "message"); + } else if (rows > 1 && mode != MONDELEFANT_QUERY_MODE_LIST) { + lua_pushliteral(L, MONDELEFANT_ERRCODE_QUERY1_MULTIPLE_ROWS); + lua_setfield(L, 5, "code"); + lua_pushliteral(L, "Got more than one result row."); + lua_setfield(L, 5, "message"); + } else { + // should not happen + abort(); + } + if (res) { + PQclear(res); + while ((res = PQgetResult(conn->pgconn))) PQclear(res); + } + if (lua_toboolean(L, 3)) { + lua_pushvalue(L, 3); + lua_pushvalue(L, 5); + lua_call(L, 1, 0); + } + return 1; + } + rows = PQntuples(res); + cols = PQnfields(res); + if (modes[command_idx] == MONDELEFANT_QUERY_MODE_LIST) { + lua_pushcfunction(L, mondelefant_conn_create_list); // 5 + lua_pushvalue(L, 1); // 6 + lua_call(L, 1, 1); // 5 + } else { + lua_pushcfunction(L, mondelefant_conn_create_object); // 5 + lua_pushvalue(L, 1); // 6 + lua_call(L, 1, 1); // 5 + } + lua_newtable(L); // column_info at atack position 6 + for (col = 0; col < cols; col++) { + lua_newtable(L); // 7 + lua_pushstring(L, PQfname(res, col)); + lua_setfield(L, 7, "field_name"); + { + Oid tmp; + tmp = PQftable(res, col); + if (tmp == InvalidOid) lua_pushnil(L); + else lua_pushinteger(L, tmp); + lua_setfield(L, 7, "table_oid"); + } + { + int tmp; + tmp = PQftablecol(res, col); + if (tmp == 0) lua_pushnil(L); + else lua_pushinteger(L, tmp); + lua_setfield(L, 7, "table_column_number"); + } + { + Oid tmp; + tmp = PQftype(res, col); + binary[col] = (tmp == MONDELEFANT_POSTGRESQL_BINARY_OID); + lua_pushinteger(L, tmp); + lua_setfield(L, 7, "type_oid"); + lua_pushstring(L, mondelefant_oid_to_typestr(tmp)); + lua_setfield(L, 7, "type"); + } + { + int tmp; + tmp = PQfmod(res, col); + if (tmp == -1) lua_pushnil(L); + else lua_pushinteger(L, tmp); + lua_setfield(L, 7, "type_modifier"); + } + lua_rawseti(L, 6, col+1); + } + lua_setfield(L, 5, "_column_info"); // stack at position 5 with result + { + char *tmp; + tmp = PQcmdTuples(res); + if (tmp[0]) { + lua_pushinteger(L, atoi(tmp)); + lua_setfield(L, 5, "_rows_affected"); + } + } + { + Oid tmp; + tmp = PQoidValue(res); + if (tmp != InvalidOid) { + lua_pushinteger(L, tmp); + lua_setfield(L, 5, "_oid"); + } + } + if (modes[command_idx] == MONDELEFANT_QUERY_MODE_LIST) { + for (row = 0; row < rows; row++) { + lua_pushcfunction(L, mondelefant_conn_create_object); // 6 + lua_pushvalue(L, 1); // 7 + lua_call(L, 1, 1); // 6 + for (col = 0; col < cols; col++) { + if (PQgetisnull(res, row, col)) { + lua_pushnil(L); + } else if (binary[col]) { + size_t binlen; + char *binval; + binval = (char *)PQunescapeBytea( + (unsigned char *)PQgetvalue(res, row, col), &binlen + ); + if (!binval) { + return luaL_error(L, + "Could not allocate memory for binary unescaping." + ); + } + lua_pushlstring(L, binval, binlen); + PQfreemem(binval); + } else { + lua_pushstring(L, PQgetvalue(res, row, col)); + } + lua_rawseti(L, 6, col+1); + } + lua_rawseti(L, 5, row+1); + } + } else if (rows == 1) { + for (col = 0; col < cols; col++) { + if (PQgetisnull(res, 0, col)) { + lua_pushnil(L); + } else if (binary[col]) { + size_t binlen; + char *binval; + binval = (char *)PQunescapeBytea( + (unsigned char *)PQgetvalue(res, 0, col), &binlen + ); + if (!binval) { + return luaL_error(L, + "Could not allocate memory for binary unescaping." + ); + } + lua_pushlstring(L, binval, binlen); + PQfreemem(binval); + } else { + lua_pushstring(L, PQgetvalue(res, 0, col)); + } + lua_rawseti(L, 5, col+1); + } + } else { + // no row in optrow mode + lua_pop(L, 1); + lua_pushnil(L); + } + lua_rawseti(L, 4, command_idx+1); + if (lua_gettop(L) != 4) abort(); // should not happen + PQclear(res); + } + // trace callback at stack position 3 + // result at stack position 4 (top of stack) + if (lua_toboolean(L, 3)) { + lua_pushvalue(L, 3); + lua_call(L, 0, 0); + } + lua_replace(L, 3); // result at stack position 3 + lua_getfield(L, 1, "output_converter"); // output converter at stack position 4 + for (command_idx = 0; command_idx < command_count; command_idx++) { + int mode; + mode = modes[command_idx]; + lua_rawgeti(L, 3, command_idx+1); // raw result at stack position 5 + if (lua_toboolean(L, 5)) { + lua_getfield(L, 5, "_column_info"); // column_info list at position 6 + cols = lua_objlen(L, 6); + if (mode == MONDELEFANT_QUERY_MODE_LIST) { + rows = lua_objlen(L, 5); + for (row = 0; row < rows; row++) { + lua_rawgeti(L, 5, row+1); // row at stack position 7 + lua_getfield(L, 7, "_data"); // _data table at stack position 8 + for (col = 0; col < cols; col++) { + lua_rawgeti(L, 6, col+1); // this column info at position 9 + lua_getfield(L, 9, "field_name"); // 10 + if (lua_toboolean(L, 4)) { + lua_pushvalue(L, 4); // output-converter + lua_pushvalue(L, 1); // connection + lua_rawgeti(L, 7, col+1); // raw-value + lua_pushvalue(L, 9); // this column info + lua_call(L, 3, 1); // converted value at position 11 + } else { + lua_rawgeti(L, 7, col+1); // raw-value at position 11 + } + lua_pushvalue(L, 11); // 12 + lua_rawseti(L, 7, col+1); + lua_rawset(L, 8); + lua_settop(L, 8); + } + lua_settop(L, 6); + } + } else { + lua_getfield(L, 5, "_data"); // _data table at stack position 7 + for (col = 0; col < cols; col++) { + lua_rawgeti(L, 6, col+1); // this column info at position 8 + lua_getfield(L, 8, "field_name"); // 9 + if (lua_toboolean(L, 4)) { + lua_pushvalue(L, 4); // output-converter + lua_pushvalue(L, 1); // connection + lua_rawgeti(L, 5, col+1); // raw-value + lua_pushvalue(L, 8); // this column info + lua_call(L, 3, 1); // converted value at position 10 + } else { + lua_rawgeti(L, 5, col+1); // raw-value at position 10 + } + lua_pushvalue(L, 10); // 11 + lua_rawseti(L, 5, col+1); + lua_rawset(L, 7); + lua_settop(L, 7); + } + } + } + lua_settop(L, 4); + } + lua_settop(L, 3); + lua_pushnil(L); + for (command_idx = 0; command_idx < command_count; command_idx++) { + lua_rawgeti(L, 3, command_idx+1); + } + return command_count+1; +} + +static int mondelefant_errorobject_escalate(lua_State *L) { + lua_settop(L, 1); + lua_getfield(L, 1, "connection"); // 2 + lua_getfield(L, 2, "error_objects"); // 3 + if (lua_toboolean(L, 3)) { + lua_settop(L, 1); + return lua_error(L); + } else { + lua_getfield(L, 1, "message"); // 4 + if (lua_isnil(L, 4)) { + return luaL_error(L, "No error message given for escalation."); + } + return lua_error(L); + } +} + +static int mondelefant_errorobject_is_kind_of(lua_State *L) { + lua_settop(L, 2); + lua_getfield(L, 1, "code"); // 3 + if (lua_isstring(L, 3)) { + lua_pushboolean(L, + mondelefant_check_error_class( + lua_tostring(L, 3), luaL_checkstring(L, 2) + ) + ); + } else { + // only happens for errors where code is not set + lua_pushboolean(L, 0); + } + return 1; +} + +static int mondelefant_conn_query(lua_State *L) { + int argc; + argc = lua_gettop(L); + lua_pushvalue(L, 1); + lua_insert(L, 1); + lua_pushcfunction(L, mondelefant_conn_try_query); + lua_insert(L, 2); + lua_call(L, argc, LUA_MULTRET); // results (with error) starting at index 2 + if (lua_toboolean(L, 2)) { + lua_pushcfunction(L, mondelefant_errorobject_escalate); + lua_pushvalue(L, 2); + lua_call(L, 1, 0); // will raise an error + return 0; // should not be executed + } else { + return lua_gettop(L) - 2; + } +} + +static int mondelefant_set_class(lua_State *L) { + lua_settop(L, 2); + lua_getmetatable(L, 1); // 3 + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_RESULT_MT_REGKEY); // 4 + luaL_argcheck(L, lua_equal(L, 3, 4), 1, "not a database result"); + lua_settop(L, 2); + lua_getmetatable(L, 2); // 3 + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CLASS_MT_REGKEY); // 4 + luaL_argcheck(L, lua_equal(L, 3, 4), 2, "not a database class"); + lua_settop(L, 2); + lua_pushvalue(L, 2); // 3 + lua_setfield(L, 1, "_class"); + lua_getfield(L, 1, "_type"); // 3 + lua_pushliteral(L, "list"); // 4 + if (lua_rawequal(L, 3, 4)) { + int i; + for (i=0; i < lua_objlen(L, 1); i++) { + lua_settop(L, 2); + lua_rawgeti(L, 1, i+1); // 3 + lua_pushvalue(L, 2); // 4 + lua_setfield(L, 3, "_class"); + } + } + lua_settop(L, 1); + return 1; +} + +static int mondelefant_new_class(lua_State *L) { + lua_settop(L, 1); + if (!lua_toboolean(L, 1)) { + lua_settop(L, 0); + lua_newtable(L); // 1 + } + lua_getfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CLASS_MT_REGKEY); // 2 + lua_setmetatable(L, 1); + lua_pushliteral(L, "prototype"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "prototype"); // 3 + lua_getfield(L, + LUA_REGISTRYINDEX, + MONDELEFANT_CLASS_PROTO_REGKEY + ); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + lua_pushliteral(L, "object"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "object"); // 3 + lua_newtable(L); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + lua_pushliteral(L, "object_get"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "object_get"); // 3 + lua_newtable(L); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + lua_pushliteral(L, "object_set"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "object_set"); // 3 + lua_newtable(L); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + lua_pushliteral(L, "list"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "list"); // 3 + lua_newtable(L); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + lua_pushliteral(L, "references"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "references"); // 3 + lua_newtable(L); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + lua_pushliteral(L, "foreign_keys"); // 2 + lua_rawget(L, 1); // 2 + if (!lua_toboolean(L, 2)) { + lua_pushliteral(L, "foreign_keys"); // 3 + lua_newtable(L); // 4 + lua_rawset(L, 1); + } + lua_settop(L, 1); + return 1; +} + +static int mondelefant_class_get_reference(lua_State *L) { + lua_settop(L, 2); + while (lua_toboolean(L, 1)) { + lua_getfield(L, 1, "references"); // 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + if (!lua_isnil(L, 4)) return 1; + lua_settop(L, 2); + lua_pushliteral(L, "prototype"); // 3 + lua_rawget(L, 1); // 3 + lua_replace(L, 1); + } + return 0; +} + +static int mondelefant_class_iterate_over_references(lua_State *L) { + return luaL_error(L, "Reference iterator not implemented yet."); // TODO +} + +static int mondelefant_class_get_foreign_key_reference_name(lua_State *L) { + lua_settop(L, 2); + while (lua_toboolean(L, 1)) { + lua_getfield(L, 1, "foreign_keys"); // 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + if (!lua_isnil(L, 4)) return 1; + lua_settop(L, 2); + lua_pushliteral(L, "prototype"); // 3 + lua_rawget(L, 1); // 3 + lua_replace(L, 1); + } + return 0; +} + +static int mondelefant_result_index(lua_State *L) { + const char *result_type; + lua_settop(L, 2); + if (lua_type(L, 2) != LUA_TSTRING || lua_tostring(L, 2)[0] == '_') { + lua_rawget(L, 1); + return 1; + } + lua_getfield(L, 1, "_class"); // 3 + if (!lua_toboolean(L, 3)) { + lua_settop(L, 2); + lua_getfield(L, + LUA_REGISTRYINDEX, + MONDELEFANT_CLASS_PROTO_REGKEY + ); // 3 + } + lua_getfield(L, 1, "_type"); // 4 + result_type = lua_tostring(L, 4); + if (result_type && !strcmp(result_type, "object")) { + lua_settop(L, 3); + // try inherited attributes, methods or getter functions: + while (lua_toboolean(L, 3)) { + lua_getfield(L, 3, "object"); // 4 + lua_pushvalue(L, 2); // 5 + lua_gettable(L, 4); // 5 + if (!lua_isnil(L, 5)) return 1; + lua_settop(L, 3); + lua_getfield(L, 3, "object_get"); // 4 + lua_pushvalue(L, 2); // 5 + lua_gettable(L, 4); // 5 + if (lua_toboolean(L, 5)) { + lua_pushvalue(L, 1); // 6 + lua_call(L, 1, 1); // 5 + return 1; + } + lua_settop(L, 3); + lua_pushliteral(L, "prototype"); // 4 + lua_rawget(L, 3); // 4 + lua_replace(L, 3); + } + lua_settop(L, 2); + // try primary keys of referenced objects: + lua_pushcfunction(L, + mondelefant_class_get_foreign_key_reference_name + ); // 3 + lua_getfield(L, 1, "_class"); // 4 + lua_pushvalue(L, 2); // 5 + lua_call(L, 2, 1); // 3 + if (!lua_isnil(L, 3)) { + // reference name at stack position 3 + lua_pushcfunction(L, mondelefant_class_get_reference); // 4 + lua_getfield(L, 1, "_class"); // 5 + lua_pushvalue(L, 3); // 6 + lua_call(L, 2, 1); // reference info at stack position 4 + lua_getfield(L, 1, "_ref"); // 5 + lua_getfield(L, 4, "ref"); // 6 + lua_gettable(L, 5); // 6 + if (!lua_isnil(L, 6)) { + if (lua_toboolean(L, 6)) { + lua_getfield(L, 4, "that_key"); // 7 + if (lua_isnil(L, 7)) { + return luaL_error(L, "Missing 'that_key' entry in model reference."); + } + lua_gettable(L, 6); // 7 + } else { + lua_pushnil(L); + } + return 1; + } + } + lua_settop(L, 2); + // try normal data field info: + lua_getfield(L, 1, "_data"); // 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + if (!lua_isnil(L, 4)) return 1; + lua_settop(L, 2); + // try cached referenced object (or cached NULL reference): + lua_getfield(L, 1, "_ref"); // 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + if (lua_isboolean(L, 4) && !lua_toboolean(L, 4)) { + lua_pushnil(L); + return 1; + } else if (!lua_isnil(L, 4)) { + return 1; + } + lua_settop(L, 2); + // try to load a referenced object: + lua_pushcfunction(L, mondelefant_class_get_reference); // 3 + lua_getfield(L, 1, "_class"); // 4 + lua_pushvalue(L, 2); // 5 + lua_call(L, 2, 1); // 3 + if (!lua_isnil(L, 3)) { + lua_settop(L, 2); + lua_getfield(L, 1, "load"); // 3 + lua_pushvalue(L, 1); // 4 (self) + lua_pushvalue(L, 2); // 5 + lua_call(L, 2, 0); + lua_settop(L, 2); + lua_getfield(L, 1, "_ref"); // 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + if (lua_isboolean(L, 4) && !lua_toboolean(L, 4)) lua_pushnil(L); // TODO: use special object instead of false + return 1; + } + return 0; + } else if (result_type && !strcmp(result_type, "list")) { + lua_settop(L, 3); + // try inherited list attributes or methods: + while (lua_toboolean(L, 3)) { + lua_getfield(L, 3, "list"); // 4 + lua_pushvalue(L, 2); // 5 + lua_gettable(L, 4); // 5 + if (!lua_isnil(L, 5)) return 1; + lua_settop(L, 3); + lua_pushliteral(L, "prototype"); // 4 + lua_rawget(L, 3); // 4 + lua_replace(L, 3); + } + } + return 0; +} + +static int mondelefant_result_newindex(lua_State *L) { + const char *result_type; + lua_settop(L, 3); + if (lua_type(L, 2) != LUA_TSTRING || lua_tostring(L, 2)[0] == '_') { + lua_rawset(L, 1); + return 1; + } + lua_getfield(L, 1, "_class"); // 4 + if (!lua_toboolean(L, 4)) { + lua_settop(L, 3); + lua_getfield(L, + LUA_REGISTRYINDEX, + MONDELEFANT_CLASS_PROTO_REGKEY + ); // 4 + } + lua_getfield(L, 1, "_type"); // 5 + result_type = lua_tostring(L, 5); + if (result_type && !strcmp(result_type, "object")) { + lua_settop(L, 4); + // try object setter functions: + while (lua_toboolean(L, 4)) { + lua_getfield(L, 4, "object_set"); // 5 + lua_pushvalue(L, 2); // 6 + lua_gettable(L, 5); // 6 + if (lua_toboolean(L, 6)) { + lua_pushvalue(L, 1); // 7 + lua_pushvalue(L, 3); // 8 + lua_call(L, 2, 0); + return 0; + } + lua_settop(L, 4); + lua_pushliteral(L, "prototype"); // 5 + lua_rawget(L, 4); // 5 + lua_replace(L, 4); + } + lua_settop(L, 3); + // check, if a object reference is changed: + lua_pushcfunction(L, mondelefant_class_get_reference); // 4 + lua_getfield(L, 1, "_class"); // 5 + lua_pushvalue(L, 2); // 6 + lua_call(L, 2, 1); // 4 + if (!lua_isnil(L, 4)) { + // store object in _ref table (use false for nil): // TODO: use special object instead of false + lua_getfield(L, 1, "_ref"); // 5 + lua_pushvalue(L, 2); // 6 + if (lua_isnil(L, 3)) lua_pushboolean(L, 0); // 7 + else lua_pushvalue(L, 3); // 7 + lua_settable(L, 5); + lua_settop(L, 4); + // delete referencing key from _data table: + lua_getfield(L, 4, "this_key"); // 5 + if (lua_isnil(L, 5)) { + return luaL_error(L, "Missing 'this_key' entry in model reference."); + } + lua_getfield(L, 1, "_data"); // 6 + lua_pushvalue(L, 5); // 7 + lua_pushnil(L); // 8 + lua_settable(L, 6); + lua_settop(L, 5); + lua_getfield(L, 1, "_dirty"); // 6 + lua_pushvalue(L, 5); // 7 + lua_pushboolean(L, 1); // 8 + lua_settable(L, 6); + return 0; + } + lua_settop(L, 3); + // store value in data field info: + lua_getfield(L, 1, "_data"); // 4 + lua_pushvalue(L, 2); // 5 + lua_pushvalue(L, 3); // 6 + lua_settable(L, 4); + lua_settop(L, 3); + // mark field as dirty (needs to be UPDATEd on save): + lua_getfield(L, 1, "_dirty"); // 4 + lua_pushvalue(L, 2); // 5 + lua_pushboolean(L, 1); // 6 + lua_settable(L, 4); + lua_settop(L, 3); + // reset reference cache, if neccessary: + lua_pushcfunction(L, + mondelefant_class_get_foreign_key_reference_name + ); // 4 + lua_getfield(L, 1, "_class"); // 5 + lua_pushvalue(L, 2); // 6 + lua_call(L, 2, 1); // 4 + if (!lua_isnil(L, 4)) { + lua_getfield(L, 1, "_ref"); // 5 + lua_pushvalue(L, 4); // 6 + lua_pushnil(L); // 7 + lua_settable(L, 5); + } + return 0; + } else { + lua_settop(L, 3); + lua_rawset(L, 1); + return 0; + } + return 0; +} + +static int mondelefant_class_index(lua_State *L) { + lua_settop(L, 2); + lua_pushliteral(L, "prototype"); // 3 + lua_rawget(L, 1); // 3 + lua_pushvalue(L, 2); // 4 + lua_gettable(L, 3); // 4 + return 1; +} + +static const struct luaL_Reg mondelefant_module_functions[] = { + {"connect", mondelefant_connect}, + {"set_class", mondelefant_set_class}, + {"new_class", mondelefant_new_class}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_conn_mt_functions[] = { + {"__gc", mondelefant_conn_free}, + {"__index", mondelefant_conn_index}, + {"__newindex", mondelefant_conn_newindex}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_conn_methods[] = { + {"close", mondelefant_conn_close}, + {"is_ok", mondelefant_conn_is_ok}, + {"get_transaction_status", mondelefant_conn_get_transaction_status}, + {"create_list", mondelefant_conn_create_list}, + {"create_object", mondelefant_conn_create_object}, + {"quote_string", mondelefant_conn_quote_string}, + {"quote_binary", mondelefant_conn_quote_binary}, + {"assemble_command", mondelefant_conn_assemble_command}, + {"try_query", mondelefant_conn_try_query}, + {"query", mondelefant_conn_query}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_errorobject_mt_functions[] = { + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_errorobject_methods[] = { + {"escalate", mondelefant_errorobject_escalate}, + {"is_kind_of", mondelefant_errorobject_is_kind_of}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_result_mt_functions[] = { + {"__index", mondelefant_result_index}, + {"__newindex", mondelefant_result_newindex}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_class_mt_functions[] = { + {"__index", mondelefant_class_index}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_class_methods[] = { + {"get_reference", mondelefant_class_get_reference}, + {"iterate_over_references", mondelefant_class_iterate_over_references}, + {"get_foreign_key_reference_name", + mondelefant_class_get_foreign_key_reference_name}, + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_object_methods[] = { + {NULL, NULL} +}; + +static const struct luaL_Reg mondelefant_list_methods[] = { + {NULL, NULL} +}; + +int luaopen_mondelefant_native(lua_State *L) { + lua_settop(L, 0); + lua_newtable(L); // module at stack position 1 + luaL_register(L, NULL, mondelefant_module_functions); + + lua_pushvalue(L, 1); // 2 + lua_setfield(L, LUA_REGISTRYINDEX, MONDELEFANT_MODULE_REGKEY); + + lua_newtable(L); // 2 + // NOTE: only PostgreSQL is supported yet: + luaL_register(L, NULL, mondelefant_conn_methods); + lua_setfield(L, 1, "postgresql_connection_prototype"); + lua_newtable(L); // 2 + lua_setfield(L, 1, "connection_prototype"); + + luaL_newmetatable(L, MONDELEFANT_CONN_MT_REGKEY); // 2 + luaL_register(L, NULL, mondelefant_conn_mt_functions); + lua_settop(L, 1); + luaL_newmetatable(L, MONDELEFANT_RESULT_MT_REGKEY); // 2 + luaL_register(L, NULL, mondelefant_result_mt_functions); + lua_setfield(L, 1, "result_metatable"); + luaL_newmetatable(L, MONDELEFANT_CLASS_MT_REGKEY); // 2 + luaL_register(L, NULL, mondelefant_class_mt_functions); + lua_setfield(L, 1, "class_metatable"); + + lua_newtable(L); // 2 + lua_newtable(L); // 3 + luaL_register(L, NULL, mondelefant_object_methods); + lua_setfield(L, 2, "object"); + lua_newtable(L); // 3 + lua_setfield(L, 2, "object_get"); + lua_newtable(L); // 3 + lua_setfield(L, 2, "object_set"); + lua_newtable(L); // 3 + luaL_register(L, NULL, mondelefant_list_methods); + lua_setfield(L, 2, "list"); + lua_newtable(L); // 3 + lua_setfield(L, 2, "references"); + lua_newtable(L); // 3 + lua_setfield(L, 2, "foreign_keys"); + lua_pushvalue(L, 2); // 3 + lua_setfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CLASS_PROTO_REGKEY); + lua_setfield(L, 1, "class_prototype"); + + lua_newtable(L); // 2 + lua_pushliteral(L, "k"); // 3 + lua_setfield(L, 2, "__mode"); + lua_newtable(L); // 3 + lua_pushvalue(L, 2); // 4 + lua_setmetatable(L, 3); + lua_setfield(L, LUA_REGISTRYINDEX, MONDELEFANT_CONN_DATA_REGKEY); + lua_settop(L, 1); + + luaL_newmetatable(L, MONDELEFANT_ERROROBJECT_MT_REGKEY); // 2 + luaL_register(L, NULL, mondelefant_errorobject_mt_functions); + lua_newtable(L); // 3 + luaL_register(L, NULL, mondelefant_errorobject_methods); + lua_setfield(L, 2, "__index"); + lua_setfield(L, 1, "errorobject_metatable"); + + return 1; +} diff -r 000000000000 -r 9fdfb27f8e67 libraries/multirand/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/multirand/Makefile Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,10 @@ +include ../../Makefile.options + +multirand.so: multirand.o + $(LD) $(LDFLAGS) -o multirand.$(SLIB_EXT) multirand.o + +multirand.o: multirand.c + $(CC) -c $(CFLAGS) -o multirand.o multirand.c + +clean:: + rm -f multirand.so multirand.o diff -r 000000000000 -r 9fdfb27f8e67 libraries/multirand/multirand.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/multirand/multirand.c Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,124 @@ +#include +#include +#include + +static FILE *multirand_dev; + +static lua_Integer multirand_range( + lua_State *L, lua_Integer from, lua_Integer to +) { + lua_Integer range; + int bits = 0; + lua_Integer bit_mask = 0; + if (to < from) return luaL_error(L, "Assertion failed in C."); + range = to - from; + { + lua_Integer tmp; + tmp = range; + while (tmp) { + bits++; + bit_mask <<= 1; + bit_mask |= 1; + tmp >>= 1; + } + } + while (1) { + int i; + lua_Integer rnd = 0; + for (i = 0; i < (bits + 7) / 8; i++) { + int b; + b = getc(multirand_dev); + if (b == EOF) { + return luaL_error(L, "I/O error while reading random."); + } + rnd = (rnd << 8) | (unsigned char)b; + } + rnd &= bit_mask; + if (rnd <= range) return from + rnd; + } +} + +static int multirand_integer(lua_State *L) { + lua_Integer arg1, arg2; + lua_settop(L, 2); + arg1 = luaL_checkinteger(L, 1); + if (lua_toboolean(L, 2)) { + arg2 = luaL_optinteger(L, 2, 0); + if (arg1 > arg2) { + return luaL_error(L, + "Upper boundary is smaller than lower boundary." + ); + } else if (arg1 == arg2) { + lua_pushinteger(L, arg1); + } else { + lua_pushinteger(L, multirand_range(L, arg1, arg2)); + } + } else { + luaL_argcheck(L, arg1 >= 1, 1, "smaller than 1"); + lua_pushinteger(L, multirand_range(L, 1, arg1)); + } + return 1; +} + +static int multirand_fraction(lua_State *L) { + int i, j; + lua_settop(L, 0); + lua_Number rnd = 0.0; + for (i = 0; i < 8; i++) { + int b; + unsigned char c; + b = getc(multirand_dev); + if (b == EOF) return luaL_error(L, "I/O error while reading random."); + c = (unsigned char) b; + for (j = 0; j < 8; j++) { + rnd /= 2.0; + if (c & 1) rnd += 0.5; + c >>= 1; + } + } + lua_pushnumber(L, rnd); + return 1; +} + +static int multirand_string(lua_State *L) { + int length; + const char *charset; + size_t charset_size; + luaL_Buffer buf; + lua_settop(L, 2); + length = luaL_checkint(L, 1); + charset = luaL_optlstring(L, 2, "abcdefghijklmnopqrstuvwxyz", &charset_size); + if (charset_size > 32767) { + return luaL_error(L, "Set of chars is too big."); + } + luaL_buffinit(L, &buf); + for (; length > 0; length--) { + luaL_addchar(&buf, + charset[multirand_range(L, 0, (lua_Integer)charset_size - 1)] + ); + } + luaL_pushresult(&buf); + return 1; +} + +static const struct luaL_Reg multirand_module_functions[] = { + {"integer", multirand_integer}, + {"fraction", multirand_fraction}, + {"string", multirand_string}, + {NULL, NULL} +}; + +int luaopen_multirand(lua_State *L) { + const char *module_name; + lua_settop(L, 1); + module_name = lua_tostring(L, 1); + if (module_name) { + luaL_register(L, module_name, multirand_module_functions); + } else { + luaL_register(L, "multirand", multirand_module_functions); + } + lua_replace(L, 1); + multirand_dev = fopen("/dev/urandom", "r"); + if (!multirand_dev) return luaL_error(L, "Could not open /dev/urandom."); + return 1; +} diff -r 000000000000 -r 9fdfb27f8e67 libraries/nihil/nihil.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/nihil/nihil.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,52 @@ +#!/usr/bin/env lua + +local error = error +local getmetatable = getmetatable +local module = module +local rawset = rawset +local setmetatable = setmetatable + +module(...) + +metatable = { + __tostring = function(self) + return "nil" .. self[1] + end, + __newindex = function() + error("Objects representing nil are immutable.") + end +} + +nils = setmetatable({}, { + __mode = "v", + __index = function(self, level) + if level > 0 then + local result = setmetatable({ level }, metatable) + rawset(self, level, result) + return result + end + end, + __newindex = function() + error("Table is immutable.") + end +}) + +function lift(value) + if value == nil then + return nils[1] + elseif getmetatable(value) == metatable then + return nils[value[1]+1] + else + return value + end +end + +function lower(value) + if value == nil then + error("Cannot lower nil.") + elseif getmetatable(value) == metatable then + return nils[value[1]-1] + else + return value + end +end diff -r 000000000000 -r 9fdfb27f8e67 libraries/rocketcgi/rocketcgi.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libraries/rocketcgi/rocketcgi.lua Sun Oct 25 12:00:00 2009 +0100 @@ -0,0 +1,219 @@ +#!/usr/bin/env lua + +local assert = assert +local error = error +local getfenv = getfenv +local getmetatable = getmetatable +local ipairs = ipairs +local next = next +local pairs = pairs +local pcall = pcall +local print = print +local rawequal = rawequal +local rawget = rawget +local rawset = rawset +local select = select +local setfenv = setfenv +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local type = type +local unpack = unpack +local xpcall = xpcall + +local io = io +local math = math +local os = os +local string = string +local table = table + +module(...) + +data_sent = false + +function add_header(...) + if data_sent then + error("Can not add header after data has been sent.", 2) + end + io.stdout:write(...) + io.stdout:write("\r\n") +end + +function send_data(...) + if not data_sent then + io.stdout:write("\r\n") + data_sent = true + end + io.stdout:write(...) +end + +function set_status(status) + add_header("Status: ", status) +end + +function redirect(location) + set_status("303 See Other") + add_header("Location: ", location) +end + +function set_content_type(content_type) + add_header("Content-Type: ", content_type) +end + +method = os.getenv("REQUEST_METHOD") or false +query = os.getenv("QUERY_STRING") or false +cookie_data = os.getenv("HTTP_COOKIE") or false +post_data = io.stdin:read("*a") or false +post_contenttype = os.getenv("CONTENT_TYPE") or false +params = {} +get_params = {} +cookies = {} +post_params = {} +post_filenames = {} +post_types = {} + +local urldecode +do + local b0 = string.byte("0") + local b9 = string.byte("9") + local bA = string.byte("A") + local bF = string.byte("F") + local ba = string.byte("a") + local bf = string.byte("f") + function urldecode(str) + return ( + string.gsub( + string.gsub(str, "%+", " "), + "%%([0-9A-Fa-f][0-9A-Fa-f])", + function(hex) + local n1, n2 = string.byte(hex, 1, 2) + if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 + elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 + elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 + else return end + if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 + elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 + elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 + else return end + return string.char(n1 * 16 + n2) + end + ) + ) + end +end + +local function proc_param(tbl, key, value) + if string.find(key, "%[%]$") then + if tbl[key] then + table.insert(tbl[key], value) + else + local list = { value } + params[key] = list + tbl[key] = list + end + else + params[key] = value + tbl[key] = value + end +end + +local function read_urlencoded_form(tbl, data) + for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do + proc_param(tbl, urldecode(rawkey), urldecode(rawvalue)) + end +end + +if query then + read_urlencoded_form(get_params, query) +end + +if cookie_data then + for rawkey, rawvalue in string.gmatch(cookie_data, "([^=; ]*)=([^=; ]*)") do + cookies[urldecode(rawkey)] = urldecode(rawvalue) + end +end + +if post_contenttype == "application/x-www-form-urlencoded" then + read_urlencoded_form(post_params, post_data) +elseif post_contenttype then + local boundary = string.match( + post_contenttype, + '^multipart/form%-data[ \t]*;[ \t]*boundary="([^"]+)"' + ) or string.match( + post_contenttype, + '^multipart/form%-data[ \t]*;[ \t]*boundary=([^"; \t]+)' + ) + if boundary then + local parts = {} + do + local boundary = "\r\n--" .. boundary + local post_data = "\r\n" .. post_data + local pos1, pos2 = string.find(post_data, boundary, 1, true) + while true do + local ind = string.sub(post_data, pos2 + 1, pos2 + 2) + if ind == "\r\n" then + local pos3, pos4 = string.find(post_data, boundary, pos2 + 1, true) + if pos3 then + parts[#parts + 1] = string.sub(post_data, pos2 + 3, pos3 - 1) + else + error("Illegal POST data.") + end + pos1, pos2 = pos3, pos4 + elseif ind == "--" then + break + else + error("Illegal POST data.") + end + end + end + for i, part in ipairs(parts) do + local pos = 1 + local name, filename, contenttype + while true do + local header + do + local oldpos = pos + pos = string.find(part, "\r\n", oldpos, true) + if not pos then + error("Illegal POST data.") + end + if pos == oldpos then break end + header = string.sub(part, oldpos, pos - 1) + pos = pos + 2 + end + if string.find( + string.lower(header), + "^content%-disposition:[ \t]*form%-data[ \t]*;" + ) then + -- TODO: handle all cases correctly + name = string.match(header, ';[ \t]*name="([^"]*)"') or + string.match(header, ';[ \t]*name=([^"; \t]+)') + filename = string.match(header, ';[ \t]*filename="([^"]*)"') or + string.match(header, ';[ \t]*filename=([^"; \t]+)') + else + local dummy, subpos = string.find( + string.lower(header), + "^content%-type:[ \t]*" + ) + if subpos then + contenttype = string.sub(header, subpos + 1, #header) + end + end + end + local content = string.sub(part, pos + 2, #part) + if not name then + error("Illegal POST data.") + end + proc_param(post_params, name, content) + post_filenames[name] = filename + post_types[name] = contenttype + end + end +end + +if post_data and #post_data > 262144 then + post_data = nil + collectgarbage("collect") +else + post_data = nil +end