print("Really fast print from init.lua") local et = require("etlua") local sql = require("lsqlite3") local zlib = require("zlib") if 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 local pagenames = { "index", "author_index", "claim", "paste", "edit", "read", "nostory", "cantedit", "noauthor", "login", "author_paste", "author_edit", } 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 local queries = {} --These are all loaded during startup, won't affect ongoing performance. 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 }) ---sql queries local stmnt_index, stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw local stmnt_author_create, stmnt_author_acct, stmnt_author_bio 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 --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 local function decodeentities(capture) local n = tonumber(capture,16) local c = string.char(n) if escapes[c] then return escapes[c] else return c end end local function sqlassert(...) local r,errcode,err = ... if not r then error(string.format("%d: %s",errcode, err)) end return r end local function sqlbind(stmnt,call,position,data) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) local f = stmnt[call](stmnt,position,data) if f ~= sql.OK then error(string.format("Failed to %s at %d with %q: %s", call, position, data, db:errmsg()),2) end 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 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)) --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)) --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: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)) --TODO assert(db:exec(queries.create_table_comments)) --TODO assert(db:exec(queries.create_table_tags)) --TODO --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") --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 ); ]])) --Select the data we need to display the on the front page 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_comments = assert(db:prepare(queries.select_comments)) stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) --TODO: actually let authors edit their bio stmnt_author_bio = assert(db: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)) --Get the data we need to display a particular author's latest --stories 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_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)) --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)) --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_insert_session = assert(db:prepare(queries.insert_session)) stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) --only refresh pages at most once every 10 seconds stmnt_cache = cache:prepare([[ SELECT data FROM cache WHERE path = :path AND ((dirty = 0) OR (strftime('%s','now') - updated) < 2) ; ]]) 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; ]]) --[=[ ]=] print("finished running configure()") end print("Created configure function") --[[ find a string url for a number ]] local function 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 local function 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 local function 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 local function dirty_cache(url) print("Dirtying cache:",url) stmnt_dirty_cache:bind_names{ path = url } err = do_sql(stmnt_dirty_cache) stmnt_dirty_cache:reset() 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 = do_sql(stmnt_insert_session) stmnt_insert_session:reset() print("Err:",err) assert(err == sql.DONE) return session end --[[ Retreive the name and authorid of the logged in person, or nil+error message if not logged in ]] local function get_session(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 = 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 --Render a page, with cacheing. If you need to dirty a cache, call dirty_cache() local function render(pagename,callback) print("Running render...") stmnt_cache:bind_names{path=pagename} local err = 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 = 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 home(req) print("Hello from lua!") print("Method:", http_method_text(req)) local method = http_method_text(req) local host = http_request_get_host(req) local path = http_request_get_path(req) local text if host == domain then --Default home page text = render(string.format("%s",domain),function() print("Cache miss, rendering index") stmnt_index:bind_names{} local err = 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() 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], }) err = stmnt_index:step() end stmnt_index:reset() return pages.index{ domain = domain, stories = latest } end) else --Home page for an author local subdomain = host:match("([^\\.]+)") text = 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) 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) table.insert(stories,{ url = encode_id(id), title = title, posted = os.date("%B %d %Y",tonumber(time)) }) err = stmnt_author:step() end stmnt_author:reset() return pages.author_index{ domain=domain, author=subdomain, stories=stories, bio=bio } end) end assert(text) http_response(req,200,text) end --We prevent people from changing their password file, this way we don't really --need to worry about logged in accounts being hijacked if someone gets at the --database. The attacker can still paste & edit from the logged in account for --a while, but whatever. function claim(req) local method = http_method_text(req) local host = http_request_get_host(req) local path = http_request_get_path(req) if host ~= domain then http_response_header(req,"Location",string.format("https://%s/_claim",domain)) http_response(req,303,"") return end assert(host == domain) local text if method == "GET" then --Get the page to claim a name text = render(string.format("%s/_claim",domain),function() print("cache miss, rendering claim page") return pages.claim{} end) elseif method == "POST" then --Actually claim a name http_request_populate_post(req) local name = assert(http_argument_get_string(req,"user")) 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 assert(text) http_response(req,200,text) end function paste(req) local host = http_request_get_host(req) local path = http_request_get_path(req) local method = http_method_text(req) local err local ret if method == "GET" then --Get the paste page if host == 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 = 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 elseif method == "POST" then --We're creatinga new paste 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 pasteas local raw = zlib.compress(text) text = string.gsub(text,"%%(%x%x)",decodeentities) text = parsers[markup](text) text = zlib.compress(text) local esctitle = string.gsub(title,"%%(%x%x)",decodeentities) --Always sanatize the title with the plain parser. no markup --in the title. esctitle = parsers.plain(title) if host == domain then --Public paste --[[ This doesn't actually do much for IPv4 addresses, since there are only 32 bits of address. Someone who got a copy of the database could just generate all 2^32 hashes and look up who posted what. Use IPv6, Tor or I2P where possible. (but then I guess it's harder to ban spammers... hmm..) ]] --local ip = http_request_get_ip(req) --local iphash = sha3(ip) --Don't store this information for now, until I come up --with a more elegent solution. sqlbind(stmnt_paste,"bind_blob",1,text) --assert(stmnt_paste:bind_blob(1,text) == sql.OK) sqlbind(stmnt_paste,"bind",2,esctitle) --assert(stmnt_paste:bind(2,esctitle) == sql.OK) sqlbind(stmnt_paste,"bind",3,-1) --assert(stmnt_paste:bind(3,-1) == sql.OK) sqlbind(stmnt_paste,"bind",4,true) --assert(stmnt_paste:bind(4,true) == sql.OK) sqlbind(stmnt_paste,"bind_blob",5,"") --assert(stmnt_paste:bind_blob(5,"") == sql.OK) err = do_sql(stmnt_paste) if err == sql.DONE then local rowid = stmnt_paste:last_insert_rowid() assert(stmnt_raw:bind(1,rowid) == sql.OK) assert(stmnt_raw:bind_blob(2,raw) == sql.OK) err = do_sql(stmnt_raw) if err ~= sql.DONE then print("Failed to save raw text, but paste still went though") end local url = encode_id(rowid) local loc = string.format("https://%s/%s",domain,url) http_response_header(req,"Location",loc) http_response(req,303,"") stmnt_paste:reset() stmnt_raw:reset() dirty_cache(string.format("%s/%s",domain,url)) dirty_cache(string.format("%s",domain)) return elseif err == sql.ERROR or err == sql.MISUSE then ret = "Failed to paste: " .. tostring(err) else error("Error pasting:" .. tostring(err)) end stmnt_paste:reset() else --Author paste local author, authorid = get_session(req) if author == nil then ret = pages.author_paste{ domain = domain, author = subdomain, err = "You are not logged in, you must be logged in to post as " .. subdomain .. ".", text = text } end local asanon = assert(http_argument_get_string(req,"pasteas")) --No need to check if the author is posting to the --"right" sudomain, just post it to the one they have --the session key for. assert(stmnt_paste:bind_blob(1,text) == sql.OK) assert(stmnt_paste:bind(2,esctitle) == sql.OK) assert(stmnt_paste:bind(3,authorid) == sql.OK) if asanon == "anonymous" then assert(stmnt_paste:bind(4,true) == sql.OK) else assert(stmnt_paste:bind(4,false) == sql.OK) end assert(stmnt_paste:bind_blob(5,"") == sql.OK) err = do_sql(stmnt_paste) if err == sql.DONE then local rowid = stmnt_paste:last_insert_rowid() assert(stmnt_raw:bind(1,rowid) == sql.OK) assert(stmnt_raw:bind_blob(2,raw) == sql.OK) assert(stmnt_raw:bind(3,markup) == sql.OK) err = do_sql(stmnt_raw) if err ~= sql.DONE then print("Failed to save raw text, but paste still went through") end local url = encode_id(rowid) local loc if asanon == "anonymous" then loc = string.format("https://%s/%s",domain,url) else loc = string.format("https://%s.%s/%s",author,domain,url) end http_response_header(req,"Location",loc) http_response(req,303,"") stmnt_paste:reset() stmnt_raw:reset() dirty_cache(string.format("%s.%s",author,domain)) dirty_cache(string.format("%s/%s",domain,url)) dirty_cache(string.format("%s",domain)) return elseif err == sql.ERROR or err == sql.MISUSE then ret = "Failed to paste: " .. tostring(err) else error("Error pasting:",err) end stmnt_paste:reset() end end assert(ret) http_response(req,200,ret) end --A helper function for below local function read_story(host,path,idp,show_comments,iam) local cachestr if show_comments then cachestr = string.format("%s%s?comments=1",host,path) else cachestr = string.format("%s%s",host,path) end print("cachestr was:",cachestr) local readstoryf = function() local id = decode_id(idp) stmnt_read:bind_names{ id = id } local err = do_sql(stmnt_read) if err == sql.DONE then stmnt_read:reset() return pages.nostory{ path = path } end assert(err == sql.ROW,"Could not get row:" .. tostring(id) .. " Error:" .. tostring(err)) local title, text, authorid, isanon, authorname = unpack(stmnt_read:get_values()) stmnt_comments:bind_names{ id = id } err = do_sql(stmnt_comments) local comments = {} while err ~= sql.DONE do local com_author, com_isanon, com_text = unpack(stmnt_comments:get_values()) table.insert(comments,{ author = com_author, isanon = com_isanon == 1, --int to boolean text = com_text }) err = stmnt_comments:step() end stmnt_comments:reset() text = zlib.decompress(text) stmnt_read:reset() return pages.read{ domain = domain, title = title, text = text, idp = idp, isanon = isanon == 1, author = authorname, comments = comments, show_comments = show_comments, iam = iam, } end --Don't cache if we're logged in, someone might see dirty cache information on the page. --(I.e. When the user has loaded comments, the form to past a comment may contain a username, --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) else return readstoryf() end end function read(req) local host = http_request_get_host(req) local path = http_request_get_path(req) local method = http_method_text(req) if method == "GET" then 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) http_request_populate_qs(req) local show_comments = http_argument_get_string(req,"comments") --parameters needed for the read page local text if author then --We're logged in as someone local id = decode_id(idp) stmnt_read:bind_names{ id = id } local err = do_sql(stmnt_read) if err == sql.DONE then --We got no story stmnt_read:reset() text = pages.nostory{ path = path } else --If we can edit this story, we don't want to cache --the page, since it'll have an edit button on it. assert(err == sql.ROW) local title, storytext, tauthor, isanon, authorname = unpack(stmnt_read:get_values()) storytext = zlib.decompress(storytext) stmnt_read:reset() if tauthor == authorid then --The story exists and we're logged in as the --owner, display the edit button text = pages.read{ domain = domain, title = title, text = storytext, idp = idp, isanon = isanon == 1, author = authorname, iam = authorname, owner = true } else text = read_story(host,path,idp,show_comments,author) end end else --We're not logged in as anyone http_request_populate_qs(req) text = read_story(host,path,idp,show_comments,author) end assert(text) http_response(req,200,text) return elseif method == "POST" then --We're posting a comment http_request_populate_post(req) http_populate_cookies(req) local author, authorid = get_session(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 = 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 = 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. dirty_cache(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 function login(req) local host = http_request_get_host(req) local path = http_request_get_path(req) local method = http_method_text(req) if host ~= domain then --Don't allow logging into subdomains, I guess http_response_header(req,"Location",string.format("https://%s/_login",domain)) http_response(req,303,"") return end local text if method == "GET" then --Just give them the login page text = render(string.format("%s/_login",domain),function() return pages.login{ err = "", } end) elseif method == "POST" then --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 err = 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 end assert(text) http_response(req,200,text) end --Edit a story function edit(req) local host = http_request_get_host(req) local path = http_request_get_path(req) local method = http_method_text(req) local author, author_id = get_session(req) local ret if method == "GET" then http_request_populate_qs(req) local story = assert(http_argument_get_string(req,"story")) local story_id = decode_id(story) 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 = 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) stmnt_edit:reset() ret = pages.edit{ title = title, text = text, markup = markup, user = author, isanon = isanon == 1, domain = domain, story = story_id, err = "", } elseif method == "POST" then 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")) 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) 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(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() 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. http_response_header(req,"Location",loc) http_response(req,303,"") return end assert(ret) http_response(req,200,ret) end --TODO function edit_bio() print("we want to edit bio") end function teardown() print("Exiting...") if db then db:close() end if cache then cache:close() end print("Finished lua teardown") end function download(req) local host = http_request_get_host(req) local path = http_request_get_path(req) print("host:",host,"path:",path) http_request_populate_qs(req) local story = assert(http_argument_get_string(req,"story")) local story_id = decode_id(story) stmnt_download:bind_names{ postid = story_id } local err = do_sql(stmnt_download) if err == sql.DONE then --No rows, story not found http_responose(req,404,pages.nostory{path=story}) stmnt_download:reset() return end local txt_compressed, title = unpack(stmnt_download:get_values()) local text = zlib.decompress(txt_compressed) stmnt_download:reset() http_response_header(req,"Content-Type","application/octet-stream") local nicetitle = title:gsub("%W","_") http_response_header(req,"Content-Disposition","attachment; filename=\"" .. nicetitle .. ".txt\"") http_response(req,200,text) end function preview(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")) 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, } http_response(req,200,ret) end print("Done with init.lua")