Rewritten to use Jester as the backend web framework.
This commit is contained in:
parent
ecbd49d2a0
commit
5e79d2191b
9 changed files with 180 additions and 372 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
16
forms.tmpl
16
forms.tmpl
|
|
@ -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
505
forum.nim
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
2
public/captchas/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
Loading…
Add table
Add a link
Reference in a new issue