build/UserALEWebExtension/background.js (430 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable */ // these are default values, which can be overridden by the user on the options page var userAleHost = 'http://localhost:8000'; var userAleScript = 'userale-2.3.0.min.js'; var toolUser = 'nobody'; var toolName = 'test_app'; var toolVersion = '2.3.0'; /* eslint-enable */ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var prefix = 'USERALE_'; var CONFIG_CHANGE = prefix + 'CONFIG_CHANGE'; var ADD_LOG = prefix + 'ADD_LOG'; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the 'License'); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Creates a function to normalize the timestamp of the provided event. * @param {Object} e An event containing a timeStamp property. * @return {timeStampScale~tsScaler} The timestamp normalizing function. */ function timeStampScale(e) { var tsScaler; if (e.timeStamp && e.timeStamp > 0) { var delta = Date.now() - e.timeStamp; /** * Returns a timestamp depending on various browser quirks. * @param {?Number} ts A timestamp to use for normalization. * @return {Number} A normalized timestamp. */ if (delta < 0) { tsScaler = function tsScaler() { return e.timeStamp / 1000; }; } else if (delta > e.timeStamp) { var navStart = performance.timing.navigationStart; tsScaler = function tsScaler(ts) { return ts + navStart; }; } else { tsScaler = function tsScaler(ts) { return ts; }; } } else { tsScaler = function tsScaler() { return Date.now(); }; } return tsScaler; } var __spreadArray = (undefined && undefined.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var BrowserInfo = /** @class */ (function () { function BrowserInfo(name, version, os) { this.name = name; this.version = version; this.os = os; this.type = 'browser'; } return BrowserInfo; }()); var NodeInfo = /** @class */ (function () { function NodeInfo(version) { this.version = version; this.type = 'node'; this.name = 'node'; this.os = process.platform; } return NodeInfo; }()); var SearchBotDeviceInfo = /** @class */ (function () { function SearchBotDeviceInfo(name, version, os, bot) { this.name = name; this.version = version; this.os = os; this.bot = bot; this.type = 'bot-device'; } return SearchBotDeviceInfo; }()); var BotInfo = /** @class */ (function () { function BotInfo() { this.type = 'bot'; this.bot = true; // NOTE: deprecated test name instead this.name = 'bot'; this.version = null; this.os = null; } return BotInfo; }()); var ReactNativeInfo = /** @class */ (function () { function ReactNativeInfo() { this.type = 'react-native'; this.name = 'react-native'; this.version = null; this.os = null; } return ReactNativeInfo; }()); // tslint:disable-next-line:max-line-length var SEARCHBOX_UA_REGEX = /alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/; var SEARCHBOT_OS_REGEX = /(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/; var REQUIRED_VERSION_PARTS = 3; var userAgentRules = [ ['aol', /AOLShield\/([0-9\._]+)/], ['edge', /Edge\/([0-9\._]+)/], ['edge-ios', /EdgiOS\/([0-9\._]+)/], ['yandexbrowser', /YaBrowser\/([0-9\._]+)/], ['kakaotalk', /KAKAOTALK\s([0-9\.]+)/], ['samsung', /SamsungBrowser\/([0-9\.]+)/], ['silk', /\bSilk\/([0-9._-]+)\b/], ['miui', /MiuiBrowser\/([0-9\.]+)$/], ['beaker', /BeakerBrowser\/([0-9\.]+)/], ['edge-chromium', /EdgA?\/([0-9\.]+)/], [ 'chromium-webview', /(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/, ], ['chrome', /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/], ['phantomjs', /PhantomJS\/([0-9\.]+)(:?\s|$)/], ['crios', /CriOS\/([0-9\.]+)(:?\s|$)/], ['firefox', /Firefox\/([0-9\.]+)(?:\s|$)/], ['fxios', /FxiOS\/([0-9\.]+)/], ['opera-mini', /Opera Mini.*Version\/([0-9\.]+)/], ['opera', /Opera\/([0-9\.]+)(?:\s|$)/], ['opera', /OPR\/([0-9\.]+)(:?\s|$)/], ['pie', /^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/], ['pie', /^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) (\d+\.\d+);.*Windows CE.*\)$/], ['netfront', /^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/], ['ie', /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/], ['ie', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/], ['ie', /MSIE\s(7\.0)/], ['bb10', /BB10;\sTouch.*Version\/([0-9\.]+)/], ['android', /Android\s([0-9\.]+)/], ['ios', /Version\/([0-9\._]+).*Mobile.*Safari.*/], ['safari', /Version\/([0-9\._]+).*Safari/], ['facebook', /FB[AS]V\/([0-9\.]+)/], ['instagram', /Instagram\s([0-9\.]+)/], ['ios-webview', /AppleWebKit\/([0-9\.]+).*Mobile/], ['ios-webview', /AppleWebKit\/([0-9\.]+).*Gecko\)$/], ['curl', /^curl\/([0-9\.]+)$/], ['searchbot', SEARCHBOX_UA_REGEX], ]; var operatingSystemRules = [ ['iOS', /iP(hone|od|ad)/], ['Android OS', /Android/], ['BlackBerry OS', /BlackBerry|BB10/], ['Windows Mobile', /IEMobile/], ['Amazon OS', /Kindle/], ['Windows 3.11', /Win16/], ['Windows 95', /(Windows 95)|(Win95)|(Windows_95)/], ['Windows 98', /(Windows 98)|(Win98)/], ['Windows 2000', /(Windows NT 5.0)|(Windows 2000)/], ['Windows XP', /(Windows NT 5.1)|(Windows XP)/], ['Windows Server 2003', /(Windows NT 5.2)/], ['Windows Vista', /(Windows NT 6.0)/], ['Windows 7', /(Windows NT 6.1)/], ['Windows 8', /(Windows NT 6.2)/], ['Windows 8.1', /(Windows NT 6.3)/], ['Windows 10', /(Windows NT 10.0)/], ['Windows ME', /Windows ME/], ['Windows CE', /Windows CE|WinCE|Microsoft Pocket Internet Explorer/], ['Open BSD', /OpenBSD/], ['Sun OS', /SunOS/], ['Chrome OS', /CrOS/], ['Linux', /(Linux)|(X11)/], ['Mac OS', /(Mac_PowerPC)|(Macintosh)/], ['QNX', /QNX/], ['BeOS', /BeOS/], ['OS/2', /OS\/2/], ]; function detect(userAgent) { if (!!userAgent) { return parseUserAgent(userAgent); } if (typeof document === 'undefined' && typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { return new ReactNativeInfo(); } if (typeof navigator !== 'undefined') { return parseUserAgent(navigator.userAgent); } return getNodeVersion(); } function matchUserAgent(ua) { // opted for using reduce here rather than Array#first with a regex.test call // this is primarily because using the reduce we only perform the regex // execution once rather than once for the test and for the exec again below // probably something that needs to be benchmarked though return (ua !== '' && userAgentRules.reduce(function (matched, _a) { var browser = _a[0], regex = _a[1]; if (matched) { return matched; } var uaMatch = regex.exec(ua); return !!uaMatch && [browser, uaMatch]; }, false)); } function parseUserAgent(ua) { var matchedRule = matchUserAgent(ua); if (!matchedRule) { return null; } var name = matchedRule[0], match = matchedRule[1]; if (name === 'searchbot') { return new BotInfo(); } // Do not use RegExp for split operation as some browser do not support it (See: http://blog.stevenlevithan.com/archives/cross-browser-split) var versionParts = match[1] && match[1].split('.').join('_').split('_').slice(0, 3); if (versionParts) { if (versionParts.length < REQUIRED_VERSION_PARTS) { versionParts = __spreadArray(__spreadArray([], versionParts, true), createVersionParts(REQUIRED_VERSION_PARTS - versionParts.length), true); } } else { versionParts = []; } var version = versionParts.join('.'); var os = detectOS(ua); var searchBotMatch = SEARCHBOT_OS_REGEX.exec(ua); if (searchBotMatch && searchBotMatch[1]) { return new SearchBotDeviceInfo(name, version, os, searchBotMatch[1]); } return new BrowserInfo(name, version, os); } function detectOS(ua) { for (var ii = 0, count = operatingSystemRules.length; ii < count; ii++) { var _a = operatingSystemRules[ii], os = _a[0], regex = _a[1]; var match = regex.exec(ua); if (match) { return os; } } return null; } function getNodeVersion() { var isNode = typeof process !== 'undefined' && process.version; return isNode ? new NodeInfo(process.version.slice(1)) : null; } function createVersionParts(count) { var output = []; for (var ii = 0; ii < count; ii++) { output.push('0'); } return output; } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ detect(); /** * Extract the millisecond and microsecond portions of a timestamp. * @param {Number} timeStamp The timestamp to split into millisecond and microsecond fields. * @return {Object} An object containing the millisecond * and microsecond portions of the timestamp. */ function extractTimeFields(timeStamp) { return { milli: Math.floor(timeStamp), micro: Number((timeStamp % 1).toFixed(3)) }; } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var sendIntervalId = null; /** * Initializes the log queue processors. * @param {Array} logs Array of logs to append to. * @param {Object} config Configuration object to use when logging. */ function initSender(logs, config) { if (sendIntervalId !== null) { clearInterval(sendIntervalId); } sendIntervalId = sendOnInterval(logs, config); sendOnClose(logs, config); } /** * Checks the provided log array on an interval, flushing the logs * if the queue has reached the threshold specified by the provided config. * @param {Array} logs Array of logs to read from. * @param {Object} config Configuration object to be read from. * @return {Number} The newly created interval id. */ function sendOnInterval(logs, config) { return setInterval(function () { if (!config.on) { return; } if (logs.length >= config.logCountThreshold) { sendLogs(logs.slice(0), config, 0); // Send a copy logs.splice(0); // Clear array reference (no reassignment) } }, config.transmitInterval); } /** * Attempts to flush the remaining logs when the window is closed. * @param {Array} logs Array of logs to be flushed. * @param {Object} config Configuration object to be read from. */ function sendOnClose(logs, config) { window.addEventListener('pagehide', function () { if (config.on && logs.length > 0) { navigator.sendBeacon(config.url, JSON.stringify(logs)); logs.splice(0); // clear log queue } }); } /** * Sends the provided array of logs to the specified url, * retrying the request up to the specified number of retries. * @param {Array} logs Array of logs to send. * @param {string} config configuration parameters (e.g., to extract URL from & send the POST request to). * @param {Number} retries Maximum number of attempts to send the logs. */ // @todo expose config object to sendLogs replate url with config.url function sendLogs(logs, config, retries) { var req = new XMLHttpRequest(); // @todo setRequestHeader for Auth var data = JSON.stringify(logs); req.open('POST', config.url); if (config.authHeader) { req.setRequestHeader('Authorization', config.authHeader); } req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) { if (retries > 0) { sendLogs(logs, config, retries--); } } }; req.send(data); } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // inherent dependency on globals.js, loaded by the webext // browser is defined in firefox, but not in chrome. In chrome, they use // the 'chrome' global instead. Let's map it to browser so we don't have // to have if-conditions all over the place. var browser = browser || chrome; var logs = []; var config = { autostart: true, url: 'http://localhost:8000', transmitInterval: 5000, logCountThreshold: 5, userId: null, version: null, resolution: 500, time: timeStampScale({}), on: true }; var sessionId = 'session_' + Date.now(); var getTimestamp = typeof performance !== 'undefined' && typeof performance.now !== 'undefined' ? function () { return performance.now() + performance.timing.navigationStart; } : Date.now; browser.storage.local.set({ sessionId: sessionId }); browser.storage.local.get({ userAleHost: userAleHost, userAleScript: userAleScript, toolUser: toolUser, toolName: toolName, toolVersion: toolVersion }, storeCallback); function storeCallback(item) { config = Object.assign({}, config, { url: item.userAleHost, userId: item.toolUser, sessionID: sessionId, toolName: item.toolName, toolVersion: item.toolVersion }); initSender(logs, config); } function dispatchTabMessage(message) { browser.tabs.query({}, function (tabs) { tabs.forEach(function (tab) { browser.tabs.sendMessage(tab.id, message); }); }); } function packageBrowserLog(type, logDetail) { var timeFields = extractTimeFields(getTimestamp()); logs.push({ 'target': null, 'path': null, 'clientTime': timeFields.milli, 'microTime': timeFields.micro, 'location': null, 'type': 'browser.' + type, 'logType': 'raw', 'userAction': true, 'details': logDetail, 'userId': toolUser, 'toolVersion': null, 'toolName': null, 'useraleVersion': null, 'sessionID': sessionId }); } browser.runtime.onMessage.addListener(function (message) { switch (message.type) { case CONFIG_CHANGE: (function () { var updatedConfig = Object.assign({}, config, { url: message.payload.userAleHost, userId: message.payload.toolUser, toolName: message.payload.toolName, toolVersion: message.payload.toolVersion }); initSender(logs, updatedConfig); dispatchTabMessage(message); })(); break; case ADD_LOG: (function () { logs.push(message.payload); })(); break; default: console.log('got unknown message type ', message); } }); function getTabDetailById(tabId, onReady) { browser.tabs.get(tabId, function (tab) { onReady({ active: tab.active, audible: tab.audible, incognito: tab.incognito, index: tab.index, muted: tab.mutedInfo ? tab.mutedInfo.muted : null, pinned: tab.pinned, selected: tab.selected, tabId: tab.id, title: tab.title, url: tab.url, windowId: tab.windowId }); }); } browser.tabs.onActivated.addListener(function (e) { getTabDetailById(e.tabId, function (detail) { packageBrowserLog('tabs.onActivated', detail); }); }); browser.tabs.onCreated.addListener(function (tab, e) { packageBrowserLog('tabs.onCreated', { active: tab.active, audible: tab.audible, incognito: tab.incognito, index: tab.index, muted: tab.mutedInfo ? tab.mutedInfo.muted : null, pinned: tab.pinned, selected: tab.selected, tabId: tab.id, title: tab.title, url: tab.url, windowId: tab.windowId }); }); browser.tabs.onDetached.addListener(function (tabId) { getTabDetailById(tabId, function (detail) { packageBrowserLog('tabs.onDetached', detail); }); }); browser.tabs.onMoved.addListener(function (tabId) { getTabDetailById(tabId, function (detail) { packageBrowserLog('tabs.onMoved', detail); }); }); browser.tabs.onRemoved.addListener(function (tabId) { packageBrowserLog('tabs.onRemoved', { tabId: tabId }); }); browser.tabs.onZoomChange.addListener(function (e) { getTabDetailById(e.tabId, function (detail) { packageBrowserLog('tabs.onZoomChange', Object.assign({}, { oldZoomFactor: e.oldZoomFactor, newZoomFactor: e.newZoomFactor }, detail)); }); }); /* eslint-enable */