/*! * jsxc v3.4.3 - 2018-12-05 * * Copyright (c) 2018 Klaus Herberth
* Released under the MIT license * * Please see https://www.jsxc.org/ * * @author Klaus Herberth * @version 3.4.3 * @license MIT */ /*! This file is concatenated for the browser. */ var jsxc = null, RTC = null, RTCPeerconnection = null; (function($) { "use strict"; /** * JavaScript Xmpp Chat namespace * * @namespace jsxc */ jsxc = { /** Version of jsxc */ version: '3.4.3', /** True if i'm the master */ master: false, /** True if the role allocation is finished */ role_allocation: false, /** Timeout for keepalive */ to: [], /** Timeout after normal keepalive starts */ toBusy: null, /** Timeout for notification */ toNotification: null, /** Timeout delay for notification */ toNotificationDelay: 500, /** Interval for keep-alive */ keepaliveInterval: null, /** True if restore is complete */ restoreCompleted: false, /** True if login through box */ triggeredFromBox: false, /** True if logout through element click */ triggeredFromElement: false, /** True if logout through logout click */ triggeredFromLogout: false, /** last values which we wrote into localstorage (IE workaround) */ ls: [], /** * storage event is even fired if I write something into storage (IE * workaround) 0: conform, 1: not conform, 2: not shure */ storageNotConform: null, /** Timeout for storageNotConform test */ toSNC: null, /** My bar id */ bid: null, /** Current state */ currentState: null, /** Current UI state */ currentUIState: null, /** Some constants */ CONST: { NOTIFICATION_DEFAULT: 'default', NOTIFICATION_GRANTED: 'granted', NOTIFICATION_DENIED: 'denied', STATUS: ['offline', 'dnd', 'xa', 'away', 'chat', 'online'], SOUNDS: { MSG: 'incomingMessage.wav', CALL: 'Rotary-Phone6.mp3', NOTICE: 'Ping1.mp3' }, REGEX: { JID: new RegExp('\\b[^"&\'\\/:<>@\\s]+@[\\w-_.]+\\b', 'ig'), URL: new RegExp(/(https?:\/\/|www\.)[^\s<>'"]+/gi), GEOURI: new RegExp(/geo:(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;crs=[\w-]+)?(?:;u=(\d+(?:\.\d+)?))?(?:;[\w-]+=(?:[\w-_.!~*'()]|%[\da-f][\da-f])+)*/) }, NS: { CARBONS: 'urn:xmpp:carbons:2', FORWARD: 'urn:xmpp:forward:0', HINTS: 'urn:xmpp:hints' }, HIDDEN: 'hidden', SHOWN: 'shown', STATE: { INITIATING: 0, PREVCONFOUND: 1, SUSPEND: 2, TRYTOINTERCEPT: 3, INTERCEPTED: 4, ESTABLISHING: 5, READY: 6 }, UISTATE: { INITIATING: 0, READY: 1 } }, /** * Parse a unix timestamp and return a formatted time string * * @memberOf jsxc * @param {Object} unixtime * @returns time of day and/or date */ getFormattedTime: function(unixtime) { var msgDate = new Date(parseInt(unixtime)); var day = ('0' + msgDate.getDate()).slice(-2); var month = ('0' + (msgDate.getMonth() + 1)).slice(-2); var year = msgDate.getFullYear(); var hours = ('0' + msgDate.getHours()).slice(-2); var minutes = ('0' + msgDate.getMinutes()).slice(-2); var dateNow = new Date(); var date = (typeof msgDate.toLocaleDateString === 'function') ? msgDate.toLocaleDateString() : day + '.' + month + '.' + year; var time = (typeof msgDate.toLocaleTimeString === 'function') ? msgDate.toLocaleTimeString() : hours + ':' + minutes; // compare dates only dateNow.setHours(0, 0, 0, 0); msgDate.setHours(0, 0, 0, 0); if (dateNow.getTime() !== msgDate.getTime()) { return date + ' ' + time; } return time; }, /** * Write debug message to console and to log. * * @memberOf jsxc * @param {String} msg Debug message * @param {Object} data * @param {String} Could be warn|error|null */ debug: function(msg, data, level) { if (level) { msg = '[' + level + '] ' + msg; } if (data) { if (jsxc.storage.getItem('debug') === true) { console.log(msg, data); } // try to convert data to string var d; try { // clone html snippet d = $("").prepend($(data).clone()).html(); } catch (err) { try { d = JSON.stringify(data); } catch (err2) { d = 'see js console'; } } jsxc.log = jsxc.log + '$ ' + msg + ': ' + d + '\n'; } else { console.log(msg); jsxc.log = jsxc.log + '$ ' + msg + '\n'; } }, /** * Write warn message. * * @memberOf jsxc * @param {String} msg Warn message * @param {Object} data */ warn: function(msg, data) { jsxc.debug(msg, data, 'WARN'); }, /** * Write error message. * * @memberOf jsxc * @param {String} msg Error message * @param {Object} data */ error: function(msg, data) { jsxc.debug(msg, data, 'ERROR'); }, /** debug log */ log: '', /** * This function initializes important core functions and event handlers. * Afterwards it performs the following actions in the given order: * *
    *
  1. If (loginForm.ifFound = 'force' and form was found) or (jid or rid or * sid was not found) intercept form, and listen for credentials.
  2. *
  3. Attach with jid, rid and sid from storage, if no form was found or * loginForm.ifFound = 'attach'
  4. *
  5. Attach with jid, rid and sid from options.xmpp, if no form was found or * loginForm.ifFound = 'attach'
  6. *
* * @memberOf jsxc * @param {object} options See {@link jsxc.options} */ init: function(options) { jsxc.runMigrations(); jsxc.changeState(jsxc.CONST.STATE.INITIATING); if (options && options.loginForm && typeof options.loginForm.attachIfFound === 'boolean' && !options.loginForm.ifFound) { // translate deprated option attachIfFound found to new ifFound options.loginForm.ifFound = (options.loginForm.attachIfFound) ? 'attach' : 'pause'; } if (options) { // override default options $.extend(true, jsxc.options, options); } // Check localStorage if (!jsxc.storage.hasSupport()) { jsxc.error("Browser doesn't support localStorage. JSXC will be disabled."); return; } /** * Getter method for options. Saved options will override default one. * * @param {string} key option key * @returns default or saved option value */ jsxc.options.get = function(key) { if (jsxc.bid) { var local = jsxc.storage.getUserItem('options') || {}; return (typeof local[key] !== 'undefined') ? local[key] : jsxc.options[key]; } return jsxc.options[key]; }; /** * Setter method for options. Will write into localstorage. * * @param {string} key option key * @param {object} value option value */ jsxc.options.set = function(key, value) { jsxc.storage.updateItem('options', key, value, true); }; jsxc.storageNotConform = jsxc.storage.getItem('storageNotConform'); if (jsxc.storageNotConform === null) { jsxc.storageNotConform = 2; } // detect language var lang; if (jsxc.storage.getItem('lang') !== null) { lang = jsxc.storage.getItem('lang'); } else if (jsxc.options.autoLang && navigator.languages && navigator.languages.length > 0) { lang = navigator.languages[0]; } else if (jsxc.options.autoLang && navigator.language) { lang = navigator.language; } else { lang = jsxc.options.defaultLang; } var availableLanguages = Object.keys(window.jsxcLanguageResources); if (availableLanguages.indexOf(lang) < 0) { var languagePrefix = lang.slice(0, 2); if (availableLanguages.indexOf(languagePrefix) > -1) { lang = languagePrefix; } else { var prefixMatch = availableLanguages.filter(function(l) { return l.slice(0, 2) === languagePrefix; }); if (prefixMatch.length > 0) { lang = prefixMatch[0]; } } } // initialize i18next translator window.i18next.init({ lng: lang, fallbackLng: 'en', resources: window.jsxcLanguageResources, returnNull: false, debug: jsxc.storage.getItem('debug') === true, interpolation: { prefix: '__', suffix: '__' } }, function() { window.jqueryI18next.init(window.i18next, $, { tName: 't', i18nName: 'i18next', handleName: 'localize', }); }); if (jsxc.storage.getItem('debug') === true) { jsxc.options.otr.debug = true; } // Register event listener for the storage event window.addEventListener('storage', jsxc.storage.onStorage, false); $(document).on('attached.jsxc', jsxc.registerLogout); $(document).on('disconnected.jsxc', jsxc.relogin); var isStorageAttachParameters = jsxc.storage.getItem('rid') && jsxc.storage.getItem('sid') && jsxc.storage.getItem('jid'); var isOptionsAttachParameters = jsxc.options.xmpp.rid && jsxc.options.xmpp.sid && jsxc.options.xmpp.jid; var isForceLoginForm = jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'force' && jsxc.isLoginForm(); // Check if we have to establish a new connection if ((!isStorageAttachParameters && !isOptionsAttachParameters) || isForceLoginForm) { if (jsxc.relogin()) { return; } jsxc.prepareNewConnection(); } else if (!jsxc.isLoginForm() || (jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'attach')) { // Restore old connection jsxc.changeState(jsxc.CONST.STATE.PREVCONFOUND); if (typeof jsxc.storage.getItem('alive') === 'undefined') { jsxc.onMaster(); } else { jsxc.checkMaster(); } } }, runMigrations: function() { var lastUsedVersion = jsxc.storage.getItem('version'); if (!lastUsedVersion) { var keys = Object.keys(localStorage).filter(function(key) { var isKeepMatch = key.match(/^jsxc:[^:]+:(key|history|msg|priv_fingerprint):?/); return (key.match(/^jsxc:/) && !isKeepMatch) || key.match(/^strophe\.caps\./) }); keys.forEach(function(key) { localStorage.removeItem(key); }); jsxc.debug('I turned out your storage and deleted ' + keys.length + ' entries.'); } if (lastUsedVersion !== jsxc.version) { jsxc.storage.setItem('version', jsxc.version); } }, prepareNewConnection: function() { // clean up rid and sid jsxc.storage.removeItem('rid'); jsxc.storage.removeItem('sid'); // Looking for a login form if (!jsxc.isLoginForm()) { jsxc.changeState(jsxc.CONST.STATE.SUSPEND); if (jsxc.options.displayRosterMinimized()) { // Show minimized roster jsxc.storage.setUserItem('roster', 'hidden'); jsxc.gui.roster.init(); jsxc.gui.roster.noConnection(); } return; } jsxc.changeState(jsxc.CONST.STATE.TRYTOINTERCEPT); if (typeof jsxc.options.formFound === 'function') { jsxc.options.formFound.call(); } // create jquery object var form = jsxc.options.loginForm.form = $(jsxc.options.loginForm.form); var events = form.data('events') || { submit: [] }; var submits = []; // save attached submit events and remove them. Will be reattached // in jsxc.submitLoginForm $.each(events.submit, function(index, val) { submits.push(val.handler); }); form.data('submits', submits); form.off('submit'); // Add jsxc login action to form form.submit(function(ev) { ev.preventDefault(); jsxc.prepareLogin(function(settings) { if (settings !== false) { // settings.xmpp.onlogin is deprecated since v2.1.0 var enabled = (settings.loginForm && settings.loginForm.enable) || (settings.xmpp && settings.xmpp.onlogin); enabled = enabled === "true" || enabled === true; if (enabled) { jsxc.options.loginForm.triggered = true; jsxc.xmpp.login(jsxc.options.xmpp.jid, jsxc.options.xmpp.password); return; } } jsxc.submitLoginForm(); }); // Trigger submit in jsxc.xmpp.connected() return false; }); jsxc.changeState(jsxc.CONST.STATE.INTERCEPTED); }, /** * Attach to previous session if jid, sid and rid are available * in storage or options (default behaviour also for {@link jsxc.init}). * * @memberOf jsxc */ /** * Start new chat session with given jid and password. * * @memberOf jsxc * @param {string} jid Jabber Id * @param {string} password Jabber password */ /** * Attach to new chat session with jid, sid and rid. * * @memberOf jsxc * @param {string} jid Jabber Id * @param {string} sid Session Id * @param {string} rid Request Id */ start: function() { var args = arguments; if (jsxc.role_allocation && !jsxc.master) { jsxc.debug('There is an other master tab'); return false; } if (jsxc.xmpp.conn && jsxc.xmpp.conn.authenticated) { jsxc.debug('We are already connected and authenticated'); return false; } if (jsxc.currentState !== jsxc.CONST.STATE.INTERCEPTED && jsxc.currentState !== jsxc.CONST.STATE.SUSPEND) { if (!jsxc.busy) { jsxc.debug('I am currently busy and will try again later. Please be patient.'); } jsxc.busy = true; setTimeout(function() { jsxc.start.apply(jsxc, args); }, 400); return; } jsxc.busy = false; if (args.length === 3) { $(document).one('attached.jsxc', function() { // save rid after first attachment jsxc.xmpp.onRidChange(jsxc.xmpp.conn._proto.rid); jsxc.onMaster(); }); } jsxc.checkMaster(function() { jsxc.xmpp.login.apply(this, args); }); }, relogin: function() { jsxc.debug('Try to relogin'); var jid = jsxc.storage.getItem('jid'); jsxc.bid = jsxc.bid || (jid ? jsxc.jidToBid(jid) : null); if (!jsxc.bid || jsxc.storage.getUserItem('forcedLogout')) { jsxc.debug('Logout was forced or I found no valid jid'); return false; } var xmppOptions = jsxc.options.get('xmpp'); if (xmppOptions.url && (xmppOptions.jid || (xmppOptions.username && xmppOptions.domain)) && xmppOptions.password) { xmppOptions.jid = xmppOptions.jid || (xmppOptions.username + '@' + xmppOptions.domain); jsxc.start(xmppOptions.jid, xmppOptions.password); return true; } var loadSettingsAllKnowing = jsxc.storage.getUserItem('loadSettingsAllKnowing'); if (xmppOptions.url && loadSettingsAllKnowing) { jsxc.options.loadSettings(null, null, function(settings) { jsxc._prepareLogin(null, null, function(settings) { if (settings !== false && jsxc.options.xmpp.jid && jsxc.options.xmpp.password) { $(document).on('connfail.jsxc', reloginFailed); $(document).on('authfail.jsxc', reloginFailed); $(document).on('connected.jsxc', removeReloginHandler); jsxc.start(jsxc.options.xmpp.jid, jsxc.options.xmpp.password); delete jsxc.options.xmpp.password; } else { reloginFailed(); } function reloginFailed() { jsxc.debug('Could not relogin.'); removeReloginHandler(); jsxc.storage.removeUserItem('loadSettingsAllKnowing'); jsxc.prepareNewConnection(); } function removeReloginHandler() { $(document).off('connfail.jsxc', reloginFailed); $(document).off('authfail.jsxc', reloginFailed); $(document).off('connected.jsxc', removeReloginHandler); } }, settings); }); return true; } jsxc.debug('I am not able to relogin'); return false; }, registerLogout: function() { // Looking for logout element if (jsxc.options.logoutElement !== null && $(jsxc.options.logoutElement).length > 0) { var logout = function(ev) { ev.stopPropagation(); ev.preventDefault(); jsxc.options.logoutElement = $(this); jsxc.triggeredFromLogout = true; jsxc.xmpp.logout(); }; jsxc.options.logoutElement = $(jsxc.options.logoutElement); jsxc.options.logoutElement.off('click', null, logout).one('click', logout); } }, /** * Returns true if login form is found. * * @memberOf jsxc * @returns {boolean} True if login form was found. */ isLoginForm: function() { return jsxc.options.loginForm.form && jsxc.el_exists(jsxc.options.loginForm.form) && jsxc.el_exists(jsxc.options.loginForm.jid) && jsxc.el_exists(jsxc.options.loginForm.pass); }, /** * Load settings and prepare jid. * * @memberOf jsxc * @param {string} username * @param {string} password * @param {function} cb Called after login is prepared with result as param */ prepareLogin: function(username, password, cb) { if (typeof username === 'function') { cb = username; username = null; } username = username || $(jsxc.options.loginForm.jid).val(); password = password || $(jsxc.options.loginForm.pass).val(); if (!jsxc.triggeredFromBox && (jsxc.options.loginForm.onConnecting === 'dialog' || typeof jsxc.options.loginForm.onConnecting === 'undefined')) { jsxc.gui.showWaitAlert($.t('Logging_in')); } var settings; if (typeof jsxc.options.loadSettings === 'function') { settings = jsxc.options.loadSettings.call(this, username, password, function(s) { jsxc._prepareLogin(username, password, cb, s); }); if (typeof settings !== 'undefined') { jsxc._prepareLogin(username, password, cb, settings); } } else { jsxc._prepareLogin(username, password, cb); } }, /** * Process xmpp settings and save loaded settings. * * @private * @memberOf jsxc * @param {string} username * @param {string} password * @param {function} cb Called after login is prepared with result as param * @param {object} [loadedSettings] additonal options */ _prepareLogin: function(username, password, cb, loadedSettings) { if (loadedSettings === false) { jsxc.warn('No settings provided'); cb(false); return; } // prevent to modify the original object var settings = $.extend(true, {}, jsxc.options); if (loadedSettings) { // overwrite current options with loaded settings; settings = $.extend(true, settings, loadedSettings); } else { loadedSettings = {}; } if (typeof settings.xmpp.username === 'string') { username = settings.xmpp.username; } if (typeof settings.xmpp.password === 'string') { password = settings.xmpp.password; delete settings.xmpp.password; } var resource = (settings.xmpp.resource) ? '/' + settings.xmpp.resource : ''; var domain = settings.xmpp.domain; var jid; if (username.match(/@(.*)$/)) { jid = (username.match(/\/(.*)$/)) ? username : username + resource; } else { jid = username + '@' + domain + resource; } if (typeof jsxc.options.loginForm.preJid === 'function') { jid = jsxc.options.loginForm.preJid(jid); } jsxc.bid = jsxc.jidToBid(jid); settings.xmpp.username = jid.split('@')[0]; settings.xmpp.domain = jid.split('@')[1].split('/')[0]; settings.xmpp.resource = jid.split('@')[1].split('/')[1] || ""; if (!loadedSettings.xmpp) { // force xmpp settings to be saved to storage loadedSettings.xmpp = {}; } jsxc.storage.setUserItem('loadSettingsAllKnowing', (!!loadedSettings.xmpp.jid || (!!loadedSettings.xmpp.username && !!loadedSettings.xmpp.domain)) && !!loadedSettings.xmpp.password); // save loaded settings to storage $.each(loadedSettings, function(key) { var old = jsxc.options.get(key); var val = settings[key]; val = $.extend(true, old, val); jsxc.options.set(key, val); }); jsxc.options.xmpp.jid = jid; jsxc.options.xmpp.password = password; cb(settings); }, /** * Called if the script is a slave */ onSlave: function() { jsxc.debug('I am the slave.'); jsxc.role_allocation = true; jsxc.bid = jsxc.jidToBid(jsxc.storage.getItem('jid')); jsxc.gui.init(); $('#jsxc_roster').removeClass('jsxc_noConnection'); jsxc.registerLogout(); jsxc.gui.avatar.update($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own'); jsxc.gui.restore(); }, /** * Called if the script is the master */ onMaster: function() { jsxc.debug('I am master.'); jsxc.master = true; // Init local storage jsxc.storage.setItem('alive', 0); jsxc.storage.setItem('alive_busy', 0); // Sending keepalive signal jsxc.startKeepAlive(); jsxc.role_allocation = true; jsxc.xmpp.login(); }, /** * Checks if there is a master * * @param {function} [cb] Called if no master was found. */ checkMaster: function(cb) { jsxc.debug('check master'); cb = (cb && typeof cb === 'function') ? cb : jsxc.onMaster; if (typeof jsxc.storage.getItem('alive') === 'undefined') { cb.call(); } else { jsxc.to.push(window.setTimeout(cb, 1000)); jsxc.keepAlive('slave'); } }, masterActions: function() { if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) { return; } //prepare notifications var noti = jsxc.storage.getUserItem('notification'); noti = (typeof noti === 'number') ? noti : 2; if (jsxc.options.notification && noti > 0 && jsxc.notification.hasSupport()) { if (jsxc.notification.hasPermission()) { jsxc.notification.init(); } else { jsxc.notification.prepareRequest(); } } else { // No support => disable jsxc.options.notification = false; } if (jsxc.options.get('otr').enable) { // create or load DSA key jsxc.otr.createDSA(); } jsxc.gui.avatar.update($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own'); }, /** * Start sending keep-alive signal */ startKeepAlive: function() { jsxc.keepaliveInterval = window.setInterval(jsxc.keepAlive, jsxc.options.timeout - 1000); }, /** * Sends the keep-alive signal to signal that the master is still there. */ keepAlive: function(role) { var next = parseInt(jsxc.storage.getItem('alive')) + 1; role = role || 'master'; jsxc.storage.setItem('alive', next + ':' + role); }, /** * Send one keep-alive signal with higher timeout, and than resume with * normal signal */ keepBusyAlive: function() { if (jsxc.toBusy) { window.clearTimeout(jsxc.toBusy); } if (jsxc.keepaliveInterval) { window.clearInterval(jsxc.keepaliveInterval); } jsxc.storage.ink('alive_busy'); jsxc.toBusy = window.setTimeout(jsxc.startKeepAlive, jsxc.options.busyTimeout - 1000); }, /** * Generates a random integer number between 0 and max * * @param {Integer} max * @return {Integer} random integer between 0 and max */ random: function(max) { return Math.floor(Math.random() * max); }, /** * Checks if there is a element with the given selector * * @param {String} selector jQuery selector * @return {Boolean} */ el_exists: function(selector) { return $(selector).length > 0; }, /** * Creates a CSS compatible string from a JID * * @param {type} jid Valid Jabber ID * @returns {String} css Compatible string */ jidToCid: function(jid) { jsxc.warn('jsxc.jidToCid is deprecated!'); var cid = Strophe.getBareJidFromJid(jid).replace('@', '-').replace(/\./g, '-').toLowerCase(); return cid; }, /** * Create comparable bar jid. * * @memberOf jsxc * @param jid * @returns comparable bar jid */ jidToBid: function(jid) { return Strophe.unescapeNode(Strophe.getBareJidFromJid(jid).toLowerCase()); }, /** * Restore roster */ restoreRoster: function() { var buddies = jsxc.storage.getUserItem('buddylist'); if (!buddies || buddies.length === 0) { jsxc.debug('No saved buddylist.'); jsxc.gui.roster.empty(); return; } $.each(buddies, function(index, value) { jsxc.gui.roster.add(value); }); jsxc.gui.roster.loaded = true; $(document).trigger('cloaded.roster.jsxc'); }, /** * Restore all windows */ restoreWindows: function() { var windows = jsxc.storage.getUserItem('windowlist'); if (windows === null) { return; } $.each(windows, function(index, bid) { var win = jsxc.storage.getUserItem('window', bid); if (!win) { jsxc.debug('Associated window-element is missing: ' + bid); return true; } jsxc.gui.window.init(bid); if (!win.minimize) { jsxc.gui.window.show(bid); } else { jsxc.gui.window.hide(bid); } jsxc.gui.window.setText(bid, win.text); }); }, /** * This method submits the specified login form. */ submitLoginForm: function() { var form = $(jsxc.options.loginForm.form).off('submit'); // Attach original events var submits = form.data('submits') || []; $.each(submits, function(index, val) { form.submit(val); }); if (form.find('#submit').length > 0) { form.find('#submit').click(); } else if (form.get(0) && typeof form.get(0).submit === 'function') { form.submit(); } else if (form.find('[type="submit"]').length > 0) { form.find('[type="submit"]').click(); } else { jsxc.warn('Could not submit login form.'); } }, /** * Escapes some characters to HTML character */ escapeHTML: function(text) { text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return text.replace(/&/g, '&').replace(//g, '>'); }, /** * Removes all html tags. * * @memberOf jsxc * @param text * @returns stripped text */ removeHTML: function(text) { return $('').html(text).text(); }, /** * Executes only one of the given events * * @param {string} obj.key event name * @param {function} obj.value function to execute * @returns {string} namespace of all events */ switchEvents: function(obj) { var ns = Math.random().toString(36).substr(2, 12); var self = this; $.each(obj, function(key, val) { $(document).one(key + '.' + ns, function() { $(document).off('.' + ns); val.apply(self, arguments); }); }); return ns; }, /** * Checks if tab is hidden. * * @returns {boolean} True if tab is hidden */ isHidden: function() { var hidden = false; if (typeof document.hidden !== 'undefined') { hidden = document.hidden; } else if (typeof document.webkitHidden !== 'undefined') { hidden = document.webkitHidden; } else if (typeof document.mozHidden !== 'undefined') { hidden = document.mozHidden; } else if (typeof document.msHidden !== 'undefined') { hidden = document.msHidden; } // handle multiple tabs if (hidden && jsxc.master) { jsxc.storage.ink('hidden', 0); } else if (!hidden && !jsxc.master) { jsxc.storage.ink('hidden'); } return hidden; }, /** * Checks if tab has focus. * * @returns {boolean} True if tabs has focus */ hasFocus: function() { var focus = true; if (typeof document.hasFocus === 'function') { focus = document.hasFocus(); } if (!focus && jsxc.master) { jsxc.storage.ink('focus', 0); } else if (focus && !jsxc.master) { jsxc.storage.ink('focus'); } return focus; }, /** * Executes the given function in jsxc namespace. * * @memberOf jsxc * @param {string} fnName Function name * @param {array} fnParams Function parameters * @returns Function return value */ exec: function(fnName, fnParams) { var fnList = fnName.split('.'); var fn = jsxc[fnList[0]]; var i; for (i = 1; i < fnList.length; i++) { fn = fn[fnList[i]]; } if (typeof fn === 'function') { return fn.apply(null, fnParams); } }, /** * Hash string into 32-bit signed integer. * * @memberOf jsxc * @param {string} str input string * @returns {integer} 32-bit signed integer */ hashStr: function(str) { var hash = 0, i; if (str.length === 0) { return hash; } for (i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer } return hash; }, isExtraSmallDevice: function() { return $(window).width() < 500; }, changeState: function(state) { jsxc.currentState = state; jsxc.debug('State changed to ' + Object.keys(jsxc.CONST.STATE)[state]); $(document).trigger('stateChange.jsxc', state); }, changeUIState: function(state) { jsxc.currentUIState = state; jsxc.debug('UI State changed to ' + Object.keys(jsxc.CONST.UISTATE)[state]); $(document).trigger('stateUIChange.jsxc', state); }, getLastMsg: function(bid) { var history = jsxc.storage.getUserItem('history', bid) || []; var i = 0; while (history.length > i) { var message = new jsxc.Message(history[i]); if (message.direction !== jsxc.Message.SYS) { return { text: message.msg, date: message.stamp }; } i++; } }, enableDebugMode: function() { jsxc.storage.setItem('debug', true); }, disableDebugMode: function() { jsxc.storage.setItem('debug', false); }, deleteAllData: function() { if (!jsxc.storage.getItem('debug')) { jsxc.warn('This action is only available in debug mode.'); return 0; } var prefix = jsxc.storage.PREFIX + jsxc.storage.SEP; var prefixRegex = new RegExp('^' + prefix); var keys = Object.keys(localStorage); var count = 0; $.each(keys, function(index, key) { if (prefixRegex.test(key) && key !== prefix + 'debug') { localStorage.removeItem(key); count++; } }); return count; } }; /** * Handle XMPP stuff. * * @namespace jsxc.xmpp */ jsxc.xmpp = { conn: null, // connection /** * Create new connection or attach to old * * @name login * @memberOf jsxc.xmpp * @private */ /** * Create new connection with given parameters. * * @name login^2 * @param {string} jid * @param {string} password * @memberOf jsxc.xmpp * @private */ /** * Attach connection with given parameters. * * @name login^3 * @param {string} jid * @param {string} sid * @param {string} rid * @memberOf jsxc.xmpp * @private */ login: function() { if (jsxc.xmpp.conn && jsxc.xmpp.conn.authenticated) { jsxc.debug('Connection already authenticated.'); return; } var jid = null, password = null, sid = null, rid = null; switch (arguments.length) { case 2: jid = arguments[0]; password = arguments[1]; break; case 3: jid = arguments[0]; sid = arguments[1]; rid = arguments[2]; break; default: sid = jsxc.storage.getItem('sid'); rid = jsxc.storage.getItem('rid'); if (sid !== null && rid !== null) { jid = jsxc.storage.getItem('jid'); } else { sid = jsxc.options.xmpp.sid || null; rid = jsxc.options.xmpp.rid || null; jid = jsxc.options.xmpp.jid; } } if (!jid) { jsxc.warn('Jid required for login'); return; } if (!jsxc.bid) { jsxc.bid = jsxc.jidToBid(jid); } var url = jsxc.options.get('xmpp').url; if (!url) { jsxc.warn('xmpp.url required for login'); return; } if (!(jsxc.xmpp.conn && jsxc.xmpp.conn.connected)) { // Register eventlistener $(document).on('connected.jsxc', jsxc.xmpp.connected); $(document).on('attached.jsxc', jsxc.xmpp.attached); $(document).on('disconnected.jsxc', jsxc.xmpp.disconnected); $(document).on('connfail.jsxc', jsxc.xmpp.onConnfail); $(document).on('authfail.jsxc', jsxc.xmpp.onAuthFail); Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts'); Strophe.addNamespace('VERSION', 'jabber:iq:version'); } // Create new connection (no login) jsxc.xmpp.conn = new Strophe.Connection(url); if (jsxc.storage.getItem('debug') === true) { jsxc.xmpp.conn.xmlInput = function(data) { console.log('<', data); }; jsxc.xmpp.conn.xmlOutput = function(data) { console.log('>', data); }; } jsxc.xmpp.conn.nextValidRid = jsxc.xmpp.onRidChange; var callback = function(status, condition) { jsxc.debug(Object.getOwnPropertyNames(Strophe.Status)[status] + ': ' + condition); switch (status) { case Strophe.Status.CONNECTING: $(document).trigger('connecting.jsxc'); break; case Strophe.Status.CONNECTED: jsxc.bid = jsxc.jidToBid(jsxc.xmpp.conn.jid.toLowerCase()); $(document).trigger('connected.jsxc'); break; case Strophe.Status.ATTACHED: $(document).trigger('attached.jsxc'); break; case Strophe.Status.DISCONNECTED: $(document).trigger('disconnected.jsxc'); break; case Strophe.Status.CONNFAIL: $(document).trigger('connfail.jsxc', condition); break; case Strophe.Status.AUTHFAIL: $(document).trigger('authfail.jsxc'); break; } }; if (jsxc.xmpp.conn.caps) { jsxc.xmpp.conn.caps.node = 'http://jsxc.org/'; } jsxc.changeState(jsxc.CONST.STATE.ESTABLISHING); if (sid && rid) { jsxc.debug('Try to attach'); jsxc.debug('SID: ' + sid); jsxc.xmpp.conn.attach(jid, sid, rid, callback); } else { jsxc.debug('New connection'); if (jsxc.xmpp.conn.caps) { // Add system handler, because user handler isn't called before // we are authenticated // @REVIEW this could maybe retrieved from jsxc.xmpp.conn.features jsxc.xmpp.conn._addSysHandler(function(stanza) { var from = jsxc.xmpp.conn.domain, c = stanza.querySelector('c'), ver = c.getAttribute('ver'), node = c.getAttribute('node'); var _jidNodeIndex = JSON.parse(localStorage.getItem('strophe.caps._jidNodeIndex')) || {}; jsxc.xmpp.conn.caps._jidVerIndex[from] = ver; _jidNodeIndex[from] = node; localStorage.setItem('strophe.caps._jidVerIndex', JSON.stringify(jsxc.xmpp.conn.caps._jidVerIndex)); localStorage.setItem('strophe.caps._jidNodeIndex', JSON.stringify(_jidNodeIndex)); }, Strophe.NS.CAPS); } jsxc.xmpp.conn.connect(jid, password || jsxc.options.xmpp.password, callback); } }, /** * Logs user out of his xmpp session and does some clean up. * * @param {boolean} complete If set to false, roster will not be removed * @returns {Boolean} */ logout: function(complete) { jsxc.storage.setUserItem('forcedLogout', true); jsxc.triggeredFromElement = (typeof complete === 'boolean') ? complete : true; if (!jsxc.master) { // instruct master jsxc.storage.removeItem('sid'); // jsxc.xmpp.disconnected is called if master deletes alive after logout return true; } // REVIEW: this should maybe moved to xmpp.disconnected // clean up jsxc.storage.removeUserItem('windowlist'); jsxc.storage.removeUserItem('unreadMsg'); if (jsxc.gui.favicon) { jsxc.gui.favicon.badge(0); } // Hide dropdown menu $('body').click(); if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) { return true; } // restore all otr objects $.each(jsxc.storage.getUserItem('otrlist') || {}, function(i, val) { jsxc.otr.create(val); }); var numOtr = Object.keys(jsxc.otr.objects || {}).length + 1; var disReady = function() { if (--numOtr <= 0) { jsxc.xmpp.conn.flush(); setTimeout(function() { jsxc.xmpp.conn.disconnect(); }, 600); } }; // end all private conversations $.each(jsxc.otr.objects || {}, function(key, obj) { if (obj.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) { obj.endOtr.call(obj, function() { obj.init.call(obj); jsxc.otr.backup(key); disReady(); }); } else { disReady(); } }); disReady(); // Trigger real logout in jsxc.xmpp.disconnected() return false; }, /** * Triggered if connection is established * * @private */ connected: function() { jsxc.xmpp.conn.pause(); jsxc.xmpp.initNewConnection(); jsxc.xmpp.saveSessionParameter(); var rosterVerSupport = $(jsxc.xmpp.conn.features).find('[xmlns="urn:xmpp:features:rosterver"]').length > 0; jsxc.storage.setUserItem('rosterVerSupport', rosterVerSupport); jsxc.storage.removeUserItem('forcedLogout'); if (jsxc.options.loginForm.triggered) { switch (jsxc.options.loginForm.onConnected || 'submit') { case 'submit': jsxc.submitLoginForm(); return; case false: return; } } // start chat jsxc.gui.dialog.close(); jsxc.xmpp.conn.resume(); jsxc.onMaster(); jsxc.changeState(jsxc.CONST.STATE.READY); $(document).trigger('attached.jsxc'); }, /** * Triggered if connection is attached * * @private */ attached: function() { $('#jsxc_roster').removeClass('jsxc_noConnection'); Strophe.addNamespace('VERSION', 'jabber:iq:version'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onRosterChanged, 'jabber:iq:roster', 'iq', 'set'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onChatMessage, null, 'message', 'chat'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onErrorMessage, null, 'message', 'error'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onHeadlineMessage, null, 'message', 'headline'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onReceived, null, 'message'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onPresence, null, 'presence'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onVersionRequest, Strophe.NS.VERSION, 'iq', 'get'); jsxc.gui.init(); var caps = jsxc.xmpp.conn.caps; var domain = jsxc.xmpp.conn.domain; if (caps) { var conditionalEnable = function() {}; if (jsxc.options.get('carbons').enable) { conditionalEnable = function() { if (jsxc.xmpp.conn.caps.hasFeatureByJid(domain, jsxc.CONST.NS.CARBONS)) { jsxc.xmpp.carbons.enable(); } }; $(document).on('caps.strophe', function onCaps(ev, from) { if (from !== domain) { return; } conditionalEnable(); $(document).off('caps.strophe', onCaps); }); } if (typeof caps._knownCapabilities[caps._jidVerIndex[domain]] === 'undefined') { var _jidNodeIndex = JSON.parse(localStorage.getItem('strophe.caps._jidNodeIndex')) || {}; jsxc.debug('Request server capabilities'); caps._requestCapabilities(jsxc.xmpp.conn.domain, _jidNodeIndex[domain], caps._jidVerIndex[domain]); } else { // We know server caps conditionalEnable(); } } var rosterLoaded = jsxc.storage.getUserItem('rosterLoaded'); // Only load roaster if necessary if (rosterLoaded !== jsxc.xmpp.conn._proto.sid) { // in order to not overide existing presence information, we send // pres first after roster is ready $(document).one('cloaded.roster.jsxc', jsxc.xmpp.sendPres); $('#jsxc_roster > p:first').remove(); var queryAttr = { xmlns: 'jabber:iq:roster' }; if (jsxc.storage.getUserItem('rosterVerSupport')) { // @TODO check if we really cached the roster queryAttr.ver = jsxc.storage.getUserItem('rosterVer') || ''; } var iq = $iq({ type: 'get' }).c('query', queryAttr); jsxc.xmpp.conn.sendIQ(iq, jsxc.xmpp.onRoster); } else { jsxc.xmpp.sendPres(); if (!jsxc.restoreCompleted) { jsxc.gui.restore(); } } jsxc.xmpp.saveSessionParameter(); jsxc.masterActions(); jsxc.changeState(jsxc.CONST.STATE.READY); }, saveSessionParameter: function() { var nomJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid).toLowerCase() + '/' + Strophe.getResourceFromJid(jsxc.xmpp.conn.jid); // Save sid and jid jsxc.storage.setItem('sid', jsxc.xmpp.conn._proto.sid); jsxc.storage.setItem('jid', nomJid); }, initNewConnection: function() { jsxc.storage.removeUserItem('windowlist'); jsxc.storage.removeUserItem('own'); jsxc.storage.removeUserItem('avatar', 'own'); jsxc.storage.removeUserItem('otrlist'); jsxc.storage.removeUserItem('unreadMsg'); jsxc.storage.removeUserItem('features'); // reset user options jsxc.storage.removeUserElement('options', 'RTCPeerConfig'); // reset http upload settings jsxc.storage.removeUserElement('options', 'httpUpload'); }, /** * Sends presence stanza to server. */ sendPres: function() { // disco stuff if (jsxc.xmpp.conn.disco) { jsxc.xmpp.conn.disco.addIdentity('client', 'web', 'JSXC', ''); jsxc.xmpp.conn.disco.addFeature(Strophe.NS.DISCO_INFO); jsxc.xmpp.conn.disco.addFeature(Strophe.NS.RECEIPTS); jsxc.xmpp.conn.disco.addFeature(Strophe.NS.VERSION); } // create presence stanza var pres = $pres(); if (jsxc.xmpp.conn.caps) { // attach caps pres.c('c', jsxc.xmpp.conn.caps.generateCapsAttrs()).up(); } var presState = jsxc.storage.getUserItem('presence') || 'online'; if (presState !== 'online') { pres.c('show').t(presState).up(); } var priority = jsxc.options.get('priority'); if (priority && typeof priority[presState] !== 'undefined' && parseInt(priority[presState]) !== 0) { pres.c('priority').t(priority[presState]).up(); } jsxc.debug('Send presence', pres.toString()); jsxc.xmpp.conn.send(pres); if (!jsxc.storage.getUserItem('features')) { jsxc.xmpp.conn.flush(); var barJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid); jsxc.xmpp.conn.disco.info(barJid, undefined, function(stanza) { var features = $(stanza).find('feature').map(function() { return $(this).attr('var'); }); jsxc.storage.setUserItem('features', features.toArray()); $(document).trigger('features.jsxc'); }); } else { $(document).trigger('features.jsxc'); } }, /** * Triggered if lost connection * * @private */ disconnected: function() { jsxc.debug('disconnected'); //jsxc.storage.removeItem('jid'); jsxc.storage.removeItem('sid'); jsxc.storage.removeItem('rid'); jsxc.storage.removeItem('hidden'); jsxc.storage.removeUserItem('avatar', 'own'); jsxc.storage.removeUserItem('otrlist'); jsxc.storage.removeUserItem('features'); $(document).off('connected.jsxc', jsxc.xmpp.connected); $(document).off('attached.jsxc', jsxc.xmpp.attached); $(document).off('disconnected.jsxc', jsxc.xmpp.disconnected); $(document).off('connfail.jsxc', jsxc.xmpp.onConnfail); $(document).off('authfail.jsxc', jsxc.xmpp.onAuthFail); jsxc.xmpp.conn = null; $('#jsxc_windowList').remove(); if (jsxc.triggeredFromElement) { $(document).trigger('toggle.roster.jsxc', ['hidden', 0]); jsxc.gui.roster.ready = false; $('#jsxc_roster').remove(); // REVIEW: logoutElement without href attribute? if (jsxc.triggeredFromLogout) { window.location = jsxc.options.logoutElement.attr('href'); } } else { jsxc.gui.roster.noConnection(); } window.clearInterval(jsxc.keepaliveInterval); jsxc.restoreCompleted = false; jsxc.role_allocation = false; jsxc.master = false; jsxc.storage.removeItem('alive'); jsxc.changeState(jsxc.CONST.STATE.SUSPEND); }, /** * Triggered on connection fault * * @param {String} condition information why we lost the connection * @private */ onConnfail: function(ev, condition) { jsxc.debug('XMPP connection failed: ' + condition); if (jsxc.options.loginForm.triggered) { jsxc.submitLoginForm(); } }, /** * Triggered on auth fail. * * @private */ onAuthFail: function() { if (jsxc.options.loginForm.triggered) { switch (jsxc.options.loginForm.onAuthFail || 'ask') { case 'ask': jsxc.gui.showAuthFail(); break; case 'submit': jsxc.submitLoginForm(); break; case 'quiet': case false: return; } } }, /** * Triggered on initial roster load * * @param {dom} iq * @private */ onRoster: function(iq) { jsxc.debug('Load roster', iq); jsxc.storage.setUserItem('rosterLoaded', jsxc.xmpp.conn._proto.sid); if ($(iq).find('query').length === 0) { jsxc.debug('Use cached roster'); var buddylist = jsxc.storage.getUserItem('buddylist') || []; $.each(buddylist, function(index, buddy) { jsxc.storage.removeUserItem('res', buddy); jsxc.storage.updateUserItem('buddy', buddy, 'status', 0); jsxc.storage.updateUserItem('buddy', buddy, 'res', []); jsxc.storage.updateUserItem('buddy', buddy, 'rnd', Math.random()); }); jsxc.restoreRoster(); return; } var buddies = []; $(iq).find('item').each(function() { var jid = $(this).attr('jid'); if (!/^[^"&'\/:<>@\s]+@[\w-_.]+$/i.test(jid)) { jsxc.warn(jid + ' is no valid JID.'); return; } var name = $(this).attr('name') || jid; var bid = jsxc.jidToBid(jid); var sub = $(this).attr('subscription'); buddies.push(bid); jsxc.storage.removeUserItem('res', bid); jsxc.storage.saveBuddy(bid, { jid: jid, name: name, status: 0, sub: sub, res: [], rnd: Math.random() // force storage event }); jsxc.gui.roster.add(bid); }); if (buddies.length === 0) { jsxc.gui.roster.empty(); } jsxc.storage.setUserItem('buddylist', buddies); if ($(iq).find('query').attr('ver')) { jsxc.storage.setUserItem('rosterVer', $(iq).find('query').attr('ver')); } // load bookmarks jsxc.xmpp.bookmarks.load(); jsxc.gui.roster.loaded = true; jsxc.debug('Roster loaded'); $(document).trigger('cloaded.roster.jsxc'); jsxc.changeUIState(jsxc.CONST.UISTATE.READY); }, /** * Triggerd on roster changes * * @param {dom} iq * @returns {Boolean} True to preserve handler * @private */ onRosterChanged: function(iq) { var iqSender = $(iq).attr('from'); var ownBareJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid); if (iqSender && iqSender !== ownBareJid) { return true; } jsxc.debug('onRosterChanged', iq); if ($(iq).find('item').length !== 1) { jsxc.warn('Roster pushes must contain only one item element'); return true; } $(iq).find('item').each(function() { var jid = $(this).attr('jid'); if (!/^[^"&'\/:<>@\s]+@[\w-_.]+$/i.test(jid)) { jsxc.warn(jid + ' is no valid JID.'); return; } var name = $(this).attr('name') || jid; var bid = jsxc.jidToBid(jid); var sub = $(this).attr('subscription'); // var ask = $(this).attr('ask'); if (sub === 'remove') { jsxc.gui.roster.purge(bid); } else { var bl = jsxc.storage.getUserItem('buddylist'); if (bl.indexOf(bid) < 0) { bl.push(bid); // (INFO) push returns the new length jsxc.storage.setUserItem('buddylist', bl); } var temp = jsxc.storage.saveBuddy(bid, { jid: jid, name: name, sub: sub }); if (temp === 'updated') { jsxc.gui.update(bid); jsxc.gui.roster.reorder(bid); } else { jsxc.gui.roster.add(bid); } } // Remove pending friendship request from notice list if (sub === 'from' || sub === 'both') { var notices = jsxc.storage.getUserItem('notices'); var noticeKey = null, notice; for (noticeKey in notices) { notice = notices[noticeKey]; if (notice.fnName === 'gui.showApproveDialog' && notice.fnParams[0] === jid) { jsxc.debug('Remove notice with key ' + noticeKey); jsxc.notice.remove(noticeKey); } } } }); if ($(iq).find('query').attr('ver')) { jsxc.storage.setUserItem('rosterVer', $(iq).find('query').attr('ver')); } if (!jsxc.storage.getUserItem('buddylist') || jsxc.storage.getUserItem('buddylist').length === 0) { jsxc.gui.roster.empty(); } else { $('#jsxc_roster > p:first').remove(); } // preserve handler return true; }, /** * Triggered on incoming presence stanzas * * @param {dom} presence * @private */ onPresence: function(presence) { /* * * * 5 * * * chat * 5 */ jsxc.debug('onPresence', presence); var ptype = $(presence).attr('type'); var from = $(presence).attr('from'); var jid = Strophe.getBareJidFromJid(from).toLowerCase(); var r = Strophe.getResourceFromJid(from); var bid = jsxc.jidToBid(jid); var data = jsxc.storage.getUserItem('buddy', bid) || {}; var res = jsxc.storage.getUserItem('res', bid) || {}; var status = null; var xVCard = $(presence).find('x[xmlns="vcard-temp:x:update"]'); if (jid === Strophe.getBareJidFromJid(jsxc.storage.getItem("jid"))) { return true; } if (ptype === 'error') { $(document).trigger('error.presence.jsxc', [from, presence]); var error = $(presence).find('error'); //TODO display error message jsxc.error('[XMPP] ' + error.attr('code') + ' ' + error.find(">:first-child").prop('tagName')); return true; } // incoming friendship request if (ptype === 'subscribe') { var bl = jsxc.storage.getUserItem('buddylist'); if (bl.indexOf(bid) > -1) { jsxc.debug('Auto approve contact request, because he is already in our contact list.'); jsxc.xmpp.resFriendReq(jid, true); if (data.sub !== 'to') { jsxc.xmpp.addBuddy(jid, data.name); } return true; } jsxc.storage.setUserItem('friendReq', { jid: jid, approve: -1 }); jsxc.notice.add({ msg: $.t('Friendship_request'), description: $.t('from') + ' ' + jid, type: 'contact' }, 'gui.showApproveDialog', [jid]); return true; } else if (ptype === 'unavailable' || ptype === 'unsubscribed') { status = jsxc.CONST.STATUS.indexOf('offline'); } else { var show = $(presence).find('show').text(); if (show === '') { status = jsxc.CONST.STATUS.indexOf('online'); } else { status = jsxc.CONST.STATUS.indexOf(show); } } if (status === 0) { delete res[r]; } else if (r) { res[r] = status; } var maxVal = []; var max = 0, prop = null; for (prop in res) { if (res.hasOwnProperty(prop)) { if (max <= res[prop]) { if (max !== res[prop]) { maxVal = []; max = res[prop]; } maxVal.push(prop); } } } if (data.status === 0 && max > 0) { // buddy has come online jsxc.notification.notify({ title: data.name, msg: $.t('has_come_online'), source: bid }); } if (data.type !== 'groupchat') { data.status = max; } data.res = maxVal; data.jid = jid; // Looking for avatar if (xVCard.length > 0 && data.type !== 'groupchat') { var photo = xVCard.find('photo'); if (photo.length > 0 && photo.text() !== data.avatar) { jsxc.storage.removeUserItem('avatar', data.avatar); data.avatar = photo.text(); } } // Reset jid if (jsxc.gui.window.get(bid).length > 0) { jsxc.gui.window.get(bid).data('jid', jid); } jsxc.storage.setUserItem('buddy', bid, data); jsxc.storage.setUserItem('res', bid, res); jsxc.debug('Presence (' + from + '): ' + jsxc.CONST.STATUS[status]); jsxc.gui.update(bid); jsxc.gui.roster.reorder(bid); $(document).trigger('presence.jsxc', [from, status, presence]); // preserve handler return true; }, /** * Triggered on incoming message stanzas * * @param {dom} presence * @returns {Boolean} * @private */ onChatMessage: function(stanza) { var forwarded = $(stanza).find('forwarded[xmlns="' + jsxc.CONST.NS.FORWARD + '"]'); var message, carbon; var originalSender = $(stanza).attr('from'); if (forwarded.length > 0) { message = forwarded.find('> message'); forwarded = true; carbon = $(stanza).find('> [xmlns="' + jsxc.CONST.NS.CARBONS + '"]'); if (carbon.length === 0) { carbon = false; } else if (originalSender !== Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid)) { // ignore this carbon copy return true; } jsxc.debug('Incoming forwarded message', message); } else { message = stanza; forwarded = false; carbon = false; jsxc.debug('Incoming message', message); } var htmlBody = $(message).find('body[xmlns="' + Strophe.NS.XHTML + '"]').first(); var body = $(message).find('>body').first().text() || htmlBody.text(); if (!body || (body.match(/\?OTR/i) && forwarded)) { return true; } var type = $(message).attr('type'); var from = $(message).attr('from'); var mid = $(message).attr('id'); var bid; var delay = $(message).find('delay[xmlns="urn:xmpp:delay"]'); var stamp = (delay.length > 0) ? new Date(delay.attr('stamp')) : new Date(); stamp = stamp.getTime(); if (carbon) { var direction = (carbon.prop("tagName") === 'sent') ? jsxc.Message.OUT : jsxc.Message.IN; bid = jsxc.jidToBid((direction === jsxc.Message.OUT) ? $(message).attr('to') : from); jsxc.gui.window.postMessage({ bid: bid, direction: direction, msg: body, encrypted: false, forwarded: forwarded, stamp: stamp }); return true; } else if (forwarded) { // Someone forwarded a message to us body = from + ' ' + $.t('to') + ' ' + $(stanza).attr('to') + '"' + body + '"'; from = $(stanza).attr('from'); } var jid = Strophe.getBareJidFromJid(from); bid = jsxc.jidToBid(jid); var data = jsxc.storage.getUserItem('buddy', bid); var request = $(message).find("request[xmlns='urn:xmpp:receipts']"); if (data === null) { // jid not in roster var chat = jsxc.storage.getUserItem('chat', bid) || []; if (chat.length === 0) { jsxc.notice.add({ msg: $.t('Unknown_sender'), description: $.t('You_received_a_message_from_an_unknown_sender_') + ' (' + bid + ').' }, 'gui.showUnknownSender', [bid]); } var msg = jsxc.removeHTML(body); msg = jsxc.escapeHTML(msg); var messageObj = new jsxc.Message({ bid: bid, msg: msg, direction: jsxc.Message.IN, encrypted: false, forwarded: forwarded, stamp: stamp }); messageObj.save(); return true; } var win = jsxc.gui.window.init(bid); // If we now the full jid, we use it if (type === 'chat') { win.data('jid', from); jsxc.storage.updateUserItem('buddy', bid, { jid: from }); } $(document).trigger('message.jsxc', [from, body]); // create related otr object if (jsxc.master && !jsxc.otr.objects[bid]) { jsxc.otr.create(bid); } if (!forwarded && mid !== null && request.length && (data.sub === 'both' || data.sub === 'from') && type === 'chat') { // Send received according to XEP-0184 jsxc.xmpp.conn.send($msg({ to: from }).c('received', { xmlns: 'urn:xmpp:receipts', id: mid })); } var attachment = jsxc.xmpp.getAttachmentFromHtmlBody(htmlBody); if (attachment) { body = null; } if (jsxc.otr.objects.hasOwnProperty(bid) && body) { // @TODO check for file upload url after decryption jsxc.otr.objects[bid].receiveMsg(body, { _uid: mid, stamp: stamp, forwarded: forwarded, attachment: attachment }); } else { jsxc.gui.window.postMessage({ _uid: mid, bid: bid, direction: jsxc.Message.IN, msg: body, encrypted: false, forwarded: forwarded, stamp: stamp, attachment: attachment }); } // preserve handler return true; }, onErrorMessage: function(message) { var bid = jsxc.jidToBid($(message).attr('from')); if (jsxc.gui.window.get(bid).length === 0 || !$(message).attr('id')) { return true; } if ($(message).find('item-not-found').length > 0) { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('message_not_send_item-not-found') }); } else if ($(message).find('forbidden').length > 0) { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('message_not_send_forbidden') }); } else if ($(message).find('not-acceptable').length > 0) { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('message_not_send_not-acceptable') }); } else if ($(message).find('remote-server-not-found').length > 0) { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('message_not_send_remote-server-not-found') }); } else if ($(message).find('service-unavailable').length > 0) { if ($(message).find('[xmlns="' + Strophe.NS.CHATSTATES + '"]').length === 0) { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('message_not_send_resource-unavailable') }); } } else { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('message_not_send') }); } jsxc.debug('error message for ' + bid, $(message).find('error')[0]); return true; }, /** * Process message stanzas of type headline. * * @param {String} stanza Message stanza of type headline * @return {Boolean} */ onHeadlineMessage: function(stanza) { stanza = $(stanza); var from = stanza.attr('from'); var domain = Strophe.getDomainFromJid(from); if (domain !== from) { if (!jsxc.storage.getUserItem('buddy', jsxc.jidToBid(from))) { return true; } } else if (domain !== Strophe.getDomainFromJid(jsxc.xmpp.conn.jid)) { return true; } var subject = stanza.find('subject:first').text() || $.t('Notification'); var body = stanza.find('body:first').text(); jsxc.notice.add({ msg: subject, description: body, type: (domain === from) ? 'announcement' : null }, 'gui.showNotification', [subject, body, from]); return true; }, /** * Respond to version request (XEP-0092). */ onVersionRequest: function(stanza) { stanza = $(stanza); var from = stanza.attr('from'); var id = stanza.attr('id'); var iq = $iq({ type: 'result', to: from, id: id }).c('query', { xmlns: Strophe.NS.VERSION }).c('name').t('JSXC').up() .c('version').t(jsxc.version); jsxc.xmpp.conn.sendIQ(iq); return true; }, /** * Triggerd if the rid changed * * @param {integer} rid next valid request id * @private */ onRidChange: function(rid) { jsxc.storage.setItem('rid', rid); }, getAttachmentFromHtmlBody: function(htmlBody) { if (htmlBody.length !== 1) { return; } var attachment; var httpUploadElement = htmlBody.find('a[data-type][data-name][data-size]'); if (httpUploadElement.length === 1) { // deprecated syntax @since 3.2.1 attachment = { type: httpUploadElement.attr('data-type'), name: httpUploadElement.attr('data-name'), size: httpUploadElement.attr('data-size'), }; if (httpUploadElement.attr('data-thumbnail') && httpUploadElement.attr('data-thumbnail').match(/^\s*data:[a-z]+\/[a-z0-9-+.*]+;base64,[a-z0-9=+/]+$/i)) { attachment.thumbnail = httpUploadElement.attr('data-thumbnail'); } if (httpUploadElement.attr('href') && httpUploadElement.attr('href').match(/^https:\/\//)) { attachment.data = httpUploadElement.attr('href'); } if (!attachment.type.match(/^[a-z]+\/[a-z0-9-+.*]+$/i) || !attachment.name.match(/^[\s\w.,-]+$/i) || !attachment.size.match(/^\d+$/i)) { attachment = undefined; jsxc.warn('Invalid file type, name or size.'); } } else if (htmlBody.find('>a').length === 1) { var linkElement = htmlBody.find('>a'); var metaString = ''; var thumbnail; if (linkElement.find('>img').length === 1) { var imgElement = linkElement.find('>img'); var src = imgElement.attr('src') || ''; var altString = imgElement.attr('alt') || ''; metaString = altString.replace(/^Preview:/, ''); if (src.match(/^\s*data:[a-z]+\/[a-z0-9-+.*]+;base64,[a-z0-9=+/]+$/i)) { thumbnail = src; } } else { metaString = linkElement.text(); } var metaMatch = metaString.match(/^([a-z]+\/[a-z0-9-+.*]+)\|(\d+)\|([\s\w.,-]+)/); if (metaMatch) { attachment = { type: metaMatch[1], size: metaMatch[2], name: metaMatch[3], }; if (thumbnail) { attachment.thumbnail = thumbnail; } if (linkElement.attr('href') && linkElement.attr('href').match(/^https?:\/\//)) { attachment.data = linkElement.attr('href'); } } else { jsxc.warn('Invalid file type, name or size.'); } } return attachment; }, /** * response to friendship request * * @param {string} from jid from original friendship req * @param {boolean} approve */ resFriendReq: function(from, approve) { if (jsxc.master) { jsxc.xmpp.conn.send($pres({ to: from, type: (approve) ? 'subscribed' : 'unsubscribed' })); jsxc.storage.removeUserItem('friendReq'); jsxc.gui.dialog.close(); } else { jsxc.storage.updateUserItem('friendReq', 'approve', approve); } }, /** * Add buddy to my friends * * @param {string} username jid * @param {string} alias */ addBuddy: function(username, alias) { var bid = jsxc.jidToBid(username); if (jsxc.master) { // add buddy to roster (trigger onRosterChanged) var iq = $iq({ type: 'set' }).c('query', { xmlns: 'jabber:iq:roster' }).c('item', { jid: username, name: alias || '' }); jsxc.xmpp.conn.sendIQ(iq); // send subscription request to buddy (trigger onRosterChanged) jsxc.xmpp.conn.send($pres({ to: username, type: 'subscribe' })); jsxc.storage.removeUserItem('add', bid); } else { jsxc.storage.setUserItem('add', bid, { username: username, alias: alias || null }); } }, /** * Remove buddy from my friends * * @param {type} jid */ removeBuddy: function(jid) { var bid = jsxc.jidToBid(jid); // Shortcut to remove buddy from roster and cancle all subscriptions var iq = $iq({ type: 'set' }).c('query', { xmlns: 'jabber:iq:roster' }).c('item', { jid: Strophe.getBareJidFromJid(jid), subscription: 'remove' }); jsxc.xmpp.conn.sendIQ(iq); jsxc.gui.roster.purge(bid); }, onReceived: function(stanza) { var received = $(stanza).find("received[xmlns='urn:xmpp:receipts']"); if (received.length) { var receivedId = received.attr('id'); var message = new jsxc.Message(receivedId); message.received(); } return true; }, /** * Public function to send message. * * @memberOf jsxc.xmpp * @param bid css jid of user * @param msg message * @param uid unique id */ sendMessage: function(message) { var bid = message.bid; var msg = message.msg; var mucRoomNames = (jsxc.xmpp.conn.muc && jsxc.xmpp.conn.muc.roomNames) ? jsxc.xmpp.conn.muc.roomNames : []; var isMucBid = mucRoomNames.indexOf(bid) >= 0; if (jsxc.otr.objects.hasOwnProperty(bid) && !isMucBid) { jsxc.otr.objects[bid].sendMsg(msg, message); } else { jsxc.xmpp._sendMessage(jsxc.gui.window.get(bid).data('jid'), msg, message); } }, /** * Create message stanza and send it. * * @memberOf jsxc.xmpp * @param jid Jabber id * @param msg Message * @param uid unique id * @private */ _sendMessage: function(jid, msg, message) { // @TODO put jid into message object var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(jid)) || {}; var isBar = (Strophe.getBareJidFromJid(jid) === jid); var type = data.type || 'chat'; message = message || {}; var xmlMsg = $msg({ to: jid, type: type, id: message._uid }); if (message.type === jsxc.Message.HTML && msg === message.msg && message.htmlMsg) { xmlMsg.c('body').t(msg); xmlMsg.up().c('html', { xmlns: Strophe.NS.XHTML_IM }).c('body', { xmlns: Strophe.NS.XHTML }).h(message.htmlMsg).up(); } else { xmlMsg.c('body').t(msg); } if (jsxc.xmpp.carbons.enabled && msg.match(/^\?OTR/)) { xmlMsg.up().c("private", { xmlns: jsxc.CONST.NS.CARBONS }); } if (msg.match(/^\?OTR/)) { xmlMsg.up().c("no-permanent-store", { xmlns: jsxc.CONST.NS.HINTS }); } if (type === 'chat' && (isBar || jsxc.xmpp.conn.caps.hasFeatureByJid(jid, Strophe.NS.RECEIPTS))) { // Add request according to XEP-0184 xmlMsg.up().c('request', { xmlns: 'urn:xmpp:receipts' }); } if (jsxc.xmpp.conn.chatstates && !jsxc.xmpp.chatState.isDisabled()) { // send active event (XEP-0085) xmlMsg.up().c('active', { xmlns: Strophe.NS.CHATSTATES }); } jsxc.xmpp.conn.send(xmlMsg); }, /** * This function loads a vcard. * * @memberOf jsxc.xmpp * @param bid * @param cb * @param error_cb */ loadVcard: function(bid, cb, error_cb) { if (jsxc.master) { jsxc.xmpp.conn.vcard.get(cb, bid, error_cb); } else { jsxc.storage.setUserItem('vcard', bid, 'request:' + (new Date()).getTime()); $(document).one('loaded.vcard.jsxc', function(ev, result) { if (result && result.state === 'success') { cb($(result.data).get(0)); } else { error_cb(); } }); } }, /** * Retrieves capabilities. * * @memberOf jsxc.xmpp * @param jid * @returns List of known capabilities */ getCapabilitiesByJid: function(jid) { if (jsxc.xmpp.conn) { return jsxc.xmpp.conn.caps.getCapabilitiesByJid(jid); } var jidVerIndex = JSON.parse(localStorage.getItem('strophe.caps._jidVerIndex')) || {}; var knownCapabilities = JSON.parse(localStorage.getItem('strophe.caps._knownCapabilities')) || {}; if (jidVerIndex[jid]) { return knownCapabilities[jidVerIndex[jid]]; } return null; }, /** * Test if jid has given features * * @param {string} jid Jabber id * @param {string[]} feature Single feature or list of features * @param {Function} cb Called with the result as first param. * @return {boolean} True, if jid has all given features. Null, if we do not know it currently. */ hasFeatureByJid: function(jid, feature, cb) { var conn = jsxc.xmpp.conn; cb = cb || function() {}; if (!feature) { return false; } if (!$.isArray(feature)) { feature = $.makeArray(feature); } var check = function(knownCapabilities) { if (!knownCapabilities) { return null; } var i; for (i = 0; i < feature.length; i++) { if (knownCapabilities['features'].indexOf(feature[i]) < 0) { return false; } } return true; }; if (conn.caps._jidVerIndex[jid] && conn.caps._knownCapabilities[conn.caps._jidVerIndex[jid]]) { var hasFeature = check(conn.caps._knownCapabilities[conn.caps._jidVerIndex[jid]]); cb(hasFeature); return hasFeature; } $(document).on('strophe.caps', function(ev, j, capabilities) { if (j === jid) { cb(check(capabilities)); $(document).off(ev); } }); return null; } }; /** * Handle carbons (XEP-0280); * * @namespace jsxc.xmpp.carbons */ jsxc.xmpp.carbons = { enabled: false, /** * Enable carbons. * * @memberOf jsxc.xmpp.carbons * @param cb callback */ enable: function(cb) { var iq = $iq({ type: 'set' }).c('enable', { xmlns: jsxc.CONST.NS.CARBONS }); jsxc.xmpp.conn.sendIQ(iq, function() { jsxc.xmpp.carbons.enabled = true; jsxc.debug('Carbons enabled'); if (cb) { cb.call(this); } }, function(stanza) { jsxc.warn('Could not enable carbons', stanza); }); }, /** * Disable carbons. * * @memberOf jsxc.xmpp.carbons * @param cb callback */ disable: function(cb) { var iq = $iq({ type: 'set' }).c('disable', { xmlns: jsxc.CONST.NS.CARBONS }); jsxc.xmpp.conn.sendIQ(iq, function() { jsxc.xmpp.carbons.enabled = false; jsxc.debug('Carbons disabled'); if (cb) { cb.call(this); } }, function(stanza) { jsxc.warn('Could not disable carbons', stanza); }); }, /** * Enable/Disable carbons depending on options key. * * @memberOf jsxc.xmpp.carbons * @param err error message */ refresh: function(err) { if (err === false) { return; } if (jsxc.options.get('carbons').enable) { return jsxc.xmpp.carbons.enable(); } return jsxc.xmpp.carbons.disable(); } }; /* global Favico, emojione*/ /** * Handle functions for chat window's and buddylist * * @namespace jsxc.gui */ jsxc.gui = { /** Smilie token to file mapping */ emotions: [ ['O:-) O:)', 'innocent'], ['>:-( >:( >:-( >:(', 'angry'], [':-) :)', 'slight_smile'], [':-D :D', 'grin'], [':-( :(', 'disappointed'], [';-) ;)', 'wink'], [':-P :P', 'stuck_out_tongue'], ['=-O', 'astonished'], [':kiss: :-*', 'kissing_heart'], ['8-) :cool:', 'sunglasses'], [':-X :X', 'zipper_mouth'], [':yes:', 'thumbsup'], [':no:', 'thumbsdown'], [':beer:', 'beer'], [':coffee:', 'coffee'], [':devil:', 'smiling_imp'], [':kiss: :kissing:', 'kissing'], ['@->-- @->--', 'rose'], [':music:', 'musical_note'], [':love:', 'heart_eyes'], [':heart:', 'heart'], [':brokenheart:', 'broken_heart'], [':zzz:', 'zzz'], [':wait:', 'hand_splayed'] ], favicon: null, regShortNames: null, emoticonList: { 'core': { ':klaus:': ['klaus'], ':jabber:': ['jabber'], ':xmpp:': ['xmpp'], ':jsxc:': ['jsxc'], ':owncloud:': ['owncloud'], ':nextcloud:': ['nextcloud'] }, 'emojione': emojione.emojioneList }, /** * Different uri query actions as defined in XEP-0147. * * @namespace jsxc.gui.queryActions */ queryActions: { /** xmpp:JID?message[;body=TEXT] */ message: function(jid, params) { var bid = jsxc.jidToBid(jid); if (!jsxc.storage.getUserItem('buddy', bid)) { // init contact jsxc.storage.saveBuddy(bid, { jid: jid, name: bid, status: 0, sub: 'none', res: [], rnd: Math.random() }); } var win = jsxc.gui.window.open(bid); if (params && typeof params.body === 'string') { win.find('.jsxc_textinput').val(params.body); } }, /** xmpp:JID?remove */ remove: function(jid) { jsxc.gui.showRemoveDialog(jsxc.jidToBid(jid)); }, /** xmpp:JID?subscribe[;name=NAME] */ subscribe: function(jid, params) { jsxc.gui.showContactDialog(jid); if (params && typeof params.name === 'string') { $('#jsxc_alias').val(params.name); } }, /** xmpp:JID?vcard */ vcard: function(jid) { jsxc.gui.showVcard(jid); }, /** xmpp:JID?join[;password=TEXT] */ join: function(jid, params) { var password = (params && params.password) ? params.password : null; jsxc.muc.showJoinChat(jid, password); } }, /** * Creates application skeleton. * * @memberOf jsxc.gui */ init: function() { // Prevent duplicate windowList if ($('#jsxc_windowList').length > 0) { return; } jsxc.changeUIState(jsxc.CONST.UISTATE.INITIATING); jsxc.gui.regShortNames = new RegExp(emojione.regShortNames.source + '|(' + Object.keys(jsxc.gui.emoticonList.core).join('|') + ')', 'gi'); $('body').append($(jsxc.gui.template.get('windowList'))); $(window).resize(jsxc.gui.updateWindowListSB); $('#jsxc_windowList').resize(jsxc.gui.updateWindowListSB); $('#jsxc_windowListSB .jsxc_scrollLeft').click(function() { jsxc.gui.scrollWindowListBy(-200); }); $('#jsxc_windowListSB .jsxc_scrollRight').click(function() { jsxc.gui.scrollWindowListBy(200); }); $('#jsxc_windowList').on('wheel', function(ev) { if ($('#jsxc_windowList').data('isOver')) { jsxc.gui.scrollWindowListBy((ev.originalEvent.wheelDelta > 0) ? 200 : -200); } }); jsxc.gui.tooltip('#jsxc_windowList'); var fo = jsxc.options.get('favicon'); if (fo && fo.enable) { jsxc.gui.favicon = new Favico({ animation: 'pop', bgColor: fo.bgColor, textColor: fo.textColor }); jsxc.gui.favicon.badge(jsxc.storage.getUserItem('unreadMsg') || 0); } if (!jsxc.el_exists('#jsxc_roster')) { jsxc.gui.roster.init(); } // prepare regexp for emotions $.each(jsxc.gui.emotions, function(i, val) { // escape characters var reg = val[0].replace(/(\/|\||\*|\.|\+|\?|\^|\$|\(|\)|\[|\]|\{|\})/g, '\\$1'); //lgtm [js/incomplete-sanitization] reg = '(' + reg.split(' ').join('|') + ')'; jsxc.gui.emotions[i][2] = new RegExp(reg, 'g'); }); // We need this often, so we creates some template jquery objects jsxc.gui.windowTemplate = $(jsxc.gui.template.get('chatWindow')); jsxc.gui.buddyTemplate = $(jsxc.gui.template.get('rosterBuddy')); }, /** * Init tooltip plugin for given jQuery selector. * * @param {String} selector jQuery selector * @memberOf jsxc.gui */ tooltip: function(selector) { $(selector).tooltip({ show: { delay: 600 }, content: function() { return $(this).attr('title').replace(/\n/g, '
'); } }); }, /** * Updates Information in roster and chatbar * * @param {String} bid bar jid */ update: function(bid) { var data = jsxc.storage.getUserItem('buddy', bid); if (!data) { jsxc.debug('No data for ' + bid); return; } var ri = jsxc.gui.roster.getItem(bid); // roster item from user var we = jsxc.gui.window.get(bid); // window element from user var ue = ri.add(we); // both var spot = $('.jsxc_spot[data-bid="' + bid + '"]'); // Attach data to corresponding roster item ri.data(data); // Add online status jsxc.gui.updatePresence(bid, jsxc.CONST.STATUS[data.status]); // Change name and add title ue.find('.jsxc_name:first').add(spot).text(data.name).attr('title', bid + ' ' + $.t('is_', { status: $.t(jsxc.CONST.STATUS[data.status]) })); // Update gui according to encryption state switch (data.msgstate) { case 0: we.find('.jsxc_transfer').removeClass('jsxc_enc jsxc_fin').attr('title', $.t('your_connection_is_unencrypted')); we.find('.jsxc_settings .jsxc_verification').addClass('jsxc_disabled'); we.find('.jsxc_settings .jsxc_transfer').text($.t('start_private')); break; case 1: we.find('.jsxc_transfer').addClass('jsxc_enc').attr('title', $.t('your_connection_is_encrypted')); we.find('.jsxc_settings .jsxc_verification').removeClass('jsxc_disabled'); we.find('.jsxc_settings .jsxc_transfer').text($.t('close_private')); break; case 2: we.find('.jsxc_settings .jsxc_verification').addClass('jsxc_disabled'); we.find('.jsxc_transfer').removeClass('jsxc_enc').addClass('jsxc_fin').attr('title', $.t('your_buddy_closed_the_private_connection')); we.find('.jsxc_settings .jsxc_transfer').text($.t('close_private')); break; } // update gui according to verification state if (data.trust) { we.find('.jsxc_transfer').addClass('jsxc_trust').attr('title', $.t('your_buddy_is_verificated')); } else { we.find('.jsxc_transfer').removeClass('jsxc_trust'); } // update gui according to subscription state if (data.sub && data.sub !== 'both') { ue.addClass('jsxc_oneway'); } else { ue.removeClass('jsxc_oneway'); } var info = Strophe.getBareJidFromJid(data.jid) + '\n'; info += $.t('Subscription') + ': ' + $.t(data.sub) + '\n'; info += $.t('Status') + ': ' + $.t(jsxc.CONST.STATUS[data.status]); ri.find('.jsxc_name').attr('title', info); jsxc.gui.avatar.update(ri.add(we.find('.jsxc_bar')), data.jid, data.avatar); $(document).trigger('update.gui.jsxc', [bid]); }, /** * Updates scrollbar handlers. * * @memberOf jsxc.gui */ updateWindowListSB: function() { if ($('#jsxc_windowList>ul').width() > $('#jsxc_windowList').width()) { $('#jsxc_windowListSB > div').removeClass('jsxc_disabled'); } else { $('#jsxc_windowListSB > div').addClass('jsxc_disabled'); $('#jsxc_windowList>ul').css('right', '0px'); } }, /** * Scroll window list by offset. * * @memberOf jsxc.gui * @param offset */ scrollWindowListBy: function(offset) { var scrollWidth = $('#jsxc_windowList>ul').width(); var width = $('#jsxc_windowList').width(); var el = $('#jsxc_windowList>ul'); var right = parseInt(el.css('right')) - offset; var padding = $("#jsxc_windowListSB").width(); if (scrollWidth < width) { return; } if (right > 0) { right = 0; } if (right < width - scrollWidth - padding) { right = width - scrollWidth - padding; } el.css('right', right + 'px'); }, /** * Returns the window element * * @deprecated Use {@link jsxc.gui.window.get} instead. * @param {String} bid * @returns {jquery} jQuery object of the window element */ getWindow: function(bid) { jsxc.warn('jsxc.gui.getWindow is deprecated!'); return jsxc.gui.window.get(bid); }, /** * Toggle list with timeout, like menu or settings * * @memberof jsxc.gui */ toggleList: function(el) { var self = el || $(this); self.disableSelection(); self.addClass('jsxc_list'); var ul = self.find('ul'); var slideUp = null; slideUp = function() { self.removeClass('jsxc_opened'); $('body').off('click', null, slideUp); }; $(this).click(function() { if (!self.hasClass('jsxc_opened')) { // hide other lists $('body').click(); $('body').one('click', slideUp); } else { $('body').off('click', null, slideUp); } window.clearTimeout(ul.data('timer')); self.toggleClass('jsxc_opened'); return false; }).mouseleave(function() { ul.data('timer', window.setTimeout(slideUp, 2000)); }).mouseenter(function() { window.clearTimeout(ul.data('timer')); }); }, /** * Creates and show loginbox */ showLoginBox: function() { // Set focus to username or password field $(document).one("complete.dialog.jsxc", function() { setTimeout(function() { if ($("#jsxc_username").val().length === 0) { $("#jsxc_username").focus(); } else { $('#jsxc_password').focus(); } }, 50); }); jsxc.gui.dialog.open(jsxc.gui.template.get('loginBox')); var alert = $('#jsxc_dialog').find('.jsxc_alert'); alert.hide(); $('#jsxc_dialog').find('form').submit(function(ev) { ev.preventDefault(); $(this).find('button[data-jsxc-loading-text]').trigger('btnloading.jsxc'); jsxc.options.loginForm.form = $(this); jsxc.options.loginForm.jid = $(this).find('#jsxc_username'); jsxc.options.loginForm.pass = $(this).find('#jsxc_password'); jsxc.triggeredFromBox = true; jsxc.options.loginForm.triggered = false; jsxc.prepareLogin(function(settings) { if (settings === false) { onAuthFail(); } else { $(document).on('authfail.jsxc', onAuthFail); $(document).on('connfail.jsxc', onAuthFail); $(document).on('connected.jsxc', removeHandler); jsxc.xmpp.login(); } }); }); function onAuthFail() { alert.show(); jsxc.gui.dialog.resize(); removeHandler(); $('#jsxc_dialog').find('button').trigger('btnfinished.jsxc'); $('#jsxc_dialog').find('input').one('keypress', function() { alert.hide(); jsxc.gui.dialog.resize(); }); } function removeHandler() { $(document).off('authfail.jsxc', null, onAuthFail); $(document).off('connfail.jsxc', null, onAuthFail); $(document).off('connected.jsxc', null, removeHandler); } }, /** * Creates and show the fingerprint dialog * * @param {String} bid */ showFingerprints: function(bid) { jsxc.gui.dialog.open(jsxc.gui.template.get('fingerprintsDialog', bid)); }, /** * Creates and show the verification dialog * * @param {String} bid */ showVerification: function(bid) { // Check if there is a open dialog if ($('#jsxc_dialog').length > 0) { setTimeout(function() { jsxc.gui.showVerification(bid); }, 3000); return; } // verification only possible if the connection is encrypted if (jsxc.storage.getUserItem('buddy', bid).msgstate !== OTR.CONST.MSGSTATE_ENCRYPTED) { jsxc.warn('Connection not encrypted'); return; } jsxc.gui.dialog.open(jsxc.gui.template.get('authenticationDialog', bid), { name: 'smp' }); // Add handler $('#jsxc_dialog > div:gt(0)').hide(); $('#jsxc_dialog > div:eq(0) button').click(function() { $(this).siblings().removeClass('active'); $(this).addClass('active'); $(this).get(0).blur(); $('#jsxc_dialog > div:gt(0)').hide(); $('#jsxc_dialog > div:eq(' + ($(this).index() + 1) + ')').show().find('input:first').focus(); }); // Manual $('#jsxc_dialog > div:eq(1) .jsxc_submit').click(function() { if (jsxc.master) { jsxc.otr.objects[bid].trust = true; } jsxc.storage.updateUserItem('buddy', bid, 'trust', true); jsxc.gui.dialog.close('smp'); jsxc.storage.updateUserItem('buddy', bid, 'trust', true); jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('conversation_is_now_verified') }); jsxc.gui.update(bid); }); // Question $('#jsxc_dialog > div:eq(2) .jsxc_submit').click(function() { var div = $('#jsxc_dialog > div:eq(2)'); var sec = div.find('#jsxc_secret2').val(); var quest = div.find('#jsxc_quest').val(); if (sec === '' || quest === '') { // Add information for the user which form is missing div.find('input[value=""]').addClass('jsxc_invalid').keyup(function() { if ($(this).val().match(/.*/)) { $(this).removeClass('jsxc_invalid'); } }); return; } if (jsxc.master) { jsxc.otr.sendSmpReq(bid, sec, quest); } else { jsxc.storage.setUserItem('smp', bid, { sec: sec, quest: quest }); } jsxc.gui.dialog.close('smp'); jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('authentication_query_sent') }); }); // Secret $('#jsxc_dialog > div:eq(3) .jsxc_submit').click(function() { var div = $('#jsxc_dialog > div:eq(3)'); var sec = div.find('#jsxc_secret').val(); if (sec === '') { // Add information for the user which form is missing div.find('#jsxc_secret').addClass('jsxc_invalid').keyup(function() { if ($(this).val().match(/.*/)) { $(this).removeClass('jsxc_invalid'); } }); return; } if (jsxc.master) { jsxc.otr.sendSmpReq(bid, sec); } else { jsxc.storage.setUserItem('smp', bid, { sec: sec, quest: null }); } jsxc.gui.dialog.close('smp'); jsxc.gui.window.postMessage({ bid: bid, direction: 'sys', msg: $.t('authentication_query_sent') }); }); }, /** * Create and show approve dialog * * @param {type} from valid jid */ showApproveDialog: function(from) { jsxc.gui.dialog.open(jsxc.gui.template.get('approveDialog'), { 'noClose': true }); $('#jsxc_dialog .jsxc_their_jid').text(Strophe.getBareJidFromJid(from)); $('#jsxc_dialog .jsxc_deny').click(function(ev) { ev.stopPropagation(); jsxc.xmpp.resFriendReq(from, false); jsxc.gui.dialog.close(); }); $('#jsxc_dialog .jsxc_approve').click(function(ev) { ev.stopPropagation(); var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(from)); jsxc.xmpp.resFriendReq(from, true); // If friendship is not mutual show contact dialog if (!data || data.sub === 'from') { jsxc.gui.showContactDialog(from); } }); }, /** * Create and show dialog to add a buddy * * @param {string} [username] jabber id */ showContactDialog: function(username) { jsxc.gui.dialog.open(jsxc.gui.template.get('contactDialog')); // If we got a friendship request, we would display the username in our // response if (username) { $('#jsxc_username').val(username); } $('#jsxc_username').keyup(function() { if (typeof jsxc.options.getUsers === 'function') { var val = $(this).val(); $('#jsxc_userlist').empty(); if (val !== '') { jsxc.options.getUsers.call(this, val, function(list) { $('#jsxc_userlist').empty(); $.each(list || {}, function(uid, displayname) { var option = $('