#!/usr/bin/haserl --upload-limit=16384 --shell=lua --accept-all
<%
local sqlite3 = require("lsqlite3");
local digest = require("openssl").digest;
local bcrypt = require("bcrypt");
local crypto = {};
local cgi = {};
local html = {};
html.board = {};
html.post = {};
html.container = {};
html.table = {};
html.list = {};
html.pdp = {};
html.string = {};
local generate = {};
local board = {};
local post = {};
post.pseudo = {};
local file = {};
local identity = {};
identity.session = {};
local captcha = {};
local log = {};
local global = {};
local misc = {}
local nanodb = sqlite3.open("nanochan.db");
-- Ensure all required tables exist.
nanodb:exec("CREATE TABLE IF NOT EXISTS Global (Name, Value)");
nanodb:exec("CREATE TABLE IF NOT EXISTS Boards (Name, Title, Subtitle, MaxPostNumber, Lock, DisplayOverboard, MaxThreadsPerHour, MinThreadChars, BumpLimit, PostLimit, ThreadLimit, RequireCaptcha, CaptchaTriggerPPH)"); -- MaxThreadsPerHour actually 12 hours instead of 1hr
nanodb:exec("CREATE TABLE IF NOT EXISTS Posts (Board, Number, Parent, Date, LastBumpDate, Name, Email, Subject, Comment, File, Sticky, Cycle, Autosage, Lock, tvolDeleteName, tvolDeleteDate)");
nanodb:exec("CREATE TABLE IF NOT EXISTS File (Name, ThumbWidth, ThumbHeight)");
nanodb:exec("CREATE TABLE IF NOT EXISTS Refs (Board, Referee, Referrer)");
nanodb:exec("CREATE TABLE IF NOT EXISTS Accounts (Name, Type, Board, PwHash, Creator, MaxActionsPerDay, MaxModifiedPostAge)");
nanodb:exec("CREATE TABLE IF NOT EXISTS Sessions (Key, Account, ExpireDate)");
nanodb:exec("CREATE TABLE IF NOT EXISTS Logs (Name, Board, Date, Description)");
nanodb:exec("CREATE TABLE IF NOT EXISTS Captchas (Text, ExpireDate)");
nanodb:busy_timeout(10000);
--
-- Additional functions.
--
-- called whenever math.random is used
local seed_generated = false
function misc.generateseed()
if seed_generated then
return;
else
seed_generated = true
end
local fd = io.open("/dev/urandom","rb");
local seed = 0;
for i=0,string.byte(fd:read(1)) do
seed = seed + string.byte(fd:read(1));
end
math.randomseed(seed);
fd:close();
end
-- create audit entries for mod actions (only deletes are supported)
function misc.audit(action, boardname, number, reason)
local result = post.retrieve(boardname, number)
local f = io.open("audit.log", "a");
f:write("-----------------------------BEGIN AUDIT ENTRY-----------------------------\n\n");
f:write("Username: ", username, "\n");
f:write("Action: ", action, "\n");
f:write("Reason: ", reason, "\n\n");
f:write("Board: ", result["Board"], "\n");
f:write("Post No: ", result["Number"], "\n");
f:write("File Name: ", result["File"], "\n");
f:write("Parent Thread: ", result["Parent"], "\n");
f:write("Date Created: ", result["Date"], "\n");
f:write("Name: ", result["Name"], "\n");
f:write("Email: ", result["Email"], "\n");
f:write("Subject: ", result["Subject"], "\n");
if result["Parent"] == 0 then
f:write("Date of Last Bump: ", result["LastBumpDate"], "\n");
f:write("Sticky?: ", result["Sticky"], "\n");
f:write("Cycle?: ", result["Cycle"], "\n");
f:write("Autosage?: ", result["Autosage"], "\n");
f:write("Lock?: ", result["Lock"], "\n");
end
f:write("\n");
f:write("Contents:\n", result["Comment"], "\n\n");
if result["Parent"] == 0 then
local threads = post.threadreplies(boardname, number);
for i = 1, #threads do
local result2 = post.retrieve(boardname, threads[i]);
f:write("----------------BEGIN CHILD POST-------------------\n");
f:write("Board: ", result2["Board"], "\n");
f:write("Post No: ", result2["Number"], "\n");
f:write("File Name: ", result2["File"], "\n");
f:write("Parent Thread: ", result2["Parent"], "\n");
f:write("Date Created: ", result2["Date"], "\n");
f:write("Name: ", result2["Name"], "\n");
f:write("Email: ", result2["Email"], "\n");
f:write("Subject: ", result2["Subject"], "\n");
f:write("\n");
f:write("Contents:\n", result2["Comment"], "\n");
f:write("-----------------END CHILD POST--------------------\n\n");
end
end
f:write("------------------------------END AUDIT ENTRY------------------------------\n");
f:write("\n\n\n\n");
f:close();
end
function file.unlink(filename)
local posts = {};
local stmt = nanodb:prepare("SELECT Board, Number, Parent FROM Posts WHERE File = ?");
stmt:bind_values(filename);
for tbl in stmt:nrows() do
posts[#posts + 1] = tbl;
end
stmt:finalize();
local stmt = nanodb:prepare("UPDATE Posts SET File = '' WHERE File = ?");
stmt:bind_values(filename);
stmt:step();
stmt:finalize();
-- smart page regeneration
local generated_overboard = false;
local generated_board = {};
local generated_thread = {};
for i = 1, #posts do
local boardname = posts[i]["Board"];
local number = posts[i]["Number"];
local threadparent = posts[i]["Parent"] == 0 and true or false;
local parent = threadparent and number or posts[i]["Parent"];
if threadparent and not generated_overboard and board.retrieve(boardname)["DisplayOverboard"] == 1 then
generate.overboard();
generated_overboard = true;
end
if threadparent and not generated_board[boardname] then
generate.catalog(boardname);
generated_board[boardname] = true;
end
if not generated_thread[boardname .. parent] then
generate.thread(boardname, parent);
generated_thread[boardname .. parent] = true;
end
end
end
function file.thumbnail_dimensions_get(filename)
local stmt = nanodb:prepare("SELECT ThumbWidth, ThumbHeight FROM File WHERE Name = ?");
stmt:bind_values(filename);
local width = 0;
local height = 0;
if stmt:step() ~= sqlite3.ROW then
stmt:finalize();
width, height = file.thumbnail_dimensions_set(filename);
else
width, height = unpack(stmt:get_values());
stmt:finalize();
end
width = (width and width ~= 1) and width or "";
height = (height and height ~= 1) and height or "";
return width, height;
end
function file.thumbnail_dimensions_set(filename)
-- hack, change if thumbnails go above 255x255
local _,_,width = os.execute("width_temp=$(gm identify -format '%w' " .. file.thumbnail(filename) .. "); exit $width_temp");
local _,_,height = os.execute("height_temp=$(gm identify -format '%h' " .. file.thumbnail(filename) .. "); exit $height_temp");
local stmt = nanodb:prepare("INSERT INTO File VALUES (?, ?, ?)");
stmt:bind_values(filename, width, height);
stmt:step();
stmt:finalize();
return width, height;
end
function global.retrieveflag(flag, default) -- default given as bool
if not global.retrieve(flag) then
default = default and "1" or "0";
global.set(flag, default);
end
return (global.retrieve(flag) == "1") and true or false;
end
function global.setflag(flag, value) -- value given as bool
value = value and "1" or "0";
global.set(flag, value);
end
function html.recentsfilter()
io.write("
");
end
function misc.recents(page, limit, regen)
local filterstr;
if not regen then -- if regen is false, don't ignore form inputs
local filter = {};
filter[#filter + 1] = FORM["sage"] and "Email = 'sage'";
filter[#filter + 1] = FORM["file"] and "File != ''";
filter[#filter + 1] = FORM["parent"] and "Parent = 0";
for i = 1, #filter do
filterstr = (filterstr and filterstr or " WHERE ") .. filter[i] .. (i ~= #filter and " AND " or "");
end
end
local stmt = nanodb:prepare("SELECT Board, Number FROM Posts" .. (filterstr or "") .. " ORDER BY Date DESC LIMIT ? OFFSET ?");
stmt:bind_values(limit, tonumber((page - 1) * limit));
local posts = {};
for tbl in stmt:nrows() do
posts[#posts + 1] = tbl;
end
stmt:finalize();
for i = 1, #posts do
html.post.render(posts[i]["Board"], posts[i]["Number"], true);
io.write(i ~= #posts and "" or "");
end
end
function html.stats()
html.container.begin("wide");
html.table.begin("stats", "Board", "TPD (24h)", "TPW (7d)", "PPH (1h)", "PPH (24h)", "PPD (24h)", "PPD (7d)", "Total Posts");
local boards = board.list();
local total = {};
for i = 1, #boards do
local rows = {board.format(boards[i]),
board.tph(boards[i], 24, false), board.tph(boards[i], 168, false),
board.pph(boards[i], 1, false), board.pph(boards[i], 24, true),
board.pph(boards[i], 24, false), board.pph(boards[i], 168, true),
board.retrieve(boards[i])["MaxPostNumber"]};
html.table.entry(rows[1],
string.format("%d", rows[2]), string.format("%d", rows[3]),
string.format("%d", rows[4]), string.format("%.1f", rows[5]),
string.format("%d", rows[6]), string.format("%.1f", rows[7] * 24),
rows[8]);
for j = 2, #rows do
total[j] = (total[j] or 0) + rows[j];
end
end
html.table.entry("total",
string.format("%d", total[2]), string.format("%d", total[3]),
string.format("%d", total[4]), string.format("%.1f", total[5]),
string.format("%d", total[6]), string.format("%.1f", total[7] * 24),
total[8]);
html.table.finish();
html.container.finish();
end
function misc.retrievestats(regen)
local time = tonumber(global.retrieve("StatsLastRegen"));
local timenow = os.time();
if not io.fileexists("stats-cached.html") or regen or not time or timenow - time > 30 then -- more than 30 seconds have passed since the last regen
time = timenow;
global.set("StatsLastRegen", tostring(time));
io.output("stats-cached.html");
html.stats();
io.close();
io.output(io.stdout);
end
local f = io.open("stats-cached.html", "r");
io.write(f:read("*a"));
f:close();
io.write("
(Table generated on ");
io.write(os.date("!%F %T", time), ", ", tostring((timenow - time) or 0));
io.write(" second", (timenow - time) == 1 and "" or "s", " ago.)
");
end
function identity.changeconfig(name, maxactions, maxmodpostage)
maxactions, maxmodpostage = tonumber(maxactions), tonumber(maxmodpostage);
if not (maxactions and maxmodpostage and maxmodpostage >= 0) then -- define ranges
return false;
elseif maxactions < -1 then
maxactions = -1;
end
local stmt = nanodb:prepare("UPDATE Accounts SET MaxActionsPerDay = ? WHERE Name = ?");
stmt:bind_values(maxactions, name);
stmt:step();
stmt:finalize();
local stmt = nanodb:prepare("UPDATE Accounts SET MaxModifiedPostAge = ? WHERE Name = ?");
stmt:bind_values(maxmodpostage, name);
stmt:step();
stmt:finalize();
return true;
end
-- pseudo deletion functions
function post.pseudo.delete(boardname, number)
local identity_tbl = identity.retrieve(username);
local post_tbl = post.retrieve(boardname, number);
local timenow = os.time();
-- perform checks on tvol (only place where tvol checks are performed, since this is the only action they can make)
if post_tbl["tvolDeleteName"] and post_tbl["tvolDeleteName"] ~= "" then
return false, "The post already pseudo-deleted.";
elseif post_tbl["Date"] < timenow - identity_tbl["MaxModifiedPostAge"] then
return false, "The post is older than your account's maximum modifiable post age.";
else
local daytosecs = 1 * 60 * 60 * 24;
local stmt = nanodb:prepare("SELECT COUNT() FROM Posts WHERE tvolDeleteName = ? and tvolDeleteDate > ?");
stmt:bind_values(name, timenow - daytosecs);
stmt:step();
local actioncount = stmt:get_value(0);
stmt:finalize();
if actioncount > identity_tbl["MaxActionsPerDay"] then
return false, "You have reached the maximum number of actions you can perform for today.";
end
end
-- perform the actual "deletion"
local stmt = nanodb:prepare("UPDATE Posts SET tvolDeleteName = ? WHERE Board = ? AND Number = ?");
stmt:bind_values(username, boardname, number);
stmt:step();
stmt:finalize();
local stmt = nanodb:prepare("UPDATE Posts SET tvolDeleteDate = ? WHERE Board = ? AND Number = ?");
stmt:bind_values(timenow, boardname, number);
stmt:step();
stmt:finalize();
return true, nil;
end
function post.pseudo.verify(boardname, number)
-- just send the mod to the post delete url lol
-- but in the same url, check if tvolDeleteName exists, and reference it in the log entry
return nil;
end
function post.pseudo.restore(boardname, number)
local stmt = nanodb:prepare("UPDATE Posts SET tvolDeleteName = '' WHERE Board = ? AND Number = ?");
stmt:bind_values(boardname, number);
stmt:step();
stmt:finalize();
local stmt = nanodb:prepare("UPDATE Posts SET tvolDeleteDate = 0 WHERE Board = ? AND Number = ?");
stmt:bind_values(boardname, number);
stmt:step();
stmt:finalize();
end
function post.movethread(boardname, number, destboard)
local threads = post.threadreplies(boardname, number);
-- html2nano -> fix refs -> post.create(bypass_spamlimits) -> post.delete()
local newnumber = nil;
return newnumber;
end
--
-- Miscellaneous functions.
--
function string.tokenize(input, delimiter)
local result = {};
if input == nil then
return {};
end
for match in (input .. delimiter):gmatch("(.-)" .. delimiter) do
result[#result + 1] = match;
end
return result;
end
function string.random(length, pattern)
length = length or 64;
pattern = pattern or "a-zA-Z0-9"
local result = "";
local ascii = {};
local dict;
misc.generateseed();
for i = 0, 255 do
ascii[#ascii + 1] = string.char(i);
end
ascii = table.concat(ascii);
dict = ascii:gsub("[^" .. pattern .. "]", "");
while string.len(result) < length do
local randidx = math.random(1, string.len(dict));
local randbyte = dict:byte(randidx);
result = result .. string.char(randbyte);
end
return result;
end
function string.striphtml(input)
local result = input;
result = result:gsub("<.->", "");
return result;
end
function string.escapehtml(input)
return input:gsub("&", "&")
:gsub("<", "<")
:gsub(">", ">")
:gsub("\"", """)
:gsub("'", "'");
end
function string.unescapehtml(input)
return input:gsub("&", "&")
:gsub("<", "<")
:gsub(">", ">")
:gsub(""", "\"")
:gsub("'", "'");
end
function io.fileexists(filename)
local f = io.open(filename, "r");
if f ~= nil then
f:close();
return true;
else
return false;
end
end
function io.filesize(filename)
local fp = io.open(filename);
local size = fp:seek("end");
fp:close();
return size;
end
--
-- CGI- and HTTP-related initialization
--
-- Initialize cgi variables.
cgi.pathinfo = ENV["PATH_INFO"] and string.tokenize(ENV["PATH_INFO"]:gsub("^/", ""), "/") or {}; -- removes preceeding slashes before tokenizing
cgi.referer = ENV["HTTP_REFERER"];
--
-- Global configuration functions.
--
function global.retrieve(name)
local stmt = nanodb:prepare("SELECT Value FROM Global WHERE Name = ?");
stmt:bind_values(name);
if stmt:step() ~= sqlite3.ROW then
stmt:finalize();
return nil;
end
local result = stmt:get_value(0);
stmt:finalize();
return result;
end
function global.delete(name)
local stmt = nanodb:prepare("DELETE FROM Global WHERE Name = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
end
function global.set(name, value)
if global.retrieve(name) ~= nil then
global.delete(name);
end
local stmt = nanodb:prepare("INSERT INTO Global VALUES (?, ?)");
stmt:bind_values(name, value);
stmt:step();
stmt:finalize();
end
--
-- Cryptographic functions.
--
function crypto.hash(hashtype, data)
-- local bstring = digest.new(hashtype):final(data);
-- local result = {};
-- for i = 1, #bstring do
-- result[#result + 1] = string.format("%02x", string.byte(bstring:sub(i,i)));
-- end
-- return table.concat(result);
return digest.new(hashtype):final(data);
end
--
-- Board-related functions.
--
function board.list()
local boards = {}
for tbl in nanodb:nrows("SELECT Name FROM Boards ORDER BY MaxPostNumber DESC") do
boards[#boards + 1] = tbl["Name"];
end
return boards;
end
function board.retrieve(name)
local stmt = nanodb:prepare("SELECT * FROM Boards WHERE Name = ?");
stmt:bind_values(name);
local stepret = stmt:step();
if stepret ~= sqlite3.ROW then
stmt:finalize();
return nil;
end
local result = stmt:get_named_values();
stmt:finalize();
return result;
end
function board.validname(name)
return name and ((not name:match("[^a-z0-9]")) and (#name > 0) and (#name <= 8));
end
function board.validtitle(title)
return title and ((#title > 0) and (#title <= 32));
end
function board.validsubtitle(subtitle)
return subtitle and ((#subtitle >= 0) and (#subtitle <= 64));
end
function board.exists(name)
local stmt = nanodb:prepare("SELECT Name FROM Boards WHERE Name = ?");
stmt:bind_values(name);
local stepret = stmt:step();
stmt:finalize();
if stepret ~= sqlite3.ROW then
return false;
else
return true;
end
end
function board.format(name)
return board.validname(name) and ("/" .. name .. "/") or nil;
end
function board.create(name, title, subtitle)
if not board.validname(name) then
return nil;
end
local maxpostnumber = 0;
local lock = 0;
local maxthreadsperhour = 0;
local minthreadchars = 0;
local bumplimit = 300;
local postlimit = 350;
local threadlimit = 200;
local displayoverboard = 1;
local requirecaptcha = 0;
local captchatrigger = 30;
local stmt = nanodb:prepare("INSERT INTO Boards VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
stmt:bind_values(name,
string.escapehtml(title),
string.escapehtml(subtitle),
maxpostnumber,
lock,
displayoverboard,
maxthreadsperhour,
minthreadchars,
bumplimit,
postlimit,
threadlimit,
requirecaptcha,
captchatrigger);
stmt:step();
stmt:finalize();
os.execute("mkdir " .. name);
generate.mainpage();
generate.catalog(name);
generate.overboard();
end
function board.update(board_tbl)
-- escapehtml() Title and Subtitle before passing board_tbl to this function if needed
local stmt = nanodb:prepare("UPDATE Boards SET " ..
"Title = ?, Subtitle = ?, Lock = ?, MaxThreadsPerHour = ?, MinThreadChars = ?, " ..
"BumpLimit = ?, PostLimit = ?, ThreadLimit = ?, DisplayOverboard = ?, RequireCaptcha = ?, " ..
"CaptchaTriggerPPH = ? WHERE Name = ?");
stmt:bind_values(board_tbl["Title"], board_tbl["Subtitle"],
board_tbl["Lock"], board_tbl["MaxThreadsPerHour"], board_tbl["MinThreadChars"],
board_tbl["BumpLimit"], board_tbl["PostLimit"], board_tbl["ThreadLimit"], board_tbl["DisplayOverboard"],
board_tbl["RequireCaptcha"], board_tbl["CaptchaTriggerPPH"], board_tbl["Name"]);
stmt:step();
stmt:finalize();
generate.catalog(board_tbl["Name"]);
generate.overboard();
local threads = post.listthreads(board_tbl["Name"]);
for i = 1, #threads do
generate.thread(board_tbl["Name"], threads[i]);
end
end
-- Delete a board.
function board.delete(name)
local stmt = nanodb:prepare("DELETE FROM Boards WHERE Name = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
stmt = nanodb:prepare("DELETE FROM Accounts WHERE Board = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
stmt = nanodb:prepare("DELETE FROM Refs WHERE Board = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
generate.mainpage();
generate.overboard();
end
-- Get number of threads made in the last 'hours' hours divided by 'hours'
function board.tph(name, hours, divide)
hours = hours or 12;
local start_time = os.time() - (hours * 3600);
local stmt = nanodb:prepare("SELECT COUNT(Number) FROM Posts WHERE Board = ? AND Date > ? AND Parent = 0");
stmt:bind_values(name, start_time);
stmt:step();
local count = stmt:get_value(0);
stmt:finalize();
return divide and count / hours or count;
end
-- Get board PPH (number of posts made in the last 'hours' hours divided by 'hours')
function board.pph(name, hours, divide)
hours = hours or 12;
local start_time = os.time() - (hours * 3600);
local stmt = nanodb:prepare("SELECT COUNT(Number) FROM Posts WHERE Board = ? AND Date > ?");
stmt:bind_values(name, start_time);
stmt:step();
local count = stmt:get_value(0);
stmt:finalize();
return divide and count / hours or count;
end
--
-- Identity (account) functions.
--
function identity.list()
local identities = {};
for tbl in nanodb:nrows("SELECT Name FROM Accounts ORDER BY Name") do
identities[#identities + 1] = tbl["Name"];
end
return identities;
end
function identity.retrieve(name)
local stmt = nanodb:prepare("SELECT * FROM Accounts WHERE Name = ?");
stmt:bind_values(name);
if stmt:step() ~= sqlite3.ROW then
stmt:finalize();
return nil;
end
local result = stmt:get_named_values();
stmt:finalize();
return result;
end
function identity.exists(name)
return identity.retrieve(name) and true or false;
end
-- Class can be either:
-- * "admin" - Site administrator, unlimited powers
-- * "bo" - Board owner, powers limited to a single board
-- * "gvol" - Global volunteer, powers limited by site administrators
-- * "lvol" - Local volunteer, powers limited by board owners, powers limited to a single board
-- * "tvol" - Trial volunteer, powers limited by global volunteers
function identity.create(class, name, password, boardname)
boardname = boardname or "Global";
local creator = username or "System";
local stmt = nanodb:prepare("INSERT INTO Accounts VALUES (?,?,?,?,?,?,?)");
local hash = bcrypt.digest(password, 13);
stmt:bind_values(name, class, boardname, hash, creator, -1, 0);
stmt:step();
stmt:finalize();
end
function identity.validname(name)
return (not name:match("[^a-zA-Z0-9]")) and (#name >= 1) and (#name <= 16);
end
function identity.delete(name)
local stmt = nanodb:prepare("DELETE FROM Accounts WHERE Name = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
stmt = nanodb:prepare("DELETE FROM Sessions WHERE Account = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
stmt = nanodb:prepare("UPDATE Logs SET Name = 'Deleted' WHERE Name = ?");
stmt:bind_values(name);
stmt:step();
stmt:finalize();
end
function identity.changepassword(name, password)
local hash = bcrypt.digest(password, 13);
local stmt = nanodb:prepare("UPDATE Accounts SET PwHash = ? WHERE Name = ?");
stmt:bind_values(hash, name);
stmt:step();
stmt:finalize();
end
function identity.validpassword(password)
return (#password >= 13) and (#password <= 64);
end
function identity.validclass(class)
return (class == "admin" or
class == "gvol" or
class == "bo" or
class == "lvol" or
class == "tvol")
end
function identity.valid(name, password)
local identity_tbl = identity.retrieve(name);
return identity_tbl and bcrypt.verify(password, identity_tbl["PwHash"]) or false;
end
function identity.session.delete(user)
local stmt = nanodb:prepare("DELETE FROM Sessions WHERE Account = ?");
stmt:bind_values(user);
stmt:step();
stmt:finalize();
end
function identity.session.create(user)
-- Clear any existing keys for this user to prevent duplicates.
identity.session.delete(user);
local key = string.random(32);
local expiry = os.time() + 3600; -- key expires in 1 hour
local stmt = nanodb:prepare("INSERT INTO Sessions VALUES (?,?,?)");
stmt:bind_values(key, user, expiry);
stmt:step();
stmt:finalize();
return key;
end
function identity.session.refresh(user)
local stmt = nanodb:prepare("UPDATE Sessions SET ExpireDate = ? WHERE Account = ?");
stmt:bind_values(os.time() + 3600, user);
stmt:step();
stmt:finalize();
end
function identity.session.valid(key)
local result = nil;
if key == nil then return nil end;
for tbl in nanodb:nrows("SELECT * FROM Sessions") do
if os.time() >= tbl["ExpireDate"] then
-- Clean away any expired session keys.
identity.session.delete(tbl["Account"]);
elseif tbl["Key"] == key then
result = tbl["Account"];
end
end
identity.session.refresh(result);
return result;
end
-- Captcha related functions.
function captcha.assemble(cc, outfile) -- cc is table of 6 characters, a-z
local xx, yy, rr, ss, bx, by = {},{},{},{},{},{},{};
misc.generateseed();
for i = 1, 6 do
xx[i] = ((48 * i - 168) + math.random(-5, 5));
yy[i] = math.random(-10, 10);
rr[i] = math.random(-30, 30);
ss[i] = math.random(-40, 40);
bx[i] = (150 + 1.1 * xx[i]);
by[i] = (40 + 2 * yy[i]);
end
os.execute(string.format(
"gm convert -size 290x70 xc:white -bordercolor black -border 5 " ..
"-fill black -stroke black -strokewidth 1 -pointsize 40 " ..
"-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
"-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
"-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
"-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
"-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
"-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
"-fill none -strokewidth 2 " ..
"-draw 'bezier %f,%d %f,%d %f,%d %f,%d' " ..
"-draw 'polyline %f,%d %f,%d %f,%d' -quality 0 -strip -colorspace GRAY JPEG:%s",
xx[1], yy[1], rr[1], ss[1], cc[1],
xx[2], yy[2], rr[2], ss[2], cc[2],
xx[3], yy[3], rr[3], ss[3], cc[3],
xx[4], yy[4], rr[4], ss[4], cc[4],
xx[5], yy[5], rr[5], ss[5], cc[5],
xx[6], yy[6], rr[6], ss[6], cc[6],
bx[1], by[1], bx[2], by[2], bx[3], by[3], bx[4], by[4],
bx[4], by[4], bx[5], by[5], bx[6], by[6],
outfile
));
end
function captcha.listall()
local captcha_list = {}
for tbl in nanodb:nrows("SELECT Text FROM Captchas") do
captcha_list[#captcha_list + 1] = tbl["Text"];
end
return captcha_list
end
function captcha.create()
local max_captchas = 50; -- captchas stored in db at a time, higher means lesser perf
--os.execute("mkdir tmp_captchas/");
local captcha_data;
local captcha_filename;
local captcha_text;
local captcha_list = captcha.listall();
captcha.deleteexpired();
if #captcha_list < max_captchas then
local cc = {};
for i = 1, 6 do
cc[i] = string.random(1, "a-z");
end
captcha_text = table.concat(cc);
captcha_filename = "tmp_captchas/" .. captcha_text;
captcha.assemble(cc, captcha_filename);
local stmt = nanodb:prepare("INSERT INTO Captchas VALUES (?, CAST(strftime('%s', 'now') AS INTEGER) + 3600)"); -- 1 hour to expiry
stmt:bind_values(captcha_text);
stmt:step();
stmt:finalize();
else
-- select existing captcha at random
misc.generateseed();
captcha_text = captcha_list[math.random(1, #captcha_list)];
captcha_filename = "tmp_captchas/" .. captcha_text;
end
local fp = io.open(captcha_filename, "r");
if not fp then
captcha.delete(captcha_text);
return nil;
else
captcha_data = fp:read("*a");
fp:close();
return captcha_data;
end
end
function captcha.retrieve(answer)
local stmt = nanodb:prepare("SELECT * FROM Captchas WHERE Text = ? AND ExpireDate > CAST(strftime('%s', 'now') AS INTEGER)");
stmt:bind_values(answer);
if stmt:step() ~= sqlite3.ROW then
stmt:finalize();
return nil;
end
local result = stmt:get_named_values();
stmt:finalize();
return result;
end
function captcha.delete(answer)
local stmt = nanodb:prepare("DELETE FROM Captchas WHERE Text = ?");
stmt:bind_values(answer);
stmt:step();
stmt:finalize();
os.remove("tmp_captchas/" .. answer);
end
function captcha.deleteexpired()
-- ALSO DELETE CAPTCHAS FROM TMPFOLDER
nanodb:exec("DELETE FROM Captchas WHERE ExpireDate < CAST(strftime('%s', 'now') AS INTEGER)");
end
function captcha.valid(answer)
if not answer then return nil end;
answer = answer:lower();
local captcha_tbl = captcha.retrieve(answer);
if captcha_tbl then captcha.delete(answer) end;
captcha.deleteexpired();
return captcha_tbl and true or false;
end
local skey = COOKIE["session_key"];
username = identity.session.valid(skey);
acctclass = username and identity.retrieve(username)["Type"] or nil;
local assignboard = username and identity.retrieve(username)["Board"] or nil;
--
-- File handling functions.
--
-- Detect the format of a file (PNG, JPG, GIF).
function file.format(path, ext) -- file path, and original provided file extension
local fd = io.open(path, "r");
local data = fd:read(128);
fd:close();
if data == nil or #data == 0 then
return nil;
end
--[[ temporarily disabled, remove the backslash in "\