· 6 years ago · May 27, 2019, 01:22 PM
1#!/usr/local/bin/haserl -u16384
2<%
3local sqlite3 = require("lsqlite3");
4local digest = require("openssl.digest");
5local bcrypt = require("bcrypt");
6
7local crypto = {};
8local cgi = {};
9local html = {};
10 html.board = {};
11 html.post = {};
12 html.container = {};
13 html.table = {};
14 html.list = {};
15 html.pdp = {};
16 html.string = {};
17local generate = {};
18local board = {};
19local post = {};
20local file = {};
21local identity = {};
22 identity.session = {};
23local captcha = {};
24local log = {};
25local global = {};
26
27local nanodb = sqlite3.open("nanochan.db");
28
29-- Ensure all required tables exist.
30nanodb:exec("CREATE TABLE IF NOT EXISTS Global (Name, Value)");
31nanodb:exec("CREATE TABLE IF NOT EXISTS Boards (Name, Title, Subtitle, MaxPostNumber, Lock, DisplayOverboard, MaxThreadsPerHour, MinThreadChars, BumpLimit, PostLimit, ThreadLimit, RequireCaptcha, CaptchaTriggerPPH)");
32nanodb:exec("CREATE TABLE IF NOT EXISTS Posts (Board, Number, Parent, Date, LastBumpDate, Name, Email, Subject, Comment, File, Sticky, Cycle, Autosage, Lock)");
33nanodb:exec("CREATE TABLE IF NOT EXISTS Refs (Board, Referee, Referrer)");
34nanodb:exec("CREATE TABLE IF NOT EXISTS Accounts (Name, Type, Board, PwHash)");
35nanodb:exec("CREATE TABLE IF NOT EXISTS Sessions (Key, Account, ExpireDate)");
36nanodb:exec("CREATE TABLE IF NOT EXISTS Logs (Name, Board, Date, Description)");
37nanodb:exec("CREATE TABLE IF NOT EXISTS Captchas (Text, ExpireDate)");
38nanodb:busy_timeout(10000);
39
40--
41-- Miscellaneous functions.
42--
43
44function 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;
56end
57
58function 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;
79end
80
81function string.striphtml(input)
82 local result = input;
83 result = result:gsub("<.->", "");
84 return result;
85end
86
87function string.escapehtml(input)
88 local result = input;
89 result = result:gsub("&", "&");
90 result = result:gsub("<", "<");
91 result = result:gsub(">", ">");
92 result = result:gsub("\"", """);
93 result = result:gsub("'", "'");
94 return result;
95end
96
97function 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
106end
107
108function io.filesize(filename)
109 local fp = io.open(filename);
110 local size = fp:seek("end");
111 fp:close();
112 return size;
113end
114
115--
116-- CGI- and HTTP-related initialization
117--
118
119-- Initialize cgi variables.
120cgi.pathinfo = ENV["PATH_INFO"] and string.tokenize(ENV["PATH_INFO"]:gsub("^/", ""), "/") or {};
121cgi.referer = ENV["HTTP_REFERER"];
122
123--
124-- Global configuration functions.
125--
126
127function 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;
139end
140
141function global.delete(name)
142 local stmt = nanodb:prepare("DELETE FROM Global WHERE Name = ?");
143 stmt:bind_values(name);
144 stmt:step();
145 stmt:finalize();
146end
147
148function 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();
157end
158
159--
160-- Cryptographic functions.
161--
162
163function 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);
172end
173
174--
175-- Board-related functions.
176--
177
178function 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;
186end
187
188function 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;
201end
202
203function board.validname(name)
204 return name and ((not name:match("[^a-z0-9]")) and (#name > 0) and (#name <= 8));
205end
206
207function board.validtitle(title)
208 return title and ((#title > 0) and (#title <= 32));
209end
210
211function board.validsubtitle(subtitle)
212 return subtitle and ((#subtitle >= 0) and (#subtitle <= 64));
213end
214
215function 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
226end
227
228function board.format(name)
229 return board.validname(name) and ("/" .. name .. "/") or nil;
230end
231
232function 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);
268end
269
270function 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
290end
291
292-- Delete a board.
293function 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();
316end
317
318-- Get number of threads made in the last 'hours' hours divided by 'hours'
319function 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;
328end
329
330-- Get board PPH (number of posts made in the last 'hours' hours divided by 'hours')
331function 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;
340end
341
342--
343-- Identity (account) functions.
344--
345
346function 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;
354end
355
356function 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;
368end
369
370function identity.exists(name)
371 return identity.retrieve(name) and true or false;
372end
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
379function 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();
386end
387
388function identity.validname(name)
389 return (not name:match("[^a-zA-Z0-9]")) and (#name >= 1) and (#name <= 16);
390end
391
392function 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();
405end
406
407function 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();
413end
414
415function identity.validpassword(password)
416 return (#password >= 6) and (#password <= 64);
417end
418
419function identity.validclass(class)
420 return (class == "admin" or
421 class == "gvol" or
422 class == "bo" or
423 class == "lvol")
424end
425
426function identity.valid(name, password)
427 local identity_tbl = identity.retrieve(name);
428 return identity_tbl and bcrypt.verify(password, identity_tbl["PwHash"]) or false;
429end
430
431function 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();
436end
437
438function 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;
452end
453
454function 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();
459end
460
461function 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;
476end
477
478-- Captcha related functions.
479
480function 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);
517end
518
519function 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;
535end
536
537function 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;
549end
550
551function captcha.delete(answer)
552 local stmt = nanodb:prepare("DELETE FROM Captchas WHERE Text = ?");
553 stmt:bind_values(answer);
554 stmt:step();
555 stmt:finalize();
556end
557
558function captcha.valid(answer)
559 local captcha_tbl = captcha.retrieve(answer);
560 captcha.delete(answer);
561 return (captcha_tbl ~= nil) and true or false;
562end
563
564local skey = COOKIE["session_key"];
565local username = identity.session.valid(skey);
566local acctclass = username and identity.retrieve(username)["Type"] or nil;
567local 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).
574function 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
613end
614
615function file.extension(filename)
616 return filename:match("%.(.-)$");
617end
618
619function 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;
635end
636
637function file.has_thumbnails(extension)
638 local file_class = file.class(extension);
639 return ((file_class == "image") or (file_class == "video") or (extension == "pdf"));
640end
641
642function file.pathname(filename)
643 return "Media/" .. filename;
644end
645
646function file.thumbnail(filename)
647 return "Media/thumb/" .. filename;
648end
649
650function file.icon(filename)
651 return "Media/icon/" .. filename;
652end
653
654function file.exists(filename)
655 if filename == nil or filename == "" then
656 return false;
657 end
658
659 return io.fileexists(file.pathname(filename));
660end
661
662function file.size(filename)
663 return io.filesize(file.pathname(filename));
664end
665
666function 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
674end
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.
679function 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
698end
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.
702function 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
721end
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.
725function 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;
761end
762
763function file.delete(filename)
764 os.remove(file.pathname(filename));
765 os.remove(file.thumbnail(filename));
766 os.remove(file.icon(filename));
767end
768
769function 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;
781end
782
783function 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;
798end
799
800function 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
811end
812
813function 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();
818end
819
820function 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
837end
838
839function 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;
850end
851
852function post.format(boardname, number)
853 return board.format(boardname) .. number;
854end
855
856-- Turn nanochan-formatting into html.
857function post.nano2html(text)
858 text = "\n" .. text .. "\n";
859
860 return text:gsub(">>(%d+)", "<a class='reference' href='#post%1'>>>%1</a>")
861 :gsub(">>>/([%d%l]-)/(%s)", "<a class='reference' href='/%1'>>>>/%1/</a>%2")
862 :gsub(">>>/([%d%l]-)/(%d+)", "<a class='reference' href='/%1/%2.html'>>>>/%1/%2</a>")
863 :gsub("\n>(.-)\n", "\n<span class='greentext'>>%1</span>\n")
864 :gsub("\n>(.-)\n", "\n<span class='greentext'>>%1</span>\n")
865 :gsub("\n<(.-)\n", "\n<span class='pinktext'><%1</span>\n")
866 :gsub("\n<(.-)\n", "\n<span class='pinktext'><%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("'''(.-)'''", "<b>%1</b>")
873 :gsub("''(.-)''", "<i>%1</i>")
874 :gsub("(https?://)([a-zA-Z0-9%./%%_%-%+=%?&;:,#%!~]+)", "<a rel='noreferrer' href='%1%2'>%1%2</a>")
875 :gsub("\n", "<br />");
876end
877
878-- This function does not delete the actual file. It simply removes the reference to that
879-- file.
880function 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"]);
889end
890
891function 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
924end
925
926function 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;
1026end
1027
1028--
1029-- Log access functions.
1030--
1031
1032function 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();
1040end
1041
1042function 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;
1056end
1057
1058--
1059-- HTML output functions.
1060--
1061
1062function 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>");
1073end
1074
1075function 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'>");
1115end
1116
1117function html.finish()
1118 io.write( "</div>");
1119 io.write( "</body>");
1120 io.write("</html>");
1121end
1122
1123function html.redheader(text)
1124 io.write("<h1 class='redheader'>", text, "</h1>");
1125end
1126
1127function html.announce()
1128 if global.retrieve("announce") then
1129 io.write("<div id='announce'>", global.retrieve("announce"), "</div>");
1130 end
1131end
1132
1133function html.container.begin(type)
1134 io.write("<div class='container ", type or "narrow", "'>");
1135end
1136
1137function html.container.finish()
1138 io.write("</div>");
1139end
1140
1141function html.container.barheader(text)
1142 io.write("<h2 class='barheader'>", text, "</h2>");
1143end
1144
1145function 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>");
1155end
1156
1157function 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>");
1166end
1167
1168function html.table.finish()
1169 io.write("</table>");
1170end
1171
1172function html.list.begin(type)
1173 io.write(type == "ordered" and "<ol>" or "<ul>");
1174end
1175
1176function html.list.entry(text, class)
1177 io.write("<li", class and (" class='" .. class .. "' ") or "", ">", text, "</li>");
1178end
1179
1180function html.list.finish(type)
1181 io.write(type == "ordered" and "</ol>" or "</ul>");
1182end
1183
1184-- Pre-defined pages.
1185function 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();
1192end
1193
1194function 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();
1201end
1202
1203function 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();
1212end
1213
1214function 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;
1231end
1232
1233function html.string.datetime(unixtime)
1234 local isotime = os.date("!%F %T", unixtime);
1235 return "<time datetime='" .. isotime .. "'>" .. isotime .. "</time>";
1236end
1237
1238function html.string.boardlink(boardname)
1239 return html.string.link(board.format(boardname));
1240end
1241
1242function html.string.threadlink(boardname, number)
1243 return html.string.link(post.format(boardname, number) .. ".html", post.format(boardname, number));
1244end
1245
1246function html.board.title(boardname)
1247 io.write("<h1 id='boardtitle'>", board.format(boardname), " - ", board.retrieve(boardname)["Title"], "</h1>");
1248end
1249
1250function html.board.subtitle(boardname)
1251 io.write("<h2 id='boardsubtitle'>", board.retrieve(boardname)["Subtitle"], "</h2>");
1252end
1253
1254function 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>");
1279end
1280
1281function 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>");
1300end
1301
1302function 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>");
1310end
1311
1312function 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>");
1345end
1346
1347-- Omitting the 'boardname' value will turn the catalog into an overboard.
1348function 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>");
1370end
1371
1372function 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"], "'>>>", 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 />");
1443end
1444
1445function 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
1453end
1454
1455function 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);
1494end
1495
1496function 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);
1508end
1509
1510function 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);
1541end
1542
1543function 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);
1558end
1559
1560-- Write HTTP headers.
1561if cgi.pathinfo[1] == "captcha.jpg" then
1562 io.write("Content-Type: image/jpeg\n");
1563else
1564 io.write("Content-Type: text/html; charset=utf-8\n");
1565end
1566
1567io.write("Cache-Control: no-cache\n");
1568io.write("\n");
1569
1570--
1571-- This is the main part of Nanochan, where all the pages are defined.
1572--
1573
1574if cgi.pathinfo[1] == nil then
1575 -- /nano
1576 html.redirect("/index.html");
1577elseif cgi.pathinfo[1] == "captcha.jpg" then
1578 io.write(captcha.create());
1579elseif 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();
1599elseif 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();
1634elseif 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
2177elseif 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
2260else
2261 html.pdp.notfound();
2262end
2263%>