all relevant files added

This commit is contained in:
Araq 2012-05-05 01:14:26 +02:00
commit 6476dc7cbb
9 changed files with 1328 additions and 1 deletions

View file

@ -1,4 +1,11 @@
nimforum
========
nimforum
This is Nimrod's forum. The code is not nice and depends on the RST parser of
the Nimrod compiler.
Copyright (c) 2012 Andreas Rumpf.
All rights reserved.

36
captchas.nim Normal file
View file

@ -0,0 +1,36 @@
#
#
# The Nimrod Forum
# (c) Copyright 2012 Andreas Rumpf
#
# All rights reserved.
#
import cairo, os, strutils
proc getCaptureFilename*(i: int): string {.inline.} =
result = "captures/capture_" & $i & ".png"
proc createCapture*(file, text: string) =
var surface = imageSurfaceCreate(FORMAT_ARGB32, 10*text.len, 10)
var cr = create(surface)
selectFontFace(cr, "serif", FONT_SLANT_NORMAL, FONT_WEIGHT_BOLD)
setFontSize(cr, 12.0)
setSourceRgb(cr, 1.0, 0.5, 0.0)
moveTo(cr, 0.0, 10.0)
showText(cr, repeatChar(text.len, 'O'))
setSourceRgb(cr, 0.0, 0.0, 1.0)
moveTo(cr, 0.0, 10.0)
showText(cr, text)
destroy(cr)
discard writeToPng(surface, file)
destroy(surface)
when isMainModule:
createCapture("test.png", "1+33")

91
createdb.nim Normal file
View file

@ -0,0 +1,91 @@
#
#
# Nimrod Forum
# (c) Copyright 2012 Andreas Rumpf
#
# All rights reserved.
#
import strutils, db_sqlite
var db = Open(connection="nimforum.db", user="postgres", password="",
database="nimforum")
const
TUserName = "varchar(20)"
TPassword = "varchar(32)"
TEmail = "varchar(30)"
db.Exec(sql"""
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now'))
);""", [])
db.Exec(sql"""
create unique index if not exists ThreadNameIx on thread (name);
""", [])
db.Exec(sql("""
create table if not exists person(
id integer primary key,
name $# not null,
password $# not null,
email $# not null,
creation timestamp not null default (DATETIME('now')),
salt varbin(128) not null,
status integer not null
);""" % [TUserName, TPassword, TEmail]), [])
# echo "person table already exists"
db.Exec(sql"""
create unique index if not exists UserNameIx on person (name);
""", [])
# ----------------------- Forum ------------------------------------------------
if not db.TryExec(sql"""
create table if not exists post(
id integer primary key,
author integer not null,
ip inet not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);""", []):
echo "post table already exists"
# -------------------- Session -------------------------------------------------
if not db.TryExec(sql("""
create table if not exists session(
id integer primary key,
ip inet not null,
password $# not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);""" % [TPassword]), []):
echo "session table already exists"
if not db.TryExec(sql"""
create table if not exists antibot(
id integer primary key,
ip inet not null,
answer varchar(30) not null,
created timestamp not null default (DATETIME('now'))
);""", []):
echo "antibot table already exists"
#discard stdin.readline()
Close(db)

187
forms.tmpl Normal file
View file

@ -0,0 +1,187 @@
#! stdtmpl
#
#template `%`(idx: expr): expr {.immediate.} =
# row[idx]
#end template
#
#
#proc genThreadsList(c: var TForumData): string =
# const query = sql"select id, name, views, modified from thread order by modified desc"
# const threadId = 0
# const name = 1
# const views = 2
#
# result = ""
<table id="threads">
<tr>
<th>Topics</th>
<th>Author</th>
<th>Posts</th>
<th>Views</th>
<th>Last reply</th>
</tr>
# for row in Rows(db, query):
<tr>
<td class="topic">${UrlButton(c, XMLencode(%name), actionShow, %threadId)}</td>
#let authorName = getValue(db, sql("select name from person where id = " &
# "(select author from post where id = " &
# "(select min(id) from post where thread = ?))"), %threadId)
<td class="author">${authorName}</td>
# let posts = GetValue(db, sql"select count(*) from post where thread = ?", %threadId)
<td class="posts">$posts</td>
<td class="views">${XMLencode(%views)}</td>
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
# "(select author from post where id = " &
# "(select max(id) from post where thread = ?))"), %threadId)
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " &
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId)
<td class="lastreply">
<span>${formatTimestamp(latestReplyDate.parseInt())}</span><br/>
<span>${latestReplyAuthor}</span>
</td>
</tr>
# end for
</table>
#end proc
#
#
#proc genPostPreview(c: var TForumData,
# title, content, author, date: string): string =
# result = ""
<table class="post">
<tr>
<th colspan="2">
<span>${XMLEncode(title)}</span>
<span style="float:right;">${XMLencode(date)}</span>
</th>
</tr>
<tr>
<td class="left">
<span>${XMLencode(author)}</span>
</td>
<td class="content">
#try:
${content.rstToHtml({roSupportSmilies})}
#except ERecoverableError:
# c.errorMsg = getCurrentExceptionMsg()
#end
</td>
</tr>
</table>
#end proc
#
#
#proc genPostsList(c: var TForumData, threadId: string): string =
# const query = sql"select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, person u where u.id = p.author and p.thread = ? order by p.id"
# const postId = 0
# const userName = 1
# const postHeader = 2
# const postContent = 3
# const postCreation = 4
# const postAuthor = 5
# const userEmail = 6
# result = ""
# for row in FastRows(db, query, threadId):
<table class="post">
<tr>
<th colspan="2">
<span>${XMLencode(%postHeader)}</span>
<span style="float:right;">${XMLencode(%postCreation)}</span>
</th>
</tr>
<tr>
<td class="left">
<span>${XMLencode(%userName)}</span>
<hr/>
${genGravatar(%userEmail)}
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/>${UrlButton(c, "Edit post", actionEditForm, %postId)}
#elif c.isAdmin and c.currentPost.subject.len == 0:
<hr/><span style="color:red">
${UrlButton(c, "Edit post", actionEditForm, %postId)}</span>
#end if
</td>
<td class="content">
${(%postContent).rstToHtml({roSupportSmilies})}
</td>
</tr>
</table>
# end for
#end proc
#
#
#proc genFormPost(c: var TForumData, action: TForumAction,
# isEdit: bool, title, content: string): string =
# result = ""
<br />
<a name="reply"></a>
<div id="replywrapper">
<div id="replytop">
<span>Reply</span>
</div>
<form action="$postAction" method="POST">
${FieldValid(c, "subject", "Subject:")}
${TextWidget(c, "subject", title, maxlength=100)}
<br />
${FieldValid(c, "content", "Content:")}<br />
${TextAreaWidget(c, "content", content, width=100, height=20)}<br />
${FormSession(c, action)}
# if isEdit:
<input type="checkbox" name="delete" value="Delete">Delete Post<br />
# end if
<br/>
<input type="submit" name="previewBtn" value="Preview" />
<input type="submit" name="postBtn" value="Post" />
<a href="http://nimrod-code.org/rst.html">Syntax Cheatsheet</a>
</form>
</div>
#end proc
#
#
#proc genFormRegister(c: var TForumData): string =
# result = ""
<form action="$postAction" method="POST">
<b>Register</b><br />
<table border="0">
<tr>
<td>${FieldValid(c, "name", "Username:")}</td>
<td>${TextWidget(c, "name", reuseText, maxlength=20)}</td>
</tr>
<tr>
<td>${FieldValid(c, "new_password", "Password:")}</td>
<td><input type="password" name="new_password" maxlength="20" /></td>
</tr>
<tr>
<td>${FieldValid(c, "email", "E-Mail:")}</td>
<td>${TextWidget(c, "email", reuseText, maxlength=30)}</td>
</tr>
<tr>
<td>${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td>
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td>
</tr>
</table>
${FormSession(c, actionRegister)}
<input type="submit" value="Register">
</form>
#end proc
#
#proc genFormLogin(c: var TForumData): string =
# result = ""
# if not c.loggedIn:
<form action="$postAction" method="POST">
<table border="0">
<tr><td>Username:</td><td>
<input type="text" name="name" maxlength="20"></td></tr>
<tr><td>Password:</td><td>
<input type="password" name="password" maxlength="20"></td></tr>
</table>
${FormSession(c, actionLogin)}
<input type="submit" value="Login">
</form>
<span style="color:red">$c.loginErrorMsg</span>
# else:
<span style="color:red">You're already logged in!</span>
# end if
#end proc

735
forum.nim Normal file
View file

@ -0,0 +1,735 @@
#
#
# The Nimrod Forum
# (c) Copyright 2012 Andreas Rumpf
#
# All rights reserved.
#
import
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
rst, docgen, msgs, captchas, sockets, scgi, cookies
const
unselectedThread = -1
transientThread = 0
websiteLoc = "/"
postAction = "/"
type
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
threadid: int
postid: int
userName, userPass, email: string
isAdmin: bool
TPost = tuple[subject, content: string]
TForumData = object of TSession
action: TForumAction
cgiData: PStringTable
ip: string
userid: string
actionContent: string
errorMsg, loginErrorMsg: string
invalidField: string
currentPost: TPost
reqUrl: string
startTime: float
cookieData: PStringTable
TStyledButton = tuple[text: string, action: TForumAction, tid: 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
db: TDbConn
proc init(c: var TForumData) =
c.userPass = ""
c.userName = ""
c.threadId = unselectedThread
c.postId = -1
c.action = actionShow
c.ip = ""
c.userid = ""
c.actionContent = ""
c.errorMsg = ""
c.loginErrorMsg = ""
c.invalidField = ""
c.currentPost = (subject: "", content: "")
proc loggedIn(c: TForumData): bool =
result = c.userName.len > 0
# --------------- HTML widgets ------------------------------------------------
# for widgets "" means the empty string as usual; should the old value be
# used again, pass `reuseText` instead:
const
reuseText = "\1"
proc TextWidget(c: TForumData, name, defaultText: string,
maxlength = 30, size = -1): string =
let x = if defaultText != reuseText: defaultText
else: XMLencode(c.cgiData[name])
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""]
proc TextAreaWidget(c: TForumData, name, defaultText: string,
width = 80, height = 20): string =
let x = if defaultText != reuseText: defaultText
else: XMLencode(c.cgiData[name])
return """<textarea name="$1" cols="$2" rows="$3">$4</textarea>""" % [
name, $width, $height, x]
proc FieldValid(c: TForumData, name, text: string): string =
if name == c.invalidField:
result = """<span style="color:red">$1</span>""" % text
else:
result = text
proc genQuery(c: var TForumData, action: TForumAction, target: string): string =
result = websiteLoc
case action
of actionShow:
if target != "":
result.add("t/" & target)
of actionReplyForm:
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 =
return """<input type="hidden" name="action" value="$1" />
<input type="hidden" name="threadid" value="$2" />
<input type="hidden" name="postid" value="$3" />""" % [
$nextAction, $c.threadId, $c.postid]
proc UrlButton(c: var TForumData, text: string,
nextAction: TForumAction, target=""): string =
return ("""<a class="url_button" href="$1">$2</a>""") % [
c.genQuery(nextAction, target), text]
proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string =
result = ""
if btns.len == 1:
var anchor = ""
if btns[0].action == actionReplyForm:
anchor = "#reply"
result = ("""<a class="active button" href="$1$3">$2</a>""") % [
c.genQuery(btns[0].action, btns[0].tid), btns[0].text, anchor]
else:
for i, btn in pairs(btns):
var anchor = ""
if btns[i].action == actionReplyForm:
anchor = "#reply"
var class = ""
if i == 0: class = "left "
elif i == btns.len()-1: class = "right "
else: class = "middle "
result.add(("""<a class="$3active button" href="$1$4">$2</a>""") % [
c.genQuery(btns[i].action, btns[i].tid), 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 =
let t2 = getGMTime(TTime(t))
result = ""
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 =
let emailMD5 = email.toLower.toMD5
result = "<img src=\"$1\" />" %
("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size &
"&d=identicon")
proc randomSalt(): string =
result = ""
for i in 0..127:
var r = random(225)
if r >= 32 and r <= 126:
result.add(chr(random(225)))
proc devRandomSalt(): string =
when defined(posix):
result = ""
var f = open("/dev/urandom")
var randomBytes: array[0..127, char]
discard f.readBuffer(addr(randomBytes), 128)
for i in 0..127:
if ord(randomBytes[i]) >= 32 and ord(randomBytes[i]) <= 126:
result.add(randomBytes[i])
f.close()
else:
result = randomSalt()
proc makeSalt(): string =
## Creates a salt using a cryptographically secure random number generator.
try:
result = devRandomSalt()
except EIO:
result = randomSalt()
proc makePassword(password, salt: string): string =
## Creates an MD5 hash by combining password and salt.
result = getMD5(salt & getMD5(password))
# -----------------------------------------------------------------------------
template `||`(x: expr): expr = (if not isNil(x): x else: "")
proc validThreadId(c: TForumData): bool =
result = GetValue(db, sql"select id from thread where id = ?",
$c.threadId).len > 0
proc antibot(c: var TForumData): string =
let a = math.random(10)+1
let b = math.random(1000)+1
let answer = $(a+b)
Exec(db, sql"delete from antibot where ip = ?", c.ip)
let captureId = TryInsertID(db,
sql"insert into antibot(ip, answer) values (?, ?)", c.ip,
answer).int mod 10_000
let captureFile = "captchas/captcha_" & $captureId & ".png"
createCapture(captureFile, $a & "+" & $b)
result = """<img src="$1" />""" % captureFile
const
SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'}
proc setError(c: var TForumData, field, msg: string): bool {.inline.} =
c.invalidField = field
c.errorMsg = "Error: " & msg
return false
proc register(c: var TForumData): 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:
if name.len == 0 or not allCharsInSet(name, SecureChars):
return setError(c, "name", "Invalid username!")
if GetValue(db, sql"select name from person where name = ?", name).len > 0:
return setError(c, "name", "Username already exists!")
# Password validation:
if pass.len < 4:
return setError(c, "new_password", "Invalid password!")
# antibot validation:
let correctRes = GetValue(db,
sql"select answer from antibot where ip = ?", c.ip)
if antibot != correctRes:
return setError(c, "antibot", "You seem to be a bot!")
# email validation
if not validEmailAddress(email):
return setError(c, "email", "Invalid email address")
# perform registration:
var salt = makeSalt()
Exec(db, sql("INSERT INTO person(name, password, email, salt, status) " &
"VALUES (?, ?, ?, ?, 'user')"), name,
makePassword(pass, salt), email, salt)
# return setError(c, "", "Could not create your account!")
return true
proc checkLoggedIn(c: var TForumData) =
let pass = c.cookieData["sid"]
if pass.len == 0: return
if ExecAffectedRows(db,
sql("update session set lastModified = DATETIME('now') " &
"where ip = ? and password = ?"),
c.ip, pass) > 0:
c.userpass = pass
c.userid = GetValue(db,
sql"select userid from session where ip = ? and password = ?",
c.ip, pass)
let row = getRow(db,
sql"select name, email, admin from person where id = ?", c.userid)
c.username = ||row[0]
c.email = ||row[1]
c.isAdmin = parseBool(||row[2])
else:
echo("not found login")
proc logout(c: var TForumData) =
const query = sql"delete from session where ip = ? and password = ?"
c.username = ""
c.userpass = ""
Exec(db, query, c.ip, c.cookieData["sid"])
proc incrementViews(c: var TForumData) =
const query = sql"update thread set views = views + 1 where id = ?"
Exec(db, query, $c.threadId)
proc isPreview(c: TForumData): bool =
result = c.cgiData["previewBtn"].len > 0
proc isDelete(c: TForumData): bool =
result = c.cgiData["delete"].len > 0
proc validateRst(c: var TForumData, content: string): bool =
result = true
try:
discard content.rstToHtml({roSupportSmilies})
except ERecoverableError:
result = setError(c, "", getCurrentExceptionMsg())
proc crud(c: TCrud, table: string, data: openArray[string]): TSqlQuery =
case c
of crCreate:
var fields = "insert into " & table & "("
var vals = ""
for i, d in data:
if i > 0:
fields.add(", ")
vals.add(", ")
fields.add(d)
vals.add('?')
result = sql(fields & ") values (" & vals & ")")
of crRead:
var res = "select "
for i, d in data:
if i > 0: res.add(", ")
res.add(d)
result = sql(res & " from " & table)
of crUpdate:
var res = "update " & table & " set "
for i, d in data:
if i > 0: res.add(", ")
res.add(d)
res.add(" = ?")
result = sql(res & " where id = ?")
of crDelete:
result = sql("delete from " & table & " where id = ?")
template retrSubject(c: expr) =
let subject = c.cgiData["subject"]
if subject.len < 3: return setError(c, "subject", "Subject not long enough")
template retrContent(c: expr) =
let content = c.cgiData["content"]
if not validateRst(c, content): return false
template retrPost(c: expr) =
retrSubject(c)
retrContent(c)
template checkLogin(c: expr) =
if not loggedIn(c): return setError(c, "", "User is not logged in")
template checkOwnership(c, postId: expr) =
if not c.isAdmin:
let x = getValue(db, sql"select author from post where id = ?",
postId)
if x != c.userId:
return setError(c, "", "You are not the owner of this post")
template setPreviewData(c: expr) =
c.currentPost.subject = subject
c.currentPost.content = content
template writeToDb(c, cr, postId: expr) =
exec(db, crud(cr, "post", "author", "ip", "header", "content", "thread"),
c.userId, c.ip, subject, content, $c.threadId, postId)
proc edit(c: var TForumData, postId: int): bool =
checkLogin(c)
if c.isPreview:
retrPost(c)
setPreviewData(c)
elif c.isDelete:
checkOwnership(c, $postId)
if not TryExec(db, crud(crDelete, "post"), $postId):
return setError(c, "", "database error")
# delete corresponding thread:
if ExecAffectedRows(db,
sql"delete from thread where id not in (select thread from post)") > 0:
# whole thread has been deleted, so:
c.threadId = unselectedThread
result = true
else:
checkOwnership(c, $postId)
retrPost(c)
exec(db, crud(crUpdate, "post", "header", "content"),
subject, content, $postId)
result = true
proc reply(c: var TForumData): bool =
checkLogin(c)
retrPost(c)
if c.isPreview:
setPreviewData(c)
else:
writeToDb(c, crCreate, "")
exec(db, sql"update thread set modified = DATETIME('now') where id = ?",
$c.threadId)
result = true
proc newThread(c: var TForumData): bool =
const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))"
checkLogin(c)
retrPost(c)
if c.isPreview:
setPreviewData(c)
c.threadID = transientThread
else:
c.threadID = TryInsertID(db, query, c.cgiData["subject"]).int
if c.threadID < 0: return setError(c, "subject", "Subject already exists")
writeToDb(c, crCreate, "")
result = true
proc login(c: var TForumData, name, pass: string): bool =
# get form data:
const query =
sql"select id, name, password, email, salt, admin from person where name = ?"
if name.len == 0:
return c.setError("name", "Username cannot be nil.")
var success = false
for row in FastRows(db, query, name):
if row[2] == makePassword(pass, row[4]):
c.userid = row[0]
c.username = row[1]
c.userpass = row[2]
c.email = row[3]
c.isAdmin = row[5].parseBool
success = true
break
if success:
# create session:
Exec(db,
sql"insert into session (ip, password, userid) values (?, ?, ?)",
c.ip, c.userpass, c.userid)
return true
else:
return c.setError("password", "Login failed!")
proc genActionMenu(c: var TForumData): string =
result = ""
var btns: seq[TStyledButton] = @[]
if c.threadId >= 0:
btns.add(("Thread List", actionShow, ""))
if c.loggedIn:
if c.threadId >= 0:
btns.add(("Reply", actionReplyForm, $c.threadId))
btns.add(("New Thread", actionNewThreadForm, ""))
result = c.genButtons(btns)
include "forms.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 =
result = if s.len == 0:
""
elif s.startswith("Re:"): s
else: "Re: " & s
proc dispatch(c: var TForumData): tuple[status, content: string,
headers: PStringTable] =
template `@`(x: expr): expr = c.cgiData[x]
template redirect(): stmt =
result[0] = "303 See Other"
let q = c.genQuery(actionShow, $c.threadId)
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 =
## 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:
var slashes = path.split('/')
case slashes[0]
of "t", "thread":
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,
headers: PStringTable] =
try:
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
echo(c.action)
result = dispatch(c)
except:
result[0] = "500 Internal Server Error"
result[1] = "Internal Error: " & getCurrentExceptionMsg()
result[2] = newStringTable()
proc extractDirFile(s: string): tuple[dir, file: string] =
var last = s.len-1
while last > 0 and s[last] == '/': dec last
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,
contentType, content: string] =
try:
let url = r.vars["DOCUMENT_URI"].normalizeDocURI
let (dir, file) = extractDirFile(url)
case dir
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 ----------------------------------------------
var s: TScgiState
proc shutdown() {.noconv.} =
s.close()
writeStackTrace()
quit 1
system.setControlCHook(shutdown)
when not defined(writeStatusContent):
proc writeStatusContent(c: TSocket, status, content: string,
headers: PStringTable) =
var strHeaders = ""
for key, value in headers:
strHeaders.add(key & ": " & value & "\r\L")
c.send("Status: " & status & "\r\L" & strHeaders & "\r\L")
c.send(content)
proc main() =
docgen.setupConfig()
math.randomize()
db = Open(connection="nimforum.db", user="postgres", password="",
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
# 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()
close(s)
close(db)
proc mainWrapper() =
for i in 0..10:
try:
main()
except:
echo "FATAL: ", getCurrentExceptionMsg()
mainWrapper()

46
main.tmpl Normal file
View file

@ -0,0 +1,46 @@
#! stdtmpl
#proc genMain(c: var TForumData, content: string): string =
# result = ""
<!doctype html>
<html lang="en">
<head>
<title>Nimrod Forum</title>
<link rel="stylesheet" href="${c.genSlash}css/normalize.css">
<link rel="stylesheet" href="${c.genSlash}css/style.css">
</head>
<body>
<div id="wrapper">
<div id="nimbtn">
<a href="http://nimrod-code.org/">Homepage</a>
</div>
<div id="header">
#let frontQuery = c.genQuery(actionShow, "")
<span><a href="${frontQuery}">Nimrod's Forum</a></span>
#if c.loggedIn:
<a href="${websiteLoc}logout" class="right">Logout</a>
<span id="welcome">$c.username</span>
${genGravatar(c.email, 26)}
#else:
<a href="${websiteLoc}register" class="right">Register</a>
<a href="${websiteLoc}login" class="right">Login</a>
#end if
</div>
<div id="topbar">
${c.genActionMenu}
</div>
<div id="content">
$content
<span style="color:red">$c.errorMsg</span>
</div>
<div id="topbar">
${c.genActionMenu}
</div>
</div>
<div id="footer">
<p>Written in Nimrod. Generated in ${int((epochTime()-c.startTime)*1000.0)}ms.</p>
</div>
</body>
</html>

11
nimrod.cfg Normal file
View file

@ -0,0 +1,11 @@
# we need the documentation generator of the compiler:
--path:"/home/nimrod/Nimrod"
--path:"/home/nimrod/Nimrod/compiler"
--path:"/home/dominik/code/outside/git/Nimrod"
--path:"/home/dominik/code/outside/git/Nimrod/compiler"
--path:"../nimrod"
--path:"../nimrod/compiler"

200
rst.txt Normal file
View file

@ -0,0 +1,200 @@
===========================================================================
reStructuredText cheat sheet
===========================================================================
This is a cheat sheet for the *reStructuredText* dialect as implemented by
Nimrod's documentation generator which has been reused for this forum. :-)
See also the
`official RST cheat sheet <http://docutils.sourceforge.net/docs/user/rst/cheatsheet.txt>`_
or the `quick reference <http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_
for further information.
Inline elements
===============
Ordinary text may contain *inline elements*:
=============================== ============================================
Plain text Result
=============================== ============================================
``*italic text*`` *italic text*
``**bold text**`` **bold text**
``***italic bold text***`` ***italic bold text***
\``verbatim text \`` ``verbatim text``
``http://docutils.sf.net/`` http://docutils.sf.net/
``\\escape`` \\escape
=============================== ============================================
Links
=====
Links are either direct URLs like ``http://nimrod-code.org`` or written like
this::
`Nimrod <http://nimrod-code.org>`_
Code blocks
===========
are done this way::
.. code-block:: nimrod
if x == "abc":
echo "xyz"
Is rendered as:
.. code-block:: nimrod
if x == "abc":
echo "xyz"
Except Nimrod, the programming languages C, C++, Java and C# have highlighting
support.
Literal blocks
==============
Are introduced by '::' and a newline. The block is indicated by indentation:
::
::
if x == "abc":
echo "xyz"
Is rendered as::
if x == "abc":
echo "xyz"
Bullet lists
============
look like this::
* Item 1
* Item 2 that
spans over multiple lines
* Item 3
* Item 4
- bullet lists may nest
- item 3b
- valid bullet characters are ``+``, ``*`` and ``-``
Is rendered as:
* Item 1
* Item 2 that
spans over multiple lines
* Item 3
* Item 4
- bullet lists may nest
- item 3b
- valid bullet characters are ``+``, ``*`` and ``-``
Enumerated lists
================
are written like this::
1. This is the first item
2. This is the second item
3. Enumerators are arabic numbers,
single letters, or roman numerals
#. This item is auto-enumerated
Is rendered as:
1. This is the first item
2. This is the second item
3. Enumerators are arabic numbers,
single letters, or roman numerals
#. This item is auto-enumerated
Quoting someone
===============
quotes are just::
**Someone said**: Indented paragraphs,
and they may nest.
Is rendered as:
**Someone said**: Indented paragraphs,
and they may nest.
Definition lists
================
are written like this::
what
Definition lists associate a term with
a definition.
how
The term is a one-line phrase, and the
definition is one or more paragraphs or
body elements, indented relative to the
term. Blank lines are not allowed
between term and definition.
and look like:
what
Definition lists associate a term with
a definition.
how
The term is a one-line phrase, and the
definition is one or more paragraphs or
body elements, indented relative to the
term. Blank lines are not allowed
between term and definition.
Tables
======
Only *simple tables* are supported. They are of the form::
================== =============== ===================
header 1 header 2 header n
================== =============== ===================
Cell 1 Cell 2 Cell 3
Cell 4 Cell 5; any Cell 6
cell that is
not in column 1
may span over
multiple lines
Cell 7 Cell 8 Cell 9
================== =============== ===================
This results in:
================== =============== ===================
header 1 header 2 header n
================== =============== ===================
Cell 1 Cell 2 Cell 3
Cell 4 Cell 5; any Cell 6
cell that is
not in column 1
may span over
multiple lines
Cell 7 Cell 8 Cell 9
================== =============== ===================

14
todo.txt Normal file
View file

@ -0,0 +1,14 @@
Version 1.0
-----------
- even better spam avoidance
- Implement automated testing.
- Implement an "edit account" feature.
- Implement a "who is online" view.
Version 2.0
-----------
- Some email notification/RSS feed system would be nice.