Index: backend/pgsql.t
==================================================================
--- backend/pgsql.t
+++ backend/pgsql.t
@@ -552,10 +552,17 @@
};
artifact_load = {
params = {uint64}, sql = [[
select content, mime from parsav_artifacts where id = $1::bigint
]];
+ };
+ artifact_folder_enum = {
+ params = {uint64}, sql = [[
+ select distinct folder from parsav_artifact_claims where
+ uid = $1::bigint and folder is not null
+ order by folder
+ ]];
};
post_attach_ctl_ins = {
params = {uint64, uint64}, cmd=true, sql = [[
update parsav_posts set
artifacts = artifacts || $2::bigint
@@ -828,10 +835,27 @@
else
return pqr {ct, res}
end
end
end
+
+local terra row_to_artifact(res: &pqr, i: intptr): lib.mem.ptr(lib.store.artifact)
+ var id = res:int(uint64,i,0)
+ var idbuf: int8[lib.math.shorthand.maxlen]
+ var idlen = lib.math.shorthand.gen(id, &idbuf[0])
+ var desc = res:_string(i,2)
+ var folder = res:_string(i,3)
+ var mime = res:_string(i,4)
+ var m = [ lib.str.encapsulate(lib.store.artifact, {
+ desc = {`desc.ptr, `desc.ct + 1};
+ folder = {`folder.ptr, `folder.ct + 1};
+ mime = {`mime.ptr, `mime.ct + 1};
+ url = {`&idbuf[0], `idlen + 1};
+ }) ]
+ m.ptr.rid = id
+ return m
+end
local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
var subj: rawstring, sblen: intptr
var cvhu: rawstring, cvhlen: intptr
if r:null(row,3)
@@ -1583,28 +1607,31 @@
)
var res = queries.artifact_enum_uid.exec(src,uid,folder)
if res.sz > 0 then
var m = lib.mem.heapa([lib.mem.ptr(lib.store.artifact)], res.sz)
for i=0,res.sz do
- var id = res:int(uint64,i,0)
- var idbuf: int8[lib.math.shorthand.maxlen]
- var idlen = lib.math.shorthand.gen(id, &idbuf[0])
- var desc = res:_string(i,2)
- var folder = res:_string(i,3)
- var mime = res:_string(i,4)
- m.ptr[i] = [ lib.str.encapsulate(lib.store.artifact, {
- desc = {`desc.ptr, `desc.ct + 1};
- folder = {`folder.ptr, `folder.ct + 1};
- mime = {`mime.ptr, `mime.ct + 1};
- url = {`&idbuf[0], `idlen + 1};
- }) ]
- m(i).ptr.rid = id
+ m.ptr[i] = row_to_artifact(&res, i)
m(i).ptr.owner = uid
end
return m
else return [lib.mem.lstptr(lib.store.artifact)].null() end
end];
+
+ artifact_fetch = [terra(
+ src: &lib.store.source,
+ uid: uint64,
+ rid: uint64
+ )
+ var res = queries.artifact_fetch.exec(src,uid,rid)
+ if res.sz > 0 then
+ var a = row_to_artifact(&res, 0)
+ a.ptr.owner = uid
+ res:free()
+ return a
+ end
+ return [lib.mem.ptr(lib.store.artifact)].null()
+ end];
artifact_load = [terra(
src: &lib.store.source,
rid: uint64
): {binblob, pstring}
@@ -1617,10 +1644,22 @@
lib.mem.cpy(bin.ptr, mbin.ptr, bin.ct)
r:free()
return bin, mime
end];
+
+ artifact_folder_enum = [terra(
+ src: &lib.store.source,
+ uid: uint64
+ )
+ var r = queries.artifact_folder_enum.exec(src,uid)
+ if r.sz == 0 then return [lib.mem.ptr(pstring)].null() end
+ defer r:free()
+ var lst = lib.mem.heapa(pstring, r.sz)
+ for i=0,r.sz do lst.ptr[i] = r:String(i,0) end
+ return lst
+ end];
post_attach_ctl = [terra(
src: &lib.store.source,
post: uint64,
artifact: uint64,
Index: html.t
==================================================================
--- html.t
+++ html.t
@@ -1,10 +1,12 @@
-- vim: ft=terra
local m={}
local pstr = lib.mem.ptr(int8)
terra m.sanitize(txt: pstr, quo: bool)
+ if txt.ptr == nil then return pstr.null() end
+ if txt.ct == 0 then txt.ct = lib.str.sz(txt.ptr) end
var a: lib.str.acc a:init(txt.ct*1.3)
for i=0,txt.ct do
if txt(i) == @'<' then a:lpush('<')
elseif txt(i) == @'>' then a:lpush('>')
elseif txt(i) == @'&' then a:lpush('&')
@@ -11,7 +13,38 @@
elseif quo and txt(i) == @'"' then a:lpush('"')
else a:push(&txt(i),1) end
end
return a:finalize()
end
+
+terra m.hexdgt(i: uint8)
+ if i >= 10 then
+ return @'A' + (i - 10)
+ else return 0x30 + i end
+end
+
+terra m.hexbyte(i: uint8): int8[2]
+ return arrayof(int8,
+ m.hexdgt(i / 0x10),
+ m.hexdgt(i % 0x10))
+end
+
+terra m.urlenc(txt: pstr, qparam: bool)
+ if txt.ptr == nil then return pstr.null() end
+ if txt.ct == 0 then txt.ct = lib.str.sz(txt.ptr) end
+ var a: lib.str.acc a:init(txt.ct*1.3)
+ for i=0,txt.ct do
+ if txt(i) == @' ' then a:lpush('+')
+ elseif txt(i) == @'&' and not qparam then a:lpush('&')
+ elseif (txt(i) < 0x2c or
+ (txt(i) > @';' and txt(i) < @'@') or
+ (txt(i) > @'Z' and txt(i) < @'a') or
+ (txt(i) >= 0x7b and txt(i) <= 0x7f)) and
+ txt(i) ~= @'_' and (qparam == true or txt(i) ~= @'=') then
+ var str = m.hexbyte(txt(i))
+ a:lpush('%'):push(&str[0], 2)
+ else a:push(&txt(i),1) end
+ end
+ return a:finalize()
+end
return m
Index: render/media-gallery.t
==================================================================
--- render/media-gallery.t
+++ render/media-gallery.t
@@ -13,91 +13,190 @@
-- eq "media"
var owner = false
if co.aid ~= 0 and co.who.id == uid then owner = true end
var ou = co.srv:actor_fetch_uid(uid)
if not ou then goto e404 end
-
- var mode: uint8 = show_new
- var folder: pstr
- if path.ct == 2 then
- if path(1):cmp(lib.str.lit'unfiled') then
- mode=show_unfiled
- elseif path(1):cmp(lib.str.lit'all') then
- mode=show_all
- else goto e404 end
- elseif path.ct == 3 and path(1):cmp(lib.str.lit'kind') then
- end
+ do defer ou:free()
+ var pfx = pstr.null()
+ if not owner then
+ var pa: lib.str.acc pa:init(32)
+ pa:lpush('/')
+ if ou(0).origin ~= 0 then pa:lpush('@') end
+ pa:push(ou(0).xid,0)
+ pfx = pa:finalize()
+ end
- if mode == show_new then
- folder = lib.str.plit''
- elseif mode == show_all then
- folder = pstr.null()
- elseif mode == show_unfiled then
- folder = lib.str.plit'' -- TODO
- -- else get folder from query str
- end
-
- var view = data.view.media_gallery {
- menu = pstr{'',0};
- folders = pstr{'',0};
- directory = pstr{'',0};
- images = pstr{'',0};
- pfx = pstr{'',0};
- }
- if not owner then
- var pa: lib.str.acc pa:init(32)
- pa:lpush('/')
- if ou(0).origin ~= 0 then pa:lpush('@') end
- pa:push(ou(0).xid,0)
- view.pfx = pa:finalize()
- end
-
- if owner then
- view.menu = P'upload
'
- end
-
- var md = co.srv:artifact_enum_uid(uid, folder)
- var gallery: lib.str.acc gallery:init(256)
- var files: lib.str.acc files:init(256)
- for i=0,md.ct do
- if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then
- gallery:lpush(':push(md(i)(0).url,0)
- :lpush(')
'):push(md(i)(0).desc,0)
- :lpush('
')
+ if path.ct >= 3 and path(1):cmp(lib.str.lit'a') then
+ var id, idok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
+ if not idok then goto e404 end
+ var art = co.srv:artifact_fetch(uid, id)
+ if not art then goto e404 end
+ if path.ct == 3 then
+ -- sniff out the artifact type and display the appropriate viewer
+ var artid = cs(art(0).url)
+ var btns: lib.str.acc
+ if owner then
+ btns:compose('deletealter')
+ else
+ btns:compose('collect')
+ end
+ var btntxt = btns:finalize() defer btntxt:free()
+ var desc = lib.smackdown.html(pstr{art(0).desc,0}, true) defer desc:free()
+ var viewerprops = {
+ pfx = pfx, desc = desc;
+ id = artid; btns = btntxt;
+ }
+ if lib.str.ncmp(art(0).mime, 'image/', 6) == 0 then
+ var view = data.view.media_image(viewerprops)
+ var pg = view:tostr()
+ co:stdpage([lib.srv.convo.page] {
+ title = lib.str.plit'media :: image';
+ class = lib.str.plit'media viewer img';
+ cache = false, body = pg;
+ })
+ pg:free()
+ elseif lib.str.cmp(art(0).mime, 'text/markdown') == 0 then
+ var view = data.view.media_text(viewerprops)
+ var text, mime = co.srv:artifact_load(id) mime:free()
+ view.text = lib.smackdown.html(pstr{[rawstring](text.ptr),text.ct}, false)
+ text:free()
+ var pg = view:tostr()
+ view.text:free()
+ co:stdpage([lib.srv.convo.page] {
+ title = lib.str.plit'media :: text';
+ class = lib.str.plit'media viewer text';
+ cache = false, body = pg;
+ })
+ pg:free()
+ elseif
+ lib.str.ncmp(art(0).mime, 'text/', 5) == 0 or
+ lib.str.cmp(art(0).mime, 'application/x-perl') == 0 or
+ lib.str.cmp(art(0).mime, 'application/sql') == 0
+ -- and so on (we need a mimelib at some point) --
+ then
+ var view = data.view.media_text(viewerprops)
+ var text, mime = co.srv:artifact_load(id) mime:free()
+ var san = lib.html.sanitize(pstr{[rawstring](text.ptr),text.ct}, false)
+ text:free()
+ view.text = lib.str.acc{}:compose('',san,'
'):finalize()
+ san:free()
+ var pg = view:tostr()
+ view.text:free()
+ co:stdpage([lib.srv.convo.page] {
+ title = lib.str.plit'media :: text';
+ class = lib.str.plit'media viewer text';
+ cache = false, body = pg;
+ })
+ pg:free()
+ else co:complain(500,'bad file type','this file type is not supported') end
+ elseif path.ct == 4 then
+ var act = path(3)
+ var curl = lib.str.acc{}:compose(pfx, '/media/a/', path(2)):finalize()
+ defer curl:free()
+ if act:cmp(lib.str.lit'avi') and lib.str.ncmp(art(0).mime, 'image/', 6) == 0 then
+ co:confirm('set avatar', 'are you sure you want this image to be your new avatar?',curl)
+ elseif act:cmp(lib.str.lit'del') then
+ co:confirm('delete', 'are you sure you want to permanently delete this artifact?',curl)
+ else goto e404 end
+ end
else
- files:lpush(''):push(md(i)(0).desc,0)
- :lpush(' '):push(md(i)(0).mime,0)
- :lpush('')
- end
- md(i):free()
- end
+ var mode: uint8 = show_new
+ var folder: pstr
+ if path.ct == 2 then
+ if path(1):cmp(lib.str.lit'unfiled') then
+ mode=show_unfiled
+ elseif path(1):cmp(lib.str.lit'all') then
+ mode=show_all
+ else goto e404 end
+ elseif path.ct == 3 and path(1):cmp(lib.str.lit'kind') then
+ end
+
+ var folders = co.srv:artifact_folder_enum(uid)
+
+ if mode == show_new then
+ folder = lib.str.plit''
+ elseif mode == show_all or mode == show_unfiled then
+ folder = pstr.null()
+ end
+
+ var view = data.view.media_gallery {
+ menu = pstr{'',0};
+ folders = pstr{'',0};
+ directory = pstr{'',0};
+ images = pstr{'',0};
+ pfx = pfx;
+ }
+
+ if folders.ct > 0 then
+ var fa: lib.str.acc fa:init(128)
+ var fldr = co:pgetv('folder')
+ for i=0,folders.ct do
+ var ule = lib.html.urlenc(folders(i), true) defer ule:free()
+ var san = lib.html.sanitize(folders(i), true) defer san:free()
+ fa:lpush(''):ppush(san):lpush('')
+ lib.dbg('checking folder ',{fldr.ptr,fldr.ct},' against ',{folders(i).ptr,folders(i).ct})
+ if fldr:ref() and folders(i):cmp(fldr)
+ then folder = folders(i) lib.dbg('folder match ',{fldr.ptr,fldr.ct})
+ else folders(i):free()
+ end
+ end
+ fa:lpush('
')
+ view.folders = fa:finalize()
+ folders:free()
+ end
+
+ if owner then
+ view.menu = P'upload
'
+ end
+
+ var md = co.srv:artifact_enum_uid(uid, folder)
+ var gallery: lib.str.acc gallery:init(256)
+ var files: lib.str.acc files:init(256)
+ for i=0,md.ct do
+ var desc = lib.smackdown.html(pstr{md(i)(0).desc,0}, true) defer desc:free()
+ if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then
+ gallery:lpush(':push(md(i)(0).url,0)
+ :lpush(')
'):ppush(desc)
+ :lpush('
')
+ else
+ var mime = lib.html.sanitize(pstr{md(i)(0).mime,0}, true) defer mime:free() --just in case
+ files:lpush(''):ppush(desc)
+ :lpush(' '):ppush(mime)
+ :lpush('')
+ end
+ md(i):free()
+ end
- view.images = gallery:finalize()
- view.directory = files:finalize()
+ view.images = gallery:finalize()
+ view.directory = files:finalize()
- if acc ~= nil then
- view:append(acc)
- else
- lib.dbg('emitting page')
- var pg = view:tostr() defer pg:free()
- lib.dbg('compiled page')
- co:stdpage([lib.srv.convo.page] {
- title = P'media';
- class = P'media manager';
- cache = false;
- body = pg;
- })
- lib.dbg('sent page')
- end
+ if acc ~= nil then
+ view:append(acc)
+ else
+ lib.dbg('emitting page')
+ var pg = view:tostr() defer pg:free()
+ lib.dbg('compiled page')
+ co:stdpage([lib.srv.convo.page] {
+ title = P'media';
+ class = P'media manager';
+ cache = false;
+ body = pg;
+ })
+ lib.dbg('sent page')
+ end
- view.images:free()
- view.directory:free()
- if not owner then view.pfx:free() end
- if md:ref() then md:free() end
- do return end
+ view.images:free()
+ view.directory:free()
+ if view.folders.ct > 0 then view.folders:free() end
+ if folder.ct > 0 then folder:free() end
+ if md:ref() then md:free() end
+ end
+ if not owner then pfx:free() end
+ return end
::e404:: co:complain(404,'media not found','no such media exists on this server')
end
return render_media_gallery
Index: route.t
==================================================================
--- route.t
+++ route.t
@@ -512,11 +512,25 @@
var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
if not idok then goto e404 end
var data, mime = co.srv:artifact_load(id)
if not data then goto e404 end
do defer data:free() defer mime:free()
- lib.net.mg_printf(co.con, 'HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n\r\n', mime.ct, mime.ptr, data.ct + 2)
+ var safemime = mime
+ -- TODO this is not a satisfactory solution; it's a bandaid on a gaping
+ -- chest wound. ultimately we need to compile a whitelist of safe mime
+ -- types as part of mimelib, but that is no small task. for now, this
+ -- will keep the patient from immediately bleeding out
+ if mime:cmp(lib.str.plit'text/html') or
+ mime:cmp(lib.str.plit'text/xml') or
+ mime:cmp(lib.str.plit'application/xhtml+xml') or
+ mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
+ then -- danger will robinson
+ safemime = lib.str.plit'text/plain'
+ elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
+ safemime = lib.str.plit'application/octet-stream'
+ end
+ lib.net.mg_printf(co.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", safemime.ct, safemime.ptr, data.ct + 2)
lib.net.mg_send(co.con, data.ptr, data.ct)
lib.net.mg_send(co.con, '\r\n', 2)
return end
::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
Index: smackdown.t
==================================================================
--- smackdown.t
+++ smackdown.t
@@ -50,24 +50,27 @@
end
local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
var sl = scanline(l,max,n,nc)
if sl == nil then return nil else sl = sl + nc end
- if sl >= l+max or isws(@sl) then return sl-nc end
+ if sl >= l+max or not isws(@(sl-1)) then return sl-nc end
return nil
end
terra m.html(input: pstr, firstline: bool)
+ if input.ptr == nil then return pstr.null() end
if input.ct == 0 then input.ct = lib.str.sz(input.ptr) end
var md = lib.html.sanitize(input,false)
var styled: lib.str.acc styled:init(md.ct)
do var i = 0 while i < md.ct do
- var wordstart = (i == 0 or isws(md.ptr[i-1]))
- var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))
+ --var wordstart = (i == 0 or isws(md.ptr[i-1]))
+ --var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))
+ var wordstart = (i + 1 < md.ct and not isws(md.ptr[i+1]))
+ var wordend = (i == md.ct - 1 or not isws(md.ptr[i-1]))
var here = md.ptr + i
var rem = md.ct - i
if @here == @'[' then
var sep = scanline(here,rem, '](', 2)
Index: srv.t
==================================================================
--- srv.t
+++ srv.t
@@ -282,10 +282,27 @@
self:statpage(code, body)
body.title:free()
body.body:free()
end
+
+terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
+ var conf = data.view.confirm {
+ title = title;
+ query = msg;
+ cancel = cancel;
+ }
+ var ti: lib.str.acc ti:compose('confirm :: ', title)
+ var body = conf:tostr() defer body:free()
+ var cf = [convo.page] {
+ title = ti:finalize();
+ class = lib.str.plit 'query';
+ body = body; cache = false;
+ }
+ self:stdpage(cf)
+ cf.title:free()
+end
convo.methods.assertpow = macro(function(self, pow)
return quote
var ok = true
if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
Index: static/style.scss
==================================================================
--- static/style.scss
+++ static/style.scss
@@ -283,10 +283,11 @@
grid-template-columns: 1.1in 1fr;
grid-template-rows: max-content 1fr;
> .avatar {
display: block;
width: 1in; height: 1in;
+ object-fit: contain;
grid-column: 1 / 2;
grid-row: 1 / 3;
border: 1px solid black;
}
> .id {
@@ -489,10 +490,12 @@
padding-bottom: 3px;
border-radius: 2px;
vertical-align: baseline;
box-shadow: 1px 1px 1px black;
}
+
+pre { @extend %teletype; white-space: pre-wrap; }
div.thread {
margin-left: 0.3in;
& + article.post { margin-top: 0.3in; }
}
@@ -1061,10 +1064,15 @@
background-repeat: no-repeat;
background-position: left;
padding-left: 0.4in;
> .label {
text-decoration: underline;
+ text-decoration-width: 1px;
+ text-underline-offset: 0.1em;
+ text-decoration-color: tone(10%,-0.5);
+ }
+ &:hover > .label {
}
> .mime {
font-style: italic;
opacity: 60%;
margin-left: 0.5ex;
@@ -1075,5 +1083,38 @@
.media.upload form {
padding: 0.1in 0.2in;
@extend %box;
}
+
+body.media div.viewer {
+ @extend %box;
+ padding: 0.2in;
+ margin-bottom: 0.2in;
+ &.img {
+ > img {
+ display: block;
+ max-width: 100%;
+ margin: auto;
+ }
+ .caption {
+ margin-top: 0.2in;
+ text-align: center;
+ &:empty {margin: 0;}
+ }
+ }
+ &.text {
+ > .desc {
+ border-bottom: 1px solid tone(-5%);
+ box-shadow: 0 2px 0 black;
+ margin-bottom: 0.1in;
+ padding-bottom: 0.1in;
+ }
+ > article {
+ font-size: 90%;
+ padding: 0 0.2in;
+ max-height: calc(100vh - 3in);
+ overflow-y: scroll;
+ text-align: justify;
+ }
+ }
+}
Index: store.t
==================================================================
--- store.t
+++ store.t
@@ -490,10 +490,12 @@
-- fetch a user's view of an artifact
-- uid: uint64
-- rid: uint64
artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t}
-- load the body of an artifact into memory (also returns mime)
+ artifact_folder_enum: {&m.source, uint64} -> lib.mem.ptr(lib.str.t)
+ -- enumerate all of a user's folders
nkvd_report_issue: {&m.source, &m.kompromat} -> {}
-- an incidence of Badthink has been detected. report it immediately
-- to the Supreme Soviet
nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
Index: view/load.lua
==================================================================
--- view/load.lua
+++ view/load.lua
@@ -11,10 +11,12 @@
'compose';
'notice';
'media-gallery';
'media-upload';
+ 'media-image';
+ 'media-text';
'login-username';
'login-challenge';
'conf';
ADDED view/media-image.tpl
Index: view/media-image.tpl
==================================================================
--- view/media-image.tpl
+++ view/media-image.tpl
@@ -0,0 +1,10 @@
+
+

+
@desc
+
+
ADDED view/media-text.tpl
Index: view/media-text.tpl
==================================================================
--- view/media-text.tpl
+++ view/media-text.tpl
@@ -0,0 +1,9 @@
+
+