From 0a80a22b8a9d624c83a6e63a0b853ba42e8c3bc0 Mon Sep 17 00:00:00 2001 From: the lemons Date: Sat, 5 Feb 2022 14:32:35 -0600 Subject: it exists --- zzcxz.cgi | 446 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100755 zzcxz.cgi (limited to 'zzcxz.cgi') 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 [[ + + + + + + zzcxz: $title + + + +

zzcxz

+
$content
+ + + +]] + +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, '
')
+				code_block = true
+			else
+				line = line:gsub("%[(.-)%]",
+					function(s)
+						return ('%s'):format(s)
+					end
+				)
+				table.insert(result, ('

%s

'):format(line)) + end + end + if code_block then + if line:sub(1,1) == ' ' then + table.insert(result, line .. '\n') + else + table.insert(result, '
') + code_block = false + end + end + ::continue:: + end + if code_block then + table.insert(result, '') + 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 [[ +

$title

+ $content + +]] +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, + ('
  • %s
  • '):format( + html_encode(a.target), html_encode(a.action))) + end + if not directives.deadend then + table.insert(actions, + ([[ +
  • %s
  • + ]]):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 +
    + $preview +
    +

    + READ THIS before touching anything. +

    +

    what do you do?

    + +

    what happens next?

    + +
    + cancel + + $submit +
    +
    +]] +local preview_template = template [[ +

    $title

    + $content +
    +]] +local submit_template = template [[ + +]] +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) -- cgit v1.2.3