From 9d0b51a2abdacb5fc4eed95cb3249934b607fdc7 Mon Sep 17 00:00:00 2001
From: Chris Xiong <chirs241097@gmail.com>
Date: Sat, 9 Feb 2019 00:04:45 +0800
Subject: Navigator.

---
 xp/navigator/cgi-src/navigator_cgi.cpp | 329 +++++++++++++++++++++++++
 xp/navigator/index.html                | 133 ++++++++++
 xp/navigator/main.js                   | 435 +++++++++++++++++++++++++++++++++
 xp/navigator/sha256.js                 | 173 +++++++++++++
 xp/navigator/shit.php                  |  38 +++
 5 files changed, 1108 insertions(+)
 create mode 100644 xp/navigator/cgi-src/navigator_cgi.cpp
 create mode 100644 xp/navigator/index.html
 create mode 100644 xp/navigator/main.js
 create mode 100644 xp/navigator/sha256.js
 create mode 100644 xp/navigator/shit.php

diff --git a/xp/navigator/cgi-src/navigator_cgi.cpp b/xp/navigator/cgi-src/navigator_cgi.cpp
new file mode 100644
index 0000000..20e152e
--- /dev/null
+++ b/xp/navigator/cgi-src/navigator_cgi.cpp
@@ -0,0 +1,329 @@
+#include <cstdio>
+#include <cstdlib>
+#include <ctime>
+#include <string>
+#include <sstream>
+
+#include <jsoncpp/json/json.h>
+
+#include <mysql/my_global.h>
+#include <mysql/mysql.h>
+MYSQL* sql;
+const char* rand_ch="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+Json::Value do_login(Json::Value o)
+{
+	MYSQL_RES* sqlr=NULL;
+	MYSQL_ROW row;
+	char q[256];
+	Json::Value ret;
+	std::string usrname=o["username"].asString();
+	std::string passwd=o["passwd"].asString();
+	std::string sessname=o["sessionname"].asString();
+	std::string token;
+	std::string qpwd;
+	if(usrname.length()<1){ret["result"]=1;goto fail;}
+	for(size_t i=0;i<usrname.length();++i)if(!isalnum(usrname[i])&&usrname[i]!='-'&&usrname[i]!='_'){ret["result"]=1;goto fail;}
+	for(size_t i=0;i<passwd.length();++i)if(!isalnum(passwd[i])){ret["result"]=1;goto fail;}
+	for(size_t i=0;i<sessname.length();++i)if(!isalnum(sessname[i])&&sessname[i]!='-'&&sessname[i]!='_'){ret["result"]=1;goto fail;}
+	
+	snprintf(q,256,"select password from navigator_user where username='%s'",usrname.c_str());
+	mysql_query(sql,q);
+	sqlr=mysql_store_result(sql);
+	if(mysql_num_rows(sqlr)!=1){ret["result"]=4;goto fail;}
+	row=mysql_fetch_row(sqlr);
+	qpwd=std::string(row[0],64);
+	if(passwd!=qpwd)
+	{
+		ret["result"]=1;
+		goto fail;
+	}
+	mysql_free_result(sqlr);sqlr=NULL;
+
+	if(!sessname.length())
+	{
+		snprintf(q,256,"delete from navigator_session where sessionname='' and username='%s'",usrname.c_str());
+		mysql_query(sql,q);
+		mysql_commit(sql);
+	}
+	else
+	{
+		snprintf(q,256,"select sessionname from navigator_session where sessionname='%s' and username='%s'",
+		sessname.c_str(),usrname.c_str());
+		mysql_query(sql,q);
+		sqlr=mysql_store_result(sql);
+		if(mysql_num_rows(sqlr)>0){ret["result"]=2;goto fail;}
+		mysql_free_result(sqlr);sqlr=NULL;
+	}
+
+	do{
+		if(sqlr){mysql_free_result(sqlr);sqlr=NULL;}
+		token="";
+		for(int i=0;i<32;++i)token.push_back(rand_ch[rand()%62]);
+		snprintf(q,256,"select token from navigator_session where token='%s'",
+		token.c_str());
+		mysql_query(sql,q);
+		sqlr=mysql_store_result(sql);
+	}while(mysql_num_rows(sqlr));
+	mysql_free_result(sqlr);sqlr=NULL;
+	
+	snprintf(q,256,"insert into navigator_session values('%s','%s','%s',%lld)",
+		usrname.c_str(),
+		token.c_str(),
+		sessname.c_str(),
+		time(NULL)
+	);
+	mysql_query(sql,q);
+	mysql_commit(sql);
+	ret["result"]=0;
+	ret["token"]=token;
+fail:
+	if(sqlr){mysql_free_result(sqlr);sqlr=NULL;}
+	return ret;
+}
+const int tsl[]={1800,86400,604800,1296000,2592000,5184000,7776000,15552000,31104000};
+void set_session_length(std::string user,int c)
+{
+	char q[256];
+	if(c>7)c=7;if(c<0)c=0;
+	snprintf(q,256,"update navigator_user set session_length=%d where username='%s'",user.c_str());
+	mysql_query(sql,q);
+	mysql_commit(sql);
+}
+int get_session_length(std::string user)
+{
+	MYSQL_RES* sqlr=NULL;
+	MYSQL_ROW row;
+	char q[256];
+	snprintf(q,256,"select session_length from navigator_user where username='%s'",user.c_str());
+	mysql_query(sql,q);
+	sqlr=mysql_store_result(sql);
+	if(mysql_num_rows(sqlr)!=1){mysql_free_result(sqlr);return -1;}
+	row=mysql_fetch_row(sqlr);
+	int sl=atoi(row[0]);
+	mysql_free_result(sqlr);
+	return sl;
+}
+std::string authenticate(std::string token)
+{
+	MYSQL_RES* sqlr=NULL;
+	MYSQL_ROW row;
+	char q[256];
+	if(token.length()!=32)return "";
+	snprintf(q,256,"select username,sessionname,date from navigator_session where token='%s'",token.c_str());
+	mysql_query(sql,q);
+	sqlr=mysql_store_result(sql);
+	if(mysql_num_rows(sqlr)!=1){mysql_free_result(sqlr);return "";}
+	row=mysql_fetch_row(sqlr);
+	std::string ret=std::string(row[0]);
+	bool is_temporary=strlen(row[1])>0;
+	long long d=atoll(row[2]);
+	mysql_free_result(sqlr);
+	int sl=get_session_length(ret);
+	if(sl<0||sl>7){return "";}
+	int reall=tsl[is_temporary?0:sl+1];
+	if(time(NULL)-d>reall)
+	{
+		snprintf(q,256,"delete from navigator_session where token='%s'",token.c_str());
+		mysql_query(sql,q);
+		mysql_commit(sql);
+		return "";
+	}
+	return ret;
+}
+Json::Value set_option(Json::Value o)
+{
+	Json::Value ret;
+	std::string token=o["token"].asString();
+	std::string usr=authenticate(token);
+	int opt=o["option"].asInt();
+	int val=o["value"].asInt();
+	if(!usr.length()){ret["result"]=1;goto fail;}
+	switch(opt)
+	{
+		case 0:
+			ret["result"]=0;
+			set_session_length(usr,val);
+		break;
+		default:
+		ret["result"]=1;
+	}
+fail:
+	return ret;
+}
+Json::Value get_option(Json::Value o)
+{
+	Json::Value ret;
+	std::string token=o["token"].asString();
+	std::string usr=authenticate(token);
+	int opt=o["option"].asInt();
+	if(!usr.length()){ret["result"]=1;goto fail;}
+	switch(opt)
+	{
+		case 0:
+			ret["result"]=0;
+			ret["value"]=get_session_length(usr);
+		break;
+		default:
+		ret["result"]=1;
+	}
+fail:
+	return ret;
+}
+Json::Value get_bookmarks(Json::Value o)
+{
+	MYSQL_RES* sqlr=NULL;
+	MYSQL_ROW row;
+	char q[256];
+	Json::Value ret;
+	std::string token=o["token"].asString();
+	std::string usr=authenticate(token);
+	if(!usr.length()){ret["result"]=1;goto fail;}
+	snprintf(q,256,"select bookmarks from navigator_user where username='%s'",usr.c_str());
+	mysql_query(sql,q);
+	sqlr=mysql_store_result(sql);
+	if(mysql_num_rows(sqlr)!=1){mysql_free_result(sqlr);sqlr=NULL;ret["result"]=4;goto fail;}
+	row=mysql_fetch_row(sqlr);
+	ret["result"]=0;
+	ret["bookmarks"]=std::string(row[0]);
+	mysql_free_result(sqlr);sqlr=NULL;
+fail:
+	if(sqlr){mysql_free_result(sqlr);sqlr=NULL;}
+	return ret;
+}
+Json::Value set_bookmarks(Json::Value o)
+{
+	char *q=(char*)malloc(65537);
+	Json::Value ret;
+	std::string token=o["token"].asString();
+	std::string usr=authenticate(token);
+	if(!usr.length()){ret["result"]=1;goto fail;}
+	snprintf(q,65536,"update navigator_user set bookmarks='%s' where username='%s'",o["bookmarks"].asString().c_str(),usr.c_str());
+	mysql_query(sql,q);
+	mysql_commit(sql);
+	ret["result"]=0;
+fail:
+	free(q);
+	return ret;
+}
+Json::Value list_sessions(Json::Value o)
+{
+	MYSQL_RES* sqlr=NULL;
+	MYSQL_ROW row;
+	char q[256];
+	Json::Value ret,ss;
+	std::string token=o["token"].asString();
+	std::string usr=authenticate(token);
+	if(!usr.length()){ret["result"]=1;goto fail;}
+	snprintf(q,256,"select sessionname,date from navigator_session where username='%s'",usr.c_str());
+	
+	mysql_query(sql,q);
+	sqlr=mysql_store_result(sql);
+	for(int i=0;row=mysql_fetch_row(sqlr);++i)
+	{
+		Json::Value c;
+		c["sessionname"]=std::string(row[0]);
+		c["date"]=atoi(row[1]);
+		ss[i]=c;
+	}
+	mysql_free_result(sqlr);sqlr=NULL;
+	ret["result"]=0;ret["sessions"]=ss;
+fail:
+	return ret;
+}
+Json::Value remove_session(Json::Value o)
+{
+	char q[256];
+	Json::Value ret;
+	std::string token=o["token"].asString();
+	std::string usr=authenticate(token);
+	std::string sess=o["session"].asString();
+	if(!usr.length()){ret["result"]=1;goto fail;}
+	snprintf(q,256,"delete from navigator_session where username='%s' and sessionname='%s'",usr.c_str(),sess.c_str());
+	mysql_query(sql,q);
+	mysql_commit(sql);
+	ret["result"]=0;
+fail:
+	return ret;
+}
+int main()
+{
+	if(!getenv("CONTENT_LENGTH"))return -1;
+	int len=atoi(getenv("CONTENT_LENGTH"));
+	char *buf;buf=(char*)malloc(len+1);
+	fread(buf,1,len,stdin);buf[len]=0;
+	std::string sbuf(buf,len);
+	free(buf);
+	std::stringstream ss(sbuf);
+	Json::Value o,r;ss>>o;
+	
+	sql=mysql_init(NULL);
+	if(!sql)return -1;
+	if(!mysql_real_connect(sql,"localhost","chrisoft",NULL,"chrisoft",0,"/var/run/mysqld/mysqld.sock",0))
+	return -1;
+	
+	switch(o.get("op",-1).asInt())
+	{
+		case 0://login
+			r=do_login(o);
+		break;
+		case 1://get bookmarks
+			r=get_bookmarks(o);
+		break;
+		case 2://set bookmarks
+			r=set_bookmarks(o);
+		break;
+		case 3://list sessions
+			r=list_sessions(o);
+		break;
+		case 4://remove session
+			r=remove_session(o);
+		break;
+		case 5://set option
+			r=set_option(o);
+		break;
+		case 6://get option
+			r=get_option(o);
+		break;
+	}
+	printf("Status: 200 OK\r\n");
+	printf("Content-type: application/json; charset=utf-8\r\n\r\n");
+	std::ostringstream oss;
+	oss<<r;
+	fputs(oss.str().c_str(),stdout);
+	return 0;
+}
+//TODO: set cookie in HTTP header
+
+/*
+ * session_length:
+ * 0=1d=86400
+ * 1=7d=604800
+ * 2=15d=1296000
+ * 3=30d=2592000
+ * 4=60d=5184000
+ * 5=90d=7776000
+ * 6=180d=15552000
+ * 7=360d=31104000
+ */
+/*navigator_user:
+ * username:varchar(PRI) passwd:char(64) bookmarks:text
+ *navigator_session:
+ * username:varchar token:char(32)(PRI) sessionname:varchar date:bigint
+ */
+/*
+create table navigator_user(
+  username varchar(32),
+  password char(64),
+  bookmarks text,
+  session_length int,
+  primary key(username)
+);
+create table navigator_session(
+  username varchar(32) not null,
+  token char(32),
+  sessionname varchar(32),
+  date bigint,
+  primary key(token)
+);
+insert into navigator_user values('chrisoft','8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92','',6);
+*/
diff --git a/xp/navigator/index.html b/xp/navigator/index.html
new file mode 100644
index 0000000..6ae2586
--- /dev/null
+++ b/xp/navigator/index.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta name="viewport" content="width=device-width">
+<title>Chrisoft::Navigator</title>
+<link rel="icon" href="/favicon.png">
+<link rel="stylesheet" type="text/css" href="/common.css">
+<link rel="stylesheet" type="text/css" href="/panel.css">
+<style>
+input[type=text],input[type=password]{
+	margin-left:5px;
+	display:table-cell;
+	max-width:40%;
+	border:none;
+	border-bottom:2px solid #4A4;
+	outline:none !important;
+	background-color:rgba(0,0,0,0) !important;
+}
+button{
+	border:none;
+	color:white;
+	padding:0.5em 2em;
+	text-align:center;
+	background-color:#4A4;
+	-webkit-transition-duration:0.4s;
+	transition-duration:0.4s;
+}
+ul#container li{
+	padding:1em;
+}
+.ghost{
+	opacity:0.4;
+}
+a.listitem{
+	padding-left:160px !important;
+	margin-left:-160px;
+}
+ul#folderlist{
+	list-style-type:none;
+}
+</style>
+</head>
+<!--
+The source code of this site, including all HTML, JavaScript and CSS
+files, are licensed under the terms of the Expat(MIT) License.
+Contents are licensed under the CC BY-SA 4.0 license.
+-->
+<body onload="init()">
+<script type="text/javascript" src="/panel.js"></script>
+<script type="text/javascript" src="main.js"></script>
+<script type="text/javascript" src="sha256.js"></script>
+<script type="text/javascript">
+function changeImage(img,e){
+	document.getElementById(e).src=img;
+}
+function changeTheme(thm){
+	document.cookie="thm="+thm;
+}
+var x1,y1,x2,y2;
+var link=document.createElement("link");
+var thm=document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)thm\\s*\\=\\s*([^;]*).*$)|^.*$"),"$1");
+switch(thm)
+{
+	case "day":
+		link.href="/colors-day.css";
+	break;
+	case "night":
+		link.href="/colors-night.css";
+	break;
+	case "auto":
+	default:
+		var c=new Date();
+		if(c.getHours()>=6&&c.getHours()<18)
+			link.href = "/colors-day.css";
+		else
+			link.href = "/colors-night.css";
+	break;
+}
+link.type="text/css";
+link.rel="stylesheet";
+document.getElementsByTagName("head")[0].appendChild(link);
+</script>
+	<div id="panel" class="TText">
+		<ul id="panellist">
+			<li><a href="/"><h1>Chrisoft</h1></a></li>
+			<li><a href="#" onclick="">Navigator</a></li>
+			<ul id="folderlist">
+			</ul>
+			<li><a href="#settings" onclick="showsettings()">Settings</a></li>
+		</ul>
+	</div>
+	<div id="content" class="TText">
+		<div class="block" id="login">
+			<h2>Login</h2><hr>
+			<form style="text-align:center;" class="TText">
+				<input class="TText" type="text" placeholder="Username" id="usrname"></input><br><br>
+				<input class="TText" type="password" placeholder="Password" id="passwd"></input><br><br>
+				<input class="TText" type="text" placeholder="Session name" id="session"></input><br><br>
+				<span id="loginerr" style="color:red;"></span><br><br>
+				<button class="TText" type="button" onclick="login()">Login</button>
+			</form>
+		</div>
+		<div class="block" id="main">
+			<ul id="container" style="list-style:none;">
+			</ul>
+		</div>
+		<div class="block" id="settings">
+			<h2>Sessions</h2>
+			<table style="text-align:center;">
+				<thead>
+					<tr>
+						<th>Session name</th>
+						<th>Login date</th>
+						<th>Invalidate session</th>
+					</tr>
+				</thead>
+				<tbody id="sessions">
+				</tbody>
+			</table>
+		</div>
+	</div>
+	<div id="templates" style="display:none;">
+		<div id="_lnkmod">
+			<input class="TText" type="text" placeholder="Name" id="_lnkname" style="max-width:auto;width:45%;" onkeypress="kp(event)"></input>
+			<input class="TText" type="text" placeholder="URL" id="_lnkhref" style="max-width:auto;width:45%;" onkeypress="kp(event)"></input>
+		</div>
+		<div id="_fdrmod">
+			<input class="TText" type="text" placeholder="Name" id="_fdrname" style="max-width:auto;width:90%;" onkeypress="kp(event)"></input>
+		</div>
+	</div>
+</body>
+</html>
diff --git a/xp/navigator/main.js b/xp/navigator/main.js
new file mode 100644
index 0000000..c17d025
--- /dev/null
+++ b/xp/navigator/main.js
@@ -0,0 +1,435 @@
+let o={};
+const ui={};
+let curo,curp;
+let editinge=null;
+function getcookie(key)
+{return document.cookie.replace(new RegExp('(?:(?:^|.*;\\s*)'+key+'\\s*\\=\\s*([^;]*).*$)|^.*$'),'$1');}
+function make_sortable(e,updatefunc)
+{
+	let dragging,nxt;
+	function _drag(ev)
+	{
+		ev.preventDefault();
+		ev.dataTransfer.dropEffect='move';
+		const targ=ev.target;
+		if(targ&&targ!=dragging&&targ.draggable&&targ.parentNode==e)
+		{
+			const rect=targ.getBoundingClientRect();
+			const after=(ev.clientY-rect.top)/(rect.bottom-rect.top)>.5;
+			e.insertBefore(dragging,after?targ.nextSibling:targ);
+		}
+	}
+	function _drag_end(ev)
+	{
+		ev.preventDefault();
+		dragging.classList.remove('ghost');
+		e.removeEventListener('dragover',_drag,false);
+		e.removeEventListener('dragend',_drag_end,false);
+		if(nxt!=dragging.nextSibling)updatefunc(dragging);
+	}
+	function _drag_start(ev)
+	{
+		dragging=ev.target;
+		nxt=dragging.nextSibling;
+		ev.dataTransfer.effectAllowed='move';
+		ev.dataTransfer.setData('Text',dragging.textContent);
+		e.addEventListener('dragover',_drag,false);
+		e.addEventListener('dragend',_drag_end,false);
+		setTimeout(function(){dragging.classList.add('ghost');},0);
+	}
+	e.addEventListener('dragstart',_drag_start,false);
+}
+function remove_children(e)
+{
+	while(e.firstChild)e.removeChild(e.firstChild);
+}
+function switch_folder(path)
+{
+	//path starts and ends with /
+	ui.main.style.display='block';
+	ui.settings.style.display=null;
+	const p=path.split('/');
+	remove_children(ui.cont);
+	let cur=o;
+	for(let i=1;cur&&i<p.length-1;++i)
+	{
+		if(cur.type!='folder')break;
+		let k=-1;
+		for(let j=0;j<cur.content.length;++j)
+		if(cur.content[j].name==p[i]){k=j;break;}
+		if(k==-1||cur.content[k].type!='folder')break;
+		cur=cur.content[k];
+	}
+	curo=cur;
+	for(let i=0;i<curo.content.length;++i)
+	{
+		const e=document.createElement('li');
+		const a=document.createElement('a');
+		a.innerHTML=e.contname=curo.content[i].name;
+		if(curo.content[i].href)
+		a.href=curo.content[i].href;
+		e.draggable=true;
+		e.ondblclick=function(e){
+			edit(this);
+		}
+		e.appendChild(a);
+		if(curo.content[i].type=='folder')
+		{
+			e.style.backgroundColor='rgba(255,255,128,0.4)';
+			e.isfolder=true;
+			a.onclick=function(e){
+				e.preventDefault();
+				switch_folder(curp+=this.innerHTML+'/');
+			}
+		}
+		ui.cont.appendChild(e);
+	}
+	const e=document.createElement('li');
+	e.innerHTML='+';
+	e.ondblclick=function(e){
+		const ne=document.createElement('li');
+		const na=document.createElement('a');
+		ne.draggable=true;
+		ne.ondblclick=function(e){
+			edit(this);
+		}
+		ne.appendChild(na);
+		ui.cont.insertBefore(ne,ui.cont.lastChild);
+		edit(ne);
+	}
+	ui.cont.appendChild(e);
+	make_sortable(ui.cont,function(x){move(ui.cont,curo,x.contname);})
+}
+function edit(e)
+{
+	if(e==editinge)return;
+	if(editinge)finish_edit();
+	let clon=ui.templates.lnkmod;
+	if(e.isfolder||e.parentNode==ui.folderlist)
+		clon=ui.templates.fdrmod;
+	const cloned=clon.cloneNode(true);
+	cloned.id=cloned.id.substring(1);
+	for(let i=0;i<cloned.childNodes.length;++i)
+	if(cloned.childNodes[i].id)
+	{
+		cloned.childNodes[i].id=cloned.childNodes[i].id.substring(1);
+	}
+	e.firstChild.style.display='none';
+	e.appendChild(cloned);
+	if(e.isfolder||e.parentNode==ui.folderlist)
+	{
+		if(e.contname)document.getElementById('fdrname').value=e.contname;
+		document.getElementById('fdrname').focus();
+	}
+	else
+	{
+		if(e.contname)document.getElementById('lnkname').value=e.contname;
+		if(e.contname)document.getElementById('lnkhref').value=e.querySelector('a').href;
+		document.getElementById('lnkname').focus();
+	}
+	editinge=e;
+}
+function finish_edit()
+{
+	if(editinge)
+	{
+		const e=document.getElementById('lnkmod')||document.getElementById('fdrmod');
+		if(e.parentNode==editinge)
+		editinge.removeChild(e);
+		editinge.firstChild.style.display=null;
+	}
+	editinge=null;
+}
+function confirm_edit()
+{
+	if(!editinge)return;
+	if(editinge.parentNode==ui.folderlist)
+	{
+		if(editinge.contname)
+		{
+			for(let i=0;i<o.content.length;++i)
+			if(o.content[i].name==editinge.contname)
+			{
+				if(!document.getElementById('fdrname').value.length)
+				{
+					o.content.splice(i,1);
+					editinge.parentNode.removeChild(editinge);
+					editinge=null;
+					return;
+				}
+				o.content[i].name=document.getElementById('fdrname').value;
+				editinge.querySelector('a').innerHTML=editinge.contname=
+					o.content[i].name;
+				break;
+			}
+		}
+		else
+		{
+			if(!document.getElementById('fdrname').value.length)
+			{
+				finish_edit();
+				editinge.parentNode.removeChild(editinge);
+			}
+			let newo={};
+			newo.type='folder';
+			newo.name=document.getElementById('fdrname').value;
+			newo.content=[];
+			o.content.push(newo);
+			editinge.querySelector('a').innerHTML=editinge.contname=newo.name;
+		}
+	}
+	else
+	{
+		if(editinge.contname)
+		{
+			for(let i=0;i<curo.content.length;++i)
+			if(curo.content[i].name==editinge.contname)
+			{
+				curo.content[i].name=document.getElementById(editinge.isfolder?'fdrname':'lnkname').value;
+				if(!curo.content[i].name)
+				{
+					curo.content.splice(i,1);
+					editinge.parentNode.removeChild(editinge);
+					editinge=null;
+					return;
+				}
+				editinge.querySelector('a').innerHTML=editinge.contname=
+					curo.content[i].name;
+				if(!editinge.isfolder)
+				{
+					curo.content[i].href=document.getElementById('lnkhref').value;
+					editinge.querySelector('a').href=curo.content[i].href;
+				}
+				break;
+			}
+		}
+		else
+		{
+			if(!document.getElementById('lnkname').value.length)
+			{
+				finish_edit();
+				editinge.parentNode.removeChild(editinge);
+			}
+			let newo={};
+			newo.type=document.getElementById('lnkhref').value.length?'link':'folder';
+			newo.name=document.getElementById('lnkname').value;
+			editinge.querySelector('a').innerHTML=editinge.contname=newo.name;
+			if(newo.type=='folder')
+			{
+				newo.content=[];
+				editinge.style.backgroundColor='rgba(255,255,128,0.4)';
+				editinge.isfolder=true;
+				editinge.querySelector('a').onclick=function(e){
+					e.preventDefault();
+					switch_folder(curp+=this.innerHTML+'/');
+				}
+			}
+			else
+			{
+				newo.href=document.getElementById('lnkhref').value;
+				editinge.querySelector('a').href=newo.href;
+			}
+			curo.content.push(newo);
+		}
+	}
+	finish_edit();
+}
+function kp(e)
+{
+	e=e||window.event;
+	const keyCode=e.keyCode||e.which;
+	if(keyCode==13)confirm_edit();
+	if(keyCode==27)finish_edit();
+}
+function move(el,o,name)
+{
+	let origpos=-1,newpos=-1;
+	const qs=el.querySelectorAll('li');
+	for(let i=0;i<qs.length;++i)
+	if(qs[i].contname==name){newpos=i;break;}
+	for(let i=0;i<o.content.length;++i)
+	if(o.content[i].name==name){origpos=i;break;}
+	const ob=o.content.splice(origpos,1);
+	o.content.splice(newpos,0,ob[0]);
+}
+async function load_bookmarks()
+{
+	const qo={};
+	qo.op=1;
+	qo.token=getcookie("navigator_token");
+	const h=new Headers();h.append('Content-Type','application/json');
+	const resp=await fetch(new Request('/navigator/cgi-bin/navigator_api.cgi',{method:"POST",headers:h,body:JSON.stringify(qo)}));
+	const r=await resp.text();
+	const ro=JSON.parse(r);
+	if(ro.result)return -1;
+	else try{o=JSON.parse(ro.bookmarks);}catch(e){o={type:"folder",name:"",content:[]};}
+	return 0;
+}
+async function save_bookmarks()
+{
+	const qo={};
+	qo.op=2;
+	qo.token=getcookie("navigator_token");
+	qo.bookmarks=JSON.stringify(o);
+	const h=new Headers();h.append('Content-Type','application/json');
+	const resp=await fetch(new Request('/navigator/cgi-bin/navigator_api.cgi',{method:"POST",headers:h,body:JSON.stringify(qo)}));
+	const r=await resp.text();
+	const ro=JSON.parse(r);
+	if(ro.result)return -1;
+	return 0;
+}
+async function login_a()
+{
+	const o={};
+	o.op=0;
+	o.username=document.getElementById('usrname').value;
+	o.passwd=Sha256.hash(document.getElementById('passwd').value);
+	o.sessionname=document.getElementById('session').value;
+	const h=new Headers();h.append('Content-Type','application/json');
+	const resp=await fetch(new Request('/navigator/cgi-bin/navigator_api.cgi',{method:"POST",headers:h,body:JSON.stringify(o)}));
+	const r=await resp.text();
+	const ro=JSON.parse(r);
+	if(ro.result)return -1;
+	else document.cookie='navigator_token='+ro.token;
+	return 0;
+}
+function login()
+{
+	login_a().then(r=>{
+			if(r)
+			{
+				document.getElementById('loginerr').innerHTML='login failed:'
+				switch(r)
+				{
+					case 1:
+					document.getElementById('loginerr').innerHTML+='authentication failure';
+					break;
+					case 2:
+					document.getElementById('loginerr').innerHTML+='duplicate session';
+					break;
+				}
+			}
+			else
+			{
+				document.getElementById('loginerr').innerHTML='';
+				init();
+			}
+		}
+	);
+}
+function display_bookmarks()
+{
+	remove_children(ui.folderlist);
+	for(let i=0;i<o.content.length;++i)
+	{
+		const e=document.createElement('li');
+		const a=document.createElement('a');
+		a.classList.add('listitem');
+		e.draggable=true;
+		a.innerHTML=e.contname=o.content[i].name;
+		a.onclick=function(e){
+			e.preventDefault();
+			switch_folder(curp='/'+this.innerHTML+'/');
+		}
+		e.ondblclick=function(e){
+			edit(this);
+		}
+		e.appendChild(a);
+		ui.folderlist.appendChild(e);
+	}
+	const e=document.createElement('li');
+	const ea=document.createElement('a');
+	ea.innerHTML='+';e.appendChild(ea);
+	ea.classList.add('listitem');
+	e.ondblclick=function(e){
+		const ne=document.createElement('li');
+		const na=document.createElement('a');
+		na.classList.add('listitem');
+		ne.draggable=true;
+		na.onclick=function(e){
+			e.preventDefault();
+			switch_folder('/'+this.innerHTML+'/');
+		}
+		ne.ondblclick=function(e){
+			edit(this);
+		}
+		ne.appendChild(na);
+		ui.folderlist.insertBefore(ne,ui.folderlist.lastChild);
+		edit(ne);
+	}
+	make_sortable(ui.folderlist,function(x){move(ui.folderlist,o,x.contname);});
+	ui.folderlist.appendChild(e);
+}
+async function list_sessions()
+{
+	const o={};
+	o.op=3;o.token=getcookie("navigator_token");
+	const h=new Headers();h.append('Content-Type','application/json');
+	const resp=await fetch(new Request('/navigator/cgi-bin/navigator_api.cgi',{method:"POST",headers:h,body:JSON.stringify(o)}));
+	const r=await resp.text();
+	const ro=JSON.parse(r);
+	if(ro.result)return -1;
+	remove_children(ui.sessionlist);
+	for(let i=0;i<ro.sessions.length;++i)
+	{
+		const tr=document.createElement('tr');
+		const td1=document.createElement('td');
+		const td2=document.createElement('td');
+		const td3=document.createElement('td');
+		const a=document.createElement('a');
+		td1.innerHTML=ro.sessions[i].sessionname;
+		const d=new Date(ro.sessions[i].date*1000);
+		td2.innerHTML=d.toString();
+		a.innerHTML='x';
+		a.sessionname=ro.sessions[i].sessionname;
+		a.onclick=function(){remove_session(this.sessionname);}
+		a.href='javascript:void(0)';
+		td3.appendChild(a);
+		tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);
+		ui.sessionlist.appendChild(tr);
+	}
+}
+async function remove_session(s)
+{
+	const o={};
+	o.op=4;o.token=getcookie("navigator_token");o.session=s;
+	const h=new Headers();h.append('Content-Type','application/json');
+	const resp=await fetch(new Request('/navigator/cgi-bin/navigator_api.cgi',{method:"POST",headers:h,body:JSON.stringify(o)}));
+	const r=await resp.text();
+	const ro=JSON.parse(r);
+	if(ro.result)return -1;
+	return 0;
+}
+function showsettings()
+{
+	ui.main.style.display=null;
+	ui.settings.style.display='block';
+	list_sessions();
+}
+function init()
+{
+	ui.login=document.getElementById('login');
+	ui.main=document.getElementById('main');
+	ui.cont=document.getElementById('container');
+	ui.folderlist=document.getElementById('folderlist');
+	ui.sessionlist=document.getElementById('sessions');
+	ui.settings=document.getElementById('settings');
+	ui.templates={};
+	ui.templates.lnkmod=document.getElementById('_lnkmod');
+	ui.templates.fdrmod=document.getElementById('_fdrmod');
+	load_bookmarks().then(
+		r=>{
+			if(r)
+			{
+				ui.login.style.display='block';
+				ui.main.style.display=null;
+			}
+			else
+			{
+				ui.login.style.display=null;
+				display_bookmarks();
+				ui.main.style.display='block';
+			}
+		}
+	);
+}
diff --git a/xp/navigator/sha256.js b/xp/navigator/sha256.js
new file mode 100644
index 0000000..5ce6b7e
--- /dev/null
+++ b/xp/navigator/sha256.js
@@ -0,0 +1,173 @@
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+/* SHA-256 (FIPS 180-4) implementation in JavaScript                  (c) Chris Veness 2002-2017  */
+/*                                                                                   MIT Licence  */
+/* www.movable-type.co.uk/scripts/sha256.html                                                     */
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+
+'use strict';
+
+
+/**
+ * SHA-256 hash function reference implementation.
+ *
+ * This is an annotated direct implementation of FIPS 180-4, without any optimisations. It is
+ * intended to aid understanding of the algorithm rather than for production use.
+ *
+ * While it could be used where performance is not critical, I would recommend using the ‘Web
+ * Cryptography API’ (developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) for the browser,
+ * or the ‘crypto’ library (nodejs.org/api/crypto.html#crypto_class_hash) in Node.js.
+ *
+ * See csrc.nist.gov/groups/ST/toolkit/secure_hashing.html
+ *     csrc.nist.gov/groups/ST/toolkit/examples.html
+ */
+class Sha256 {
+
+    /**
+     * Generates SHA-256 hash of string.
+     *
+     * @param   {string} msg - (Unicode) string to be hashed.
+     * @param   {Object} [options]
+     * @param   {string} [options.msgFormat=string] - Message format: 'string' for JavaScript string
+     *   (gets converted to UTF-8 for hashing); 'hex-bytes' for string of hex bytes ('616263' ≡ 'abc') .
+     * @param   {string} [options.outFormat=hex] - Output format: 'hex' for string of contiguous
+     *   hex bytes; 'hex-w' for grouping hex bytes into groups of (4 byte / 8 character) words.
+     * @returns {string} Hash of msg as hex character string.
+     */
+    static hash(msg, options) {
+        const defaults = { msgFormat: 'string', outFormat: 'hex' };
+        const opt = Object.assign(defaults, options);
+
+        // note use throughout this routine of 'n >>> 0' to coerce Number 'n' to unsigned 32-bit integer
+
+        switch (opt.msgFormat) {
+            default: // default is to convert string to UTF-8, as SHA only deals with byte-streams
+            case 'string':   msg = utf8Encode(msg);       break;
+            case 'hex-bytes':msg = hexBytesToString(msg); break; // mostly for running tests
+        }
+
+        // constants [§4.2.2]
+        const K = [
+            0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+            0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+            0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+            0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+            0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+            0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+            0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ];
+
+        // initial hash value [§5.3.3]
+        const H = [
+            0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ];
+
+        // PREPROCESSING [§6.2.1]
+
+        msg += String.fromCharCode(0x80);  // add trailing '1' bit (+ 0's padding) to string [§5.1.1]
+
+        // convert string msg into 512-bit blocks (array of 16 32-bit integers) [§5.2.1]
+        const l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length
+        const N = Math.ceil(l/16);  // number of 16-integer (512-bit) blocks required to hold 'l' ints
+        const M = new Array(N);     // message M is N×16 array of 32-bit integers
+
+        for (let i=0; i<N; i++) {
+            M[i] = new Array(16);
+            for (let j=0; j<16; j++) { // encode 4 chars per integer (64 per block), big-endian encoding
+                M[i][j] = (msg.charCodeAt(i*64+j*4+0)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16)
+                        | (msg.charCodeAt(i*64+j*4+2)<< 8) | (msg.charCodeAt(i*64+j*4+3)<< 0);
+            } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0
+        }
+        // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]
+        // note: most significant word would be (len-1)*8 >>> 32, but since JS converts
+        // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
+        const lenHi = ((msg.length-1)*8) / Math.pow(2, 32);
+        const lenLo = ((msg.length-1)*8) >>> 0;
+        M[N-1][14] = Math.floor(lenHi);
+        M[N-1][15] = lenLo;
+
+
+        // HASH COMPUTATION [§6.2.2]
+
+        for (let i=0; i<N; i++) {
+            const W = new Array(64);
+
+            // 1 - prepare message schedule 'W'
+            for (let t=0;  t<16; t++) W[t] = M[i][t];
+            for (let t=16; t<64; t++) {
+                W[t] = (Sha256.σ1(W[t-2]) + W[t-7] + Sha256.σ0(W[t-15]) + W[t-16]) >>> 0;
+            }
+
+            // 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value
+            let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
+
+            // 3 - main loop (note '>>> 0' for 'addition modulo 2^32')
+            for (let t=0; t<64; t++) {
+                const T1 = h + Sha256.Σ1(e) + Sha256.Ch(e, f, g) + K[t] + W[t];
+                const T2 =     Sha256.Σ0(a) + Sha256.Maj(a, b, c);
+                h = g;
+                g = f;
+                f = e;
+                e = (d + T1) >>> 0;
+                d = c;
+                c = b;
+                b = a;
+                a = (T1 + T2) >>> 0;
+            }
+
+            // 4 - compute the new intermediate hash value (note '>>> 0' for 'addition modulo 2^32')
+            H[0] = (H[0]+a) >>> 0;
+            H[1] = (H[1]+b) >>> 0;
+            H[2] = (H[2]+c) >>> 0;
+            H[3] = (H[3]+d) >>> 0;
+            H[4] = (H[4]+e) >>> 0;
+            H[5] = (H[5]+f) >>> 0;
+            H[6] = (H[6]+g) >>> 0;
+            H[7] = (H[7]+h) >>> 0;
+        }
+
+        // convert H0..H7 to hex strings (with leading zeros)
+        for (let h=0; h<H.length; h++) H[h] = ('00000000'+H[h].toString(16)).slice(-8);
+
+        // concatenate H0..H7, with separator if required
+        const separator = opt.outFormat=='hex-w' ? ' ' : '';
+
+        return H.join(separator);
+
+        /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+
+        function utf8Encode(str) {
+            try {
+                return new TextEncoder().encode(str, 'utf-8').reduce((prev, curr) => prev + String.fromCharCode(curr), '');
+            } catch (e) { // no TextEncoder available?
+                return unescape(encodeURIComponent(str)); // monsur.hossa.in/2012/07/20/utf-8-in-javascript.html
+            }
+        }
+
+        function hexBytesToString(hexStr) { // convert string of hex numbers to a string of chars (eg '616263' -> 'abc').
+            const str = hexStr.replace(' ', ''); // allow space-separated groups
+            return str=='' ? '' : str.match(/.{2}/g).map(byte => String.fromCharCode(parseInt(byte, 16))).join('');
+        }
+    }
+
+
+
+    /**
+     * Rotates right (circular right shift) value x by n positions [§3.2.4].
+     * @private
+     */
+    static ROTR(n, x) {
+        return (x >>> n) | (x << (32-n));
+    }
+
+
+    /**
+     * Logical functions [§4.1.2].
+     * @private
+     */
+    static Σ0(x) { return Sha256.ROTR(2,  x) ^ Sha256.ROTR(13, x) ^ Sha256.ROTR(22, x); }
+    static Σ1(x) { return Sha256.ROTR(6,  x) ^ Sha256.ROTR(11, x) ^ Sha256.ROTR(25, x); }
+    static σ0(x) { return Sha256.ROTR(7,  x) ^ Sha256.ROTR(18, x) ^ (x>>>3);  }
+    static σ1(x) { return Sha256.ROTR(17, x) ^ Sha256.ROTR(19, x) ^ (x>>>10); }
+    static Ch(x, y, z)  { return (x & y) ^ (~x & z); }          // 'choice'
+    static Maj(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); } // 'majority'
+
+}
\ No newline at end of file
diff --git a/xp/navigator/shit.php b/xp/navigator/shit.php
new file mode 100644
index 0000000..8602a94
--- /dev/null
+++ b/xp/navigator/shit.php
@@ -0,0 +1,38 @@
+<?php
+	function do_login($pdo,$o)
+	{
+		$usrname=$o['username'];
+		$passwd=$o['passwd'];
+		$sessname=$o['sessionname'];
+		$qr=$pdo->query(sprintf('select passwd from navigator_user where username=\'%s\'',$usrname));
+		if($qr->rowCount()!=1){return array('result'=>1);}
+		if($passwd!=$qr->fetchColumn(0)){return array('result'=>1);}
+		$token=uniqid();
+		$pdo->query(sprintf('insert into navigator_session values(\'%s\',\'%s\',\'%s\',%d)',
+		$usrname,$passwd,$sessname,time()));
+		$pdo->commit();
+		{return array('result'=>0,'token'=>$token);}
+	}
+	$pdo=new PDO('mysql:host=localhost;dbname=chrisoft','chrisoft',null);
+	$o=json_decode(file_get_contents('php://stdin'));
+	$r=null;
+	switch($o['op'])
+	{
+		case 0:
+			$r=do_login($pdo,$o);
+		break;
+		case 1:
+			//get bookmarks
+		break;
+		case 2:
+			//set bookmarks
+		break;
+		case 3:
+			//list sessions
+		break;
+		case 4:
+			//remove session
+		break;
+	}
+	echo json_encode($r);
+?>
-- 
cgit v1.2.3