parsav  Check-in [d6024624c6]

Overview
Comment:enable passwords
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d6024624c61e176bcbd760d0797db07a3a667a0b86456d00ef898418770871c6
User & Date: lexi on 2021-01-08 05:58:30
Other Links: manifest | tags
Context
2021-01-09
07:15
user mgmt and rt improvements check-in: 05af79b909 user: lexi tags: trunk
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
Changes

Modified backend/pgsql.t from [cb3e1743a5] to [9c53eed84d].

   119    119   
   120    120   	actor_enum_local = {
   121    121   		params = {}, sql = [[
   122    122   			select id, nym, handle, origin, bio,
   123    123   			       null::text, rank, quota, key, epithet,
   124    124   			       knownsince::bigint,
   125    125   					'@' || handle,
   126         -				   invites
          126  +				   invites, avatarid
   127    127   			from parsav_actors where origin is null
   128    128   			order by nullif(rank,0) nulls last, handle
   129    129   		]];
   130    130   	};
   131    131   
   132    132   	actor_enum = {
   133    133   		params = {}, sql = [[
................................................................................
   502    502   	};
   503    503   	artifact_disclaim = {
   504    504   		params = {uint64, uint64}, cmd = true, sql = [[
   505    505   			delete from parsav_artifact_claims where
   506    506   				uid = $1::bigint and
   507    507   				rid = $2::bigint
   508    508   		]];
          509  +	};
          510  +	artifact_collect_garbage = {
          511  +		params = {}, cmd = true, sql = [[
          512  +			delete from parsav_artifacts where
          513  +				id not in (select rid from parsav_artifact_claims) and
          514  +				content is not null -- avoid stepping on toes of ban mech
          515  +		]];
   509    516   	};
   510    517   	artifact_excise_forget = {
   511    518   		-- delete the blasted thing and pretend it never existed
   512    519   		params = {uint64}, cmd=true, sql = [[
   513    520   			delete from parsav_artifacts where id = $1::bigint
   514    521   		]];
   515    522   	};
................................................................................
   946    953   	}) ]
   947    954   	a.ptr.id = r:int(uint64, row, 0);
   948    955   	a.ptr.rights = lib.store.rights_default();
   949    956   	a.ptr.rights.rank = r:int(uint16, row, 6);
   950    957   	a.ptr.rights.quota = r:int(uint32, row, 7);
   951    958   	a.ptr.rights.invites = r:int(uint32, row, 12);
   952    959   	a.ptr.knownsince = r:int(int64,row, 10);
          960  +	a.ptr.avatarid = r:int(uint64,row, 13);
   953    961   	if r:null(row,8) then
   954    962   		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
   955    963   	else
   956    964   		a.ptr.key = r:bin(row,8)
   957    965   	end
   958    966   	a.ptr.origin = origin
   959    967   	if avia.buf ~= nil then avia:free() end
................................................................................
  1500   1508   		var r = queries.auth_enum_uid.exec(src,uid)
  1501   1509   		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
  1502   1510   		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
  1503   1511   		for i=0, r.sz do
  1504   1512   			var kind = r:_string(i, 1)
  1505   1513   			var comment = r:_string(i, 2)
  1506   1514   			var a = [ lib.str.encapsulate(lib.store.auth, {
  1507         -				kind = {`kind.ptr, `kind.ct};
  1508         -				comment = {`comment.ptr, `comment.ct};
         1515  +				kind = {`kind.ptr, `kind.ct+1};
         1516  +				comment = {`comment.ptr, `comment.ct+1};
  1509   1517   			}) ]
  1510   1518   			a.ptr.aid = r:int(uint64, i, 0)
  1511         -			a.ptr.netmask = r:cidr(i, 3)
         1519  +			if r:null(i,3)
         1520  +				then a.ptr.netmask.pv = 0
         1521  +				else a.ptr.netmask = r:cidr(i, 3)
         1522  +			end
  1512   1523   			a.ptr.blacklist = r:bool(i, 4)
  1513   1524   			ret.ptr[i] = a
  1514   1525   		end
  1515   1526   		return ret
  1516   1527   	end];
  1517   1528   
  1518   1529   	auth_attach_pw = [terra(
................................................................................
  1595   1606   		uid: uint64,
  1596   1607   		artifact: uint64,
  1597   1608   		desc: pstring,
  1598   1609   		folder: pstring
  1599   1610   	): {}
  1600   1611   		queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil))
  1601   1612   	end];
         1613  +
         1614  +	artifact_disclaim = [terra(
         1615  +		src: &lib.store.source,
         1616  +		uid: uint64,
         1617  +		artifact: uint64
         1618  +	)
         1619  +		queries.artifact_disclaim.exec(src,uid,artifact)
         1620  +		queries.artifact_collect_garbage.exec(src) -- TODO add a config option to change GC strategies, instead of just always running a cycle after an artifact is disclaimed, which is not very efficient
         1621  +	end];
  1602   1622   
  1603   1623   	artifact_enum_uid = [terra(
  1604   1624   		src: &lib.store.source,
  1605   1625   		uid: uint64,
  1606   1626   		folder: pstring
  1607   1627   	)
  1608   1628   		var res = queries.artifact_enum_uid.exec(src,uid,folder)

Modified backend/schema/pgsql-views.sql from [ca832d14af] to [25f9d405bc].

    54     54   	avataruri	text,
    55     55   	rank		smallint,
    56     56   	quota		integer,
    57     57   	key			bytea,
    58     58   	epithet		text,
    59     59   	knownsince	bigint,
    60     60   	xid			text,
    61         -	invites		integer
           61  +	invites		integer,
           62  +	avatarid	bigint
    62     63   );
    63     64   
    64     65   create or replace function
    65     66   pg_temp.parsavpg_translate_actor(parsav_actors)
    66     67   returns pg_temp.parsavpg_intern_actor as $$
    67     68   	select
    68     69   		($1).id,        ($1).nym,  ($1).handle, ($1).origin, ($1).bio,
    69     70   		($1).avataruri, ($1).rank, ($1).quota,  ($1).key,    ($1).epithet,
    70     71   		($1).knownsince::bigint,
    71     72   		coalesce(($1).handle || '@' ||
    72     73   				(select domain from parsav_servers as s where s.id = ($1).origin),
    73     74   			'@' || ($1).handle) as xid,
    74         -		($1).invites
           75  +		($1).invites, ($1).avatarid
    75     76   $$ language sql;
    76     77   
    77     78   --drop type if exists pg_temp.parsavpg_intern_post;
    78     79   create type pg_temp.parsavpg_intern_post as (
    79     80   	-- order is crucially important, and must match the order used
    80     81   	-- in row_to_actor. names don't matter
    81     82   	localpost	bool,

Modified parsav.md from [7a2c43b008] to [52b33381db].

    21     21   
    22     22   additional preconfigure dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary:
    23     23   
    24     24   * inkscape, for rendering out some of the UI graphics that can't be represented with standard svg
    25     25   * cwebp (libwebp package), for transforming inkscape PNGs to webp
    26     26   * sassc, for compiling the SCSS stylesheet into its final CSS
    27     27   
    28         -all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le.
           28  +all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. 
    29     29   
    30         -i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra (or llvm?) people will fix this eventually.
           30  +i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form. as a workaround, i've tried generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. it doesn't work. the generated binaries seem to run but they crash with bizarre errors and are impossible to debug, as llc refuses to include debug symbols. for these reasons, parsav will (almost certainly) not run on any architecture besides x86-64, at least until terra and/or llvm are fixed.
    31     31   
    32     32   also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed.
    33     33   
    34     34   ## building
    35     35   
    36     36   first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD.
    37     37   

Modified parsav.t from [7b59e1e979] to [2fffe11917].

    39     39   				if v.tree.type == ty then return fn(v,...) end
    40     40   			end
    41     41   			return (tbl[false])(v,...)
    42     42   		end)
    43     43   	end;
    44     44   	emit_unitary = function(nl,fd,...)
    45     45   		local code = {}
           46  +		local defs = {}
    46     47   		for i,v in ipairs{...} do
    47         -			if type(v) == 'string' or type(v) == 'number' then
    48         -				local str = tostring(v)
    49         -				code[#code+1] = `lib.io.send(2, str, [#str])
    50         -			elseif type(v) == 'table' and #v == 2 then
    51         -				code[#code+1] = `lib.io.send(2, [v[1]], [v[2]])
    52         -			elseif v.tree:is 'constant' then
    53         -				local str = tostring(v:asvalue())
    54         -				code[#code+1] = `lib.io.send(2, str, [#str])
           48  +			local str, ct
           49  +			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
           50  +				if v.tree.type.convertible == 'tuple' then
           51  +					str = `v._0
           52  +					ct = `v._1
           53  +				else
           54  +					local n = symbol(v.tree.type)
           55  +					defs[#defs + 1] = quote var [n] = v end
           56  +					str = n
           57  +					ct = `lib.str.sz(n)
           58  +				end
    55     59   			else
    56         -				code[#code+1] = quote var n = v in
    57         -					lib.io.send(2, n, lib.str.sz(n)) end
           60  +				if type(v) == 'string' or type(v) == 'number' then
           61  +					str = tostring(v) 
           62  +				else--if v.tree:is 'constant' then
           63  +					str = tostring(v:asvalue())
           64  +				end
           65  +				ct = ct or #str
    58     66   			end
           67  +
           68  +			code[#code+1] = `lib.io.send(fd, str, ct)
    59     69   		end
    60     70   		if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1)
    61     71   		elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end
    62         -		return code
           72  +		return quote [defs] in [code] end
    63     73   	end;
    64     74   	emitv = function(nl,fd,...)
    65     75   		local vec = {}
    66     76   		local defs = {}
    67     77   		for i,v in ipairs{...} do
    68     78   			local str, ct
    69     79   			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
................................................................................
   160    170   	end;
   161    171   	osclock = terralib.includec 'time.h';
   162    172   }
   163    173   if config.posix then
   164    174   	lib.uio = terralib.includec 'sys/uio.h';
   165    175   	lib.emit = lib.emitv -- use more efficient call where available
   166    176   else lib.emit = lib.emit_unitary end
   167         -
   168    177   
   169    178   lib.noise = {
   170    179   	level = global(uint8,1);
   171    180   	starttime = global(lib.osclock.time_t);
   172    181   	lasttime = global(lib.osclock.time_t);
   173    182   	header = function(code,txt,mod)
   174    183   		if mod then

Modified render/conf.t from [60d6b764a8] to [241a1c4277].

     2      2   local pstr = lib.mem.ptr(int8)
     3      3   local pref = lib.mem.ref(int8)
     4      4   
     5      5   local mappings = {
     6      6   	{url = 'profile', title = 'account profile', render = 'profile'};
     7      7   	{url = 'avi', title = 'avatar', render = 'avatar'};
     8      8   	{url = 'ui', title = 'user interface', render = 'ui'};
     9         -	{url = 'sec', title = 'security', render = 'sec'};
            9  +	{url = 'sec', title = 'security', render = 'sec_overlay'};
    10     10   	{url = 'rel', title = 'relationships', render = 'rel'};
    11     11   	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
    12     12   	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
    13     13   	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
    14     14   	{url = 'circles', title = 'circles', render = 'circles'};
    15     15   
    16     16   	{url = 'srv', title = 'server settings', render = 'srv'};
................................................................................
    29     29   
    30     30   for i, m in ipairs(mappings) do
    31     31   	if lib.render.conf[m.render] then
    32     32   		invoker = quote
    33     33   			if path(1):cmp(lib.str.lit([m.url])) then
    34     34   				var body = [lib.render.conf[m.render]] (co, path)
    35     35   				var a: lib.str.acc a:init(body.ct+48)
    36         -				a:lpush(['<h1>' .. m.title .. '</h1>']):ppush(body)
    37         -				panel = a:finalize()
    38         -				body:free()
           36  +				if not body then
           37  +					a:lpush(['<h1>' .. m.title .. ' :: error</h1>' ..
           38  +						'<p>the requested resource is not available.</p>'])
           39  +					panel = a:finalize()
           40  +				else
           41  +					a:lpush(['<h1>' .. m.title .. '</h1>']):ppush(body)
           42  +					panel = a:finalize()
           43  +					body:free()
           44  +				end
    39     45   			else [invoker] end
    40     46   		end
    41     47   	end
    42     48   end
    43     49   
    44     50   local terra 
    45     51   render_conf([co], [path], notify: pstr)

Modified render/conf/sec.t from [2ed8642241] to [7f83a40056].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.mem.ptr(int8)
     3      3   local pref = lib.mem.ref(int8)
            4  +
     4      5   local terra 
     5         -render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
     6         -	var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id)
            6  +render_conf_sec(co: &lib.srv.convo, uid: uint64): pstr
            7  +	var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(uid)
     7      8   	var tstr: int8[26]
     8      9   	lib.osclock.ctime_r(&time, &tstr[0])
     9     10   	var body = data.view.conf_sec {
    10     11   		lastreset = pstr {
    11     12   			ptr = &tstr[0], ct = lib.str.sz(&tstr[0])
    12     13   		}
    13     14   	}
    14     15   	
    15     16   	if co.srv.cfg.credmgd then
           17  +		var new = co:pgetv('new')
    16     18   		var a: lib.str.acc a:init(768)
    17         -		body:append(&a)
    18         -		var credmgr = data.view.conf_sec_credmg {
    19         -			credlist = '<option>your password</option>'
    20         -		}
    21         -		credmgr:append(&a)
           19  +		if not new then
           20  +			body:append(&a)
           21  +			var credmgr = data.view.conf_sec_credmg {
           22  +				credlist = pstr{'',0};
           23  +			}
           24  +			var creds = co.srv:auth_enum_uid(uid)
           25  +			if creds.ct > 0 then defer creds:free()
           26  +				var cl: lib.str.acc cl:init(256)
           27  +				for i=0, creds.ct do var c = creds(i).ptr
           28  +					if not c.blacklist then
           29  +						cl:lpush('<option value="'):shpush(c.aid):lpush('"> ['):push(c.kind,0):lpush('] '):push(c.comment,0)
           30  +						if c.netmask.pv ~= 0 then
           31  +							-- push string rep
           32  +						end
           33  +						cl:lpush('</option>')
           34  +					end
           35  +				end
           36  +				credmgr.credlist = cl:finalize()
           37  +			end
           38  +			credmgr:append(&a)
           39  +			if credmgr.credlist.ct > 0 then credmgr.credlist:free() end
           40  +		elseif new:cmp(lib.str.plit'pw') then
           41  +			var d: data.view.conf_sec_pwnew
           42  +			var time = lib.osclock.time(nil)
           43  +			var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
           44  +			var cmt: lib.str.acc
           45  +			cmt:init(48):lpush('enrolled over http on '):push(&timestr[0],0)
           46  +			d.comment = cmt:finalize()
           47  +
           48  +			var st = d:tostr()
           49  +			d.comment:free()
           50  +			return st
           51  +		elseif new:cmp(lib.str.plit'challenge') then
           52  +		-- we're going to break the rules a bit and do database munging from
           53  +		-- the rendering code, because doing otherwise in this case would be
           54  +		-- genuinely nightmarish
           55  +		elseif new:cmp(lib.str.plit'otp') then
           56  +		elseif new:cmp(lib.str.plit'api') then
           57  +		else return pstr.null() end
    22     58   		return a:finalize()
    23     59   	else return body:tostr() end
    24     60   end
           61  +
           62  +terra lib.render.conf.sec_overlay
           63  +(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
           64  + -- render the credential panel for the current user, allowing
           65  + -- it to be reused in the administration UI
           66  +	return render_conf_sec(co,co.who.id)
           67  +end
           68  +
    25     69   return render_conf_sec

Modified render/conf/users.t from [0f343f8c98] to [41f55e0682].

   103    103   		if punct ~= nil then a:push(punct, 1) end
   104    104   		a:ipush(rnd(uint16,0,65535))
   105    105   	end
   106    106   
   107    107   	if xXx then a:lpush('_xXx') end
   108    108   
   109    109   end
          110  +
          111  +local terra 
          112  +suggest_domain(a: &lib.str.acc)
          113  +	var tlds = array('tld','club','town','space','xxx')
          114  +end
   110    115   
   111    116   local push_num_field = macro(function(acc,name,lbl,min,max,value,disable)
   112    117   	name = name:asvalue()
   113    118   	lbl = lbl:asvalue()
   114    119   	local start = '<div class="elem small">'
   115    120   	local enabled = start .. string.format('<label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name)
   116    121   	local disabled = start .. string.format('<label>%s</label><div class="txtbox">', lbl)
................................................................................
   154    159   local push_checkbox = input_pusher('checkbox',true,false)
   155    160   local push_pickbox = input_pusher('checkbox',false,false)
   156    161   local push_radio = input_pusher('radio',false,true)
   157    162   
   158    163   local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5
   159    164   local terra 
   160    165   render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
   161         -	if path.ct == 3 then
          166  +	if path.ct >= 3 then
   162    167   		var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
   163    168   		if not ok then goto e404 end
   164    169   		var user = co.srv:actor_fetch_uid(uid)
   165    170   		-- FIXME allow xids as well, for manual queries
   166    171   		if not user then goto e404 end
   167    172   		defer user:free()
   168    173   		if not co.who:overpowers(user.ptr) then goto e403 end
   169    174   
   170         -		var cinp: lib.str.acc cinp:init(256)
   171         -		var clnk: lib.str.acc clnk:init(512)
   172         -		cinp:lpush('<div class="elem-group">')
   173         -		if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then
   174         -			var max = co.who.rights.rank
   175         -			if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end
   176         -			var min = co.srv.cfg.nranks
   177         -			if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end
   178         -
   179         -			push_num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank, user.ptr.id == co.who.id)
   180         -		end
   181         -		if co.who.rights.powers.herald() then
   182         -			var sanitized: pstr
   183         -			if user.ptr.epithet == nil
   184         -				then sanitized = pstr {ptr='', ct=0}
   185         -				else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
          175  +		if path.ct == 4 then
          176  +			if path(3):cmp(lib.str.lit'cred') then
          177  +				var pg: lib.str.acc pg:init(1024)
          178  +				pg:lpush('<div class="context">editing credentials for user <a href="/conf/users/'):rpush(path(2)):lpush('">'):push(user(0).xid,0):lpush('</a></div>')
          179  +				var credmgr = lib.render.conf.sec(co, uid)
          180  +				pg:ppush(credmgr)
          181  +				credmgr:free()
          182  +				return pg:finalize()
          183  +			else goto e404 end
          184  +		elseif path.ct == 3 then
          185  +			var cinp: lib.str.acc cinp:init(256)
          186  +			cinp:lpush('<div class="elem-group">')
          187  +			if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then
          188  +				var max = co.who.rights.rank
          189  +				if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end
          190  +				var min = co.srv.cfg.nranks
          191  +				if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end
          192  +
          193  +				push_num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank, user.ptr.id == co.who.id)
          194  +			end
          195  +			if co.who.rights.powers.herald() then
          196  +				var sanitized: pstr
          197  +				if user.ptr.epithet == nil
          198  +					then sanitized = pstr {ptr='', ct=0}
          199  +					else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
          200  +				end
          201  +				cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
          202  +				if user.ptr.epithet ~= nil then sanitized:free() end
          203  +			end
          204  +			if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
          205  +				var min: uint32 = 0
          206  +				if not (co.who.rights.powers.discipline() or
          207  +					co.who.rights.powers.demote() and co.who.rights.powers.invite())
          208  +						then min = user.ptr.rights.invites end
          209  +				var max: uint32 = co.srv.cfg.maxinvites
          210  +				if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end
          211  +
          212  +				push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
          213  +			end
          214  +			if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then
          215  +				var max: uint32 = 5000
          216  +				if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end
          217  +				var min: uint32 = 0
          218  +				if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end
          219  +
          220  +				push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1)
   186    221   			end
   187         -			cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
   188         -			if user.ptr.epithet ~= nil then sanitized:free() end
   189         -		end
   190         -		if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
   191         -			var min: uint32 = 0
   192         -			if not (co.who.rights.powers.discipline() or
   193         -				co.who.rights.powers.demote() and co.who.rights.powers.invite())
   194         -					then min = user.ptr.rights.invites end
   195         -			var max: uint32 = co.srv.cfg.maxinvites
   196         -			if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end
          222  +			cinp:lpush('</div><div class="elem"><div class="check-panel">')
          223  +
          224  +			if user.ptr.id ~= co.who.id and
          225  +			   ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
          226  +				(user.ptr.rights.rank >  0 and co.who.rights.powers.demote())) then
          227  +				push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
          228  +			end
   197    229   
   198         -			push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
   199         -		end
   200         -		if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then
   201         -			var max: uint32 = 5000
   202         -			if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end
   203         -			var min: uint32 = 0
   204         -			if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end
          230  +			cinp:lpush('</div></div>')
   205    231   
   206         -			push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1)
   207         -		end
   208         -		cinp:lpush('</div><div class="elem"><div class="check-panel">')
   209         -
   210         -		if user.ptr.id ~= co.who.id and
   211         -		   ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
   212         -		    (user.ptr.rights.rank >  0 and co.who.rights.powers.demote())) then
   213         -			push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
   214         -		end
   215         -
   216         -		cinp:lpush('</div></div>')
   217         -
   218         -		if (co.who.rights.powers.elevate() or
   219         -		   co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then
   220         -			var map = array([lib.store.privmap])
   221         -			cinp:lpush('<details><summary>powers</summary><div class="pick-list">')
   222         -				for i=0, [map.type.N] do
   223         -					if (co.who.rights.powers and map[i].priv):sz() > 0 then
   224         -						var on = (user.ptr.rights.powers and map[i].priv):sz() > 0
   225         -						var enabled = (     on  and co.who.rights.powers.demote() ) or
   226         -									  ((not on) and co.who.rights.powers.elevate())
   227         -						var namea: lib.str.acc namea:compose('power-', map[i].name)
   228         -						var name = namea:finalize()
   229         -						push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null())
   230         -						name:free()
          232  +			if (co.who.rights.powers.elevate() or
          233  +			   co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then
          234  +				var map = array([lib.store.privmap])
          235  +				cinp:lpush('<details><summary>powers</summary><div class="pick-list">')
          236  +					for i=0, [map.type.N] do
          237  +						if (co.who.rights.powers and map[i].priv):sz() > 0 then
          238  +							var on = (user.ptr.rights.powers and map[i].priv):sz() > 0
          239  +							var enabled = (     on  and co.who.rights.powers.demote() ) or
          240  +										  ((not on) and co.who.rights.powers.elevate())
          241  +							var namea: lib.str.acc namea:compose('power-', map[i].name)
          242  +							var name = namea:finalize()
          243  +							push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null())
          244  +							name:free()
          245  +						end
   231    246   					end
          247  +				cinp:lpush('</div></details>')
          248  +			end
          249  +
          250  +			if co.who.id ~= uid and co.who.rights.powers.purge() then
          251  +				var purgeconf: lib.str.acc purgeconf:init(48)
          252  +				var purgestrs = array(
          253  +					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
          254  +					'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst', 'glory',
          255  +					'hope', 'grace', 'pearl', 'carnation', 'rose', 'peony', 'poppy'
          256  +				)
          257  +				for i=0,3 do
          258  +					purgeconf:push(purgestrs[lib.crypt.random(intptr,0,[purgestrs.type.N])],0)
          259  +					if i ~= 2 then purgeconf:lpush('-') end
   232    260   				end
   233         -			cinp:lpush('</div></details>')
   234         -		end
          261  +				cinp:lpush('<details><summary>purge account</summary><p>you have the authority to destroy this account and all its associated content irreversibly and irretrievably. if you really wish to apply such an extreme sanction, enter the confirmation string <strong style="user-select:none">'):push(purgeconf.buf,purgeconf.sz):lpush('</strong> below and press the “alter” button to begin the process.</p><div class="elem"><label for="purge">purge confirmation string</label><input type="text" id="purge" name="purgekey"></div><input type="hidden" name="purgestr" value="'):push(purgeconf.buf,purgeconf.sz):lpush('"></details>')
          262  +				purgeconf:free()
          263  +			end
          264  +
          265  +			-- TODO black mark system? e.g. resolution option for badthink reports
          266  +			-- adds a black mark to the offending user; they can be automatically banned
          267  +			-- or brought up for review after a certain number of offenses; possibly lower
          268  +			-- set of default privs for marked users
   235    269   
   236         -		-- TODO black mark system? e.g. resolution option for badthink reports
   237         -		-- adds a black mark to the offending user; they can be automatically banned
   238         -		-- or brought up for review after a certain number of offenses; possibly lower
   239         -		-- set of default privs for marked users
          270  +			var cinpp = cinp:finalize() defer cinpp:free()
          271  +			var unym: lib.str.acc unym:init(64)
          272  +			unym:lpush('<a href="/')
          273  +			if user(0).origin ~= 0 then unym:lpush('@') end
          274  +			do var sanxid = lib.html.sanitize(user(0).xid, true)
          275  +				unym:ppush(sanxid)
          276  +				sanxid:free() end
          277  +			unym:lpush('" class="id">')
          278  +			lib.render.nym(user.ptr,0,&unym,false)
          279  +			unym:lpush('</a>')
          280  +			var ctlbox = data.view.conf_user_ctl {
          281  +				name = unym:finalize();
          282  +				inputcontent = cinpp;
          283  +				btns = pstr{'',0};
          284  +			}
          285  +			if co.who.id ~= uid and co.who.rights.powers.cred() then
          286  +				ctlbox.btns = lib.str.acc{}:compose('<a class="button" href="/conf/users/',path(2),'/cred">security &amp; credentials</a>'):finalize()
          287  +			end
          288  +			var pg: lib.str.acc pg:init(512)
          289  +			ctlbox:append(&pg)
          290  +			ctlbox.name:free()
          291  +			if ctlbox.btns.ct > 0 then ctlbox.btns:free() end
   240    292   
   241         -		var cinpp = cinp:finalize() defer cinpp:free()
   242         -		var clnkp: pstr
   243         -		if clnk.sz > 0 then clnkp = clnk:finalize() else
   244         -			clnk:free()
   245         -			clnkp = pstr { ptr='', ct=0 }
          293  +			return pg:finalize()
   246    294   		end
   247         -		var unym: lib.str.acc unym:init(64)
   248         -		unym:lpush('<a href="/')
   249         -		if user(0).origin ~= 0 then unym:lpush('@') end
   250         -		do var sanxid = lib.html.sanitize(user(0).xid, true)
   251         -			unym:ppush(sanxid)
   252         -			sanxid:free() end
   253         -		unym:lpush('" class="id">')
   254         -		lib.render.nym(user.ptr,0,&unym,false)
   255         -		unym:lpush('</a>')
   256         -		var pg = data.view.conf_user_ctl {
   257         -			name = unym:finalize();
   258         -			inputcontent = cinpp;
   259         -			linkcontent = clnkp;
   260         -		}
   261         -		var ret = pg:tostr()
   262         -		pg.name:free()
   263         -		if clnkp.ct > 0 then clnkp:free() end
   264         -		return ret
   265    295   	else
   266    296   		var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all')
   267    297   		var idbuf: int8[lib.math.shorthand.maxlen]
   268    298   		var ulst: lib.str.acc ulst:init(256)
   269    299   		var mode: uint8 = mode_local
   270    300   		var modestr = co:pgetv('show')
   271    301   		ulst:lpush('<div style="text-align: right"><em>showing ')

Modified render/nav.t from [5194b2263f] to [50b4e7c2b2].

     3      3   render_nav(co: &lib.srv.convo)
     4      4   	var t: lib.str.acc t:init(64)
     5      5   	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
     6      6   		t:lpush(' <a accesskey="t" href="/">timeline</a>')
     7      7   	end
     8      8   	if co.who ~= nil then
     9      9   		t:lpush(' <a accesskey="c" href="/compose">compose</a> <a accesskey="p" href="/'):push(co.who.xid,0)
    10         -		t:lpush('">profile</a> <a accesskey="m" href="/media">media</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a> <a class="bell" href="/notices">notices</a>')
           10  +		t:lpush('">profile</a> <a accesskey="m" href="/media">media</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a> <a class="bell" accesskey="x" href="/notices">notices</a>')
    11     11   	else
    12     12   		t:lpush(' <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/login">log in</a>')
    13     13   	end
    14     14   	return t:finalize()
    15     15   end
    16     16   return render_nav

Modified render/tweet.t from [18c58bf27c] to [83917dbbe9].

    31     31   		author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
    32     32   	end
    33     33   	if p.rtdby ~= 0 and retweeter == nil then
    34     34   		retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr
    35     35   	end
    36     36   
    37     37   	::foundauth::
    38         -	var avistr: lib.str.acc if author.origin == 0 then
    39         -		avistr:compose('/avi/',author.handle)
    40         -	end
    41     38   	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
    42     39   	for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄
    43     40   
    44     41   	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0},false)
    45     42   	defer bhtml:free()
    46     43   
    47     44   	var idbuf: int8[lib.math.shorthand.maxlen]

Modified route.t from [881176d2e1] to [b4f49ac3f6].

   284    284   	lib.render.tweet_page(co, path, post.ptr)
   285    285   	do return end
   286    286   
   287    287   	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
   288    288   	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   289    289   	::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
   290    290   end
          291  +
          292  +local terra 
          293  +credsec_for_uid(co: &lib.srv.convo, uid: uint64)
          294  +	var act = co:ppostv('act')
          295  +	if act:cmp(lib.str.plit 'invalidate') then
          296  +		lib.dbg('setting user\'s cookie validation time to now')
          297  +		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
          298  +		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
          299  +		co:installkey('/conf/sec',co.aid)
          300  +		return
          301  +	elseif act:cmp(lib.str.plit 'newcred') then
          302  +		var cmt = co:ppostv('comment')
          303  +		var pw = co:ppostv('newpw')
          304  +		if pw:ref() then
          305  +			var cpw = co:ppostv('rptpw')
          306  +			if not pw:cmp(cpw) then
          307  +				co:complain(400,'enrollment failure','the passwords you supplied do not match')
          308  +				return
          309  +			end
          310  +			co.srv:auth_attach_pw(uid, false, pw, cmt)
          311  +			co:reroute('?')
          312  +			return
          313  +		else
          314  +			var key = co:ppostv('newkey')
          315  +			if key:ref() then
          316  +
          317  +			end
          318  +		end
          319  +	end
          320  +	co:complain(400,'bad request','the operation you have requested is not meaningful in this context')
          321  +end
   291    322   
   292    323   terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
   293    324   	var msg = pstring.null()
   294    325   	-- first things first, do priv checks
   295    326   	if path.ct >= 1 then
   296    327   		if not co.who.rights.powers.config() and (
   297    328   			path(1):cmp(lib.str.lit 'srv')   or
................................................................................
   345    376   				co.ui_hue = co.srv.cfg.ui_hue
   346    377   			end
   347    378   
   348    379   			msg = lib.str.plit 'profile changes saved'
   349    380   			--user_refresh = true -- not really necessary here, actually
   350    381   
   351    382   		elseif path(1):cmp(lib.str.lit 'sec') then
   352         -			var act = co:ppostv('act')
   353         -			if act:cmp(lib.str.plit 'invalidate') then
   354         -				lib.dbg('setting user\'s cookie validation time to now')
   355         -				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
   356         -				-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
   357         -				co:installkey('/conf/sec',co.aid)
   358         -				return
   359         -			end
          383  +			credsec_for_uid(co, co.who.id)
   360    384   		elseif path(1):cmp(lib.str.lit 'users') then
   361    385   			if path.ct >= 3 then
   362    386   				var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
   363    387   				if ok then
   364    388   					var usr = co.srv:actor_fetch_uid(userid)
   365    389   					if usr:ref() then defer usr:free()
   366    390   						if not co.who:overpowers(usr.ptr) then goto nopriv end
   367    391   					end
   368    392   				end
   369         -			elseif path.ct == 2 then
          393  +			elseif path.ct == 2 and meth == method.post then
          394  +				var act = co:ppostv('act')
          395  +				if act:cmp(lib.str.plit'create') then
          396  +					var newname = co:ppostv('handle')
          397  +					if not newname or not lib.store.actor.handle_validate(newname.ptr) then
          398  +						co:complain(400,'invalid handle','the handle you have requested is not valid')
          399  +					end
          400  +					var tu = co.srv:actor_fetch_xid(newname)
          401  +					if tu:ref() then tu:free()
          402  +						co:complain(409,'handle clash','that handle conflicts with one that already exists')
          403  +						return
          404  +					end
          405  +					var kbuf: uint8[lib.crypt.const.maxdersz]
          406  +					var na = lib.store.actor.mk(&kbuf[0])
          407  +					na.handle = newname.ptr
          408  +					var newuid = co.srv:actor_create(&na)
          409  +					var shid: int8[lib.math.shorthand.maxlen]
          410  +					var shidlen = lib.math.shorthand.gen(newuid, &shid[0])
          411  +					var url = lib.str.acc{}:compose('/conf/users/',pstring{&shid[0],shidlen}):finalize() defer url:free()
          412  +					co:reroute(url.ptr)
          413  +					return
          414  +				elseif act:cmp(lib.str.plit'inst') then
          415  +				else goto badop end
   370    416   			end
   371    417   		end
   372    418   
   373    419   		if user_refresh then -- refresh the user info for the renderer
   374    420   			var usr = co.srv:actor_fetch_uid(co.who.id)
   375    421   			lib.mem.heapf(co.who)
   376    422   			co.who = usr.ptr
................................................................................
   380    426   			co:reroute(go)
   381    427   			return
   382    428   		end
   383    429   	end
   384    430   	lib.render.conf(co,path,msg)
   385    431   	do return end
   386    432   
   387         -	::nopriv:: co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action')
          433  +	::nopriv:: do co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') return end
          434  +	::badop:: do co:complain(400,'bad request','the operation you have requested is not meaningful in this context') return end
   388    435   end
   389    436   
   390    437   terra http.user_notices(co: &lib.srv.convo, meth: method.t)
   391    438   	if meth == method.post then
   392    439   		var act = co:ppostv('act')
   393    440   		if act:cmp(lib.str.plit'clear') then
   394    441   			co.srv:actor_conf_int_set(co.who.id, 'notice-clear-time', lib.osclock.time(nil))
................................................................................
   399    446   
   400    447   	lib.render.notices(co)
   401    448   	do return end
   402    449   
   403    450   	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   404    451   end
   405    452   
   406         -terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t)
   407         -	if meth == method.post then
   408         -		goto badop
   409         -	end
   410         -
   411         -	if path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
          453  +terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t, uid: uint64)
          454  +	if co.aid ~= 0 and co.who.id == uid and path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
   412    455   		if meth == method.get then
   413    456   			var view = data.view.media_upload {
   414    457   				folders = ''
   415    458   			}
   416    459   			var pg = view:tostr() defer pg:free()
   417    460   			co:stdpage([lib.srv.convo.page] {
   418    461   				title = lib.str.plit'media :: upload';
................................................................................
   453    496   			var idbuf: int8[lib.math.shorthand.maxlen]
   454    497   			var idlen = lib.math.shorthand.gen(id,&idbuf[0])
   455    498   
   456    499   			var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
   457    500   			co:reroute(url.ptr)
   458    501   			url:free()
   459    502   		else goto badop end
          503  +	elseif co.aid ~= 0 and path.ct == 4 and path(1):cmp(lib.str.lit'a') and meth==method.post then 
          504  +		var act = co:ppostv('act')
          505  +		if not act or not act:cmp(lib.str.plit'confirm') then goto badop end
          506  +		var artid, aok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
          507  +		if not aok then goto e404 end
          508  +		var art = co.srv:artifact_fetch(uid,artid)
          509  +		if not art then goto e404 end
          510  +		defer art:free()
          511  +
          512  +		if path(3):cmp(lib.str.lit'avi') then
          513  +		 -- user wants to set avatar
          514  +			co.who.avatarid = artid
          515  +			co.srv:actor_save(co.who)
          516  +			co:reroute('/conf/avi')
          517  +		elseif path(3):cmp(lib.str.lit'del') then 
          518  +			co.srv:artifact_disclaim(co.who.id, artid)
          519  +			co:reroute('/media')
          520  +		else goto badop end
   460    521   	else
   461    522   		if meth == method.post then goto badop end
   462         -		lib.render.media_gallery(co,path,co.who.id,nil)
          523  +		lib.render.media_gallery(co,path,uid,nil)
   463    524   	end
   464    525   	do return end
   465    526   
   466    527   	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   467    528   	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
   468    529   end
   469    530   
................................................................................
   501    562   		::[send]:: page:send(co.con) return true
   502    563   	end
   503    564   end
   504    565   
   505    566   
   506    567   terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
   507    568   	-- TODO retrieve user avatars
   508         -	co:reroute('/s/default-avatar.webp')
          569  +	var usr = co.srv:actor_fetch_xid(handle)
          570  +	if not usr then
          571  +	goto default end
          572  +	if usr(0).origin == 0 then
          573  +		if usr(0).avatarid == 0 then goto default end
          574  +		var avi, mime = co.srv:artifact_load(usr(0).avatarid)
          575  +		if not avi then goto default end
          576  +		defer avi:free() defer mime:free()
          577  +		co:bytestream(mime,avi)
          578  +	else
          579  +		co:reroute(usr(0).avatar)
          580  +	end
          581  +	do return end
          582  +	::default:: co:reroute('/s/default-avatar.webp')
   509    583   end
   510    584   
   511    585   terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
   512    586   	var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
   513    587   	if not idok then goto e404 end
   514    588   	var data, mime = co.srv:artifact_load(id)
   515    589   	if not data then goto e404 end
   516    590   	do defer data:free() defer mime:free()
   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)
   532         -		lib.net.mg_send(co.con, data.ptr, data.ct)
   533         -		lib.net.mg_send(co.con, '\r\n', 2)
          591  +		co:bytestream(mime,data)
   534    592   	return end
   535    593   
   536    594   	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
   537    595   end
   538    596   
   539    597   -- entry points
   540    598   terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
................................................................................
   580    638   			http.actor_profile_uid(co, path, meth)
   581    639   		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
   582    640   			http.tweet_page(co, path, meth)
   583    641   		elseif path(0):cmp(lib.str.lit('tl')) then
   584    642   			http.timeline(co, path)
   585    643   		elseif path(0):cmp(lib.str.lit('media')) then
   586    644   			if co.aid == 0 then goto unauth end
   587         -			http.media_manager(co, path, meth)
          645  +			http.media_manager(co, path, meth, co.who.id)
   588    646   		elseif path(0):cmp(lib.str.lit('doc')) then
   589    647   			if not meth_get(meth) then goto wrongmeth end
   590    648   			http.documentation(co, path)
   591    649   		elseif path(0):cmp(lib.str.lit('conf')) then
   592    650   			if co.aid == 0 then goto unauth end
   593    651   			http.configure(co,path,meth)
   594    652   		else goto notfound end

Modified srv.t from [ca6d27c8d7] to [56e1fe84a6].

   228    228   		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
   229    229   			ptr = &hdrs[0], ct = 3
   230    230   		})
   231    231   	end
   232    232   end
   233    233   
   234    234   terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end
          235  +
          236  +terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
          237  +	-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
          238  +	-- chest wound. ultimately we need to compile a whitelist of safe mime
          239  +	-- types as part of mimelib, but that is no small task. for now, this
          240  +	-- will keep the patient from immediately bleeding out
          241  +	if mime:cmp(lib.str.plit'text/html') or
          242  +		mime:cmp(lib.str.plit'text/xml') or
          243  +		mime:cmp(lib.str.plit'application/xhtml+xml') or
          244  +		mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
          245  +	then -- danger will robinson
          246  +		mime = lib.str.plit'text/plain'
          247  +	elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
          248  +		mime = lib.str.plit'application/octet-stream'
          249  +	end
          250  +	lib.net.mg_printf(self.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", mime.ct, mime.ptr, data.ct + 2)
          251  +	lib.net.mg_send(self.con, data.ptr, data.ct)
          252  +	lib.net.mg_send(self.con, '\r\n', 2)
          253  +end
   235    254   
   236    255   terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
   237    256   	var hdrs = array(
   238    257   		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   239    258   		lib.http.header { key = 'Location',     value = dest },
   240    259   		lib.http.header { key = 'Set-Cookie',   value = cookie }
   241    260   	)

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

   631    631   }
   632    632   
   633    633   menu { all: unset; display: block; }
   634    634   body.conf main {
   635    635   	display: grid;
   636    636   	grid-template-columns: 2in 1fr;
   637    637   	grid-template-rows: max-content 1fr;
          638  +	div.context {
          639  +		border-radius: 4px;
          640  +		text-align: center;
          641  +		background: tone(-53%);
          642  +		box-shadow: 0 1px 0 1px tone(-55%);
          643  +		border: 1px solid tone(-20%);
          644  +		font-style: italic;
          645  +		padding: 0.1in;
          646  +	}
   638    647   	> menu { @extend %navmenu; }
   639    648   	> .panel {
   640    649   		grid-column: 2/3; grid-row: 1/3;
   641    650   		padding-left: 0.15in;
   642    651   		> h1 {
   643    652   			padding-bottom: 0.1in;
   644    653   			margin-bottom: 0.1in;

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

   163    163   	var newkp = lib.crypt.genkp()
   164    164   	var privsz = lib.crypt.der(false,&newkp,kbuf)
   165    165   	return m.actor {
   166    166   		id = 0; nym = nil; handle = nil;
   167    167   		origin = 0; bio = nil; avatar = nil;
   168    168   		knownsince = lib.osclock.time(nil);
   169    169   		rights = m.rights_default();
          170  +		avatarid = 0;
   170    171   		epithet = nil, key = [lib.mem.ptr(uint8)] {
   171    172   			ptr = &kbuf[0], ct = privsz
   172    173   		};
   173    174   	}
   174    175   end
   175    176   
   176    177   struct m.actor_stats {
................................................................................
   407    408   	actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
   408    409   	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
   409    410   	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
   410    411   	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship
   411    412   
   412    413   	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
   413    414   	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
   414         -	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
          415  +	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> {}
          416  +	auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {}
   415    417   		-- uid: uint64
   416    418   		-- reset: bool (delete other passwords?)
   417    419   		-- pw: pstring
   418    420   		-- comment: pstring
   419    421   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   420    422   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
   421    423   	auth_purge_trust: {&m.source, uint64, rawstring} -> {}

Modified view/conf-sec-credmg.tpl from [43efff9618] to [6f4f9fa693].

     1      1   <hr>
     2      2   <form method="post">
     3         -	<p>your account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
            3  +	<p>this account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
     4      4   	<select size="6" name="cred">
     5      5   		@credlist
     6      6   	</select>
     7      7   	<menu class="horizontal choice">
     8      8   		<button name="act" value="reset">reset</button>
     9      9   		<button name="act" value="revoke">revoke</button>
    10     10   	</menu>
    11     11   </form>
    12     12   <hr>
    13         -<form method="post">
    14         -	<p>you can associate extra credentials with your account. you can also limit how much of your authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read your timeline, but not post as you or access any administrative powers you may have. if you don't select a capability set, the credential will be able to wield the full scope of your powers.</p>
           13  +<form method="get">
           14  +	<p>you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.</p>
    15     15   	<div class="check-panel">
    16     16   		<label><input type="checkbox" name="allow-post"> post</label>
    17     17   		<label><input type="checkbox" name="allow-edit"> edit</label>
    18     18   		<label><input type="checkbox" name="allow-acct"> manage account</label>
    19     19   		<label><input type="checkbox" name="allow-upload"> upload artifacts</label>
    20     20   		<label><input type="checkbox" name="allow-censor"> moderation</label>
    21     21   		<label><input type="checkbox" name="allow-admin"> other admin powers</label>
................................................................................
    23     23   	</div>
    24     24   	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
    25     25   	<div class="elem">
    26     26   		<label for="netmask">netmask</label>
    27     27   		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
    28     28   	</div>
    29     29   	<menu class="vertical choice">
    30         -		<button name="kind" value="pw">new password</button>
    31         -		<button name="kind" value="otp">new OTP key</button>
    32         -		<button name="kind" value="api">new API token</button>
    33         -		<button name="kind" value="challenge">new challenge key</button>
    34         -	</div>
           30  +		<button name="new" value="pw">new password</button>
           31  +		<button name="new" value="otp">new OTP key</button>
           32  +		<button name="new" value="api">new API token</button>
           33  +		<button name="new" value="challenge">new challenge key</button>
           34  +	</menu>
    35     35   </form>

Added view/conf-sec-pwnew.tpl version [5878dd4701].

            1  +<form method="post">
            2  +	<div class="elem">
            3  +		<label for="comment">comment</label>
            4  +		<input type="text" id="comment" name="comment" value="@comment" required>
            5  +	</div>
            6  +	<div class="elem">
            7  +		<label for="newpw">new password</label>
            8  +		<input type="password" id="newpw" name="newpw" required>
            9  +	</div>
           10  +	<div class="elem">
           11  +		<label for="rptpw">confirm password</label>
           12  +		<input type="password" id="rptpw" name="rptpw" required>
           13  +	</div>
           14  +	<menu class="choice horizontal">
           15  +		<button name="act" value="newcred">enroll</button>
           16  +		<a class="button" href="?">cancel</a>
           17  +	</menu>
           18  +</form>

Modified view/conf-sec.tpl from [de1cf7e8f0] to [49c75b1abd].

     1      1   <form method="post">
     2         -	<p>if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p>
            2  +	<p>if you are concerned that this account may have been compromised, you can terminate conflicting login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p>
     3      3   	<div class="elem">
     4      4   		<label> sessions valid from </label>
     5      5   		<div class="txtbox">@lastreset</div>
     6      6   	</div>
     7      7   	<button type="submit" name="act" value="invalidate">
     8      8   		invalidate other sessions
     9      9   	</button>
    10     10   </form>

Modified view/conf-user-ctl.tpl from [7abbc95a91] to [7e2cfb9c6e].

     1      1   <form method="post">
     2      2   	<div class="elem">
     3      3   		<label>user</label>
     4      4   		<div class="txtbox">@name</div>
     5      5   	</div>
     6      6   	@inputcontent
     7         -	<button>alter</button>
            7  +	<menu class="vertical choice">
            8  +		<button>alter</button>
            9  +		@btns 
           10  +	</menu>
     8     11   </form>
     9         -@linkcontent

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

    19     19   	'login-username';
    20     20   	'login-challenge';
    21     21   
    22     22   	'conf';
    23     23   	'conf-profile';
    24     24   	'conf-sec';
    25     25   	'conf-sec-credmg';
           26  +	'conf-sec-pwnew';
    26     27   	'conf-user-ctl';
    27     28   }
    28     29   
    29     30   local ingest = function(filename)
    30     31   	local hnd = io.open(path..'/'..filename)
    31     32   	local txt = hnd:read('*a')
    32     33   	io.close(hnd)