var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; /// <reference path="../base/BaseClass.ts" /> /// <reference path="../env/LocalStorage.ts" /> /// <reference path="../env/SessionStorage.ts" /> /// <reference path="../privacyprotection/ProfilePermission.ts" /> /// <reference path="../util/Json.ts" /> /// <reference path="../data.ts" /> /// <reference path="../profileaccess/PublicEndpointRequest.ts" /> var econda; (function (econda) { var privacyprotection; (function (privacyprotection) { var LocalStorage = econda.env.LocalStorage; var ProfilePermission = econda.privacyprotection.ProfilePermission; var SessionStorage = econda.env.SessionStorage; var Json = econda.util.Json; /** * Class for helper function for privacy protection. * * @class econda.privacyprotection.EmosPrivacy */ var EmosPrivacy = (function (_super) { __extends(EmosPrivacy, _super); function EmosPrivacy() { _super.apply(this, arguments); } EmosPrivacy.PRIVACY_PROTECTION_KEY = 'econda.privacy.protection'; EmosPrivacy.PROFILE_STATE_KEY = 'profile.state'; EmosPrivacy.PRIVACY_PROTECTION_UPDATED_HASH = 'econda.privacy.updatehash'; return EmosPrivacy; }(econda.base.BaseClass)); privacyprotection.EmosPrivacy = EmosPrivacy; /** * Lockup for localstorage permissions.profile.state * * @returns {boolean} if ALLOW */ privacyprotection.hasProfileOptIn = function () { var permissions = privacyprotection.getPermissionsFromLocalStorage(); return permissions.profile.state === "ALLOW"; }; /** * Lockup for localstorage permissions.profile.version * * @returns {string} the version */ privacyprotection.getProfileOptInVersion = function () { var permissions = privacyprotection.getPermissionsFromLocalStorage(); return permissions.profile.version; }; /** * Lockup for localstorage channel (e.g. BILLING) permission state * * @returns {boolean} if ALLOW */ privacyprotection.hasChannelOptIn = function (channel) { var permissions = privacyprotection.getPermissionsFromLocalStorage(); var state = permissions.channels[channel].state; return state === "ALLOW"; }; /** * Wrapper function for personal data that returns the original value in case of opt-in or an empty value otherwise. * * Personal data must only be transferred and stored if the current visitor has given the according permission. * This function reads and applies the current privacy settings. Personal data includes identifiers such as user ID or email hash. * * This function should be used to wrap all personal data as input to the econda Analytics library (emos.js). * Based on the default events measured in Analytics, this includes login, registration and order. * * <h3> Example for login:</h3> * * <script type="text/javascript"> * window.emos3.send({ * content: 'login/success', // page name * login: [econda.privacyprotection.emptyIfNotProfileOptIn('4A923bDc'), 0] // login * }); * </script> * </pre> * * This wrapper function can only be used for optional values. For values that need to be non empty and unique * (such as the order number) {@link econda.privacyprotection.EmosPrivacy econda.privacyprotection.anonymiseIfNotProfileOptIn(value)} must be used * * @param {string} value the value to check * @returns {string} value or empty */ privacyprotection.emptyIfNotProfileOptIn = function (value) { var permission = privacyprotection.getPermissionsFromLocalStorage(); if (permission.profile.state === "ALLOW") { return value; } else { return ''; } }; /** * Wrapper function for personal data that returns the original value in case of opt-in or an unique anonymous * value otherwise. * * Personal data must only be transferred and stored if the current visitor has given the according permission. * This function reads and applies the current privacy settings. Personal data includes identifiers such as the order number. * * This function should be used to wrap all personal data as input to the econda Analytics library (emos.js) * that needs to be non empty and unique. In the default data model, the order number as part of the order event is the only effected value.: * * <h3>Example for order:</h3> * * <script type="text/javascript"> * window.emos3.send({ * content: 'checkout/confirmation', // page name * billing: [ // billing informat * econda.privacyprotection.anonymiseIfNotProfileOptIn('I-501-1234-23'), * econda.privacyprotection.emptyIfNotProfileOptIn('4A923bDc'), * 'DE/Karlsruhe/76135', * 29.90 * ], * ec_Event: [ // purchased products * { * type: 'buy', * pid: 'P-1003', * sku: 'P-1003-001-23-125', * name: 'POWERFIX overall', * price: 29.90, * group: 'men/workwear', * count: 1, * var1: 'XXL', * var2: 'blue', * var3: 'multipockets' * } * ] * }); * </script> * * Most of personal data is optional in econda Analytics. For optional data the wrapper function * {@link econda.privacyprotection.EmosPrivacy econda.privacyprotection.emptyIfNotProfileOptIn()} * should be used. * * @param {string} value the value to anonymized * @returns {string} the anonymized value */ privacyprotection.anonymiseIfNotProfileOptIn = function (value) { var permission = privacyprotection.getPermissionsFromLocalStorage(); if (permission.profile.state === "ALLOW") { return value; } else { var anonymisedValue = SessionStorage.getItem("ecAnonValues." + value); if (!anonymisedValue || anonymisedValue === '') { anonymisedValue = "X-" + Date.now().toString(16) + Math.floor(Math.random() * 4294967296).toString(16); SessionStorage.setItem("ecAnonValues." + value, anonymisedValue); } return anonymisedValue; } }; /** * Updates the privacy settings for the current visitor from the backend. The main scenario is the propagation of * opt-outs or updates of channel permissions. * * These permissions could have been updated on other devices or using other channels (e.g. call center). * updating the settings from the backend applies the changes to the current device and therefore synchronizes * the privacy settings across devices and channels. * * As prerequisite, a profile endpoint with key 'ENDPOINT-KEY' has to be created in the econda ARP UI, that * returns the profile properties 'permissions:profile' and 'permissions:channels' without authentication * <a href="http://doku.econda.org/display/CSDE/Endpoints+erstellen">LINK: Endpoints+erstellen</a> . * * As result, the current privacy settings will be written or updated in local storage. The update is * asynchronous and will have effect on future access, e.g. the next click or page load. * * The actual call to the service will only be triggered if new ID information is available. * This includes changes in the Cookie ID as well as new logins * * {@link econda.recengine.VisitorProfile#login econda.data.visitor.login} * * In case of no changes there will be no operation. * * @param {string} clientKey access for client * @param {string} endpointKey key for specific endpoint */ privacyprotection.updatePrivacySettingsFromBackend = function (clientKey, endpointKey) { if (!clientKey || typeof clientKey !== "string" || !endpointKey || typeof endpointKey !== "string") { econda.debug.log('updatePrivacySettingsFromBackend called with incorrect input', clientKey, endpointKey); return; } if (!econda.data.visitor.getVisitorId()) { return; } var actualHash = SessionStorage.getItem(EmosPrivacy.PRIVACY_PROTECTION_UPDATED_HASH); var currentIds = econda.data.visitor.getIds(); if (actualHash === privacyprotection._getHashCodeFor(currentIds)) { return; } privacyprotection._callProfileEndpoint(clientKey, endpointKey); SessionStorage.setItem(EmosPrivacy.PRIVACY_PROTECTION_UPDATED_HASH, privacyprotection._getHashCodeFor(currentIds)); }; /** * Get current privacy settings from local storage and return ProfilePermission. ProfilePermission has a profile * with state "UNKNOWN||DENY||ALLOW" and Channels with the same structure. * * Main scenario is e.g. to check for each contact the profile permission state and inform the visitor explicit * about options to contradict collection of personal data, in case that permission state is "UNKNOWN". * * If local storage is not available or no OptIn/OptOut is set permissions.profile.state is UNKNOWN. * * For more information follow the link: * <a href="https://support.econda.de/display/CSDE/Anforderungen+aus+DSGVO">LINK: Anforderungen+aus+DSGVO</a> * * @returns {econda.privacyprotection.ProfilePermission} */ privacyprotection.getPermissionsFromLocalStorage = function () { var permissions = new ProfilePermission(); if (LocalStorage.isAvailable()) { var privacyProtectionData = LocalStorage.getItem(EmosPrivacy.PRIVACY_PROTECTION_KEY); if (privacyProtectionData) { try { //privacySettings = JSON.parse(privacyProtectionData); permissions = JSON.parse(privacyProtectionData); } catch (error) { console.log("Error parsing item from local storage"); } } else { permissions.profile.state = econda.privacyprotection.STATE_UNKNOWN; } } else { permissions.profile.state = econda.privacyprotection.STATE_UNKNOWN; } return permissions; }; /** * Function to change the current privacy settings. The settings will be updated on the current device, and * additionally the updated will be added to an emos property object. The privacy settings in the backend are updated by sending this modified emos property object. * * The format of the updated corresponds to the format of permissions that are returned from the backend by * profile endpoints. * * <h3>Example:</h3> * * <script type="text/javascript"> * window.emosProps = {...}; * * econda.privacyprotection.applyAndStoreNewPrivacySettings( * window.emosProps, * { * "permissions:profile": { * state: "ALLOW|DENY", * version: "VERSION", * source: "SOURCE" * }, * "permissions:channels": { * "CHANNEL": { * state: "ALLOW|DENY", * version: "VERSION", * source: "SOURCE" * } * } * } * ); * window.emos3.send(window.emosProps); * * </script> * * Updates of the profile permissions as well as updates of channel permissions are optional, only updated values * should be given as input to this function. * * The update of the backend is asynchronous and will be active in the timespan of ~5 minutes. The privacy * settings on the current device are stored locally and are active immediately. * * In case of profile opt-out ("permissions:profile"."state" = DENY), the current cookie ID will still be used to * send the privacy settings updates. The cookie ID will be deleted before the sending the next page impression when calling * {@link econda.privacyprotection.EmosPrivacy econda.privacyprotection.setEmos3PrivacySettings()} * * @param {Object} emosProps actual props * @param {Object} externalPermissions the new permissions to merge */ privacyprotection.applyAndStoreNewPrivacySettings = function (emosProps, externalPermissions) { var newPermissions = privacyprotection._convertExternalPermissionsToProfilePermissions(externalPermissions); var currentPermissions = privacyprotection.getPermissionsFromLocalStorage(); var mergedPermissions = privacyprotection._mergePermissions(currentPermissions, newPermissions); LocalStorage.setItem(EmosPrivacy.PRIVACY_PROTECTION_KEY, JSON.stringify(mergedPermissions)); var isOptOutAction = newPermissions.profile.state == "DENY" && currentPermissions.profile.state == "ALLOW"; // emos Privacy Einstellungen bei Opt-Out basierend auf altem Stand, // ansonsten basierend auf gemergetem Stand setzen // -> bei Opt-Out wird somit für den aktuellen PI noch das ALLOW verwendet _setEmos3PrivacySettingsBasedOn(isOptOutAction ? currentPermissions : mergedPermissions); _setEmosArpProps(mergedPermissions, emosProps); }; /** * Note: Its intended to allow duplicated permissions in ARPPROPS for permissions. The user should call this * function only once at the beginning of site rendering. Duplications will be ignored at the backend. * * @param {econda.privacyprotection.ProfilePermission} mergedPermissions * @param {Object} emosProps * @private */ function _setEmosArpProps(mergedPermissions, emosProps) { if (!mergedPermissions) { return; } if (!emosProps["arpprops"]) { emosProps["arpprops"] = []; } if (mergedPermissions.profile) { var state = mergedPermissions.profile.state ? mergedPermissions.profile.state : ''; var version = mergedPermissions.profile.version ? mergedPermissions.profile.version : ''; var source = mergedPermissions.profile.source ? mergedPermissions.profile.source : ''; var profilePermission = "profile/" + state + "/" + version + "/" + source; emosProps["arpprops"].push(["PERMISSION", profilePermission]); } var channelKeys = Object.keys(mergedPermissions.channels); for (var i = 0; i < channelKeys.length; i++) { var channel = channelKeys[i]; var state = mergedPermissions.channels[channel].state ? mergedPermissions.channels[channel].state : ''; var version = mergedPermissions.channels[channel].version ? mergedPermissions.channels[channel].version : ''; var source = mergedPermissions.channels[channel].source ? mergedPermissions.channels[channel].source : ''; var channelPermission = "channel/" + channel + "/" + state + "/" + version + "/" + source; emosProps["arpprops"].push(["PERMISSION", channelPermission]); } } privacyprotection._setEmosArpProps = _setEmosArpProps; /** * Reads the current privacy settings and applies all relevant settings to the econda Analytics library (emos.js). * * If there are no changes to permission, this function should be called before triggering the measurement: * * <h3>Example:</h3> * * <script type="text/javascript"> * econda.privacyprotection.setEmos3PrivacySettings(); * window.emos3.send(...); * </script> * </pre> * * In case of opt-in, the cookie ID is used or generated. In case of opt-out, the cookie ID will not be used and * won't be generated if not present. * * If there are changes to the privacy settings, the function * {@link econda.privacyprotection.EmosPrivacy econda.privacyprotection.applyAndStoreNewPrivacySettings()} * has to be used instead of this function. */ privacyprotection.setEmos3PrivacySettings = function () { var permissions = privacyprotection.getPermissionsFromLocalStorage(); _setEmos3PrivacySettingsBasedOn(permissions); }; var _setEmos3PrivacySettingsBasedOn = function (permissions) { if (window['emos3']) { if (permissions.profile.state === "ALLOW") { window['emos3'].VCL = 730; // Aktiviere Auslesen der Recipient-ID window['emos3'].PARAM_TO_PROP_MERGE = { ecmUid: 'newsuid' }; } else { // Visitor-Cookie löschen falls vorhanden var hostname = window.location.hostname; document.cookie = privacyprotection._getVisitorCookieToOverwrite(hostname); window['emos3'].VCL = 0; // Deaktiviere Auslesen der Recipient-ID window['emos3'].PARAM_TO_PROP_MERGE = { ecmUid: null }; } } }; privacyprotection._getVisitorCookieToOverwrite = function (hostname) { var domainParts = hostname.split('.'); var cookieDomain = hostname; if (domainParts.length > 1) { var lastPart = domainParts[domainParts.length - 1]; var secondLastPart = domainParts[domainParts.length - 2]; cookieDomain = '.'.concat(secondLastPart, '.', lastPart); } var cookieName = "emos_jcvid="; return cookieName + "; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=" + cookieDomain + ";"; }; privacyprotection._callProfileEndpoint = function (clientKey, endpointKey) { var request = new econda.profileaccess.PublicEndpointRequest({ accountId: clientKey, endpointKey: endpointKey, context: { appendVisitorData: true }, success: function (response) { privacyprotection._endpointCallback(response); }, error: function (error) { econda.debug.log('Error calling profile endpoint to get permissions' + endpointKey, error); } }); request.send(); }; privacyprotection._endpointCallback = function (response) { var serverPermissions = privacyprotection._convertExternalPermissionsToProfilePermissions(response); var browserPermissions = privacyprotection.getPermissionsFromLocalStorage(); var mergedPermissions = privacyprotection._mergePermissions(browserPermissions, serverPermissions); LocalStorage.setItem(EmosPrivacy.PRIVACY_PROTECTION_KEY, JSON.stringify(mergedPermissions)); }; privacyprotection._convertExternalPermissionsToProfilePermissions = function (externalSettings) { var permissionsToReturn = new ProfilePermission(); var responseProfile = externalSettings["permissions:profile"]; var parsePassedDateStringOrNow = function (responseTimestamp) { // timestamps returned from server are always ISO formatted strings, // see e.g. here http://confluence.econda.org/display/IN/Rohprofile+aus+API+laden+und+in+S3+oder+HDFS+ablegen return responseTimestamp ? Date.parse(responseTimestamp) : new Date().getTime(); }; if (responseProfile) { var timestamp = parsePassedDateStringOrNow(responseProfile.timestamp); permissionsToReturn.profile = { state: responseProfile.state, version: responseProfile.version, source: responseProfile.source, timestamp: timestamp }; } var responseChannels = externalSettings["permissions:channels"]; if (responseChannels) { permissionsToReturn.channels = {}; var allProperties = Object.getOwnPropertyNames(responseChannels); for (var i = 0; i < allProperties.length; i++) { var channel = allProperties[i]; var timestamp = parsePassedDateStringOrNow(responseChannels[channel].timestamp); permissionsToReturn.channels[channel] = { state: responseChannels[channel].state, version: responseChannels[channel].version, source: responseChannels[channel].source, timestamp: timestamp }; } } return permissionsToReturn; }; privacyprotection._mergePermissions = function (permissionsA, permissionsB) { var mergedSettings = new ProfilePermission(); if (!permissionsB.profile.timestamp) { mergedSettings.profile = permissionsA.profile; } else if (!permissionsA.profile.timestamp) { mergedSettings.profile = permissionsB.profile; } else { mergedSettings.profile = permissionsA.profile.timestamp > permissionsB.profile.timestamp ? permissionsA.profile : permissionsB.profile; } mergedSettings.channels = {}; var permissionsAKeys = Object.keys(permissionsA.channels); var permissionsBKeys = Object.keys(permissionsB.channels); var allKeys = permissionsAKeys.concat(permissionsBKeys); for (var i = 0; i < allKeys.length; i++) { var key = allKeys[i]; if (!permissionsB.channels[key]) { mergedSettings.channels[key] = permissionsA.channels[key]; } else if (!permissionsA.channels[key]) { mergedSettings.channels[key] = permissionsB.channels[key]; } else { mergedSettings.channels[key] = permissionsA.channels[key].timestamp > permissionsB.channels[key].timestamp ? permissionsA.channels[key] : permissionsB.channels[key]; } } return mergedSettings; }; /** * Internal hash function. Just an easy shifting thing. * * @param {Object} obj the object which should be hashed * @returns {string} the hash * @private */ privacyprotection._getHashCodeFor = function (obj) { if (!obj) { return; } var objAsString = Json.stringify(obj); var hash = 0; var length = objAsString.length; var i = 0; if (length > 0) { while (i < length) { hash = (hash << 5) - hash + objAsString.charCodeAt(i++) | 0; } } return hash.toString(); }; })(privacyprotection = econda.privacyprotection || (econda.privacyprotection = {})); })(econda || (econda = {}));