Guest User

nano.lua

a guest
May 27th, 2019
888
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