/*! @file @id $Id$ This is the main application as it is fully run in the user's browser. @dot digraph X { start [URL="\ref start()"]; newuser [URL="\ref newuser()"]; login [URL="\ref login()"]; createkeypair [URL="\ref createkeypair()"]; chat [URL="\ref chat()"]; getpwd [URL="\ref getpwd()"]; setpw [URL="\ref setpw()"]; get [URL="\ref get()"]; sendmessage [URL="\ref sendmessage()"]; start -> newuser [label="if no keys exist"]; start -> login [label="if keys exist"]; newuser -> createkeypair [label="on submit"]; createkeypair -> "openpgp.generateKeyPair"; "openpgp.generateKeyPair" -> login [label="keys generated in local store"]; login -> chat [label="user is valid on server"]; chat -> getpwd [label="password not yet entered"]; getpwd -> setpw [label="on input"]; setpw -> chat [label="password is valid"]; chat -> chat [label="remain in chat"]; chat -> get [label="start timer"]; get -> get [label="restart timer"]; chat -> sendmessage [label="on submit"]; sendmessage -> chat [label="remain in chat"]; } @enddot */ // 1 2 3 4 5 6 7 8 // 45678901234567890123456789012345678901234567890123456789012345678901234567890 var password = null; ///< password, only stored temporary, until reload var username = null; ///< username, only used during registration var filecontent = new Array(); ///< temporary storage for attachments var socket = io.connect(); var reboottimer = null; /// Show error messsage /** Fades in an error message and logs to console. @param data (optional) The error can be a string or any structure. Strings are shown to the user, structures are logged only. @param stay (optional) If not given as @c true, reloads page after 5s. */ function error(data, stay) { $("#status").hide(); $("#status").addClass("error") $("#status").removeClass("notice") $("#status").removeClass("success") if (data) { if (typeof data == 'string') { $("#status").html(data); console.log("error: "+data); } else { $("#status").html('unknown error: '+JSON.stringify(data)); console.log("error: "+JSON.stringify(data)); } } else { $("#status").html('error'); console.log("error"); } $("#status").show(); if (!stay) { console.log("reboot in 5s"); console.log((new Error('stacktrace')).stack); if (!reboottimer) reboottimer = setTimeout(function() { reboottimer = null; start(); }, 5000); } } /// Show notice messsage /** Fades in an notice message and logs to console. @param text (optional) The data is a string. */ function notice(text) { $("#status").hide() $("#status").addClass("notice") $("#status").removeClass("error") $("#status").removeClass("success") if (text) { $("#status").html(text); console.log("notice: "+text); } else { $("#status").html(''); console.log("notice"); } $("#status").show(); } /// Show notice messsage /** Fades in an success message and logs to console. @param text (optional) The data is a string. */ function success(text) { $("#status").hide(); $("#status").addClass("success") $("#status").removeClass("error") $("#status").removeClass("notice") if (text) { $("#status").html(text); console.log("success: "+text); } else { $("#status").html(''); console.log("success"); } $("#status").show(); } /// Show status message in the main screen area /** @param id HTML id to be shown. @param msg The success message text */ function status(id, msg) { console.log("state: "+id); if (msg) success(msg); else $("#status").hide(); $("#main").children(":not(#"+id+")").hide(); $("#main #"+id).show(); $("#main #"+id+" form input:first-child").focus(); } function emit(signal, data) { console.log("<-snd "+signal); socket.emit(signal, data); } function connected() { console.log("server connected"); $("#connectionstatus #bad").hide(); $("#connectionstatus #good").show(); success("server connected"); } function disconnected() { console.log("server disconnected"); $("#connectionstatus #good").hide(); $("#connectionstatus #bad").show(); error("server disconnected", true); } function connectionstatus() { if (socket.connected) connected(); else disconnected(); } function htmlenc(html) { return $('
').text(html).html(); } function htmldec(data) { return $('').html(data).text(); } /// Alert user /** Alert user, e.g. that a new message has arrived. */ function beep(user) { if (user) success("message from "+htmlenc(user)+" received"); navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate; if (navigator.vibrate) { // vibration API supported navigator.vibrate(1000); } (new Audio("sounds/A-Tone-His_Self-1266414414.mp3")).play(); } /// Toggle Menu Display function togglemenu() { $("#menu").toggle(); } /// Download Profile Backup function backup() { var download = document.createElement('a'); download.href = 'data:attachment/text,'+encodeURI(JSON.stringify(localStorage)); download.target = '_blank'; function pad(n) {return n<10 ? '0'+n : n} var now = new Date(); download.download = pad(now.getFullYear())+pad(now.getMonth()+1)+pad(now.getDate())+ "-"+userid()+"@"+window.location.hostname+".bak"; var clickEvent = new MouseEvent("click", { "view": window, "bubbles": true, "cancelable": false }); download.dispatchEvent(clickEvent); togglemenu(); } /// Upload Profile Backup function restore(evt) { if (!window.FileReader) return error("your browser dows not support file upload", true); for (var i=0, f; f=evt.target.files[i]; ++i) { var file = f; var reader = new FileReader(); reader.onload = function(evt) { if (evt.target.error) return error("error reading file", true); if (evt.target.readyState==0) return notice("waiting for data …"); if (evt.target.readyState==1) return notice("loading data …"); var parsed=JSON.parse(evt.target.result); togglemenu(); localStorage.pubkey = parsed.pubkey; localStorage.privkey = parsed.privkey; success("backup is restored"); console.log("reboot after restore in 2s"); if (!reboottimer) reboottimer = setTimeout(function() { reboottimer = null; start(); }, 2000); } reader.readAsText(file); } } /// Configure local groups /** … */ function groups() { } /// Check if password is set and matches the repeated password /** Checks if both passwords are identical and valid and gives feedback to the user. Called when user edits the password fields. Sets @ref username and checks @ref password - if both are well defined, enables the submit button. @param pwd The password. @param pwd2 The repeated password. */ function checkpwd(pwd, pwd2) { $("#register").submit(function(event) { return false; }); if (pwd==pwd2) password=pwd; else password=null; if (!password||password.length<1) password=null; $("#createuser").prop("disabled", !(username && password)); if (password) { if (username) success("user is ready to be created"); else notice("password matches, please chose a valid user name"); } else { if (username) notice("passwords don't match"); else if ($('#user').val()) notice("user name is already in use"); else notice("please chose a user name"); } } /// Checks if the receiver of a message exists on server. /** Calls checknewuser.php on server and enables the message submit button if the receiver of the message exists on the server. */ function checkpartner(user) { $("#chat").submit(function(event) { return false; }); emit("user", user); } /// Create Local Public-/Private-Key Pair /** Called if user has not yet his keys, just generates a new key pair. */ function createkeypair(user, pwd) { notice("generating keys"); openpgp.generateKeyPair({ numBits: 4096, userId: user, passphrase: pwd }).then(function(keyPair) { success("keys generated"); localStorage.pubkey = keyPair.publicKeyArmored; localStorage.privkey = keyPair.privateKeyArmored; login(); }).catch(function(e) { error("generating key pairs failed"); }); } /// Get Own Public Key /** @return public key object */ function publicKey() { if (typeof localStorage.pubkey == 'undefined') { if (typeof localStorage.pubKey == 'undefined') { return null; } else { localStorage.pubkey = localStorage.pubKey; localStorage.removeItem(pubKey); } } return openpgp.key.readArmored(localStorage.pubkey); } /// Get Own Private Key /** @return private key object */ function privateKey() { if (typeof localStorage.privkey == 'undefined') { if (typeof localStorage.privKey == 'undefined') { return null; } else { localStorage.privkey = localStorage.privKey; localStorage.removeItem(privKey); } } return openpgp.key.readArmored(localStorage.privkey); } /// Get Own User Name /** Get user name as user id of first public key */ function userid() { if (!publicKey() || publicKey().keys.length < 1 || publicKey().keys[0].getUserIds().length < 1) return null return publicKey().keys[0].getUserIds()[0]; } /// Clear Message Text And Attachments /** Does not remove the receiver's name */ function clearmessage() { $("#message").prop(":disabled", true); filecontent = new Array(); $('#preview').empty(); $("#msg").val(""); $("#message").prop(":disabled", false); } /// Display Image Attachments function attachments(files, id) { if (files) files.forEach(function(file) { console.log(file); var img = document.createElement('img'); img.title = file.name; if (file.type.match('^image/')) { img.src = file.content; } else { img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"; } var a = document.createElement('a'); a.href = file.content; a.target = '_blank'; a.appendChild(img); $(id).append(a); }); } /// Upload Attachment /** Prepares attachment to be sent in a message. If the attachment is an image, it resizes the image to 400px on the lager side. By now, only images are supported. Stores data in global variable @ref filecontent. */ function fileupload(evt) { if (!window.FileReader) return error("your browser does not support file upload", true); for (var i=0, f; f=evt.target.files[i]; ++i) { var file = f; console.log(file); var reader = new FileReader(); reader.onload = function(evt) { if (evt.target.error) return error("error reading file", true); if (evt.target.readyState==0) return notice("waiting for data …"); if (evt.target.readyState==1) return notice("loading data …"); if (file.type.match('^image/')) { var img = document.createElement("img"); img.onload = function() { var MAX = 400; var width = img.width; var height = img.height; if (width > MAX) { height *= MAX / width; width = MAX; } if (height > MAX) { width *= MAX / height; height = MAX; } var canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); img.onload = function() { filecontent.push({name:file.name, type: file.type, content: img.src}); $("#preview").append(img); success('image of type '+file.type+' is ready to be sent'); } img.title = file.name; img.src = canvas.toDataURL(file.type); } img.src=evt.target.result; } else { filecontent.push({name:file.name, type: file.type, content: evt.target.result}); var img = document.createElement("img"); img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"; img.title = file.name; $("#preview").append(img); } } reader.readAsDataURL(file); } } /// Sets Receiver's Name /** Called when clicked on a receiver's name. Sets focus to the message text field. @param name The receiver's name. */ function setreceiver(name) { $("#recv").val(name); checkpartner(name); $("#msg").focus(); } var userMap = null; function users(userlist) { console.log("rcv-> users"); userMap = new Array(); $("#allusers").empty(); userlist.forEach(function(usr) { userMap[usr.name] = usr.pubkey; $("#allusers").append('