/*! @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 /// 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").fadeOut("slow", function() { $("#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('error'); console.log("error: "+JSON.stringify(data)); } } else { $("#status").html('error'); console.log("error"); } $("#status").fadeIn("slow"); if (!stay) setTimeout(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").fadeOut("slow", function() { $("#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").fadeIn("slow"); }); } /// Show notice messsage /** Fades in an success message and logs to console. @param text (optional) The data is a string. */ function success(text) { $("#status").fadeOut("slow", function() { $("#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").fadeIn("slow"); }); } /// Show status message in the main screen area /** @param text Text is a message or some complex HTML from the server. @param msg The success message text */ function status(text, msg) { $("#main").fadeOut("slow", function() { $("#main").html(text); success(msg); $("#main").fadeIn("slow", function() { $("form input:first-child").focus(); }) }); } var getLoopTimeout = null; ///< store get timeout to make sure only one is running /// Set timeout for next get request /** @param time timeout time in ms, defaults to 10000 */ function getLoop(time) { if (!time) time = 10000; getLoopStop(); getLoopTimeout = setTimeout(get, time); } /// Stop get loop if it is running function getLoopStop() { if (getLoopTimeout) clearTimeout(getLoopTimeout); getLoopTimeout = null; } /// Alert user /** Alert user, e.g. that a new message has arrived. @param */ function alert() { navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate; if (navigator.vibrate) { // vibration API supported navigator.vibrate(1000); } (new Audio("A-Tone-His_Self-1266414414.mp3")).play(); } /// Toggle Menu Display function togglemenu() { $("#menu").toggle(); } /// Download Profile Backup function backup() { getLoopStop(); status("
Starting backup download ...
"); 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())+ "-safechat.bak"; var clickEvent = new MouseEvent("click", { "view": window, "bubbles": true, "cancelable": false }); download.dispatchEvent(clickEvent); togglemenu(); setTimeout(start, 2000); } /// Upload Profile Backup function restore(evt) { getLoopStop(); status("Starting backup restore ...
"); 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; setTimeout(start, 2000); } reader.readAsText(file); } } /// Configure local groups /** ... */ function groups() { } /// Check if user name is available /** Calls checknewuser.php on server and displays an error, if the user name is already in use. This function is used when creating a new user. It immediately gives the user a feedback, whether the chosen user name is available or not. Called when user edits the user name fields. Sets @ref username and checks @ref password - if both are well defined, enables the submit button. @param user User name to check. */ function checkuser(user) { $("#register").submit(function(event) { return false; }); $.post("checknewuser.php", {user: user}) .done(function(res) { username=JSON.parse(res); if (!username||username.length<1) username=null; $("#createuser").prop("disabled", !(username && password)); if (username) { if (password) success("user is ready to be created"); else notice("user name is available, please set password"); } else notice("user name is not available"); }).fail(function(res) { username=null; $("#createuser").prop("disabled", !(username && password)); error("offline"); }); } /// 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 notice("passwords don't match"); } /// 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; }); $.post("checknewuser.php", {user: user}) .done(function(res) { if (JSON.parse(res)) { notice("receiver does not exist"); $("#send").prop("disabled", true); return; } $("#send").prop("disabled", false); success("receiver exists"); }).fail(function(res) { error("offline", true); $("#send").prop("disabled", true); }); } /// Create Local Public-/Private-Key Pair /** Called if user has not yet his keys, just generates a new key pair. */ function createkeypair(user, pwd) { status("generate keys"); openpgp.generateKeyPair({ numBits: 1024, userId: user, passphrase: pwd }).then(function(keyPair) { success("keys generated"); localStorage["pubKey"] = keyPair.publicKeyArmored; localStorage["privKey"] = keyPair.privateKeyArmored; login(); }).catch(function(e) { error(e); }); } /// Get Own Public Key /** @return public key object */ function publicKey() { if (typeof localStorage["pubKey"] == 'undefined') return null; return openpgp.key.readArmored(localStorage["pubKey"]); } /// Get Own Private Key /** @return private key object */ function privateKey() { if (typeof localStorage["privKey"] == 'undefined') return null; 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() { filecontent = new Array(); $('#preview').empty(); $("#msg").val(""); notice("message cleared"); } /// Display Image Attachments function attachments(files, id) { if (files) files.forEach(function(file) { if (file.content.length<100000) { var img = document.createElement('img'); img.src = file.content; $(id).append(img); } }); } /// 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 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 ..."); if (!file.type.match('^image/')) return error(file.name+": not an image", true); 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({type: file.type, content: img.src}); $("#preview").append(img); success('image of type '+file.type+' is ready to be sent'); } img.src = canvas.toDataURL(file.type); } img.src=evt.target.result; } 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 startmsg = 0; ///< number of last downloaded message /// Poll For New Messages, Get And Show Them /** The global variable @ref startmsg stores the id of the last downloaded message. This function is called by timer in regulary periods. It calls get on server and passes @ref startmsg. The server returns all newer messages. They are then decrypted. If decryption is successful, then the message is shown, including attachments. If decryption fails, the message is sent to someone else, so failure is simply ignored. Beeps a sound once, if new messages have been displayed. */ function get() { var beeped = false; // beep only once $.post("get.php", {start: startmsg}) .done(function(res) { // new messages from server received var msgs = JSON.parse(res); if (msgs) { msgs.forEach(function(e) { // one single message if (startmsg