Compare commits
70 Commits
9d2c95bb33
...
1930ade3f7
Author | SHA1 | Date |
---|---|---|
Robin Malley | 1930ade3f7 | |
Robin Malley | 8b03c78346 | |
Robin Malley | afe144d554 | |
Robin Malley | 720e826d4d | |
Robin Malley | 9daf7e90cd | |
Robin Malley | 411bcb494d | |
Robin Malley | e25d2fd06a | |
Robin Malley | 3431daee0b | |
Robin Malley | 223cfb9e46 | |
Robin Malley | 49d0b0a397 | |
Robin Malley | d5ec6d6864 | |
Robin Malley | f5c729bfde | |
Robin Malley | f0c7ae13fe | |
Robin Malley | acebec5d73 | |
Robin Malley | 872760c9ff | |
Robin Malley | 040587701e | |
Robin Malley | ec6aed9866 | |
Robin Malley | 647e7f2ac2 | |
Robin Malley | 3b1d3dd910 | |
Robin Malley | e0fabca908 | |
Robin Malley | a0c8907f71 | |
Robin Malley | 77d8c0e66b | |
Robin Malley | 138cf12028 | |
Robin Malley | 87556f77cc | |
Robin Malley | 63e2b0b663 | |
Robin Malley | ac3fc81741 | |
Robin Malley | 9667aa1c3e | |
Robin Malley | 16054156a1 | |
Robin Malley | 68561443a5 | |
Robin Malley | 069c75b72e | |
Robin Malley | 81ad49ae80 | |
Robin Malley | 680a341db5 | |
Robin Malley | 296777d3fc | |
Robin Malley | 1487835478 | |
Robin Malley | e0a8b3d60a | |
Robin Malley | de76d31fe8 | |
Robin Malley | 37a9bbd63d | |
Robin Malley | d5a3197262 | |
Robin Malley | c00903505b | |
Robin Malley | 9dcc743199 | |
Robin Malley | 6398e97498 | |
Robin Malley | 4e0a23ee95 | |
Robin Malley | 1ddd446297 | |
Robin Malley | 9444d300b8 | |
Robin Malley | 3bd07ebf6a | |
Robin Malley | a16f2dfe02 | |
Robin Malley | ffc34295e9 | |
Robin Malley | e0a8e01513 | |
Robin Malley | ab6572314e | |
Robin Malley | 41f68f45b8 | |
Robin Malley | d11695b5eb | |
Robin Malley | eac2a38c6c | |
Robin Malley | 4913e7765e | |
Robin Malley | f88ec0e22a | |
Robin Malley | 7e5e38c3f2 | |
Robin Malley | e3468136e5 | |
Robin Malley | 3db891800b | |
Robin Malley | 3b6a631dc4 | |
Robin Malley | e5d1904b1f | |
Robin Malley | 7cc5e8d0ef | |
Robin Malley | 9e51de6c8e | |
Robin Malley | fd87cf95ee | |
Robin Malley | fdf0b67f3a | |
Robin Malley | 33a23ef20c | |
Robin Malley | 58565bc088 | |
Robin Malley | 73df8d400e | |
Robin Malley | 53b1a19c05 | |
Robin Malley | 55923a9cd6 | |
Robin Malley | 8cf7344e7b | |
Robin Malley | 701800cfe2 |
|
@ -5,3 +5,6 @@ smr.so
|
|||
assets.h
|
||||
cert
|
||||
kore_chroot/*
|
||||
conf/smr.conf
|
||||
src/lua/config.lua
|
||||
src/pages/error.etlua
|
||||
|
|
70
Makefile
70
Makefile
|
@ -6,7 +6,9 @@ COPY=cp
|
|||
RM=rm -f
|
||||
SPP=spp
|
||||
CD=cd
|
||||
|
||||
AWK=awk
|
||||
GREP=grep
|
||||
SORT=sort
|
||||
|
||||
# Config
|
||||
chroot_dir=kore_chroot/
|
||||
|
@ -19,7 +21,8 @@ user=robin
|
|||
port=8888
|
||||
domain=test.monster:$(port)
|
||||
|
||||
#squelch prints
|
||||
SPPFLAGS=-D port=$(port) -D kore_chroot=$(chroot_dir) -D chuser=$(user) -D domain=$(domain)
|
||||
# squelch prints, flip to print verbose information
|
||||
Q=@
|
||||
#Q=
|
||||
|
||||
|
@ -38,28 +41,34 @@ part_files=$(in_part_files:%.in=%) $(shell find src/pages/parts/*.etlua -type f)
|
|||
built_pages=$(page_files:src/pages/%.etlua=$(chroot_dir)pages/%.etlua)
|
||||
built_sql=$(sql_files:src/sql/%.sql=$(chroot_dir)sql/%.sql)
|
||||
built=$(built_files) $(built_sql) $(built_pages) $(built_tests)
|
||||
asset_in_files=$(wildcard assets/*.in -type f)
|
||||
asset_files=$(asset_in_files:%.in=%)
|
||||
|
||||
all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql)
|
||||
help: ## Print this help
|
||||
$(Q)$(GREP) -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | $(SORT) | $(AWK) 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
|
||||
|
||||
all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql) ## Build and run smr in a chroot
|
||||
$(Q)$(ECHO) "[running] $@"
|
||||
$(Q)$(KODEV) run
|
||||
|
||||
conf/smr.conf : conf/smr.conf.in Makefile
|
||||
$(Q)$(ECHO) "[preprocess] $@"
|
||||
$(Q)$(SPP) -o $@ -D port=$(port) -D kore_chroot=$(chroot_dir) -D chuser=$(user) $<
|
||||
$(Q)$(SPP) -o $@ $(SPPFLAGS) $<
|
||||
|
||||
apk-tools-static-$(version).apk:
|
||||
# wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk
|
||||
|
||||
clean:
|
||||
clean: ## clean up all the files generated by this makefile
|
||||
$(Q)$(ECHO) "[clean] $@"
|
||||
$(Q)$(KODEV) clean
|
||||
$(Q)$(RM) $(page_files)
|
||||
$(Q)$(RM) conf/smr.conf
|
||||
$(Q)$(RM) src/pages/parts/story_breif.etlua
|
||||
$(Q)$(RM) src/lua/config.lua
|
||||
$(Q)$(RM) $(asset_files)
|
||||
|
||||
cloc:
|
||||
cloc src
|
||||
cloc: ## calculate source lines of code in smr
|
||||
cloc --force-lang="HTML",etlua.in src assets
|
||||
|
||||
$(chroot_dir): apk-tools-static-$(version).apk
|
||||
$(Q)$(MKDIR) $(chroot_dir)
|
||||
|
@ -68,31 +77,6 @@ $(chroot_dir): apk-tools-static-$(version).apk
|
|||
$(Q)$(MKDIR) $(chroot_dir)/data
|
||||
$(Q)$(MKDIR) $(chroot_dir)/data/archive
|
||||
$(Q)$(MKDIR) $(chroot_dir)/endpoints
|
||||
#cd $(chroot_dir) && tar -xvzf ../apk-tools-static-*.apk
|
||||
#cd $(chroot_dir) && sudo ./sbin/apk.static -X $(mirror)latest-stable/main -U --allow-untrusted --root $(chroot_dir) --no-cache --initdb add alpine-base
|
||||
#ln -s /dev/urandom $(chroot_dir)/dev/random #Prevent an attacker with access to the chroot from exhausting our entropy pool and causing a dos
|
||||
#ln -s /dev/urandom $(chroot_dir)/dev/urandom
|
||||
#mount /dev/ $(chroot_dir)/dev --bind
|
||||
#mount -o remount,ro,bind $(chroot_dir)/dev
|
||||
#mount -t proc none $(chroot_dir)/proc
|
||||
#mount -o bind /sys $(chroot_dir)/sys
|
||||
#cp /etc/resolv.conf $(chroot_dir)/etc/resolv.conf
|
||||
#echo "$(mirror)/$(branch)/main" > $(chroot)/etc/apk/repositories
|
||||
#echo "$(mirror)/$(branch)/community" >> $(chroot)/etc/apk/repositories
|
||||
#cp /etc/apk/repositories $(chroot_dir)/etc/apk/repositories
|
||||
#mkdir $(chroot_dir)/var/sm
|
||||
## Things to build lua libraries
|
||||
#chroot $(chroot_dir) apk add luarocks5.1 sqlite sqlite-dev lua5.1-dev build-base zlib zlib-dev
|
||||
#chroot $(chroot_dir) luarocks-5.1 install etlua
|
||||
#chroot $(chroot_dir) luarocks-5.1 install lsqlite3
|
||||
#chroot $(chroot_dir) luarocks-5.1 install lpeg
|
||||
#chroot $(chroot_dir) luarocks-5.1 install lua-zlib ZLIB_LIBDIR=/lib #for some reason lzlib looks in /usr/lib for libz, when it needs to look at /lib
|
||||
## Once we've built + installed everything, delete extra stuff from the chroot
|
||||
#chroot $(chroot_dir) apk del sqlite-dev lua5.1-dev build-base zlib-dev
|
||||
## SSL certificates, if you don't trust EFF (they have an antifa black block member as their favicon at time of writing) you may want to replace this.
|
||||
#chroot $(chroot_dir) apk add certbot
|
||||
## After chroot, apk add luarocks5.1 sqlite sqlite-dev lua5.1-dev build-base
|
||||
## After chroot, luarocks install etlua; luarocks install lsqlite3
|
||||
|
||||
code : $(built_files)
|
||||
|
||||
|
@ -106,15 +90,15 @@ $(built_pages): $(chroot_dir)pages/%.etlua : src/pages/%.etlua
|
|||
|
||||
src/lua/config.lua : src/lua/config.lua.in Makefile
|
||||
$(Q)$(ECHO) "[preprocess] $@"
|
||||
$(Q)$(SPP) -o $@ -D domain=$(domain) $<
|
||||
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
|
||||
|
||||
$(page_files) : % : %.in $(part_files)
|
||||
$(Q)$(ECHO) "[preprocess] $@"
|
||||
$(Q)$(SPP) -o $@ $<
|
||||
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
|
||||
|
||||
src/pages/parts/story_breif.etlua : src/pages/parts/story_breif.etlua.in
|
||||
$(Q)$(ECHO) "[preprocess] $@"
|
||||
$(Q)$(SPP) -o $@ $<
|
||||
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
|
||||
|
||||
$(built_sql): $(chroot_dir)sql/%.sql : src/sql/%.sql
|
||||
$(Q)$(ECHO) "[copy] $@"
|
||||
|
@ -124,9 +108,19 @@ $(built_tests) : $(chroot_dir)% : %
|
|||
$(Q)$(ECHO) "[copy] $@"
|
||||
$(Q)$(COPY) $^ $@
|
||||
|
||||
smr.so : $(src_files) conf/smr.conf conf/build.conf
|
||||
$(asset_files) : % : %.in
|
||||
$(Q)$(ECHO) "[preprocess] $@"
|
||||
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
|
||||
|
||||
smr.so : $(src_files) conf/smr.conf conf/build.conf $(asset_files)
|
||||
$(Q)$(ECHO) "[build] $@"
|
||||
$(Q)$(KODEV) build
|
||||
|
||||
test : $(built)
|
||||
$(Q)$(CD) kore_chroot && busted
|
||||
test : $(built) ## run the unit tests
|
||||
$(Q)$(CD) kore_chroot && busted -v --no-keep-going #--exclude-tags slow
|
||||
|
||||
cov : $(built) ## code coverage (based on unit tests)
|
||||
$(Q)$(RM) kore_chroot/luacov.stats.out
|
||||
$(Q)$(CD) kore_chroot && busted -v -c --no-keep-going #--exclude-tags slow
|
||||
$(Q)$(CD) kore_chroot && luacov endpoints/
|
||||
$(Q)$(ECHO) "open kore_chroot/luacov.report.out to view coverage results."
|
||||
|
|
41
README.md
41
README.md
|
@ -6,8 +6,26 @@ This repository contains the source code to a pastebin clone. It was made after
|
|||
concerns with pastebin.com taking down certain kinds of content. SMR aims to
|
||||
be small, fast, and secure. It is built on top of [Kore](https://kore.io), using
|
||||
[luajit](https://luajit.org) to expose a Lua programming environment. It uses
|
||||
[sqlite3](https://sqlite.org) as it's database. SMR is implemented in just over
|
||||
2k SLOC and is expected to never exceed 5k SLOC. Contributions welcome.
|
||||
[sqlite3](https://sqlite.org) as it's database. SMR is implemented in about
|
||||
4k SLOC and is expected to never exceed 5k SLOC. Contributions welcome.
|
||||
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
Language files blank comment code
|
||||
-------------------------------------------------------------------------------
|
||||
Lua 36 190 306 1993
|
||||
C 4 61 116 709
|
||||
HTML 18 21 0 561
|
||||
SQL 36 6 35 266
|
||||
JavaScript 3 19 21 203
|
||||
CSS 3 4 8 73
|
||||
C/C++ Header 4 3 0 46
|
||||
-------------------------------------------------------------------------------
|
||||
SUM: 104 304 486 3851
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
@ -15,12 +33,21 @@ be small, fast, and secure. It is built on top of [Kore](https://kore.io), using
|
|||
* Comments (complete)
|
||||
* Tags (complete)
|
||||
* Search (complete)
|
||||
* Author biographies
|
||||
* Archive
|
||||
* Archive (complete)
|
||||
* Author biographies (complete)
|
||||
* Kore 4.2.0 (complete)
|
||||
* addon api
|
||||
|
||||
TODO's:
|
||||
Currently, people can post comments to unlisted stories even if they don't have
|
||||
|
||||
* Currently, people can post comments to unlisted stories even if they don't have
|
||||
the correct link.
|
||||
* Find a replacement preprocessor
|
||||
* The archive is currently generated weekly from a cron job, and served
|
||||
syncronously. We can generate a zip file on-the-fly instead, and if the client
|
||||
disconnects, it's fine to drop the whole thing.
|
||||
* We can simplify a lot of error handling logic by setting sql prepared statements to reset during error unwinding.
|
||||
* We can simplify a lot of business logic by having requests parse their parameters eagerly.
|
||||
|
||||
## Hacking
|
||||
|
||||
|
@ -37,9 +64,9 @@ If you want to contribute to this repository:
|
|||
but everything should still work with later versions.
|
||||
6. Install [spp](https://github.com/radare/spp)
|
||||
7. Clone this repository into the smr folder, cd into the root, and run `make`!
|
||||
* You may need to modify the configuration in the Makefile, add test.monster 127.0.0.1 to your `/etc/hosts`, modify command invocation, ect.
|
||||
* You may need to modify the configuration in the Makefile, add `test.monster 127.0.0.1` to your `/etc/hosts`, modify command invocation, ect.
|
||||
|
||||
## Misc notes.
|
||||
## Misc. notes
|
||||
|
||||
SMR requires a slightly modified version of Kore to run. See [my kore patches](https://git.fuwafuwa.moe/rmalley/kore_patches)
|
||||
for the changes I needed to make to get the JIT compiler playing nice with
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
Stores the scroll location on a story to local storage, and re-scroll to the
|
||||
position next time the page is loaded.
|
||||
*/
|
||||
window.onbeforeunload = function(e) {
|
||||
localStorage.setItem(window.location.pathname,window.scrollY)
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function(e) {
|
||||
var scrollpos = localStorage.getItem(window.location.pathname)
|
||||
if(scrollpos)
|
||||
window.scrollTo(0,scrollpos)
|
||||
})
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
There's a delete buttotn to delete a post. If javascript is enabled, replace
|
||||
the button with one that will ask for confirmation before deleting.
|
||||
*/
|
||||
|
||||
function delete_intervine(){
|
||||
var forms = document.getElementsByTagName("form");
|
||||
if(forms.length == 0){return;}//Don't load if the story is missing.
|
||||
var delete_form;
|
||||
for(var i = 0; i < forms.length; i++){
|
||||
if(forms[i].action.endsWith("_delete")){
|
||||
delete_form = forms[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(delete_form == null){return;}//Don't load if we're not logged in
|
||||
var delete_parent = delete_form.parentNode;
|
||||
delete_parent.removeChild(delete_form);
|
||||
var delete_wrapper = document.createElement("div");
|
||||
var delete_button = document.createElement("button");
|
||||
delete_button.classList.add("button");
|
||||
delete_button.classList.add("column");
|
||||
delete_button.classList.add("column-0");
|
||||
delete_button.textContent = "Delete";
|
||||
delete_button.addEventListener("click",function(){
|
||||
if(confirm("Are you sure you want to delete this story?")){
|
||||
document.documentElement.appendChild(delete_form);
|
||||
delete_form.submit();
|
||||
}
|
||||
});
|
||||
delete_parent.appendChild(delete_wrapper);
|
||||
delete_wrapper.appendChild(delete_button);
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded",delete_intervine,false);
|
|
@ -9,6 +9,7 @@ body{
|
|||
}
|
||||
h1,h2,h3{line-height:1.2}
|
||||
p,.tag-list{margin-bottom:0px}
|
||||
.spacer{margin-bottom:1em}
|
||||
.spoiler,.spoiler2{background:#444}
|
||||
.spoiler:hover,.spoiler2:hover{color:#FFF}
|
||||
.greentext{color:#282}
|
||||
|
@ -31,6 +32,7 @@ p,.tag-list{margin-bottom:0px}
|
|||
}
|
||||
.column-0{margin-right:5px}
|
||||
.label-inline{margin:0.5rem}
|
||||
.biography{border:1px solid #9b4dca}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
body, input, select, textarea, pre, code{
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
.tag-suggestion, .tag-suggestion>input {
|
||||
height: 1rem !important;
|
||||
margin:0px;
|
||||
}
|
||||
.tag-suggestion{
|
||||
font-size:0.8rem !important;
|
||||
display:block !important;
|
||||
}
|
||||
.tag-suggestion>input{
|
||||
line-height:1rem !important;
|
||||
width:100% !important;
|
||||
text-align:left;
|
||||
background-color:transparent;
|
||||
color:black;
|
||||
border:none;
|
||||
padding:0px;
|
||||
}
|
||||
.tag-suggestion-list{
|
||||
list-style: none;
|
||||
margin-top:3.8rem;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
border-top: 0px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
body, input, select, textarea, pre, code, .tag-suggestion-list{
|
||||
background: #1c1428;
|
||||
color: #d0d4d8 !important;
|
||||
}
|
||||
.spoiler, .spoiler2{color:#444;}
|
||||
}
|
|
@ -1,11 +1,41 @@
|
|||
console.log("Hello, world!");
|
||||
|
||||
tag_suggestion_list = {
|
||||
list_element: null,
|
||||
/*Singleton object*/
|
||||
var tag_suggestion_list = {
|
||||
input_el: null,
|
||||
list_element: document.createElement('ol'),
|
||||
suggestion_elements: [],
|
||||
hover_last: -1,
|
||||
}
|
||||
tag_suggestion_list.list_element.setAttribute("class","tag-suggestion-list");
|
||||
tag_suggestion_list.list_element.setAttribute("style","position:absolute;");
|
||||
|
||||
function appendTag(name){
|
||||
return function(event){
|
||||
var ie = tag_suggestion_list.input_el;
|
||||
var prev = ie.value.split(";");
|
||||
prev.pop();
|
||||
prev.push(name);
|
||||
ie.value = prev.join(";");
|
||||
ie.value += ";";
|
||||
ie.focus();
|
||||
tag_suggestion_list.list_element.style = "display:none;";
|
||||
}
|
||||
}
|
||||
|
||||
var lel = document.createElement('div');
|
||||
function hoverTag(name, root){
|
||||
return function(event){
|
||||
var ie = tag_suggestion_list.input_el;
|
||||
if(ie.value.slice(-1) == ";"){//comming from another tab completion
|
||||
var prev = ie.value.slice(hover_last);
|
||||
|
||||
}else{
|
||||
var prev = ie.value.split(";");
|
||||
prev.pop()
|
||||
prev.push(name)
|
||||
ie.value = prev.join(";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stolen from medium.com/@jh3y
|
||||
|
@ -15,75 +45,114 @@ var lel = document.createElement('div');
|
|||
* @param {number} selectionPoint - the selection point for the input
|
||||
*/
|
||||
function getCursorXY(input, selectionPoint){
|
||||
const {
|
||||
offsetLeft: inputX,
|
||||
offsetTop: inputY,
|
||||
} = input
|
||||
// create a dummy element that will be a clone of our input
|
||||
const div = document.createElement('div')
|
||||
// get the computed style of the input and clone it onto the dummy element
|
||||
const copyStyle = getComputedStyle(input)
|
||||
for (const prop of copyStyle) {
|
||||
div.style[prop] = copyStyle[prop]
|
||||
}
|
||||
// we need a character that will replace whitespace when filling our dummy element if it's a single line <input/>
|
||||
const swap = '.'
|
||||
const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
|
||||
// set the div content to that of the textarea up until selection
|
||||
const textContent = inputValue.substr(0, selectionPoint)
|
||||
// set the text content of the dummy element div
|
||||
div.textContent = textContent
|
||||
if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
|
||||
// if a single line input then the div needs to be single line and not break out like a text area
|
||||
if (input.tagName === 'INPUT') div.style.width = 'auto'
|
||||
// create a marker element to obtain caret position
|
||||
const span = document.createElement('span')
|
||||
// give the span the textContent of remaining content so that the recreated dummy element is as close as possible
|
||||
span.textContent = inputValue.substr(selectionPoint) || '.'
|
||||
// append the span marker to the div
|
||||
div.appendChild(span)
|
||||
// append the dummy element to the body
|
||||
document.body.appendChild(div)
|
||||
// get the marker position, this is the caret position top and left relative to the input
|
||||
const { offsetLeft: spanX, offsetTop: spanY } = span
|
||||
// lastly, remove that dummy element
|
||||
// NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered
|
||||
document.body.removeChild(div)
|
||||
// return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input
|
||||
return {
|
||||
x: inputX + spanX,
|
||||
y: inputY + spanY,
|
||||
}
|
||||
const {
|
||||
offsetLeft: inputX,
|
||||
offsetTop: inputY,
|
||||
} = input
|
||||
const div = document.createElement('div')
|
||||
const copyStyle = getComputedStyle(input)
|
||||
for (const prop of copyStyle) {
|
||||
div.style[prop] = copyStyle[prop]
|
||||
}
|
||||
const swap = '.'
|
||||
const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
|
||||
const textContent = inputValue.substr(0, selectionPoint)
|
||||
div.textContent = textContent
|
||||
if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
|
||||
if (input.tagName === 'INPUT') div.style.width = 'auto'
|
||||
const span = document.createElement('span')
|
||||
span.textContent = inputValue.substr(selectionPoint) || '.'
|
||||
div.appendChild(span)
|
||||
document.body.appendChild(div)
|
||||
const { offsetLeft: spanX, offsetTop: spanY } = span
|
||||
document.body.removeChild(div)
|
||||
return {
|
||||
x: inputX + spanX,
|
||||
y: inputY + spanY,
|
||||
}
|
||||
}
|
||||
|
||||
function display_suggestions(elem, sugg, event){
|
||||
console.log("sugg:",sugg);
|
||||
//Check that the value hasn't change since we fired
|
||||
//off the request
|
||||
var tags_so_far = elem.value.split(";");
|
||||
recent = elem.value.split(";").pop().trim();
|
||||
if(recent == sugg[0]){
|
||||
var sugx, sugy = getCursorXY(elem,elem.value.length);
|
||||
console.log("Looking at position to display suggestions:",sugx, sugy);
|
||||
var v = getCursorXY(elem,elem.value.length);
|
||||
var sugx = v.x;
|
||||
var sugy = v.y;
|
||||
var sty = `position:absolute; margin-left:${sugx}px;`;
|
||||
tag_suggestion_list.list_element.style = sty;
|
||||
for(var i in tag_suggestion_list.suggestion_elements){
|
||||
tag_suggestion_list.list_element.removeChild(tag_suggestion_list.suggestion_elements[i]);
|
||||
|
||||
}
|
||||
tag_suggestion_list.suggestion_elements = [];
|
||||
var hover_last = 0;
|
||||
for(var i in tags_so_far){
|
||||
hover_last += tags_so_far[i].length + 1;
|
||||
}
|
||||
tag_suggestion_list.hover_last = hover_last;
|
||||
for(var i in sugg){
|
||||
console.log("Displaying suggestion:",sugg[i]);
|
||||
lel.setAttribute('style',`left: $(sugx)px; top: $(sugy)px;`);
|
||||
if(i == 0){
|
||||
continue;
|
||||
}
|
||||
var suggestion_el = document.createElement("li");
|
||||
var suggestion_but = document.createElement("input")
|
||||
suggestion_el.appendChild(suggestion_but);
|
||||
suggestion_but.setAttribute("type","button");
|
||||
suggestion_but.setAttribute("value",sugg[i]);
|
||||
suggestion_el.setAttribute("class"," button-clear tag-suggestion");
|
||||
tag_suggestion_list.list_element.appendChild(suggestion_el);
|
||||
tag_suggestion_list.suggestion_elements.push(suggestion_el);
|
||||
suggestion_but.onkeyup = function(event){
|
||||
if(event.key == "Tab"){
|
||||
hoverTag(event.target.value)(event);
|
||||
}else if(event.key == ";"){
|
||||
appendTag(event.target.value)(event);
|
||||
}
|
||||
|
||||
}
|
||||
suggestion_but.onclick = function(event){
|
||||
appendTag(event.target.value)(event);
|
||||
}
|
||||
suggestion_but.onblur = function(event){
|
||||
var other_input = false;
|
||||
for(var i in tag_suggestion_list.suggestion_elements){
|
||||
if(tag_suggestion_list.suggestion_elements[i].firstChild == event.relatedTarget){
|
||||
other_input = true;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if(!other_input){
|
||||
tag_suggestion_list.list_element.style = "display:none;";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(tag_suggestion_list.suggestion_elements.length > 0){
|
||||
|
||||
var last_element = tag_suggestion_list.suggestion_elements[tag_suggestion_list.suggestion_elements.length - 1];
|
||||
//last_element.firstChild.last_element = true;
|
||||
last_element.firstChild.onblur = function(event){
|
||||
tag_suggestion_list.suggestion_elements[0].firstChild.focus();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hint_tags(elem, event){
|
||||
//Get the most recent tag
|
||||
recent = elem.value.split(";").pop().trim();
|
||||
var recent = elem.value.split(";").pop().trim();
|
||||
if(recent.length > 0){
|
||||
console.log("Most recent tag:",recent);
|
||||
//Ask the server for tags that look like this
|
||||
xhr = new XMLHttpRequest();
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "/_api?call=suggest&data=" + recent);
|
||||
xhr.onreadystatechange = function(e){
|
||||
if(xhr.readyState === 4){
|
||||
console.log("Event:",e);
|
||||
suggestions = xhr.response.split(";");
|
||||
console.log("suggestions:",suggestions);
|
||||
display_suggestions(elem,suggestions, event);
|
||||
|
||||
}
|
||||
|
@ -93,13 +162,34 @@ function hint_tags(elem, event){
|
|||
}
|
||||
|
||||
function init(){
|
||||
tag_el_list = document.getElementsByName("tags");
|
||||
var head_el = document.head;
|
||||
var extra_css_el = document.createElement("link");
|
||||
document.head.appendChild(extra_css_el);
|
||||
extra_css_el.setAttribute("rel","stylesheet");
|
||||
extra_css_el.setAttribute("href","/_css/suggest_tags.css");
|
||||
var tag_el_list = document.getElementsByName("tags");
|
||||
console.assert(tag_el_list.length == 1);
|
||||
tag_el = tag_el_list[0];
|
||||
var tag_el = tag_el_list[0];
|
||||
tag_suggestion_list.input_el = tag_el;
|
||||
tag_el.onkeyup = function(event){
|
||||
console.log("Looking at tag:", event);
|
||||
console.log("And element:",tag_el);
|
||||
hint_tags(tag_el, event);
|
||||
}
|
||||
tag_el.onblur = function(event){
|
||||
var not_suggestion = true;
|
||||
var ies = tag_suggestion_list.suggestion_elements;
|
||||
for(var i in ies){
|
||||
if(event.relatedTarget == ies[i].firstChild){
|
||||
not_suggestion = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(not_suggestion){
|
||||
tag_suggestion_list.list_element.style = "display:none;";
|
||||
}
|
||||
}
|
||||
|
||||
var fieldset = tag_el.parentNode;
|
||||
fieldset.appendChild(tag_suggestion_list.list_element);
|
||||
var paste_el = document.getElementsByName("tags");
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded",init,false);
|
||||
|
|
|
@ -26,7 +26,7 @@ dev {
|
|||
# These flags are added to the shared ones when
|
||||
# you build the "dev" flavor.
|
||||
ldflags=-llua5.1
|
||||
cflags=-g -Wextra
|
||||
cflags=-g -Wall -Wextra -Werror
|
||||
cflags=-I/usr/include/lua5.1
|
||||
cxxflags=-g -Wextra
|
||||
}
|
||||
|
|
237
conf/smr.conf.in
237
conf/smr.conf.in
|
@ -6,14 +6,21 @@ server tls {
|
|||
}
|
||||
|
||||
seccomp_tracing yes
|
||||
|
||||
privsep worker {
|
||||
runas <{get chuser }>
|
||||
|
||||
root <{get kore_chroot }>
|
||||
|
||||
}
|
||||
privsep keymgr {
|
||||
runas <{get chuser }>
|
||||
|
||||
root .
|
||||
}
|
||||
|
||||
load ./smr.so
|
||||
root <{get kore_chroot}>
|
||||
|
||||
runas <{get chuser }>
|
||||
|
||||
keymgr_runas <{get chuser }>
|
||||
|
||||
keymgr_root .
|
||||
workers 1
|
||||
|
||||
http_body_max 8388608
|
||||
|
@ -38,83 +45,169 @@ domain * {
|
|||
#I run kore behind a lighttpd reverse proxy, so this is a bit useless to me
|
||||
accesslog /dev/null
|
||||
|
||||
route / home
|
||||
route /_css/style.css asset_serve_style_css
|
||||
route /_css/milligram.css asset_serve_milligram_css
|
||||
route /_css/milligram.min.css.map asset_serve_milligram_min_css_map
|
||||
route /_faq asset_serve_faq_html
|
||||
route /_js/suggest_tags.js asset_serve_suggest_tags_js
|
||||
route /favicon.ico asset_serve_favicon_ico
|
||||
route /_paste post_story
|
||||
route /_edit edit_story
|
||||
route /_bio edit_bio
|
||||
route /_login login
|
||||
route /_logout logout
|
||||
route ^/_claim claim
|
||||
route /_download download
|
||||
route /_preview preview
|
||||
route /_search search
|
||||
route /_archive archive
|
||||
route /_api api
|
||||
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
|
||||
route ^/[^_].* read_story
|
||||
route / {
|
||||
handler home
|
||||
methods get
|
||||
}
|
||||
|
||||
params get /_edit {
|
||||
validate story v_storyid
|
||||
route /_css/style.css {
|
||||
handler asset_serve_style_css
|
||||
methods get
|
||||
}
|
||||
params get /_download {
|
||||
validate story v_storyid
|
||||
validate pwd v_hex_128
|
||||
|
||||
route /_css/suggest_tags.css {
|
||||
handler asset_serve_style_css
|
||||
methods get
|
||||
}
|
||||
params post /_edit {
|
||||
validate title v_any
|
||||
validate story v_storyid
|
||||
validate text v_any
|
||||
validate pasteas v_subdomain
|
||||
validate markup v_markup
|
||||
validate tags v_any
|
||||
validate unlisted v_checkbox
|
||||
|
||||
route /_css/milligram.css {
|
||||
handler asset_serve_milligram_css
|
||||
methods get
|
||||
}
|
||||
params post /_paste {
|
||||
validate title v_any
|
||||
validate text v_any
|
||||
validate pasteas v_subdomain
|
||||
validate markup v_markup
|
||||
validate tags v_any
|
||||
validate unlisted v_checkbox
|
||||
|
||||
route /_css/milligram.min.css.map {
|
||||
handler asset_serve_milligram_min_css_map
|
||||
methods get
|
||||
}
|
||||
params post /_preview {
|
||||
validate title v_any
|
||||
validate text v_any
|
||||
validate pasteas v_subdomain
|
||||
validate markup v_markup
|
||||
validate tags v_any
|
||||
validate unlisted v_checkbox
|
||||
|
||||
route /_faq {
|
||||
handler asset_serve_faq_html
|
||||
methods get
|
||||
}
|
||||
params get /_search {
|
||||
validate q v_any
|
||||
|
||||
route /_js/suggest_tags.js {
|
||||
handler asset_serve_suggest_tags_js
|
||||
methods get
|
||||
}
|
||||
params get /_archive {
|
||||
validate t v_time
|
||||
|
||||
route /_js/bookmark.js {
|
||||
handler asset_serve_bookmark_js
|
||||
methods get
|
||||
}
|
||||
params get ^/[^_].* {
|
||||
validate comments v_bool
|
||||
validate pwd v_hex_128
|
||||
|
||||
route /_js/intervine_deletion.js {
|
||||
handler asset_serve_intervine_deletion_js
|
||||
methods get
|
||||
}
|
||||
params post ^/[^_].* {
|
||||
validate text v_any
|
||||
validate postas v_subdomain
|
||||
validate pwd v_hex_128
|
||||
|
||||
route /favicon.ico {
|
||||
handler asset_serve_favicon_ico
|
||||
methods get
|
||||
}
|
||||
params post /_login {
|
||||
validate user v_subdomain
|
||||
validate pass v_any
|
||||
|
||||
route /_paste {
|
||||
handler post_story
|
||||
methods get post
|
||||
|
||||
validate post title v_any
|
||||
validate post text v_any
|
||||
validate post pasteas v_subdomain
|
||||
validate post markup v_markup
|
||||
validate post tags v_any
|
||||
validate post unlisted v_checkbox
|
||||
}
|
||||
params post ^/_claim {
|
||||
validate user v_subdomain
|
||||
|
||||
route /_edit {
|
||||
handler edit_story
|
||||
methods get post
|
||||
|
||||
validate qs:get story v_storyid
|
||||
|
||||
validate post title v_any
|
||||
validate post story v_storyid
|
||||
validate post text v_any
|
||||
validate post pasteas v_subdomain
|
||||
validate post markup v_markup
|
||||
validate post tags v_any
|
||||
validate post unlisted v_checkbox
|
||||
}
|
||||
params get /_api {
|
||||
validate call v_any
|
||||
validate data v_any
|
||||
|
||||
route /_bio {
|
||||
handler edit_bio
|
||||
methods get post
|
||||
|
||||
validate post text v_any
|
||||
validate post author v_subdomain
|
||||
}
|
||||
|
||||
route /_login {
|
||||
handler login
|
||||
methods get post
|
||||
|
||||
validate post user v_subdomain
|
||||
validate post pass v_any
|
||||
}
|
||||
|
||||
route /_logout {
|
||||
handler logout
|
||||
methods get
|
||||
}
|
||||
|
||||
route ^/_claim {
|
||||
handler claim
|
||||
methods get post
|
||||
|
||||
validate post user v_subdomain
|
||||
}
|
||||
|
||||
route /_download {
|
||||
handler download
|
||||
methods get
|
||||
|
||||
validate qs:get story v_storyid
|
||||
validate qs:get pwd v_hex_128
|
||||
}
|
||||
|
||||
route /_preview {
|
||||
handler preview
|
||||
methods post
|
||||
|
||||
validate post title v_any
|
||||
validate post text v_any
|
||||
validate post pasteas v_subdomain
|
||||
validate post markup v_markup
|
||||
validate post tags v_any
|
||||
validate post unlisted v_checkbox
|
||||
}
|
||||
|
||||
route /_search {
|
||||
handler search
|
||||
methods get
|
||||
|
||||
validate qs:get q v_any
|
||||
}
|
||||
|
||||
route /_archive {
|
||||
handler archive
|
||||
methods get
|
||||
|
||||
validate qs:get t v_time
|
||||
}
|
||||
|
||||
route /_api {
|
||||
handler api
|
||||
methods get
|
||||
|
||||
validate qs:get call v_any
|
||||
validate qs:get data v_any
|
||||
}
|
||||
|
||||
route /_delete {
|
||||
handler delete
|
||||
methods post
|
||||
|
||||
validate post story v_storyid
|
||||
}
|
||||
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
|
||||
route ^/[^_].* {
|
||||
handler read_story
|
||||
methods get post
|
||||
|
||||
validate qs:get comments v_bool
|
||||
validate qs:get pwd v_hex_128
|
||||
|
||||
validate post text v_any
|
||||
validate post postas v_subdomain
|
||||
validate post pwd v_hex_128
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
_G.spy = spy
|
||||
local mock_env = require("spec.env_mock")
|
||||
local rng = require("spec.fuzzgen")
|
||||
|
||||
describe("smr biography",function()
|
||||
setup(mock_env.setup)
|
||||
teardown(mock_env.teardown)
|
||||
it("should allow users to set their biography",function()
|
||||
local claim_post = require("endpoints.claim_post")
|
||||
local login_post = require("endpoints.login_post")
|
||||
local index_get = require("endpoints.index_get")
|
||||
local bio_get = require("endpoints.bio_get")
|
||||
local bio_post = require("endpoints.bio_post")
|
||||
local db = require("db")
|
||||
local config = require("config")
|
||||
config.domain = "test.host"
|
||||
configure()
|
||||
local username = rng.subdomain()
|
||||
local claim_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_claim",
|
||||
args = {
|
||||
user = username
|
||||
}
|
||||
}
|
||||
claim_post(claim_req)
|
||||
local login_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_login",
|
||||
args = {
|
||||
user = username
|
||||
},
|
||||
file = {
|
||||
pass = claim_req.response
|
||||
}
|
||||
}
|
||||
login_post(login_req)
|
||||
local cookie = login_req.response_headers["set-cookie"]
|
||||
local sessionid = cookie:match("session=([^;]+)")
|
||||
local home_req_get = {
|
||||
method = "GET",
|
||||
host = username .. ".test.host",
|
||||
path = "/",
|
||||
cookies = {
|
||||
session = sessionid
|
||||
}
|
||||
}
|
||||
index_get(home_req_get)
|
||||
local edit_bio_button = '<a href="/_bio"'
|
||||
assert(
|
||||
home_req_get.response:find(edit_bio_button),
|
||||
"After logging in the user should have a button to" ..
|
||||
" edit their biography. Looking for " .. edit_bio_button
|
||||
.. " but didn't find it in " .. home_req_get.response
|
||||
)
|
||||
local edit_bio_req_get = {
|
||||
method = "GET",
|
||||
host = username .. ".test.host",
|
||||
path = "/_bio",
|
||||
cookies = {
|
||||
session = sessionid
|
||||
},
|
||||
args = {}
|
||||
}
|
||||
bio_get(edit_bio_req_get)
|
||||
assert(edit_bio_req_get.responsecode == 200)
|
||||
--[=[
|
||||
local paste_req_post = {
|
||||
method = "POST",
|
||||
host = username .. ".test.host",
|
||||
path = "/_paste",
|
||||
cookies = {
|
||||
session = sessionid
|
||||
},
|
||||
args = {
|
||||
title = "post title",
|
||||
text = "post text",
|
||||
markup = "plain",
|
||||
tags = "",
|
||||
}
|
||||
}
|
||||
paste_post(paste_req_post)
|
||||
for row in db.conn:rows("SELECT COUNT(*) FROM posts") do
|
||||
assert(row[1] == 1, "Expected exactly 1 post in sample db")
|
||||
end
|
||||
local code = paste_req_post.responsecode
|
||||
assert(code >= 300 and code <= 400, "Should receive a redirect after posting, got:" .. tostring(code))
|
||||
assert(paste_req_post.response_headers, "Should have received some response headers")
|
||||
assert(paste_req_post.response_headers.Location, "Should have received a location in response headers")
|
||||
local redirect = paste_req_post.response_headers.Location:match("(/[^/]*)$")
|
||||
local read_req_get = {
|
||||
method = "GET",
|
||||
host = username .. ".test.host",
|
||||
path = redirect,
|
||||
cookies = {
|
||||
session = sessionid
|
||||
},
|
||||
args = {}
|
||||
}
|
||||
read_get(read_req_get)
|
||||
local response = read_req_get.response
|
||||
assert(
|
||||
response:find([[post title]]),
|
||||
"Failed to find post title in response."
|
||||
)
|
||||
assert(
|
||||
response:find('By <a href="https://' .. username .. '.test.host">' .. username .. '</a>'),
|
||||
"Failed to find the author name after a paste."
|
||||
)
|
||||
assert(
|
||||
response:find([[post text]]),
|
||||
"Failed to find post text in response."
|
||||
)
|
||||
]=]
|
||||
end)
|
||||
end)
|
|
@ -0,0 +1,120 @@
|
|||
|
||||
_G.spy = spy
|
||||
local mock_env = require("spec.env_mock")
|
||||
|
||||
describe("smr cacheing",function()
|
||||
setup(mock_env.setup)
|
||||
teardown(mock_env.teardown)
|
||||
it("caches a page if the page is requested twice #working",function()
|
||||
local read_get = require("endpoints.read_get")
|
||||
local cache = require("cache")
|
||||
renderspy = spy.on(cache,"render")
|
||||
configure()
|
||||
local req = {
|
||||
method = "GET",
|
||||
path = "/a",
|
||||
args = {},
|
||||
host = "test.host"
|
||||
}
|
||||
assert.spy(renderspy).called(0)
|
||||
read_get(req)
|
||||
assert.spy(renderspy).called(1)
|
||||
read_get(req)
|
||||
assert.spy(renderspy).called(2)
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 1, string.format(
|
||||
"Exepected only one cache entry after" ..
|
||||
"calling test.host/a 2 times " ..
|
||||
", but got %d rows.", row[1]
|
||||
))
|
||||
end
|
||||
end)
|
||||
it("does not cache the page if the user is logged in", function()
|
||||
local read_get = require("endpoints.read_get")
|
||||
local cache = require("cache")
|
||||
renderspy = spy.on(cache,"render")
|
||||
configure()
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 0, string.format(
|
||||
"Cache should not have any rows before " ..
|
||||
"request have been made."
|
||||
))
|
||||
end
|
||||
local req = mock_env.session()
|
||||
req.method = "GET"
|
||||
req.path = "/a"
|
||||
req.args = {}
|
||||
read_get(req)
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 0, string.format(
|
||||
"Cache should not cache requests made by " ..
|
||||
"logged in users."
|
||||
))
|
||||
end
|
||||
end)
|
||||
it("caches one page for domain/id and author.domain/id",function()
|
||||
local read_get = require("endpoints.read_get")
|
||||
local cache = require("cache")
|
||||
configure()
|
||||
local req_m = {__index = {
|
||||
method = "GET",
|
||||
path = "/a",
|
||||
args = {}
|
||||
}}
|
||||
local base_host = {host="test.host"}
|
||||
local base_req = setmetatable({host="test.host"},req_m)
|
||||
read_get(base_req)
|
||||
local user_req = setmetatable({host="admin.test.host"},req_m)
|
||||
read_get(user_req)
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 1, string.format(
|
||||
"Exepected only one cache entry for" ..
|
||||
"'test.host/a' and 'admin.test.host/a'" ..
|
||||
", but got %d rows.", row[1]
|
||||
))
|
||||
end
|
||||
end)
|
||||
it("detours configure",function()
|
||||
local s = {}
|
||||
local c = false
|
||||
local oldconfigure = configure
|
||||
--local index_get = require("endpoints.index_get")
|
||||
--configure(s)
|
||||
--assert(c)
|
||||
end)
|
||||
describe("author home page",function()
|
||||
it("lists all stories by that author",function()
|
||||
local read_get = require("endpoints.index_get")
|
||||
local cache = require("cache")
|
||||
configure()
|
||||
local req_m = {__index = {
|
||||
method = "GET",
|
||||
path = "/a",
|
||||
args = {}
|
||||
}}
|
||||
local base_host = {host="user.test.host"}
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 0, string.format(
|
||||
"Before requesting user homepage " ..
|
||||
"there should not be any pages in the " ..
|
||||
"cache."
|
||||
))
|
||||
end
|
||||
local base_req = setmetatable({host="user.test.host"},req_m)
|
||||
read_get(base_req)
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 1, string.format(
|
||||
"After reading the autor home page, " ..
|
||||
" only that page should be cached."
|
||||
))
|
||||
end
|
||||
read_get(base_req)
|
||||
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
|
||||
assert(row[1] == 1, string.format(
|
||||
"After reading the autor home page " ..
|
||||
" twice only that page should be cached."
|
||||
))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
|
@ -0,0 +1,249 @@
|
|||
|
||||
local config = require("config")
|
||||
config.db = "data/unittest.db"
|
||||
local mock = {}
|
||||
local env = {}
|
||||
mock.env = env
|
||||
--Mirror print prior to lua 5.4
|
||||
--local oldprint = print
|
||||
local ntostring
|
||||
|
||||
-- Modules that get required lazily
|
||||
local login_post
|
||||
local fuzzy
|
||||
local claim_post
|
||||
print_table= function(...)
|
||||
print("Print called")
|
||||
local args = {...}
|
||||
local mapped_args = {}
|
||||
for k,v in ipairs(args) do
|
||||
print("mapping",v)
|
||||
mapped_args[k] = ntostring(v)
|
||||
end
|
||||
print(table.concat(mapped_args,"\t"))
|
||||
end
|
||||
|
||||
local tables_called = {}
|
||||
function ntostring(arg)
|
||||
io.stdout:write("Calling tostring with:",tostring(arg),"\n")
|
||||
if type(arg) ~= "table" then
|
||||
return tostring(arg)
|
||||
end
|
||||
local function tbl_to_string(tbl,indent)
|
||||
if tables_called[tbl] then
|
||||
return tostring(tbl)
|
||||
end
|
||||
tables_called[tbl] = true
|
||||
if type(tbl) ~= "table" then
|
||||
error("tbl_to_string must be called with a table, got a " .. type(tbl))
|
||||
end
|
||||
local lines = {string.rep("\t",indent) .. "{"}
|
||||
for k, v in pairs(tbl) do
|
||||
local kv = {}
|
||||
for i,n in pairs{k,v} do
|
||||
if type(n) == "table" then
|
||||
kv[i] = string.format("%q",tbl_to_string(n,indent+1))
|
||||
else
|
||||
kv[i] = string.format("%q",tostring(n))
|
||||
end
|
||||
end
|
||||
table.insert(
|
||||
lines,
|
||||
string.rep("\t",indent+1) .. kv[1] .. ":" .. kv[2]
|
||||
)
|
||||
end
|
||||
table.insert(lines,string.rep("\t",indent) .. "}")
|
||||
return table.concat(lines,"\n")
|
||||
end
|
||||
--It's a table
|
||||
local ret = tbl_to_string(arg,0)
|
||||
tables_called = {}
|
||||
return ret
|
||||
end
|
||||
|
||||
local smr_mock_env = {
|
||||
--An empty function that gets called to set up databases and do other
|
||||
--startup-time stuff, runs once for each worker process.
|
||||
configure = spy.new(function(...) end),
|
||||
http_request_get_host = spy.new(function(req) return req.host or "test.host" 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_post = spy.new(function(req) req.post_populated = true end),
|
||||
http_populate_multipart_form = spy.new(function(req) req.post_populated = true end),
|
||||
http_argument_get_string = spy.new(function(req,str)
|
||||
assert(req.args,"requests should have a .args table")
|
||||
assert(
|
||||
req.method == "GET" and req.qs_populated or
|
||||
req.method == "POST" and req.post_populated,[[
|
||||
http_argument_get_string() can only be called after
|
||||
the appropriate populate method has been called, either
|
||||
http_request_populate_qs(req) or
|
||||
http_request_populate_post(req)]]
|
||||
)
|
||||
return req.args[str]
|
||||
end),
|
||||
http_file_get = spy.new(function(req,filename)
|
||||
assert(req.multipart_forum_populated,[[
|
||||
http_file_get() can only be called after the approriate
|
||||
populate method has been called. (http_populate_multipart_form())
|
||||
]])
|
||||
return req.file["pass"]
|
||||
end),
|
||||
http_response = spy.new(function(req,errcode,html)
|
||||
req.responsecode = errcode
|
||||
req.response = html
|
||||
end),
|
||||
http_response_header = spy.new(function(req,name,value)
|
||||
req.response_headers = req.response_headers or {}
|
||||
req.response_headers[name] = value
|
||||
end),
|
||||
http_method_text = spy.new(function(req) return req.method end),
|
||||
http_populate_cookies = spy.new(function(req)
|
||||
req.cookies_populated = true
|
||||
req.cookies = req.cookies or {}
|
||||
end),
|
||||
http_request_cookie = spy.new(function(req,cookie_name)
|
||||
assert(req.cookies_populated,[[
|
||||
http_request_cookie() can only be called after
|
||||
http_populate_cookies() has been called.
|
||||
]])
|
||||
return req.cookies[cookie_name]
|
||||
end),
|
||||
http_response_cookie = spy.new(function(req,name,value) req.cookies = {[name] = value} end),
|
||||
log = spy.new(function(priority, message) --[[print(string.format("[LOG %q]: %s",priority,message))]] end),
|
||||
--Logging:
|
||||
LOG_DEBUG = "debug",
|
||||
LOG_INFO = "info",
|
||||
LOG_NOTICE = "notice",
|
||||
LOG_WARNING = "warning",
|
||||
LOG_ERR = "error",
|
||||
LOG_CRIT = "critical",
|
||||
LOG_ALERT = "alert",
|
||||
LOG_EMERG = "emergency",
|
||||
sha3 = spy.new(function(message) return "digest" end),
|
||||
}
|
||||
|
||||
local smr_mock_env_m = {
|
||||
__index = smr_mock_env,
|
||||
__newindex = function(self,key,value)
|
||||
local setter = debug.getinfo(2)
|
||||
if setter.source ~= "=[C]" and key ~= "configure" then
|
||||
error(string.format(
|
||||
"Tried to create a global %q with value %s\n%s",
|
||||
key,
|
||||
tostring(value),
|
||||
debug.traceback()
|
||||
),2)
|
||||
else
|
||||
rawset(self,key,value)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local sfmt = string.format
|
||||
local string_fmt_override = {
|
||||
format = spy.new(function(fmt,...)
|
||||
local args = {...}
|
||||
for i = 1,#args do
|
||||
if args[i] == nil then
|
||||
args[i] = "nil"
|
||||
end
|
||||
end
|
||||
table.insert(args,1,fmt)
|
||||
return sfmt(unpack(args))
|
||||
end)
|
||||
}
|
||||
setmetatable(string_fmt_override,{__index = string})
|
||||
local smr_override_env = {
|
||||
--Detour assert so we don't actually perform any checks
|
||||
assert = spy.new(function(bool,msg,level) return bool end),
|
||||
--Allow string.format to accept nil as arguments
|
||||
string = string_fmt_override
|
||||
}
|
||||
|
||||
mock.olds = {}
|
||||
|
||||
function mock.setup()
|
||||
setmetatable(_G,smr_mock_env_m)
|
||||
for k,v in pairs(smr_override_env) do
|
||||
mock.olds[k] = _G[k]
|
||||
_G[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function mock.mockdb()
|
||||
local config = require("config")
|
||||
config.db = "data/unittest.db"
|
||||
assert(os.execute("rm " .. config.db))
|
||||
package.loaded.db = nil
|
||||
local db = require("db")
|
||||
end
|
||||
|
||||
function mock.teardown()
|
||||
setmetatable(_G,{})
|
||||
for k,v in pairs(mock.olds) do
|
||||
_G[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
local session_m = {__index = {
|
||||
login = function(self, who, pass)
|
||||
if not self.args then
|
||||
error("Request should have a .args table")
|
||||
end
|
||||
print("Right before requireing login_post endpoint, self.args is " .. tostring(self.args))
|
||||
print("After requireing login_post edpoint, self.args is " .. tostring(self.args))
|
||||
self.args.user = who
|
||||
self.args.pass = pass
|
||||
login_post(self)
|
||||
error("TODO")
|
||||
end,
|
||||
logout = function(self)
|
||||
error("TODO")
|
||||
end,
|
||||
req = function(self, args)
|
||||
|
||||
end
|
||||
}}
|
||||
|
||||
function mock.session(tbl)
|
||||
if post_login == nil then
|
||||
login_post = require("endpoints.login_post")
|
||||
fuzzy = require("spec.fuzzgen")
|
||||
claim_post = require("endpoints.claim_post")
|
||||
configure()
|
||||
end
|
||||
local username = fuzzy.subdomain()
|
||||
local claim_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_claim",
|
||||
args = {
|
||||
user = username
|
||||
}
|
||||
}
|
||||
claim_post(claim_req)
|
||||
local login_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_login",
|
||||
args = {
|
||||
user = username
|
||||
},
|
||||
file = {
|
||||
pass = claim_req.response
|
||||
}
|
||||
}
|
||||
login_post(login_req)
|
||||
local cookie = login_req.response_headers["set-cookie"]
|
||||
local sessionid = cookie:match("session=([^;]+)")
|
||||
local req = {
|
||||
host = "test.host",
|
||||
cookies = {
|
||||
session = sessionid
|
||||
}
|
||||
}
|
||||
return req, username
|
||||
end
|
||||
|
||||
return mock
|
|
@ -0,0 +1,44 @@
|
|||
local rng = {}
|
||||
function rng.markup() return math.random() > 0.5 and "plain" or "imageboard" end
|
||||
function rng.generate_str(length,characters)
|
||||
return function()
|
||||
local t = {}
|
||||
local rnglength = math.random(2,length)
|
||||
for i = 1,rnglength do
|
||||
local rngpos = math.random(#characters)
|
||||
local rngchar = string.sub(characters,rngpos,rngpos)
|
||||
table.insert(t,rngchar)
|
||||
end
|
||||
local ret = table.concat(t)
|
||||
return ret
|
||||
end
|
||||
end
|
||||
function rng.characters(mask)
|
||||
local t = {}
|
||||
for i = 1,255 do
|
||||
if string.match(string.char(i), mask) then
|
||||
table.insert(t,string.char(i))
|
||||
end
|
||||
end
|
||||
return table.concat(t)
|
||||
end
|
||||
function rng.maybe(input,chance)
|
||||
chance = chance or 0.5
|
||||
if math.random() < chance then
|
||||
return input
|
||||
end
|
||||
end
|
||||
rng.any = rng.generate_str(1024,rng.characters("."))
|
||||
rng.subdomain = rng.generate_str(30,rng.characters("[0-9a-z]"))
|
||||
rng.storyname = rng.generate_str(10,"[a-zA-Z0-9$+!*'(),-]")
|
||||
rng.storyid = function() return tostring(math.random(1,10)) end
|
||||
rng.tags = function()
|
||||
local tag_gen = rng.generate_str(10,"[%w%d ]")
|
||||
local t = {}
|
||||
for i = 1,10 do
|
||||
table.insert(t,tag_gen())
|
||||
end
|
||||
return table.concat(t,";")
|
||||
end
|
||||
|
||||
return rng
|
|
@ -0,0 +1,224 @@
|
|||
|
||||
_G.spy = spy
|
||||
local mock_env = require("spec.env_mock")
|
||||
local rng = require("spec.fuzzgen")
|
||||
|
||||
describe("smr login",function()
|
||||
setup(mock_env.setup)
|
||||
teardown(mock_env.teardown)
|
||||
it("should allow someone to claim an account",function()
|
||||
mock_env.mockdb()
|
||||
local claim_post = require("endpoints.claim_post")
|
||||
configure()
|
||||
claim_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_claim",
|
||||
args = {
|
||||
user = "user"
|
||||
}
|
||||
}
|
||||
claim_post(claim_req)
|
||||
assert(
|
||||
claim_req.responsecode == 200,
|
||||
"Login did not respond with a 200 code"
|
||||
)
|
||||
assert(
|
||||
claim_req.response_headers,
|
||||
"Login did not have response headers."
|
||||
)
|
||||
assert(
|
||||
claim_req.response_headers["Content-Disposition"],
|
||||
"Login did not have a Content Disposition header to set filename"
|
||||
)
|
||||
assert(
|
||||
string.find(claim_req.response_headers["Content-Disposition"],"attachment"),
|
||||
"Login did not mark passfile as an attachment"
|
||||
)
|
||||
assert(
|
||||
claim_req.response_headers["Content-Disposition"]:find(".passfile"),
|
||||
"Login did not name the returned file with the .passfile extension."
|
||||
)
|
||||
assert(
|
||||
claim_req.response_headers["Content-Type"],
|
||||
"Login did not respond with a Content-Type"
|
||||
)
|
||||
assert(
|
||||
claim_req.response_headers["Content-Type"] == "application/octet-stream",
|
||||
"Login did not mark Content-Type correctly (application/octet-stream)"
|
||||
)
|
||||
assert(
|
||||
claim_req.response,
|
||||
"Login did not return a passfile"
|
||||
)
|
||||
end)
|
||||
it("should give a session cookie when logging in with a user",function()
|
||||
local claim_post = require("endpoints.claim_post")
|
||||
local login_post = require("endpoints.login_post")
|
||||
local config = require("config")
|
||||
local db = require("db")
|
||||
local session = require("session")
|
||||
configure()
|
||||
|
||||
local username = rng.subdomain()
|
||||
local claim_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_claim",
|
||||
args = {
|
||||
user = username
|
||||
}
|
||||
}
|
||||
claim_post(claim_req)
|
||||
login_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_login",
|
||||
args = {
|
||||
user = username
|
||||
},
|
||||
file = {
|
||||
pass = claim_req.response
|
||||
}
|
||||
}
|
||||
sessionspy = spy.on(session,"start")
|
||||
login_post(login_req)
|
||||
assert.spy(sessionspy).was.called()
|
||||
local code = login_req.responsecode
|
||||
assert(
|
||||
code >= 300 and code <= 400,
|
||||
"Sucessful login should redirect the user, code:" .. tostring(code)
|
||||
)
|
||||
assert(
|
||||
login_req.response_headers,
|
||||
"Sucessful login should have response headers"
|
||||
)
|
||||
assert(
|
||||
login_req.response_headers["set-cookie"],
|
||||
"Sucessful login should set a cookie on the client"
|
||||
)
|
||||
local cookie = login_req.response_headers["set-cookie"]
|
||||
local domain_noport = string.match(config.domain,"(.-):?%d*$")
|
||||
assert(
|
||||
string.find(cookie,"session="),
|
||||
"Sucessful login should set a cookie named 'session'"
|
||||
)
|
||||
assert(
|
||||
string.find(cookie,"Domain="..domain_noport),
|
||||
"Cookies should only be set for the configured domain"
|
||||
)
|
||||
assert(
|
||||
string.find(cookie,"HttpOnly"),
|
||||
"Cookies should have the HttpOnly flag set"
|
||||
)
|
||||
assert(
|
||||
string.find(cookie,"Secure"),
|
||||
"Cookies should have the secure flag set"
|
||||
)
|
||||
assert(
|
||||
login_req.response_headers["Location"],
|
||||
"Sucessful login should redirect to a location"
|
||||
)
|
||||
assert(
|
||||
login_req.response_headers["Location"] == "https://" .. username .. "." .. config.domain,
|
||||
"Login redirect should get domain from config file"
|
||||
)
|
||||
end)
|
||||
it("should allow logged in users the option of posting under their username",function()
|
||||
local claim_post = require("endpoints.claim_post")
|
||||
local login_post = require("endpoints.login_post")
|
||||
local paste_get = require("endpoints.paste_get")
|
||||
local paste_post = require("endpoints.paste_post")
|
||||
local read_get = require("endpoints.read_get")
|
||||
local db = require("db")
|
||||
local config = require("config")
|
||||
config.domain = "test.host"
|
||||
configure()
|
||||
local username = rng.subdomain()
|
||||
local claim_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_claim",
|
||||
args = {
|
||||
user = username
|
||||
}
|
||||
}
|
||||
claim_post(claim_req)
|
||||
login_req = {
|
||||
method = "POST",
|
||||
host = "test.host",
|
||||
path = "/_login",
|
||||
args = {
|
||||
user = username
|
||||
},
|
||||
file = {
|
||||
pass = claim_req.response
|
||||
}
|
||||
}
|
||||
login_post(login_req)
|
||||
local cookie = login_req.response_headers["set-cookie"]
|
||||
local sessionid = cookie:match("session=([^;]+)")
|
||||
local paste_req_get = {
|
||||
method = "GET",
|
||||
host = username .. ".test.host",
|
||||
path = "/_paste",
|
||||
cookies = {
|
||||
session = sessionid
|
||||
}
|
||||
}
|
||||
paste_get(paste_req_get)
|
||||
local option = '<option value="' .. username .. '">' .. username .. '</option>'
|
||||
assert(
|
||||
paste_req_get.response:find(option),
|
||||
"After logging in the user should have an option to "..
|
||||
"make posts as themselves. Looking for " .. option ..
|
||||
" but didn't find it in " .. paste_req_get.response
|
||||
)
|
||||
local paste_req_post = {
|
||||
method = "POST",
|
||||
host = username .. ".test.host",
|
||||
path = "/_paste",
|
||||
cookies = {
|
||||
session = sessionid
|
||||
},
|
||||
args = {
|
||||
title = "post title",
|
||||
text = "post text",
|
||||
markup = "plain",
|
||||
tags = "",
|
||||
}
|
||||
}
|
||||
paste_post(paste_req_post)
|
||||
for row in db.conn:rows("SELECT COUNT(*) FROM posts") do
|
||||
assert(row[1] == 1, "Expected exactly 1 post in sample db")
|
||||
end
|
||||
local code = paste_req_post.responsecode
|
||||
assert(code >= 300 and code <= 400, "Should receive a redirect after posting, got:" .. tostring(code))
|
||||
assert(paste_req_post.response_headers, "Should have received some response headers")
|
||||
assert(paste_req_post.response_headers.Location, "Should have received a location in response headers")
|
||||
local redirect = paste_req_post.response_headers.Location:match("(/[^/]*)$")
|
||||
local read_req_get = {
|
||||
method = "GET",
|
||||
host = username .. ".test.host",
|
||||
path = redirect,
|
||||
cookies = {
|
||||
session = sessionid
|
||||
},
|
||||
args = {}
|
||||
}
|
||||
read_get(read_req_get)
|
||||
local response = read_req_get.response
|
||||
assert(
|
||||
response:find([[post title]]),
|
||||
"Failed to find post title in response."
|
||||
)
|
||||
assert(
|
||||
response:find('By <a href="https://' .. username .. '.test.host">' .. username .. '</a>'),
|
||||
"Failed to find the author name after a paste."
|
||||
)
|
||||
assert(
|
||||
response:find([[post text]]),
|
||||
"Failed to find post text in response."
|
||||
)
|
||||
end)
|
||||
end)
|
|
@ -251,6 +251,7 @@ describe("smr",function()
|
|||
end)
|
||||
it("should be named appropriately",function()
|
||||
local f = assert(io.open("endpoints/"..fname .. ".lua","r"))
|
||||
f:close()
|
||||
end)
|
||||
it("should run without errors",function()
|
||||
require("endpoints." .. fname)
|
||||
|
@ -263,9 +264,9 @@ describe("smr",function()
|
|||
local pagefunc = assert(require("endpoints." .. fname))
|
||||
assert(type(pagefunc) == "function")
|
||||
end)
|
||||
it("should call http_response() at some point",function()
|
||||
it("should call http_response() at some point #slow",function()
|
||||
local pagefunc = require("endpoints." .. fname)
|
||||
for i = 1,1000 do
|
||||
for i = 1,10 do
|
||||
local req = {}
|
||||
req.method = method
|
||||
req.path = obj.route
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
|
||||
describe("smr imageboard parser",function()
|
||||
function assertf(stmt, fmt, ...)
|
||||
if not stmt then
|
||||
error(string.format(fmt,...))
|
||||
end
|
||||
end
|
||||
describe("smr imageboard parser #parsers",function()
|
||||
it("should load without error",function()
|
||||
local parser = require("parser_imageboard")
|
||||
end)
|
||||
|
@ -9,4 +13,149 @@ describe("smr imageboard parser",function()
|
|||
local output = parser(input)
|
||||
assert(type(output) == "string","Expected string, got: %s",type(output))
|
||||
end)
|
||||
it("should spoiler text in asterisks ",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, **world**!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <span class="spoiler">world</span>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should spoiler text in [spoiler] tags",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, [spoiler]world[/spoiler]!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <span class="spoiler2">world</span>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should italicize words in double single quotes ('')",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, ''world''!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <i>world</i>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should bold words in tripple single quotes (''')",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, '''world'''!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <b>world</b>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should underline words in double underscores (__)",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, __world__!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <u>world</u>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should make a heading out of things in double equals(==)",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, ==world==!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <h2>world</h2>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should strikethrough words in double tildes (~~)",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, ~~world~~!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <s>world</s>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should codify words in [code] tags",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, [code]world[/code]!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello, <pre><code>world</code></pre>!</p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should greentext lines that start with >",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello,\n> world!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello,</p> <p><span class="greentext">> world!</span></p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should pinktext lines that start with <",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello,\n< world!"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello,</p> <p><span class="pinktext">< world!</span></p> ]]
|
||||
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
|
||||
end)
|
||||
it("should allow for bold+italic text",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello,'''''world!'''''"
|
||||
local output = parser(input)
|
||||
local expected = [[<p>Hello,<i><b>world!</b></i></p> ]]
|
||||
end)
|
||||
local formatting = {
|
||||
{"**","**"},
|
||||
{"[spoiler]","[/spoiler]"},
|
||||
{"''","''"},
|
||||
{"'''","'''"},
|
||||
{"__","__"},
|
||||
{"==","=="},
|
||||
{"~~","~~"},
|
||||
{"[code]","[/code]"}
|
||||
}
|
||||
local formatting_line = {"> ", "< "}
|
||||
for k,v in pairs(formatting) do
|
||||
for i = 1, 50 do
|
||||
it("should not break with " .. i .. " " .. v[1] .. " indicators in a row ",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = "Hello, " .. string.rep(v[1],i) .. " world!"
|
||||
local start_time = os.clock()
|
||||
local output = parser(input)
|
||||
local end_time = os.clock()
|
||||
assert(end_time - start_time < 1, "Took too long")
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, 50 do
|
||||
it("Should withstand a random string of " .. i .. " formatters and words. ",function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = {}
|
||||
local function random_text()
|
||||
if math.random() > 0.5 then
|
||||
return "Hello"
|
||||
else
|
||||
return "world"
|
||||
end
|
||||
end
|
||||
local function random_wrap(text)
|
||||
local rngwrap = formatting[math.random(#formatting)]
|
||||
return rngwrap[1] .. text .. rngwrap[2]
|
||||
end
|
||||
local function random_text_recursive(i)
|
||||
if i == 0 then
|
||||
return ""
|
||||
end
|
||||
local j = math.random()
|
||||
if j < 0.33 then
|
||||
return random_text_recursive(i-1) .. random_wrap(random_text())
|
||||
elseif j < 0.66 then
|
||||
return random_wrap(random_text() .. random_text_recursive(i-1)) .. random_wrap(random_text())
|
||||
else
|
||||
return random_wrap(random_text() .. random_text_recursive(i - 1))
|
||||
end
|
||||
end
|
||||
input = random_text_recursive(i)
|
||||
local start_time = os.clock()
|
||||
local output = parser(input)
|
||||
local end_time = os.clock()
|
||||
assert(end_time - start_time < 1, "Took too long")
|
||||
end)
|
||||
end
|
||||
for _,file_name in ipairs{
|
||||
"Beauty_and_the_Banchou_1"
|
||||
} do
|
||||
it("should parser " .. file_name,function()
|
||||
local parser = require("parser_imageboard")
|
||||
local input = require("spec.parser_tests." .. file_name)
|
||||
local output = parser(input)
|
||||
--print("output:",output)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
_G.spy = spy
|
||||
function assertf(stmt, fmt, ...)
|
||||
if not stmt then
|
||||
error(string.format(fmt,...))
|
||||
end
|
||||
end
|
||||
|
||||
local mock_env = require("spec.env_mock")
|
||||
|
||||
describe("smr search parser #parsers #working",function()
|
||||
setup(mock_env.setup)
|
||||
teardown(mock_env.teardown)
|
||||
it("should load without error",function()
|
||||
local parser = require("parser_search")
|
||||
end)
|
||||
it("should accept a string and return a string",function()
|
||||
local parser = require("parser_search")
|
||||
local input = "Hello, world!"
|
||||
local output = parser(input)
|
||||
assert(type(output) == "string","Expected string, got: %s",type(output))
|
||||
end)
|
||||
it("should parse a string into it's components",function()
|
||||
local parser = require("parser_search")
|
||||
local input = "+search +test +author=admin"
|
||||
local search_tag, test_tag, author_parsed = false, false, false
|
||||
local sql, ast = parser(input)
|
||||
for _,v in pairs(ast.tags) do
|
||||
if v[3] == "Search" then
|
||||
search_tag = true
|
||||
elseif v[3] == "Test" then
|
||||
test_tag = true
|
||||
end
|
||||
end
|
||||
for _,v in pairs(ast.author) do
|
||||
if v[3] == "%admin%" then
|
||||
author_parsed = true
|
||||
end
|
||||
end
|
||||
|
||||
assert(search_tag, "Search tag must be found")
|
||||
assert(test_tag, "Test tag must be found")
|
||||
assert(author_parsed, "Author tag must be found")
|
||||
end)
|
||||
it("should parse tags with a hyphen in the middle",function()
|
||||
local parser = require("parser_search")
|
||||
local input = "+post-modern"
|
||||
local sql, ast = parser(input)
|
||||
assert(#ast.tags == 1, "+post-modern should be one tag")
|
||||
end)
|
||||
it("should parse an empty string without errors",function()
|
||||
local parser = require("parser_search")
|
||||
local input = ""
|
||||
local sql, ast = parser(input)
|
||||
assert(sql,"Did not receive sql")
|
||||
assert(ast,"Did not receive ast")
|
||||
end)
|
||||
it("should parse a hits request",function()
|
||||
local parser = require("parser_search")
|
||||
local input = "+hits>=0"
|
||||
local sql, ast = parser(input)
|
||||
assert(ast.hits, "should have a .hits table")
|
||||
local hit = ast.hits[1]
|
||||
assert(hit[1] == "+", "Failed to have an intersect constraint for hits, got " .. hit[1])
|
||||
assert(hit[2] == ">=", "Failed to have a greater-than-or-equal constraint for hits, got " .. hit[2])
|
||||
assert(hit[3] == 0, "Failed to find >=0 for hits, got " .. hit[3])
|
||||
end)
|
||||
it("should parse a title request", function()
|
||||
local parser = require("parser_search")
|
||||
local input = "+title=the balled of pala-al-din"
|
||||
local sql, ast = parser(input)
|
||||
assert(ast.title, "should have a .title table")
|
||||
local title = ast.title[1]
|
||||
assert(title[1] == "+", "Failed to have an intersect constraint for title, got " .. title[1])
|
||||
assert(title[2] == "=", "Failed to have a like constraint for title, got " .. title[2])
|
||||
assert(title[3] == "%the balled of pala-al-din%", "Failed to find title name, got " .. title[3])
|
||||
end)
|
||||
end)
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
return [==[
|
||||
The angry dogs, sirens, and the occasional angry shout in the distance. A normal high schooler would be afraid to walk a rough road like this. The road to the local high-school, it's a testament to one's strength in itself. "Last Shot High" they call it, the school that takes on all the kids that aren't accepted at any other educational institution. That includes the massive bodybuilder-esque man in a school uniform walking down the road right now. His pants are baggy and held up by a studded belt around his waist with a chain dangling on his leg. His blazer is modified to hang down to his knees, his long pompadour stands strong against the wind. Who is this menace to society? This rebellious youth?
|
||||
He is Tankaroshi Ryuji, the ones who orbit him call him Tank or Tank-sama. And for who he is? He's the second year banchou of Last Shot High.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
I open the front door of the school and march past the lockers to the stairway. A group of first years are squatting at the bottom and keep their heads down as I pass. I climb the stairs to the second year floor. When I pass one of the science rooms I can hear jeers and sounds of fighting. The door opens and a teacher hustles out, deciding it's best to go somewhere else and wait for it to blow over. The usual scenes that play out before me cause me to daze out before I realize I've reached my destination. The old Art room. It's long since been abandoned. After all, not much use for an art room when most of the kids here have never even picked up a pencil. So now it's the hangout for me and anyone who associates with me. As I reach for the doorknob a sound catches my attention, the sound of a feminine yelp. I stand there with my hand on the door, listening for any familiar voices in the mix. All I can hear is a few guys and that one voice.
|
||||
"Hmm..." Situations like this are tricky, this isn't exactly a school with damsels in distress, usually when I try to help some girl out it turns out she started the fight and now I'm the asshole for interfering. But as usual I can't keep myself from it, I'm a man after all. I walk down the hall and turn the corner, from the back it looks like three third years are gathered around someone.
|
||||
"Oi!" I yell at the three seniors who turn towards me.
|
||||
"EH?! Well if isn't the big bad Tankaroshi-kun." The tall one seems to be the leader here as his cronies laugh.
|
||||
"Picking on a girl in my hallway eh? I hope you weren't planning on getting out of here alive." I slide my blazer off and set it to the side. A freezing wind blows right through my tank top and bites at my arms, I can't help but shiver lightly. Jesus we can't afford heat anymore?
|
||||
"Oh!? Is that right? Look at the little second year shivering in his britches." They shove someone behind them as they saunter up to me.
|
||||
The tallest one stops right in front of me and looks up. He starts to poke my chest as he talks to me. "You think just because you're some roided up gorilla with half a foot on us you can take all three of us? How about you just go back to your classroo-"
|
||||
I cut him off my grabbing his finger with my hand and lifting him up off the ground by it, his feet flailing wildly under him. I rear my head back and slam my forehead into his shocked face. Quickly, I throw him onto the mook on the left before turning my attention to the senior on my right. He swings his arm into my frame but it stops like he had just punched a wall. I grab onto his hair and swing him face first into the wall beside me.
|
||||
"Ora!" The leader had gotten back up and tried to tackle me in the back. I stumble a foot forward and drop my elbow backwards on top of him, slamming him straight down into the linoleum. I turn around to see the last one staring at me in shock.
|
||||
"Well come on senior, teach me something." I sneer at him. And with that the fight is over, he ran away as his "friend" is clutching his face on the ground beside the wall. I take a step and kick the leader in the stomach. "Ora! You thought you could take me?! You're a thousand years too early!" He grabs his stomach and I throw my hands into my pockets, then turn to walk away.
|
||||
"W-Wait please!" the sound of wood against tile approaches me at the sound of the feminine voice. Oh right, I did come here to save some chick huh?
|
||||
I turn around to see a slender, tall girl wearing wooden sandals, white socks, and a long white kimono. Oh it's her.
|
||||
"You're Tankaroshi-san Ryuji." She asks smiling.
|
||||
I look her up and down, up to this point I've got glempses of her but haven't gotten to really see her up close yet.
|
||||
I reach my hand out towards her cheek and pluck a frozen tear off of her and flick it down the hallway. "Yeah, Fubuki right?"
|
||||
Her cheeks turn a dark shade of blue, "Oh I didn't know you knew me...and you're using my first name too..."
|
||||
Hard not to, she sticks out more than any other person here. The new kid Tsuma Fubuki, and she's a Yuki-Onna.Now, monster girls aren't too common outside of the large metropolises, even a big city like ours seeing one is a rare sight. But even still that's not the only reason she sticks out.
|
||||
She wears long outdated traditional garb around. (The girls here wouldn't be caught in anything so lady-like)
|
||||
She scores the highest in all of the school. (Not a hard achievement by any means, but they rival even outside schools as well.)
|
||||
And she's probably the only one here who wont end up dead in the streets or in the Yakuza.
|
||||
Hell she's a model student, I'd say the only reason she ended up here is because no school wants a ghost haunting their halls.
|
||||
We sit there in silence for a a few minutes as we awkwardly stare at each other. Her white kimono grips all the right spots, her hourglass figure and modest bust accented by it. Her face is spotless and pure as snow. Her light blue skin and white, almost dead looking eyes are pretty in a way. Whitish blue hair hangs down to her lower back right above her Kimono's sash. Well this conversation is going nowhere.
|
||||
"Right...bye." I turn around with my hands in my pocket and walk away.
|
||||
"Oh! Please wait!"
|
||||
I sigh and turn back around.
|
||||
"I wanted to thank you for saving me." She gives me a deep bow at the waist and holds it for an uncomfortable amount of time.
|
||||
"Right...bye." I turn around and take a step forward before I feel something on the back of my arm.
|
||||
"Tankaroshi-san!" Her small hand tries to grip the back of my arm. It felt icy cold at first but has quickly grown to a comforting coolness. After she notices me staring at her hand she takes it back and folds her hands into her lap.
|
||||
"Tankaroshi-san!"
|
||||
I pinch the bridge of my nose, "Just call me tank or Tankaroshi if you'd rather. Drop the san."
|
||||
She nods and continues, "Tankaroshi! What do you like to eat?"
|
||||
"Meat. Protein." I answer her out of left field question.
|
||||
She nods her head and looks at me with a look of fiery determination, "From now on I will make you lunch! Chicken and Beef!" She bows deeply and walks off down the hall, she walks so elegantly she seems to glide over the linoleum floors.
|
||||
I shrug and go back to the Art Room.
|
||||
|
||||
"Please accept this!" Fubuki hands me a small box. It's of course cold to the touch, like it has been for the last week. I crack open the lid and pick out a brownish ice cube with my fingers, I toss it into my mouth. I never thought I could get brain freeze from fried chicken.
|
||||
"Well how is it?" She studies my face as I crunch through the ice.
|
||||
"It's...homemade."
|
||||
She nods her head and keeps staring; I guess she wasn't satisfied with that answer.
|
||||
I sigh, "Well it's the thought that counts Fubuki, you don't have to keep doing this we're even now you can forget about last week."
|
||||
She cocks her head at me, "Hmm? OH! I'm not cooking for you because of that!" She hides her mouth as she giggles. I guess I'm left out of the loop here. "A woman cooks for her husband right?"
|
||||
The new ice cube I had thrown in my mouth now slowly slides out from my lips and falls back into the box with a clatter.
|
||||
"Uh...yeah." I raise my brow and lean back from the box. "So what does that have to do with this situation here?"
|
||||
"Oh I've taking a liking to you! So now we're married." She leans in and tries to wrap herself around my arm, she can just barely do it. Uh...I've never been confessed to before. Is this a confession? Is this how they work? And she's confessing to me? I'm terrible with the ladies, I'm rough, they say I'm way too muscly and big, and the worst part, they say my face is scary. I'm not going around trying to make my face scary dammit!
|
||||
Her cold body feels comfortable on my arm even in the middle of fall, I've always had a high body temperature so I've grown fond of her touch over the past week, but now I kind of feel self conscious about it.
|
||||
"So married now huh? I don't think that's really how it works-"
|
||||
"Oh maybe not for you humans but for Yuki-Onnas it is." She cuts me off. "Yeah I saw you walking down the hall one day and took an interest in you. You saving me is what made me decide to keep you." She smiles as if she's remembering a good childhood memory.
|
||||
Yeah, this isn't a proposal this is a fucking notice of marriage.
|
||||
"You know we haven't even been on a date yet, right?"
|
||||
She jumps in the bench we're sitting in, "Oh! Yes let's go on a date!" She claps as she pesters me about where we're going.
|
||||
Man I want to say she misunderstood, but I got a feeling she knows what she's doing. Maybe I should just roll with it. Really, she's extremely attractive, like one of the prettiest women I've ever seen. Plus if I don't she'll probably freeze me to death or some shit, and I really don't want to die some Yanki Virgin.
|
||||
"Alright." I groan, but a grin forms on my face. "This weekend then." She hugs my arm tightly before opening up a box of her own.
|
||||
I see she doesn't eat her own cooking...
|
||||
|
||||
"Hey come on man, I left my wallet on the bus, just let me borrow a bit huh? Everything in it should do." The kid from a rival school shrinks and hands me the money from his wallet.
|
||||
"Hey you're the best pal." I pat him on the back and put it into my wallet.
|
||||
"That should be enough." I mumble to myself as I make my way to the school gates. These posers want to act tough but they sure get shook down easy. "Man I'm like crazy nervous right now." I wipe the sweat from my palms on my jeans as I walk down the road. First date, and with the prettiest girl you've ever seen...who is apparently your new wife. The air around me grows cold and I look up to see Fubuki waiting by the school. I jog the distance up to her.
|
||||
"Wow..." Her kimono looks fancy, icicle like trinkets dangle from it and ornate designs are woven into the fabric.
|
||||
"Now love don't stare, it's embarrassing." She shifts to the side giving me a real good look at her side profile.
|
||||
>deposited into spank bank
|
||||
I clear my throat, "Y-you-" *Ahem*, "You look really pretty today Fubuki."
|
||||
Her smile is blinding. She holds out her arms, her light blue hands and perfect nails glisten. Unsure of what to do I step forward. She wraps herself around my forearm. "Alright husband, where are we going?"
|
||||
I've thought of a few places over the past few days but I sure as hell don't know what girls like. I think the arcade's a bad idea, she probably doesn't want to go walking around looking for trouble, the gym would make a bad date. I'll stick with a classic, dinner. Just got to think of a place to eat at now.
|
||||
"Welcome to NcDaniels, what can I get for you today?"
|
||||
My eyes squeeze shut in frustration. This is the best I could come up with...?
|
||||
"Oh, hamburgers, I'm fond of these. Love, will you order for me?" She squeezes the arm she's wrapped around.
|
||||
She seems genuinely happy with the place I picked, looking at her I figured she'd have a more expensive pallet.
|
||||
"Don't see why not." I order my food first to get the easy part out of the way. I sweat bullets as the menu hanging over the employee starts to blur. Oh shit what do I get here? She's a girl so she shouldn't eat too much right? But if I get a kids meal or something I'll definitely be in for it then. I take a breath and grab the employee's collar, "I want a NcSingle with small fry, it better be the best burger you all ever made if you know what's good for ya!" I show my top teeth as I let go of him. Ah fuck, I panicked and went into delinquent mode in front of her, now she'll think I'm just a big, dumb brute like the rest of the gir-
|
||||
"Oh that's so sweet Tankaroshi! Taking care of me like that." She puts her hand on her cheek as she blushes a dark blue.
|
||||
Alright so far so good...
|
||||
I look around to find the best seat in the building, in the back corner there is a fantastic view of the bustling city, but of course there's obstacles. I stare a hole in the back of one of the kid's head. He rubs his neck and turns around to see me staring at him. He grabs his friend and runs out as fast as he can. Seat secured.
|
||||
I escort the lady back to the booth and help her into her side.
|
||||
"Oh what wonderful seats! Not as lovely as the view on the mountains are, but the city's activity sure is fun to watch!" She gazes out the window in awe as the passing cars blur and the occasional pedestrian walks by.
|
||||
"Hey Fubuki, just a question here..." I rub the back of my head awkwardly. She perks up, giving me her undivided attention. "Why did you decide to date me?" It's kind of a shitty question on a first date, but I'm genuinely curious.
|
||||
She puts her delicate finger to her pouty, blue lips. "Well...Maybe it's your strength." She smiles and continues, "Your hair is super cool, the way you talk when you get into a fight, that look of determination."
|
||||
So she's saying she just likes me because I'm the toughest kid in school, that's disappointing.
|
||||
"Even when you're out of a fight and you're thinking that same look is on your face. I guess I like your face." She giggles behind her hand as she squeezes her eyes shut in embarrassment. She looks back at the window, "I think...maybe it's fate really..."
|
||||
I switch the subject to keep from getting too red in front of everybody. And as we enjoy each others company while the food is prepared we overhear a conversation behind us.
|
||||
"Ugh it sure is ugly out, I know it was so pretty out just a while ago. I don't know how it got so cold so quickly, I even thought I saw a snowflake outside."
|
||||
The girl in front of me shifts uncomfortable as she hangs her head in shame, "Umm...Tankaroshi. I'm sorry..."
|
||||
My body and mind wants me to beat the people behind me until an apology is cried out from them, but even I know that wouldn't help this situation. I have to think tactfuly here but I can't lie to her either. "Fubuki. I love the winter, I love snow. The grey sky out is as beautiful to me as any sunny day and to be honest I think that's because I've gotten used to being around you." I nod earnestly as I say that. She looks in my eyes shocked to see that I'm telling the truth. She smiles at me. I've grown soft to this girl over the past couple weeks, more than I thought normal, maybe it's just a Yuki-Onna's ability over men, maybe I'm just actually into her. The prospect of such a ludicrously sudden marriage seems less and less profound as I spend time with her.
|
||||
The bell rings giving me the perfect time to run from this face redening moment. I shake my thoughts out of my head. No it's definitely crazy.
|
||||
As I sit down with the food she does a little prayer and we dig in.
|
||||
"You sure eat a lot Tankaroshi." She giggles as she watches me stuff the burgers into my mouth.
|
||||
"Well you know, I'm still growing after all..." I mumble to myself as she keeps giggling.
|
||||
We enjoy our dinner and laugh with each other, a date well done if it wasn't for the people who walked in next.
|
||||
Four delinquent chicks had walked by the window and caught eye of me. Now I'm the biggest, baddest banchou in all of the world. Though I do have one weakness...
|
||||
"Oi! It's Tank-Chan!" One of the girls slam their hands on the table as they hover around us.
|
||||
I don't hit women. And around my school the girls are just as bad as the boys.
|
||||
"Finally got yourself a girl eh? Your ugly ass wasn't too interested in me huh? But you'll settle for some monster bitch!?" She points at Fubuki's face.
|
||||
I shrug, never looking her in the eye "It isn't any of your business, please leave sis."
|
||||
She sneers and laughs, "Oh am I ruining your date? What are you going to do about it? You gunna take a swing at me big man?" She pats her cheek as she leans in to give me a shot.
|
||||
"Go away, you're bothering me again." I roll my eyes as I ignore her. After a while she gets her jeers in. Guess she gets off on talking down to people she has to look up to. I slump into the booth, ready for another four or five minutes of this shit.
|
||||
"Yeah just lay back and ta-huurk*" My eyes widen as I see her face being slammed into the table by her blond hair.
|
||||
"Eh!? You ruining my date bitch?" I see Fubuki's usually feminine and beautiful face twisted into a jeer as she shows her teeth and curls her lip, her eyelids half open as if uninterested in the prey in front of her. The bully is grabbing at her head where Fubuki has her hand wrapped up. "Tch, you shouldn't poke your nose where it doesn't belong, it's bad for your health you know?"
|
||||
As Fubuki's fist tightens my fem-bully winces. "Now what did you call my stud? Ugly? I'll show you ugly." She slams her face on the table one more time before letting her fall backwards onto the floor. Fubuki lights up a cigarette as she stands up. Even for a guy she's quite tall, so she has a good five or six inches over the other girls. She lets the cigarette dangle from her pretty blue lips as she leans over them, "Idiots. You bore me, go home and get stuffed." The air kicks up inside the NcDaniels as drinks freeze in peoples hands and nipples poke through sweaters. They all scramble off the floor and run out as she tosses the cigarette at them.
|
||||
"Tch, shit eaters..." She mumbles as she sits in the booth next to me and takes my arm around her.
|
||||
"Mmm, I love this date dear! Let's have many more!" She smiles cutely as she nuzzles into my side.
|
||||
|
||||
On that day Tankaroshi, the banchou of Last Chance High fell in love with Fubuki the ex-banchou of Frozen Pass high.
|
||||
]==]
|
|
@ -0,0 +1,43 @@
|
|||
_G.spy = spy
|
||||
local mock_env = require("spec.env_mock")
|
||||
local fuzzy = require("spec.fuzzgen")
|
||||
require("spec.utils")
|
||||
|
||||
describe("smr",function()
|
||||
setup(mock_env.setup)
|
||||
teardown(mock_env.teardown)
|
||||
it("should display an anonymously submitted post on the front page", function()
|
||||
local paste_post = require("endpoints.paste_post")
|
||||
local index_get = require("endpoints.index_get")
|
||||
local pages = require("pages")
|
||||
local config = require("config")
|
||||
config.domain = "test.host"
|
||||
pages_mock = mock(pages)
|
||||
configure()
|
||||
assert.stub(pages_mock.index).was_not_called()
|
||||
local post_req = {
|
||||
host = "test.host",
|
||||
method = "POST",
|
||||
path = "/_paste",
|
||||
args = {
|
||||
title = fuzzy.any(),
|
||||
text = fuzzy.any(),
|
||||
pasteas = "anonymous",
|
||||
markup = "plain",
|
||||
tags = "one;two;",
|
||||
}
|
||||
}
|
||||
paste_post(post_req)
|
||||
local get_req = {
|
||||
host = "test.host",
|
||||
method = "GET",
|
||||
path = "/",
|
||||
args = {},
|
||||
}
|
||||
index_get(get_req)
|
||||
assert.stub(pages_mock.index).was_called()
|
||||
assertf(get_req.responsecode >= 200 and get_req.responsecode < 300, "Should give a 2XX response code, got %d", get_req.responsecode)
|
||||
assert(get_req.responsecode == 200, "Error code should be 200 - OK")
|
||||
assert(get_req.response:find(get_req.response,1,true), "Failed to find title in string")
|
||||
end)
|
||||
end)
|
|
@ -0,0 +1,26 @@
|
|||
--Make sure the type checking works
|
||||
|
||||
describe("smr type checking",function()
|
||||
it("should load without errors",function()
|
||||
local types = require("types")
|
||||
end)
|
||||
it("should not error when an argument is a number",function()
|
||||
local types = require("types")
|
||||
local n = 5
|
||||
assert(types.number(n))
|
||||
end)
|
||||
it("should error when an argument is a table",function()
|
||||
local types = require("types")
|
||||
local t = {}
|
||||
assert.has.errors(function()
|
||||
types.number(t)
|
||||
end)
|
||||
end)
|
||||
it("should check multiple types passed as arugments", function()
|
||||
local types = require("types")
|
||||
local num, tbl = 5, {}
|
||||
types.check(num, types.number, nil)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
function assertf(bool, ...)
|
||||
if bool then return end
|
||||
local args = {...}
|
||||
local assertmsg = args[1] or "Assetion failed"
|
||||
table.remove(args,1)
|
||||
error(string.format(assertmsg, table.unpack(args)),2)
|
||||
end
|
|
@ -30,6 +30,7 @@ void KeccakF1600(void *s)
|
|||
void Keccak(ui r, ui c, const u8 *in, u64 inLen, u8 sfx, u8 *out, u64 outLen)
|
||||
{
|
||||
/*initialize*/ u8 s[200]; ui R=r/8; ui i,b=0; FOR(i,200) s[i]=0;
|
||||
/*san-check*/ if (((r+c)!= 1600) || ((r % 8 ) != 0)) return;
|
||||
/*absorb*/ while(inLen>0) { b=(inLen<R)?inLen:R; FOR(i,b) s[i]^=in[i]; in+=b; inLen-=b; if (b==R) { KeccakF1600(s); b=0; } }
|
||||
/*pad*/ s[b]^=sfx; if((sfx&0x80)&&(b==(R-1))) KeccakF1600(s); s[R-1]^=0x80; KeccakF1600(s);
|
||||
/*squeeze*/ while(outLen>0) { b=(outLen<R)?outLen:R; FOR(i,b) out[i]=s[i]; out+=b; outLen-=b; if(outLen>0) KeccakF1600(s); }
|
||||
|
|
115
src/libkore.c
115
src/libkore.c
|
@ -45,148 +45,157 @@ lhttp_response(lua_State *L){
|
|||
return 0;
|
||||
}
|
||||
|
||||
char response[] = "0\r\n\r\n";
|
||||
|
||||
/*Helpers for response coroutines*/
|
||||
int
|
||||
coroutine_iter_sent(struct netbuf *buf){
|
||||
printf("Iter sent called\n");
|
||||
struct co_obj *obj = (struct co_obj*)buf->extra;
|
||||
int ret;
|
||||
lua_State *L = obj->L;
|
||||
printf("\tbuf:%p\n",(void*)buf);
|
||||
printf("\tobj:%p\n",(void*)obj);
|
||||
printf("\tL:%p\n",(void*)L);
|
||||
|
||||
printf("Top is: %d\n",lua_gettop(L));
|
||||
printf("Getting status...\n");
|
||||
lua_getglobal(L,"coroutine");
|
||||
printf("Found coroutine...\n");
|
||||
lua_getfield(L,-1,"status");
|
||||
printf("Found status...\n");
|
||||
lua_rawgeti(L,LUA_REGISTRYINDEX,obj->ref);
|
||||
printf("About to get status\n");
|
||||
lua_call(L,1,1);
|
||||
printf("Status got\n");
|
||||
const char *status = luaL_checklstring(L,-1,NULL);
|
||||
printf("status in sent: %s\n",status);
|
||||
|
||||
if(strcmp(status,"dead") == 0){
|
||||
printf("Cleanup\n");
|
||||
return KORE_RESULT_OK;
|
||||
ret = KORE_RESULT_OK;
|
||||
}else{
|
||||
printf("About to call iter_next from iter_sent\n");
|
||||
return coroutine_iter_next(obj);
|
||||
ret = coroutine_iter_next(obj);
|
||||
}
|
||||
|
||||
if(ret == KORE_RESULT_RETRY){
|
||||
ret = KORE_RESULT_OK;
|
||||
}else{
|
||||
if(obj->removed == 0){
|
||||
http_start_recv(obj->c);
|
||||
}
|
||||
obj->c->hdlr_extra = NULL;
|
||||
obj->c->disconnect = NULL;
|
||||
obj->c->flags &= ~CONN_IS_BUSY;
|
||||
net_send_queue(obj->c,response,strlen(response));
|
||||
net_send_flush(obj->c);
|
||||
free(obj);
|
||||
}
|
||||
return (ret);
|
||||
}
|
||||
const char response[] = "0\r\n\r\n";
|
||||
|
||||
int coroutine_iter_next(struct co_obj *obj){
|
||||
printf("Coroutine iter next called\n");
|
||||
lua_State *L = obj->L;
|
||||
lua_getglobal(L,"coroutine");
|
||||
lua_getfield(L,-1,"status");
|
||||
lua_rawgeti(L,LUA_REGISTRYINDEX,obj->ref);
|
||||
lua_call(L,1,1);
|
||||
const char *status = luaL_checklstring(L,-1,NULL);
|
||||
printf("status in next: %s\n",status);
|
||||
if(strcmp(status,"dead") == 0){
|
||||
kore_log(LOG_ERR,"Coroutine was dead when it was passed to coroutine iter next");
|
||||
lua_pushstring(L,"Coroutine was dead when passed to coroutine iter next");
|
||||
lua_error(L);
|
||||
}
|
||||
lua_pop(L,lua_gettop(L));
|
||||
printf("Calling resume\n");
|
||||
lua_getglobal(L,"coroutine");
|
||||
printf("Getting resume()\n");
|
||||
lua_getfield(L,-1,"resume");
|
||||
printf("Getting function\n");
|
||||
lua_rawgeti(L,LUA_REGISTRYINDEX,obj->ref);
|
||||
printf("Checking type\n");
|
||||
luaL_checktype(L,-1,LUA_TTHREAD);
|
||||
printf("About to call resume()\n");
|
||||
int err = lua_pcall(L,1,2,0);
|
||||
printf("Done calling resume()\n");
|
||||
if(err != 0){
|
||||
return (KORE_RESULT_ERROR);
|
||||
}
|
||||
if(!lua_toboolean(L,-2)){ //Runtime error
|
||||
printf("Runtime error\n");
|
||||
lua_pushstring(L,":\n");//"error",":"
|
||||
printf("top1:%d\n",lua_gettop(L));
|
||||
lua_getglobal(L,"debug");//"error",":",{debug}
|
||||
printf("top2:%d\n",lua_gettop(L));
|
||||
lua_getfield(L,-1,"traceback");//"error",":",{debug},debug.traceback()
|
||||
printf("top3:%d\n",lua_gettop(L));
|
||||
lua_call(L,0,1);//"error",":",{debug},"traceback"
|
||||
printf("top4:%d\n",lua_gettop(L));
|
||||
lua_remove(L,-2);//"error",":","traceback"
|
||||
printf("top5:%d\n",lua_gettop(L));
|
||||
lua_concat(L,3);
|
||||
printf("top6:%d\n",lua_gettop(L));
|
||||
size_t size;
|
||||
const char *s = luaL_checklstring(L,-1,&size);
|
||||
printf("Error: %s\n",s);
|
||||
kore_log(LOG_ERR,"Error: %s\n",s);
|
||||
lua_pop(L,lua_gettop(L));
|
||||
return (KORE_RESULT_ERROR);
|
||||
}
|
||||
//No runtime error
|
||||
if(lua_type(L,-1) == LUA_TSTRING){
|
||||
printf("Data yielded\n");
|
||||
size_t size;
|
||||
const char *data = luaL_checklstring(L,-1,&size);
|
||||
struct netbuf *nb;
|
||||
printf("Yielding data stream size %lld\n",size);
|
||||
struct kore_buf *kb = kore_buf_alloc(0);
|
||||
kore_buf_appendf(kb,"%x\r\n",size);
|
||||
struct kore_buf *kb = kore_buf_alloc(4096);
|
||||
kore_buf_appendf(kb,"%lu\r\n",size);
|
||||
kore_buf_append(kb,data,size);
|
||||
size_t ssize;
|
||||
char *sstr = kore_buf_stringify(kb,&ssize);
|
||||
net_send_stream(obj->c, sstr, ssize, coroutine_iter_sent, &nb);
|
||||
kore_buf_appendf(kb,"\r\n");
|
||||
//size_t ssize;
|
||||
//char *sstr = kore_buf_stringify(kb,&ssize);
|
||||
net_send_stream(obj->c, kb->data, kb->offset, coroutine_iter_sent, &nb);
|
||||
nb->extra = obj;
|
||||
lua_pop(L,lua_gettop(L));
|
||||
kore_buf_free(kb);
|
||||
return (KORE_RESULT_RETRY);
|
||||
//return err == 0 ? (KORE_RESULT_OK) : (KORE_RESULT_RETRY);
|
||||
}else if(lua_type(L,-1) == LUA_TNIL){
|
||||
printf("Done with function\n");
|
||||
struct netbuf *nb;
|
||||
printf("About to send final bit\n");
|
||||
net_send_stream(obj->c, response, strlen(response) + 1, coroutine_iter_sent, &nb);
|
||||
struct kore_buf *kb = kore_buf_alloc(4096);
|
||||
kore_buf_appendf(kb,"0\r\n\r\n");
|
||||
net_send_queue(obj->c, kb->data, kb->offset);
|
||||
net_send_stream(obj->c, response, strlen(response) + 0, coroutine_iter_sent, &nb);
|
||||
nb->extra = obj;
|
||||
printf("Done sending final bit\n");
|
||||
|
||||
lua_pop(L,lua_gettop(L));
|
||||
printf("Poped everything\n");
|
||||
kore_buf_free(kb);
|
||||
return (KORE_RESULT_OK);
|
||||
}else{
|
||||
printf("Coroutine used for response returned something that was not a string:%s\n",lua_typename(L,lua_type(L,-1)));
|
||||
kore_log(LOG_CRIT,"Coroutine used for response returned something that was not a string:%s\n",lua_typename(L,lua_type(L,-1)));
|
||||
return (KORE_RESULT_ERROR);
|
||||
}
|
||||
}
|
||||
static void
|
||||
coroutine_disconnect(struct connection *c){
|
||||
printf("Disconnect routine called\n");
|
||||
kore_log(LOG_ERR,"Disconnect routine called\n");
|
||||
struct co_obj *obj = (struct co_obj*)c->hdlr_extra;
|
||||
lua_State *L = obj->L;
|
||||
int ref = obj->ref;
|
||||
int Lref = obj->Lref;
|
||||
obj->removed = 1;
|
||||
luaL_unref(L,LUA_REGISTRYINDEX,ref);
|
||||
free(obj);
|
||||
printf("Done with disconnect\n");
|
||||
luaL_unref(L,LUA_REGISTRYINDEX,Lref);
|
||||
c->hdlr_extra = NULL;
|
||||
}
|
||||
/*
|
||||
The coroutine passed to this function should yield() the data to send to the
|
||||
client, then return when done.
|
||||
|
||||
TODO: Broken and leaks memory
|
||||
http_response_co(request::userdata, co::coroutine)
|
||||
*/
|
||||
int
|
||||
lhttp_response_co(lua_State *L){
|
||||
struct connection *c;
|
||||
printf("Start response coroutine\n");
|
||||
int coroutine_ref = luaL_ref(L,LUA_REGISTRYINDEX);
|
||||
struct http_request *req = luaL_checkrequest(L,-1);
|
||||
c = req->owner;
|
||||
if(c->state == CONN_STATE_DISCONNECTING){
|
||||
return 0;
|
||||
}
|
||||
lua_pop(L,1);
|
||||
req->flags |= HTTP_REQUEST_NO_CONTENT_LENGTH;
|
||||
struct co_obj *obj = (struct co_obj*)malloc(sizeof(struct co_obj));
|
||||
obj->removed = 0;
|
||||
obj->L = lua_newthread(L);
|
||||
obj->Lref = luaL_ref(L,LUA_REGISTRYINDEX);
|
||||
obj->ref = coroutine_ref;
|
||||
obj->c = req->owner;
|
||||
obj->c->flags |= CONN_IS_BUSY;
|
||||
obj->c = c;
|
||||
obj->c->disconnect = coroutine_disconnect;
|
||||
|
||||
obj->c->hdlr_extra = obj;
|
||||
printf("About to call iter next\n");
|
||||
obj->c->flags |= CONN_IS_BUSY;
|
||||
req->flags |= HTTP_REQUEST_NO_CONTENT_LENGTH;
|
||||
http_response_header(req,"transfer-encoding","chunked");
|
||||
http_response(req,200,NULL,0);
|
||||
printf("About to call iter next\n");
|
||||
coroutine_iter_next(obj);
|
||||
printf("Done calling iter next\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -438,6 +447,7 @@ lhttp_set_flags(lua_State *L){
|
|||
struct http_request *req = luaL_checkrequest(L,-2);
|
||||
lua_pop(L,2);
|
||||
req->flags = flags;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -448,6 +458,7 @@ lhttp_get_flags(lua_State *L){
|
|||
struct http_request *req = luaL_checkrequest(L,-1);
|
||||
lua_pop(L,1);
|
||||
lua_pushnumber(L,req->flags);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
struct co_obj {
|
||||
lua_State *L;
|
||||
int ref;
|
||||
int Lref;
|
||||
int removed;
|
||||
struct connection *c;
|
||||
};
|
||||
int lhttp_response(lua_State *L);
|
||||
int lhttp_response_co(lua_State *L);
|
||||
int coroutine_iter_sent(struct netbuf *buf);
|
||||
int coroutine_iter_next(struct co_obj *obj);
|
||||
int lhttp_response_header(lua_State *L);
|
||||
int lhttp_request_header(lua_State *L);
|
||||
int lhttp_method_text(lua_State *L);
|
||||
int lhttp_request_get_path(lua_State *L);
|
||||
int lhttp_request_get_host(lua_State *L);
|
||||
|
@ -19,6 +23,8 @@ int lhttp_argument_get_string(lua_State *L);
|
|||
int lhttp_request_get_ip(lua_State *L);
|
||||
int lhttp_populate_cookies(lua_State *L);
|
||||
int lhttp_file_get(lua_State *L);
|
||||
int lhttp_set_flags(lua_State *L);
|
||||
int lhttp_get_flags(lua_State *L);
|
||||
int lhttp_populate_multipart_form(lua_State *L);
|
||||
int lkore_log(lua_State *L);
|
||||
void load_kore_libs(lua_State *L);
|
||||
|
|
|
@ -9,6 +9,7 @@ local sql = require("lsqlite3")
|
|||
|
||||
local queries = require("queries")
|
||||
local util = require("util")
|
||||
local db = require("db")
|
||||
|
||||
local ret = {}
|
||||
|
||||
|
@ -16,10 +17,11 @@ local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
|
|||
|
||||
local oldconfigure = configure
|
||||
function configure(...)
|
||||
local cache = util.sqlassert(sql.open_memory())
|
||||
local cache = db.sqlassert(sql.open_memory())
|
||||
ret.cache = cache -- Expose db for testing
|
||||
--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
|
||||
--swapping to disk. TODO
|
||||
assert(cache:exec([[
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
path TEXT PRIMARY KEY,
|
||||
|
@ -54,7 +56,7 @@ end
|
|||
--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache()
|
||||
function ret.render(pagename,callback)
|
||||
stmnt_cache:bind_names{path=pagename}
|
||||
local err = util.do_sql(stmnt_cache)
|
||||
local err = db.do_sql(stmnt_cache)
|
||||
if err == sql.DONE then
|
||||
stmnt_cache:reset()
|
||||
--page is not cached
|
||||
|
@ -72,7 +74,7 @@ function ret.render(pagename,callback)
|
|||
path=pagename,
|
||||
data=text,
|
||||
}
|
||||
err = util.do_sql(stmnt_insert_cache)
|
||||
err = db.do_sql(stmnt_insert_cache)
|
||||
if err == sql.ERROR or err == sql.MISUSE then
|
||||
error("Failed to update cache for page " .. pagename)
|
||||
end
|
||||
|
@ -80,11 +82,13 @@ function ret.render(pagename,callback)
|
|||
return text
|
||||
end
|
||||
|
||||
-- Dirty a cached page, causing it to be re-rendered the next time it's
|
||||
-- requested. Doesn't actually delete it or anything, just sets it's dirty bit
|
||||
function ret.dirty(url)
|
||||
stmnt_dirty_cache:bind_names{
|
||||
path = url
|
||||
}
|
||||
util.do_sql(stmnt_dirty_cache)
|
||||
db.do_sql(stmnt_dirty_cache)
|
||||
stmnt_dirty_cache:reset()
|
||||
end
|
||||
|
||||
|
|
|
@ -5,5 +5,6 @@ A one-stop-shop for runtime configuration
|
|||
return {
|
||||
domain = "<{get domain}>",
|
||||
production = false,
|
||||
legacy_url_cutoff = 144
|
||||
legacy_url_cutoff = 144,
|
||||
db = "data/posts.db"
|
||||
}
|
||||
|
|
|
@ -6,10 +6,88 @@ local sql = require("lsqlite3")
|
|||
|
||||
local queries = require("queries")
|
||||
local util = require("util")
|
||||
local config = require("config")
|
||||
|
||||
local db = {}
|
||||
|
||||
--[[
|
||||
Runs an sql query and receives the 3 arguments back, prints a nice error
|
||||
message on fail, and returns true on success.
|
||||
]]
|
||||
function db.sqlassert(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 db.do_sql(stmnt)
|
||||
if not stmnt then error("No statement",2) end
|
||||
local err
|
||||
local i = 0
|
||||
repeat
|
||||
err = stmnt:step()
|
||||
if err == sql.BUSY then
|
||||
i = i + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
until(err ~= sql.BUSY or i > 10)
|
||||
assert(i < 10, "Database busy")
|
||||
return err
|
||||
end
|
||||
|
||||
--[[
|
||||
Provides an iterator that loops over results in an sql statement
|
||||
or throws an error, then resets the statement after the loop is done.
|
||||
]]
|
||||
function db.sql_rows(stmnt)
|
||||
if not stmnt then error("No statement",2) end
|
||||
local err
|
||||
return function()
|
||||
err = stmnt:step()
|
||||
if err == sql.BUSY then
|
||||
coroutine.yield()
|
||||
elseif err == sql.ROW then
|
||||
return unpack(stmnt:get_values())
|
||||
elseif err == sql.DONE then
|
||||
stmnt:reset()
|
||||
return nil
|
||||
else
|
||||
stmnt:reset()
|
||||
local msg = string.format(
|
||||
"SQL Iteration failed: %s : %s\n%s",
|
||||
tostring(err),
|
||||
db.conn:errmsg(),
|
||||
debug.traceback()
|
||||
)
|
||||
log(LOG_CRIT,msg)
|
||||
error(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Binds an argument to as statement with nice error reporting on failure
|
||||
stmnt :: sql.stmnt - the prepared sql statemnet
|
||||
call :: string - a string "bind" or "bind_blob"
|
||||
position :: number - the argument position to bind to
|
||||
data :: string - The data to bind
|
||||
]]
|
||||
function db.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 call %s(%d,%q): %s", call, position, data, db.conn:errmsg()),2)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local oldconfigure = configure
|
||||
db.conn = util.sqlassert(sql.open("data/posts.db"))
|
||||
db.conn = db.sqlassert(sql.open(config.db))
|
||||
function configure(...)
|
||||
|
||||
--Create sql tables
|
||||
|
|
|
@ -8,40 +8,21 @@ local stmnt_tags_get
|
|||
|
||||
local oldconfigure = configure
|
||||
function configure(...)
|
||||
stmnt_tags_get = util.sqlassert(db.conn:prepare(queries.select_suggest_tags))
|
||||
stmnt_tags_get = db.sqlassert(db.conn:prepare(queries.select_suggest_tags))
|
||||
return oldconfigure(...)
|
||||
end
|
||||
|
||||
local function suggest_tags(req,data)
|
||||
print("Suggesting tags!")
|
||||
stmnt_tags_get:bind_names{
|
||||
match = data .. "%"
|
||||
}
|
||||
local err = util.do_sql(stmnt_tags_get)
|
||||
if err == sql.ROW or err == sql.DONE then
|
||||
local tags = {data}
|
||||
for tag in stmnt_tags_get:rows() do
|
||||
print("Found tag:",tag[1])
|
||||
table.insert(tags,tag[1])
|
||||
end
|
||||
stmnt_tags_get:reset()
|
||||
http_response_header(req,"Content-Type","text/plain")
|
||||
http_response(req,200,table.concat(tags,";"))
|
||||
else
|
||||
log(LOG_ALERT,"Failed to get tag suggestions in an unusual way:" .. err .. ":" .. db.conn:errmsg())
|
||||
--This is bad though
|
||||
local page = pages.error({
|
||||
errcode = 500,
|
||||
errcodemsg = "Server error",
|
||||
explanation = string.format(
|
||||
"Failed to retreive tags from database:%d:%q",
|
||||
err,
|
||||
db.conn:errmsg()
|
||||
),
|
||||
})
|
||||
stmnt_tags_get:reset()
|
||||
http_response(req,500,page)
|
||||
local tags = {data}
|
||||
for tag in stmnt_tags_get:rows() do
|
||||
table.insert(tags,tag[1])
|
||||
end
|
||||
stmnt_tags_get:reset()
|
||||
http_response_header(req,"Content-Type","text/plain")
|
||||
http_response(req,200,table.concat(tags,";"))
|
||||
end
|
||||
|
||||
local function api_get(req)
|
||||
|
@ -55,7 +36,7 @@ local function api_get(req)
|
|||
we're searching for, potentially causing a DoS with a
|
||||
sufficiently backtrack-ey search/tag combination.
|
||||
]]
|
||||
assert(data:match("^[a-zA-Z0-9,%s-]+$"),"Bad characters in tag")
|
||||
assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data))
|
||||
return suggest_tags(req,data)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
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 session = require("session")
|
||||
local config = require("config")
|
||||
|
||||
local stmnt_bio
|
||||
local oldconfigure = configure
|
||||
function configure(...)
|
||||
stmnt_bio = assert(db.conn:prepare(queries.select_author_bio))
|
||||
return oldconfigure(...)
|
||||
end
|
||||
|
||||
local function bio_edit_get(req)
|
||||
local host = http_request_get_host(req)
|
||||
local path = http_request_get_path(req)
|
||||
local author, authorid = session.get(req)
|
||||
|
||||
http_request_populate_qs(req)
|
||||
local ret
|
||||
|
||||
if (not author) or (not authorid) then
|
||||
ret = pages.error{
|
||||
errcode = 401,
|
||||
errcodemsg = "Not authorized",
|
||||
explanation = "You must be logged in to edit your biography."
|
||||
}
|
||||
http_response(req,401,ret)
|
||||
end
|
||||
|
||||
--Get the logged in author's bio to display
|
||||
stmnt_bio:bind_names{
|
||||
authorid = authorid
|
||||
}
|
||||
local err = db.do_sql(stmnt_bio)
|
||||
if err == sql.DONE then
|
||||
--No rows, we're logged in but an author with our id doesn't
|
||||
--exist? Something has gone wrong.
|
||||
ret = pages.error{
|
||||
errcode = 500,
|
||||
errcodemsg = "Server error",
|
||||
explanation = string.format([[
|
||||
Tried to get the biography of author %q (%d) but no author with that id was
|
||||
found, please report this error.
|
||||
]], author, authorid),
|
||||
should_traceback=true
|
||||
}
|
||||
stmnt_bio:reset()
|
||||
http_response(req,500,ret)
|
||||
return
|
||||
end
|
||||
assert(err == sql.ROW)
|
||||
local data = stmnt_bio:get_values()
|
||||
local bio_text = data[1]
|
||||
if data[1] ~= "" then
|
||||
bio_text = zlib.decompress(data[1])
|
||||
end
|
||||
stmnt_bio:reset()
|
||||
ret = pages.edit_bio{
|
||||
text = bio_text,
|
||||
user = author,
|
||||
domain = config.domain,
|
||||
}
|
||||
http_response(req,200,ret)
|
||||
end
|
||||
|
||||
return bio_edit_get
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
local sql = require("lsqlite3")
|
||||
local zlib = require("zlib")
|
||||
|
||||
local db = require("db")
|
||||
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 session = require("session")
|
||||
|
||||
local stmnt_update_bio
|
||||
|
||||
local oldconfigure = configure
|
||||
function configure(...)
|
||||
stmnt_update_bio = assert(db.conn:prepare(queries.update_bio))
|
||||
return oldconfigure(...)
|
||||
end
|
||||
|
||||
local function edit_bio(req)
|
||||
local host = http_request_get_host(req)
|
||||
local path = http_request_get_path(req)
|
||||
local author, author_id = session.get(req)
|
||||
|
||||
http_request_populate_post(req)
|
||||
local text = http_argument_get_string(req,"text") or ""
|
||||
|
||||
local parsed = parsers.plain(text) -- Make sure the plain parser can deal with it, even though we don't store this result.
|
||||
local compr_raw = zlib.compress(text)
|
||||
local compr = zlib.compress(parsed)
|
||||
|
||||
db.sqlbind(stmnt_update_bio, "bind_blob", 1,compr_raw)
|
||||
db.sqlbind(stmnt_update_bio, "bind", 2, author_id)
|
||||
if db.do_sql(stmnt_update_bio) ~= sql.DONE then
|
||||
stmnt_update_bio:reset()
|
||||
error("Faled to update biography")
|
||||
end
|
||||
stmnt_update_bio:reset()
|
||||
local loc = string.format("https://%s.%s",author,config.domain)
|
||||
-- Dirty the cache for the author's index, the only place where the bio is displayed.
|
||||
cache.dirty(string.format("%s.%s",author,config.domain))
|
||||
http_response_header(req,"Location",loc)
|
||||
http_response(req,303,"")
|
||||
return
|
||||
end
|
||||
|
||||
return edit_bio
|
|
@ -9,10 +9,14 @@ local config = require("config")
|
|||
|
||||
local stmnt_author_create
|
||||
|
||||
--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.
|
||||
local oldconfigure = configure
|
||||
function configure(...)
|
||||
|
||||
stmnt_author_create = util.sqlassert(db.conn:prepare(queries.insert_author))
|
||||
stmnt_author_create = db.sqlassert(db.conn:prepare(queries.insert_author))
|
||||
return oldconfigure(...)
|
||||
end
|
||||
|
||||
|
@ -42,7 +46,7 @@ local function claim_post(req)
|
|||
}
|
||||
stmnt_author_create:bind_blob(2,salt)
|
||||
stmnt_author_create:bind_blob(3,hash)
|
||||
local err = util.do_sql(stmnt_author_create)
|
||||
local err = db.do_sql(stmnt_author_create)
|
||||
if err == sql.DONE then
|
||||
log(LOG_INFO,"Account creation successful:" .. name)
|
||||
--We sucessfully made the new author
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
local tags = require("tags")
|
||||
local util = require("util")
|
||||
local pages = require("pages")
|
||||
local config = require("config")
|
||||
local session = require("session")
|
||||
local db = require("db")
|
||||
local queries = require("queries")
|
||||
local sql = require("lsqlite3")
|
||||
local cache = require("cache")
|
||||
|
||||
local oldconfigure = configure
|
||||
local stmnt_delete
|
||||
function configure(...)
|
||||
stmnt_delete = assert(db.conn:prepare(queries.delete_post),db.conn:errmsg())
|
||||
return oldconfigure(...)
|
||||
end
|
||||
|
||||
local function delete_post(req)
|
||||
local host = http_request_get_host(req)
|
||||
local path = http_request_get_path(req)
|
||||
http_request_populate_post(req)
|
||||
local storystr = assert(http_argument_get_string(req,"story"))
|
||||
print("Looking at storystr:",storystr)
|
||||
local storyid = util.decode_id(storystr)
|
||||
local author, authorid = session.get(req)
|
||||
if not author then
|
||||
http_response(req, 401, pages.error{
|
||||
errcode = 401,
|
||||
errcodemsg = "Not authorized",
|
||||
explanation = "You must be logged in to delete posts. You are either not logged in or your session has expired.",
|
||||
should_traceback = true
|
||||
})
|
||||
return
|
||||
end
|
||||
log(LOG_DEBUG,string.format("Deleting post %d with proposed owner %d",storyid, authorid))
|
||||
stmnt_delete:bind_names{
|
||||
postid = storyid,
|
||||
authorid = authorid
|
||||
}
|
||||
local err = db.do_sql(stmnt_delete)
|
||||
if err ~= sql.DONE then
|
||||
log(LOG_DEBUG,string.format("Failed to delete: %d:%s",err, db.conn:errmsg()))
|
||||
http_response(req,500,pages.error{
|
||||
errcode = 500,
|
||||
errcodemsg = "Internal error",
|
||||
explanation = "Failed to delete posts from database:" .. db.conn:errmsg(),
|
||||
should_traceback = true,
|
||||
})
|
||||
stmnt_delete:reset()
|
||||
else
|
||||
local loc = string.format("https://%s/%s",config.domain,storystr)
|
||||
http_response_header(req,"Location",loc)
|
||||
http_response(req,303,"")
|
||||
stmnt_delete:reset()
|
||||
cache.dirty(string.format("%s",config.domain))
|
||||
cache.dirty(string.format("%s-logout",config.domain))
|
||||
cache.dirty(string.format("%s.%s",author,config.domain))
|
||||
cache.dirty(string.format("%s",storystr))
|
||||
cache.dirty(string.format("%s?comments=1",storystr))
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
return delete_post
|
|
@ -25,7 +25,7 @@ local function download_get(req)
|
|||
stmnt_download:bind_names{
|
||||
postid = story_id
|
||||
}
|
||||
local err = util.do_sql(stmnt_download)
|
||||
local err = db.do_sql(stmnt_download)
|
||||
if err == sql.DONE then
|
||||
--No rows, story not found
|
||||
http_response(req,404,pages.nostory{path=story})
|
||||
|
|
|
@ -32,7 +32,7 @@ local function edit_get(req)
|
|||
postid = story_id,
|
||||
authorid = authorid
|
||||
}
|
||||
local err = util.do_sql(stmnt_edit)
|
||||
local err = db.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)
|
||||
|
@ -46,7 +46,6 @@ local function edit_get(req)
|
|||
assert(err == sql.ROW)
|
||||
local data = stmnt_edit:get_values()
|
||||
local txt_compressed, markup, isanon, title, unlisted = unpack(data)
|
||||
print("from query, unlisted was:",unlisted)
|
||||
local text = zlib.decompress(txt_compressed)
|
||||
local tags = tags.get(story_id)
|
||||
local tags_txt = table.concat(tags,";")
|
||||
|
|
|
@ -38,10 +38,19 @@ local function edit_post(req)
|
|||
stmnt_author_of:bind_names{
|
||||
id = storyid
|
||||
}
|
||||
local err = util.do_sql(stmnt_author_of)
|
||||
local err = db.do_sql(stmnt_author_of)
|
||||
if err ~= sql.ROW then
|
||||
stmnt_author_of:reset()
|
||||
error("No author found for story:" .. storyid)
|
||||
local msg = string.format("No author found for story: %d", storyid)
|
||||
log(LOG_ERR,msg)
|
||||
local response = pages.error{
|
||||
errcode = 404,
|
||||
errcodemsg = "Not Found",
|
||||
explanation = msg,
|
||||
should_traceback = true,
|
||||
}
|
||||
http_response(req,404,response)
|
||||
return
|
||||
end
|
||||
local data = stmnt_author_of:get_values()
|
||||
stmnt_author_of:reset()
|
||||
|
@ -57,14 +66,14 @@ local function edit_post(req)
|
|||
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")
|
||||
assert(db.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,unlisted) == sql.OK)
|
||||
assert(stmnt_update:bind(5,storyid) == sql.OK)
|
||||
assert(util.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
|
||||
assert(db.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
|
||||
stmnt_update:reset()
|
||||
tagslib.set(storyid,tags)
|
||||
local id_enc = util.encode_id(storyid)
|
||||
|
@ -72,7 +81,7 @@ local function edit_post(req)
|
|||
local loc = string.format("https://%s/%s",config.domain,id_enc)
|
||||
if unlisted then
|
||||
stmnt_hash:bind_names{id=storyid}
|
||||
local err = util.do_sql(stmnt_hash)
|
||||
local err = db.do_sql(stmnt_hash)
|
||||
if err ~= sql.ROW then
|
||||
error("Failed to get a post's hash while trying to make it unlisted")
|
||||
end
|
||||
|
@ -92,6 +101,7 @@ local function edit_post(req)
|
|||
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.
|
||||
cache.dirty(string.format("%s-logout",config.domain))
|
||||
http_response_header(req,"Location",loc)
|
||||
http_response(req,303,"")
|
||||
return
|
||||
|
|
|
@ -8,6 +8,8 @@ local config = require("config")
|
|||
local pages = require("pages")
|
||||
local libtags = require("tags")
|
||||
local session = require("session")
|
||||
local parsers = require("parsers")
|
||||
local zlib = require("zlib")
|
||||
|
||||
local stmnt_index, stmnt_author, stmnt_author_bio
|
||||
|
||||
|
@ -26,7 +28,7 @@ local function get_site_home(req, loggedin)
|
|||
log(LOG_DEBUG,"Cache miss, rendering site index")
|
||||
stmnt_index:bind_names{}
|
||||
local latest = {}
|
||||
for idr, title, iar, dater, author, hits in util.sql_rows(stmnt_index) do
|
||||
for idr, title, iar, dater, author, hits in db.sql_rows(stmnt_index) do
|
||||
table.insert(latest,{
|
||||
url = util.encode_id(idr),
|
||||
title = title,
|
||||
|
@ -43,13 +45,12 @@ local function get_site_home(req, loggedin)
|
|||
loggedin = loggedin
|
||||
}
|
||||
end
|
||||
local function get_author_home(req)
|
||||
--print("Looking at author home...")
|
||||
local function get_author_home(req, loggedin)
|
||||
local host = http_request_get_host(req)
|
||||
local subdomain = host:match("([^\\.]+)")
|
||||
stmnt_author_bio:bind_names{author=subdomain}
|
||||
local err = util.do_sql(stmnt_author_bio)
|
||||
local author, authorid = session.get(req)
|
||||
local err = db.do_sql(stmnt_author_bio)
|
||||
if err == sql.DONE then
|
||||
log(LOG_INFO,"No such author:" .. subdomain)
|
||||
stmnt_author_bio:reset()
|
||||
|
@ -57,14 +58,20 @@ local function get_author_home(req)
|
|||
author = subdomain
|
||||
}
|
||||
end
|
||||
assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err))
|
||||
if err ~= sql.ROW then
|
||||
stmnt_author_bio:reset()
|
||||
error(string.format("Failed to get author %q error: %q",subdomain, tostring(err)))
|
||||
end
|
||||
local data = stmnt_author_bio:get_values()
|
||||
local bio = data[1]
|
||||
local bio_text = data[1]
|
||||
if data[1] ~= "" then
|
||||
bio_text = zlib.decompress(data[1])
|
||||
end
|
||||
local bio = parsers.plain(bio_text)
|
||||
stmnt_author_bio:reset()
|
||||
stmnt_author:bind_names{author=subdomain}
|
||||
local stories = {}
|
||||
for id, title, time, hits, unlisted, hash in util.sql_rows(stmnt_author) do
|
||||
--print("Looking at:",id,title,time,hits,unlisted)
|
||||
for id, title, time, hits, unlisted, hash in db.sql_rows(stmnt_author) do
|
||||
if unlisted == 1 and author == subdomain then
|
||||
local url = util.encode_id(id) .. "?pwd=" .. util.encode_unlisted(hash)
|
||||
table.insert(stories,{
|
||||
|
@ -112,18 +119,19 @@ local function index_get(req)
|
|||
end)
|
||||
elseif host == config.domain and author then
|
||||
--Display home page with "log out" button
|
||||
text = cache.render(config.domain .. "-logout", function()
|
||||
local cachepath = string.format("%s-logout",config.domain)
|
||||
text = cache.render(cachepath, function()
|
||||
return get_site_home(req,true)
|
||||
end)
|
||||
elseif host ~= config.domain and author ~= subdomain then
|
||||
--author home page
|
||||
local cachepath = string.format("%s.%s",subdomain,config.domain)
|
||||
text = cache.render(cachepath, function()
|
||||
return get_author_home(req)
|
||||
return get_author_home(req, author ~= nil)
|
||||
end)
|
||||
elseif host ~= config.domain and author == subdomain then
|
||||
--author's home page for the author, don't cache, display unlisted
|
||||
text = get_author_home(req)
|
||||
text = get_author_home(req, author ~= nil)
|
||||
end
|
||||
assert(text)
|
||||
http_response(req,200,text)
|
||||
|
|
|
@ -27,7 +27,7 @@ local function login_post(req)
|
|||
name = name
|
||||
}
|
||||
local text
|
||||
local err = util.do_sql(stmnt_author_acct)
|
||||
local err = db.do_sql(stmnt_author_acct)
|
||||
if err == sql.ROW then
|
||||
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
|
||||
stmnt_author_acct:reset()
|
||||
|
@ -35,7 +35,10 @@ local function login_post(req)
|
|||
local hash = sha3(todigest)
|
||||
if hash == passhash then
|
||||
local mysession = session.start(id)
|
||||
http_response_cookie(req,"session",mysession,"/",0,0)
|
||||
local domain_no_port = config.domain:match("(.*):.*") or config.domain
|
||||
http_response_header(req,"set-cookie",string.format(
|
||||
[[session=%s; SameSite=Lax; Path=/; Domain=%s; HttpOnly; Secure]],mysession,domain_no_port
|
||||
))
|
||||
local loc = string.format("https://%s.%s",name,config.domain)
|
||||
http_response_header(req,"Location",loc)
|
||||
http_response(req,303,"")
|
||||
|
|
|
@ -13,7 +13,6 @@ local function paste_get(req)
|
|||
http_response(req,303,"")
|
||||
return
|
||||
elseif host == config.domain and author == nil then
|
||||
print("doing anon paste")
|
||||
text = cache.render(string.format("%s/_paste",host),function()
|
||||
log(LOG_DEBUG, "Cache missing, rendering post page")
|
||||
return assert(pages.paste{
|
||||
|
@ -26,7 +25,6 @@ local function paste_get(req)
|
|||
end)
|
||||
http_response(req,200,text)
|
||||
elseif host ~= config.domain and author then
|
||||
print("doing author paste")
|
||||
text = assert(pages.author_paste{
|
||||
domain = config.domain,
|
||||
user = author,
|
||||
|
|
|
@ -43,16 +43,15 @@ local function anon_paste(req,ps)
|
|||
--with a more elegent solution.
|
||||
|
||||
log(LOG_DEBUG,string.format("new story: %q, length: %d",ps.title,string.len(ps.text)))
|
||||
print("Unlisted:",ps.unlisted)
|
||||
local textsha3 = sha3(ps.text .. get_random_bytes(32))
|
||||
util.sqlbind(stmnt_paste,"bind_blob",1,ps.text)
|
||||
util.sqlbind(stmnt_paste,"bind",2,ps.title)
|
||||
util.sqlbind(stmnt_paste,"bind",3,-1)
|
||||
util.sqlbind(stmnt_paste,"bind",4,true)
|
||||
util.sqlbind(stmnt_paste,"bind_blob",5,"")
|
||||
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
|
||||
util.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
|
||||
err = util.do_sql(stmnt_paste)
|
||||
db.sqlbind(stmnt_paste,"bind_blob",1,ps.text)
|
||||
db.sqlbind(stmnt_paste,"bind",2,ps.title)
|
||||
db.sqlbind(stmnt_paste,"bind",3,-1)
|
||||
db.sqlbind(stmnt_paste,"bind",4,true)
|
||||
db.sqlbind(stmnt_paste,"bind_blob",5,"")
|
||||
db.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
|
||||
db.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
|
||||
local err = db.do_sql(stmnt_paste)
|
||||
stmnt_paste:reset()
|
||||
if err == sql.DONE then
|
||||
local rowid = stmnt_paste:last_insert_rowid()
|
||||
|
@ -63,7 +62,7 @@ local function anon_paste(req,ps)
|
|||
assert(stmnt_raw:bind(1,rowid) == sql.OK)
|
||||
assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK)
|
||||
assert(stmnt_raw:bind(3,ps.markup) == sql.OK)
|
||||
err = util.do_sql(stmnt_raw)
|
||||
err = db.do_sql(stmnt_raw)
|
||||
stmnt_raw:reset()
|
||||
if err ~= sql.DONE then
|
||||
local msg = string.format(
|
||||
|
@ -113,9 +112,9 @@ local function author_paste(req,ps)
|
|||
assert(stmnt_paste:bind(3,authorid) == sql.OK)
|
||||
assert(stmnt_paste:bind(4,asanon == "anonymous") == sql.OK)
|
||||
assert(stmnt_paste:bind_blob(5,"") == sql.OK)
|
||||
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
|
||||
util.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
|
||||
local err = util.do_sql(stmnt_paste)
|
||||
db.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
|
||||
db.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
|
||||
local err = db.do_sql(stmnt_paste)
|
||||
stmnt_paste:reset()
|
||||
if err == sql.DONE then
|
||||
local rowid = stmnt_paste:last_insert_rowid()
|
||||
|
@ -126,7 +125,7 @@ local function author_paste(req,ps)
|
|||
assert(stmnt_raw:bind(1,rowid) == sql.OK)
|
||||
assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK)
|
||||
assert(stmnt_raw:bind(3,ps.markup) == sql.OK)
|
||||
err = util.do_sql(stmnt_raw)
|
||||
err = db.do_sql(stmnt_raw)
|
||||
stmnt_raw:reset()
|
||||
if err ~= sql.DONE then
|
||||
local msg = string.format(
|
||||
|
@ -149,6 +148,7 @@ local function author_paste(req,ps)
|
|||
cache.dirty(string.format("%s.%s",author,config.domain))
|
||||
cache.dirty(string.format("%s/%s",config.domain,url))
|
||||
cache.dirty(string.format("%s",config.domain))
|
||||
cache.dirty(string.format("%s-logout",config.domain))
|
||||
end
|
||||
http_response_header(req,"Location",loc)
|
||||
http_response(req,303,"")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
local sql = require("sqlite3")
|
||||
local sql = require("lsqlite3")
|
||||
|
||||
local session = require("session")
|
||||
local tags = require("tags")
|
||||
|
@ -8,6 +8,7 @@ local util = require("util")
|
|||
local cache = require("cache")
|
||||
local pages = require("pages")
|
||||
local config = require("config")
|
||||
local zlib = require("zlib")
|
||||
|
||||
local stmnt_read, stmnt_update_views, stmnt_comments
|
||||
|
||||
|
@ -27,7 +28,7 @@ local function add_view(storyid)
|
|||
stmnt_update_views:bind_names{
|
||||
id = storyid
|
||||
}
|
||||
local err = util.do_sql(stmnt_update_views)
|
||||
local err = db.do_sql(stmnt_update_views)
|
||||
assert(err == sql.DONE, "Failed to update view counter:"..tostring(err))
|
||||
stmnt_update_views:reset()
|
||||
end
|
||||
|
@ -41,7 +42,7 @@ local function populate_ps_story(req,ps)
|
|||
stmnt_read:bind_names{
|
||||
id = ps.storyid,
|
||||
}
|
||||
local err = util.do_sql(stmnt_read)
|
||||
local err = db.do_sql(stmnt_read)
|
||||
if err == sql.DONE then
|
||||
--We got no story
|
||||
stmnt_read:reset()
|
||||
|
@ -80,7 +81,7 @@ local function get_comments(req,ps)
|
|||
id = ps.storyid
|
||||
}
|
||||
local comments = {}
|
||||
for com_author, com_isanon, com_text in util.sql_rows(stmnt_comments) do
|
||||
for com_author, com_isanon, com_text in db.sql_rows(stmnt_comments) do
|
||||
table.insert(comments,{
|
||||
author = com_author,
|
||||
isanon = com_isanon == 1, --int to boolean
|
||||
|
@ -97,6 +98,10 @@ local function read_get(req)
|
|||
host = http_request_get_host(req),
|
||||
path = http_request_get_path(req),
|
||||
method = http_method_text(req),
|
||||
extra_load = {
|
||||
'<script src="/_js/bookmark.js"></script>',
|
||||
'<script src="/_js/intervine_deletion.js"></script>',
|
||||
}
|
||||
}
|
||||
local err
|
||||
--Get our story id
|
||||
|
@ -148,7 +153,6 @@ local function read_get(req)
|
|||
table.insert(params,"pwd=" .. hashstr)
|
||||
end
|
||||
local cachestrparts = {
|
||||
ps.host,
|
||||
ps.path,
|
||||
}
|
||||
if #params > 0 then
|
||||
|
@ -173,6 +177,12 @@ local function read_get(req)
|
|||
text = pages.read(ps)
|
||||
end
|
||||
end
|
||||
|
||||
--If this isn't unlisted, dirty everywhere the hit counter is shown
|
||||
cache.dirty(string.format("%s",config.domain))
|
||||
cache.dirty(string.format("%s/%s",config.domain,ps.idp)) -- This place to read this post
|
||||
cache.dirty(string.format("%s.%s",config.domain,ps.idp)) -- The author's index page
|
||||
|
||||
assert(text)
|
||||
http_response(req,200,text)
|
||||
return
|
||||
|
|
|
@ -38,7 +38,7 @@ local function read_post(req)
|
|||
isanon = isanon,
|
||||
comment_text = comment_text,
|
||||
}
|
||||
local err = util.do_sql(stmnt_comment_insert)
|
||||
local err = db.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.")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
-- Various global functions to cause less typing.
|
||||
|
||||
|
||||
function assertf(bool, fmt, ...)
|
||||
fmt = fmt or "Assetion Failed"
|
||||
if not bool then
|
||||
error(string.format(fmt,...),2)
|
||||
end
|
||||
end
|
186
src/lua/init.lua
186
src/lua/init.lua
|
@ -11,39 +11,17 @@ local et = require("etlua")
|
|||
local sql = require("lsqlite3")
|
||||
local zlib = require("zlib")
|
||||
|
||||
--stub for detouring
|
||||
--stubs for detouring
|
||||
function configure(...) end
|
||||
|
||||
--smr code
|
||||
require("global")
|
||||
local cache = require("cache")
|
||||
local pages = require("pages")
|
||||
local util = require("util")
|
||||
local config = require("config")
|
||||
local db = require("db")
|
||||
|
||||
--Pages
|
||||
local endpoint_names = {
|
||||
read = {"get","post"},
|
||||
preview = {"post"},
|
||||
index = {"get"},
|
||||
paste = {"get","post"},
|
||||
download = {"get"},
|
||||
login = {"get","post"},
|
||||
logout = {"get"},
|
||||
edit = {"get","post"},
|
||||
claim = {"get","post"},
|
||||
search = {"get"},
|
||||
archive = {"get"},
|
||||
api = {"get"},
|
||||
}
|
||||
local endpoints = {}
|
||||
for name, methods in pairs(endpoint_names) do
|
||||
for _,method in pairs(methods) do
|
||||
local epn = string.format("%s_%s",name,method)
|
||||
endpoints[epn] = require("endpoints." .. epn)
|
||||
end
|
||||
end
|
||||
|
||||
print("Hello from init.lua")
|
||||
local oldconfigure = configure
|
||||
function configure(...)
|
||||
|
@ -57,71 +35,79 @@ function configure(...)
|
|||
end
|
||||
print("Created configure function")
|
||||
|
||||
function home(req)
|
||||
local method = http_method_text(req)
|
||||
if method == "GET" then
|
||||
endpoints.index_get(req)
|
||||
-- TODO: Fill this out
|
||||
local http_methods = {"GET","POST"}
|
||||
local http_m_rev = {}
|
||||
for k,v in pairs(http_methods) do
|
||||
http_m_rev[v] = true
|
||||
end
|
||||
|
||||
--Endpoints, all this stuff gets required here.
|
||||
for funcname, spec in pairs({
|
||||
home = {
|
||||
GET = require("endpoints.index_get"),
|
||||
},
|
||||
claim = {
|
||||
GET = require("endpoints.claim_get"),
|
||||
POST = require("endpoints.claim_post"),
|
||||
},
|
||||
paste = {
|
||||
GET = require("endpoints.paste_get"),
|
||||
POST = require("endpoints.paste_post"),
|
||||
},
|
||||
read = {
|
||||
GET = require("endpoints.read_get"),
|
||||
POST = require("endpoints.read_post"),
|
||||
},
|
||||
login = {
|
||||
GET = require("endpoints.login_get"),
|
||||
POST = require("endpoints.login_post"),
|
||||
},
|
||||
logout = {
|
||||
GET = require("endpoints.logout_get"),
|
||||
},
|
||||
edit = {
|
||||
GET = require("endpoints.edit_get"),
|
||||
POST = require("endpoints.edit_post"),
|
||||
},
|
||||
delete = {
|
||||
POST = require("endpoints.delete_post"),
|
||||
},
|
||||
edit_bio = {
|
||||
GET = require("endpoints.bio_get"),
|
||||
POST = require("endpoints.bio_post"),
|
||||
},
|
||||
download = {
|
||||
GET = require("endpoints.download_get"),
|
||||
},
|
||||
preview = {
|
||||
POST = require("endpoints.preview_post"),
|
||||
},
|
||||
search = {
|
||||
GET = require("endpoints.search_get"),
|
||||
},
|
||||
archive = {
|
||||
GET = require("endpoints.archive_get"),
|
||||
},
|
||||
api = {
|
||||
GET = require("endpoints.api_get"),
|
||||
},
|
||||
}) do
|
||||
assert(_G[funcname] == nil, "Tried to overwrite an endpoint, please define endpoints exactly once")
|
||||
for k,v in pairs(spec) do
|
||||
assert(http_m_rev[k], "Unknown http method '" .. k .. "' defined for endpoint '" .. funcname .. "'")
|
||||
assert(type(v) == "function", "Endpoint %s %s must be a function, but was a %s",funcname, k, type(v))
|
||||
end
|
||||
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)
|
||||
if method == "GET" then
|
||||
endpoints.claim_get(req)
|
||||
elseif method == "POST" then
|
||||
endpoints.claim_post(req)
|
||||
_G[funcname] = function(req)
|
||||
local method = http_method_text(req)
|
||||
if spec[method] == nil then
|
||||
log(LOG_WARNING,string.format("Endpoint %s called with http method %s, but no such route defined.", funcname, method))
|
||||
else
|
||||
log(LOG_DEBUG,string.format("Endpoint %s called with method %s",funcname,method))
|
||||
end
|
||||
spec[method](req)
|
||||
end
|
||||
end
|
||||
|
||||
--Create a new paste on the site
|
||||
function paste(req)
|
||||
local method = http_method_text(req)
|
||||
if method == "GET" then
|
||||
endpoints.paste_get(req)
|
||||
elseif method == "POST" then
|
||||
endpoints.paste_post(req)
|
||||
end
|
||||
end
|
||||
|
||||
function read(req)
|
||||
local method = http_method_text(req)
|
||||
if method == "GET" then
|
||||
endpoints.read_get(req)
|
||||
elseif method == "POST" then
|
||||
endpoints.read_post(req)
|
||||
end
|
||||
end
|
||||
|
||||
function login(req)
|
||||
local method = http_method_text(req)
|
||||
if method == "GET" then
|
||||
endpoints.login_get(req)
|
||||
elseif method == "POST" then
|
||||
endpoints.login_post(req)
|
||||
end
|
||||
end
|
||||
|
||||
function logout(req)
|
||||
endpoints.logout_get(req)
|
||||
end
|
||||
|
||||
--Edit a story
|
||||
function edit(req)
|
||||
local method = http_method_text(req)
|
||||
if method == "GET" then
|
||||
endpoints.edit_get(req)
|
||||
elseif method == "POST" then
|
||||
endpoints.edit_post(req)
|
||||
end
|
||||
end
|
||||
|
||||
--TODO
|
||||
function edit_bio()
|
||||
error("Not yet implemented")
|
||||
log(LOG_INFO,string.format("Associateing endpoint %q", funcname))
|
||||
end
|
||||
|
||||
function teardown()
|
||||
|
@ -135,30 +121,4 @@ function teardown()
|
|||
print("Finished lua teardown")
|
||||
end
|
||||
|
||||
function download(req)
|
||||
endpoints.download_get(req)
|
||||
end
|
||||
|
||||
function preview(req)
|
||||
endpoints.preview_post(req)
|
||||
end
|
||||
|
||||
function search(req)
|
||||
endpoints.search_get(req)
|
||||
end
|
||||
|
||||
function archive(req)
|
||||
print("archive method:",http_method_text(req))
|
||||
endpoints.archive_get(req)
|
||||
end
|
||||
|
||||
function api(req)
|
||||
local method = http_method_text(req)
|
||||
if method == "GET" then
|
||||
endpoints.api_get(req)
|
||||
elseif method == "POST" then
|
||||
endpoints.api_post(req)
|
||||
end
|
||||
end
|
||||
|
||||
print("Done with init.lua")
|
||||
|
|
|
@ -18,6 +18,7 @@ local pagenames = {
|
|||
"author_edit",
|
||||
"search",
|
||||
"error",
|
||||
"edit_bio",
|
||||
}
|
||||
local pages = {}
|
||||
for k,v in pairs(pagenames) do
|
||||
|
|
|
@ -60,8 +60,9 @@ local function wrap(seq,format,V"sup")
|
|||
end
|
||||
end
|
||||
]]
|
||||
|
||||
local function wrap(seq,format,s)
|
||||
return P(seq) * Cs((((V"marked" - s) + word + P"\n"))^1) * P(seq) / function(a)
|
||||
return P(seq) * Cs(((s + word + P"\n"))^0) * P(seq) / function(a)
|
||||
return string.format(format,a)
|
||||
end
|
||||
end
|
||||
|
@ -71,21 +72,22 @@ end
|
|||
local function tag(name,format)
|
||||
local start_tag = P(string.format("[%s]",name))
|
||||
local end_tag = P(string.format("[/%s]",name))
|
||||
return start_tag * Cs(((1 - end_tag))^1) * end_tag / function(a)
|
||||
return start_tag * Cs(((1 - end_tag))^0) * end_tag / function(a)
|
||||
return string.format(format,sanitize(a))
|
||||
end
|
||||
end
|
||||
|
||||
--local grammar = P(require('pegdebug').trace({
|
||||
local grammar = P{
|
||||
"chunk";
|
||||
--regular
|
||||
spoiler = wrap("**",[[<span class="spoiler">%s</span>]],V"spoiler"),
|
||||
spoiler2 = tag("spoiler",[[<span class="spoiler2">%s</span>]]),
|
||||
italic = wrap("''",[[<i>%s</i>]], V"italic"),
|
||||
bold = wrap("'''",[[<b>%s</b>]], V"bold"),
|
||||
underline = wrap("__",[[<u>%s</u>]], V"underline"),
|
||||
heading = wrap("==",[[<h2>%s</h2>]], V"heading"),
|
||||
strike = wrap("~~",[[<s>%s</s>]], V"strike"),
|
||||
heading = wrap("==",[[<h2>%s</h2>]], V"underline" + V"strike" + V"italic"),
|
||||
bold = wrap("'''",[[<b>%s</b>]], V"italic" + V"underline" + V"strike"),
|
||||
italic = wrap("''",[[<i>%s</i>]], V"underline" + V"strike"),
|
||||
underline = wrap("__",[[<u>%s</u>]], V"strike"),
|
||||
strike = wrap("~~",[[<s>%s</s>]], P("blah")),
|
||||
spoiler = wrap("**",[[<span class="spoiler">%s</span>]],V"spoiler2" + V"bold" + V"italic" + V"underline" + V"strike"),
|
||||
spoiler2 = tag("spoiler",[[<span class="spoiler2">%s</span>]],V"spoiler" + V"bold" + V"italic" + V"underline" + V"strike"),
|
||||
code = tag("code",[[<pre><code>%s</code></pre>]]),
|
||||
greentext = P">" * (B"\n>" + B">") * Cs((V"marked" + word)^0) / function(a)
|
||||
return string.format([[<span class="greentext">>%s</span>]],a)
|
||||
|
@ -97,7 +99,7 @@ local grammar = P{
|
|||
plainline = (V"marked" + word)^0,
|
||||
line = Cs(V"greentext" + V"pinktext" + V"plainline" + P"") * P"\n" / function(a)
|
||||
if a == "\r" then
|
||||
return "<br/>"
|
||||
return [[<p class="spacer"></p>]]
|
||||
else
|
||||
return string.format("<p>%s</p>",a)
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ local fields
|
|||
local grammar = P{
|
||||
"chunk";
|
||||
whitespace = S" \t\n"^0,
|
||||
itm = C(P(1-S"+-")^0), --go until the next '+' or '-'
|
||||
itm = C((P(1 - (P" " * S"+-")))^0), --go until the next '+' or '-'
|
||||
likefield = C(P"title" + P"author") * V"whitespace" * C(P"=") * V"whitespace" * V"itm",
|
||||
rangeop = P"<=" + P">=" + P">" + P"<" + P"=",
|
||||
rangefield = C(P"date" + P"hits") * V"whitespace" * C(V"rangeop") * V"whitespace" * C(V"itm"),
|
||||
|
@ -54,7 +54,7 @@ local grammar = P{
|
|||
table.insert(fields.tags,{pn,"=",field})
|
||||
end
|
||||
end,
|
||||
chunk = V"field"^0
|
||||
chunk = V"field" * (P" " * V"field")^0
|
||||
|
||||
}
|
||||
--Grammar
|
||||
|
|
|
@ -28,8 +28,9 @@ function session.get(req)
|
|||
stmnt_get_session:bind_names{
|
||||
key = sessionid
|
||||
}
|
||||
local err = util.do_sql(stmnt_get_session)
|
||||
local err = db.do_sql(stmnt_get_session)
|
||||
if err ~= sql.ROW then
|
||||
stmnt_get_session:reset()
|
||||
return nil, "No such session by logged in users"
|
||||
end
|
||||
local data = stmnt_get_session:get_values()
|
||||
|
@ -56,7 +57,7 @@ function session.start(who)
|
|||
sessionid = session,
|
||||
authorid = who
|
||||
}
|
||||
local err = util.do_sql(stmnt_insert_session)
|
||||
local err = db.do_sql(stmnt_insert_session)
|
||||
stmnt_insert_session:reset()
|
||||
assert(err == sql.DONE)
|
||||
return session
|
||||
|
@ -70,7 +71,7 @@ function session.finish(who,sessionid)
|
|||
authorid = who,
|
||||
sessionid = sessionid
|
||||
}
|
||||
local err = util.do_sql(stmnt_delete_session)
|
||||
local err = db.do_sql(stmnt_delete_session)
|
||||
stmnt_delete_session:reset()
|
||||
assert(err == sql.DONE)
|
||||
return true
|
||||
|
|
|
@ -41,13 +41,13 @@ end
|
|||
|
||||
function tags.set(storyid,tags)
|
||||
assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK)
|
||||
util.do_sql(stmnt_drop_tags)
|
||||
db.do_sql(stmnt_drop_tags)
|
||||
stmnt_drop_tags:reset()
|
||||
local err
|
||||
for _,tag in pairs(tags) do
|
||||
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)
|
||||
err = db.do_sql(stmnt_ins_tag)
|
||||
stmnt_ins_tag:reset()
|
||||
end
|
||||
if err ~= sql.DONE then
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
--[[
|
||||
Type checking, vaguely inspired by Python3's typing module.
|
||||
]]
|
||||
|
||||
local types = {}
|
||||
|
||||
function types.positive(arg)
|
||||
local is_number, err = types.number(arg)
|
||||
if not is_number then
|
||||
return false, err
|
||||
end
|
||||
if arg < 0 then
|
||||
return false, string.format("was not positive")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--Basic lua types
|
||||
local builtin_types = {
|
||||
"nil","boolean","number","string","table","function","coroutine","userdata"
|
||||
}
|
||||
for _,type_ in pairs(builtin_types) do
|
||||
types[type_] = function(arg)
|
||||
local argtype = type(arg)
|
||||
if not argtype == type_ then
|
||||
return false, string.format("was not a %s, was a %s",type_,argtype)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function types.matches_pattern(pattern)
|
||||
return function(arg)
|
||||
local is_string, err = types.string(arg)
|
||||
if not is_string then
|
||||
return false, err
|
||||
end
|
||||
if not string.match(arg, pattern) then
|
||||
return false, string.format(
|
||||
"Expected %q to match pattern %q, but it did not.",
|
||||
arg,
|
||||
pattern
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function types.check(...)
|
||||
|
||||
end
|
||||
|
||||
return types
|
109
src/lua/util.lua
109
src/lua/util.lua
|
@ -1,83 +1,9 @@
|
|||
|
||||
local sql = require("lsqlite3")
|
||||
local config = require("config")
|
||||
local types = require("types")
|
||||
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
|
||||
local i = 0
|
||||
repeat
|
||||
err = stmnt:step()
|
||||
if err == sql.BUSY then
|
||||
i = i + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
until(err ~= sql.BUSY or i > 10)
|
||||
assert(i < 10, "Database busy")
|
||||
return err
|
||||
end
|
||||
|
||||
--[[
|
||||
Provides an iterator that loops over results in an sql statement
|
||||
or throws an error, then resets the statement after the loop is done.
|
||||
]]
|
||||
function util.sql_rows(stmnt)
|
||||
if not stmnt then error("No statement",2) end
|
||||
local err
|
||||
return function()
|
||||
err = stmnt:step()
|
||||
if err == sql.BUSY then
|
||||
coroutine.yield()
|
||||
elseif err == sql.ROW then
|
||||
return unpack(stmnt:get_values())
|
||||
elseif err == sql.DONE then
|
||||
stmnt:reset()
|
||||
return nil
|
||||
else
|
||||
stmnt:reset()
|
||||
local msg = string.format(
|
||||
"SQL Iteration failed: %s : %s\n%s",
|
||||
tostring(err),
|
||||
db.conn:errmsg(),
|
||||
debug.traceback()
|
||||
)
|
||||
log(LOG_CRIT,msg)
|
||||
error(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Binds an argument to as statement with nice error reporting on failure
|
||||
stmnt :: sql.stmnt - the prepared sql statemnet
|
||||
call :: string - a string "bind" or "bind_blob"
|
||||
position :: number - the argument position to bind to
|
||||
data :: string - The data to bind
|
||||
]]
|
||||
function util.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.conn:errmsg()),2)
|
||||
end
|
||||
end
|
||||
|
||||
--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
|
||||
--no underscore because we use that for our operative pages
|
||||
local url_characters =
|
||||
|
@ -144,10 +70,8 @@ function util.decode_id(s)
|
|||
return n
|
||||
end)
|
||||
if res then
|
||||
print("returning id:",id)
|
||||
return id
|
||||
else
|
||||
print("Failed to decode id:" .. s)
|
||||
return false,"Failed to decode id:" .. s
|
||||
end
|
||||
end
|
||||
|
@ -191,7 +115,6 @@ end
|
|||
|
||||
--hex encoded string to arbitrary data
|
||||
function util.decode_unlisted(str)
|
||||
print("str was:",str)
|
||||
local output = {}
|
||||
for byte in str:gmatch("%x%x") do
|
||||
table.insert(output, string.char(tonumber(byte,16)))
|
||||
|
@ -224,6 +147,36 @@ function util.parse_tags(str)
|
|||
return tags
|
||||
end
|
||||
|
||||
if config.debugging then
|
||||
function util.checktypes(...)
|
||||
local args = {...}
|
||||
if #args == 1 then
|
||||
args = table.unpack(args)
|
||||
end
|
||||
assert(
|
||||
#args % 3 == 0,
|
||||
"Arguments to checktypes() must be triplets of " ..
|
||||
"<variable>, <lua type>, <type check function> "
|
||||
)
|
||||
for i = 1,#args,3 do
|
||||
local var, ltype, veri_f = args[i+0], args[i+1], args[i+2]
|
||||
assert(
|
||||
type(var) == ltype,
|
||||
string.format(
|
||||
"Expected argument %d (%q) to be type %s, but was %s",
|
||||
i/3
|
||||
)
|
||||
)
|
||||
if veri_f then
|
||||
assert(veri_f(var))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
function util.checktypes(...)
|
||||
end
|
||||
end
|
||||
|
||||
local function decodeentity(capture)
|
||||
return string.char(tonumber(capture,16)) --Decode base 16 and conver to character
|
||||
end
|
||||
|
|
|
@ -5,11 +5,28 @@
|
|||
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
|
||||
</h1>
|
||||
<div class="container">
|
||||
<a href="/_paste" class="button">New paste</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<%= bio %>
|
||||
<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>
|
||||
<form action="https://<%= domain %>/_search" method="get" class="search column row">
|
||||
<input class="column" type="text" name="q" placeholder="+greentext -dotr +hits>20"/>
|
||||
<input class="column column-0 button button-clear" type="submit" value="🔎"/>
|
||||
</form>
|
||||
</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.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<form action="https://<%= user %>.<%= domain %>/_edit" method="post" class="container">
|
||||
<fieldset>
|
||||
<div class="row">
|
||||
<input type="text" name="title" placeholder="Title" class="column column-70" value="<%= title %>"></input>
|
||||
<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 %>
|
||||
|
@ -21,7 +21,7 @@
|
|||
<option value="plain">Plain</option>
|
||||
<option value="imageboard">Imageboard</option>
|
||||
</select>
|
||||
<div class="column column-10">
|
||||
<div class="column column-20">
|
||||
<label for="unlisted" class="label-inline">Unlisted</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<% if author then %>
|
||||
<meta name="author" content="<%= author %>">
|
||||
<% end %>
|
||||
<% if title then %>
|
||||
<title><%- title %></title>
|
||||
<% else %>
|
||||
<title>🍑</title>
|
||||
<% end %>
|
||||
<link href="/_css/milligram.css" rel="stylesheet">
|
||||
<link href="/_css/style.css" rel="stylesheet">
|
||||
<% if extra_load then %>
|
||||
<% for _,load in ipairs(extra_load) do %>
|
||||
<%- load %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</head>
|
||||
<body class="container">
|
||||
<main class="wrapper">
|
||||
|
||||
<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>
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<{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>
|
||||
<{cat src/pages/parts/footer.etlua}>
|
||||
|
|
@ -8,17 +8,20 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<a href="/_paste" class="button column column-0">New paste</a>
|
||||
<a href="/_login" class="button column column-0">Log in</a>
|
||||
<a href="/_claim" class="button column column-0">Register</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 %>
|
||||
<form action="https://<%= domain %>/_search" method="get" class="search column row">
|
||||
<input class="column" type="text" name="q" placeholder="+greentext -dotr +hits>20"/>
|
||||
<input class="column column-0 button button-clear" type="submit" value="🔎"/>
|
||||
</form>
|
||||
</div>
|
||||
<p>
|
||||
Welcome to slash.monster, stories of fiction and fantasy<br/>
|
||||
Not safe for work<br/>
|
||||
18+
|
||||
<{ system cat src/pages/parts/motd.etlua }>
|
||||
</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
<tr><td>
|
||||
<% if unlisted then %>
|
||||
<% if story.unlisted then %>
|
||||
⛔
|
||||
<% end %>
|
||||
</td><td>
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
<% 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-70"></input>
|
||||
<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-10">
|
||||
<div class="column column-20">
|
||||
<label for="unlisted" class="label-inline">Unlisted</label>
|
||||
<input type="checkbox" name="unlisted" id="unlisted"></input>
|
||||
</div>
|
||||
|
|
|
@ -3,10 +3,16 @@
|
|||
<a href="https://<%= domain %>"><%= domain %></a>/<a href="https://<%= domain %>/<%= idp %>"><%= idp %></a>
|
||||
</nav>
|
||||
<% if owner then -%>
|
||||
<div class="row">
|
||||
<form action="https://<%= domain %>/_edit" method="get"><fieldset>
|
||||
<input type="hidden" name="story" value="<%= idp %>"/>
|
||||
<input type="submit" value="edit" class="button"/>
|
||||
<input type="submit" value="edit" class="button column column-0"/>
|
||||
</fieldset></form>
|
||||
<form action="https://<%= domain %>/_delete" method="post"><fieldset>
|
||||
<input type="hidden" name="story" value="<%= idp %>"/>
|
||||
<input type="submit" value="delete" class="button column column-0"/>
|
||||
</fieldset></form>
|
||||
</div>
|
||||
<% end -%>
|
||||
<article>
|
||||
<h2 class="title"> <%- title %> </h2>
|
||||
|
|
54
src/smr.c
54
src/smr.c
|
@ -29,10 +29,13 @@ int archive(struct http_request *);
|
|||
int api(struct http_request *);
|
||||
int style(struct http_request *);
|
||||
int miligram(struct http_request *);
|
||||
int delete(struct http_request *);
|
||||
int do_lua(struct http_request *req, const char *name);
|
||||
int errhandeler(lua_State *);
|
||||
lua_State *L;
|
||||
|
||||
/* These should be defined in in kore somewhere and included here */
|
||||
void kore_worker_configure(void);
|
||||
void kore_worker_teardown(void);
|
||||
/*
|
||||
static / index
|
||||
static / _post post
|
||||
|
@ -56,32 +59,32 @@ KORE_SECCOMP_FILTER("app",
|
|||
);
|
||||
|
||||
int
|
||||
errhandeler(lua_State *L){
|
||||
printf("Error: %s\n",lua_tostring(L,1));//"error"
|
||||
lua_getglobal(L,"debug");//"error",{debug}
|
||||
lua_getglobal(L,"print");//"error",{debug},print()
|
||||
lua_getfield(L,-2,"traceback");//"error",{debug},print(),traceback()
|
||||
lua_call(L,0,1);//"error",{debug},print(),"traceback"
|
||||
lua_call(L,1,0);//"error",{debug}
|
||||
errhandeler(lua_State *state){
|
||||
printf("Error: %s\n",lua_tostring(state,1));//"error"
|
||||
lua_getglobal(state,"debug");//"error",{debug}
|
||||
lua_getglobal(state,"print");//"error",{debug},print()
|
||||
lua_getfield(state,-2,"traceback");//"error",{debug},print(),traceback()
|
||||
lua_call(state,0,1);//"error",{debug},print(),"traceback"
|
||||
lua_call(state,1,0);//"error",{debug}
|
||||
printf("Called print()\n");
|
||||
lua_getfield(L,-1,"traceback");//"error",{debug},traceback()
|
||||
lua_getfield(state,-1,"traceback");//"error",{debug},traceback()
|
||||
printf("got traceback\n");
|
||||
lua_call(L,0,1);//"error",{debug},"traceback"
|
||||
lua_pushstring(L,"\n");
|
||||
lua_call(state,0,1);//"error",{debug},"traceback"
|
||||
lua_pushstring(state,"\n");
|
||||
printf("called traceback\n");
|
||||
lua_pushvalue(L,-4);//"error",{debug},"traceback","error"
|
||||
lua_pushvalue(state,-4);//"error",{debug},"traceback","error"
|
||||
printf("pushed error\n");
|
||||
lua_concat(L,3);//"error",{debug},"traceback .. error"
|
||||
lua_concat(state,3);//"error",{debug},"traceback .. error"
|
||||
printf("concated\n");
|
||||
int ref = luaL_ref(L,LUA_REGISTRYINDEX);//"error",{debug}
|
||||
lua_pop(L,2);//
|
||||
lua_rawgeti(L,LUA_REGISTRYINDEX,ref);//"traceback .. error"
|
||||
int ref = luaL_ref(state,LUA_REGISTRYINDEX);//"error",{debug}
|
||||
lua_pop(state,2);//
|
||||
lua_rawgeti(state,LUA_REGISTRYINDEX,ref);//"traceback .. error"
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
do_lua(struct http_request *req, const char *name){
|
||||
printf("About to do lua %s\n",name);
|
||||
//printf("About to do lua %s\n",name);
|
||||
lua_pushcfunction(L,errhandeler);
|
||||
lua_getglobal(L,name);//err(),name()
|
||||
if(!lua_isfunction(L,-1)){
|
||||
|
@ -104,61 +107,51 @@ do_lua(struct http_request *req, const char *name){
|
|||
|
||||
int
|
||||
post_story(struct http_request *req){
|
||||
printf("We want to post!\n");
|
||||
return do_lua(req,"paste");
|
||||
}
|
||||
|
||||
int
|
||||
edit_story(struct http_request *req){
|
||||
printf("We want to edit!\n");
|
||||
return do_lua(req,"edit");
|
||||
}
|
||||
|
||||
int
|
||||
edit_bio(struct http_request *req){
|
||||
printf("We want to edit bio!\n");
|
||||
return do_lua(req,"edit_bio");
|
||||
}
|
||||
|
||||
int
|
||||
read_story(struct http_request *req){
|
||||
printf("We want to read!\n");
|
||||
return do_lua(req,"read");
|
||||
}
|
||||
|
||||
int
|
||||
login(struct http_request *req){
|
||||
printf("We want to login!\n");
|
||||
return do_lua(req,"login");
|
||||
}
|
||||
|
||||
int
|
||||
logout(struct http_request *req){
|
||||
printf("We want to log out!\n");
|
||||
return do_lua(req,"logout");
|
||||
}
|
||||
|
||||
int
|
||||
claim(struct http_request *req){
|
||||
printf("We want to claim!\n");
|
||||
return do_lua(req,"claim");
|
||||
}
|
||||
|
||||
int
|
||||
download(struct http_request *req){
|
||||
printf("We want to do download!\n");
|
||||
return do_lua(req,"download");
|
||||
}
|
||||
|
||||
int
|
||||
preview(struct http_request *req){
|
||||
printf("We want to do preview!\n");
|
||||
return do_lua(req,"preview");
|
||||
}
|
||||
|
||||
int
|
||||
search(struct http_request *req){
|
||||
printf("We want to do search!\n");
|
||||
return do_lua(req,"search");
|
||||
}
|
||||
|
||||
|
@ -176,13 +169,11 @@ archive(struct http_request *req){
|
|||
return KORE_RESULT_OK;
|
||||
}
|
||||
*/
|
||||
printf("We want to do archive!\n");
|
||||
return do_lua(req,"archive");
|
||||
}
|
||||
|
||||
int
|
||||
api(struct http_request *req){
|
||||
printf("Api call!\n");
|
||||
return do_lua(req,"api");
|
||||
}
|
||||
|
||||
|
@ -191,6 +182,11 @@ home(struct http_request *req){
|
|||
return do_lua(req,"home");
|
||||
}
|
||||
|
||||
int
|
||||
delete(struct http_request *req){
|
||||
return do_lua(req,"delete");
|
||||
}
|
||||
|
||||
void
|
||||
kore_worker_configure(void){
|
||||
printf("Configuring worker...\n");
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
DELETE FROM posts
|
||||
WHERE posts.id = :postid AND
|
||||
posts.authorid = :authorid
|
|
@ -0,0 +1,3 @@
|
|||
SELECT biography
|
||||
FROM authors
|
||||
WHERE authors.id = :authorid;
|
|
@ -3,4 +3,4 @@ FROM authors, sessions
|
|||
WHERE
|
||||
sessions.key = :key AND
|
||||
sessions.author = authors.id AND
|
||||
sessions.start - strftime('%s','now') < 60*60*24;
|
||||
sessions.start - strftime('%s','now') <= 60*60*24;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE authors
|
||||
SET biography = ?
|
||||
WHERE authors.id = ?;
|
Loading…
Reference in New Issue