UNPKG

@jaxolotl/wdclib

Version:

Required Library to bridge javascript with Tableau. Use the scripts below to create the combined shim library. i.e. npm run-script build_and_copy

485 lines (402 loc) 16.9 kB
/* globals require */ import * as ApprovedOrigins from './ApprovedOrigins'; import { getBuildNumber } from './DevUtils/BuildNumber'; import deStringsMap from './resources/Shim_lib_resources_de-DE.json'; import enStringsMap from './resources/Shim_lib_resources_en-US.json'; import esStringsMap from './resources/Shim_lib_resources_es-ES.json'; import jaStringsMap from './resources/Shim_lib_resources_ja-JP.json'; import frStringsMap from './resources/Shim_lib_resources_fr-FR.json'; import koStringsMap from './resources/Shim_lib_resources_ko-KR.json'; import ptStringsMap from './resources/Shim_lib_resources_pt-BR.json'; import zhStringsMap from './resources/Shim_lib_resources_zh-CN.json'; // Required for IE & Edge which don't support endsWith // require('String.prototype.endsWith'); // now using babel-polyfill const BUILD_NUMBER = getBuildNumber(); /** * Used for communicating between the simulator and web data connector. It does * this by passing messages between the WDC window and its parent window */ class SimulatorDispatcher { /** * * @param {Object} globalObj - the global object to find tableau interfaces as well * as register events (usually window) */ constructor (globalObj) { this.globalObj = globalObj; this._initMessageHandling(); this._initPublicInterface(); this._initPrivateInterface(); } /** * @returns {Undefined} */ _initMessageHandling () { console.log('Initializing message handling'); this.globalObj.addEventListener('message', this._receiveMessage.bind(this), false); this.globalObj.document.addEventListener('DOMContentLoaded', this._onDomContentLoaded.bind(this)); } /** * @returns {Undefined} */ _onDomContentLoaded () { // Attempt to notify the simulator window that the WDC has loaded if (this.globalObj.parent !== window) { this.globalObj.parent.postMessage(this._buildMessagePayload('loaded'), '*'); } if (this.globalObj.opener) { // Wrap in try/catch for older versions of IE try { this.globalObj.opener.postMessage(this._buildMessagePayload('loaded'), '*'); } catch (e) { console.warn('Some versions of IE may not accurately simulate the Web Data Connector. Please retry on a Webkit based browser'); } } } /** * @returns {Object} */ _packagePropertyValues () { let propValues = { 'connectionName': this.globalObj.tableau.connectionName, 'connectionData': this.globalObj.tableau.connectionData, 'password': this.globalObj.tableau.password, 'username': this.globalObj.tableau.username, 'usernameAlias': this.globalObj.tableau.usernameAlias, 'incrementalExtractColumn': this.globalObj.tableau.incrementalExtractColumn, 'versionNumber': this.globalObj.tableau.versionNumber, 'locale': this.globalObj.tableau.locale, 'authPurpose': this.globalObj.tableau.authPurpose, 'platformOS': this.globalObj.tableau.platformOS, 'platformVersion': this.globalObj.tableau.platformVersion, 'platformEdition': this.globalObj.tableau.platformEdition, 'platformBuildNumber': this.globalObj.tableau.platformBuildNumber }; return propValues; } /** * * @param {Object} props * @returns {Undefined} */ _applyPropertyValues (props) { if (props) { this.globalObj.tableau.connectionName = props.connectionName; this.globalObj.tableau.connectionData = props.connectionData; this.globalObj.tableau.password = props.password; this.globalObj.tableau.username = props.username; this.globalObj.tableau.usernameAlias = props.usernameAlias; this.globalObj.tableau.incrementalExtractColumn = props.incrementalExtractColumn; this.globalObj.tableau.locale = props.locale; this.globalObj.tableau.language = props.locale; this.globalObj.tableau.authPurpose = props.authPurpose; this.globalObj.tableau.platformOS = props.platformOS; this.globalObj.tableau.platformVersion = props.platformVersion; this.globalObj.tableau.platformEdition = props.platformEdition; this.globalObj.tableau.platformBuildNumber = props.platformBuildNumber; } } /** * * @param {String} msgName * @param {String} msgData * @param {String} props * @returns {String} */ _buildMessagePayload (msgName, msgData, props) { let msgObj = { 'msgName': msgName, 'msgData': msgData, 'props': props, 'version': BUILD_NUMBER }; return JSON.stringify(msgObj); } /** * * @param {*} msgName * @param {*} msgData * @returns {Undefined} */ _sendMessage (msgName, msgData) { let messagePayload = this._buildMessagePayload(msgName, msgData, this._packagePropertyValues()); let wdcHandler; try { // try to use webkit.messageHandlers but don't fail since we have a fallback wdcHandler = this.globalObj.webkit.messageHandlers.wdcHandler; } catch (e) { // wdcHandler not found on webkit.messageHandlers to post messagePayload // we might be on the simulator } // Check first to see if we have a wdcHandler defined on messageHandlers to post the message to if (wdcHandler) { wdcHandler.postMessage(messagePayload); } else if (!this._sourceWindow) { throw new Error('Looks like the WDC is calling a tableau function before tableau.init() has been called.'); } else { // Make sure we only post this info back to the source origin the user approved in _getWebSecurityWarningConfirm this._sourceWindow.postMessage(messagePayload, this._sourceOrigin); } } /** * * @param {String} payloadString * @returns {Object|null} */ _getPayloadObj (payloadString) { let payload; try { payload = JSON.parse(payloadString); } catch (e) { return null; } return payload; } /** * @returns {Boolean} */ _getWebSecurityWarningConfirm () { // Due to cross-origin security issues over https, we may not be able to retrieve _sourceWindow. // Use sourceOrigin instead. let origin = this._sourceOrigin; let Uri = require('jsuri'); // @todo review this let parsedOrigin = new Uri(origin); let hostName = parsedOrigin.host(); const SUPPORTED_HOSTS = ['localhost', 'tableau.github.io']; if (SUPPORTED_HOSTS.indexOf(hostName) >= 0) { return true; } // Whitelist Tableau domains if (hostName && hostName.endsWith('online.tableau.com')) { return true; } let alreadyApprovedOrigins = ApprovedOrigins.getApprovedOrigins(); if (alreadyApprovedOrigins.indexOf(origin) >= 0) { // The user has already approved this origin, no need to ask again console.log(`Already approved the origin ${origin} , not asking again`); return true; } let localizedWarningTitle = this._getLocalizedString('webSecurityWarning'); let completeWarningMsg = localizedWarningTitle + '\n\n' + hostName + '\n'; let isConfirmed = confirm(completeWarningMsg); if (isConfirmed) { // Set a session cookie to mark that we've approved this already ApprovedOrigins.addApprovedOrigin(origin); } return isConfirmed; } /** * @returns {String} */ _getCurrentLocale () { // Use current browser's locale to get a localized warning message let currentBrowserLanguage = (navigator.language || navigator.userLanguage); let locale = currentBrowserLanguage ? currentBrowserLanguage.substring(0, 2) : 'en'; let supportedLocales = ['de', 'en', 'es', 'fr', 'ja', 'ko', 'pt', 'zh']; // can we move this to package json as we did with connectors? // Fall back to English for other unsupported lanaguages if (supportedLocales.indexOf(locale) < 0) { locale = 'en'; } return locale; } /** * * @param {String} stringKey * @returns {String} */ _getLocalizedString (stringKey) { let locale = this._getCurrentLocale(); // Use static require here, otherwise webpack would generate a much bigger JS file // ( the increment in size is irrelevant, specially when minimized, moving to the top and import [JAX]) // let deStringsMap = require('json-loader!./resources/Shim_lib_resources_de-DE.json'); // let enStringsMap = require('json-loader!./resources/Shim_lib_resources_en-US.json'); // let esStringsMap = require('json-loader!./resources/Shim_lib_resources_es-ES.json'); // let jaStringsMap = require('json-loader!./resources/Shim_lib_resources_ja-JP.json'); // let frStringsMap = require('json-loader!./resources/Shim_lib_resources_fr-FR.json'); // let koStringsMap = require('json-loader!./resources/Shim_lib_resources_ko-KR.json'); // let ptStringsMap = require('json-loader!./resources/Shim_lib_resources_pt-BR.json'); // let zhStringsMap = require('json-loader!./resources/Shim_lib_resources_zh-CN.json'); let stringJsonMapByLocale = { 'de': deStringsMap, 'en': enStringsMap, 'es': esStringsMap, 'fr': frStringsMap, 'ja': jaStringsMap, 'ko': koStringsMap, 'pt': ptStringsMap, 'zh': zhStringsMap }; let localizedStringsJson = stringJsonMapByLocale[locale]; return localizedStringsJson[stringKey]; } /** * * @param {Object} evt * @returns {Undefined} */ _receiveMessage (evt) { console.log('Received message!'); let wdc = this.globalObj._wdc; if (!wdc) { throw new Error('No WDC registered. Did you forget to call tableau.registerConnector?'); } let payloadObj = this._getPayloadObj(evt.data); if (!payloadObj) { return; // This message is not needed for WDC } if (!this._sourceWindow) { this._sourceWindow = evt.source; this._sourceOrigin = evt.origin; } let msgData = payloadObj.msgData; this._applyPropertyValues(payloadObj.props); switch (payloadObj.msgName) { case 'init': // Warn users about possible phinishing attacks if (!this._getWebSecurityWarningConfirm()) { window.close(); } else { this.globalObj.tableau.phase = msgData.phase; this.globalObj._tableau.triggerInitialization(); } break; case 'shutdown': this.globalObj._tableau.triggerShutdown(); break; case 'getSchema': this.globalObj._tableau.triggerSchemaGathering(); break; case 'getData': this.globalObj._tableau.triggerDataGathering(msgData.tablesAndIncrementValues); break; } } /** * PUBLIC INTERFACE * @returns {Undefined} */ _initPublicInterface () { console.log('Initializing public interface'); this._submitCalled = false; // Assign the public interface to this this.publicInterface = { abortForAuth: this._abortForAuth.bind(this), abortWithError: this._abortWithError.bind(this), addCrossOriginException: this._addCrossOriginException.bind(this), log: this._log.bind(this), reportProgress: this._reportProgress.bind(this), submit: this._submit.bind(this) }; } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.abortforauth * * @param {String} msg * @returns {Undefined} */ _abortForAuth (msg) { this._sendMessage('abortForAuth', { 'msg': msg }); } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.abortwitherror * * @param {String} msg * @returns {Undefined} */ _abortWithError (msg) { this._sendMessage('abortWithError', { 'errorMsg': msg }); } /** * Missing documentation online, we need to add one * * @param {Array} destOriginList * @returns {Undefined} */ _addCrossOriginException (destOriginList) { // Don't bother passing this back to the simulator since there's nothing it can // do. Just call back to the WDC indicating that it worked console.log('Cross Origin Exception requested in the simulator. Pretending to work.'); // can we return a promise so we can listen for resolution instead of blindly wait? setTimeout(function () { this.globalObj._wdc.addCrossOriginExceptionCompleted(destOriginList); }.bind(this), 0); } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.log * * @param {String} msg * @returns {Undefined} */ _log (msg) { this._sendMessage('log', { 'logMsg': msg }); } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.reportProgress * * @param {String} progressMessage * @returns {Undefined} */ _reportProgress (progressMessage) { this._sendMessage('reportProgress', { 'progressMsg': progressMessage }); } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.submit * * @returns {Undefined} */ _submit () { this._sendMessage('submit'); } /** * PRIVATE INTERFACE * @returns {Undefined} */ _initPrivateInterface () { console.log('Initializing private interface'); // Assign the private interface to this this.privateInterface = { _initCallback: this._initCallback.bind(this), _shutdownCallback: this._shutdownCallback.bind(this), _schemaCallback: this._schemaCallback.bind(this), _tableDataCallback: this._tableDataCallback.bind(this), _dataDoneCallback: this._dataDoneCallback.bind(this) }; } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.initcallback * * @returns {Undefined} */ _initCallback () { this._sendMessage('initCallback'); } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.shutdowncallback * * @returns {Undefined} */ _shutdownCallback () { this._sendMessage('shutdownCallback'); } /** * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.schemacallback * * @param {Array<TableInfo>} schema TableInfo @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableinfo-1 * @param {Array<StandardConnection>} standardConnections StandardConnection @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.standardconnection * @returns {Undefined} */ _schemaCallback (schema, standardConnections = []) { this._sendMessage('_schemaCallback', { 'schema': schema, 'standardConnections': standardConnections }); } /** * * @param {String} tableName * @param {*} data * @returns {Undefined} */ _tableDataCallback (tableName, data) { this._sendMessage('_tableDataCallback', { 'tableName': tableName, 'data': data }); } /** * @returns {Undefined} */ _dataDoneCallback () { this._sendMessage('_dataDoneCallback'); } } export default SimulatorDispatcher;