SHARE
TWEET

nano.lua

a guest May 27th, 2019 562 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/local/bin/haserl -u16384
  2. <%
  3. local sqlite3 = require("lsqlite3");
  4. local digest = require("openssl.digest");
  5. local bcrypt = require("bcrypt");
  6.  
  7. local crypto = {};
  8. local cgi = {};
  9. local html = {};
  10.       html.board = {};
  11.       html.post = {};
  12.       html.container = {};
  13.       html.table = {};
  14.       html.list = {};
  15.       html.pdp = {};
  16.       html.string = {};
  17. local generate = {};
  18. local board = {};
  19. local post = {};
  20. local file = {};
  21. local identity = {};
  22.       identity.session = {};
  23. local captcha = {};
  24. local log = {};
  25. local global = {};
  26.  
  27. local nanodb = sqlite3.open("nanochan.db");
  28.  
  29. -- Ensure all required tables exist.
  30. nanodb:exec("CREATE TABLE IF NOT EXISTS Global (Name, Value)");
  31. nanodb:exec("CREATE TABLE IF NOT EXISTS Boards (Name, Title, Subtitle, MaxPostNumber, Lock, DisplayOverboard, MaxThreadsPerHour, MinThreadChars, BumpLimit, PostLimit, ThreadLimit, RequireCaptcha, CaptchaTriggerPPH)");
  32. nanodb:exec("CREATE TABLE IF NOT EXISTS Posts (Board, Number, Parent, Date, LastBumpDate, Name, Email, Subject, Comment, File, Sticky, Cycle, Autosage, Lock)");
  33. nanodb:exec("CREATE TABLE IF NOT EXISTS Refs (Board, Referee, Referrer)");
  34. nanodb:exec("CREATE TABLE IF NOT EXISTS Accounts (Name, Type, Board, PwHash)");
  35. nanodb:exec("CREATE TABLE IF NOT EXISTS Sessions (Key, Account, ExpireDate)");
  36. nanodb:exec("CREATE TABLE IF NOT EXISTS Logs (Name, Board, Date, Description)");
  37. nanodb:exec("CREATE TABLE IF NOT EXISTS Captchas (Text, ExpireDate)");
  38. nanodb:busy_timeout(10000);
  39.  
  40. --
  41. -- Miscellaneous functions.
  42. --
  43.  
  44. function string.tokenize(input, delimiter)
  45.     local result = {};
  46.  
  47.     if input == nil then
  48.         return {};
  49.     end
  50.  
  51.     for match in (input .. delimiter):gmatch("(.-)" .. delimiter) do
  52.         result[#result + 1] = match;
  53.     end
  54.  
  55.     return result;
  56. end
  57.  
  58. function string.random(length, pattern)
  59.     length = length or 64;
  60.     pattern = pattern or "a-zA-Z0-9"
  61.     local result = "";
  62.     local ascii = {};
  63.     local dict;
  64.  
  65.     for i = 0, 255 do
  66.         ascii[#ascii + 1] = string.char(i);
  67.     end
  68.  
  69.     ascii = table.concat(ascii);
  70.     dict = ascii:gsub("[^" .. pattern .. "]", "");
  71.  
  72.     while string.len(result) < length do
  73.         local randidx = math.random(1, string.len(dict));
  74.         local randbyte = dict:byte(randidx);
  75.         result = result .. string.char(randbyte);
  76.     end
  77.  
  78.     return result;
  79. end
  80.  
  81. function string.striphtml(input)
  82.     local result = input;
  83.     result = result:gsub("<.->", "");
  84.     return result;
  85. end
  86.  
  87. function string.escapehtml(input)
  88.     local result = input;
  89.     result = result:gsub("&", "&amp;");
  90.     result = result:gsub("<", "&lt;");
  91.     result = result:gsub(">", "&gt;");
  92.     result = result:gsub("\"", "&quot;");
  93.     result = result:gsub("'", "&#39;");
  94.     return result;
  95. end
  96.  
  97. function io.fileexists(filename)
  98.     local f = io.open(filename, "r");
  99.  
  100.     if f ~= nil then
  101.         f:close();
  102.         return true;
  103.     else
  104.         return false;
  105.     end
  106. end
  107.  
  108. function io.filesize(filename)
  109.     local fp = io.open(filename);
  110.     local size = fp:seek("end");
  111.     fp:close();
  112.     return size;
  113. end
  114.  
  115. --
  116. -- CGI- and HTTP-related initialization
  117. --
  118.  
  119. -- Initialize cgi variables.
  120. cgi.pathinfo = ENV["PATH_INFO"] and string.tokenize(ENV["PATH_INFO"]:gsub("^/", ""), "/") or {};
  121. cgi.referer = ENV["HTTP_REFERER"];
  122.  
  123. --
  124. -- Global configuration functions.
  125. --
  126.  
  127. function global.retrieve(name)
  128.     local stmt = nanodb:prepare("SELECT Value FROM Global WHERE Name = ?");
  129.     stmt:bind_values(name);
  130.  
  131.     if stmt:step() ~= sqlite3.ROW then
  132.         stmt:finalize();
  133.         return nil;
  134.     end
  135.  
  136.     local result = stmt:get_value(0);
  137.     stmt:finalize();
  138.     return result;
  139. end
  140.  
  141. function global.delete(name)
  142.     local stmt = nanodb:prepare("DELETE FROM Global WHERE Name = ?");
  143.     stmt:bind_values(name);
  144.     stmt:step();
  145.     stmt:finalize();
  146. end
  147.  
  148. function global.set(name, value)
  149.     if global.retrieve(name) ~= nil then
  150.         global.delete(name);
  151.     end
  152.  
  153.     local stmt = nanodb:prepare("INSERT INTO Global VALUES (?, ?)");
  154.     stmt:bind_values(name, value);
  155.     stmt:step();
  156.     stmt:finalize();
  157. end
  158.  
  159. --
  160. -- Cryptographic functions.
  161. --
  162.  
  163. function crypto.hash(hashtype, data)
  164.     local bstring = digest.new(hashtype):final(data);
  165.     local result = {};
  166.  
  167.     for i = 1, #bstring do
  168.         result[#result + 1] = string.format("%02x", string.byte(bstring:sub(i,i)));
  169.     end
  170.  
  171.     return table.concat(result);
  172. end
  173.  
  174. --
  175. -- Board-related functions.
  176. --
  177.  
  178. function board.list()
  179.     local boards = {}
  180.  
  181.     for tbl in nanodb:nrows("SELECT Name FROM Boards ORDER BY MaxPostNumber DESC") do
  182.         boards[#boards + 1] = tbl["Name"];
  183.     end
  184.  
  185.     return boards;
  186. end
  187.  
  188. function board.retrieve(name)
  189.     local stmt = nanodb:prepare("SELECT * FROM Boards WHERE Name = ?");
  190.     stmt:bind_values(name);
  191.     local stepret = stmt:step();
  192.  
  193.     if stepret ~= sqlite3.ROW then
  194.         stmt:finalize();
  195.         return nil;
  196.     end
  197.  
  198.     local result = stmt:get_named_values();
  199.     stmt:finalize();
  200.     return result;
  201. end
  202.  
  203. function board.validname(name)
  204.     return name and ((not name:match("[^a-z0-9]")) and (#name > 0) and (#name <= 8));
  205. end
  206.  
  207. function board.validtitle(title)
  208.     return title and ((#title > 0) and (#title <= 32));
  209. end
  210.  
  211. function board.validsubtitle(subtitle)
  212.     return subtitle and ((#subtitle >= 0) and (#subtitle <= 64));
  213. end
  214.  
  215. function board.exists(name)
  216.     local stmt = nanodb:prepare("SELECT Name FROM Boards WHERE Name = ?");
  217.     stmt:bind_values(name);
  218.     local stepret = stmt:step();
  219.     stmt:finalize();
  220.  
  221.     if stepret ~= sqlite3.ROW then
  222.         return false;
  223.     else
  224.         return true;
  225.     end
  226. end
  227.  
  228. function board.format(name)
  229.     return board.validname(name) and ("/" .. name .. "/") or nil;
  230. end
  231.  
  232. function board.create(name, title, subtitle)
  233.     if not board.validname(name) then
  234.         return nil;
  235.     end
  236.  
  237.     local stmt = nanodb:prepare("INSERT INTO Boards VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
  238.  
  239.     local maxpostnumber = 0;
  240.     local lock = 0;
  241.     local maxthreadsperhour = 0;
  242.     local minthreadchars = 0;
  243.     local bumplimit = 300;
  244.     local postlimit = 350;
  245.     local threadlimit = 300;
  246.     local displayoverboard = 1;
  247.     local requirecaptcha = 0;
  248.     local captchatrigger = 30;
  249.  
  250.     stmt:bind_values(name,
  251.                      string.escapehtml(title),
  252.                      string.escapehtml(subtitle),
  253.                      maxpostnumber,
  254.                      lock,
  255.                      displayoverboard,
  256.                      maxthreadsperhour,
  257.                      minthreadchars,
  258.                      bumplimit,
  259.                      postlimit,
  260.                      threadlimit,
  261.                      requirecaptcha,
  262.                      captchatrigger);
  263.     stmt:step();
  264.     stmt:finalize();
  265.  
  266.     generate.mainpage();
  267.     generate.catalog(name);
  268. end
  269.  
  270. function board.update(board_tbl)
  271.     local stmt = nanodb:prepare("UPDATE Boards SET " ..
  272.                                 "Title = ?, Subtitle = ?, Lock = ?, MaxThreadsPerHour = ?, MinThreadChars = ?, " ..
  273.                                 "BumpLimit = ?, PostLimit = ?, ThreadLimit = ?, DisplayOverboard = ?, RequireCaptcha = ?, " ..
  274.                                 "CaptchaTriggerPPH = ? WHERE Name = ?");
  275.  
  276.     stmt:bind_values(string.escapehtml(board_tbl["Title"]), string.escapehtml(board_tbl["Subtitle"]),
  277.                      board_tbl["Lock"], board_tbl["MaxThreadsPerHour"], board_tbl["MinThreadChars"],
  278.                      board_tbl["BumpLimit"], board_tbl["PostLimit"], board_tbl["ThreadLimit"], board_tbl["DisplayOverboard"],
  279.                      board_tbl["RequireCaptcha"], board_tbl["CaptchaTriggerPPH"], board_tbl["Name"]);
  280.     stmt:step();
  281.     stmt:finalize();
  282.  
  283.     generate.catalog(board_tbl["Name"]);
  284.     generate.overboard();
  285.  
  286.     local threads = post.listthreads(board_tbl["Name"]);
  287.     for i = 1, #threads do
  288.         generate.thread(board_tbl["Name"], threads[i]);
  289.     end
  290. end
  291.  
  292. -- Delete a board.
  293. function board.delete(name)
  294.     local stmt = nanodb:prepare("DELETE FROM Boards WHERE Name = ?");
  295.     stmt:bind_values(name);
  296.     stmt:step();
  297.     stmt:finalize();
  298.  
  299.     stmt = nanodb:prepare("DELETE FROM Accounts WHERE Board = ?");
  300.     stmt:bind_values(name);
  301.     stmt:step();
  302.     stmt:finalize();
  303.  
  304.     stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ?");
  305.     stmt:bind_values(name);
  306.     stmt:step();
  307.     stmt:finalize();
  308.  
  309.     stmt = nanodb:prepare("DELETE FROM Refs WHERE Board = ?");
  310.     stmt:bind_values(name);
  311.     stmt:step();
  312.     stmt:finalize();
  313.  
  314.     generate.mainpage();
  315.     generate.overboard();
  316. end
  317.  
  318. -- Get number of threads made in the last 'hours' hours divided by 'hours'
  319. function board.tph(name, hours)
  320.     hours = hours or 12;
  321.     local start_time = os.time() - (hours * 3600);
  322.     local stmt = nanodb:prepare("SELECT COUNT(Number) FROM Posts WHERE Board = ? AND Date > ? AND Parent = 0");
  323.     stmt:bind_values(name, start_time);
  324.     stmt:step();
  325.     local count = stmt:get_value(0);
  326.     stmt:finalize();
  327.     return count / hours;
  328. end
  329.  
  330. -- Get board PPH (number of posts made in the last 'hours' hours divided by 'hours')
  331. function board.pph(name, hours)
  332.     hours = hours or 12;
  333.     local start_time = os.time() - (hours * 3600);
  334.     local stmt = nanodb:prepare("SELECT COUNT(Number) FROM Posts WHERE Board = ? AND Date > ?");
  335.     stmt:bind_values(name, start_time);
  336.     stmt:step();
  337.     local count = stmt:get_value(0);
  338.     stmt:finalize();
  339.     return count / hours;
  340. end
  341.  
  342. --
  343. -- Identity (account) functions.
  344. --
  345.  
  346. function identity.list()
  347.     local identities = {};
  348.  
  349.     for tbl in nanodb:nrows("SELECT Name FROM Accounts ORDER BY Name") do
  350.         identities[#identities + 1] = tbl["Name"];
  351.     end
  352.  
  353.     return identities;
  354. end
  355.  
  356. function identity.retrieve(name)
  357.     local stmt = nanodb:prepare("SELECT * FROM Accounts WHERE Name = ?");
  358.     stmt:bind_values(name);
  359.  
  360.     if stmt:step() ~= sqlite3.ROW then
  361.         stmt:finalize();
  362.         return nil;
  363.     end
  364.  
  365.     local result = stmt:get_named_values();
  366.     stmt:finalize();
  367.     return result;
  368. end
  369.  
  370. function identity.exists(name)
  371.     return identity.retrieve(name) and true or false;
  372. end
  373.  
  374. -- Class can be either:
  375. --   * "admin" - Site administrator, unlimited powers
  376. --   * "bo" - Board owner, powers limited to a single board
  377. --   * "gvol" - Global volunteer, powers limited by site administrators
  378. --   * "lvol" - Local volunteer, powers limited by board owners, powers limited to a single board
  379. function identity.create(class, name, password, boardname)
  380.     boardname = boardname or "Global";
  381.     local stmt = nanodb:prepare("INSERT INTO Accounts VALUES (?,?,?,?)");
  382.     local hash = bcrypt.digest(password, 13);
  383.     stmt:bind_values(name, class, boardname, hash);
  384.     stmt:step();
  385.     stmt:finalize();
  386. end
  387.  
  388. function identity.validname(name)
  389.     return (not name:match("[^a-zA-Z0-9]")) and (#name >= 1) and (#name <= 16);
  390. end
  391.  
  392. function identity.delete(name)
  393.     local stmt = nanodb:prepare("DELETE FROM Accounts WHERE Name = ?");
  394.     stmt:bind_values(name);
  395.     stmt:step();
  396.     stmt:finalize();
  397.     stmt = nanodb:prepare("DELETE FROM Sessions WHERE Account = ?");
  398.     stmt:bind_values(name);
  399.     stmt:step();
  400.     stmt:finalize();
  401.     stmt = nanodb:prepare("UPDATE Logs SET Name = '<i>Deleted</i>' WHERE Name = ?");
  402.     stmt:bind_values(name);
  403.     stmt:step();
  404.     stmt:finalize();
  405. end
  406.  
  407. function identity.changepassword(name, password)
  408.     local hash = bcrypt.digest(password, 13);
  409.     local stmt = nanodb:prepare("UPDATE Accounts SET PwHash = ? WHERE Name = ?");
  410.     stmt:bind_values(hash, name);
  411.     stmt:step();
  412.     stmt:finalize();
  413. end
  414.  
  415. function identity.validpassword(password)
  416.     return (#password >= 6) and (#password <= 64);
  417. end
  418.  
  419. function identity.validclass(class)
  420.     return (class == "admin" or
  421.             class == "gvol" or
  422.             class == "bo" or
  423.             class == "lvol")
  424. end
  425.  
  426. function identity.valid(name, password)
  427.     local identity_tbl = identity.retrieve(name);
  428.     return identity_tbl and bcrypt.verify(password, identity_tbl["PwHash"]) or false;
  429. end
  430.  
  431. function identity.session.delete(user)
  432.     local stmt = nanodb:prepare("DELETE FROM Sessions WHERE Account = ?");
  433.     stmt:bind_values(user);
  434.     stmt:step();
  435.     stmt:finalize();
  436. end
  437.  
  438. function identity.session.create(user)
  439.     -- Clear any existing keys for this user to prevent duplicates.
  440.     identity.session.delete(user);
  441.  
  442.     local key = string.random(32);
  443.     local expiry = os.time() + 3600; -- key expires in 1 hour
  444.  
  445.     local stmt = nanodb:prepare("INSERT INTO Sessions VALUES (?,?,?)");
  446.  
  447.     stmt:bind_values(key, user, expiry);
  448.     stmt:step();
  449.     stmt:finalize();
  450.  
  451.     return key;
  452. end
  453.  
  454. function identity.session.refresh(user)
  455.     local stmt = nanodb:prepare("UPDATE Sessions SET ExpireDate = ? WHERE Account = ?");
  456.     stmt:bind_values(os.time() + 3600, user);
  457.     stmt:step();
  458.     stmt:finalize();
  459. end
  460.  
  461. function identity.session.valid(key)
  462.     local result = nil;
  463.     if key == nil then return nil end;
  464.  
  465.     for tbl in nanodb:nrows("SELECT * FROM Sessions") do
  466.         if os.time() > tbl["ExpireDate"] then
  467.             -- Clean away any expired session keys.
  468.             identity.session.delete(tbl["Account"]);
  469.         elseif tbl["Key"] == key then
  470.             result = tbl["Account"];
  471.         end
  472.     end
  473.  
  474.     identity.session.refresh(result);
  475.     return result;
  476. end
  477.  
  478. -- Captcha related functions.
  479.  
  480. function captcha.assemble(outfile)
  481.     local xx, yy, rr, ss, cc, bx, by = {},{},{},{},{},{},{};
  482.  
  483.     for i = 1, 6 do
  484.         xx[i] = ((48 * i - 168) + math.random(-5, 5));
  485.         yy[i] = math.random(-10, 10);
  486.         rr[i] = math.random(-30, 30);
  487.         ss[i] = math.random(-40, 40);
  488.         cc[i] = string.random(1, "a-z");
  489.         bx[i] = (150 + 1.1 * xx[i]);
  490.         by[i] = (40 + 2 * yy[i]);
  491.     end
  492.  
  493.     os.execute(string.format(
  494.         "gm convert -size 290x70 xc:white -bordercolor black -border 5 " ..
  495.         "-fill black -stroke black -strokewidth 1 -pointsize 40 " ..
  496.         "-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
  497.         "-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
  498.         "-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
  499.         "-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
  500.         "-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
  501.         "-draw \"translate %d,%d rotate %d skewX %d gravity center text 0,0 '%s'\" " ..
  502.         "-fill none -strokewidth 2 " ..
  503.         "-draw 'bezier %f,%d %f,%d %f,%d %f,%d' " ..
  504.         "-draw 'polyline %f,%d %f,%d %f,%d' -quality 0 -strip -colorspace GRAY JPEG:%s",
  505.         xx[1], yy[1], rr[1], ss[1], cc[1],
  506.         xx[2], yy[2], rr[2], ss[2], cc[2],
  507.         xx[3], yy[3], rr[3], ss[3], cc[3],
  508.         xx[4], yy[4], rr[4], ss[4], cc[4],
  509.         xx[5], yy[5], rr[5], ss[5], cc[5],
  510.         xx[6], yy[6], rr[6], ss[6], cc[6],
  511.         bx[1], by[1], bx[2], by[2], bx[3], by[3], bx[4], by[4],
  512.         bx[4], by[4], bx[5], by[5], bx[6], by[6],
  513.         outfile
  514.     ));
  515.  
  516.     return table.concat(cc);
  517. end
  518.  
  519. function captcha.create()
  520.     local outfile = "/tmp/captcha_" .. string.random(6);
  521.     local captcha_text = captcha.assemble(outfile);
  522.     local stmt = nanodb:prepare("INSERT INTO Captchas VALUES (?, CAST(strftime('%s', 'now') AS INTEGER) + 900)");
  523.     stmt:bind_values(captcha_text);
  524.     stmt:step();
  525.     stmt:finalize();
  526.  
  527.     nanodb:exec("DELETE FROM Captchas WHERE ExpireDate < CAST(strftime('%s', 'now') AS INTEGER)");
  528.  
  529.     local fp = io.open(outfile, "r");
  530.     local captcha_data = fp:read("*a");
  531.     fp:close();
  532.     os.remove(outfile);
  533.  
  534.     return captcha_data;
  535. end
  536.  
  537. function captcha.retrieve(answer)
  538.     local stmt = nanodb:prepare("SELECT * FROM Captchas WHERE Text = ? AND ExpireDate > CAST(strftime('%s', 'now') AS INTEGER)");
  539.     stmt:bind_values(answer);
  540.  
  541.     if stmt:step() ~= sqlite3.ROW then
  542.         stmt:finalize();
  543.         return nil;
  544.     end
  545.  
  546.     local result = stmt:get_named_values();
  547.     stmt:finalize();
  548.     return result;
  549. end
  550.  
  551. function captcha.delete(answer)
  552.     local stmt = nanodb:prepare("DELETE FROM Captchas WHERE Text = ?");
  553.     stmt:bind_values(answer);
  554.     stmt:step();
  555.     stmt:finalize();
  556. end
  557.  
  558. function captcha.valid(answer)
  559.     local captcha_tbl = captcha.retrieve(answer);
  560.     captcha.delete(answer);
  561.     return (captcha_tbl ~= nil) and true or false;
  562. end
  563.  
  564. local skey = COOKIE["session_key"];
  565. local username = identity.session.valid(skey);
  566. local acctclass = username and identity.retrieve(username)["Type"] or nil;
  567. local assignboard = username and identity.retrieve(username)["Board"] or nil;
  568.  
  569. --
  570. -- File handling functions.
  571. --
  572.  
  573. -- Detect the format of a file (PNG, JPG, GIF).
  574. function file.format(path)
  575.     local fd = io.open(path, "r");
  576.     local data = fd:read(128);
  577.     fd:close();
  578.  
  579.     if data == nil or #data == 0 then
  580.         return nil;
  581.     end
  582.  
  583.     if data:sub(1,8) == "\x89PNG\x0D\x0A\x1A\x0A" then
  584.         return "png";
  585.     elseif data:sub(1,3) == "\xFF\xD8\xFF" then
  586.         return "jpg";
  587.     elseif data:sub(1,6) == "GIF87a"
  588.         or data:sub(1,6) == "GIF89a" then
  589.         return "gif";
  590.     elseif data:find("DOCTYPE svg", 1, true)
  591.         or data:find("<svg", 1, true) then
  592.         return "svg";
  593.     elseif data:sub(1,4) == "\x1A\x45\xDF\xA3" then
  594.         return "webm";
  595.     elseif data:sub(5,12) == "ftypmp42"
  596.         or data:sub(5,12) == "ftypisom" then
  597.         return "mp4";
  598.     elseif data:sub(1,2) == "\xFF\xFB"
  599.         or data:sub(1,3) == "ID3" then
  600.         return "mp3";
  601.     elseif data:sub(1,4) == "OggS" then
  602.         return "ogg";
  603.     elseif data:sub(1,4) == "fLaC" then
  604.         return "flac";
  605.     elseif data:sub(1,4) == "%PDF" then
  606.         return "pdf";
  607.     elseif data:sub(1,4) == "PK\x03\x04"
  608.        and data:sub(31,58) == "mimetypeapplication/epub+zip" then
  609.         return "epub";
  610.     else
  611.         return nil;
  612.     end
  613. end
  614.  
  615. function file.extension(filename)
  616.     return filename:match("%.(.-)$");
  617. end
  618.  
  619. function file.class(extension)
  620.     local lookup = {
  621.         ["png"] =   "image",
  622.         ["jpg"] =   "image",
  623.         ["gif"] =   "image",
  624.         ["svg"] =       "image",
  625.         ["webm"] =  "video",
  626.         ["mp4"] =   "video",
  627.         ["mp3"] =   "audio",
  628.         ["ogg"] =   "audio",
  629.         ["flac"] =  "audio",
  630.         ["pdf"] =   "document",
  631.         ["epub"] =  "document"
  632.     };
  633.  
  634.     return lookup[extension] or extension;
  635. end
  636.  
  637. function file.has_thumbnails(extension)
  638.     local file_class = file.class(extension);
  639.     return ((file_class == "image") or (file_class == "video") or (extension == "pdf"));
  640. end
  641.  
  642. function file.pathname(filename)
  643.     return "Media/" .. filename;
  644. end
  645.  
  646. function file.thumbnail(filename)
  647.     return "Media/thumb/" .. filename;
  648. end
  649.  
  650. function file.icon(filename)
  651.     return "Media/icon/" .. filename;
  652. end
  653.  
  654. function file.exists(filename)
  655.     if filename == nil or filename == "" then
  656.         return false;
  657.     end
  658.  
  659.     return io.fileexists(file.pathname(filename));
  660. end
  661.  
  662. function file.size(filename)
  663.     return io.filesize(file.pathname(filename));
  664. end
  665.  
  666. function file.format_size(size)
  667.     if size > (1024 * 1024) then
  668.         return string.format("%.2f MiB", (size / 1024 / 1024));
  669.     elseif size > 1024 then
  670.         return string.format("%.2f KiB", (size / 1024));
  671.     else
  672.         return string.format("%d B", size);
  673.     end
  674. end
  675.  
  676. -- Create a thumbnail which will fit into a 200x200 grid.
  677. -- Graphicsmagick (gm convert) must be installed for this to work.
  678. -- Will not modify images which are smaller than 200x200.
  679. function file.create_thumbnail(filename)
  680.     local path_orig = file.pathname(filename);
  681.     local path_thumb = file.thumbnail(filename);
  682.     local file_extension = file.extension(filename);
  683.     local file_class = file.class(file_extension);
  684.  
  685.     if io.fileexists(path_thumb) then
  686.         -- Don't recreate thumbnails if they already exist.
  687.         return 0;
  688.     end
  689.  
  690.     if file_class == "video" then
  691.         return os.execute("ffmpeg -i " .. path_orig .. " -ss 00:00:01.000 -vframes 1 -f image2 - |" ..
  692.                           "gm convert -strip - -filter Box -thumbnail 200x200\\> JPEG:" .. path_thumb);
  693.     elseif file_class == "image" or file_extension == "pdf" then
  694.         return os.execute("gm convert -strip " .. path_orig .. "[0] -filter Box -thumbnail 200x200\\> " ..
  695.                           ((file_extension == "pdf" or file_extension == "svg") and "PNG:" or "")
  696.                           .. path_thumb);
  697.     end
  698. end
  699.  
  700. -- Create a catalog icon (even smaller than a normal thumbnail).
  701. -- Catalog icons must be extremely small and quality is not particularly important.
  702. function file.create_icon(filename)
  703.     local path_orig = file.pathname(filename);
  704.     local path_icon = file.icon(filename);
  705.     local file_class = file.class(file.extension(filename));
  706.  
  707.     if io.fileexists(path_icon) then
  708.         -- Don't recreate icons if they already exist.
  709.         return 0;
  710.     end
  711.  
  712.     if file_class == "video" then
  713.         return os.execute("ffmpeg -i " .. path_orig .. " -ss 00:00:01.000 -vframes 1 -f image2 - |" ..
  714.                           "gm convert -background '#BDC' -flatten -strip - -filter Box -quality 60 " ..
  715.                           "-thumbnail 100x70\\> JPEG:" .. path_icon);
  716.     else
  717.         return os.execute("gm convert -background '#BDC' -flatten -strip " .. path_orig ..
  718.                           "[0] -filter Box -quality 60 -thumbnail 100x70\\> JPEG:"
  719.                           .. path_icon);
  720.     end
  721. end
  722.  
  723. -- Save a file and return its hashed filename. Errors will result in returning nil.
  724. -- File hashes are always SHA-256 for compatibility with 8chan and friends.
  725. function file.save(path, create_catalog_icon)
  726.     local extension = file.format(path);
  727.  
  728.     if extension == nil then
  729.         return nil;
  730.     end
  731.  
  732.     local fd = io.open(path);
  733.     local data = fd:read("*a");
  734.     fd:close();
  735.     os.remove(path);
  736.  
  737.     local hash = crypto.hash("sha256", data);
  738.     local filename = hash .. "." .. extension;
  739.  
  740.     if file.exists(filename) then
  741.         if create_catalog_icon then
  742.             -- The file.create_icon() function will not recreate the icon if it
  743.             -- already exists, so we call it unconditionally here.
  744.             file.create_icon(filename);
  745.         end
  746.  
  747.         return filename;
  748.     end
  749.  
  750.     fd = io.open("Media/" .. filename, "w");
  751.     fd:write(data);
  752.     fd:close();
  753.  
  754.     file.create_thumbnail(filename);
  755.  
  756.     if create_catalog_icon then
  757.         file.create_icon(filename);
  758.     end
  759.  
  760.     return filename;
  761. end
  762.  
  763. function file.delete(filename)
  764.     os.remove(file.pathname(filename));
  765.     os.remove(file.thumbnail(filename));
  766.     os.remove(file.icon(filename));
  767. end
  768.  
  769. function post.retrieve(boardname, number)
  770.     local stmt = nanodb:prepare("SELECT * FROM Posts WHERE Board = ? AND Number = ?");
  771.     stmt:bind_values(boardname, tonumber(number));
  772.  
  773.     if stmt:step() ~= sqlite3.ROW then
  774.         stmt:finalize();
  775.         return nil;
  776.     end
  777.  
  778.     local result = stmt:get_named_values();
  779.     stmt:finalize();
  780.     return result;
  781. end
  782.  
  783. function post.listthreads(boardname)
  784.     local threads = {};
  785.  
  786.     if boardname then
  787.         local stmt = nanodb:prepare("SELECT Number FROM Posts WHERE Board = ? AND Parent = 0 ORDER BY Sticky DESC, LastBumpDate DESC");
  788.         stmt:bind_values(boardname);
  789.  
  790.         for tbl in stmt:nrows() do
  791.             threads[#threads + 1] = tonumber(tbl["Number"]);
  792.         end
  793.  
  794.         stmt:finalize();
  795.     end
  796.  
  797.     return threads;
  798. end
  799.  
  800. function post.exists(boardname, number)
  801.     local stmt = nanodb:prepare("SELECT Number FROM Posts WHERE Board = ? AND Number = ?");
  802.     stmt:bind_values(boardname, number);
  803.     local stepret = stmt:step();
  804.     stmt:finalize();
  805.  
  806.     if stepret ~= sqlite3.ROW then
  807.         return false;
  808.     else
  809.         return true;
  810.     end
  811. end
  812.  
  813. function post.bump(boardname, number)
  814.     local stmt = nanodb:prepare("UPDATE Posts SET LastBumpDate = CAST(strftime('%s', 'now') AS INTEGER) WHERE Board = ? AND Number = ? AND Autosage = 0");
  815.     stmt:bind_values(boardname, tonumber(number));
  816.     stmt:step();
  817.     stmt:finalize();
  818. end
  819.  
  820. function post.toggle(attribute, boardname, number)
  821.     local post_tbl = post.retrieve(boardname, number);
  822.     local current_value = post_tbl[attribute];
  823.     local new_value = (current_value == 1) and 0 or 1;
  824.     local stmt = nanodb:prepare("UPDATE Posts SET " .. attribute .. " = ? WHERE Board = ? AND Number = ?");
  825.     stmt:bind_values(new_value, boardname, number);
  826.     stmt:step();
  827.     stmt:finalize();
  828.  
  829.     generate.overboard();
  830.  
  831.     if post_tbl["Parent"] == 0 then
  832.     generate.catalog(boardname);
  833.     generate.thread(boardname, number);
  834.     else
  835.     generate.thread(boardname, post_tbl["Parent"]);
  836.     end
  837. end
  838.  
  839. function post.threadreplies(boardname, number)
  840.     local replies = {};
  841.     local stmt = nanodb:prepare("SELECT Number FROM Posts WHERE Board = ? AND Parent = ? ORDER BY Number");
  842.     stmt:bind_values(boardname, number);
  843.  
  844.     for tbl in stmt:nrows() do
  845.         replies[#replies + 1] = tonumber(tbl["Number"]);
  846.     end
  847.  
  848.     stmt:finalize();
  849.     return replies;
  850. end
  851.  
  852. function post.format(boardname, number)
  853.     return board.format(boardname) .. number;
  854. end
  855.  
  856. -- Turn nanochan-formatting into html.
  857. function post.nano2html(text)
  858.     text = "\n" .. text .. "\n";
  859.  
  860.     return text:gsub("&gt;&gt;(%d+)", "<a class='reference' href='#post%1'>&gt;&gt;%1</a>")
  861.     :gsub("&gt;&gt;&gt;/([%d%l]-)/(%s)", "<a class='reference' href='/%1'>&gt;&gt;&gt;/%1/</a>%2")
  862.     :gsub("&gt;&gt;&gt;/([%d%l]-)/(%d+)", "<a class='reference' href='/%1/%2.html'>&gt;&gt;&gt;/%1/%2</a>")
  863.     :gsub("\n&gt;(.-)\n", "\n<span class='greentext'>&gt;%1</span>\n")
  864.     :gsub("\n&gt;(.-)\n", "\n<span class='greentext'>&gt;%1</span>\n")
  865.     :gsub("\n&lt;(.-)\n", "\n<span class='pinktext'>&lt;%1</span>\n")
  866.     :gsub("\n&lt;(.-)\n", "\n<span class='pinktext'>&lt;%1</span>\n")
  867.     :gsub("%(%(%((.-)%)%)%)", "<span class='kiketext'>(((%1)))</span>")
  868.     :gsub("==(.-)==", "<span class='redtext'>%1</span>")
  869.     :gsub("%*%*(.-)%*%*", "<span class='spoiler'>%1</span>")
  870.     :gsub("~~(.-)~~", "<s>%1</s>")
  871.     :gsub("__(.-)__", "<u>%1</u>")
  872.     :gsub("&#39;&#39;&#39;(.-)&#39;&#39;&#39;", "<b>%1</b>")
  873.     :gsub("&#39;&#39;(.-)&#39;&#39;", "<i>%1</i>")
  874.     :gsub("(https?://)([a-zA-Z0-9%./%%_%-%+=%?&;:,#%!~]+)", "<a rel='noreferrer' href='%1%2'>%1%2</a>")
  875.     :gsub("\n", "<br />");
  876. end
  877.  
  878. -- This function does not delete the actual file. It simply removes the reference to that
  879. -- file.
  880. function post.unlink(boardname, number)
  881.     local post_tbl = post.retrieve(boardname, number);
  882.  
  883.     local stmt = nanodb:prepare("UPDATE Posts SET File = '' WHERE Board = ? AND Number = ?");
  884.     stmt:bind_values(boardname, number);
  885.     stmt:step();
  886.     stmt:finalize();
  887.  
  888.     generate.thread(boardname, post_tbl["Parent"]);
  889. end
  890.  
  891. function post.delete(boardname, number)
  892.     local post_tbl = post.retrieve(boardname, number);
  893.     local stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Number = ?");
  894.     stmt:bind_values(boardname, number);
  895.     stmt:step();
  896.     stmt:finalize();
  897.  
  898.     -- Delete descendants of that post too, if that post is a thread.
  899.     stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Parent = ?");
  900.     stmt:bind_values(boardname, number);
  901.     stmt:step();
  902.     stmt:finalize();
  903.  
  904.     -- Delete references to and from that post.
  905.     stmt = nanodb:prepare("DELETE FROM Refs WHERE Board = ? AND (Referrer = ? OR Referee = ?)");
  906.     stmt:bind_values(boardname, number, number);
  907.     stmt:step();
  908.     stmt:finalize();
  909.  
  910.     -- Delete references to and from every descendant post.
  911.     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 = ?))");
  912.     stmt:bind_values(boardname, boardname, number, boardname, number);
  913.     stmt:step();
  914.     stmt:finalize();
  915.  
  916.     generate.catalog(boardname);
  917.     generate.overboard();
  918.  
  919.     if post_tbl["Parent"] == 0 then
  920.     os.remove(boardname .. "/" .. number .. ".html");
  921.     else
  922.     generate.thread(boardname, post_tbl["Parent"]);
  923.     end
  924. end
  925.  
  926. function post.create(boardname, parent, name, email, subject, comment, filename)
  927.     local stmt;
  928.     local board_tbl = board.retrieve(boardname);
  929.     parent = parent or 0;
  930.     name = (name and name ~= "") and string.escapehtml(name) or "Nanonymous";
  931.     email = email and string.escapehtml(email) or "";
  932.     subject = subject and string.escapehtml(subject) or "";
  933.     local references = {};
  934.  
  935.     -- Find >>xxxxx in posts before formatting is applied.
  936.     for reference in comment:gmatch(">>([0123456789]+)") do
  937.         references[#references + 1] = tonumber(reference);
  938.     end
  939.  
  940.     comment = comment and post.nano2html(string.escapehtml(comment)) or "";
  941.     filename = filename or "";
  942.     local date = os.time();
  943.     local lastbumpdate = date;
  944.     local autosage = email == "sage" and 1 or 0;
  945.  
  946.     if name == "##" and username ~= nil then
  947.         local capcode;
  948.  
  949.         if acctclass == "admin" then
  950.             capcode = "Nanochan Administrator";
  951.         elseif acctclass == "bo" then
  952.             capcode = "Board Owner (" .. board.format(assignboard) .. ")";
  953.         elseif acctclass == "gvol" then
  954.             capcode = "Global Volunteer";
  955.         elseif acctclass == "lvol" then
  956.             capcode = "Board Volunteer (" .. board.format(assignboard) .. ")";
  957.         end
  958.  
  959.         name = username .. " <span class='capcode'>## " .. capcode .. "</span>";
  960.     end
  961.  
  962.     name = name:gsub("!(.+)", "<span class='tripcode'>!%1</span>");
  963.  
  964.     if parent ~= 0 and #post.threadreplies(boardname, parent) >= board_tbl["PostLimit"] then
  965.         -- Delete earliest replies in cyclical thread.
  966.         local stmt = nanodb:prepare("DELETE FROM Posts WHERE Board = ? AND Number = (SELECT Number FROM Posts WHERE Parent = ? AND Board = ? ORDER BY Number LIMIT 1)");
  967.         stmt:bind_values(boardname, parent, boardname);
  968.         stmt:step();
  969.         stmt:finalize();
  970.     elseif parent == 0 and #post.listthreads(boardname) >= board_tbl["ThreadLimit"] then
  971.         -- Slide threads off the bottom of the catalog.
  972.         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)");
  973.         stmt:bind_values(boardname, boardname);
  974.         stmt:step();
  975.         stmt:finalize();
  976.     end
  977.  
  978.     nanodb:exec("BEGIN EXCLUSIVE TRANSACTION");
  979.     stmt = nanodb:prepare("UPDATE Boards SET MaxPostNumber = MaxPostNumber + 1 WHERE Name = ?");
  980.     stmt:bind_values(boardname);
  981.     stmt:step();
  982.     stmt:finalize();
  983.     stmt = nanodb:prepare("SELECT MaxPostNumber FROM Boards WHERE Name = ?");
  984.     stmt:bind_values(boardname);
  985.     stmt:step();
  986.     local number = stmt:get_value(0);
  987.     stmt:finalize();
  988.     nanodb:exec("END TRANSACTION");
  989.  
  990.     stmt = nanodb:prepare("INSERT INTO Posts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
  991.     stmt:bind_values(boardname, number, parent, date, lastbumpdate, name, email, subject, comment, filename, 0, 0, autosage, 0);
  992.     stmt:step();
  993.     stmt:finalize()
  994.  
  995.     -- Enable the captcha if too many posts were created, and it was not already enabled.
  996.     if board_tbl["CaptchaTriggerPPH"] > 0 and
  997.        board.pph(boardname, 1) > board_tbl["CaptchaTriggerPPH"] and
  998.        board_tbl["RequireCaptcha"] == 0 then
  999.         board_tbl["RequireCaptcha"] = 1;
  1000.         board.update(board_tbl);
  1001.         log.create("Automatically enabled captcha due to excessive PPH", "<i>System</i>", boardname);
  1002.     end
  1003.  
  1004.     for i = 1, #references do
  1005.         stmt = nanodb:prepare("INSERT INTO Refs SELECT ?, ?, ? WHERE (SELECT COUNT(*) FROM Refs WHERE Board = ? AND Referee = ? AND Referrer = ?) = 0");
  1006.         stmt:bind_values(boardname, references[i], number, boardname, references[i], number);
  1007.         stmt:step();
  1008.         stmt:finalize();
  1009.     end
  1010.  
  1011.     if parent ~= 0 then
  1012.         if not (string.lower(email) == "sage") and
  1013.            not (#post.threadreplies(boardname, parent) > board_tbl["BumpLimit"]) then
  1014.             post.bump(boardname, parent);
  1015.         end
  1016.     end
  1017.  
  1018.     generate.thread(boardname, (parent ~= 0 and parent or number));
  1019.     generate.catalog(boardname);
  1020.  
  1021.     if board_tbl["DisplayOverboard"] == 1 then
  1022.         generate.overboard();
  1023.     end
  1024.  
  1025.     return number;
  1026. end
  1027.  
  1028. --
  1029. -- Log access functions.
  1030. --
  1031.  
  1032. function log.create(desc, account, boardname)
  1033.     account = account or "<i>System</i>";
  1034.     boardname = html.string.boardlink(boardname) or "<i>Global</i>";
  1035.     local date = os.time();
  1036.     local stmt = nanodb:prepare("INSERT INTO Logs VALUES (?,?,?,?)");
  1037.     stmt:bind_values(account, boardname, date, desc);
  1038.     stmt:step();
  1039.     stmt:finalize();
  1040. end
  1041.  
  1042. function log.retrieve(limit, offset)
  1043.     limit = limit or 128;
  1044.     offset = offset or 0;
  1045.     local entries = {};
  1046.  
  1047.     local stmt = nanodb:prepare("SELECT * FROM Logs ORDER BY Date DESC LIMIT ? OFFSET ?");
  1048.     stmt:bind_values(limit, offset);
  1049.  
  1050.     for tbl in stmt:nrows() do
  1051.         entries[#entries + 1] = tbl;
  1052.     end
  1053.  
  1054.     stmt:finalize();
  1055.     return entries;
  1056. end
  1057.  
  1058. --
  1059. -- HTML output functions.
  1060. --
  1061.  
  1062. function html.redirect(location)
  1063.     io.write("<!DOCTYPE html>\n");
  1064.     io.write("<html>");
  1065.     io.write(  "<head>");
  1066.     io.write(    "<title>Redirecting...</title>");
  1067.     io.write(    "<meta http-equiv='refresh' content='0;url=", location or "/", "' />");
  1068.     io.write(  "</head>");
  1069.     io.write(  "<body>");
  1070.     io.write(    "Redirecting to <a href='", location,"'>", location, "</a>");
  1071.     io.write(  "</body>");
  1072.     io.write("</html>");
  1073. end
  1074.  
  1075. function html.begin(title, name, value)
  1076.     if title == nil then
  1077.         title = ""
  1078.     else
  1079.         title = title .. " - "
  1080.     end
  1081.  
  1082.     io.write("<!DOCTYPE html>\n");
  1083.     io.write("<html>");
  1084.     io.write(  "<head>");
  1085.     io.write(    "<title>", title, "nanochan</title>");
  1086.     io.write(    "<link rel='stylesheet' type='text/css' href='/Static/nanochan.css' />");
  1087.     io.write(    "<link rel='shortcut icon' type='image/png' href='/Static/favicon.png' />");
  1088.  
  1089.     if name and value then
  1090.         io.write("<meta http-equiv='set-cookie' content='", name, "=", value, ";Path=/Nano' />");
  1091.     end
  1092.  
  1093.     io.write(    "<meta charset='utf-8' />");
  1094.     io.write(    "<meta name='viewport' content='width=device-width, initial-scale=1.0' />");
  1095.     io.write(  "</head>");
  1096.     io.write(  "<body>");
  1097.     io.write(    "<div id='topbar'>");
  1098.     io.write(      "<nav id='topnav'>");
  1099.     io.write(        "<ul>");
  1100.     io.write(          "<li class='system'><a href='/index.html'>main</a></li>");
  1101.     io.write(          "<li class='system'><a href='/Nano/mod'>mod</a></li>");
  1102.     io.write(          "<li class='system'><a href='/Nano/log'>log</a></li>");
  1103.     io.write(          "<li class='system'><a href='/Nano/stats'>stats</a></li>");
  1104.     io.write(          "<li class='system'><a href='/overboard.html'>overboard</a></li>");
  1105.  
  1106.     local boards = board.list();
  1107.     for i = 1, #boards do
  1108.         io.write("<li class='board'><a href='/", boards[i], "'>", board.format(boards[i]), "</a></li>");
  1109.     end
  1110.  
  1111.     io.write(        "</ul>");
  1112.     io.write(      "</nav>");
  1113.     io.write(    "</div>");
  1114.     io.write(    "<div id='content'>");
  1115. end
  1116.  
  1117. function html.finish()
  1118.     io.write(    "</div>");
  1119.     io.write(  "</body>");
  1120.     io.write("</html>");
  1121. end
  1122.  
  1123. function html.redheader(text)
  1124.     io.write("<h1 class='redheader'>", text, "</h1>");
  1125. end
  1126.  
  1127. function html.announce()
  1128.     if global.retrieve("announce") then
  1129.         io.write("<div id='announce'>", global.retrieve("announce"), "</div>");
  1130.     end
  1131. end
  1132.  
  1133. function html.container.begin(type)
  1134.     io.write("<div class='container ", type or "narrow", "'>");
  1135. end
  1136.  
  1137. function html.container.finish()
  1138.     io.write("</div>");
  1139. end
  1140.  
  1141. function html.container.barheader(text)
  1142.     io.write("<h2 class='barheader'>", text, "</h2>");
  1143. end
  1144.  
  1145. function html.table.begin(...)
  1146.     local arg = {...};
  1147.     io.write("<table>");
  1148.     io.write("<tr>");
  1149.  
  1150.     for i = 1, #arg do
  1151.         io.write("<th>", arg[i], "</th>");
  1152.     end
  1153.  
  1154.     io.write("</tr>");
  1155. end
  1156.  
  1157. function html.table.entry(...)
  1158.     local arg = {...};
  1159.     io.write("<tr>");
  1160.  
  1161.     for i = 1, #arg do
  1162.         io.write("<td>", tostring(arg[i]), "</td>");
  1163.     end
  1164.  
  1165.     io.write("</tr>");
  1166. end
  1167.  
  1168. function html.table.finish()
  1169.     io.write("</table>");
  1170. end
  1171.  
  1172. function html.list.begin(type)
  1173.     io.write(type == "ordered" and "<ol>" or "<ul>");
  1174. end
  1175.  
  1176. function html.list.entry(text, class)
  1177.     io.write("<li", class and (" class='" .. class .. "' ") or "", ">", text, "</li>");
  1178. end
  1179.  
  1180. function html.list.finish(type)
  1181.     io.write(type == "ordered" and "</ol>" or "</ul>");
  1182. end
  1183.  
  1184. -- Pre-defined pages.
  1185. function html.pdp.authorization_denied()
  1186.     html.begin("permission denied");
  1187.     html.redheader("Permission denied");
  1188.     html.container.begin();
  1189.     io.write("Your account class lacks authorization to perform this action. <a href='/Nano/mod'>Go back.</a>");
  1190.     html.container.finish();
  1191.     html.finish();
  1192. end
  1193.  
  1194. function html.pdp.error(heading, explanation)
  1195.     html.begin("error");
  1196.     html.redheader(heading);
  1197.     html.container.begin();
  1198.     io.write(explanation);
  1199.     html.container.finish();
  1200.     html.finish();
  1201. end
  1202.  
  1203. function html.pdp.notfound()
  1204.     html.begin("404");
  1205.     html.redheader("404 Not Found");
  1206.     html.container.begin();
  1207.     io.write("The resource which was requested does not appear to exist. Please check the URL");
  1208.     io.write(" and try again. Alternatively, if you believe this error message to in itself");
  1209.     io.write(" be an error, try contacting the nanochan administration.");
  1210.     html.container.finish();
  1211.     html.finish();
  1212. end
  1213.  
  1214. function html.string.link(href, text, title)
  1215.     if not href then
  1216.         return nil;
  1217.     end
  1218.  
  1219.     local result = "<a href='" .. href .. "'";
  1220.  
  1221.     if href:sub(1, 1) ~= "/" then
  1222.         result = result .. " rel='noreferrer' target='_blank'";
  1223.     end
  1224.  
  1225.     if title then
  1226.         result = result .. " title='" .. title .. "'";
  1227.     end
  1228.  
  1229.     result = result .. ">" .. (text or href) .. "</a>";
  1230.     return result;
  1231. end
  1232.  
  1233. function html.string.datetime(unixtime)
  1234.     local isotime = os.date("!%F %T", unixtime);
  1235.     return "<time datetime='" .. isotime .. "'>" .. isotime .. "</time>";
  1236. end
  1237.  
  1238. function html.string.boardlink(boardname)
  1239.     return html.string.link(board.format(boardname));
  1240. end
  1241.  
  1242. function html.string.threadlink(boardname, number)
  1243.     return html.string.link(post.format(boardname, number) .. ".html", post.format(boardname, number));
  1244. end
  1245.  
  1246. function html.board.title(boardname)
  1247.     io.write("<h1 id='boardtitle'>", board.format(boardname), " - ", board.retrieve(boardname)["Title"], "</h1>");
  1248. end
  1249.  
  1250. function html.board.subtitle(boardname)
  1251.     io.write("<h2 id='boardsubtitle'>", board.retrieve(boardname)["Subtitle"], "</h2>");
  1252. end
  1253.  
  1254. function html.post.postform(boardname, parent)
  1255.     local board_tbl = board.retrieve(boardname)
  1256.  
  1257.     if board_tbl["Lock"] == 1 and not username then
  1258.         return;
  1259.     end
  1260.  
  1261.     io.write("<a id='new-post' href='#postform' accesskey='p'>", (parent == 0) and "[Start a New Thread]" or "[Make a Post]", "</a>");
  1262.     io.write("<fieldset><form id='postform' action='/Nano/post' method='post' enctype='multipart/form-data'>");
  1263.     io.write("<input type='hidden' name='board' value='", boardname, "' />");
  1264.     io.write("<input type='hidden' name='parent' value='", parent, "' />");
  1265.     io.write("<a href='##' class='close-button' accesskey='w'>[X]</a>");
  1266.     io.write("<label for='name'>Name</label><input type='text' id='name' name='name' maxlength='64' /><br />");
  1267.     io.write("<label for='email'>Email</label><input type='text' id='email' name='email' maxlength='64' /><br />");
  1268.     io.write("<label for='subject'>Subject</label><input type='text' id='subject' name='subject' autocomplete='off' maxlength='64' />");
  1269.     io.write("<input type='submit' value='Post' accesskey='s' /><br />");
  1270.     io.write("<label for='comment'>Comment</label><textarea id='comment' name='comment' form='postform' rows='5' cols='35' maxlength='32768'></textarea><br />");
  1271.     io.write("<label for='file'>File</label><input type='file' id='file' name='file' /><br />");
  1272.  
  1273.     if board_tbl["RequireCaptcha"] == 1 then
  1274.         io.write("<label for='captcha'>Captcha</label><input type='text' id='captcha' name='captcha' autocomplete='off' maxlength='6' /><br />");
  1275.         io.write("<img id='captcha-image' width='290' height='70' src='/Nano/captcha.jpg' />");
  1276.     end
  1277.  
  1278.     io.write("</form></fieldset>");
  1279. end
  1280.  
  1281. function html.post.modlinks(boardname, number)
  1282.     local post_tbl = post.retrieve(boardname, number);
  1283.  
  1284.     io.write("<span class='thread-mod-links'>");
  1285.     io.write("<a href='/Nano/mod/post/delete/", boardname, "/", number, "' title='Delete'>[D]</a>");
  1286.  
  1287.     if file.exists(post_tbl["File"]) then
  1288.         io.write("<a href='/Nano/mod/post/unlink/", boardname, "/", number, "' title='Unlink File'>[U]</a>");
  1289.         io.write("<a href='/Nano/mod/file/delete/", post_tbl["File"], "' title='Delete File'>[F]</a>");
  1290.     end
  1291.  
  1292.     if post_tbl["Parent"] == 0 then
  1293.         io.write("<a href='/Nano/mod/post/sticky/", boardname, "/", number, "' title='Sticky'>[S]</a>");
  1294.         io.write("<a href='/Nano/mod/post/lock/", boardname, "/", number, "' title='Lock'>[L]</a>");
  1295.         io.write("<a href='/Nano/mod/post/autosage/", boardname, "/", number, "' title='Autosage'>[A]</a>");
  1296.         io.write("<a href='/Nano/mod/post/cycle/", boardname, "/", number, "' title='Cycle'>[C]</a>");
  1297.     end
  1298.  
  1299.     io.write("</span>");
  1300. end
  1301.  
  1302. function html.post.threadflags(boardname, number)
  1303.     local post_tbl = post.retrieve(boardname, number);
  1304.     io.write("<span class='thread-info-flags'>");
  1305.     if post_tbl["Sticky"] == 1 then io.write("(S)"); end;
  1306.     if post_tbl["Lock"] == 1 then io.write("(L)"); end;
  1307.     if post_tbl["Autosage"] == 1 then io.write("(A)"); end;
  1308.     if post_tbl["Cycle"] == 1 then io.write("(C)"); end;
  1309.     io.write("</span>");
  1310. end
  1311.  
  1312. function html.post.render_catalog(boardname, number)
  1313.     local post_tbl = post.retrieve(boardname, number);
  1314.  
  1315.     io.write("<div class='catalog-thread'>");
  1316.     io.write(  "<div class='catalog-thread-link'><a href='/", boardname, "/", number, ".html'>");
  1317.  
  1318.     if file.exists(post_tbl["File"]) then
  1319.         local file_ext = file.extension(post_tbl["File"]);
  1320.         local file_class = file.class(file_ext);
  1321.  
  1322.         if file.has_thumbnails(file_ext) then
  1323.             io.write("<img src='/" .. file.icon(post_tbl["File"]) .. "' alt='***' />");
  1324.         else
  1325.             io.write("<img width='100' height='70' src='/Static/", file_class, ".png' />");
  1326.         end
  1327.     else
  1328.         io.write("***");
  1329.     end
  1330.  
  1331.     io.write(  "</a></div>");
  1332.     io.write(  "<div class='thread-info'>");
  1333.     io.write(    "<span class='thread-board-link'><a href='/", boardname, "'>", board.format(boardname), "</a></span> ");
  1334.     io.write(    "<span class='thread-info-replies'>R:", #post.threadreplies(boardname, number), "</span>");
  1335.     html.post.threadflags(boardname, number);
  1336.     io.write(  "</div>");
  1337.     html.post.modlinks(boardname, number);
  1338.     io.write(  "<div class='catalog-thread-subject'>");
  1339.     io.write(     post_tbl["Subject"] or "");
  1340.     io.write(  "</div>");
  1341.     io.write(  "<div class='catalog-thread-comment'>");
  1342.     io.write(     post_tbl["Comment"]);
  1343.     io.write(  "</div>");
  1344.     io.write("</div>");
  1345. end
  1346.  
  1347. -- Omitting the 'boardname' value will turn the catalog into an overboard.
  1348. function html.post.catalog(boardname)
  1349.     io.write("<a href='' accesskey='r'>[Update]</a>");
  1350.     io.write("<hr />");
  1351.     io.write("<div class='catalog-container'>");
  1352.  
  1353.     if boardname ~= nil then
  1354.         -- Catalog mode.
  1355.         local threadlist = post.listthreads(boardname);
  1356.         for i = 1, #threadlist do
  1357.             local number = threadlist[i];
  1358.             html.post.render_catalog(boardname, number);
  1359.             io.write("<hr class='invisible' />");
  1360.         end
  1361.     else
  1362.         -- Overboard mode.
  1363.         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
  1364.             html.post.render_catalog(post_tbl["Board"], post_tbl["Number"]);
  1365.             io.write("<hr class='invisible' />");
  1366.         end
  1367.     end
  1368.  
  1369.     io.write("</div>");
  1370. end
  1371.  
  1372. function html.post.render(boardname, number)
  1373.     local post_tbl = post.retrieve(boardname, number);
  1374.  
  1375.     io.write("<div class='post' id='post", number, "'>");
  1376.     io.write(  "<div class='post-header'>");
  1377.  
  1378.     io.write(    "<span class='post-subject'>", post_tbl["Subject"] or "", "</span> ");
  1379.     io.write(    "<span class='post-name'>");
  1380.  
  1381.     if post_tbl["Email"] ~= "" then
  1382.         io.write("<a class='post-email' href='mailto:", post_tbl["Email"], "'>");
  1383.     end
  1384.  
  1385.     io.write(    post_tbl["Name"]);
  1386.  
  1387.     if post_tbl["Email"] ~= "" then
  1388.         io.write("</a>");
  1389.     end
  1390.  
  1391.     io.write(    "</span> ");
  1392.     io.write(    "<span class='post-date'>", html.string.datetime(post_tbl["Date"]), "</span> ");
  1393.     io.write(    "<span class='post-number'>No.<a href='#postform'>", post_tbl["Number"], "</a></span> ");
  1394.  
  1395.     if post_tbl["Parent"] == 0 then
  1396.         html.post.threadflags(boardname, number);
  1397.     end
  1398.  
  1399.     html.post.modlinks(boardname, number);
  1400.  
  1401.     local stmt = nanodb:prepare("SELECT Referrer FROM Refs WHERE Board = ? AND Referee = ? ORDER BY Referrer");
  1402.     stmt:bind_values(boardname, number);
  1403.  
  1404.     for referee in stmt:nrows() do
  1405.         io.write(" <a class='referee' href='#post", referee["Referrer"], "'>&gt;&gt;", referee["Referrer"], "</a>");
  1406.     end
  1407.  
  1408.     stmt:finalize();
  1409.  
  1410.     io.write(  "</div>");
  1411.  
  1412.     if file.exists(post_tbl["File"]) then
  1413.         local file_ext = file.extension(post_tbl["File"]);
  1414.         local file_class = file.class(file_ext);
  1415.  
  1416.         io.write("<div class='post-file-info'>");
  1417.         io.write("File: <a href='/Media/", post_tbl["File"], "' target='_blank'>", post_tbl["File"], "</a>");
  1418.         io.write(" (<a href='/Media/", post_tbl["File"], "' download>", "dl</a>)");
  1419.         io.write(" (", file.format_size(file.size(post_tbl["File"])), ")");
  1420.         io.write("</div>");
  1421.  
  1422.         if file.has_thumbnails(file_ext) then
  1423.             io.write("<a target='_blank' href='/Media/" .. post_tbl["File"] .. "'>");
  1424.             io.write(  "<img class='post-file-thumbnail' src='/", file.thumbnail(post_tbl["File"]), "' />");
  1425.             io.write("</a>");
  1426.         elseif file_ext == "epub" then
  1427.             io.write("<a target='_blank' href='/Media/" .. post_tbl["File"] .. "'>");
  1428.             io.write(  "<img width='100' height='70' class='post-file-thumbnail' src='/Static/document.png' />");
  1429.             io.write("</a>");
  1430.         elseif file_class == "audio" then
  1431.             io.write("<audio class='post-audio' preload='none' controls loop>");
  1432.             io.write(  "<source src='/Media/", post_tbl["File"], "' type='audio/", file_ext, "' />");
  1433.             io.write("</audio>");
  1434.         end
  1435.     end
  1436.  
  1437.     io.write(  "<div class='post-comment'>");
  1438.     io.write(  post_tbl["Comment"]);
  1439.     io.write(  "</div>");
  1440.     io.write("</div>");
  1441.  
  1442.     io.write("<br />");
  1443. end
  1444.  
  1445. function html.post.renderthread(boardname, number)
  1446.     local replies = post.threadreplies(boardname, number);
  1447.     html.post.render(boardname, number);
  1448.  
  1449.     for i = 1, #replies do
  1450.         io.write("<hr class='invisible' />");
  1451.         html.post.render(boardname, replies[i]);
  1452.     end
  1453. end
  1454.  
  1455. function generate.mainpage()
  1456.     io.output("index.html");
  1457.  
  1458.     html.begin();
  1459.     html.redheader("Welcome to Nanochan");
  1460.     html.announce(global.retrieve("announce"));
  1461.     html.container.begin("narrow");
  1462.     io.write("<img id='front-page-logo' src='/Static/logo.png' alt='Nanochan logo' width=400 height=400 />");
  1463.     html.container.barheader("Boards");
  1464.  
  1465.     local boards = board.list();
  1466.     html.list.begin("ordered");
  1467.     for i = 1, #boards do
  1468.         local board_tbl = board.retrieve(boards[i]);
  1469.         html.list.entry(html.string.boardlink(board_tbl["Name"]) .. " - " .. board_tbl["Title"]);
  1470.     end
  1471.     html.list.finish("ordered");
  1472.  
  1473.     html.container.barheader("Rules");
  1474.     io.write("These rules apply to all boards on nanochan:");
  1475.     html.list.begin("ordered");
  1476.     html.list.entry("Child pornography is not permitted. Links to child pornography are not permitted either, " ..
  1477.                     "and neither are links to websites which contain a significant number of direct links to CP.");
  1478.     html.list.entry("Flooding is not permitted. We define flooding as posting similar posts more " ..
  1479.                     "than 3 times per hour, making a thread on a topic for which a thread already exists, " ..
  1480.                     "or posting in such a way that it significantly " ..
  1481.                     "changes the composition of a board. Common sense will be utilized.");
  1482.     html.list.finish("ordered");
  1483.     io.write("Individual boards may set their own rules which apply to that board. However, note");
  1484.     io.write(" that the nanochan rules stated above apply to everything done on the website.");
  1485.  
  1486.     html.container.barheader("Miscellaneous");
  1487.     io.write("Source code for Nanochan can be found ", html.string.link("/source.lua", "here"), ".<br />");
  1488.     io.write("To contact the administration, send an e-mail to ", html.string.link("mailto:37564N@memeware.net", "this address"), ".");
  1489.  
  1490.     html.container.finish();
  1491.     html.finish();
  1492.  
  1493.     io.output(io.stdout);
  1494. end
  1495.  
  1496. function generate.overboard()
  1497.     io.output("overboard.html");
  1498.     nanodb:exec("BEGIN TRANSACTION");
  1499.  
  1500.     html.begin("overboard");
  1501.     html.redheader("Nanochan Overboard");
  1502.     html.announce();
  1503.     html.post.catalog();
  1504.     html.finish();
  1505.  
  1506.     nanodb:exec("END TRANSACTION");
  1507.     io.output(io.stdout);
  1508. end
  1509.  
  1510. function generate.thread(boardname, number)
  1511.     local post_tbl = post.retrieve(boardname, number);
  1512.     if not post_tbl then return; end;
  1513.  
  1514.     io.output(boardname .. "/" .. number .. ".html");
  1515.     nanodb:exec("BEGIN TRANSACTION");
  1516.  
  1517.     local desc = (#post_tbl["Subject"] > 0 and post_tbl["Subject"] or string.striphtml(post_tbl["Comment"]):sub(1, 64));
  1518.     html.begin(board.format(boardname) .. ((#desc > 0) and (" - " .. desc) or ""));
  1519.  
  1520.     html.board.title(boardname);
  1521.     html.board.subtitle(boardname);
  1522.     html.announce();
  1523.  
  1524.     html.post.postform(boardname, number);
  1525.     io.write("<hr />");
  1526.     html.post.renderthread(boardname, number);
  1527.     io.write("<hr />");
  1528.  
  1529.     io.write("<div id='bottom-links' />");
  1530.     io.write("<a href='/", boardname, "/catalog.html'>[Catalog]</a>");
  1531.     io.write("<a href='/overboard.html'>[Overboard]</a>");
  1532.     io.write("<a href='' accesskey='r'>[Update]</a>");
  1533.     io.write("<div id='thread-reply'>");
  1534.     io.write("<a href='#postform'>[Reply]</a>");
  1535.     io.write(#post.threadreplies(boardname, number), " replies");
  1536.     io.write("</div></div>");
  1537.  
  1538.     html.finish();
  1539.     nanodb:exec("END TRANSACTION");
  1540.     io.output(io.stdout);
  1541. end
  1542.  
  1543. function generate.catalog(boardname)
  1544.     io.output(boardname .. "/" .. "catalog.html");
  1545.     nanodb:exec("BEGIN TRANSACTION");
  1546.     html.begin(board.format(boardname));
  1547.  
  1548.     html.board.title(boardname);
  1549.     html.board.subtitle(boardname);
  1550.     html.announce();
  1551.  
  1552.     html.post.postform(boardname, 0);
  1553.     html.post.catalog(boardname);
  1554.  
  1555.     html.finish();
  1556.     nanodb:exec("END TRANSACTION");
  1557.     io.output(io.stdout);
  1558. end
  1559.  
  1560. -- Write HTTP headers.
  1561. if cgi.pathinfo[1] == "captcha.jpg" then
  1562.     io.write("Content-Type: image/jpeg\n");
  1563. else
  1564.     io.write("Content-Type: text/html; charset=utf-8\n");
  1565. end
  1566.  
  1567. io.write("Cache-Control: no-cache\n");
  1568. io.write("\n");
  1569.  
  1570. --
  1571. -- This is the main part of Nanochan, where all the pages are defined.
  1572. --
  1573.  
  1574. if cgi.pathinfo[1] == nil then
  1575.     -- /nano
  1576.     html.redirect("/index.html");
  1577. elseif cgi.pathinfo[1] == "captcha.jpg" then
  1578.     io.write(captcha.create());
  1579. elseif cgi.pathinfo[1] == "stats" then
  1580.     html.begin("stats");
  1581.     html.redheader("Nanochan Statistics");
  1582.     html.container.begin("wide");
  1583.     html.table.begin("Board", "TPH (1h)", "TPH (12h)", "PPH (1h)", "PPH (12h)", "PPD (24h)", "Total Posts");
  1584.  
  1585.     local boards = board.list();
  1586.     for i = 1, #boards do
  1587.         html.table.entry(board.format(boards[i]),
  1588.                          string.format("%d", board.tph(boards[i], 1)),
  1589.                          string.format("%.1f", board.tph(boards[i], 12)),
  1590.                          string.format("%d", board.pph(boards[i], 1)),
  1591.                          string.format("%.1f", board.pph(boards[i], 12)),
  1592.                          string.format("%d", board.pph(boards[i], 24) * 24),
  1593.                          board.retrieve(boards[i])["MaxPostNumber"]);
  1594.     end
  1595.  
  1596.     html.table.finish();
  1597.     html.container.finish();
  1598.     html.finish();
  1599. elseif cgi.pathinfo[1] == "log" then
  1600.     -- /Nano/log/...
  1601.     html.begin("logs");
  1602.     html.redheader("Nanochan Log");
  1603.     html.container.begin("wide");
  1604.  
  1605.     local page = tonumber(GET["page"]);
  1606.  
  1607.     if page == nil or page <= 0 then
  1608.         page = 1;
  1609.     end
  1610.  
  1611.     io.write("<div class='log-page-switcher'>");
  1612.     io.write("<a class='log-page-switcher-prev' href='?page=", page - 1, "'>[Prev]</a>");
  1613.     io.write("<a class='log-page-switcher-next' href='?page=", page + 1, "'>[Next]</a>");
  1614.     io.write("</div>");
  1615.  
  1616.     html.table.begin("Account", "Board", "Time", "Description");
  1617.  
  1618.     local entries = log.retrieve(128, tonumber((page - 1) * 128));
  1619.     for i = 1, #entries do
  1620.         html.table.entry(entries[i]["Name"],
  1621.                          entries[i]["Board"],
  1622.                          html.string.datetime(entries[i]["Date"]),
  1623.                          entries[i]["Description"]);
  1624.     end
  1625.  
  1626.     html.table.finish();
  1627.     io.write("<div class='log-page-switcher'>");
  1628.     io.write("<a class='log-page-switcher-prev' href='?page=", page - 1, "'>[Prev]</a>");
  1629.     io.write("<a class='log-page-switcher-next' href='?page=", page + 1, "'>[Next]</a>");
  1630.     io.write("</div>");
  1631.     html.container.finish();
  1632.     html.finish();
  1633.     os.exit();
  1634. elseif cgi.pathinfo[1] == "mod" then
  1635.     -- /Nano/mod/...
  1636.     if cgi.pathinfo[2] == "login" then
  1637.         -- /Nano/mod/login
  1638.         -- This area is the only area in /Nano/mod which unauthenticated users are
  1639.         -- allowed to access.
  1640.         if POST["username"] and POST["password"] then
  1641.             if #identity.list() == 0 then
  1642.                 -- Special case: if there are no mod accounts, use the first supplied credentials to
  1643.                 -- establish an administration account (to allow for board creation and the like).
  1644.                 identity.create("admin", POST["username"], POST["password"]);
  1645.                 log.create("Created a new admin account for board Global: " .. POST["username"]);
  1646.                 html.redirect("/Nano/mod/login");
  1647.             else
  1648.                 -- User has supplied a username and a password. Check if valid.
  1649.                 if identity.valid(POST["username"], POST["password"]) then
  1650.                     -- Set authentication cookie.
  1651.                     html.begin("successful login", "session_key", identity.session.create(POST["username"]));
  1652.                     html.redheader("Login successful");
  1653.                     html.container.begin();
  1654.                     io.write("You have successfully logged in. You may now ", html.string.link("/Nano/mod", "continue"), " to the moderation tools.");
  1655.                     html.container.finish();
  1656.                     html.finish();
  1657.                 else
  1658.                     html.begin("invalid credentials");
  1659.                     html.redheader("Error");
  1660.                     html.container.begin();
  1661.                     io.write("Either your username, your password, or both your username and your");
  1662.                     io.write(" password were invalid. Please ", html.string.link("/Nano/mod/login", "return"));
  1663.                     io.write(" and try again.");
  1664.                     html.container.finish();
  1665.                     html.finish();
  1666.                 end
  1667.             end
  1668.  
  1669.             os.exit();
  1670.         end
  1671.  
  1672.         html.begin("moderation");
  1673.         html.redheader("Moderator login");
  1674.         html.container.begin();
  1675.         io.write("The moderation tools require a login. Access to moderation tools is restricted");
  1676.         io.write(" to administrators, global volunteers, board owners and board volunteers.");
  1677.  
  1678.         if #identity.list() == 0 then
  1679.             io.write("<br /><b>There are currently no moderator accounts. As such, the credentials you");
  1680.             io.write(" type in the box below will become those of the first administrator account.</b>");
  1681.         end
  1682.  
  1683.         html.container.barheader("Login");
  1684.         io.write("<fieldset><form method='post'>");
  1685.         io.write(  "<label for='username'>Username</label><input type='text' id='username' name='username' /><br />");
  1686.         io.write(  "<label for='password'>Password</label><input type='password' id='password' name='password' /><br />");
  1687.         io.write(  "<label for='submit'>Submit</label><input id='submit' type='submit' value='Continue' />");
  1688.         io.write("</form></fieldset>");
  1689.         html.container.finish();
  1690.         html.finish();
  1691.         os.exit();
  1692.     end
  1693.  
  1694.     if username == nil then
  1695.         -- The user does not have a valid session key. User must log in.
  1696.         html.redirect("/Nano/mod/login");
  1697.         os.exit();
  1698.     end
  1699.  
  1700.     if cgi.pathinfo[2] == nil then
  1701.         -- /Nano/mod
  1702.         html.begin("moderation");
  1703.         html.redheader("Moderation Tools");
  1704.         html.container.begin();
  1705.         io.write("<a id='logout-button' href='/Nano/mod/logout'>[Logout]</a>");
  1706.         io.write("You are logged in as <b>", username, "</b>.");
  1707.         io.write(" Your account class is <b>", acctclass, "</b>.");
  1708.  
  1709.         if acctclass == "bo" or acctclass == "lvol" then
  1710.             io.write("<br />You are assigned to <b>", html.string.boardlink(assignboard), "</b></a>.");
  1711.         end
  1712.  
  1713.         if acctclass == "admin" then
  1714.             html.container.barheader("Global");
  1715.             html.list.begin("unordered");
  1716.             html.list.entry(html.string.link("/Nano/mod/global/announce", "Change top-bar announcement"));
  1717.             html.list.finish("unordered");
  1718.         end
  1719.  
  1720.         if acctclass == "admin" or acctclass == "bo" then
  1721.             html.container.barheader("Boards");
  1722.             html.list.begin("unordered");
  1723.  
  1724.             if acctclass == "admin" then
  1725.                 html.list.entry(html.string.link("/Nano/mod/board/create", "Create a board"));
  1726.                 html.list.entry(html.string.link("/Nano/mod/board/delete", "Delete a board"));
  1727.             end
  1728.  
  1729.             if acctclass == "admin" then
  1730.                 html.list.entry(html.string.link("/Nano/mod/board/config", "Configure a board"));
  1731.             elseif acctclass == "bo" then
  1732.                 html.list.entry(html.string.link("/Nano/mod/board/config/" .. assignboard, "Configure your board"));
  1733.             end
  1734.  
  1735.             html.list.finish("unordered");
  1736.         end
  1737.  
  1738.         html.container.barheader("Accounts");
  1739.         html.list.begin("unordered");
  1740.  
  1741.         if acctclass == "admin" or acctclass == "bo" then
  1742.             html.list.entry(html.string.link("/Nano/mod/account/create", "Create an account"));
  1743.             html.list.entry(html.string.link("/Nano/mod/account/delete", "Delete an account"));
  1744.             html.list.entry(html.string.link("/Nano/mod/account/config", "Configure an account"));
  1745.         end
  1746.  
  1747.         html.list.entry(html.string.link("/Nano/mod/account/config/" .. username, "Account settings"));
  1748.         html.list.finish("unordered");
  1749.         html.container.finish();
  1750.         html.finish();
  1751.     elseif cgi.pathinfo[2] == "logout" then
  1752.         identity.session.delete(username);
  1753.         html.redirect("/Nano/mod/login");
  1754.     elseif cgi.pathinfo[2] == "board" then
  1755.         -- /Nano/mod/board/...
  1756.         if cgi.pathinfo[3] == "create" then
  1757.             if acctclass ~= "admin" then
  1758.                 html.pdp.authorization_denied();
  1759.                 os.exit();
  1760.             end
  1761.  
  1762.             -- /Nano/mod/board/create
  1763.             html.begin("create board");
  1764.             html.redheader("Create a board");
  1765.             html.container.begin();
  1766.  
  1767.             if POST["board"] and POST["title"] then
  1768.                 if board.exists(POST["board"]) then
  1769.                     io.write("That board already exists.");
  1770.                 elseif not board.validname(POST["board"]) then
  1771.                     io.write("Invalid board name.");
  1772.                 elseif not board.validtitle(POST["title"]) then
  1773.                     io.write("Invalid board title.");
  1774.                 elseif POST["subtitle"] and not board.validsubtitle(POST["subtitle"]) then
  1775.                     io.write("Invalid board subtitle.");
  1776.                 else
  1777.                     board.create(POST["board"],
  1778.                                  POST["title"],
  1779.                                  POST["subtitle"] or "");
  1780.                     log.create("Created a new board: " .. html.string.boardlink(POST["board"]), username);
  1781.                     io.write("Board created: ", html.string.boardlink(POST["board"]));
  1782.                 end
  1783.             end
  1784.  
  1785.             html.container.barheader("Instructions");
  1786.             html.list.begin("unordered");
  1787.             html.list.entry("<b>Board names</b> must consist of only lowercase characters and" ..
  1788.                             " numerals. They must be from one to eight characters long.");
  1789.             html.list.entry("<b>Board titles</b> must be from one to 32 characters long.");
  1790.             html.list.entry("<b>Board subtitles</b> must be from zero to 64 characters long.");
  1791.             html.list.finish("unordered");
  1792.  
  1793.             html.container.barheader("Enter board information");
  1794.             io.write("<fieldset><form method='post'>");
  1795.             io.write(  "<label for='board'>Name</label><input type='text' id='board' name='board' required /><br />");
  1796.             io.write(  "<label for='title'>Title</label><input type='text' id='title' name='title' required /><br />");
  1797.             io.write(  "<label for='subtitle'>Subtitle</label><input type='text' id='subtitle' name='subtitle' /><br />");
  1798.             io.write(  "<label for='submit'>Submit</label><input type='submit' id='submit' value='Create' /><br />");
  1799.             io.write("</form></fieldset>");
  1800.  
  1801.             html.container.finish();
  1802.             html.finish();
  1803.         elseif cgi.pathinfo[3] == "delete" then
  1804.             -- /Nano/mod/board/delete
  1805.             if acctclass ~= "admin" then
  1806.                 html.pdp.authorization_denied();
  1807.                 os.exit();
  1808.             end
  1809.  
  1810.             html.begin("delete board");
  1811.             html.redheader("Delete a board");
  1812.             html.container.begin();
  1813.  
  1814.             if POST["board"] then
  1815.                 if not board.exists(POST["board"]) then
  1816.                     io.write("The board you specified does not exist.");
  1817.                 else
  1818.                     board.delete(POST["board"]);
  1819.                     log.create("Deleted board " .. board.format(POST["board"]) ..
  1820.                                (POST["reason"] and (" with reason: " .. string.escapehtml(POST["reason"])) or ""), username);
  1821.                     io.write("Board deleted.");
  1822.                 end
  1823.             end
  1824.  
  1825.             html.container.barheader("Instructions");
  1826.             io.write("Deleting a board removes the board itself, along with all posts on that board,");
  1827.             io.write(" and all accounts assigned to that board. Board deletion is irreversible.");
  1828.  
  1829.             html.container.barheader("Enter information");
  1830.             io.write("<fieldset><form method='post'>");
  1831.             io.write(  "<label for='board'>Board</label><input type='text' id='board' name='board' required /><br />");
  1832.             io.write(  "<label for='reason'>Reason</label><input type='text' id='reason' name='reason' /><br />");
  1833.             io.write(  "<label for='submit'>Submit</label><input type='submit' id='submit' value='Delete' /><br />");
  1834.             io.write("</form></fieldset>");
  1835.  
  1836.             html.container.finish();
  1837.             html.finish();
  1838.         elseif cgi.pathinfo[3] == "config" then
  1839.             -- /Nano/mod/board/config
  1840.             if acctclass ~= "admin" and acctclass ~= "bo" then
  1841.                 html.pdp.authorization_denied();
  1842.                 os.exit();
  1843.             end
  1844.  
  1845.             if POST["board"] then
  1846.                 html.redirect("/Nano/mod/board/config/" .. POST["board"]);
  1847.                 os.exit();
  1848.             end
  1849.  
  1850.             html.begin("configure board");
  1851.             html.redheader("Configure " .. (cgi.pathinfo[4] and board.format(cgi.pathinfo[4]) or "a board"));
  1852.             html.container.begin();
  1853.  
  1854.             if cgi.pathinfo[4] then
  1855.                 -- /Nano/mod/board/config/...
  1856.                 if not board.exists(cgi.pathinfo[4]) then
  1857.                     io.write("That board does not exist. ", html.string.link("/Nano/mod/board/config", "Go back"));
  1858.                     io.write(" and try again.");
  1859.                 elseif acctclass == "bo" and cgi.pathinfo[4] ~= assignboard then
  1860.                     io.write("You are not assigned to that board and are unable to configure it.");
  1861.                 else
  1862.                     if POST["action"] then
  1863.                         local new_settings = {
  1864.                             Name =           cgi.pathinfo[4],
  1865.                             Title =          POST["title"] or "",
  1866.                             Subtitle =       POST["subtitle"] or "",
  1867.                             Lock =          (POST["lock"] ~= nil and 1 or 0),
  1868.                             DisplayOverboard = (POST["displayoverboard"] ~= nil and 1 or 0),
  1869.                             RequireCaptcha = (POST["requirecaptcha"] ~= nil and 1 or 0),
  1870.                             CaptchaTriggerPPH = tonumber(POST["captchatrigger"]) or 0,
  1871.                             MaxThreadsPerHour = tonumber(POST["mtph"]) or 0,
  1872.                             MinThreadChars = tonumber(POST["mtc"]) or 0,
  1873.                             BumpLimit = tonumber(POST["bumplimit"]) or 300,
  1874.                             PostLimit = tonumber(POST["postlimit"]) or 350,
  1875.                             ThreadLimit = tonumber(POST["threadlimit"]) or 300
  1876.                         };
  1877.  
  1878.                         board.update(new_settings);
  1879.                         log.create("Edited board settings", username, cgi.pathinfo[4]);
  1880.                         io.write("Board settings modified.");
  1881.                     end
  1882.  
  1883.                     local existing = board.retrieve(cgi.pathinfo[4]);
  1884.  
  1885.                     io.write("<fieldset><form method='post'>");
  1886.                     io.write(  "<input type='hidden' name='action' value='yes' />");
  1887.                     io.write(  "<label for='name'>Name</label><input id='name' name='name' type='text' value='", existing["Name"], "' disabled /><br />");
  1888.                     io.write(  "<label for='title'>Title</label><input id='title' name='title' type='text' value='", existing["Title"], "' /><br />");
  1889.                     io.write(  "<label for='subtitle'>Subtitle</label><input id='subtitle' name='subtitle' type='text' value='", existing["Subtitle"], "' /><br />");
  1890.                     io.write(  "<label for='lock'>Lock</label><input id='lock' name='lock' type='checkbox' ", (existing["Lock"] == 0 and "" or "checked "), "/><br />");
  1891.                     io.write(  "<label for='displayoverboard'>Overboard</label><input id='displayoverboard' name='displayoverboard' type='checkbox' ",
  1892.                                (existing["DisplayOverboard"] == 0 and "" or "checked "), "/><br />");
  1893.                     io.write(  "<label for='requirecaptcha'>Captcha</label><input id='requirecaptcha' name='requirecaptcha' type='checkbox' ",
  1894.                                (existing["RequireCaptcha"] == 0 and "" or "checked "), "/><br />");
  1895.                     io.write(  "<label for='captchatrigger'>Captcha Trig</label><input id='captchatrigger' name='captchatrigger' type='number' value='", existing["CaptchaTriggerPPH"], "' /><br />");
  1896.                     io.write(  "<label for='mtph'>Max Threads/hr</label><input id='mtph' name='mtph' type='number' value='", existing["MaxThreadsPerHour"], "' /><br />");
  1897.                     io.write(  "<label for='mtc'>Min Thr. Len.</label><input id='mtc' name='mtc' type='number' value='", existing["MinThreadChars"], "' /><br />");
  1898.                     io.write(  "<label for='bumplimit'>Bump Limit</label><input id='bumplimit' name='bumplimit' type='number' value='", existing["BumpLimit"], "' /><br />");
  1899.                     io.write(  "<label for='postlimit'>Post Limit</label><input id='postlimit' name='postlimit' type='number' value='", existing["PostLimit"], "' /><br />");
  1900.                     io.write(  "<label for='threadliit'>Thread Limit</label><input id='threadlimit' name='threadlimit' type='number' value='", existing["ThreadLimit"], "' /><br />");
  1901.                     io.write(  "<label for='submit'>Submit</label><input id='submit' type='submit' value='Update' />");
  1902.                     io.write("</form></fieldset>");
  1903.                 end
  1904.             else
  1905.                 html.container.barheader("Enter information");
  1906.                 io.write("<fieldset><form method='post'>");
  1907.                 io.write(  "<label for='board'>Board</label><input type='text' id='board' name='board' required /><br />");
  1908.                 io.write(  "<label for='submit'>Submit</label><input type='submit' id='submit' value='Configure' /><br />");
  1909.                 io.write("</form></fieldset>");
  1910.             end
  1911.  
  1912.             html.container.finish();
  1913.             html.finish();
  1914.         end
  1915.     elseif cgi.pathinfo[2] == "global" then
  1916.         -- /Nano/mod/global
  1917.         if cgi.pathinfo[3] == "announce" then
  1918.             html.begin("edit global announcement");
  1919.             html.redheader("Edit global announcement");
  1920.             html.container.begin();
  1921.  
  1922.             if POST["action"] ~= nil then
  1923.                 global.set("announce", POST["announce"] or "");
  1924.                 log.create("Edited global announcement", username);
  1925.                 io.write("Global announcement updated.");
  1926.                 generate.mainpage();
  1927.                 generate.overboard();
  1928.             end
  1929.  
  1930.             io.write("<fieldset><form id='globalannounce' method='post'>");
  1931.             io.write(  "<input type='hidden' name='action' value='yes' />");
  1932.             io.write(  "<label for='announce'>Announcement</label><textarea form='globalannounce' rows=5 cols=35 id='announce' name='announce'>",
  1933.                         string.escapehtml(global.retrieve("announce") or ""), "</textarea><br />");
  1934.             io.write(  "<label for='submit'>Submit</label><input type='submit' id='submit' value='Update' />");
  1935.             io.write("</form></fieldset>");
  1936.  
  1937.             html.container.finish();
  1938.             html.finish();
  1939.         end
  1940.     elseif cgi.pathinfo[2] == "account" then
  1941.         -- /Nano/mod/account/...
  1942.         if cgi.pathinfo[3] == "create" then
  1943.             -- /Nano/mod/account/create
  1944.  
  1945.             if acctclass ~= "admin" and acctclass ~= "bo" then
  1946.                 html.pdp.authorization_denied();
  1947.                 os.exit();
  1948.             end
  1949.  
  1950.             html.begin("create account");
  1951.             html.redheader("Create an account");
  1952.             html.container.begin();
  1953.  
  1954.             if POST["account"] and POST["password"] then
  1955.                 if acctclass == "bo" then
  1956.                     POST["class"] = "lvol";
  1957.                     POST["board"] = assignboard;
  1958.                 elseif POST["class"] == "gvol" or POST["class"] == "admin" then
  1959.                     POST["board"] = nil;
  1960.                 end
  1961.  
  1962.                 if identity.exists(POST["account"]) then
  1963.                     io.write("That account already exists.");
  1964.                 elseif not identity.validname(POST["account"]) then
  1965.                     io.write("Invalid account name.");
  1966.                 elseif not identity.validpassword(POST["password"]) then
  1967.                     io.write("Invalid password.");
  1968.                 elseif not identity.validclass(POST["class"]) then
  1969.                     io.write("Invalid account class.");
  1970.                 elseif POST["board"] and not board.exists(POST["board"]) then
  1971.                     io.write("Board does not exist.");
  1972.                 else
  1973.                     identity.create(POST["class"],
  1974.                                     POST["account"],
  1975.                                     POST["password"],
  1976.                                     POST["board"]);
  1977.                     log.create("Created a new " .. POST["class"] .. " account for board " ..
  1978.                                (html.string.boardlink(POST["board"]) or "Global") .. ": " .. POST["account"], username);
  1979.                     io.write("Account created.");
  1980.                 end
  1981.             end
  1982.  
  1983.             html.container.barheader("Instructions");
  1984.             html.list.begin("unordered");
  1985.             html.list.entry("<b>Usernames</b> can only consist of alphanumerics. They must be from 1 to 16 characters long.");
  1986.             html.list.entry("<b>Passwords</b> must be from 6 to 64 characters long.");
  1987.             if acctclass == "admin" then
  1988.                 html.list.entry("An account's <b>board</b> has no effect for Global Volunteers and " ..
  1989.                                 "Administrators. For Board Owners and Board Volunteers, the board " ..
  1990.                                 "parameter defines the board in which that account can operate.");
  1991.             end
  1992.             html.list.finish("unordered");
  1993.  
  1994.             html.container.barheader("Enter account information");
  1995.             io.write("<fieldset><form id='acctinfo' method='post'>");
  1996.             if acctclass == "admin" then
  1997.                 io.write("<label for='class'>Type</label>");
  1998.                 io.write("<select id='class' name='class' form='acctinfo'>");
  1999.                 io.write(  "<option value='admin'>Administrator</option>");
  2000.                 io.write(  "<option value='gvol'>Global Volunteer</option>");
  2001.                 io.write(  "<option value='bo'>Board Owner</option>");
  2002.                 io.write(  "<option value='lvol'>Board Volunteer</option>");
  2003.                 io.write("</select><br />");
  2004.                 io.write("<label for='board'>Board</label><input type='text' id='board' name='board' /><br />");
  2005.             end
  2006.             io.write("<label for='account'>Username</label><input type='text' id='account' name='account' required /><br />");
  2007.             io.write("<label for='password'>Password</label><input type='password' id='password' name='password' required /><br />");
  2008.             io.write("<label for='submit'>Submit</label><input type='submit' id='submit' value='Create' /><br />");
  2009.             io.write("</form></fieldset>");
  2010.  
  2011.             html.container.finish();
  2012.             html.finish();
  2013.         elseif cgi.pathinfo[3] == "delete" then
  2014.             -- /Nano/account/delete
  2015.             if acctclass ~= "admin" and acctclass ~= "bo" then
  2016.                 html.pdp.authorization_denied();
  2017.                 os.exit();
  2018.             end
  2019.  
  2020.             html.begin("delete account");
  2021.             html.redheader("Delete an account");
  2022.             html.container.begin();
  2023.  
  2024.             if POST["account"] then
  2025.                 if not identity.exists(POST["account"]) then
  2026.                     io.write("The account which you have specified does not exist.");
  2027.                 elseif acctclass == "bo" and identity.retrieve(POST["account"])["Board"] ~= assignboard then
  2028.                     io.write("You are not authorized to delete that account.");
  2029.                 else
  2030.                     identity.delete(POST["account"]);
  2031.                     log.create("Deleted account " .. POST["account"] ..
  2032.                                (POST["reason"] and (" with reason: " .. string.escapehtml(POST["reason"])) or ""), username);
  2033.                     io.write("Account deleted.");
  2034.                 end
  2035.             end
  2036.  
  2037.             html.container.barheader("Instructions");
  2038.             html.list.begin("unordered");
  2039.             html.list.entry("Deleting an account will log the user out of all active sessions.");
  2040.             html.list.entry("Deleting an account will replace names all logs associated with that account with '<i>Deleted</i>'.");
  2041.             html.list.finish("unordered");
  2042.  
  2043.             html.container.barheader("Enter information");
  2044.             io.write("<fieldset><form method='post'>");
  2045.             io.write("<label for='account'>Username</label><input type='text' id='account' name='account' /><br />");
  2046.             io.write("<label for='reason'>Reason</label><input type='text' id='reason' name='reason' /><br />");
  2047.             io.write("<label for='submit'>Submit</label><input type='submit' id='submit' value='Delete' /><br />");
  2048.             io.write("</form></fieldset>");
  2049.             html.container.finish();
  2050.             html.finish();
  2051.         elseif cgi.pathinfo[3] == "config" then
  2052.             -- /Nano/mod/account/config/...
  2053.             if POST["account"] then
  2054.                 html.redirect("/Nano/mod/account/config/" .. POST["account"]);
  2055.                 os.exit();
  2056.             end
  2057.  
  2058.             if cgi.pathinfo[4] then
  2059.                 if acctclass ~= "admin" and cgi.pathinfo[4] ~= username then
  2060.                     html.pdp.authorization_denied();
  2061.                     os.exit();
  2062.                 elseif not identity.exists(cgi.pathinfo[4]) then
  2063.                     html.pdp.error("Account not found", "The account that you specified does not exist.");
  2064.                     os.exit();
  2065.                 end
  2066.  
  2067.                 html.begin("configure account");
  2068.                 html.redheader("Configure account " .. cgi.pathinfo[4]);
  2069.                 html.container.begin();
  2070.  
  2071.                 if POST["password1"] and POST["password2"] then
  2072.                     if POST["password1"] ~= POST["password2"] then
  2073.                         io.write("The two passwords did not match.");
  2074.                     elseif not identity.validpassword(POST["password1"]) then
  2075.                         io.write("Invalid password.");
  2076.                     else
  2077.                         identity.changepassword(cgi.pathinfo[4], POST["password1"]);
  2078.                         log.create("Changed password for account: " .. cgi.pathinfo[4], username);
  2079.                         io.write("Password changed.");
  2080.                     end
  2081.                 end
  2082.  
  2083.                 html.container.barheader("Instructions");
  2084.                 html.list.begin();
  2085.                 html.list.entry("<b>Passwords</b> must be from 6 to 64 characters long.");
  2086.                 html.list.finish();
  2087.                 html.container.barheader("Enter information");
  2088.                 io.write("<fieldset><form method='post'>");
  2089.                 io.write("<label for='password1'>New password</label><input type='password' id='password1' name='password1' /><br />");
  2090.                 io.write("<label for='password2'>Repeat</label><input type='password' id='password2' name='password2' /><br />");
  2091.                 io.write("<label for='submit'>Submit</label><input type='submit' id='submit' value='Change' /><br />");
  2092.                 io.write("</form></fieldset>");
  2093.                 html.container.finish();
  2094.                 html.finish();
  2095.             else
  2096.                 html.begin("configure account");
  2097.                 html.redheader("Configure an account");
  2098.                 html.container.begin();
  2099.                 html.container.barheader("Enter information");
  2100.                 io.write("<fieldset><form method='post'>");
  2101.                 io.write("<label for='account'>Username</label><input type='text' id='account' name='account' /><br />");
  2102.                 io.write("<label for='submit'>Submit</label><input type='submit' label='submit' value='Configure' /><br />");
  2103.                 io.write("</form></fieldset>");
  2104.                 html.container.finish();
  2105.                 html.finish();
  2106.             end
  2107.         end
  2108.     elseif cgi.pathinfo[2] == "file" then
  2109.         local filename = cgi.pathinfo[4];
  2110.  
  2111.         if acctclass ~= "admin" and acctclass ~= "gvol" then
  2112.             html.pdp.authorization_denied();
  2113.             os.exit();
  2114.         elseif not file.exists(filename) then
  2115.             html.pdp.error("Invalid file", "The file you are trying to modify does not exist.");
  2116.             os.exit();
  2117.         end
  2118.  
  2119.         if cgi.pathinfo[3] == "delete" then
  2120.             log.create("Deleted file " .. filename .. " from all boards", username);
  2121.             file.delete(filename);
  2122.         end
  2123.  
  2124.         html.redirect(cgi.referer);
  2125.     elseif cgi.pathinfo[2] == "post" then
  2126.         local boardname = cgi.pathinfo[4];
  2127.         local number = tonumber(cgi.pathinfo[5]);
  2128.         local reason = POST["reason"] and string.escapehtml(POST["reason"]) or nil;
  2129.  
  2130.         if not post.exists(boardname, number) then
  2131.             html.pdp.error("Invalid post", "The post you are trying to modify does not exist.");
  2132.             os.exit();
  2133.         elseif (acctclass == "bo" or acctclass == "lvol") and assignboard ~= boardname then
  2134.             html.pdp.authorization_denied();
  2135.             os.exit();
  2136.         end
  2137.  
  2138.         if not reason then
  2139.             html.begin();
  2140.             html.redheader("Post Modification/Deletion");
  2141.             html.container.begin();
  2142.             io.write("This is the post you are trying to modify:<br />");
  2143.             html.post.render(boardname, number);
  2144.             io.write("The action is: <b>", cgi.pathinfo[3], "</b><br />");
  2145.             io.write("<fieldset><form action='' method='POST'>");
  2146.             io.write(  "<input type='hidden' name='referer' value='", cgi.referer or "/overboard.html", "' />");
  2147.             io.write(  "<label for='reason'>Reason</label><input type='text' id='reason' name='reason' required /><br />");
  2148.             io.write(  "<label for='submit'>Submit</label><input type='submit' id='submit' value='Modify' />");
  2149.             io.write("</form></fieldset>");
  2150.             html.container.finish();
  2151.             html.finish();
  2152.             os.exit();
  2153.         end
  2154.  
  2155.         if cgi.pathinfo[3] == "sticky" then
  2156.             log.create("Toggled sticky on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
  2157.             post.toggle("Sticky", boardname, number);
  2158.         elseif cgi.pathinfo[3] == "lock" then
  2159.             log.create("Toggled lock on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
  2160.             post.toggle("Lock", boardname, number);
  2161.         elseif cgi.pathinfo[3] == "autosage" then
  2162.             log.create("Toggled autosage on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
  2163.             post.toggle("Autosage", boardname, number);
  2164.         elseif cgi.pathinfo[3] == "cycle" then
  2165.             log.create("Toggled cycle on thread " .. html.string.threadlink(boardname, number) .. " for reason: " .. reason, username, boardname);
  2166.             post.toggle("Cycle", boardname, number);
  2167.         elseif cgi.pathinfo[3] == "delete" then
  2168.             log.create("Deleted post " .. post.format(boardname, number) .. " for reason: " .. reason, username, boardname);
  2169.             post.delete(boardname, number);
  2170.         elseif cgi.pathinfo[3] == "unlink" then
  2171.             log.create("Unlinked file from post " .. post.format(boardname, number) .. " for reason: " .. reason, username, boardname);
  2172.             post.unlink(boardname, number);
  2173.         end
  2174.  
  2175.         html.redirect(POST["referer"] or "/overboard.html");
  2176.     end
  2177. elseif cgi.pathinfo[1] == "post" then
  2178.     -- /Nano/post
  2179.     local post_board = POST["board"];
  2180.     local post_parent = tonumber(POST["parent"]);
  2181.     local post_name = POST["name"];
  2182.     local post_email = POST["email"];
  2183.     local post_subject = POST["subject"];
  2184.     local post_comment = POST["comment"];
  2185.     local post_tmp_filepath = HASERL["file_path"];
  2186.     local post_tmp_filename = POST["file_name"];
  2187.     local post_captcha = POST["captcha"];
  2188.     local parent_tbl = post.retrieve(post_board, post_parent);
  2189.     local board_tbl = board.retrieve(post_board);
  2190.  
  2191.     if POST["board"] and POST["parent"] then
  2192.         if not board_tbl then
  2193.             html.pdp.error("Invalid board", "The board you tried to post to does not exist.");
  2194.             os.exit();
  2195.         elseif post_parent ~= 0 and not post.exists(post_board, post_parent) then
  2196.             html.pdp.error("Invalid thread", "The thread you tried to post in does not exist. Perhaps it has been deleted.");
  2197.             os.exit();
  2198.         elseif parent_tbl ~= nil and parent_tbl["Lock"] == 1 and username == nil then
  2199.             html.pdp.error("Thread locked", "The thread you tried to post in is currently locked.");
  2200.             os.exit();
  2201.         elseif board_tbl["Lock"] == 1 and username == nil then
  2202.             html.pdp.error("Board locked", "The board you tried to post in is currently locked.");
  2203.             os.exit();
  2204.         elseif post_parent == 0 and (board_tbl["MaxThreadsPerHour"] > 0 and board.tph(post_board, 1) >= board_tbl["MaxThreadsPerHour"]) then
  2205.             html.pdp.error("Thread limit reached", "The board you tried to post in has reached its hourly thread limit.");
  2206.             os.exit();
  2207.         elseif post_parent ~= 0 and parent_tbl["Parent"] ~= 0 then
  2208.             html.pdp.error("Invalid thread", "The thread you tried to post in is not a thread. This is not supported.");
  2209.             os.exit();
  2210.         elseif post_parent == 0 and (board_tbl["MinThreadChars"] > 0 and #post_comment < board_tbl["MinThreadChars"]) then
  2211.             html.pdp.error("Post too short", "Your post text was too short. On this board, threads require at least " ..
  2212.                            tonumber(board_tbl["MinThreadChars"]) .. " characters.");
  2213.             os.exit();
  2214.         elseif post_comment and #post_comment > 32768 then
  2215.             html.pdp.error("Post too long", "Your post text was over 32 KiB. Please reduce its length.");
  2216.             os.exit();
  2217.         elseif post_comment and select(2, post_comment:gsub("\n", "")) > 128 then
  2218.             html.pdp.error("Too many newlines", "Your post contained over 128 newlines. Please reduce its length.");
  2219.             os.exit();
  2220.         elseif post_name and #post_name > 64 then
  2221.             html.pdp.error("Name too long", "The text in the name field was over 64 bytes. Please reduce its length.");
  2222.             os.exit();
  2223.         elseif post_email and #post_email > 64 then
  2224.             html.pdp.error("Email too long", "The text in the email field was over 64 bytes. Please reduce its length.");
  2225.             os.exit();
  2226.         elseif post_subject and #post_subject > 64 then
  2227.             html.pdp.error("Subject too long", "The text in the subject field was over 64 bytes. Please reduce its length.");
  2228.             os.exit();
  2229.         elseif (#post_comment == 0) and (#post_tmp_filename == 0) then
  2230.             html.pdp.error("Blank post", "You must either upload a file or write something in the comment field.");
  2231.             os.exit();
  2232.         elseif post_parent ~= 0 and parent_tbl["Cycle"] == 0 and #post.threadreplies(post_board, post_parent) >= board_tbl["PostLimit"] then
  2233.             html.pdp.error("Thread full", "The thread you tried to post in is full. Please start a new thread instead.");
  2234.             os.exit();
  2235.         elseif (board_tbl["RequireCaptcha"] == 1) and not captcha.valid(post_captcha) then
  2236.             html.pdp.error("Invalid captcha", "The captcha you entered was incorrect. Go back, and refresh the page to get a new one.");
  2237.             os.exit();
  2238.         end
  2239.  
  2240.         local post_filename = "";
  2241.         if post_tmp_filename and post_tmp_filename ~= "" then
  2242.             post_filename = file.save(post_tmp_filepath, (post_parent == 0));
  2243.  
  2244.             if not post_filename then
  2245.                 html.pdp.error("File error", "There was a problem with the file you uploaded.");
  2246.                 os.exit();
  2247.             end
  2248.         end
  2249.  
  2250.         local post_number = post.create(post_board, post_parent, post_name, post_email, post_subject, post_comment, post_filename);
  2251.  
  2252.         if post_parent == 0 then
  2253.             -- Redirect to the newly created thread.
  2254.             html.redirect("/" .. post_board .. "/" .. post_number .. ".html");
  2255.         else
  2256.             -- Redirect to the parent thread, but scroll down to the newly created post.
  2257.             html.redirect("/" .. post_board .. "/" .. post_parent .. ".html" .. "#post" .. post_number);
  2258.         end
  2259.     end
  2260. else
  2261.     html.pdp.notfound();
  2262. end
  2263. %>
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top