//@define UBNativeMessage //@require ./UB.js //@tag UBCore /* @author xmax, mpv */ /** * Registered features. * @property {Object} features * @member UBNativeMessage */ UBNativeMessage.features = { extension: { host: 'none', UIName: 'NMUBExtension', minVersion: '1.0.0', installer: 'pgffhmifenmomiabibdpnceahangimdi' //downloads/UBBrowserNativeMessagesHostApp.exe }, dstu: { host: 'com.inbase.dstu', UIName: 'NMFeatureDSTU', minVersion: '1.0.0.3', installer: 'downloads/UBHostDSTUIITSetup{0}.exe' }, iit: { host: 'com.inbase.iit', UIName: 'NMFeatureIIT', minVersion: '1.0.0.3', installer: 'downloads/UBHostDSTUIITSetup{0}.exe' }, pdfsigner: { host: 'com.inbase.pdfsigner', UIName: 'NMFeaturePDFSigner', minVersion: '1.0.0.3', installer: 'downloads/UBHostPdfSignSetup{0}.' + (UB.isMac ? 'pkg' : 'exe') }, scanner: { host: 'com.inbase.scanner', UIName: 'NMFeatureScanner', minVersion: '1.0.0.4', installer: 'downloads/UBHostScannerSetup{0}.exe', libraryName: 'UBHostScanner.dll' }, docedit: { host: 'com.inbase.docedit', UIName: 'NMFeatureDocEdit', minVersion: '1.0.0.1', installer: 'downloads/UBHostDocEditSetup{0}.exe' } }; UBNativeMessage.features.iit.minVersion = UBNativeMessage.features.dstu.minVersion; /** * Class for communicate with plugin content script using message. * Require DOM element with id ubExtensionPageMessageObj. When use page in iframe parent page must have DOM element with id ubExtensionPageMessageObj. * Usage: * * var nm = new UBNativeMessage('scanner'); * nm.connect().done(UB.logDebug); * nm.connect.then(function(nm){ * UB.logDebug('connected to feature version', nm.featureVersion); * return nm.invoke('methodName', {a: 10, b: 20}) * }).then(UB.logDebug).then(nm.disconnect.bind(nm)); * * * var nm = new UBNativeMessage(); * nm.onMessage = function(message){ * console.log(message); * }; * nm.onDisconnected = function(sender){ * console.log('disconnected'); * }; * nm.connect(5000).done( function(nm){ * nm.sendMessage({text: 'Message : Hello!'}); * }); * * @constructor * @param {String} [feature] Feature we want from plugin. Feature<->application decoding is accessible via {@link UBNativeMessage#features} object */ function UBNativeMessage(feature){ var me = this, __messageCounter = 0; me.getMessageId = function(){ return 'm' + (++__messageCounter); }; ++UBNativeMessage.prototype.idCounter; me.id = 'UBPlugin' + UBNativeMessage.prototype.idCounter; me.pendingMessages = {}; /** * @readonly * @property {String} Feature native messages registered for */ me.feature = feature || 'extension'; /** * Name of plugin interface in host application. * @type {string} */ me.pluginName = feature; me.hostAppName = UBNativeMessage.features[feature].host; if (!me.hostAppName){ throw new Error('unknown feature ' + feature + ' for UBNativeMessage') } /** * Feature version. Defined after success connect() call. * @property {string} featureVersion */ me.featureVersion = ''; /** * Default operation timeout * @property {number} callTimeOut */ me.callTimeOut = 30000; if (UB.isElectron) { me.eventElm = {}; UB.EventEmitter.call(me.eventElm); _.assign(me.eventElm, UB.EventEmitter.prototype); me.eventElm.addEventListener = me.eventElm.addListener; } else { me.eventElm = document.getElementById('ubExtensionPageMessageObj'); if (window.parent){ me.eventElm = window.parent.document.getElementById('ubExtensionPageMessageObj'); } if (!me.eventElm) { throw new Error('Message exchange element with id="ubExtensionPageMessageObj" not found'); } } // must be defined inside constructor for removeEventListener work properly me.onContentMessage = function(event){ var msg, pending, messageID, msgType, totalParts, currentPart, data; msg = event.detail; if (!msg || !msg.hasOwnProperty('msgType') || !msg.hasOwnProperty('messageID') || !msg.hasOwnProperty('clientID')){ console.error('Empty or invalid content message'); } if (msg.clientID !== me.id){ // this is message to another UBNativeMessage instance return; } messageID = msg['messageID']; msgType = msg['msgType']; data = msg['data']; pending = me.pendingMessages[messageID]; if (pending){ clearTimeout(pending.timerID); } if (msgType === 'disconnected') { if (pending) { // disconnect is sended from this delete me.pendingMessages[messageID]; pending.deffer.resolve(data); } me.doOnDisconnect(data); } else { if (msgType === 'notify'){ if (!pending && me.onMessage){ // notification from plugin without messageID me.onMessage.call(me, data); } else { // notification to request. Increase timeout pending.timerID = setTimeout(function () { me.onMsgTimeOut(messageID);}, pending.timeoutValue); pending.deffer.notify(data); } } else if (!pending){ console.error('UBNativeMessage. unknown messageID:' + messageID); } else if (msgType === 'resolve'){ if (msg.hasOwnProperty('part') && msg.hasOwnProperty('totalParts')){ // partial response totalParts = msg['totalParts']; currentPart = msg['part']; if (!pending.partials){ if (totalParts > 100){ //100 Mb limit pending.deffer.reject(new UB.UBError('unknownError', 'UBNativeMessage. Result exceed 100Mb limit')); delete me.pendingMessages[messageID]; throw new Error(new UB.UBError('unknownError', 'UBNativeMessage. Result exceed 100Mb limit')); } pending.partials = new Array(totalParts) } else { if ((totalParts !== pending.partials.length) || (currentPart >= totalParts)){ pending.deffer.reject('Invalid part count'); delete me.pendingMessages[messageID]; throw new Error('Invalid part count'); } } pending.partials[currentPart] = data; if (_.indexOf(pending.partials, undefined) ===-1){ // all parts come - ready to resolve. lodash using is important here - Array.indexOf not wok with `undefined` data = pending.partials.join(''); delete me.pendingMessages[messageID]; if ((data.charAt(0) === '{') || (data.charAt(0) === '[')) { // data is JSON data = JSON.parse(data); } pending.deffer.resolve(data) } else { pending.timerID = setTimeout(function () { me.onMsgTimeOut(messageID);}, pending.timeoutValue); } } else { delete me.pendingMessages[messageID]; pending.deffer.resolve(data); } } else if (msgType === 'reject'){ delete me.pendingMessages[messageID]; var isUserMessage = false, err; if ( /<<<.*>>>/.test(data)){ data = data.match(/<<<(.*)>>>/)[1]; isUserMessage = true; } if (isUserMessage){ err = new UB.UBError(data); } else { err = new UB.UBError('unknownError', data); } // pending.deffer.reject(new Error(data)); pending.deffer.reject(err); } else { throw new Error('UBNativeMessage. Invalid msgType type in: ' + msg); } } }; me.eventElm.addEventListener('UBExtensionMsg', me.onContentMessage); /** * Called when disconnecting the plugin. * @property {Function} onDisconnected */ me.onDisconnected = null; /** * Called when receive new `notify` message from host application not to invoked method. * @property {Function} onMessage */ me.onMessage = null; } UBNativeMessage.versionToNumber = function (versionStr){ var arr = versionStr.split('.'), mutliplier = 1, i, l =arr.length, res = 0; if (arr.length > 4){ throw new Error ('Invalid version number ' + versionStr); } for(i=l-1; i>=0; i--){ res += parseInt(arr[i], 10)*mutliplier; mutliplier *= (i===l-1) ? 10000 : 1000; //last number may be 4 digit 1.2.3.1234 } return res; }; /** * Invoke feature method with optional params * @param {String} methodName * @param {Object} [methodParams] Do not pass empty object {} here! * @param {Number} [timeout] operation timeout. Default to {@link UBNativeMessage#callTimeOut} * @return {Promise} */ UBNativeMessage.prototype.invoke = function(methodName, methodParams, timeout){ var me = this, msgID = me.getMessageId(), messageToSend, defer, pendingRequest; if (!me.connected && methodName.substr(0,2) !== '__'){ //allow pseudo methods return Q.reject(new UB.UBError('unknownError', 'UBNativeMessage. Not connected. call connect() first')); } //methodParams = methodParams || null; timeout = timeout || me.callTimeOut; messageToSend = {clientID: me.id, messageID: msgID, method: methodName, params: methodParams}; defer = Q.defer(); pendingRequest = { request: null, //MPV - do not store - we do not need it! messageToSend, deffer: defer, timerID: setTimeout(function(){ me.onMsgTimeOut(msgID); }, timeout ), partials: null, timeoutValue: timeout || me.callTimeOut, stTime: (new Date()).getTime() }; me.pendingMessages[msgID] = pendingRequest; if (UB.isElectron) { if (methodName === '__extensionVersion') { me.eventElm.emit('UBExtensionMsg', { detail: { clientID: me.id, messageID: msgID, msgType: 'resolve', data: UBNativeMessage.features.extension.minVersion } }); } else if (methodName === '__connect') { var path = require('path'), ffi =require(path.join(process.cwd(),'node_modules','ffi')), Library = ffi.Library; me.doInvoke = new Library( path.join(process.cwd(), UBNativeMessage.features[me.feature].libraryName), {'invoke': ['void', ['string', 'pointer'], { async: true }]} ).invoke; me.funcPtr = ffi.Callback('void', [ 'string' ], function(param) { console.log('***', param, '***'); debugger; var detail = JSON.parse(param); //console.log('+++', detail, '+'); var detail = JSON.parse(param); me.eventElm.emit('UBExtensionMsg', { detail: detail }); } ); messageToSend.method = 'getVersion'; me.doInvoke(JSON.stringify(messageToSend), me.funcPtr, function(){}); } else { me.doInvoke(JSON.stringify(messageToSend), me.funcPtr, function(){}); } } else { me.eventElm.dispatchEvent(new CustomEvent('UBPageMsg', {detail: messageToSend})); } return defer.promise; }; /** * Return true if browser extension was installed * @returns {boolean} */ UBNativeMessage.extensionExists = function(){ if (UB.isElectron) return true; var e; e = document.getElementById('ubExtensionPageMessageObj'); if (window.parent){ e = window.parent.document.getElementById('ubExtensionPageMessageObj'); } return !!e && (e.getAttribute('data-extensionAttached') === 'YES'); }; UBNativeMessage.prototype.doOnDisconnect = function(reason){ var me = this, rejections = me.pendingMessages; me.pendingMessages = {}; // prevent several rejection me.connected = false; if (rejections) { _.forEach(rejections, function (pendingRequest) { if (pendingRequest && pendingRequest.deffer) { pendingRequest.deffer.reject(reason); } }); } }; /** * Connect to native messages host. Check extension & host is installed and up to date (according to UBNativeMessage.features). * @param {Number} [timeOut] Connection timeOut in millisecond. Default to UBNativeMessage.callTimeOut * @returns {Promise} resolved to UBNativeMessage or rejected to installation/upgrade message */ UBNativeMessage.prototype.connect = function(timeOut){ var me = this; if (me.connected){ return Q.resolve(me); } else { if (!UBNativeMessage.extensionExists()){ return Q.reject(new UB.UBError(UBNativeMessage.createFeatureUpdateMsg('extension', '-', false))); } else { return me.invoke('__extensionVersion').then(function(extensionVersion) { var versionNum = UBNativeMessage.versionToNumber(extensionVersion); if (versionNum < UBNativeMessage.versionToNumber(UBNativeMessage.features.extension.minVersion)) { UB.logDebug('browser extension version', extensionVersion, 'is smaller when required', UBNativeMessage.features.extension.minVersion); throw new UB.UBError(UBNativeMessage.createFeatureUpdateMsg('extension', extensionVersion, true)); } else { if (versionNum !== UBNativeMessage.versionToNumber(UBNativeMessage.features.extension.minVersion)){ UB.logDebug('Current version of extension', extensionVersion, 'is more than required', UBNativeMessage.features.extension.minVersion); } return true; } }).then(function(){ return me.invoke('__connect', {hostAppName: me.hostAppName}, timeOut).then(function(featureVersion) { var requiredVersion = UBNativeMessage.features[me.feature].minVersion; me.connected = true; me.featureVersion = featureVersion; if (UBNativeMessage.versionToNumber(featureVersion) < UBNativeMessage.versionToNumber(requiredVersion)){ throw new UB.UBError(UBNativeMessage.createFeatureUpdateMsg(me.feature, featureVersion, true)); } else if (featureVersion !== requiredVersion){ UB.logDebug('Current version of feature', me.feature, featureVersion, 'is more than required', requiredVersion) } return me; }, function(reason){ UB.logError(reason); throw new UB.UBError(UBNativeMessage.createFeatureUpdateMsg(me.feature, '-', false)); }) }).fail(function(reason){ me.disconnect().done(); throw reason; }); } } }; /** * Disconnect from native * @return {*} */ UBNativeMessage.prototype.disconnect = function(){ var me = this; if (!me.connected){ return Q.resolve(true); } return me.invoke('__disconnect').then(function(message){ UB.logDebug('UBNativeMessage. Disconnected with message', message); me.connected = false; if (me.eventElm) { me.eventElm.removeEventListener('UBExtensionMsg', me.onContentMessage); } return true; }); }; UBNativeMessage.prototype.onMsgTimeOut = function(msgID){ var me = this, pending; pending = me.pendingMessages[msgID]; if (pending){ pending.timerID = null; delete me.pendingMessages[msgID]; pending.deffer.reject(new UB.UBError('unknownError', 'pluginMethodCallTimedOut')); } }; /** * @private * @type {number} */ UBNativeMessage.prototype.idCounter = 0; UBNativeMessage.createFeatureUpdateMsg = function(featureName, currentVersion, isUpdate){ var featureInfo = UBNativeMessage.features[featureName], res, msg, installer = UB.format(featureInfo.installer, featureInfo.minVersion /*.replace(/\./g, '_')*/); msg = 'NM' + (isUpdate ? 'Update' : 'Install') + ((featureName === 'extension') ? 'Extension' + (UB.isOpera ? 'Opera': 'Chrome') : 'Feature'); res = UB.format(UB.i18n(msg), UB.i18n(featureInfo.UIName), featureInfo.minVersion, currentVersion, installer); return res; };