parsav  Check-in [93aea04a05]

Overview
Comment:media uploads work now, some types can be viewed
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 93aea04a05d7e6e0ea44a8a308c0f7d33fd4bbcd8422e7f95f95b176851885d3
User & Date: lexi on 2021-01-07 20:39:57
Other Links: manifest | tags
Context
2021-01-08
05:58
enable passwords check-in: d6024624c6 user: lexi tags: trunk
2021-01-07
20:39
media uploads work now, some types can be viewed check-in: 93aea04a05 user: lexi tags: trunk
09:39
further iteration on media check-in: af5ed65b68 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [fc1c52be55] to [cb3e1743a5].

   550    550   			from parsav_artifact_claims as a where uid = $1::bigint and rid = $2::bigint
   551    551   		]];
   552    552   	};
   553    553   	artifact_load = {
   554    554   		params = {uint64}, sql = [[
   555    555   			select content, mime from parsav_artifacts where id = $1::bigint
   556    556   		]];
          557  +	};
          558  +	artifact_folder_enum = {
          559  +		params = {uint64}, sql = [[
          560  +			select distinct folder from parsav_artifact_claims where
          561  +				uid = $1::bigint and folder is not null
          562  +				order by folder
          563  +		]];
   557    564   	};
   558    565   	post_attach_ctl_ins = {
   559    566   		params = {uint64, uint64}, cmd=true, sql = [[
   560    567   			update parsav_posts set
   561    568   				artifacts = artifacts || $2::bigint
   562    569   			where id = $1::bigint and not
   563    570   				artifacts @> array[$2::bigint] -- prevent duplication
................................................................................
   826    833   			lib.pq.PQclear(res)
   827    834   			return pqr {0, nil}
   828    835   		else
   829    836   			return pqr {ct, res}
   830    837   		end
   831    838   	end
   832    839   end
          840  +
          841  +local terra row_to_artifact(res: &pqr, i: intptr): lib.mem.ptr(lib.store.artifact)
          842  +	var id = res:int(uint64,i,0)
          843  +	var idbuf: int8[lib.math.shorthand.maxlen]
          844  +	var idlen = lib.math.shorthand.gen(id, &idbuf[0])
          845  +	var desc = res:_string(i,2)
          846  +	var folder = res:_string(i,3)
          847  +	var mime = res:_string(i,4)
          848  +	var m = [ lib.str.encapsulate(lib.store.artifact, {
          849  +		desc =  {`desc.ptr, `desc.ct + 1};
          850  +		folder = {`folder.ptr, `folder.ct + 1};
          851  +		mime = {`mime.ptr, `mime.ct + 1};
          852  +		url = {`&idbuf[0], `idlen + 1};
          853  +	}) ]
          854  +	m.ptr.rid = id
          855  +	return m
          856  +end
   833    857   
   834    858   local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
   835    859   	var subj: rawstring, sblen: intptr
   836    860   	var cvhu: rawstring, cvhlen: intptr
   837    861   	if r:null(row,3)
   838    862   		then subj = nil sblen = 0
   839    863   		else subj = r:string(row,3) sblen = r:len(row,3)+1
................................................................................
  1581   1605   		uid: uint64,
  1582   1606   		folder: pstring
  1583   1607   	)
  1584   1608   		var res = queries.artifact_enum_uid.exec(src,uid,folder)
  1585   1609   		if res.sz > 0 then
  1586   1610   			var m = lib.mem.heapa([lib.mem.ptr(lib.store.artifact)], res.sz)
  1587   1611   			for i=0,res.sz do
  1588         -				var id = res:int(uint64,i,0)
  1589         -				var idbuf: int8[lib.math.shorthand.maxlen]
  1590         -				var idlen = lib.math.shorthand.gen(id, &idbuf[0])
  1591         -				var desc = res:_string(i,2)
  1592         -				var folder = res:_string(i,3)
  1593         -				var mime = res:_string(i,4)
  1594         -				m.ptr[i] = [ lib.str.encapsulate(lib.store.artifact, {
  1595         -					desc =  {`desc.ptr, `desc.ct + 1};
  1596         -					folder = {`folder.ptr, `folder.ct + 1};
  1597         -					mime = {`mime.ptr, `mime.ct + 1};
  1598         -					url = {`&idbuf[0], `idlen + 1};
  1599         -				}) ]
  1600         -				m(i).ptr.rid = id
         1612  +				m.ptr[i] = row_to_artifact(&res, i)
  1601   1613   				m(i).ptr.owner = uid
  1602   1614   			end
  1603   1615   			return m
  1604   1616   		else return [lib.mem.lstptr(lib.store.artifact)].null() end
  1605   1617   	end];
         1618  +
         1619  +	artifact_fetch = [terra(
         1620  +		src: &lib.store.source,
         1621  +		uid: uint64,
         1622  +		rid: uint64
         1623  +	)
         1624  +		var res = queries.artifact_fetch.exec(src,uid,rid)
         1625  +		if res.sz > 0 then
         1626  +			var a = row_to_artifact(&res, 0)
         1627  +			a.ptr.owner = uid
         1628  +			res:free()
         1629  +			return a
         1630  +		end
         1631  +		return [lib.mem.ptr(lib.store.artifact)].null()
         1632  +	end];
  1606   1633   
  1607   1634   	artifact_load = [terra(
  1608   1635   		src: &lib.store.source,
  1609   1636   		rid: uint64
  1610   1637   	): {binblob, pstring}
  1611   1638   		var r = queries.artifact_load.exec(src,rid)
  1612   1639   		if r.sz == 0 then return binblob.null(), pstring.null() end
................................................................................
  1615   1642   		var mbin = r:bin(0,0)
  1616   1643   		var bin = lib.mem.heapa(uint8,mbin.ct)
  1617   1644   		lib.mem.cpy(bin.ptr, mbin.ptr, bin.ct)
  1618   1645   
  1619   1646   		r:free()
  1620   1647   		return bin, mime
  1621   1648   	end];
         1649  +
         1650  +	artifact_folder_enum = [terra(
         1651  +		src: &lib.store.source,
         1652  +		uid: uint64
         1653  +	)
         1654  +		var r = queries.artifact_folder_enum.exec(src,uid)
         1655  +		if r.sz == 0 then return [lib.mem.ptr(pstring)].null() end
         1656  +		defer r:free()
         1657  +		var lst = lib.mem.heapa(pstring, r.sz)
         1658  +		for i=0,r.sz do lst.ptr[i] = r:String(i,0) end
         1659  +		return lst
         1660  +	end];
  1622   1661   
  1623   1662   	post_attach_ctl = [terra(
  1624   1663   		src: &lib.store.source,
  1625   1664   		post: uint64,
  1626   1665   		artifact: uint64,
  1627   1666   		detach: bool
  1628   1667   	): {}

Modified html.t from [ee4d50abb4] to [f6f8bc0dcc].

     1      1   -- vim: ft=terra
     2      2   local m={}
     3      3   local pstr = lib.mem.ptr(int8)
     4      4   
     5      5   terra m.sanitize(txt: pstr, quo: bool)
            6  +	if txt.ptr == nil then return pstr.null() end
            7  +	if txt.ct == 0 then txt.ct = lib.str.sz(txt.ptr) end
     6      8   	var a: lib.str.acc a:init(txt.ct*1.3)
     7      9   	for i=0,txt.ct do
     8     10   		if txt(i) == @'<' then a:lpush('&lt;')
     9     11   		elseif txt(i) == @'>' then a:lpush('&gt;')
    10     12   		elseif txt(i) == @'&' then a:lpush('&amp;')
    11     13   		elseif quo and txt(i) == @'"' then a:lpush('&quot;')
    12     14   		else a:push(&txt(i),1) end
    13     15   	end
    14     16   	return a:finalize()
    15     17   end
           18  +
           19  +terra m.hexdgt(i: uint8)
           20  +	if i >= 10 then
           21  +		return @'A' + (i - 10)
           22  +	else return 0x30 + i end
           23  +end
           24  +
           25  +terra m.hexbyte(i: uint8): int8[2]
           26  +	return arrayof(int8,
           27  +		m.hexdgt(i / 0x10),
           28  +		m.hexdgt(i % 0x10))
           29  +end
           30  +
           31  +terra m.urlenc(txt: pstr, qparam: bool)
           32  +	if txt.ptr == nil then return pstr.null() end
           33  +	if txt.ct == 0 then txt.ct = lib.str.sz(txt.ptr) end
           34  +	var a: lib.str.acc a:init(txt.ct*1.3)
           35  +	for i=0,txt.ct do
           36  +		if txt(i) == @' ' then a:lpush('+')
           37  +		elseif txt(i) == @'&' and not qparam then a:lpush('&amp;')
           38  +		elseif (txt(i) < 0x2c or
           39  +			(txt(i) > @';'  and txt(i) < @'@') or
           40  +			(txt(i) > @'Z'  and txt(i) < @'a') or
           41  +			(txt(i) >= 0x7b and txt(i) <= 0x7f)) and
           42  +			 txt(i) ~= @'_' and (qparam == true or txt(i) ~= @'=') then
           43  +			var str = m.hexbyte(txt(i))
           44  +			a:lpush('%'):push(&str[0], 2)
           45  +		else a:push(&txt(i),1) end
           46  +	end
           47  +	return a:finalize()
           48  +end
    16     49   
    17     50   return m

Modified render/media-gallery.t from [7a98efa9ff] to [da27a31f83].

    11     11   render_media_gallery(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), uid: uint64, acc: &lib.str.acc)
    12     12    -- note that when calling this function, path must be adjusted so that path(0)
    13     13    -- eq "media"
    14     14   	var owner = false
    15     15   	if co.aid ~= 0 and co.who.id == uid then owner = true end
    16     16   	var ou = co.srv:actor_fetch_uid(uid)
    17     17   	if not ou then goto e404 end
    18         -
    19         -	var mode: uint8 = show_new
    20         -	var folder: pstr
    21         -	if path.ct == 2 then
    22         -		if path(1):cmp(lib.str.lit'unfiled') then
    23         -			mode=show_unfiled
    24         -		elseif path(1):cmp(lib.str.lit'all') then
    25         -			mode=show_all
    26         -		else goto e404 end
    27         -	elseif path.ct == 3 and path(1):cmp(lib.str.lit'kind') then
    28         -	end
           18  +	do defer ou:free()
           19  +		var pfx = pstr.null()
           20  +		if not owner then
           21  +			var pa: lib.str.acc pa:init(32)
           22  +			pa:lpush('/')
           23  +			if ou(0).origin ~= 0 then pa:lpush('@') end
           24  +			pa:push(ou(0).xid,0)
           25  +			pfx = pa:finalize()
           26  +		end
    29     27   
    30         -	if mode == show_new then
    31         -		folder = lib.str.plit''
    32         -	elseif mode == show_all then
    33         -		folder = pstr.null()
    34         -	elseif mode == show_unfiled then
    35         -		folder = lib.str.plit'' -- TODO
    36         -	-- else get folder from query str
    37         -	end
    38         -
    39         -	var view = data.view.media_gallery {
    40         -		menu = pstr{'',0};
    41         -		folders = pstr{'',0};
    42         -		directory = pstr{'',0};
    43         -		images = pstr{'',0};
    44         -		pfx = pstr{'',0};
    45         -	}
    46         -	if not owner then
    47         -		var pa: lib.str.acc pa:init(32)
    48         -		pa:lpush('/')
    49         -		if ou(0).origin ~= 0 then pa:lpush('@') end
    50         -		pa:push(ou(0).xid,0)
    51         -		view.pfx = pa:finalize()
    52         -	end
    53         -
    54         -	if owner then
    55         -		view.menu = P'<a class="pos" href="/media/upload">upload</a><hr>'
    56         -	end
    57         -
    58         -	var md = co.srv:artifact_enum_uid(uid, folder)
    59         -	var gallery: lib.str.acc gallery:init(256)
    60         -	var files: lib.str.acc files:init(256) 
    61         -	for i=0,md.ct do
    62         -		if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then
    63         -			gallery:lpush('<a class="thumb" href="'):ppush(view.pfx):lpush('/media/a/')
    64         -				:push(md(i)(0).url,0):lpush('"><img src="/file/'):push(md(i)(0).url,0)
    65         -				:lpush('"><div class="caption">'):push(md(i)(0).desc,0)
    66         -				:lpush('</div></a>')
           28  +		if path.ct >= 3 and path(1):cmp(lib.str.lit'a') then
           29  +			var id, idok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
           30  +			if not idok then goto e404 end
           31  +			var art = co.srv:artifact_fetch(uid, id)
           32  +			if not art then goto e404 end
           33  +			if path.ct == 3 then
           34  +			-- sniff out the artifact type and display the appropriate viewer
           35  +				var artid = cs(art(0).url)
           36  +				var btns: lib.str.acc
           37  +				if owner then
           38  +					btns:compose('<a class="neg button" href="',pfx,'/media/a/',artid,'/del">delete</a><a class="button" href="',pfx,'/media/a/',artid,'/edit">alter</a>')
           39  +				else
           40  +					btns:compose('<a class="pos button" href="',pfx,'/media/a/',artid,'/collect">collect</a>')
           41  +				end
           42  +				var btntxt = btns:finalize() defer btntxt:free()
           43  +				var desc = lib.smackdown.html(pstr{art(0).desc,0}, true) defer desc:free()
           44  +				var viewerprops = {
           45  +					pfx = pfx, desc = desc;
           46  +					id = artid; btns = btntxt;
           47  +				}
           48  +				if lib.str.ncmp(art(0).mime, 'image/', 6) == 0 then
           49  +					var view = data.view.media_image(viewerprops)
           50  +					var pg = view:tostr()
           51  +					co:stdpage([lib.srv.convo.page] {
           52  +						title = lib.str.plit'media :: image';
           53  +						class = lib.str.plit'media viewer img';
           54  +						cache = false, body = pg;
           55  +					})
           56  +					pg:free()
           57  +				elseif lib.str.cmp(art(0).mime, 'text/markdown') == 0 then
           58  +					var view = data.view.media_text(viewerprops)
           59  +					var text, mime = co.srv:artifact_load(id) mime:free()
           60  +					view.text = lib.smackdown.html(pstr{[rawstring](text.ptr),text.ct}, false)
           61  +					text:free()
           62  +					var pg = view:tostr()
           63  +					view.text:free()
           64  +					co:stdpage([lib.srv.convo.page] {
           65  +						title = lib.str.plit'media :: text';
           66  +						class = lib.str.plit'media viewer text';
           67  +						cache = false, body = pg;
           68  +					})
           69  +					pg:free()
           70  +				elseif
           71  +					lib.str.ncmp(art(0).mime, 'text/', 5) == 0          or
           72  +					lib.str.cmp(art(0).mime, 'application/x-perl') == 0 or
           73  +					lib.str.cmp(art(0).mime, 'application/sql') == 0
           74  +					-- and so on (we need a mimelib at some point) --
           75  +				then
           76  +					var view = data.view.media_text(viewerprops)
           77  +					var text, mime = co.srv:artifact_load(id) mime:free()
           78  +					var san = lib.html.sanitize(pstr{[rawstring](text.ptr),text.ct}, false)
           79  +					text:free()
           80  +					view.text = lib.str.acc{}:compose('<pre>',san,'</pre>'):finalize()
           81  +					san:free()
           82  +					var pg = view:tostr()
           83  +					view.text:free()
           84  +					co:stdpage([lib.srv.convo.page] {
           85  +						title = lib.str.plit'media :: text';
           86  +						class = lib.str.plit'media viewer text';
           87  +						cache = false, body = pg;
           88  +					})
           89  +					pg:free()
           90  +				else co:complain(500,'bad file type','this file type is not supported') end
           91  +			elseif path.ct == 4 then
           92  +				var act = path(3)
           93  +				var curl = lib.str.acc{}:compose(pfx, '/media/a/', path(2)):finalize()
           94  +				defer curl:free()
           95  +				if act:cmp(lib.str.lit'avi') and lib.str.ncmp(art(0).mime, 'image/', 6) == 0 then
           96  +					co:confirm('set avatar', 'are you sure you want this image to be your new avatar?',curl)
           97  +				elseif act:cmp(lib.str.lit'del') then
           98  +					co:confirm('delete', 'are you sure you want to permanently delete this artifact?',curl)
           99  +				else goto e404 end
          100  +			end
    67    101   		else
    68         -			files:lpush('<a class="file" href="'):ppush(view.pfx):lpush('/media/a/')
    69         -				:push(md(i)(0).url,0):lpush('"><span class="label">'):push(md(i)(0).desc,0)
    70         -				:lpush('</span> <span class="mime">'):push(md(i)(0).mime,0)
    71         -				:lpush('</span></a>')
    72         -		end
    73         -		md(i):free()
    74         -	end
          102  +			var mode: uint8 = show_new
          103  +			var folder: pstr
          104  +			if path.ct == 2 then
          105  +				if path(1):cmp(lib.str.lit'unfiled') then
          106  +					mode=show_unfiled
          107  +				elseif path(1):cmp(lib.str.lit'all') then
          108  +					mode=show_all
          109  +				else goto e404 end
          110  +			elseif path.ct == 3 and path(1):cmp(lib.str.lit'kind') then
          111  +			end
          112  +
          113  +			var folders = co.srv:artifact_folder_enum(uid)
          114  +
          115  +			if mode == show_new then
          116  +				folder = lib.str.plit''
          117  +			elseif mode == show_all or mode == show_unfiled then
          118  +				folder = pstr.null()
          119  +			end
          120  +
          121  +			var view = data.view.media_gallery {
          122  +				menu = pstr{'',0};
          123  +				folders = pstr{'',0};
          124  +				directory = pstr{'',0};
          125  +				images = pstr{'',0};
          126  +				pfx = pfx;
          127  +			}
          128  +
          129  +			if folders.ct > 0 then
          130  +				var fa: lib.str.acc fa:init(128)
          131  +				var fldr = co:pgetv('folder')
          132  +				for i=0,folders.ct do
          133  +					var ule = lib.html.urlenc(folders(i), true) defer ule:free()
          134  +					var san = lib.html.sanitize(folders(i), true) defer san:free()
          135  +					fa:lpush('<a href="'):ppush(pfx):lpush('/media?folder='):ppush(ule)
          136  +						:lpush('">'):ppush(san):lpush('</a>')
          137  +					lib.dbg('checking folder ',{fldr.ptr,fldr.ct},' against ',{folders(i).ptr,folders(i).ct})
          138  +					if fldr:ref() and folders(i):cmp(fldr)
          139  +						then folder = folders(i) lib.dbg('folder match ',{fldr.ptr,fldr.ct})
          140  +						else folders(i):free()
          141  +					end
          142  +				end
          143  +				fa:lpush('<hr>')
          144  +				view.folders = fa:finalize()
          145  +				folders:free()
          146  +			end
          147  +
          148  +			if owner then
          149  +				view.menu = P'<a class="pos" href="/media/upload">upload</a><hr>'
          150  +			end
          151  +
          152  +			var md = co.srv:artifact_enum_uid(uid, folder)
          153  +			var gallery: lib.str.acc gallery:init(256)
          154  +			var files: lib.str.acc files:init(256) 
          155  +			for i=0,md.ct do
          156  +				var desc = lib.smackdown.html(pstr{md(i)(0).desc,0}, true) defer desc:free()
          157  +				if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then
          158  +					gallery:lpush('<a class="thumb" href="'):ppush(pfx):lpush('/media/a/')
          159  +						:push(md(i)(0).url,0):lpush('"><img src="/file/'):push(md(i)(0).url,0)
          160  +						:lpush('"><div class="caption">'):ppush(desc)
          161  +						:lpush('</div></a>')
          162  +				else
          163  +					var mime = lib.html.sanitize(pstr{md(i)(0).mime,0}, true) defer mime:free() --just in case
          164  +					files:lpush('<a class="file" href="'):ppush(pfx):lpush('/media/a/')
          165  +						:push(md(i)(0).url,0):lpush('"><span class="label">'):ppush(desc)
          166  +						:lpush('</span> <span class="mime">'):ppush(mime)
          167  +						:lpush('</span></a>')
          168  +				end
          169  +				md(i):free()
          170  +			end
    75    171   
    76         -	view.images = gallery:finalize()
    77         -	view.directory = files:finalize()
          172  +			view.images = gallery:finalize()
          173  +			view.directory = files:finalize()
    78    174   
    79         -	if acc ~= nil then
    80         -		view:append(acc)
    81         -	else
    82         -	lib.dbg('emitting page')
    83         -		var pg = view:tostr() defer pg:free()
    84         -	lib.dbg('compiled page')
    85         -		co:stdpage([lib.srv.convo.page] {
    86         -			title = P'media';
    87         -			class = P'media manager';
    88         -			cache = false;
    89         -			body = pg;
    90         -		})
    91         -	lib.dbg('sent page')
    92         -	end
          175  +			if acc ~= nil then
          176  +				view:append(acc)
          177  +			else
          178  +			lib.dbg('emitting page')
          179  +				var pg = view:tostr() defer pg:free()
          180  +			lib.dbg('compiled page')
          181  +				co:stdpage([lib.srv.convo.page] {
          182  +					title = P'media';
          183  +					class = P'media manager';
          184  +					cache = false;
          185  +					body = pg;
          186  +				})
          187  +			lib.dbg('sent page')
          188  +			end
    93    189   
    94         -	view.images:free()
    95         -	view.directory:free()
    96         -	if not owner then view.pfx:free() end
    97         -	if md:ref() then md:free() end
    98         -	do return end
          190  +			view.images:free()
          191  +			view.directory:free()
          192  +			if view.folders.ct > 0 then view.folders:free() end
          193  +			if folder.ct > 0 then folder:free() end
          194  +			if md:ref() then md:free() end
          195  +		end
          196  +		if not owner then pfx:free() end
          197  +	return end
    99    198   
   100    199   	::e404:: co:complain(404,'media not found','no such media exists on this server')
   101    200   end
   102    201   
   103    202   return render_media_gallery

Modified route.t from [8605ea50bd] to [881176d2e1].

   510    510   
   511    511   terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
   512    512   	var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
   513    513   	if not idok then goto e404 end
   514    514   	var data, mime = co.srv:artifact_load(id)
   515    515   	if not data then goto e404 end
   516    516   	do defer data:free() defer mime:free()
   517         -		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)
          517  +		var safemime = mime
          518  +		-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
          519  +		-- chest wound. ultimately we need to compile a whitelist of safe mime
          520  +		-- types as part of mimelib, but that is no small task. for now, this
          521  +		-- will keep the patient from immediately bleeding out
          522  +		if mime:cmp(lib.str.plit'text/html') or
          523  +			mime:cmp(lib.str.plit'text/xml') or
          524  +			mime:cmp(lib.str.plit'application/xhtml+xml') or
          525  +			mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
          526  +		then -- danger will robinson
          527  +			safemime = lib.str.plit'text/plain'
          528  +		elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
          529  +			safemime = lib.str.plit'application/octet-stream'
          530  +		end
          531  +		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)
   518    532   		lib.net.mg_send(co.con, data.ptr, data.ct)
   519    533   		lib.net.mg_send(co.con, '\r\n', 2)
   520    534   	return end
   521    535   
   522    536   	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
   523    537   end
   524    538   

Modified smackdown.t from [926ec15b69] to [e99ea3e622].

    48     48   		do return l+i end
    49     49   	::nexti::end
    50     50   end
    51     51   
    52     52   local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
    53     53   	var sl = scanline(l,max,n,nc)
    54     54   	if sl == nil then return nil else sl = sl + nc end
    55         -	if sl >= l+max or isws(@sl) then return sl-nc end
           55  +	if sl >= l+max or not isws(@(sl-1)) then return sl-nc end
    56     56   	return nil
    57     57   end
    58     58   
    59     59   terra m.html(input: pstr, firstline: bool)
           60  +	if input.ptr == nil then return pstr.null() end
    60     61   	if input.ct == 0 then input.ct = lib.str.sz(input.ptr) end
    61     62   
    62     63   	var md = lib.html.sanitize(input,false)
    63     64   
    64     65   	var styled: lib.str.acc styled:init(md.ct)
    65     66   
    66     67   	do var i = 0 while i < md.ct do
    67         -		var wordstart = (i == 0 or isws(md.ptr[i-1]))
    68         -		var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))
           68  +		--var wordstart = (i == 0 or isws(md.ptr[i-1]))
           69  +		--var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))
           70  +		var wordstart = (i + 1 < md.ct and not isws(md.ptr[i+1]))
           71  +		var wordend = (i == md.ct - 1 or not isws(md.ptr[i-1]))
    69     72   
    70     73   		var here = md.ptr + i
    71     74   		var rem = md.ct - i
    72     75   		if @here == @'[' then
    73     76   			var sep = scanline(here,rem, '](', 2)
    74     77   			var term = scanline(sep+2,rem - ((sep+2)-here), ')', 1)
    75     78   			if sep ~= nil and term ~= nil then

Modified srv.t from [31acbbce83] to [ca6d27c8d7].

   280    280   	}
   281    281   
   282    282   	self:statpage(code, body)
   283    283   
   284    284   	body.title:free()
   285    285   	body.body:free()
   286    286   end
          287  +
          288  +terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
          289  +	var conf = data.view.confirm {
          290  +		title = title;
          291  +		query = msg;
          292  +		cancel = cancel;
          293  +	}
          294  +	var ti: lib.str.acc ti:compose('confirm :: ', title)
          295  +	var body = conf:tostr() defer body:free()
          296  +	var cf = [convo.page] {
          297  +		title = ti:finalize();
          298  +		class = lib.str.plit 'query';
          299  +		body = body; cache = false;
          300  +	}
          301  +	self:stdpage(cf)
          302  +	cf.title:free()
          303  +end
   287    304   
   288    305   convo.methods.assertpow = macro(function(self, pow)
   289    306   	return quote
   290    307   		var ok = true
   291    308   		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
   292    309   			ok = false
   293    310   			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])

Modified static/style.scss from [f5bd822625] to [fb57063208].

   281    281   		grid-row: 1 / 2;
   282    282   		display: grid;
   283    283   		grid-template-columns: 1.1in 1fr;
   284    284   		grid-template-rows: max-content 1fr;
   285    285   		> .avatar {
   286    286   			display: block;
   287    287   			width: 1in; height: 1in;
          288  +			object-fit: contain;
   288    289   			grid-column: 1 / 2;
   289    290   			grid-row: 1 / 3;
   290    291   			border: 1px solid black;
   291    292   		}
   292    293   		> .id {
   293    294   			grid-column: 2 / 3;
   294    295   			grid-row: 1 / 2;
................................................................................
   487    488   	font-size: 1.5ex !important;
   488    489   	letter-spacing: 1.3px;
   489    490   	padding-bottom: 3px;
   490    491   	border-radius: 2px;
   491    492   	vertical-align: baseline;
   492    493   	box-shadow: 1px 1px 1px black;
   493    494   }
          495  +
          496  +pre { @extend %teletype; white-space: pre-wrap; }
   494    497   
   495    498   div.thread {
   496    499   	margin-left: 0.3in;
   497    500   	& + article.post { margin-top: 0.3in; }
   498    501   }
   499    502   
   500    503   a[href].username {
................................................................................
  1059   1062   			height: max-content;
  1060   1063   			background-image: url(/s/file.webp); //TODO different icons for different mime types
  1061   1064   			background-repeat: no-repeat;
  1062   1065   			background-position: left;
  1063   1066   			padding-left: 0.4in;
  1064   1067   			> .label {
  1065   1068   				text-decoration: underline;
         1069  +				text-decoration-width: 1px;
         1070  +				text-underline-offset: 0.1em;
         1071  +				text-decoration-color: tone(10%,-0.5);
         1072  +			}
         1073  +			&:hover > .label {
  1066   1074   			}
  1067   1075   			> .mime {
  1068   1076   				font-style: italic;
  1069   1077   				opacity: 60%;
  1070   1078   				margin-left: 0.5ex;
  1071   1079   			}
  1072   1080   		}
................................................................................
  1073   1081   	}
  1074   1082   }
  1075   1083   
  1076   1084   .media.upload form {
  1077   1085   	padding: 0.1in 0.2in;
  1078   1086   	@extend %box;
  1079   1087   }
         1088  +
         1089  +body.media div.viewer {
         1090  +	@extend %box;
         1091  +	padding: 0.2in;
         1092  +	margin-bottom: 0.2in;
         1093  +	&.img {
         1094  +		> img {
         1095  +			display: block;
         1096  +			max-width: 100%;
         1097  +			margin: auto;
         1098  +		}
         1099  +		.caption {
         1100  +			margin-top: 0.2in;
         1101  +			text-align: center;
         1102  +			&:empty {margin: 0;}
         1103  +		}
         1104  +	}
         1105  +	&.text {
         1106  +		> .desc {
         1107  +			border-bottom: 1px solid tone(-5%);
         1108  +			box-shadow: 0 2px 0 black;
         1109  +			margin-bottom: 0.1in;
         1110  +			padding-bottom: 0.1in;
         1111  +		}
         1112  +		> article {
         1113  +			font-size: 90%;
         1114  +			padding: 0 0.2in;
         1115  +			max-height: calc(100vh - 3in);
         1116  +			overflow-y: scroll;
         1117  +			text-align: justify;
         1118  +		}
         1119  +	}
         1120  +}

Modified store.t from [fdc1c1d9e2] to [eca94a58d5].

   488    488   		-- restricted by folder (empty string = new only)
   489    489   	artifact_fetch: {&m.source, uint64, uint64} -> lib.mem.ptr(m.artifact)
   490    490   		-- fetch a user's view of an artifact
   491    491   			-- uid: uint64
   492    492   			-- rid: uint64
   493    493   	artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t}
   494    494   		-- load the body of an artifact into memory (also returns mime)
          495  +	artifact_folder_enum: {&m.source, uint64} -> lib.mem.ptr(lib.str.t)
          496  +		-- enumerate all of a user's folders
   495    497   
   496    498   	nkvd_report_issue: {&m.source, &m.kompromat} -> {}
   497    499   		-- an incidence of Badthink has been detected. report it immediately
   498    500   		-- to the Supreme Soviet
   499    501   	nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
   500    502   		-- search through the Archives
   501    503   			-- proto: kompromat (null for all records, or a prototype describing the records to return)

Modified view/load.lua from [fbf23a2927] to [9f4f065de0].

     9      9   	'tweet';
    10     10   	'profile';
    11     11   	'compose';
    12     12   	'notice';
    13     13   
    14     14   	'media-gallery';
    15     15   	'media-upload';
           16  +	'media-image';
           17  +	'media-text';
    16     18   
    17     19   	'login-username';
    18     20   	'login-challenge';
    19     21   
    20     22   	'conf';
    21     23   	'conf-profile';
    22     24   	'conf-sec';

Added view/media-image.tpl version [df3a1820c7].

            1  +<div class="viewer img">
            2  +	<img src="/file/@id">
            3  +	<div class="caption">@desc</div>
            4  +</div>
            5  +<menu class="choice horizontal">
            6  +	<a class="button" href="@pfx/media">index</a>
            7  +	<a class="button" href="@pfx/media/a/@id/avi">set as avatar</a>
            8  +	@btns
            9  +	<a class="button" href="/file/@id" download>download</a>
           10  +</menu>

Added view/media-text.tpl version [73c12d23b4].

            1  +<div class="viewer text">
            2  +	<div class="desc">@desc</div>
            3  +	<article>@text</article>
            4  +</div>
            5  +<menu class="choice horizontal">
            6  +	<a class="button" href="@pfx/media">index</a>
            7  +	@btns
            8  +	<a class="button" href="/file/@id" download>download</a>
            9  +</menu>