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>

515
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
template redirect(): stmt = c.startTime = epochTime()
result[0] = "303 See Other" if request.cookies.len > 0:
let q = c.genQuery(actionShow, $c.threadId) checkLoggedIn(c)
result[2] = {"Location": q}.newStringTable()
template successfulLogin() =
redirect()
# TODO: Security risk: I'm not sure that using the hashed password as the
# sid is the best idea...
var tim = TTime(int(getTime()) + 7 * (60 * 60 * 24)) # 7 days added
result[2]["Set-Cookie"] = setCookie("sid", c.userpass,
tim.getGMTime(), noName = true)
let tid = @"threadid"
if tid.len > 0:
parseInt(tid, c.threadId, -1..1000_000)
let pid = @"postid"
if pid.len > 0:
parseInt(pid, c.postId, -1..1000_000)
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:
result[1] = genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime()))
result[1].add genFormPost(c, actionReply, false, reuseText, reuseText)
of actionEditForm:
const query = sql"select header, content from post where id = ?"
let row = getRow(db, query, $c.postId)
let header = ||row[0]
let content = ||row[1]
result[1] = genFormPost(c, actionEdit, true, header, content)
of actionEdit:
if edit(c, c.postId):
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 = get "/":
## Adds a leading / if one doesn't exist. createTFD()
result = if url[url.len-1] != '/': url & '/' else: url resp genMain(c, genThreadsList(c))
proc getAction(cgiData: PStringTable): TForumAction = get "/t/@threadid/?":
var a = cgiData["action"] createTFD()
parseInt(@"threadid", c.threadId, -1..1000_000)
if (@"postid").len > 0:
parseInt(@"postid", c.postId, -1..1000_000)
var path = "" if (@"action").len > 0:
let url = cgiData["DOCUMENT_URI"].normalizeDocURI case @"action"
if url.startswith(websiteLoc): of "reply":
path = url.substr(websiteLoc.len) let subject = GetValue(db,
if path.len != 0: sql"select header from post where id = (select max(id) from post where thread = ?)",
if path[path.len-1] == '/': path = path.substr(0, path.len()-2) $c.threadId).prependRe
echo "PATH: ", path body = genPostsList(c, $c.threadId)
else: return action404 if c.isPreview:
body = genPostPreview(c, @"subject", @"content",
if path == "": c.userName, $getGMTime(getTime()))
if a != "": body.add genFormPost(c, "doreply", false, subject, "")
result = parseEnum[TForumAction](a, action404) of "edit":
else: cond c.postId != -1
result = actionShow const query = sql"select header, content from post where id = ?"
let row = getRow(db, query, $c.postId)
let header = ||row[0]
let content = ||row[1]
body = genFormPost(c, "doedit", true, header, content)
resp c.genMain(body)
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
c.cgiData = r.vars
c.cookieData = parseCookies(c.cgiData["HTTP_COOKIE"])
echo(c.cookieData)
if c.cookieData.len > 0:
checkLoggedIn(c)
if c.cgiData.len > 0:
c.action = getAction(c.cgiData)
echo c.cgiData get "/logout/?":
echo(c.action) createTFD()
result = dispatch(c) logout(c)
except: redirect(uri("/"))
result[0] = "500 Internal Server Error"
result[1] = "Internal Error: " & getCurrentExceptionMsg()
result[2] = newStringTable()
proc extractDirFile(s: string): tuple[dir, file: string] = get "/register/?":
var last = s.len-1 createTFD()
while last > 0 and s[last] == '/': dec last resp genMain(c, genFormRegister(c))
var splitPoint = last-1
while splitPoint >= 0 and s[splitPoint] != '/': dec splitPoint
result.file = s.substr(splitPoint+1, last)
# skip '/'
var splitPoint2 = splitPoint-1
while splitPoint2 >= 0 and s[splitPoint2] != '/': dec splitPoint2
result.dir = s.substr(splitPoint2+1, splitPoint-1)
proc processFile(r: TRequest): tuple[isFile: bool, template readIDs(): stmt =
contentType, content: string] = # Retrieve the threadid and postid
try: if (@"threadid").len > 0:
let url = r.vars["DOCUMENT_URI"].normalizeDocURI parseInt(@"threadid", c.threadId, -1..1000_000)
let (dir, file) = extractDirFile(url) if (@"postid").len > 0:
case dir parseInt(@"postid", c.postId, -1..1000_000)
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 ---------------------------------------------- template finishLogin(): stmt =
setCookie("sid", c.userpass, daysForward(7))
redirect(uri("/"))
var s: TScgiState template handleError(action: string, isEdit: bool): stmt =
if c.isPreview:
body.add genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime()))
body.add genFormPost(c, action, isEdit, reuseText, reuseText)
resp genMain(c, body)
proc shutdown() {.noconv.} = post "/dologin":
s.close() createTFD()
writeStackTrace() if login(c, @"name", @"password"):
quit 1 finishLogin()
else:
system.setControlCHook(shutdown) resp c.genMain(genFormLogin(c))
when not defined(writeStatusContent): post "/doregister":
proc writeStatusContent(c: TSocket, status, content: string, createTFD()
headers: PStringTable) = if c.register(@"name", @"new_password", @"antibot", @"email"):
var strHeaders = "" discard c.login(@"name", @"new_password")
for key, value in headers: finishLogin()
strHeaders.add(key & ": " & value & "\r\L") else:
c.send("Status: " & status & "\r\L" & strHeaders & "\r\L") resp c.genMain(genFormRegister(c))
c.send(content)
proc mainLoop() = post "/donewthread":
try: createTFD()
db = Open(connection="nimforum.db", user="postgres", password="", if newThread(c):
database="nimforum") redirect(uri("/"))
open(s, 9000.TPort) else:
while next(s): body = ""
var r: TRequest handleError("donewthread", false)
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
# 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() = post "/doreply":
createTFD()
readIDs()
if reply(c):
redirect(c.genThreadUrl())
else:
body = genPostsList(c, $c.threadId)
handleError("doreply", false)
post "/doedit":
createTFD()
readIDs()
if edit(c, c.postId):
redirect(c.genThreadUrl())
else:
body = ""
handleError("doedit", true)
get "/newthread/?":
createTFD()
resp genMain(c, genFormPost(c, "donewthread", false, "", ""))
when isMainModule:
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