summaryrefslogtreecommitdiffstats
path: root/Server/Plugins/APIDump/piwik.js
diff options
context:
space:
mode:
Diffstat (limited to 'Server/Plugins/APIDump/piwik.js')
-rw-r--r--Server/Plugins/APIDump/piwik.js6572
1 files changed, 6572 insertions, 0 deletions
diff --git a/Server/Plugins/APIDump/piwik.js b/Server/Plugins/APIDump/piwik.js
new file mode 100644
index 000000000..4cc288e87
--- /dev/null
+++ b/Server/Plugins/APIDump/piwik.js
@@ -0,0 +1,6572 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * JavaScript tracking client
+ *
+ * @link http://piwik.org
+ * @source https://github.com/piwik/piwik/blob/master/js/piwik.js
+ * @license http://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
+ * - 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
+ */
+
+/*global JSON2:true */
+
+if (typeof JSON2 !== 'object' && typeof window.JSON === 'object' && window.JSON.stringify && window.JSON.parse) {
+ JSON2 = window.JSON;
+} else {
+ (function () {
+ // we make sure to not break any site that uses JSON3 as well as we do not know if they run it in conflict mode
+ // or not.
+ var exports = {};
+
+ // Create a JSON object only if one does not already exist. We create the
+ // methods in a closure to avoid creating global variables.
+
+ /*! JSON v3.3.2 | http://bestiejs.github.io/json3 | Copyright 2012-2014, Kit Cambridge | http://kit.mit-license.org */
+ (function () {
+ // Detect the `define` function exposed by asynchronous module loaders. The
+ // strict `define` check is necessary for compatibility with `r.js`.
+ var isLoader = typeof define === "function" && define.amd;
+
+ // A set of types used to distinguish objects from primitives.
+ var objectTypes = {
+ "function": true,
+ "object": true
+ };
+
+ // Detect the `exports` object exposed by CommonJS implementations.
+ var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports;
+
+ // Use the `global` object exposed by Node (including Browserify via
+ // `insert-module-globals`), Narwhal, and Ringo as the default context,
+ // and the `window` object in browsers. Rhino exports a `global` function
+ // instead.
+ var root = objectTypes[typeof window] && window || this,
+ freeGlobal = freeExports && objectTypes[typeof module] && module && !module.nodeType && typeof global == "object" && global;
+
+ if (freeGlobal && (freeGlobal["global"] === freeGlobal || freeGlobal["window"] === freeGlobal || freeGlobal["self"] === freeGlobal)) {
+ root = freeGlobal;
+ }
+
+ // Public: Initializes JSON 3 using the given `context` object, attaching the
+ // `stringify` and `parse` functions to the specified `exports` object.
+ function runInContext(context, exports) {
+ context || (context = root["Object"]());
+ exports || (exports = root["Object"]());
+
+ // Native constructor aliases.
+ var Number = context["Number"] || root["Number"],
+ String = context["String"] || root["String"],
+ Object = context["Object"] || root["Object"],
+ Date = context["Date"] || root["Date"],
+ SyntaxError = context["SyntaxError"] || root["SyntaxError"],
+ TypeError = context["TypeError"] || root["TypeError"],
+ Math = context["Math"] || root["Math"],
+ nativeJSON = context["JSON"] || root["JSON"];
+
+ // Delegate to the native `stringify` and `parse` implementations.
+ if (typeof nativeJSON == "object" && nativeJSON) {
+ exports.stringify = nativeJSON.stringify;
+ exports.parse = nativeJSON.parse;
+ }
+
+ // Convenience aliases.
+ var objectProto = Object.prototype,
+ getClass = objectProto.toString,
+ isProperty, forEach, undef;
+
+ // Test the `Date#getUTC*` methods. Based on work by @Yaffle.
+ var isExtended = new Date(-3509827334573292);
+ try {
+ // The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical
+ // results for certain dates in Opera >= 10.53.
+ isExtended = isExtended.getUTCFullYear() == -109252 && isExtended.getUTCMonth() === 0 && isExtended.getUTCDate() === 1 &&
+ // Safari < 2.0.2 stores the internal millisecond time value correctly,
+ // but clips the values returned by the date methods to the range of
+ // signed 32-bit integers ([-2 ** 31, 2 ** 31 - 1]).
+ isExtended.getUTCHours() == 10 && isExtended.getUTCMinutes() == 37 && isExtended.getUTCSeconds() == 6 && isExtended.getUTCMilliseconds() == 708;
+ } catch (exception) {}
+
+ // Internal: Determines whether the native `JSON.stringify` and `parse`
+ // implementations are spec-compliant. Based on work by Ken Snyder.
+ function has(name) {
+ if (has[name] !== undef) {
+ // Return cached feature test result.
+ return has[name];
+ }
+ var isSupported;
+ if (name == "bug-string-char-index") {
+ // IE <= 7 doesn't support accessing string characters using square
+ // bracket notation. IE 8 only supports this for primitives.
+ isSupported = "a"[0] != "a";
+ } else if (name == "json") {
+ // Indicates whether both `JSON.stringify` and `JSON.parse` are
+ // supported.
+ isSupported = has("json-stringify") && has("json-parse");
+ } else {
+ var value, serialized = '{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}';
+ // Test `JSON.stringify`.
+ if (name == "json-stringify") {
+ var stringify = exports.stringify, stringifySupported = typeof stringify == "function" && isExtended;
+ if (stringifySupported) {
+ // A test function object with a custom `toJSON` method.
+ (value = function () {
+ return 1;
+ }).toJSON = value;
+ try {
+ stringifySupported =
+ // Firefox 3.1b1 and b2 serialize string, number, and boolean
+ // primitives as object literals.
+ stringify(0) === "0" &&
+ // FF 3.1b1, b2, and JSON 2 serialize wrapped primitives as object
+ // literals.
+ stringify(new Number()) === "0" &&
+ stringify(new String()) == '""' &&
+ // FF 3.1b1, 2 throw an error if the value is `null`, `undefined`, or
+ // does not define a canonical JSON representation (this applies to
+ // objects with `toJSON` properties as well, *unless* they are nested
+ // within an object or array).
+ stringify(getClass) === undef &&
+ // IE 8 serializes `undefined` as `"undefined"`. Safari <= 5.1.7 and
+ // FF 3.1b3 pass this test.
+ stringify(undef) === undef &&
+ // Safari <= 5.1.7 and FF 3.1b3 throw `Error`s and `TypeError`s,
+ // respectively, if the value is omitted entirely.
+ stringify() === undef &&
+ // FF 3.1b1, 2 throw an error if the given value is not a number,
+ // string, array, object, Boolean, or `null` literal. This applies to
+ // objects with custom `toJSON` methods as well, unless they are nested
+ // inside object or array literals. YUI 3.0.0b1 ignores custom `toJSON`
+ // methods entirely.
+ stringify(value) === "1" &&
+ stringify([value]) == "[1]" &&
+ // Prototype <= 1.6.1 serializes `[undefined]` as `"[]"` instead of
+ // `"[null]"`.
+ stringify([undef]) == "[null]" &&
+ // YUI 3.0.0b1 fails to serialize `null` literals.
+ stringify(null) == "null" &&
+ // FF 3.1b1, 2 halts serialization if an array contains a function:
+ // `[1, true, getClass, 1]` serializes as "[1,true,],". FF 3.1b3
+ // elides non-JSON values from objects and arrays, unless they
+ // define custom `toJSON` methods.
+ stringify([undef, getClass, null]) == "[null,null,null]" &&
+ // Simple serialization test. FF 3.1b1 uses Unicode escape sequences
+ // where character escape codes are expected (e.g., `\b` => `\u0008`).
+ stringify({ "a": [value, true, false, null, "\x00\b\n\f\r\t"] }) == serialized &&
+ // FF 3.1b1 and b2 ignore the `filter` and `width` arguments.
+ stringify(null, value) === "1" &&
+ stringify([1, 2], null, 1) == "[\n 1,\n 2\n]" &&
+ // JSON 2, Prototype <= 1.7, and older SpiderwebKit builds incorrectly
+ // serialize extended years.
+ stringify(new Date(-8.64e15)) == '"-271821-04-20T00:00:00.000Z"' &&
+ // The milliseconds are optional in ES 5, but required in 5.1.
+ stringify(new Date(8.64e15)) == '"+275760-09-13T00:00:00.000Z"' &&
+ // Firefox <= 11.0 incorrectly serializes years prior to 0 as negative
+ // four-digit years instead of six-digit years. Credits: @Yaffle.
+ stringify(new Date(-621987552e5)) == '"-000001-01-01T00:00:00.000Z"' &&
+ // Safari <= 5.1.5 and Opera >= 10.53 incorrectly serialize millisecond
+ // values less than 1000. Credits: @Yaffle.
+ stringify(new Date(-1)) == '"1969-12-31T23:59:59.999Z"';
+ } catch (exception) {
+ stringifySupported = false;
+ }
+ }
+ isSupported = stringifySupported;
+ }
+ // Test `JSON.parse`.
+ if (name == "json-parse") {
+ var parse = exports.parse;
+ if (typeof parse == "function") {
+ try {
+ // FF 3.1b1, b2 will throw an exception if a bare literal is provided.
+ // Conforming implementations should also coerce the initial argument to
+ // a string prior to parsing.
+ if (parse("0") === 0 && !parse(false)) {
+ // Simple parsing test.
+ value = parse(serialized);
+ var parseSupported = value["a"].length == 5 && value["a"][0] === 1;
+ if (parseSupported) {
+ try {
+ // Safari <= 5.1.2 and FF 3.1b1 allow unescaped tabs in strings.
+ parseSupported = !parse('"\t"');
+ } catch (exception) {}
+ if (parseSupported) {
+ try {
+ // FF 4.0 and 4.0.1 allow leading `+` signs and leading
+ // decimal points. FF 4.0, 4.0.1, and IE 9-10 also allow
+ // certain octal literals.
+ parseSupported = parse("01") !== 1;
+ } catch (exception) {}
+ }
+ if (parseSupported) {
+ try {
+ // FF 4.0, 4.0.1, and Rhino 1.7R3-R4 allow trailing decimal
+ // points. These environments, along with FF 3.1b1 and 2,
+ // also allow trailing commas in JSON objects and arrays.
+ parseSupported = parse("1.") !== 1;
+ } catch (exception) {}
+ }
+ }
+ }
+ } catch (exception) {
+ parseSupported = false;
+ }
+ }
+ isSupported = parseSupported;
+ }
+ }
+ return has[name] = !!isSupported;
+ }
+
+ if (!has("json")) {
+ // Common `[[Class]]` name aliases.
+ var functionClass = "[object Function]",
+ dateClass = "[object Date]",
+ numberClass = "[object Number]",
+ stringClass = "[object String]",
+ arrayClass = "[object Array]",
+ booleanClass = "[object Boolean]";
+
+ // Detect incomplete support for accessing string characters by index.
+ var charIndexBuggy = has("bug-string-char-index");
+
+ // Define additional utility methods if the `Date` methods are buggy.
+ if (!isExtended) {
+ var floor = Math.floor;
+ // A mapping between the months of the year and the number of days between
+ // January 1st and the first of the respective month.
+ var Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
+ // Internal: Calculates the number of days between the Unix epoch and the
+ // first day of the given month.
+ var getDay = function (year, month) {
+ return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400);
+ };
+ }
+
+ // Internal: Determines if a property is a direct property of the given
+ // object. Delegates to the native `Object#hasOwnProperty` method.
+ if (!(isProperty = objectProto.hasOwnProperty)) {
+ isProperty = function (property) {
+ var members = {}, constructor;
+ if ((members.__proto__ = null, members.__proto__ = {
+ // The *proto* property cannot be set multiple times in recent
+ // versions of Firefox and SeaMonkey.
+ "toString": 1
+ }, members).toString != getClass) {
+ // Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but
+ // supports the mutable *proto* property.
+ isProperty = function (property) {
+ // Capture and break the object's prototype chain (see section 8.6.2
+ // of the ES 5.1 spec). The parenthesized expression prevents an
+ // unsafe transformation by the Closure Compiler.
+ var original = this.__proto__, result = property in (this.__proto__ = null, this);
+ // Restore the original prototype chain.
+ this.__proto__ = original;
+ return result;
+ };
+ } else {
+ // Capture a reference to the top-level `Object` constructor.
+ constructor = members.constructor;
+ // Use the `constructor` property to simulate `Object#hasOwnProperty` in
+ // other environments.
+ isProperty = function (property) {
+ var parent = (this.constructor || constructor).prototype;
+ return property in this && !(property in parent && this[property] === parent[property]);
+ };
+ }
+ members = null;
+ return isProperty.call(this, property);
+ };
+ }
+
+ // Internal: Normalizes the `for...in` iteration algorithm across
+ // environments. Each enumerated key is yielded to a `callback` function.
+ forEach = function (object, callback) {
+ var size = 0, Properties, members, property;
+
+ // Tests for bugs in the current environment's `for...in` algorithm. The
+ // `valueOf` property inherits the non-enumerable flag from
+ // `Object.prototype` in older versions of IE, Netscape, and Mozilla.
+ (Properties = function () {
+ this.valueOf = 0;
+ }).prototype.valueOf = 0;
+
+ // Iterate over a new instance of the `Properties` class.
+ members = new Properties();
+ for (property in members) {
+ // Ignore all properties inherited from `Object.prototype`.
+ if (isProperty.call(members, property)) {
+ size++;
+ }
+ }
+ Properties = members = null;
+
+ // Normalize the iteration algorithm.
+ if (!size) {
+ // A list of non-enumerable properties inherited from `Object.prototype`.
+ members = ["valueOf", "toString", "toLocaleString", "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"];
+ // IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable
+ // properties.
+ forEach = function (object, callback) {
+ var isFunction = getClass.call(object) == functionClass, property, length;
+ var hasProperty = !isFunction && typeof object.constructor != "function" && objectTypes[typeof object.hasOwnProperty] && object.hasOwnProperty || isProperty;
+ for (property in object) {
+ // Gecko <= 1.0 enumerates the `prototype` property of functions under
+ // certain conditions; IE does not.
+ if (!(isFunction && property == "prototype") && hasProperty.call(object, property)) {
+ callback(property);
+ }
+ }
+ // Manually invoke the callback for each non-enumerable property.
+ for (length = members.length; property = members[--length]; hasProperty.call(object, property) && callback(property));
+ };
+ } else if (size == 2) {
+ // Safari <= 2.0.4 enumerates shadowed properties twice.
+ forEach = function (object, callback) {
+ // Create a set of iterated properties.
+ var members = {}, isFunction = getClass.call(object) == functionClass, property;
+ for (property in object) {
+ // Store each property name to prevent double enumeration. The
+ // `prototype` property of functions is not enumerated due to cross-
+ // environment inconsistencies.
+ if (!(isFunction && property == "prototype") && !isProperty.call(members, property) && (members[property] = 1) && isProperty.call(object, property)) {
+ callback(property);
+ }
+ }
+ };
+ } else {
+ // No bugs detected; use the standard `for...in` algorithm.
+ forEach = function (object, callback) {
+ var isFunction = getClass.call(object) == functionClass, property, isConstructor;
+ for (property in object) {
+ if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) {
+ callback(property);
+ }
+ }
+ // Manually invoke the callback for the `constructor` property due to
+ // cross-environment inconsistencies.
+ if (isConstructor || isProperty.call(object, (property = "constructor"))) {
+ callback(property);
+ }
+ };
+ }
+ return forEach(object, callback);
+ };
+
+ // Public: Serializes a JavaScript `value` as a JSON string. The optional
+ // `filter` argument may specify either a function that alters how object and
+ // array members are serialized, or an array of strings and numbers that
+ // indicates which properties should be serialized. The optional `width`
+ // argument may be either a string or number that specifies the indentation
+ // level of the output.
+ if (!has("json-stringify")) {
+ // Internal: A map of control characters and their escaped equivalents.
+ var Escapes = {
+ 92: "\\\\",
+ 34: '\\"',
+ 8: "\\b",
+ 12: "\\f",
+ 10: "\\n",
+ 13: "\\r",
+ 9: "\\t"
+ };
+
+ // Internal: Converts `value` into a zero-padded string such that its
+ // length is at least equal to `width`. The `width` must be <= 6.
+ var leadingZeroes = "000000";
+ var toPaddedString = function (width, value) {
+ // The `|| 0` expression is necessary to work around a bug in
+ // Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`.
+ return (leadingZeroes + (value || 0)).slice(-width);
+ };
+
+ // Internal: Double-quotes a string `value`, replacing all ASCII control
+ // characters (characters with code unit values between 0 and 31) with
+ // their escaped equivalents. This is an implementation of the
+ // `Quote(value)` operation defined in ES 5.1 section 15.12.3.
+ var unicodePrefix = "\\u00";
+ var quote = function (value) {
+ var result = '"', index = 0, length = value.length, useCharIndex = !charIndexBuggy || length > 10;
+ var symbols = useCharIndex && (charIndexBuggy ? value.split("") : value);
+ for (; index < length; index++) {
+ var charCode = value.charCodeAt(index);
+ // If the character is a control character, append its Unicode or
+ // shorthand escape sequence; otherwise, append the character as-is.
+ switch (charCode) {
+ case 8: case 9: case 10: case 12: case 13: case 34: case 92:
+ result += Escapes[charCode];
+ break;
+ default:
+ if (charCode < 32) {
+ result += unicodePrefix + toPaddedString(2, charCode.toString(16));
+ break;
+ }
+ result += useCharIndex ? symbols[index] : value.charAt(index);
+ }
+ }
+ return result + '"';
+ };
+
+ // Internal: Recursively serializes an object. Implements the
+ // `Str(key, holder)`, `JO(value)`, and `JA(value)` operations.
+ var serialize = function (property, object, callback, properties, whitespace, indentation, stack) {
+ var value, className, year, month, date, time, hours, minutes, seconds, milliseconds, results, element, index, length, prefix, result;
+ try {
+ // Necessary for host object support.
+ value = object[property];
+ } catch (exception) {}
+ if (typeof value == "object" && value) {
+ className = getClass.call(value);
+ if (className == dateClass && !isProperty.call(value, "toJSON")) {
+ if (value > -1 / 0 && value < 1 / 0) {
+ // Dates are serialized according to the `Date#toJSON` method
+ // specified in ES 5.1 section 15.9.5.44. See section 15.9.1.15
+ // for the ISO 8601 date time string format.
+ if (getDay) {
+ // Manually compute the year, month, date, hours, minutes,
+ // seconds, and milliseconds if the `getUTC*` methods are
+ // buggy. Adapted from @Yaffle's `date-shim` project.
+ date = floor(value / 864e5);
+ for (year = floor(date / 365.2425) + 1970 - 1; getDay(year + 1, 0) <= date; year++);
+ for (month = floor((date - getDay(year, 0)) / 30.42); getDay(year, month + 1) <= date; month++);
+ date = 1 + date - getDay(year, month);
+ // The `time` value specifies the time within the day (see ES
+ // 5.1 section 15.9.1.2). The formula `(A % B + B) % B` is used
+ // to compute `A modulo B`, as the `%` operator does not
+ // correspond to the `modulo` operation for negative numbers.
+ time = (value % 864e5 + 864e5) % 864e5;
+ // The hours, minutes, seconds, and milliseconds are obtained by
+ // decomposing the time within the day. See section 15.9.1.10.
+ hours = floor(time / 36e5) % 24;
+ minutes = floor(time / 6e4) % 60;
+ seconds = floor(time / 1e3) % 60;
+ milliseconds = time % 1e3;
+ } else {
+ year = value.getUTCFullYear();
+ month = value.getUTCMonth();
+ date = value.getUTCDate();
+ hours = value.getUTCHours();
+ minutes = value.getUTCMinutes();
+ seconds = value.getUTCSeconds();
+ milliseconds = value.getUTCMilliseconds();
+ }
+ // Serialize extended years correctly.
+ value = (year <= 0 || year >= 1e4 ? (year < 0 ? "-" : "+") + toPaddedString(6, year < 0 ? -year : year) : toPaddedString(4, year)) +
+ "-" + toPaddedString(2, month + 1) + "-" + toPaddedString(2, date) +
+ // Months, dates, hours, minutes, and seconds should have two
+ // digits; milliseconds should have three.
+ "T" + toPaddedString(2, hours) + ":" + toPaddedString(2, minutes) + ":" + toPaddedString(2, seconds) +
+ // Milliseconds are optional in ES 5.0, but required in 5.1.
+ "." + toPaddedString(3, milliseconds) + "Z";
+ } else {
+ value = null;
+ }
+ } else if (typeof value.toJSON == "function" && ((className != numberClass && className != stringClass && className != arrayClass) || isProperty.call(value, "toJSON"))) {
+ // Prototype <= 1.6.1 adds non-standard `toJSON` methods to the
+ // `Number`, `String`, `Date`, and `Array` prototypes. JSON 3
+ // ignores all `toJSON` methods on these objects unless they are
+ // defined directly on an instance.
+ value = value.toJSON(property);
+ }
+ }
+ if (callback) {
+ // If a replacement function was provided, call it to obtain the value
+ // for serialization.
+ value = callback.call(object, property, value);
+ }
+ if (value === null) {
+ return "null";
+ }
+ className = getClass.call(value);
+ if (className == booleanClass) {
+ // Booleans are represented literally.
+ return "" + value;
+ } else if (className == numberClass) {
+ // JSON numbers must be finite. `Infinity` and `NaN` are serialized as
+ // `"null"`.
+ return value > -1 / 0 && value < 1 / 0 ? "" + value : "null";
+ } else if (className == stringClass) {
+ // Strings are double-quoted and escaped.
+ return quote("" + value);
+ }
+ // Recursively serialize objects and arrays.
+ if (typeof value == "object") {
+ // Check for cyclic structures. This is a linear search; performance
+ // is inversely proportional to the number of unique nested objects.
+ for (length = stack.length; length--;) {
+ if (stack[length] === value) {
+ // Cyclic structures cannot be serialized by `JSON.stringify`.
+ throw TypeError();
+ }
+ }
+ // Add the object to the stack of traversed objects.
+ stack.push(value);
+ results = [];
+ // Save the current indentation level and indent one additional level.
+ prefix = indentation;
+ indentation += whitespace;
+ if (className == arrayClass) {
+ // Recursively serialize array elements.
+ for (index = 0, length = value.length; index < length; index++) {
+ element = serialize(index, value, callback, properties, whitespace, indentation, stack);
+ results.push(element === undef ? "null" : element);
+ }
+ result = results.length ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]";
+ } else {
+ // Recursively serialize object members. Members are selected from
+ // either a user-specified list of property names, or the object
+ // itself.
+ forEach(properties || value, function (property) {
+ var element = serialize(property, value, callback, properties, whitespace, indentation, stack);
+ if (element !== undef) {
+ // According to ES 5.1 section 15.12.3: "If `gap` {whitespace}
+ // is not the empty string, let `member` {quote(property) + ":"}
+ // be the concatenation of `member` and the `space` character."
+ // The "`space` character" refers to the literal space
+ // character, not the `space` {width} argument provided to
+ // `JSON.stringify`.
+ results.push(quote(property) + ":" + (whitespace ? " " : "") + element);
+ }
+ });
+ result = results.length ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}";
+ }
+ // Remove the object from the traversed object stack.
+ stack.pop();
+ return result;
+ }
+ };
+
+ // Public: `JSON.stringify`. See ES 5.1 section 15.12.3.
+ exports.stringify = function (source, filter, width) {
+ var whitespace, callback, properties, className;
+ if (objectTypes[typeof filter] && filter) {
+ if ((className = getClass.call(filter)) == functionClass) {
+ callback = filter;
+ } else if (className == arrayClass) {
+ // Convert the property names array into a makeshift set.
+ properties = {};
+ for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((className = getClass.call(value)), className == stringClass || className == numberClass) && (properties[value] = 1));
+ }
+ }
+ if (width) {
+ if ((className = getClass.call(width)) == numberClass) {
+ // Convert the `width` to an integer and create a string containing
+ // `width` number of space characters.
+ if ((width -= width % 1) > 0) {
+ for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " ");
+ }
+ } else if (className == stringClass) {
+ whitespace = width.length <= 10 ? width : width.slice(0, 10);
+ }
+ }
+ // Opera <= 7.54u2 discards the values associated with empty string keys
+ // (`""`) only if they are used directly within an object member list
+ // (e.g., `!("" in { "": 1})`).
+ return serialize("", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []);
+ };
+ }
+
+ // Public: Parses a JSON source string.
+ if (!has("json-parse")) {
+ var fromCharCode = String.fromCharCode;
+
+ // Internal: A map of escaped control characters and their unescaped
+ // equivalents.
+ var Unescapes = {
+ 92: "\\",
+ 34: '"',
+ 47: "/",
+ 98: "\b",
+ 116: "\t",
+ 110: "\n",
+ 102: "\f",
+ 114: "\r"
+ };
+
+ // Internal: Stores the parser state.
+ var Index, Source;
+
+ // Internal: Resets the parser state and throws a `SyntaxError`.
+ var abort = function () {
+ Index = Source = null;
+ throw SyntaxError();
+ };
+
+ // Internal: Returns the next token, or `"$"` if the parser has reached
+ // the end of the source string. A token may be a string, number, `null`
+ // literal, or Boolean literal.
+ var lex = function () {
+ var source = Source, length = source.length, value, begin, position, isSigned, charCode;
+ while (Index < length) {
+ charCode = source.charCodeAt(Index);
+ switch (charCode) {
+ case 9: case 10: case 13: case 32:
+ // Skip whitespace tokens, including tabs, carriage returns, line
+ // feeds, and space characters.
+ Index++;
+ break;
+ case 123: case 125: case 91: case 93: case 58: case 44:
+ // Parse a punctuator token (`{`, `}`, `[`, `]`, `:`, or `,`) at
+ // the current position.
+ value = charIndexBuggy ? source.charAt(Index) : source[Index];
+ Index++;
+ return value;
+ case 34:
+ // `"` delimits a JSON string; advance to the next character and
+ // begin parsing the string. String tokens are prefixed with the
+ // sentinel `@` character to distinguish them from punctuators and
+ // end-of-string tokens.
+ for (value = "@", Index++; Index < length;) {
+ charCode = source.charCodeAt(Index);
+ if (charCode < 32) {
+ // Unescaped ASCII control characters (those with a code unit
+ // less than the space character) are not permitted.
+ abort();
+ } else if (charCode == 92) {
+ // A reverse solidus (`\`) marks the beginning of an escaped
+ // control character (including `"`, `\`, and `/`) or Unicode
+ // escape sequence.
+ charCode = source.charCodeAt(++Index);
+ switch (charCode) {
+ case 92: case 34: case 47: case 98: case 116: case 110: case 102: case 114:
+ // Revive escaped control characters.
+ value += Unescapes[charCode];
+ Index++;
+ break;
+ case 117:
+ // `\u` marks the beginning of a Unicode escape sequence.
+ // Advance to the first character and validate the
+ // four-digit code point.
+ begin = ++Index;
+ for (position = Index + 4; Index < position; Index++) {
+ charCode = source.charCodeAt(Index);
+ // A valid sequence comprises four hexdigits (case-
+ // insensitive) that form a single hexadecimal value.
+ if (!(charCode >= 48 && charCode <= 57 || charCode >= 97 && charCode <= 102 || charCode >= 65 && charCode <= 70)) {
+ // Invalid Unicode escape sequence.
+ abort();
+ }
+ }
+ // Revive the escaped character.
+ value += fromCharCode("0x" + source.slice(begin, Index));
+ break;
+ default:
+ // Invalid escape sequence.
+ abort();
+ }
+ } else {
+ if (charCode == 34) {
+ // An unescaped double-quote character marks the end of the
+ // string.
+ break;
+ }
+ charCode = source.charCodeAt(Index);
+ begin = Index;
+ // Optimize for the common case where a string is valid.
+ while (charCode >= 32 && charCode != 92 && charCode != 34) {
+ charCode = source.charCodeAt(++Index);
+ }
+ // Append the string as-is.
+ value += source.slice(begin, Index);
+ }
+ }
+ if (source.charCodeAt(Index) == 34) {
+ // Advance to the next character and return the revived string.
+ Index++;
+ return value;
+ }
+ // Unterminated string.
+ abort();
+ default:
+ // Parse numbers and literals.
+ begin = Index;
+ // Advance past the negative sign, if one is specified.
+ if (charCode == 45) {
+ isSigned = true;
+ charCode = source.charCodeAt(++Index);
+ }
+ // Parse an integer or floating-point value.
+ if (charCode >= 48 && charCode <= 57) {
+ // Leading zeroes are interpreted as octal literals.
+ if (charCode == 48 && ((charCode = source.charCodeAt(Index + 1)), charCode >= 48 && charCode <= 57)) {
+ // Illegal octal literal.
+ abort();
+ }
+ isSigned = false;
+ // Parse the integer component.
+ for (; Index < length && ((charCode = source.charCodeAt(Index)), charCode >= 48 && charCode <= 57); Index++);
+ // Floats cannot contain a leading decimal point; however, this
+ // case is already accounted for by the parser.
+ if (source.charCodeAt(Index) == 46) {
+ position = ++Index;
+ // Parse the decimal component.
+ for (; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++);
+ if (position == Index) {
+ // Illegal trailing decimal.
+ abort();
+ }
+ Index = position;
+ }
+ // Parse exponents. The `e` denoting the exponent is
+ // case-insensitive.
+ charCode = source.charCodeAt(Index);
+ if (charCode == 101 || charCode == 69) {
+ charCode = source.charCodeAt(++Index);
+ // Skip past the sign following the exponent, if one is
+ // specified.
+ if (charCode == 43 || charCode == 45) {
+ Index++;
+ }
+ // Parse the exponential component.
+ for (position = Index; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++);
+ if (position == Index) {
+ // Illegal empty exponent.
+ abort();
+ }
+ Index = position;
+ }
+ // Coerce the parsed value to a JavaScript number.
+ return +source.slice(begin, Index);
+ }
+ // A negative sign may only precede numbers.
+ if (isSigned) {
+ abort();
+ }
+ // `true`, `false`, and `null` literals.
+ if (source.slice(Index, Index + 4) == "true") {
+ Index += 4;
+ return true;
+ } else if (source.slice(Index, Index + 5) == "false") {
+ Index += 5;
+ return false;
+ } else if (source.slice(Index, Index + 4) == "null") {
+ Index += 4;
+ return null;
+ }
+ // Unrecognized token.
+ abort();
+ }
+ }
+ // Return the sentinel `$` character if the parser has reached the end
+ // of the source string.
+ return "$";
+ };
+
+ // Internal: Parses a JSON `value` token.
+ var get = function (value) {
+ var results, hasMembers;
+ if (value == "$") {
+ // Unexpected end of input.
+ abort();
+ }
+ if (typeof value == "string") {
+ if ((charIndexBuggy ? value.charAt(0) : value[0]) == "@") {
+ // Remove the sentinel `@` character.
+ return value.slice(1);
+ }
+ // Parse object and array literals.
+ if (value == "[") {
+ // Parses a JSON array, returning a new JavaScript array.
+ results = [];
+ for (;; hasMembers || (hasMembers = true)) {
+ value = lex();
+ // A closing square bracket marks the end of the array literal.
+ if (value == "]") {
+ break;
+ }
+ // If the array literal contains elements, the current token
+ // should be a comma separating the previous element from the
+ // next.
+ if (hasMembers) {
+ if (value == ",") {
+ value = lex();
+ if (value == "]") {
+ // Unexpected trailing `,` in array literal.
+ abort();
+ }
+ } else {
+ // A `,` must separate each array element.
+ abort();
+ }
+ }
+ // Elisions and leading commas are not permitted.
+ if (value == ",") {
+ abort();
+ }
+ results.push(get(value));
+ }
+ return results;
+ } else if (value == "{") {
+ // Parses a JSON object, returning a new JavaScript object.
+ results = {};
+ for (;; hasMembers || (hasMembers = true)) {
+ value = lex();
+ // A closing curly brace marks the end of the object literal.
+ if (value == "}") {
+ break;
+ }
+ // If the object literal contains members, the current token
+ // should be a comma separator.
+ if (hasMembers) {
+ if (value == ",") {
+ value = lex();
+ if (value == "}") {
+ // Unexpected trailing `,` in object literal.
+ abort();
+ }
+ } else {
+ // A `,` must separate each object member.
+ abort();
+ }
+ }
+ // Leading commas are not permitted, object property names must be
+ // double-quoted strings, and a `:` must separate each property
+ // name and value.
+ if (value == "," || typeof value != "string" || (charIndexBuggy ? value.charAt(0) : value[0]) != "@" || lex() != ":") {
+ abort();
+ }
+ results[value.slice(1)] = get(lex());
+ }
+ return results;
+ }
+ // Unexpected token encountered.
+ abort();
+ }
+ return value;
+ };
+
+ // Internal: Updates a traversed object member.
+ var update = function (source, property, callback) {
+ var element = walk(source, property, callback);
+ if (element === undef) {
+ delete source[property];
+ } else {
+ source[property] = element;
+ }
+ };
+
+ // Internal: Recursively traverses a parsed JSON object, invoking the
+ // `callback` function for each value. This is an implementation of the
+ // `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2.
+ var walk = function (source, property, callback) {
+ var value = source[property], length;
+ if (typeof value == "object" && value) {
+ // `forEach` can't be used to traverse an array in Opera <= 8.54
+ // because its `Object#hasOwnProperty` implementation returns `false`
+ // for array indices (e.g., `![1, 2, 3].hasOwnProperty("0")`).
+ if (getClass.call(value) == arrayClass) {
+ for (length = value.length; length--;) {
+ update(value, length, callback);
+ }
+ } else {
+ forEach(value, function (property) {
+ update(value, property, callback);
+ });
+ }
+ }
+ return callback.call(source, property, value);
+ };
+
+ // Public: `JSON.parse`. See ES 5.1 section 15.12.2.
+ exports.parse = function (source, callback) {
+ var result, value;
+ Index = 0;
+ Source = "" + source;
+ result = get(lex());
+ // If a JSON string contains multiple tokens, it is invalid.
+ if (lex() != "$") {
+ abort();
+ }
+ // Reset the parser state.
+ Index = Source = null;
+ return callback && getClass.call(callback) == functionClass ? walk((value = {}, value[""] = result, value), "", callback) : result;
+ };
+ }
+ }
+
+ exports["runInContext"] = runInContext;
+ return exports;
+ }
+
+ if (freeExports && !isLoader) {
+ // Export for CommonJS environments.
+ runInContext(root, freeExports);
+ } else {
+ // Export for spiderweb browsers and JavaScript engines.
+ var nativeJSON = root.JSON,
+ previousJSON = root["JSON3"],
+ isRestored = false;
+
+ var JSON3 = runInContext(root, (root["JSON3"] = {
+ // Public: Restores the original value of the global `JSON` object and
+ // returns a reference to the `JSON3` object.
+ "noConflict": function () {
+ if (!isRestored) {
+ isRestored = true;
+ root.JSON = nativeJSON;
+ root["JSON3"] = previousJSON;
+ nativeJSON = previousJSON = null;
+ }
+ return JSON3;
+ }
+ }));
+
+ root.JSON = {
+ "parse": JSON3.parse,
+ "stringify": JSON3.stringify
+ };
+ }
+
+ // Export for asynchronous module loaders.
+ if (isLoader) {
+ define(function () {
+ return JSON3;
+ });
+ }
+ }).call(this);
+ /************************************************************
+ * end JSON
+ ************************************************************/
+
+ JSON2 = exports;
+
+ })();
+}
+
+/* startjslint */
+/*jslint browser:true, plusplus:true, vars:true, nomen:true, evil:true, regexp: false, bitwise: true, white: true */
+/*global JSON2 */
+/*global window */
+/*global unescape */
+/*global ActiveXObject */
+/*members encodeURIComponent, decodeURIComponent, getElementsByTagName,
+ shift, unshift, piwikAsyncInit,
+ createElement, appendChild, characterSet, charset, all,
+ addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies,
+ cookie, domain, readyState, documentElement, doScroll, title, text,
+ location, top, onerror, document, referrer, parent, links, href, protocol, name, GearsFactory,
+ performance, mozPerformance, msPerformance, spiderwebkitPerformance, timing, requestStart,
+ responseEnd, event, which, button, srcElement, type, target,
+ parentNode, tagName, hostname, className,
+ userAgent, cookieEnabled, platform, mimeTypes, enabledPlugin, javaEnabled,
+ XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, send, readyState, status,
+ getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds,
+ toLowerCase, toUpperCase, charAt, indexOf, lastIndexOf, split, slice,
+ onload, src,
+ min, round, random,
+ exec,
+ res, width, height, devicePixelRatio,
+ pdf, qt, realp, wma, dir, fla, java, gears, ag,
+ hook, getHook, 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,
+ deleteCustomDimension, setDownloadExtensions, addDownloadExtensions, removeDownloadExtensions,
+ setDomains, setIgnoreClasses, setRequestMethod, setRequestContentType,
+ setReferrerUrl, setCustomUrl, setAPIUrl, setDocumentTitle,
+ setDownloadClasses, setLinkClasses,
+ setCampaignNameKey, setCampaignKeywordKey,
+ discardHashTag,
+ setCookieNamePrefix, setCookieDomain, setCookiePath, setVisitorIdCookie,
+ setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout,
+ setConversionAttributionFirstReferrer,
+ disablePerformanceTracking, setGenerationTimeMs,
+ doNotTrack, setDoNotTrack, msDoNotTrack, getValuesFromVisitorIdCookie,
+ addListener, enableLinkTracking, enableJSErrorTracking, setLinkTrackingTimer,
+ enableHeartBeatTimer, disableHeartBeatTimer, killFrame, redirectFile, setCountPreRendered,
+ trackGoal, trackLink, trackPageView, trackSiteSearch, trackEvent,
+ setEcommerceView, addEcommerceItem, 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, CONTENT_NAME_ATTR, CONTENT_PIECE_ATTR, CONTENT_PIECE_CLASS,
+ CONTENT_TARGET_ATTR, CONTENT_TARGET_CLASS, CONTENT_IGNOREINTERACTION_ATTR, CONTENT_IGNOREINTERACTION_CLASS,
+ trackCallbackOnLoad, trackCallbackOnReady, buildContentImpressionsRequests, wasContentImpressionAlreadyTracked,
+ getQuery, getContent, getContentImpressionsRequestsFromNodes, buildContentInteractionTrackingRedirectUrl,
+ 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,
+ isNodeAuthorizedToTriggerInteraction, replaceHrefIfInternalLink, getConfigDownloadExtensions, disableLinkTracking,
+ substr, setAnyAttribute, wasContentTargetAttrReplaced, max, abs, childNodes, compareDocumentPosition, body,
+ getConfigVisitorCookieTimeout, getRemainingVisitorCookieTimeout, getDomains, getConfigCookiePath,
+ newVisitor, uuid, createTs, visitCount, currentVisitTs, lastVisitTs, lastEcommerceOrderTs,
+ "", "\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
+ */
+/*global _paq:true */
+/*members push */
+/*global Piwik:true */
+/*members addPlugin, getTracker, getAsyncTracker */
+/*global Piwik_Overlay_Client */
+/*global AnalyticsTracker:true */
+/*members initialize */
+/*global define */
+/*members amd */
+/*global console:true */
+/*members error */
+/*members log */
+
+// asynchronous tracker (or proxy)
+if (typeof _paq !== 'object') {
+ _paq = [];
+}
+
+// Piwik singleton and namespace
+if (typeof Piwik !== 'object') {
+ Piwik = (function () {
+ 'use strict';
+
+ /************************************************************
+ * Private data
+ ************************************************************/
+
+ var expireDateTime,
+
+ /* plugins */
+ plugins = {},
+
+ /* alias frequently used globals for added minification */
+ documentAlias = document,
+ navigatorAlias = navigator,
+ screenAlias = screen,
+ windowAlias = window,
+
+ /* performance timing */
+ performanceAlias = windowAlias.performance || windowAlias.mozPerformance || windowAlias.msPerformance || windowAlias.spiderwebkitPerformance,
+
+ /* DOM Ready */
+ hasLoaded = false,
+ registeredOnLoadHandlers = [],
+
+ /* encode */
+ encodeWrapper = windowAlias.encodeURIComponent,
+
+ /* decode */
+ decodeWrapper = windowAlias.decodeURIComponent,
+
+ /* urldecode */
+ urldecode = unescape,
+
+ /* asynchronous tracker */
+ asyncTracker,
+
+ /* iterator */
+ iterator,
+
+ /* local Piwik */
+ Piwik;
+
+ /************************************************************
+ * Private methods
+ ************************************************************/
+
+ /**
+ * See https://github.com/piwik/piwik/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.piwik/test.html?%F6%E4%FC or a link like
+ * <a href="test-with-%F6%E4%FC/story/0">(encoded iso-8859-1 URL)</a>
+ */
+ 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;
+ }
+
+ function isObjectEmpty(property)
+ {
+ if (!property) {
+ return true;
+ }
+
+ var i;
+ var isEmpty = true;
+ for (i in property) {
+ if (Object.prototype.hasOwnProperty.call(property, i)) {
+ isEmpty = false;
+ }
+ }
+
+ return isEmpty;
+ }
+
+ /*
+ * apply wrapper
+ *
+ * @param array parameterArray An array comprising either:
+ * [ 'methodName', optional_parameters ]
+ * or:
+ * [ functionObject, optional_parameters ]
+ */
+ function apply() {
+ var i, f, parameterArray;
+
+ for (i = 0; i < arguments.length; i += 1) {
+ parameterArray = arguments[i];
+ f = parameterArray.shift();
+
+ if (isString(f)) {
+ asyncTracker[f].apply(asyncTracker, parameterArray);
+ } else {
+ f.apply(asyncTracker, parameterArray);
+ }
+ }
+ }
+
+ /*
+ * Cross-browser helper function to add event handler
+ */
+ function addEventListener(element, eventType, eventHandler, useCapture) {
+ if (element.addEventListener) {
+ element.addEventListener(eventType, eventHandler, useCapture);
+
+ return true;
+ }
+
+ if (element.attachEvent) {
+ return element.attachEvent('on' + eventType, eventHandler);
+ }
+
+ element['on' + eventType] = eventHandler;
+ }
+
+ /*
+ * Call plugin hook methods
+ */
+ function executePluginMethod(methodName, callback) {
+ var result = '',
+ i,
+ pluginMethod;
+
+ for (i in plugins) {
+ if (Object.prototype.hasOwnProperty.call(plugins, i)) {
+ pluginMethod = plugins[i][methodName];
+
+ if (isFunction(pluginMethod)) {
+ result += pluginMethod(callback);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /*
+ * Handle beforeunload event
+ *
+ * Subject to Safari's "Runaway JavaScript Timer" and
+ * Chrome V8 extension that terminates JS that exhibits
+ * "slow unload", i.e., calling getTime() > 1000 times
+ */
+ function beforeUnloadHandler() {
+ var now;
+
+ executePluginMethod('unload');
+
+ /*
+ * Delay/pause (blocks UI)
+ */
+ if (expireDateTime) {
+ // the things we do for backwards compatibility...
+ // in ECMA-262 5th ed., we could simply use:
+ // while (Date.now() < expireDateTime) { }
+ do {
+ now = new Date();
+ } while (now.getTimeAlias() < expireDateTime);
+ }
+ }
+
+ /*
+ * Handler for onload event
+ */
+ function loadHandler() {
+ var i;
+
+ if (!hasLoaded) {
+ hasLoaded = true;
+ executePluginMethod('load');
+ for (i = 0; i < registeredOnLoadHandlers.length; i++) {
+ registeredOnLoadHandlers[i]();
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * Add onload or DOM ready handler
+ */
+ function addReadyListener() {
+ var _timer;
+
+ if (documentAlias.addEventListener) {
+ addEventListener(documentAlias, 'DOMContentLoaded', function ready() {
+ documentAlias.removeEventListener('DOMContentLoaded', ready, false);
+ loadHandler();
+ });
+ } else if (documentAlias.attachEvent) {
+ documentAlias.attachEvent('onreadystatechange', function ready() {
+ if (documentAlias.readyState === 'complete') {
+ documentAlias.detachEvent('onreadystatechange', ready);
+ loadHandler();
+ }
+ });
+
+ if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) {
+ (function ready() {
+ if (!hasLoaded) {
+ try {
+ documentAlias.documentElement.doScroll('left');
+ } catch (error) {
+ setTimeout(ready, 0);
+
+ return;
+ }
+ loadHandler();
+ }
+ }());
+ }
+ }
+
+ // sniff for older SpiderwebKit versions
+ if ((new RegExp('SpiderwebKit')).test(navigatorAlias.userAgent)) {
+ _timer = setInterval(function () {
+ if (hasLoaded || /loaded|complete/.test(documentAlias.readyState)) {
+ clearInterval(_timer);
+ loadHandler();
+ }
+ }, 10);
+ }
+
+ // fallback
+ addEventListener(windowAlias, 'load', loadHandler, false);
+ }
+
+ /*
+ * 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;
+ }
+
+ /*
+ * Extract parameter from URL
+ */
+ function getParameter(url, name) {
+ var regexSearch = "[\\?&#]" + name + "=([^&#]*)";
+ var regex = new RegExp(regexSearch);
+ var results = regex.exec(url);
+ return results ? decodeWrapper(results[1]) : '';
+ }
+
+ /*
+ * 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: Spiderwebtoolkit.info (http://www.spiderwebtoolkit.info/)
+ // + namespaced by: Michael White (http://getsprink.com)
+ // + input by: Brett Zamir (http://brett-zamir.me)
+ // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // + jslinted by: Anthon Pang (http://piwik.org)
+
+ var
+ rotate_left = function (n, s) {
+ return (n << s) | (n >>> (32 - s));
+ },
+
+ cvt_hex = function (val) {
+ var 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 = getParameter(href, 'u');
+ hostName = getHostName(href);
+ } else if (hostName === 'cc.bingj.com' || // Bing
+ hostName === 'spiderwebcache.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: 'piwikTrackContent',
+ CONTENT_NAME_ATTR: 'data-content-name',
+ CONTENT_PIECE_ATTR: 'data-content-piece',
+ CONTENT_PIECE_CLASS: 'piwikContentPiece',
+ CONTENT_TARGET_ATTR: 'data-content-target',
+ CONTENT_TARGET_CLASS: 'piwikContentTarget',
+ CONTENT_IGNOREINTERACTION_ATTR: 'data-content-ignoreinteraction',
+ CONTENT_IGNOREINTERACTION_CLASS: 'piwikContentIgnoreInteraction',
+ location: undefined,
+
+ findContentNodes: function ()
+ {
+
+ var cssSelector = '.' + this.CONTENT_CLASS;
+ var attrSelector = '[' + this.CONTENT_ATTR + ']';
+ var contentNodes = query.findMultiple([cssSelector, 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);
+ 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);
+ }
+
+ 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;
+ }
+
+ 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) {
+ 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;
+ }
+ },
+ 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)
+ {
+ if (text && String(text) === text) {
+ return text.replace(/^\s+|\s+$/g, '');
+ }
+
+ return 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);
+ }
+
+ return params;
+ },
+ buildImpressionRequestParams: function (name, piece, target)
+ {
+ var params = 'c_n=' + encodeWrapper(name) +
+ '&c_p=' + encodeWrapper(piece);
+
+ if (target) {
+ params += '&c_t=' + encodeWrapper(target);
+ }
+
+ 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:..., javasript:... 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)
+ {
+ var hasAttr = query.hasNodeAttribute(targetNode, this.CONTENT_IGNOREINTERACTION_ATTR);
+ var hasClass = query.hasNodeCssClass(targetNode, this.CONTENT_IGNOREINTERACTION_CLASS);
+ return hasAttr || hasClass;
+ }
+ };
+
+ /************************************************************
+ * Page Overlay
+ ************************************************************/
+
+ function getPiwikUrlForOverlay(trackerUrl, apiUrl) {
+ if (apiUrl) {
+ return apiUrl;
+ }
+
+ if (trackerUrl.slice(-9) === 'piwik.php') {
+ trackerUrl = trackerUrl.slice(0, trackerUrl.length - 9);
+ }
+
+ return trackerUrl;
+ }
+
+ /*
+ * Check whether this is a page overlay session
+ *
+ * @return boolean
+ *
+ * {@internal side-effect: modifies window.name }}
+ */
+ function isOverlaySession(configTrackerSiteId) {
+ var windowName = 'Piwik_Overlay';
+
+ // check whether we were redirected from the piwik 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],
+ piwikUrl = getPiwikUrlForOverlay(configTrackerUrl, configApiUrl);
+
+ loadScript(
+ piwikUrl + 'plugins/Overlay/client/client.js?v=1',
+ function () {
+ Piwik_Overlay_Client.initialize(piwikUrl, configTrackerSiteId, period, date, segment);
+ }
+ );
+ }
+
+ /************************************************************
+ * End Page Overlay
+ ************************************************************/
+
+ /*
+ * Piwik Tracker class
+ *
+ * trackerUrl and trackerSiteId are optional arguments to the constructor
+ *
+ * See: Tracker.setTrackerUrl() and Tracker.setSiteId()
+ */
+ function Tracker(trackerUrl, siteId) {
+
+ /************************************************************
+ * Private members
+ ************************************************************/
+
+ var
+/*<DEBUG>*/
+ /*
+ * registered test hooks
+ */
+ registeredHooks = {},
+/*</DEBUG>*/
+
+ // Current URL and Referrer URL
+ locationArray = urlFixup(documentAlias.domain, windowAlias.location.href, getReferrer()),
+ domainAlias = domainFixup(locationArray[0]),
+ locationHrefAlias = safeDecodeWrapper(locationArray[1]),
+ configReferrerUrl = safeDecodeWrapper(locationArray[2]),
+
+ enableJSErrorTracking = false,
+
+ defaultRequestMethod = 'GET',
+
+ // Request method (GET or POST)
+ configRequestMethod = defaultRequestMethod,
+
+ defaultRequestContentType = 'application/x-www-form-urlencoded; charset=UTF-8',
+
+ // Request Content-Type header value; applicable when POST request method is used for submitting tracking events
+ configRequestContentType = defaultRequestContentType,
+
+ // Tracker URL
+ configTrackerUrl = trackerUrl || '',
+
+ // API URL (only set if it differs from the Tracker URL)
+ configApiUrl = '',
+
+ // This string is appended to the Tracker URL Request (eg. to send data that is not handled by the existing setters/getters)
+ configAppendToTrackingUrl = '',
+
+ // Site ID
+ configTrackerSiteId = siteId || '',
+
+ // User ID
+ configUserId = '',
+
+ // Visitor UUID
+ visitorUUID = '',
+
+ // Document URL
+ configCustomUrl,
+
+ // Document title
+ configTitle = documentAlias.title,
+
+ // Extensions to be treated as download links
+ configDownloadExtensions = ['7z','aac','apk','arc','arj','asf','asx','avi','azw3','bin','csv','deb','dmg','doc','docx','epub','exe','flv','gif','gz','gzip','hqx','ibooks','jar','jpg','jpeg','js','mobi','mp2','mp3','mp4','mpg','mpeg','mov','movie','msi','msp','odb','odf','odg','ods','odt','ogg','ogv','pdf','phps','png','ppt','pptx','qt','qtm','ra','ram','rar','rpm','sea','sit','tar','tbz','tbz2','bz','bz2','tgz','torrent','txt','wav','wma','wmv','wpd','xls','xlsx','xml','z','zip'],
+
+ // Hosts or alias(es) to not treat as outlinks
+ configHostsAlias = [domainAlias],
+
+ // HTML anchor element classes to not track
+ configIgnoreClasses = [],
+
+ // HTML anchor element classes to treat as downloads
+ configDownloadClasses = [],
+
+ // HTML anchor element classes to treat at outlinks
+ configLinkClasses = [],
+
+ // Maximum delay to wait for spiderweb bug image to be fetched (in milliseconds)
+ configTrackerPause = 500,
+
+ // Minimum visit time after initial page view (in milliseconds)
+ configMinimumVisitTime,
+
+ // Recurring heart beat after initial ping (in milliseconds)
+ configHeartBeatDelay,
+
+ // alias to circumvent circular function dependency (JSLint requires this)
+ heartBeatPingIfActivityAlias,
+
+ // Disallow hash tags in URL
+ configDiscardHashTag,
+
+ // Custom data
+ configCustomData,
+
+ // Campaign names
+ configCampaignNameParameters = [ 'pk_campaign', 'piwik_campaign', 'utm_campaign', 'utm_source', 'utm_medium' ],
+
+ // Campaign keywords
+ configCampaignKeywordParameters = [ 'pk_kwd', 'piwik_kwd', 'utm_term' ],
+
+ // First-party cookie name prefix
+ configCookieNamePrefix = '_pk_',
+
+ // First-party cookie domain
+ // User agent defaults to origin hostname
+ configCookieDomain,
+
+ // First-party cookie path
+ // Default is user agent defined.
+ configCookiePath,
+
+ // Cookies are disabled
+ configCookiesDisabled = false,
+
+ // Do Not Track
+ configDoNotTrack,
+
+ // Count sites which are pre-rendered
+ configCountPreRendered,
+
+ // Do we attribute the conversion to the first referrer or the most recent referrer?
+ configConversionAttributionFirstReferrer,
+
+ // Life of the visitor cookie (in milliseconds)
+ configVisitorCookieTimeout = 33955200000, // 13 months (365 days + 28days)
+
+ // Life of the session cookie (in milliseconds)
+ configSessionCookieTimeout = 1800000, // 30 minutes
+
+ // Life of the referral cookie (in milliseconds)
+ configReferralCookieTimeout = 15768000000, // 6 months
+
+ // Is performance tracking enabled
+ configPerformanceTrackingEnabled = true,
+
+ // Generation time set from the server
+ configPerformanceGenerationTime = 0,
+
+ // Whether Custom Variables scope "visit" should be stored in a cookie during the time of the visit
+ configStoreCustomVariablesInCookie = false,
+
+ // Custom Variables read from cookie, scope "visit"
+ customVariables = false,
+
+ configCustomRequestContentProcessing,
+
+ // Custom Variables, scope "page"
+ customVariablesPage = {},
+
+ // Custom Variables, scope "event"
+ customVariablesEvent = {},
+
+ // Custom Dimensions (can be any scope)
+ customDimensions = {},
+
+ // Custom Variables names and values are each truncated before being sent in the request or recorded in the cookie
+ customVariableMaximumLength = 200,
+
+ // Ecommerce items
+ ecommerceItems = {},
+
+ // Browser features via client-side data collection
+ browserFeatures = {},
+
+ // Keeps track of previously tracked content impressions
+ trackedContentImpressions = [],
+ isTrackOnlyVisibleContentEnabled = false,
+
+ // Guard to prevent empty visits see #6415. If there is a new visitor and the first 2 (or 3 or 4)
+ // tracking requests are at nearly same time (eg trackPageView and trackContentImpression) 2 or more
+ // visits will be created
+ timeNextTrackingRequestCanBeExecutedImmediately = false,
+
+ // Guard against installing the link tracker more than once per Tracker instance
+ linkTrackingInstalled = false,
+ linkTrackingEnabled = false,
+
+ // Guard against installing the activity tracker more than once per Tracker instance
+ heartBeatSetUp = false,
+
+ // Timestamp of last tracker request sent to Piwik
+ lastTrackerRequestTime = null,
+
+ // Handle to the current heart beat timeout
+ heartBeatTimeout,
+
+ // Internal state of the pseudo click handler
+ lastButton,
+ lastTarget,
+
+ // Hash function
+ hash = sha1,
+
+ // Domain hash value
+ domainHash;
+
+ /*
+ * Set cookie value
+ */
+ function setCookie(cookieName, value, msToExpire, path, domain, secure) {
+ if (configCookiesDisabled) {
+ return;
+ }
+
+ var expiryDate;
+
+ // relative time to expire in milliseconds
+ if (msToExpire) {
+ expiryDate = new Date();
+ expiryDate.setTime(expiryDate.getTime() + msToExpire);
+ }
+
+ documentAlias.cookie = cookieName + '=' + encodeWrapper(value) +
+ (msToExpire ? ';expires=' + expiryDate.toGMTString() : '') +
+ ';path=' + (path || '/') +
+ (domain ? ';domain=' + domain : '') +
+ (secure ? ';secure' : '');
+ }
+
+ /*
+ * Get cookie value
+ */
+ function getCookie(cookieName) {
+ if (configCookiesDisabled) {
+ return 0;
+ }
+
+ var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)'),
+ cookieMatch = cookiePattern.exec(documentAlias.cookie);
+
+ return cookieMatch ? decodeWrapper(cookieMatch[2]) : 0;
+ }
+
+ /*
+ * Removes hash tag from the URL
+ *
+ * URLs are purified before being recorded in the cookie,
+ * or before being sent as GET parameters
+ */
+ function purify(url) {
+ var targetPattern;
+
+ if (configDiscardHashTag) {
+ targetPattern = new RegExp('#.*');
+
+ return url.replace(targetPattern, '');
+ }
+
+ return url;
+ }
+
+ /*
+ * Resolve relative reference
+ *
+ * Note: not as described in rfc3986 section 5.2
+ */
+ function resolveRelativeReference(baseUrl, url) {
+ var protocol = getProtocolScheme(url),
+ i;
+
+ if (protocol) {
+ return url;
+ }
+
+ if (url.slice(0, 1) === '/') {
+ return getProtocolScheme(baseUrl) + '://' + getHostName(baseUrl) + url;
+ }
+
+ baseUrl = purify(baseUrl);
+
+ i = baseUrl.indexOf('?');
+ if (i >= 0) {
+ baseUrl = baseUrl.slice(0, i);
+ }
+
+ i = baseUrl.lastIndexOf('/');
+ if (i !== baseUrl.length - 1) {
+ baseUrl = baseUrl.slice(0, i + 1);
+ }
+
+ return baseUrl + url;
+ }
+
+ function isSameHost (hostName, alias) {
+ var offset;
+
+ hostName = String(hostName).toLowerCase();
+ alias = String(alias).toLowerCase();
+
+ if (hostName === alias) {
+ return true;
+ }
+
+ if (alias.slice(0, 1) === '.') {
+ if (hostName === alias.slice(1)) {
+ return true;
+ }
+
+ offset = hostName.length - alias.length;
+
+ if ((offset > 0) && (hostName.slice(offset) === alias)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function stringEndsWith(str, suffix) {
+ str = String(str);
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+
+ function removeCharactersFromEndOfString(str, numCharactersToRemove) {
+ str = String(str);
+ return str.substr(0, str.length - numCharactersToRemove);
+ }
+
+ /*
+ * Extract pathname from URL. element.pathname is actually supported by pretty much all browsers including
+ * IE6 apart from some rare very old ones
+ */
+ function getPathName(url) {
+ var parser = document.createElement('a');
+ if (url.indexOf('//') !== 0 && url.indexOf('http') !== 0) {
+ url = 'http://' + url;
+ }
+
+ parser.href = content.toAbsoluteUrl(url);
+ if (parser.pathname) {
+ return parser.pathname;
+ }
+
+ return '';
+ }
+
+ function isSitePath (path, pathAlias)
+ {
+ var matchesAnyPath = (!pathAlias || pathAlias === '/');
+
+ if (matchesAnyPath) {
+ return true;
+ }
+
+ if (path === pathAlias) {
+ return true;
+ }
+
+ if (!path) {
+ return false;
+ }
+
+ pathAlias = String(pathAlias).toLowerCase();
+ path = String(path).toLowerCase();
+
+ // we need to append slashes so /foobarbaz won't match a site /foobar
+ if (!stringEndsWith(path, '/')) {
+ path += '/';
+ }
+
+ if (!stringEndsWith(pathAlias, '/')) {
+ pathAlias += '/';
+ }
+
+ return path.indexOf(pathAlias) === 0;
+ }
+
+ function isSiteHostPath(host, path)
+ {
+ var i,
+ alias,
+ configAlias,
+ aliasHost,
+ aliasPath;
+
+ for (i = 0; i < configHostsAlias.length; i++) {
+ aliasHost = domainFixup(configHostsAlias[i]);
+ aliasPath = getPathName(configHostsAlias[i]);
+
+ if (isSameHost(host, aliasHost) && isSitePath(path, aliasPath)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Is the host local? (i.e., not an outlink)
+ */
+ function isSiteHostName(hostName) {
+
+ var i,
+ alias,
+ offset;
+
+ for (i = 0; i < configHostsAlias.length; i++) {
+ alias = domainFixup(configHostsAlias[i].toLowerCase());
+
+ if (hostName === alias) {
+ return true;
+ }
+
+ if (alias.slice(0, 1) === '.') {
+ if (hostName === alias.slice(1)) {
+ return true;
+ }
+
+ offset = hostName.length - alias.length;
+
+ if ((offset > 0) && (hostName.slice(offset) === alias)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Send image request to Piwik server using GET.
+ * The infamous spiderweb bug (or beacon) is a transparent, single pixel (1x1) image
+ */
+ function getImage(request, callback) {
+ var image = new Image(1, 1);
+
+ image.onload = function () {
+ iterator = 0; // To avoid JSLint warning of empty block
+ if (typeof callback === 'function') { callback(); }
+ };
+ image.src = configTrackerUrl + (configTrackerUrl.indexOf('?') < 0 ? '?' : '&') + request;
+ }
+
+ /*
+ * POST request to Piwik server using XMLHttpRequest.
+ */
+ function sendXmlHttpRequest(request, callback, fallbackToGet) {
+ if (!isDefined(fallbackToGet) || null === fallbackToGet) {
+ fallbackToGet = true;
+ }
+
+ try {
+ // we use the progid Microsoft.XMLHTTP because
+ // IE5.5 included MSXML 2.5; the progid MSXML2.XMLHTTP
+ // is pinned to MSXML2.XMLHTTP.3.0
+ var xhr = windowAlias.XMLHttpRequest
+ ? new windowAlias.XMLHttpRequest()
+ : windowAlias.ActiveXObject
+ ? new ActiveXObject('Microsoft.XMLHTTP')
+ : null;
+
+ xhr.open('POST', configTrackerUrl, true);
+
+ // fallback on error
+ xhr.onreadystatechange = function () {
+ if (this.readyState === 4 && !(this.status >= 200 && this.status < 300) && fallbackToGet) {
+ getImage(request, callback);
+ } else {
+ if (typeof callback === 'function') { callback(); }
+ }
+ };
+
+ xhr.setRequestHeader('Content-Type', configRequestContentType);
+
+ xhr.send(request);
+ } catch (e) {
+ if (fallbackToGet) {
+ // fallback
+ getImage(request, callback);
+ }
+ }
+ }
+
+ function setExpireDateTime(delay) {
+
+ var now = new Date();
+ var time = now.getTime() + delay;
+
+ if (!expireDateTime || time > expireDateTime) {
+ expireDateTime = time;
+ }
+ }
+
+ /*
+ * Sets up the heart beat timeout.
+ */
+ function heartBeatUp(delay) {
+ if (heartBeatTimeout
+ || !configHeartBeatDelay
+ ) {
+ return;
+ }
+
+ heartBeatTimeout = setTimeout(function heartBeat() {
+ heartBeatTimeout = null;
+ if (heartBeatPingIfActivityAlias()) {
+ return;
+ }
+
+ var now = new Date(),
+ heartBeatDelay = configHeartBeatDelay - (now.getTime() - lastTrackerRequestTime);
+ // sanity check
+ heartBeatDelay = Math.min(configHeartBeatDelay, heartBeatDelay);
+ heartBeatUp(heartBeatDelay);
+ }, delay || configHeartBeatDelay);
+ }
+
+ /*
+ * Removes the heart beat timeout.
+ */
+ function heartBeatDown() {
+ if (!heartBeatTimeout) {
+ return;
+ }
+
+ clearTimeout(heartBeatTimeout);
+ heartBeatTimeout = null;
+ }
+
+ function heartBeatOnFocus() {
+ // since it's possible for a user to come back to a tab after several hours or more, we try to send
+ // a ping if the page is active. (after the ping is sent, the heart beat timeout will be set)
+ if (heartBeatPingIfActivityAlias()) {
+ return;
+ }
+
+ heartBeatUp();
+ }
+
+ function heartBeatOnBlur() {
+ heartBeatDown();
+ }
+
+ /*
+ * Setup event handlers and timeout for initial heart beat.
+ */
+ function setUpHeartBeat() {
+ if (heartBeatSetUp
+ || !configHeartBeatDelay
+ ) {
+ return;
+ }
+
+ heartBeatSetUp = true;
+
+ addEventListener(windowAlias, 'focus', heartBeatOnFocus);
+ addEventListener(windowAlias, 'blur', heartBeatOnBlur);
+
+ heartBeatUp();
+ }
+
+ function makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(callback)
+ {
+ var now = new Date();
+ var timeNow = now.getTime();
+
+ lastTrackerRequestTime = timeNow;
+
+ if (timeNextTrackingRequestCanBeExecutedImmediately && timeNow < timeNextTrackingRequestCanBeExecutedImmediately) {
+ // we are in the time frame shortly after the first request. we have to delay this request a bit to make sure
+ // a visitor has been created meanwhile.
+
+ var timeToWait = timeNextTrackingRequestCanBeExecutedImmediately - timeNow;
+
+ setTimeout(callback, timeToWait);
+ setExpireDateTime(timeToWait + 50); // set timeout is not necessarily executed at timeToWait so delay a bit more
+ timeNextTrackingRequestCanBeExecutedImmediately += 50; // delay next tracking request by further 50ms to next execute them at same time
+
+ return;
+ }
+
+ if (timeNextTrackingRequestCanBeExecutedImmediately === false) {
+ // it is the first request, we want to execute this one directly and delay all the next one(s) within a delay.
+ // All requests after this delay can be executed as usual again
+ var delayInMs = 800;
+ timeNextTrackingRequestCanBeExecutedImmediately = timeNow + delayInMs;
+ }
+
+ callback();
+ }
+
+ /*
+ * Send request
+ */
+ function sendRequest(request, delay, callback) {
+ if (!configDoNotTrack && request) {
+ makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () {
+ if (configRequestMethod === 'POST') {
+ sendXmlHttpRequest(request, callback);
+ } else {
+ getImage(request, callback);
+ }
+
+ setExpireDateTime(delay);
+ });
+ }
+
+ if (!heartBeatSetUp) {
+ setUpHeartBeat(); // setup window events too, but only once
+ } else {
+ heartBeatUp();
+ }
+ }
+
+ function canSendBulkRequest(requests)
+ {
+ if (configDoNotTrack) {
+ return false;
+ }
+
+ return (requests && requests.length);
+ }
+
+ /*
+ * Send requests using bulk
+ */
+ function sendBulkRequest(requests, delay)
+ {
+ if (!canSendBulkRequest(requests)) {
+ return;
+ }
+
+ var bulk = '{"requests":["?' + requests.join('","?') + '"]}';
+
+ makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () {
+ 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 PiwikTracker.php which
+ // will attempt to discover first party cookies. eg. See the PHP Client method getVisitorId()
+ return configCookieNamePrefix + baseName + '.' + configTrackerSiteId + '.' + domainHash;
+ }
+
+ /*
+ * Does browser have cookies enabled (for this site)?
+ */
+ function hasCookies() {
+ if (configCookiesDisabled) {
+ return '0';
+ }
+
+ if (!isDefined(navigatorAlias.cookieEnabled)) {
+ var testCookieName = getCookieName('testcookie');
+ setCookie(testCookieName, '1');
+
+ return getCookie(testCookieName) === '1' ? '1' : '0';
+ }
+
+ return navigatorAlias.cookieEnabled ? '1' : '0';
+ }
+
+ /*
+ * Update domain hash
+ */
+ function updateDomainHash() {
+ domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits
+ }
+
+ /*
+ * Inits the custom variables object
+ */
+ function getCustomVariablesFromCookie() {
+ var cookieName = getCookieName('cvar'),
+ cookie = getCookie(cookieName);
+
+ if (cookie.length) {
+ cookie = JSON2.parse(cookie);
+
+ if (isObject(cookie)) {
+ return cookie;
+ }
+ }
+
+ return {};
+ }
+
+ /*
+ * Lazy loads the custom variables from the cookie, only once during this page view
+ */
+ function loadCustomVariables() {
+ if (customVariables === false) {
+ customVariables = getCustomVariablesFromCookie();
+ }
+ }
+
+ /*
+ * Generate a pseudo-unique ID to fingerprint this user
+ * 16 hexits = 64 bits
+ * note: this isn't a RFC4122-compliant UUID
+ */
+ function generateRandomUuid() {
+ return hash(
+ (navigatorAlias.userAgent || '') +
+ (navigatorAlias.platform || '') +
+ JSON2.stringify(browserFeatures) +
+ (new Date()).getTime() +
+ Math.random()
+ ).slice(0, 16);
+ }
+
+ /*
+ * Load visitor ID cookie
+ */
+ function loadVisitorIdCookie() {
+ 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,
+
+ // visitCount - 0 = no previous visit
+ 0,
+
+ // current visit timestamp
+ nowTs,
+
+ // last visit timestamp - blank = no previous visit
+ '',
+
+ // last ecommerce order timestamp
+ ''
+ ];
+
+ 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],
+ visitCount = cookieVisitorIdValue[3],
+ currentVisitTs = cookieVisitorIdValue[4],
+ lastVisitTs = cookieVisitorIdValue[5];
+
+ // case migrating from pre-1.5 cookies
+ if (!isDefined(cookieVisitorIdValue[6])) {
+ cookieVisitorIdValue[6] = "";
+ }
+
+ var lastEcommerceOrderTs = cookieVisitorIdValue[6];
+
+ return {
+ newVisitor: newVisitor,
+ uuid: uuid,
+ createTs: createTs,
+ visitCount: visitCount,
+ currentVisitTs: currentVisitTs,
+ lastVisitTs: lastVisitTs,
+ lastEcommerceOrderTs: lastEcommerceOrderTs
+ };
+ }
+
+
+ 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 + '.' +
+ visitorIdCookieValues.visitCount + '.' +
+ nowTs + '.' +
+ visitorIdCookieValues.lastVisitTs + '.' +
+ visitorIdCookieValues.lastEcommerceOrderTs;
+
+ setCookie(getCookieName('id'), cookieValue, getRemainingVisitorCookieTimeout(), configCookiePath, configCookieDomain);
+ }
+
+ /*
+ * 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 = JSON2.parse(cookie);
+ if (isObject(cookie)) {
+ return cookie;
+ }
+ } catch (ignore) {
+ // Pre 1.3, this cookie was not JSON encoded
+ }
+ }
+
+ return [
+ '',
+ '',
+ 0,
+ ''
+ ];
+ }
+
+ function deleteCookie(cookieName, path, domain) {
+ setCookie(cookieName, '', -86400, path, domain);
+ }
+
+ function isPossibleToSetCookieOnDomain(domainToTest)
+ {
+ var valueToSet = 'testvalue';
+ setCookie('test', valueToSet, 10000, null, domainToTest);
+
+ if (getCookie('test') === valueToSet) {
+ deleteCookie('test', null, domainToTest);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ function deleteCookies() {
+ var savedConfigCookiesDisabled = configCookiesDisabled;
+
+ // Temporarily allow cookies just to delete the existing ones
+ configCookiesDisabled = false;
+
+ var cookiesToDelete = ['id', 'ses', 'cvar', 'ref'];
+ var index, cookieName;
+
+ for (index = 0; index < cookiesToDelete.length; index++) {
+ cookieName = getCookieName(cookiesToDelete[index]);
+ if (0 !== getCookie(cookieName)) {
+ deleteCookie(cookieName, configCookiePath, configCookieDomain);
+ }
+ }
+
+ configCookiesDisabled = savedConfigCookiesDisabled;
+ }
+
+ function setSiteId(siteId) {
+ configTrackerSiteId = siteId;
+ setVisitorIdCookie();
+ }
+
+ 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'), '*', configSessionCookieTimeout, configCookiePath, configCookieDomain);
+ }
+
+ /**
+ * Returns the URL to call piwik.php,
+ * with the standard parameters (plugins, resolution, url, referrer, etc.).
+ * Sends the pageview and browser settings with every request in case of race conditions.
+ */
+ function getRequest(request, customData, pluginMethod, currentEcommerceOrderTs) {
+ var i,
+ now = new Date(),
+ nowTs = Math.round(now.getTime() / 1000),
+ referralTs,
+ referralUrl,
+ referralUrlMaxLength = 1024,
+ currentReferrerHostName,
+ originalReferrerHostName,
+ customVariablesCopy = customVariables,
+ cookieSessionName = getCookieName('ses'),
+ cookieReferrerName = getCookieName('ref'),
+ cookieCustomVariablesName = getCookieName('cvar'),
+ cookieSessionValue = getCookie(cookieSessionName),
+ attributionCookie = loadReferrerAttributionCookie(),
+ currentUrl = configCustomUrl || locationHrefAlias,
+ campaignNameDetected,
+ campaignKeywordDetected;
+
+ if (configCookiesDisabled) {
+ deleteCookies();
+ }
+
+ if (configDoNotTrack) {
+ return '';
+ }
+
+ var cookieVisitorIdValues = getValuesFromVisitorIdCookie();
+ if (!isDefined(currentEcommerceOrderTs)) {
+ currentEcommerceOrderTs = "";
+ }
+
+ // 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 Piwik
+ var charSet = documentAlias.characterSet || documentAlias.charset;
+
+ if (!charSet || charSet.toLowerCase() === 'utf-8') {
+ charSet = null;
+ }
+
+ campaignNameDetected = attributionCookie[0];
+ campaignKeywordDetected = attributionCookie[1];
+ referralTs = attributionCookie[2];
+ referralUrl = attributionCookie[3];
+
+ if (!cookieSessionValue) {
+ // cookie 'ses' was not found: we consider this the start of a 'session'
+
+ // here we make sure that if 'ses' cookie is deleted few times within the visit
+ // and so this code path is triggered many times for one visit,
+ // we only increase visitCount once per Visit window (default 30min)
+ var visitDuration = configSessionCookieTimeout / 1000;
+ if (!cookieVisitorIdValues.lastVisitTs
+ || (nowTs - cookieVisitorIdValues.lastVisitTs) > visitDuration) {
+ cookieVisitorIdValues.visitCount++;
+ cookieVisitorIdValues.lastVisitTs = cookieVisitorIdValues.currentVisitTs;
+ }
+
+
+ // Detect the campaign information from the current URL
+ // Only if campaign wasn't previously set
+ // Or if it was set but we must attribute to the most recent one
+ // Note: we are working on the currentUrl before purify() since we can parse the campaign parameters in the hash tag
+ if (!configConversionAttributionFirstReferrer
+ || !campaignNameDetected.length) {
+ for (i in configCampaignNameParameters) {
+ if (Object.prototype.hasOwnProperty.call(configCampaignNameParameters, i)) {
+ campaignNameDetected = getParameter(currentUrl, configCampaignNameParameters[i]);
+
+ if (campaignNameDetected.length) {
+ break;
+ }
+ }
+ }
+
+ for (i in configCampaignKeywordParameters) {
+ if (Object.prototype.hasOwnProperty.call(configCampaignKeywordParameters, i)) {
+ campaignKeywordDetected = getParameter(currentUrl, configCampaignKeywordParameters[i]);
+
+ if (campaignKeywordDetected.length) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Store the referrer URL and time in the cookie;
+ // referral URL depends on the first or last referrer attribution
+ currentReferrerHostName = getHostName(configReferrerUrl);
+ originalReferrerHostName = referralUrl.length ? getHostName(referralUrl) : '';
+
+ if (currentReferrerHostName.length && // there is a referrer
+ !isSiteHostName(currentReferrerHostName) && // domain is not the current domain
+ (!configConversionAttributionFirstReferrer || // attribute to last known referrer
+ !originalReferrerHostName.length || // previously empty
+ isSiteHostName(originalReferrerHostName))) { // previously set but in current domain
+ referralUrl = configReferrerUrl;
+ }
+
+ // Set the referral cookie if we have either a Referrer URL, or detected a Campaign (or both)
+ if (referralUrl.length
+ || campaignNameDetected.length) {
+ referralTs = nowTs;
+ attributionCookie = [
+ campaignNameDetected,
+ campaignKeywordDetected,
+ referralTs,
+ purify(referralUrl.slice(0, referralUrlMaxLength))
+ ];
+
+ setCookie(cookieReferrerName, JSON2.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain);
+ }
+ }
+
+ // build out the rest of the request
+ request += '&idsite=' + configTrackerSiteId +
+ '&rec=1' +
+ '&r=' + String(Math.random()).slice(2, 8) + // keep the string to a minimum
+ '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() +
+ '&url=' + encodeWrapper(purify(currentUrl)) +
+ (configReferrerUrl.length ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') +
+ ((configUserId && configUserId.length) ? '&uid=' + encodeWrapper(configUserId) : '') +
+ '&_id=' + cookieVisitorIdValues.uuid + '&_idts=' + cookieVisitorIdValues.createTs + '&_idvc=' + cookieVisitorIdValues.visitCount +
+ '&_idn=' + cookieVisitorIdValues.newVisitor + // currently unused
+ (campaignNameDetected.length ? '&_rcn=' + encodeWrapper(campaignNameDetected) : '') +
+ (campaignKeywordDetected.length ? '&_rck=' + encodeWrapper(campaignKeywordDetected) : '') +
+ '&_refts=' + referralTs +
+ '&_viewts=' + cookieVisitorIdValues.lastVisitTs +
+ (String(cookieVisitorIdValues.lastEcommerceOrderTs).length ? '&_ects=' + cookieVisitorIdValues.lastEcommerceOrderTs : '') +
+ (String(referralUrl).length ? '&_ref=' + encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))) : '') +
+ (charSet ? '&cs=' + encodeWrapper(charSet) : '') +
+ '&send_image=0';
+
+ // 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 + '=' + customData[i];
+ delete customData[i];
+ }
+ }
+ }
+
+ if (customData && isObjectEmpty(customData)) {
+ customData = null;
+ // we deleted all keys from custom data
+ }
+
+ // custom dimensions
+ for (i in customDimensions) {
+ if (Object.prototype.hasOwnProperty.call(customDimensions, i)) {
+ var isNotSetYet = (-1 === customDimensionIdsAlreadyHandled.indexOf(i));
+ if (isNotSetYet) {
+ request += '&dimension' + i + '=' + customDimensions[i];
+ }
+ }
+ }
+
+ // custom data
+ if (customData) {
+ request += '&data=' + encodeWrapper(JSON2.stringify(customData));
+ } else if (configCustomData) {
+ request += '&data=' + encodeWrapper(JSON2.stringify(configCustomData));
+ }
+
+ // Custom Variables, scope "page"
+ function appendCustomVariablesToRequest(customVariables, parameterName) {
+ var customVariablesStringified = JSON2.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, JSON2.stringify(customVariables), configSessionCookieTimeout, configCookiePath, configCookieDomain);
+ }
+ }
+
+ // performance tracking
+ if (configPerformanceTrackingEnabled) {
+ if (configPerformanceGenerationTime) {
+ request += '&gt_ms=' + configPerformanceGenerationTime;
+ } else if (performanceAlias && performanceAlias.timing
+ && performanceAlias.timing.requestStart && performanceAlias.timing.responseEnd) {
+ request += '&gt_ms=' + (performanceAlias.timing.responseEnd - performanceAlias.timing.requestStart);
+ }
+ }
+
+ // update cookies
+ cookieVisitorIdValues.lastEcommerceOrderTs = isDefined(currentEcommerceOrderTs) && String(currentEcommerceOrderTs).length ? currentEcommerceOrderTs : cookieVisitorIdValues.lastEcommerceOrderTs;
+ setVisitorIdCookie(cookieVisitorIdValues);
+ setSessionCookie();
+
+ // tracker plugin hook
+ request += executePluginMethod(pluginMethod);
+
+ 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();
+ if (lastTrackerRequestTime + configHeartBeatDelay <= now.getTime()) {
+ var requestPing = getRequest('ping=1', null, 'ping');
+ sendRequest(requestPing, configTrackerPause);
+
+ return true;
+ }
+
+ return false;
+ };
+
+ function logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount) {
+ var request = 'idgoal=0',
+ lastEcommerceOrderTs,
+ now = new Date(),
+ items = [],
+ sku;
+
+ if (String(orderId).length) {
+ request += '&ec_id=' + encodeWrapper(orderId);
+ // Record date of order in the visitor cookie
+ lastEcommerceOrderTs = Math.round(now.getTime() / 1000);
+ }
+
+ request += '&revenue=' + grandTotal;
+
+ if (String(subTotal).length) {
+ request += '&ec_st=' + subTotal;
+ }
+
+ if (String(tax).length) {
+ request += '&ec_tx=' + tax;
+ }
+
+ if (String(shipping).length) {
+ request += '&ec_sh=' + shipping;
+ }
+
+ if (String(discount).length) {
+ request += '&ec_dt=' + discount;
+ }
+
+ if (ecommerceItems) {
+ // Removing the SKU index in the array before JSON encoding
+ for (sku in ecommerceItems) {
+ if (Object.prototype.hasOwnProperty.call(ecommerceItems, sku)) {
+ // Ensure name and category default to healthy value
+ if (!isDefined(ecommerceItems[sku][1])) {
+ ecommerceItems[sku][1] = "";
+ }
+
+ if (!isDefined(ecommerceItems[sku][2])) {
+ ecommerceItems[sku][2] = "";
+ }
+
+ // Set price to zero
+ if (!isDefined(ecommerceItems[sku][3])
+ || String(ecommerceItems[sku][3]).length === 0) {
+ ecommerceItems[sku][3] = 0;
+ }
+
+ // Set quantity to 1
+ if (!isDefined(ecommerceItems[sku][4])
+ || String(ecommerceItems[sku][4]).length === 0) {
+ ecommerceItems[sku][4] = 1;
+ }
+
+ items.push(ecommerceItems[sku]);
+ }
+ }
+ request += '&ec_items=' + encodeWrapper(JSON2.stringify(items));
+ }
+ request = getRequest(request, configCustomData, 'ecommerce', lastEcommerceOrderTs);
+ sendRequest(request, configTrackerPause);
+ }
+
+ function logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount) {
+ if (String(orderId).length
+ && isDefined(grandTotal)) {
+ logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount);
+ }
+ }
+
+ function logEcommerceCartUpdate(grandTotal) {
+ if (isDefined(grandTotal)) {
+ logEcommerce("", grandTotal, "", "", "", "");
+ }
+ }
+
+ /*
+ * Log the page view / visit
+ */
+ function logPageView(customTitle, customData) {
+ var now = new Date(),
+ request = getRequest('action_name=' + encodeWrapper(titleFixup(customTitle || configTitle)), customData, 'log');
+
+ sendRequest(request, configTrackerPause);
+ }
+
+ /*
+ * Construct regular expression of classes
+ */
+ function getClassesRegExp(configClasses, defaultClass) {
+ var i,
+ classesRegExp = '(^| )(piwik[_-]' + defaultClass;
+
+ if (configClasses) {
+ for (i = 0; i < configClasses.length; i++) {
+ classesRegExp += '|' + configClasses[i];
+ }
+ }
+
+ classesRegExp += ')( |$)';
+
+ return new RegExp(classesRegExp);
+ }
+
+ 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');
+
+ if (startsUrlWithTrackerUrl(href)) {
+ return;
+ }
+
+ 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):', '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 buildContentInteractionTrackingRedirectUrl(url, contentInteraction, contentName, contentPiece, contentTarget)
+ {
+ if (!isDefined(url)) {
+ return;
+ }
+
+ if (startsUrlWithTrackerUrl(url)) {
+ return url;
+ }
+
+ var redirectUrl = content.toAbsoluteUrl(url);
+ var request = 'redirecturl=' + encodeWrapper(redirectUrl) + '&';
+ request += buildContentInteractionRequest(contentInteraction, contentName, contentPiece, (contentTarget || url));
+
+ var separator = '&';
+ if (configTrackerUrl.indexOf('?') < 0) {
+ separator = '?';
+ }
+
+ return configTrackerUrl + separator + request;
+ }
+
+ 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:
+ * <div data-track-content><a href="Y" data-content-target>Y</a><img src=""/><a href="Z">Z</a></div>
+ *
+ * The user clicked in this case on link Z and not on target Y
+ */
+ return false;
+ }
+
+ return true;
+ }
+
+ function getContentInteractionToRequestIfPossible (anyNode, interaction, fallbackTarget)
+ {
+ if (!anyNode) {
+ return;
+ }
+
+ var contentNode = content.findParentContentNode(anyNode);
+
+ if (!contentNode) {
+ // we are not within a content block
+ return;
+ }
+
+ if (!isNodeAuthorizedToTriggerInteraction(contentNode, anyNode)) {
+ return;
+ }
+
+ var contentBlock = content.buildContentBlock(contentNode);
+
+ if (!contentBlock) {
+ return;
+ }
+
+ if (!contentBlock.target && fallbackTarget) {
+ contentBlock.target = fallbackTarget;
+ }
+
+ return content.buildInteractionRequestParams(interaction, contentBlock.name, contentBlock.piece, contentBlock.target);
+ }
+
+ function wasContentImpressionAlreadyTracked(contentBlock)
+ {
+ if (!trackedContentImpressions || !trackedContentImpressions.length) {
+ return false;
+ }
+
+ var index, trackedContent;
+
+ for (index = 0; index < trackedContentImpressions.length; index++) {
+ trackedContent = trackedContentImpressions[index];
+
+ if (trackedContent &&
+ trackedContent.name === contentBlock.name &&
+ trackedContent.piece === contentBlock.piece &&
+ trackedContent.target === contentBlock.target) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function replaceHrefIfInternalLink(contentBlock)
+ {
+ if (!contentBlock) {
+ return false;
+ }
+
+ var targetNode = content.findTargetNode(contentBlock);
+
+ if (!targetNode || content.shouldIgnoreInteraction(targetNode)) {
+ return false;
+ }
+
+ var link = getLinkIfShouldBeProcessed(targetNode);
+ if (linkTrackingEnabled && link && link.type) {
+
+ return false; // will be handled via outlink or download.
+ }
+
+ if (query.isLinkElement(targetNode) &&
+ query.hasNodeAttributeWithValue(targetNode, 'href')) {
+ var url = String(query.getAttributeValueFromNode(targetNode, 'href'));
+
+ if (0 === url.indexOf('#')) {
+ return false;
+ }
+
+ if (startsUrlWithTrackerUrl(url)) {
+ return true;
+ }
+
+ if (!content.isUrlToCurrentDomain(url)) {
+ return false;
+ }
+
+ var block = content.buildContentBlock(contentBlock);
+
+ if (!block) {
+ return;
+ }
+
+ var contentName = block.name;
+ var contentPiece = block.piece;
+ var contentTarget = block.target;
+
+ if (!query.hasNodeAttributeWithValue(targetNode, content.CONTENT_TARGET_ATTR) || targetNode.wasContentTargetAttrReplaced) {
+ // make sure we still track the correct content target when an interaction is happening
+ targetNode.wasContentTargetAttrReplaced = true;
+ contentTarget = content.toAbsoluteUrl(url);
+ query.setAnyAttribute(targetNode, content.CONTENT_TARGET_ATTR, contentTarget);
+ }
+
+ var targetUrl = buildContentInteractionTrackingRedirectUrl(url, 'click', contentName, contentPiece, contentTarget);
+
+ // location.href does not respect target=_blank so we prefer to use this
+ content.setHrefAttribute(targetNode, targetUrl);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ function replaceHrefsIfInternalLink(contentNodes)
+ {
+ if (!contentNodes || !contentNodes.length) {
+ return;
+ }
+
+ var index;
+ for (index = 0; index < contentNodes.length; index++) {
+ replaceHrefIfInternalLink(contentNodes[index]);
+ }
+ }
+
+ function trackContentImpressionClickInteraction (targetNode)
+ {
+ return function (event) {
+
+ if (!targetNode) {
+ return;
+ }
+
+ var contentBlock = content.findParentContentNode(targetNode);
+
+ var interactedElement;
+ if (event) {
+ interactedElement = event.target || event.srcElement;
+ }
+ if (!interactedElement) {
+ interactedElement = targetNode;
+ }
+
+ if (!isNodeAuthorizedToTriggerInteraction(contentBlock, interactedElement)) {
+ return;
+ }
+
+ setExpireDateTime(configTrackerPause);
+
+ if (query.isLinkElement(targetNode) &&
+ query.hasNodeAttributeWithValue(targetNode, 'href') &&
+ query.hasNodeAttributeWithValue(targetNode, content.CONTENT_TARGET_ATTR)) {
+ // there is a href attribute, the link was replaced with piwik.php but later the href was changed again by the application.
+ var href = query.getAttributeValueFromNode(targetNode, 'href');
+ if (!startsUrlWithTrackerUrl(href) && targetNode.wasContentTargetAttrReplaced) {
+ query.setAnyAttribute(targetNode, content.CONTENT_TARGET_ATTR, '');
+ }
+ }
+
+ var link = getLinkIfShouldBeProcessed(targetNode);
+
+ if (linkTrackingInstalled && link && link.type) {
+ // click ignore, will be tracked via processClick, we do not want to track it twice
+
+ return link.type;
+ }
+
+ if (replaceHrefIfInternalLink(contentBlock)) {
+ return 'href';
+ }
+
+ var block = content.buildContentBlock(contentBlock);
+
+ if (!block) {
+ return;
+ }
+
+ var contentName = block.name;
+ var contentPiece = block.piece;
+ var contentTarget = block.target;
+
+ // click on any non link element, or on a link element that has not an href attribute or on an anchor
+ var request = buildContentInteractionRequest('click', contentName, contentPiece, contentTarget);
+ sendRequest(request, configTrackerPause);
+
+ return request;
+ };
+ }
+
+ function setupInteractionsTracking(contentNodes)
+ {
+ if (!contentNodes || !contentNodes.length) {
+ return;
+ }
+
+ var index, targetNode;
+ for (index = 0; index < contentNodes.length; index++) {
+ targetNode = content.findTargetNode(contentNodes[index]);
+
+ if (targetNode && !targetNode.contentInteractionTrackingSetupDone) {
+ targetNode.contentInteractionTrackingSetupDone = true;
+
+ addEventListener(targetNode, 'click', trackContentImpressionClickInteraction(targetNode));
+ }
+ }
+ }
+
+ /*
+ * Log all content pieces
+ */
+ function buildContentImpressionsRequests(contents, contentNodes)
+ {
+ if (!contents || !contents.length) {
+ return [];
+ }
+
+ var index, request;
+
+ for (index = 0; index < contents.length; index++) {
+
+ if (wasContentImpressionAlreadyTracked(contents[index])) {
+ contents.splice(index, 1);
+ index--;
+ } else {
+ trackedContentImpressions.push(contents[index]);
+ }
+ }
+
+ if (!contents || !contents.length) {
+ return [];
+ }
+
+ replaceHrefsIfInternalLink(contentNodes);
+ setupInteractionsTracking(contentNodes);
+
+ var requests = [];
+
+ for (index = 0; index < contents.length; index++) {
+
+ request = getRequest(
+ content.buildImpressionRequestParams(contents[index].name, contents[index].piece, contents[index].target),
+ undefined,
+ 'contentImpressions'
+ );
+
+ requests.push(request);
+ }
+
+ return requests;
+ }
+
+ /*
+ * Log all content pieces
+ */
+ function getContentImpressionsRequestsFromNodes(contentNodes)
+ {
+ var contents = content.collectContent(contentNodes);
+
+ return buildContentImpressionsRequests(contents, contentNodes);
+ }
+
+ /*
+ * Log currently visible content pieces
+ */
+ function getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes)
+ {
+ if (!contentNodes || !contentNodes.length) {
+ return [];
+ }
+
+ var index;
+
+ for (index = 0; index < contentNodes.length; index++) {
+ if (!content.isNodeVisible(contentNodes[index])) {
+ contentNodes.splice(index, 1);
+ index--;
+ }
+ }
+
+ if (!contentNodes || !contentNodes.length) {
+ return [];
+ }
+
+ return getContentImpressionsRequestsFromNodes(contentNodes);
+ }
+
+ function buildContentImpressionRequest(contentName, contentPiece, contentTarget)
+ {
+ var params = content.buildImpressionRequestParams(contentName, contentPiece, contentTarget);
+
+ return getRequest(params, null, 'contentImpression');
+ }
+
+ function buildContentInteractionRequestNode(node, contentInteraction)
+ {
+ if (!node) {
+ return;
+ }
+
+ var contentNode = content.findParentContentNode(node);
+ var contentBlock = content.buildContentBlock(contentNode);
+
+ if (!contentBlock) {
+ return;
+ }
+
+ if (!contentInteraction) {
+ contentInteraction = 'Unknown';
+ }
+
+ return buildContentInteractionRequest(contentInteraction, contentBlock.name, contentBlock.piece, contentBlock.target);
+ }
+
+ function buildEventRequest(category, action, name, value)
+ {
+ return 'e_c=' + encodeWrapper(category)
+ + '&e_a=' + encodeWrapper(action)
+ + (isDefined(name) ? '&e_n=' + encodeWrapper(name) : '')
+ + (isDefined(value) ? '&e_v=' + encodeWrapper(value) : '');
+ }
+
+ /*
+ * Log the event
+ */
+ function logEvent(category, action, name, value, customData)
+ {
+ // Category and Action are required parameters
+ if (String(category).length === 0 || String(action).length === 0) {
+ return false;
+ }
+ var request = getRequest(
+ buildEventRequest(category, action, name, value),
+ customData,
+ 'event'
+ );
+
+ sendRequest(request, configTrackerPause);
+ }
+
+ /*
+ * Log the site search request
+ */
+ function logSiteSearch(keyword, category, resultsCount, customData) {
+ var request = getRequest('search=' + encodeWrapper(keyword)
+ + (category ? '&search_cat=' + encodeWrapper(category) : '')
+ + (isDefined(resultsCount) ? '&search_count=' + resultsCount : ''), customData, 'sitesearch');
+
+ sendRequest(request, configTrackerPause);
+ }
+
+ /*
+ * Log the goal with the server
+ */
+ function logGoal(idGoal, customRevenue, customData) {
+ var request = getRequest('idgoal=' + idGoal + (customRevenue ? '&revenue=' + customRevenue : ''), customData, 'goal');
+
+ sendRequest(request, configTrackerPause);
+ }
+
+ /*
+ * Log the link or click with the server
+ */
+ function logLink(url, linkType, customData, callback, sourceElement) {
+
+ var linkParams = linkType + '=' + encodeWrapper(purify(url));
+
+ var interaction = getContentInteractionToRequestIfPossible(sourceElement, 'click', url);
+
+ if (interaction) {
+ linkParams += '&' + interaction;
+ }
+
+ var request = getRequest(linkParams, customData, 'link');
+
+ sendRequest(request, (callback ? 0 : configTrackerPause), callback);
+ }
+
+ /*
+ * Browser prefix
+ */
+ function prefixPropertyName(prefix, propertyName) {
+ if (prefix !== '') {
+ return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1);
+ }
+
+ return propertyName;
+ }
+
+ /*
+ * Check for pre-rendered spiderweb pages, and log the page view/link/goal
+ * according to the configuration and/or visibility
+ *
+ * @see http://dvcs.w3.org/hg/spiderwebperf/raw-file/tip/specs/PageVisibility/Overview.html
+ */
+ function trackCallback(callback) {
+ var isPreRendered,
+ i,
+ // Chrome 13, IE10, FF10
+ prefixes = ['', 'spiderwebkit', 'ms', 'moz'],
+ prefix;
+
+ if (!configCountPreRendered) {
+ for (i = 0; i < prefixes.length; i++) {
+ prefix = prefixes[i];
+
+ // does this browser support the page visibility API?
+ if (Object.prototype.hasOwnProperty.call(documentAlias, prefixPropertyName(prefix, 'hidden'))) {
+ // if pre-rendered, then defer callback until page visibility changes
+ if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') {
+ isPreRendered = true;
+ }
+ break;
+ }
+ }
+ }
+
+ if (isPreRendered) {
+ // note: the event name doesn't follow the same naming convention as vendor properties
+ addEventListener(documentAlias, prefix + 'visibilitychange', function ready() {
+ documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false);
+ callback();
+ });
+
+ return;
+ }
+
+ // configCountPreRendered === true || isPreRendered === false
+ callback();
+ }
+
+ function trackCallbackOnLoad(callback)
+ {
+ if (documentAlias.readyState === 'complete') {
+ callback();
+ } else if (windowAlias.addEventListener) {
+ windowAlias.addEventListener('load', callback);
+ } 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();
+ } else if (documentAlias.addEventListener) {
+ documentAlias.addEventListener('DOMContentLoaded', callback);
+ } else if (documentAlias.attachEvent) {
+ documentAlias.attachEvent('onreadystatechange', callback);
+ }
+ }
+
+ /*
+ * Process clicks
+ */
+ function processClick(sourceElement) {
+ var link = getLinkIfShouldBeProcessed(sourceElement);
+
+ if (link && link.type) {
+ link.href = safeDecodeWrapper(link.href);
+ logLink(link.href, link.type, undefined, null, sourceElement);
+ }
+ }
+
+ function isIE8orOlder()
+ {
+ return documentAlias.all && !documentAlias.addEventListener;
+ }
+
+ function getKeyCodeFromEvent(event)
+ {
+ // event.which is deprecated https://developer.mozilla.org/en-US/docs/Spiderweb/API/KeyboardEvent/which
+ var which = event.which;
+
+ /**
+ 1 : Left mouse button
+ 2 : Wheel button or middle button
+ 3 : Right mouse button
+ */
+
+ var typeOfEventButton = (typeof event.button);
+
+ if (!which && typeOfEventButton !== 'undefined' ) {
+ /**
+ -1: No button pressed
+ 0 : Main button pressed, usually the left button
+ 1 : Auxiliary button pressed, usually the wheel button or themiddle button (if present)
+ 2 : Secondary button pressed, usually the right button
+ 3 : Fourth button, typically the Browser Back button
+ 4 : Fifth button, typically the Browser Forward button
+
+ IE8 and earlier has different values:
+ 1 : Left mouse button
+ 2 : Right mouse button
+ 4 : Wheel button or middle button
+
+ For a left-hand configured mouse, the return values are reversed. We do not take care of that.
+ */
+
+ if (isIE8orOlder()) {
+ if (event.button & 1) {
+ which = 1;
+ } else if (event.button & 2) {
+ which = 3;
+ } else if (event.button & 4) {
+ which = 2;
+ }
+ } else {
+ if (event.button === 0 || event.button === '0') {
+ which = 1;
+ } else if (event.button & 1) {
+ which = 2;
+ } else if (event.button & 2) {
+ which = 3;
+ }
+ }
+ }
+
+ return which;
+ }
+
+ function getNameOfClickedButton(event)
+ {
+ switch (getKeyCodeFromEvent(event)) {
+ case 1:
+ return 'left';
+ case 2:
+ return 'middle';
+ case 3:
+ return 'right';
+ }
+ }
+
+ function getTargetElementFromEvent(event)
+ {
+ return event.target || event.srcElement;
+ }
+
+ /*
+ * Handle click event
+ */
+ function clickHandler(enable) {
+
+ return function (event) {
+
+ event = event || windowAlias.event;
+
+ var button = getNameOfClickedButton(event);
+ var target = getTargetElementFromEvent(event);
+
+ if (event.type === 'click') {
+
+ var ignoreClick = false;
+ if (enable && button === 'middle') {
+ // if enabled, we track middle clicks via mouseup
+ // some browsers (eg chrome) trigger click and mousedown/up events when middle is clicked,
+ // whereas some do not. This way we make "sure" to track them only once, either in click
+ // (default) or in mouseup (if enable == true)
+ ignoreClick = true;
+ }
+
+ if (target && !ignoreClick) {
+ processClick(target);
+ }
+ } else if (event.type === 'mousedown') {
+ if (button === 'middle' && target) {
+ lastButton = button;
+ lastTarget = target;
+ } else {
+ lastButton = lastTarget = null;
+ }
+ } else if (event.type === 'mouseup') {
+ if (button === lastButton && target === lastTarget) {
+ processClick(target);
+ }
+ lastButton = lastTarget = null;
+ } else if (event.type === 'contextmenu') {
+ processClick(target);
+ }
+ };
+ }
+
+ /*
+ * Add click listener to a DOM element
+ */
+ function addClickListener(element, enable) {
+ addEventListener(element, 'click', clickHandler(enable), false);
+
+ if (enable) {
+ addEventListener(element, 'mouseup', clickHandler(enable), false);
+ addEventListener(element, 'mousedown', clickHandler(enable), false);
+ addEventListener(element, 'contextmenu', clickHandler(enable), false);
+ }
+ }
+
+ /*
+ * Add click handlers to anchor and AREA elements, except those to be ignored
+ */
+ function addClickListeners(enable) {
+ if (!linkTrackingInstalled) {
+ linkTrackingInstalled = true;
+
+ // iterate through anchor elements with href and AREA elements
+
+ var i,
+ ignorePattern = getClassesRegExp(configIgnoreClasses, 'ignore'),
+ linkElements = documentAlias.links;
+
+ if (linkElements) {
+ for (i = 0; i < linkElements.length; i++) {
+ if (!ignorePattern.test(linkElements[i].className)) {
+ addClickListener(linkElements[i], enable);
+ }
+ }
+ }
+ }
+ }
+
+
+ function enableTrackOnlyVisibleContent (checkOnSroll, timeIntervalInMs, tracker) {
+
+ if (isTrackOnlyVisibleContentEnabled) {
+ // already enabled, do not register intervals again
+ return true;
+ }
+
+ isTrackOnlyVisibleContentEnabled = true;
+
+ var didScroll = false;
+ var events, index;
+
+ function setDidScroll() { didScroll = true; }
+
+ trackCallbackOnLoad(function () {
+
+ function checkContent(intervalInMs) {
+ setTimeout(function () {
+ if (!isTrackOnlyVisibleContentEnabled) {
+ return; // the tests stopped tracking only visible content
+ }
+ didScroll = false;
+ tracker.trackVisibleContentImpressions();
+ checkContent(intervalInMs);
+ }, intervalInMs);
+ }
+
+ function checkContentIfDidScroll(intervalInMs) {
+
+ setTimeout(function () {
+ if (!isTrackOnlyVisibleContentEnabled) {
+ return; // the tests stopped tracking only visible content
+ }
+
+ if (didScroll) {
+ didScroll = false;
+ tracker.trackVisibleContentImpressions();
+ }
+
+ checkContentIfDidScroll(intervalInMs);
+ }, intervalInMs);
+ }
+
+ if (checkOnSroll) {
+
+ // scroll event is executed after each pixel, so we make sure not to
+ // execute event too often. otherwise FPS goes down a lot!
+ events = ['scroll', 'resize'];
+ for (index = 0; index < events.length; index++) {
+ if (documentAlias.addEventListener) {
+ documentAlias.addEventListener(events[index], setDidScroll);
+ } else {
+ windowAlias.attachEvent('on' + events[index], setDidScroll);
+ }
+ }
+
+ checkContentIfDidScroll(100);
+ }
+
+ if (timeIntervalInMs && timeIntervalInMs > 0) {
+ timeIntervalInMs = parseInt(timeIntervalInMs, 10);
+ checkContent(timeIntervalInMs);
+ }
+
+ });
+ }
+
+ /**
+ * Note: While we check whether the user is on a configHostAlias path we do not check whether the user is
+ * actually on the configHostAlias domain. This is already done where this method is called and for
+ * simplicity we do not check this again.
+ *
+ * Also we currently assume that all configHostAlias domains start with the same wild card of '*.', '.' or
+ * none. Eg either all like '*.piwik.org' or '.piwik.org' or 'piwik.org'. Piwik always adds '*.' so it
+ * should be fine.
+ */
+ function findConfigCookiePathToUse(configHostAlias, currentUrl)
+ {
+ var aliasPath = getPathName(configHostAlias);
+ var currentPath = getPathName(currentUrl);
+
+ if (!aliasPath || aliasPath === '/' || !currentPath || currentPath === '/') {
+ // no path set that would be useful for cookiePath
+ return;
+ }
+
+ var aliasDomain = domainFixup(configHostAlias);
+
+ if (isSiteHostPath(aliasDomain, '/')) {
+ // there is another configHostsAlias having same domain that allows all paths
+ // eg this alias is for piwik.org/support but there is another alias allowing
+ // piwik.org
+ return;
+ }
+
+ if (stringEndsWith(aliasPath, '/')) {
+ aliasPath = removeCharactersFromEndOfString(aliasPath, 1);
+ }
+
+ // eg if we're in the case of "apache.piwik/foo/bar" we check whether there is maybe
+ // also a config alias allowing "apache.piwik/foo". In this case we're not allowed to set
+ // the cookie for "/foo/bar" but "/foo"
+ var pathAliasParts = aliasPath.split('/');
+ var i;
+ for (i = 2; i < pathAliasParts.length; i++) {
+ var lessRestrctivePath = pathAliasParts.slice(0, i).join('/');
+ if (isSiteHostPath(aliasDomain, lessRestrctivePath)) {
+ aliasPath = lessRestrctivePath;
+ break;
+ }
+ }
+
+ if (!isSitePath(currentPath, aliasPath)) {
+ // current path of current URL does not match the alias
+ // eg user is on piwik.org/demo but configHostAlias is for piwik.org/support
+ return;
+ }
+
+ return aliasPath;
+ }
+
+ /*
+ * Browser features (plugins, resolution, cookies)
+ */
+ function detectBrowserFeatures() {
+ var i,
+ mimeType,
+ pluginMap = {
+ // document types
+ pdf: 'application/pdf',
+
+ // media players
+ qt: 'video/quicktime',
+ realp: 'audio/x-pn-realaudio-plugin',
+ wma: 'application/x-mplayer2',
+
+ // interactive multimedia
+ dir: 'application/x-director',
+ fla: 'application/x-shockwave-flash',
+
+ // RIA
+ java: 'application/x-java-vm',
+ gears: 'application/x-googlegears',
+ ag: 'application/x-silverlight'
+ },
+ devicePixelRatio = windowAlias.devicePixelRatio || 1;
+
+ // 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
+ if (typeof navigator.javaEnabled !== 'unknown' &&
+ isDefined(navigatorAlias.javaEnabled) &&
+ navigatorAlias.javaEnabled()) {
+ browserFeatures.java = '1';
+ }
+
+ // Firefox
+ if (isFunction(windowAlias.GearsFactory)) {
+ browserFeatures.gears = '1';
+ }
+
+ // other browser features
+ browserFeatures.cookie = hasCookies();
+ }
+
+ var width = parseInt(screenAlias.width, 10) * devicePixelRatio;
+ var height = parseInt(screenAlias.height, 10) * devicePixelRatio;
+ browserFeatures.res = parseInt(width, 10) + 'x' + parseInt(height, 10);
+ }
+
+/*<DEBUG>*/
+ /*
+ * Register a test hook. Using eval() permits access to otherwise
+ * privileged members.
+ */
+ function registerHook(hookName, userHook) {
+ var hookObj = null;
+
+ if (isString(hookName) && !isDefined(registeredHooks[hookName]) && userHook) {
+ if (isObject(userHook)) {
+ hookObj = userHook;
+ } else if (isString(userHook)) {
+ try {
+ eval('hookObj =' + userHook);
+ } catch (ignore) { }
+ }
+
+ registeredHooks[hookName] = hookObj;
+ }
+
+ return hookObj;
+ }
+/*</DEBUG>*/
+
+ /************************************************************
+ * Constructor
+ ************************************************************/
+
+ /*
+ * initialize tracker
+ */
+ detectBrowserFeatures();
+ updateDomainHash();
+ setVisitorIdCookie();
+
+/*<DEBUG>*/
+ /*
+ * initialize test plugin
+ */
+ executePluginMethod('run', registerHook);
+/*</DEBUG>*/
+
+ /************************************************************
+ * Public data and methods
+ ************************************************************/
+
+ return {
+/*<DEBUG>*/
+ /*
+ * Test hook accessors
+ */
+ hook: registeredHooks,
+ getHook: function (hookName) {
+ return registeredHooks[hookName];
+ },
+ getQuery: function () {
+ return query;
+ },
+ getContent: function () {
+ return content;
+ },
+
+ buildContentImpressionRequest: buildContentImpressionRequest,
+ buildContentInteractionRequest: buildContentInteractionRequest,
+ buildContentInteractionRequestNode: buildContentInteractionRequestNode,
+ buildContentInteractionTrackingRedirectUrl: buildContentInteractionTrackingRedirectUrl,
+ getContentImpressionsRequestsFromNodes: getContentImpressionsRequestsFromNodes,
+ getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet: getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet,
+ trackCallbackOnLoad: trackCallbackOnLoad,
+ trackCallbackOnReady: trackCallbackOnReady,
+ buildContentImpressionsRequests: buildContentImpressionsRequests,
+ wasContentImpressionAlreadyTracked: wasContentImpressionAlreadyTracked,
+ appendContentInteractionToRequestIfPossible: getContentInteractionToRequestIfPossible,
+ setupInteractionsTracking: setupInteractionsTracking,
+ trackContentImpressionClickInteraction: trackContentImpressionClickInteraction,
+ internalIsNodeVisible: isVisible,
+ isNodeAuthorizedToTriggerInteraction: isNodeAuthorizedToTriggerInteraction,
+ replaceHrefIfInternalLink: replaceHrefIfInternalLink,
+ getDomains: function () {
+ return configHostsAlias;
+ },
+ getConfigCookiePath: function () {
+ return configCookiePath;
+ },
+ getConfigDownloadExtensions: function () {
+ return configDownloadExtensions;
+ },
+ enableTrackOnlyVisibleContent: function (checkOnScroll, timeIntervalInMs) {
+ return enableTrackOnlyVisibleContent(checkOnScroll, timeIntervalInMs, this);
+ },
+ clearTrackedContentImpressions: function () {
+ trackedContentImpressions = [];
+ },
+ getTrackedContentImpressions: function () {
+ return trackedContentImpressions;
+ },
+ clearEnableTrackOnlyVisibleContent: function () {
+ isTrackOnlyVisibleContentEnabled = false;
+ },
+ disableLinkTracking: function () {
+ linkTrackingInstalled = false;
+ linkTrackingEnabled = false;
+ },
+ getConfigVisitorCookieTimeout: function () {
+ return configVisitorCookieTimeout;
+ },
+ getRemainingVisitorCookieTimeout: getRemainingVisitorCookieTimeout,
+/*</DEBUG>*/
+
+ /**
+ * Get visitor ID (from first party cookie)
+ *
+ * @return string Visitor ID in hexits (or empty string, if not yet known)
+ */
+ getVisitorId: function () {
+ return getValuesFromVisitorIdCookie().uuid;
+ },
+
+ /**
+ * Get the visitor information (from first party cookie)
+ *
+ * @return array
+ */
+ getVisitorInfo: function () {
+ // Note: in a new method, we could return also return getValuesFromVisitorIdCookie()
+ // which returns named parameters rather than returning integer indexed array
+ return loadVisitorIdCookie();
+ },
+
+ /**
+ * Get the Attribution information, which is an array that contains
+ * the Referrer used to reach the site as well as the campaign name and keyword
+ * It is useful only when used in conjunction with Tracker API function setAttributionInfo()
+ * To access specific data point, you should use the other functions getAttributionReferrer* and getAttributionCampaign*
+ *
+ * @return array Attribution array, Example use:
+ * 1) Call JSON2.stringify(piwikTracker.getAttributionInfo())
+ * 2) Pass this json encoded string to the Tracking API (php or java client): setAttributionInfo()
+ */
+ getAttributionInfo: function () {
+ return loadReferrerAttributionCookie();
+ },
+
+ /**
+ * Get the Campaign name that was parsed from the landing page URL when the visitor
+ * landed on the site originally
+ *
+ * @return string
+ */
+ getAttributionCampaignName: function () {
+ return loadReferrerAttributionCookie()[0];
+ },
+
+ /**
+ * Get the Campaign keyword that was parsed from the landing page URL when the visitor
+ * landed on the site originally
+ *
+ * @return string
+ */
+ getAttributionCampaignKeyword: function () {
+ return loadReferrerAttributionCookie()[1];
+ },
+
+ /**
+ * Get the time at which the referrer (used for Goal Attribution) was detected
+ *
+ * @return int Timestamp or 0 if no referrer currently set
+ */
+ getAttributionReferrerTimestamp: function () {
+ return loadReferrerAttributionCookie()[2];
+ },
+
+ /**
+ * Get the full referrer URL that will be used for Goal Attribution
+ *
+ * @return string Raw URL, or empty string '' if no referrer currently set
+ */
+ getAttributionReferrerUrl: function () {
+ return loadReferrerAttributionCookie()[3];
+ },
+
+ /**
+ * Specify the Piwik server URL
+ *
+ * @param string trackerUrl
+ */
+ setTrackerUrl: function (trackerUrl) {
+ configTrackerUrl = trackerUrl;
+ },
+
+
+ /**
+ * Returns the Piwik server URL
+ * @returns string
+ */
+ getTrackerUrl: function () {
+ return configTrackerUrl;
+ },
+
+
+ /**
+ * Returns the site ID
+ *
+ * @returns int
+ */
+ getSiteId: function() {
+ return configTrackerSiteId;
+ },
+
+ /**
+ * Specify the site ID
+ *
+ * @param int|string siteId
+ */
+ setSiteId: function (siteId) {
+ setSiteId(siteId);
+ },
+
+ /**
+ * Sets a User ID to this user (such as an email address or a username)
+ *
+ * @param string User ID
+ */
+ setUserId: function (userId) {
+ if(!isDefined(userId) || !userId.length) {
+ return;
+ }
+ configUserId = userId;
+ visitorUUID = hash(configUserId).substr(0, 16);
+ },
+
+ /**
+ * Gets the User ID if set.
+ *
+ * @returns string User ID
+ */
+ getUserId: function() {
+ return configUserId;
+ },
+
+ /**
+ * Pass custom data to the server
+ *
+ * Examples:
+ * tracker.setCustomData(object);
+ * tracker.setCustomData(key, value);
+ *
+ * @param mixed key_or_obj
+ * @param mixed opt_value
+ */
+ setCustomData: function (key_or_obj, opt_value) {
+ if (isObject(key_or_obj)) {
+ configCustomData = key_or_obj;
+ } else {
+ if (!configCustomData) {
+ configCustomData = {};
+ }
+ configCustomData[key_or_obj] = opt_value;
+ }
+ },
+
+ /**
+ * Get custom data
+ *
+ * @return mixed
+ */
+ getCustomData: function () {
+ return configCustomData;
+ },
+
+ /**
+ * Configure function with custom request content processing logic.
+ * It gets called after request content in form of query parameters string has been prepared and before request content gets sent.
+ *
+ * Examples:
+ * tracker.setCustomRequestProcessing(function(request){
+ * var pairs = request.split('&');
+ * var result = {};
+ * pairs.forEach(function(pair) {
+ * pair = pair.split('=');
+ * result[pair[0]] = decodeURIComponent(pair[1] || '');
+ * });
+ * return JSON.stringify(result);
+ * });
+ *
+ * @param function customRequestContentProcessingLogic
+ */
+ setCustomRequestProcessing: function (customRequestContentProcessingLogic) {
+ configCustomRequestContentProcessing = customRequestContentProcessingLogic;
+ },
+
+ /**
+ * Appends the specified query string to the piwik.php?... Tracking API URL
+ *
+ * @param string queryString eg. 'lat=140&long=100'
+ */
+ appendToTrackingUrl: function (queryString) {
+ configAppendToTrackingUrl = queryString;
+ },
+
+ /**
+ * Returns the query string for the current HTTP Tracking API request.
+ * Piwik would prepend the hostname and path to Piwik: http://example.org/piwik/piwik.php?
+ * prior to sending the request.
+ *
+ * @param request eg. "param=value&param2=value2"
+ */
+ getRequest: function (request) {
+ return getRequest(request);
+ },
+
+ /**
+ * Add plugin defined by a name and a callback function.
+ * The callback function will be called whenever a tracking request is sent.
+ * This can be used to append data to the tracking request, or execute other custom logic.
+ *
+ * @param string pluginName
+ * @param Object pluginObj
+ */
+ addPlugin: function (pluginName, pluginObj) {
+ plugins[pluginName] = pluginObj;
+ },
+
+ /**
+ * Set Custom Dimensions. Any set Custom Dimension will be cleared after a tracked pageview. Make
+ * sure to set them again if needed.
+ *
+ * @param int index A Custom Dimension index
+ * @param string value
+ */
+ setCustomDimension: function (customDimensionId, value) {
+ customDimensionId = parseInt(customDimensionId, 10);
+ if (customDimensionId > 0) {
+ if (!isDefined(value)) {
+ value = '';
+ }
+ if (!isString(value)) {
+ value = String(value);
+ }
+ customDimensions[customDimensionId] = value;
+ }
+ },
+
+ /**
+ * Get a stored value for a specific Custom Dimension index.
+ *
+ * @param int index A Custom Dimension index
+ */
+ getCustomDimension: function (customDimensionId) {
+ customDimensionId = parseInt(customDimensionId, 10);
+ if (customDimensionId > 0 && Object.prototype.hasOwnProperty.call(customDimensions, customDimensionId)) {
+ return customDimensions[customDimensionId];
+ }
+ },
+
+ /**
+ * Delete a custom dimension.
+ *
+ * @param int index Custom dimension Id
+ */
+ deleteCustomDimension: function (customDimensionId) {
+ customDimensionId = parseInt(customDimensionId, 10);
+ if (customDimensionId > 0) {
+ delete customDimensions[customDimensionId];
+ }
+ },
+
+ /**
+ * Set custom variable within this visit
+ *
+ * @param int index Custom variable slot ID from 1-5
+ * @param string name
+ * @param string value
+ * @param string scope Scope of Custom Variable:
+ * - "visit" will store the name/value in the visit and will persist it in the cookie for the duration of the visit,
+ * - "page" will store the name/value in the next page view tracked.
+ * - "event" will store the name/value in the next event tracked.
+ */
+ setCustomVariable: function (index, name, value, scope) {
+ var toRecord;
+
+ if (!isDefined(scope)) {
+ scope = 'visit';
+ }
+ if (!isDefined(name)) {
+ return;
+ }
+ if (!isDefined(value)) {
+ value = "";
+ }
+ if (index > 0) {
+ name = !isString(name) ? String(name) : name;
+ value = !isString(value) ? String(value) : value;
+ toRecord = [name.slice(0, customVariableMaximumLength), value.slice(0, customVariableMaximumLength)];
+ // numeric scope is there for GA compatibility
+ if (scope === 'visit' || scope === 2) {
+ loadCustomVariables();
+ customVariables[index] = toRecord;
+ } else if (scope === 'page' || scope === 3) {
+ customVariablesPage[index] = toRecord;
+ } else if (scope === 'event') { /* GA does not have 'event' scope but we do */
+ customVariablesEvent[index] = toRecord;
+ }
+ }
+ },
+
+ /**
+ * Get custom variable
+ *
+ * @param int index Custom variable slot ID from 1-5
+ * @param string scope Scope of Custom Variable: "visit" or "page" or "event"
+ */
+ getCustomVariable: function (index, scope) {
+ var cvar;
+
+ if (!isDefined(scope)) {
+ scope = "visit";
+ }
+
+ if (scope === "page" || scope === 3) {
+ cvar = customVariablesPage[index];
+ } else if (scope === "event") {
+ cvar = customVariablesEvent[index];
+ } else if (scope === "visit" || scope === 2) {
+ loadCustomVariables();
+ cvar = customVariables[index];
+ }
+
+ if (!isDefined(cvar)
+ || (cvar && cvar[0] === '')) {
+ return false;
+ }
+
+ return cvar;
+ },
+
+ /**
+ * Delete custom variable
+ *
+ * @param int index Custom variable slot ID from 1-5
+ * @param string scope
+ */
+ deleteCustomVariable: function (index, scope) {
+ // Only delete if it was there already
+ if (this.getCustomVariable(index, scope)) {
+ this.setCustomVariable(index, '', '', scope);
+ }
+ },
+
+ /**
+ * When called then the Custom Variables of scope "visit" will be stored (persisted) in a first party cookie
+ * for the duration of the visit. This is useful if you want to call getCustomVariable later in the visit.
+ *
+ * By default, Custom Variables of scope "visit" are not stored on the visitor's computer.
+ */
+ storeCustomVariablesInCookie: function () {
+ configStoreCustomVariablesInCookie = true;
+ },
+
+ /**
+ * Set delay for link tracking (in milliseconds)
+ *
+ * @param int delay
+ */
+ setLinkTrackingTimer: function (delay) {
+ configTrackerPause = delay;
+ },
+
+ /**
+ * Set list of file extensions to be recognized as downloads
+ *
+ * @param string|array extensions
+ */
+ setDownloadExtensions: function (extensions) {
+ if(isString(extensions)) {
+ extensions = extensions.split('|');
+ }
+ configDownloadExtensions = extensions;
+ },
+
+ /**
+ * Specify additional file extensions to be recognized as downloads
+ *
+ * @param string|array extensions for example 'custom' or ['custom1','custom2','custom3']
+ */
+ addDownloadExtensions: function (extensions) {
+ var i;
+ if(isString(extensions)) {
+ extensions = extensions.split('|');
+ }
+ for (i=0; i < extensions.length; i++) {
+ configDownloadExtensions.push(extensions[i]);
+ }
+ },
+
+ /**
+ * Removes specified file extensions from the list of recognized downloads
+ *
+ * @param string|array extensions for example 'custom' or ['custom1','custom2','custom3']
+ */
+ removeDownloadExtensions: function (extensions) {
+ var i, newExtensions = [];
+ if(isString(extensions)) {
+ extensions = extensions.split('|');
+ }
+ for (i=0; i < configDownloadExtensions.length; i++) {
+ if (indexOfArray(extensions, configDownloadExtensions[i]) === -1) {
+ newExtensions.push(configDownloadExtensions[i]);
+ }
+ }
+ configDownloadExtensions = newExtensions;
+ },
+
+ /**
+ * Set array of domains to be treated as local. Also supports path, eg '.piwik.org/subsite1'. In this
+ * case all links that don't go to '*.piwik.org/subsite1/ *' would be treated as outlinks.
+ * For example a link to 'piwik.org/' or 'piwik.org/subsite2' both would be treated as outlinks.
+ *
+ * We might automatically set a cookieConfigPath to avoid creating several cookies under one domain
+ * if there is a hostAlias defined with a path. Say a user is visiting 'http://piwik.org/subsite1'
+ * and '.piwik.org/subsite1' is set as a hostsAlias. Piwik will automatically use '/subsite1' as
+ * cookieConfigPath.
+ *
+ * @param string|array hostsAlias
+ */
+ setDomains: function (hostsAlias) {
+ configHostsAlias = isString(hostsAlias) ? [hostsAlias] : hostsAlias;
+
+ var hasDomainAliasAlready = false, i;
+ for (i in configHostsAlias) {
+ if (Object.prototype.hasOwnProperty.call(configHostsAlias, i)
+ && isSameHost(domainAlias, domainFixup(String(configHostsAlias[i])))) {
+ hasDomainAliasAlready = true;
+
+ if (!configCookiePath) {
+ var path = findConfigCookiePathToUse(configHostsAlias[i], locationHrefAlias);
+ if (path) {
+ this.setCookiePath(path);
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (!hasDomainAliasAlready) {
+ /**
+ * eg if domainAlias = 'piwik.org' and someone set hostsAlias = ['piwik.org/foo'] then we should
+ * not add piwik.org as it would increase the allowed scope.
+ */
+ configHostsAlias.push(domainAlias);
+ }
+ },
+
+ /**
+ * Set array of classes to be ignored if present in link
+ *
+ * @param string|array ignoreClasses
+ */
+ setIgnoreClasses: function (ignoreClasses) {
+ configIgnoreClasses = isString(ignoreClasses) ? [ignoreClasses] : ignoreClasses;
+ },
+
+ /**
+ * Set request method
+ *
+ * @param string method GET or POST; default is GET
+ */
+ setRequestMethod: function (method) {
+ configRequestMethod = method || defaultRequestMethod;
+ },
+
+ /**
+ * Set request Content-Type header value, applicable when POST request method is used for submitting tracking events.
+ * See XMLHttpRequest Level 2 spec, section 4.7.2 for invalid headers
+ * @link http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html
+ *
+ * @param string requestContentType; default is 'application/x-www-form-urlencoded; charset=UTF-8'
+ */
+ setRequestContentType: function (requestContentType) {
+ configRequestContentType = requestContentType || defaultRequestContentType;
+ },
+
+ /**
+ * Override referrer
+ *
+ * @param string url
+ */
+ setReferrerUrl: function (url) {
+ configReferrerUrl = url;
+ },
+
+ /**
+ * Override url
+ *
+ * @param string url
+ */
+ setCustomUrl: function (url) {
+ configCustomUrl = resolveRelativeReference(locationHrefAlias, url);
+ },
+
+ /**
+ * Override document.title
+ *
+ * @param string title
+ */
+ setDocumentTitle: function (title) {
+ configTitle = title;
+ },
+
+ /**
+ * Set the URL of the Piwik API. It is used for Page Overlay.
+ * This method should only be called when the API URL differs from the tracker URL.
+ *
+ * @param string apiUrl
+ */
+ setAPIUrl: function (apiUrl) {
+ configApiUrl = apiUrl;
+ },
+
+ /**
+ * Set array of classes to be treated as downloads
+ *
+ * @param string|array downloadClasses
+ */
+ setDownloadClasses: function (downloadClasses) {
+ configDownloadClasses = isString(downloadClasses) ? [downloadClasses] : downloadClasses;
+ },
+
+ /**
+ * Set array of classes to be treated as outlinks
+ *
+ * @param string|array linkClasses
+ */
+ setLinkClasses: function (linkClasses) {
+ configLinkClasses = isString(linkClasses) ? [linkClasses] : linkClasses;
+ },
+
+ /**
+ * Set array of campaign name parameters
+ *
+ * @see http://piwik.org/faq/how-to/#faq_120
+ * @param string|array campaignNames
+ */
+ setCampaignNameKey: function (campaignNames) {
+ configCampaignNameParameters = isString(campaignNames) ? [campaignNames] : campaignNames;
+ },
+
+ /**
+ * Set array of campaign keyword parameters
+ *
+ * @see http://piwik.org/faq/how-to/#faq_120
+ * @param string|array campaignKeywords
+ */
+ setCampaignKeywordKey: function (campaignKeywords) {
+ configCampaignKeywordParameters = isString(campaignKeywords) ? [campaignKeywords] : campaignKeywords;
+ },
+
+ /**
+ * Strip hash tag (or anchor) from URL
+ * Note: this can be done in the Piwik>Settings>Spiderwebsites on a per-spiderwebsite basis
+ *
+ * @deprecated
+ * @param bool enableFilter
+ */
+ discardHashTag: function (enableFilter) {
+ configDiscardHashTag = enableFilter;
+ },
+
+ /**
+ * Set first-party cookie name prefix
+ *
+ * @param string cookieNamePrefix
+ */
+ setCookieNamePrefix: function (cookieNamePrefix) {
+ configCookieNamePrefix = cookieNamePrefix;
+ // Re-init the Custom Variables cookie
+ customVariables = getCustomVariablesFromCookie();
+ },
+
+ /**
+ * Set first-party cookie domain
+ *
+ * @param string domain
+ */
+ setCookieDomain: function (domain) {
+ var domainFixed = domainFixup(domain);
+
+ if (isPossibleToSetCookieOnDomain(domainFixed)) {
+ configCookieDomain = domainFixed;
+ updateDomainHash();
+ }
+ },
+
+ /**
+ * Set first-party cookie path
+ *
+ * @param string domain
+ */
+ setCookiePath: function (path) {
+ configCookiePath = path;
+ updateDomainHash();
+ },
+
+ /**
+ * Set visitor cookie timeout (in seconds)
+ * Defaults to 13 months (timeout=33955200)
+ *
+ * @param int timeout
+ */
+ setVisitorCookieTimeout: function (timeout) {
+ configVisitorCookieTimeout = timeout * 1000;
+ },
+
+ /**
+ * Set session cookie timeout (in seconds).
+ * Defaults to 30 minutes (timeout=1800000)
+ *
+ * @param int timeout
+ */
+ setSessionCookieTimeout: function (timeout) {
+ configSessionCookieTimeout = timeout * 1000;
+ },
+
+ /**
+ * Set referral cookie timeout (in seconds).
+ * Defaults to 6 months (15768000000)
+ *
+ * @param int timeout
+ */
+ setReferralCookieTimeout: function (timeout) {
+ configReferralCookieTimeout = timeout * 1000;
+ },
+
+ /**
+ * Set conversion attribution to first referrer and campaign
+ *
+ * @param bool if true, use first referrer (and first campaign)
+ * if false, use the last referrer (or campaign)
+ */
+ setConversionAttributionFirstReferrer: function (enable) {
+ configConversionAttributionFirstReferrer = enable;
+ },
+
+ /**
+ * Disables all cookies from being set
+ *
+ * Existing cookies will be deleted on the next call to track
+ */
+ disableCookies: function () {
+ configCookiesDisabled = true;
+ browserFeatures.cookie = '0';
+
+ if (configTrackerSiteId) {
+ deleteCookies();
+ }
+ },
+
+ /**
+ * One off cookies clearing. Useful to call this when you know for sure a new visitor is using the same browser,
+ * it maybe helps to "reset" tracking cookies to prevent data reuse for different users.
+ */
+ deleteCookies: function () {
+ deleteCookies();
+ },
+
+ /**
+ * Handle do-not-track requests
+ *
+ * @param bool enable If true, don't track if user agent sends 'do-not-track' header
+ */
+ setDoNotTrack: function (enable) {
+ var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack;
+ configDoNotTrack = enable && (dnt === 'yes' || dnt === '1');
+
+ // do not track also disables cookies and deletes existing cookies
+ if (configDoNotTrack) {
+ this.disableCookies();
+ }
+ },
+
+ /**
+ * Add click listener to a specific link element.
+ * When clicked, Piwik will log the click automatically.
+ *
+ * @param DOMElement element
+ * @param bool enable If true, use pseudo click-handler (middle click + context menu)
+ */
+ addListener: function (element, enable) {
+ addClickListener(element, enable);
+ },
+
+ /**
+ * Install link tracker
+ *
+ * The default behaviour is to use actual click events. However, some browsers
+ * (e.g., Firefox, Opera, and Konqueror) don't generate click events for the middle mouse button.
+ *
+ * To capture more "clicks", the pseudo click-handler uses mousedown + mouseup events.
+ * This is not industry standard and is vulnerable to false positives (e.g., drag events).
+ *
+ * There is a Safari/Chrome/Spiderwebkit bug that prevents tracking requests from being sent
+ * by either click handler. The workaround is to set a target attribute (which can't
+ * be "_self", "_top", or "_parent").
+ *
+ * @see https://bugs.spiderwebkit.org/show_bug.cgi?id=54783
+ *
+ * @param bool enable If "true", use pseudo click-handler (treat middle click and open contextmenu as
+ * left click). A right click (or any click that opens the context menu) on a link
+ * will be tracked as clicked even if "Open in new tab" is not selected. If
+ * "false" (default), nothing will be tracked on open context menu or middle click.
+ * The context menu is usually opened to open a link / download in a new tab
+ * therefore you can get more accurate results by treat it as a click but it can lead
+ * to wrong click numbers.
+ */
+ enableLinkTracking: function (enable) {
+ linkTrackingEnabled = true;
+
+ if (hasLoaded) {
+ // the load event has already fired, add the click listeners now
+ addClickListeners(enable);
+ } else {
+ // defer until page has loaded
+ registeredOnLoadHandlers.push(function () {
+ addClickListeners(enable);
+ });
+ }
+ },
+
+ /**
+ * Enable tracking of uncatched JavaScript errors
+ *
+ * If enabled, uncaught JavaScript Errors will be tracked as an event by defining a
+ * window.onerror handler. If a window.onerror handler is already defined we will make
+ * sure to call this previously registered error handler after tracking the error.
+ *
+ * By default we return false in the window.onerror handler to make sure the error still
+ * appears in the browser's console etc. Note: Some older browsers might behave differently
+ * so it could happen that an actual JavaScript error will be suppressed.
+ * If a window.onerror handler was registered we will return the result of this handler.
+ *
+ * Make sure not to overwrite the window.onerror handler after enabling the JS error
+ * tracking as the error tracking won't work otherwise. To capture all JS errors we
+ * recommend to include the Piwik JavaScript tracker in the HTML as early as possible.
+ * If possible directly in <head></head> before loading any other JavaScript.
+ */
+ enableJSErrorTracking: function () {
+ if (enableJSErrorTracking) {
+ return;
+ }
+
+ enableJSErrorTracking = true;
+ var onError = windowAlias.onerror;
+
+ windowAlias.onerror = function (message, url, linenumber, column, error) {
+ trackCallback(function () {
+ var category = 'JavaScript Errors';
+
+ var action = url + ':' + linenumber;
+ if (column) {
+ action += ':' + column;
+ }
+
+ logEvent(category, action, message);
+ });
+
+ if (onError) {
+ return onError(message, url, linenumber, column, error);
+ }
+
+ return false;
+ };
+ },
+
+ /**
+ * Disable automatic performance tracking
+ */
+ disablePerformanceTracking: function () {
+ configPerformanceTrackingEnabled = false;
+ },
+
+ /**
+ * Set the server generation time.
+ * If set, the browser's performance.timing API in not used anymore to determine the time.
+ *
+ * @param int generationTime
+ */
+ setGenerationTimeMs: function (generationTime) {
+ configPerformanceGenerationTime = parseInt(generationTime, 10);
+ },
+
+ /**
+ * Set heartbeat (in seconds)
+ *
+ * @param int heartBeatDelayInSeconds Defaults to 15. Cannot be lower than 1.
+ */
+ enableHeartBeatTimer: function (heartBeatDelayInSeconds) {
+ heartBeatDelayInSeconds = Math.max(heartBeatDelayInSeconds, 1);
+ configHeartBeatDelay = (heartBeatDelayInSeconds || 15) * 1000;
+
+ // if a tracking request has already been sent, start the heart beat timeout
+ if (lastTrackerRequestTime !== null) {
+ setUpHeartBeat();
+ }
+ },
+
+/*<DEBUG>*/
+ /**
+ * Clear heartbeat.
+ */
+ disableHeartBeatTimer: function () {
+ heartBeatDown();
+ configHeartBeatDelay = null;
+
+ window.removeEventListener('focus', heartBeatOnFocus);
+ window.removeEventListener('blur', heartBeatOnBlur);
+ },
+/*</DEBUG>*/
+
+ /**
+ * Frame buster
+ */
+ killFrame: function () {
+ if (windowAlias.location !== windowAlias.top.location) {
+ windowAlias.top.location = windowAlias.location;
+ }
+ },
+
+ /**
+ * Redirect if browsing offline (aka file: buster)
+ *
+ * @param string url Redirect to this URL
+ */
+ redirectFile: function (url) {
+ if (windowAlias.location.protocol === 'file:') {
+ windowAlias.location = url;
+ }
+ },
+
+ /**
+ * Count sites in pre-rendered state
+ *
+ * @param bool enable If true, track when in pre-rendered state
+ */
+ setCountPreRendered: function (enable) {
+ configCountPreRendered = enable;
+ },
+
+ /**
+ * Trigger a goal
+ *
+ * @param int|string idGoal
+ * @param int|float customRevenue
+ * @param mixed customData
+ */
+ trackGoal: function (idGoal, customRevenue, customData) {
+ trackCallback(function () {
+ logGoal(idGoal, customRevenue, customData);
+ });
+ },
+
+ /**
+ * Manually log a click from your own code
+ *
+ * @param string sourceUrl
+ * @param string linkType
+ * @param mixed customData
+ * @param function callback
+ */
+ trackLink: function (sourceUrl, linkType, customData, callback) {
+ trackCallback(function () {
+ logLink(sourceUrl, linkType, customData, callback);
+ });
+ },
+
+ /**
+ * Log visit to this page
+ *
+ * @param string customTitle
+ * @param mixed customData
+ */
+ trackPageView: function (customTitle, customData) {
+ trackedContentImpressions = [];
+
+ if (isOverlaySession(configTrackerSiteId)) {
+ trackCallback(function () {
+ injectOverlayScripts(configTrackerUrl, configApiUrl, configTrackerSiteId);
+ });
+ } else {
+ trackCallback(function () {
+ logPageView(customTitle, customData);
+ });
+ }
+ },
+
+ /**
+ * Scans the entire DOM for all content blocks and tracks all impressions once the DOM ready event has
+ * been triggered.
+ *
+ * If you only want to track visible content impressions have a look at `trackVisibleContentImpressions()`.
+ * We do not track an impression of the same content block twice if you call this method multiple times
+ * unless `trackPageView()` is called meanwhile. This is useful for single page applications.
+ */
+ trackAllContentImpressions: function () {
+ if (isOverlaySession(configTrackerSiteId)) {
+ return;
+ }
+
+ trackCallback(function () {
+ trackCallbackOnReady(function () {
+ // we have to wait till DOM ready
+ var contentNodes = content.findContentNodes();
+ var requests = getContentImpressionsRequestsFromNodes(contentNodes);
+
+ sendBulkRequest(requests, configTrackerPause);
+ });
+ });
+ },
+
+ /**
+ * Scans the entire DOM for all content blocks as soon as the page is loaded. It tracks an impression
+ * only if a content block is actually visible. Meaning it is not hidden and the content is or was at
+ * some point in the viewport.
+ *
+ * If you want to track all content blocks have a look at `trackAllContentImpressions()`.
+ * We do not track an impression of the same content block twice if you call this method multiple times
+ * unless `trackPageView()` is called meanwhile. This is useful for single page applications.
+ *
+ * Once you have called this method you can no longer change `checkOnScroll` or `timeIntervalInMs`.
+ *
+ * If you do want to only track visible content blocks but not want us to perform any automatic checks
+ * as they can slow down your frames per second you can call `trackVisibleContentImpressions()` or
+ * `trackContentImpressionsWithinNode()` manually at any time to rescan the entire DOM for newly
+ * visible content blocks.
+ * o Call `trackVisibleContentImpressions(false, 0)` to initially track only visible content impressions
+ * o Call `trackVisibleContentImpressions()` at any time again to rescan the entire DOM for newly visible content blocks or
+ * o Call `trackContentImpressionsWithinNode(node)` at any time to rescan only a part of the DOM for newly visible content blocks
+ *
+ * @param boolean [checkOnScroll=true] Optional, you can disable rescanning the entire DOM automatically
+ * after each scroll event by passing the value `false`. If enabled,
+ * we check whether a previously hidden content blocks became visible
+ * after a scroll and if so track the impression.
+ * Note: If a content block is placed within a scrollable element
+ * (`overflow: scroll`), we can currently not detect when this block
+ * becomes visible.
+ * @param integer [timeIntervalInMs=750] Optional, you can define an interval to rescan the entire DOM
+ * for new impressions every X milliseconds by passing
+ * for instance `timeIntervalInMs=500` (rescan DOM every 500ms).
+ * Rescanning the entire DOM and detecting the visible state of content
+ * blocks can take a while depending on the browser and amount of content.
+ * In case your frames per second goes down you might want to increase
+ * this value or disable it by passing the value `0`.
+ */
+ trackVisibleContentImpressions: function (checkOnSroll, timeIntervalInMs) {
+ if (isOverlaySession(configTrackerSiteId)) {
+ return;
+ }
+
+ if (!isDefined(checkOnSroll)) {
+ checkOnSroll = true;
+ }
+
+ if (!isDefined(timeIntervalInMs)) {
+ timeIntervalInMs = 750;
+ }
+
+ enableTrackOnlyVisibleContent(checkOnSroll, timeIntervalInMs, this);
+
+ trackCallback(function () {
+ trackCallbackOnLoad(function () {
+ // we have to wait till CSS parsed and applied
+ var contentNodes = content.findContentNodes();
+ var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes);
+
+ sendBulkRequest(requests, configTrackerPause);
+ });
+ });
+ },
+
+ /**
+ * Tracks a content impression using the specified values. You should not call this method too often
+ * as each call causes an XHR tracking request and can slow down your site or your server.
+ *
+ * @param string contentName For instance "Ad Sale".
+ * @param string [contentPiece='Unknown'] For instance a path to an image or the text of a text ad.
+ * @param string [contentTarget] For instance the URL of a landing page.
+ */
+ trackContentImpression: function (contentName, contentPiece, contentTarget) {
+ if (isOverlaySession(configTrackerSiteId)) {
+ return;
+ }
+
+ if (!contentName) {
+ return;
+ }
+
+ contentPiece = contentPiece || 'Unknown';
+
+ trackCallback(function () {
+ var request = buildContentImpressionRequest(contentName, contentPiece, contentTarget);
+ sendRequest(request, configTrackerPause);
+ });
+ },
+
+ /**
+ * Scans the given DOM node and its children for content blocks and tracks an impression for them if
+ * no impression was already tracked for it. If you have called `trackVisibleContentImpressions()`
+ * upfront only visible content blocks will be tracked. You can use this method if you, for instance,
+ * dynamically add an element using JavaScript to your DOM after we have tracked the initial impressions.
+ *
+ * @param Element domNode
+ */
+ trackContentImpressionsWithinNode: function (domNode) {
+ if (isOverlaySession(configTrackerSiteId) || !domNode) {
+ return;
+ }
+
+ trackCallback(function () {
+ if (isTrackOnlyVisibleContentEnabled) {
+ trackCallbackOnLoad(function () {
+ // we have to wait till CSS parsed and applied
+ var contentNodes = content.findContentNodesWithinNode(domNode);
+
+ var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes);
+ sendBulkRequest(requests, configTrackerPause);
+ });
+ } else {
+ trackCallbackOnReady(function () {
+ // we have to wait till DOM ready
+ var contentNodes = content.findContentNodesWithinNode(domNode);
+
+ var requests = getContentImpressionsRequestsFromNodes(contentNodes);
+ sendBulkRequest(requests, configTrackerPause);
+ });
+ }
+ });
+ },
+
+ /**
+ * Tracks a content interaction using the specified values. You should use this method only in conjunction
+ * with `trackContentImpression()`. The specified `contentName` and `contentPiece` has to be exactly the
+ * same as the ones that were used in `trackContentImpression()`. Otherwise the interaction will not count.
+ *
+ * @param string contentInteraction The type of interaction that happened. For instance 'click' or 'submit'.
+ * @param string contentName The name of the content. For instance "Ad Sale".
+ * @param string [contentPiece='Unknown'] The actual content. For instance a path to an image or the text of a text ad.
+ * @param string [contentTarget] For instance the URL of a landing page.
+ */
+ trackContentInteraction: function (contentInteraction, contentName, contentPiece, contentTarget) {
+ if (isOverlaySession(configTrackerSiteId)) {
+ return;
+ }
+
+ if (!contentInteraction || !contentName) {
+ return;
+ }
+
+ contentPiece = contentPiece || 'Unknown';
+
+ trackCallback(function () {
+ var request = buildContentInteractionRequest(contentInteraction, contentName, contentPiece, contentTarget);
+ sendRequest(request, configTrackerPause);
+ });
+ },
+
+ /**
+ * Tracks an interaction with the given DOM node / content block.
+ *
+ * By default we track interactions on click but sometimes you might want to track interactions yourself.
+ * For instance you might want to track an interaction manually on a double click or a form submit.
+ * Make sure to disable the automatic interaction tracking in this case by specifying either the CSS
+ * class `piwikContentIgnoreInteraction` or the attribute `data-content-ignoreinteraction`.
+ *
+ * @param Element domNode This element itself or any of its parent elements has to be a content block
+ * element. Meaning one of those has to have a `piwikTrackContent` CSS class or
+ * a `data-track-content` attribute.
+ * @param string [contentInteraction='Unknown] The name of the interaction that happened. For instance
+ * 'click', 'formSubmit', 'DblClick', ...
+ */
+ trackContentInteractionNode: function (domNode, contentInteraction) {
+ if (isOverlaySession(configTrackerSiteId) || !domNode) {
+ return;
+ }
+
+ trackCallback(function () {
+ var request = buildContentInteractionRequestNode(domNode, contentInteraction);
+ sendRequest(request, configTrackerPause);
+ });
+ },
+
+ /**
+ * Useful to debug content tracking. This method will log all detected content blocks to console
+ * (if the browser supports the console). It will list the detected name, piece, and target of each
+ * content block.
+ */
+ logAllContentBlocksOnPage: function () {
+ var contentNodes = content.findContentNodes();
+ var contents = content.collectContent(contentNodes);
+
+ if (console !== undefined && console && console.log) {
+ console.log(contents);
+ }
+ },
+
+ /**
+ * Records an event
+ *
+ * @param string category The Event Category (Videos, Music, Games...)
+ * @param string action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...)
+ * @param string name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...)
+ * @param float value (optional) The Event's value
+ * @param mixed customData
+ */
+ trackEvent: function (category, action, name, value, customData) {
+ trackCallback(function () {
+ logEvent(category, action, name, value, customData);
+ });
+ },
+
+ /**
+ * Log special pageview: Internal search
+ *
+ * @param string keyword
+ * @param string category
+ * @param int resultsCount
+ * @param mixed customData
+ */
+ trackSiteSearch: function (keyword, category, resultsCount, customData) {
+ trackCallback(function () {
+ logSiteSearch(keyword, category, resultsCount, customData);
+ });
+ },
+
+ /**
+ * Used to record that the current page view is an item (product) page view, or a Ecommerce Category page view.
+ * This must be called before trackPageView() on the product/category page.
+ * It will set 3 custom variables of scope "page" with the SKU, Name and Category for this page view.
+ * Note: Custom Variables of scope "page" slots 3, 4 and 5 will be used.
+ *
+ * On a category page, you can set the parameter category, and set the other parameters to empty string or false
+ *
+ * Tracking Product/Category page views will allow Piwik to report on Product & Categories
+ * conversion rates (Conversion rate = Ecommerce orders containing this product or category / Visits to the product or category)
+ *
+ * @param string sku Item's SKU code being viewed
+ * @param string name Item's Name being viewed
+ * @param string category Category page being viewed. On an Item's page, this is the item's category
+ * @param float price Item's display price, not use in standard Piwik reports, but output in API product reports.
+ */
+ setEcommerceView: function (sku, name, category, price) {
+ if (!isDefined(category) || !category.length) {
+ category = "";
+ } else if (category instanceof Array) {
+ category = JSON2.stringify(category);
+ }
+
+ customVariablesPage[5] = ['_pkc', category];
+
+ if (isDefined(price) && String(price).length) {
+ customVariablesPage[2] = ['_pkp', price];
+ }
+
+ // On a category page, do not track Product name not defined
+ if ((!isDefined(sku) || !sku.length)
+ && (!isDefined(name) || !name.length)) {
+ return;
+ }
+
+ if (isDefined(sku) && sku.length) {
+ customVariablesPage[3] = ['_pks', sku];
+ }
+
+ if (!isDefined(name) || !name.length) {
+ name = "";
+ }
+
+ customVariablesPage[4] = ['_pkn', name];
+ },
+
+ /**
+ * Adds an item (product) that is in the current Cart or in the Ecommerce order.
+ * This function is called for every item (product) in the Cart or the Order.
+ * The only required parameter is sku.
+ *
+ * @param string sku (required) Item's SKU Code. This is the unique identifier for the product.
+ * @param string name (optional) Item's name
+ * @param string name (optional) Item's category, or array of up to 5 categories
+ * @param float price (optional) Item's price. If not specified, will default to 0
+ * @param float quantity (optional) Item's quantity. If not specified, will default to 1
+ */
+ addEcommerceItem: function (sku, name, category, price, quantity) {
+ if (sku.length) {
+ ecommerceItems[sku] = [ sku, name, category, price, quantity ];
+ }
+ },
+
+ /**
+ * Tracks an Ecommerce order.
+ * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order.
+ * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Piwik reports.
+ * Parameters orderId and grandTotal are required. For others, you can set to false if you don't need to specify them.
+ *
+ * @param string|int orderId (required) Unique Order ID.
+ * This will be used to count this order only once in the event the order page is reloaded several times.
+ * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Piwik.
+ * @param float grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.)
+ * @param float subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied)
+ * @param float tax (optional) Tax amount for this order
+ * @param float shipping (optional) Shipping amount for this order
+ * @param float discount (optional) Discounted amount in this order
+ */
+ trackEcommerceOrder: function (orderId, grandTotal, subTotal, tax, shipping, discount) {
+ logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount);
+ },
+
+ /**
+ * Tracks a Cart Update (add item, remove item, update item).
+ * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart, including the items that haven't been updated since the last cart update.
+ * Then you can call this function with the Cart grandTotal (typically the sum of all items' prices)
+ *
+ * @param float grandTotal (required) Items (products) amount in the Cart
+ */
+ trackEcommerceCartUpdate: function (grandTotal) {
+ logEcommerceCartUpdate(grandTotal);
+ }
+
+ };
+ }
+
+ /************************************************************
+ * Proxy object
+ * - this allows the caller to continue push()'ing to _paq
+ * after the Tracker has been initialized and loaded
+ ************************************************************/
+
+ function TrackerProxy() {
+ return {
+ push: apply
+ };
+ }
+
+ /**
+ * Applies the given methods in the given order if they are present in paq.
+ *
+ * @param {Array} paq
+ * @param {Array} methodsToApply an array containing method names in the order that they should be applied
+ * eg ['setSiteId', 'setTrackerUrl']
+ * @returns {Array} the modified paq array with the methods that were already applied set to undefined
+ */
+ function applyMethodsInOrder(paq, methodsToApply)
+ {
+ var appliedMethods = {};
+ var index, iterator;
+
+ for (index = 0; index < methodsToApply.length; index++) {
+ var methodNameToApply = methodsToApply[index];
+ appliedMethods[methodNameToApply] = 1;
+
+ for (iterator = 0; iterator < paq.length; iterator++) {
+ if (paq[iterator] && paq[iterator][0]) {
+ var methodName = paq[iterator][0];
+
+ if (methodNameToApply === methodName) {
+ apply(paq[iterator]);
+ delete paq[iterator];
+
+ if (appliedMethods[methodName] > 1) {
+ if (console !== undefined && console && console.error) {
+ console.error('The method ' + methodName + ' is registered more than once in "paq" variable. Only the last call has an effect. Please have a look at the multiple Piwik trackers documentation: http://developer.piwik.org/guides/tracking-javascript-guide#multiple-piwik-trackers');
+ }
+ }
+
+ appliedMethods[methodName]++;
+ }
+ }
+ }
+ }
+
+ return paq;
+ }
+
+ /************************************************************
+ * Constructor
+ ************************************************************/
+
+ // initialize the Piwik singleton
+ addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false);
+ addReadyListener();
+
+ Date.prototype.getTimeAlias = Date.prototype.getTime;
+
+ asyncTracker = new Tracker();
+
+ var applyFirst = ['disableCookies', 'setTrackerUrl', 'setAPIUrl', 'setCookiePath', 'setCookieDomain', 'setDomains', 'setUserId', 'setSiteId', 'enableLinkTracking'];
+ _paq = applyMethodsInOrder(_paq, applyFirst);
+
+ // apply the queue of actions
+ for (iterator = 0; iterator < _paq.length; iterator++) {
+ if (_paq[iterator]) {
+ apply(_paq[iterator]);
+ }
+ }
+
+ // replace initialization array with proxy object
+ _paq = new TrackerProxy();
+
+ /************************************************************
+ * Public data and methods
+ ************************************************************/
+
+ Piwik = {
+ /**
+ * Add plugin
+ *
+ * @param string pluginName
+ * @param Object pluginObj
+ */
+ addPlugin: function (pluginName, pluginObj) {
+ plugins[pluginName] = pluginObj;
+ },
+
+ /**
+ * Get Tracker (factory method)
+ *
+ * @param string piwikUrl
+ * @param int|string siteId
+ * @return Tracker
+ */
+ getTracker: function (piwikUrl, siteId) {
+ if(!isDefined(siteId)) {
+ siteId = this.getAsyncTracker().getSiteId();
+ }
+ if(!isDefined(piwikUrl)) {
+ piwikUrl = this.getAsyncTracker().getTrackerUrl();
+ }
+ return new Tracker(piwikUrl, siteId);
+ },
+
+ /**
+ * Get internal asynchronous tracker object
+ *
+ * @return Tracker
+ */
+ getAsyncTracker: function () {
+ return asyncTracker;
+ }
+ };
+
+ // Expose Piwik as an AMD module
+ if (typeof define === 'function' && define.amd) {
+ define('piwik', [], function () { return Piwik; });
+ }
+
+ return Piwik;
+ }());
+}
+
+if (window && window.piwikAsyncInit) {
+ window.piwikAsyncInit();
+}
+
+/*jslint sloppy: true */
+(function () {
+ var jsTrackerType = (typeof AnalyticsTracker);
+ if (jsTrackerType === 'undefined') {
+ AnalyticsTracker = Piwik;
+ }
+}());
+/*jslint sloppy: false */
+
+/************************************************************
+ * Deprecated functionality below
+ * Legacy piwik.js compatibility ftw
+ ************************************************************/
+
+/*
+ * Piwik globals
+ *
+ * var piwik_install_tracker, piwik_tracker_pause, piwik_download_extensions, piwik_hosts_alias, piwik_ignore_classes;
+ */
+/*global piwik_log:true */
+/*global piwik_track:true */
+
+/**
+ * Track page visit
+ *
+ * @param string documentTitle
+ * @param int|string siteId
+ * @param string piwikUrl
+ * @param mixed customData
+ */
+if (typeof piwik_log !== 'function') {
+ piwik_log = function (documentTitle, siteId, piwikUrl, customData) {
+ 'use strict';
+
+ function getOption(optionName) {
+ try {
+ if (window['piwik_' + optionName]) {
+ return window['piwik_' + optionName];
+ }
+ } catch (ignore) { }
+
+ return; // undefined
+ }
+
+ // instantiate the tracker
+ var option,
+ piwikTracker = Piwik.getTracker(piwikUrl, siteId);
+
+ // initialize tracker
+ piwikTracker.setDocumentTitle(documentTitle);
+ piwikTracker.setCustomData(customData);
+
+ // handle Piwik globals
+ option = getOption('tracker_pause');
+
+ if (option) {
+ piwikTracker.setLinkTrackingTimer(option);
+ }
+
+ option = getOption('download_extensions');
+
+ if (option) {
+ piwikTracker.setDownloadExtensions(option);
+ }
+
+ option = getOption('hosts_alias');
+
+ if (option) {
+ piwikTracker.setDomains(option);
+ }
+
+ option = getOption('ignore_classes');
+
+ if (option) {
+ piwikTracker.setIgnoreClasses(option);
+ }
+
+ // track this page view
+ piwikTracker.trackPageView();
+
+ // default is to install the link tracker
+ if (getOption('install_tracker')) {
+
+ /**
+ * Track click manually (function is defined below)
+ *
+ * @param string sourceUrl
+ * @param int|string siteId
+ * @param string piwikUrl
+ * @param string linkType
+ */
+ piwik_track = function (sourceUrl, siteId, piwikUrl, linkType) {
+ piwikTracker.setSiteId(siteId);
+ piwikTracker.setTrackerUrl(piwikUrl);
+ piwikTracker.trackLink(sourceUrl, linkType);
+ };
+
+ // set-up link tracking
+ piwikTracker.enableLinkTracking();
+ }
+ };
+}
+
+/*! @license-end */