Work on ripping out spp

This commit is contained in:
Robin Malley 2023-05-17 21:18:24 +00:00
parent 60213086dc
commit e430d9b512
42 changed files with 347 additions and 647 deletions

View File

@ -16,12 +16,14 @@ kmgr_chroot = $(smr_var)/kore_kmgr
parent_chroot = $(smr_var)/kore_parent parent_chroot = $(smr_var)/kore_parent
conf_path = /etc/smr conf_path = /etc/smr
smr_bin_path = /usr/local/lib smr_bin_path = /usr/local/lib
app_root=$(worker_chroot)/var/smr
ifeq ($(DEV),true) ifeq ($(DEV),true)
worker_chroot = ./kore_chroot worker_chroot = ./kore_chroot
kmgr_chroot = ./kore_chroot kmgr_chroot = ./kore_chroot
parent_chroot = ./kore_chroot parent_chroot = ./kore_chroot
conf_path = ./kore_chroot/conf conf_path = ./kore_chroot/conf
smr_bin_path = ./kore_chroot smr_bin_path = ./kore_chroot
app_root=./kore_chroot/app
endif endif
mirror=http://dl-cdn.alpinelinux.org/alpine/ mirror=http://dl-cdn.alpinelinux.org/alpine/
@ -35,11 +37,11 @@ domain=test.monster:$(port)
server_cert=/root/cert/server.pem server_cert=/root/cert/server.pem
server_key=/root/cert/key.pem server_key=/root/cert/key.pem
SPPFLAGS=-D port=$(port) -D kore_chroot=$(build_dir) -D chuser=$(user) -D domain=$(domain) -D bin_path="$(bin_path)" -D server_cert="$(server_cert)" -D server_key="$(server_key)" -D worker_chroot="$(worker_chroot)" -D kmgr_chroot="$(kmgr_chroot)" SPPFLAGS=-D port=$(port) -D kore_chroot=$(worker_chroot) -D chuser=$(user) -D domain=$(domain) -D bin_path="$(bin_path)" -D server_cert="$(server_cert)" -D server_key="$(server_key)" -D worker_chroot="$(worker_chroot)" -D kmgr_chroot="$(kmgr_chroot)"
# squelch prints, flip to print verbose information # squelch prints, flip to print verbose information
#Q=@ #Q=@
Q= Q=
LUAROCKS_FLAGS=--tree $(build_dir)/usr/lib/luarocks --lua-version 5.1 LUAROCKS_FLAGS=--tree $(worker_chroot)/usr/lib/luarocks --lua-version 5.1
chroot_packages=\ chroot_packages=\
-p luarocks5.1 \ -p luarocks5.1 \
-p "build-base" \ -p "build-base" \
@ -62,21 +64,18 @@ lua_packages = \
zlib zlib
# Probably don't change stuff past here if you're just using smr # Probably don't change stuff past here if you're just using smr
lua_in_files=$(shell find src/lua/*.in -type f) lua_files=$(shell find src/lua/*.lua -type f) $(shell find src/lua/endpoints -type f)
lua_files=$(shell find src/lua/*.lua -type f) $(shell find src/lua/endpoints -type f) $(lua_in_files:%.in=%)
src_files=$(shell find src -type f) $(shell find conf -type f) src_files=$(shell find src -type f) $(shell find conf -type f)
sql_files=$(shell find src/sql -type f) sql_files=$(shell find src/sql -type f)
test_files=$(shell find spec -type f) test_files=$(shell find spec -type f) $(shell find spec/parser_tests -type f)
built_tests=$(test_files:%=$(build_dir)%) page_files=$(shell find src/pages -type f)
built_files=$(lua_files:src/lua/%.lua=$(build_dir)%.lua) built_tests=$(test_files:%=$(app_root)/%)
in_page_files=$(shell find src/pages/*.in -type f) built_files=$(lua_files:src/lua/%.lua=$(app_root)/%.lua)
page_files=$(in_page_files:%.in=%)
part_files=$(shell find src/pages/parts/*.etlua -type f) part_files=$(shell find src/pages/parts/*.etlua -type f)
built_pages=$(page_files:src/pages/%.etlua=$(build_dir)pages/%.etlua) built_parts=$(part_files:src/%=$(app_root)/%)
built_sql=$(sql_files:src/sql/%.sql=$(build_dir)sql/%.sql) built_pages=$(page_files:src/pages/%.etlua=$(app_root)/pages/%.etlua)
built_sql=$(sql_files:src/sql/%.sql=$(app_root)/sql/%.sql)
built=$(built_files) $(built_sql) $(built_pages) $(built_tests) built=$(built_files) $(built_sql) $(built_pages) $(built_tests)
asset_in_files=$(wildcard assets/*.in -type f)
asset_files=$(asset_in_files:%.in=%)
initscript=/lib/systemd/system/smr.service initscript=/lib/systemd/system/smr.service
config=$(conf_path)/smr.conf config=$(conf_path)/smr.conf
built_bin=$(smr_bin_path)/smr.so built_bin=$(smr_bin_path)/smr.so
@ -88,7 +87,7 @@ apk_hash := $(APK_$(arch)_HASH)
help: ## Print this help help: ## Print this help
$(Q)$(GREP) -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | $(SORT) | $(AWK) 'BEGIN {FS = ":.*?## "}; {printf "%-10s %s\n", $$1, $$2}' $(Q)$(GREP) -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | $(SORT) | $(AWK) 'BEGIN {FS = ":.*?## "}; {printf "%-10s %s\n", $$1, $$2}'
all: $(build_dir) smr.so $(built_files) $(built_pages) $(built_sql) ## Build and run smr in a chroot all: $(app_root) smr.so $(built_files) $(built_pages) $(built_sql) ## Build and run smr in a chroot
apk-tools-static-$(version).apk: apk-tools-static-$(version).apk:
wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk
@ -100,7 +99,7 @@ clean: ## clean up all the files generated by this makefile
$(Q)$(RM) $(asset_files) $(Q)$(RM) $(asset_files)
$(Q)$(RM) smr.so $(Q)$(RM) smr.so
install: $(worker_chroot) $(kmgr_chroot) $(parent_chroot) $(initscript) $(config) smr.so $(built_files) $(built_pages) $(built_sql) ## Install smr into a new host system install: $(app_root) $(kmgr_chroot) $(parent_chroot) $(initscript) $(config) smr.so $(built_files) $(built_pages) $(built_sql) ## Install smr into a new host system
$(Q)$(COPY) smr.so $(built_bin) $(Q)$(COPY) smr.so $(built_bin)
$(config) : conf/smr.conf $(config) : conf/smr.conf
@ -111,15 +110,19 @@ $(initscript) : packaging/systemd/smr.service
$(Q)$(COPY) $< $@ $(Q)$(COPY) $< $@
cloc: ## calculate source lines of code in smr cloc: ## calculate source lines of code in smr
cloc --force-lang="html",etlua.in --force-lang="html",etlua --force-lang="lua",lua.in src assets Makefile cloc --force-lang="html",etlua --force-lang="lua",luasrc assets Makefile
$(build_dir): $(app_root):
$(Q)$(MKDIR) $(build_dir) $(Q)$(MKDIR) $(app_root)
$(Q)$(MKDIR) $(build_dir)/pages
$(Q)$(MKDIR) $(build_dir)/sql $(app_root): $(worker_chroot)
$(Q)$(MKDIR) $(build_dir)/data $(Q)$(MKDIR) $(app_root)
$(Q)$(MKDIR) $(build_dir)/data/archive $(Q)$(MKDIR) $(app_root)/pages
$(Q)$(MKDIR) $(build_dir)/endpoints $(Q)$(MKDIR) $(app_root)/pages/parts
$(Q)$(MKDIR) $(app_root)/sql
$(Q)$(MKDIR) $(app_root)/data
$(Q)$(MKDIR) $(app_root)/data/archive
$(Q)$(MKDIR) $(app_root)/endpoints
alpine-chroot-install: alpine-chroot-install:
$(Q)wget https://raw.githubusercontent.com/alpinelinux/alpine-chroot-install/v0.14.0/alpine-chroot-install \ $(Q)wget https://raw.githubusercontent.com/alpinelinux/alpine-chroot-install/v0.14.0/alpine-chroot-install \
@ -134,43 +137,39 @@ $(worker_chroot) $(kmgr_chroot) $(parent_chroot): alpine-chroot-install
code : $(built_files) code : $(built_files)
$(built_files): $(build_dir)%.lua : src/lua/%.lua $(build_dir) $(built_files): $(app_root)/%.lua : src/lua/%.lua $(app_root)
$(Q)$(ECHO) "[copy] $@" $(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $< $@ $(Q)$(COPY) $< $@
$(built_pages): $(build_dir)pages/%.etlua : src/pages/%.etlua $(build_dir) $(built_pages): $(app_root)/pages/%.etlua : src/pages/%.etlua $(app_root)
$(Q)$(ECHO) "[copy] $@" $(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $< $@ $(Q)$(COPY) $< $@
src/lua/config.lua : src/lua/config.lua.in Makefile $(built_parts): $(app_root)/% : src/%
$(Q)$(ECHO) "[preprocess] $@" $(Q)$(ECHO) "[copy] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $< $(Q)$(COPY) $< $@
$(page_files) : % : %.in $(part_files) $(built_sql): $(app_root)/sql/%.sql : src/sql/%.sql
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(built_sql): $(build_dir)sql/%.sql : src/sql/%.sql
$(Q)$(ECHO) "[copy] $@" $(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $^ $@ $(Q)$(COPY) $^ $@
$(built_tests) : $(build_dir)% : src/spec/% $(built_tests) : $(app_root)/spec/% : spec/% $(app_root)/spec
$(Q)$(ECHO) "[copy] $@" $(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $^ $@ $(Q)$(COPY) $< $@
$(asset_files) : % : %.in $(app_root)/spec: $(app_root)
$(Q)$(ECHO) "[preprocess] $@" $(Q)$(MKDIR) $@
$(Q)$(SPP) $(SPPFLAGS) -o $@ $< $(Q)$(MKDIR) $@/parser_tests
smr.so : $(src_files) conf/build.conf $(asset_files) smr.so : $(src_files) conf/build.conf $(asset_files)
$(Q)$(ECHO) "[build] $@" $(Q)$(ECHO) "[build] $@"
$(Q)$(KODEV) build $(Q)$(KODEV) build
test : $(built) ## run the unit tests test : $(built) ## run the unit tests
$(Q)$(CD) kore_chroot && busted -v --no-keep-going #--exclude-tags slow $(Q)$(CD) $(app_root) && busted -v --no-keep-going --exclude-tags "slow,todo,working"
cov : $(built) ## code coverage (based on unit tests) cov : $(built) ## code coverage (based on unit tests)
$(Q)$(RM) $(kore_chroot)/luacov.stats.out $(Q)$(RM) $(kore_chroot)/luacov.stats.out
$(Q)$(CD) $(kore_chroot) && busted -v -c --no-keep-going #--exclude-tags slow $(Q)$(CD) $(kore_chroot) && busted -v -c --no-keep-going --exclude-tags slow
$(Q)$(CD) $(kore_chroot) && luacov endpoints/ $(Q)$(CD) $(kore_chroot) && luacov endpoints/
$(Q)$(ECHO) "open kore_chroot/luacov.report.out to view coverage results." $(Q)$(ECHO) "open kore_chroot/luacov.report.out to view coverage results."

View File

@ -2,7 +2,7 @@ _G.spy = spy
local mock_env = require("spec.env_mock") local mock_env = require("spec.env_mock")
local rng = require("spec.fuzzgen") local rng = require("spec.fuzzgen")
describe("smr biography",function() describe("smr biography #todo",function()
setup(mock_env.setup) setup(mock_env.setup)
teardown(mock_env.teardown) teardown(mock_env.teardown)
it("should allow users to set their biography",function() it("should allow users to set their biography",function()

View File

@ -1,10 +1,10 @@
_G.spy = spy _G.spy = spy
local mock_env = require("spec.env_mock") local env_mock = require("spec.env_mock")
describe("smr cacheing",function() describe("smr cacheing",function()
setup(mock_env.setup) setup(env_mock.setup)
teardown(mock_env.teardown) teardown(env_mock.teardown)
it("caches a page if the page is requested twice #working",function() it("caches a page if the page is requested twice #working",function()
local read_get = require("endpoints.read_get") local read_get = require("endpoints.read_get")
local cache = require("cache") local cache = require("cache")
@ -29,9 +29,15 @@ describe("smr cacheing",function()
)) ))
end end
end) end)
it("should expose the database connection", function()
local cache = require("cache")
configure()
assert(cache.cache, "Not exposed under .cache")
end)
it("does not cache the page if the user is logged in", function() it("does not cache the page if the user is logged in", function()
local read_get = require("endpoints.read_get") local read_get = require("endpoints.read_get")
local cache = require("cache") local cache = require("cache")
env_mock.mockdb()
renderspy = spy.on(cache,"render") renderspy = spy.on(cache,"render")
configure() configure()
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
@ -40,7 +46,7 @@ describe("smr cacheing",function()
"request have been made." "request have been made."
)) ))
end end
local req = mock_env.session() local req = env_mock.session()
req.method = "GET" req.method = "GET"
req.path = "/a" req.path = "/a"
req.args = {} req.args = {}

View File

@ -12,6 +12,7 @@ local ntostring
local login_post local login_post
local fuzzy local fuzzy
local claim_post local claim_post
local session
print_table= function(...) print_table= function(...)
print("Print called") print("Print called")
local args = {...} local args = {...}
@ -69,7 +70,10 @@ local smr_mock_env = {
http_request_get_path = spy.new(function(req) return req.path or "/" end), http_request_get_path = spy.new(function(req) return req.path or "/" end),
http_request_populate_qs = spy.new(function(req) req.qs_populated = true end), http_request_populate_qs = spy.new(function(req) req.qs_populated = true end),
http_request_populate_post = spy.new(function(req) req.post_populated = true end), http_request_populate_post = spy.new(function(req) req.post_populated = true end),
http_populate_multipart_form = spy.new(function(req) req.post_populated = true end), http_populate_multipart_form = spy.new(function(req)
req.post_populated = true
req.multipart_form_populated = true
end),
http_argument_get_string = spy.new(function(req,str) http_argument_get_string = spy.new(function(req,str)
assert(req.args,"requests should have a .args table") assert(req.args,"requests should have a .args table")
assert( assert(
@ -83,7 +87,7 @@ local smr_mock_env = {
return req.args[str] return req.args[str]
end), end),
http_file_get = spy.new(function(req,filename) http_file_get = spy.new(function(req,filename)
assert(req.multipart_forum_populated,[[ assert(req.multipart_form_populated,[[
http_file_get() can only be called after the approriate http_file_get() can only be called after the approriate
populate method has been called. (http_populate_multipart_form()) populate method has been called. (http_populate_multipart_form())
]]) ]])
@ -156,9 +160,9 @@ local string_fmt_override = {
setmetatable(string_fmt_override,{__index = string}) setmetatable(string_fmt_override,{__index = string})
local smr_override_env = { local smr_override_env = {
--Detour assert so we don't actually perform any checks --Detour assert so we don't actually perform any checks
assert = spy.new(function(bool,msg,level) return bool end), --assert = spy.new(function(bool,msg,level) return bool end),
--Allow string.format to accept nil as arguments --Allow string.format to accept nil as arguments
string = string_fmt_override --string = string_fmt_override
} }
mock.olds = {} mock.olds = {}
@ -173,10 +177,12 @@ end
function mock.mockdb() function mock.mockdb()
local config = require("config") local config = require("config")
config.db = "data/unittest.db" --config.db = "data/unittest.db"
config.db = ":memory:"
assert(os.execute("rm " .. config.db)) assert(os.execute("rm " .. config.db))
package.loaded.db = nil package.loaded.db = nil
local db = require("db") local db = require("db")
configure()
end end
function mock.teardown() function mock.teardown()
@ -207,7 +213,7 @@ local session_m = {__index = {
}} }}
function mock.session(tbl) function mock.session(tbl)
if post_login == nil then if login_post == nil then
login_post = require("endpoints.login_post") login_post = require("endpoints.login_post")
fuzzy = require("spec.fuzzgen") fuzzy = require("spec.fuzzgen")
claim_post = require("endpoints.claim_post") claim_post = require("endpoints.claim_post")

View File

@ -1,13 +1,13 @@
_G.spy = spy _G.spy = spy
local mock_env = require("spec.env_mock") local env_mock = require("spec.env_mock")
local rng = require("spec.fuzzgen") local rng = require("spec.fuzzgen")
describe("smr login",function() describe("smr login",function()
setup(mock_env.setup) setup(env_mock.setup)
teardown(mock_env.teardown) teardown(env_mock.teardown)
it("should allow someone to claim an account",function() it("should allow someone to claim an account",function()
mock_env.mockdb() env_mock.mockdb()
local claim_post = require("endpoints.claim_post") local claim_post = require("endpoints.claim_post")
configure() configure()
claim_req = { claim_req = {
@ -186,6 +186,7 @@ describe("smr login",function()
text = "post text", text = "post text",
markup = "plain", markup = "plain",
tags = "", tags = "",
pasteas = username
} }
} }
paste_post(paste_req_post) paste_post(paste_req_post)

View File

@ -10,8 +10,18 @@ borrowed sha3 implementation from https://keccak.team
#include "libcrypto.h" #include "libcrypto.h"
#include "keccak.h" #include "keccak.h"
/* /* rst
sha3(data::string)::string @name lua/sha3
Provides a sha3 implementation.
::signature
sha3(data :: {{lua/string}}) :: {{lua/string}}
::params
{{lua/string}} data - The data to hash
::returns
{{lua/string}} - The hash of the input string
*/ */
int int
lsha3(lua_State *L){ lsha3(lua_State *L){

View File

@ -1,7 +1,9 @@
--[[ --[[ md
@name lua/addon
Addon loader - Addons are either: Addon loader - Addons are either:
* A folder with at least two files: * A folder with at least two files:
- meta.lua - contains load order information - meta.lua - contains addon information
- init.lua - entrypoint that gets run to load the addon - init.lua - entrypoint that gets run to load the addon
* A zip file with the same * A zip file with the same
* A sqlite3 database with a table "files" that has at least the columns * A sqlite3 database with a table "files" that has at least the columns
@ -15,9 +17,15 @@ each worker), and should return a table with at least the following information
{ {
name :: string - A name for the addon (all addons must have unique names) name :: string - A name for the addon (all addons must have unique names)
desc :: string - A description for the addon. desc :: string - A description for the addon.
entry :: table[number -> string] - Describes the load order for this order :: number - When should we run init.lua relative to other addons?
addon. Each addon's meta.lua is run, and sorted to get a load Each addon's meta.lua is run (in any order), addons are sorted
order for entrypoints (the strings are the names files) according to their order, and finally each addon's init.lua is
called according to this order.
}
meta.lua may include additional information that can be read and used by other
addons. meta.lua is run in a restricted environment with almost no functions
available.
]] ]]
local oldconfigure = configure local oldconfigure = configure

View File

@ -17,12 +17,11 @@ local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
local oldconfigure = configure local oldconfigure = configure
function configure(...) function configure(...)
local cache = db.sqlassert(sql.open_memory()) ret.cache = db.sqlassert(sql.open_memory())-- Expose db for testing
ret.cache = cache -- Expose db for testing
--A cache table to store rendered pages that do not need to be --A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start --rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO --swapping to disk. TODO
assert(cache:exec([[ assert(ret.cache:exec([[
CREATE TABLE IF NOT EXISTS cache ( CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY, path TEXT PRIMARY KEY,
data BLOB, data BLOB,
@ -30,7 +29,7 @@ function configure(...)
dirty INTEGER dirty INTEGER
); );
]])) ]]))
stmnt_cache = assert(cache:prepare([[ stmnt_cache = assert(ret.cache:prepare([[
SELECT data SELECT data
FROM cache FROM cache
WHERE WHERE
@ -38,14 +37,14 @@ function configure(...)
((dirty = 0) OR (strftime('%s','now') - updated) > 20) ((dirty = 0) OR (strftime('%s','now') - updated) > 20)
; ;
]])) ]]))
stmnt_insert_cache = assert(cache:prepare([[ stmnt_insert_cache = assert(ret.cache:prepare([[
INSERT OR REPLACE INTO cache ( INSERT OR REPLACE INTO cache (
path, data, updated, dirty path, data, updated, dirty
) VALUES ( ) VALUES (
:path, :data, strftime('%s','now'), 0 :path, :data, strftime('%s','now'), 0
); );
]])) ]]))
stmnt_dirty_cache = assert(cache:prepare([[ stmnt_dirty_cache = assert(ret.cache:prepare([[
UPDATE OR IGNORE cache UPDATE OR IGNORE cache
SET dirty = 1 SET dirty = 1
WHERE path = :path; WHERE path = :path;

View File

@ -1,13 +0,0 @@
--[[
Holds configuration.
A one-stop-shop for runtime configuration
]]
local config = {
domain = "<{get domain}>",
production = false,
legacy_url_cutoff = 144,
approot = "<{get approot}>"
}
config.db = config.approot .. "data/posts.db"
return config

View File

@ -1,4 +1,5 @@
--[[ --[[ md
@name lua/db
Does most of the database interaction. Does most of the database interaction.
Notably, holds a connection to the open sqlite3 database in .conn Notably, holds a connection to the open sqlite3 database in .conn
]] ]]
@ -9,9 +10,34 @@ local config = require("config")
local db = {} local db = {}
--[[ --[[ md
@name lua/db/sqlassert
Runs an sql query and receives the 3 arguments back, prints a nice error Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success. message on fail, and returns true on success.
Parameters:
0. r - {{lsqlite/stmnt}} | {{lua/nil}} - The userdata returned from
{{lsqlite/db/prepare}}
0. errcode - {{lua/nil}} | {{lua/number}} - If the first argument back from
{{lsqlite/db/prepare}} is nil, this second argument is a numeric errorcode,
see {{lsqlite/errcodes}}
0. err - {{lua/nil}} | {{lua/string}} - The string error returned from
{{lsqlite/db/prepare}}. Only non-nil if the first return value was nil. A
string message describing what went wrong in the statment. If this argument is
also {{lua/nil}}, this function retrives the error mssage from
{{lsqlite/db/errmsg}}.
Returns:
0. r - {{lua/userdata}} | {{lua/nil}} - The first argument passed in. Used so
that error checking and assignment can all be done on a single line.
Example:
> db = require("db")
> query = db.sqlassert(db.conn:parepare("SELECT 'Hello, world!'"))
]] ]]
function db.sqlassert(r, errcode, err) function db.sqlassert(r, errcode, err)
if not r then if not r then
@ -24,8 +50,27 @@ function db.sqlassert(r, errcode, err)
return r return r
end end
--[[ --[[ md
Continuously tries to perform an sql statement until it goes through @name lua/db/do_sql
Continuously tries to perform an sql statement until it goes through. This function may call {{lua/coroutine/yield}}
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The userdata returned form {{lsqlite/db/prepare}}
Returns:
0. err - {{lua/number}} - The error code returned from running the statement. Will be `lsqlite.OK` on success, see {{lsqlite/errcodes}}
Example:
> sql = require("lsqlite3")
> configure = function(...) end -- Mock smr environment
> db = require("db")
> configure()
> query = db.conn:prepare("SELECT 'Hello, world!';")
> assert(db.do_sql(query))
]] ]]
function db.do_sql(stmnt) function db.do_sql(stmnt)
if not stmnt then error("No statement",2) end if not stmnt then error("No statement",2) end
@ -42,11 +87,33 @@ function db.do_sql(stmnt)
return err return err
end end
--[[ --[[ md
Provides an iterator that loops over results in an sql statement @name lua/db/sql_rows
or throws an error, then resets the statement after the loop is done.
Provides an iterator that loops over results in an sql statement or throws an
error, then resets the statement after the loop is done.
Returned iterator returns varargs, so the values can be unpacked in-line in the Returned iterator returns varargs, so the values can be unpacked in-line in the
for loop. for loop. This statement is approximately the same as {{sqlite/stmt/rows}}, but
may yield when the db connection is busy, and continue execution when the
connection is free again.
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The userdata returned from {{sqlite/db/prepare}}
Returns:
0. iterator - {{lua/iterator}} - The iterator function that returns varargs of the returns from the sql statement.
Example:
> db = require("db")
> query = db.conn:prepare("SELECT 'Hello, world!';")
> for row in db.sql_rows(query) do
> print(row)
> end
Hello, world!
]] ]]
function db.sql_rows(stmnt) function db.sql_rows(stmnt)
if not stmnt then error("No statement",2) end if not stmnt then error("No statement",2) end
@ -75,7 +142,9 @@ function db.sql_rows(stmnt)
end end
--[[ --[[
Binds an argument to as statement with nice error reporting on failure @name lua/db/sqlbind
Binds an argument to as statement with nice error reporting on failure. Approximatly the same as {{lsqlite/stmt/bind_names}}, but with better error reporting.
stmnt :: sql.stmnt - the prepared sql statemnet stmnt :: sql.stmnt - the prepared sql statemnet
call :: string - a string "bind" or "bind_blob" call :: string - a string "bind" or "bind_blob"
position :: number - the argument position to bind to position :: number - the argument position to bind to
@ -85,7 +154,15 @@ function db.sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
local f = stmnt[call](stmnt,position,data) local f = stmnt[call](stmnt,position,data)
if f ~= sql.OK then if f ~= sql.OK then
error(string.format("Failed call %s(%d,%q): %s", call, position, data, db.conn:errmsg()),2) local errs = string.format(
"Failed call %s(%d,%q): %s",
call,
position,
data,
db.conn:errmsg()
)
log(LOG_ERR,errs)
error(errs,2)
end end
end end
@ -94,39 +171,25 @@ end
local oldconfigure = configure local oldconfigure = configure
db.conn = db.sqlassert(sql.open(config.db)) db.conn = db.sqlassert(sql.open(config.db))
function configure(...) function configure(...)
local statements = {
--Create sql tables "create_table_authors",
assert(db.conn:exec(queries.create_table_authors)) "insert_anon_author",
--Create a fake "anonymous" user, so we don't run into trouble "create_table_posts",
--so that no one runs into trouble being able to paste under this account. "create_table_raw_text",
assert(db.conn:exec(queries.insert_anon_author)) "create_table_images",
--If/when an author deletes their account, all posts "create_table_comments",
--and comments by that author are also deleted (on "create_table_tags",
--delete cascade) this is intentional. This also "create_index_tags",
--means that all comments by other users on a post "create_table_session"
--an author makes will also be deleted. }
-- -- ipairs() needed, "create table authors" must be executed before
--Post text uses zlib compression -- "insert anon author"
assert(db.conn:exec(queries.create_table_posts)) for _, statement in ipairs(statements) do
--Store the raw text so people can download it later, maybe db.sqlassert(db.conn:exec(queries[statement]))
--we can use it for "download as image" or "download as pdf" end
--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.conn:exec(queries.create_table_session))
return oldconfigure(...) return oldconfigure(...)
end end
configure()
function db.close() function db.close()
db.conn:close() db.conn:close()

View File

@ -15,7 +15,6 @@ local stmnt_author_create
--a while, but whatever. --a while, but whatever.
local oldconfigure = configure local oldconfigure = configure
function configure(...) function configure(...)
stmnt_author_create = db.sqlassert(db.conn:prepare(queries.insert_author)) stmnt_author_create = db.sqlassert(db.conn:prepare(queries.insert_author))
return oldconfigure(...) return oldconfigure(...)
end end

View File

@ -35,7 +35,7 @@ local function get_site_home(req, loggedin)
isanon = tonumber(iar) == 1, isanon = tonumber(iar) == 1,
posted = os.date("%B %d %Y",tonumber(dater)), posted = os.date("%B %d %Y",tonumber(dater)),
author = author, author = author,
tags = libtags.get(idr), taglist = libtags.get(idr),
hits = hits, hits = hits,
ncomments = cmts ncomments = cmts
} }

View File

@ -5,6 +5,7 @@ local util = require("util")
local session = require("session") local session = require("session")
local config = require("config") local config = require("config")
local pages = require("pages") local pages = require("pages")
local api = require("hooks")
local stmnt_author_acct local stmnt_author_acct
@ -18,47 +19,43 @@ function configure(...)
return oldconfigure(...) return oldconfigure(...)
end end
local old_authenticate = api.authenticate
function api.authenticate(data)
stmnt_author_acct:bind_names{name=data.user}
local err = db.do_sql(stmnt_author_acct)
if err ~= sql.ROW then
stmnt_author_acct:reset()
log(LOG_NOTICE,string.format("User %q failed to log in",data.user))
end
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
stmnt_author_acct:reset()
local hash = sha3(salt .. data.pass)
if hash == passhash then
return id
end
end
local function login_post(req) local function login_post(req)
--Try to log in --Try to log in
http_populate_multipart_form(req) http_populate_multipart_form(req)
local name = assert(http_argument_get_string(req,"user")) local name = assert(http_argument_get_string(req,"user"))
local pass = assert(http_file_get(req,"pass")) local pass = assert(http_file_get(req,"pass"))
stmnt_author_acct:bind_names{ local uid, err = api.authenticate({user=name,pass=pass})
name = name if not uid then
} http_response(req,200,pages.login{err=err})
local text return
local err = db.do_sql(stmnt_author_acct) end
if err == sql.ROW then local user_session = session.start(uid)
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 mysession = session.start(id)
local domain_no_port = config.domain:match("(.*):.*") or config.domain local domain_no_port = config.domain:match("(.*):.*") or config.domain
http_response_header(req,"set-cookie",string.format( local cookie_string = string.format(
[[session=%s; SameSite=Lax; Path=/; Domain=%s; HttpOnly; Secure]],mysession,domain_no_port [[session=%s; SameSite=Lax; Path=/; Domain=%s; HttpOnly; Secure]],
)) user_session,
domain_no_port
)
http_response_header(req,"set-cookie",cookie_string)
local loc = string.format("https://%s.%s",name,config.domain) local loc = string.format("https://%s.%s",name,config.domain)
http_response_header(req,"Location",loc) http_response_header(req,"Location",loc)
http_response(req,303,"") 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 end
return login_post return login_post

View File

@ -102,7 +102,7 @@ local function author_paste(req,ps)
text = ps.text text = ps.text
} }
end end
local asanon = assert(http_argument_get_string(req,"pasteas")) local asanon = assert(http_argument_get_string(req,"pasteas") or "anonymous")
local textsha3 = sha3(ps.text .. get_random_bytes(32)) local textsha3 = sha3(ps.text .. get_random_bytes(32))
--No need to check if the author is posting to the --No need to check if the author is posting to the
--"right" sudomain, just post it to the one they have --"right" sudomain, just post it to the one they have

View File

@ -14,9 +14,9 @@ local stmnt_read, stmnt_update_views, stmnt_comments
local oldconfigure = configure local oldconfigure = configure
function configure(...) function configure(...)
stmnt_read = assert(db.conn:prepare(queries.select_post)) stmnt_read = db.sqlassert(db.conn:prepare(queries.select_post))
stmnt_update_views = assert(db.conn:prepare(queries.update_views)) stmnt_update_views = db.sqlassert(db.conn:prepare(queries.update_views))
stmnt_comments = assert(db.conn:prepare(queries.select_comments)) stmnt_comments = db.sqlassert(db.conn:prepare(queries.select_comments))
return oldconfigure(...) return oldconfigure(...)
end end

View File

@ -28,6 +28,18 @@ local pagenames = {
"parts/story_breif", "parts/story_breif",
"parts/taglist" "parts/taglist"
} }
--Functions available to all templates
local global_env = {
include = function(filename)
local fp = assert(io.open(filename,"r"))
local data = assert(fp:read("*a"))
fp:close()
return data
end,
}
local global_env_m = {
__index=global_env
}
local pages = {} local pages = {}
for k,v in pairs(pagenames) do for k,v in pairs(pagenames) do
local path = string.format(config.approot .. "pages/%s.etlua",v) local path = string.format(config.approot .. "pages/%s.etlua",v)
@ -46,6 +58,12 @@ for k,v in pairs(pagenames) do
assert(func, "Failed to load " .. path) assert(func, "Failed to load " .. path)
pages[v] = function(env) pages[v] = function(env)
assert(type(env) == "table","env must be a table") assert(type(env) == "table","env must be a table")
-- Add our global metatable functions at the bottom metatable's __index
local cursor = env
while cursor ~= nil and getmetatable(cursor) and getmetatable(cursor).__index do
cursor = getmetatable(cursor)
end
setmetatable(cursor,global_env_m)
local buff, err = parser:run(func,env) local buff, err = parser:run(func,env)
if not buff then if not buff then
errorf("Failed to render %s : %s", path, err) errorf("Failed to render %s : %s", path, err)

View File

@ -4,12 +4,13 @@ local db = require("db")
local util = require("util") local util = require("util")
local queries = require("queries") local queries = require("queries")
local oldconfigure = configure
local stmnt_get_session, stmnt_insert_session, stmnt_delete_session local stmnt_get_session, stmnt_insert_session, stmnt_delete_session
local oldconfigure = configure
function configure(...) function configure(...)
stmnt_get_session = assert(db.conn:prepare(queries.select_valid_sessions)) stmnt_get_session = db.sqlassert(db.conn:prepare(queries.select_valid_sessions))
stmnt_insert_session = assert(db.conn:prepare(queries.insert_session)) stmnt_insert_session = db.sqlassert(db.conn:prepare(queries.insert_session))
stmnt_delete_session = assert(db.conn:prepare(queries.delete_session)) stmnt_delete_session = db.sqlassert(db.conn:prepare(queries.delete_session))
return oldconfigure(...) return oldconfigure(...)
end end
@ -59,7 +60,7 @@ function session.start(who)
} }
local err = db.do_sql(stmnt_insert_session) local err = db.do_sql(stmnt_insert_session)
stmnt_insert_session:reset() stmnt_insert_session:reset()
assert(err == sql.DONE) assert(err == sql.DONE, "Error should have been 'DONE', was: " .. tostring(err))
return session return session
end end

View File

@ -1,3 +1,14 @@
--[[ md
@name lua/tags
Helper methods for cleaning story tags.
Tags are the main way to search smr, a simple `+<tag>` or `-<tag>` will show all
stories that include (+) or do not include (-) a particular tag.
Tags are stored in the {{table_tags}} and are deleted if the story they are
attached to is deleted. If an author is deleted, all their stories are deleted,
and this will cascade to deleting tags on their stories too.
]]
local sql = require("lsqlite3") local sql = require("lsqlite3")
local db = require("db") local db = require("db")

View File

@ -1,3 +1,9 @@
--[[ md
@name lua/util
Various utilities that aren't big enough for their own module, but are still
used in more than one place.
]]
local sql = require("lsqlite3") local sql = require("lsqlite3")
local config = require("config") local config = require("config")
@ -15,8 +21,26 @@ function configure(...)
return oldconfigure(...) return oldconfigure(...)
end end
--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/ --[[ md
--no underscore because we use that for our operative pages @name doc/url_spec
URLs generated from smr use letters and numbers to encode a monotonically
increasing post id into a url that can easily be shared (and ends up
considerably shorter). The characters used in url generation are:
[a-z][A-Z][0-9], and numbers are encoded to use the second available 1-character
permuation, then the first available 2-character permutation, and so on.
For example, the first post is encoded as 'b', the second as 'c', the thrid
as 'd', and so on. The off-by-one nature is to simplify implementation of
2-character and 3-character combinations with Lua's 1-indexed arrays.
see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
no underscore because we use that for our operative pages
A set of legacy characters that are no longer in use (because they were invalid
to use in URL's) is also defined, but unused as long as
{{config/legacy_url_cutoff}} is set to 0.
]]
local url_characters = local url_characters =
[[abcdefghijklmnopqrstuvwxyz]].. [[abcdefghijklmnopqrstuvwxyz]]..
[[ABCDEFGHIJKLMNOPQRSTUVWXYZ]].. [[ABCDEFGHIJKLMNOPQRSTUVWXYZ]]..
@ -34,8 +58,11 @@ local url_characters_rev_legacy = {}
for i = 1,string.len(url_characters_legacy) do for i = 1,string.len(url_characters_legacy) do
url_characters_rev_legacy[string.sub(url_characters_legacy,i,i)] = i url_characters_rev_legacy[string.sub(url_characters_legacy,i,i)] = i
end end
--[[
Encode a number to a shorter HTML-safe url path --[[ md
@name lua/util/encode_id
Encode a number to a shorter HTML-safe url path. Url paths are generated
according to the {{doc/url_spec}
]] ]]
function util.encode_id(number) function util.encode_id(number)
local result = {} local result = {}

View File

@ -1,27 +0,0 @@
<% assert(author,"No author specified") %>
<% assert(bio,"No bio included") %>
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="content">
<form action="https://<%= author %>.<%= domain %>/" method="post" class="container">
<textarea name="" cols=80 rows=24 class="column">
<%= bio %>
</textarea><br/>
<input type="submit">
</form>
</div>
<div class="content">
<% if #stories == 0 then %>
This author has not made any pastes yet.
<% else %>
<table>
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,39 +0,0 @@
<% assert(author,"No author specified") %>
<% assert(bio,"No bio included") %>
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<% if not loggedin then %>
<a href="/_login" class="button column column-0">Log in</a>
<% else %>
<a href="/_logout" class="button column column-0">Log out</a>
<a href="/_bio" class="button column column-0">Edit bio</a>
<% end %>
<span class="column column-0"></span>
<{system cat src/pages/parts/search.etlua}>
</div>
</div>
<% if bio ~= "" then %>
<div class="container">
<blockquote class="biography">
<%- bio %>
</blockquote>
</div>
<% end %>
<div class="content">
<% if #stories == 0 then %>
This author has not made any pastes yet.
<% else %>
<table>
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,34 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_paste" method="post" class="container">
<fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-70"></input>
<select id="pasteas" name="pasteas" class="column column-10">
<option value="<%= user %>"><%= user %></option>
<option value="anonymous">Anonymous</option>
</select>
<select id="markup" name="markup" class="column column-10">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-10">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>
</div>
<div class="row">
<input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input>
</div>
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,11 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
&#128577;
</h1>
<div class="container">
<p>
You don't have permission to edit: <%= path %>
</p>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,20 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Register
</h1>
Once you press submit, you will be prompted to download a file.<br/>
slash.monster uses this file in place of a password, keep it safe.<br/>
Consider keeping a copy on a USB in case your hard drive fails.<br/>
The admin cannot recover your passfile, and will not reset accounts.<br/>
<b>Names may be up to 30 characters, alphanumeric, no symbols, all lower case.</b><br/>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="/_claim" method="post">
<fieldset>
<label for="user">Name:</label>
<input type="text" name="user" id="user" placeholder="name">
<input type="submit">
</fieldset>
</form>
Once you have your file, you can <a href="/_login">log in</a>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,48 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_edit" method="post" class="container">
<fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-60" value="<%= title %>"></input>
<input type="hidden" name="story" value="<%= story %>">
<select id="pasteas" name="pasteas" class="column column-10">
<% if isanon then %>
<option value="<%= user %>"><%= user %></option>
<option value="anonymous" selected>Anonymous</option>
<% else %>
<option value="<%= user %>" selected><%= user %></option>
<option value="anonymous">Anonymous</option>
<% end %>
</select>
<select id="markup" name="markup" class="column column-10">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-20">
<label for="unlisted" class="label-inline">Unlisted</label>
<input
type="checkbox"
name="unlisted"
id="unlisted"
<% if unlisted then %>
checked
<% end %>
>
</input>
</div>
</div>
<div class="row">
<input type="text" name="tags" value="<%= tags %>" placeholder="Tags (semicolon;seperated)" class="column"></input>
</div>
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,18 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Edit Biography for <%= user %>
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_bio" method="post" class="container">
<fieldset>
<input type="hidden" name="author" value="<%= user %>">
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<div class="row">
<input type="submit">
</div>
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,14 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<% if errcode then -%><%= errcode -%><% end -%>:
<% if errcodemsg then -%><%= errcodemsg -%><% end %>
</h1>
<% if explanation then -%><%= explanation -%><% end -%>
<% if should_traceback then %>
<code><pre>
<%- debug.traceback() %>
</pre></code>
<% end %>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,36 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title column">
<a href="https://<%= domain %>">
<%= domain %>
</a>
</h1>
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<% if not loggedin then %>
<a href="/_login" class="button column column-0">Log in</a>
<a href="/_claim" class="button column column-0">Register</a>
<% else %>
<a href="/_logout" class="button column column-0">Log out</a>
<span class="column column-0"></span>
<% end %>
<{system cat src/pages/parts/search.etlua}>
</div>
<p>
<{ system cat src/pages/parts/motd.etlua }>
</p>
</div>
<div class="content">
<% if #stories == 0 then %>
No stories available.
<% else %>
<table id="story_list">
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,15 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Login
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="/_login" method="post" enctype="multipart/form-data">
<fieldset>
<label for="user">Name:</label>
<input type="text" name="user" id="user" placeholder="name" autocorrect="off" autocapitalize="none">
<label for="pass">Passfile:</label>
<input type="file" name="pass" id="pass">
<input type="submit" value="Log In"/>
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,11 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
&#128577;
</h1>
<div class="container">
<p>
No author found: <%= author %>
</p>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,10 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
&#128577;
</h1>
<div class="container">
<p>
No story found: <%= path %>
</p>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,27 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= domain %>/_paste" method="post" class="container"><fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-60"></input>
<select id="markup" name="markup" class="column column-20">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-20">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>
</div>
<div class="row">
<input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input>
</div>
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset></form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,84 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<% local api = require("hooks") %>
<nav>
<a href="https://<%= domain %>"><%= domain %></a>/<a href="https://<%= domain %>/<%= short %>"><%= short %></a>
</nav>
<% if owner then -%>
<div class="row">
<% for _, spec in ipairs(api.get.page_owner(getfenv(1))) do %>
<form action="<%= spec.endpoint %>" method="<%= spec.method %>"><fieldset>
<% for key, value in pairs(spec.fields) do %>
<input type="hidden" name="<%= key %>" value="<%= value %>"/>
<% end %>
<input type="submit" value="<%= spec.text %>" class="button column column-0"/>
</fieldset></form>
<% end %>
</div>
<% end -%>
<article>
<h2 class="title"> <%- title %> </h2>
<h3>
<% if isanon or author == nil then -%>
By Anonymous
<% else -%>
By <a href="https://<%= author %>.<%= domain %>"><%= author %></a>
<% end -%>
</h3>
<ul class="tag-list">
<% for _,tag in pairs(tags) do -%>
<{system cat src/pages/parts/taglist.etlua}>
<% end -%>
</ul>
<%- text %>
</article>
<hr/>
<p><%= views %> Hits, <%= #comments %> Comments</p>
<div class="row">
<% for _, spec in ipairs(api.get.page_reader(getfenv(1))) do %>
<form action="<%= spec.endpoint %>" method="<%= spec.method %>"><fieldset>
<% for key, value in pairs(spec.fields) do %>
<input type="hidden" name="<%= key %>" value="<%= value %>"/>
<% end %>
<input type="submit" value="<%= spec.text %>" class="button">
</fieldset></form>
<% end %>
</div>
<form action="https://<%= domain %>/<%= short %>" method="POST">
<% if unlisted then %>
<input type="hidden" name="pwd" value="<%= hashstr %>"/>
<% end %>
<textarea name="text" cols=60 rows=10 class="column"></textarea>
</div><% if iam then %>
<select id="postas" name="postas">
<option value="Anonymous">Anonymous</option>
<option value="<%= iam %>"><%= iam %></option>
</select>
<input type="submit" value="post" class="button">
<% else %>
<input type="hidden" name="postas" value="Anonymous">
<input type="submit" value="post" class="button">
<% end %>
</form>
<% if comments and #comments == 0 then %>
<p><i>No comments yet</i></p>
<% else %>
<section>
<% for _,comment in pairs(comments) do %>
<article>
<% if comment.isanon then %>
<p><b>Anonymous</b></p>
<% else %>
<p><b><%= comment.author %></b></p>
<% end %>
<p><%= comment.text %></p>
</article>
<% end %>
</section>
<% end %>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,19 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<a href="https://<%= domain %>"><%= domain %></a>/
</h1>
<div class="row">
<{system cat src/pages/parts/search.etlua}>
</div>
<div class="content">
<% if #results == 0 then %>
No stories matched your search.
<% else %>
<table>
<% for k,story in pairs(results) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,56 +0,0 @@
SELECT
posts.id,
posts.post_title,
posts.isanon,
authors.name,
posts.post_time,
posts.views,
COUNT(comments.id)
FROM
posts,authors
LEFT JOIN comments ON comments.postid = posts.id
WHERE
authors.id = posts.authorid
AND posts.unlisted = 0
<% for field, values in pairs(result) do -%>
<% for _,value in pairs(values) do -%>
<% local pn,expr,value = unpack(value) -%>
<% local n = (pn == "+" and "" or "NOT") -%>
<% if field == "title" then -%>
AND <%= n %> posts.post_title LIKE ?
<% elseif field == "author" then -%>
AND <%= n %> authors.name LIKE ?
<% elseif field == "date" then -%>
AND <%= n %> posts.post_time <%- expr %> ?
<% elseif field == "hits" then -%>
AND posts.views <%- expr -%> ?
<% end -%>
<% end -%>
<% end -%>
GROUP BY
posts.id
<% for _,tag in pairs(result.tags) do -%>
INTERSECT
SELECT
posts.id,
posts.post_title,
posts.isanon,
authors.name,
posts.post_time,
posts.views,
COUNT(comments.id)
FROM
posts,authors,tags
LEFT JOIN comments ON comments.postid = posts.id
WHERE
posts.authorid = authors.id
AND tags.postid = posts.id
<% local n,v,t = unpack(tag) -%>
<% n = (n == "-" and "NOT" or "") -%>
AND <%= n %> tags.tag = ?
GROUP BY
posts.id
<% end -%>
ORDER BY
posts.post_time DESC
;

View File

@ -33,7 +33,9 @@ int delete(struct http_request *);
int do_lua(struct http_request *req, const char *name); int do_lua(struct http_request *req, const char *name);
int errhandeler(lua_State *); int errhandeler(lua_State *);
lua_State *L; lua_State *L;
/* These should be defined in in kore somewhere and included here */ /*
These should be defined in in kore somewhere and included here
*/
void kore_worker_configure(void); void kore_worker_configure(void);
void kore_worker_teardown(void); void kore_worker_teardown(void);
/* /*
@ -111,7 +113,8 @@ do_lua(struct http_request *req, const char *name){
return do_lua(req,#lua_method);\ return do_lua(req,#lua_method);\
} }
/*** /* md
@name http/_paste
Called at the endpoint <domain>/_paste. Called at the endpoint <domain>/_paste.
This method doesn't need any parameters for GET requests. This method doesn't need any parameters for GET requests.
This method expects the following for POST requests: This method expects the following for POST requests:
@ -121,10 +124,14 @@ This method expects the following for POST requests:
In addition to the normal assets, this page includes In addition to the normal assets, this page includes
suggest_tags.js, which suggests tags that have been suggest_tags.js, which suggests tags that have been
submitted to the site before. submitted to the site before.
@function _G.paste
@custom http_method GET POST @custom http_method GET POST
*/
/* md
@name lua/paste
This function is called automatically with the request submitted at
<endpoint>/_paste
@param http_request req The request to service @param http_request req The request to service
***/ */
route(post_story,"paste"); route(post_story,"paste");
/*** /***
@ -180,10 +187,14 @@ route(read_story, "read");
/*** /***
Called at the endpoint <domain>/_login Called at the endpoint <domain>/_login
This method does not requirei any parameters for GET requests. This method does not require any parameters for GET requests.
This method requiries the following for POST requests: This method requiries the following for POST requests:
* user :: [a-z0-9]{1,30} - The username to log in as * user :: [a-z0-9]{1,30} - The username to log in as
* pass :: any - The passfile for this user * pass :: any - The passfile for this user
To overload login functionality in an addon, see @{api.authenticate}
@function _G.login
@custom http_method GET POST
@param http_request req The request to service.
***/ ***/
int int
login(struct http_request *req){ login(struct http_request *req){

View File

@ -1 +1,4 @@
/*
The tags table is indexed on tag, so that search is fast
*/
CREATE INDEX tag_index ON tags(tag); CREATE INDEX tag_index ON tags(tag);

View File

@ -1,3 +1,11 @@
/* md
@name sql/table/authors
If/when an author deletes 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.
*/
CREATE TABLE IF NOT EXISTS authors ( CREATE TABLE IF NOT EXISTS authors (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT UNIQUE ON CONFLICT FAIL, name TEXT UNIQUE ON CONFLICT FAIL,

View File

@ -1,3 +1,9 @@
/*
Comments on a post.
When an author deletes their account or the posts this comment
is posted on is deleted, this comment will also be deleted.
*/
CREATE TABLE IF NOT EXISTS comments ( CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
postid REFERENCES posts(id) ON DELETE CASCADE, postid REFERENCES posts(id) ON DELETE CASCADE,

View File

@ -1,3 +1,6 @@
/*
We may want to store images one day. This is unused for now
*/
CREATE TABLE IF NOT EXISTS images ( CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT, name TEXT,

View File

@ -1,8 +1,10 @@
/* /*
Store a cookie for logged in users. Logged in users can edit Store a cookie for logged in users. Logged in users can edit
their own posts. their own posts, edit their biographies, and post stories and comment under their own name.
TODO: WE can hash the "key" so that even if the database gets
dumped, a hacker can't cookie-steal with only read access
to the db.
*/ */
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
author REFERENCES authors(id) ON DELETE CASCADE, author REFERENCES authors(id) ON DELETE CASCADE,

View File

@ -1,3 +1,7 @@
/*
Tags on a post
A post's tags are deleted if the post is deleted.
*/
CREATE TABLE IF NOT EXISTS tags ( CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
postid REFERENCES posts(id) ON DELETE CASCADE, postid REFERENCES posts(id) ON DELETE CASCADE,