diff --git a/t/piwik/piwik.js b/t/piwik/piwik.js
new file mode 100644
index 00000000..bc97d40c
--- /dev/null
+++ b/t/piwik/piwik.js
@@ -0,0 +1,7871 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * JavaScript tracking client For PokeTube
+ *
+ * @link https://piwik.org
+ * @source https://github.com/matomo-org/matomo/blob/master/js/piwik.js
+ * @license https://piwik.org/free-software/bsd/ BSD-3 Clause (also in js/LICENSE.txt)
+ * @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
+ */
+// NOTE: if you change this above Piwik comment block, you must also change `$byteStart` in js/tracker.php
+
+// Refer to README.md for build instructions when minifying this file for distribution.
+
+/*
+ * Browser [In]Compatibility
+ * - minimum required ECMAScript: ECMA-262, edition 3
+ *
+ * Incompatible with these (and earlier) versions of:
+ * - IE4 - try..catch and for..in introduced in IE5
+ * - IE5 - named anonymous functions, array.push, encodeURIComponent, decodeURIComponent, and getElementsByTagName introduced in IE5.5
+ * - IE6 and 7 - window.JSON introduced in IE8
+ * - Firefox 1.0 and Netscape 8.x - FF1.5 adds array.indexOf, among other things
+ * - Mozilla 1.7 and Netscape 6.x-7.x
+ * - Netscape 4.8
+ * - Opera 6 - Error object (and Presto) introduced in Opera 7
+ * - Opera 7
+ */
+
+/* startjslint */
+/*jslint browser:true, plusplus:true, vars:true, nomen:true, evil:true, regexp: false, bitwise: true, white: true */
+/*global window */
+/*global unescape */
+/*global ActiveXObject */
+/*global Blob */
+/*members Piwik, Matomo, encodeURIComponent, decodeURIComponent, getElementsByTagName,
+ shift, unshift, piwikAsyncInit, matomoAsyncInit, matomoPluginAsyncInit , frameElement, self, hasFocus,
+ createElement, appendChild, characterSet, charset, all, piwik_log, AnalyticsTracker,
+ addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies, setCookieConsentGiven,
+ areCookiesEnabled, getRememberedCookieConsent, rememberCookieConsentGiven, forgetCookieConsentGiven, requireCookieConsent,
+ cookie, domain, readyState, documentElement, doScroll, title, text, contentWindow, postMessage,
+ location, top, onerror, document, referrer, parent, links, href, protocol, name,
+ performance, mozPerformance, msPerformance, webkitPerformance, timing, getEntriesByType, connectEnd, requestStart,
+ responseStart, responseEnd, fetchStart, domInteractive, domLoading, domComplete, loadEventStart, loadEventEnd,
+ event, which, button, srcElement, type, target, data,
+ parentNode, tagName, hostname, className,
+ userAgent, cookieEnabled, sendBeacon, platform, mimeTypes, enabledPlugin, javaEnabled,
+ userAgentData, getHighEntropyValues, brands, uaFullVersion, fullVersionList,
+ serviceWorker, ready, then, sync, register,
+ XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, send, readyState, status,
+ getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds,
+ toLowerCase, toUpperCase, charAt, indexOf, lastIndexOf, split, slice,
+ onload, src,
+ min, round, random, floor,
+ exec, success, trackerUrl, isSendBeacon, xhr,
+ res, width, height,
+ pdf, qt, realp, wma, fla, java, ag, showModalDialog,
+ _rcn, _rck, _refts, _ref,
+ maq_initial_value, maq_opted_in, maq_optout_by_default, maq_url,
+ initialized, hook, getHook, resetUserId, getVisitorId, getVisitorInfo, setUserId, getUserId, setSiteId, getSiteId, setTrackerUrl, getTrackerUrl, appendToTrackingUrl, getRequest, addPlugin,
+ getAttributionInfo, getAttributionCampaignName, getAttributionCampaignKeyword,
+ getAttributionReferrerTimestamp, getAttributionReferrerUrl,
+ setCustomData, getCustomData,
+ setCustomRequestProcessing,
+ setCustomVariable, getCustomVariable, deleteCustomVariable, storeCustomVariablesInCookie, setCustomDimension, getCustomDimension,
+ deleteCustomVariables, deleteCustomDimension, setDownloadExtensions, addDownloadExtensions, removeDownloadExtensions,
+ setDomains, setIgnoreClasses, setRequestMethod, setRequestContentType, setGenerationTimeMs, setPagePerformanceTiming,
+ setReferrerUrl, setCustomUrl, setAPIUrl, setDocumentTitle, setPageViewId, getPiwikUrl, getMatomoUrl, getCurrentUrl,
+ setExcludedReferrers, getExcludedReferrers,
+ setDownloadClasses, setLinkClasses,
+ setCampaignNameKey, setCampaignKeywordKey,
+ getConsentRequestsQueue, requireConsent, getRememberedConsent, hasRememberedConsent, isConsentRequired,
+ setConsentGiven, rememberConsentGiven, forgetConsentGiven, unload, hasConsent,
+ discardHashTag, alwaysUseSendBeacon, disableAlwaysUseSendBeacon, isUsingAlwaysUseSendBeacon,
+ setCookieNamePrefix, setCookieDomain, setCookiePath, setSecureCookie, setVisitorIdCookie, getCookieDomain, hasCookies, setSessionCookie,
+ setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout, getCookie, getCookiePath, getSessionCookieTimeout,
+ setExcludedQueryParams, setConversionAttributionFirstReferrer, tracker, request,
+ disablePerformanceTracking, maq_confirm_opted_in,
+ doNotTrack, setDoNotTrack, msDoNotTrack, getValuesFromVisitorIdCookie,
+ enableCrossDomainLinking, disableCrossDomainLinking, isCrossDomainLinkingEnabled, setCrossDomainLinkingTimeout, getCrossDomainLinkingUrlParameter,
+ addListener, enableLinkTracking, disableBrowserFeatureDetection, enableBrowserFeatureDetection, enableJSErrorTracking, setLinkTrackingTimer, getLinkTrackingTimer,
+ enableHeartBeatTimer, disableHeartBeatTimer, killFrame, redirectFile, setCountPreRendered, setVisitStandardLength,
+ trackGoal, trackLink, trackPageView, getNumTrackedPageViews, trackRequest, ping, queueRequest, trackSiteSearch, trackEvent,
+ requests, timeout, enabled, sendRequests, queueRequest, canQueue, pushMultiple, disableQueueRequest,setRequestQueueInterval,interval,getRequestQueue, getJavascriptErrors, unsetPageIsUnloading,
+ setEcommerceView, getEcommerceItems, addEcommerceItem, removeEcommerceItem, clearEcommerceCart, trackEcommerceOrder, trackEcommerceCartUpdate,
+ deleteCookie, deleteCookies, offsetTop, offsetLeft, offsetHeight, offsetWidth, nodeType, defaultView,
+ innerHTML, scrollLeft, scrollTop, currentStyle, getComputedStyle, querySelectorAll, splice,
+ getAttribute, hasAttribute, attributes, nodeName, findContentNodes, findContentNodes, findContentNodesWithinNode,
+ findPieceNode, findTargetNodeNoDefault, findTargetNode, findContentPiece, children, hasNodeCssClass,
+ getAttributeValueFromNode, hasNodeAttributeWithValue, hasNodeAttribute, findNodesByTagName, findMultiple,
+ makeNodesUnique, concat, find, htmlCollectionToArray, offsetParent, value, nodeValue, findNodesHavingAttribute,
+ findFirstNodeHavingAttribute, findFirstNodeHavingAttributeWithValue, getElementsByClassName,
+ findNodesHavingCssClass, findFirstNodeHavingClass, isLinkElement, findParentContentNode, removeDomainIfIsInLink,
+ findContentName, findMediaUrlInNode, toAbsoluteUrl, findContentTarget, getLocation, origin, host, isSameDomain,
+ search, trim, getBoundingClientRect, bottom, right, left, innerWidth, innerHeight, clientWidth, clientHeight,
+ isOrWasNodeInViewport, isNodeVisible, buildInteractionRequestParams, buildImpressionRequestParams,
+ shouldIgnoreInteraction, setHrefAttribute, setAttribute, buildContentBlock, collectContent, setLocation,
+ CONTENT_ATTR, CONTENT_CLASS, LEGACY_CONTENT_CLASS, CONTENT_NAME_ATTR, CONTENT_PIECE_ATTR, CONTENT_PIECE_CLASS, LEGACY_CONTENT_PIECE_CLASS,
+ CONTENT_TARGET_ATTR, CONTENT_TARGET_CLASS, LEGACY_CONTENT_TARGET_CLASS, CONTENT_IGNOREINTERACTION_ATTR, CONTENT_IGNOREINTERACTION_CLASS, LEGACY_CONTENT_IGNOREINTERACTION_CLASS,
+ trackCallbackOnLoad, trackCallbackOnReady, buildContentImpressionsRequests, wasContentImpressionAlreadyTracked,
+ getQuery, getContent, setVisitorId, getContentImpressionsRequestsFromNodes,
+ buildContentInteractionRequestNode, buildContentInteractionRequest, buildContentImpressionRequest,
+ appendContentInteractionToRequestIfPossible, setupInteractionsTracking, trackContentImpressionClickInteraction,
+ internalIsNodeVisible, clearTrackedContentImpressions, getTrackerUrl, trackAllContentImpressions,
+ getTrackedContentImpressions, getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet,
+ contentInteractionTrackingSetupDone, contains, match, pathname, piece, trackContentInteractionNode,
+ trackContentInteractionNode, trackContentImpressionsWithinNode, trackContentImpression,
+ enableTrackOnlyVisibleContent, trackContentInteraction, clearEnableTrackOnlyVisibleContent, logAllContentBlocksOnPage,
+ trackVisibleContentImpressions, isTrackOnlyVisibleContentEnabled, port, isUrlToCurrentDomain, matomoTrackers,
+ isNodeAuthorizedToTriggerInteraction, getConfigDownloadExtensions, disableLinkTracking,
+ substr, setAnyAttribute, max, abs, childNodes, compareDocumentPosition, body,
+ getConfigVisitorCookieTimeout, getRemainingVisitorCookieTimeout, getDomains, getConfigCookiePath,
+ getConfigCookieSameSite, getCustomPagePerformanceTiming, setCookieSameSite,
+ getConfigIdPageView, newVisitor, uuid, createTs, currentVisitTs,
+ "", "\b", "\t", "\n", "\f", "\r", "\"", "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace,
+ sort, slice, stringify, test, toJSON, toString, valueOf, objectToJSON, addTracker, removeAllAsyncTrackersButFirst,
+ optUserOut, forgetUserOptOut, isUserOptedOut, withCredentials, visibilityState
+ */
+/*global _paq:true */
+/*members push */
+/*global Piwik:true */
+/*global Matomo:true */
+/*members addPlugin, getTracker, getAsyncTracker, getAsyncTrackers, addTracker, trigger, on, off, retryMissedPluginCalls,
+ DOM, onLoad, onReady, isNodeVisible, isOrWasNodeVisible, JSON */
+/*global Matomo_Overlay_Client */
+/*global AnalyticsTracker:true */
+/*members initialize */
+/*global define */
+/*global console */
+/*members amd */
+/*members error */
+/*members log */
+
+// asynchronous tracker (or proxy)
+if (typeof _paq !== 'object') {
+ _paq = [];
+}
+
+// Matomo singleton and namespace
+if (typeof window.Matomo !== 'object') {
+ window.Matomo = window.Piwik = (function () {
+ 'use strict';
+
+ /************************************************************
+ * Private data
+ ************************************************************/
+
+ var expireDateTime,
+
+ /* plugins */
+ plugins = {},
+
+ eventHandlers = {},
+
+ /* alias frequently used globals for added minification */
+ documentAlias = document,
+ navigatorAlias = navigator,
+ screenAlias = screen,
+ windowAlias = window,
+
+ /* performance timing */
+ performanceAlias = windowAlias.performance || windowAlias.mozPerformance || windowAlias.msPerformance || windowAlias.webkitPerformance,
+
+ /* encode */
+ encodeWrapper = windowAlias.encodeURIComponent,
+
+ /* decode */
+ decodeWrapper = windowAlias.decodeURIComponent,
+
+ /* urldecode */
+ urldecode = unescape,
+
+ /* asynchronous tracker */
+ asyncTrackers = [],
+
+ /* iterator */
+ iterator,
+
+ /* local Matomo */
+ Matomo,
+
+ missedPluginTrackerCalls = [],
+
+ coreConsentCounter = 0,
+ coreHeartBeatCounter = 0,
+
+ trackerIdCounter = 0,
+
+ isPageUnloading = false;
+
+ /************************************************************
+ * Private methods
+ ************************************************************/
+
+ /**
+ * See https://github.com/matomo-org/matomo/issues/8413
+ * To prevent Javascript Error: Uncaught URIError: URI malformed when encoding is not UTF-8. Use this method
+ * instead of decodeWrapper if a text could contain any non UTF-8 encoded characters eg
+ * a URL like http://apache.matomo/test.html?%F6%E4%FC or a link like
+ * (encoded iso-8859-1 URL)
+ */
+ function safeDecodeWrapper(url)
+ {
+ try {
+ return decodeWrapper(url);
+ } catch (e) {
+ return unescape(url);
+ }
+ }
+
+ /*
+ * Is property defined?
+ */
+ function isDefined(property) {
+ // workaround https://github.com/douglascrockford/JSLint/commit/24f63ada2f9d7ad65afc90e6d949f631935c2480
+ var propertyType = typeof property;
+
+ return propertyType !== 'undefined';
+ }
+
+ /*
+ * 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;
+ }
+
+ /*
+ * Is property a string?
+ */
+ function isNumber(property) {
+ return typeof property === 'number' || property instanceof Number;
+ }
+
+ /*
+ * Is property a string?
+ */
+ function isNumberOrHasLength(property) {
+ return isDefined(property) && (isNumber(property) || (isString(property) && property.length));
+ }
+
+ function isObjectEmpty(property)
+ {
+ if (!property) {
+ return true;
+ }
+
+ var i;
+ for (i in property) {
+ if (Object.prototype.hasOwnProperty.call(property, i)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Logs an error in the console.
+ * Note: it does not generate a JavaScript error, so make sure to also generate an error if needed.
+ * @param message
+ */
+ function logConsoleError(message) {
+ // needed to write it this way for jslint
+ var consoleType = typeof console;
+ if (consoleType !== 'undefined' && console && console.error) {
+ console.error(message);
+ }
+ }
+
+ /*
+ * apply wrapper
+ *
+ * @param array parameterArray An array comprising either:
+ * [ 'methodName', optional_parameters ]
+ * or:
+ * [ functionObject, optional_parameters ]
+ */
+ function apply() {
+ var i, j, f, parameterArray, trackerCall;
+
+ for (i = 0; i < arguments.length; i += 1) {
+ trackerCall = null;
+ if (arguments[i] && arguments[i].slice) {
+ trackerCall = arguments[i].slice();
+ }
+ parameterArray = arguments[i];
+ f = parameterArray.shift();
+
+ var fParts, context;
+
+ var isStaticPluginCall = isString(f) && f.indexOf('::') > 0;
+ if (isStaticPluginCall) {
+ // a static method will not be called on a tracker and is not dependent on the existence of a
+ // tracker etc
+ fParts = f.split('::');
+ context = fParts[0];
+ f = fParts[1];
+
+ if ('object' === typeof Matomo[context] && 'function' === typeof Matomo[context][f]) {
+ Matomo[context][f].apply(Matomo[context], parameterArray);
+ } else if (trackerCall) {
+ // we try to call that method again later as the plugin might not be loaded yet
+ // a plugin can call "Matomo.retryMissedPluginCalls();" once it has been loaded and then the
+ // method call to "Matomo[context][f]" may be executed
+ missedPluginTrackerCalls.push(trackerCall);
+ }
+
+ } else {
+ for (j = 0; j < asyncTrackers.length; j++) {
+ if (isString(f)) {
+ context = asyncTrackers[j];
+
+ var isPluginTrackerCall = f.indexOf('.') > 0;
+
+ if (isPluginTrackerCall) {
+ fParts = f.split('.');
+ if (context && 'object' === typeof context[fParts[0]]) {
+ context = context[fParts[0]];
+ f = fParts[1];
+ } else if (trackerCall) {
+ // we try to call that method again later as the plugin might not be loaded yet
+ missedPluginTrackerCalls.push(trackerCall);
+ break;
+ }
+ }
+
+ if (context[f]) {
+ context[f].apply(context, parameterArray);
+ } else {
+ var message = 'The method \'' + f + '\' was not found in "_paq" variable. Please have a look at the Matomo tracker documentation: https://developer.matomo.org/api-reference/tracking-javascript';
+ logConsoleError(message);
+
+ if (!isPluginTrackerCall) {
+ // do not trigger an error if it is a call to a plugin as the plugin may just not be
+ // loaded yet etc
+ throw new TypeError(message);
+ }
+ }
+
+ if (f === 'addTracker') {
+ // addTracker adds an entry to asyncTrackers and would otherwise result in an endless loop
+ break;
+ }
+
+ if (f === 'setTrackerUrl' || f === 'setSiteId') {
+ // these two methods should be only executed on the first tracker
+ break;
+ }
+ } else {
+ f.apply(asyncTrackers[j], 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;
+ }
+
+ function trackCallbackOnLoad(callback)
+ {
+ if (documentAlias.readyState === 'complete') {
+ callback();
+ } else if (windowAlias.addEventListener) {
+ windowAlias.addEventListener('load', callback, false);
+ } else if (windowAlias.attachEvent) {
+ windowAlias.attachEvent('onload', callback);
+ }
+ }
+
+ function trackCallbackOnReady(callback)
+ {
+ var loaded = false;
+
+ if (documentAlias.attachEvent) {
+ loaded = documentAlias.readyState === 'complete';
+ } else {
+ loaded = documentAlias.readyState !== 'loading';
+ }
+
+ if (loaded) {
+ callback();
+ return;
+ }
+
+ var _timer;
+
+ if (documentAlias.addEventListener) {
+ addEventListener(documentAlias, 'DOMContentLoaded', function ready() {
+ documentAlias.removeEventListener('DOMContentLoaded', ready, false);
+ if (!loaded) {
+ loaded = true;
+ callback();
+ }
+ });
+ } else if (documentAlias.attachEvent) {
+ documentAlias.attachEvent('onreadystatechange', function ready() {
+ if (documentAlias.readyState === 'complete') {
+ documentAlias.detachEvent('onreadystatechange', ready);
+ if (!loaded) {
+ loaded = true;
+ callback();
+ }
+ }
+ });
+
+ if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) {
+ (function ready() {
+ if (!loaded) {
+ try {
+ documentAlias.documentElement.doScroll('left');
+ } catch (error) {
+ setTimeout(ready, 0);
+
+ return;
+ }
+ loaded = true;
+ callback();
+ }
+ }());
+ }
+ }
+
+ // fallback
+ addEventListener(windowAlias, 'load', function () {
+ if (!loaded) {
+ loaded = true;
+ callback();
+ }
+ }, false);
+ }
+
+ /*
+ * Call plugin hook methods
+ */
+ function executePluginMethod(methodName, params, callback) {
+ if (!methodName) {
+ return '';
+ }
+
+ var result = '',
+ i,
+ pluginMethod, value, isFunction;
+
+ for (i in plugins) {
+ if (Object.prototype.hasOwnProperty.call(plugins, i)) {
+ isFunction = plugins[i] && 'function' === typeof plugins[i][methodName];
+
+ if (isFunction) {
+ pluginMethod = plugins[i][methodName];
+ value = pluginMethod(params || {}, callback);
+
+ if (value) {
+ result += value;
+ }
+ }
+ }
+ }
+
+ 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(event) {
+ var now;
+ isPageUnloading = true;
+
+ executePluginMethod('unload');
+ now = new Date();
+ var aliasTime = now.getTimeAlias();
+ if ((expireDateTime - aliasTime) > 3000) {
+ expireDateTime = aliasTime + 3000;
+ }
+
+ /*
+ * 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);
+ }
+ }
+
+ /*
+ * Load JavaScript file (asynchronously)
+ */
+ function loadScript(src, onLoad) {
+ var script = documentAlias.createElement('script');
+
+ script.type = 'text/javascript';
+ script.src = src;
+
+ if (script.readyState) {
+ script.onreadystatechange = function () {
+ var state = this.readyState;
+
+ if (state === 'loaded' || state === 'complete') {
+ script.onreadystatechange = null;
+ onLoad();
+ }
+ };
+ } else {
+ script.onload = onLoad;
+ }
+
+ documentAlias.getElementsByTagName('head')[0].appendChild(script);
+ }
+
+ /*
+ * 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;
+ }
+ function isPositiveNumberString(str) {
+ // !isNaN(str) could be used but does not cover '03' (octal) and '0xA' (hex)
+ // nor negative numbers
+ return (/^[0-9][0-9]*(\.[0-9]+)?$/).test(str);
+ }
+ function filterIn(object, byFunction) {
+ var result = {}, k;
+ for (k in object) {
+ if (object.hasOwnProperty(k) && byFunction(object[k])) {
+ result[k] = object[k];
+ }
+ }
+ return result;
+ }
+ function onlyPositiveIntegers(data) {
+ var result = {}, k;
+ for (k in data) {
+ if (data.hasOwnProperty(k)) {
+ if (isPositiveNumberString(data[k])) {
+ result[k] = Math.round(data[k]);
+ } else {
+ throw new Error('Parameter "' + k + '" provided value "' + data[k] +
+ '" is not valid. Please provide a numeric value.');
+ }
+ }
+ }
+ return result;
+ }
+ function queryStringify(data) {
+ var queryString = '', k;
+ for (k in data) {
+ if (data.hasOwnProperty(k)) {
+ queryString += '&' + encodeWrapper(k) + '=' + encodeWrapper(data[k]);
+ }
+ }
+ return queryString;
+ }
+
+ function stringStartsWith(str, prefix) {
+ str = String(str);
+ return str.lastIndexOf(prefix, 0) === 0;
+ }
+
+ function stringEndsWith(str, suffix) {
+ str = String(str);
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+
+ function stringContains(str, needle) {
+ str = String(str);
+ return str.indexOf(needle) !== -1;
+ }
+
+ function removeCharactersFromEndOfString(str, numCharactersToRemove) {
+ str = String(str);
+ return str.substr(0, str.length - numCharactersToRemove);
+ }
+
+ /**
+ * We do not check whether URL contains already url parameter, please use removeUrlParameter() if needed
+ * before calling this method.
+ * This method makes sure to append URL parameters before a possible hash. Will escape (encode URI component)
+ * the set name and value
+ */
+ function addUrlParameter(url, name, value) {
+ url = String(url);
+
+ if (!value) {
+ value = '';
+ }
+
+ var hashPos = url.indexOf('#');
+ var urlLength = url.length;
+
+ if (hashPos === -1) {
+ hashPos = urlLength;
+ }
+
+ var baseUrl = url.substr(0, hashPos);
+ var urlHash = url.substr(hashPos, urlLength - hashPos);
+
+ if (baseUrl.indexOf('?') === -1) {
+ baseUrl += '?';
+ } else if (!stringEndsWith(baseUrl, '?')) {
+ baseUrl += '&';
+ }
+ // nothing to if ends with ?
+
+ return baseUrl + encodeWrapper(name) + '=' + encodeWrapper(value) + urlHash;
+ }
+
+ function removeUrlParameter(url, name) {
+ url = String(url);
+
+ if (url.indexOf('?' + name + '=') === -1 && url.indexOf('&' + name + '=') === -1) {
+ // nothing to remove, url does not contain this parameter
+ return url;
+ }
+
+ var searchPos = url.indexOf('?');
+ if (searchPos === -1) {
+ // nothing to remove, no query parameters
+ return url;
+ }
+
+ var queryString = url.substr(searchPos + 1);
+ var baseUrl = url.substr(0, searchPos);
+
+ if (queryString) {
+ var urlHash = '';
+ var hashPos = queryString.indexOf('#');
+ if (hashPos !== -1) {
+ urlHash = queryString.substr(hashPos + 1);
+ queryString = queryString.substr(0, hashPos);
+ }
+
+ var param;
+ var paramsArr = queryString.split('&');
+ var i = paramsArr.length - 1;
+
+ for (i; i >= 0; i--) {
+ param = paramsArr[i].split('=')[0];
+ if (param === name) {
+ paramsArr.splice(i, 1);
+ }
+ }
+
+ var newQueryString = paramsArr.join('&');
+
+ if (newQueryString) {
+ baseUrl = baseUrl + '?' + newQueryString;
+ }
+
+ if (urlHash) {
+ baseUrl += '#' + urlHash;
+ }
+ }
+
+ return baseUrl;
+ }
+
+ /*
+ * Extract parameter from URL
+ */
+ function getUrlParameter(url, name) {
+ var regexSearch = "[\\?]" + name + "=([^]*)";
+ var regex = new RegExp(regexSearch);
+ var results = regex.exec(url);
+ return results ? safeDecodeWrapper(results[1]) : '';
+ }
+
+ function trim(text)
+ {
+ if (text && String(text) === text) {
+ return text.replace(/^\s+|\s+$/g, '');
+ }
+
+ return text;
+ }
+
+ /*
+ * UTF-8 encoding
+ */
+ function utf8_encode(argString) {
+ return unescape(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 (https://matomo.org)
+
+ var
+ rotate_left = function (n, s) {
+ return (n << s) | (n >>> (32 - s));
+ },
+
+ cvt_hex = function (val) {
+ var strout = '',
+ i,
+ v;
+
+ for (i = 7; i >= 0; i--) {
+ v = (val >>> (i * 4)) & 0x0f;
+ strout += v.toString(16);
+ }
+
+ return strout;
+ },
+
+ 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) {
+ hostName = '';
+ }
+
+ if (!href) {
+ href = '';
+ }
+
+ if (hostName === 'translate.googleusercontent.com') { // Google
+ if (referrer === '') {
+ referrer = href;
+ }
+
+ href = getUrlParameter(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);
+ }
+
+ if (domain.indexOf('/') !== -1) {
+ domain = domain.substr(0, domain.indexOf('/'));
+ }
+
+ return domain;
+ }
+
+ /*
+ * Title fixup
+ */
+ function titleFixup(title) {
+ title = title && title.text ? title.text : title;
+
+ if (!isString(title)) {
+ var tmp = documentAlias.getElementsByTagName('title');
+
+ if (tmp && isDefined(tmp[0])) {
+ title = tmp[0].text;
+ }
+ }
+
+ return title;
+ }
+
+ function getChildrenFromNode(node)
+ {
+ if (!node) {
+ return [];
+ }
+
+ if (!isDefined(node.children) && isDefined(node.childNodes)) {
+ return node.children;
+ }
+
+ if (isDefined(node.children)) {
+ return node.children;
+ }
+
+ return [];
+ }
+
+ function containsNodeElement(node, containedNode)
+ {
+ if (!node || !containedNode) {
+ return false;
+ }
+
+ if (node.contains) {
+ return node.contains(containedNode);
+ }
+
+ if (node === containedNode) {
+ return true;
+ }
+
+ if (node.compareDocumentPosition) {
+ return !!(node.compareDocumentPosition(containedNode) & 16);
+ }
+
+ return false;
+ }
+
+ // Polyfill for IndexOf for IE6-IE8
+ function indexOfArray(theArray, searchElement)
+ {
+ if (theArray && theArray.indexOf) {
+ return theArray.indexOf(searchElement);
+ }
+
+ // 1. Let O be the result of calling ToObject passing
+ // the this value as the argument.
+ if (!isDefined(theArray) || theArray === null) {
+ return -1;
+ }
+
+ if (!theArray.length) {
+ return -1;
+ }
+
+ var len = theArray.length;
+
+ if (len === 0) {
+ return -1;
+ }
+
+ var k = 0;
+
+ // 9. Repeat, while k < len
+ while (k < len) {
+ // a. Let Pk be ToString(k).
+ // This is implicit for LHS operands of the in operator
+ // b. Let kPresent be the result of calling the
+ // HasProperty internal method of O with argument Pk.
+ // This step can be combined with c
+ // c. If kPresent is true, then
+ // i. Let elementK be the result of calling the Get
+ // internal method of O with the argument ToString(k).
+ // ii. Let same be the result of applying the
+ // Strict Equality Comparison Algorithm to
+ // searchElement and elementK.
+ // iii. If same is true, return k.
+ if (theArray[k] === searchElement) {
+ return k;
+ }
+ k++;
+ }
+ return -1;
+ }
+
+ /************************************************************
+ * Element Visiblility
+ ************************************************************/
+
+ /**
+ * Author: Jason Farrell
+ * Author URI: http://useallfive.com/
+ *
+ * Description: Checks if a DOM element is truly visible.
+ * Package URL: https://github.com/UseAllFive/true-visibility
+ * License: MIT (https://github.com/UseAllFive/true-visibility/blob/master/LICENSE.txt)
+ */
+ function isVisible(node) {
+
+ if (!node) {
+ return false;
+ }
+
+ //-- Cross browser method to get style properties:
+ function _getStyle(el, property) {
+ if (windowAlias.getComputedStyle) {
+ return documentAlias.defaultView.getComputedStyle(el,null)[property];
+ }
+ if (el.currentStyle) {
+ return el.currentStyle[property];
+ }
+ }
+
+ function _elementInDocument(element) {
+ element = element.parentNode;
+
+ while (element) {
+ if (element === documentAlias) {
+ return true;
+ }
+ element = element.parentNode;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if a DOM element is visible. Takes into
+ * consideration its parents and overflow.
+ *
+ * @param (el) the DOM element to check if is visible
+ *
+ * These params are optional that are sent in recursively,
+ * you typically won't use these:
+ *
+ * @param (t) Top corner position number
+ * @param (r) Right corner position number
+ * @param (b) Bottom corner position number
+ * @param (l) Left corner position number
+ * @param (w) Element width number
+ * @param (h) Element height number
+ */
+ function _isVisible(el, t, r, b, l, w, h) {
+ var p = el.parentNode,
+ VISIBLE_PADDING = 1; // has to be visible at least one px of the element
+
+ if (!_elementInDocument(el)) {
+ return false;
+ }
+
+ //-- Return true for document node
+ if (9 === p.nodeType) {
+ return true;
+ }
+
+ //-- Return false if our element is invisible
+ if (
+ '0' === _getStyle(el, 'opacity') ||
+ 'none' === _getStyle(el, 'display') ||
+ 'hidden' === _getStyle(el, 'visibility')
+ ) {
+ return false;
+ }
+
+ if (!isDefined(t) ||
+ !isDefined(r) ||
+ !isDefined(b) ||
+ !isDefined(l) ||
+ !isDefined(w) ||
+ !isDefined(h)) {
+ t = el.offsetTop;
+ l = el.offsetLeft;
+ b = t + el.offsetHeight;
+ r = l + el.offsetWidth;
+ w = el.offsetWidth;
+ h = el.offsetHeight;
+ }
+
+ if (node === el && (0 === h || 0 === w) && 'hidden' === _getStyle(el, 'overflow')) {
+ return false;
+ }
+
+ //-- If we have a parent, let's continue:
+ if (p) {
+ //-- Check if the parent can hide its children.
+ if (('hidden' === _getStyle(p, 'overflow') || 'scroll' === _getStyle(p, 'overflow'))) {
+ //-- Only check if the offset is different for the parent
+ if (
+ //-- If the target element is to the right of the parent elm
+ l + VISIBLE_PADDING > p.offsetWidth + p.scrollLeft ||
+ //-- If the target element is to the left of the parent elm
+ l + w - VISIBLE_PADDING < p.scrollLeft ||
+ //-- If the target element is under the parent elm
+ t + VISIBLE_PADDING > p.offsetHeight + p.scrollTop ||
+ //-- If the target element is above the parent elm
+ t + h - VISIBLE_PADDING < p.scrollTop
+ ) {
+ //-- Our target element is out of bounds:
+ return false;
+ }
+ }
+ //-- Add the offset parent's left/top coords to our element's offset:
+ if (el.offsetParent === p) {
+ l += p.offsetLeft;
+ t += p.offsetTop;
+ }
+ //-- Let's recursively check upwards:
+ return _isVisible(p, t, r, b, l, w, h);
+ }
+ return true;
+ }
+
+ return _isVisible(node);
+ }
+
+ /************************************************************
+ * Query
+ ************************************************************/
+
+ var query = {
+ htmlCollectionToArray: function (foundNodes)
+ {
+ var nodes = [], index;
+
+ if (!foundNodes || !foundNodes.length) {
+ return nodes;
+ }
+
+ for (index = 0; index < foundNodes.length; index++) {
+ nodes.push(foundNodes[index]);
+ }
+
+ return nodes;
+ },
+ find: function (selector)
+ {
+ // we use querySelectorAll only on document, not on nodes because of its unexpected behavior. See for
+ // instance http://stackoverflow.com/questions/11503534/jquery-vs-document-queryselectorall and
+ // http://jsfiddle.net/QdMc5/ and http://ejohn.org/blog/thoughts-on-queryselectorall
+ if (!document.querySelectorAll || !selector) {
+ return []; // we do not support all browsers
+ }
+
+ var foundNodes = document.querySelectorAll(selector);
+
+ return this.htmlCollectionToArray(foundNodes);
+ },
+ findMultiple: function (selectors)
+ {
+ if (!selectors || !selectors.length) {
+ return [];
+ }
+
+ var index, foundNodes;
+ var nodes = [];
+ for (index = 0; index < selectors.length; index++) {
+ foundNodes = this.find(selectors[index]);
+ nodes = nodes.concat(foundNodes);
+ }
+
+ nodes = this.makeNodesUnique(nodes);
+
+ return nodes;
+ },
+ findNodesByTagName: function (node, tagName)
+ {
+ if (!node || !tagName || !node.getElementsByTagName) {
+ return [];
+ }
+
+ var foundNodes = node.getElementsByTagName(tagName);
+
+ return this.htmlCollectionToArray(foundNodes);
+ },
+ makeNodesUnique: function (nodes)
+ {
+ var copy = [].concat(nodes);
+ nodes.sort(function(n1, n2){
+ if (n1 === n2) {
+ return 0;
+ }
+
+ var index1 = indexOfArray(copy, n1);
+ var index2 = indexOfArray(copy, n2);
+
+ if (index1 === index2) {
+ return 0;
+ }
+
+ return index1 > index2 ? -1 : 1;
+ });
+
+ if (nodes.length <= 1) {
+ return nodes;
+ }
+
+ var index = 0;
+ var numDuplicates = 0;
+ var duplicates = [];
+ var node;
+
+ node = nodes[index++];
+
+ while (node) {
+ if (node === nodes[index]) {
+ numDuplicates = duplicates.push(index);
+ }
+
+ node = nodes[index++] || null;
+ }
+
+ while (numDuplicates--) {
+ nodes.splice(duplicates[numDuplicates], 1);
+ }
+
+ return nodes;
+ },
+ getAttributeValueFromNode: function (node, attributeName)
+ {
+ if (!this.hasNodeAttribute(node, attributeName)) {
+ return;
+ }
+
+ if (node && node.getAttribute) {
+ return node.getAttribute(attributeName);
+ }
+
+ if (!node || !node.attributes) {
+ return;
+ }
+
+ var typeOfAttr = (typeof node.attributes[attributeName]);
+ if ('undefined' === typeOfAttr) {
+ return;
+ }
+
+ if (node.attributes[attributeName].value) {
+ return node.attributes[attributeName].value; // nodeValue is deprecated ie Chrome
+ }
+
+ if (node.attributes[attributeName].nodeValue) {
+ return node.attributes[attributeName].nodeValue;
+ }
+
+ var index;
+ var attrs = node.attributes;
+
+ if (!attrs) {
+ return;
+ }
+
+ for (index = 0; index < attrs.length; index++) {
+ if (attrs[index].nodeName === attributeName) {
+ return attrs[index].nodeValue;
+ }
+ }
+
+ return null;
+ },
+ hasNodeAttributeWithValue: function (node, attributeName)
+ {
+ var value = this.getAttributeValueFromNode(node, attributeName);
+
+ return !!value;
+ },
+ hasNodeAttribute: function (node, attributeName)
+ {
+ if (node && node.hasAttribute) {
+ return node.hasAttribute(attributeName);
+ }
+
+ if (node && node.attributes) {
+ var typeOfAttr = (typeof node.attributes[attributeName]);
+ return 'undefined' !== typeOfAttr;
+ }
+
+ return false;
+ },
+ hasNodeCssClass: function (node, klassName)
+ {
+ if (node && klassName && node.className) {
+ var classes = typeof node.className === "string" ? node.className.split(' ') : [];
+ if (-1 !== indexOfArray(classes, klassName)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+ findNodesHavingAttribute: function (nodeToSearch, attributeName, nodes)
+ {
+ if (!nodes) {
+ nodes = [];
+ }
+
+ if (!nodeToSearch || !attributeName) {
+ return nodes;
+ }
+
+ var children = getChildrenFromNode(nodeToSearch);
+
+ if (!children || !children.length) {
+ return nodes;
+ }
+
+ var index, child;
+ for (index = 0; index < children.length; index++) {
+ child = children[index];
+ if (this.hasNodeAttribute(child, attributeName)) {
+ nodes.push(child);
+ }
+
+ nodes = this.findNodesHavingAttribute(child, attributeName, nodes);
+ }
+
+ return nodes;
+ },
+ findFirstNodeHavingAttribute: function (node, attributeName)
+ {
+ if (!node || !attributeName) {
+ return;
+ }
+
+ if (this.hasNodeAttribute(node, attributeName)) {
+ return node;
+ }
+
+ var nodes = this.findNodesHavingAttribute(node, attributeName);
+
+ if (nodes && nodes.length) {
+ return nodes[0];
+ }
+ },
+ findFirstNodeHavingAttributeWithValue: function (node, attributeName)
+ {
+ if (!node || !attributeName) {
+ return;
+ }
+
+ if (this.hasNodeAttributeWithValue(node, attributeName)) {
+ return node;
+ }
+
+ var nodes = this.findNodesHavingAttribute(node, attributeName);
+
+ if (!nodes || !nodes.length) {
+ return;
+ }
+
+ var index;
+ for (index = 0; index < nodes.length; index++) {
+ if (this.getAttributeValueFromNode(nodes[index], attributeName)) {
+ return nodes[index];
+ }
+ }
+ },
+ findNodesHavingCssClass: function (nodeToSearch, className, nodes)
+ {
+ if (!nodes) {
+ nodes = [];
+ }
+
+ if (!nodeToSearch || !className) {
+ return nodes;
+ }
+
+ if (nodeToSearch.getElementsByClassName) {
+ var foundNodes = nodeToSearch.getElementsByClassName(className);
+ return this.htmlCollectionToArray(foundNodes);
+ }
+
+ var children = getChildrenFromNode(nodeToSearch);
+
+ if (!children || !children.length) {
+ return [];
+ }
+
+ var index, child;
+ for (index = 0; index < children.length; index++) {
+ child = children[index];
+ if (this.hasNodeCssClass(child, className)) {
+ nodes.push(child);
+ }
+
+ nodes = this.findNodesHavingCssClass(child, className, nodes);
+ }
+
+ return nodes;
+ },
+ findFirstNodeHavingClass: function (node, className)
+ {
+ if (!node || !className) {
+ return;
+ }
+
+ if (this.hasNodeCssClass(node, className)) {
+ return node;
+ }
+
+ var nodes = this.findNodesHavingCssClass(node, className);
+
+ if (nodes && nodes.length) {
+ return nodes[0];
+ }
+ },
+ isLinkElement: function (node)
+ {
+ if (!node) {
+ return false;
+ }
+
+ var elementName = String(node.nodeName).toLowerCase();
+ var linkElementNames = ['a', 'area'];
+ var pos = indexOfArray(linkElementNames, elementName);
+
+ return pos !== -1;
+ },
+ setAnyAttribute: function (node, attrName, attrValue)
+ {
+ if (!node || !attrName) {
+ return;
+ }
+
+ if (node.setAttribute) {
+ node.setAttribute(attrName, attrValue);
+ } else {
+ node[attrName] = attrValue;
+ }
+ }
+ };
+
+ /************************************************************
+ * Content Tracking
+ ************************************************************/
+
+ var content = {
+ CONTENT_ATTR: 'data-track-content',
+ CONTENT_CLASS: 'matomoTrackContent',
+ LEGACY_CONTENT_CLASS: 'piwikTrackContent',
+ CONTENT_NAME_ATTR: 'data-content-name',
+ CONTENT_PIECE_ATTR: 'data-content-piece',
+ CONTENT_PIECE_CLASS: 'matomoContentPiece',
+ LEGACY_CONTENT_PIECE_CLASS: 'piwikContentPiece',
+ CONTENT_TARGET_ATTR: 'data-content-target',
+ CONTENT_TARGET_CLASS: 'matomoContentTarget',
+ LEGACY_CONTENT_TARGET_CLASS: 'piwikContentTarget',
+ CONTENT_IGNOREINTERACTION_ATTR: 'data-content-ignoreinteraction',
+ CONTENT_IGNOREINTERACTION_CLASS: 'matomoContentIgnoreInteraction',
+ LEGACY_CONTENT_IGNOREINTERACTION_CLASS: 'piwikContentIgnoreInteraction',
+ location: undefined,
+
+ findContentNodes: function ()
+ {
+ var cssSelector = '.' + this.CONTENT_CLASS;
+ var cssSelector2 = '.' + this.LEGACY_CONTENT_CLASS;
+ var attrSelector = '[' + this.CONTENT_ATTR + ']';
+ var contentNodes = query.findMultiple([cssSelector, cssSelector2, attrSelector]);
+
+ return contentNodes;
+ },
+ findContentNodesWithinNode: function (node)
+ {
+ if (!node) {
+ return [];
+ }
+
+ // NOTE: we do not use query.findMultiple here as querySelectorAll would most likely not deliver the result we want
+
+ var nodes1 = query.findNodesHavingCssClass(node, this.CONTENT_CLASS);
+ nodes1 = query.findNodesHavingCssClass(node, this.LEGACY_CONTENT_CLASS, nodes1);
+ var nodes2 = query.findNodesHavingAttribute(node, this.CONTENT_ATTR);
+
+ if (nodes2 && nodes2.length) {
+ var index;
+ for (index = 0; index < nodes2.length; index++) {
+ nodes1.push(nodes2[index]);
+ }
+ }
+
+ if (query.hasNodeAttribute(node, this.CONTENT_ATTR)) {
+ nodes1.push(node);
+ } else if (query.hasNodeCssClass(node, this.CONTENT_CLASS)) {
+ nodes1.push(node);
+ } else if (query.hasNodeCssClass(node, this.LEGACY_CONTENT_CLASS)) {
+ nodes1.push(node);
+ }
+
+ nodes1 = query.makeNodesUnique(nodes1);
+
+ return nodes1;
+ },
+ findParentContentNode: function (anyNode)
+ {
+ if (!anyNode) {
+ return;
+ }
+
+ var node = anyNode;
+ var counter = 0;
+
+ while (node && node !== documentAlias && node.parentNode) {
+ if (query.hasNodeAttribute(node, this.CONTENT_ATTR)) {
+ return node;
+ }
+ if (query.hasNodeCssClass(node, this.CONTENT_CLASS)) {
+ return node;
+ }
+ if (query.hasNodeCssClass(node, this.LEGACY_CONTENT_CLASS)) {
+ return node;
+ }
+
+ node = node.parentNode;
+
+ if (counter > 1000) {
+ break; // prevent loop, should not happen anyway but better we do this
+ }
+ counter++;
+ }
+ },
+ findPieceNode: function (node)
+ {
+ var contentPiece;
+
+ contentPiece = query.findFirstNodeHavingAttribute(node, this.CONTENT_PIECE_ATTR);
+
+ if (!contentPiece) {
+ contentPiece = query.findFirstNodeHavingClass(node, this.CONTENT_PIECE_CLASS);
+ }
+ if (!contentPiece) {
+ contentPiece = query.findFirstNodeHavingClass(node, this.LEGACY_CONTENT_PIECE_CLASS);
+ }
+
+ if (contentPiece) {
+ return contentPiece;
+ }
+
+ return node;
+ },
+ findTargetNodeNoDefault: function (node)
+ {
+ if (!node) {
+ return;
+ }
+
+ var target = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_TARGET_ATTR);
+ if (target) {
+ return target;
+ }
+
+ target = query.findFirstNodeHavingAttribute(node, this.CONTENT_TARGET_ATTR);
+ if (target) {
+ return target;
+ }
+
+ target = query.findFirstNodeHavingClass(node, this.CONTENT_TARGET_CLASS);
+ if (target) {
+ return target;
+ }
+
+ target = query.findFirstNodeHavingClass(node, this.LEGACY_CONTENT_TARGET_CLASS);
+ if (target) {
+ return target;
+ }
+ },
+ findTargetNode: function (node)
+ {
+ var target = this.findTargetNodeNoDefault(node);
+ if (target) {
+ return target;
+ }
+
+ return node;
+ },
+ findContentName: function (node)
+ {
+ if (!node) {
+ return;
+ }
+
+ var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_NAME_ATTR);
+
+ if (nameNode) {
+ return query.getAttributeValueFromNode(nameNode, this.CONTENT_NAME_ATTR);
+ }
+
+ var contentPiece = this.findContentPiece(node);
+ if (contentPiece) {
+ return this.removeDomainIfIsInLink(contentPiece);
+ }
+
+ if (query.hasNodeAttributeWithValue(node, 'title')) {
+ return query.getAttributeValueFromNode(node, 'title');
+ }
+
+ var clickUrlNode = this.findPieceNode(node);
+
+ if (query.hasNodeAttributeWithValue(clickUrlNode, 'title')) {
+ return query.getAttributeValueFromNode(clickUrlNode, 'title');
+ }
+
+ var targetNode = this.findTargetNode(node);
+
+ if (query.hasNodeAttributeWithValue(targetNode, 'title')) {
+ return query.getAttributeValueFromNode(targetNode, 'title');
+ }
+ },
+ findContentPiece: function (node)
+ {
+ if (!node) {
+ return;
+ }
+
+ var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_PIECE_ATTR);
+
+ if (nameNode) {
+ return query.getAttributeValueFromNode(nameNode, this.CONTENT_PIECE_ATTR);
+ }
+
+ var contentNode = this.findPieceNode(node);
+
+ var media = this.findMediaUrlInNode(contentNode);
+ if (media) {
+ return this.toAbsoluteUrl(media);
+ }
+ },
+ findContentTarget: function (node)
+ {
+ if (!node) {
+ return;
+ }
+
+ var targetNode = this.findTargetNode(node);
+
+ if (query.hasNodeAttributeWithValue(targetNode, this.CONTENT_TARGET_ATTR)) {
+ return query.getAttributeValueFromNode(targetNode, this.CONTENT_TARGET_ATTR);
+ }
+
+ var href;
+ if (query.hasNodeAttributeWithValue(targetNode, 'href')) {
+ href = query.getAttributeValueFromNode(targetNode, 'href');
+ return this.toAbsoluteUrl(href);
+ }
+
+ var contentNode = this.findPieceNode(node);
+
+ if (query.hasNodeAttributeWithValue(contentNode, 'href')) {
+ href = query.getAttributeValueFromNode(contentNode, 'href');
+ return this.toAbsoluteUrl(href);
+ }
+ },
+ isSameDomain: function (url)
+ {
+ if (!url || !url.indexOf) {
+ return false;
+ }
+
+ if (0 === url.indexOf(this.getLocation().origin)) {
+ return true;
+ }
+
+ var posHost = url.indexOf(this.getLocation().host);
+ if (8 >= posHost && 0 <= posHost) {
+ return true;
+ }
+
+ return false;
+ },
+ removeDomainIfIsInLink: function (text)
+ {
+ // we will only remove if domain === location.origin meaning is not an outlink
+ var regexContainsProtocol = '^https?:\/\/[^\/]+';
+ var regexReplaceDomain = '^.*\/\/[^\/]+';
+
+ if (text &&
+ text.search &&
+ -1 !== text.search(new RegExp(regexContainsProtocol))
+ && this.isSameDomain(text)) {
+
+ text = text.replace(new RegExp(regexReplaceDomain), '');
+ if (!text) {
+ text = '/';
+ }
+ }
+
+ return text;
+ },
+ findMediaUrlInNode: function (node)
+ {
+ if (!node) {
+ return;
+ }
+
+ var mediaElements = ['img', 'embed', 'video', 'audio'];
+ var elementName = node.nodeName.toLowerCase();
+
+ if (-1 !== indexOfArray(mediaElements, elementName) &&
+ query.findFirstNodeHavingAttributeWithValue(node, 'src')) {
+
+ var sourceNode = query.findFirstNodeHavingAttributeWithValue(node, 'src');
+
+ return query.getAttributeValueFromNode(sourceNode, 'src');
+ }
+
+ if (elementName === 'object' &&
+ query.hasNodeAttributeWithValue(node, 'data')) {
+
+ return query.getAttributeValueFromNode(node, 'data');
+ }
+
+ if (elementName === 'object') {
+ var params = query.findNodesByTagName(node, 'param');
+ if (params && params.length) {
+ var index;
+ for (index = 0; index < params.length; index++) {
+ if ('movie' === query.getAttributeValueFromNode(params[index], 'name') &&
+ query.hasNodeAttributeWithValue(params[index], 'value')) {
+
+ return query.getAttributeValueFromNode(params[index], 'value');
+ }
+ }
+ }
+
+ var embed = query.findNodesByTagName(node, 'embed');
+ if (embed && embed.length) {
+ return this.findMediaUrlInNode(embed[0]);
+ }
+ }
+ },
+ trim: function (text)
+ {
+ return trim(text);
+ },
+ isOrWasNodeInViewport: function (node)
+ {
+ if (!node || !node.getBoundingClientRect || node.nodeType !== 1) {
+ return true;
+ }
+
+ var rect = node.getBoundingClientRect();
+ var html = documentAlias.documentElement || {};
+
+ var wasVisible = rect.top < 0;
+ if (wasVisible && node.offsetTop) {
+ wasVisible = (node.offsetTop + rect.height) > 0;
+ }
+
+ var docWidth = html.clientWidth; // The clientWidth attribute returns the viewport width excluding the size of a rendered scroll bar
+
+ if (windowAlias.innerWidth && docWidth > windowAlias.innerWidth) {
+ docWidth = windowAlias.innerWidth; // The innerWidth attribute must return the viewport width including the size of a rendered scroll bar
+ }
+
+ var docHeight = html.clientHeight; // The clientWidth attribute returns the viewport width excluding the size of a rendered scroll bar
+
+ if (windowAlias.innerHeight && docHeight > windowAlias.innerHeight) {
+ docHeight = windowAlias.innerHeight; // The innerWidth attribute must return the viewport width including the size of a rendered scroll bar
+ }
+
+ return (
+ (rect.bottom > 0 || wasVisible) &&
+ rect.right > 0 &&
+ rect.left < docWidth &&
+ ((rect.top < docHeight) || wasVisible) // rect.top < 0 we assume user has seen all the ones that are above the current viewport
+ );
+ },
+ isNodeVisible: function (node)
+ {
+ var isItVisible = isVisible(node);
+ var isInViewport = this.isOrWasNodeInViewport(node);
+ return isItVisible && isInViewport;
+ },
+ buildInteractionRequestParams: function (interaction, name, piece, target)
+ {
+ var params = '';
+
+ if (interaction) {
+ params += 'c_i='+ encodeWrapper(interaction);
+ }
+ if (name) {
+ if (params) {
+ params += '&';
+ }
+ params += 'c_n='+ encodeWrapper(name);
+ }
+ if (piece) {
+ if (params) {
+ params += '&';
+ }
+ params += 'c_p='+ encodeWrapper(piece);
+ }
+ if (target) {
+ if (params) {
+ params += '&';
+ }
+ params += 'c_t='+ encodeWrapper(target);
+ }
+
+ if (params) {
+ params += '&ca=1';
+ }
+
+ return params;
+ },
+ buildImpressionRequestParams: function (name, piece, target)
+ {
+ var params = 'c_n=' + encodeWrapper(name) +
+ '&c_p=' + encodeWrapper(piece);
+
+ if (target) {
+ params += '&c_t=' + encodeWrapper(target);
+ }
+
+ if (params) {
+ params += '&ca=1';
+ }
+
+ return params;
+ },
+ buildContentBlock: function (node)
+ {
+ if (!node) {
+ return;
+ }
+
+ var name = this.findContentName(node);
+ var piece = this.findContentPiece(node);
+ var target = this.findContentTarget(node);
+
+ name = this.trim(name);
+ piece = this.trim(piece);
+ target = this.trim(target);
+
+ return {
+ name: name || 'Unknown',
+ piece: piece || 'Unknown',
+ target: target || ''
+ };
+ },
+ collectContent: function (contentNodes)
+ {
+ if (!contentNodes || !contentNodes.length) {
+ return [];
+ }
+
+ var contents = [];
+
+ var index, contentBlock;
+ for (index = 0; index < contentNodes.length; index++) {
+ contentBlock = this.buildContentBlock(contentNodes[index]);
+ if (isDefined(contentBlock)) {
+ contents.push(contentBlock);
+ }
+ }
+
+ return contents;
+ },
+ setLocation: function (location)
+ {
+ this.location = location;
+ },
+ getLocation: function ()
+ {
+ var locationAlias = this.location || windowAlias.location;
+
+ if (!locationAlias.origin) {
+ locationAlias.origin = locationAlias.protocol + "//" + locationAlias.hostname + (locationAlias.port ? ':' + locationAlias.port: '');
+ }
+
+ return locationAlias;
+ },
+ toAbsoluteUrl: function (url)
+ {
+ if ((!url || String(url) !== url) && url !== '') {
+ // we only handle strings
+ return url;
+ }
+
+ if ('' === url) {
+ return this.getLocation().href;
+ }
+
+ // Eg //example.com/test.jpg
+ if (url.search(/^\/\//) !== -1) {
+ return this.getLocation().protocol + url;
+ }
+
+ // Eg http://example.com/test.jpg
+ if (url.search(/:\/\//) !== -1) {
+ return url;
+ }
+
+ // Eg #test.jpg
+ if (0 === url.indexOf('#')) {
+ return this.getLocation().origin + this.getLocation().pathname + url;
+ }
+
+ // Eg ?x=5
+ if (0 === url.indexOf('?')) {
+ return this.getLocation().origin + this.getLocation().pathname + url;
+ }
+
+ // Eg mailto:x@y.z tel:012345, ... market:... sms:..., javascript:... ecmascript: ... and many more
+ if (0 === url.search('^[a-zA-Z]{2,11}:')) {
+ return url;
+ }
+
+ // Eg /test.jpg
+ if (url.search(/^\//) !== -1) {
+ return this.getLocation().origin + url;
+ }
+
+ // Eg test.jpg
+ var regexMatchDir = '(.*\/)';
+ var base = this.getLocation().origin + this.getLocation().pathname.match(new RegExp(regexMatchDir))[0];
+ return base + url;
+ },
+ isUrlToCurrentDomain: function (url) {
+
+ var absoluteUrl = this.toAbsoluteUrl(url);
+
+ if (!absoluteUrl) {
+ return false;
+ }
+
+ var origin = this.getLocation().origin;
+ if (origin === absoluteUrl) {
+ return true;
+ }
+
+ if (0 === String(absoluteUrl).indexOf(origin)) {
+ if (':' === String(absoluteUrl).substr(origin.length, 1)) {
+ return false; // url has port whereas origin has not => different URL
+ }
+
+ return true;
+ }
+
+ return false;
+ },
+ setHrefAttribute: function (node, url)
+ {
+ if (!node || !url) {
+ return;
+ }
+
+ query.setAnyAttribute(node, 'href', url);
+ },
+ shouldIgnoreInteraction: function (targetNode)
+ {
+ if (query.hasNodeAttribute(targetNode, this.CONTENT_IGNOREINTERACTION_ATTR)) {
+ return true;
+ }
+ if (query.hasNodeCssClass(targetNode, this.CONTENT_IGNOREINTERACTION_CLASS)) {
+ return true;
+ }
+ if (query.hasNodeCssClass(targetNode, this.LEGACY_CONTENT_IGNOREINTERACTION_CLASS)) {
+ return true;
+ }
+ return false;
+ }
+ };
+
+ /************************************************************
+ * Page Overlay
+ ************************************************************/
+
+ function getMatomoUrlForOverlay(trackerUrl, apiUrl) {
+ if (apiUrl) {
+ return apiUrl;
+ }
+
+ trackerUrl = content.toAbsoluteUrl(trackerUrl);
+
+ // if eg http://www.example.com/js/tracker.php?version=232323 => http://www.example.com/js/tracker.php
+ if (stringContains(trackerUrl, '?')) {
+ var posQuery = trackerUrl.indexOf('?');
+ trackerUrl = trackerUrl.slice(0, posQuery);
+ }
+
+ if (stringEndsWith(trackerUrl, 'matomo.php')) {
+ // if eg without domain or path "matomo.php" => ''
+ trackerUrl = removeCharactersFromEndOfString(trackerUrl, 'matomo.php'.length);
+ } else if (stringEndsWith(trackerUrl, 'piwik.php')) {
+ // if eg without domain or path "piwik.php" => ''
+ trackerUrl = removeCharactersFromEndOfString(trackerUrl, 'piwik.php'.length);
+ } else if (stringEndsWith(trackerUrl, '.php')) {
+ // if eg http://www.example.com/js/matomo.php => http://www.example.com/js/
+ // or if eg http://www.example.com/tracker.php => http://www.example.com/
+ var lastSlash = trackerUrl.lastIndexOf('/');
+ var includeLastSlash = 1;
+ trackerUrl = trackerUrl.slice(0, lastSlash + includeLastSlash);
+ }
+
+ // if eg http://www.example.com/js/ => http://www.example.com/ (when not minified Matomo JS loaded)
+ if (stringEndsWith(trackerUrl, '/js/')) {
+ trackerUrl = removeCharactersFromEndOfString(trackerUrl, 'js/'.length);
+ }
+
+ // http://www.example.com/
+ return trackerUrl;
+ }
+
+ /*
+ * Check whether this is a page overlay session
+ *
+ * @return boolean
+ *
+ * {@internal side-effect: modifies window.name }}
+ */
+ function isOverlaySession(configTrackerSiteId) {
+ var windowName = 'Matomo_Overlay';
+
+ // check whether we were redirected from the matomo overlay plugin
+ var referrerRegExp = new RegExp('index\\.php\\?module=Overlay&action=startOverlaySession'
+ + '&idSite=([0-9]+)&period=([^&]+)&date=([^&]+)(&segment=[^&]*)?');
+
+ var match = referrerRegExp.exec(documentAlias.referrer);
+
+ if (match) {
+ // check idsite
+ var idsite = match[1];
+
+ if (idsite !== String(configTrackerSiteId)) {
+ return false;
+ }
+
+ // store overlay session info in window name
+ var period = match[2],
+ date = match[3],
+ segment = match[4];
+
+ if (!segment) {
+ segment = '';
+ } else if (segment.indexOf('&segment=') === 0) {
+ segment = segment.substr('&segment='.length);
+ }
+
+ windowAlias.name = windowName + '###' + period + '###' + date + '###' + segment;
+ }
+
+ // retrieve and check data from window name
+ var windowNameParts = windowAlias.name.split('###');
+
+ return windowNameParts.length === 4 && windowNameParts[0] === windowName;
+ }
+
+ /*
+ * Inject the script needed for page overlay
+ */
+ function injectOverlayScripts(configTrackerUrl, configApiUrl, configTrackerSiteId) {
+ var windowNameParts = windowAlias.name.split('###'),
+ period = windowNameParts[1],
+ date = windowNameParts[2],
+ segment = windowNameParts[3],
+ matomoUrl = getMatomoUrlForOverlay(configTrackerUrl, configApiUrl);
+
+ loadScript(
+ matomoUrl + 'plugins/Overlay/client/client.js?v=1',
+ function () {
+ Matomo_Overlay_Client.initialize(matomoUrl, configTrackerSiteId, period, date, segment);
+ }
+ );
+ }
+
+ function isInsideAnIframe () {
+ var frameElement;
+
+ try {
+ // If the parent window has another origin, then accessing frameElement
+ // throws an Error in IE. see issue #10105.
+ frameElement = windowAlias.frameElement;
+ } catch(e) {
+ // When there was an Error, then we know we are inside an iframe.
+ return true;
+ }
+
+ if (isDefined(frameElement)) {
+ return (frameElement && String(frameElement.nodeName).toLowerCase() === 'iframe') ? true : false;
+ }
+
+ try {
+ return windowAlias.self !== windowAlias.top;
+ } catch (e2) {
+ return true;
+ }
+ }
+
+ /************************************************************
+ * End Page Overlay
+ ************************************************************/
+
+ /*
+ * Matomo Tracker class
+ *
+ * trackerUrl and trackerSiteId are optional arguments to the constructor
+ *
+ * See: Tracker.setTrackerUrl() and Tracker.setSiteId()
+ */
+ function Tracker(trackerUrl, siteId) {
+
+ /************************************************************
+ * Private members
+ ************************************************************/
+
+ var
+ /*configHasConsent
value. Ensures that any
+ * change to the user opt-in/out status in another browser window will be respected.
+ */
+ function refreshConsentStatus() {
+ if (getCookie(CONSENT_REMOVED_COOKIE_NAME)) {
+ configHasConsent = false;
+ } else if (getCookie(CONSENT_COOKIE_NAME)) {
+ configHasConsent = true;
+ }
+ }
+
+ function injectClientHints(request) {
+ if (!clientHints) {
+ return request;
+ }
+
+ var i, appendix = '&uadata=' + encodeWrapper(windowAlias.JSON.stringify(clientHints));
+
+ if (request instanceof Array) {
+ for (i = 0; i < request.length; i++) {
+ request[i] += appendix;
+ }
+ } else {
+ request += appendix;
+ }
+
+ return request;
+ }
+
+ function detectClientHints (callback) {
+ if (!configBrowserFeatureDetection || !isDefined(navigatorAlias.userAgentData) || !isFunction(navigatorAlias.userAgentData.getHighEntropyValues)) {
+ callback();
+ return;
+ }
+
+ // Initialize with low entropy values that are always available
+ clientHints = {
+ brands: navigatorAlias.userAgentData.brands,
+ platform: navigatorAlias.userAgentData.platform
+ };
+
+ // try to gather high entropy values
+ // currently this methods simply returns the requested values through a Promise
+ // In later versions it might require a user permission
+ navigatorAlias.userAgentData.getHighEntropyValues(
+ ['brands', 'model', 'platform', 'platformVersion', 'uaFullVersion', 'fullVersionList']
+ ).then(function(ua) {
+ var i;
+ if (ua.fullVersionList) {
+ // if fullVersionList is available, brands and uaFullVersion isn't needed
+ delete ua.brands;
+ delete ua.uaFullVersion;
+ }
+
+ clientHints = ua;
+ callback();
+ }, function (message) {
+ callback();
+ });
+ }
+
+ /*
+ * Send request
+ */
+ function sendRequest(request, delay, callback) {
+ if (!clientHintsResolved) {
+ clientHintsRequestQueue.push(request);
+ return;
+ }
+
+ refreshConsentStatus();
+ if (!configHasConsent) {
+ consentRequestsQueue.push(request);
+ return;
+ }
+
+ hasSentTrackingRequestYet = true;
+
+ if (!configDoNotTrack && request) {
+ if (configConsentRequired && configHasConsent) { // send a consent=1 when explicit consent is given for the apache logs
+ request += '&consent=1';
+ }
+
+ request = injectClientHints(request);
+
+ makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () {
+ if (configAlwaysUseSendBeacon && sendPostRequestViaSendBeacon(request, callback, true)) {
+ setExpireDateTime(100);
+ return;
+ }
+
+ if (shouldForcePost(request)) {
+ sendXmlHttpRequest(request, callback);
+ } else {
+ getImage(request, callback);
+ }
+
+ setExpireDateTime(delay);
+ });
+ }
+ if (!heartBeatSetUp) {
+ setUpHeartBeat(); // setup window events too, but only once
+ }
+ }
+
+ function canSendBulkRequest(requests)
+ {
+ if (configDoNotTrack) {
+ return false;
+ }
+
+ return (requests && requests.length);
+ }
+
+ function arrayChunk(theArray, chunkSize)
+ {
+ if (!chunkSize || chunkSize >= theArray.length) {
+ return [theArray];
+ }
+
+ var index = 0;
+ var arrLength = theArray.length;
+ var chunks = [];
+
+ for (index; index < arrLength; index += chunkSize) {
+ chunks.push(theArray.slice(index, index + chunkSize));
+ }
+
+ return chunks;
+ }
+
+ /*
+ * Send requests using bulk
+ */
+ function sendBulkRequest(requests, delay)
+ {
+ if (!canSendBulkRequest(requests)) {
+ return;
+ }
+
+ if (!clientHintsResolved) {
+ clientHintsRequestQueue.push(requests);
+ return;
+ }
+
+ if (!configHasConsent) {
+ consentRequestsQueue.push(requests);
+ return;
+ }
+
+ hasSentTrackingRequestYet = true;
+
+ makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () {
+ var chunks = arrayChunk(requests, 50);
+
+ var i = 0, bulk;
+ for (i; i < chunks.length; i++) {
+ bulk = '{"requests":["?' + injectClientHints(chunks[i]).join('","?') + '"],"send_image":0}';
+ if (configAlwaysUseSendBeacon && sendPostRequestViaSendBeacon(bulk, null, false)) {
+ // makes sure to load the next page faster by not waiting as long
+ // we apply this once we know send beacon works
+ setExpireDateTime(100);
+ } else {
+ sendXmlHttpRequest(bulk, null, false);
+ }
+ }
+
+ setExpireDateTime(delay);
+ });
+ }
+
+ /*
+ * Get cookie name with prefix and domain hash
+ */
+ function getCookieName(baseName) {
+ // NOTE: If the cookie name is changed, we must also update the MatomoTracker.php which
+ // will attempt to discover first party cookies. eg. See the PHP Client method getVisitorId()
+ return configCookieNamePrefix + baseName + '.' + configTrackerSiteId + '.' + domainHash;
+ }
+
+ function deleteCookie(cookieName, path, domain) {
+ setCookie(cookieName, '', -129600000, path, domain);
+ }
+
+ /*
+ * Does browser have cookies enabled (for this site)?
+ */
+ function hasCookies() {
+ if (configCookiesDisabled) {
+ return '0';
+ }
+
+ if(!isDefined(windowAlias.showModalDialog) && isDefined(navigatorAlias.cookieEnabled)) {
+ return navigatorAlias.cookieEnabled ? '1' : '0';
+ }
+
+ // for IE we want to actually set the cookie to avoid trigger a warning eg in IE see #11507
+ var testCookieName = configCookieNamePrefix + 'testcookie';
+ setCookie(testCookieName, '1', undefined, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite);
+
+ var hasCookie = getCookie(testCookieName) === '1' ? '1' : '0';
+ deleteCookie(testCookieName);
+ return hasCookie;
+ }
+
+ /*
+ * Update domain hash
+ */
+ function updateDomainHash() {
+ domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits
+ }
+
+ /*
+ * Browser features (plugins, resolution, cookies)
+ */
+ function detectBrowserFeatures() {
+ detectClientHints(function() {
+ var i, requestType;
+ clientHintsResolved = true;
+ for (i = 0; i < clientHintsRequestQueue.length; i++) {
+ requestType = typeof clientHintsRequestQueue[i];
+ if (requestType === 'string') {
+ sendRequest(clientHintsRequestQueue[i], configTrackerPause);
+ } else if (requestType === 'object') {
+ sendBulkRequest(clientHintsRequestQueue[i], configTrackerPause);
+ }
+ }
+ clientHintsRequestQueue = [];
+ });
+
+ // Browser Feature is disabled return empty object
+ if (!configBrowserFeatureDetection) {
+ return {};
+ }
+ if (isDefined(browserFeatures.res)) {
+ return browserFeatures;
+ }
+ 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
+ fla: 'application/x-shockwave-flash',
+
+ // RIA
+ java: 'application/x-java-vm',
+ ag: 'application/x-silverlight'
+ };
+
+ // detect browser features except IE < 11 (IE 11 user agent is no longer MSIE)
+ 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
+ // on Edge navigator.javaEnabled() always returns `true`, so ignore it
+ if (!((new RegExp('Edge[ /](\\d+[\\.\\d]+)')).test(navigatorAlias.userAgent)) &&
+ typeof navigator.javaEnabled !== 'unknown' &&
+ isDefined(navigatorAlias.javaEnabled) &&
+ navigatorAlias.javaEnabled()) {
+ browserFeatures.java = '1';
+ }
+
+ if (!isDefined(windowAlias.showModalDialog) && isDefined(navigatorAlias.cookieEnabled)) {
+ browserFeatures.cookie = navigatorAlias.cookieEnabled ? '1' : '0';
+ } else {
+ // Eg IE11 ... prevent error when cookieEnabled is requested within modal dialog. see #11507
+ browserFeatures.cookie = hasCookies();
+ }
+ }
+
+ var width = parseInt(screenAlias.width, 10);
+ var height = parseInt(screenAlias.height, 10);
+ browserFeatures.res = parseInt(width, 10) + 'x' + parseInt(height, 10);
+ return browserFeatures;
+ }
+
+ /*
+ * Inits the custom variables object
+ */
+ function getCustomVariablesFromCookie() {
+ var cookieName = getCookieName('cvar'),
+ cookie = getCookie(cookieName);
+
+ if (cookie && cookie.length) {
+ cookie = windowAlias.JSON.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();
+ }
+ }
+
+ /*
+ * Generate a pseudo-unique ID to fingerprint this user
+ * 16 hexits = 64 bits
+ * note: this isn't a RFC4122-compliant UUID
+ */
+ function generateRandomUuid() {
+ var browserFeatures = detectBrowserFeatures();
+ return hash(
+ (navigatorAlias.userAgent || '') +
+ (navigatorAlias.platform || '') +
+ windowAlias.JSON.stringify(browserFeatures) +
+ (new Date()).getTime() +
+ Math.random()
+ ).slice(0, 16);
+ }
+
+ function generateBrowserSpecificId() {
+ var browserFeatures = detectBrowserFeatures();
+
+ return hash(
+ (navigatorAlias.userAgent || '') +
+ (navigatorAlias.platform || '') +
+ windowAlias.JSON.stringify(browserFeatures)).slice(0, 6);
+ }
+
+ function getCurrentTimestampInSeconds()
+ {
+ return Math.floor((new Date()).getTime() / 1000);
+ }
+
+ function makeCrossDomainDeviceId()
+ {
+ var timestamp = getCurrentTimestampInSeconds();
+ var browserId = generateBrowserSpecificId();
+ var deviceId = String(timestamp) + browserId;
+
+ return deviceId;
+ }
+
+ function isSameCrossDomainDevice(deviceIdFromUrl)
+ {
+ deviceIdFromUrl = String(deviceIdFromUrl);
+
+ var thisBrowserId = generateBrowserSpecificId();
+ var lengthBrowserId = thisBrowserId.length;
+
+ var browserIdInUrl = deviceIdFromUrl.substr(-1 * lengthBrowserId, lengthBrowserId);
+ var timestampInUrl = parseInt(deviceIdFromUrl.substr(0, deviceIdFromUrl.length - lengthBrowserId), 10);
+
+ if (timestampInUrl && browserIdInUrl && browserIdInUrl === thisBrowserId) {
+ // we only reuse visitorId when used on same device / browser
+
+ var currentTimestampInSeconds = getCurrentTimestampInSeconds();
+
+ if (configVisitorIdUrlParameterTimeoutInSeconds <= 0) {
+ return true;
+ }
+ if (currentTimestampInSeconds >= timestampInUrl
+ && currentTimestampInSeconds <= (timestampInUrl + configVisitorIdUrlParameterTimeoutInSeconds)) {
+ // we only use visitorId if it was generated max 180 seconds ago
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function getVisitorIdFromUrl(url) {
+ if (!crossDomainTrackingEnabled) {
+ return '';
+ }
+
+ // problem different timezone or when the time on the computer is not set correctly it may re-use
+ // the same visitorId again. therefore we also have a factor like hashed user agent to reduce possible
+ // activation of a visitorId on other device
+ var visitorIdParam = getUrlParameter(url, configVisitorIdUrlParameter);
+
+ if (!visitorIdParam) {
+ return '';
+ }
+
+ visitorIdParam = String(visitorIdParam);
+
+ var pattern = new RegExp("^[a-zA-Z0-9]+$");
+
+ if (visitorIdParam.length === 32 && pattern.test(visitorIdParam)) {
+ var visitorDevice = visitorIdParam.substr(16, 32);
+
+ if (isSameCrossDomainDevice(visitorDevice)) {
+ var visitorId = visitorIdParam.substr(0, 16);
+ return visitorId;
+ }
+ }
+
+ return '';
+ }
+
+ /*
+ * Load visitor ID cookie
+ */
+ function loadVisitorIdCookie() {
+
+ if (!visitorUUID) {
+ // we are using locationHrefAlias and not currentUrl on purpose to for sure get the passed URL parameters
+ // from original URL
+ visitorUUID = getVisitorIdFromUrl(locationHrefAlias);
+ }
+
+ var now = new Date(),
+ nowTs = Math.round(now.getTime() / 1000),
+ visitorIdCookieName = getCookieName('id'),
+ id = getCookie(visitorIdCookieName),
+ cookieValue,
+ uuid;
+
+ // Visitor ID cookie found
+ if (id) {
+ cookieValue = id.split('.');
+
+ // returning visitor flag
+ cookieValue.unshift('0');
+
+ if(visitorUUID.length) {
+ cookieValue[1] = visitorUUID;
+ }
+ return cookieValue;
+ }
+
+ if(visitorUUID.length) {
+ uuid = visitorUUID;
+ } else if ('0' === hasCookies()){
+ uuid = '';
+ } else {
+ uuid = generateRandomUuid();
+ }
+
+ // No visitor ID cookie, let's create a new one
+ cookieValue = [
+ // new visitor
+ '1',
+
+ // uuid
+ uuid,
+
+ // creation timestamp - seconds since Unix epoch
+ nowTs
+ ];
+
+ return cookieValue;
+ }
+
+
+ /**
+ * Loads the Visitor ID cookie and returns a named array of values
+ */
+ function getValuesFromVisitorIdCookie() {
+ var cookieVisitorIdValue = loadVisitorIdCookie(),
+ newVisitor = cookieVisitorIdValue[0],
+ uuid = cookieVisitorIdValue[1],
+ createTs = cookieVisitorIdValue[2];
+
+ return {
+ newVisitor: newVisitor,
+ uuid: uuid,
+ createTs: createTs
+ };
+ }
+
+ function getRemainingVisitorCookieTimeout() {
+ var now = new Date(),
+ nowTs = now.getTime(),
+ cookieCreatedTs = getValuesFromVisitorIdCookie().createTs;
+
+ var createTs = parseInt(cookieCreatedTs, 10);
+ var originalTimeout = (createTs * 1000) + configVisitorCookieTimeout - nowTs;
+ return originalTimeout;
+ }
+
+ /*
+ * Sets the Visitor ID cookie
+ */
+ function setVisitorIdCookie(visitorIdCookieValues) {
+
+ if(!configTrackerSiteId) {
+ // when called before Site ID was set
+ return;
+ }
+
+ var now = new Date(),
+ nowTs = Math.round(now.getTime() / 1000);
+
+ if(!isDefined(visitorIdCookieValues)) {
+ visitorIdCookieValues = getValuesFromVisitorIdCookie();
+ }
+
+ var cookieValue = visitorIdCookieValues.uuid + '.' +
+ visitorIdCookieValues.createTs + '.';
+
+ setCookie(getCookieName('id'), cookieValue, getRemainingVisitorCookieTimeout(), configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite);
+ }
+
+ /*
+ * 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, System tests,
+ // and notify other tracking clients (eg. Java) of the changes
+ var cookie = getCookie(getCookieName('ref'));
+
+ if (cookie.length) {
+ try {
+ cookie = windowAlias.JSON.parse(cookie);
+ if (isObject(cookie)) {
+ return cookie;
+ }
+ } catch (ignore) {
+ // Pre 1.3, this cookie was not JSON encoded
+ }
+ }
+
+ return [
+ '',
+ '',
+ 0,
+ ''
+ ];
+ }
+
+ function isPossibleToSetCookieOnDomain(domainToTest)
+ {
+ var testCookieName = configCookieNamePrefix + 'testcookie_domain';
+ var valueToSet = 'testvalue';
+ setCookie(testCookieName, valueToSet, 10000, null, domainToTest, configCookieIsSecure, configCookieSameSite);
+
+ if (getCookie(testCookieName) === valueToSet) {
+ deleteCookie(testCookieName, null, domainToTest);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ function deleteCookies() {
+ var savedConfigCookiesDisabled = configCookiesDisabled;
+
+ // Temporarily allow cookies just to delete the existing ones
+ configCookiesDisabled = false;
+
+ var index, cookieName;
+
+ for (index = 0; index < configCookiesToDelete.length; index++) {
+ cookieName = getCookieName(configCookiesToDelete[index]);
+ if (cookieName !== CONSENT_REMOVED_COOKIE_NAME && cookieName !== CONSENT_COOKIE_NAME && 0 !== getCookie(cookieName)) {
+ deleteCookie(cookieName, configCookiePath, configCookieDomain);
+ }
+ }
+
+ configCookiesDisabled = savedConfigCookiesDisabled;
+ }
+
+ function setSiteId(siteId) {
+ configTrackerSiteId = siteId;
+ }
+
+ function sortObjectByKeys(value) {
+ if (!value || !isObject(value)) {
+ return;
+ }
+
+ // Object.keys(value) is not supported by all browsers, we get the keys manually
+ var keys = [];
+ var key;
+
+ for (key in value) {
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
+ keys.push(key);
+ }
+ }
+
+ var normalized = {};
+ keys.sort();
+ var len = keys.length;
+ var i;
+
+ for (i = 0; i < len; i++) {
+ normalized[keys[i]] = value[keys[i]];
+ }
+
+ return normalized;
+ }
+
+ /**
+ * Creates the session cookie
+ */
+ function setSessionCookie() {
+ setCookie(getCookieName('ses'), '1', configSessionCookieTimeout, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite);
+ }
+
+ function generateUniqueId() {
+ var id = '';
+ var chars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ var charLen = chars.length;
+ var i;
+
+ for (i = 0; i < 6; i++) {
+ id += chars.charAt(Math.floor(Math.random() * charLen));
+ }
+
+ return id;
+ }
+
+ function appendAvailablePerformanceMetrics(request) {
+ if (customPagePerformanceTiming !== '') {
+ request += customPagePerformanceTiming;
+ performanceTracked = true;
+ return request;
+ }
+
+ if (!performanceAlias) {
+ return request;
+ }
+
+ var performanceData = (typeof performanceAlias.timing === 'object') && performanceAlias.timing ? performanceAlias.timing : undefined;
+
+ if (!performanceData) {
+ performanceData = (typeof performanceAlias.getEntriesByType === 'function') && performanceAlias.getEntriesByType('navigation') ? performanceAlias.getEntriesByType('navigation')[0] : undefined;
+ }
+
+ if (!performanceData) {
+ return request;
+ }
+
+ // note: there might be negative values because of browser bugs see https://github.com/matomo-org/matomo/pull/16516 in this case we ignore the values
+ var timings = '';
+
+ if (performanceData.connectEnd && performanceData.fetchStart) {
+
+ if (performanceData.connectEnd < performanceData.fetchStart) {
+ return request;
+ }
+
+ timings += '&pf_net=' + Math.round(performanceData.connectEnd - performanceData.fetchStart);
+ }
+
+ if (performanceData.responseStart && performanceData.requestStart) {
+
+ if (performanceData.responseStart < performanceData.requestStart) {
+ return request;
+ }
+
+ timings += '&pf_srv=' + Math.round(performanceData.responseStart - performanceData.requestStart);
+ }
+
+ if (performanceData.responseStart && performanceData.responseEnd) {
+
+ if (performanceData.responseEnd < performanceData.responseStart) {
+ return request;
+ }
+
+ timings += '&pf_tfr=' + Math.round(performanceData.responseEnd - performanceData.responseStart);
+ }
+
+ if (isDefined(performanceData.domLoading)) {
+ if (performanceData.domInteractive && performanceData.domLoading) {
+
+ if (performanceData.domInteractive < performanceData.domLoading) {
+ return request;
+ }
+
+ timings += '&pf_dm1=' + Math.round(performanceData.domInteractive - performanceData.domLoading);
+ }
+ } else {
+ if (performanceData.domInteractive && performanceData.responseEnd) {
+
+ if (performanceData.domInteractive < performanceData.responseEnd) {
+ return request;
+ }
+
+ timings += '&pf_dm1=' + Math.round(performanceData.domInteractive - performanceData.responseEnd);
+ }
+ }
+
+ if (performanceData.domComplete && performanceData.domInteractive) {
+
+ if (performanceData.domComplete < performanceData.domInteractive) {
+ return request;
+ }
+
+ timings += '&pf_dm2=' + Math.round(performanceData.domComplete - performanceData.domInteractive);
+ }
+
+ if (performanceData.loadEventEnd && performanceData.loadEventStart) {
+
+ if (performanceData.loadEventEnd < performanceData.loadEventStart) {
+ return request;
+ }
+
+ timings += '&pf_onl=' + Math.round(performanceData.loadEventEnd - performanceData.loadEventStart);
+ }
+
+ return request + timings;
+ }
+
+ /**
+ * Returns if the given url contains a parameter to ignore the referrer
+ * e.g. ignore_referer or ignore_referrer
+ * @param url
+ * @returns {boolean}
+ */
+ function hasIgnoreReferrerParameter(url) {
+ return getUrlParameter(url, 'ignore_referrer') === "1" || getUrlParameter(url, 'ignore_referer') === "1";
+ }
+
+ function detectReferrerAttribution() {
+ var i,
+ now = new Date(),
+ nowTs = Math.round(now.getTime() / 1000),
+ referralTs,
+ referralUrl,
+ referralUrlMaxLength = 1024,
+ currentReferrerHostName,
+ originalReferrerHostName,
+ cookieSessionName = getCookieName('ses'),
+ cookieReferrerName = getCookieName('ref'),
+ cookieSessionValue = getCookie(cookieSessionName),
+ attributionCookie = loadReferrerAttributionCookie(),
+ currentUrl = configCustomUrl || locationHrefAlias,
+ campaignNameDetected,
+ campaignKeywordDetected,
+ attributionValues = {};
+
+ campaignNameDetected = attributionCookie[0];
+ campaignKeywordDetected = attributionCookie[1];
+ referralTs = attributionCookie[2];
+ referralUrl = attributionCookie[3];
+
+ if (!hasIgnoreReferrerParameter(currentUrl) && !cookieSessionValue) {
+ // cookie 'ses' was not found: we consider this the start of a 'session'
+
+ // 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 = getUrlParameter(currentUrl, configCampaignNameParameters[i]);
+
+ if (campaignNameDetected.length) {
+ break;
+ }
+ }
+ }
+
+ for (i in configCampaignKeywordParameters) {
+ if (Object.prototype.hasOwnProperty.call(configCampaignKeywordParameters, i)) {
+ campaignKeywordDetected = getUrlParameter(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
+ && !isReferrerExcluded(configReferrerUrl) // referrer is excluded
+ && (
+ !configConversionAttributionFirstReferrer // attribute to last known referrer
+ || !originalReferrerHostName.length // previously empty
+ || isSiteHostName(originalReferrerHostName) // previously set but in current domain
+ || isReferrerExcluded(referralUrl) // previously set but excluded
+ )
+ ) {
+ 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(cookieReferrerName, windowAlias.JSON.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite);
+ }
+ }
+
+ if (campaignNameDetected.length) {
+ attributionValues._rcn = encodeWrapper(campaignNameDetected);
+ }
+
+ if (campaignKeywordDetected.length) {
+ attributionValues._rck = encodeWrapper(campaignKeywordDetected);
+ }
+
+ attributionValues._refts = referralTs;
+
+ if (String(referralUrl).length) {
+ attributionValues._ref = encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength)));
+ }
+
+
+ return attributionValues;
+ }
+
+ /**
+ * Returns the URL to call matomo.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) {
+ var i,
+ now = new Date(),
+ customVariablesCopy = customVariables,
+ cookieCustomVariablesName = getCookieName('cvar'),
+ currentUrl = configCustomUrl || locationHrefAlias,
+ hasIgnoreReferrerParam = hasIgnoreReferrerParameter(currentUrl);
+
+ if (configCookiesDisabled) {
+ deleteCookies();
+ }
+
+ if (configDoNotTrack) {
+ return '';
+ }
+
+ var cookieVisitorIdValues = getValuesFromVisitorIdCookie();
+
+ // send charset if document charset is not utf-8. sometimes encoding
+ // of urls will be the same as this and not utf-8, which will cause problems
+ // do not send charset if it is utf8 since it's assumed by default in Matomo
+ var charSet = documentAlias.characterSet || documentAlias.charset;
+
+ if (!charSet || charSet.toLowerCase() === 'utf-8') {
+ charSet = null;
+ }
+
+ // 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 && !isReferrerExcluded(configReferrerUrl) && !hasIgnoreReferrerParam ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') +
+ (isNumberOrHasLength(configUserId) ? '&uid=' + encodeWrapper(configUserId) : '') +
+ '&_id=' + cookieVisitorIdValues.uuid +
+ '&_idn=' + cookieVisitorIdValues.newVisitor + // currently unused
+ (charSet ? '&cs=' + encodeWrapper(charSet) : '') +
+ '&send_image=0';
+
+ var referrerAttribution = detectReferrerAttribution();
+ // referrer attribution
+ for (i in referrerAttribution) {
+ if (Object.prototype.hasOwnProperty.call(referrerAttribution, i)) {
+ request += '&' + i + '=' + referrerAttribution[i];
+ }
+ }
+
+ var browserFeatures = detectBrowserFeatures();
+ // browser features
+ for (i in browserFeatures) {
+ if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) {
+ request += '&' + i + '=' + browserFeatures[i];
+ }
+ }
+
+ var customDimensionIdsAlreadyHandled = [];
+ if (customData) {
+ for (i in customData) {
+ if (Object.prototype.hasOwnProperty.call(customData, i) && /^dimension\d+$/.test(i)) {
+ var index = i.replace('dimension', '');
+ customDimensionIdsAlreadyHandled.push(parseInt(index, 10));
+ customDimensionIdsAlreadyHandled.push(String(index));
+ request += '&' + i + '=' + encodeWrapper(customData[i]);
+ delete customData[i];
+ }
+ }
+ }
+
+ if (customData && isObjectEmpty(customData)) {
+ customData = null;
+ // we deleted all keys from custom data
+ }
+
+ // product page view
+ for (i in ecommerceProductView) {
+ if (Object.prototype.hasOwnProperty.call(ecommerceProductView, i)) {
+ request += '&' + i + '=' + encodeWrapper(ecommerceProductView[i]);
+ }
+ }
+
+ // custom dimensions
+ for (i in customDimensions) {
+ if (Object.prototype.hasOwnProperty.call(customDimensions, i)) {
+ var isNotSetYet = (-1 === indexOfArray(customDimensionIdsAlreadyHandled, i));
+ if (isNotSetYet) {
+ request += '&dimension' + i + '=' + encodeWrapper(customDimensions[i]);
+ }
+ }
+ }
+
+ // custom data
+ if (customData) {
+ request += '&data=' + encodeWrapper(windowAlias.JSON.stringify(customData));
+ } else if (configCustomData) {
+ request += '&data=' + encodeWrapper(windowAlias.JSON.stringify(configCustomData));
+ }
+
+ // Custom Variables, scope "page"
+ function appendCustomVariablesToRequest(customVariables, parameterName) {
+ var customVariablesStringified = windowAlias.JSON.stringify(customVariables);
+ if (customVariablesStringified.length > 2) {
+ return '&' + parameterName + '=' + encodeWrapper(customVariablesStringified);
+ }
+ return '';
+ }
+
+ var sortedCustomVarPage = sortObjectByKeys(customVariablesPage);
+ var sortedCustomVarEvent = sortObjectByKeys(customVariablesEvent);
+
+ request += appendCustomVariablesToRequest(sortedCustomVarPage, 'cvar');
+ request += appendCustomVariablesToRequest(sortedCustomVarEvent, 'e_cvar');
+
+ // Custom Variables, scope "visit"
+ if (customVariables) {
+ request += appendCustomVariablesToRequest(customVariables, '_cvar');
+
+ // 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];
+ }
+ }
+ }
+
+ if (configStoreCustomVariablesInCookie) {
+ setCookie(cookieCustomVariablesName, windowAlias.JSON.stringify(customVariables), configSessionCookieTimeout, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite);
+ }
+ }
+
+ // performance tracking
+ if (configPerformanceTrackingEnabled && performanceAvailable && !performanceTracked) {
+ request = appendAvailablePerformanceMetrics(request);
+ performanceTracked = true;
+ }
+
+ if (configIdPageView) {
+ request += '&pv_id=' + configIdPageView;
+ }
+
+ // update cookies
+ setVisitorIdCookie(cookieVisitorIdValues);
+ setSessionCookie();
+
+ // tracker plugin hook
+ request += executePluginMethod(pluginMethod, {tracker: trackerInstance, request: request});
+
+ if (configAppendToTrackingUrl.length) {
+ request += '&' + configAppendToTrackingUrl;
+ }
+
+ if (isFunction(configCustomRequestContentProcessing)) {
+ request = configCustomRequestContentProcessing(request);
+ }
+
+ return request;
+ }
+
+ /*
+ * If there was user activity since the last check, and it's been configHeartBeatDelay seconds
+ * since the last tracker, send a ping request (the heartbeat timeout will be reset by sendRequest).
+ */
+ heartBeatPingIfActivityAlias = function heartBeatPingIfActivity() {
+ var now = new Date();
+ now = now.getTime();
+
+ if (!lastTrackerRequestTime) {
+ return false; // no tracking request was ever sent so lets not send heartbeat now
+ }
+
+ if (lastTrackerRequestTime + configHeartBeatDelay <= now) {
+ trackerInstance.ping();
+
+ return true;
+ }
+
+ return false;
+ };
+
+ function logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount) {
+ var request = 'idgoal=0',
+ now = new Date(),
+ items = [],
+ sku,
+ isEcommerceOrder = String(orderId).length;
+
+ if (isEcommerceOrder) {
+ request += '&ec_id=' + encodeWrapper(orderId);
+ }
+
+ 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(windowAlias.JSON.stringify(items));
+ }
+ request = getRequest(request, configCustomData, 'ecommerce');
+ sendRequest(request, configTrackerPause);
+
+ if (isEcommerceOrder) {
+ ecommerceItems = {};
+ }
+ }
+
+ 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, callback) {
+ if (!configIdPageViewSetManually) {
+ configIdPageView = generateUniqueId();
+ }
+
+ var request = getRequest('action_name=' + encodeWrapper(titleFixup(customTitle || configTitle)), customData, 'log');
+
+ // append already available performance metrics if they were not already tracked (or appended)
+ if (configPerformanceTrackingEnabled && !performanceTracked) {
+ request = appendAvailablePerformanceMetrics(request);
+ }
+
+ sendRequest(request, configTrackerPause, callback);
+ }
+
+ /*
+ * Construct regular expression of classes
+ */
+ function getClassesRegExp(configClasses, defaultClass) {
+ var i,
+ classesRegExp = '(^| )(piwik[_-]' + defaultClass + '|matomo[_-]' + defaultClass;
+
+ if (configClasses) {
+ for (i = 0; i < configClasses.length; i++) {
+ classesRegExp += '|' + configClasses[i];
+ }
+ }
+
+ classesRegExp += ')( |$)';
+
+ return new RegExp(classesRegExp);
+ }
+
+ function startsUrlWithTrackerUrl(url) {
+ return (configTrackerUrl && url && 0 === String(url).indexOf(configTrackerUrl));
+ }
+
+ /*
+ * Link or Download?
+ */
+ function getLinkType(className, href, isInLink, hasDownloadAttribute) {
+ if (startsUrlWithTrackerUrl(href)) {
+ return 0;
+ }
+
+ // 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.join('|') + ')([?]|$)', 'i');
+
+ if (linkPattern.test(className)) {
+ return 'link';
+ }
+
+ if (hasDownloadAttribute || downloadPattern.test(className) || downloadExtensionsPattern.test(href)) {
+ return 'download';
+ }
+
+ if (isInLink) {
+ return 0;
+ }
+
+ return 'link';
+ }
+
+ function getSourceElement(sourceElement)
+ {
+ var parentElement;
+
+ parentElement = sourceElement.parentNode;
+ while (parentElement !== null &&
+ /* buggy IE5.5 */
+ isDefined(parentElement)) {
+
+ if (query.isLinkElement(sourceElement)) {
+ break;
+ }
+ sourceElement = parentElement;
+ parentElement = sourceElement.parentNode;
+ }
+
+ return sourceElement;
+ }
+
+ function getLinkIfShouldBeProcessed(sourceElement)
+ {
+ sourceElement = getSourceElement(sourceElement);
+
+ if (!query.hasNodeAttribute(sourceElement, 'href')) {
+ return;
+ }
+
+ if (!isDefined(sourceElement.href)) {
+ return;
+ }
+
+ var href = query.getAttributeValueFromNode(sourceElement, 'href');
+
+ var originalSourcePath = sourceElement.pathname || getPathName(sourceElement.href);
+
+ // browsers, such as Safari, don't downcase hostname and href
+ var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href);
+ var sourceHostName = originalSourceHostName.toLowerCase();
+ var sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName);
+
+ // browsers, such as Safari, don't downcase hostname and href
+ var scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto|tel):', 'i');
+
+ if (!scriptProtocol.test(sourceHref)) {
+ // track outlinks and all downloads
+ var linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostPath(sourceHostName, originalSourcePath), query.hasNodeAttribute(sourceElement, 'download'));
+
+ if (linkType) {
+ return {
+ type: linkType,
+ href: sourceHref
+ };
+ }
+ }
+ }
+
+ function buildContentInteractionRequest(interaction, name, piece, target)
+ {
+ var params = content.buildInteractionRequestParams(interaction, name, piece, target);
+
+ if (!params) {
+ return;
+ }
+
+ return getRequest(params, null, 'contentInteraction');
+ }
+
+ function isNodeAuthorizedToTriggerInteraction(contentNode, interactedNode)
+ {
+ if (!contentNode || !interactedNode) {
+ return false;
+ }
+
+ var targetNode = content.findTargetNode(contentNode);
+
+ if (content.shouldIgnoreInteraction(targetNode)) {
+ // interaction should be ignored
+ return false;
+ }
+
+ targetNode = content.findTargetNodeNoDefault(contentNode);
+ if (targetNode && !containsNodeElement(targetNode, interactedNode)) {
+ /**
+ * There is a target node defined but the clicked element is not within the target node. example:
+ *