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).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('
'):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
+
+ + index + set as avatar + @btns + download + ADDED view/media-text.tpl Index: view/media-text.tpl ================================================================== --- view/media-text.tpl +++ view/media-text.tpl @@ -0,0 +1,9 @@ +
+
@desc
+
@text
+
+ + index + @btns + download +