UNPKG

ihave.to

Version:

Catch ideas. As they come and let them grow with your team in real time

662 lines (543 loc) 22.1 kB
/*global $*/ /*global io*/ /*global CONF*/ /*global Board*/ /*global Apprise*/ /*global CryptoJS*/ /*global Template*/ /*global Screens*/ /*global Timeline*/ /*global showMessage*/ var Connection; (function () { "use strict"; /** * The handler for websocket connection and sync * The initial creating of an board and further security usages are placed here * After init a new board the data is encrypted in AES before push them to server * * @module Client * @submodule Classes * @class Connection * @constructor */ Connection = function () { this.socket = io.connect(); this.socket.on('disconnect', function () { showMessage('RECONNECTING'.translate(), 'error'); $('.screen, #cmd').empty(); CONF.COM.SOCKET.connect(); }); }; /** * The socket.io connection * @property socket * @type {null} */ Connection.prototype.socket = null; /** * Here the encryption phrase is stored * @property _encryption * @type {null|String} * @private */ Connection.prototype._encryption = null; /** * The whole board as AES encrypted string * AES encryption is enabled after first initialisation of a board * @property _data * @type {String} * @private */ Connection.prototype._data = null; /** * The verifier which is required for write permissins after handshake * and need to be decrypted out of the requested boardfile * @property _verifier * @type {String} * @private */ Connection.prototype._verifier = null; /** * The SHA-3 hashed name of the board * * @property _board * @type {String} * @private */ Connection.prototype._board = null; /** * Connect to Socket.io server * * @method connect * @param {Object} board */ Connection.prototype.connect = function (board) { var self = this; if (this._board === null && board !== undefined) { this._board = board; } if (this._board !== undefined && this._board !== null) { this.socket.emit('init', this._board); this.socket.on('connected', function (data) { $('#cmd').removeAttr('style'); // regulary this._data is an crypted string and need to be decrypted for further use self.setData(data); self.handleData(); }); //this.socket.on('notify', function (data) { //showMessage(data.message, data.type); //}); } else { window.location.reload(); } }; /** * Initialize all listeners to merge diffs on change on other clients which have the same * board openened and do a change on it * * @method initBroadcast * @param {String} sVerifier */ Connection.prototype.initBroadcast = function (sVerifier) { var self = this; // Notify about entrance of person this.socket.on('enter-' + sVerifier, function (data) { showMessage(self.decrypt(data) + ' ' + 'ENTERED_BOARD'.translate()); }); // Notify about exit of person this.socket.on('goodbye-' + sVerifier, function (data) { showMessage(self.decrypt(data) + ' ' + 'LEFT_BOARD'.translate(), 'error'); }); this.socket.on('bc-' + sVerifier, function (data) { var oScreen; var iTimeEntry; var sScreenName; var sFirstScreen; var sActiveScreen; var oBoard; var oDiff = JSON.parse(CONF.COM.SOCKET.decrypt(data)); // Patch the local board representation $.extend(true, CONF.BOARD, oDiff); sActiveScreen = CONF.DOM.BOARDPOSTS.data('activescreen'); // Detect if screen object contains changes and if so cleanup screen object if (oDiff.PRIVATE !== undefined && oDiff.PRIVATE.SCREENS !== undefined) { for (sScreenName in CONF.BOARD.PRIVATE.SCREENS) { if (CONF.BOARD.PRIVATE.SCREENS.hasOwnProperty(sScreenName) && !CONF.BOARD.PRIVATE.SCREENS[sScreenName]) { delete CONF.BOARD.PRIVATE.SCREENS[sScreenName]; } } // Detect if the change affected the active screen if (oDiff.PRIVATE.SCREENS[sActiveScreen] !== undefined) { // Not good implementation (everything is recreated) if (CONF.BOARD.PRIVATE.SCREENS[sActiveScreen] !== undefined) { var bUpdated = false; // Update the existing posts for (iTimeEntry in oDiff.PRIVATE.SCREENS[sActiveScreen].POSTS) { if (oDiff.PRIVATE.SCREENS[sActiveScreen].POSTS.hasOwnProperty(iTimeEntry)) { var oItem = oDiff.PRIVATE.SCREENS[sActiveScreen].POSTS[iTimeEntry]; bUpdated = self.updateScreen(oItem); } } // Only handle a fully recreation if there are new Posts if (!bUpdated) { oScreen = CONF.BOARD.PRIVATE.SCREENS[sActiveScreen]; showMessage('RESTORE_CONSISTENCY_NOW'); oBoard = new Board({ NAME: sActiveScreen, SCREEN: oScreen, FROMTIME: false }); CONF.DOM.BOARDSCREENS.html(new Template(oBoard.getTemplate()).toHtml()); CONF.DOM.BOARD.trigger('uiBoard'); } } else { sFirstScreen = Object.keys(CONF.BOARD.PRIVATE.SCREENS)[0]; CONF.DOM.BOARDPOSTS.data('activescreen', sFirstScreen); oScreen = CONF.BOARD.PRIVATE.SCREENS[sFirstScreen]; showMessage('A_USER_DELETED_THIS_SCREEN_CHANGE_NOW'); $('#back, .mobile').trigger('click'); if ($('#edit').length === 1) { CONF.DOM.CMD.trigger('setMainNav'); } oBoard = new Board({ NAME: sFirstScreen, SCREEN: oScreen, FROMTIME: false }); CONF.DOM.BOARDSCREENS.html(new Template(oBoard.getTemplate()).toHtml()); CONF.DOM.BOARD.trigger('uiBoard'); } } self.updateCurrentView(); } // Detect if a user was added if (oDiff.USERS !== undefined) { var sUserName = Object.keys(oDiff.USERS)[0]; showMessage(sUserName + ' ' + 'WAS_ADDED_TO_BOARD'.translate()); } }); }; /** * The handler for serveral view updates * @method updateCurrentView */ Connection.prototype.updateCurrentView = function () { if (CONF.DOM.UIWINDOW.children('.cmd').children('.screen').length > 0) { this.updateScreenOverview(); } if (CONF.DOM.UIWINDOW.children('.cmd').children('.lifecycles').length > 0) { this.updateLifecycleOverview(); } }; /** * Update the ScreenOverview and create an updated expected Screenoverview * @method updateScreenOverview */ Connection.prototype.updateScreenOverview = function () { var i; var aScreens = []; var oCurrentScreenItem; var oTrashVButton = $('#trash_empty.active, #trash_full'); if (oTrashVButton.length === 1) { $.each(CONF.DOM.UIWINDOW.children('.cmd').children('.screen.do'), function () { aScreens.push($(this).attr('id')); }); oTrashVButton.prev().remove(); oTrashVButton.removeAttr('id').attr('id', 'trash_empty').removeClass('active'); } CONF.DOM.UIWINDOW.children('.cmd').children('.screen').remove(); CONF.DOM.UIWINDOW.children('.cmd').append(new Template(new Screens().getOverview()).toHtml()); if (aScreens.length > 0) { $('#trash_empty').trigger(CONF.EVENTS.CLICK); for (i = 0; i < aScreens.length; i += 1) { oCurrentScreenItem = $('#' + aScreens[i]); if (oCurrentScreenItem.length === 1) { oCurrentScreenItem.trigger(CONF.EVENTS.CLICK); } } } }; /** * Update the ScreenOverview and create an updated expected Screenoverview * @method updateScreenOverview */ Connection.prototype.updateLifecycleOverview = function () { var sActiveScreen = CONF.DOM.BOARDPOSTS.data('activescreen'); CONF.DOM.UIWINDOW.trigger('showUi'); CONF.DOM.CMD.trigger('setTimelineNav'); var oTimeline = new Timeline(CONF.BOARD.PRIVATE.SCREENS[sActiveScreen].POSTS, CONF.BOARD.SETTINGS.COLORS); CONF.DOM.UIWINDOW.children('.cmd').html(oTimeline.render()); }; /** * Update the curretn screen if theres an incoming change * @method updateScreen * @param {Object} oItem * @return {Boolean} */ Connection.prototype.updateScreen = function updateScreen(oItem) { var i; var sUser; var oTarget; var sRMClasses; var bUpdated = false; // Array means new Post (content and color) if (!( oItem instanceof Array)) { if (oItem.TGT !== undefined) { oTarget = $('div.screen').find('#' + oItem.TGT); // If Target exists if (oTarget.length === 1) { // Tell further process no further consistency operations are required bUpdated = true; // Update Position if (oItem.ACN === 'position') { $(oTarget).animate({ left: oItem.TO[0] + '%', top: oItem.TO[1] + '%' }, 750); } // Update the Content if (oItem.ACN === 'content') { $(oTarget).find('.content').children('p').html(oItem.TO); } // Handle Post deletion (if an update comes after the postit willl be recreated) if (oItem.ACN === 'deleted') { $(oTarget).fadeOut(250, function () { $(this).remove(); }); } if (oItem.ACN === 'color') { sRMClasses = Object.keys(CONF.BOARD.SETTINGS.COLORS).join(' ').toLowerCase(); // Remove class to change to from string sRMClasses = sRMClasses.replace(oItem.TO, ''); $(oTarget).removeClass(sRMClasses).addClass(oItem.TO); } for (sUser in CONF.BOARD.USERS) { if (CONF.BOARD.USERS.hasOwnProperty(sUser) && CONF.BOARD.USERS[sUser] === oItem.BY) { break; } } // Tells who did what but i think it not nice //showMessage(sUser + ' ' + sChange.translate(), 'warning'); } } } else { for (i = 0; i < oItem.length; i += 1) { if (!bUpdated) { bUpdated = this.updateScreen(oItem[i]); } else { this.updateScreen(oItem[i]); } } } return bUpdated; }; /** * Handle the data to get synced to server and other online clients * @method handleData */ Connection.prototype.handleData = function () { var sActiveScreen; var sFirstScreen; var oInitialBoard; var sInitScreenName; var iTargetId; // Will fail if get data is already aes crypte if (this.getData().indexOf('{') === -1) { // Actually aes crypted so encrypt it with the before entered password CONF.BOARD = JSON.parse(this.decrypt()); // ADD BROADCAST LISTNER this.initBroadcast(CONF.BOARD.META.VERIFIER); if (!window.lock) { this.personalize(); } // Setup initial screen CONF.DOM.UIWINDOW.trigger('hideUi'); CONF.DOM.CMD.trigger('setMainNav'); showMessage('BOARD_WAS_ENCRYPTED'); if (CONF.DOM.BOARDPOSTS !== null) { sActiveScreen = CONF.DOM.BOARDPOSTS.data('activescreen'); } // Get the First screen name on Board //@LOCALSTORAGE (the last screen used on this machine) sFirstScreen = Object.keys(CONF.BOARD.PRIVATE.SCREENS)[0]; // If the board is reconnecting get the currently active screen if (sActiveScreen !== undefined) { if (CONF.BOARD.PRIVATE.SCREENS[sActiveScreen] !== undefined) { sFirstScreen = sActiveScreen; } } // Render the Board CONF.DOM.BOARD.trigger('setupBoard', { NAME: sFirstScreen, SCREEN: CONF.BOARD.PRIVATE.SCREENS[sFirstScreen], FROMTIME: false }); } else { // If parsing as json wil not fail its a new Board and // further execution is ensured oInitialBoard = JSON.parse(this.getData()); // Here the initial settings could be made sInitScreenName = 'WORKSPACE'.translate(); iTargetId = parseInt(new Date().getTime(), 10); // Setup the First Workspace with a First postit on it oInitialBoard.PRIVATE.SCREENS[sInitScreenName] = { META: { BG: CONF.PROPS.STRING.SCREEN_DEFAULT_BG }, POSTS: {} }; oInitialBoard.PRIVATE.SCREENS[sInitScreenName].POSTS[iTargetId] = [ { TGT: iTargetId, ACN: 'color', TO: 'blue' }, { TGT: iTargetId, ACN: 'content', TO: 'EXAMPLE_TEXT'.translate() }, { TGT: iTargetId, ACN: 'position', TO: [10, 10] } ]; // Convert board back to String before crypt it first time back CONF.COM.SOCKET.setData(JSON.stringify(oInitialBoard)); // The verifier is required to verify the permisson to write into the Board file this.setVerifier(oInitialBoard.META.VERIFIER); this.socket.emit(this.getVerifier(), this.encrypt().toString()); $('#do-login').trigger(CONF.EVENTS.CLICK); } }; /** * Saves the changes and push it on the server (after encryption) * * @method saveChanges * @param {Object} oDiff (optional) The difference to share with all current viewers */ Connection.prototype.saveChanges = function (oDiff) { this.setData(JSON.stringify(CONF.BOARD)); // Push to server this.socket.emit(CONF.BOARD.META.VERIFIER, this.encrypt().toString()); // Syncing diff to the clients (best would be in the right direction) if (oDiff !== undefined) { this.socket.emit('sync', this.encrypt(JSON.stringify(oDiff)).toString()); } }; /** * Set the Passphrase for decrypt/encrypt the board * @method setEncryptionPhrase * @param {String} sEncryption */ Connection.prototype.setEncryptionPhrase = function (sEncryption) { this._encryption = CryptoJS.SHA3(sEncryption).toString(); }; /** * Get the user defined password * @method getEncryptionPhrase * @return {String} the useres selected password */ Connection.prototype.getEncryptionPhrase = function () { return this._encryption; }; /** * Encrypt the diffdata or if no diff given the complete board code (JSON reperesentation) * @method encrypt * @param {String} diffdata (only required if not the complete board should be crypted e.g. the diffdata) * @return {String} the AES crypted Board representation */ Connection.prototype.encrypt = function (diffdata) { var diffInternal = diffdata; // If no data was given take the whole board if (diffInternal === undefined) { diffInternal = this.getData(); } return CryptoJS.AES.encrypt(diffInternal, this.getEncryptionPhrase()); }; /** * Decrypt the diffdata or the complete board * @method decrypt * @param {String} diffdata */ Connection.prototype.decrypt = function (diffdata) { var sJson = null; var bSuccess = false; var diffInternal = diffdata; // If no diffdata was given take the complete data (whole board) in memory if (diffInternal === undefined) { diffInternal = this.getData(); } while (!bSuccess) { try { sJson = CryptoJS.AES.decrypt(diffInternal, this.getEncryptionPhrase()).toString(CryptoJS.enc.Utf8); bSuccess = true; } catch (e) { bSuccess = false; } } return sJson; }; /** * Fill the holder for the board representation * Note: * Normally this string is the AES crypted representation of the whole board BUT initially it's the unencrypted initial Board * @method setData * @param {String} data */ Connection.prototype.setData = function (data) { this._data = data; }; /** * Return the AES crypted representation of the board * @method getData * @return {String} the representation of the Board normally AES crypted */ Connection.prototype.getData = function () { return this._data; }; /** * The Verifier of the Board (the file the is stored in) * The verifier is generated on the serverside and will be stored inside the crypted boardfile * So a successfull decryption is required to get write privileges * @method setVerifier * @param {String} verifier is a SHA3 representation */ Connection.prototype.setVerifier = function (verifier) { this._verifier = verifier; }; /** * Gives the verifier back wich was generated on the server and stored inside the Boardfile * @method getVerifier * @return {String} the verifier */ Connection.prototype.getVerifier = function () { return this._verifier; }; /** * Get the username * @method personalize */ Connection.prototype.personalize = function () { var sBoardName = this._board; var sUserName = CONF.PROPS.OBJECT.STORAGE.getItem(sBoardName); var self = this; if (sUserName === null) { window.lock = true; Apprise('ENTER_YOUR_NAME'.translate(), { animation: 250, // Animation speed buttons: { confirm: { action: function (e) { var sName; var iIdUser; var oDiff; // For better mobile integration $('input').blur(); delete window.lock; sName = (e.input !== null && e.input.length > 0) ? e.input : 'Anonymous'; CONF.PROPS.OBJECT.STORAGE.setItem(sBoardName, sName); if (CONF.BOARD.USERS[sName] === undefined) { iIdUser = Object.keys(CONF.BOARD.USERS).length; oDiff = JSON.parse('{"USERS":{}}'); oDiff.USERS[sName] = iIdUser; CONF.BOARD.USERS[sName] = iIdUser; self.saveChanges(oDiff); } // Set the User ID CONF.PROPS.INT.WHO = CONF.BOARD.USERS[sName]; Apprise('close'); }, className: 'blue', id: 'confirm', text: 'OK'.translate() } }, input: true, override: true }); } else { var sStoredName; // reset if a user comes back with name from deleted board if (CONF.BOARD.USERS[sUserName] === undefined) { CONF.PROPS.OBJECT.STORAGE.removeItem(sBoardName); this.personalize(); } // If user is known set User id and send welcome message else { for (sStoredName in CONF.BOARD.USERS) { if (CONF.BOARD.USERS.hasOwnProperty(sStoredName)) { if (sUserName === sStoredName) { CONF.PROPS.INT.WHO = CONF.BOARD.USERS[sStoredName]; CONF.COM.SOCKET.socket.emit('enter', this.encrypt(sUserName).toString()); break; } } } } } }; })();