Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/local/bin/haserl -u16384
- <%
- 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 = {};
- local file = {};
- local identity = {};
- identity.session = {};
- local captcha = {};
- local log = {};
- local global = {};
- 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)");
- nanodb:exec("CREATE TABLE IF NOT EXISTS Posts (Board, Number, Parent, Date, LastBumpDate, Name, Email, Subject, Comment, File, Sticky, Cycle, Autosage, Lock)");
- nanodb:exec("CREATE TABLE IF NOT EXISTS Refs (Board, Referee, Referrer)");
- nanodb:exec("CREATE TABLE IF NOT EXISTS Accounts (Name, Type, Board, PwHash)");
- 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);
- --
- -- 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;
- 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)
- local result = input;
- result = result:gsub("&", "&");
- result = result:gsub("<", "<");
- result = result:gsub(">", ">");
- result = result:gsub("\"", """);
- result = result:gsub("'", "'");
- return result;
- 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 {};
- 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);
- 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 stmt = nanodb:prepare("INSERT INTO Boards VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
- local maxpostnumber = 0;
- local lock = 0;
- local maxthreadsperhour = 0;
- local minthreadchars = 0;
- local bumplimit = 300;
- local postlimit = 350;
- local threadlimit = 300;
- local displayoverboard = 1;
- local requirecaptcha = 0;
- local captchatrigger = 30;
- stmt:bind_values(name,
- string.escapehtml(title),
- string.escapehtml(subtitle),
- maxpostnumber,
- lock,
- displayoverboard,
- maxthreadsperhour,
- minthreadchars,
- bumplimit,
- postlimit,
- threadlimit,
- requirecaptcha,
- captchatrigger);
- stmt:step();
- stmt:finalize();
- generate.mainpage();
- generate.catalog(name);
- end
- function board.update(board_tbl)
- local stmt = nanodb:prepare("UPDATE Boards SET " ..
- "Title = ?, Subtitle = ?, Lock = ?, MaxThreadsPerHour = ?, MinThreadChars = ?, " ..
- "BumpLimit = ?, PostLimit = ?, ThreadLimit = ?, DisplayOverboard = ?, RequireCaptcha = ?, " ..
- "CaptchaTriggerPPH = ? WHERE Name = ?");
- stmt:bind_values(string.escapehtml(board_tbl["Title"]), string.escapehtml(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)
- 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 count / hours;
- end
- -- Get board PPH (number of posts made in the last 'hours' hours divided by 'hours')
- function board.pph(name, hours)
- 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 count / hours;
- 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
- function identity.create(class, name, password, boardname)
- boardname = boardname or "Global";
- local stmt = nanodb:prepare("INSERT INTO Accounts VALUES (?,?,?,?)");
- local hash = bcrypt.digest(password, 13);
- stmt:bind_values(name, class, boardname, hash);
- 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 = '<i>Deleted</i>' 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 >= 6) and (#password <= 64);
- end
- function identity.validclass(class)
- return (class == "admin" or
- class == "gvol" or
- class == "bo" or
- class == "lvol")
- 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(outfile)
- local xx, yy, rr, ss, cc, bx, by = {},{},{},{},{},{},{};
- 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);
- cc[i] = string.random(1, "a-z");
- 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
- ));
- return table.concat(cc);
- end
- function captcha.create()
- local outfile = "/tmp/captcha_" .. string.random(6);
- local captcha_text = captcha.assemble(outfile);
- local stmt = nanodb:prepare("INSERT INTO Captchas VALUES (?, CAST(strftime('%s', 'now') AS INTEGER) + 900)");
- stmt:bind_values(captcha_text);
- stmt:step();
- stmt:finalize();
- nanodb:exec("DELETE FROM Captchas WHERE ExpireDate < CAST(strftime('%s', 'now') AS INTEGER)");
- local fp = io.open(outfile, "r");
- local captcha_data = fp:read("*a");
- fp:close();
- os.remove(outfile);
- return captcha_data;
- 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();
- end
- function captcha.valid(answer)
- local captcha_tbl = captcha.retrieve(answer);
- captcha.delete(answer);
- return (captcha_tbl ~= nil) and true or false;
- end
- local skey = COOKIE["session_key"];
- local username = identity.session.valid(skey);
- local 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)
- local fd = io.open(path, "r");
- local data = fd:read(128);
- fd:close();
- if data == nil or #data == 0 then
- return nil;
- end
- if data:sub(1,8) == "\x89PNG\x0D\x0A\x1A\x0A" then
- return "png";
- elseif data:sub(1,3) == "\xFF\xD8\xFF" then
- return "jpg";
- elseif data:sub(1,6) == "GIF87a"
- or data:sub(1,6) == "GIF89a" then
- return "gif";
- elseif data:find("DOCTYPE svg", 1, true)
- or data:find("<svg", 1, true) then
- return "svg";
- elseif data:sub(1,4) == "\x1A\x45\xDF\xA3" then
- return "webm";
- elseif data:sub(5,12) == "ftypmp42"
- or data:sub(5,12) == "ftypisom" then
- return "mp4";
- elseif data:sub(1,2) == "\xFF\xFB"
- or data:sub(1,3) == "ID3" then
- return "mp3";
- elseif data:sub(1,4) == "OggS" then
- return "ogg";
- elseif data:sub(1,4) == "fLaC" then
- return "flac";
- elseif data:sub(1,4) == "%PDF" then
- return "pdf";
- elseif data:sub(1,4) == "PK\x03\x04"
- and data:sub(31,58) == "mimetypeapplication/epub+zip" then
- return "epub";
- else
- return nil;
- end
- end
- function file.extension(filename)
- return filename:match("%.(.-)$");
- end
- function file.class(extension)
- local lookup = {
- ["png"] = "image",
- ["jpg"] = "image",
- ["gif"] = "image",
- ["svg"] = "image",
- ["webm"] = "video",
- ["mp4"] = "video",
- ["mp3"] = "audio",
- ["ogg"] = "audio",
- ["flac"] = "audio",
- ["pdf"] = "document",
- ["epub"] = "document"
- };
- return lookup[extension] or extension;
- end
- function file.has_thumbnails(extension)
- local file_class = file.class(extension);
- return ((file_class == "image") or (file_class == "video") or (extension == "pdf"));
- end
- function file.pathname(filename)
- return "Media/" .. filename;
- end
- function file.thumbnail(filename)
- return "Media/thumb/" .. filename;
- end
- function file.icon(filename)
- return "Media/icon/" .. filename;
- end
- function file.exists(filename)
- if filename == nil or filename == "" then
- return false;
- end
- return io.fileexists(file.pathname(filename));
- end
- function file.size(filename)
- return io.filesize(file.pathname(filename));
- end
- function file.format_size(size)
- if size > (1024 * 1024) then
- return string.format("%.2f MiB", (size / 1024 / 1024));
- elseif size > 1024 then
- return string.format("%.2f KiB", (size / 1024));
- else
- return string.format("%d B", size);
- end
- end
- -- Create a thumbnail which will fit into a 200x200 grid.
- -- Graphicsmagick (gm convert) must be installed for this to work.
- -- Will not modify images which are smaller than 200x200.
- function file.create_thumbnail(filename)
- local path_orig = file.pathname(filename);
- local path_thumb = file.thumbnail(filename);
- local file_extension = file.extension(filename);
- local file_class = file.class(file_extension);
- if io.fileexists(path_thumb) then
- -- Don't recreate thumbnails if they already exist.
- return 0;
- end
- if file_class == "video" then
- return os.execute("ffmpeg -i " .. path_orig .. " -ss 00:00:01.000 -vframes 1 -f image2 - |" ..
- "gm convert -strip - -filter Box -thumbnail 200x200\\> JPEG:" .. path_thumb);
- elseif file_class == "image" or file_extension == "pdf" then
- return os.execute("gm convert -strip " .. path_orig .. "[0] -filter Box -thumbnail 200x200\\> " ..
- ((file_extension == "pdf" or file_extension == "svg") and "PNG:" or "")
- .. path_thumb);
- end
- end
- -- Create a catalog icon (even smaller than a normal thumbnail).
- -- Catalog icons must be extremely small and quality is not particularly important.
- function file.create_icon(filename)
- local path_orig = file.pathname(filename);
- local path_icon = file.icon(filename);
- local file_class = file.class(file.extension(filename));
- if io.fileexists(path_icon) then
- -- Don't recreate icons if they already exist.
- return 0;
- end
- if file_class == "video" then
- return os.execute("ffmpeg -i " .. path_orig .. " -ss 00:00:01.000 -vframes 1 -f image2 - |" ..
- "gm convert -background '#BDC' -flatten -strip - -filter Box -quality 60 " ..
- "-thumbnail 100x70\\> JPEG:" .. path_icon);
- else
- return os.execute("gm convert -background '#BDC' -flatten -strip " .. path_orig ..
- "[0] -filter Box -quality 60 -thumbnail 100x70\\> JPEG:"
- .. path_icon);
- end
- end
- -- Save a file and return its hashed filename. Errors will result in returning nil.
- -- File hashes are always SHA-256 for compatibility with 8chan and friends.
- function file.save(path, create_catalog_icon)
- local extension = file.format(path);
- if extension == nil then
- return nil;
- end
- local fd = io.open(path);
- local data = fd:read("*a");
- fd:close();
- os.remove(path);
- local hash = crypto.hash("sha256", data);
- local filename = hash .. "." .. extension;
- if file.exists(filename) then
- if create_catalog_icon then
- -- The file.create_icon() function will not recreate the icon if it
- -- already exists, so we call it unconditionally here.
- file.create_icon(filename);
- end
- return filename;
- end
- fd = io.open("Media/" .. filename, "w");
- fd:write(data);
- fd:close();
- file.create_thumbnail(filename);
- if create_catalog_icon then
- file.create_icon(filename);
- end
- return filename;
- end
- function file.delete(filename)
- os.remove(file.pathname(filename));
- os.remove(file.thumbnail(filename));
- os.remove(file.icon(filename));
- end
- function post.retrieve(boardname, number)
- local stmt = nanodb:prepare("SELECT * FROM Posts WHERE Board = ? AND Number = ?");
- stmt:bind_values(boardname, tonumber(number));
- if stmt:step() ~= sqlite3.ROW then
- stmt:finalize();
- return nil;
- end
- local result = stmt:get_named_values();
- stmt:finalize();
- return result;
- end
- function post.listthreads(boardname)
- local threads = {};
- if boardname then
- local stmt = nanodb:prepare("SELECT Number FROM Posts WHERE Board = ? AND Parent = 0 ORDER BY Sticky DESC, LastBumpDate DESC");
- stmt:bind_values(boardname);
- for tbl in stmt:nrows() do
- threads[#threads + 1] = tonumber(tbl["Number"]);
- end
- stmt:finalize();
- end
- return threads;
- end
- function post.exists(boardname, number)
- local stmt = nanodb:prepare("SELECT Number FROM Posts WHERE Board = ? AND Number = ?");
- stmt:bind_values(boardname, number);
- local stepret = stmt:step();
- stmt:finalize();
- if stepret ~= sqlite3.ROW then
- return false;
- else
- return true;
- end
- end
- function post.bump(boardname, number)
- local stmt = nanodb:prepare("UPDATE Posts SET LastBumpDate = CAST(strftime('%s', 'now') AS INTEGER) WHERE Board = ? AND Number = ? AND Autosage = 0");
- stmt:bind_values(boardname, tonumber(number));
- stmt:step();
- stmt:finalize();
- end
- function post.toggle(attribute, boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- local current_value = post_tbl[attribute];
- local new_value = (current_value == 1) and 0 or 1;
- local stmt = nanodb:prepare("UPDATE Posts SET " .. attribute .. " = ? WHERE Board = ? AND Number = ?");
- stmt:bind_values(new_value, boardname, number);
- stmt:step();
- stmt:finalize();
- generate.overboard();
- if post_tbl["Parent"] == 0 then
- generate.catalog(boardname);
- generate.thread(boardname, number);
- else
- generate.thread(boardname, post_tbl["Parent"]);
- end
- end
- function post.threadreplies(boardname, number)
- local replies = {};
- local stmt = nanodb:prepare("SELECT Number FROM Posts WHERE Board = ? AND Parent = ? ORDER BY Number");
- stmt:bind_values(boardname, number);
- for tbl in stmt:nrows() do
- replies[#replies + 1] = tonumber(tbl["Number"]);
- end
- stmt:finalize();
- return replies;
- end
- function post.format(boardname, number)
- return board.format(boardname) .. number;
- end
- -- Turn nanochan-formatting into html.
- function post.nano2html(text)
- text = "\n" .. text .. "\n";
- return text:gsub(">>(%d+)", "<a class='reference' href='#post%1'>>>%1</a>")
- :gsub(">>>/([%d%l]-)/(%s)", "<a class='reference' href='/%1'>>>>/%1/</a>%2")
- :gsub(">>>/([%d%l]-)/(%d+)", "<a class='reference' href='/%1/%2.html'>>>>/%1/%2</a>")
- :gsub("\n>(.-)\n", "\n<span class='greentext'>>%1</span>\n")
- :gsub("\n>(.-)\n", "\n<span class='greentext'>>%1</span>\n")
- :gsub("\n<(.-)\n", "\n<span class='pinktext'><%1</span>\n")
- :gsub("\n<(.-)\n", "\n<span class='pinktext'><%1</span>\n")
- :gsub("%(%(%((.-)%)%)%)", "<span class='kiketext'>(((%1)))</span>")
- :gsub("==(.-)==", "<span class='redtext'>%1</span>")
- :gsub("%*%*(.-)%*%*", "<span class='spoiler'>%1</span>")
- :gsub("~~(.-)~~", "<s>%1</s>")
- :gsub("__(.-)__", "<u>%1</u>")
- :gsub("'''(.-)'''", "<b>%1</b>")
- :gsub("''(.-)''", "<i>%1</i>")
- :gsub("(https?://)([a-zA-Z0-9%./%%_%-%+=%?&;:,#%!~]+)", "<a rel='noreferrer' href='%1%2'>%1%2</a>")
- :gsub("\n", "<br />");
- end
- -- This function does not delete the actual file. It simply removes the reference to that
- -- file.
- function post.unlink(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- local stmt = nanodb:prepare("UPDATE Posts SET File = '' WHERE Board = ? AND Number = ?");
- stmt:bind_values(boardname, number);
- stmt:step();
- stmt:finalize();
- generate.thread(boardname, post_tbl["Parent"]);
- end
- function post.delete(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- local stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Number = ?");
- stmt:bind_values(boardname, number);
- stmt:step();
- stmt:finalize();
- -- Delete descendants of that post too, if that post is a thread.
- stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Parent = ?");
- stmt:bind_values(boardname, number);
- stmt:step();
- stmt:finalize();
- -- Delete references to and from that post.
- stmt = nanodb:prepare("DELETE FROM Refs WHERE Board = ? AND (Referrer = ? OR Referee = ?)");
- stmt:bind_values(boardname, number, number);
- stmt:step();
- stmt:finalize();
- -- Delete references to and from every descendant post.
- stmt = nanodb:prepare("DELETE FROM Refs WHERE Board = ? AND (Referrer = (SELECT Number FROM Posts WHERE Board = ? AND Parent = ?) OR Referee = (SELECT Number FROM Posts WHERE Board = ? AND Parent = ?))");
- stmt:bind_values(boardname, boardname, number, boardname, number);
- stmt:step();
- stmt:finalize();
- generate.catalog(boardname);
- generate.overboard();
- if post_tbl["Parent"] == 0 then
- os.remove(boardname .. "/" .. number .. ".html");
- else
- generate.thread(boardname, post_tbl["Parent"]);
- end
- end
- function post.create(boardname, parent, name, email, subject, comment, filename)
- local stmt;
- local board_tbl = board.retrieve(boardname);
- parent = parent or 0;
- name = (name and name ~= "") and string.escapehtml(name) or "Nanonymous";
- email = email and string.escapehtml(email) or "";
- subject = subject and string.escapehtml(subject) or "";
- local references = {};
- -- Find >>xxxxx in posts before formatting is applied.
- for reference in comment:gmatch(">>([0123456789]+)") do
- references[#references + 1] = tonumber(reference);
- end
- comment = comment and post.nano2html(string.escapehtml(comment)) or "";
- filename = filename or "";
- local date = os.time();
- local lastbumpdate = date;
- local autosage = email == "sage" and 1 or 0;
- if name == "##" and username ~= nil then
- local capcode;
- if acctclass == "admin" then
- capcode = "Nanochan Administrator";
- elseif acctclass == "bo" then
- capcode = "Board Owner (" .. board.format(assignboard) .. ")";
- elseif acctclass == "gvol" then
- capcode = "Global Volunteer";
- elseif acctclass == "lvol" then
- capcode = "Board Volunteer (" .. board.format(assignboard) .. ")";
- end
- name = username .. " <span class='capcode'>## " .. capcode .. "</span>";
- end
- name = name:gsub("!(.+)", "<span class='tripcode'>!%1</span>");
- if parent ~= 0 and #post.threadreplies(boardname, parent) >= board_tbl["PostLimit"] then
- -- Delete earliest replies in cyclical thread.
- local stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Number = (SELECT Number FROM Posts WHERE Parent = ? AND Board = ? ORDER BY Number LIMIT 1)");
- stmt:bind_values(boardname, parent, boardname);
- stmt:step();
- stmt:finalize();
- elseif parent == 0 and #post.listthreads(boardname) >= board_tbl["ThreadLimit"] then
- -- Slide threads off the bottom of the catalog.
- local stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Number = (SELECT Number FROM Posts WHERE Parent = 0 AND Sticky = 0 AND Board = ? ORDER BY LastBumpDate LIMIT 1)");
- stmt:bind_values(boardname, boardname);
- stmt:step();
- stmt:finalize();
- end
- nanodb:exec("BEGIN EXCLUSIVE TRANSACTION");
- stmt = nanodb:prepare("UPDATE Boards SET MaxPostNumber = MaxPostNumber + 1 WHERE Name = ?");
- stmt:bind_values(boardname);
- stmt:step();
- stmt:finalize();
- stmt = nanodb:prepare("SELECT MaxPostNumber FROM Boards WHERE Name = ?");
- stmt:bind_values(boardname);
- stmt:step();
- local number = stmt:get_value(0);
- stmt:finalize();
- nanodb:exec("END TRANSACTION");
- stmt = nanodb:prepare("INSERT INTO Posts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
- stmt:bind_values(boardname, number, parent, date, lastbumpdate, name, email, subject, comment, filename, 0, 0, autosage, 0);
- stmt:step();
- stmt:finalize()
- -- Enable the captcha if too many posts were created, and it was not already enabled.
- if board_tbl["CaptchaTriggerPPH"] > 0 and
- board.pph(boardname, 1) > board_tbl["CaptchaTriggerPPH"] and
- board_tbl["RequireCaptcha"] == 0 then
- board_tbl["RequireCaptcha"] = 1;
- board.update(board_tbl);
- log.create("Automatically enabled captcha due to excessive PPH", "<i>System</i>", boardname);
- end
- for i = 1, #references do
- stmt = nanodb:prepare("INSERT INTO Refs SELECT ?, ?, ? WHERE (SELECT COUNT(*) FROM Refs WHERE Board = ? AND Referee = ? AND Referrer = ?) = 0");
- stmt:bind_values(boardname, references[i], number, boardname, references[i], number);
- stmt:step();
- stmt:finalize();
- end
- if parent ~= 0 then
- if not (string.lower(email) == "sage") and
- not (#post.threadreplies(boardname, parent) > board_tbl["BumpLimit"]) then
- post.bump(boardname, parent);
- end
- end
- generate.thread(boardname, (parent ~= 0 and parent or number));
- generate.catalog(boardname);
- if board_tbl["DisplayOverboard"] == 1 then
- generate.overboard();
- end
- return number;
- end
- --
- -- Log access functions.
- --
- function log.create(desc, account, boardname)
- account = account or "<i>System</i>";
- boardname = html.string.boardlink(boardname) or "<i>Global</i>";
- local date = os.time();
- local stmt = nanodb:prepare("INSERT INTO Logs VALUES (?,?,?,?)");
- stmt:bind_values(account, boardname, date, desc);
- stmt:step();
- stmt:finalize();
- end
- function log.retrieve(limit, offset)
- limit = limit or 128;
- offset = offset or 0;
- local entries = {};
- local stmt = nanodb:prepare("SELECT * FROM Logs ORDER BY Date DESC LIMIT ? OFFSET ?");
- stmt:bind_values(limit, offset);
- for tbl in stmt:nrows() do
- entries[#entries + 1] = tbl;
- end
- stmt:finalize();
- return entries;
- end
- --
- -- HTML output functions.
- --
- function html.redirect(location)
- io.write("<!DOCTYPE html>\n");
- io.write("<html>");
- io.write( "<head>");
- io.write( "<title>Redirecting...</title>");
- io.write( "<meta http-equiv='refresh' content='0;url=", location or "/", "' />");
- io.write( "</head>");
- io.write( "<body>");
- io.write( "Redirecting to <a href='", location,"'>", location, "</a>");
- io.write( "</body>");
- io.write("</html>");
- end
- function html.begin(title, name, value)
- if title == nil then
- title = ""
- else
- title = title .. " - "
- end
- io.write("<!DOCTYPE html>\n");
- io.write("<html>");
- io.write( "<head>");
- io.write( "<title>", title, "nanochan</title>");
- io.write( "<link rel='stylesheet' type='text/css' href='/Static/nanochan.css' />");
- io.write( "<link rel='shortcut icon' type='image/png' href='/Static/favicon.png' />");
- if name and value then
- io.write("<meta http-equiv='set-cookie' content='", name, "=", value, ";Path=/Nano' />");
- end
- io.write( "<meta charset='utf-8' />");
- io.write( "<meta name='viewport' content='width=device-width, initial-scale=1.0' />");
- io.write( "</head>");
- io.write( "<body>");
- io.write( "<div id='topbar'>");
- io.write( "<nav id='topnav'>");
- io.write( "<ul>");
- io.write( "<li class='system'><a href='/index.html'>main</a></li>");
- io.write( "<li class='system'><a href='/Nano/mod'>mod</a></li>");
- io.write( "<li class='system'><a href='/Nano/log'>log</a></li>");
- io.write( "<li class='system'><a href='/Nano/stats'>stats</a></li>");
- io.write( "<li class='system'><a href='/overboard.html'>overboard</a></li>");
- local boards = board.list();
- for i = 1, #boards do
- io.write("<li class='board'><a href='/", boards[i], "'>", board.format(boards[i]), "</a></li>");
- end
- io.write( "</ul>");
- io.write( "</nav>");
- io.write( "</div>");
- io.write( "<div id='content'>");
- end
- function html.finish()
- io.write( "</div>");
- io.write( "</body>");
- io.write("</html>");
- end
- function html.redheader(text)
- io.write("<h1 class='redheader'>", text, "</h1>");
- end
- function html.announce()
- if global.retrieve("announce") then
- io.write("<div id='announce'>", global.retrieve("announce"), "</div>");
- end
- end
- function html.container.begin(type)
- io.write("<div class='container ", type or "narrow", "'>");
- end
- function html.container.finish()
- io.write("</div>");
- end
- function html.container.barheader(text)
- io.write("<h2 class='barheader'>", text, "</h2>");
- end
- function html.table.begin(...)
- local arg = {...};
- io.write("<table>");
- io.write("<tr>");
- for i = 1, #arg do
- io.write("<th>", arg[i], "</th>");
- end
- io.write("</tr>");
- end
- function html.table.entry(...)
- local arg = {...};
- io.write("<tr>");
- for i = 1, #arg do
- io.write("<td>", tostring(arg[i]), "</td>");
- end
- io.write("</tr>");
- end
- function html.table.finish()
- io.write("</table>");
- end
- function html.list.begin(type)
- io.write(type == "ordered" and "<ol>" or "<ul>");
- end
- function html.list.entry(text, class)
- io.write("<li", class and (" class='" .. class .. "' ") or "", ">", text, "</li>");
- end
- function html.list.finish(type)
- io.write(type == "ordered" and "</ol>" or "</ul>");
- end
- -- Pre-defined pages.
- function html.pdp.authorization_denied()
- html.begin("permission denied");
- html.redheader("Permission denied");
- html.container.begin();
- io.write("Your account class lacks authorization to perform this action. <a href='/Nano/mod'>Go back.</a>");
- html.container.finish();
- html.finish();
- end
- function html.pdp.error(heading, explanation)
- html.begin("error");
- html.redheader(heading);
- html.container.begin();
- io.write(explanation);
- html.container.finish();
- html.finish();
- end
- function html.pdp.notfound()
- html.begin("404");
- html.redheader("404 Not Found");
- html.container.begin();
- io.write("The resource which was requested does not appear to exist. Please check the URL");
- io.write(" and try again. Alternatively, if you believe this error message to in itself");
- io.write(" be an error, try contacting the nanochan administration.");
- html.container.finish();
- html.finish();
- end
- function html.string.link(href, text, title)
- if not href then
- return nil;
- end
- local result = "<a href='" .. href .. "'";
- if href:sub(1, 1) ~= "/" then
- result = result .. " rel='noreferrer' target='_blank'";
- end
- if title then
- result = result .. " title='" .. title .. "'";
- end
- result = result .. ">" .. (text or href) .. "</a>";
- return result;
- end
- function html.string.datetime(unixtime)
- local isotime = os.date("!%F %T", unixtime);
- return "<time datetime='" .. isotime .. "'>" .. isotime .. "</time>";
- end
- function html.string.boardlink(boardname)
- return html.string.link(board.format(boardname));
- end
- function html.string.threadlink(boardname, number)
- return html.string.link(post.format(boardname, number) .. ".html", post.format(boardname, number));
- end
- function html.board.title(boardname)
- io.write("<h1 id='boardtitle'>", board.format(boardname), " - ", board.retrieve(boardname)["Title"], "</h1>");
- end
- function html.board.subtitle(boardname)
- io.write("<h2 id='boardsubtitle'>", board.retrieve(boardname)["Subtitle"], "</h2>");
- end
- function html.post.postform(boardname, parent)
- local board_tbl = board.retrieve(boardname)
- if board_tbl["Lock"] == 1 and not username then
- return;
- end
- io.write("<a id='new-post' href='#postform' accesskey='p'>", (parent == 0) and "[Start a New Thread]" or "[Make a Post]", "</a>");
- io.write("<fieldset><form id='postform' action='/Nano/post' method='post' enctype='multipart/form-data'>");
- io.write("<input type='hidden' name='board' value='", boardname, "' />");
- io.write("<input type='hidden' name='parent' value='", parent, "' />");
- io.write("<a href='##' class='close-button' accesskey='w'>[X]</a>");
- io.write("<label for='name'>Name</label><input type='text' id='name' name='name' maxlength='64' /><br />");
- io.write("<label for='email'>Email</label><input type='text' id='email' name='email' maxlength='64' /><br />");
- io.write("<label for='subject'>Subject</label><input type='text' id='subject' name='subject' autocomplete='off' maxlength='64' />");
- io.write("<input type='submit' value='Post' accesskey='s' /><br />");
- io.write("<label for='comment'>Comment</label><textarea id='comment' name='comment' form='postform' rows='5' cols='35' maxlength='32768'></textarea><br />");
- io.write("<label for='file'>File</label><input type='file' id='file' name='file' /><br />");
- if board_tbl["RequireCaptcha"] == 1 then
- io.write("<label for='captcha'>Captcha</label><input type='text' id='captcha' name='captcha' autocomplete='off' maxlength='6' /><br />");
- io.write("<img id='captcha-image' width='290' height='70' src='/Nano/captcha.jpg' />");
- end
- io.write("</form></fieldset>");
- end
- function html.post.modlinks(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- io.write("<span class='thread-mod-links'>");
- io.write("<a href='/Nano/mod/post/delete/", boardname, "/", number, "' title='Delete'>[D]</a>");
- if file.exists(post_tbl["File"]) then
- io.write("<a href='/Nano/mod/post/unlink/", boardname, "/", number, "' title='Unlink File'>[U]</a>");
- io.write("<a href='/Nano/mod/file/delete/", post_tbl["File"], "' title='Delete File'>[F]</a>");
- end
- if post_tbl["Parent"] == 0 then
- io.write("<a href='/Nano/mod/post/sticky/", boardname, "/", number, "' title='Sticky'>[S]</a>");
- io.write("<a href='/Nano/mod/post/lock/", boardname, "/", number, "' title='Lock'>[L]</a>");
- io.write("<a href='/Nano/mod/post/autosage/", boardname, "/", number, "' title='Autosage'>[A]</a>");
- io.write("<a href='/Nano/mod/post/cycle/", boardname, "/", number, "' title='Cycle'>[C]</a>");
- end
- io.write("</span>");
- end
- function html.post.threadflags(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- io.write("<span class='thread-info-flags'>");
- if post_tbl["Sticky"] == 1 then io.write("(S)"); end;
- if post_tbl["Lock"] == 1 then io.write("(L)"); end;
- if post_tbl["Autosage"] == 1 then io.write("(A)"); end;
- if post_tbl["Cycle"] == 1 then io.write("(C)"); end;
- io.write("</span>");
- end
- function html.post.render_catalog(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- io.write("<div class='catalog-thread'>");
- io.write( "<div class='catalog-thread-link'><a href='/", boardname, "/", number, ".html'>");
- if file.exists(post_tbl["File"]) then
- local file_ext = file.extension(post_tbl["File"]);
- local file_class = file.class(file_ext);
- if file.has_thumbnails(file_ext) then
- io.write("<img src='/" .. file.icon(post_tbl["File"]) .. "' alt='***' />");
- else
- io.write("<img width='100' height='70' src='/Static/", file_class, ".png' />");
- end
- else
- io.write("***");
- end
- io.write( "</a></div>");
- io.write( "<div class='thread-info'>");
- io.write( "<span class='thread-board-link'><a href='/", boardname, "'>", board.format(boardname), "</a></span> ");
- io.write( "<span class='thread-info-replies'>R:", #post.threadreplies(boardname, number), "</span>");
- html.post.threadflags(boardname, number);
- io.write( "</div>");
- html.post.modlinks(boardname, number);
- io.write( "<div class='catalog-thread-subject'>");
- io.write( post_tbl["Subject"] or "");
- io.write( "</div>");
- io.write( "<div class='catalog-thread-comment'>");
- io.write( post_tbl["Comment"]);
- io.write( "</div>");
- io.write("</div>");
- end
- -- Omitting the 'boardname' value will turn the catalog into an overboard.
- function html.post.catalog(boardname)
- io.write("<a href='' accesskey='r'>[Update]</a>");
- io.write("<hr />");
- io.write("<div class='catalog-container'>");
- if boardname ~= nil then
- -- Catalog mode.
- local threadlist = post.listthreads(boardname);
- for i = 1, #threadlist do
- local number = threadlist[i];
- html.post.render_catalog(boardname, number);
- io.write("<hr class='invisible' />");
- end
- else
- -- Overboard mode.
- for post_tbl in nanodb:nrows("SELECT Board, Number FROM Posts WHERE Parent = 0 AND Autosage = 0 AND (SELECT DisplayOverboard FROM Boards WHERE Name = Board) = 1 ORDER BY LastBumpDate DESC LIMIT 100") do
- html.post.render_catalog(post_tbl["Board"], post_tbl["Number"]);
- io.write("<hr class='invisible' />");
- end
- end
- io.write("</div>");
- end
- function html.post.render(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- io.write("<div class='post' id='post", number, "'>");
- io.write( "<div class='post-header'>");
- io.write( "<span class='post-subject'>", post_tbl["Subject"] or "", "</span> ");
- io.write( "<span class='post-name'>");
- if post_tbl["Email"] ~= "" then
- io.write("<a class='post-email' href='mailto:", post_tbl["Email"], "'>");
- end
- io.write( post_tbl["Name"]);
- if post_tbl["Email"] ~= "" then
- io.write("</a>");
- end
- io.write( "</span> ");
- io.write( "<span class='post-date'>", html.string.datetime(post_tbl["Date"]), "</span> ");
- io.write( "<span class='post-number'>No.<a href='#postform'>", post_tbl["Number"], "</a></span> ");
- if post_tbl["Parent"] == 0 then
- html.post.threadflags(boardname, number);
- end
- html.post.modlinks(boardname, number);
- local stmt = nanodb:prepare("SELECT Referrer FROM Refs WHERE Board = ? AND Referee = ? ORDER BY Referrer");
- stmt:bind_values(boardname, number);
- for referee in stmt:nrows() do
- io.write(" <a class='referee' href='#post", referee["Referrer"], "'>>>", referee["Referrer"], "</a>");
- end
- stmt:finalize();
- io.write( "</div>");
- if file.exists(post_tbl["File"]) then
- local file_ext = file.extension(post_tbl["File"]);
- local file_class = file.class(file_ext);
- io.write("<div class='post-file-info'>");
- io.write("File: <a href='/Media/", post_tbl["File"], "' target='_blank'>", post_tbl["File"], "</a>");
- io.write(" (<a href='/Media/", post_tbl["File"], "' download>", "dl</a>)");
- io.write(" (", file.format_size(file.size(post_tbl["File"])), ")");
- io.write("</div>");
- if file.has_thumbnails(file_ext) then
- io.write("<a target='_blank' href='/Media/" .. post_tbl["File"] .. "'>");
- io.write( "<img class='post-file-thumbnail' src='/", file.thumbnail(post_tbl["File"]), "' />");
- io.write("</a>");
- elseif file_ext == "epub" then
- io.write("<a target='_blank' href='/Media/" .. post_tbl["File"] .. "'>");
- io.write( "<img width='100' height='70' class='post-file-thumbnail' src='/Static/document.png' />");
- io.write("</a>");
- elseif file_class == "audio" then
- io.write("<audio class='post-audio' preload='none' controls loop>");
- io.write( "<source src='/Media/", post_tbl["File"], "' type='audio/", file_ext, "' />");
- io.write("</audio>");
- end
- end
- io.write( "<div class='post-comment'>");
- io.write( post_tbl["Comment"]);
- io.write( "</div>");
- io.write("</div>");
- io.write("<br />");
- end
- function html.post.renderthread(boardname, number)
- local replies = post.threadreplies(boardname, number);
- html.post.render(boardname, number);
- for i = 1, #replies do
- io.write("<hr class='invisible' />");
- html.post.render(boardname, replies[i]);
- end
- end
- function generate.mainpage()
- io.output("index.html");
- html.begin();
- html.redheader("Welcome to Nanochan");
- html.announce(global.retrieve("announce"));
- html.container.begin("narrow");
- io.write("<img id='front-page-logo' src='/Static/logo.png' alt='Nanochan logo' width=400 height=400 />");
- html.container.barheader("Boards");
- local boards = board.list();
- html.list.begin("ordered");
- for i = 1, #boards do
- local board_tbl = board.retrieve(boards[i]);
- html.list.entry(html.string.boardlink(board_tbl["Name"]) .. " - " .. board_tbl["Title"]);
- end
- html.list.finish("ordered");
- html.container.barheader("Rules");
- io.write("These rules apply to all boards on nanochan:");
- html.list.begin("ordered");
- html.list.entry("Child pornography is not permitted. Links to child pornography are not permitted either, " ..
- "and neither are links to websites which contain a significant number of direct links to CP.");
- html.list.entry("Flooding is not permitted. We define flooding as posting similar posts more " ..
- "than 3 times per hour, making a thread on a topic for which a thread already exists, " ..
- "or posting in such a way that it significantly " ..
- "changes the composition of a board. Common sense will be utilized.");
- html.list.finish("ordered");
- io.write("Individual boards may set their own rules which apply to that board. However, note");
- io.write(" that the nanochan rules stated above apply to everything done on the website.");
- html.container.barheader("Miscellaneous");
- io.write("Source code for Nanochan can be found ", html.string.link("/source.lua", "here"), ".<br />");
- io.write("To contact the administration, send an e-mail to ", html.string.link("mailto:[email protected]", "this address"), ".");
- html.container.finish();
- html.finish();
- io.output(io.stdout);
- end
- function generate.overboard()
- io.output("overboard.html");
- nanodb:exec("BEGIN TRANSACTION");
- html.begin("overboard");
- html.redheader("Nanochan Overboard");
- html.announce();
- html.post.catalog();
- html.finish();
- nanodb:exec("END TRANSACTION");
- io.output(io.stdout);
- end
- function generate.thread(boardname, number)
- local post_tbl = post.retrieve(boardname, number);
- if not post_tbl then return; end;
- io.output(boardname .. "/" .. number .. ".html");
- nanodb:exec("BEGIN TRANSACTION");
- local desc = (#post_tbl["Subject"] > 0 and post_tbl["Subject"] or string.striphtml(post_tbl["Comment"]):sub(1, 64));
- html.begin(board.format(boardname) .. ((#desc > 0) and (" - " .. desc) or ""));
- html.board.title(boardname);
- html.board.subtitle(boardname);
- html.announce();
- html.post.postform(boardname, number);
- io.write("<hr />");
- html.post.renderthread(boardname, number);
- io.write("<hr />");
- io.write("<div id='bottom-links' />");
- io.write("<a href='/", boardname, "/catalog.html'>[Catalog]</a>");
- io.write("<a href='/overboard.html'>[Overboard]</a>");
- io.write("<a href='' accesskey='r'>[Update]</a>");
- io.write("<div id='thread-reply'>");
- io.write("<a href='#postform'>[Reply]</a>");
- io.write(#post.threadreplies(boardname, number), " replies");
- io.write("</div></div>");
- html.finish();
- nanodb:exec("END TRANSACTION");
- io.output(io.stdout);
- end
- function generate.catalog(boardname)
- io.output(boardname .. "/" .. "catalog.html");
- nanodb:exec("BEGIN TRANSACTION");
- html.begin(board.format(boardname));
- html.board.title(boardname);
- html.board.subtitle(boardname);
- html.announce();
- html.post.postform(boardname, 0);
- html.post.catalog(boardname);
- html.finish();
- nanodb:exec("END TRANSACTION");
- io.output(io.stdout);
- end
- -- Write HTTP headers.
- if cgi.pathinfo[1] == "captcha.jpg" then
- io.write("Content-Type: image/jpeg\n");
- else
- io.write("Content-Type: text/html; charset=utf-8\n");
- end
- io.write("Cache-Control: no-cache\n");
- io.write("\n");
- --
- -- This is the main part of Nanochan, where all the pages are defined.
- --
- if cgi.pathinfo[1] == nil then
- -- /nano
- html.redirect("/index.html");
- elseif cgi.pathinfo[1] == "captcha.jpg" then
- io.write(captcha.create());
- elseif cgi.pathinfo[1] == "stats" then
- html.begin("stats");
- html.redheader("Nanochan Statistics");
- html.container.begin("wide");
- html.table.begin("Board", "TPH (1h)", "TPH (12h)", "PPH (1h)", "PPH (12h)", "PPD (24h)", "Total Posts");
- local boards = board.list();
- for i = 1, #boards do
- html.table.entry(board.format(boards[i]),
- string.format("%d", board.tph(boards[i], 1)),
- string.format("%.1f", board.tph(boards[i], 12)),
- string.format("%d", board.pph(boards[i], 1)),
- string.format("%.1f", board.pph(boards[i], 12)),
- string.format("%d", board.pph(boards[i], 24) * 24),
- board.retrieve(boards[i])["MaxPostNumber"]);
- end
- html.table.finish();
- html.container.finish();
- html.finish();
- elseif cgi.pathinfo[1] == "log" then
- -- /Nano/log/...
- html.begin("logs");
- html.redheader("Nanochan Log");
- html.container.begin("wide");
- local page = tonumber(GET["page"]);
- if page == nil or page <= 0 then
- page = 1;
- end
- io.write("<div class='log-page-switcher'>");
- io.write("<a class='log-page-switcher-prev' href='?page=", page - 1, "'>[Prev]</a>");
- io.write("<a class='log-page-switcher-next' href='?page=", page + 1, "'>[Next]</a>");
- io.write("</div>");
- html.table.begin("Account", "Board", "Time", "Description");
- local entries = log.retrieve(128, tonumber((page - 1) * 128));
- for i = 1, #entries do
- html.table.entry(entries[i]["Name"],
- entries[i]["Board"],
- html.string.datetime(entries[i]["Date"]),
- entries[i]["Description"]);
- end
- html.table.finish();
- io.write("<div class='log-page-switcher'>");
- io.write("<a class='log-page-switcher-prev' href='?page=", page - 1, "'>[Prev]</a>");
- io.write("<a class='log-page-switcher-next' href='?page=", page + 1, "'>[Next]</a>");
- io.write("</div>");
- html.container.finish();
- html.finish();
- os.exit();
- elseif cgi.pathinfo[1] == "mod" then
- -- /Nano/mod/...
- if cgi.pathinfo[2] == "login" then
- -- /Nano/mod/login
- -- This area is the only area in /Nano/mod which unauthenticated users are
- -- allowed to access.
- if POST["username"] and POST["password"] then
- if #identity.list() == 0 then
- -- Special case: if there are no mod accounts, use the first supplied credentials to
- -- establish an administration account (to allow for board creation and the like).
- identity.create("admin", POST["username"], POST["password"]);
- log.create("Created a new admin account for board Global: " .. POST["username"]);
- html.redirect("/Nano/mod/login");
- else
- -- User has supplied a username and a password. Check if valid.
- if identity.valid(POST["username"], POST["password"]) then
- -- Set authentication cookie.
- html.begin("successful login", "session_key", identity.session.create(POST["username"]));
- html.redheader("Login successful");
- html.container.begin();
- io.write("You have successfully logged in. You may now ", html.string.link("/Nano/mod", "continue"), " to the moderation tools.");
- html.container.finish();
- html.finish();
- else
- html.begin("invalid credentials");
- html.redheader("Error");
- html.container.begin();
- io.write("Either your username, your password, or both your username and your");
- io.write(" password were invalid. Please ", html.string.link("/Nano/mod/login", "return"));
- io.write(" and try again.");
- html.container.finish();
- html.finish();
- end
- end
- os.exit();
- end
- html.begin("moderation");
- html.redheader("Moderator login");
- html.container.begin();
- io.write("The moderation tools require a login. Access to moderation tools is restricted");
- io.write(" to administrators, global volunteers, board owners and board volunteers.");
- if #identity.list() == 0 then
- io.write("<br /><b>There are currently no moderator accounts. As such, the credentials you");
- io.write(" type in the box below will become those of the first administrator account.</b>");
- end
- html.container.barheader("Login");
- io.write("<fieldset><form method='post'>");
- io.write( "<label for='username'>Username</label><input type='text' id='username' name='username' /><br />");
- io.write( "<label for='password'>Password</label><input type='password' id='password' name='password' /><br />");
- io.write( "<label for='submit'>Submit</label><input id='submit' type='submit' value='Continue' />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- os.exit();
- end
- if username == nil then
- -- The user does not have a valid session key. User must log in.
- html.redirect("/Nano/mod/login");
- os.exit();
- end
- if cgi.pathinfo[2] == nil then
- -- /Nano/mod
- html.begin("moderation");
- html.redheader("Moderation Tools");
- html.container.begin();
- io.write("<a id='logout-button' href='/Nano/mod/logout'>[Logout]</a>");
- io.write("You are logged in as <b>", username, "</b>.");
- io.write(" Your account class is <b>", acctclass, "</b>.");
- if acctclass == "bo" or acctclass == "lvol" then
- io.write("<br />You are assigned to <b>", html.string.boardlink(assignboard), "</b></a>.");
- end
- if acctclass == "admin" then
- html.container.barheader("Global");
- html.list.begin("unordered");
- html.list.entry(html.string.link("/Nano/mod/global/announce", "Change top-bar announcement"));
- html.list.finish("unordered");
- end
- if acctclass == "admin" or acctclass == "bo" then
- html.container.barheader("Boards");
- html.list.begin("unordered");
- if acctclass == "admin" then
- html.list.entry(html.string.link("/Nano/mod/board/create", "Create a board"));
- html.list.entry(html.string.link("/Nano/mod/board/delete", "Delete a board"));
- end
- if acctclass == "admin" then
- html.list.entry(html.string.link("/Nano/mod/board/config", "Configure a board"));
- elseif acctclass == "bo" then
- html.list.entry(html.string.link("/Nano/mod/board/config/" .. assignboard, "Configure your board"));
- end
- html.list.finish("unordered");
- end
- html.container.barheader("Accounts");
- html.list.begin("unordered");
- if acctclass == "admin" or acctclass == "bo" then
- html.list.entry(html.string.link("/Nano/mod/account/create", "Create an account"));
- html.list.entry(html.string.link("/Nano/mod/account/delete", "Delete an account"));
- html.list.entry(html.string.link("/Nano/mod/account/config", "Configure an account"));
- end
- html.list.entry(html.string.link("/Nano/mod/account/config/" .. username, "Account settings"));
- html.list.finish("unordered");
- html.container.finish();
- html.finish();
- elseif cgi.pathinfo[2] == "logout" then
- identity.session.delete(username);
- html.redirect("/Nano/mod/login");
- elseif cgi.pathinfo[2] == "board" then
- -- /Nano/mod/board/...
- if cgi.pathinfo[3] == "create" then
- if acctclass ~= "admin" then
- html.pdp.authorization_denied();
- os.exit();
- end
- -- /Nano/mod/board/create
- html.begin("create board");
- html.redheader("Create a board");
- html.container.begin();
- if POST["board"] and POST["title"] then
- if board.exists(POST["board"]) then
- io.write("That board already exists.");
- elseif not board.validname(POST["board"]) then
- io.write("Invalid board name.");
- elseif not board.validtitle(POST["title"]) then
- io.write("Invalid board title.");
- elseif POST["subtitle"] and not board.validsubtitle(POST["subtitle"]) then
- io.write("Invalid board subtitle.");
- else
- board.create(POST["board"],
- POST["title"],
- POST["subtitle"] or "");
- log.create("Created a new board: " .. html.string.boardlink(POST["board"]), username);
- io.write("Board created: ", html.string.boardlink(POST["board"]));
- end
- end
- html.container.barheader("Instructions");
- html.list.begin("unordered");
- html.list.entry("<b>Board names</b> must consist of only lowercase characters and" ..
- " numerals. They must be from one to eight characters long.");
- html.list.entry("<b>Board titles</b> must be from one to 32 characters long.");
- html.list.entry("<b>Board subtitles</b> must be from zero to 64 characters long.");
- html.list.finish("unordered");
- html.container.barheader("Enter board information");
- io.write("<fieldset><form method='post'>");
- io.write( "<label for='board'>Name</label><input type='text' id='board' name='board' required /><br />");
- io.write( "<label for='title'>Title</label><input type='text' id='title' name='title' required /><br />");
- io.write( "<label for='subtitle'>Subtitle</label><input type='text' id='subtitle' name='subtitle' /><br />");
- io.write( "<label for='submit'>Submit</label><input type='submit' id='submit' value='Create' /><br />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- elseif cgi.pathinfo[3] == "delete" then
- -- /Nano/mod/board/delete
- if acctclass ~= "admin" then
- html.pdp.authorization_denied();
- os.exit();
- end
- html.begin("delete board");
- html.redheader("Delete a board");
- html.container.begin();
- if POST["board"] then
- if not board.exists(POST["board"]) then
- io.write("The board you specified does not exist.");
- else
- board.delete(POST["board"]);
- log.create("Deleted board " .. board.format(POST["board"]) ..
- (POST["reason"] and (" with reason: " .. string.escapehtml(POST["reason"])) or ""), username);
- io.write("Board deleted.");
- end
- end
- html.container.barheader("Instructions");
- io.write("Deleting a board removes the board itself, along with all posts on that board,");
- io.write(" and all accounts assigned to that board. Board deletion is irreversible.");
- html.container.barheader("Enter information");
- io.write("<fieldset><form method='post'>");
- io.write( "<label for='board'>Board</label><input type='text' id='board' name='board' required /><br />");
- io.write( "<label for='reason'>Reason</label><input type='text' id='reason' name='reason' /><br />");
- io.write( "<label for='submit'>Submit</label><input type='submit' id='submit' value='Delete' /><br />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- elseif cgi.pathinfo[3] == "config" then
- -- /Nano/mod/board/config
- if acctclass ~= "admin" and acctclass ~= "bo" then
- html.pdp.authorization_denied();
- os.exit();
- end
- if POST["board"] then
- html.redirect("/Nano/mod/board/config/" .. POST["board"]);
- os.exit();
- end
- html.begin("configure board");
- html.redheader("Configure " .. (cgi.pathinfo[4] and board.format(cgi.pathinfo[4]) or "a board"));
- html.container.begin();
- if cgi.pathinfo[4] then
- -- /Nano/mod/board/config/...
- if not board.exists(cgi.pathinfo[4]) then
- io.write("That board does not exist. ", html.string.link("/Nano/mod/board/config", "Go back"));
- io.write(" and try again.");
- elseif acctclass == "bo" and cgi.pathinfo[4] ~= assignboard then
- io.write("You are not assigned to that board and are unable to configure it.");
- else
- if POST["action"] then
- local new_settings = {
- Name = cgi.pathinfo[4],
- Title = POST["title"] or "",
- Subtitle = POST["subtitle"] or "",
- Lock = (POST["lock"] ~= nil and 1 or 0),
- DisplayOverboard = (POST["displayoverboard"] ~= nil and 1 or 0),
- RequireCaptcha = (POST["requirecaptcha"] ~= nil and 1 or 0),
- CaptchaTriggerPPH = tonumber(POST["captchatrigger"]) or 0,
- MaxThreadsPerHour = tonumber(POST["mtph"]) or 0,
- MinThreadChars = tonumber(POST["mtc"]) or 0,
- BumpLimit = tonumber(POST["bumplimit"]) or 300,
- PostLimit = tonumber(POST["postlimit"]) or 350,
- ThreadLimit = tonumber(POST["threadlimit"]) or 300
- };
- board.update(new_settings);
- log.create("Edited board settings", username, cgi.pathinfo[4]);
- io.write("Board settings modified.");
- end
- local existing = board.retrieve(cgi.pathinfo[4]);
- io.write("<fieldset><form method='post'>");
- io.write( "<input type='hidden' name='action' value='yes' />");
- io.write( "<label for='name'>Name</label><input id='name' name='name' type='text' value='", existing["Name"], "' disabled /><br />");
- io.write( "<label for='title'>Title</label><input id='title' name='title' type='text' value='", existing["Title"], "' /><br />");
- io.write( "<label for='subtitle'>Subtitle</label><input id='subtitle' name='subtitle' type='text' value='", existing["Subtitle"], "' /><br />");
- io.write( "<label for='lock'>Lock</label><input id='lock' name='lock' type='checkbox' ", (existing["Lock"] == 0 and "" or "checked "), "/><br />");
- io.write( "<label for='displayoverboard'>Overboard</label><input id='displayoverboard' name='displayoverboard' type='checkbox' ",
- (existing["DisplayOverboard"] == 0 and "" or "checked "), "/><br />");
- io.write( "<label for='requirecaptcha'>Captcha</label><input id='requirecaptcha' name='requirecaptcha' type='checkbox' ",
- (existing["RequireCaptcha"] == 0 and "" or "checked "), "/><br />");
- io.write( "<label for='captchatrigger'>Captcha Trig</label><input id='captchatrigger' name='captchatrigger' type='number' value='", existing["CaptchaTriggerPPH"], "' /><br />");
- io.write( "<label for='mtph'>Max Threads/hr</label><input id='mtph' name='mtph' type='number' value='", existing["MaxThreadsPerHour"], "' /><br />");
- io.write( "<label for='mtc'>Min Thr. Len.</label><input id='mtc' name='mtc' type='number' value='", existing["MinThreadChars"], "' /><br />");
- io.write( "<label for='bumplimit'>Bump Limit</label><input id='bumplimit' name='bumplimit' type='number' value='", existing["BumpLimit"], "' /><br />");
- io.write( "<label for='postlimit'>Post Limit</label><input id='postlimit' name='postlimit' type='number' value='", existing["PostLimit"], "' /><br />");
- io.write( "<label for='threadliit'>Thread Limit</label><input id='threadlimit' name='threadlimit' type='number' value='", existing["ThreadLimit"], "' /><br />");
- io.write( "<label for='submit'>Submit</label><input id='submit' type='submit' value='Update' />");
- io.write("</form></fieldset>");
- end
- else
- html.container.barheader("Enter information");
- io.write("<fieldset><form method='post'>");
- io.write( "<label for='board'>Board</label><input type='text' id='board' name='board' required /><br />");
- io.write( "<label for='submit'>Submit</label><input type='submit' id='submit' value='Configure' /><br />");
- io.write("</form></fieldset>");
- end
- html.container.finish();
- html.finish();
- end
- elseif cgi.pathinfo[2] == "global" then
- -- /Nano/mod/global
- if cgi.pathinfo[3] == "announce" then
- html.begin("edit global announcement");
- html.redheader("Edit global announcement");
- html.container.begin();
- if POST["action"] ~= nil then
- global.set("announce", POST["announce"] or "");
- log.create("Edited global announcement", username);
- io.write("Global announcement updated.");
- generate.mainpage();
- generate.overboard();
- end
- io.write("<fieldset><form id='globalannounce' method='post'>");
- io.write( "<input type='hidden' name='action' value='yes' />");
- io.write( "<label for='announce'>Announcement</label><textarea form='globalannounce' rows=5 cols=35 id='announce' name='announce'>",
- string.escapehtml(global.retrieve("announce") or ""), "</textarea><br />");
- io.write( "<label for='submit'>Submit</label><input type='submit' id='submit' value='Update' />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- end
- elseif cgi.pathinfo[2] == "account" then
- -- /Nano/mod/account/...
- if cgi.pathinfo[3] == "create" then
- -- /Nano/mod/account/create
- if acctclass ~= "admin" and acctclass ~= "bo" then
- html.pdp.authorization_denied();
- os.exit();
- end
- html.begin("create account");
- html.redheader("Create an account");
- html.container.begin();
- if POST["account"] and POST["password"] then
- if acctclass == "bo" then
- POST["class"] = "lvol";
- POST["board"] = assignboard;
- elseif POST["class"] == "gvol" or POST["class"] == "admin" then
- POST["board"] = nil;
- end
- if identity.exists(POST["account"]) then
- io.write("That account already exists.");
- elseif not identity.validname(POST["account"]) then
- io.write("Invalid account name.");
- elseif not identity.validpassword(POST["password"]) then
- io.write("Invalid password.");
- elseif not identity.validclass(POST["class"]) then
- io.write("Invalid account class.");
- elseif POST["board"] and not board.exists(POST["board"]) then
- io.write("Board does not exist.");
- else
- identity.create(POST["class"],
- POST["account"],
- POST["password"],
- POST["board"]);
- log.create("Created a new " .. POST["class"] .. " account for board " ..
- (html.string.boardlink(POST["board"]) or "Global") .. ": " .. POST["account"], username);
- io.write("Account created.");
- end
- end
- html.container.barheader("Instructions");
- html.list.begin("unordered");
- html.list.entry("<b>Usernames</b> can only consist of alphanumerics. They must be from 1 to 16 characters long.");
- html.list.entry("<b>Passwords</b> must be from 6 to 64 characters long.");
- if acctclass == "admin" then
- html.list.entry("An account's <b>board</b> has no effect for Global Volunteers and " ..
- "Administrators. For Board Owners and Board Volunteers, the board " ..
- "parameter defines the board in which that account can operate.");
- end
- html.list.finish("unordered");
- html.container.barheader("Enter account information");
- io.write("<fieldset><form id='acctinfo' method='post'>");
- if acctclass == "admin" then
- io.write("<label for='class'>Type</label>");
- io.write("<select id='class' name='class' form='acctinfo'>");
- io.write( "<option value='admin'>Administrator</option>");
- io.write( "<option value='gvol'>Global Volunteer</option>");
- io.write( "<option value='bo'>Board Owner</option>");
- io.write( "<option value='lvol'>Board Volunteer</option>");
- io.write("</select><br />");
- io.write("<label for='board'>Board</label><input type='text' id='board' name='board' /><br />");
- end
- io.write("<label for='account'>Username</label><input type='text' id='account' name='account' required /><br />");
- io.write("<label for='password'>Password</label><input type='password' id='password' name='password' required /><br />");
- io.write("<label for='submit'>Submit</label><input type='submit' id='submit' value='Create' /><br />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- elseif cgi.pathinfo[3] == "delete" then
- -- /Nano/account/delete
- if acctclass ~= "admin" and acctclass ~= "bo" then
- html.pdp.authorization_denied();
- os.exit();
- end
- html.begin("delete account");
- html.redheader("Delete an account");
- html.container.begin();
- if POST["account"] then
- if not identity.exists(POST["account"]) then
- io.write("The account which you have specified does not exist.");
- elseif acctclass == "bo" and identity.retrieve(POST["account"])["Board"] ~= assignboard then
- io.write("You are not authorized to delete that account.");
- else
- identity.delete(POST["account"]);
- log.create("Deleted account " .. POST["account"] ..
- (POST["reason"] and (" with reason: " .. string.escapehtml(POST["reason"])) or ""), username);
- io.write("Account deleted.");
- end
- end
- html.container.barheader("Instructions");
- html.list.begin("unordered");
- html.list.entry("Deleting an account will log the user out of all active sessions.");
- html.list.entry("Deleting an account will replace names all logs associated with that account with '<i>Deleted</i>'.");
- html.list.finish("unordered");
- html.container.barheader("Enter information");
- io.write("<fieldset><form method='post'>");
- io.write("<label for='account'>Username</label><input type='text' id='account' name='account' /><br />");
- io.write("<label for='reason'>Reason</label><input type='text' id='reason' name='reason' /><br />");
- io.write("<label for='submit'>Submit</label><input type='submit' id='submit' value='Delete' /><br />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- elseif cgi.pathinfo[3] == "config" then
- -- /Nano/mod/account/config/...
- if POST["account"] then
- html.redirect("/Nano/mod/account/config/" .. POST["account"]);
- os.exit();
- end
- if cgi.pathinfo[4] then
- if acctclass ~= "admin" and cgi.pathinfo[4] ~= username then
- html.pdp.authorization_denied();
- os.exit();
- elseif not identity.exists(cgi.pathinfo[4]) then
- html.pdp.error("Account not found", "The account that you specified does not exist.");
- os.exit();
- end
- html.begin("configure account");
- html.redheader("Configure account " .. cgi.pathinfo[4]);
- html.container.begin();
- if POST["password1"] and POST["password2"] then
- if POST["password1"] ~= POST["password2"] then
- io.write("The two passwords did not match.");
- elseif not identity.validpassword(POST["password1"]) then
- io.write("Invalid password.");
- else
- identity.changepassword(cgi.pathinfo[4], POST["password1"]);
- log.create("Changed password for account: " .. cgi.pathinfo[4], username);
- io.write("Password changed.");
- end
- end
- html.container.barheader("Instructions");
- html.list.begin();
- html.list.entry("<b>Passwords</b> must be from 6 to 64 characters long.");
- html.list.finish();
- html.container.barheader("Enter information");
- io.write("<fieldset><form method='post'>");
- io.write("<label for='password1'>New password</label><input type='password' id='password1' name='password1' /><br />");
- io.write("<label for='password2'>Repeat</label><input type='password' id='password2' name='password2' /><br />");
- io.write("<label for='submit'>Submit</label><input type='submit' id='submit' value='Change' /><br />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- else
- html.begin("configure account");
- html.redheader("Configure an account");
- html.container.begin();
- html.container.barheader("Enter information");
- io.write("<fieldset><form method='post'>");
- io.write("<label for='account'>Username</label><input type='text' id='account' name='account' /><br />");
- io.write("<label for='submit'>Submit</label><input type='submit' label='submit' value='Configure' /><br />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- end
- end
- elseif cgi.pathinfo[2] == "file" then
- local filename = cgi.pathinfo[4];
- if acctclass ~= "admin" and acctclass ~= "gvol" then
- html.pdp.authorization_denied();
- os.exit();
- elseif not file.exists(filename) then
- html.pdp.error("Invalid file", "The file you are trying to modify does not exist.");
- os.exit();
- end
- if cgi.pathinfo[3] == "delete" then
- log.create("Deleted file " .. filename .. " from all boards", username);
- file.delete(filename);
- end
- html.redirect(cgi.referer);
- elseif cgi.pathinfo[2] == "post" then
- local boardname = cgi.pathinfo[4];
- local number = tonumber(cgi.pathinfo[5]);
- local reason = POST["reason"] and string.escapehtml(POST["reason"]) or nil;
- if not post.exists(boardname, number) then
- html.pdp.error("Invalid post", "The post you are trying to modify does not exist.");
- os.exit();
- elseif (acctclass == "bo" or acctclass == "lvol") and assignboard ~= boardname then
- html.pdp.authorization_denied();
- os.exit();
- end
- if not reason then
- html.begin();
- html.redheader("Post Modification/Deletion");
- html.container.begin();
- io.write("This is the post you are trying to modify:<br />");
- html.post.render(boardname, number);
- io.write("The action is: <b>", cgi.pathinfo[3], "</b><br />");
- io.write("<fieldset><form action='' method='POST'>");
- io.write( "<input type='hidden' name='referer' value='", cgi.referer or "/overboard.html", "' />");
- io.write( "<label for='reason'>Reason</label><input type='text' id='reason' name='reason' required /><br />");
- io.write( "<label for='submit'>Submit</label><input type='submit' id='submit' value='Modify' />");
- io.write("</form></fieldset>");
- html.container.finish();
- html.finish();
- os.exit();
- end
- if cgi.pathinfo[3] == "sticky" then
- log.create("Toggled sticky on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
- post.toggle("Sticky", boardname, number);
- elseif cgi.pathinfo[3] == "lock" then
- log.create("Toggled lock on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
- post.toggle("Lock", boardname, number);
- elseif cgi.pathinfo[3] == "autosage" then
- log.create("Toggled autosage on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
- post.toggle("Autosage", boardname, number);
- elseif cgi.pathinfo[3] == "cycle" then
- log.create("Toggled cycle on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
- post.toggle("Cycle", boardname, number);
- elseif cgi.pathinfo[3] == "delete" then
- log.create("Deleted post " .. post.format(boardname, number) .. " for reason: " .. reason, username, boardname);
- post.delete(boardname, number);
- elseif cgi.pathinfo[3] == "unlink" then
- log.create("Unlinked file from post " .. post.format(boardname, number) .. " for reason: " .. reason, username, boardname);
- post.unlink(boardname, number);
- end
- html.redirect(POST["referer"] or "/overboard.html");
- end
- elseif cgi.pathinfo[1] == "post" then
- -- /Nano/post
- local post_board = POST["board"];
- local post_parent = tonumber(POST["parent"]);
- local post_name = POST["name"];
- local post_email = POST["email"];
- local post_subject = POST["subject"];
- local post_comment = POST["comment"];
- local post_tmp_filepath = HASERL["file_path"];
- local post_tmp_filename = POST["file_name"];
- local post_captcha = POST["captcha"];
- local parent_tbl = post.retrieve(post_board, post_parent);
- local board_tbl = board.retrieve(post_board);
- if POST["board"] and POST["parent"] then
- if not board_tbl then
- html.pdp.error("Invalid board", "The board you tried to post to does not exist.");
- os.exit();
- elseif post_parent ~= 0 and not post.exists(post_board, post_parent) then
- html.pdp.error("Invalid thread", "The thread you tried to post in does not exist. Perhaps it has been deleted.");
- os.exit();
- elseif parent_tbl ~= nil and parent_tbl["Lock"] == 1 and username == nil then
- html.pdp.error("Thread locked", "The thread you tried to post in is currently locked.");
- os.exit();
- elseif board_tbl["Lock"] == 1 and username == nil then
- html.pdp.error("Board locked", "The board you tried to post in is currently locked.");
- os.exit();
- elseif post_parent == 0 and (board_tbl["MaxThreadsPerHour"] > 0 and board.tph(post_board, 1) >= board_tbl["MaxThreadsPerHour"]) then
- html.pdp.error("Thread limit reached", "The board you tried to post in has reached its hourly thread limit.");
- os.exit();
- elseif post_parent ~= 0 and parent_tbl["Parent"] ~= 0 then
- html.pdp.error("Invalid thread", "The thread you tried to post in is not a thread. This is not supported.");
- os.exit();
- elseif post_parent == 0 and (board_tbl["MinThreadChars"] > 0 and #post_comment < board_tbl["MinThreadChars"]) then
- html.pdp.error("Post too short", "Your post text was too short. On this board, threads require at least " ..
- tonumber(board_tbl["MinThreadChars"]) .. " characters.");
- os.exit();
- elseif post_comment and #post_comment > 32768 then
- html.pdp.error("Post too long", "Your post text was over 32 KiB. Please reduce its length.");
- os.exit();
- elseif post_comment and select(2, post_comment:gsub("\n", "")) > 128 then
- html.pdp.error("Too many newlines", "Your post contained over 128 newlines. Please reduce its length.");
- os.exit();
- elseif post_name and #post_name > 64 then
- html.pdp.error("Name too long", "The text in the name field was over 64 bytes. Please reduce its length.");
- os.exit();
- elseif post_email and #post_email > 64 then
- html.pdp.error("Email too long", "The text in the email field was over 64 bytes. Please reduce its length.");
- os.exit();
- elseif post_subject and #post_subject > 64 then
- html.pdp.error("Subject too long", "The text in the subject field was over 64 bytes. Please reduce its length.");
- os.exit();
- elseif (#post_comment == 0) and (#post_tmp_filename == 0) then
- html.pdp.error("Blank post", "You must either upload a file or write something in the comment field.");
- os.exit();
- elseif post_parent ~= 0 and parent_tbl["Cycle"] == 0 and #post.threadreplies(post_board, post_parent) >= board_tbl["PostLimit"] then
- html.pdp.error("Thread full", "The thread you tried to post in is full. Please start a new thread instead.");
- os.exit();
- elseif (board_tbl["RequireCaptcha"] == 1) and not captcha.valid(post_captcha) then
- html.pdp.error("Invalid captcha", "The captcha you entered was incorrect. Go back, and refresh the page to get a new one.");
- os.exit();
- end
- local post_filename = "";
- if post_tmp_filename and post_tmp_filename ~= "" then
- post_filename = file.save(post_tmp_filepath, (post_parent == 0));
- if not post_filename then
- html.pdp.error("File error", "There was a problem with the file you uploaded.");
- os.exit();
- end
- end
- local post_number = post.create(post_board, post_parent, post_name, post_email, post_subject, post_comment, post_filename);
- if post_parent == 0 then
- -- Redirect to the newly created thread.
- html.redirect("/" .. post_board .. "/" .. post_number .. ".html");
- else
- -- Redirect to the parent thread, but scroll down to the newly created post.
- html.redirect("/" .. post_board .. "/" .. post_parent .. ".html" .. "#post" .. post_number);
- end
- end
- else
- html.pdp.notfound();
- end
- %>
Advertisement
Add Comment
Please, Sign In to add comment