diff options
author | the lemons <citrons@mondecitronne.com> | 2022-02-05 14:32:35 -0600 |
---|---|---|
committer | the lemons <citrons@mondecitronne.com> | 2022-02-05 14:34:10 -0600 |
commit | 0a80a22b8a9d624c83a6e63a0b853ba42e8c3bc0 (patch) | |
tree | 024722d4f0c823eb45e04c6a5466b599ce235dbb /zzcxz.cgi |
it exists
Diffstat (limited to 'zzcxz.cgi')
-rwxr-xr-x | zzcxz.cgi | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/zzcxz.cgi b/zzcxz.cgi new file mode 100755 index 0000000..df8d3b7 --- /dev/null +++ b/zzcxz.cgi @@ -0,0 +1,446 @@ +#!/usr/bin/env lua +-- this software is licensed under the terms of the GNU affero public license +-- v3 or later. view LICENSE.txt for more information. + +-- if you host your own instance, please change the email address in the about +-- page and clearly distinguish your instance from https://zzcxz.citrons.xyz. + +local env = os.getenv + +local f = io.open("/dev/urandom", 'r') +local e = f and f:read(1) +if f then f:close() end +math.randomseed(os.time() + string.byte(e)) + +local function url_encode(str) + return (str:gsub("([^A-Za-z0-9%_%.%-%~])", function(v) + return string.upper(string.format("%%%02x", string.byte(v))) + end)) +end + +local esc_sequences = { + ["<"] = "<", + [">"] = ">", + ['"'] = """ +} +local function html_encode(x) + local escaped = tostring(x) + escaped = escaped:gsub("&", "&") + + for char,esc in pairs(esc_sequences) do + escaped = string.gsub(escaped, char, esc) + end + return escaped +end + +local function parse_qs(str) + local function decode(str, path) + local str = str + if not path then + str = str:gsub('+', ' ') + end + return (str:gsub("%%(%x%x)", function(c) + return string.char(tonumber(c, 16)) + end)) + end + + local values = {} + for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', '&', '&')) do + local key = decode(key) + local keys = {} + key = key:gsub('%[([^%]]*)%]', function(v) + -- extract keys between balanced brackets + if string.find(v, "^-?%d+$") then + v = tonumber(v) + else + v = decode(v) + end + table.insert(keys, v) + return "=" + end) + key = key:gsub('=+.*$', "") + key = key:gsub('%s', "_") -- remove spaces in parameter name + val = val:gsub('^=+', "") + + if not values[key] then + values[key] = {} + end + if #keys > 0 and type(values[key]) ~= 'table' then + values[key] = {} + elseif #keys == 0 and type(values[key]) == 'table' then + values[key] = decode(val) + end + + local t = values[key] + for i,k in ipairs(keys) do + if type(t) ~= 'table' then + t = {} + end + if k == "" then + k = #t+1 + end + if not t[k] then + t[k] = {} + end + if i == #keys then + t[k] = decode(val) + end + t = t[k] + end + end + return values +end + +local function redirect(to) + return "", { + status = '303 see other', + headers = { location = to }, + } +end + +local function template(str) + return function (t) + return (str:gsub("$([A-Za-z][A-Za-z0-9]*)", function(v) + return t[v] or "" + end)) + end +end + +local base = template [[ +<!doctype html> +<html> + <head> + <link rel="stylesheet" href="/static/amethyst.css" /> + <meta charset="utf-8"/> + <title>zzcxz: $title</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body> + <h1>zzcxz</h1> + <main>$content</main> + <footer> + <div id="about-links"> + <p><a href="/about">help</a></p> + <p><a href="https://citrons.xyz/git/zzcxz.git/about"> + source code + </a></p> + <p><a href="https://citrons.xyz">citrons.xyz</a></p> + </div> + </footer> + </body> +</html> +]] + +local not_found = function() + return + base { + title = "not found", + content = "the content requested was not found.", + }, { status = '404 not found' } +end + +local function parse_directive(line, directives) + local directive, args = line:match "^#([A-Z]+)%s*(.-)\n?$" + if not directive then + return + elseif directive == "BACKLINK" then + local page, action = args:match "^(%w%w%w%w%w)%s+(.+)$" + if not page then return end + directives.backlinks = directives.backlinks or {} + table.insert(directives.backlinks, {page = page, action = action}) + elseif directive == "DEADEND" then + directives.deadend = true + else + return + end + return true +end + +local function convert_markup(m) + local result = {} + local directives = {} + local code_block = false + for line in (m..'\n'):gmatch "(.-)\n" do + line = html_encode(line) + if not code_block then + if line:match "^%s*$" then + goto continue + end + if line:sub(1,1) == '#' and + parse_directive(line, directives) then + goto continue + end + if line:sub(1,1) == ' ' then + table.insert(result, '<pre><code>') + code_block = true + else + line = line:gsub("%[(.-)%]", + function(s) + return ('<span class="important">%s</span>'):format(s) + end + ) + table.insert(result, ('<p>%s</p>'):format(line)) + end + end + if code_block then + if line:sub(1,1) == ' ' then + table.insert(result, line .. '\n') + else + table.insert(result, '</code></pre>') + code_block = false + end + end + ::continue:: + end + if code_block then + table.insert(result, '</code></pre>') + code_block = false + end + return table.concat(result), directives +end + +local function parse_page(s) + local page = {} + page.title = s:match "^(.-)\n" + page.actions = {} + local content = {} + for line in (s..'\n'):gmatch "(.-\n)" do + if line:sub(1,1) == '\t' then + table.insert(content, line:sub(2)) + else + local target, action = line:match "^(%w%w%w%w%w):(.-)\n$" + if action then + table.insert(page.actions, {action = action, target = target}) + end + end + end + page.content = table.concat(content) + + return page +end + +local function load_page(p, raw) + if not p:match("^%w%w%w%w%w$") then return nil end + local f, bee = io.open('content/'..p) + if not f then return nil end + local s = f:read "a" + f:close() + if not s then return nil end + if raw then return s end + return parse_page(s) +end + +local function new_action(page, action, result) + local new_name = {} + for i=1,5 do + table.insert(new_name, string.char(string.byte 'a' + math.random(0,25))) + end + new_name = table.concat(new_name) + assert(not io.open('content/'..new_name, 'r'), "page already exists!") + + local new = assert(io.open('content/'..new_name, 'w')) + local old = assert(io.open('content/'..page, 'a')) + + action = action:gsub('\n', ' ') + assert(new:write(action..'\n')) + for line in (result..'\n'):gmatch "(.-\n)" do + assert(new:write('\t' .. line)) + end + assert(old:write(('%s:%s\n'):format(new_name, action))) + + local _, directives = convert_markup(result) + if directives.backlinks then + for _,d in ipairs(directives.backlinks) do + assert(new:write(('%s:%s\n'):format(d.page, d.action))) + end + end + + new:close() + old:close() + + return new_name +end + +local map = {} + +local page_template = template [[ + <h2>$title</h2> + $content + <ul class="actions"> + $actions + </ul> +]] +map["^/g/(%w%w%w%w%w)/?$"] = function(p) + local page = load_page(p) + if not page then return not_found() end + local _, directives = convert_markup(page.content) + + if env "REQUEST_METHOD" ~= "POST" then + local actions = {} + for _,a in ipairs(page.actions) do + table.insert(actions, + ('<li><a href="%s">%s</a></li>'):format( + html_encode(a.target), html_encode(a.action))) + end + if not directives.deadend then + table.insert(actions, + ([[ + <li><a class="important" href="%s/act#what">%s</a></li> + ]]):format(p, #page.actions == 0 and + "do something..." or "do something else...") + ) + end + + return base { + title = html_encode(page.title), + content = page_template { + title = html_encode(page.title), + content = convert_markup(page.content), + actions = table.concat(actions), + }, + } + else + if directives.deadend then + return base { + title = "error", + content = "forbidden", + }, { status = '403 forbidden' } + end + + local form = parse_qs(io.read "a") + + form.wyd = form.wyd or "something" + form.happens = form.happens or "something" + if utf8.len(form.wyd) > 150 then form.wyd = "something" end + if utf8.len(form.happens) > 10000 then form.wyd = "something" end + + local new = new_action(p, form.wyd, form.happens) + return redirect("/g/"..new) + end +end + +local edit_template = template [[ + $content + <hr id="what"/> + $preview + <form method="POST"> + <p> + <a href="/about#rules">READ THIS</a> before touching anything.</a> + </p> + <h2>what do you do?</h2> + <input + type="text" + id="wyd" name="wyd" + value="$title" + maxlength="150" required + /> + <h2>what happens next?</h2> + <textarea + id="happens" name="happens" + maxlength="10000" required + >$editing</textarea> + <div class="buttons"> + <a href="../$page">cancel</a> + <input type="submit" formaction="act#what" value="preview" /> + $submit + </div> + </form> +]] +local preview_template = template [[ + <h2>$title</h2> + $content + <hr /> +]] +local submit_template = template [[ + <input type="submit" formaction="/g/$page" value="submit" /> +]] +map["^/g/(%w%w%w%w%w)/act$"] = function(p) + local page = load_page(p) + if not page then return not_found() end + + local _, directives = convert_markup(page.content) + if directives.deadend then return not_found() end + + if env "REQUEST_METHOD" ~= "POST" then + return base { + title = "do something new", + content = edit_template { + page = p, + content = convert_markup(page.content), + }, + } + else + local form = parse_qs(io.read "a") + form.wyd = form.wyd or "something" + form.happens = form.happens or "something" + + return base { + title = "do something new", + content = edit_template { + page = p, + content = convert_markup(page.content), + preview = preview_template { + title = html_encode(form.wyd), + content = convert_markup(form.happens), + }, + title = html_encode(form.wyd), + editing = html_encode(form.happens), + submit = submit_template { page = p }, + }, + } + end +end + +map["^/g/(%w%w%w%w%w)/raw$"] = function(p) + local page = load_page(p, true) + if not page then return not_found() end + + return page, { content_type = 'text/plain' } +end + +map["^/about/?$"] = function() + local f = assert(io.open("about.html", 'r')) + local h = assert(f:read 'a') + f:close() + + return h +end + +local function main() + if env "PATH_INFO" == "/" then + return redirect "/g/zzcxz" + end + + for k,v in pairs(map) do + local m = {(env "PATH_INFO"):match(k)} + if m[1] then + return v(table.unpack(m)) + end + end + return not_found() +end + +local ok, content, resp = pcall(main) +if not ok or type(content) ~= 'string' then + io.stderr:write(content..'\n') + + content = base { + title = "internal error", + content = "an internal error occurred." + } + resp = { status = '500 internal server error' } +end + +resp = resp or {} +resp.content_type = resp.content_type or 'text/html' +resp.status = resp.status or '200 OK' +resp.headers = resp.headers or {} +resp.headers['content-type'] = resp.content_type + +print(resp.status) +for k,v in pairs(resp.headers) do + print(("%s: %s"):format(k, v)) +end + +print "" +io.write(content) |