#!/usr/bin/env lua5.3 -- 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,sep) sep = sep or '&' local function decode(str, path) local str = str if not path then str = str:gsub('+', ' ') end str = str:gsub("%%(%x%x)", function(c) return string.char(tonumber(c, 16)) end) return (str:gsub('\r\n', '\n')) end local values = {} for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) 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 cookies = env "HTTP_COOKIE" and parse_qs(env "HTTP_COOKIE","; ") or {} local history = {} if cookies.history then for page in cookies.history:gmatch "(%w%w%w%w%w)%," do table.insert(history, page) end end local flags = {} for k,v in pairs(cookies) do local flag = k:match "flag_(.+)" if flag then flags[flag] = v end 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, tmp_flags) flags = flags or {} local directive, args = line:match "^#([A-Za-z]+)%s*(.-)\n?$" directive = directive and directive:lower() if not directive then return elseif directive == "redirect" then local redirect = args:match "^%s*(%w%w%w%w%w)%s*$" if not redirect then return end directives.redirect = redirect elseif directive == "deadend" then directives.deadend = true elseif directive == "set" then if args:match "^%s*$" then return end local flagname, text = args:match "^%s*([%w%-_]+)%s*(.-)%s*$" if not flagname then return end text = text or "" if utf8.len(flagname) > 150 or utf8.len(text) > 1000 then return end directives.flags_updated[flagname] = text tmp_flags[flagname] = text elseif directive == "unset" then local flagname = args:match "^%s*([%w%-_]+)%s*$" if not flagname then return end directives.flags_updated[flagname] = false tmp_flags[flagname] = nil elseif directive == "require" then local flagname = args:match "^%s*([%w%-_]+)%s*$" if not flagname then return end directives.require[flagname] = true else return end return true end local load_page local function convert_markup(m) local result = {} local tmp_flags = {} for name, value in pairs(flags) do tmp_flags[name] = value end local directives = {flags_updated = {}, require = {}} local code_block = false for line in (m..'\n'):gmatch "(.-)\n" do if not code_block then if line:match "^%s*$" then goto continue end if line:sub(1,1) == '#' and parse_directive(line, directives, tmp_flags) then if directives.redirect then local to = load_page(directives.redirect) if to then local m, d = convert_markup(to.content) -- the final destination will not have a redirect -- value in this directive. as such, this will store -- final destination of a chain of redirects. d.redirect = d.redirect or to return m, d else directives.redirect = nil end else goto continue end end line = html_encode(line) if line:sub(1,1) == ' ' then table.insert(result, '
')
				code_block = true
			else
				line = line:gsub("\\\\([%[%]\\])", "\%1")
				line = line:gsub("\\([%[%]])", 
					{ ['['] = "[", [']'] = "]" })
				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 target_requirements(p) local page = load_page(p, false, true) if not page then return true end local _,directives = convert_markup(page.content) for k,v in pairs(directives.require) do if not flags[k] then return false end end return true 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)) elseif line:match("^!image") then page.illustration = line:match "^!image%s+(%w+)" elseif line:match("^!archive") then page.archive = true 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 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 local page = parse_page(s) page.id = p return page end local function new_action(page, action, result) local _, directives = convert_markup(result) ::generate_name:: 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) local exists = io.open('content/'..new_name, 'r') if exists then exists:close() goto generate_name end local old = assert(io.open('content/'..page, 'a')) local new = assert(io.open('content/'..new_name, 'w')) 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))) if env "REMOTE_ADDR" then new:write(('!ip %s\n'):format(env "REMOTE_ADDR")) end 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 hist_template = template [[ ]] local function show_hist(show_ids) local log = {} if #history == 0 then return "" end for i=#history,1,-1 do local page = load_page(history[i]) if not page then goto continue end -- highlight the current page local title = i ~= #history and html_encode(page.title) or ''..html_encode(page.title)..'' if show_ids then table.insert(log, ('
  • %s %s
  • ') :format(title, history[i])) else table.insert(log, ('
  • %s
  • '):format(title)) end ::continue:: end return hist_template { log = table.concat(log), } end local map = {} local page_template = template [[

    $title

    $illustration $content $drawthis $log ]] local draw_this = [[

    illustrate this

    ]] map["^/g/(%w%w%w%w%w)$"] = function(p) local page = load_page(p) if not page then return not_found() end local content, directives = convert_markup(page.content) if env "REQUEST_METHOD" ~= "POST" then if history[#history] ~= p then table.insert(history, p) end if #history > 75 then table.remove(history, 1) end local title = page.title if directives.redirect then page = directives.redirect end local actions = {} for _,a in ipairs(page.actions) do if target_requirements(a.target) then table.insert(actions, ('
  • %s
  • '):format( html_encode(a.target), html_encode(a.action))) end end if not directives.deadend and not page.archive then table.insert(actions, ([[
  • %s
  • ]]):format(page.id, p, #page.actions == 0 and "do something..." or "do something else...") ) end local illustration, draw_this if page.illustration then illustration = ([[ ]]):format(page.id, page.illustration) else -- draw_this = ([[ --

    -- illustrate this --

    -- ]]):format(p) end local cookies = {} table.insert(cookies, ('history=%s; path=/; secure; max-age=99999999999') :format(table.concat(history, ',')..',')) for flagname, text in pairs(directives.flags_updated) do if text then table.insert(cookies, ('flag_%s=%s; secure; max-age=999999999999') :format(flagname, url_encode(text))) else -- clear the cookie by expiring it in the past table.insert(cookies, ('flag_%s=; expires=Thu, 01 Jan 1970 00:00:00 GMT;') :format(flagname)) end end return base { title = html_encode(title), content = page_template { title = html_encode(title), content = content, actions = table.concat(actions), illustration = illustration, drawthis = draw_this, log = show_hist(), }, }, {headers = {["set-cookie"] = cookies, bees = "3.14"}} 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 map["^/g/(%w%w%w%w%w)/$"] = function(p) return redirect('/g/'..p) end local edit_template = template [[ $content
    $preview

    READ THIS before touching anything.

    what do you do?

    what happens next?

    cancel $submit
    $log
    ]] 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 local query = parse_qs(env "QUERY_STRING" or "") local cancel = '/g/'..(query.back or p) if env "REQUEST_METHOD" ~= "POST" then return base { title = "do something new", content = edit_template { page = p, content = convert_markup(page.content), log = show_hist(true), cancel = html_encode(cancel), }, } else local form = parse_qs(io.read "a") form.wyd = form.wyd or "something" form.happens = form.happens or "something" local prev, prev_direct = convert_markup(form.happens) local prev_title = prev_direct.title and html_encode(prev_direct.title) or html_encode(form.wyd) if prev_direct.redirect then local note = ('previewing %s') :format(prev_direct.redirect.id) prev = note..prev end return base { title = "do something new", content = edit_template { page = p, content = convert_markup(page.content), preview = preview_template { title = prev_title, content = prev, }, title = html_encode(form.wyd), editing = html_encode(form.happens), submit = submit_template { page = p }, cancel = html_encode(cancel), log = show_hist(true), }, } end end local illustrate_template = template [[

    $title

    $content

    what does this look like?

    ]] -- map["^/g/(%w%w%w%w%w)/illustrate"] = function(p) -- local page = load_page(p) -- if not page then return not_found() end -- -- if env "REQUEST_METHOD" ~= "POST" then -- return base { -- title = "illustration: " .. html_encode(page.title), -- content = illustrate_template { -- title = html_encode(page.title), -- content = convert_markup(page.content), -- page = p, -- }, -- } -- else -- end -- end map["^/i/(%w%w%w%w%w).(%w+)$"] = function(p, format) local page = load_page(p) if not page or not page.illustration or not page.illustration == format then return not_found() end return assert(io.open(('content/%s.%s'):format(p, format), 'r')), { content_type = 'image/'..format } 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 page = page:gsub("\n!ip [^\n]*", "") return page, { content_type = 'text/plain', headers = { ['access-control-allow-origin'] = "*" } } end map["^/g/?$"] = function() if #history > 0 then return redirect('/g/'..history[#history]) else return redirect '/g/zzcxz' end end map["^/about/?$"] = function() return assert(io.open("about.html", 'r')) end map["^/robots.txt$"] = function() return assert(io.open("robots.txt", 'r')), { content_type = 'text/plain' } end local index_template = template [[ zzcxz
    ]] map["^/$"] = function() local last_page = load_page(history[#history] or 'zzcxz') or load_page 'zzcxz' local actions = ('
  • %s
  • ') :format(last_page.id, html_encode(last_page.title)) if last_page.id ~= 'zzcxz' then actions = actions .. '
  • reenter the zzcxz
  • ' end return index_template {actions = actions} end local function main() 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' and type(content) ~= 'userdata') 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 resp.headers['apioforms'] = math.random(0,104942)/2 print("status: "..resp.status) for k,v in pairs(resp.headers) do if type(v) == "table" then for _, v2 in ipairs(v) do print(("%s: %s"):format(k, v2)) end else print(("%s: %s"):format(k, v)) end end print "" if type(content) == 'string' then io.write(content) else while 1 do local data = content:read(1024) if not data then break end io.write(data) end end