diff --git a/.gitignore b/.gitignore index 42e3f2e..8e44a59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ experienceA experienceB result -src/v +src/janusweb janusweb* myverse diff --git a/src/.init.lua b/src/.init.lua index f6e79f2..b6424b5 100644 --- a/src/.init.lua +++ b/src/.init.lua @@ -4,8 +4,10 @@ package.path = package.path .. ";middleware/?.lua" package.path = package.path .. ";cmd/?.lua" package.path = package.path .. ";plugin/?.lua" -util = require "util" -json = require "json" +util = require "util" +json = require "json" +liluat = require "liluat" +template = require "template" -- special script called by main redbean process at startup HidePath('/usr/share/zoneinfo/') @@ -22,10 +24,16 @@ app = require("soakbean") { cmd={ help = {file="cmd/help.lua", info="display all possible flags"}, server = {file="cmd/server.lua", info="starts the webserver"}, + admin = {file="cmd/server.lua", info="launch adminpanel to configure stuff (/admin)"}, createplugin = {file="cmd/createplugin.lua", info="generates skeleton plugin"}, generate = {file="cmd/generate.lua", info="generate www-folder with XR experiences"}, test = {file="cmd/test.lua", info="run testsuite (selftest)"} - } + }, + addPlugin = function(file) + local p = require(file) + table.insert(app.plugin,p) + return p + end } -- api skeleton (implemented via soakbean's app.on(...)) @@ -35,16 +43,16 @@ app.init = function() end -- load config app.opts = require('config')(app) --- load system plugins -app.plugin.moderation = require('plugin/moderation') -app.plugin.diskscan = require('plugin/diskscan') -app.plugin.xrfragments = require('plugin/xrfragments') -app.plugin.janusxr = require('plugin/janusxr') -app.plugin.manyfold = require('plugin/manyfold') -app.plugin.git = require('plugin/git') -app.plugin.clouddrive = require('plugin/clouddrive') -app.plugin.webhook = require('plugin/webhook') -app.plugin.markdown = require('plugin/markdown') +-- load system plugins (in specific order for admin panel) +app.plugin.diskscan = app.addPlugin('plugin/diskscan') +app.plugin.moderation = app.addPlugin('plugin/moderation') +app.plugin.xrfragments = app.addPlugin('plugin/xrfragments') +app.plugin.janusxr = app.addPlugin('plugin/janusxr') +app.plugin.manyfold = app.addPlugin('plugin/manyfold') +app.plugin.git = app.addPlugin('plugin/git') +app.plugin.clouddrive = app.addPlugin('plugin/clouddrive') +app.plugin.webhook = app.addPlugin('plugin/webhook') +app.plugin.markdown = app.addPlugin('plugin/markdown') -- ensure plugin config-defaults foreach( app.plugin, function(name,plugin) @@ -56,13 +64,6 @@ foreach( app.plugin, function(name,plugin) end) end) --- set order preference (optional) -app.plugin[1] = app.plugin.diskscan -app.plugin[2] = app.plugin.moderation -app.plugin[3] = app.plugin.janusxr -app.plugin[4] = app.plugin.xrfragments -app.plugin[5] = app.plugin.manyfold - app.runcmd(app) -- run clicmds print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on @@ -87,69 +88,8 @@ end) app -- .use( require("middleware/httpauth")() ) -.use( function(req,res,next) - local tile = require('page/partial/tile') - local rootpath = "/" .. app.opts.plugin_diskscan_dirname - local urlpath = GetPath() - local isdir = urlpath:sub(-1) == "/" - local nslashes = util.countChar('/',urlpath) - res.items = {} - local match = function(a,b) return util.stripPath(a) == util.stripPath(b) end - if urlpath:match("^"..rootpath) then - local redirect = false - if not app.cache.url[ urlpath ] then - foreach( app.cache.items, function(k,v) - if v.parent then - trace( "path = " .. util.stripPath(v.path) .. " " .. - "parent = " .. util.stripPath(v.parent.path) .. " " .. - "urlpath = " .. util.stripPath(urlpath) - ) - if match(v.path, urlpath) then -- show parent folder if any - local parent = app.cache.items[ v.parent.id ] - if v.portal then - redirect = v.portal.url - end - if match(parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed - local root = app.cache.items[ app.opts.plugin_diskscan_dir ] - res.items[ root.id ] = root - end - end - - -- add items from dir - if match(v.parent.path, urlpath) then - local parent = app.cache.items[ v.parent.id ] - res.items[ v.id ] = v - --if parent.parent then - -- res.items[ v.id ] = v - --else - -- res.items[ parent.id ] = parent - --end - end - end - end) - local data = util.merge({items=''},{ opts = app.opts }) - foreach( res.items, function(k,v) - data.items = data.items .. tile( util.flatten(v,"item") ) - end) - app.cache.url[ urlpath ] = util.template( LoadAsset("page/dir.html"), data) - end - if redirect then - print("REDIRECT! " .. redirect ) - res.status(303) - res.header('Location', redirect ) - print_r(res) - next() - else - res.status(200) - res.header('content-type','text/html; charset=utf-8') - res.body( app.cache.url[ urlpath ] ) - next() - end - else - next() - end -end) .use( require("json").middleware() ) -- try plug'n'play json API middleware +.use( require('page/dir').middleware ) -- directory browser .use( app.router( app.url ) ) -- try url router .use( app.response() ) -- try serve app response (if any) .use( function(req,next) Route() end) -- fallback default redbean fileserver diff --git a/src/.lua/liluat.lua b/src/.lua/liluat.lua new file mode 100644 index 0000000..83c72ff --- /dev/null +++ b/src/.lua/liluat.lua @@ -0,0 +1,535 @@ +--[[ +-- liluat - Lightweight Lua Template engine +-- +-- Project page: https://github.com/FSMaxB/liluat +-- +-- liluat is based on slt2 by henix, see https://github.com/henix/slt2 +-- +-- Copyright © 2016 Max Bruckner +-- Copyright © 2011-2016 henix +-- +-- 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. +--]] + +local liluat = { + private = {} --used to expose private functions for testing +} + +-- print the current version +liluat.version = function () + return "1.2.0" +end + +-- returns a string containing the fist line until the last line +local function string_lines(lines, first, last) + -- allow negative line numbers + first = (first >= 1) and first or 1 + + local start_position + local current_position = 1 + local line_counter = 1 + repeat + if line_counter == first then + start_position = current_position + end + current_position = lines:find('\n', current_position + 1, true) + line_counter = line_counter + 1 + until (line_counter == (last + 1)) or (not current_position) + + return lines:sub(start_position, current_position) +end +liluat.private.string_lines = string_lines + +-- escape a string for use in lua patterns +-- (this simply prepends all non alphanumeric characters with '%' +local function escape_pattern(text) + return text:gsub("([^%w])", "%%%1" --[[function (match) return "%"..match end--]]) +end +liluat.private.escape_pattern = escape_pattern + +-- recursively copy a table +local function clone_table(table) + local clone = {} + + for key, value in pairs(table) do + if type(value) == "table" then + clone[key] = clone_table(value) + else + clone[key] = value + end + end + + return clone +end +liluat.private.clone_table = clone_table + +-- recursively merge two tables, the second one has precedence +-- if 'shallow' is set, the second table isn't copied recursively, +-- its content is only referenced instead +local function merge_tables(a, b, shallow) + a = a or {} + b = b or {} + + local merged = clone_table(a) + + for key, value in pairs(b) do + if (type(value) == "table") and (not shallow) then + if a[key] then + merged[key] = merge_tables(a[key], value) + else + merged[key] = clone_table(value) + end + else + merged[key] = value + end + end + + return merged +end +liluat.private.merge_tables = merge_tables + +local default_options = { + start_tag = "{{", + end_tag = "}}", + trim_right = "code", + trim_left = "code" +} + +-- initialise table of options (use the provided, default otherwise) +local function initialise_options(options) + return merge_tables(default_options, options) +end + +-- creates an iterator that iterates over all chunks in the given template +-- a chunk is either a template delimited by start_tag and end_tag or a normal text +-- the iterator also returns the type of the chunk as second return value +local function all_chunks(template, options) + options = initialise_options(options) + + -- pattern to match a template chunk + local template_pattern = escape_pattern(options.start_tag) .. "([+-]?)(.-)([+-]?)" .. escape_pattern(options.end_tag) + local include_pattern = "^"..escape_pattern(options.start_tag) .. "[+-]?include:(.-)[+-]?" .. escape_pattern(options.end_tag) + local expression_pattern = "^"..escape_pattern(options.start_tag) .. "[+-]?=(.-)[+-]?" .. escape_pattern(options.end_tag) + local position = 1 + + return function () + if not position then + return nil + end + + local template_start, template_end, trim_left, template_capture, trim_right = template:find(template_pattern, position) + + local chunk = {} + if template_start == position then -- next chunk is a template chunk + if trim_left == "+" then + chunk.trim_left = false + elseif trim_left == "-" then + chunk.trim_left = true + end + if trim_right == "+" then + chunk.trim_right = false + elseif trim_right == "-" then + chunk.trim_right = true + end + + local include_start, include_end, include_capture = template:find(include_pattern, position) + local expression_start, expression_end, expression_capture + if not include_start then + expression_start, expression_end, expression_capture = template:find(expression_pattern, position) + end + + if include_start then + chunk.type = "include" + chunk.text = include_capture + elseif expression_start then + chunk.type = "expression" + chunk.text = expression_capture + else + chunk.type = "code" + chunk.text = template_capture + end + + position = template_end + 1 + return chunk + elseif template_start then -- next chunk is a text chunk + chunk.type = "text" + chunk.text = template:sub(position, template_start - 1) + position = template_start + return chunk + else -- no template chunk found --> either text chunk until end of file or no chunk at all + chunk.text = template:sub(position) + chunk.type = "text" + position = nil + return (#chunk.text > 0) and chunk or nil + end + end +end +liluat.private.all_chunks = all_chunks + +local function read_entire_file(path) + assert(path) + local file = assert(io.open(path)) + local file_content = file:read('*a') + file:close() + return file_content +end +liluat.private.read_entire_file = read_entire_file + +-- a whitelist of allowed functions +local sandbox_whitelist = { + ipairs = ipairs, + next = next, + pairs = pairs, + rawequal = rawequal, + rawget = rawget, + rawset = rawset, + select = select, + tonumber = tonumber, + tostring = tostring, + type = type, + unpack = unpack, + string = string, + table = table, + math = math, + os = { + date = os.date, + difftime = os.difftime, + time = os.time, + }, + coroutine = coroutine +} + +-- puts line numbers in front of a string and optionally highlights a single line +local function prepend_line_numbers(lines, first, highlight) + first = (first and (first >= 1)) and first or 1 + lines = lines:gsub("\n$", "") -- make sure the last line isn't empty + lines = lines:gsub("^\n", "") -- make sure the first line isn't empty + + local current_line = first + 1 + return string.format("%3d: ", first) .. lines:gsub('\n', function () + local highlight_char = ' ' + if current_line == tonumber(highlight) then + highlight_char = '> ' + end + + local replacement = string.format("\n%3d:%s", current_line, highlight_char) + current_line = current_line + 1 + + return replacement + end) +end +liluat.private.prepend_line_numbers = prepend_line_numbers + +-- creates a function in a sandbox from a given code, +-- name of the execution context and an environment +-- that will be available inside the sandbox, +-- optionally overwrite the whitelist +local function sandbox(code, name, environment, whitelist, reference) + whitelist = whitelist or sandbox_whitelist + name = name or 'unknown' + + -- prepare the environment + environment = merge_tables(whitelist, environment, reference) + + local func + local error_message + if setfenv then --Lua 5.1 and compatible + if code:byte(1) == 27 then + error("Lua bytecode not permitted.", 2) + end + func, error_message = loadstring(code) + if func then + setfenv(func, environment) + end + else -- Lua 5.2 and later + func, error_message = load(code, name, 't', environment) + end + + -- handle compile error and print pretty error message + if not func then + local line_number, message = error_message:match(":(%d+):(.*)") + -- lines before and after the error + local lines = string_lines(code, line_number - 3, line_number + 3) + error( + 'Syntax error in sandboxed code "' .. name .. '" in line ' .. line_number .. ':\n' + .. message .. '\n\n' + .. prepend_line_numbers(lines, line_number - 3, line_number), + 3 + ) + end + + return func +end +liluat.private.sandbox = sandbox + +local function parse_string_literal(string_literal) + return sandbox('return' .. string_literal, nil, nil, {})() +end +liluat.private.parse_string_literal = parse_string_literal + +-- add an include to the include_list and throw an error if +-- an inclusion cycle is detected +local function add_include_and_detect_cycles(include_list, path) + local parent = include_list[0] + while parent do -- while the root hasn't been reached + if parent[path] then + error("Cyclic inclusion detected") + end + + parent = parent[0] + end + + include_list[path] = { + [0] = include_list + } +end +liluat.private.add_include_and_detect_cycles = add_include_and_detect_cycles + +-- extract the name of a directory from a path +local function dirname(path) + return path:match("^(.*/).-$") or "" +end +liluat.private.dirname = dirname + +-- splits a template into chunks +-- chunks are either a template delimited by start_tag and end_tag +-- or a text chunk (everything else) +-- @return table +local function parse(template, options, output, include_list, current_path) + options = initialise_options(options) + current_path = current_path or "." -- current include path + + include_list = include_list or {} -- a list of files that were included + local output = output or {} + + for chunk in all_chunks(template, options) do + -- handle includes + if chunk.type == "include" then -- include chunk + local include_path_literal = chunk.text + local path = parse_string_literal(include_path_literal) + + -- build complete path + if path:find("^/") then + --absolute path, don't modify + elseif options.base_path then + path = options.base_path .. "/" .. path + else + path = dirname(current_path) .. path + end + + add_include_and_detect_cycles(include_list, path) + + local included_template = read_entire_file(path) + parse(included_template, options, output, include_list[path], path) + elseif (chunk.type == "text") and output[#output] and (output[#output].type == "text") then + -- ensure that no two text chunks follow each other + output[#output].text = output[#output].text .. chunk.text + else -- other chunk + table.insert(output, chunk) + end + + end + + return output +end +liluat.private.parse = parse + +-- inline included template files +-- @return string +function liluat.inline(template, options, start_path) + options = initialise_options(options) + + local output = {} + for _,chunk in ipairs(parse(template, options, nil, nil, start_path)) do + if chunk.type == "expression" then + table.insert(output, options.start_tag .. "=" .. chunk.text .. options.end_tag) + elseif chunk.type == "code" then + table.insert(output, options.start_tag .. chunk.text .. options.end_tag) + else + table.insert(output, chunk.text) + end + end + + return table.concat(output) +end + +-- @return { string } +function liluat.get_dependencies(template, options, start_path) + options = initialise_options(options) + + local include_list = {} + parse(template, options, nil, include_list, start_path) + + local dependencies = {} + local have_seen = {} -- list of includes that were already added + local function recursive_traversal(list) + for key, value in pairs(list) do + if (type(key) == "string") and (not have_seen[key]) then + have_seen[key] = true + table.insert(dependencies, key) + recursive_traversal(value) + end + end + end + + recursive_traversal(include_list) + return dependencies +end + +-- compile a template into lua code +-- @return { name = string, code = string / function} +function liluat.compile(template, options, template_name, start_path) + options = initialise_options(options) + template_name = template_name or 'liluat.compile' + + local output_function = "__liluat_output_function" + + -- split the template string into chunks + local lexed_template = parse(template, options, nil, nil, start_path) + + -- table of code fragments the template is compiled into + local lua_code = {} + + for i, chunk in ipairs(lexed_template) do + -- check if the chunk is a template (either code or expression) + if chunk.type == "expression" then + table.insert(lua_code, output_function..'('..chunk.text..')') + elseif chunk.type == "code" then + table.insert(lua_code, chunk.text) + else --text chunk + -- determine if this block needs to be trimmed right + -- (strip newline) + local trim_right = false + if lexed_template[i - 1] and (lexed_template[i - 1].trim_right == true) then + trim_right = true + elseif lexed_template[i - 1] and (lexed_template[i - 1].trim_right == false) then + trim_right = false + elseif options.trim_right == "all" then + trim_right = true + elseif options.trim_right == "code" then + trim_right = lexed_template[i - 1] and (lexed_template[i - 1].type == "code") + elseif options.trim_right == "expression" then + trim_right = lexed_template[i - 1] and (lexed_template[i - 1].type == "expression") + end + + -- determine if this block needs to be trimmed left + -- (strip whitespaces in front) + local trim_left = false + if lexed_template[i + 1] and (lexed_template[i + 1].trim_left == true) then + trim_left = true + elseif lexed_template[i + 1] and (lexed_template[i + 1].trim_left == false) then + trim_left = false + elseif options.trim_left == "all" then + trim_left = true + elseif options.trim_left == "code" then + trim_left = lexed_template[i + 1] and (lexed_template[i + 1].type == "code") + elseif options.trim_left == "expression" then + trim_left = lexed_template[i + 1] and (lexed_template[i + 1].type == "expression") + end + + if trim_right and trim_left then + -- both at once + if i == 1 then + if chunk.text:find("^.*\n") then + chunk.text = chunk.text:match("^(.*\n)%s-$") + elseif chunk.text:find("^%s-$") then + chunk.text = "" + end + elseif chunk.text:find("^\n") then --have to trim a newline + if chunk.text:find("^\n.*\n") then --at least two newlines + chunk.text = chunk.text:match("^\n(.*\n)%s-$") or chunk.text:match("^\n(.*)$") + elseif chunk.text:find("^\n%s-$") then + chunk.text = "" + else + chunk.text = chunk.text:gsub("^\n", "") + end + else + chunk.text = chunk.text:match("^(.*\n)%s-$") or chunk.text + end + elseif trim_left then + if i == 1 and chunk.text:find("^%s-$") then + chunk.text = "" + else + chunk.text = chunk.text:match("^(.*\n)%s-$") or chunk.text + end + elseif trim_right then + chunk.text = chunk.text:gsub("^\n", "") + end + if not (chunk.text == "") then + table.insert(lua_code, output_function..'('..string.format("%q", chunk.text)..')') + end + end + end + + return { + name = template_name, + code = table.concat(lua_code, '\n') + } +end + +-- compile a file +-- @return { name = string, code = string / function } +function liluat.compile_file(filename, options) + return liluat.compile(read_entire_file(filename), options, filename, filename) +end + +-- @return a coroutine function +function liluat.render_coroutine(template, environment, options) + options = initialise_options(options) + environment = merge_tables({__liluat_output_function = coroutine.yield}, environment, options.reference) + + return sandbox(template.code, template.name, environment, nil, options.reference) +end + +-- @return string +function liluat.render(t, env, options) + options = initialise_options(options) + + local result = {} + + -- add closure that renders the text into the result table + env = merge_tables({ + __liluat_output_function = function (text) + table.insert(result, text) end + }, + env, + options.reference + ) + + -- compile and run the lua code + local render_function = sandbox(t.code, t.name, env, nil, options.reference) + local status, error_message = pcall(render_function) + if not status then + local line_number, message = error_message:match(":(%d+):(.*)") + message = message or '' + line_number = line_number or 0 + -- lines before and after the error + local lines = string_lines(t.code, line_number - 3, line_number + 3) + error( + 'Runtime error in sandboxed code "' .. t.name .. '" in line ' .. line_number .. ':\n' + .. message .. '\n\n' + .. prepend_line_numbers(lines, line_number - 3, line_number), + 2 + ) + end + + return table.concat(result) +end + +return liluat + diff --git a/src/.lua/soakbean.lua b/src/.lua/soakbean.lua index 06ef10d..05d4074 100644 --- a/src/.lua/soakbean.lua +++ b/src/.lua/soakbean.lua @@ -22,8 +22,9 @@ sb = { if type(v) == "function" then sb.data[k] = (function(k,v) return function(a,b,c,d,e,f) - v(a,b,c,d,e,f) + local ret = v(a,b,c,d,e,f) sb.pub(k,v,a,b,c,d,e,f) + return ret end end)(k,v) else @@ -52,9 +53,64 @@ sb = { -- print( util.tpl("${name} is ${value}", {name = "foo", value = "bar"}) ) -- "foo is bar" - tpl = function(s,tab) - if not s then return "" end - return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end)) + -- print( util.tpl("${foo(1)}", { foo = function(i) return i+1 end}) ) + -- 2 + -- print( util.tpl("${if foo then} bar ${end}", { foo = true }) ) + -- bar + tpl = function(s, tab) + tab = tab or sb.data + return (s:gsub('($%b{})', function(w) + local inner = w:sub(3, -2):match("^%s*(.-)%s*$") + -- Execute as raw Lua code if it contains spaces (e.g. "if bar then") or equals "end" + if inner:find("%s") or inner == "end" then + local fn = (loadstring or load)("return function(tab) " .. inner .. " end") + if fn then + setfenv(fn(), setmetatable(tab, { __index = _G }))() + end + return "" + end + + local k, arg = w:match("^%${%s*([%w_]+)%s*%(?(.-)%)?%s*}") + local v = tab[k] + if type(v) == "function" then + return tostring(v(load("return " .. arg)()) or '') + end + return v ~= nil and tostring(v) or w + end)) + end, + tpl = function(s, tab) + tab = tab or sb.data + return (s:gsub('($%b{})', function(w) + local inner = w:sub(3, -2):match("^%s*(.-)%s*$") + -- Execute as raw Lua code if it contains spaces or equals "end" + if inner:find("%s") or inner == "end" then + local fn = (loadstring or load)("return function(tab) " .. inner .. " end") + if fn then + setfenv(fn(), setmetatable(tab, { __index = _G }))() + end + return "" + end + -- Allow dots in variable names along with alphanumeric characters and underscores + local path, arg = w:match("^%${%s*([%w_%.]+)" .. "%s*%(?(.-)%)?%s*}") + -- Traverse the nested table path (e.g. "foo.bar" -> tab["foo"]["bar"]) + local v = tab + if path then + for key in path:gmatch("[%w_]+") do + if type(v) == "table" then + v = v[key] + else + v = nil + break + end + end + else + v = nil + end + if type(v) == "function" then + return tostring(v(load("return " .. arg)()) or '') + end + return v ~= nil and tostring(v) or w + end)) end, write = function(str) diff --git a/src/.lua/template.lua b/src/.lua/template.lua new file mode 100644 index 0000000..0a0496e --- /dev/null +++ b/src/.lua/template.lua @@ -0,0 +1,130 @@ +-- local compile = require "tmpl" +-- +-- local s = [[ +-- +-- +--
+-- {! this is a comment !} +-- +--