added some comments and dokus
This commit is contained in:
@@ -1,4 +1,20 @@
|
||||
<?php
|
||||
/*! @file
|
||||
|
||||
API-call checknewuser.php
|
||||
|
||||
Check if a user exists in the server's user table.
|
||||
|
||||
@param user user name to check
|
||||
@return json encoded value:
|
||||
- 'user name as string', if user does exist
|
||||
- null, if user does not exist or in case of error
|
||||
|
||||
@id $Id$
|
||||
*/
|
||||
// 1 2 3 4 5 6 7 8
|
||||
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
|
||||
require_once("usertable.php");
|
||||
try {
|
||||
$user = $db->real_escape_string($_REQUEST['user']);
|
||||
|
54
html/documentation.dox
Normal file
54
html/documentation.dox
Normal file
@@ -0,0 +1,54 @@
|
||||
/*! @file
|
||||
|
||||
@id $Id$
|
||||
*/
|
||||
// 1 2 3 4 5 6 7 8
|
||||
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
|
||||
/** @page protocol SafeChat Protocol
|
||||
|
||||
@tableofcontents
|
||||
|
||||
@section security Security Concept
|
||||
|
||||
Neither the password nor the private key are sent to the
|
||||
server. They remain under the user's control and in the user's
|
||||
property. Only the user name and the public key are sent to the
|
||||
server.
|
||||
|
||||
- The password is only kept in the browser's transient memory.
|
||||
- The private key is kept in encrypted form in the browser's
|
||||
persistent local storage.
|
||||
- The public key is stored on server, so that other users can
|
||||
lookup for a user's public key.
|
||||
|
||||
There are two secret security tokens: The password, that is in the
|
||||
user's mind and the private key, which is in the user's device, in
|
||||
the local storage of his browser. Messages can only be sent or
|
||||
read with access to both security tokens.
|
||||
|
||||
@section newuser Create New User
|
||||
|
||||
If no credentials exist in the browser's local storage, the
|
||||
browser asks the user for a user name and a password and creates a
|
||||
private key that is encrypted with the password.
|
||||
|
||||
In the login(), the browser sends the user's name and public key
|
||||
to the server. The server creates a new user, if the user does not
|
||||
exist yet. Then the server returns, whether user name and public
|
||||
key match to what he has in his table.
|
||||
|
||||
@msc
|
||||
user, browser, server;
|
||||
user -> browser [label="https://safechat.ch"];
|
||||
browser -> server [label="index.html"];
|
||||
browser <- server [label="safechat.js",URL="\ref safechat.js"];
|
||||
user <- browser [label="register new user"];
|
||||
user -> browser [label="username / password"];
|
||||
browser -> browser [label="create openpgp-public/private keys"];
|
||||
browser -> server [label="login.php(username, public-key)"];
|
||||
server -> server [label="if user name does not exist:\nstore username/public-key"];
|
||||
server -> browser [label="success"];
|
||||
@endmsc
|
||||
|
||||
*/
|
@@ -1,10 +1,34 @@
|
||||
<?php
|
||||
|
||||
/// Send Error To Client
|
||||
/** @return error message from server to client
|
||||
|
||||
Function calls exit to terminate.
|
||||
|
||||
Message format is json:
|
||||
@code
|
||||
{
|
||||
success: false,
|
||||
txt: 'error message string';
|
||||
}
|
||||
@endcode */
|
||||
function error($txt) {
|
||||
echo json_encode(array('success' => false, 'txt' => $txt));
|
||||
exit;
|
||||
}
|
||||
|
||||
/// Send Success To Client
|
||||
/** @return success message from server to client
|
||||
|
||||
Function calls exit to terminate.
|
||||
|
||||
Message format is json:
|
||||
@code
|
||||
{
|
||||
success: true,
|
||||
txt: 'success message string';
|
||||
}
|
||||
@endcode */
|
||||
function success($txt) {
|
||||
echo json_encode(array('success' => true, 'txt' => $txt));
|
||||
exit;
|
||||
|
@@ -6,15 +6,19 @@
|
||||
EXTRA_DIST = ${www_DATA}
|
||||
|
||||
wwwdir = ${pkgdatadir}/html
|
||||
www_DATA = index.html chat.html newuser.html safechat.js jquery.js \
|
||||
openpgp.js jquery.cssemoticons.js safechat.css \
|
||||
jquery.cssemoticons.css checknewuser.php get.php login.php \
|
||||
messagetable.php pubkey.php send.php usertable.php \
|
||||
abort.svg A-Tone-His_Self-1266414414.mp3 attachment.svg \
|
||||
audio.svg chat-rodrigo-angleton.svg \
|
||||
Checkout-Scanner-Beep-SoundBible.com-593325210-by-Mike-Koenig.mp3 \
|
||||
envelope.svg functions.php menu.svg pfeil.svg photo.png \
|
||||
photo.svg safechat-rodrigo-angleton.svg safe-mimooh.svg \
|
||||
send.svg update-messages.js video.png video.svg
|
||||
dist_www_DATA = index.html chat.html newuser.html safechat.js \
|
||||
jquery.js openpgp.js jquery.cssemoticons.js \
|
||||
safechat.css jquery.cssemoticons.css checknewuser.php \
|
||||
get.php login.php messagetable.php pubkey.php \
|
||||
send.php usertable.php abort.svg \
|
||||
A-Tone-His_Self-1266414414.mp3 attachment.svg \
|
||||
audio.svg chat-rodrigo-angleton.svg \
|
||||
Checkout-Scanner-Beep-SoundBible.com-593325210-by-Mike-Koenig.mp3 \
|
||||
envelope.svg functions.php menu.svg pfeil.svg \
|
||||
photo.png photo.svg safechat-rodrigo-angleton.svg \
|
||||
safe-mimooh.svg send.svg update-messages.js video.png \
|
||||
video.svg
|
||||
|
||||
EXTRA_DIST = documentation.dox
|
||||
|
||||
MAINTAINERCLEANFILES = makefile.in
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<h2>Register User (Step 1 of 1)</h2>
|
||||
<form id="register" onsubmit="createNewUser(this.elements['user'].value, this.elements['pwd'].value)">
|
||||
<form id="register" onsubmit="createkeypair(this.elements['user'].value, this.elements['pwd'].value)">
|
||||
<input placeholder="user name" type="text" id="user" oninput="checkuser(this.value)"/>
|
||||
<input placeholder="password" type="password" id="pwd" oninput="checkpwd(this.value, document.getElementById('pwd2').value)"/>
|
||||
<input placeholder="repeat password" type="password" id="pwd2" oninput="checkpwd(document.getElementById('pwd').value, this.value)"/>
|
||||
|
216
html/safechat.js
216
html/safechat.js
@@ -1,7 +1,51 @@
|
||||
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
|
||||
/*! @file
|
||||
|
||||
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
|
||||
|
||||
@id $Id$
|
||||
*/
|
||||
// 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")
|
||||
@@ -24,6 +68,9 @@ function error(data, stay) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Show notice messsage
|
||||
/** Fades in an notice message and logs to console.
|
||||
@param data (optional) The data is a string. */
|
||||
function notice(text) {
|
||||
$("#status").fadeOut("slow", function() {
|
||||
$("#status").addClass("notice")
|
||||
@@ -40,6 +87,9 @@ function notice(text) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Show notice messsage
|
||||
/** Fades in an success message and logs to console.
|
||||
@param data (optional) The data is a string. */
|
||||
function success(text) {
|
||||
$("#status").fadeOut("slow", function() {
|
||||
$("#status").addClass("success")
|
||||
@@ -56,6 +106,9 @@ function success(text) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@@ -66,6 +119,18 @@ function status(text, msg) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -86,6 +151,17 @@ function checkuser(user) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -100,6 +176,9 @@ function checkpwd(pwd, pwd2) {
|
||||
} 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;
|
||||
@@ -119,7 +198,9 @@ function checkpartner(user) {
|
||||
});
|
||||
}
|
||||
|
||||
function createNewUser(user, pwd) {
|
||||
/// 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,
|
||||
@@ -135,16 +216,22 @@ function createNewUser(user, pwd) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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 ||
|
||||
@@ -152,6 +239,8 @@ function userid() {
|
||||
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();
|
||||
@@ -159,6 +248,7 @@ function clearmessage() {
|
||||
notice("message cleared");
|
||||
}
|
||||
|
||||
/// Display Image Attachments
|
||||
function attachments(files, id) {
|
||||
if (files) files.forEach(function(file) {
|
||||
if (file.content.length<100000) {
|
||||
@@ -169,8 +259,16 @@ function attachments(files, id) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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();
|
||||
@@ -178,7 +276,8 @@ function fileupload(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);
|
||||
if (!file.type.match('^image/'))
|
||||
return error(file.name+": not an image", true);
|
||||
var img = document.createElement("img");
|
||||
img.onload = function() {
|
||||
var MAX = 400;
|
||||
@@ -210,49 +309,36 @@ function fileupload(evt) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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 ...");
|
||||
var base64 = btoa(evt.target.result);
|
||||
filecontent.push({type: file.type, content: base64});
|
||||
if (file.type.match('^image/')) {
|
||||
var img = document.createElement('img');
|
||||
img.src = 'data:'+file.type+';base64,' + base64;
|
||||
$("#preview").append(img);
|
||||
success('image of type '+file.type+' is ready to be sent');
|
||||
} else {
|
||||
success('file of type '+file.type+' is ready to be sent');
|
||||
}
|
||||
}
|
||||
reader.readAsBinaryString(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
|
||||
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;
|
||||
var beeped = false; // beep only once
|
||||
$.post("get.php", {start: startmsg})
|
||||
.done(function(res) {
|
||||
.done(function(res) { // new messages from server received
|
||||
var msgs = JSON.parse(res);
|
||||
if (msgs) {
|
||||
msgs.forEach(function(e) {
|
||||
msgs.forEach(function(e) { // one single message
|
||||
if (startmsg<Number(e.id)) startmsg = Number(e.id);
|
||||
$.post("pubkey.php", {user: e.user})
|
||||
$.post("pubkey.php", {user: e.user}) // get sender's key
|
||||
.done(function(pk) {
|
||||
var res=JSON.parse(pk);
|
||||
var key=openpgp.key.readArmored(res);
|
||||
@@ -262,9 +348,10 @@ function get() {
|
||||
}
|
||||
var message = openpgp.message.readArmored(e.msg);
|
||||
var privkey = privateKey().keys[0];
|
||||
if (privkey.decrypt(password))
|
||||
if (privkey.decrypt(password)) // prepare own key
|
||||
openpgp.decryptAndVerifyMessage(privkey, key.keys, message)
|
||||
.then(function(msg) {
|
||||
.then(function(msg) { // decryption succeded
|
||||
// prepend message to list of messages
|
||||
var message = JSON.parse(msg.text);
|
||||
$("#msgs") // todo: check msg.signatures[0].valid
|
||||
.prepend('<div id="id'+(e.id)+'" class="msg '+
|
||||
@@ -280,15 +367,20 @@ function get() {
|
||||
'<div class="text">'+
|
||||
message.text+
|
||||
'</div></div><div class="clear"/>');
|
||||
// show attachments
|
||||
attachments(message.files, '#id'+e.id+' .text');
|
||||
// calculate and show emoticons
|
||||
$('#id'+e.id).emoticonize();
|
||||
// beep for the first new message
|
||||
if (!beeped)
|
||||
(new Audio("A-Tone-His_Self-1266414414.mp3"))
|
||||
.play();
|
||||
beeped = true;
|
||||
success();
|
||||
})
|
||||
.catch(function(e) {
|
||||
// not for me
|
||||
success();
|
||||
});
|
||||
}).fail(function(e) {
|
||||
error("offline", true);
|
||||
@@ -298,30 +390,33 @@ function get() {
|
||||
}).fail(function(e) {
|
||||
error("offline", true)
|
||||
});
|
||||
setTimeout(get, 10000);
|
||||
setTimeout(get, 10000); // repeat every 10 seconds
|
||||
}
|
||||
|
||||
/// Send Message To Server
|
||||
/** User wants to send a message. Encrypt message with own private and
|
||||
the receiver's public key, then send it to the server. */
|
||||
function sendmessage(recv, txt) {
|
||||
notice("1/3 preparing message ...");
|
||||
$("#message").fadeOut("slow");
|
||||
$.post("pubkey.php", {user: recv})
|
||||
$.post("pubkey.php", {user: recv}) // get receiver's public key
|
||||
.done(function(pk) {
|
||||
var res=JSON.parse(pk);
|
||||
var key=openpgp.key.readArmored(res);
|
||||
if (!res||key.err) {
|
||||
$("#message").fadeIn("slow");
|
||||
error("key of receiver not found", true);
|
||||
error("receiver's key not found", true);
|
||||
return;
|
||||
}
|
||||
var privkey = privateKey().keys[0];
|
||||
privkey.decrypt(password);
|
||||
privkey.decrypt(password); // get own private key ready
|
||||
var message = JSON.stringify({text: txt, files: filecontent});
|
||||
notice("2/3 encrypting message ...");
|
||||
openpgp.signAndEncryptMessage(key.keys.concat(publicKey().keys), privkey, message)
|
||||
.then(function(msg) {
|
||||
.then(function(msg) { // message is encrypted
|
||||
notice("3/3 sending message ...");
|
||||
$.post("send.php", {user: userid(), msg: msg})
|
||||
.done(function(res) {
|
||||
.done(function(res) { // message has been sent to server
|
||||
var st = JSON.parse(res);
|
||||
if (st.success) {
|
||||
$("#message").fadeIn("slow");
|
||||
@@ -348,6 +443,12 @@ function sendmessage(recv, txt) {
|
||||
$("#message").fadeIn("slow");
|
||||
}
|
||||
|
||||
/// Check And Set Password
|
||||
/** Check if given password matches to decrypt the private key. If so,
|
||||
store it in global temporary variable @ref password and start the
|
||||
chat. The password matches, when the private key can be decrypted.
|
||||
|
||||
@param pwd The password to check. */
|
||||
function setpw(pwd) {
|
||||
if (privateKey().keys[0].decrypt(pwd)) {
|
||||
password = pwd;
|
||||
@@ -355,6 +456,11 @@ function setpw(pwd) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create Password Entry Field
|
||||
/** Asks user for password. When user starts to enter it, it is
|
||||
permanentely checked in setpw(). As soon as the password matches,
|
||||
setpw() continues automatically. No submit is required by the
|
||||
user. */
|
||||
function getpwd() {
|
||||
status('<form>'+
|
||||
' <input placeholder="password for '+userid()+
|
||||
@@ -362,6 +468,9 @@ function getpwd() {
|
||||
'</form>');
|
||||
}
|
||||
|
||||
/// Main Chat Window
|
||||
/** Gets chat widgets from server and displays them. Starts timer for
|
||||
get() which polls for new messages. */
|
||||
function chat() {
|
||||
if (!password) return getpwd();
|
||||
$.ajax({url: "chat.html", success: function(res) {
|
||||
@@ -372,6 +481,17 @@ function chat() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Login User
|
||||
/** This is not really a login, it is just some kind of validation.
|
||||
The server does not care if a user is online or not, it is only
|
||||
interesting to the client to make sure, everything is fine. User
|
||||
is logged in the following way: User name and public key are sent
|
||||
to the server. If the user name exists on the server and the
|
||||
public key is the same, the user is considered logged in, his
|
||||
credentials seem to be valid. If user does not yet exits on
|
||||
server, it is created now. If user exists, but public key is
|
||||
different, then this is a complete failure, something went
|
||||
terribly wrong. */
|
||||
function login() {
|
||||
status("login ...");
|
||||
$.post("login.php", {user: userid(),
|
||||
@@ -390,6 +510,9 @@ function login() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Get And Display Form To Create New User
|
||||
/** Shows user creation form. On submit, a private key is generated in
|
||||
createkeypair(), then login() creates the user. */
|
||||
function newuser() {
|
||||
status("new user ...");
|
||||
$.ajax({url: "newuser.html", success: function(res) {
|
||||
@@ -399,6 +522,8 @@ function newuser() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Initial Function: Startup
|
||||
/** Decide whether to login or to create a new user */
|
||||
function start() {
|
||||
try {
|
||||
status("Starting up ...");
|
||||
@@ -412,4 +537,5 @@ function start() {
|
||||
}
|
||||
}
|
||||
|
||||
/// On Load, Call @ref start
|
||||
$(start);
|
||||
|
Reference in New Issue
Block a user