From 8ea28de6e3cae17c99f8aba3d058ba782ec28240 Mon Sep 17 00:00:00 2001 From: robocoder <anthon.pang@gmail.com> Date: Wed, 1 Aug 2012 00:39:48 +0000 Subject: [PATCH] convert tabs to spaces git-svn-id: http://dev.piwik.org/svn/trunk@6620 59fd770c-687e-43c8-a1e3-f5a4ff64c105 --- js/piwik.js | 4722 +++++++++++++++++++++++++-------------------------- 1 file changed, 2361 insertions(+), 2361 deletions(-) diff --git a/js/piwik.js b/js/piwik.js index 1151a480ca..983069528d 100644 --- a/js/piwik.js +++ b/js/piwik.js @@ -42,7 +42,7 @@ // methods in a closure to avoid creating global variables. if (!this.JSON2) { - this.JSON2 = {}; + this.JSON2 = {}; } (function () { @@ -58,12 +58,12 @@ if (!this.JSON2) { if (objectType === '[object Date]') { return isFinite(value.valueOf()) ? - value.getUTCFullYear() + '-' + - f(value.getUTCMonth() + 1) + '-' + - f(value.getUTCDate()) + 'T' + - f(value.getUTCHours()) + ':' + - f(value.getUTCMinutes()) + ':' + - f(value.getUTCSeconds()) + 'Z' : null; + value.getUTCFullYear() + '-' + + f(value.getUTCMonth() + 1) + '-' + + f(value.getUTCDate()) + 'T' + + f(value.getUTCHours()) + ':' + + f(value.getUTCMinutes()) + ':' + + f(value.getUTCSeconds()) + 'Z' : null; } if (objectType === '[object String]' || @@ -81,7 +81,7 @@ if (!this.JSON2) { } var cx = new RegExp('[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]', 'g'), - // hack: workaround Snort false positive (sid 8443) + // hack: workaround Snort false positive (sid 8443) pattern = '\\\\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]', escapable = new RegExp('[' + pattern, 'g'), gap, @@ -108,7 +108,7 @@ if (!this.JSON2) { return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } @@ -354,7 +354,7 @@ if (!this.JSON2) { // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? - walk({'': j}, '') : j; + walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. @@ -373,2417 +373,2417 @@ if (!this.JSON2) { /*global ActiveXObject */ /*global _paq:true */ /*members encodeURIComponent, decodeURIComponent, getElementsByTagName, - shift, unshift, - addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies, - cookie, domain, readyState, documentElement, doScroll, title, text, - location, top, document, referrer, parent, links, href, protocol, GearsFactory, - event, which, button, srcElement, type, target, - parentNode, tagName, hostname, className, - userAgent, cookieEnabled, platform, mimeTypes, enabledPlugin, javaEnabled, - XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, send, readyState, status, - getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds, - toLowerCase, charAt, indexOf, lastIndexOf, split, slice, toUpperCase, - onload, src, - round, random, - exec, - res, width, height, - pdf, qt, realp, wma, dir, fla, java, gears, ag, - hook, getHook, getVisitorId, getVisitorInfo, setTrackerUrl, setSiteId, - getAttributionInfo, getAttributionCampaignName, getAttributionCampaignKeyword, - getAttributionReferrerTimestamp, getAttributionReferrerUrl, - setCustomData, getCustomData, - setCustomVariable, getCustomVariable, deleteCustomVariable, - setDownloadExtensions, addDownloadExtensions, - setDomains, setIgnoreClasses, setRequestMethod, - setReferrerUrl, setCustomUrl, setDocumentTitle, - setDownloadClasses, setLinkClasses, - setCampaignNameKey, setCampaignKeywordKey, - discardHashTag, - setCookieNamePrefix, setCookieDomain, setCookiePath, setVisitorIdCookie, - setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout, - setConversionAttributionFirstReferrer, - doNotTrack, setDoNotTrack, msDoNotTrack, - addListener, enableLinkTracking, setLinkTrackingTimer, - setHeartBeatTimer, killFrame, redirectFile, setCountPreRendered, - trackGoal, trackLink, trackPageView, setEcommerceView, addEcommerceItem, trackEcommerceOrder, trackEcommerceCartUpdate, - addPlugin, getTracker, getAsyncTracker + shift, unshift, + addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies, + cookie, domain, readyState, documentElement, doScroll, title, text, + location, top, document, referrer, parent, links, href, protocol, GearsFactory, + event, which, button, srcElement, type, target, + parentNode, tagName, hostname, className, + userAgent, cookieEnabled, platform, mimeTypes, enabledPlugin, javaEnabled, + XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, send, readyState, status, + getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds, + toLowerCase, charAt, indexOf, lastIndexOf, split, slice, toUpperCase, + onload, src, + round, random, + exec, + res, width, height, + pdf, qt, realp, wma, dir, fla, java, gears, ag, + hook, getHook, getVisitorId, getVisitorInfo, setTrackerUrl, setSiteId, + getAttributionInfo, getAttributionCampaignName, getAttributionCampaignKeyword, + getAttributionReferrerTimestamp, getAttributionReferrerUrl, + setCustomData, getCustomData, + setCustomVariable, getCustomVariable, deleteCustomVariable, + setDownloadExtensions, addDownloadExtensions, + setDomains, setIgnoreClasses, setRequestMethod, + setReferrerUrl, setCustomUrl, setDocumentTitle, + setDownloadClasses, setLinkClasses, + setCampaignNameKey, setCampaignKeywordKey, + discardHashTag, + setCookieNamePrefix, setCookieDomain, setCookiePath, setVisitorIdCookie, + setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout, + setConversionAttributionFirstReferrer, + doNotTrack, setDoNotTrack, msDoNotTrack, + addListener, enableLinkTracking, setLinkTrackingTimer, + setHeartBeatTimer, killFrame, redirectFile, setCountPreRendered, + trackGoal, trackLink, trackPageView, setEcommerceView, addEcommerceItem, trackEcommerceOrder, trackEcommerceCartUpdate, + addPlugin, getTracker, getAsyncTracker */ var - // asynchronous tracker (or proxy) - _paq = _paq || [], - - // Piwik singleton and namespace - Piwik = Piwik || (function () { - "use strict"; - - /************************************************************ - * Private data - ************************************************************/ - - var expireDateTime, - - /* plugins */ - plugins = {}, - - /* alias frequently used globals for added minification */ - documentAlias = document, - navigatorAlias = navigator, - screenAlias = screen, - windowAlias = window, - - /* DOM Ready */ - hasLoaded = false, - registeredOnLoadHandlers = [], - - /* encode */ - encodeWrapper = windowAlias.encodeURIComponent, - - /* decode */ - decodeWrapper = windowAlias.decodeURIComponent, - - /* urldecode */ - urldecode = unescape, - - /* asynchronous tracker */ - asyncTracker, - - /* iterator */ - i; - - /************************************************************ - * Private methods - ************************************************************/ - - /* - * Is property defined? - */ - function isDefined(property) { - // workaround https://github.com/douglascrockford/JSLint/commit/24f63ada2f9d7ad65afc90e6d949f631935c2480 - return 'undefined' !== typeof property; - } - - /* - * Is property a function? - */ - function isFunction(property) { - return typeof property === 'function'; - } - - /* - * Is property an object? - * - * @return bool Returns true if property is null, an Object, or subclass of Object (i.e., an instanceof String, Date, etc.) - */ - function isObject(property) { - return typeof property === 'object'; - } - - /* - * Is property a string? - */ - function isString(property) { - return typeof property === 'string' || property instanceof String; - } - - /* - * apply wrapper - * - * @param array parameterArray An array comprising either: - * [ 'methodName', optional_parameters ] - * or: - * [ functionObject, optional_parameters ] - */ - function apply() { - var i, f, parameterArray; - - for (i = 0; i < arguments.length; i += 1) { - parameterArray = arguments[i]; - f = parameterArray.shift(); - - if (isString(f)) { - asyncTracker[f].apply(asyncTracker, parameterArray); - } else { - f.apply(asyncTracker, parameterArray); - } - } - } - - /* - * Cross-browser helper function to add event handler - */ - function addEventListener(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; - } - - /* - * Call plugin hook methods - */ - function executePluginMethod(methodName, callback) { - var result = '', - i, - pluginMethod; - - for (i in plugins) { - if (Object.prototype.hasOwnProperty.call(plugins, i)) { - pluginMethod = plugins[i][methodName]; - if (isFunction(pluginMethod)) { - result += pluginMethod(callback); - } - } - } - - return result; - } - - /* - * Handle beforeunload event - * - * Subject to Safari's "Runaway JavaScript Timer" and - * Chrome V8 extension that terminates JS that exhibits - * "slow unload", i.e., calling getTime() > 1000 times - */ - function beforeUnloadHandler() { - var now; - - executePluginMethod('unload'); - - /* - * Delay/pause (blocks UI) - */ - if (expireDateTime) { - // the things we do for backwards compatibility... - // in ECMA-262 5th ed., we could simply use: - // while (Date.now() < expireDateTime) { } - do { - now = new Date(); - } while (now.getTimeAlias() < expireDateTime); - } - } - - /* - * Handler for onload event - */ - function loadHandler() { - var i; - - if (!hasLoaded) { - hasLoaded = true; - executePluginMethod('load'); - for (i = 0; i < registeredOnLoadHandlers.length; i++) { - registeredOnLoadHandlers[i](); - } - } - return true; - } - - /* - * Add onload or DOM ready handler - */ - function addReadyListener() { - var _timer; - - if (documentAlias.addEventListener) { - addEventListener(documentAlias, 'DOMContentLoaded', function ready() { - documentAlias.removeEventListener('DOMContentLoaded', ready, false); - loadHandler(); - }); - } else if (documentAlias.attachEvent) { - documentAlias.attachEvent('onreadystatechange', function ready() { - if (documentAlias.readyState === 'complete') { - documentAlias.detachEvent('onreadystatechange', ready); - loadHandler(); - } - }); - - if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) { - (function ready() { - if (!hasLoaded) { - try { - documentAlias.documentElement.doScroll('left'); - } catch (error) { - setTimeout(ready, 0); - return; - } - loadHandler(); - } - }()); - } - } - - // sniff for older WebKit versions - if ((new RegExp('WebKit')).test(navigatorAlias.userAgent)) { - _timer = setInterval(function () { - if (hasLoaded || /loaded|complete/.test(documentAlias.readyState)) { - clearInterval(_timer); - loadHandler(); - } - }, 10); - } - - // fallback - addEventListener(windowAlias, 'load', loadHandler, false); - } - - /* - * Get page referrer - */ - function getReferrer() { - var referrer = ''; - - try { - referrer = windowAlias.top.document.referrer; - } catch (e) { - if (windowAlias.parent) { - try { - referrer = windowAlias.parent.document.referrer; - } catch (e2) { - referrer = ''; - } - } - } - if (referrer === '') { - referrer = documentAlias.referrer; - } - - return referrer; - } - - /* - * Extract scheme/protocol from URL - */ - function getProtocolScheme(url) { - var e = new RegExp('^([a-z]+):'), - matches = e.exec(url); - - return matches ? matches[1] : null; - } - - /* - * Extract hostname from URL - */ - function getHostName(url) { - // scheme : // [username [: password] @] hostame [: port] [/ [path] [? query] [# fragment]] - var e = new RegExp('^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)'), - matches = e.exec(url); - - return matches ? matches[1] : url; - } - - /* - * Extract parameter from URL - */ - function getParameter(url, name) { - // scheme : // [username [: password] @] hostame [: port] [/ [path] [? query] [# fragment]] - var e = new RegExp('^(?:https?|ftp)(?::/*(?:[^?]+)[?])([^#]+)'), - matches = e.exec(url), - f = new RegExp('(?:^|&)' + name + '=([^&]*)'), - result = matches ? f.exec(matches[1]) : 0; - - return result ? decodeWrapper(result[1]) : ''; - } - - /* - * UTF-8 encoding - */ - function utf8_encode(argString) { - return urldecode(encodeWrapper(argString)); - } - - /************************************************************ - * sha1 - * - based on sha1 from http://phpjs.org/functions/sha1:512 (MIT / GPL v2) - ************************************************************/ - function sha1(str) { - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + namespaced by: Michael White (http://getsprink.com) - // + input by: Brett Zamir (http://brett-zamir.me) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + jslinted by: Anthon Pang (http://piwik.org) - - var - rotate_left = function (n, s) { - return (n << s) | (n >>> (32 - s)); - }, - - cvt_hex = function (val) { - var str = '', - i, - v; - - for (i = 7; i >= 0; i--) { - v = (val >>> (i * 4)) & 0x0f; - str += v.toString(16); - } - return str; - }, - - blockstart, - i, - j, - W = [], - H0 = 0x67452301, - H1 = 0xEFCDAB89, - H2 = 0x98BADCFE, - H3 = 0x10325476, - H4 = 0xC3D2E1F0, - A, - B, - C, - D, - E, - temp, - str_len, - word_array = []; - - str = utf8_encode(str); - str_len = str.length; - - for (i = 0; i < str_len - 3; i += 4) { - j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | - str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3); - word_array.push(j); - } - - switch (str_len & 3) { - case 0: - i = 0x080000000; - break; - case 1: - i = str.charCodeAt(str_len - 1) << 24 | 0x0800000; - break; - case 2: - i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000; - break; - case 3: - i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) << 8 | 0x80; - break; - } - - word_array.push(i); - - while ((word_array.length & 15) !== 14) { - word_array.push(0); - } - - word_array.push(str_len >>> 29); - word_array.push((str_len << 3) & 0x0ffffffff); - - for (blockstart = 0; blockstart < word_array.length; blockstart += 16) { - for (i = 0; i < 16; i++) { - W[i] = word_array[blockstart + i]; - } - - for (i = 16; i <= 79; i++) { - W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); - } - - A = H0; - B = H1; - C = H2; - D = H3; - E = H4; - - for (i = 0; i <= 19; i++) { - temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; - E = D; - D = C; - C = rotate_left(B, 30); - B = A; - A = temp; - } - - for (i = 20; i <= 39; i++) { - temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; - E = D; - D = C; - C = rotate_left(B, 30); - B = A; - A = temp; - } - - for (i = 40; i <= 59; i++) { - temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; - E = D; - D = C; - C = rotate_left(B, 30); - B = A; - A = temp; - } - - for (i = 60; i <= 79; i++) { - temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; - E = D; - D = C; - C = rotate_left(B, 30); - B = A; - A = temp; - } - - H0 = (H0 + A) & 0x0ffffffff; - H1 = (H1 + B) & 0x0ffffffff; - H2 = (H2 + C) & 0x0ffffffff; - H3 = (H3 + D) & 0x0ffffffff; - H4 = (H4 + E) & 0x0ffffffff; - } - - temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4); - return temp.toLowerCase(); - } - /************************************************************ - * end sha1 - ************************************************************/ - - /* - * Fix-up URL when page rendered from search engine cache or translated page - */ - function urlFixup(hostName, href, referrer) { - if (hostName === 'translate.googleusercontent.com') { // Google - if (referrer === '') { - referrer = href; - } - href = getParameter(href, 'u'); - hostName = getHostName(href); - } else if (hostName === 'cc.bingj.com' || // Bing - hostName === 'webcache.googleusercontent.com' || // Google - hostName.slice(0, 5) === '74.6.') { // Yahoo (via Inktomi 74.6.0.0/16) - href = documentAlias.links[0].href; - hostName = getHostName(href); - } - return [hostName, href, referrer]; - } - - /* - * Fix-up domain - */ - function domainFixup(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; - } - - /* - * Title fixup - */ - function titleFixup(title) { - if (!isString(title)) { - title = title.text || ''; - - var tmp = documentAlias.getElementsByTagName('title'); - if (tmp && isDefined(tmp[0])) { - title = tmp[0].text; - } - } - return title; - } - - /* - * Piwik Tracker class - * - * trackerUrl and trackerSiteId are optional arguments to the constructor - * - * See: Tracker.setTrackerUrl() and Tracker.setSiteId() - */ - function Tracker(trackerUrl, siteId) { - - /************************************************************ - * Private members - ************************************************************/ - - var + // asynchronous tracker (or proxy) + _paq = _paq || [], + + // Piwik singleton and namespace + Piwik = Piwik || (function () { + "use strict"; + + /************************************************************ + * Private data + ************************************************************/ + + var expireDateTime, + + /* plugins */ + plugins = {}, + + /* alias frequently used globals for added minification */ + documentAlias = document, + navigatorAlias = navigator, + screenAlias = screen, + windowAlias = window, + + /* DOM Ready */ + hasLoaded = false, + registeredOnLoadHandlers = [], + + /* encode */ + encodeWrapper = windowAlias.encodeURIComponent, + + /* decode */ + decodeWrapper = windowAlias.decodeURIComponent, + + /* urldecode */ + urldecode = unescape, + + /* asynchronous tracker */ + asyncTracker, + + /* iterator */ + i; + + /************************************************************ + * Private methods + ************************************************************/ + + /* + * Is property defined? + */ + function isDefined(property) { + // workaround https://github.com/douglascrockford/JSLint/commit/24f63ada2f9d7ad65afc90e6d949f631935c2480 + return 'undefined' !== typeof property; + } + + /* + * Is property a function? + */ + function isFunction(property) { + return typeof property === 'function'; + } + + /* + * Is property an object? + * + * @return bool Returns true if property is null, an Object, or subclass of Object (i.e., an instanceof String, Date, etc.) + */ + function isObject(property) { + return typeof property === 'object'; + } + + /* + * Is property a string? + */ + function isString(property) { + return typeof property === 'string' || property instanceof String; + } + + /* + * apply wrapper + * + * @param array parameterArray An array comprising either: + * [ 'methodName', optional_parameters ] + * or: + * [ functionObject, optional_parameters ] + */ + function apply() { + var i, f, parameterArray; + + for (i = 0; i < arguments.length; i += 1) { + parameterArray = arguments[i]; + f = parameterArray.shift(); + + if (isString(f)) { + asyncTracker[f].apply(asyncTracker, parameterArray); + } else { + f.apply(asyncTracker, parameterArray); + } + } + } + + /* + * Cross-browser helper function to add event handler + */ + function addEventListener(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; + } + + /* + * Call plugin hook methods + */ + function executePluginMethod(methodName, callback) { + var result = '', + i, + pluginMethod; + + for (i in plugins) { + if (Object.prototype.hasOwnProperty.call(plugins, i)) { + pluginMethod = plugins[i][methodName]; + if (isFunction(pluginMethod)) { + result += pluginMethod(callback); + } + } + } + + return result; + } + + /* + * Handle beforeunload event + * + * Subject to Safari's "Runaway JavaScript Timer" and + * Chrome V8 extension that terminates JS that exhibits + * "slow unload", i.e., calling getTime() > 1000 times + */ + function beforeUnloadHandler() { + var now; + + executePluginMethod('unload'); + + /* + * Delay/pause (blocks UI) + */ + if (expireDateTime) { + // the things we do for backwards compatibility... + // in ECMA-262 5th ed., we could simply use: + // while (Date.now() < expireDateTime) { } + do { + now = new Date(); + } while (now.getTimeAlias() < expireDateTime); + } + } + + /* + * Handler for onload event + */ + function loadHandler() { + var i; + + if (!hasLoaded) { + hasLoaded = true; + executePluginMethod('load'); + for (i = 0; i < registeredOnLoadHandlers.length; i++) { + registeredOnLoadHandlers[i](); + } + } + return true; + } + + /* + * Add onload or DOM ready handler + */ + function addReadyListener() { + var _timer; + + if (documentAlias.addEventListener) { + addEventListener(documentAlias, 'DOMContentLoaded', function ready() { + documentAlias.removeEventListener('DOMContentLoaded', ready, false); + loadHandler(); + }); + } else if (documentAlias.attachEvent) { + documentAlias.attachEvent('onreadystatechange', function ready() { + if (documentAlias.readyState === 'complete') { + documentAlias.detachEvent('onreadystatechange', ready); + loadHandler(); + } + }); + + if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) { + (function ready() { + if (!hasLoaded) { + try { + documentAlias.documentElement.doScroll('left'); + } catch (error) { + setTimeout(ready, 0); + return; + } + loadHandler(); + } + }()); + } + } + + // sniff for older WebKit versions + if ((new RegExp('WebKit')).test(navigatorAlias.userAgent)) { + _timer = setInterval(function () { + if (hasLoaded || /loaded|complete/.test(documentAlias.readyState)) { + clearInterval(_timer); + loadHandler(); + } + }, 10); + } + + // fallback + addEventListener(windowAlias, 'load', loadHandler, false); + } + + /* + * Get page referrer + */ + function getReferrer() { + var referrer = ''; + + try { + referrer = windowAlias.top.document.referrer; + } catch (e) { + if (windowAlias.parent) { + try { + referrer = windowAlias.parent.document.referrer; + } catch (e2) { + referrer = ''; + } + } + } + if (referrer === '') { + referrer = documentAlias.referrer; + } + + return referrer; + } + + /* + * Extract scheme/protocol from URL + */ + function getProtocolScheme(url) { + var e = new RegExp('^([a-z]+):'), + matches = e.exec(url); + + return matches ? matches[1] : null; + } + + /* + * Extract hostname from URL + */ + function getHostName(url) { + // scheme : // [username [: password] @] hostame [: port] [/ [path] [? query] [# fragment]] + var e = new RegExp('^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)'), + matches = e.exec(url); + + return matches ? matches[1] : url; + } + + /* + * Extract parameter from URL + */ + function getParameter(url, name) { + // scheme : // [username [: password] @] hostame [: port] [/ [path] [? query] [# fragment]] + var e = new RegExp('^(?:https?|ftp)(?::/*(?:[^?]+)[?])([^#]+)'), + matches = e.exec(url), + f = new RegExp('(?:^|&)' + name + '=([^&]*)'), + result = matches ? f.exec(matches[1]) : 0; + + return result ? decodeWrapper(result[1]) : ''; + } + + /* + * UTF-8 encoding + */ + function utf8_encode(argString) { + return urldecode(encodeWrapper(argString)); + } + + /************************************************************ + * sha1 + * - based on sha1 from http://phpjs.org/functions/sha1:512 (MIT / GPL v2) + ************************************************************/ + function sha1(str) { + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + namespaced by: Michael White (http://getsprink.com) + // + input by: Brett Zamir (http://brett-zamir.me) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + jslinted by: Anthon Pang (http://piwik.org) + + var + rotate_left = function (n, s) { + return (n << s) | (n >>> (32 - s)); + }, + + cvt_hex = function (val) { + var str = '', + i, + v; + + for (i = 7; i >= 0; i--) { + v = (val >>> (i * 4)) & 0x0f; + str += v.toString(16); + } + return str; + }, + + blockstart, + i, + j, + W = [], + H0 = 0x67452301, + H1 = 0xEFCDAB89, + H2 = 0x98BADCFE, + H3 = 0x10325476, + H4 = 0xC3D2E1F0, + A, + B, + C, + D, + E, + temp, + str_len, + word_array = []; + + str = utf8_encode(str); + str_len = str.length; + + for (i = 0; i < str_len - 3; i += 4) { + j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | + str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3); + word_array.push(j); + } + + switch (str_len & 3) { + case 0: + i = 0x080000000; + break; + case 1: + i = str.charCodeAt(str_len - 1) << 24 | 0x0800000; + break; + case 2: + i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000; + break; + case 3: + i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) << 8 | 0x80; + break; + } + + word_array.push(i); + + while ((word_array.length & 15) !== 14) { + word_array.push(0); + } + + word_array.push(str_len >>> 29); + word_array.push((str_len << 3) & 0x0ffffffff); + + for (blockstart = 0; blockstart < word_array.length; blockstart += 16) { + for (i = 0; i < 16; i++) { + W[i] = word_array[blockstart + i]; + } + + for (i = 16; i <= 79; i++) { + W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); + } + + A = H0; + B = H1; + C = H2; + D = H3; + E = H4; + + for (i = 0; i <= 19; i++) { + temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B, 30); + B = A; + A = temp; + } + + for (i = 20; i <= 39; i++) { + temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B, 30); + B = A; + A = temp; + } + + for (i = 40; i <= 59; i++) { + temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B, 30); + B = A; + A = temp; + } + + for (i = 60; i <= 79; i++) { + temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B, 30); + B = A; + A = temp; + } + + H0 = (H0 + A) & 0x0ffffffff; + H1 = (H1 + B) & 0x0ffffffff; + H2 = (H2 + C) & 0x0ffffffff; + H3 = (H3 + D) & 0x0ffffffff; + H4 = (H4 + E) & 0x0ffffffff; + } + + temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4); + return temp.toLowerCase(); + } + /************************************************************ + * end sha1 + ************************************************************/ + + /* + * Fix-up URL when page rendered from search engine cache or translated page + */ + function urlFixup(hostName, href, referrer) { + if (hostName === 'translate.googleusercontent.com') { // Google + if (referrer === '') { + referrer = href; + } + href = getParameter(href, 'u'); + hostName = getHostName(href); + } else if (hostName === 'cc.bingj.com' || // Bing + hostName === 'webcache.googleusercontent.com' || // Google + hostName.slice(0, 5) === '74.6.') { // Yahoo (via Inktomi 74.6.0.0/16) + href = documentAlias.links[0].href; + hostName = getHostName(href); + } + return [hostName, href, referrer]; + } + + /* + * Fix-up domain + */ + function domainFixup(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; + } + + /* + * Title fixup + */ + function titleFixup(title) { + if (!isString(title)) { + title = title.text || ''; + + var tmp = documentAlias.getElementsByTagName('title'); + if (tmp && isDefined(tmp[0])) { + title = tmp[0].text; + } + } + return title; + } + + /* + * Piwik Tracker class + * + * trackerUrl and trackerSiteId are optional arguments to the constructor + * + * See: Tracker.setTrackerUrl() and Tracker.setSiteId() + */ + function Tracker(trackerUrl, siteId) { + + /************************************************************ + * Private members + ************************************************************/ + + var /*<DEBUG>*/ - /* - * registered test hooks - */ - registeredHooks = {}, + /* + * registered test hooks + */ + registeredHooks = {}, /*</DEBUG>*/ - // Current URL and Referrer URL - locationArray = urlFixup(documentAlias.domain, windowAlias.location.href, getReferrer()), - domainAlias = domainFixup(locationArray[0]), - locationHrefAlias = locationArray[1], - configReferrerUrl = locationArray[2], + // Current URL and Referrer URL + locationArray = urlFixup(documentAlias.domain, windowAlias.location.href, getReferrer()), + domainAlias = domainFixup(locationArray[0]), + locationHrefAlias = locationArray[1], + configReferrerUrl = locationArray[2], + + // Request method (GET or POST) + configRequestMethod = 'GET', + + // Tracker URL + configTrackerUrl = trackerUrl || '', + + // Site ID + configTrackerSiteId = siteId || '', + + // Document URL + configCustomUrl, + + // Document title + configTitle = documentAlias.title, + + // Extensions to be treated as download links + configDownloadExtensions = '7z|aac|ar[cj]|as[fx]|avi|bin|csv|deb|dmg|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|ms[ip]|od[bfgpst]|og[gv]|pdf|phps|png|ppt|qtm?|ra[mr]?|rpm|sea|sit|tar|t?bz2?|tgz|torrent|txt|wav|wm[av]|wpd||xls|xml|z|zip', + + // Hosts or alias(es) to not treat as outlinks + configHostsAlias = [domainAlias], + + // HTML anchor element classes to not track + configIgnoreClasses = [], + + // HTML anchor element classes to treat as downloads + configDownloadClasses = [], + + // HTML anchor element classes to treat at outlinks + configLinkClasses = [], + + // Maximum delay to wait for web bug image to be fetched (in milliseconds) + configTrackerPause = 500, + + // Minimum visit time after initial page view (in milliseconds) + configMinimumVisitTime, + + // Recurring heart beat after initial ping (in milliseconds) + configHeartBeatTimer, + + // Disallow hash tags in URL + configDiscardHashTag, + + // Custom data + configCustomData, + + // Campaign names + configCampaignNameParameters = [ 'pk_campaign', 'piwik_campaign', 'utm_campaign', 'utm_source', 'utm_medium' ], + + // Campaign keywords + configCampaignKeywordParameters = [ 'pk_kwd', 'piwik_kwd', 'utm_term' ], + + // First-party cookie name prefix + configCookieNamePrefix = '_pk_', + + // First-party cookie domain + // User agent defaults to origin hostname + configCookieDomain, + + // First-party cookie path + // Default is user agent defined. + configCookiePath, + + // Cookies are disabled + configCookiesDisabled = false, + + // Do Not Track + configDoNotTrack, + + // Count sites which are pre-rendered + configCountPreRendered, + + // Do we attribute the conversion to the first referrer or the most recent referrer? + configConversionAttributionFirstReferrer, + + // Life of the visitor cookie (in milliseconds) + configVisitorCookieTimeout = 63072000000, // 2 years + + // Life of the session cookie (in milliseconds) + configSessionCookieTimeout = 1800000, // 30 minutes + + // Life of the referral cookie (in milliseconds) + configReferralCookieTimeout = 15768000000, // 6 months + + // Should cookies have the secure flag set + cookieSecure = documentAlias.location.protocol === 'https', + + // Custom Variables read from cookie, scope "visit" + customVariables = false, + + // Custom Variables, scope "page" + customVariablesPage = {}, + + // Custom Variables names and values are each truncated before being sent in the request or recorded in the cookie + customVariableMaximumLength = 200, + + // Ecommerce items + ecommerceItems = {}, + + // Browser features via client-side data collection + browserFeatures = {}, + + // Guard against installing the link tracker more than once per Tracker instance + linkTrackingInstalled = false, + + // Guard against installing the activity tracker more than once per Tracker instance + activityTrackingInstalled = false, + + // Last activity timestamp + lastActivityTime, + + // Internal state of the pseudo click handler + lastButton, + lastTarget, + + // Hash function + hash = sha1, + + // Domain hash value + domainHash, + + // Visitor UUID + visitorUUID; + + + /* + * Set cookie value + */ + function setCookie(cookieName, value, msToExpire, path, domain, secure) { + if (configCookiesDisabled) { + return; + } + var expiryDate; + + // relative time to expire in milliseconds + if (msToExpire) { + expiryDate = new Date(); + expiryDate.setTime(expiryDate.getTime() + msToExpire); + } + + documentAlias.cookie = cookieName + '=' + encodeWrapper(value) + + (msToExpire ? ';expires=' + expiryDate.toGMTString() : '') + + ';path=' + (path || '/') + + (domain ? ';domain=' + domain : '') + + (secure ? ';secure' : ''); + } + + /* + * Get cookie value + */ + function getCookie(cookieName) { + if (configCookiesDisabled) { + return 0; + } + var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)'), + cookieMatch = cookiePattern.exec(documentAlias.cookie); + + return cookieMatch ? decodeWrapper(cookieMatch[2]) : 0; + } + + /* + * Removes hash tag from the URL + * + * URLs are purified before being recorded in the cookie, + * or before being sent as GET parameters + */ + function purify(url) { + var targetPattern; + + if (configDiscardHashTag) { + targetPattern = new RegExp('#.*'); + return url.replace(targetPattern, ''); + } + return url; + } + + /* + * Resolve relative reference + * + * Note: not as described in rfc3986 section 5.2 + */ + function resolveRelativeReference(baseUrl, url) { + var protocol = getProtocolScheme(url), + i; + + if (protocol) { + return url; + } + + if (url.slice(0, 1) === '/') { + return getProtocolScheme(baseUrl) + '://' + getHostName(baseUrl) + url; + } + + baseUrl = purify(baseUrl); + if ((i = baseUrl.indexOf('?')) >= 0) { + baseUrl = baseUrl.slice(0, i); + } + if ((i = baseUrl.lastIndexOf('/')) !== baseUrl.length - 1) { + baseUrl = baseUrl.slice(0, i + 1); + } + + return baseUrl + url; + } + + /* + * Is the host local? (i.e., not an outlink) + */ + function isSiteHostName(hostName) { + var i, + alias, + offset; + + for (i = 0; i < configHostsAlias.length; i++) { + alias = domainFixup(configHostsAlias[i].toLowerCase()); + + if (hostName === alias) { + return true; + } + + if (alias.slice(0, 1) === '.') { + if (hostName === alias.slice(1)) { + return true; + } + + offset = hostName.length - alias.length; + if ((offset > 0) && (hostName.slice(offset) === alias)) { + return true; + } + } + } + return false; + } + + /* + * Send image request to Piwik server using GET. + * The infamous web bug (or beacon) is a transparent, single pixel (1x1) image + */ + function getImage(request) { + var image = new Image(1, 1); - // Request method (GET or POST) - configRequestMethod = 'GET', + image.onload = function () { }; + image.src = configTrackerUrl + (configTrackerUrl.indexOf('?') < 0 ? '?' : '&') + request; + } + + /* + * POST request to Piwik server using XMLHttpRequest. + */ + function sendXmlHttpRequest(request) { + try { + // we use the progid Microsoft.XMLHTTP because + // IE5.5 included MSXML 2.5; the progid MSXML2.XMLHTTP + // is pinned to MSXML2.XMLHTTP.3.0 + var xhr = windowAlias.XMLHttpRequest ? new windowAlias.XMLHttpRequest() : + windowAlias.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : + null; + + xhr.open('POST', configTrackerUrl, true); + + // fallback on error + xhr.onreadystatechange = function () { + if (this.readyState === 4 && this.status !== 200) { + getImage(request); + } + }; + + // see XMLHttpRequest Level 2 spec, section 4.7.2 for invalid headers + // @link http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + + xhr.send(request); + } catch (e) { + // fallback + getImage(request); + } + } + + /* + * Send request + */ + function sendRequest(request, delay) { + var now = new Date(); + + if (!configDoNotTrack) { + if (configRequestMethod === 'POST') { + sendXmlHttpRequest(request); + } else { + getImage(request); + } + + expireDateTime = now.getTime() + delay; + } + } + + /* + * Get cookie name with prefix and domain hash + */ + function getCookieName(baseName) { + // NOTE: If the cookie name is changed, we must also update the PiwikTracker.php which + // will attempt to discover first party cookies. eg. See the PHP Client method getVisitorId() + return configCookieNamePrefix + baseName + '.' + configTrackerSiteId + '.' + domainHash; + } + + /* + * Does browser have cookies enabled (for this site)? + */ + function hasCookies() { + if (configCookiesDisabled) { + return '0'; + } + if (!isDefined(navigatorAlias.cookieEnabled)) { + var testCookieName = getCookieName('testcookie'); + setCookie(testCookieName, '1'); + return getCookie(testCookieName) === '1' ? '1' : '0'; + } + + return navigatorAlias.cookieEnabled ? '1' : '0'; + } + + /* + * Update domain hash + */ + function updateDomainHash() { + domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits + } + + /* + * Inits the custom variables object + */ + function getCustomVariablesFromCookie() { + var cookieName = getCookieName('cvar'), + cookie = getCookie(cookieName); + + if (cookie.length) { + cookie = JSON2.parse(cookie); + if (isObject(cookie)) { + return cookie; + } + } + return {}; + } + + /* + * Lazy loads the custom variables from the cookie, only once during this page view + */ + function loadCustomVariables() { + if (customVariables === false) { + customVariables = getCustomVariablesFromCookie(); + } + } + + /* + * Process all "activity" events. + * For performance, this function must have low overhead. + */ + function activityHandler() { + var now = new Date(); + + lastActivityTime = now.getTime(); + } + + /* + * Sets the Visitor ID cookie: either the first time loadVisitorIdCookie is called + * or when there is a new visit or a new page view + */ + function setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs, lastEcommerceOrderTs) { + setCookie(getCookieName('id'), uuid + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs + '.' + lastEcommerceOrderTs, configVisitorCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); + } - // Tracker URL - configTrackerUrl = trackerUrl || '', + /* + * Load visitor ID cookie + */ + function loadVisitorIdCookie() { + var now = new Date(), + nowTs = Math.round(now.getTime() / 1000), + id = getCookie(getCookieName('id')), + tmpContainer; + + if (id) { + tmpContainer = id.split('.'); + + // returning visitor flag + tmpContainer.unshift('0'); + } else { + // uuid - generate a pseudo-unique ID to fingerprint this user; + // note: this isn't a RFC4122-compliant UUID + if (!visitorUUID) { + visitorUUID = hash( + (navigatorAlias.userAgent || '') + + (navigatorAlias.platform || '') + + JSON2.stringify(browserFeatures) + nowTs + ).slice(0, 16); // 16 hexits = 64 bits + } - // Site ID - configTrackerSiteId = siteId || '', + tmpContainer = [ + // new visitor + '1', - // Document URL - configCustomUrl, + // uuid + visitorUUID, - // Document title - configTitle = documentAlias.title, + // creation timestamp - seconds since Unix epoch + nowTs, - // Extensions to be treated as download links - configDownloadExtensions = '7z|aac|ar[cj]|as[fx]|avi|bin|csv|deb|dmg|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|ms[ip]|od[bfgpst]|og[gv]|pdf|phps|png|ppt|qtm?|ra[mr]?|rpm|sea|sit|tar|t?bz2?|tgz|torrent|txt|wav|wm[av]|wpd||xls|xml|z|zip', + // visitCount - 0 = no previous visit + 0, - // Hosts or alias(es) to not treat as outlinks - configHostsAlias = [domainAlias], + // current visit timestamp + nowTs, - // HTML anchor element classes to not track - configIgnoreClasses = [], + // last visit timestamp - blank = no previous visit + '', - // HTML anchor element classes to treat as downloads - configDownloadClasses = [], + // last ecommerce order timestamp + '' + ]; + } + return tmpContainer; + } - // HTML anchor element classes to treat at outlinks - configLinkClasses = [], + /* + * Loads the referrer attribution information + * + * @returns array + * 0: campaign name + * 1: campaign keyword + * 2: timestamp + * 3: raw URL + */ + function loadReferrerAttributionCookie() { + // NOTE: if the format of the cookie changes, + // we must also update JS tests, PHP tracker, Integration tests, + // and notify other tracking clients (eg. Java) of the changes + var cookie = getCookie(getCookieName('ref')); + + if (cookie.length) { + try { + cookie = JSON2.parse(cookie); + if (isObject(cookie)) { + return cookie; + } + } catch (err) { + // Pre 1.3, this cookie was not JSON encoded + } + } + return [ + '', + '', + 0, + '' + ]; + } - // Maximum delay to wait for web bug image to be fetched (in milliseconds) - configTrackerPause = 500, + /* + * Returns the URL to call piwik.php, + * with the standard parameters (plugins, resolution, url, referrer, etc.). + * Sends the pageview and browser settings with every request in case of race conditions. + */ + function getRequest(request, customData, pluginMethod, currentEcommerceOrderTs) { + var i, + now = new Date(), + nowTs = Math.round(now.getTime() / 1000), + newVisitor, + uuid, + visitCount, + createTs, + currentVisitTs, + lastVisitTs, + lastEcommerceOrderTs, + referralTs, + referralUrl, + referralUrlMaxLength = 1024, + currentReferrerHostName, + originalReferrerHostName, + customVariablesCopy = customVariables, + idname = getCookieName('id'), + sesname = getCookieName('ses'), + refname = getCookieName('ref'), + cvarname = getCookieName('cvar'), + id = loadVisitorIdCookie(), + ses = getCookie(sesname), + attributionCookie = loadReferrerAttributionCookie(), + currentUrl = configCustomUrl || locationHrefAlias, + campaignNameDetected, + campaignKeywordDetected; + + if (configCookiesDisabled) { + // Temporarily allow cookies just to delete the existing ones + configCookiesDisabled = false; + setCookie(idname, '', -86400, configCookiePath, configCookieDomain); + setCookie(sesname, '', -86400, configCookiePath, configCookieDomain); + setCookie(cvarname, '', -86400, configCookiePath, configCookieDomain); + setCookie(refname, '', -86400, configCookiePath, configCookieDomain); + configCookiesDisabled = true; + } - // Minimum visit time after initial page view (in milliseconds) - configMinimumVisitTime, + if (configDoNotTrack) { + return ''; + } - // Recurring heart beat after initial ping (in milliseconds) - configHeartBeatTimer, + newVisitor = id[0]; + uuid = id[1]; + createTs = id[2]; + visitCount = id[3]; + currentVisitTs = id[4]; + lastVisitTs = id[5]; + // case migrating from pre-1.5 cookies + if (!isDefined(id[6])) { + id[6] = ""; + } + lastEcommerceOrderTs = id[6]; - // Disallow hash tags in URL - configDiscardHashTag, + if (!isDefined(currentEcommerceOrderTs)) { + currentEcommerceOrderTs = ""; + } - // Custom data - configCustomData, + campaignNameDetected = attributionCookie[0]; + campaignKeywordDetected = attributionCookie[1]; + referralTs = attributionCookie[2]; + referralUrl = attributionCookie[3]; + + if (!ses) { + // new session (aka new visit) + visitCount++; + + lastVisitTs = currentVisitTs; + + // Detect the campaign information from the current URL + // Only if campaign wasn't previously set + // Or if it was set but we must attribute to the most recent one + // Note: we are working on the currentUrl before purify() since we can parse the campaign parameters in the hash tag + if (!configConversionAttributionFirstReferrer + || !campaignNameDetected.length) { + for (i in configCampaignNameParameters) { + if (Object.prototype.hasOwnProperty.call(configCampaignNameParameters, i)) { + campaignNameDetected = getParameter(currentUrl, configCampaignNameParameters[i]); + if (campaignNameDetected.length) { + break; + } + } + } + for (i in configCampaignKeywordParameters) { + if (Object.prototype.hasOwnProperty.call(configCampaignKeywordParameters, i)) { + campaignKeywordDetected = getParameter(currentUrl, configCampaignKeywordParameters[i]); + if (campaignKeywordDetected.length) { + break; + } + } + } + } - // Campaign names - configCampaignNameParameters = [ 'pk_campaign', 'piwik_campaign', 'utm_campaign', 'utm_source', 'utm_medium' ], + // Store the referrer URL and time in the cookie; + // referral URL depends on the first or last referrer attribution + currentReferrerHostName = getHostName(configReferrerUrl); + originalReferrerHostName = referralUrl.length ? getHostName(referralUrl) : ''; + if (currentReferrerHostName.length && // there is a referrer + !isSiteHostName(currentReferrerHostName) && // domain is not the current domain + (!configConversionAttributionFirstReferrer || // attribute to last known referrer + !originalReferrerHostName.length || // previously empty + isSiteHostName(originalReferrerHostName))) { // previously set but in current domain + referralUrl = configReferrerUrl; + } - // Campaign keywords - configCampaignKeywordParameters = [ 'pk_kwd', 'piwik_kwd', 'utm_term' ], + // Set the referral cookie if we have either a Referrer URL, or detected a Campaign (or both) + if (referralUrl.length + || campaignNameDetected.length) { + referralTs = nowTs; + attributionCookie = [ + campaignNameDetected, + campaignKeywordDetected, + referralTs, + purify(referralUrl.slice(0, referralUrlMaxLength)) + ]; + + setCookie(refname, JSON2.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); + } + } + // build out the rest of the request + request += '&idsite=' + configTrackerSiteId + + '&rec=1' + + '&r=' + String(Math.random()).slice(2, 8) + // keep the string to a minimum + '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() + + '&url=' + encodeWrapper(purify(currentUrl)) + + (configReferrerUrl.length ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') + + '&_id=' + uuid + '&_idts=' + createTs + '&_idvc=' + visitCount + + '&_idn=' + newVisitor + // currently unused + (campaignNameDetected.length ? '&_rcn=' + encodeWrapper(campaignNameDetected) : '') + + (campaignKeywordDetected.length ? '&_rck=' + encodeWrapper(campaignKeywordDetected) : '') + + '&_refts=' + referralTs + + '&_viewts=' + lastVisitTs + + (String(lastEcommerceOrderTs).length ? '&_ects=' + lastEcommerceOrderTs : '') + + (String(referralUrl).length ? '&_ref=' + encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))) : ''); + + // Custom Variables, scope "page" + var customVariablesPageStringified = JSON2.stringify(customVariablesPage); + if (customVariablesPageStringified.length > 2) { + request += '&cvar=' + encodeWrapper(customVariablesPageStringified); + } - // First-party cookie name prefix - configCookieNamePrefix = '_pk_', + // browser features + for (i in browserFeatures) { + if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { + request += '&' + i + '=' + browserFeatures[i]; + } + } - // First-party cookie domain - // User agent defaults to origin hostname - configCookieDomain, + // custom data + if (customData) { + request += '&data=' + encodeWrapper(JSON2.stringify(customData)); + } else if (configCustomData) { + request += '&data=' + encodeWrapper(JSON2.stringify(configCustomData)); + } - // First-party cookie path - // Default is user agent defined. - configCookiePath, + // Custom Variables, scope "visit" + if (customVariables) { + var customVariablesStringified = JSON2.stringify(customVariables); + // Don't sent empty custom variables {} + if (customVariablesStringified.length > 2) { + request += '&_cvar=' + encodeWrapper(customVariablesStringified); + } - // Cookies are disabled - configCookiesDisabled = false, + // Don't save deleted custom variables in the cookie + for (i in customVariablesCopy) { + if (Object.prototype.hasOwnProperty.call(customVariablesCopy, i)) { + if (customVariables[i][0] === '' || customVariables[i][1] === '') { + delete customVariables[i]; + } + } + } - // Do Not Track - configDoNotTrack, + setCookie(cvarname, JSON2.stringify(customVariables), configSessionCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); + } - // Count sites which are pre-rendered - configCountPreRendered, + // update cookies + setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs, isDefined(currentEcommerceOrderTs) && String(currentEcommerceOrderTs).length ? currentEcommerceOrderTs : lastEcommerceOrderTs); + setCookie(sesname, '*', configSessionCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); - // Do we attribute the conversion to the first referrer or the most recent referrer? - configConversionAttributionFirstReferrer, + // tracker plugin hook + request += executePluginMethod(pluginMethod); - // Life of the visitor cookie (in milliseconds) - configVisitorCookieTimeout = 63072000000, // 2 years + return request; + } - // Life of the session cookie (in milliseconds) - configSessionCookieTimeout = 1800000, // 30 minutes + function logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount) { + var request = 'idgoal=0', + lastEcommerceOrderTs, + now = new Date(), + items = [], + sku; + + if (String(orderId).length) { + request += '&ec_id=' + encodeWrapper(orderId); + // Record date of order in the visitor cookie + lastEcommerceOrderTs = Math.round(now.getTime() / 1000); + } - // Life of the referral cookie (in milliseconds) - configReferralCookieTimeout = 15768000000, // 6 months + request += '&revenue=' + grandTotal; + if (String(subTotal).length) { + request += '&ec_st=' + subTotal; + } + if (String(tax).length) { + request += '&ec_tx=' + tax; + } + if (String(shipping).length) { + request += '&ec_sh=' + shipping; + } + if (String(discount).length) { + request += '&ec_dt=' + discount; + } + if (ecommerceItems) { + // Removing the SKU index in the array before JSON encoding + for (sku in ecommerceItems) { + if (Object.prototype.hasOwnProperty.call(ecommerceItems, sku)) { + // Ensure name and category default to healthy value + if (!isDefined(ecommerceItems[sku][1])) { + ecommerceItems[sku][1] = ""; + } + if (!isDefined(ecommerceItems[sku][2])) { + ecommerceItems[sku][2] = ""; + } + // Set price to zero + if (!isDefined(ecommerceItems[sku][3]) + || String(ecommerceItems[sku][3]).length === 0) { + ecommerceItems[sku][3] = 0; + } + // Set quantity to 1 + if (!isDefined(ecommerceItems[sku][4]) + || String(ecommerceItems[sku][4]).length === 0) { + ecommerceItems[sku][4] = 1; + } + items.push(ecommerceItems[sku]); + } + } + request += '&ec_items=' + encodeWrapper(JSON2.stringify(items)); + } + request = getRequest(request, configCustomData, 'ecommerce', lastEcommerceOrderTs); + sendRequest(request, configTrackerPause); + } - // Should cookies have the secure flag set - cookieSecure = documentAlias.location.protocol === 'https', + function logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount) { + if (String(orderId).length + && isDefined(grandTotal)) { + logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount); + } + } - // Custom Variables read from cookie, scope "visit" - customVariables = false, + function logEcommerceCartUpdate(grandTotal) { + if (isDefined(grandTotal)) { + logEcommerce("", grandTotal, "", "", "", ""); + } + } - // Custom Variables, scope "page" - customVariablesPage = {}, + /* + * Log the page view / visit + */ + function logPageView(customTitle, customData) { + var now = new Date(), + request = getRequest('action_name=' + encodeWrapper(titleFixup(customTitle || configTitle)), customData, 'log'); + + sendRequest(request, configTrackerPause); + + // send ping + if (configMinimumVisitTime && configHeartBeatTimer && !activityTrackingInstalled) { + activityTrackingInstalled = true; + + // add event handlers; cross-browser compatibility here varies significantly + // @see http://quirksmode.org/dom/events + addEventListener(documentAlias, 'click', activityHandler); + addEventListener(documentAlias, 'mouseup', activityHandler); + addEventListener(documentAlias, 'mousedown', activityHandler); + addEventListener(documentAlias, 'mousemove', activityHandler); + addEventListener(documentAlias, 'mousewheel', activityHandler); + addEventListener(windowAlias, 'DOMMouseScroll', activityHandler); + addEventListener(windowAlias, 'scroll', activityHandler); + addEventListener(documentAlias, 'keypress', activityHandler); + addEventListener(documentAlias, 'keydown', activityHandler); + addEventListener(documentAlias, 'keyup', activityHandler); + addEventListener(windowAlias, 'resize', activityHandler); + addEventListener(windowAlias, 'focus', activityHandler); + addEventListener(windowAlias, 'blur', activityHandler); + + // periodic check for activity + lastActivityTime = now.getTime(); + setTimeout(function heartBeat() { + var now = new Date(), + request; + + // there was activity during the heart beat period; + // on average, this is going to overstate the visitDuration by configHeartBeatTimer/2 + if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) { + // send ping if minimum visit time has elapsed + if (configMinimumVisitTime < now.getTime()) { + request = getRequest('ping=1', customData, 'ping'); + + sendRequest(request, configTrackerPause); + } - // Custom Variables names and values are each truncated before being sent in the request or recorded in the cookie - customVariableMaximumLength = 200, + // resume heart beat + setTimeout(heartBeat, configHeartBeatTimer); + } + // else heart beat cancelled due to inactivity + }, configHeartBeatTimer); + } + } - // Ecommerce items - ecommerceItems = {}, + /* + * Log the goal with the server + */ + function logGoal(idGoal, customRevenue, customData) { + var request = getRequest('idgoal=' + idGoal + (customRevenue ? '&revenue=' + customRevenue : ''), customData, 'goal'); - // Browser features via client-side data collection - browserFeatures = {}, + sendRequest(request, configTrackerPause); + } - // Guard against installing the link tracker more than once per Tracker instance - linkTrackingInstalled = false, + /* + * Log the link or click with the server + */ + function logLink(url, linkType, customData) { + var request = getRequest(linkType + '=' + encodeWrapper(purify(url)), customData, 'link'); - // Guard against installing the activity tracker more than once per Tracker instance - activityTrackingInstalled = false, + sendRequest(request, configTrackerPause); + } - // Last activity timestamp - lastActivityTime, + /* + * Browser prefix + */ + function prefixPropertyName(prefix, propertyName) { + if (prefix !== '') { + return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1); + } - // Internal state of the pseudo click handler - lastButton, - lastTarget, + return propertyName; + } - // Hash function - hash = sha1, + /* + * Check for pre-rendered web pages, and log the page view/link/goal + * according to the configuration and/or visibility + * + * @see http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/PageVisibility/Overview.html + */ + function trackCallback(callback) { + var isPreRendered, + i, + // Chrome 13, IE10, FF10 + prefixes = ['', 'webkit', 'ms', 'moz'], + prefix; + + if (!configCountPreRendered) { + for (i = 0; i < prefixes.length; i++) { + prefix = prefixes[i]; + + // does this browser support the page visibility API? + if (Object.prototype.hasOwnProperty.call(documentAlias, prefixPropertyName(prefix, 'hidden'))) { + // if pre-rendered, then defer callback until page visibility changes + if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') { + isPreRendered = true; + } + break; + } + } + } - // Domain hash value - domainHash, + if (isPreRendered) { + // note: the event name doesn't follow the same naming convention as vendor properties + addEventListener(documentAlias, prefix + 'visibilitychange', function ready() { + documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false); + callback(); + }); + return; + } - // Visitor UUID - visitorUUID; + // configCountPreRendered === true || isPreRendered === false + callback(); + } + /* + * Construct regular expression of classes + */ + function getClassesRegExp(configClasses, defaultClass) { + var i, + classesRegExp = '(^| )(piwik[_-]' + defaultClass; - /* - * Set cookie value - */ - function setCookie(cookieName, value, msToExpire, path, domain, secure) { - if (configCookiesDisabled) { - return; - } - var expiryDate; - - // relative time to expire in milliseconds - if (msToExpire) { - expiryDate = new Date(); - expiryDate.setTime(expiryDate.getTime() + msToExpire); - } - - documentAlias.cookie = cookieName + '=' + encodeWrapper(value) + - (msToExpire ? ';expires=' + expiryDate.toGMTString() : '') + - ';path=' + (path || '/') + - (domain ? ';domain=' + domain : '') + - (secure ? ';secure' : ''); - } - - /* - * Get cookie value - */ - function getCookie(cookieName) { - if (configCookiesDisabled) { - return 0; - } - var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)'), - cookieMatch = cookiePattern.exec(documentAlias.cookie); - - return cookieMatch ? decodeWrapper(cookieMatch[2]) : 0; - } - - /* - * Removes hash tag from the URL - * - * URLs are purified before being recorded in the cookie, - * or before being sent as GET parameters - */ - function purify(url) { - var targetPattern; - - if (configDiscardHashTag) { - targetPattern = new RegExp('#.*'); - return url.replace(targetPattern, ''); - } - return url; - } - - /* - * Resolve relative reference - * - * Note: not as described in rfc3986 section 5.2 - */ - function resolveRelativeReference(baseUrl, url) { - var protocol = getProtocolScheme(url), - i; - - if (protocol) { - return url; - } - - if (url.slice(0, 1) === '/') { - return getProtocolScheme(baseUrl) + '://' + getHostName(baseUrl) + url; - } - - baseUrl = purify(baseUrl); - if ((i = baseUrl.indexOf('?')) >= 0) { - baseUrl = baseUrl.slice(0, i); - } - if ((i = baseUrl.lastIndexOf('/')) !== baseUrl.length - 1) { - baseUrl = baseUrl.slice(0, i + 1); - } - - return baseUrl + url; - } - - /* - * Is the host local? (i.e., not an outlink) - */ - function isSiteHostName(hostName) { - var i, - alias, - offset; - - for (i = 0; i < configHostsAlias.length; i++) { - alias = domainFixup(configHostsAlias[i].toLowerCase()); - - if (hostName === alias) { - return true; - } - - if (alias.slice(0, 1) === '.') { - if (hostName === alias.slice(1)) { - return true; - } - - offset = hostName.length - alias.length; - if ((offset > 0) && (hostName.slice(offset) === alias)) { - return true; - } - } - } - return false; - } - - /* - * Send image request to Piwik server using GET. - * The infamous web bug (or beacon) is a transparent, single pixel (1x1) image - */ - function getImage(request) { - var image = new Image(1, 1); - - image.onload = function () { }; - image.src = configTrackerUrl + (configTrackerUrl.indexOf('?') < 0 ? '?' : '&') + request; - } - - /* - * POST request to Piwik server using XMLHttpRequest. - */ - function sendXmlHttpRequest(request) { - try { - // we use the progid Microsoft.XMLHTTP because - // IE5.5 included MSXML 2.5; the progid MSXML2.XMLHTTP - // is pinned to MSXML2.XMLHTTP.3.0 - var xhr = windowAlias.XMLHttpRequest ? new windowAlias.XMLHttpRequest() : - windowAlias.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : - null; - - xhr.open('POST', configTrackerUrl, true); - - // fallback on error - xhr.onreadystatechange = function () { - if (this.readyState === 4 && this.status !== 200) { - getImage(request); - } - }; - - // see XMLHttpRequest Level 2 spec, section 4.7.2 for invalid headers - // @link http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); - - xhr.send(request); - } catch (e) { - // fallback - getImage(request); - } - } - - /* - * Send request - */ - function sendRequest(request, delay) { - var now = new Date(); - - if (!configDoNotTrack) { - if (configRequestMethod === 'POST') { - sendXmlHttpRequest(request); - } else { - getImage(request); - } - - expireDateTime = now.getTime() + delay; - } - } - - /* - * Get cookie name with prefix and domain hash - */ - function getCookieName(baseName) { - // NOTE: If the cookie name is changed, we must also update the PiwikTracker.php which - // will attempt to discover first party cookies. eg. See the PHP Client method getVisitorId() - return configCookieNamePrefix + baseName + '.' + configTrackerSiteId + '.' + domainHash; - } - - /* - * Does browser have cookies enabled (for this site)? - */ - function hasCookies() { - if (configCookiesDisabled) { - return '0'; - } - if (!isDefined(navigatorAlias.cookieEnabled)) { - var testCookieName = getCookieName('testcookie'); - setCookie(testCookieName, '1'); - return getCookie(testCookieName) === '1' ? '1' : '0'; - } - - return navigatorAlias.cookieEnabled ? '1' : '0'; - } - - /* - * Update domain hash - */ - function updateDomainHash() { - domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits - } - - /* - * Inits the custom variables object - */ - function getCustomVariablesFromCookie() { - var cookieName = getCookieName('cvar'), - cookie = getCookie(cookieName); - - if (cookie.length) { - cookie = JSON2.parse(cookie); - if (isObject(cookie)) { - return cookie; - } - } - return {}; - } - - /* - * Lazy loads the custom variables from the cookie, only once during this page view - */ - function loadCustomVariables() { - if (customVariables === false) { - customVariables = getCustomVariablesFromCookie(); - } - } - - /* - * Process all "activity" events. - * For performance, this function must have low overhead. - */ - function activityHandler() { - var now = new Date(); - - lastActivityTime = now.getTime(); - } - - /* - * Sets the Visitor ID cookie: either the first time loadVisitorIdCookie is called - * or when there is a new visit or a new page view - */ - function setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs, lastEcommerceOrderTs) { - setCookie(getCookieName('id'), uuid + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs + '.' + lastEcommerceOrderTs, configVisitorCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); - } - - /* - * Load visitor ID cookie - */ - function loadVisitorIdCookie() { - var now = new Date(), - nowTs = Math.round(now.getTime() / 1000), - id = getCookie(getCookieName('id')), - tmpContainer; - - if (id) { - tmpContainer = id.split('.'); - - // returning visitor flag - tmpContainer.unshift('0'); - } else { - // uuid - generate a pseudo-unique ID to fingerprint this user; - // note: this isn't a RFC4122-compliant UUID - if (!visitorUUID) { - visitorUUID = hash( - (navigatorAlias.userAgent || '') + - (navigatorAlias.platform || '') + - JSON2.stringify(browserFeatures) + nowTs - ).slice(0, 16); // 16 hexits = 64 bits - } - - tmpContainer = [ - // new visitor - '1', - - // uuid - visitorUUID, - - // creation timestamp - seconds since Unix epoch - nowTs, - - // visitCount - 0 = no previous visit - 0, - - // current visit timestamp - nowTs, - - // last visit timestamp - blank = no previous visit - '', - - // last ecommerce order timestamp - '' - ]; - } - return tmpContainer; - } - - /* - * Loads the referrer attribution information - * - * @returns array - * 0: campaign name - * 1: campaign keyword - * 2: timestamp - * 3: raw URL - */ - function loadReferrerAttributionCookie() { - // NOTE: if the format of the cookie changes, - // we must also update JS tests, PHP tracker, Integration tests, - // and notify other tracking clients (eg. Java) of the changes - var cookie = getCookie(getCookieName('ref')); - - if (cookie.length) { - try { - cookie = JSON2.parse(cookie); - if (isObject(cookie)) { - return cookie; - } - } catch (err) { - // Pre 1.3, this cookie was not JSON encoded - } - } - return [ - '', - '', - 0, - '' - ]; - } - - /* - * Returns the URL to call piwik.php, - * with the standard parameters (plugins, resolution, url, referrer, etc.). - * Sends the pageview and browser settings with every request in case of race conditions. - */ - function getRequest(request, customData, pluginMethod, currentEcommerceOrderTs) { - var i, - now = new Date(), - nowTs = Math.round(now.getTime() / 1000), - newVisitor, - uuid, - visitCount, - createTs, - currentVisitTs, - lastVisitTs, - lastEcommerceOrderTs, - referralTs, - referralUrl, - referralUrlMaxLength = 1024, - currentReferrerHostName, - originalReferrerHostName, - customVariablesCopy = customVariables, - idname = getCookieName('id'), - sesname = getCookieName('ses'), - refname = getCookieName('ref'), - cvarname = getCookieName('cvar'), - id = loadVisitorIdCookie(), - ses = getCookie(sesname), - attributionCookie = loadReferrerAttributionCookie(), - currentUrl = configCustomUrl || locationHrefAlias, - campaignNameDetected, - campaignKeywordDetected; - - if (configCookiesDisabled) { - // Temporarily allow cookies just to delete the existing ones - configCookiesDisabled = false; - setCookie(idname, '', -86400, configCookiePath, configCookieDomain); - setCookie(sesname, '', -86400, configCookiePath, configCookieDomain); - setCookie(cvarname, '', -86400, configCookiePath, configCookieDomain); - setCookie(refname, '', -86400, configCookiePath, configCookieDomain); - configCookiesDisabled = true; - } - - if (configDoNotTrack) { - return ''; - } - - newVisitor = id[0]; - uuid = id[1]; - createTs = id[2]; - visitCount = id[3]; - currentVisitTs = id[4]; - lastVisitTs = id[5]; - // case migrating from pre-1.5 cookies - if (!isDefined(id[6])) { - id[6] = ""; - } - lastEcommerceOrderTs = id[6]; - - if (!isDefined(currentEcommerceOrderTs)) { - currentEcommerceOrderTs = ""; - } - - campaignNameDetected = attributionCookie[0]; - campaignKeywordDetected = attributionCookie[1]; - referralTs = attributionCookie[2]; - referralUrl = attributionCookie[3]; - - if (!ses) { - // new session (aka new visit) - visitCount++; - - lastVisitTs = currentVisitTs; - - // Detect the campaign information from the current URL - // Only if campaign wasn't previously set - // Or if it was set but we must attribute to the most recent one - // Note: we are working on the currentUrl before purify() since we can parse the campaign parameters in the hash tag - if (!configConversionAttributionFirstReferrer - || !campaignNameDetected.length) { - for (i in configCampaignNameParameters) { - if (Object.prototype.hasOwnProperty.call(configCampaignNameParameters, i)) { - campaignNameDetected = getParameter(currentUrl, configCampaignNameParameters[i]); - if (campaignNameDetected.length) { - break; - } - } - } - for (i in configCampaignKeywordParameters) { - if (Object.prototype.hasOwnProperty.call(configCampaignKeywordParameters, i)) { - campaignKeywordDetected = getParameter(currentUrl, configCampaignKeywordParameters[i]); - if (campaignKeywordDetected.length) { - break; - } - } - } - } - - // Store the referrer URL and time in the cookie; - // referral URL depends on the first or last referrer attribution - currentReferrerHostName = getHostName(configReferrerUrl); - originalReferrerHostName = referralUrl.length ? getHostName(referralUrl) : ''; - if (currentReferrerHostName.length && // there is a referrer - !isSiteHostName(currentReferrerHostName) && // domain is not the current domain - (!configConversionAttributionFirstReferrer || // attribute to last known referrer - !originalReferrerHostName.length || // previously empty - isSiteHostName(originalReferrerHostName))) { // previously set but in current domain - referralUrl = configReferrerUrl; - } - - // Set the referral cookie if we have either a Referrer URL, or detected a Campaign (or both) - if (referralUrl.length - || campaignNameDetected.length) { - referralTs = nowTs; - attributionCookie = [ - campaignNameDetected, - campaignKeywordDetected, - referralTs, - purify(referralUrl.slice(0, referralUrlMaxLength)) - ]; - - setCookie(refname, JSON2.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); - } - } - // build out the rest of the request - request += '&idsite=' + configTrackerSiteId + - '&rec=1' + - '&r=' + String(Math.random()).slice(2, 8) + // keep the string to a minimum - '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() + - '&url=' + encodeWrapper(purify(currentUrl)) + - (configReferrerUrl.length ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') + - '&_id=' + uuid + '&_idts=' + createTs + '&_idvc=' + visitCount + - '&_idn=' + newVisitor + // currently unused - (campaignNameDetected.length ? '&_rcn=' + encodeWrapper(campaignNameDetected) : '') + - (campaignKeywordDetected.length ? '&_rck=' + encodeWrapper(campaignKeywordDetected) : '') + - '&_refts=' + referralTs + - '&_viewts=' + lastVisitTs + - (String(lastEcommerceOrderTs).length ? '&_ects=' + lastEcommerceOrderTs : '') + - (String(referralUrl).length ? '&_ref=' + encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))) : ''); - - // Custom Variables, scope "page" - var customVariablesPageStringified = JSON2.stringify(customVariablesPage); - if (customVariablesPageStringified.length > 2) { - request += '&cvar=' + encodeWrapper(customVariablesPageStringified); - } - - // browser features - for (i in browserFeatures) { - if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { - request += '&' + i + '=' + browserFeatures[i]; - } - } - - // custom data - if (customData) { - request += '&data=' + encodeWrapper(JSON2.stringify(customData)); - } else if (configCustomData) { - request += '&data=' + encodeWrapper(JSON2.stringify(configCustomData)); - } - - // Custom Variables, scope "visit" - if (customVariables) { - var customVariablesStringified = JSON2.stringify(customVariables); - // Don't sent empty custom variables {} - if (customVariablesStringified.length > 2) { - request += '&_cvar=' + encodeWrapper(customVariablesStringified); - } - - // Don't save deleted custom variables in the cookie - for (i in customVariablesCopy) { - if (Object.prototype.hasOwnProperty.call(customVariablesCopy, i)) { - if (customVariables[i][0] === '' || customVariables[i][1] === '') { - delete customVariables[i]; - } - } - } - - setCookie(cvarname, JSON2.stringify(customVariables), configSessionCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); - } - - // update cookies - setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs, isDefined(currentEcommerceOrderTs) && String(currentEcommerceOrderTs).length ? currentEcommerceOrderTs : lastEcommerceOrderTs); - setCookie(sesname, '*', configSessionCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); - - // tracker plugin hook - request += executePluginMethod(pluginMethod); - - return request; - } - - function logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount) { - var request = 'idgoal=0', - lastEcommerceOrderTs, - now = new Date(), - items = [], - sku; - - if (String(orderId).length) { - request += '&ec_id=' + encodeWrapper(orderId); - // Record date of order in the visitor cookie - lastEcommerceOrderTs = Math.round(now.getTime() / 1000); - } - - request += '&revenue=' + grandTotal; - if (String(subTotal).length) { - request += '&ec_st=' + subTotal; - } - if (String(tax).length) { - request += '&ec_tx=' + tax; - } - if (String(shipping).length) { - request += '&ec_sh=' + shipping; - } - if (String(discount).length) { - request += '&ec_dt=' + discount; - } - if (ecommerceItems) { - // Removing the SKU index in the array before JSON encoding - for (sku in ecommerceItems) { - if (Object.prototype.hasOwnProperty.call(ecommerceItems, sku)) { - // Ensure name and category default to healthy value - if (!isDefined(ecommerceItems[sku][1])) { - ecommerceItems[sku][1] = ""; - } - if (!isDefined(ecommerceItems[sku][2])) { - ecommerceItems[sku][2] = ""; - } - // Set price to zero - if (!isDefined(ecommerceItems[sku][3]) - || String(ecommerceItems[sku][3]).length === 0) { - ecommerceItems[sku][3] = 0; - } - // Set quantity to 1 - if (!isDefined(ecommerceItems[sku][4]) - || String(ecommerceItems[sku][4]).length === 0) { - ecommerceItems[sku][4] = 1; - } - items.push(ecommerceItems[sku]); - } - } - request += '&ec_items=' + encodeWrapper(JSON2.stringify(items)); - } - request = getRequest(request, configCustomData, 'ecommerce', lastEcommerceOrderTs); - sendRequest(request, configTrackerPause); - } - - function logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount) { - if (String(orderId).length - && isDefined(grandTotal)) { - logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount); - } - } - - function logEcommerceCartUpdate(grandTotal) { - if (isDefined(grandTotal)) { - logEcommerce("", grandTotal, "", "", "", ""); - } - } - - /* - * Log the page view / visit - */ - function logPageView(customTitle, customData) { - var now = new Date(), - request = getRequest('action_name=' + encodeWrapper(titleFixup(customTitle || configTitle)), customData, 'log'); - - sendRequest(request, configTrackerPause); - - // send ping - if (configMinimumVisitTime && configHeartBeatTimer && !activityTrackingInstalled) { - activityTrackingInstalled = true; - - // add event handlers; cross-browser compatibility here varies significantly - // @see http://quirksmode.org/dom/events - addEventListener(documentAlias, 'click', activityHandler); - addEventListener(documentAlias, 'mouseup', activityHandler); - addEventListener(documentAlias, 'mousedown', activityHandler); - addEventListener(documentAlias, 'mousemove', activityHandler); - addEventListener(documentAlias, 'mousewheel', activityHandler); - addEventListener(windowAlias, 'DOMMouseScroll', activityHandler); - addEventListener(windowAlias, 'scroll', activityHandler); - addEventListener(documentAlias, 'keypress', activityHandler); - addEventListener(documentAlias, 'keydown', activityHandler); - addEventListener(documentAlias, 'keyup', activityHandler); - addEventListener(windowAlias, 'resize', activityHandler); - addEventListener(windowAlias, 'focus', activityHandler); - addEventListener(windowAlias, 'blur', activityHandler); - - // periodic check for activity - lastActivityTime = now.getTime(); - setTimeout(function heartBeat() { - var now = new Date(), - request; - - // there was activity during the heart beat period; - // on average, this is going to overstate the visitDuration by configHeartBeatTimer/2 - if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) { - // send ping if minimum visit time has elapsed - if (configMinimumVisitTime < now.getTime()) { - request = getRequest('ping=1', customData, 'ping'); - - sendRequest(request, configTrackerPause); - } - - // resume heart beat - setTimeout(heartBeat, configHeartBeatTimer); - } - // else heart beat cancelled due to inactivity - }, configHeartBeatTimer); - } - } - - /* - * Log the goal with the server - */ - function logGoal(idGoal, customRevenue, customData) { - var request = getRequest('idgoal=' + idGoal + (customRevenue ? '&revenue=' + customRevenue : ''), customData, 'goal'); - - sendRequest(request, configTrackerPause); - } - - /* - * Log the link or click with the server - */ - function logLink(url, linkType, customData) { - var request = getRequest(linkType + '=' + encodeWrapper(purify(url)), customData, 'link'); - - sendRequest(request, configTrackerPause); - } - - /* - * Browser prefix - */ - function prefixPropertyName(prefix, propertyName) { - if (prefix !== '') { - return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1); - } - - return propertyName; - } - - /* - * Check for pre-rendered web pages, and log the page view/link/goal - * according to the configuration and/or visibility - * - * @see http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/PageVisibility/Overview.html - */ - function trackCallback(callback) { - var isPreRendered, - i, - // Chrome 13, IE10, FF10 - prefixes = ['', 'webkit', 'ms', 'moz'], - prefix; - - if (!configCountPreRendered) { - for (i = 0; i < prefixes.length; i++) { - prefix = prefixes[i]; - - // does this browser support the page visibility API? - if (Object.prototype.hasOwnProperty.call(documentAlias, prefixPropertyName(prefix, 'hidden'))) { - // if pre-rendered, then defer callback until page visibility changes - if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') { - isPreRendered = true; - } - break; - } - } - } - - if (isPreRendered) { - // note: the event name doesn't follow the same naming convention as vendor properties - addEventListener(documentAlias, prefix + 'visibilitychange', function ready() { - documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false); - callback(); - }); - return; - } - - // configCountPreRendered === true || isPreRendered === false - callback(); - } - - /* - * Construct regular expression of classes - */ - function getClassesRegExp(configClasses, defaultClass) { - var i, - classesRegExp = '(^| )(piwik[_-]' + defaultClass; - - if (configClasses) { - for (i = 0; i < configClasses.length; i++) { - classesRegExp += '|' + configClasses[i]; - } - } - classesRegExp += ')( |$)'; - - return new RegExp(classesRegExp); - } - - /* - * Link or Download? - */ - function getLinkType(className, href, isInLink) { - // outlinks - if (!isInLink) { - return 'link'; - } - - // does class indicate whether it is an (explicit/forced) outlink or a download? - var downloadPattern = getClassesRegExp(configDownloadClasses, 'download'), - linkPattern = getClassesRegExp(configLinkClasses, 'link'), - - // does file extension indicate that it is a download? - downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions + ')([?&#]|$)', 'i'); - - // optimization of the if..elseif..else construct below - return linkPattern.test(className) ? 'link' : (downloadPattern.test(className) || downloadExtensionsPattern.test(href) ? 'download' : 0); + if (configClasses) { + for (i = 0; i < configClasses.length; i++) { + classesRegExp += '|' + configClasses[i]; + } + } + classesRegExp += ')( |$)'; + + return new RegExp(classesRegExp); + } + + /* + * Link or Download? + */ + function getLinkType(className, href, isInLink) { + // outlinks + if (!isInLink) { + return 'link'; + } + + // does class indicate whether it is an (explicit/forced) outlink or a download? + var downloadPattern = getClassesRegExp(configDownloadClasses, 'download'), + linkPattern = getClassesRegExp(configLinkClasses, 'link'), + + // does file extension indicate that it is a download? + downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions + ')([?&#]|$)', 'i'); + + // optimization of the if..elseif..else construct below + return linkPattern.test(className) ? 'link' : (downloadPattern.test(className) || downloadExtensionsPattern.test(href) ? 'download' : 0); /* - var linkType; - - if (linkPattern.test(className)) { - // class attribute contains 'piwik_link' (or user's override) - linkType = 'link'; - } else if (downloadPattern.test(className)) { - // class attribute contains 'piwik_download' (or user's override) - linkType = 'download'; - } else if (downloadExtensionsPattern.test(sourceHref)) { - // file extension matches a defined download extension - linkType = 'download'; - } else { - // otherwise none of the above - linkType = 0; - } - - return linkType; + var linkType; + + if (linkPattern.test(className)) { + // class attribute contains 'piwik_link' (or user's override) + linkType = 'link'; + } else if (downloadPattern.test(className)) { + // class attribute contains 'piwik_download' (or user's override) + linkType = 'download'; + } else if (downloadExtensionsPattern.test(sourceHref)) { + // file extension matches a defined download extension + linkType = 'download'; + } else { + // otherwise none of the above + linkType = 0; + } + + return linkType; */ - } - - /* - * Process clicks - */ - function processClick(sourceElement) { - var parentElement, - tag, - linkType; - - while ((parentElement = sourceElement.parentNode) !== null && - isDefined(parentElement) && // buggy IE5.5 - ((tag = sourceElement.tagName.toUpperCase()) !== 'A' && tag !== 'AREA')) { - sourceElement = parentElement; - } - - if (isDefined(sourceElement.href)) { - // browsers, such as Safari, don't downcase hostname and href - var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href), - sourceHostName = originalSourceHostName.toLowerCase(), - sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), - scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); - - // ignore script pseudo-protocol links - if (!scriptProtocol.test(sourceHref)) { - // track outlinks and all downloads - linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName)); - if (linkType) { - // urldecode %xx - sourceHref = urldecode(sourceHref); - logLink(sourceHref, linkType); - } - } - } - } - - /* - * Handle click event - */ - function clickHandler(evt) { - var button, - target; - - evt = evt || windowAlias.event; - button = evt.which || evt.button; - target = evt.target || evt.srcElement; - - // Using evt.type (added in IE4), we avoid defining separate handlers for mouseup and mousedown. - if (evt.type === 'click') { - if (target) { - processClick(target); - } - } else if (evt.type === 'mousedown') { - if ((button === 1 || button === 2) && target) { - lastButton = button; - lastTarget = target; - } else { - lastButton = lastTarget = null; - } - } else if (evt.type === 'mouseup') { - if (button === lastButton && target === lastTarget) { - processClick(target); - } - lastButton = lastTarget = null; - } - } - - /* - * Add click listener to a DOM element - */ - function addClickListener(element, enable) { - if (enable) { - // for simplicity and performance, we ignore drag events - addEventListener(element, 'mouseup', clickHandler, false); - addEventListener(element, 'mousedown', clickHandler, false); - } else { - addEventListener(element, 'click', clickHandler, false); - } - } - - /* - * Add click handlers to anchor and AREA elements, except those to be ignored - */ - function addClickListeners(enable) { - if (!linkTrackingInstalled) { - linkTrackingInstalled = true; - - // iterate through anchor elements with href and AREA elements - - var i, - ignorePattern = getClassesRegExp(configIgnoreClasses, 'ignore'), - linkElements = documentAlias.links; - - if (linkElements) { - for (i = 0; i < linkElements.length; i++) { - if (!ignorePattern.test(linkElements[i].className)) { - addClickListener(linkElements[i], enable); - } - } - } - } - } - - /* - * Browser features (plugins, resolution, cookies) - */ - function detectBrowserFeatures() { - var i, - mimeType, - pluginMap = { - // document types - pdf: 'application/pdf', - - // media players - qt: 'video/quicktime', - realp: 'audio/x-pn-realaudio-plugin', - wma: 'application/x-mplayer2', - - // interactive multimedia - dir: 'application/x-director', - fla: 'application/x-shockwave-flash', - - // RIA - java: 'application/x-java-vm', - gears: 'application/x-googlegears', - ag: 'application/x-silverlight' - }; - - if (!((new RegExp('MSIE')).test(navigatorAlias.userAgent))) { - // general plugin detection - if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { - for (i in pluginMap) { - if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { - mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; - browserFeatures[i] = (mimeType && mimeType.enabledPlugin) ? '1' : '0'; - } - } - } - - // Safari and Opera - // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly - if (typeof navigator.javaEnabled !== 'unknown' && - isDefined(navigatorAlias.javaEnabled) && - navigatorAlias.javaEnabled()) { - browserFeatures.java = '1'; - } - - // Firefox - if (isFunction(windowAlias.GearsFactory)) { - browserFeatures.gears = '1'; - } - - // other browser features - browserFeatures.cookie = hasCookies(); - } - - // screen resolution - browserFeatures.res = screenAlias.width + 'x' + screenAlias.height; - } + } + + /* + * Process clicks + */ + function processClick(sourceElement) { + var parentElement, + tag, + linkType; + + while ((parentElement = sourceElement.parentNode) !== null && + isDefined(parentElement) && // buggy IE5.5 + ((tag = sourceElement.tagName.toUpperCase()) !== 'A' && tag !== 'AREA')) { + sourceElement = parentElement; + } + + if (isDefined(sourceElement.href)) { + // browsers, such as Safari, don't downcase hostname and href + var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href), + sourceHostName = originalSourceHostName.toLowerCase(), + sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), + scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); + + // ignore script pseudo-protocol links + if (!scriptProtocol.test(sourceHref)) { + // track outlinks and all downloads + linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName)); + if (linkType) { + // urldecode %xx + sourceHref = urldecode(sourceHref); + logLink(sourceHref, linkType); + } + } + } + } + + /* + * Handle click event + */ + function clickHandler(evt) { + var button, + target; + + evt = evt || windowAlias.event; + button = evt.which || evt.button; + target = evt.target || evt.srcElement; + + // Using evt.type (added in IE4), we avoid defining separate handlers for mouseup and mousedown. + if (evt.type === 'click') { + if (target) { + processClick(target); + } + } else if (evt.type === 'mousedown') { + if ((button === 1 || button === 2) && target) { + lastButton = button; + lastTarget = target; + } else { + lastButton = lastTarget = null; + } + } else if (evt.type === 'mouseup') { + if (button === lastButton && target === lastTarget) { + processClick(target); + } + lastButton = lastTarget = null; + } + } + + /* + * Add click listener to a DOM element + */ + function addClickListener(element, enable) { + if (enable) { + // for simplicity and performance, we ignore drag events + addEventListener(element, 'mouseup', clickHandler, false); + addEventListener(element, 'mousedown', clickHandler, false); + } else { + addEventListener(element, 'click', clickHandler, false); + } + } + + /* + * Add click handlers to anchor and AREA elements, except those to be ignored + */ + function addClickListeners(enable) { + if (!linkTrackingInstalled) { + linkTrackingInstalled = true; + + // iterate through anchor elements with href and AREA elements + + var i, + ignorePattern = getClassesRegExp(configIgnoreClasses, 'ignore'), + linkElements = documentAlias.links; + + if (linkElements) { + for (i = 0; i < linkElements.length; i++) { + if (!ignorePattern.test(linkElements[i].className)) { + addClickListener(linkElements[i], enable); + } + } + } + } + } + + /* + * Browser features (plugins, resolution, cookies) + */ + function detectBrowserFeatures() { + var i, + mimeType, + pluginMap = { + // document types + pdf: 'application/pdf', + + // media players + qt: 'video/quicktime', + realp: 'audio/x-pn-realaudio-plugin', + wma: 'application/x-mplayer2', + + // interactive multimedia + dir: 'application/x-director', + fla: 'application/x-shockwave-flash', + + // RIA + java: 'application/x-java-vm', + gears: 'application/x-googlegears', + ag: 'application/x-silverlight' + }; + + if (!((new RegExp('MSIE')).test(navigatorAlias.userAgent))) { + // general plugin detection + if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { + for (i in pluginMap) { + if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { + mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; + browserFeatures[i] = (mimeType && mimeType.enabledPlugin) ? '1' : '0'; + } + } + } + + // Safari and Opera + // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly + if (typeof navigator.javaEnabled !== 'unknown' && + isDefined(navigatorAlias.javaEnabled) && + navigatorAlias.javaEnabled()) { + browserFeatures.java = '1'; + } + + // Firefox + if (isFunction(windowAlias.GearsFactory)) { + browserFeatures.gears = '1'; + } + + // other browser features + browserFeatures.cookie = hasCookies(); + } + + // screen resolution + browserFeatures.res = screenAlias.width + 'x' + screenAlias.height; + } /*<DEBUG>*/ - /* - * Register a test hook. Using eval() permits access to otherwise - * privileged members. - */ - function registerHook(hookName, userHook) { - var hookObj = null; - - if (isString(hookName) && !isDefined(registeredHooks[hookName]) && userHook) { - if (isObject(userHook)) { - hookObj = userHook; - } else if (isString(userHook)) { - try { - eval('hookObj =' + userHook); - } catch (e) { } - } - - registeredHooks[hookName] = hookObj; - } - return hookObj; - } + /* + * Register a test hook. Using eval() permits access to otherwise + * privileged members. + */ + function registerHook(hookName, userHook) { + var hookObj = null; + + if (isString(hookName) && !isDefined(registeredHooks[hookName]) && userHook) { + if (isObject(userHook)) { + hookObj = userHook; + } else if (isString(userHook)) { + try { + eval('hookObj =' + userHook); + } catch (e) { } + } + + registeredHooks[hookName] = hookObj; + } + return hookObj; + } /*</DEBUG>*/ - /************************************************************ - * Constructor - ************************************************************/ + /************************************************************ + * Constructor + ************************************************************/ - /* - * initialize tracker - */ - detectBrowserFeatures(); - updateDomainHash(); + /* + * initialize tracker + */ + detectBrowserFeatures(); + updateDomainHash(); /*<DEBUG>*/ - /* - * initialize test plugin - */ - executePluginMethod('run', registerHook); + /* + * initialize test plugin + */ + executePluginMethod('run', registerHook); /*</DEBUG>*/ - /************************************************************ - * Public data and methods - ************************************************************/ + /************************************************************ + * Public data and methods + ************************************************************/ - return { + return { /*<DEBUG>*/ - /* - * Test hook accessors - */ - hook: registeredHooks, - getHook: function (hookName) { - return registeredHooks[hookName]; - }, + /* + * Test hook accessors + */ + hook: registeredHooks, + getHook: function (hookName) { + return registeredHooks[hookName]; + }, /*</DEBUG>*/ - /** - * Get visitor ID (from first party cookie) - * - * @return string Visitor ID in hexits (or null, if not yet known) - */ - getVisitorId: function () { - return (loadVisitorIdCookie())[1]; - }, - - /** - * Get the visitor information (from first party cookie) - * - * @return array - */ - getVisitorInfo: function () { - return loadVisitorIdCookie(); - }, - - /** - * Get the Attribution information, which is an array that contains - * the Referrer used to reach the site as well as the campaign name and keyword - * It is useful only when used in conjunction with Tracker API function setAttributionInfo() - * To access specific data point, you should use the other functions getAttributionReferrer* and getAttributionCampaign* - * - * @return array Attribution array, Example use: - * 1) Call JSON2.stringify(piwikTracker.getAttributionInfo()) - * 2) Pass this json encoded string to the Tracking API (php or java client): setAttributionInfo() - */ - getAttributionInfo: function () { - return loadReferrerAttributionCookie(); - }, - - /** - * Get the Campaign name that was parsed from the landing page URL when the visitor - * landed on the site originally - * - * @return string - */ - getAttributionCampaignName: function () { - return loadReferrerAttributionCookie()[0]; - }, - - /** - * Get the Campaign keyword that was parsed from the landing page URL when the visitor - * landed on the site originally - * - * @return string - */ - getAttributionCampaignKeyword: function () { - return loadReferrerAttributionCookie()[1]; - }, - - /** - * Get the time at which the referrer (used for Goal Attribution) was detected - * - * @return int Timestamp or 0 if no referrer currently set - */ - getAttributionReferrerTimestamp: function () { - return loadReferrerAttributionCookie()[2]; - }, - - /** - * Get the full referrer URL that will be used for Goal Attribution - * - * @return string Raw URL, or empty string '' if no referrer currently set - */ - getAttributionReferrerUrl: function () { - return loadReferrerAttributionCookie()[3]; - }, - - /** - * Specify the Piwik server URL - * - * @param string trackerUrl - */ - setTrackerUrl: function (trackerUrl) { - configTrackerUrl = trackerUrl; - }, - - /** - * Specify the site ID - * - * @param int|string siteId - */ - setSiteId: function (siteId) { - configTrackerSiteId = siteId; - }, - - /** - * Pass custom data to the server - * - * Examples: - * tracker.setCustomData(object); - * tracker.setCustomData(key, value); - * - * @param mixed key_or_obj - * @param mixed opt_value - */ - setCustomData: function (key_or_obj, opt_value) { - if (isObject(key_or_obj)) { - configCustomData = key_or_obj; - } else { - if (!configCustomData) { - configCustomData = []; - } - configCustomData[key_or_obj] = opt_value; - } - }, - - /** - * Get custom data - * - * @return mixed - */ - getCustomData: function () { - return configCustomData; - }, - - /** - * Set custom variable within this visit - * - * @param int index - * @param string name - * @param string value - * @param string scope Scope of Custom Variable: - * - "visit" will store the name/value in the visit and will persist it in the cookie for the duration of the visit, - * - "page" will store the name/value in the page view. - */ - setCustomVariable: function (index, name, value, scope) { - var toRecord; - if (!isDefined(scope)) { - scope = 'visit'; - } - if (index > 0) { - name = isDefined(name) && !isString(name) ? String(name) : name; - value = isDefined(value) && !isString(value) ? String(value) : value; - toRecord = [name.slice(0, customVariableMaximumLength), value.slice(0, customVariableMaximumLength)]; - if (scope === 'visit' || scope === 2) { /* GA compatibility/misuse */ - loadCustomVariables(); - customVariables[index] = toRecord; - } else if (scope === 'page' || scope === 3) { /* GA compatibility/misuse */ - customVariablesPage[index] = toRecord; - } - } - }, - - /** - * Get custom variable - * - * @param int index - * @param string scope Scope of Custom Variable: "visit" or "page" - */ - getCustomVariable: function (index, scope) { - var cvar; - if (!isDefined(scope)) { - scope = "visit"; - } - if (scope === "page" || scope === 3) { - cvar = customVariablesPage[index]; - } else if (scope === "visit" || scope === 2) { - loadCustomVariables(); - cvar = customVariables[index]; - } - if (!isDefined(cvar) - || (cvar && cvar[0] === '')) { - return false; - } - return cvar; - }, - - /** - * Delete custom variable - * - * @param int index - */ - deleteCustomVariable: function (index, scope) { - // Only delete if it was there already - if (this.getCustomVariable(index, scope)) { - this.setCustomVariable(index, '', '', scope); - } - }, - - /** - * Set delay for link tracking (in milliseconds) - * - * @param int delay - */ - setLinkTrackingTimer: function (delay) { - configTrackerPause = delay; - }, - - /** - * Set list of file extensions to be recognized as downloads - * - * @param string extensions - */ - setDownloadExtensions: function (extensions) { - configDownloadExtensions = extensions; - }, - - /** - * Specify additional file extensions to be recognized as downloads - * - * @param string extensions - */ - addDownloadExtensions: function (extensions) { - configDownloadExtensions += '|' + extensions; - }, - - /** - * Set array of domains to be treated as local - * - * @param string|array hostsAlias - */ - setDomains: function (hostsAlias) { - configHostsAlias = isString(hostsAlias) ? [hostsAlias] : hostsAlias; - configHostsAlias.push(domainAlias); - }, - - /** - * Set array of classes to be ignored if present in link - * - * @param string|array ignoreClasses - */ - setIgnoreClasses: function (ignoreClasses) { - configIgnoreClasses = isString(ignoreClasses) ? [ignoreClasses] : ignoreClasses; - }, - - /** - * Set request method - * - * @param string method GET or POST; default is GET - */ - setRequestMethod: function (method) { - configRequestMethod = method || 'GET'; - }, - - /** - * Override referrer - * - * @param string url - */ - setReferrerUrl: function (url) { - configReferrerUrl = url; - }, - - /** - * Override url - * - * @param string url - */ - setCustomUrl: function (url) { - configCustomUrl = resolveRelativeReference(locationHrefAlias, url); - }, - - /** - * Override document.title - * - * @param string title - */ - setDocumentTitle: function (title) { - configTitle = title; - }, - - /** - * Set array of classes to be treated as downloads - * - * @param string|array downloadClasses - */ - setDownloadClasses: function (downloadClasses) { - configDownloadClasses = isString(downloadClasses) ? [downloadClasses] : downloadClasses; - }, - - /** - * Set array of classes to be treated as outlinks - * - * @param string|array linkClasses - */ - setLinkClasses: function (linkClasses) { - configLinkClasses = isString(linkClasses) ? [linkClasses] : linkClasses; - }, - - /** - * Set array of campaign name parameters - * - * @see http://piwik.org/faq/how-to/#faq_120 - * @param string|array campaignNames - */ - setCampaignNameKey: function (campaignNames) { - configCampaignNameParameters = isString(campaignNames) ? [campaignNames] : campaignNames; - }, - - /** - * Set array of campaign keyword parameters - * - * @see http://piwik.org/faq/how-to/#faq_120 - * @param string|array campaignKeywords - */ - setCampaignKeywordKey: function (campaignKeywords) { - configCampaignKeywordParameters = isString(campaignKeywords) ? [campaignKeywords] : campaignKeywords; - }, - - /** - * Strip hash tag (or anchor) from URL - * - * @param bool enableFilter - */ - discardHashTag: function (enableFilter) { - configDiscardHashTag = enableFilter; - }, - - /** - * Set first-party cookie name prefix - * - * @param string cookieNamePrefix - */ - setCookieNamePrefix: function (cookieNamePrefix) { - configCookieNamePrefix = cookieNamePrefix; - // Re-init the Custom Variables cookie - customVariables = getCustomVariablesFromCookie(); - }, - - /** - * Set first-party cookie domain - * - * @param string domain - */ - setCookieDomain: function (domain) { - configCookieDomain = domainFixup(domain); - updateDomainHash(); - }, - - /** - * Set first-party cookie path - * - * @param string domain - */ - setCookiePath: function (path) { - configCookiePath = path; - updateDomainHash(); - }, - - /** - * Set visitor cookie timeout (in seconds) - * - * @param int timeout - */ - setVisitorCookieTimeout: function (timeout) { - configVisitorCookieTimeout = timeout * 1000; - }, - - /** - * Set session cookie timeout (in seconds) - * - * @param int timeout - */ - setSessionCookieTimeout: function (timeout) { - configSessionCookieTimeout = timeout * 1000; - }, - - /** - * Set referral cookie timeout (in seconds) - * - * @param int timeout - */ - setReferralCookieTimeout: function (timeout) { - configReferralCookieTimeout = timeout * 1000; - }, - - /** - * Set conversion attribution to first referrer and campaign - * - * @param bool if true, use first referrer (and first campaign) - * if false, use the last referrer (or campaign) - */ - setConversionAttributionFirstReferrer: function (enable) { - configConversionAttributionFirstReferrer = enable; - }, - - /** - * Disables all cookies from being set - * Existing cookies will be deleted on the next call to track* - * - */ - disableCookies: function () { - configCookiesDisabled = true; - browserFeatures.cookie = '0'; - }, - - /** - * Handle do-not-track requests - * - * @param bool enable If true, don't track if user agent sends 'do-not-track' header - */ - setDoNotTrack: function (enable) { - var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack; - configDoNotTrack = enable && (dnt === 'yes' || dnt === '1'); - - // do not track also disables cookies and deletes existing cookies - if (configDoNotTrack) { - this.disableCookies(); - } - }, - - /** - * Add click listener to a specific link element. - * When clicked, Piwik will log the click automatically. - * - * @param DOMElement element - * @param bool enable If true, use pseudo click-handler (mousedown+mouseup) - */ - addListener: function (element, enable) { - addClickListener(element, enable); - }, - - /** - * Install link tracker - * - * The default behaviour is to use actual click events. However, some browsers - * (e.g., Firefox, Opera, and Konqueror) don't generate click events for the middle mouse button. - * - * To capture more "clicks", the pseudo click-handler uses mousedown + mouseup events. - * This is not industry standard and is vulnerable to false positives (e.g., drag events). - * - * There is a Safari/Chrome/Webkit bug that prevents tracking requests from being sent - * by either click handler. The workaround is to set a target attribute (which can't - * be "_self", "_top", or "_parent"). - * - * @see https://bugs.webkit.org/show_bug.cgi?id=54783 - * - * @param bool enable If true, use pseudo click-handler (mousedown+mouseup) - */ - enableLinkTracking: function (enable) { - if (hasLoaded) { - // the load event has already fired, add the click listeners now - addClickListeners(enable); - } else { - // defer until page has loaded - registeredOnLoadHandlers.push(function () { - addClickListeners(enable); - }); - } - }, - - /** - * Set heartbeat (in seconds) - * - * @param int minimumVisitLength - * @param int heartBeatDelay - */ - setHeartBeatTimer: function (minimumVisitLength, heartBeatDelay) { - var now = new Date(); - - configMinimumVisitTime = now.getTime() + minimumVisitLength * 1000; - configHeartBeatTimer = heartBeatDelay * 1000; - }, - - /** - * Frame buster - */ - killFrame: function () { - if (windowAlias.location !== windowAlias.top.location) { - windowAlias.top.location = windowAlias.location; - } - }, - - /** - * Redirect if browsing offline (aka file: buster) - * - * @param string url Redirect to this URL - */ - redirectFile: function (url) { - if (windowAlias.location.protocol === 'file:') { - windowAlias.location = url; - } - }, - - /** - * Count sites in pre-rendered state - * - * @param bool enable If true, track when in pre-rendered state - */ - setCountPreRendered: function (enable) { - configCountPreRendered = enable; - }, - - /** - * Trigger a goal - * - * @param int|string idGoal - * @param int|float customRevenue - * @param mixed customData - */ - trackGoal: function (idGoal, customRevenue, customData) { - trackCallback(function () { - logGoal(idGoal, customRevenue, customData); - }); - }, - - /** - * Manually log a click from your own code - * - * @param string sourceUrl - * @param string linkType - * @param mixed customData - */ - trackLink: function (sourceUrl, linkType, customData) { - trackCallback(function () { - logLink(sourceUrl, linkType, customData); - }); - }, - - /** - * Log visit to this page - * - * @param string customTitle - * @param mixed customData - */ - trackPageView: function (customTitle, customData) { - trackCallback(function () { - logPageView(customTitle, customData); - }); - }, - - - /** - * Used to record that the current page view is an item (product) page view, or a Ecommerce Category page view. - * This must be called before trackPageView() on the product/category page. - * It will set 3 custom variables of scope "page" with the SKU, Name and Category for this page view. - * Note: Custom Variables of scope "page" slots 3, 4 and 5 will be used. - * - * On a category page, you can set the parameter category, and set the other parameters to empty string or false - * - * Tracking Product/Category page views will allow Piwik to report on Product & Categories - * conversion rates (Conversion rate = Ecommerce orders containing this product or category / Visits to the product or category) - * - * @param string sku Item's SKU code being viewed - * @param string name Item's Name being viewed - * @param string category Category page being viewed. On an Item's page, this is the item's category - * @param float price Item's display price, not use in standard Piwik reports, but output in API product reports. - */ - setEcommerceView: function (sku, name, category, price) { - if (!isDefined(category) || !category.length) { - category = ""; - } else if (category instanceof Array) { - category = JSON2.stringify(category); - } - customVariablesPage[5] = ['_pkc', category]; - if (isDefined(price) && String(price).length) { - customVariablesPage[2] = ['_pkp', price]; - } - // On a category page, do not track Product name not defined - if ((!isDefined(sku) || !sku.length) - && (!isDefined(name) || !name.length)) { - return; - } - - if (isDefined(sku) && sku.length) { - customVariablesPage[3] = ['_pks', sku]; - } - if (!isDefined(name) || !name.length) { - name = ""; - } - customVariablesPage[4] = ['_pkn', name]; - }, - - /** - * Adds an item (product) that is in the current Cart or in the Ecommerce order. - * This function is called for every item (product) in the Cart or the Order. - * The only required parameter is sku. - * - * @param string sku (required) Item's SKU Code. This is the unique identifier for the product. - * @param string name (optional) Item's name - * @param string name (optional) Item's category, or array of up to 5 categories - * @param float price (optional) Item's price. If not specified, will default to 0 - * @param float quantity (optional) Item's quantity. If not specified, will default to 1 - */ - addEcommerceItem: function (sku, name, category, price, quantity) { - if (sku.length) { - ecommerceItems[sku] = [ sku, name, category, price, quantity ]; - } - }, - - /** - * Tracks an Ecommerce order. - * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order. - * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Piwik reports. - * Parameters orderId and grandTotal are required. For others, you can set to false if you don't need to specify them. - * - * @param string|int orderId (required) Unique Order ID. - * This will be used to count this order only once in the event the order page is reloaded several times. - * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Piwik. - * @param float grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.) - * @param float subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied) - * @param float tax (optional) Tax amount for this order - * @param float shipping (optional) Shipping amount for this order - * @param float discount (optional) Discounted amount in this order - */ - trackEcommerceOrder: function (orderId, grandTotal, subTotal, tax, shipping, discount) { - logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount); - }, - - /** - * Tracks a Cart Update (add item, remove item, update item). - * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart, including the items that haven't been updated since the last cart update. - * Then you can call this function with the Cart grandTotal (typically the sum of all items' prices) - * - * @param float grandTotal (required) Items (products) amount in the Cart - */ - trackEcommerceCartUpdate: function (grandTotal) { - logEcommerceCartUpdate(grandTotal); - } - - }; - } - - /************************************************************ - * Proxy object - * - this allows the caller to continue push()'ing to _paq - * after the Tracker has been initialized and loaded - ************************************************************/ - - function TrackerProxy() { - return { - push: apply - }; - } - - /************************************************************ - * Constructor - ************************************************************/ - - // initialize the Piwik singleton - addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); - addReadyListener(); - - Date.prototype.getTimeAlias = Date.prototype.getTime; - - asyncTracker = new Tracker(); - - for (i = 0; i < _paq.length; i++) { - apply(_paq[i]); - } - - // replace initialization array with proxy object - _paq = new TrackerProxy(); - - /************************************************************ - * Public data and methods - ************************************************************/ - - return { - /** - * Add plugin - * - * @param string pluginName - * @param Object pluginObj - */ - addPlugin: function (pluginName, pluginObj) { - plugins[pluginName] = pluginObj; - }, - - /** - * Get Tracker (factory method) - * - * @param string piwikUrl - * @param int|string siteId - * @return Tracker - */ - getTracker: function (piwikUrl, siteId) { - return new Tracker(piwikUrl, siteId); - }, - - /** - * Get internal asynchronous tracker object - * - * @return Tracker - */ - getAsyncTracker: function () { - return asyncTracker; - } - }; - }()), - - /************************************************************ - * Deprecated functionality below - * - for legacy piwik.js compatibility - ************************************************************/ - - /* - * Piwik globals - * - * var piwik_install_tracker, piwik_tracker_pause, piwik_download_extensions, piwik_hosts_alias, piwik_ignore_classes; - */ - - piwik_track, - - /** - * Track page visit - * - * @param string documentTitle - * @param int|string siteId - * @param string piwikUrl - * @param mixed customData - */ - piwik_log = function (documentTitle, siteId, piwikUrl, customData) { - "use strict"; - - function getOption(optionName) { - try { - return eval('piwik_' + optionName); - } catch (e) { } - - return; // undefined - } - - // instantiate the tracker - var option, - piwikTracker = Piwik.getTracker(piwikUrl, siteId); - - // initialize tracker - piwikTracker.setDocumentTitle(documentTitle); - piwikTracker.setCustomData(customData); - - // handle Piwik globals - option = getOption('tracker_pause'); - if (option) { - piwikTracker.setLinkTrackingTimer(option); - } - option = getOption('download_extensions'); - if (option) { - piwikTracker.setDownloadExtensions(option); - } - option = getOption('hosts_alias'); - if (option) { - piwikTracker.setDomains(option); - } - option = getOption('ignore_classes'); - if (option) { - piwikTracker.setIgnoreClasses(option); - } - - // track this page view - piwikTracker.trackPageView(); - - // default is to install the link tracker - if (getOption('install_tracker')) { - - /** - * Track click manually (function is defined below) - * - * @param string sourceUrl - * @param int|string siteId - * @param string piwikUrl - * @param string linkType - */ - piwik_track = function (sourceUrl, siteId, piwikUrl, linkType) { - piwikTracker.setSiteId(siteId); - piwikTracker.setTrackerUrl(piwikUrl); - piwikTracker.trackLink(sourceUrl, linkType); - }; - - // set-up link tracking - piwikTracker.enableLinkTracking(); - } - }; + /** + * Get visitor ID (from first party cookie) + * + * @return string Visitor ID in hexits (or null, if not yet known) + */ + getVisitorId: function () { + return (loadVisitorIdCookie())[1]; + }, + + /** + * Get the visitor information (from first party cookie) + * + * @return array + */ + getVisitorInfo: function () { + return loadVisitorIdCookie(); + }, + + /** + * Get the Attribution information, which is an array that contains + * the Referrer used to reach the site as well as the campaign name and keyword + * It is useful only when used in conjunction with Tracker API function setAttributionInfo() + * To access specific data point, you should use the other functions getAttributionReferrer* and getAttributionCampaign* + * + * @return array Attribution array, Example use: + * 1) Call JSON2.stringify(piwikTracker.getAttributionInfo()) + * 2) Pass this json encoded string to the Tracking API (php or java client): setAttributionInfo() + */ + getAttributionInfo: function () { + return loadReferrerAttributionCookie(); + }, + + /** + * Get the Campaign name that was parsed from the landing page URL when the visitor + * landed on the site originally + * + * @return string + */ + getAttributionCampaignName: function () { + return loadReferrerAttributionCookie()[0]; + }, + + /** + * Get the Campaign keyword that was parsed from the landing page URL when the visitor + * landed on the site originally + * + * @return string + */ + getAttributionCampaignKeyword: function () { + return loadReferrerAttributionCookie()[1]; + }, + + /** + * Get the time at which the referrer (used for Goal Attribution) was detected + * + * @return int Timestamp or 0 if no referrer currently set + */ + getAttributionReferrerTimestamp: function () { + return loadReferrerAttributionCookie()[2]; + }, + + /** + * Get the full referrer URL that will be used for Goal Attribution + * + * @return string Raw URL, or empty string '' if no referrer currently set + */ + getAttributionReferrerUrl: function () { + return loadReferrerAttributionCookie()[3]; + }, + + /** + * Specify the Piwik server URL + * + * @param string trackerUrl + */ + setTrackerUrl: function (trackerUrl) { + configTrackerUrl = trackerUrl; + }, + + /** + * Specify the site ID + * + * @param int|string siteId + */ + setSiteId: function (siteId) { + configTrackerSiteId = siteId; + }, + + /** + * Pass custom data to the server + * + * Examples: + * tracker.setCustomData(object); + * tracker.setCustomData(key, value); + * + * @param mixed key_or_obj + * @param mixed opt_value + */ + setCustomData: function (key_or_obj, opt_value) { + if (isObject(key_or_obj)) { + configCustomData = key_or_obj; + } else { + if (!configCustomData) { + configCustomData = []; + } + configCustomData[key_or_obj] = opt_value; + } + }, + + /** + * Get custom data + * + * @return mixed + */ + getCustomData: function () { + return configCustomData; + }, + + /** + * Set custom variable within this visit + * + * @param int index + * @param string name + * @param string value + * @param string scope Scope of Custom Variable: + * - "visit" will store the name/value in the visit and will persist it in the cookie for the duration of the visit, + * - "page" will store the name/value in the page view. + */ + setCustomVariable: function (index, name, value, scope) { + var toRecord; + if (!isDefined(scope)) { + scope = 'visit'; + } + if (index > 0) { + name = isDefined(name) && !isString(name) ? String(name) : name; + value = isDefined(value) && !isString(value) ? String(value) : value; + toRecord = [name.slice(0, customVariableMaximumLength), value.slice(0, customVariableMaximumLength)]; + if (scope === 'visit' || scope === 2) { /* GA compatibility/misuse */ + loadCustomVariables(); + customVariables[index] = toRecord; + } else if (scope === 'page' || scope === 3) { /* GA compatibility/misuse */ + customVariablesPage[index] = toRecord; + } + } + }, + + /** + * Get custom variable + * + * @param int index + * @param string scope Scope of Custom Variable: "visit" or "page" + */ + getCustomVariable: function (index, scope) { + var cvar; + if (!isDefined(scope)) { + scope = "visit"; + } + if (scope === "page" || scope === 3) { + cvar = customVariablesPage[index]; + } else if (scope === "visit" || scope === 2) { + loadCustomVariables(); + cvar = customVariables[index]; + } + if (!isDefined(cvar) + || (cvar && cvar[0] === '')) { + return false; + } + return cvar; + }, + + /** + * Delete custom variable + * + * @param int index + */ + deleteCustomVariable: function (index, scope) { + // Only delete if it was there already + if (this.getCustomVariable(index, scope)) { + this.setCustomVariable(index, '', '', scope); + } + }, + + /** + * Set delay for link tracking (in milliseconds) + * + * @param int delay + */ + setLinkTrackingTimer: function (delay) { + configTrackerPause = delay; + }, + + /** + * Set list of file extensions to be recognized as downloads + * + * @param string extensions + */ + setDownloadExtensions: function (extensions) { + configDownloadExtensions = extensions; + }, + + /** + * Specify additional file extensions to be recognized as downloads + * + * @param string extensions + */ + addDownloadExtensions: function (extensions) { + configDownloadExtensions += '|' + extensions; + }, + + /** + * Set array of domains to be treated as local + * + * @param string|array hostsAlias + */ + setDomains: function (hostsAlias) { + configHostsAlias = isString(hostsAlias) ? [hostsAlias] : hostsAlias; + configHostsAlias.push(domainAlias); + }, + + /** + * Set array of classes to be ignored if present in link + * + * @param string|array ignoreClasses + */ + setIgnoreClasses: function (ignoreClasses) { + configIgnoreClasses = isString(ignoreClasses) ? [ignoreClasses] : ignoreClasses; + }, + + /** + * Set request method + * + * @param string method GET or POST; default is GET + */ + setRequestMethod: function (method) { + configRequestMethod = method || 'GET'; + }, + + /** + * Override referrer + * + * @param string url + */ + setReferrerUrl: function (url) { + configReferrerUrl = url; + }, + + /** + * Override url + * + * @param string url + */ + setCustomUrl: function (url) { + configCustomUrl = resolveRelativeReference(locationHrefAlias, url); + }, + + /** + * Override document.title + * + * @param string title + */ + setDocumentTitle: function (title) { + configTitle = title; + }, + + /** + * Set array of classes to be treated as downloads + * + * @param string|array downloadClasses + */ + setDownloadClasses: function (downloadClasses) { + configDownloadClasses = isString(downloadClasses) ? [downloadClasses] : downloadClasses; + }, + + /** + * Set array of classes to be treated as outlinks + * + * @param string|array linkClasses + */ + setLinkClasses: function (linkClasses) { + configLinkClasses = isString(linkClasses) ? [linkClasses] : linkClasses; + }, + + /** + * Set array of campaign name parameters + * + * @see http://piwik.org/faq/how-to/#faq_120 + * @param string|array campaignNames + */ + setCampaignNameKey: function (campaignNames) { + configCampaignNameParameters = isString(campaignNames) ? [campaignNames] : campaignNames; + }, + + /** + * Set array of campaign keyword parameters + * + * @see http://piwik.org/faq/how-to/#faq_120 + * @param string|array campaignKeywords + */ + setCampaignKeywordKey: function (campaignKeywords) { + configCampaignKeywordParameters = isString(campaignKeywords) ? [campaignKeywords] : campaignKeywords; + }, + + /** + * Strip hash tag (or anchor) from URL + * + * @param bool enableFilter + */ + discardHashTag: function (enableFilter) { + configDiscardHashTag = enableFilter; + }, + + /** + * Set first-party cookie name prefix + * + * @param string cookieNamePrefix + */ + setCookieNamePrefix: function (cookieNamePrefix) { + configCookieNamePrefix = cookieNamePrefix; + // Re-init the Custom Variables cookie + customVariables = getCustomVariablesFromCookie(); + }, + + /** + * Set first-party cookie domain + * + * @param string domain + */ + setCookieDomain: function (domain) { + configCookieDomain = domainFixup(domain); + updateDomainHash(); + }, + + /** + * Set first-party cookie path + * + * @param string domain + */ + setCookiePath: function (path) { + configCookiePath = path; + updateDomainHash(); + }, + + /** + * Set visitor cookie timeout (in seconds) + * + * @param int timeout + */ + setVisitorCookieTimeout: function (timeout) { + configVisitorCookieTimeout = timeout * 1000; + }, + + /** + * Set session cookie timeout (in seconds) + * + * @param int timeout + */ + setSessionCookieTimeout: function (timeout) { + configSessionCookieTimeout = timeout * 1000; + }, + + /** + * Set referral cookie timeout (in seconds) + * + * @param int timeout + */ + setReferralCookieTimeout: function (timeout) { + configReferralCookieTimeout = timeout * 1000; + }, + + /** + * Set conversion attribution to first referrer and campaign + * + * @param bool if true, use first referrer (and first campaign) + * if false, use the last referrer (or campaign) + */ + setConversionAttributionFirstReferrer: function (enable) { + configConversionAttributionFirstReferrer = enable; + }, + + /** + * Disables all cookies from being set + * + * Existing cookies will be deleted on the next call to track + */ + disableCookies: function () { + configCookiesDisabled = true; + browserFeatures.cookie = '0'; + }, + + /** + * Handle do-not-track requests + * + * @param bool enable If true, don't track if user agent sends 'do-not-track' header + */ + setDoNotTrack: function (enable) { + var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack; + configDoNotTrack = enable && (dnt === 'yes' || dnt === '1'); + + // do not track also disables cookies and deletes existing cookies + if (configDoNotTrack) { + this.disableCookies(); + } + }, + + /** + * Add click listener to a specific link element. + * When clicked, Piwik will log the click automatically. + * + * @param DOMElement element + * @param bool enable If true, use pseudo click-handler (mousedown+mouseup) + */ + addListener: function (element, enable) { + addClickListener(element, enable); + }, + + /** + * Install link tracker + * + * The default behaviour is to use actual click events. However, some browsers + * (e.g., Firefox, Opera, and Konqueror) don't generate click events for the middle mouse button. + * + * To capture more "clicks", the pseudo click-handler uses mousedown + mouseup events. + * This is not industry standard and is vulnerable to false positives (e.g., drag events). + * + * There is a Safari/Chrome/Webkit bug that prevents tracking requests from being sent + * by either click handler. The workaround is to set a target attribute (which can't + * be "_self", "_top", or "_parent"). + * + * @see https://bugs.webkit.org/show_bug.cgi?id=54783 + * + * @param bool enable If true, use pseudo click-handler (mousedown+mouseup) + */ + enableLinkTracking: function (enable) { + if (hasLoaded) { + // the load event has already fired, add the click listeners now + addClickListeners(enable); + } else { + // defer until page has loaded + registeredOnLoadHandlers.push(function () { + addClickListeners(enable); + }); + } + }, + + /** + * Set heartbeat (in seconds) + * + * @param int minimumVisitLength + * @param int heartBeatDelay + */ + setHeartBeatTimer: function (minimumVisitLength, heartBeatDelay) { + var now = new Date(); + + configMinimumVisitTime = now.getTime() + minimumVisitLength * 1000; + configHeartBeatTimer = heartBeatDelay * 1000; + }, + + /** + * Frame buster + */ + killFrame: function () { + if (windowAlias.location !== windowAlias.top.location) { + windowAlias.top.location = windowAlias.location; + } + }, + + /** + * Redirect if browsing offline (aka file: buster) + * + * @param string url Redirect to this URL + */ + redirectFile: function (url) { + if (windowAlias.location.protocol === 'file:') { + windowAlias.location = url; + } + }, + + /** + * Count sites in pre-rendered state + * + * @param bool enable If true, track when in pre-rendered state + */ + setCountPreRendered: function (enable) { + configCountPreRendered = enable; + }, + + /** + * Trigger a goal + * + * @param int|string idGoal + * @param int|float customRevenue + * @param mixed customData + */ + trackGoal: function (idGoal, customRevenue, customData) { + trackCallback(function () { + logGoal(idGoal, customRevenue, customData); + }); + }, + + /** + * Manually log a click from your own code + * + * @param string sourceUrl + * @param string linkType + * @param mixed customData + */ + trackLink: function (sourceUrl, linkType, customData) { + trackCallback(function () { + logLink(sourceUrl, linkType, customData); + }); + }, + + /** + * Log visit to this page + * + * @param string customTitle + * @param mixed customData + */ + trackPageView: function (customTitle, customData) { + trackCallback(function () { + logPageView(customTitle, customData); + }); + }, + + + /** + * Used to record that the current page view is an item (product) page view, or a Ecommerce Category page view. + * This must be called before trackPageView() on the product/category page. + * It will set 3 custom variables of scope "page" with the SKU, Name and Category for this page view. + * Note: Custom Variables of scope "page" slots 3, 4 and 5 will be used. + * + * On a category page, you can set the parameter category, and set the other parameters to empty string or false + * + * Tracking Product/Category page views will allow Piwik to report on Product & Categories + * conversion rates (Conversion rate = Ecommerce orders containing this product or category / Visits to the product or category) + * + * @param string sku Item's SKU code being viewed + * @param string name Item's Name being viewed + * @param string category Category page being viewed. On an Item's page, this is the item's category + * @param float price Item's display price, not use in standard Piwik reports, but output in API product reports. + */ + setEcommerceView: function (sku, name, category, price) { + if (!isDefined(category) || !category.length) { + category = ""; + } else if (category instanceof Array) { + category = JSON2.stringify(category); + } + customVariablesPage[5] = ['_pkc', category]; + if (isDefined(price) && String(price).length) { + customVariablesPage[2] = ['_pkp', price]; + } + // On a category page, do not track Product name not defined + if ((!isDefined(sku) || !sku.length) + && (!isDefined(name) || !name.length)) { + return; + } + + if (isDefined(sku) && sku.length) { + customVariablesPage[3] = ['_pks', sku]; + } + if (!isDefined(name) || !name.length) { + name = ""; + } + customVariablesPage[4] = ['_pkn', name]; + }, + + /** + * Adds an item (product) that is in the current Cart or in the Ecommerce order. + * This function is called for every item (product) in the Cart or the Order. + * The only required parameter is sku. + * + * @param string sku (required) Item's SKU Code. This is the unique identifier for the product. + * @param string name (optional) Item's name + * @param string name (optional) Item's category, or array of up to 5 categories + * @param float price (optional) Item's price. If not specified, will default to 0 + * @param float quantity (optional) Item's quantity. If not specified, will default to 1 + */ + addEcommerceItem: function (sku, name, category, price, quantity) { + if (sku.length) { + ecommerceItems[sku] = [ sku, name, category, price, quantity ]; + } + }, + + /** + * Tracks an Ecommerce order. + * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order. + * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Piwik reports. + * Parameters orderId and grandTotal are required. For others, you can set to false if you don't need to specify them. + * + * @param string|int orderId (required) Unique Order ID. + * This will be used to count this order only once in the event the order page is reloaded several times. + * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Piwik. + * @param float grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.) + * @param float subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied) + * @param float tax (optional) Tax amount for this order + * @param float shipping (optional) Shipping amount for this order + * @param float discount (optional) Discounted amount in this order + */ + trackEcommerceOrder: function (orderId, grandTotal, subTotal, tax, shipping, discount) { + logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount); + }, + + /** + * Tracks a Cart Update (add item, remove item, update item). + * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart, including the items that haven't been updated since the last cart update. + * Then you can call this function with the Cart grandTotal (typically the sum of all items' prices) + * + * @param float grandTotal (required) Items (products) amount in the Cart + */ + trackEcommerceCartUpdate: function (grandTotal) { + logEcommerceCartUpdate(grandTotal); + } + + }; + } + + /************************************************************ + * Proxy object + * - this allows the caller to continue push()'ing to _paq + * after the Tracker has been initialized and loaded + ************************************************************/ + + function TrackerProxy() { + return { + push: apply + }; + } + + /************************************************************ + * Constructor + ************************************************************/ + + // initialize the Piwik singleton + addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); + addReadyListener(); + + Date.prototype.getTimeAlias = Date.prototype.getTime; + + asyncTracker = new Tracker(); + + for (i = 0; i < _paq.length; i++) { + apply(_paq[i]); + } + + // replace initialization array with proxy object + _paq = new TrackerProxy(); + + /************************************************************ + * Public data and methods + ************************************************************/ + + return { + /** + * Add plugin + * + * @param string pluginName + * @param Object pluginObj + */ + addPlugin: function (pluginName, pluginObj) { + plugins[pluginName] = pluginObj; + }, + + /** + * Get Tracker (factory method) + * + * @param string piwikUrl + * @param int|string siteId + * @return Tracker + */ + getTracker: function (piwikUrl, siteId) { + return new Tracker(piwikUrl, siteId); + }, + + /** + * Get internal asynchronous tracker object + * + * @return Tracker + */ + getAsyncTracker: function () { + return asyncTracker; + } + }; + }()), + + /************************************************************ + * Deprecated functionality below + * - for legacy piwik.js compatibility + ************************************************************/ + + /* + * Piwik globals + * + * var piwik_install_tracker, piwik_tracker_pause, piwik_download_extensions, piwik_hosts_alias, piwik_ignore_classes; + */ + + piwik_track, + + /** + * Track page visit + * + * @param string documentTitle + * @param int|string siteId + * @param string piwikUrl + * @param mixed customData + */ + piwik_log = function (documentTitle, siteId, piwikUrl, customData) { + "use strict"; + + function getOption(optionName) { + try { + return eval('piwik_' + optionName); + } catch (e) { } + + return; // undefined + } + + // instantiate the tracker + var option, + piwikTracker = Piwik.getTracker(piwikUrl, siteId); + + // initialize tracker + piwikTracker.setDocumentTitle(documentTitle); + piwikTracker.setCustomData(customData); + + // handle Piwik globals + option = getOption('tracker_pause'); + if (option) { + piwikTracker.setLinkTrackingTimer(option); + } + option = getOption('download_extensions'); + if (option) { + piwikTracker.setDownloadExtensions(option); + } + option = getOption('hosts_alias'); + if (option) { + piwikTracker.setDomains(option); + } + option = getOption('ignore_classes'); + if (option) { + piwikTracker.setIgnoreClasses(option); + } + + // track this page view + piwikTracker.trackPageView(); + + // default is to install the link tracker + if (getOption('install_tracker')) { + + /** + * Track click manually (function is defined below) + * + * @param string sourceUrl + * @param int|string siteId + * @param string piwikUrl + * @param string linkType + */ + piwik_track = function (sourceUrl, siteId, piwikUrl, linkType) { + piwikTracker.setSiteId(siteId); + piwikTracker.setTrackerUrl(piwikUrl); + piwikTracker.trackLink(sourceUrl, linkType); + }; + + // set-up link tracking + piwikTracker.enableLinkTracking(); + } + }; -- GitLab