debian-mirror-gitlab/snowplow-javascript-tracker/src/js/lib/helpers.js

437 lines
12 KiB
JavaScript
Executable File

/*
* JavaScript tracker for Snowplow: Snowplow.js
*
* Significant portions copyright 2010 Anthon Pang. Remainder copyright
* 2012-2014 Snowplow Analytics Ltd. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the
* names of their contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
;(function () {
var
isString = require('lodash/isString'),
isUndefined = require('lodash/isUndefined'),
isObject = require('lodash/isObject'),
map = require('lodash/map'),
cookie = require('browser-cookie-lite'),
object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support
/**
* Cleans up the page title
*/
object.fixupTitle = function (title) {
if (!isString(title)) {
title = title.text || '';
var tmp = document.getElementsByTagName('title');
if (tmp && !isUndefined(tmp[0])) {
title = tmp[0].text;
}
}
return title;
};
/**
* Extract hostname from URL
*/
object.getHostName = function (url) {
// scheme : // [username [: password] @] hostname [: port] [/ [path] [? query] [# fragment]]
var e = new RegExp('^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)'),
matches = e.exec(url);
return matches ? matches[1] : url;
};
/**
* Fix-up domain
*/
object.fixupDomain = function (domain) {
var dl = domain.length;
// remove trailing '.'
if (domain.charAt(--dl) === '.') {
domain = domain.slice(0, dl);
}
// remove leading '*'
if (domain.slice(0, 2) === '*.') {
domain = domain.slice(1);
}
return domain;
};
/**
* Get page referrer. In the case of a single-page app,
* if the URL changes without the page reloading, pass
* in the old URL. It will be returned unless overriden
* by a "refer(r)er" parameter in the querystring.
*
* @param string oldLocation Optional.
* @return string The referrer
*/
object.getReferrer = function (oldLocation) {
var referrer = '';
var fromQs = object.fromQuerystring('referrer', window.location.href) ||
object.fromQuerystring('referer', window.location.href);
// Short-circuit
if (fromQs) {
return fromQs;
}
// In the case of a single-page app, return the old URL
if (oldLocation) {
return oldLocation;
}
try {
referrer = window.top.document.referrer;
} catch (e) {
if (window.parent) {
try {
referrer = window.parent.document.referrer;
} catch (e2) {
referrer = '';
}
}
}
if (referrer === '') {
referrer = document.referrer;
}
return referrer;
};
/**
* Cross-browser helper function to add event handler
*/
object.addEventListener = function (element, eventType, eventHandler, useCapture) {
if (element.addEventListener) {
element.addEventListener(eventType, eventHandler, useCapture);
return true;
}
if (element.attachEvent) {
return element.attachEvent('on' + eventType, eventHandler);
}
element['on' + eventType] = eventHandler;
};
/**
* Return value from name-value pair in querystring
*/
object.fromQuerystring = function (field, url) {
var match = new RegExp('^[^#]*[?&]' + field + '=([^&#]*)').exec(url);
if (!match) {
return null;
}
return decodeURIComponent(match[1].replace(/\+/g, ' '));
};
/*
* Find dynamic context generating functions and merge their results into the static contexts
* Combine an array of unchanging contexts with the result of a context-creating function
*
* @param {(object|function(...*): ?object)[]} dynamicOrStaticContexts Array of custom context Objects or custom context generating functions
* @param {...*} Parameters to pass to dynamic callbacks
*/
object.resolveDynamicContexts = function (dynamicOrStaticContexts) {
let params = Array.prototype.slice.call(arguments, 1);
return map(dynamicOrStaticContexts, function(context) {
if (typeof context === 'function') {
try {
return context.apply(null, params);
} catch (e) {
//TODO: provide warning
}
} else {
return context;
}
});
};
/**
* Only log deprecation warnings if they won't cause an error
*/
object.warn = function(message) {
if (typeof console !== 'undefined') {
console.warn('Snowplow: ' + message);
}
};
/**
* List the classes of a DOM element without using elt.classList (for compatibility with IE 9)
*/
object.getCssClasses = function (elt) {
return elt.className.match(/\S+/g) || [];
};
/**
* Check whether an element has at least one class from a given list
*/
function checkClass(elt, classList) {
var classes = object.getCssClasses(elt),
i;
for (i = 0; i < classes.length; i++) {
if (classList[classes[i]]) {
return true;
}
}
return false;
}
/**
* Convert a criterion object to a filter function
*
* @param object criterion Either {whitelist: [array of allowable strings]}
* or {blacklist: [array of allowable strings]}
* or {filter: function (elt) {return whether to track the element}
* @param boolean byClass Whether to whitelist/blacklist based on an element's classes (for forms)
* or name attribute (for fields)
*/
object.getFilter = function (criterion, byClass) {
// If the criterion argument is not an object, add listeners to all elements
if (Array.isArray(criterion) || !isObject(criterion)) {
return function () {
return true;
};
}
if (criterion.hasOwnProperty('filter')) {
return criterion.filter;
} else {
var inclusive = criterion.hasOwnProperty('whitelist');
var specifiedClasses = criterion.whitelist || criterion.blacklist;
if (!Array.isArray(specifiedClasses)) {
specifiedClasses = [specifiedClasses];
}
// Convert the array of classes to an object of the form {class1: true, class2: true, ...}
var specifiedClassesSet = {};
for (var i=0; i<specifiedClasses.length; i++) {
specifiedClassesSet[specifiedClasses[i]] = true;
}
if (byClass) {
return function (elt) {
return checkClass(elt, specifiedClassesSet) === inclusive;
};
} else {
return function (elt) {
return elt.name in specifiedClassesSet === inclusive;
};
}
}
};
/**
* Convert a criterion object to a transform function
*
* @param object criterion {transform: function (elt) {return the result of transform function applied to element}
*/
object.getTransform = function (criterion) {
if (!isObject(criterion)) {
return function(x) { return x };
}
if (criterion.hasOwnProperty('transform')) {
return criterion.transform;
} else {
return function(x) { return x };
}
return function(x) { return x };
};
/**
* Add a name-value pair to the querystring of a URL
*
* @param string url URL to decorate
* @param string name Name of the querystring pair
* @param string value Value of the querystring pair
*/
object.decorateQuerystring = function (url, name, value) {
var initialQsParams = name + '=' + value;
var hashSplit = url.split('#');
var qsSplit = hashSplit[0].split('?');
var beforeQuerystring = qsSplit.shift();
// Necessary because a querystring may contain multiple question marks
var querystring = qsSplit.join('?');
if (!querystring) {
querystring = initialQsParams;
} else {
// Whether this is the first time the link has been decorated
var initialDecoration = true;
var qsFields = querystring.split('&');
for (var i=0; i<qsFields.length; i++) {
if (qsFields[i].substr(0, name.length + 1) === name + '=') {
initialDecoration = false;
qsFields[i] = initialQsParams;
querystring = qsFields.join('&');
break;
}
}
if (initialDecoration) {
querystring = initialQsParams + '&' + querystring;
}
}
hashSplit[0] = beforeQuerystring + '?' + querystring;
return hashSplit.join('#');
};
/**
* Attempt to get a value from localStorage
*
* @param string key
* @return string The value obtained from localStorage, or
* undefined if localStorage is inaccessible
*/
object.attemptGetLocalStorage = function (key) {
try {
return localStorage.getItem(key);
} catch(e) {}
};
/**
* Attempt to write a value to localStorage
*
* @param string key
* @param string value
* @return boolean Whether the operation succeeded
*/
object.attemptWriteLocalStorage = function (key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch(e) {
return false;
}
};
/**
* Finds the root domain
*/
object.findRootDomain = function () {
var cookiePrefix = '_sp_root_domain_test_';
var cookieName = cookiePrefix + new Date().getTime();
var cookieValue = '_test_value_' + new Date().getTime();
var split = window.location.hostname.split('.');
var position = split.length - 1;
while (position >= 0) {
var currentDomain = split.slice(position, split.length).join('.');
cookie.cookie(cookieName, cookieValue, 0, '/', currentDomain);
if (cookie.cookie(cookieName) === cookieValue) {
// Clean up created cookie(s)
object.deleteCookie(cookieName, currentDomain);
var cookieNames = object.getCookiesWithPrefix(cookiePrefix);
for (var i = 0; i < cookieNames.length; i++) {
object.deleteCookie(cookieNames[i], currentDomain);
}
return currentDomain;
}
position -= 1;
}
// Cookies cannot be read
return window.location.hostname;
};
/**
* Checks whether a value is present within an array
*
* @param val The value to check for
* @param array The array to check within
* @return boolean Whether it exists
*/
object.isValueInArray = function (val, array) {
for (var i = 0; i < array.length; i++) {
if (array[i] === val) {
return true;
}
}
return false;
};
/**
* Deletes an arbitrary cookie by setting the expiration date to the past
*
* @param cookieName The name of the cookie to delete
* @param domainName The domain the cookie is in
*/
object.deleteCookie = function (cookieName, domainName) {
cookie.cookie(cookieName, '', -1, '/', domainName);
};
/**
* Fetches the name of all cookies beginning with a certain prefix
*
* @param cookiePrefix The prefix to check for
* @return array The cookies that begin with the prefix
*/
object.getCookiesWithPrefix = function (cookiePrefix) {
var cookies = document.cookie.split("; ");
var cookieNames = [];
for (var i = 0; i < cookies.length; i++) {
if (cookies[i].substring(0, cookiePrefix.length) === cookiePrefix) {
cookieNames.push(cookies[i]);
}
}
return cookieNames;
};
/**
* Parses an object and returns either the
* integer or undefined.
*
* @param obj The object to parse
* @return the result of the parse operation
*/
object.parseInt = function (obj) {
var result = parseInt(obj);
return isNaN(result) ? undefined : result;
};
/**
* Parses an object and returns either the
* number or undefined.
*
* @param obj The object to parse
* @return the result of the parse operation
*/
object.parseFloat = function (obj) {
var result = parseFloat(obj);
return isNaN(result) ? undefined : result;
}
}());