From 2cd10b6968fc26e519244ff33bed8f69d86b74b3 Mon Sep 17 00:00:00 2001 From: Robin Malley Date: Sun, 20 Dec 2020 08:16:23 +0000 Subject: [PATCH] Started work on refactor Started work on moveing each endpoint into it's own file. --- src/lua/cache.lua | 93 +++++++++++++++++++ src/lua/claim_get.lua | 16 ++++ src/lua/claim_post.lua | 66 +++++++++++++ src/lua/config.lua | 5 + src/lua/db.lua | 48 ++++++++++ src/lua/edit_get.lua | 64 +++++++++++++ src/lua/edit_post.lua | 85 +++++++++++++++++ src/lua/index_get.lua | 110 ++++++++++++++++++++++ src/lua/init.lua | 149 ++++++++++++++++++++---------- src/lua/login_get.lua | 11 +++ src/lua/login_post.lua | 58 ++++++++++++ src/lua/pages.lua | 25 +++++ src/lua/parsers.lua | 7 ++ src/lua/paste_get.lua | 81 ++++++++++++++++ src/lua/preview_post.lua | 32 +++++++ src/lua/queries.lua | 13 +++ src/lua/read_get.lua | 140 ++++++++++++++++++++++++++++ src/lua/read_post.lua | 53 +++++++++++ src/lua/render.lua | 2 + src/lua/session.lua | 67 ++++++++++++++ src/lua/tags.lua | 54 +++++++++++ src/lua/util.lua | 103 +++++++++++++++++++++ src/pages/author_index.etlua | 78 ++++++++-------- src/smr.c | 14 ++- src/sql/create_table_posts.sql | 9 ++ src/sql/create_table_raw_text.sql | 5 + src/sql/create_table_session.sql | 5 + src/sql/insert_anon_author.sql | 4 + src/sql/insert_comment.sql | 1 + src/sql/select_author_index.sql | 2 + src/sql/select_author_of_post.sql | 4 + src/sql/select_comments.sql | 1 + src/sql/select_post.sql | 4 + src/sql/select_site_index.sql | 1 + src/sql/update_views.sql | 1 + 35 files changed, 1319 insertions(+), 92 deletions(-) create mode 100644 src/lua/cache.lua create mode 100644 src/lua/claim_get.lua create mode 100644 src/lua/claim_post.lua create mode 100644 src/lua/config.lua create mode 100644 src/lua/db.lua create mode 100644 src/lua/edit_get.lua create mode 100644 src/lua/edit_post.lua create mode 100644 src/lua/index_get.lua create mode 100644 src/lua/login_get.lua create mode 100644 src/lua/login_post.lua create mode 100644 src/lua/pages.lua create mode 100644 src/lua/parsers.lua create mode 100644 src/lua/paste_get.lua create mode 100644 src/lua/preview_post.lua create mode 100644 src/lua/queries.lua create mode 100644 src/lua/read_get.lua create mode 100644 src/lua/read_post.lua create mode 100644 src/lua/render.lua create mode 100644 src/lua/session.lua create mode 100644 src/lua/tags.lua create mode 100644 src/lua/util.lua diff --git a/src/lua/cache.lua b/src/lua/cache.lua new file mode 100644 index 0000000..6290cef --- /dev/null +++ b/src/lua/cache.lua @@ -0,0 +1,93 @@ +local sql = require("lsqlite3") + +local queries = require("queries") +local util = require("util") + +local ret = {} + +local stmnt_cache, stmnt_insert_cache + +local oldconfigure = configure +function configure(...) + local cache = sqlassert(sql.open_memory()) + --A cache table to store rendered pages that do not need to be + --rerendered. In theory this could OOM the program eventually and start + --swapping to disk. TODO: fixme + assert(cache:exec([[ + CREATE TABLE IF NOT EXISTS cache ( + path TEXT PRIMARY KEY, + data BLOB, + updated INTEGER, + dirty INTEGER + ); + ]])) + stmnt_cache = cache:prepare([[ + SELECT data + FROM cache + WHERE + path = :path AND + ((dirty = 0) OR (strftime('%s','now') - updated) < 20) + ; + ]]) + stmnt_insert_cache = cache:prepare([[ + INSERT OR REPLACE INTO cache ( + path, data, updated, dirty + ) VALUES ( + :path, :data, strftime('%s','now'), 0 + ); + ]]) + stmnt_dirty_cache = cache:prepare([[ + UPDATE OR IGNORE cache + SET dirty = 1 + WHERE path = :path; + ]]) + return oldconfigure(...) +end + +--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache() +function ret.render(pagename,callback) + print("Running render...") + stmnt_cache:bind_names{path=pagename} + local err = util.do_sql(stmnt_cache) + if err == sql.DONE then + stmnt_cache:reset() + --page is not cached + elseif err == sql.ROW then + print("Cache hit:" .. pagename) + data = stmnt_cache:get_values() + stmnt_cache:reset() + return data[1] + else --sql.ERROR or sql.MISUSE + error("Failed to check cache for page " .. pagename) + end + --We didn't have the paged cached, render it + print("Cache miss, running function") + local text = callback() + print("Saving data...") + --And save the data back into the cache + stmnt_insert_cache:bind_names{ + path=pagename, + data=text, + } + err = util.do_sql(stmnt_insert_cache) + if err == sql.ERROR or err == sql.MISUSE then + error("Failed to update cache for page " .. pagename) + end + stmnt_insert_cache:reset() + return text +end + +function ret.dirty(url) + print("Dirtying cache:",url) + stmnt_dirty_cache:bind_names{ + path = url + } + err = util.do_sql(stmnt_dirty_cache) + stmnt_dirty_cache:reset() +end + +function ret.close() + +end + +return ret diff --git a/src/lua/claim_get.lua b/src/lua/claim_get.lua new file mode 100644 index 0000000..cd13079 --- /dev/null +++ b/src/lua/claim_get.lua @@ -0,0 +1,16 @@ +local cache = require("cache") +local config = require("config") +local pages = require("pages") + + +local function claim_get(req) + --Get the page to claim a name + local cachestr = string.format("%s/_claim",config.domain) + local text = cache.render(cachestr,function() + print("cache miss, rendering claim page") + return pages.claim{err=""} + end) + http_response(req,200,text) +end + +return claim_get diff --git a/src/lua/claim_post.lua b/src/lua/claim_post.lua new file mode 100644 index 0000000..a950fe8 --- /dev/null +++ b/src/lua/claim_post.lua @@ -0,0 +1,66 @@ +local sql = require("lsqlite3") + +local pages = require("pages") +local db = require("db") +local queries = require("queries") + +local stmnt_author_create + +local oldconfigure = configure +function configure(...) + + stmnt_author_create = assert(db.conn:prepare(queries.insert_author)) + return oldconfigure(...) +end + +local function claim_post(req) + --Actually claim a name + http_request_populate_post(req) + local name = assert(http_argument_get_string(req,"user")) + local text + --What in the world, Kore should be rejecting names that + --are not lower case & no symbols, but some still get through somehow. + if not name:match("^[a-z0-9]*$") then + print("Bad username:",name) + text = pages.claim{ + err = "Usernames must match ^[a-z0-9]{1,30}$" + } + http_response(req,200,text) + return + end + local rngf = assert(io.open("/dev/urandom","rb")) + local passlength = string.byte(rngf:read(1)) + 64 + local salt = rngf:read(64) + local password = rngf:read(passlength) + rngf:close() + local hash = sha3(salt .. password) + stmnt_author_create:bind_names{ + name = name, + } + stmnt_author_create:bind_blob(2,salt) + stmnt_author_create:bind_blob(3,hash) + local err = do_sql(stmnt_author_create) + if err == sql.DONE then + --We sucessfully made athe new author + local id = stmnt_author_create:last_insert_rowid() + stmnt_author_create:reset() + --Give them a file back + http_response_header(req,"Content-Type","application/octet-stream") + http_response_header(req,"Content-Disposition","attachment; filename=\"" .. name .. "." .. domain .. ".passfile\"") + local session = start_session(id) + text = password + elseif err == sql.CONSTRAINT then + --If the creation failed, they probably just tried + --to use a name that was already taken + text = pages.claim { + err = "Failed to claim. That name may already be taken." + } + elseif err == sql.ERROR or err == sql.MISUSE then + --This is bad though + text = pages.claim { + err = "Failed to claim" + } + end + stmnt_author_create:reset() +end +return claim_post diff --git a/src/lua/config.lua b/src/lua/config.lua new file mode 100644 index 0000000..804038f --- /dev/null +++ b/src/lua/config.lua @@ -0,0 +1,5 @@ + +return { + domain = "test.monster:8888", + production = false, +} diff --git a/src/lua/db.lua b/src/lua/db.lua new file mode 100644 index 0000000..f8dee8c --- /dev/null +++ b/src/lua/db.lua @@ -0,0 +1,48 @@ + +local sql = require("lsqlite3") + +local queries = require("queries") + +local db = {} +local oldconfigure = configure +function configure(...) + db.conn = sqlassert(sql.open("data/posts.db")) + + --Create sql tables + assert(db.conn:exec(queries.create_table_authors)) + --Create a fake "anonymous" user, so we don't run into trouble + --so that no one runs into trouble being able to paste under this account. + assert(db.conn:exec(queries.insert_anon_author)) + --If/when an author delets their account, all posts + --and comments by that author are also deleted (on + --delete cascade) this is intentional. This also + --means that all comments by other users on a post + --an author makes will also be deleted. + -- + --Post text uses zlib compression + assert(db.conn:exec(queries.create_table_posts)) + --Store the raw text so people can download it later, maybe + --we can use it for "download as image" or "download as pdf" + --in the future too. Stil stored zlib compressed + assert(db.conn:exec(queries.create_table_raw_text)) + --Maybe we want to store images one day? + assert(db.conn:exec(queries.create_table_images)) + --Comments on a post + assert(db.conn:exec(queries.create_table_comments)) + --Tags for a post + assert(db.conn:exec(queries.create_table_tags)) + --Index for tags + assert(db.conn:exec(queries.create_index_tags)) + --Store a cookie for logged in users. Logged in users can edit + --their own posts, and edit their biographies. + assert(db:exec(queries.create_table_session)) + print("Created db tables") + + return configure(...) +end + +function db.close() + db.conn:close() +end + +return db diff --git a/src/lua/edit_get.lua b/src/lua/edit_get.lua new file mode 100644 index 0000000..600e0e1 --- /dev/null +++ b/src/lua/edit_get.lua @@ -0,0 +1,64 @@ +local zlib = require("zlib") +local sql = require("lsqlite3") + +local db = require("db") +local queries = require("queries") +local util = require("util") +local pages = require("pages") +local tags = require("tags") + +local stmnt_edit +local oldconfigure = configure +function configure(...) + stmnt_edit = assert(db.conn:prepare(queries.select_edit)) + return configure(...) +end + +local function edit_get(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + local author, authorid = get_session(req) + + http_request_populate_qs(req) + local story = assert(http_argument_get_string(req,"story")) + local story_id = decode_id(story) + local ret + + print("we want to edit story:",story) + --Check that the logged in user is the owner of the story + --sql-side. If we're not the owner, we'll get 0 rows back. + stmnt_edit:bind_names{ + postid = story_id, + authorid = author_id + } + local err = util.do_sql(stmnt_edit) + if err == sql.DONE then + --No rows, we're probably not the owner (it might + --also be because there's no such story) + ret = pages.cantedit{ + path = story, + } + stmnt_edit:reset() + http_response(req,200,ret) + return + end + assert(err == sql.ROW) + local data = stmnt_edit:get_values() + local txt_compressed, markup, isanon, title = unpack(data) + local text = zlib.decompress(txt_compressed) + local tags = tags.get(story_id) + local tags_txt = table.concat(tags,";") + stmnt_edit:reset() + ret = pages.edit{ + title = title, + text = text, + markup = markup, + user = author, + isanon = isanon == 1, + domain = domain, + story = story_id, + err = "", + tags = tags_txt + } + http_response(req,200,ret) +end diff --git a/src/lua/edit_post.lua b/src/lua/edit_post.lua new file mode 100644 index 0000000..7bb093e --- /dev/null +++ b/src/lua/edit_post.lua @@ -0,0 +1,85 @@ +local sql = require("lsqlite3") +local zlib = require("zlib") + +local queries = require("queries") +local pages = require("pages") +local parsers = require("parsers") +local util = require("util") +local tagslib = require("tags") +local cache = require("cache") +local config = require("config") + +local stmnt_author_of, stmnt_update_raw, stmnt_update + +local oldconfigure = configure +function configure(...) + stmnt_author_of = assert(db.conn:prepare(queries.select_author_of_post)) + stmnt_update_raw = assert(db.conn:prepare(queries.update_raw)) + stmnt_update = assert(db.conn:prepare(queries.update_post)) + return oldconfigure(...) +end + +local function edit_post(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + local author, author_id = get_session(req) + + http_request_populate_post(req) + local storyid = tonumber(assert(http_argument_get_string(req,"story"))) + local title = assert(http_argument_get_string(req,"title")) + local text = assert(http_argument_get_string(req,"text")) + local pasteas = assert(http_argument_get_string(req,"pasteas")) + local markup = assert(http_argument_get_string(req,"markup")) + local tags_str = http_argument_get_string(req,"tags") + stmnt_author_of:bind_names{ + id = storyid + } + local err = do_sql(stmnt_author_of) + if err ~= sql.ROW then + stmnt_author_of:reset() + error("No author found for story:" .. storyid) + end + local data = stmnt_author_of:get_values() + stmnt_author_of:reset() + local realauthor = data[1] + assert(realauthor == author_id) --Make sure the author of the story is the currently logged in user + local parsed = parsers[markup](text) + local compr_raw = zlib.compress(text) + local compr = zlib.compress(parsed) + local tags = {} + if tags_str then + tags = util.parse_tags(tags_str) + end + assert(stmnt_update_raw:bind_blob(1,compr_raw) == sql.OK) + assert(stmnt_update_raw:bind(2,markup) == sql.OK) + assert(stmnt_update_raw:bind(3,storyid) == sql.OK) + assert(util.do_sql(stmnt_update_raw) == sql.DONE, "Failed to update raw") + stmnt_update_raw:reset() + assert(stmnt_update:bind(1,title) == sql.OK) + assert(stmnt_update:bind_blob(2,compr) == sql.OK) + assert(stmnt_update:bind(3,pasteas == "anonymous" and 1 or 0) == sql.OK) + assert(stmnt_update:bind(4,storyid) == sql.OK) + assert(do_sql(stmnt_update) == sql.DONE, "Failed to update text") + stmnt_update:reset() + tagslib.set(storyid,tags) + --[[ + assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK) + do_sql(stmnt_drop_tags) + stmnt_drop_tags:reset() + for _,tag in pairs(tags) do + print("Looking at tag",tag) + assert(stmnt_ins_tag:bind(1,storyid) == sql.OK) + assert(stmnt_ins_tag:bind(2,tag) == sql.OK) + err = do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end + ]] + local id_enc = util.encode_id(storyid) + local loc = string.format("https://%s/%s",config.domain,id_enc) + cache.dirty(string.format("%s/%s",config.domain,id_enc)) -- This place to read this post + cache.dirty(string.format("%s",config.domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that). + cache.dirty(string.format("%s.%s",author,config.domain)) -- The author's index, same reasoning as above. + http_response_header(req,"Location",loc) + http_response(req,303,"") + return +end diff --git a/src/lua/index_get.lua b/src/lua/index_get.lua new file mode 100644 index 0000000..7e65fe0 --- /dev/null +++ b/src/lua/index_get.lua @@ -0,0 +1,110 @@ +local sql = require("lsqlite3") + +local cache = require("cache") +local queries = require("queries") +local db = require("db") +local util = require("util") +local config = require("config") +local pages = require("pages") + +local stmnt_index, stmnt_author, stmnt_author_bio + +local oldconfigure = configure +function configure(...) + stmnt_index = assert(db.conn:prepare(queries.select_site_index)) + --TODO: actually let authors edit their bio + stmnt_author_bio = assert(db.conn:prepare([[ + SELECT authors.biography FROM authors WHERE authors.name = :author; + ]])) + stmnt_author = assert(db.conn:prepare(queries.select_author_index)) + return configure(...) +end + +local function get_site_home(req) + print("Cache miss, rendering index") + stmnt_index:bind_names{} + local err = util.do_sql(stmnt_index) + local latest = {} + --err may be sql.ROW or sql.DONE if we don't have any stories yet + while err == sql.ROW do + local data = stmnt_index:get_values() + local storytags = tags.get(data[1]) + table.insert(latest,{ + url = encode_id(data[1]), + title = data[2], + isanon = data[3] == 1, + posted = os.date("%B %d %Y",tonumber(data[4])), + author = data[5], + tags = storytags, + }) + err = stmnt_index:step() + end + stmnt_index:reset() + return pages.index{ + domain = config.domain, + stories = latest + } +end +local function get_author_home(req) + local host = http_request_get_host(req) + local subdomain = host:match("([^\\.]+)") + stmnt_author_bio:bind_names{author=subdomain} + local err = do_sql(stmnt_author_bio) + if err == sql.DONE then + print("No such author") + stmnt_author_bio:reset() + return pages.noauthor{ + author = subdomain + } + end + print("err:",err) + assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err)) + local data = stmnt_author_bio:get_values() + local bio = data[1] + stmnt_author_bio:reset() + print("Getting author's stories") + stmnt_author:bind_names{author=subdomain} + err = do_sql(stmnt_author) + print("err:",err) + local stories = {} + while err == sql.ROW do + local data = stmnt_author:get_values() + local id, title, time = unpack(data) + local tags = get_tags(id) + table.insert(stories,{ + url = encode_id(id), + title = title, + posted = os.date("%B %d %Y",tonumber(time)), + tags = tags, + }) + err = stmnt_author:step() + end + stmnt_author:reset() + return pages.author_index{ + domain=config.domain, + author=subdomain, + stories=stories, + bio=bio + } + +end + +local function index_get(req) + local method = http_method_text(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + --Default home page + local subdomain = host:match("([^\\.]+)") + local text + if host == config.domain then + local cachepath = string.format("%s",config.domain), + text = cache.render(cachepath, function() + return get_site_home(req) + end) + else --author home page + local cachepath = string.format("%s.%s",subdomain,config.domain) + text = cache.render(cachepath, function() + return get_author_home(req) + end) + end +end diff --git a/src/lua/init.lua b/src/lua/init.lua index 2a971f7..9bb7161 100644 --- a/src/lua/init.lua +++ b/src/lua/init.lua @@ -1,19 +1,34 @@ + print("Really fast print from init.lua") +--Luarocks libraries local et = require("etlua") local sql = require("lsqlite3") local zlib = require("zlib") -if PRODUCTION then +--smr code +local cache = require("cache") +local pages = require("pages") +local util = require("util") +local config = require("config") +local db = require("db") +if config.production then local function print() end --squash prints end +--[[ local parser_names = {"plain","imageboard"} local parsers = {} for _,v in pairs(parser_names) do parsers[v] = require("parser_" .. v) end -local db,cache --databases -local domain = "test.monster:8888" --The domain to write links as +]] +--pages +read_get = require("read_get") +read_post = require("read_post") +preview_post = require("preview_post") +--local db,cache --databases +--local domain = "test.monster:8888" --The domain to write links as +--[[ local pagenames = { "index", "author_index", @@ -36,7 +51,9 @@ for k,v in pairs(pagenames) do pages[v] = assert(et.compile(f:read("*a"))) f:close() end +]] +--[=[ local queries = {} --These are all loaded during startup, won't affect ongoing performance. setmetatable(queries,{ @@ -47,13 +64,13 @@ setmetatable(queries,{ return ret end }) - +]=] ---sql queries -local stmnt_index, stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw +local --[[stmnt_index,]] stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw local stmnt_update_views local stmnt_ins_tag, stmnt_drop_tags, stmnt_get_tags local stmnt_author_create, stmnt_author_acct, stmnt_author_bio -local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache +--local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache local stmnt_get_session, stmnt_insert_session local stmnt_edit, stmnt_update, stmnt_update_raw, stmnt_author_of local stmnt_comments, stmnt_comment_insert @@ -79,6 +96,7 @@ local function decodeentities(capture) end end +--[[ local function sqlassert(...) local r,errcode,err = ... if not r then @@ -86,6 +104,7 @@ local function sqlassert(...) end return r end +]] local function sqlbind(stmnt,call,position,data) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) @@ -99,22 +118,22 @@ end print("Hello from init.lua") function configure() - db = sqlassert(sql.open("data/posts.db")) - --db = sqlassert(sql.open_memory()) - cache = sqlassert(sql.open_memory()) - print("Compiled pages...") - --Test that compression works + --db = sqlassert(sql.open("data/posts.db")) + ----db = sqlassert(sql.open_memory()) + --cache = sqlassert(sql.open_memory()) + + --Test that compression works. For some reason, the zlib library + --fails if this is done as a one-liner local msg = "test message" local one = zlib.compress(msg) local two = zlib.decompress(one) - --For some reason, the zlib library fails if this is done as a oneliner assert(two == msg, "zlib not working as expected") --Create sql tables - assert(db:exec(queries.create_table_authors)) + --assert(db:exec(queries.create_table_authors)) --Create a fake "anonymous" user, so we don't run into trouble --so that no one runs into touble being able to paste under this account. - assert(db:exec(queries.insert_anon_author)) + --assert(db:exec(queries.insert_anon_author)) --If/when an author delets their account, all posts --and comments by that author are also deleted (on --delete cascade) this is intentional. This also @@ -122,23 +141,24 @@ function configure() --an author makes will also be deleted. -- --Post text uses zlib compression - assert(db:exec(queries.create_table_posts)) + --assert(db:exec(queries.create_table_posts)) --Store the raw text so people can download it later, maybe --we can use it for "download as image" or "download as pdf" --in the future too. Stil stored zlib compressed - assert(db:exec(queries.create_table_raw_text)) - assert(db:exec(queries.create_table_images)) - assert(db:exec(queries.create_table_comments)) - assert(db:exec(queries.create_table_tags)) - assert(db:exec(queries.create_index_tags)) + --assert(db:exec(queries.create_table_raw_text)) + --assert(db:exec(queries.create_table_images)) + --assert(db:exec(queries.create_table_comments)) + --assert(db:exec(queries.create_table_tags)) + --assert(db:exec(queries.create_index_tags)) --Store a cookie for logged in users. Logged in users can edit --their own posts. - assert(db:exec(queries.create_table_session)) - print("Created db tables") + --assert(db:exec(queries.create_table_session)) + --print("Created db tables") --A cache table to store rendered pages that do not need to be --rerendered. In theory this could OOM the program eventually and start --swapping to disk. TODO: fixme + --[=[ assert(cache:exec([[ CREATE TABLE IF NOT EXISTS cache ( path TEXT PRIMARY KEY, @@ -147,62 +167,71 @@ function configure() dirty INTEGER ); ]])) + ]=] --Select the data we need to display the on the front page - stmnt_index = assert(db:prepare(queries.select_site_index)) + --stmnt_index = assert(db:prepare(queries.select_site_index)) --Select the data we need to read a story (and maybe display an edit --button - stmnt_read = assert(db:prepare(queries.select_post)) + --stmnt_read = assert(db:prepare(queries.select_post)) --Update the view counter when someone reads a story - stmnt_update_views = assert(db:prepare(queries.update_views)) + --stmnt_update_views = assert(db:prepare(queries.update_views)) --Retreive comments on a story - stmnt_comments = assert(db:prepare(queries.select_comments)) + --stmnt_comments = assert(db:prepare(queries.select_comments)) --Add a new comment to a story - stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) + --stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) --TODO: actually let authors edit their bio - stmnt_author_bio = assert(db:prepare([[ + --[=[stmnt_author_bio = assert(db.conn:prepare([[ SELECT authors.biography FROM authors WHERE authors.name = :author; ]])) + ]=] --Get the author of a story, used to check when editing that the --author really owns the story they're trying to edit - stmnt_author_of = assert(db:prepare(queries.select_author_of_post)) + --stmnt_author_of = assert(db:prepare(queries.select_author_of_post)) --Get the data we need to display a particular author's latest --stories - stmnt_author = assert(db:prepare(queries.select_author_index)) + --stmnt_author = assert(db:prepare(queries.select_author_index)) --Get the data we need to check if someone can log in + --[=[ stmnt_author_acct = assert(db:prepare([[ SELECT id, salt, passhash FROM authors WHERE name = :name; ]])) + ]=] --Create a new author on the site - stmnt_author_create = assert(db:prepare(queries.insert_author)) + --stmnt_author_create = assert(db:prepare(queries.insert_author)) + --[=[ stmnt_author_login = assert(db:prepare([[ SELECT name, passhash FROM authors WHERE name = :name; ]])) + ]=] --Create a new post stmnt_paste = assert(db:prepare(queries.insert_post)) --Keep a copy of the plain text of a post so we can edit it later --It might also be useful for migrations, if that ever needs to happen stmnt_raw = assert(db:prepare(queries.insert_raw)) --Tags for a story + --[[ stmnt_ins_tag = assert(db:prepare(queries.insert_tag)) stmnt_get_tags = assert(db:prepare(queries.select_tags)) stmnt_drop_tags = assert(db:prepare(queries.delete_tags)) + ]] --Get the data we need to display the edit screen stmnt_edit = assert(db:prepare(queries.select_edit)) --Get the data we need when someone wants to download a paste stmnt_download = assert(db:prepare(queries.select_download)) --When we update a post, store the plaintext again - stmnt_update_raw = assert(db:prepare(queries.update_raw)) + --stmnt_update_raw = assert(db:prepare(queries.update_raw)) --Should we really reset the update time every time someone makes a post? --Someone could keep their story on the front page by just editing it a lot. --If it gets abused I can disable it I guess. - stmnt_update = assert(db:prepare(queries.update_post)) + --stmnt_update = assert(db:prepare(queries.update_post)) --Check sessions for login support stmnt_insert_session = assert(db:prepare(queries.insert_session)) stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) --Search by tag name stmnt_search = assert(db:prepare(queries.select_post_tags)) --only refresh pages at most once every 10 seconds + --[=[ stmnt_cache = cache:prepare([[ SELECT data FROM cache @@ -223,6 +252,7 @@ function configure() SET dirty = 1 WHERE path = :path; ]]) + ]=] --[=[ ]=] print("finished running configure()") @@ -296,6 +326,7 @@ local function get_tags(id) until false end +--[=[ local function dirty_cache(url) print("Dirtying cache:",url) stmnt_dirty_cache:bind_names{ @@ -304,6 +335,7 @@ local function dirty_cache(url) err = do_sql(stmnt_dirty_cache) stmnt_dirty_cache:reset() end +]=] --[[ @@ -356,6 +388,7 @@ local function get_session(req) return author,authorid end +--[=[ --Render a page, with cacheing. If you need to dirty a cache, call dirty_cache() local function render(pagename,callback) print("Running render...") @@ -388,7 +421,9 @@ local function render(pagename,callback) stmnt_insert_cache:reset() return text end +]=] +--[=[ --[[Parses a semicolon seperated string into it's parts, trims whitespace, lowercases, and capitalizes the first letter. Tags will not be empty. Returns an array of tags]] local function parse_tags(str) local tags = {} @@ -404,6 +439,7 @@ local function parse_tags(str) end return tags end +]=] function home(req) print("Hello from lua!") @@ -414,7 +450,7 @@ function home(req) local text if host == domain then --Default home page - text = render(string.format("%s",domain),function() + text = cache.render(string.format("%s",domain),function() print("Cache miss, rendering index") stmnt_index:bind_names{} local err = do_sql(stmnt_index) @@ -442,7 +478,7 @@ function home(req) else --Home page for an author local subdomain = host:match("([^\\.]+)") - text = render(string.format("%s.%s",subdomain,domain),function() + text = cache.render(string.format("%s.%s",subdomain,domain),function() print("Cache miss, rendering author:" .. subdomain) stmnt_author_bio:bind_names{author=subdomain} local err = do_sql(stmnt_author_bio) @@ -505,7 +541,7 @@ function claim(req) local text if method == "GET" then --Get the page to claim a name - text = render(string.format("%s/_claim",domain),function() + text = cache.render(string.format("%s/_claim",domain),function() print("cache miss, rendering claim page") return pages.claim{err=""} end) @@ -578,7 +614,7 @@ function paste(req) return else --For an anonymous user - ret = render(string.format("%s/_paste",host),function() + ret = cache.render(string.format("%s/_paste",host),function() print("Cache missing, rendering post page") return pages.paste{ domain = domain, @@ -686,8 +722,8 @@ function paste(req) local loc = string.format("https://%s/%s",domain,url) http_response_header(req,"Location",loc) http_response(req,303,"") - dirty_cache(string.format("%s/%s",domain,url)) - dirty_cache(string.format("%s",domain)) + cache.dirty(string.format("%s/%s",domain,url)) + cache.dirty(string.format("%s",domain)) return elseif err == sql.ERROR or err == sql.MISUSE then ret = "Failed to paste: " .. tostring(err) @@ -748,9 +784,9 @@ function paste(req) end http_response_header(req,"Location",loc) http_response(req,303,"") - dirty_cache(string.format("%s.%s",author,domain)) - dirty_cache(string.format("%s/%s",domain,url)) - dirty_cache(string.format("%s",domain)) + cache.dirty(string.format("%s.%s",author,domain)) + cache.dirty(string.format("%s/%s",domain,url)) + cache.dirty(string.format("%s",domain)) return elseif err == sql.ERROR or err == sql.MISUSE then ret = "Failed to paste: " .. tostring(err) @@ -778,7 +814,7 @@ local function read_story(host,path,idp,show_comments,iam) } print("update:",do_sql(stmnt_update_views)) stmnt_update_views:reset() - dirty_cache(cachestr) + cache.dirty(cachestr) print("cachestr was:",cachestr) local readstoryf = function() stmnt_read:bind_names{ @@ -830,7 +866,7 @@ local function read_story(host,path,idp,show_comments,iam) --which is not the user's, from whoever loaded the cache last) to fix this bug, don't cache --pages when the user is logged in. All non-logged-in users can see the same page no problem. if not iam then - return render(cachestr,readstoryf) + return cache.render(cachestr,readstoryf) else return readstoryf() end @@ -841,6 +877,8 @@ function read(req) local path = http_request_get_path(req) local method = http_method_text(req) if method == "GET" then + read_get(req) + --[=[ local idp = string.sub(path,2)--remove leading "/" assert(string.len(path) > 0,"Tried to read 0-length story id") local author, authorid = get_session(req) @@ -902,7 +940,10 @@ function read(req) assert(text) http_response(req,200,text) return + ]=] elseif method == "POST" then + read_post(req) + --[=[ --We're posting a comment http_request_populate_post(req) http_populate_cookies(req) @@ -928,11 +969,12 @@ function read(req) http_response(req,500,"Internal error, failed to post comment. Go back and try again.") else --When we post a comment, we need to dirty the cache for the "comments displayed" page. - dirty_cache(string.format("%s%s?comments=1",host,path)) + cache.dirty(string.format("%s%s?comments=1",host,path)) local redir = string.format("https://%s%s?comments=1", domain, path) http_response_header(req,"Location",redir) http_response(req,303,"") end + ]=] end end @@ -949,7 +991,7 @@ function login(req) local text if method == "GET" then --Just give them the login page - text = render(string.format("%s/_login",domain),function() + text = cache.render(string.format("%s/_login",domain),function() return pages.login{ err = "", } @@ -1093,9 +1135,9 @@ function edit(req) end local id_enc = encode_id(storyid) local loc = string.format("https://%s/%s",domain,id_enc) - dirty_cache(string.format("%s/%s",domain,id_enc)) -- This place to read this post - dirty_cache(string.format("%s",domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that). - dirty_cache(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above. + cache.dirty(string.format("%s/%s",domain,id_enc)) -- This place to read this post + cache.dirty(string.format("%s",domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that). + cache.dirty(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above. http_response_header(req,"Location",loc) http_response(req,303,"") return @@ -1112,10 +1154,10 @@ end function teardown() print("Exiting...") if db then - db:close() + db.close() end if cache then - cache:close() + cache.close() end print("Finished lua teardown") end @@ -1127,6 +1169,7 @@ function download(req) http_request_populate_qs(req) local story = assert(http_argument_get_string(req,"story")) local story_id = decode_id(story) + print("Downloading", story_id) stmnt_download:bind_names{ postid = story_id } @@ -1137,6 +1180,7 @@ function download(req) stmnt_download:reset() return end + assert(err == sql.ROW, "after doing download sql, result was not a row, was:" .. tostring(err)) local txt_compressed, title = unpack(stmnt_download:get_values()) local text = zlib.decompress(txt_compressed) stmnt_download:reset() @@ -1147,6 +1191,8 @@ function download(req) end function preview(req) + preview_post(req) + --[[ print("We want to preview a paste!") local host = http_request_get_host(req) local path = http_request_get_path(req) @@ -1170,6 +1216,7 @@ function preview(req) tags = tags, } http_response(req,200,ret) + ]] end function search(req) diff --git a/src/lua/login_get.lua b/src/lua/login_get.lua new file mode 100644 index 0000000..4ad9c90 --- /dev/null +++ b/src/lua/login_get.lua @@ -0,0 +1,11 @@ +local config = require("config") + + +local function login_get(req) + --Just give them the login page + return cache.render(string.format("%s/_login",domain),function() + return pages.login{ + err = "", + } + end) +end diff --git a/src/lua/login_post.lua b/src/lua/login_post.lua new file mode 100644 index 0000000..d8cc902 --- /dev/null +++ b/src/lua/login_post.lua @@ -0,0 +1,58 @@ +local sql = require("lsqlite3") + +local db = require("db") +local util = require("util") + +local stmnt_author_acct + +local oldconfigure = configure +function configure(...) + --Get the data we need to check if someone can log in + stmnt_author_acct = assert(db.conn:prepare([[ + SELECT id, salt, passhash FROM authors WHERE name = :name; + ]])) + + return configure(...) +end + +local function login_post(req) + --Try to log in + http_populate_multipart_form(req) + local name = assert(http_argument_get_string(req,"user")) + local pass = assert(http_file_get(req,"pass")) + stmnt_author_acct:bind_names{ + name = name + } + local text + local err = util.do_sql(stmnt_author_acct) + if err == sql.ROW then + local id, salt, passhash = unpack(stmnt_author_acct:get_values()) + stmnt_author_acct:reset() + local todigest = salt .. pass + local hash = sha3(todigest) + if hash == passhash then + local session = start_session(id) + http_response_cookie(req,"session",session,"/",0,0) + local loc = string.format("https://%s.%s",name,domain) + http_response_header(req,"Location",loc) + http_response(req,303,"") + return + else + text = pages.login{ + err = "Incorrect username or password" + } + end + elseif err == sql.DONE then --Allows user enumeration, do we want this? + --Probably not a problem since all passwords are forced to be "good" + stmnt_author_acct:reset() + text = pages.login{ + err = "Failed to find user:" .. name + } + else + stmnt_author_acct:reset() + error("Other sql error during login") + end + http_response(req,200,text) +end + +return login_post diff --git a/src/lua/pages.lua b/src/lua/pages.lua new file mode 100644 index 0000000..e492cad --- /dev/null +++ b/src/lua/pages.lua @@ -0,0 +1,25 @@ +local et = require("etlua") +local pagenames = { + "index", + "author_index", + "claim", + "paste", + "edit", + "read", + "nostory", + "cantedit", + "noauthor", + "login", + "author_paste", + "author_edit", + "search", +} +local pages = {} +for k,v in pairs(pagenames) do + print("Compiling page:",v) + local f = assert(io.open("pages/" .. v .. ".etlua","r")) + pages[v] = assert(et.compile(f:read("*a"))) + f:close() +end + +return pages diff --git a/src/lua/parsers.lua b/src/lua/parsers.lua new file mode 100644 index 0000000..1f679c9 --- /dev/null +++ b/src/lua/parsers.lua @@ -0,0 +1,7 @@ + +local parser_names = {"plain","imageboard"} +local parsers = {} +for _,v in pairs(parser_names) do + parsers[v] = require("parser_" .. v) +end +return parsers diff --git a/src/lua/paste_get.lua b/src/lua/paste_get.lua new file mode 100644 index 0000000..440f722 --- /dev/null +++ b/src/lua/paste_get.lua @@ -0,0 +1,81 @@ +local config = require("config") + +local function paste_get(req) + --Get the paste page + local host = http_request_get_host(req) + local text + local author,_ = get_session(req) + if host == config.domain and author then + http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,config.domain)) + http_response(req,303,"") + return + elseif host == config.domain and author == nil then + text = cache.render(string.format("%s/_paste",host),function() + print("Cache missing, rendering post page") + return pages.paste{ + domain = domain, + err = "", + } + end) + http_response(req,200,text) + elseif host ~= config.domain and author then + + elseif host ~= config.domain and author == nil then + + else + error(string.format( + "Unable to find a good case for paste:%s,%s,%s", + host, + config.domain, + author + )) + end + if host == config.domain then + local author,_ = get_session(req) + if author then + http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,domain)) + http_response(req,303,"") + return + else + --For an anonymous user + ret = cache.render(string.format("%s/_paste",host),function() + print("Cache missing, rendering post page") + return pages.paste{ + domain = domain, + err = "", + } + end) + end + + else + --Or for someone that's logged in + print("Looks like a logged in user wants to paste!") + local subdomain = host:match("([^%.]+)") + local author,_ = get_session(req) + print("subdomain:",subdomain,"author:",author) + --If they try to paste as an author, but are on the + --wrong subdomain, or or not logged in, redirect them + --to the right place. Their own subdomain for authors + --or the anonymous paste page for not logged in users. + if author == nil then + http_response_header(req,"Location","https://"..domain.."/_paste") + http_response(req,303,"") + return + end + if author ~= subdomain then + http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,domain)) + http_response(req,303,"") + return + end + assert(author == subdomain,"someone wants to paste as someone else") + --We're where we want to be, serve up this users's + --paste page. No cache, because how often is a user + --going to paste? + ret = pages.author_paste{ + domain = domain, + user = author, + text = "", + err = "", + } + end +end diff --git a/src/lua/preview_post.lua b/src/lua/preview_post.lua new file mode 100644 index 0000000..b7886be --- /dev/null +++ b/src/lua/preview_post.lua @@ -0,0 +1,32 @@ +local parsers = require("parsers") +local tags = require("tags") +local util = require("util") +local pages = require("pages") + +local function preview_post(req) + print("We want to preview a paste!") + local host = http_request_get_host(req) + local path = http_request_get_path(req) + http_request_populate_post(req) + local title = assert(http_argument_get_string(req,"title")) + local text = assert(http_argument_get_string(req,"text")) + local markup = assert(http_argument_get_string(req,"markup")) + local tag_str = http_argument_get_string(req,"tags") + local tags = {} + if tag_str then + tags = util.parse_tags(tag_str) + end + print("title:",title,"text:",text,"markup:",markup) + local parsed = parsers[markup](text) + local ret = pages.read{ + domain = domain, + title = title, + author = "preview", + idp = "preview", + text = parsed, + tags = tags, + } + http_response(req,200,ret) +end + +return preview_post diff --git a/src/lua/queries.lua b/src/lua/queries.lua new file mode 100644 index 0000000..3d4e500 --- /dev/null +++ b/src/lua/queries.lua @@ -0,0 +1,13 @@ + +local queries = {} + +setmetatable(queries,{ + __index = function(self,key) + local f = assert(io.open("sql/" .. key .. ".sql","r")) + local ret = f:read("*a") + f:close() + return ret + end +}) + +return queries diff --git a/src/lua/read_get.lua b/src/lua/read_get.lua new file mode 100644 index 0000000..2fbed09 --- /dev/null +++ b/src/lua/read_get.lua @@ -0,0 +1,140 @@ +local sql = require("sqlite3") + +local session = require("session") +local tags = require("tags") +local db = require("db") +local queries = require("queries") + +local stmnt_read, stmnt_update_views, stmnt_comments + +local oldconfigure = configure +function configure(...) + stmnt_read = assert(db.conn:prepare(queries.select_post)) + stmnt_update_views = assert(db.conn:prepare(queries.update_views)) + stmnt_comments = assert(db.conn:prepare(queries.select_comments)) + return configure(...) +end + + +--[[ +Increases a story's hit counter by 1 +]] +local function add_view(storyid) + stmnt_update_views:bind_names{ + id = storyid + } + local err = do_sql(stmnt_update_views) + assert(err == sql.DONE, "Failed to update view counter:"..tostring(err)) + stmnt_update_views:reset() +end + +--[[ +Populates ps with story settings, returns true if story was found, +or nil if it wasn't +]] +local function populate_ps_story(req,ps) + --Make sure our story exists + stmnt_read:bind_names{ + id = ps.storyid + } + local err = do_sql(stmnt_read) + if err == sql.DONE then + --We got no story + stmnt_read:reset() + return nil + end + --If we've made it here, we have a story. Populate our settings + --with title, text, ect. + assert(err == sql.ROW) + add_view(storyid) + local title, storytext, tauthor, isanon, authorname, views = unpack( + stmnt_read:get_values() + ) + ps.title = title + ps.storytext = zlib.decompress(storytext) + ps.tauthor = tauthor + ps.isanon = isanon == 1 + ps.authorname = authorname + ps.views = views + stmnt_read:reset() + --Tags + ps.tags = tags.get(id) + return true +end + +--[[ +Get the comments for a story +]] +local function get_comments(req,ps) + +end + +--[[ +The author is viewing their own story, give them an edit button +]] +local function read_get_author(req,storyid,author,authorid,comments) + +end + +--[[ +An author is viewing a story, allow them to post comments as themselves +]] +local function read_get_loggedin(req,ps) + if ps.tauthor == ps.authorid then + --The story exists and we're logged in as the + --owner, display the edit button + return read_get_author(req,ps) + end + return pages.read(ps) +end + + +local function read_get(req) + --Pages settings + local ps = { + host = http_request_get_host(req), + path = http_request_get_path(req), + method = http_method_text(req), + } + + --Get our story id + assert(string.len(ps.path) > 0,"Tried to read 0-length story id") + local idp = string.sub(ps.path,2)--remove leading "/" + ps.storyid = util.decode_id(idp) + + --If we're logged in, set author and authorid + local author, authorid = session.get(req) + if author and authorid then + ps.author = author + ps.authorid = authorid + end + + --If we need to show comments + http_request_populate_qs(req) + ps.show_comments = http_argument_get_string(req,"comments") + if ps.show_comments then + ps.comments = get_comments(req,ps) + end + + --normal story display + if (not ps.author) then + local cachestr = string.format("%s%s%s", + ps.host, + ps.path, + ps.show_comments and "?comments=1" or "" + ) + local text = cache.render(cachestr,function() + populate_ps_story(req,ps) + return pages.read(ps) + end) + else --we are logged in, don't cache + populate_ps_story(req,ps) + ps.owner = (ps.author == ps.tauthor) + text = pages.read(ps) + end + assert(text) + http_response(req,200,text) + return +end + +return read_get diff --git a/src/lua/read_post.lua b/src/lua/read_post.lua new file mode 100644 index 0000000..81907da --- /dev/null +++ b/src/lua/read_post.lua @@ -0,0 +1,53 @@ +local sql = require("sqlite3") + +local cache = require("cache") +local session = require("session") +local util = require("util") +local db = require("db") +local queries = require("queries") +local config = require("config") + +local stmnt_comment_insert + +local oldconfigure = configure +function configure(...) + stmnt_comment_insert = assert(db.conn:prepare(queries.insert_comment)) + return oldconfigure(...) +end + +local function read_post(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + --We're posting a comment + http_request_populate_post(req) + http_populate_cookies(req) + local author, authorid = session.get(req) + local comment_text = assert(http_argument_get_string(req,"text")) + local pasteas = assert(http_argument_get_string(req,"postas")) + local idp = string.sub(path,2)--remove leading "/" + local id = util.decode_id(idp) + local isanon = 1 + --Even if an author is logged in, they may post their comment anonymously + if author and pasteas ~= "Anonymous" then + isanon = 0 + end + stmnt_comment_insert:bind_names{ + postid=id, + authorid = author and authorid or -1, + isanon = isanon, + comment_text = comment_text, + } + local err = util.do_sql(stmnt_comment_insert) + stmnt_comment_insert:reset() + if err ~= sql.DONE then + http_response(req,500,"Internal error, failed to post comment. Go back and try again.") + else + --When we post a comment, we need to dirty the cache for the "comments displayed" page. + cache.dirty(string.format("%s%s?comments=1",host,path)) + local redir = string.format("https://%s%s?comments=1", domain, path) + http_response_header(req,"Location",redir) + http_response(req,303,"") + end + +end +return read_post diff --git a/src/lua/render.lua b/src/lua/render.lua new file mode 100644 index 0000000..139597f --- /dev/null +++ b/src/lua/render.lua @@ -0,0 +1,2 @@ + + diff --git a/src/lua/session.lua b/src/lua/session.lua new file mode 100644 index 0000000..7912d01 --- /dev/null +++ b/src/lua/session.lua @@ -0,0 +1,67 @@ +local sql = require("lsqlite3") + +local db = require("db") +local util = require("util") + +local oldconfigure = configure +local stmnt_get_session, stmnt_insert_session +function configure(...) + stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) + + return oldconfigure(...) +end + +local session = {} + +--[[ +Retreive the name and authorid of the logged in person, +or nil+error message if not logged in +]] +function session.get(req) + http_populate_cookies(req) + local sessionid = http_request_cookie(req,"session") + if sessionid == nil then + return nil, "No session cookie passed by client" + end + stmnt_get_session:bind_names{ + key = sessionid + } + local err = util.do_sql(stmnt_get_session) + if err ~= sql.ROW then + return nil, "No such session by logged in users" + end + print("get session err:",err) + local data = stmnt_get_session:get_values() + stmnt_get_session:reset() + local author = data[1] + local authorid = data[2] + return author,authorid +end + +--[[ +Start a session for someone who logged in +]] +local function start_session(who) + local rngf = assert(io.open("/dev/urandom","rb")) + local session_t = {} + for i = 1,64 do + local r = string.byte(rngf:read(1)) + local s = string.char((r % 26) + 65) + table.insert(session_t,s) + end + local session = table.concat(session_t) + rngf:close() + print("sessionid:",session) + print("authorid:",who) + stmnt_insert_session:bind_names{ + sessionid = session, + authorid = who + } + local err = util.do_sql(stmnt_insert_session) + stmnt_insert_session:reset() + print("Err:",err) + assert(err == sql.DONE) + return session +end + +return session diff --git a/src/lua/tags.lua b/src/lua/tags.lua new file mode 100644 index 0000000..9612c66 --- /dev/null +++ b/src/lua/tags.lua @@ -0,0 +1,54 @@ +local db = require("db") +local queries = require("queries") +local util = require("util") +local tags = {} + +local stmnt_get_tags, stmnt_ins_tag, stmnt_drop_tags + +local oldconfigure = configure +function configure(...) + --Tags for a story + stmnt_ins_tag = assert(db.conn:prepare(queries.insert_tag)) + stmnt_get_tags = assert(db.conn:prepare(queries.select_tags)) + stmnt_drop_tags = assert(db.conn:prepare(queries.delete_tags)) + + return configure(...) +end + + +function tags.get(id) + local ret = {} + stmnt_get_tags:bind_names{ + id = id + } + local err + repeat + err = stmnt_get_tags:step() + if err == sql.BUSY then + coroutine.yield() + elseif err == sql.ROW then + table.insert(ret,stmnt_get_tags:get_value(0)) + elseif err == sql.DONE then + stmnt_get_tags:reset() + return ret + else + error(string.format("Failed to get tags for story %d : %d", id, err)) + end + until false +end + +function tags.set(storyid,tags) + assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK) + util.do_sql(stmnt_drop_tags) + stmnt_drop_tags:reset() + for _,tag in pairs(tags) do + print("Looking at tag",tag) + assert(stmnt_ins_tag:bind(1,storyid) == sql.OK) + assert(stmnt_ins_tag:bind(2,tag) == sql.OK) + err = util.do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end + +end + +return tags diff --git a/src/lua/util.lua b/src/lua/util.lua new file mode 100644 index 0000000..a798141 --- /dev/null +++ b/src/lua/util.lua @@ -0,0 +1,103 @@ + +local util = {} + +--[[ +Runs an sql query and receives the 3 arguments back, prints a nice error +message on fail, and returns true on success. +]] +function util.sqlassert(...) + local r,errcode,err = ... + if not r then + error(string.format("%d: %s",errcode, err)) + end + return r +end + +--[[ +Continuously tries to perform an sql statement until it goes through +]] +function util.do_sql(stmnt) + if not stmnt then error("No statement",2) end + local err + repeat + err = stmnt:step() + print("After stepping, err is", err) + if err == sql.BUSY then + coroutine.yield() + end + until(err ~= sql.BUSY) + return err +end + +--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/ +--no underscore because we use that for our operative pages +local url_characters = + [[abcdefghijklmnopqrstuvwxyz]].. + [[ABCDEFGHIJKLMNOPQRSTUVWXYZ]].. + [[0123456789]].. + [[$-+!*'(),]] +local url_characters_rev = {} +for i = 1,string.len(url_characters) do + url_characters_rev[string.sub(url_characters,i,i)] = i +end +--[[ +Encode a number to a shorter HTML-safe url path +]] +function util.encode_id(number) + local result = {} + local charlen = string.len(url_characters) + repeat + local pos = (number % charlen) + 1 + number = math.floor(number / charlen) + table.insert(result,string.sub(url_characters,pos,pos)) + until number == 0 + return table.concat(result) +end + +--[[ +Given a short HTML-safe url path, convert it to a storyid +]] +function util.decode_id(s) + local res, id = pcall(function() + local n = 0 + local charlen = string.len(url_characters) + for i = 1,string.len(s) do + local char = string.sub(s,i,i) + local pos = url_characters_rev[char] - 1 + n = n + (pos*math.pow(charlen,i-1)) + end + return n + end) + if res then + return id + else + error("Failed to decode id:" .. s) + end +end + +--[[ +Parses a semicolon seperated string into it's parts: +1. seperates by semicolon +2. trims whitespace +3. lowercases +4. capitalizes the first letter. +Returns an array of zero or more strings. +There is no blank tag, parsing "one;two;;three" will yield +{"one","two","three"} +]] +function util.parse_tags(str) + local tags = {} + for tag in string.gmatch(str,"([^;]+)") do + assert(tag, "Found a nil or false tag in:" .. str) + local tag_trimmed = string.match(tag,"%s*(.*)%s*") + local tag_lower = string.lower(tag_trimmed) + local tag_capitalized = string.gsub(tag_lower,"^.",string.upper) + assert(tag_capitalized, "After processing tag:" .. tag .. " it was falsey.") + if string.len(tag_capitalized) > 0 then + table.insert(tags, tag_capitalized) + end + end + return tags +end + +return util diff --git a/src/pages/author_index.etlua b/src/pages/author_index.etlua index 8654483..98b8f76 100644 --- a/src/pages/author_index.etlua +++ b/src/pages/author_index.etlua @@ -1,51 +1,49 @@ -<% assert(author,"No author specified") %> -<% assert(bio,"No bio included") %> - - - - - +<% assert(author,"No author specified") %> <% assert(bio,"No bio included") %> 🍑 -

- <%= author %>.<%= domain %> -

- -
- <%= bio %> -
-
- <% if #stories == 0 then %> - This author has not made any pastes yet. - <% else %> - - <% for k,v in pairs(stories) do %> - +
+

+ <%= author %>.<%= domain %> +

+
+ New paste +
+
+ <%= bio %> +
+
+ <% if #stories == 0 then %> + This author has not made any pastes yet. + <% else %> +
- - <%- v.title %> - - - By <%= author %> - - - - <%= v.posted %> -
+ <% for k,v in pairs(stories) do %> + + <% end %> +
+ + <%- v.title %> + + + By <%= author %> + + + + <%= v.posted %> +
<% end %> - - <% end %> -
+ - + diff --git a/src/smr.c b/src/smr.c index 6030602..3e14a5c 100644 --- a/src/smr.c +++ b/src/smr.c @@ -12,6 +12,7 @@ #include "libkore.h" #include "libcrypto.h" #include +#include int home(struct http_request *); int post_story(struct http_request *); @@ -38,6 +39,15 @@ lua_State *L; static / _claim claim */ +/*Allow seccomp things for luajit and sqlite*/ +KORE_SECCOMP_FILTER("app", + KORE_SYSCALL_ALLOW(pread64), + KORE_SYSCALL_ALLOW(pwrite64), + KORE_SYSCALL_ALLOW(fdatasync), + KORE_SYSCALL_ALLOW(unlinkat), + KORE_SYSCALL_ALLOW(mremap) +); + int errhandeler(lua_State *L){ printf("Error: %s\n",lua_tostring(L,1)); @@ -66,8 +76,10 @@ do_lua(struct http_request *req, const char *name){ printf("About to pcall\n"); int err = lua_pcall(L,1,0,-3); if(err != LUA_OK){ + size_t retlen; + const char *ret = lua_tolstring(L,-1,&retlen); printf("Failed to run %s: %s\n",name,lua_tostring(L,-1)); - http_response(req, 500, NULL, 0); + http_response(req, 500, ret, retlen); lua_pop(L,lua_gettop(L)); return (KORE_RESULT_OK); } diff --git a/src/sql/create_table_posts.sql b/src/sql/create_table_posts.sql index 76cc858..1f6f5dc 100644 --- a/src/sql/create_table_posts.sql +++ b/src/sql/create_table_posts.sql @@ -1,3 +1,12 @@ +/* +If/when an author delets their account, all posts +and comments by that author are also deleted (on +delete cascade) this is intentional. This also +means that all comments by other users on a post +an author makes will also be deleted. + +Post text uses zlib compression +*/ CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, post_text BLOB, diff --git a/src/sql/create_table_raw_text.sql b/src/sql/create_table_raw_text.sql index a424f9a..5ca8c51 100644 --- a/src/sql/create_table_raw_text.sql +++ b/src/sql/create_table_raw_text.sql @@ -1,3 +1,8 @@ +/* +Store the raw text so people can download it later, maybe +we can use it for "download as image" or "download as pdf" +in the future too. Stil stored zlib compressed +*/ CREATE TABLE IF NOT EXISTS raw_text ( id INTEGER PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE, post_text BLOB, diff --git a/src/sql/create_table_session.sql b/src/sql/create_table_session.sql index 28e5f11..e4f4c8c 100644 --- a/src/sql/create_table_session.sql +++ b/src/sql/create_table_session.sql @@ -1,3 +1,8 @@ +/* +Store a cookie for logged in users. Logged in users can edit +their own posts. +*/ + CREATE TABLE IF NOT EXISTS sessions ( key TEXT PRIMARY KEY, author REFERENCES authors(id) ON DELETE CASCADE, diff --git a/src/sql/insert_anon_author.sql b/src/sql/insert_anon_author.sql index b2ff0e6..0036f83 100644 --- a/src/sql/insert_anon_author.sql +++ b/src/sql/insert_anon_author.sql @@ -1,3 +1,7 @@ +/* +Create a fake "anonymous" user, so +that no one runs into touble being able to paste under this account. +*/ INSERT OR IGNORE INTO authors ( id, name, diff --git a/src/sql/insert_comment.sql b/src/sql/insert_comment.sql index 0adf012..3986b31 100644 --- a/src/sql/insert_comment.sql +++ b/src/sql/insert_comment.sql @@ -1,3 +1,4 @@ +/* Add a new comment to a story */ INSERT INTO comments( postid, author, diff --git a/src/sql/select_author_index.sql b/src/sql/select_author_index.sql index ac536f8..319da48 100644 --- a/src/sql/select_author_index.sql +++ b/src/sql/select_author_index.sql @@ -1,3 +1,5 @@ +/* Get the data we need to display a particular author's latest stories */ + SELECT posts.id, posts.post_title, diff --git a/src/sql/select_author_of_post.sql b/src/sql/select_author_of_post.sql index 8527b90..e875211 100644 --- a/src/sql/select_author_of_post.sql +++ b/src/sql/select_author_of_post.sql @@ -1,3 +1,7 @@ +/* +Get the author of a story, used to check when editing that the +author really owns the story they're trying to edit +*/ SELECT authors.id, authors.name diff --git a/src/sql/select_comments.sql b/src/sql/select_comments.sql index 05b5419..d147f79 100644 --- a/src/sql/select_comments.sql +++ b/src/sql/select_comments.sql @@ -1,3 +1,4 @@ +/* Retreive comments on a story */ SELECT authors.name, comments.isanon, diff --git a/src/sql/select_post.sql b/src/sql/select_post.sql index c614596..9e36fdc 100644 --- a/src/sql/select_post.sql +++ b/src/sql/select_post.sql @@ -1,3 +1,7 @@ +/* +Select the data we need to read a story (and maybe display an edit button) +*/ + SELECT post_title, post_text, diff --git a/src/sql/select_site_index.sql b/src/sql/select_site_index.sql index 024b692..99d8abc 100644 --- a/src/sql/select_site_index.sql +++ b/src/sql/select_site_index.sql @@ -1,3 +1,4 @@ +/* Select the data we need to display the on the front page */ SELECT posts.id, posts.post_title, diff --git a/src/sql/update_views.sql b/src/sql/update_views.sql index 3af22f7..685f6fe 100644 --- a/src/sql/update_views.sql +++ b/src/sql/update_views.sql @@ -1 +1,2 @@ +/* Update the view counter when someone reads a story */ UPDATE posts SET views = views + 1 WHERE id = :id;