Rewritten to use Jester as the backend web framework.

This commit is contained in:
dom96 2012-05-16 14:14:44 +01:00
commit 5e79d2191b
9 changed files with 180 additions and 372 deletions

View file

@ -8,10 +8,13 @@
import cairo, os, strutils import cairo, os, strutils
proc getCaptureFilename*(i: int): string {.inline.} = proc getCaptchaFilename*(i: int): string {.inline.} =
result = "captures/capture_" & $i & ".png" result = "public/captchas/capture_" & $i & ".png"
proc createCapture*(file, text: string) = proc getCaptchaUrl*(i: int): string =
result = "/captchas/capture_" & $i & ".png"
proc createCaptcha*(file, text: string) =
var surface = imageSurfaceCreate(FORMAT_ARGB32, 10*text.len, 10) var surface = imageSurfaceCreate(FORMAT_ARGB32, 10*text.len, 10)
var cr = create(surface) var cr = create(surface)

View file

@ -36,7 +36,8 @@ create table if not exists person(
email $# not null, email $# not null,
creation timestamp not null default (DATETIME('now')), creation timestamp not null default (DATETIME('now')),
salt varbin(128) not null, salt varbin(128) not null,
status integer not null status integer not null,
admin bool default false
);""" % [TUserName, TPassword, TEmail]), []) );""" % [TUserName, TPassword, TEmail]), [])
# echo "person table already exists" # echo "person table already exists"

View file

@ -22,7 +22,7 @@
</tr> </tr>
# for row in Rows(db, query): # for row in Rows(db, query):
<tr> <tr>
<td class="topic">${UrlButton(c, XMLencode(%name), actionShow, %threadId)}</td> <td class="topic">${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))}</td>
#let authorName = getValue(db, sql("select name from person where id = " & #let authorName = getValue(db, sql("select name from person where id = " &
# "(select author from post where id = " & # "(select author from post where id = " &
# "(select min(id) from post where thread = ?))"), %threadId) # "(select min(id) from post where thread = ?))"), %threadId)
@ -95,10 +95,10 @@
<hr/> <hr/>
${genGravatar(%userEmail)} ${genGravatar(%userEmail)}
#if c.userId == %postAuthor and c.currentPost.subject.len == 0: #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/>${UrlButton(c, "Edit post", actionEditForm, %postId)} <hr/>${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))}
#elif c.isAdmin and c.currentPost.subject.len == 0: #elif c.isAdmin and c.currentPost.subject.len == 0:
<hr/><span style="color:red"> <hr/><span style="color:red">
${UrlButton(c, "Edit post", actionEditForm, %postId)}</span> ${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))}</span>
#end if #end if
</td> </td>
<td class="content"> <td class="content">
@ -114,7 +114,7 @@
#end proc #end proc
# #
# #
#proc genFormPost(c: var TForumData, action: TForumAction, #proc genFormPost(c: var TForumData, action: string,
# isEdit: bool, title, content: string): string = # isEdit: bool, title, content: string): string =
# result = "" # result = ""
<br /> <br />
@ -123,7 +123,7 @@
<div id="replytop"> <div id="replytop">
<span>Reply</span> <span>Reply</span>
</div> </div>
<form action="$postAction" method="POST"> <form action="/$action" method="POST">
${FieldValid(c, "subject", "Subject:")} ${FieldValid(c, "subject", "Subject:")}
${TextWidget(c, "subject", title, maxlength=100)} ${TextWidget(c, "subject", title, maxlength=100)}
<br /> <br />
@ -146,7 +146,7 @@
# #
#proc genFormRegister(c: var TForumData): string = #proc genFormRegister(c: var TForumData): string =
# result = "" # result = ""
<form action="$postAction" method="POST"> <form action="/doregister" method="POST">
<b>Register</b><br /> <b>Register</b><br />
<table border="0"> <table border="0">
<tr> <tr>
@ -166,7 +166,6 @@
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td> <td>${TextWidget(c, "antibot", "", maxlength=4)}</td>
</tr> </tr>
</table> </table>
${FormSession(c, actionRegister)}
<input type="submit" value="Register"> <input type="submit" value="Register">
</form> </form>
#end proc #end proc
@ -174,14 +173,13 @@
#proc genFormLogin(c: var TForumData): string = #proc genFormLogin(c: var TForumData): string =
# result = "" # result = ""
# if not c.loggedIn: # if not c.loggedIn:
<form action="$postAction" method="POST"> <form action="/dologin" method="POST">
<table border="0"> <table border="0">
<tr><td>Username:</td><td> <tr><td>Username:</td><td>
<input type="text" name="name" maxlength="20"></td></tr> <input type="text" name="name" maxlength="20"></td></tr>
<tr><td>Password:</td><td> <tr><td>Password:</td><td>
<input type="password" name="password" maxlength="20"></td></tr> <input type="password" name="password" maxlength="20"></td></tr>
</table> </table>
${FormSession(c, actionLogin)}
<input type="submit" value="Login"> <input type="submit" value="Login">
</form> </form>
<span style="color:red">$c.loginErrorMsg</span> <span style="color:red">$c.loginErrorMsg</span>

505
forum.nim
View file

@ -8,30 +8,15 @@
import import
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
rst, rstgen, captchas, sockets, scgi, cookies rst, rstgen, captchas, sockets, scgi, jester
const const
unselectedThread = -1 unselectedThread = -1
transientThread = 0 transientThread = 0
websiteLoc = "/" websiteLoc = ""
postAction = "/"
type type
TCrud = enum crCreate, crRead, crUpdate, crDelete TCrud = enum crCreate, crRead, crUpdate, crDelete
TForumAction = enum
actionShow = "show",
actionLoginForm = "login",
actionLogin = "dologin",
actionLogout = "logout",
actionRegisterForm = "register",
actionRegister = "doregister",
actionNewThreadForm = "newthread",
actionNewThread = "donewthread",
actionReplyForm = "reply",
actionReply = "doreply",
actionEditForm = "edit",
actionEdit = "doedit",
action404
TSession = object of TObject TSession = object of TObject
threadid: int threadid: int
@ -42,25 +27,15 @@ type
TPost = tuple[subject, content: string] TPost = tuple[subject, content: string]
TForumData = object of TSession TForumData = object of TSession
action: TForumAction req: TRequest
cgiData: PStringTable
ip: string
userid: string userid: string
actionContent: string actionContent: string
errorMsg, loginErrorMsg: string errorMsg, loginErrorMsg: string
invalidField: string invalidField: string
currentPost: TPost currentPost: TPost
reqUrl: string
startTime: float startTime: float
cookieData: PStringTable
TStyledButton = tuple[text: string, action: TForumAction, tid: string] TStyledButton = tuple[text: string, link: string]
TRequest* {.final.} = object ## a request for the application to process
ip*: string ## IP of request
url*: string ## requested URL
vars*: PStringTable ## other variables
startTime*: float
var var
db: TDbConn db: TDbConn
@ -72,8 +47,6 @@ proc init(c: var TForumData) =
c.threadId = unselectedThread c.threadId = unselectedThread
c.postId = -1 c.postId = -1
c.action = actionShow
c.ip = ""
c.userid = "" c.userid = ""
c.actionContent = "" c.actionContent = ""
c.errorMsg = "" c.errorMsg = ""
@ -94,14 +67,14 @@ const
proc TextWidget(c: TForumData, name, defaultText: string, proc TextWidget(c: TForumData, name, defaultText: string,
maxlength = 30, size = -1): string = maxlength = 30, size = -1): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: XMLencode(c.cgiData[name]) else: XMLencode(c.req.params[name])
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [ return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""]
proc TextAreaWidget(c: TForumData, name, defaultText: string, proc TextAreaWidget(c: TForumData, name, defaultText: string,
width = 80, height = 20): string = width = 80, height = 20): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: XMLencode(c.cgiData[name]) else: XMLencode(c.req.params[name])
return """<textarea name="$1" cols="$2" rows="$3">$4</textarea>""" % [ return """<textarea name="$1" cols="$2" rows="$3">$4</textarea>""" % [
name, $width, $height, x] name, $width, $height, x]
@ -111,71 +84,44 @@ proc FieldValid(c: TForumData, name, text: string): string =
else: else:
result = text result = text
proc genQuery(c: var TForumData, action: TForumAction, target: string): string = proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = ""): string =
result = websiteLoc result = "/t/" & (if threadid == "": $c.threadId else: threadid)
case action if action != "":
of actionShow: result.add("?action=" & action)
if target != "": if postId != "":
result.add("t/" & target) result.add("&postid=" & postid)
of actionReplyForm: result = c.req.makeUri(result, absolute = false)
result.add("t/" & target & "?action=reply")
of actionEditForm:
result.add("t/" & $c.threadid & "?action=edit&postid=" & target)
else:
result.add($action & "/" & target)
proc FormSession(c: var TForumData, nextAction: TForumAction): string = proc FormSession(c: var TForumData, nextAction: string): string =
return """<input type="hidden" name="action" value="$1" /> return """<input type="hidden" name="threadid" value="1" />
<input type="hidden" name="threadid" value="$2" /> <input type="hidden" name="postid" value="$2" />""" % [
<input type="hidden" name="postid" value="$3" />""" % [ $c.threadId, $c.postid]
$nextAction, $c.threadId, $c.postid]
proc UrlButton(c: var TForumData, text: string, proc UrlButton(c: var TForumData, text, url: string): string =
nextAction: TForumAction, target=""): string =
return ("""<a class="url_button" href="$1">$2</a>""") % [ return ("""<a class="url_button" href="$1">$2</a>""") % [
c.genQuery(nextAction, target), text] url, text]
proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string =
if btns.len == 1: if btns.len == 1:
var anchor = "" var anchor = ""
if btns[0].action == actionReplyForm:
anchor = "#reply"
result = ("""<a class="active button" href="$1$3">$2</a>""") % [ result = ("""<a class="active button" href="$1$3">$2</a>""") % [
c.genQuery(btns[0].action, btns[0].tid), btns[0].text, anchor] btns[0].link, btns[0].text, anchor]
else: else:
result = "" result = ""
for i, btn in pairs(btns): for i, btn in pairs(btns):
var anchor = "" var anchor = ""
if btns[i].action == actionReplyForm:
anchor = "#reply"
var class = "" var class = ""
if i == 0: class = "left " if i == 0: class = "left "
elif i == btns.len()-1: class = "right " elif i == btns.len()-1: class = "right "
else: class = "middle " else: class = "middle "
result.add(("""<a class="$3active button" href="$1$4">$2</a>""") % [ result.add(("""<a class="$3active button" href="$1$4">$2</a>""") % [
c.genQuery(btns[i].action, btns[i].tid), btns[i].text, class, anchor]) btns[i].link, btns[i].text, class, anchor])
proc genSlash(c: var TForumData): string =
let reqPath = c.reqUrl
if reqPath.endswith("/"):
return ""
else: return reqPath & "/"
proc formatTimestamp(t: int): string = proc formatTimestamp(t: int): string =
let t2 = getGMTime(TTime(t)) let t2 = getGMTime(TTime(t))
result = "" return t2.format("ddd',' d MMM yyyy HH':'mm 'UTC'")
result.add(`$`(t2.weekday)[ .. 2] & ", ")
result.add($t2.monthday & " ")
result.add(`$`(t2.month)[ .. 2] & " ")
result.add($t2.year & " ")
if t2.hour < 10:
result.add("0")
result.add($t2.hour & ":")
if t2.minute < 10:
result.add("0")
result.add($t2.minute)
proc genGravatar(email: string, size: int = 80): string = proc genGravatar(email: string, size: int = 80): string =
let emailMD5 = email.toLower.toMD5 let emailMD5 = email.toLower.toMD5
@ -226,13 +172,13 @@ proc antibot(c: var TForumData): string =
let b = math.random(1000)+1 let b = math.random(1000)+1
let answer = $(a+b) let answer = $(a+b)
Exec(db, sql"delete from antibot where ip = ?", c.ip) Exec(db, sql"delete from antibot where ip = ?", c.req.ip)
let captureId = TryInsertID(db, let CaptchaId = TryInsertID(db,
sql"insert into antibot(ip, answer) values (?, ?)", c.ip, sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip,
answer).int mod 10_000 answer).int mod 10_000
let captureFile = "captchas/captcha_" & $captureId & ".png" let CaptchaFile = getCaptchaFilename(CaptchaId)
createCapture(captureFile, $a & "+" & $b) createCaptcha(CaptchaFile, $a & "+" & $b)
result = """<img src="$1" />""" % captureFile result = """<img src="$1" />""" % getCaptchaUrl(captchaId)
const const
SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'}
@ -242,12 +188,7 @@ proc setError(c: var TForumData, field, msg: string): bool {.inline.} =
c.errorMsg = "Error: " & msg c.errorMsg = "Error: " & msg
return false return false
proc register(c: var TForumData): bool = proc register(c: var TForumData, name, pass, antibot, email: string): bool =
# get form data:
let name = c.cgiData["name"]
let pass = c.cgiData["new_password"]
let antibot = c.cgiData["antibot"]
let email = c.cgiData["email"]
# Username validation: # Username validation:
if name.len == 0 or not allCharsInSet(name, SecureChars): if name.len == 0 or not allCharsInSet(name, SecureChars):
return setError(c, "name", "Invalid username!") return setError(c, "name", "Invalid username!")
@ -260,7 +201,7 @@ proc register(c: var TForumData): bool =
# antibot validation: # antibot validation:
let correctRes = GetValue(db, let correctRes = GetValue(db,
sql"select answer from antibot where ip = ?", c.ip) sql"select answer from antibot where ip = ?", c.req.ip)
if antibot != correctRes: if antibot != correctRes:
return setError(c, "antibot", "You seem to be a bot!") return setError(c, "antibot", "You seem to be a bot!")
@ -277,16 +218,16 @@ proc register(c: var TForumData): bool =
return true return true
proc checkLoggedIn(c: var TForumData) = proc checkLoggedIn(c: var TForumData) =
let pass = c.cookieData["sid"] let pass = c.req.cookies["sid"]
if pass.len == 0: return if pass.len == 0: return
if ExecAffectedRows(db, if ExecAffectedRows(db,
sql("update session set lastModified = DATETIME('now') " & sql("update session set lastModified = DATETIME('now') " &
"where ip = ? and password = ?"), "where ip = ? and password = ?"),
c.ip, pass) > 0: c.req.ip, pass) > 0:
c.userpass = pass c.userpass = pass
c.userid = GetValue(db, c.userid = GetValue(db,
sql"select userid from session where ip = ? and password = ?", sql"select userid from session where ip = ? and password = ?",
c.ip, pass) c.req.ip, pass)
let row = getRow(db, let row = getRow(db,
sql"select name, email, admin from person where id = ?", c.userid) sql"select name, email, admin from person where id = ?", c.userid)
@ -294,23 +235,23 @@ proc checkLoggedIn(c: var TForumData) =
c.email = ||row[1] c.email = ||row[1]
c.isAdmin = parseBool(||row[2]) c.isAdmin = parseBool(||row[2])
else: else:
echo("not found login") echo("SID not found in sessions. Assuming logged out.")
proc logout(c: var TForumData) = proc logout(c: var TForumData) =
const query = sql"delete from session where ip = ? and password = ?" const query = sql"delete from session where ip = ? and password = ?"
c.username = "" c.username = ""
c.userpass = "" c.userpass = ""
Exec(db, query, c.ip, c.cookieData["sid"]) Exec(db, query, c.req.ip, c.req.cookies["sid"])
proc incrementViews(c: var TForumData) = proc incrementViews(c: var TForumData) =
const query = sql"update thread set views = views + 1 where id = ?" const query = sql"update thread set views = views + 1 where id = ?"
Exec(db, query, $c.threadId) Exec(db, query, $c.threadId)
proc isPreview(c: TForumData): bool = proc isPreview(c: TForumData): bool =
result = c.cgiData["previewBtn"].len > 0 result = c.req.params["previewBtn"].len > 0 # TODO: Could be wrong?
proc isDelete(c: TForumData): bool = proc isDelete(c: TForumData): bool =
result = c.cgiData["delete"].len > 0 result = c.req.params["delete"].len > 0
proc rstToHtml(content: string): string = proc rstToHtml(content: string): string =
result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown},
@ -352,11 +293,11 @@ proc crud(c: TCrud, table: string, data: openArray[string]): TSqlQuery =
result = sql("delete from " & table & " where id = ?") result = sql("delete from " & table & " where id = ?")
template retrSubject(c: expr) = template retrSubject(c: expr) =
let subject = c.cgiData["subject"] let subject = c.req.params["subject"]
if subject.len < 3: return setError(c, "subject", "Subject not long enough") if subject.len < 3: return setError(c, "subject", "Subject not long enough")
template retrContent(c: expr) = template retrContent(c: expr) =
let content = c.cgiData["content"] let content = c.req.params["content"]
if not validateRst(c, content): return false if not validateRst(c, content): return false
template retrPost(c: expr) = template retrPost(c: expr) =
@ -379,7 +320,7 @@ template setPreviewData(c: expr) =
template writeToDb(c, cr, postId: expr) = template writeToDb(c, cr, postId: expr) =
exec(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), exec(db, crud(cr, "post", "author", "ip", "header", "content", "thread"),
c.userId, c.ip, subject, content, $c.threadId, postId) c.userId, c.req.ip, subject, content, $c.threadId, postId)
proc edit(c: var TForumData, postId: int): bool = proc edit(c: var TForumData, postId: int): bool =
checkLogin(c) checkLogin(c)
@ -422,7 +363,7 @@ proc newThread(c: var TForumData): bool =
setPreviewData(c) setPreviewData(c)
c.threadID = transientThread c.threadID = transientThread
else: else:
c.threadID = TryInsertID(db, query, c.cgiData["subject"]).int c.threadID = TryInsertID(db, query, c.req.params["subject"]).int
if c.threadID < 0: return setError(c, "subject", "Subject already exists") if c.threadID < 0: return setError(c, "subject", "Subject already exists")
writeToDb(c, crCreate, "") writeToDb(c, crCreate, "")
result = true result = true
@ -447,7 +388,7 @@ proc login(c: var TForumData, name, pass: string): bool =
# create session: # create session:
Exec(db, Exec(db,
sql"insert into session (ip, password, userid) values (?, ?, ?)", sql"insert into session (ip, password, userid) values (?, ?, ?)",
c.ip, c.userpass, c.userid) c.req.ip, c.userpass, c.userid)
return true return true
else: else:
return c.setError("password", "Login failed!") return c.setError("password", "Login failed!")
@ -456,288 +397,150 @@ proc genActionMenu(c: var TForumData): string =
result = "" result = ""
var btns: seq[TStyledButton] = @[] var btns: seq[TStyledButton] = @[]
if c.threadId >= 0: if c.threadId >= 0:
btns.add(("Thread List", actionShow, "")) btns.add(("Thread List", c.req.makeUri("/", false)))
if c.loggedIn: if c.loggedIn:
if c.threadId >= 0: if c.threadId >= 0:
btns.add(("Reply", actionReplyForm, $c.threadId)) let replyUrl = c.genThreadUrl("", "reply") & "#reply"
btns.add(("New Thread", actionNewThreadForm, "")) btns.add(("Reply", replyUrl))
btns.add(("New Thread", c.req.makeUri("/newthread", false)))
result = c.genButtons(btns) result = c.genButtons(btns)
include "forms.tmpl" include "forms.tmpl"
include "main.tmpl" include "main.tmpl"
proc contentAtPosition(c: var TForumData): string =
if c.threadId == transientThread:
result = c.genPostPreview(c.currentPost.subject,
c.currentPost.content,
c.username, $getGMTime(getTime()))
elif validThreadId(c):
result = genPostsList(c, $c.threadId)
else:
result = genThreadsList(c)
proc prependRe(s: string): string = proc prependRe(s: string): string =
result = if s.len == 0: result = if s.len == 0:
"" ""
elif s.startswith("Re:"): s elif s.startswith("Re:"): s
else: "Re: " & s else: "Re: " & s
proc dispatch(c: var TForumData): tuple[status, content: string, template createTFD(): stmt =
headers: PStringTable] = var c: TForumData
template `@`(x: expr): expr = c.cgiData[x] init(c)
c.req = request
c.startTime = epochTime()
if request.cookies.len > 0:
checkLoggedIn(c)
template redirect(): stmt = get "/":
result[0] = "303 See Other" createTFD()
let q = c.genQuery(actionShow, $c.threadId) resp genMain(c, genThreadsList(c))
result[2] = {"Location": q}.newStringTable()
template successfulLogin() = get "/t/@threadid/?":
redirect() createTFD()
# TODO: Security risk: I'm not sure that using the hashed password as the parseInt(@"threadid", c.threadId, -1..1000_000)
# sid is the best idea... if (@"postid").len > 0:
var tim = TTime(int(getTime()) + 7 * (60 * 60 * 24)) # 7 days added parseInt(@"postid", c.postId, -1..1000_000)
result[2]["Set-Cookie"] = setCookie("sid", c.userpass,
tim.getGMTime(), noName = true)
let tid = @"threadid" if (@"action").len > 0:
if tid.len > 0: case @"action"
parseInt(tid, c.threadId, -1..1000_000) of "reply":
let pid = @"postid" let subject = GetValue(db,
if pid.len > 0: sql"select header from post where id = (select max(id) from post where thread = ?)",
parseInt(pid, c.postId, -1..1000_000) $c.threadId).prependRe
body = genPostsList(c, $c.threadId)
result[0] = "200 OK"
result[1] = ""
result[2] = {"Content-Type": "text/html"}.newStringTable
case c.action
of actionShow:
if tid.len > 0:
incrementViews(c)
result[1] = contentAtPosition(c)
of actionRegisterForm:
result[1] = genFormRegister(c)
of actionRegister:
if register(c):
discard login(c, @"name", @"new_password")
successfulLogin()
else:
result[1] = genFormRegister(c)
of actionLoginForm:
result[1] = genFormLogin(c)
of actionLogin:
if login(c, @"name", @"password"):
successfulLogin()
else:
result[1] = genFormLogin(c)
of actionLogout:
logout(c)
redirect()
of actionReplyForm:
let subject = GetValue(db,
sql"select header from post where id = (select max(id) from post where thread = ?)",
$c.threadId).prependRe
result[1] = contentAtPosition(c)
result[1].add genFormPost(c, actionReply, false, subject, "")
of actionReply:
if reply(c):
redirect()
else:
result[1] = contentAtPosition(c)
if c.isPreview: if c.isPreview:
result[1] = genPostPreview(c, @"subject", @"content", body = genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime())) c.userName, $getGMTime(getTime()))
result[1].add genFormPost(c, actionReply, false, reuseText, reuseText) body.add genFormPost(c, "doreply", false, subject, "")
of actionEditForm: of "edit":
const query = sql"select header, content from post where id = ?" cond c.postId != -1
let row = getRow(db, query, $c.postId) const query = sql"select header, content from post where id = ?"
let header = ||row[0] let row = getRow(db, query, $c.postId)
let content = ||row[1] let header = ||row[0]
result[1] = genFormPost(c, actionEdit, true, header, content) let content = ||row[1]
of actionEdit: body = genFormPost(c, "doedit", true, header, content)
if edit(c, c.postId): resp c.genMain(body)
redirect()
else:
if c.isPreview:
result[1] = genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime()))
result[1].add genFormPost(c, actionEdit, true, reuseText, reuseText)
of actionNewThreadForm:
result[1] = genFormPost(c, actionNewThread, false, "", "")
of actionNewThread:
if newThread(c):
redirect()
else:
if c.isPreview:
result[1] = genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime()))
result[1].add genFormPost(c, actionNewThread, false, reuseText, reuseText)
of action404:
result[0] = "404 Not Found"
result[1] = ""
result[2] = newStringTable()
if result[0] == "200 OK":
result[1] = genMain(c, result[1])
proc normalizeDocURI(url: string): string =
## Adds a leading / if one doesn't exist.
result = if url[url.len-1] != '/': url & '/' else: url
proc getAction(cgiData: PStringTable): TForumAction =
var a = cgiData["action"]
var path = ""
let url = cgiData["DOCUMENT_URI"].normalizeDocURI
if url.startswith(websiteLoc):
path = url.substr(websiteLoc.len)
if path.len != 0:
if path[path.len-1] == '/': path = path.substr(0, path.len()-2)
echo "PATH: ", path
else: return action404
if path == "":
if a != "":
result = parseEnum[TForumAction](a, action404)
else:
result = actionShow
else: else:
var slashes = path.split('/') cond validThreadId(c)
case slashes[0] incrementViews(c)
of "t", "thread": resp genMain(c, genPostsList(c, $c.threadId))
if slashes.len() > 1:
cgiData["threadid"] = slashes[1]
case a
of "reply": result = actionReplyForm
of "edit": result = actionEditForm
else: result = actionShow
else:
result = action404
else:
result = parseEnum[TForumAction](path, action404)
echo("Parsed ", path, " as ", result)
proc processRequest(r: TRequest): tuple[status, content: string, get "/login/?":
headers: PStringTable] = createTFD()
try: resp genMain(c, genFormLogin(c))
var c: TForumData
init(c)
c.ip = r.ip
c.reqUrl = r.url
c.startTime = r.startTime
assert c.ip.len > 0 get "/logout/?":
c.cgiData = r.vars createTFD()
c.cookieData = parseCookies(c.cgiData["HTTP_COOKIE"]) logout(c)
echo(c.cookieData) redirect(uri("/"))
if c.cookieData.len > 0:
checkLoggedIn(c)
if c.cgiData.len > 0:
c.action = getAction(c.cgiData)
echo c.cgiData get "/register/?":
echo(c.action) createTFD()
result = dispatch(c) resp genMain(c, genFormRegister(c))
except:
result[0] = "500 Internal Server Error"
result[1] = "Internal Error: " & getCurrentExceptionMsg()
result[2] = newStringTable()
proc extractDirFile(s: string): tuple[dir, file: string] = template readIDs(): stmt =
var last = s.len-1 # Retrieve the threadid and postid
while last > 0 and s[last] == '/': dec last if (@"threadid").len > 0:
var splitPoint = last-1 parseInt(@"threadid", c.threadId, -1..1000_000)
while splitPoint >= 0 and s[splitPoint] != '/': dec splitPoint if (@"postid").len > 0:
parseInt(@"postid", c.postId, -1..1000_000)
result.file = s.substr(splitPoint+1, last) template finishLogin(): stmt =
# skip '/' setCookie("sid", c.userpass, daysForward(7))
var splitPoint2 = splitPoint-1 redirect(uri("/"))
while splitPoint2 >= 0 and s[splitPoint2] != '/': dec splitPoint2
result.dir = s.substr(splitPoint2+1, splitPoint-1)
proc processFile(r: TRequest): tuple[isFile: bool, template handleError(action: string, isEdit: bool): stmt =
contentType, content: string] = if c.isPreview:
try: body.add genPostPreview(c, @"subject", @"content",
let url = r.vars["DOCUMENT_URI"].normalizeDocURI c.userName, $getGMTime(getTime()))
let (dir, file) = extractDirFile(url) body.add genFormPost(c, action, isEdit, reuseText, reuseText)
case dir resp genMain(c, body)
of "css":
result = (true, "text/css", readFile("style/" & file))
of "captchas":
result = (true, "image/png", readFile("captchas/" & file))
of "smilies":
result = (true, "image/gif", readFile("images/smilies/" & file))
else:
result = (false, "", "")
except:
result = (false, "", "")
# ------------------ main file ---------------------------------------------- post "/dologin":
createTFD()
if login(c, @"name", @"password"):
finishLogin()
else:
resp c.genMain(genFormLogin(c))
var s: TScgiState post "/doregister":
createTFD()
if c.register(@"name", @"new_password", @"antibot", @"email"):
discard c.login(@"name", @"new_password")
finishLogin()
else:
resp c.genMain(genFormRegister(c))
proc shutdown() {.noconv.} = post "/donewthread":
s.close() createTFD()
writeStackTrace() if newThread(c):
quit 1 redirect(uri("/"))
else:
body = ""
handleError("donewthread", false)
system.setControlCHook(shutdown) post "/doreply":
createTFD()
readIDs()
if reply(c):
redirect(c.genThreadUrl())
else:
body = genPostsList(c, $c.threadId)
handleError("doreply", false)
when not defined(writeStatusContent): post "/doedit":
proc writeStatusContent(c: TSocket, status, content: string, createTFD()
headers: PStringTable) = readIDs()
var strHeaders = "" if edit(c, c.postId):
for key, value in headers: redirect(c.genThreadUrl())
strHeaders.add(key & ": " & value & "\r\L") else:
c.send("Status: " & status & "\r\L" & strHeaders & "\r\L") body = ""
c.send(content) handleError("doedit", true)
proc mainLoop() = get "/newthread/?":
try: createTFD()
db = Open(connection="nimforum.db", user="postgres", password="", resp genMain(c, genFormPost(c, "donewthread", false, "", ""))
database="nimforum")
open(s, 9000.TPort)
while next(s):
var r: TRequest
r.vars = s.headers
r.ip = r.vars["REMOTE_ADDR"]
r.url = r.vars["DOCUMENT_URI"]
r.startTime = epochTime()
# the server software (nginx) seems to f*ck up SCGI + POST/GET, so I work when isMainModule:
# around this issue here:
try:
for key, val in cgi.decodeData(r.vars["QUERY_STRING"]):
r.vars[key] = val
except ECgi:
nil
try:
for key, val in cgi.decodeData(s.input):
r.vars[key] = val
except ECgi:
nil
let fi = processFile(r)
if fi.isFile:
writeStatusOkTextContent(s.client, fi.contentType)
send(s.client, fi.content)
else:
let (status, resp, headers) = processRequest(r)
s.client.writeStatusContent(status, resp, headers)
s.client.close()
finally:
close(s)
close(db)
proc main() =
docConfig = rstgen.defaultConfig() docConfig = rstgen.defaultConfig()
math.randomize() math.randomize()
db = Open(connection="nimforum.db", user="postgres", password="",
for i in 0..10: database="nimforum")
try: var http = true
mainLoop() if paramCount() > 0:
except: if paramStr(1) == "scgi":
echo "FATAL: ", getCurrentExceptionMsg() http = false
os.sleep(1000) run(websiteLoc, port = TPort(9001), http = http)
db.close()
main()

View file

@ -5,8 +5,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Nimrod Forum</title> <title>Nimrod Forum</title>
<link rel="stylesheet" href="${c.genSlash}css/normalize.css"> <link rel="stylesheet" href="/css/normalize.css">
<link rel="stylesheet" href="${c.genSlash}css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
@ -15,7 +15,7 @@
</div> </div>
<div id="header"> <div id="header">
#let frontQuery = c.genQuery(actionShow, "") #let frontQuery = c.req.makeUri("/")
<span><a href="${frontQuery}">Nimrod's Forum</a></span> <span><a href="${frontQuery}">Nimrod's Forum</a></span>
#if c.loggedIn: #if c.loggedIn:
<a href="${websiteLoc}logout" class="right">Logout</a> <a href="${websiteLoc}logout" class="right">Logout</a>

View file

@ -3,3 +3,4 @@
--path:"$nimrod/packages/docutils" --path:"$nimrod/packages/docutils"
--path:"$nimrod" --path:"$nimrod"
--path:"/home/dominik/code/nimrod/jester"

2
public/captchas/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore