all relevant files added
This commit is contained in:
parent
fc2484712d
commit
6476dc7cbb
9 changed files with 1328 additions and 1 deletions
|
|
@ -1,4 +1,11 @@
|
||||||
nimforum
|
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
36
captchas.nim
Normal 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
91
createdb.nim
Normal 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
187
forms.tmpl
Normal 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
735
forum.nim
Normal 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
46
main.tmpl
Normal 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
11
nimrod.cfg
Normal 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
200
rst.txt
Normal 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
14
todo.txt
Normal 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.
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue