<!--- (c) Copyright 2019-2021, James Stevens ... see LICENSE for details --->
<!--- Alternative license arrangements are possible, contact me for more information --->
<html>
<head>
	<title>PowerDNS WebUI - Login</title>
</head>
<script language="Javascript">
	const debugAPI = false;
</script>


<style type='text/css'>

.fullvis { font-family:Verdana,Arial,Helvetica,sans-serif; font-size:14pt; visibility: visible; opacity: 1; text-align: center; }
.fadein  { font-family:Verdana,Arial,Helvetica,sans-serif; font-size:14pt; visibility: visible; opacity: 1; transition: opacity 0.5s linear; }
.fadeout { font-family:Verdana,Arial,Helvetica,sans-serif; font-size:14pt; visibility: hidden; opacity: 0; transition: visibility 0s 1s, opacity 1s linear; }

#topSpan{
	background-color: #666688;
	height: 55px;
	line-height: 55px;
	margin-top: 0px;
	padding-left: 0px;
	padding-top: 1px;
	position: -webkit-sticky;
	position: sticky;
	top: 0px;
	}

html {
	color: white;
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:10pt;
	background-color:#666688;
	scrollbar-base-color:#666699;
	scrollbar-arrow-color:#ddddff;
	}

th {
	color: black;
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:10pt;
	background-color:#a0a0cc;
	font-weight: bold;
	padding-left: 6px;
	padding-right: 6px;
	}

select {
	color: black;
	font-family: Verdana,Arial,Helvetica,sans-serif;
	font-size: 8pt;
	}

input {
	color: black;
	background-color: white;
	font-family: Verdana,Arial,Helvetica,sans-serif;
	font-size: 8pt;
	}

.inputBad {
	color: black;
	background-color: #ffd0d0;
	font-family: Verdana,Arial,Helvetica,sans-serif;
	font-size: 8pt;
	}

body {
	color: white;
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:10pt;
	margin-top: 0px;
	background-color:#666688;
	scrollbar-base-color:#666699;
	scrollbar-arrow-color:#ddddff;
	}


a {
	color: #b0b0ff;
	text-decoration:none;
	}

a:hover {
	text-decoration: underline;
	}

form {
	margin:0px;
	}

.myBtn {
	text-align: center;
	display: inline-block;
	background-color: #d0d0ff;
	color: black;
	font-family: Verdana,Arial,Helvetica,sans-serif;
	font-size: 10pt;
	font-weight: bold;
	border-radius: 25px;
	padding: 5px;
	padding-left: 10px;
	padding-right: 10px;
	cursor: pointer;
	margin-top: 0px;
	margin-left: 10px;
	margin-bottom: 2px;
	margin-right: 2px;
	}

.myBtn:hover {
	box-shadow: 3px 3px #222244;
	text-shadow: 1px 1px #ffffff;
	}

.myBtn:active {
	position: relative;
	box-shadow: none;
	text-shadow: none;
	top: 1px;
	left: 1px;
	}

.dnskey {
	color: #383855;
	}

td {
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:10pt;
	vertical-align: top;
	padding-left: 7px;
	padding-right: 7px;
	white-space: nowrap;
	}

.dataCell {
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:10pt;
	vertical-align: top;
	padding-left: 7px;
	padding-right: 7px;
	white-space: normal;
	min-width: 50%;
	overflow-wrap: anywhere;
	}

.dataRow {
	font-family: Verdana,Arial,Helvetica,sans-serif;
	font-size: 10pt;
	cursor: pointer;
	color: white;
	background-color: #666688;
	}

.dataRow:hover {
	background-color: #444466;
	}


.formPrompt {
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:10pt;
	color: white;
	text-align: right;
	}


.msgPop {
	position: absolute;
	left: 10px;
	top: 20px;
	font-family:Verdana,Arial,Helvetica,sans-serif;
	font-size:16pt;
	width: 95%;
	background: #ffd0d0;
	text-align: center;
	color: black;
	padding: 5px;
	border-radius: 10px;
	}

.msgPopNo {
	visibility: hidden;
	opacity: 0; transition: visibility 0s 1s, opacity 1s linear;
	}

.msgPopYes {
	visibility: visible;
	opacity: 1; transition: opacity 0.5s linear;
	}

</style>

<script language="Javascript">

console.clear();

var gbl = {
	server: null,
	apikey: null,
	with_https: true,
	fast_zone_list: false,
	zone_list: "zones",
	default_ttl: 86400,
	state: { state: "login" },

	tick: "&#x2611;",
	cross: "&#x2612;",
	timer: "&#x23f3;",
	bin: "&#x2702;",
	warn: "&#x26a0;",
	page: "&#x1F5CE;",
	nsec: "&#x1F512;",
	nsec3: "&#x1F510;",
	clip: "&#x1F4CE;",
	};

var ctx = { }; // will store the context of what the user is up to


//=============================================================================================
// Some system constants
//=============================================================================================

// add a trailing '.' for these record types, if the user forgets
const autoAddDot = { rrMX: true, rrNS: true, rrCNAME: true, };

const pdnsCookies = { "pdns_fast_zone_list":1, "pdns_with_https":1,"pdns_server":1 };


// regex for an FQDN hostname
const fqdnCheck = /^(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_])\.)*(xn--|)[A-Za-z0-9\-]+[.]$/;

// regex for adding a host name
const hostnameCheck = /^([a-zA-Z0-9_*]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_])(\.([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_]))*$/


// regex's for RR validation - just add more here and they will work
const validations = {
	rrAAAA: /^(([0-9A-F]{1,4}:){7,7}[0-9A-F]{1,4}|([0-9A-F]{1,4}:){1,7}:|([0-9A-F]{1,4}:){1,6}:[0-9A-F]{1,4}|([0-9A-F]{1,4}:){1,5}(:[0-9A-F]{1,4}){1,2}|([0-9A-F]{1,4}:){1,4}(:[0-9A-F]{1,4}){1,3}|([0-9A-F]{1,4}:){1,3}(:[0-9A-F]{1,4}){1,4}|([0-9A-F]{1,4}:){1,2}(:[0-9A-F]{1,4}){1,5}|[0-9A-F]{1,4}:((:[0-9A-F]{1,4}){1,6})|:((:[0-9A-F]{1,4}){1,7}|:)|fe80:(:[0-9A-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9A-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/i,
	   rrA: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
	  rrMX: /^[1-9][0-9]{0,5} (([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_])\.)+([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\-_]*[A-Za-z0-9_])[.]$/,
	  rrNS: fqdnCheck,
   rrCNAME: fqdnCheck,
 rrCATALOG: fqdnCheck,
	  rrDS: /^(([123456]?[0-9]{1,4}|[0-9]1,4) ([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]) [1234] [0-9A-F ]{40,100})$/i,
	};


// validation regexp for metadata fields
const trueOrFalse = /^[01]$/;
const validateMeta = {
	"API-RECTIFY": trueOrFalse,
	"IXFR": trueOrFalse,
	"NSEC3NARROW": trueOrFalse,
	"PRESIGNED": trueOrFalse,
	"PUBLISH-CDNSKEY": trueOrFalse,
	"SLAVE-RENOTIFY": trueOrFalse,
	"SOA-EDIT": /^(INCREMENT-WEEKS|INCEPTION-EPOCH|INCEPTION-INCREMENT|INCEPTION)$/i,
	"SOA-EDIT-API": /^(DEFAULT|INCREASE|EPOCH|SOA-EDIT|SOA-EDIT-INCREASE)$/i,
	"SOA-EDIT-DNSUPDATE": /^(DEFAULT|INCREASE|EPOCH|SOA-EDIT|SOA-EDIT-INCREASE)$/i,
	};


// Descriptions for "DS" record algorythm numbers from IANA
const dsAlg = [ "Reserved","SHA1","SHA256","GOST","SHA384"];


// drop downs for metadata items
const metaPickLists = {
	"PUBLISH-CDNSKEY" : { "Enabled":1, "Disabled":0 },
	"API-RECTIFY" : { "Enabled":1, "Disabled":0 },
	"PRESIGNED" : { "Enabled":1, "Disabled":0 },
	"NSEC3NARROW" : { "Enabled":1, "Disabled":0 },
	"IXFR" : { "Enabled":1, "Disabled":0 },
	"eSLAVE-RENOTIFY" : { "Enabled":1, "Disabled":0 },
	"SOA-EDIT" : [ "INCREMENT-WEEKS","INCEPTION-EPOCH","INCEPTION-INCREMENT","INCEPTION" ],
	"SOA-EDIT-API": [ "DEFAULT","INCREASE","EPOCH","SOA-EDIT","SOA-EDIT-INCREASE" ],
	"SOA-EDIT-DNSUPDATE": [ "DEFAULT","INCREASE","EPOCH","SOA-EDIT","SOA-EDIT-INCREASE" ],
	};


// these appear as metadata items but are handled as zone properties - lol
const blockedMeta = {
	"SOA-EDIT": { name: "soa_edit", list: false, delete: false },
	"SOA-EDIT-API": { name: "soa_edit_api", list: false, delete: true },
	"NSEC3PARAM": { name: "nsec3param", list: false, delete: true },
	"TSIG-ALLOW-AXFR": { name: "master_tsig_key_ids", list: true, delete: true },
	"AXFR-MASTER-TSIG": { name: "slave_tsig_key_ids", list: true, delete: true },
	};


// All Meta Data Items
const allMetaItems = [
	'ALLOW-AXFR-FROM',
	'ALLOW-DNSUPDATE-FROM',
	'ALSO-NOTIFY',
	'AXFR-MASTER-TSIG',
	'AXFR-SOURCE',
	'FORWARD-DNSUPDATE',
	'GSS-ACCEPTOR-PRINCIPAL',
	'GSS-ALLOW-AXFR-PRINCIPAL',
	'IXFR',
	'LUA-AXFR-SCRIPT',
	'NOTIFY-DNSUPDATE',
	'NSEC3NARROW',
	'NSEC3PARAM',
	'PRESIGNED',
	'PUBLISH-CDNSKEY',
	'PUBLISH-CDS',
	'SOA-EDIT',
	'SOA-EDIT-API',
	'SOA-EDIT-DNSUPDATE',
	'TSIG-ALLOW-AXFR',
	'TSIG-ALLOW-DNSUPDATE',
	];

//=============================================================================================
// Some library functions
//=============================================================================================

function callApi(sfx,callback,inData)
{
	document.body.style.cursor="progress";

	if (debugAPI) {
		console.log("API>>>",sfx);
		console.log("API>>>",inData);
		}

	gbl.forms.forEach(i => i.style.display = "none");

	if ((inData != null)&&(inData.method == "PUT"))
		msg(`Requesting ... ${gbl.timer}`);
	else
		msg(`Loading ... ${gbl.timer}`);

	let p = "https";
	if (!gbl.with_https) p = "http";
	let url = `${p}://${gbl.server}/api/v1/servers/localhost/${sfx}`;

	let okResp = 200;
	let httpCmd = {
		headers: { 'X-API-Key': gbl.apikey },
		method: 'GET',
		};

	if (inData != null) {
		httpCmd.method = inData.method;
		httpCmd.body = inData.json;
		if ("okResp" in inData) okResp = inData.okResp;
		}

	fetch(url,httpCmd).then(response => {
		if (debugAPI) console.log("API-Resp>>>",response);
		if (response.status != okResp) {
			response.text().then(
				data => {
					try {
						e = JSON.parse(data);
						errMsg(e.error);
						}
					catch {
						errMsg(`ERROR: ${response.status} ${response.statusText}`)
						}
					},
				() => errMsg(`ERROR: ${response.status} ${response.statusText}`)
				);
			msg("");
			if ((inData != null)&&("callErr" in inData)) return callback(null);
			return;
			}

		response.text().then(data => {
			if ((inData != null)&&(inData.noData)) {
				msg("");
				callback(true);
			} else {
				let param = data;
				try {
					param = JSON.parse(data); }
				catch {
					param = data; }
				msg("");
				callback(param);
				}
			});

		})
		.catch(err => errMsg(`ERROR: Failed to connect to PowerDNS`))

	document.body.style.cursor="auto";
}



// This one function is danallison/downloadString.js
// https://gist.github.com/danallison/3ec9d5314788b337b682

function downloadFile(text, fileType, fileName) {
	var blob = new Blob([text], { type: fileType });

	var a = document.createElement('a');
	a.download = fileName;
	a.href = URL.createObjectURL(blob);
	a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
	a.style.display = "none";
	document.body.appendChild(a);
	a.click();
	document.body.removeChild(a);
	setTimeout(() => URL.revokeObjectURL(a.href), 1500);
	msg("");
}

// end of -> danallison/downloadString.js



function setState(title,state)
{
	gbl.state = state;
	if (!("param" in gbl)) window.history.pushState(state,title,gbl.pathname);
	document.title = title;
	delete gbl.param;
}


function nameToCatalog(name) {
	/**
	* Secure Hash Algorithm (SHA1)
	* http://www.webtoolkit.info/
	**/
	function SHA1(in_msg,msg_len) {

		function rotate_left(n,s) {
			 var t4 = ( n<<s ) | (n>>>(32-s));
			 return t4;
			 };

		function cvt_hex(val) {
			 var str='';
			 var i;
			 var v;
				 for( i=7; i>=0; i-- ) {
				 v = (val>>>(i*4))&0x0f;
				 str += v.toString(16);
				 }
			 return str;
			 };

		var blockstart;
		var i, j;
		var W = new Array(80);
		var H0 = 0x67452301;
		var H1 = 0xEFCDAB89;
		var H2 = 0x98BADCFE;
		var H3 = 0x10325476;
		var H4 = 0xC3D2E1F0;
		var A, B, C, D, E;
		var temp;

		let msg = new Uint8Array(in_msg);

		var word_array = new Array();

		for( i=0; i<msg_len-3; i+=4 ) {
			j = msg[i]<<24 | msg[i+1]<<16 | msg[i+2]<<8 | msg[i+3];
			word_array.push( j );
			}

		switch( msg_len % 4 ) {
		 case 0:
			 i = 0x080000000;
			 break;
		 case 1:
			 i = msg[msg_len-1]<<24 | 0x0800000;
			 break;
		 case 2:
			 i = msg[msg_len-2]<<24 | msg[msg_len-1]<<16 | 0x08000;
			 break;
		 case 3:
			 i = msg[msg_len-3]<<24 | msg[msg_len-2]<<16 | msg[msg_len-1]<<8 | 0x80;
			 break;
		 }
		word_array.push( i );
		while( (word_array.length % 16) != 14 ) word_array.push( 0 );
		word_array.push( msg_len>>>29 );
		word_array.push( (msg_len<<3)&0x0ffffffff );
		for ( blockstart=0; blockstart<word_array.length; blockstart+=16 ) {
			for( i=0; i<16; i++ ) W[i] = word_array[blockstart+i];
			for( i=16; i<=79; i++ ) W[i] = rotate_left(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
			A = H0;
			B = H1;
			C = H2;
			D = H3;
			E = H4;
			for( i= 0; i<=19; i++ ) {
				temp = (rotate_left(A,5) + ((B&C) | (~B&D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
				E = D;
				D = C;
				C = rotate_left(B,30);
				B = A;
				A = temp;
				}
			for( i=20; i<=39; i++ ) {
				temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
				E = D;
				D = C;
				C = rotate_left(B,30);
				B = A;
				A = temp;
				}
			for( i=40; i<=59; i++ ) {
				temp = (rotate_left(A,5) + ((B&C) | (B&D) | (C&D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
				E = D;
				D = C;
				C = rotate_left(B,30);
				B = A;
				A = temp;
				}
			for( i=60; i<=79; i++ ) {
				temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
				E = D;
				D = C;
				C = rotate_left(B,30);
				B = A;
				A = temp;
				}
			H0 = (H0 + A) & 0x0ffffffff;
			H1 = (H1 + B) & 0x0ffffffff;
			H2 = (H2 + C) & 0x0ffffffff;
			H3 = (H3 + D) & 0x0ffffffff;
			H4 = (H4 + E) & 0x0ffffffff;
			}
		var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);

		return temp.toLowerCase();
		}

	let buffer = new ArrayBuffer(name.length+2);
	let msg = new Uint8Array(buffer);

	let last_len = 99;
	let idx = 0;

	for(let frag of name.split(".")) {
		msg[idx++] = frag.length;
		for(let i=0;i<frag.length;i++) msg[idx++] = frag[i].toLowerCase().charCodeAt(0);
		last_len = frag.length
		}

	if (last_len > 0) msg[idx++] = 0;
	return SHA1(msg,idx);
}



//=============================================================================================
// Stats handling
//=============================================================================================

function loadStats()
{
	callApi("statistics",showStats);
}


function showOneStats(data)
{
	let item = null;
	for(let i of data) if (i.name==this) item = i;
	if (item == null) {
		errMsg(`Could not find stats item '${this}'`);
		return loadStats();
		}

	ctx.stats = data;
	setState(`${gbl.server}: Stats: ${item.name}`, { state: "stats", server: gbl.server, stats: item.name });

	let x = "<table width=100% border=0 cellspacing=1 cellpadding=0><tr>";
	for (let i in item.value[0]) x += `<th>${i}</th>`;
	x += "</tr>";

	for(r in item.value) {
		x += "<tr class=dataRow>";
		for(i in item.value[r])
			x += `<td>${item.value[r][i]}</td>`;
		x += "</tr>";
		}

	x += "</table>";
	gbl.bot.innerHTML = x;
	topLine();
}



function oneStat(item)
{
	callApi('statistics',showOneStats.bind(item));
}



function showStats(data)
{
	data.sort((a,b) => {
		if (a.name < b.name) return -1;
		if (a.name > b.name) return 1;
		return 0;
		});

	ctx.stats = data;

	setState(`${gbl.server}: Stats`, { state: "stats", server: gbl.server });

	let x = "<table width=100% border=0 cellspacing=1 cellpadding=0>";
	data.forEach(i => {
		let clk=`onClick="oneStat('${i.name}');"`;
		if (typeof(i.value) != "object") clk="";
		x += `<tr ${clk} class=dataRow><td>${i.name}</td><td>${i.type}</td>`;
		if (typeof(i.value) != "object") x += `<td>${i.value}</td></tr>`;
		else {
			if (i.value.length > 0) x += "<td>[Click to view]</td>";
			else x += "<td></td>";
			}
		});
	x += "</table>";
	gbl.bot.innerHTML = x;
	topLine()
}


//=============================================================================================
// Zone META data handling
//=============================================================================================


function doEditMeta(isDel)
{
	let f = document.newMetaForm;
	let dataIdx = parseInt(f.dataIdx.value);
	let itemIdx = parseInt(f.itemIdx.value);
	let metaValue = document.newMetaForm.metaValue.value;

	let i = ctx.zoneMeta[dataIdx];

	if ((i.kind in validateMeta)&&(!(validateMeta[i.kind].test(metaValue)))) {
		errMsg(`Invalid value for ${i.kind}`);
		document.newMetaForm.metaValue.focus();
		document.newMetaForm.metaValue.select();
		return false;
		}

	if (isDel) {
		metaValue = "";
		i.metadata.splice(itemIdx,1);
		}
	else {
		i.metadata[itemIdx] = metaValue;
		i.metadata.sort((a,b) => {
			if (a < b) return -1;
			if (b < a) return 1;
			return 0;
			});
		}

	if (i.kind in blockedMeta) {
		let json = {}
		json[blockedMeta[i.kind].name] = metaValue;
		if (blockedMeta[i.kind].list) json[blockedMeta[i.kind].name] = i.metadata;

		callApi(`zones/${ctx.zone}`, zoneMeta, { method: "PUT", okResp: 204, json: JSON.stringify(json) } );
		}
	else {
		let hdr = { method: "PUT", okResp: 200, json: JSON.stringify({ metadata: i.metadata }) };
		callApi(`zones/${ctx.zone}/metadata/${i.kind}`, zoneMeta, hdr);
		}

	return false; // prevent form submit
}



function editMetaItem(dataIdx,itemIdx)
{
	let metaItem = ctx.zoneMeta[dataIdx].kind;
	let metaValue = ctx.zoneMeta[dataIdx].metadata[itemIdx];
	clickAddMeta(metaItem.toUpperCase(),metaValue,dataIdx,itemIdx);
}



function formAddMeta()
{
	let f = document.newMetaForm;
	let metaItem = f.metaItem.value;
	let metaValue = [ f.metaValue.value ];

	let hdr = { method: "POST", okResp: 201, callErr: true };
	let url = `zones/${ctx.zone}/metadata`;

	for(let i of ctx.zoneMeta)
		if (i.kind.toUpperCase() == metaItem) {
			if (i.metadata.includes(f.metaValue.value)) return zoneMeta();
			metaValue = i.metadata;
			metaValue.push(f.metaValue.value);
			hdr.method = "PUT";
			hdr.okResp = 200;
			url = `${url}/${metaItem}`;
			break;
			}

	let nxtFn = function(res) { if (res == null) clickAddMeta(metaItem.toUpperCase(),metaValue); else zoneMeta(); };
	hdr.json = JSON.stringify( { type: "Metadata", kind:metaItem, metadata: metaValue } );

	if (!(metaItem in blockedMeta)) callApi(url, nxtFn, hdr);
	else {
		let json = {}

		json[blockedMeta[metaItem].name] = f.metaValue.value;
		if (blockedMeta[metaItem].list) json[blockedMeta[metaItem].name] = metaValue;

		callApi(`zones/${ctx.zone}`, nxtFn,
			{ method: "PUT", callErr: true, okResp: 204, json: JSON.stringify(json) } );
		}

	return false;
}



function showDelete(item,val) {
	if (!(item in blockedMeta)) return (val != null);
	if (blockedMeta[item].delete) return (val != null);
	return false;
}



function clickAddMeta(item,val,dataIdx,itemIdx)
{
	var formCall = "formAddMeta();"
	if (dataIdx != null) formCall = "doEditMeta(false);"

	x = `<form method=post action="/none.cgi" name=newMetaForm onSubmit="return ${formCall}">`

	if (dataIdx != null) x += `<input type=hidden name=dataIdx value=${dataIdx}>`
	if (itemIdx != null) x += `<input type=hidden name=itemIdx value=${itemIdx}>`

	x += '<table width=40% align=center>'
	x += '<colgroup><col width=35%/><col width=65%/></colgroup>';
	let addBtn = "Change";

	if (val == null) {
		addBtn = "Add Metadata";
		x += "<tr><td colspan=2 align=center><h2>Add a New Metadata Item</h2></td></tr>"
			+ "<tr><td class=formPrompt>Metadata: </td><td><select onChange='clickAddMeta(this.value.toUpperCase());' name=metaItem>";
		allMetaItems.forEach(i => { c=""; if (i==item) c="selected"; x += `<option ${c}>${i}` });
		x += '</select></td></tr>';
		}
	else {
		x += `<tr><td colspan=2 align=center><h2>Edit a Metadata Item</h2></td></tr>`
			+ `<tr><td class=formPrompt>Metadata Item: </td><td>${item}</td></tr>`
			+ `<input type=hidden name=metaItem value="${item}">`;
		}

	x += '<tr><Td class=formPrompt>Value: </td><td>';

	if (item in metaPickLists) {
		x += "<select name=metaValue>";
		if (Array.isArray(metaPickLists[item]))
			metaPickLists[item].forEach(i => x += `<option>${i}`);
		else
			for(let i in metaPickLists[item]) x += `<option value=${metaPickLists[item][i]}>${i}`;
		x += "</select>";
		}
	else
		x += `<input size=50 name=metaValue></td></tr>`;

	x += '<tr><td colspan=2><hr></td></tr>'
		+ '<tr><td align=center colspan=2>'
		+ '<span class=myBtn title="Cancel adding Metadata" onClick="zoneMeta();">Cancel</span>'

	if (showDelete(item,val))
		x += '<span title="Delete this Metadata Item" class=myBtn onClick="doEditMeta(true);">Delete</span>'

	x += `<span title="Add this Metadata Record" class=myBtn onClick="${formCall}">${addBtn}</span></td></tr>`
		+ '<input type=submit style="display: none;">'
		+ '</table></form>';

	gbl.bot.innerHTML = x;

	if (val != null) document.newMetaForm.metaValue.value = val;
	else {
		if (item in metaPickLists)
			document.newMetaForm.metaValue.selectedIndex = 0;
		else
			document.newMetaForm.metaValue.value = "";
		}

	document.newMetaForm.metaValue.focus();
}



function zoneMeta()
{
	ctx.meta = true; topLine();
	callApi(`zones/${ctx.zone}/metadata`,zoneMetaDetails);
	return false;
}



function zoneMetaDetails(data)
{
	data.sort((a,b) => {
		if (a.kind < b.kind) return -1;
		if (a.kind > b.kind) return 1;
		return 0;
		});

	for(let i of data)
		i.metadata.sort((a,b) => {
			if (a < b) return -1;
			if (b < a) return 1;
			return 0;
			});

	setState(`${gbl.server}: ${ctx.zone}, META Data`,
		{ state: "meta", server: gbl.server, zone: ctx.zone, meta:true });
	ctx.zoneMeta = data;

	let x = "<table align=center width=60% border=0 cellspacing=1 cellpadding=0>";
	for(let i in data) {
		for(let y in data[i].metadata) {
			let td = `<td onClick="editMetaItem(${i},${y});">`;
			x += `<tr class=dataRow>${td}${data[i].kind.toUpperCase()}</td>`;
			x += `${td}${data[i].metadata[y]}</td></tr>`;
			}
		}

	x += "</table>";
	gbl.bot.innerHTML = x;
	topLine();
}



//=============================================================================================
// Zone Details Handling
//=============================================================================================


function addNewRR()
{
	ctx.rec.rrs.records.push({ content:"", disabled: false });
	drawOneRecord();
	clickData(ctx.rec.rrs.records.length-1);
}



function madeChange(res)
{
	if (res)
		loadThisZone();
	else
		errMsg("Update Failed");
}



function patchRRdata()
{
	return JSON.stringify({
		rrsets: [
			{
			name: ctx.rec.name,
			ttl: ctx.rec.rrs.ttl,
			type: ctx.rec.type,
			changetype: "REPLACE",
			records : ctx.rec.rrs.records,
			}
		]
		});
}



function saveRRChanges(withSave)
{
	delete ctx.edit;
	topLine();

	if (withSave)
		callApi(`zones/${ctx.zone}`,madeChange,
			{
			method: "PATCH",
			noData: true,
			okResp: 204,
			json: patchRRdata()
			}
		 );
	else
		getZoneRecord();
}



function changeRR()
{
	let newVal = document.rrForm.new.value;

	if (newVal == "") {
		if (ctx.rec.rrs.records[ctx.edit.dta].content != "") ctx.needSave = "saveRRChanges";
		ctx.rec.rrs.records.splice(ctx.edit.dta,1);
		drawOneRecord();
		}
	else {
		if ((ctx.rec.type == "TXT")&&(newVal.substr(0,1)!='"')&&(newVal.substr(newVal.length-1)!='"'))
			newVal = `"${newVal}"`;

		s = document.getElementById(`rrs${ctx.edit.dta}`);
		if (newVal != ctx.rec.rrs.records[ctx.edit.dta].content) {

			idx = `rr${ctx.rec.type}`;
			if ((idx in autoAddDot)&&(newVal.substr(newVal.length-1)!=".")) newVal += ".";

			if (idx in validations) {
				if (!validations[idx].test(newVal)) {
					errMsg("Invalid value");
					document.rrForm.new.focus();
					document.rrForm.new.select();
					return false;
					}
				}

			ctx.rec.rrs.records[ctx.edit.dta].content = newVal;
			s.innerHTML = newVal;
			ctx.needSave = "saveRRChanges";
			}
		else
			s.innerHTML = ctx.rec.rrs.records[ctx.edit.dta].content;
		}

	delete ctx.edit;
	topLine();

	return false; // cancel the form submit
}



function changeTTL()
{
	let newVal = document.rrForm.new.value;

	if (!validInt(newVal)) {
		errMsg("Invalid TTL");
		document.rrForm.new.focus();
		document.rrForm.new.select();
		return false;
		}

	if (newVal != ctx.rec.rrs.ttl) {
		ctx.needSave = "saveRRChanges";
		ctx.rec.rrs.ttl = newVal;
		drawOneRecord();
		}

	delete ctx.edit;
	topLine();

	return false; // cancel the form submit
}



function clickTTL(idx)
{
	if ("edit" in ctx) { if (("ttl" in ctx.edit)&&(ctx.edit.ttl == idx)) return; else cancelAllEdits(); }

	let s = document.getElementById(`ttl${idx}`);
	if (s.innerHTML.indexOf("<form") >= 0) return;

	ctx.edit = { ttl: idx, };

	let x = "<form onSubmit='return changeTTL();' name=rrForm>"
		+ `<input onkeydown="keyEsc(event);" size=10 name=new value="${ctx.rec.rrs.ttl}"></form>`;

	s.innerHTML = x;
	document.rrForm.new.focus();
	topLine();
}



function clickData(idx)
{
	if ("edit" in ctx) { if (("dta" in ctx.edit)&&(ctx.edit.dta == idx)) return; else cancelAllEdits(); }

	let s = document.getElementById(`rrs${idx}`);
	if (s.innerHTML.indexOf("<form") >= 0) return;

	let len = 50
	if ((ctx.rec.type=="SOA")||(ctx.rec.type=="TXT")) len=100;

	let x = "<form onSubmit='return changeRR();' name=rrForm>"
		+ `<input onkeydown="keyEsc(event);" size=${len} name=new`
		+ ` value='${ctx.rec.rrs.records[idx].content.replace(/'/g,"&apos;")}'></form>`;

	ctx.edit = { dta: idx };

	topLine();
	s.innerHTML = x;
	document.rrForm.new.focus();
}



function findZoneName(name)
{
	let ret = "";
	for(let n of ctx.zoneList) {
		if (
			(name.length >= n.name.length) &&
			(n.name.length > ret.length) &&
			((name==n.name)||(name.substr(name.length - n.name.length)==n.name))
			)
			ret = n.name;
		}
	return ret;
}



function changeName()
{
	let z = null;
	let name = document.rrForm.new.value;
	if (name.substr(name.length-1)!=".") {
		let t = findZoneName(`${name}.`);
		if (t != null) {
			z = t;
			name += ".";
			}
		else {
			name = `${name}.${ctx.zone}`;
			z = ctx.zone;
			}
		}
	else
		z = findZoneName(name);

	if (z != "") ctx.zone = z;
	else {
		errMsg("No matching zone");
		document.rrForm.new.focus();
		return false;
		}

	if (ctx.rec.name != name) {
		ctx.rec.name = name;
		ctx.rec.rrs.name = name;
		ctx.needSave = "saveRRChanges";
		}

	delete ctx.edit;
	topLine();
	drawOneRecord();

	return false; // block form submit
}



function clickRecName(idx)
{
	if ("edit" in ctx) { if (("nme" in ctx.edit)&&(ctx.edit.nme == idx)) return; else cancelAllEdits(); }

	let x = "<form onSubmit='return changeName();' name=rrForm>"
		+ `<input onkeydown="keyEsc(event);" size=50 name=new value='${ctx.rec.name}'></form>`;

	ctx.edit = { nme: idx, };

	let s = document.getElementById(`nme${idx}`);
	topLine();
	s.innerHTML = x;
	document.rrForm.new.focus();
}



function cancelAllEdits()
{
	if (!("edit" in ctx)) return;

	if ("nme" in ctx.edit) {
		let s = document.getElementById(`nme${ctx.edit.nme}`);
		s.innerHTML = ctx.rec.name;
		}

	if ("ttl" in ctx.edit) {
		let s = document.getElementById(`ttl${ctx.edit.ttl}`);
		s.innerHTML = ctx.rec.rrs.ttl;
		}

	if ("dta" in ctx.edit) {
		if (ctx.rec.rrs.records[ctx.edit.dta].content == "") {
			ctx.rec.rrs.records.pop();

			if ((ctx.rec.loadedRows==0)&&(ctx.rec.rrs.records.length==0))
				return loadThisZone();

			drawOneRecord();
			}
		else {
			let s = document.getElementById(`rrs${ctx.edit.dta}`);
			s.innerHTML = ctx.rec.rrs.records[ctx.edit.dta].content;
			}
		}

	delete ctx.edit;
	topLine();
}



function deleteRRitem(idx)
{
	if ("edit" in ctx) return cancelAllEdits();

	ctx.rec.rrs.records.splice(idx,1);
	ctx.needSave = "saveRRChanges";
	topLine();
	drawOneRecord();
}




function findRec(rrsets,rec)
{
	for(let tag of rrsets)
		if ((tag.name == rec.name)&&(tag.type == rec.type)) return tag;

	return null;
}



function oneRecord(data)
{
	if (data.name != ctx.zone) {
		return errMsg("ERROR: Zone does not match loaded data");
		}

	let rrs = findRec(data.rrsets,ctx.rec);

	if (rrs == null) {
		if ("rrs" in ctx.rec) {
			while ((ctx.rec.rrs.records.length > 0)&&(ctx.rec.rrs.records[ctx.rec.rrs.records.length-1].content == ""))
				ctx.rec.rrs.records.pop();
			}
		return errMsg(`No matching data for ${ctx.rec.name}/${ctx.rec.type}`);
		}

	rrs.records.sort((a,b) => {
		if (a.content < b.content) return -1;
		if (a.content > b.content) return 1;
		return 0;
		})

	ctx.rec.rrs = rrs;
	ctx.rec.loadedRows = rrs.records.length;

	drawOneRecord();
}



function drawOneRecord(rrs)
{
	delete ctx.edit;

	let x = `${gbl.server}: ${ctx.zone}: `;
	if ("showName" in ctx.rec)
		x += ctx.rec.showName;
	else
		x += ctx.rec.name;

	setState(`${x}/${ctx.rec.type}`, { state: "record", server: gbl.server, zone: ctx.zone, name:ctx.rec.name, type: ctx.rec.type });

	x = "<table width=100% border=0 cellspacing=1 cellpadding=0>";
	x += '<colgroup><col width="10%" /><col width="25%" /><col width="99px" /><col /><col width="75%" /></colgroup>'
	for(let idx in ctx.rec.rrs.records) {
		let i = ctx.rec.rrs.records[idx];
		x += "<tr class=dataRow>"
			+ `<td onClick='deleteRRitem(${idx});' align=center>${gbl.bin}</td>`
			+ `<td onClick='clickRecName(${idx});'><span id=nme${idx}>${ctx.rec.name}</span></td>`
			+ `<td align=right onClick='clickTTL(${idx});'><span id=ttl${idx}>${ctx.rec.rrs.ttl}</span></td>`
			+ `<td>${ctx.rec.rrs.type}</td>`
			+ `<td class=dataCell onClick='clickData(${idx});'><span id=rrs${idx}>${i.content}</span></td></tr>`;
		}
	gbl.bot.innerHTML = `${x}</table>`;
	topLine();
}



function clickZoneRecord(name,type)
{
	if (ctx.zoneDetails.kind == "Slave") {
		popMsg("Slave zone, editing is disabled");
		delete gbl.param; return; }

	ctx.rec = {
		name: name,
		type: type,
		};

	getZoneRecord();
}



function getZoneRecord()
{
	delete ctx.edit;
	delete ctx.needSave;

	topLine();
	callApi(`zones/${ctx.zone}`,oneRecord);
}


//=============================================================================================
// DNSSEC Stuff
//=============================================================================================

function showHideBits(formName)
{
	let f = document.forms[formName];
	let k = document.getElementById("key2Lens");
	let c = document.getElementById("key1Lens");

	let s = "table-row-group";
	if ((f.keyAlg.value=="ecdsa256")||(f.keyAlg.value=="ecdsa384")) {
		k.style.display = "none";
		c.style.display = "none";
		}
	else {
		if (formName == "addKeyForm") c.style.display = "table-row-group";
		else {
			if (f.keyPolicy.value == "CSK") {
				c.style.display = "table-row-group";
				k.style.display = "none";
				}
			else {
				k.style.display = "table-row-group";
				c.style.display = "none";
				}
			}
	}
}



function addAlg(formName)
{
	let keyAlg = [
		"ecdsa256","ecdsa384","rsasha1","rsasha1-nsec3-sha1","rsasha256","rsasha512","dh","dsa",
		"dsa-nsec3-sha1","ecc-gost","ed25519","ed448","gost","indirect","rsamd5"
	]
	let keyLens = ["1024","2048","4096"];

	let x = `<tr><Td class=formPrompt>Key Algrythm :</td><td><select onChange='showHideBits("${formName}");' name=keyAlg>`;
	keyAlg.forEach(i => { x += `<option value=${i}>${i.toUpperCase()}` } );
	x += "</select></td></tr>";

	let txt = "CSK";
	if (formName=="addKeyForm") txt = "Key";
	x += "<tbody style='display:none;' id='key1Lens'>"
		+ `<tr><Td class=formPrompt>${txt} Length (bits) :</td><td><select name=cskBits>`;
	keyLens.forEach(i => { x += `<option>${i}`; });
	x += "</select></td></tr></tbody>";

	x += "<tbody style='display:none;' id='key2Lens'>"
	x += "<tr><Td class=formPrompt>KSK Length (bits) :</td><td><select name=kskBits>";
	keyLens.forEach(i => { x += `<option>${i}`; });
	x += "</select></td></tr>";

	x += "<tr><Td class=formPrompt>ZSK Length (bits) :</td><td><select name=zskBits>";
	keyLens.forEach(i => { x += `<option>${i}`; });
	x += "</select></td></tr></tbody>";

	return x;
}



function setNSEC3(delnsec3)
{
	let nsec3 = "1 0 5 ";
	let nxtFn = function() {
		gbl.params = { state: "keys", server: gbl.server, zone: ctx.zone, keys:true };
		gbl.state = gbl.params;
		loadThisZone();
		}

	if (delnsec3 == null) {
		let old = "0000000000";
		let sfx="";

		if (ctx.zoneDetails.nsec3param != "") {
			i = ctx.zoneDetails.nsec3param.split(" ");
			nsec3 = `${i[0]} ${i[1]} ${i[2]}`
			old = i[3];
			}

		while(sfx.length < old.length)
			sfx = sfx + Math.floor(Math.random() * 256).toString(16)

		nsec3 = `${nsec3} ${sfx}`;

		nxtFn = function() {
				ctx.zoneDetails.nsec3param = nsec3;
				callApi(`zones/${ctx.zone}/rectify`, zoneKeys, { method: "PUT" });
				}
		}
	else nsec3 = "";

	callApi(`zones/${ctx.zone}`, nxtFn,
		{ method: "PUT", okResp: 204, json: JSON.stringify({ nsec3param: nsec3 }) } );
}



function doSignZone()
{
	let nextFn = zoneKeys;
	let keyAlg = document.signZoneForm.keyAlg.value;
	let kskBits = parseInt(document.signZoneForm.kskBits.value);
	let zskBits = parseInt(document.signZoneForm.zskBits.value);
	let cskBits = parseInt(document.signZoneForm.cskBits.value);
	let keyPolicy = document.signZoneForm.keyPolicy.value;
	let keyDNSSEC = document.signZoneForm.keyDNSSEC.value;

	if (keyAlg == "ecdsa256") {
		kskBits = 256;
		zskBits = 256;
		cskBits = 256;
		}
	else if (keyAlg == "ecdsa384") {
		kskBits = 384;
		zskBits = 384;
		cskBits = 384;
		}

	if (keyDNSSEC == "NSEC3") { nextFn = function() { setNSEC3(null); }; };

	if (keyPolicy == "CSK") {

		let json = { "keytype": "csk", "active": true, "algorithm": keyAlg, "bits": cskBits, };

		callApi(`zones/${ctx.zone}/cryptokeys`,nextFn,
			{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
		}
	else {
		let json_ksk = { "keytype": "ksk", "active": true, "algorithm": keyAlg, "bits": kskBits, }

		callApi(`zones/${ctx.zone}/cryptokeys`, () => {

				let json_zsk = { "keytype": "zsk", "active": true, "algorithm": keyAlg, "bits": zskBits, }

				callApi(`zones/${ctx.zone}/cryptokeys`,nextFn,
					{ method: "POST", okResp: 201, json: JSON.stringify(json_zsk) } );
				},

			{ method: "POST", okResp: 201, json: JSON.stringify(json_ksk) } );
		}

	return false; // prevent submit
}



function signZone()
{
	let x = "<form onSubmit='return doSignZone();' name=signZoneForm><table align=center>";

	x += "<tr><td><div style='height: 15px;'></div></td></tr>"
		+ `<tr><th colspan=2>DNSSEC Sign: ${ctx.zone}</th></tr>`;

	x += `<tr><td class=formPrompt>Key Policy: </td><td><select onChange="showHideBits('signZoneForm');" name=keyPolicy>`;
	["KSK+ZSK", "CSK"].forEach(i => { x += `<option>${i}` } );
	x += `</select></td></tr>${addAlg("signZoneForm")}`;


	x += "<tr><td class=formPrompt>DNSSEC Policy: </td><td><select name=keyDNSSEC>";
	["NSEC3","NSEC"].forEach(i => { x += `<option>${i}` } );
	x += "</select></td></tr>";

	x += "<tr><td><div style='height: 15px;'></div></td></tr>"
		+ "<tr><td align=center colspan=2>"
		+ btn("loadThisZone()","Cancel",`Do not sign ${ctx.zone}`)
		+ btn("doSignZone()","Sign Zone",`DNSSEC Sign ${ctx.zone}`)
		+ "</td></tr><tr><td><div style='height: 15px;'></div></td></tr>"
		+ "<input type=submit style='display: none;'></form>";

	x += "</table>";

	x += "<P style='height: 25px;'>&nbsp;</p><table border=0 cellspacing=20 align=center>"
		+ "<tr><Td colspan=2>Your friendly paper-clip helper buddy says ....</td></tr>"
		+ `<tr><td><span style='font-size: 65pt'>${gbl.clip}</span></td><td>`
		+ "<P>Unless you have a really good reason, you should probably sign your zone using <b>ECDSA256</b><P>"
		+ "<b>NSEC</b> is more space & bandwidth efficient than <b>NSEC3</b>.<br><b>NSEC3</b> will enlarge your zone by typically twice as much as <b>NSEC</b>, but <b>NSEC</b> exposes all your DNS names in a linked list.<P>"
		+ "<b>CSK</b> uses one key instead of two, but means your zone will double in size during key rollover.<br>With <b>KSK+ZSK</b> you have two keys to roll-over, but you can keep the same zone size during the process."
		+ "</td></tr></table>"

	gbl.bot.innerHTML = x;
}



function clickDS(txt)
{
	let x = txt.split(" ");
	if (x.length < 4) {
		errMsg("Copy to clipboard failed");
		return false;
		}

	navigator.clipboard.writeText(x[x.length-1]).then(
		() => popMsg("Digest copied to clipboard"),
		() => errMsg("Copy to clipboard failed")
		);

	return false;
}



function doAddKey()
{
	let json = {
		"keytype": document.addKeyForm.keyKind.value,
		"active": (document.addKeyForm.keyActive.value=="true"),
		"algorithm": document.addKeyForm.keyAlg.value,
		"bits": parseInt(document.addKeyForm.cskBits.value),
		};

	if (json.algorithm == "ecdsa256") json.bits = 256;
	else if (json.algorithm == "ecdsa384") json.bits = 384;

	callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
		{ method: "POST", okResp: 201, json: JSON.stringify(json) } );

	return false // prevent submit
}



function clickAddKey()
{
	let x = "<form onSubmit='return doAddKey();' name=addKeyForm><table align=center>"
		+ "<tr><td><div style='height: 15px;'></div></td></tr>"
		+ "<tr><th colspan=2>Add a DNSSEC Key</th></tr>"
		+ "<tr><Td class=formPrompt>Key Type :</td><td><select name=keyKind>";

	["ksk","zsk","csk"].forEach(i => { x += `<option value=${i}>${i.toUpperCase()}` } );

	x += `</select></td></tr>${addAlg("addKeyForm")}`;

	x += "<tr><Td class=formPrompt>Key Active :</td><td><select name=keyActive>";
	["true","false"].forEach(i => { x += `<option>${i}`; });
	x += "</select></td></tr>";

	x += "<tr><td><div style='height: 15px;'></div></td></tr>"
		+ "<tr><td align=center colspan=2>"
		+ btn("cancelAddKey()","Cancel",`Do not add a key to the zone ${ctx.zone}`)
		+ btn("doAddKey()","Add a Key",`Add a key to the zone ${ctx.zone}`)
		+ "</td></tr><tr><td><div style='height: 15px;'></div></td></tr>"
		+ "<input type=submit style='display: none;'></form>";

	ctx.dnssec.t.style.display = "none";
	ctx.dnssec.b.innerHTML = x;
}



function cancelAddKey()
{
	ctx.dnssec.b.innerHTML = "";
	ctx.dnssec.t.style.display = "block";
}



function activateZSK(a,b)
{
	callApi(`zones/${ctx.zone}/cryptokeys/${ctx.zoneKeys[a].id}`,
		() => {
			callApi(`zones/${ctx.zone}/cryptokeys/${ctx.zoneKeys[b].id}`,zoneKeys,
				{
				method: "PUT",
				okResp: 204,
				json: '{ "active": true }'
				} );
		}, {
			method: "PUT",
			okResp: 204,
			json: '{ "active": false }'
			}
		);
}



function startZSK(template)
{
	let k = ctx.zoneKeys[template];
	let json = {
		"keytype": "zsk",
		"active": false,
		"algorithm": k.algorithm,
		"bits": k.bits,
		};

	callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
		{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}



function startCSK(template)
{
	let k = ctx.zoneKeys[template];
	let json = {
		"keytype": "csk",
		"active": true,
		"algorithm": k.algorithm,
		"bits": k.bits,
		}

	callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
		{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}



function startKSK(template)
{
	let k = ctx.zoneKeys[template];
	let json = {
		"keytype": "ksk",
		"active": true,
		"algorithm": k.algorithm,
		"bits": k.bits,
		}

	callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
		{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}



function toggleKey(id,act)
{
	let p = { "active": (!(act)) };
	callApi(`zones/${ctx.zone}/cryptokeys/${id}`,zoneKeys,
		{ method: "PUT", okResp: 204, json: JSON.stringify(p) } );
}



function deleteAllKeys()
{
	let nxtFn = deleteAllKeys;
	let kId = ctx.zoneKeys[ctx.zoneKeys.length-1].id;

	ctx.zoneKeys.pop();

	if (ctx.zoneKeys.length == 0) {
		if (("nsec3param" in ctx.zoneDetails)&&(ctx.zoneDetails.nsec3param != ""))
			nxtFn = function() { setNSEC3(true); };
		else
			nxtFn = zoneKeys;
		}

	callApi(`zones/${ctx.zone}/cryptokeys/${kId}`,nxtFn, { method: "DELETE", okResp: 204 } );
}



function deleteKey(idx)
{
	let k = ctx.zoneKeys[idx];
	callApi(`zones/${ctx.zone}/cryptokeys/${k.id}`,zoneKeys, { method: "DELETE", okResp: 204 } );
}



function cancelKeyAction()
{
	ctx.dnssec.b.innerHTML = "";
	ctx.dnssec.t.style.display = "block";
	delete ctx.dnssec.clickKey;
}



function clickDnssecKey(idx)
{
	if (("clickKey" in ctx.dnssec)&&(ctx.dnssec.clickKey == idx)) {
		ctx.dnssec.b.innerHTML = "";
		ctx.dnssec.t.style.display = "block";
		delete ctx.dnssec.clickKey;
		return;
		}

	ctx.dnssec.clickKey = idx;

	let sz = 150;
	let k = ctx.zoneKeys[idx];
	let x = "<table align=center>";
	let txt = `Activate Key-${k.id}`;
	if (k.active) txt = `Deactivate Key-${k.id}`;

	x += "<tr><td>" + btn(`toggleKey(${k.id},${k.active})`,txt,txt,sz) + "</td></tr>"
		+ "<tr><td>" + btn(`deleteKey(${idx})`,`Delete Key-${k.id}`,`Delete Key ${k.id}`,sz) + "</td></tr>"
		+ "<tr><td>" + btn('cancelKeyAction()',"Cancel Action","Cancel an action on this key",sz) + "</td></tr>"
		+ "</table>";

	ctx.dnssec.t.style.display = "none";
	ctx.dnssec.b.innerHTML = x
}



function showKeys(data)
{
	data.sort((a,b) => {
		if (a.keytype < b.keytype) return -1;
		if (a.keytype > b.keytype) return 1;
		if (a.active != b.active) {
			if (a.active) return -1;
			if (b.active) return 1;
			}
		if (a.id > b.id) return -1;
		if (a.id < b.id) return 1;
		return 0;
		})

	ctx.zoneKeys = data;
	setState(`${gbl.server}: ${ctx.zone}, DNSSEC Keys`, { state: "keys", server: gbl.server, zone: ctx.zone, keys:true });

	let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>";
	x += "<tr><td width=100%><table width=100% border=0 cellspacing=1 cellpadding=0><colgroup>";
	for(let i=0;i<5;i++) x += '<col width="50px">';
	x += "</colgroup><tr>";

	for (let i of ["Active","Id","Type/Flags","Bits","Algrythm",""])
		x += `<th>${i}</th>`;
	x += "</tr>";

	let keyTypes = { num: 0 };

	if (("nsec3param" in ctx.zoneDetails)&&(ctx.zoneDetails.nsec3param != "")) {
		x += `<tr class=dataRow><td align=center>${gbl.tick}`
			+ `</td><td colspan=3></td><td>NSEC3PARAM</td><td>${ctx.zoneDetails.nsec3param.toUpperCase()}</td></tr>`
		}

	for(let idx in data) {
		i = data[idx];
		x += `<tr onClick='clickDnssecKey(${idx});' class=dataRow><td align=center>`
		let aflag = 0;
		if (i.active) { aflag=1; x += gbl.tick; } else x += gbl.cross;
		x += `</td><td align=center>${i.id}</td>`;

		if (i.keytype in keyTypes) {
			let k = keyTypes[i.keytype];
			k.num++; k.active += aflag;
			k.ids.push({ idx: parseInt(idx), id: i.id, act: i.active });
			}
		else {
			keyTypes[i.keytype] = { ids: [], active: aflag, num: 1, }
			keyTypes[i.keytype].ids.push({ idx: parseInt(idx), id: i.id, act: i.active });
			keyTypes.num++;
			}

		x += `<td align=center>${i.keytype.toUpperCase()}/${i.flags}</td>`
			+ `<td align=center>${i.bits}</td>`
			+ `<td align=left>${i.algorithm}`
			+ `</td><td class=dnskey>${i.dnskey.substr(1,50)}...</td></tr>`;

		if ("ds" in i) {
			for (let ds of i.ds) {
				let dscol = ds.split(" ");
				x += `<tr onClick="clickDS('${ds}');" class=dataRow><td></td><td><li>DS`
					+ `</td><td>${dsAlg[parseInt(dscol[2])]}</td><td colspan=3>${ds}</td></tr>`;
				}
			}
		x += "<tr><td><div style='height: 15px;'></div></td></tr>";
		}

	let sp = "<div style='height: 7px;'></div>";
	let sz = 100;

	x += "</table></td><td>"
		+ btn('clickAddKey()',"Add Key",`Add a DNSSEC Key to ${ctx.zone}`,sz) + sp;
		+ btn('deleteAllKeys()',"Unsign",`Remove all DNSSEC on ${ctx.zone}`,sz) + sp;

	if (("nsec3param" in ctx.zoneDetails)&&(ctx.zoneDetails.nsec3param != "")) {
		x += btn("setNSEC3(true)","Del NSEC3",`Switch ${ctx.zone} from NSEC3 to NSEC`,sz) + sp
			+ btn("setNSEC3()","Roll NSEC3",`Change the NSEC3PARAM for ${ctx.zone}`,sz) + sp;
		}
	else
		x += btn("setNSEC3()","Add NSEC3",`Switch this zone to NSEC3 ${ctx.zone}`,sz) + sp;

	x += "</td></tr></table>";

	sz = 200
	x += "<div id='dnssecTop'><table align=center cellspacing=10 cellpadding=0 border=0>"

	let notes = "If your zone is currently validating, after <b>every</b> Key Rollover step you must leave a few days<br>"
		+ "for the internet to take-up your change. Probably a minimum of around 3 to 4 days";


	let key_txt = null;
	let zkey_txt = "";

	if ((keyTypes.num==1)&&("csk" in keyTypes)) {
		if (
			([1,2].indexOf(keyTypes.csk.num) >= 0)&&
			(keyTypes.csk.num == keyTypes.csk.active)
			) {
			let cid0 = keyTypes.csk.ids[0].idx;
			let cid1 = keyTypes.csk.ids[keyTypes.csk.ids.length-1].idx;

			let csk_rollover = [
				`<tr><td>Step 1 of 2: </td><Td>${btn(`startCSK(${cid0})`,"Start CSK Rollover","Start a CSK Rollover",sz)}</td></tr>`,
				`<tr><td>Step 2 of 2: </td><Td>${btn(`deleteKey(${cid1})`,"Complete CSK Rollover","Complete a CSK Rollover",sz)}</td></tr>`,
				]

			if (keyTypes.csk.num == 1) key_txt = csk_rollover[0];
			else if (keyTypes.csk.num == 2) key_txt = csk_rollover[1];
			}
		}

	else if ((keyTypes.num==2)&&("ksk" in keyTypes)&&("zsk" in keyTypes)) {

		if (
			([1,2].indexOf(keyTypes.ksk.num) >= 0)&&
			([1,2].indexOf(keyTypes.zsk.num) >= 0)&&
			(keyTypes.ksk.active == keyTypes.ksk.num)&&
			(keyTypes.zsk.active == 1)
			) {
			let ksk_link = "<a target=_blank href='https://doc.powerdns.com/authoritative/guides/kskroll.html'>";
			let zsk_link = "<a target=_blank href='https://doc.powerdns.com/authoritative/guides/zskroll.html'>";

			let kid0 = keyTypes.ksk.ids[0].idx;
			let kid1 = keyTypes.ksk.ids[keyTypes.ksk.ids.length-1].idx;
			let zid0 = keyTypes.zsk.ids[0].idx;
			let zid1 = keyTypes.zsk.ids[keyTypes.zsk.ids.length-1].idx;

			let ksk_rollover = [
				`<tr><td>${ksk_link}Step 1 of 2</a>: </td><Td>`
					+ btn(`startKSK(${kid0})`,"Start KSK Rollover","Start a KSK Rollover",sz) + "</td></tr>",

				`<tr><td>${ksk_link}Step 2 of 2</a>: </td><Td>`
						 + btn(`deleteKey(${kid1})`,"Complete KSK Rollover","Complete the KSK Rollover",sz) + "</td></tr>",
				];

			let zsk_rollover = [
				`<tr><td>${zsk_link}Step 1 of 3</a>: </td><Td>`
					 + btn(`startZSK(${zid0})`,"Start ZSK Rollover","Start a ZSK Rollover",sz) + "</td></tr>",

				`<tr><td>${zsk_link}Step 2 of 3</a>: </td><Td>`
					+ btn(`activateZSK(${zid0},${zid1})`,"Activate ZSK Rollover","Activate the ZSK Rollover",sz) + "</td></tr>",

				`<tr><td>${zsk_link}Step 3 of 3</a>: </td><Td>`
					+ btn(`deleteKey(${zid1})`,"Complete ZSK Rollover","Complete the ZSK Rollover",sz) + "</td></tr>",
				];

			if (keyTypes.ksk.num == 1)
				key_txt = ksk_rollover[0];

			else if (keyTypes.ksk.num == 2) {

				key_txt = ksk_rollover[1];

				notes = "<u>If you have only just created a new KSK, wait a few days before changing your DS records</u>"
					+ "<P>Once you have changed your DS reocrds, wait a few more days before completing the KSK rollover."
					+ `<P>The DS records for Key-Id: ${keyTypes.ksk.ids[0].id}, should be the only ones present<br>`
					+ "in the parent zone, for a few days, before completing the KSK rollover.";
				}

			if (keyTypes.zsk.num == 1)
				zkey_txt = zsk_rollover[0];
			else if (keyTypes.zsk.num == 2) {
				if (keyTypes.zsk.ids[0].id < keyTypes.zsk.ids[1].id)
					zkey_txt = zsk_rollover[1];
				else
					zkey_txt = zsk_rollover[2];
				}
			if (key_txt == null) zkey_txt = "";
			}
		}


	x += "<tr><td colspan=2><hr></td></tr>"

	if (key_txt == null)
		x += "<tr><td><h3>DNSSEC Key Rollover State could not be detected</h3></td></tr>"
	else
		x += key_txt + zkey_txt;

	x += "<tr><td colspan=2><hr></td></tr>"

	x += "</table>";
	if (notes != null) x += `<center>${notes}</center>`;
	x += "</div><div id='dnssecBot'></div>"

	gbl.bot.innerHTML = x;

	topLine();

	ctx.dnssec = {
		t: document.getElementById("dnssecTop"),
		b: document.getElementById("dnssecBot"),
		}
}



function zoneKeys()
{
	callApi(`zones/${ctx.zone}/cryptokeys`,showKeys);
}


//=============================================================================================
// Zone detils code
//=============================================================================================


function zoneKindChange(sel)
{
	m = document.getElementById("asMaster");
	s = document.getElementById("asSlave");
	if (sel.value == "Master") {
		m.style.display = "table-row-group";
		s.style.display = "none";
		}
	else {
		m.style.display = "none";
		s.style.display = "table-row-group";
		}
}



function zoneNotify()
{
	callApi(`zones/${ctx.zone}/notify`, ret => {
		if (ret!=null) popMsg(`${gbl.tick} Zone notified`); else errMsg("Notify failed");
		}, { method: "PUT", callErr: true });
}



function zoneRectify()
{
	callApi(`zones/${ctx.zone}/rectify`, ret => {
		if (ret!=null) popMsg(`${gbl.tick} Zone rectified`); else errMsg("Rectify failed");
		}, { method: "PUT", callErr: true });
}



function zoneRetrieve()
{
	callApi(`zones/${ctx.zone}/axfr-retrieve`, ret => {
		if (ret!=null) popMsg(`${gbl.tick} Zone retrieval requested`); else errMsg("Zone retrieval failed");
		}, { method: "PUT", callErr: true });
}



function clickAddZone(isFresh)
{
	ctx.addZone = true;
	topLine();
	gbl.bot.innerHTML = "";
	gbl.addZ.style.display = "block";
	if (isFresh) {
		let f = document.newZoneForm;
		f.zoneName.value = "";
		f.zoneName.focus();
		}
}



function dropZone()
{
	let x = "<table align=center><tr><td align=center><h3>Are you sure you want to delete"
		+ `<br>the zone '${ctx.zone}' and all its records ?</h3></td></tr>`
		+ "<tr><td align=center>"
		+ btn("loadThisZone();","Cancel",`Do not delete the zone ${ctx.zone}`)
		+ "&nbsp;"
		+ btn("doDropZone()",`Drop '${ctx.zone}'`,`Delete the zone ${ctx.zone} and ALL its records`);
	gbl.bot.innerHTML = x;
}


function doDropZone()
{
	callApi(`zones/${ctx.zone}`,zoneList, { method: "DELETE", noData: true, okResp: 204 });
}



function doNewName()
{
	let f = document.newNameForm;
	let rrType = f.rrType.value;

	if (!validInt(f.rrTTL.value)) {
		errMsg("Invalid TTL");
		f.rrTTL.focus();
		f.rrTTL.select();
		return;
		}

	if ((f.rrName.value == "")||(f.rrName.value == "@"))
		name = ctx.zone;
	else {
		if (!hostnameCheck.test(f.rrName.value)) {
			errMsg("Invalid host name");
			f.rrName.focus();
			f.rrName.select();
			return false;
			}
		name = `${f.rrName.value}.${ctx.zone}`;
		}

	gbl.addH.style.display = "none";

	if (rrType=="CATALOG") {
		name = f.rrName.value + ".";
		catname = nameToCatalog(name) + `.zones.${ctx.zone}`;
		ctx.rec = {
			name: catname,
			type: "PTR",
			rrs: {
				name: catname,
				type: "PTR",
				ttl: f.rrTTL.value,
				records: [ { "content":name, "disabled":false } ],
				},
			};
		return saveRRChanges(true);
		}

	if (findRec(ctx.zoneDetails.rrsets,{ name: name, type: rrType })!=null)
		return clickZoneRecord(name,rrType);

	ctx.rec = {
		name: name,
		type: rrType,
		rrs: {
			name: name,
			type: rrType,
			ttl: f.rrTTL.value,
			records: [ ],
			},
		};

	ctx.rec.loadedRows = 0;
	ctx.needSave = "saveRRChanges";
	addNewRR();

	return false; // prevent submit
}



function zoneAddHost()
{
	topLine();
	gbl.bot.innerHTML = "";
	gbl.addH.style.display = "block";
	let f = document.newNameForm;
	f.rrName.value = "";
	f.rrName.focus();
}



function zoneDetails(data)
{
	data.rrsets.sort((a,b) => {
		if ((a.name == data.name)&&(b.name == data.name)) {
			for(tag of ["SOA","NS","MX"]) {
				if (a.type == tag) return -1;
				if (b.type == tag) return 1;
				}
			}
		else {
			if (a.name == data.name) return -1;
			if (b.name == data.name) return 1;
			if (a.name < b.name) return -1;
			if (a.name > b.name) return 1;
			}
		if (a.type < b.type) return -1;
		if (a.type > b.type) return 1;
		return 0;
		})

	ctx.zoneDetails = data;

	if (("param" in gbl)&&("zone" in gbl.param)) {
		if ("meta" in gbl.param) return zoneMeta();
		if ("keys" in gbl.param) return zoneKeys();
		if ((data.kind != "Slave")&&("name" in gbl.param)&&("type" in gbl.param)) {
			return clickZoneRecord(gbl.param.name,gbl.param.type);
			}
		}

	setState(`${gbl.server}: ${ctx.zone}`, { state: "zone", server: gbl.server, zone: ctx.zone });

	let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>"
		+ "<tr><td width=100%><table width=100% border=0 cellspacing=0 cellpadding=0>";

	for(let i of data.rrsets) {
		x += `<tr onClick="clickZoneRecord('${i.name}','${i.type}');" class=dataRow>`
			+ `<td valign=top>${i.name.substr(0,i.name.length - ctx.zone.length - 1)}</td>`
			+ `<td valign=top align=right>${i.ttl}</td>`
			+ `<td valign=top>${i.type}</td><td class=dataCell valign=top>`;

		if (i.type == "SOA") {
			ctx.soaSplit = i.records[0].content.split(" ");
			x += formatSOA(ctx.soaSplit);
			}
		else {
			i.records.sort((a,b) => {
				if (a.content < b.content) return -1;
				if (a.content > b.content) return 1;
				return 0;
				})

			for(let rrs of i.records)
				x += `${rrs.content}<br>`;
			}
		x += "</td></tr>";
		}

	let sp = "<div style='height: 7px;'></div>";
	let sz = 100;

	x += "</table></td><td align=center>";

	if (ctx.zoneDetails.kind != "Slave")
		x += btn("zoneAddHost()","Add Host",`Add a host to the zone ${ctx.zone}`,sz) + sp;

	x += btn("zoneAxfr()","Download",`Download ${ctx.zone} in RFC/AXFR format`,sz) + sp
		+ btn('zoneMeta()',"Metadata",`Load the META data for ${ctx.zone}`,sz) + sp
		+ btn('zoneNotify()',"Notify Zone",`Notify the slaves of ${ctx.zone}`,sz) + sp;

	if (ctx.zoneDetails.dnssec)
		x += btn('zoneRectify()',"Rectify Zone",`Rectify the data for ${ctx.zone}`,sz) + sp;

	if (ctx.zoneDetails.kind == "Slave")
		x += btn('zoneRetrieve()',"Retrieve Zone",`Retrieve ${ctx.zone} from its master(s)`,sz) + sp;

	x += btn('dropZone()',"Drop Zone",`Delete ${ctx.zone} and ALL its records`,sz);

	x += "<hr>";

	if (ctx.zoneDetails.dnssec) {
		x += btn('zoneKeys()',"DNSSEC Keys",`View DNSSEC keys for ${ctx.zone}`,sz) + sp;
		}
	else
		x += btn('signZone()',"Sign Zone",`DNSSEC sign ${ctx.zone}`,sz) + sp;

	x += "</td></tr></table>";
	gbl.bot.innerHTML = x;

	topLine();
}



function loadThisZone()
{
	return zoneDetailsLoad(ctx.zone);
}



function zoneDetailsLoad(zone)
{
	document.body.scrollTop = 0;
	ctx = {
		zone: zone,
		zoneList: ctx.zoneList,
		};
	callApi(`zones/${ctx.zone}`,zoneDetails);
}



//=============================================================================================
// Search handling code
//=============================================================================================


function searchLoadRecord(zone,name,type)
{
	gbl.param = { state: "record", server: gbl.server, zone: zone, name:name, type: type };
	gbl.state = gbl.param;
	title = `${gbl.server}, Search results - ${name}:${type}`
	window.history.pushState(gbl.param,title,gbl.pathname);
	zoneDetailsLoad(zone);
}



function searchResults(data)
{
	if (data.length <= 0) {
		popMsg("No search results");
		return;
		}

	setState(`${gbl.server}: Search Results`, { state: "search", server: gbl.server, search: ctx.searchTerm });

	ctx = { searchTerm: ctx.searchTerm }; topLine(); ctx = {};

	let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>";
	for(let i of data) {
		x += "<tr class=dataRow";
		if (i.object_type != "zone") {
			x += ` onClick='ctx = { zone: "${i.zone}" }; `
				+ ` searchLoadRecord("${i.zone}","${i.name}","${i.type}");'`
				+ `><td valign=top>${i.name}</td>`
				+ `<td valign=top align=right>${i.ttl}</td>`
				+ `<td valign=top>${i.type}</td>`;

			if (i.type == "SOA")
				x += `<td valign=top>${formatSOA(i.content.split(" "))}</td>`;
			else
				x += `<td valign=top>${i.content}</td>`;
			}

		else {
			x += ` onClick='return zoneDetailsLoad("${i.name}");'>`
				+ `<td>${i.name}</td><td colspan=3>[Zone Apex]</td>`;
			}
		x += "</tr>";
		}
	x += "</table>";
	gbl.bot.innerHTML = x;
}



function searchFormSubmit()
{
	let s = document.searchForm.searchTerm.value;
	ctx.searchTerm = s;
	document.searchForm.searchTerm.value = "";
	callApi(`search-data?q=${s}`,searchResults);
	return false;
}


//=============================================================================================
// TSIGs
//=============================================================================================


function loadTsigList()
{
	callApi("tsigkeys",showTsigList);
}



function clickTSIG(txt,name)
{
	navigator.clipboard.writeText(txt).then(
		() => popMsg(`${name} copied to clipboard`),
		() => errMsg("Copy to clipboard failed")
		);
}



function deleteTSIG(id)
{
	callApi(`tsigkeys/${id}`,loadTsigList,{ method: "DELETE", okResp: 204 });
}



function formAddTsig()
{
	let f = document.newTsigForm;
	let json = {
		name: f.tsigName.value,
		algorithm: f.tsigAlg.value,
		key: f.tsigKey.value,
		};

	if (json.name.substr(json.name.length-1)!=".")
		json.name += ".";

	if (!fqdnCheck.test(json.name)) {
		errMsg("Invalid TSIG Name");
		document.newTsigForm.tsigName.focus();
		document.newTsigForm.tsigName.select();
		return false;
		}

	callApi("tsigkeys",data => {
		if (data == null) addTSIG(false); else showTSIG(data.id);
		},
		{ method: "POST", callErr: true, okResp: 201, json: JSON.stringify(json) });

	return false; // prevent form submit
}



function regenTSIG(id,alg)
{
	callApi(`tsigkeys/${id}`,() => {
		callApi("tsigkeys",() => showTSIG(id),
			{ method: "POST", okResp: 201, json: JSON.stringify({ name:id, algorithm: alg }) });
		}
		,{ method: "DELETE", okResp: 204 });
}



function addTSIG(withClr)
{
	gbl.bot.innerHTML = "";
	gbl.addT.style.display = "block";

	let t = document.newTsigForm.tsigName;
	let k = document.newTsigForm.tsigKey;
	if (withClr) { t.value = ""; k.value = ""; }
	t.focus();
}



function showTSIG(tsig)
{
	callApi(`tsigkeys/${tsig}`,data => {

		setState(`${gbl.server}: TSIG ${data.id}`, { state: "tsigdata", server: gbl.server, tsig: data.id });
		ctx = {}; topLine();

		let x = "<table style='margin-top: 50px;' align=center border=0 cellspacing=0 cellpadding=0>"
			+ `<tr class=dataRow onClick=\"clickTSIG('${data.id}','Name');\"><td class=formPrompt>TSIG Name :</td><td>${data.id}</td></tr>`
			+ `<tr><td class=formPrompt>TSIG Algorithm :</td><td>${data.algorithm.toUpperCase()}</td></tr>`
			+ `<tr onClick=\"clickTSIG('${data.key}','Key');\" class=dataRow><td class=formPrompt>TSIG Key :</td><td>${data.key}</td></tr>`
			+ "<tr><td align=center colspan=2><hr></td></tr>"
			+ "<tr><td align=center colspan=2>"
			+ btn("loadTsigList()","Back","Go back to the list of TSIGs")
			+ btn(`regenTSIG('${data.id}','${data.algorithm}')`,"Rollover","Replace this TSIG Key, keeping the name")
			+ btn(`deleteTSIG('${data.id}')`,"Delete","Delete this TSIG Key")
			+ "</td></tr></table>";

		gbl.bot.innerHTML = x;
		topLine();
		});
}



function showTsigList(data)
{
	if (data.length <= 0) {
		popMsg("No TSIGs found");
		return;
		}

	data.sort((a,b) => {
		if (a.id < b.id) return -1;
		if (a.id > b.id) return 1;
		return 0;
		});

	if (("param" in gbl)&&("tsig" in gbl.param)&&(typeof(gbl.param.tsig) != "boolean"))
		return showTSIG(gbl.param.tsig);

	setState(`${gbl.server}: TSIGs`, { state: "tsig", server: gbl.server, tsig: true });
	ctx = {}; topLine();

	let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>"
		+ "<colgroup><col width=100%/><col/></colgroup><tr><Td>"
		+ "<table align=center border=0 cellspacing=2 cellpadding=2>"
		+ "<tr><th>TSIG Name</td><th>Algorithm</td></tr>";

	for(let i of data)
		x += `<tr class=dataRow onClick="showTSIG('${i.id}');"><td>${i.id}</td>`
			+ `<td>${i.algorithm.toUpperCase()}</td></tr>`;

	x += "</table></td><td>" + btn("addTSIG(true)","Add TSIG","Add a TSIG Key") + "</td></tr></table>";

	gbl.bot.innerHTML = x;
}


//=============================================================================================
// Code to handle the list of zones
//=============================================================================================


function doMakeZone(data)
{
	if (data != null)
		zoneDetailsLoad(data.name);
	else {
		clickAddZone(false);
		errMsg("Adding zone failed");
		}
}



function doNewZone()
{
	let name = document.newZoneForm.zoneName.value;
	if (name.substr(name.length-1)!=".") name += ".";
	if (!fqdnCheck.test(name)) {
		errMsg("Invalid domain name");
		document.newZoneForm.zoneName.focus();
		document.newZoneForm.zoneName.select();
		return false;
		}

	let kind = document.newZoneForm.zoneKind.value;

	let ns = [];
	let ip = [];
	for(let i=0;i<4;i++) {
		let x = document.newZoneForm[`ns${i}`];
		let v = x.value;

		if (v != "") {
			if (v.substr(v.length-1)!=".") v += ".";

			if (!fqdnCheck.test(v)) {
				errMsg(`Invalid Host Name: ${v}`);
				x.focus(); x.select();
				return false;
				}

			ns.push(v);
			}

		x = document.newZoneForm[`ip${i}`];
		if (x.value != "") {
			if ((validations["rrA"].test(x.value))||(validations["rrA"].test(x.value)))
				ip.push(x.value);
			else {
				errMsg(`Invalid IP Address: ${x.value}`);
				x.focus(); x.select();
				return false;
				}
			}
		}

	let data = {
		name: name,
		kind: kind,
		masters: ip,
		nameservers: ns,
		};

	callApi(gbl.zone_list,doMakeZone, { method: "POST", callErr: true, okResp: 201, json: JSON.stringify(data) } );

	return false; // block form submit
}



function zoneListDetails(data)
{
	data.sort((a,b) => {
		if (a.id < b.id) return -1;
		if (a.id > b.id) return 1;
		return 0;
		});

	ctx.zoneList = data;

	if ("param" in gbl) {
		if (gbl.param.state == "stats") {
			if ("stats" in gbl.param) return oneStat(gbl.param.stats);
			return loadStats();
			}
		if ("tsig" in gbl.param) return loadTsigList();
		if ("search" in gbl.param) {
			ctx.searchTerm = gbl.param.search;
			return callApi(`search-data?q=${ctx.searchTerm}`,searchResults);
			}
		if ("zone" in gbl.param) return zoneDetailsLoad(gbl.param.zone);
		}

	setState(`${gbl.server}: Zone List`, { state: "zonelist", server: gbl.server });

	let x = "<table border=0 cellspacing=0 cellpadding=0 width=100%><colgroup><col width=100%/><col/></colgroup>"
	x += "<tr><td><table width=100% border=0 cellspacing=0 cellpadding=0>";
	for(let i of data) {
		x += `<tr class=dataRow onClick='zoneDetailsLoad("${i.name}")'>`
			+ `<td>${i.name}</td><td>${i.kind}</td>`;

		t = "";
		hlp = "Zone is NOT dnssec signed";
		if (i.dnssec) {
			t = gbl.nsec;
			hlp = "Zone is DNSSEC signed";
			}
		x += `<td title='${hlp}'>${t}</td><td>${i.serial}</td>`;

		t = ""; hlp = "";
		if (i.notified_serial != i.serial) {
			t = gbl.timer;
			hlp = "Zone has unnotified changes";
			}
		x += `<td title='${hlp}'>${t}</td></tr>`;
		}
	x += "</table></td><td>";

	let sp = "<div style='height: 7px;'></div>";
	let sz = 100;

	x += btn("clickAddZone(true)","Add Zone","Add a new zone to the database",sz)+sp;

	x += "</td></tr></table>";

	topLine();
	gbl.bot.innerHTML = x;
}



function zoneList()
{
	document.body.scrollTop = 0;
	ctx = { }; topLine();
	callApi(gbl.zone_list,zoneListDetails);
	return false;
}


//=============================================================================================
// Zone AXFR
//=============================================================================================


function zoneAxfrData(data)
{
	let name = ctx.zone.replace(/\./g,"_");
	name = `${name.substr(0,name.length-1)}.txt`;
	downloadFile(data,"text/plain",name);
}




function zoneAxfr()
{
	callApi(`zones/${ctx.zone}/export`,zoneAxfrData);
}


//=============================================================================================
// Common functions
//=============================================================================================


function validInt(x) {
	return ((x != "") && (x != null) && (!(isNaN(x))));
}


function btn(call,txt,hlp,sz) {
	let ex=""
	if (sz != null) ex = `style='width: ${sz}px;'`
	return `<span ${ex} title="${hlp}" class=myBtn onClick="${call}; return false;">${txt}</span>`;
}


function goLogin()
{
	document.location = gbl.pathname;
}


function topLine()
{
	if (!("zoneList" in ctx)) return;

	gbl.forms.forEach(i => i.style.display = "none");

	let y = "<table style='margin-top:10px;' border=0 cellspacing=0 cellpadding=0 width=100%><tr><td width=1 class=dataRow>"
		+ `<font size=+1><a title='Open this page in a new window' target=_blank href='${window.origin}${gbl.pathname}?`;

	let x = `'>${gbl.page}</a></font></td><td>`
		+ btn("goLogin()",gbl.server,"Change server")
		+ btn("zoneList()","Zones",`Reload the list of zones at ${gbl.server}`);

	if ((gbl.state.state == "tsig")||(gbl.state.state == "tsigdata"))
		x += btn("loadTsigList()","TSIGs","Show the list of TSIGs");

	if ("searchTerm" in ctx) delete ctx.searchTerm;

	if ("zone" in ctx) {
		x += btn('loadThisZone()',ctx.zone.substr(0,ctx.zone.length-1),`Reload all records in ${ctx.zone}`);

		if ("rec" in ctx) {
			let name = "@";
			if (ctx.rec.name != ctx.zone)
				name = ctx.rec.name.substr(0,ctx.rec.name.length - ctx.zone.length - 1);

			ctx.rec.showName = name;
			x += btn('getZoneRecord()',`${name}/${ctx.rec.type}`,`Reload the ${ctx.rec.type} recods for ${name}`);
			}
		else if (gbl.state.state == "meta") {
				x += btn('zoneMeta()',"META",`Reload the META data for ${ctx.zone}`);
				}
		else if ("zoneKeys" in ctx) {
			x += btn('zoneKeys()',"DNSSEC",`Reload the DNSSEC Keys for ${ctx.zone}`);
			}
		}
	else if (("state" in gbl)&&(gbl.state.state == "stats")) {
		x += btn("loadStats()","Stats","View the server stats");
		if ("stats" in gbl.state)
			x += btn(`oneStat('${gbl.state.stats}')`,gbl.state.stats,`View the server stat - ${gbl.state.stats}`);
		}

	x += "</td><td width=100% align=center><span class='fullvis' id='showMsg'></span></td><td>";

	if ("zone" in ctx) {
		if ("edit" in ctx) {
			if ("needCancel" in ctx.edit)
				x += btn(`${ctx.edit.needCancel}(false)`,"Cancel","Cancel this edit");
			else
				x += btn("cancelAllEdits()","Cancel","Cancel this edit");
			}
		else {
			if ("rec" in ctx)
				x += btn("addNewRR()","Add RR",`Add another ${ctx.rec.type} record to ${ctx.rec.name}`);

			if ("needSave" in ctx)
				x += btn(`${ctx.needSave}(false)`,"Cancel","Abandom all changes")
					+ btn(`${ctx.needSave}(true)`,"Save","Save your changes to the zone file");

			if (gbl.state.state == "meta")
				x += btn('clickAddMeta()',"Add Meta",`Add a META data record to ${ctx.zone}`);
			}

		}

	if (gbl.state.state == "zonelist") {
		x += btn("loadTsigList()","TSIGs","Show the list of TSIGs")
		if (!("stats" in ctx))
			x += btn("loadStats()","Stats","View the server stats");
		}

	x += "</td><td width=1 align=right><form name=searchForm onSubmit='return searchFormSubmit();'>"
		+ "<input placeholder='Search' size=30 name=searchTerm></form></td>"
		+ "</tr><table><hr>";

	gbl.top.innerHTML = y + btoa(JSON.stringify(gbl.state)) + x;
}



function loadCookies()
{
	let ckobj = {}
	let cookies = document.cookie.split(";");
	for(let i in cookies) {
		ck = cookies[i];
		while (ck.charAt(0) == ' ') ck = ck.substring(1);

		if ((idx = ck.indexOf("=")) >= 0) {
			let itm = ck.substr(0,idx);
			if (itm in pdnsCookies) {
				try { ckobj[itm] = atob(ck.substr(idx+1)); } catch(e) {}
				}
			}
		}

	let f = document.loginForm;
	if ("pdns_server" in ckobj) f.server.value = ckobj["pdns_server"];
	if ("pdns_with_https" in ckobj) f.with_https.checked = (ckobj["pdns_with_https"] == "true");
	if ("pdns_fast_zone_list" in ckobj) f.fast_zone_list.checked = (ckobj["pdns_fast_zone_list"] == "true");
}



function startUp()
{
	gbl.pathname = window.location.pathname;

	let f = document.loginForm;
	if (f.server.value == "") f.server.value = window.location.host;
	f.with_https.checked = (window.location.protocol == "https:");
	f.fast_zone_list.checked = gbl.fast_zone_list;

	loadCookies()

	window.addEventListener('popstate', e => {
		if (e.state == null) goLogin();

		gbl.param = e.state;
		if ("server" in gbl.param) {
			gbl.server = gbl.param.server;
			zoneList();
			return true;
			}
		delete gbl.param;
		document.location = gbl.pathname;
		return true;
		});

	gbl.top = document.getElementById("topSpan");
	gbl.bot = document.getElementById("lowerSpan");

	gbl.addH = document.getElementById("addName");
	gbl.addZ = document.getElementById("addZone");
	gbl.addT = document.getElementById("addTsig");
	gbl.optM = document.getElementById("metaOpt");

	gbl.forms = [ gbl.addH, gbl.addZ, gbl.addT ];

	if (window.location.search != "") {
		gbl.param = JSON.parse(atob(window.location.search.substr(1)));
		if ("server" in gbl.param) {
			gbl.state = gbl.param;
			gbl.server = gbl.param.server;
			zoneList();
			}
		}

	document.newNameForm.rrTTL.value = gbl.default_ttl;
}



function initialLogin()
{
	let f = document.loginForm;
	gbl.server = f.server.value;
	gbl.apikey = f.apikey.value;
	gbl.with_https = f.with_https.checked;
	gbl.fast_zone_list = f.fast_zone_list.checked;

	gbl.pdns_server = gbl.server;
	gbl.pdns_with_https = gbl.with_https;
	gbl.pdns_fast_zone_list = gbl.fast_zone_list;

	if (gbl.pdns_fast_zone_list) gbl.zone_list = "zones?dnssec=false"; else gbl.zone_list="zones";

	let milliDays = 30 * 86400000; // 30 days in milliseconds
	let d = new Date();
	d.setTime(d.getTime() + milliDays);
	let expires = "; samesite=strict; expires="+ d.toUTCString();

	for(let i in pdnsCookies)
		document.cookie = i + "= ; samesite=strict; expires = Thu, 01 Jan 1970 00:00:00 GMT"

	for(let i in pdnsCookies)
		document.cookie = i + "=" + btoa(gbl[i]) + expires;

	zoneList();

	return false; // block form submit
}



function formatSOA(i)
{
	let soa = `${i[0]} ${i[1]}<br>`;
	let idx = 2;
	for(let tag of ["serial","refresh","retry","expire","minimum"]) {
		soa += `<span style='display: inline-block; width:50px'></span>`;
		soa += `<span style='display: inline-block; width:100px'>${i[idx++]}</span>;${tag}<br>`;
		}
	return soa;
}




function unerrMsg()
{
	let m = document.getElementById("myMsgPop");
	let t1 = m.innerHTML;
	let t2 = gbl.lastErrMsg;
	if (t2 == null) t2 = "";
	if (t1 == t2) m.className = "msgPop msgPopNo";
	delete gbl.lastErrMsg;
}



function errMsg(txt)
{
	let m = document.getElementById("myMsgPop");
	m.className = 'msgPop msgPopYes';
	m.innerHTML = `${gbl.warn} ${txt}`;
	gbl.lastErrMsg = m.innerHTML;
	msg("");
	setTimeout(unerrMsg,2500);
}



function unpopMsg()
{
	let m = document.getElementById("showMsg");
	let t1 = m.innerHTML;
	let t2 = gbl.lastMsg;
	if (t2 == null) t2 = "";
	if (t1 == t2) m.className = "fadeout";
	delete gbl.lastMsg;
}



function popMsg(txt)
{
	msg(txt,"fadein");
	gbl.lastMsg = document.getElementById("showMsg").innerHTML;
	setTimeout(unpopMsg,2500);
}



function msg(myMsg,newClass)
{
	let m = document.getElementById("showMsg");
	if (!m) m = document.getElementById("lowerMsg");
	if (!m) return;

	if (newClass == null) newClass = "fullvis";
	m.className = newClass;
	m.innerHTML = myMsg;
	delete gbl.lastMsg;
}



function keyEsc(e) {
	if (e.key == "Escape") {
		if ("edit" in ctx) return cancelAllEdits();
		if ("addRR" in ctx) return loadThisZone();
		if ("addZone" in ctx) return zoneList();
		}
}


//=============================================================================================
</script>



<head>
	<title>PowerDNS WebUI - Login</title>
</head>
<body onLoad="startUp();">

<div id="topSpan">
<table border=0 cellspacing=0 cellpadding=0 width=100%>
<tr><td><h1 style="text-align:center">PowerDNS WebUI</h1></td></tr>
</table>
</div>

<div id="lowerSpan">
	<table align=center>
	<form action="/" onSubmit="return initialLogin()" name=loginForm>
		<tr><td class=formPrompt>Server:</td><td><input title="PowerDNS RestAPI, can include optional :[port] and sub-directory" name=server size=40 value=""></td></tr>
		<tr><td class=formPrompt>API-Key:</td><td><input autocomplete="current-password" type=password size=40 name=apikey value=""></td></tr>
		<tr><td class=formPrompt>HTTPS (SSL/TLS):</td><td><input title="Connect to API over HTTPS, not HTTP" type=checkbox name=with_https></td></tr>
		<tr><td class=formPrompt>Fast Zone List:</td><td><input title="Fast load long zone lists by excluding some information" type=checkbox name=fast_zone_list></td></tr>
		<tr><td align=center colspan=2><span title="Load zone list" class=myBtn onClick="initialLogin()">Load Zone List</span></td></tr>
		</tr>
	<input type=submit style="display: none;">
	</form>
	</table>
<P>
<div class='fullvis' id="lowerMsg"></div>
</div>



<div id=addZone style="display: none;">
	<table width=40% align=center><tbody id="startBody">
	<colgroup><col width=35%/><col width=65%/></colgroup>
	<tr><td colspan=2 align=center><h2>Add a New Zone</h2></td></tr>
	<form method=post action="/" name=newZoneForm onSubmit="return doNewZone();">
		<tr><td class=formPrompt>Name :</td><td><input onkeydown="keyEsc(event);" name=zoneName></td></tr>
		<tr><td class=formPrompt>Type :</td><td><select onChange="zoneKindChange(this);" name=zoneKind><option>Master<option>Slave<option>Native</select></td></tr>
		</tbody><tbody id="asMaster">
			<tr><td class=formPrompt>Name Servers :</td><td>
				<input size=40 name=ns0><br>
				<input size=40 name=ns1><br>
				<input size=40 name=ns2><br>
				<input size=40 name=ns3><br></td></tr>
		</tbody><tbody id="asSlave" style="display: none;">
			<tr><td class=formPrompt>Master's IPs :</td><td>
				<input size=40 name=ip0><br>
				<input size=40 name=ip1><br>
				<input size=40 name=ip2><br>
				<input size=40 name=ip3><br></td></tr>
		</tbody>
		<tr><td colspan=2><hr></td></tr>
		<tr><td align=center colspan=2><span title="Cancel adding zone" class=myBtn onClick="zoneList();">Cancel</span><span title="Add new zone" class=myBtn onClick="doNewZone();">Add Zone</span></td></tr>
		</tr>
	<input type=submit style="display: none;">
	</form>
	</table>
</div>



<div id=addName style="display: none;">
	<table width=40% align=center>
	<colgroup><col width=35%/><col width=65%/></colgroup>
	<tr><td colspan=2 align=center><h2>Add a New Host</h2></td></tr>
	<form method=post action="/" name=newNameForm onSubmit="return doNewName();">
		<tr><td class=formPrompt>Host name:</td><td><input onkeydown="keyEsc(event);" name=rrName></td></tr>
		<tr><td class=formPrompt>RR Type:</td><td><select name=rrType>
			<option>A<option>AAAA<option>MX<option>NS<option>TXT
			<option value=CATALOG>Add Zone to Catalog

			<option>A6<option>ADDR<option>AFSDB<option>ALIAS
			<option>CAA<option>CDNSKEY<option>CDS<option>CERT
			<option>CNAME<option>DHCID<option>DLV<option>DNAME<option>DNSKEY<option>DS
			<option>EUI48<option>EUI64<option>HINFO<option>IPSECKEY<option>KEY
			<option>KX<option>LOC<option>LUA<option>MAILA<option>MAILB<option>MB
			<option>MG<option>MINFO<option>MR<option>MX<option>NAPTR<option>NS
			<option>OPENPGPKEY<option>OPT<option>PTR
			<option>RKEY<option>RP<option>RRSIG<option>SIG<option>SMIMEA<option>SOA
			<option>SPF<option>SRV<option>SSHFP<option>TKEY<option>TLSA<option>TXT
			<option>URI<option>WKS

		</select></td></tr>
		<tr><td class=formPrompt>TTL:</td><td><input value=86400 name=rrTTL></td></tr>
		<tr><td colspan=2><hr></td></tr>
		<tr><td align=center colspan=2><span class=myBtn title="Cancel adding RR" onClick="loadThisZone();">Cancel</span><span title="Add this RR" class=myBtn onClick="doNewName();">Add Record</span></td></tr>
		</tr>
	<input type=submit style="display: none;">
	</form>
	</table>
</div>


<div id=addTsig style="display: none;">
	<table width=40% align=center>
	<colgroup><col width=35%/><col width=65%/></colgroup>
	<tr><td colspan=2 align=center><h2>Add a New TSIG Key</h2></td></tr>
	<form method=post action="/" name=newTsigForm onSubmit="return formAddTsig();">
		<tr><Td class=formPrompt>TSIG Name: </td><td><input size=50 name=tsigName></td></tr>
		<tr><td class=formPrompt>TSIG Algrythm: </td><td><select name=tsigAlg>
			<option value="hmac-sha512">HMAC-SHA512<option value="hmac-sha384">HMAC-SHA384<option value="hmac-sha256">HMAC-SHA256
			<option value="hmac-sha224">HMAC-SHA224<option value="hmac-sha1">HMAC-SHA1<option value="hmac-md5">HMAC-MD5
		</select></td></tr>
		<tr><Td class=formPrompt>TSIG Key: </td><td><input placeholder='Leave blank, for auto-generated key' size=100 name=tsigKey></td></tr>
		<tr><td colspan=2><hr></td></tr>
		<tr><td align=center colspan=2><span class=myBtn title="Cancel adding a TSIG Key" onClick="loadTsigList();">Cancel</span><span title="Add this TSIG Key" class=myBtn onClick="formAddTsig();">Add TSIG</span></td></tr>
	<input type=submit style="display: none;">
	</form>
	</table>
</div>

<div id=myMsgPop class='msgPop msgPopNo'></div>
</body></html>