Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrobocoder <anthon.pang@gmail.com>2009-05-28 03:10:11 +0400
committerrobocoder <anthon.pang@gmail.com>2009-05-28 03:10:11 +0400
commitc654e7382820fa5c1155eca30ee9617bd5b86d6d (patch)
treef02932d8b4491834e41a8ee7285bc3da1eef9de0
parent7d6ba208948710b78336a4c63a072ddfd21b979a (diff)
fixes #355 - OO version of piwik.js which no longer modifies DOM;
remove misc/testJavascripTracker (interactive tests); add tests/javascript (QUnit unit tests); fixes #661 - use click event instead of mousedown; fixes #549 - define your own download/outlink tracking classes; fixes #82 - add hook interface for module git-svn-id: http://dev.piwik.org/svn/trunk@1151 59fd770c-687e-43c8-a1e3-f5a4ff64c105
-rwxr-xr-xjs/index.php41
-rw-r--r--js/piwik.js1008
-rw-r--r--misc/testJavascriptTracker/index.php55
-rw-r--r--misc/testJavascriptTracker/page2.php69
-rw-r--r--misc/testJavascriptTracker/test.jpg0
-rw-r--r--misc/testJavascriptTracker/test.pdf0
-rw-r--r--misc/testJavascriptTracker/test.rar0
-rw-r--r--misc/testJavascriptTracker/test.zip0
-rw-r--r--piwik.js265
-rw-r--r--tests/javascript/assets/testrunner.js780
-rw-r--r--tests/javascript/assets/testsuite.css120
-rw-r--r--tests/javascript/index.php295
-rw-r--r--tests/javascript/piwik.php81
-rw-r--r--tests/javascript/piwiktest.js45
14 files changed, 2385 insertions, 374 deletions
diff --git a/js/index.php b/js/index.php
new file mode 100755
index 0000000000..60786f2849
--- /dev/null
+++ b/js/index.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
+ * @version $Id$
+ */
+
+$file = "../piwik.js";
+
+/*
+ * Conditional GET
+ */
+if (file_exists($file)) {
+ $modifiedSince = '';
+ if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+ $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+ }
+ $lastModified = gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT';
+
+ // strip any trailing data appended to header
+ if (false !== ($semicolon = strpos($modifiedSince, ';'))) {
+ $modifiedSince = substr($modifiedSince, 0, $semicolon);
+ }
+
+ if ($modifiedSince == $lastModified) {
+ header('HTTP/1.1 304 Not Modified');
+ } else {
+ header('Last-Modified: ' . $lastModified);
+ header('Content-Length: ' . filesize($file));
+ header('Content-Type: application/x-javascript');
+
+ if (!readfile($file)) {
+ header ("HTTP/1.0 505 Internal server error");
+ }
+ }
+} else {
+ header ("HTTP/1.0 404 Not Found");
+}
+exit;
diff --git a/js/piwik.js b/js/piwik.js
new file mode 100644
index 0000000000..63b26796ac
--- /dev/null
+++ b/js/piwik.js
@@ -0,0 +1,1008 @@
+/*!
+ * Piwik - Web Analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
+ * @version $Id$
+ */
+
+/*jslint browser:true, forin:true, plusplus:false, onevar:false, eqeqeq:false */
+/*global window escape unescape ActiveXObject */
+
+// Note: YUICompressor 2.4.2 won't compress piwik_log() because of the the "evil" eval().
+// Override this behaviour using http://yuilibrary.com/projects/yuicompressor/ticket/2343811
+/*jslint evil:true */
+
+/*
+ * Browser [In]Compatibility
+ *
+ * This version of piwik.js is known to not work with:
+ * - IE4 (and below) - try..catch and for..in not introduced until IE5
+ */
+
+// Guard against loading the script twice
+var Piwik;
+try {
+ if (Piwik.getTracker) { }
+} catch (e) {
+ // Piwik singleton and namespace
+ Piwik = (function () {
+ /************************************************************
+ * Private data
+ ************************************************************/
+
+ var expireDateTime,
+
+ /* plugins */
+ plugins = {},
+
+ /* alias frequently used globals for added minification */
+ documentAlias = document,
+ navigatorAlias = navigator,
+ screenAlias = screen,
+ windowAlias = window,
+
+ /* DOM Ready */
+ hasLoaded = false,
+ registeredOnLoadHandlers = [];
+
+ /************************************************************
+ * Private methods
+ ************************************************************/
+
+ /*
+ * Is property (or variable) defined?
+ */
+ function isDefined(property) {
+ return typeof property !== 'undefined';
+ }
+
+ /*
+ * Cross-browser helper function to add event handler
+ */
+ function addEventListener(element, eventType, eventHandler, useCapture) {
+ if (element.addEventListener) {
+ element.addEventListener(eventType, eventHandler, useCapture);
+ return true;
+ } else 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) {
+ pluginMethod = plugins[i][methodName];
+ if (typeof pluginMethod === 'function') {
+ result += pluginMethod(callback);
+ }
+ }
+
+ return result;
+ }
+
+ /*
+ * Handle beforeunload event
+ */
+ function beforeUnloadHandler(unloadEvent /* not used */) {
+ /*
+ * Delay/pause (blocks UI)
+ */
+ if (isDefined(expireDateTime)) {
+ var now = new Date();
+
+ while (now.getTime() < expireDateTime) {
+ now = new Date();
+ }
+ }
+
+ executePluginMethod('unload');
+ }
+
+ /*
+ * Handler for onload event
+ */
+ function loadHandler(loadEvent /* unused */) {
+ if (!hasLoaded) {
+ hasLoaded = true;
+ executePluginMethod('load');
+ for (var i = 0; i < registeredOnLoadHandlers.length; i++) {
+ registeredOnLoadHandlers[i]();
+ }
+ }
+ return true;
+ }
+
+ /*
+ * Add onload or DOM ready handler
+ */
+ function addReadyListener() {
+ if (documentAlias.addEventListener) {
+ addEventListener(documentAlias, "DOMContentLoaded", function () {
+ documentAlias.removeEventListener("DOMContentLoaded", arguments.callee, false);
+ loadHandler();
+ });
+ } else if (documentAlias.attachEvent) {
+ documentAlias.attachEvent("onreadystatechange", function () {
+ if (documentAlias.readyState === "complete") {
+ documentAlias.detachEvent("onreadystatechange", arguments.callee);
+ loadHandler();
+ }
+ });
+
+ if (documentAlias.documentElement.doScroll && windowAlias == windowAlias.top) {
+ (function () {
+ if (hasLoaded) {
+ return;
+ }
+ try {
+ documentAlias.documentElement.doScroll("left");
+ } catch (error) {
+ setTimeout(arguments.callee, 0);
+ return;
+ }
+ loadHandler();
+ }());
+ }
+ }
+ // fallback
+ addEventListener(windowAlias, 'load', loadHandler, false);
+ }
+
+ /*
+ * 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 // Tracker URL
+ configTrackerUrl = trackerUrl || '',
+
+ // Site ID
+ configTrackerSiteId = siteId || '',
+
+ // Document title
+ configTitle = documentAlias.title || '',
+
+ // Extensions to be treated as download links
+ configDownloadExtensions = '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd||xls|xml|z|zip',
+
+ // Hosts or alias(es) to not treat as outlinks
+ configHostsAlias = [windowAlias.location.hostname],
+
+ // Anchor classes to not track
+ configIgnoreClasses = [],
+
+ // Download class name
+ configDownloadClass = 'piwik_download',
+
+ // (Out) Link class name
+ configLinkClass = 'piwik_link',
+
+ // Maximum delay to wait for web bug image to be fetched (in milliseconds)
+ configTrackerPause = 500,
+
+ // Custom data
+ configCustomData,
+
+ // Client-side data collection
+ browserHasCookies = '0',
+ pageReferrer,
+
+ // Plugin, Parameter name, MIME type, ActiveX progid, detected, version
+ pluginMap = {
+ director: ['dir', 'application/x-director',
+ ['SWCtl.SWctl.1'],
+ '0', ''],
+ flash: ['fla', 'application/x-shockwave-flash',
+ ['ShockwaveFlash.ShockwaveFlash.1'],
+ '0', ''],
+ pdf: ['pdf', 'application/pdf',
+ ['AcroPDF.PDF.1', 'PDF.PdfCtrl.6', 'PDF.PdfCtrl.5', 'PDF.PdfCtrl.1'],
+ '0', ''],
+ realplayer: ['realp', 'audio/x-pn-realaudio-plugin',
+ ['rmocx.RealPlayer G2 Control.1', 'RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)'],
+ '0', ''],
+ wma: ['wma', 'application/x-mplayer2',
+ ['WMPlayer.OCX', 'MediaPlayer.MediaPlayer.1'],
+ '0', '']
+ },
+
+ // Guard against installing the link tracker more than once per Tracker instance
+ linkTrackingInstalled = false,
+
+ /*
+ * encode or escape
+ * - encodeURIComponent added in IE5.5
+ */
+ escapeWrapper = windowAlias.encodeURIComponent || escape,
+
+ /*
+ * decode or unescape
+ * - decodeURIComponent added in IE5.5
+ */
+ unescapeWrapper = windowAlias.decodeURIComponent || unescape,
+
+ /*
+ * stringify
+ * - based on public domain JSON implementation at http://www.json.org/json2.js (2009-04-16)
+ */
+ stringify = function (value) {
+
+ var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ // table of character substitutions
+ meta = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'};
+
+ // If the string contains no control characters, no quote characters, and no
+ // backslash characters, then we can safely slap some quotes around it.
+ // Otherwise we must also replace the offending characters with safe escape
+ // sequences.
+ function quote(string) {
+ escapable.lastIndex = 0;
+ return escapable.test(string) ?
+ '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string' ? c :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' :
+ '"' + string + '"';
+ }
+
+ function f(n) {
+ return n < 10 ? '0' + n : n;
+ }
+
+ // Produce a string from holder[key].
+ function str(key, holder) {
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ partial,
+ value = holder[key];
+
+ if (value === null) {
+ return 'null';
+ }
+
+ // If the value has a toJSON method, call it to obtain a replacement value.
+ if (value && typeof value === 'object' && typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+ // What happens next depends on the value's type.
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+ // JSON numbers must be finite. Encode non-finite numbers as null.
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+ // If the value is a boolean or null, convert it to a string. Note:
+ // typeof null does not produce 'null'. The case is included here in
+ // the remote chance that this gets fixed someday.
+ return String(value);
+
+ case 'object':
+ // Make an array to hold the partial results of stringifying this object value.
+ partial = [];
+
+ // Is the value an array?
+// if (Object.prototype.toString.call(value)=="[object Array]") { // call added in IE5.5
+ if (value instanceof Array) {
+ // The value is an array. Stringify every element. Use null as a placeholder
+ // for non-JSON values.
+ for (i = 0; i < value.length; i++) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+ // Join all of the elements together, separated with commas, and wrap them in
+ // brackets.
+ v = partial.length === 0 ? '[]' : '[' + partial.join(',') + ']';
+ return v;
+ }
+
+// if (Object.prototype.toString.call(value)=="[object Date]") { // call added in IE5.5
+ if (value instanceof Date) {
+ return quote(value.getUTCFullYear() + '-' +
+ f(value.getUTCMonth() + 1) + '-' +
+ f(value.getUTCDate()) + 'T' +
+ f(value.getUTCHours()) + ':' +
+ f(value.getUTCMinutes()) + ':' +
+ f(value.getUTCSeconds()) + 'Z');
+ }
+
+ // Otherwise, iterate through all of the keys in the object.
+ for (k in value) {
+ v = str(k, value);
+ if (v) {
+ // partial.push(quote(k) + ':' + v); // array.push added in IE5.5
+ partial[partial.length] = quote(k) + ':' + v;
+ }
+ }
+
+ // Join all of the member texts together, separated with commas,
+ // and wrap them in braces.
+ v = partial.length === 0 ? '{}' : '{' + partial.join(',') + '}';
+ return v;
+ }
+ }
+
+ return str('', {'': value});
+ },
+
+ /*
+ * registered (user-defined) hooks
+ */
+ registeredHooks = {};
+
+ /*
+ * Platform test for Internet Explorer on Windows
+ */
+ function isWindowsIE() {
+ var agent = navigatorAlias.userAgent.toLowerCase();
+
+ return (agent.indexOf('msie') != -1) && (agent.indexOf('opera') == -1) &&
+ ((agent.indexOf('win') != -1) || (agent.indexOf('32bit') != -1));
+ }
+
+ /*
+ * Set cookie value
+ */
+ function setCookie(cookieName, value, daysToExpire, path, domain, secure) {
+ var expiryDate;
+
+ if (daysToExpire) {
+ // time is in milliseconds
+ expiryDate = new Date();
+ // there are 1000 * 60 * 60 * 24 milliseconds in a day (i.e., 86400000 or 8.64e7)
+ expiryDate.setTime(expiryDate.getTime() + daysToExpire * 8.64e7);
+ }
+
+ documentAlias.cookie = cookieName + '=' + escapeWrapper(value) +
+ (daysToExpire ? ';expires=' + expiryDate.toGMTString() : '') +
+ ';path=' + (path ? path : '/') +
+ (domain ? ';domain=' + domain : '') +
+ (secure ? ';secure' : '');
+ }
+
+ /*
+ * Get cookie value
+ */
+ function getCookie(cookieName, path, domain) {
+ var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)' +
+ (path ? '(;[ ]*expires=[^;]*)?;[ ]*path=' + path.replace('/', '\\/') + '' : '') +
+ (domain ? ';[ ]*domain=' + domain + '(;|$)' : '')),
+
+ cookieMatch = cookiePattern.exec(documentAlias.cookie);
+
+ return cookieMatch ? unescapeWrapper(cookieMatch[2]) : 0;
+ }
+
+ /*
+ * Discard cookie
+ */
+/* // NOT CURRENTLY USED
+ function dropCookie(cookieName, path, domain) {
+ // browser may not delete cookie until browser closed (session ends)
+ if (getCookie(cookieName)) {
+ // clear value, set expires in the past
+ setCookie(cookieName, '', -1, path, domain);
+ }
+ }
+*/
+
+ /*
+ * Send image request to Piwik server using GET.
+ */
+ function getImage(url, delay) {
+ var now = new Date(),
+ image = new Image(1, 1);
+
+ expireDateTime = now.getTime() + delay;
+
+ image.onLoad = function () { };
+ image.src = url;
+ }
+
+ /*
+ * Browser plugin tests
+ */
+ function detectBrowserPlugins() {
+ var i, mimeTypes = '';
+
+ function setPluginInfo(pluginName, mimeType) {
+ if (mimeTypes.indexOf(mimeType) != -1 && navigatorAlias.mimeTypes[mimeType].enabledPlugin !== null) {
+ pluginMap[pluginName][3] = '1';
+ }
+ }
+
+ /*
+ * Note: an ActiveXObject object has no intrinsic properties or methods;
+ * thus, no standard means of getting a plugin's version number
+ */
+ function setIEPluginInfo(pluginName, progids) {
+ if (progids !== null && isDefined(windowAlias.ActiveXObject)) {
+ for (var j = 0; j < progids.length; j++) {
+ try {
+ if (new ActiveXObject(progids[j])) {
+ pluginMap[pluginName][3] = '1';
+ break;
+ }
+ } catch (e) { }
+ }
+ }
+ }
+
+ // Begin detection of browser plugins
+ if (isWindowsIE()) {
+ for (i in pluginMap) {
+ setIEPluginInfo(i, pluginMap[i][2]);
+ }
+ } else {
+ for (i = 0; i < navigatorAlias.mimeTypes.length; i++) {
+ mimeTypes += navigatorAlias.mimeTypes[i].type.toLowerCase();
+ }
+
+ for (i in pluginMap) {
+ setPluginInfo(i, pluginMap[i][1]);
+ }
+ }
+ }
+
+ /*
+ * Get page referrer
+ */
+ function getReferrer() {
+ var referrer = '';
+ try {
+ referrer = top.document.referrer;
+ } catch (e) {
+ if (parent) {
+ try {
+ referrer = parent.document.referrer;
+ } catch (e2) {
+ referrer = '';
+ }
+ }
+ }
+ if (referrer === '') {
+ referrer = documentAlias.referrer;
+ }
+
+ return referrer;
+ }
+
+ /*
+ * Does browser have cookies enabled (for this site)?
+ */
+ function hasCookies() {
+ var testCookieName = '_pk_testcookie';
+ if (!isDefined(navigatorAlias.cookieEnabled)) {
+ setCookie(testCookieName, '1');
+ return getCookie(testCookieName) == '1' ? '1' : '0';
+ }
+
+ return navigatorAlias.cookieEnabled ? '1' : '0';
+ }
+
+
+ /*
+ * Get the web bug image (transparent single pixel, 1x1, image) to log visit in Piwik
+ */
+ function getWebBug() {
+ var i, customDataString, pluginString, extraString, now, request;
+
+ /*
+ * encode custom vars
+ */
+ customDataString = '';
+ if (isDefined(configCustomData)) {
+ customDataString = '&data=' + escapeWrapper(stringify(configCustomData));
+ }
+
+ /*
+ * encode plugin data
+ */
+ pluginString = '';
+ for (i in pluginMap) {
+ pluginString += '&' + pluginMap[i][0] + '=' + pluginMap[i][3];
+ }
+
+ extraString = executePluginMethod('log');
+
+ now = new Date();
+ request = configTrackerUrl + '?idsite=' + configTrackerSiteId +
+ '&url=' + escapeWrapper(documentAlias.location.href) +
+ '&action_name=' + escapeWrapper(configTitle) + // refs #530
+ '&res=' + screenAlias.width + 'x' + screenAlias.height +
+ '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() +
+ '&cookie=' + browserHasCookies +
+ '&urlref=' + escapeWrapper(pageReferrer) +
+ pluginString + customDataString + extraString;
+
+ getImage(request, configTrackerPause);
+ }
+
+ /*
+ * Log the click with the server
+ */
+ function logClick(url, linkType, customData) {
+ var customDataString, extraString, request;
+
+ /*
+ * encode custom data
+ */
+ customDataString = '';
+ if (isDefined(customData)) {
+ customDataString = '&data=' + escapeWrapper(stringify(configCustomData));
+ }
+
+ extraString = executePluginMethod('click');
+
+ request = configTrackerUrl + '?idsite=' + configTrackerSiteId +
+ '&' + linkType + '=' + escapeWrapper(url) +
+ '&rand=' + Math.random() +
+ '&redirect=0' +
+ customDataString + extraString;
+
+ getImage(request, configTrackerPause);
+ }
+
+ /*
+ * Is the host local? (i.e., not an outlink)
+ */
+ function isSiteHostName(hostName) {
+ var i, alias, offset;
+
+ for (i = 0; i < configHostsAlias.length; i++) {
+ alias = configHostsAlias[i];
+
+ if (hostName == alias) {
+ return true;
+ }
+
+ if (alias.substr(0, 2) == '*.') {
+ if ((hostName) == alias.substr(2)) {
+ return true;
+ }
+
+ offset = hostName.length - alias.length + 1;
+ if ((offset > 0) && (hostName.substr(offset) == alias.substr(1))) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Link or Download?
+ */
+ function getLinkType(className, href, isInLink) {
+ // outlinks
+ if (!isInLink) {
+ return 'link';
+ }
+
+ // does class indicate whether it is an (explicit/forced) outlink or a download?
+ var downloadOrLinkPattern = new RegExp('(^| )(' + configDownloadClass + '|' + configLinkClass + ')( |$)'),
+ match = downloadOrLinkPattern.exec(className),
+
+ // does file extension indicate that it is a download?
+ downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions + ')$', 'i');
+
+ // optimization of the nested if..elseif..else construct below
+ return match ? (match[2] == configDownloadClass ? 'download' : 'link') :
+ (downloadExtensionsPattern.test(href) ? 'download' : 0);
+
+/*
+ var linkType;
+
+ if (match) {
+ if (match[2] == configDownloadClass) {
+ // class attribute contains 'piwik_download' (or user's override)
+ linkType = 'download';
+ } else {
+ // class attribute contains 'piwik_link' (or user's override)
+ linkType = 'link';
+ }
+ } else if (downloadExtensionsPattern.test(sourceHref)) {
+ // file extension matches a defined download extension
+ linkType = 'download';
+ } else {
+ // otherwise none of the above
+ linkType = 0;
+ }
+
+ return linkType;
+ */
+ }
+
+ /*
+ * Handle click event
+ */
+ function clickHandler(clickEvent) {
+ var sourceElement, tag, linkType;
+
+ if (!isDefined(clickEvent)) {
+ clickEvent = windowAlias.event;
+ }
+
+ if (isDefined(clickEvent.target)) {
+ sourceElement = clickEvent.target;
+ } else if (isDefined(clickEvent.srcElement)) {
+ sourceElement = clickEvent.srcElement;
+ } else {
+ return true;
+ }
+
+ while ((tag = sourceElement.tagName) != 'A' && tag != 'AREA') {
+ sourceElement = sourceElement.parentNode;
+ }
+
+ if (isDefined(sourceElement.href)) {
+ // browsers, such as Safari, don't downcase hostname and href
+ var originalSourceHostName = sourceElement.hostname,
+ sourceHostName = originalSourceHostName.toLowerCase(),
+ sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName),
+ scriptProtocol = /^(javascript|vbscript|jscript|mocha|livescript|ecmascript):/i;
+
+ // ignore script pseudo-protocol links
+ if (!scriptProtocol.test(sourceHref)) {
+ // track outlinks and all downloads
+ linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName));
+ if (linkType) {
+ logClick(sourceHref, linkType);
+ }
+ }
+ }
+
+ // Returns true so href isn't cancelled
+ return true;
+ }
+
+ /*
+ * Construct regular expression of classes to be ignored
+ */
+ function getIgnoreRegExp() {
+ var i, ignoreRegExp = '(^| )(piwik_ignore';
+
+ if (isDefined(configIgnoreClasses)) {
+ for (i = 0; i < configIgnoreClasses.length; i++) {
+ ignoreRegExp += '|' + configIgnoreClasses[i];
+ }
+ }
+ ignoreRegExp += ')( |$)';
+
+ return new RegExp(ignoreRegExp);
+ }
+
+ /*
+ * Add click listener to a DOM element
+ */
+ function addClickListener(element) {
+ addEventListener(element, 'click', clickHandler, false);
+ }
+
+ /*
+ * Add click handlers to anchor and AREA elements, except those to be ignored
+ */
+ function addClickListeners() {
+ if (!linkTrackingInstalled) {
+ linkTrackingInstalled = true;
+
+ // iterate through anchor elements with href and AREA elements
+
+ var i, ignorePattern = getIgnoreRegExp(), linkElements = documentAlias.links;
+
+ if (linkElements) {
+ for (i = 0; i < linkElements.length; i++) {
+ if (!ignorePattern.test(linkElements[i].className)) {
+ addClickListener(linkElements[i]);
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * Register a user-defined hook
+ * - if userHook is a string, object construction is deferred,
+ * permitting the addition of privileged members
+ */
+ function registerHook(hookName, userHook) {
+ var hookObj = null;
+
+ if (typeof hookName == 'string' && !isDefined(registeredHooks[hookName])) {
+ if (typeof userHook == 'object') {
+ hookObj = userHook;
+ } else if (typeof userHook == 'string') {
+ try {
+ eval('hookObj =' + userHook);
+ } catch (e) { }
+ }
+
+ registeredHooks[hookName] = hookObj;
+ }
+ return hookObj;
+ }
+
+ /************************************************************
+ * Constructor
+ ************************************************************/
+
+ /*
+ * initialize tracker
+ */
+ pageReferrer = getReferrer();
+ browserHasCookies = hasCookies();
+ detectBrowserPlugins();
+
+ /*
+ * initialize plugins
+ */
+ executePluginMethod('run', registerHook);
+
+ /************************************************************
+ * Public data and methods
+ ************************************************************/
+
+ return {
+ /*
+ * Hook accessors
+ */
+ hook: registeredHooks,
+ getHook: function (hookName) {
+ return registeredHooks[hookName];
+ },
+
+ /*
+ * Specify the Piwik server URL
+ */
+ setTrackerUrl: function (trackerUrl) {
+ if (isDefined(trackerUrl)) {
+ configTrackerUrl = trackerUrl;
+ }
+ },
+
+ /*
+ * Specify the site ID
+ */
+ setSiteId: function (siteId) {
+ if (isDefined(siteId)) {
+ configTrackerSiteId = siteId;
+ }
+ },
+
+ /*
+ * Pass custom data to the server
+ */
+ setCustomData: function (customData) {
+ if (isDefined(customData)) {
+ configCustomData = customData;
+ }
+ },
+
+ /*
+ * Set delay for link tracking (in milliseconds)
+ */
+ setLinkTrackingTimer: function (delay) {
+ if (isDefined(delay)) {
+ configTrackerPause = delay;
+ }
+ },
+
+ /*
+ * Set list of file extensions to be recognized as downloads
+ */
+ setDownloadExtensions: function (extensions) {
+ if (isDefined(extensions)) {
+ configDownloadExtensions = extensions;
+ }
+ },
+
+ /*
+ * Specify additional file extensions to be recognized as downloads
+ */
+ addDownloadExtensions: function (extensions) {
+ if (isDefined(extensions)) {
+ configDownloadExtensions += '|' + extensions;
+ }
+ },
+
+ /*
+ * Set array of domains to be treated as local
+ */
+ setDomains: function (hostsAlias) {
+ if (typeof hostsAlias == 'object' && hostsAlias instanceof Array) {
+ configHostsAlias = hostsAlias;
+ // configHostAlias.push(windowAlias.location.hostname); // array.push added in IE5.5
+ configHostsAlias[configHostsAlias.length] = windowAlias.location.hostname;
+ } else if (typeof hostsAlias == 'string') {
+ configHostsAlias = [hostsAlias, windowAlias.location.hostname];
+ }
+ },
+
+ /*
+ * Set array of classes to be ignored if present in link
+ */
+ setIgnoreClasses: function (ignoreClasses) {
+ if (typeof ignoreClasses == 'object' && ignoreClasses instanceof Array) {
+ configIgnoreClasses = ignoreClasses;
+ } else if (typeof ignoreClasses == 'string') {
+ configIgnoreClasses = [ignoreClasses];
+ }
+ },
+
+ /*
+ * Override document.title
+ */
+ setDocumentTitle: function (title) {
+ if (isDefined(title)) {
+ configTitle = title;
+ }
+ },
+
+ /*
+ * Set download class name (i.e., override default: piwik_download)
+ */
+ setDownloadClass: function (className) {
+ if (typeof className == 'string' && className.length > 0) {
+ configDownloadClass = className;
+ }
+ },
+
+ /*
+ * Set outlink class name (i.e., override default: piwik_link)
+ */
+ setLinkClass: function (className) {
+ if (typeof className == 'string' && className.length > 0) {
+ configLinkClass = className;
+ }
+ },
+
+ /*
+ * Add click listener to a specific link element.
+ * When clicked, Piwik will log the click automatically.
+ */
+ addListener: function (element) {
+ if (isDefined(element)) {
+ addClickListener(element);
+ }
+ },
+
+ /*
+ * Install link tracker
+ */
+ enableLinkTracking: function () {
+ if (hasLoaded) {
+ // the load event has already fired, add the click listeners now
+ addClickListeners();
+ } else {
+ // defer until page has loaded
+ registeredOnLoadHandlers[registeredOnLoadHandlers.length] = function () {
+ addClickListeners();
+ };
+ }
+ },
+
+ /*
+ * Manually log a click from your own code.
+ */
+ trackLink: function (sourceUrl, linkType, customData) {
+ logClick(sourceUrl, linkType, customData);
+ },
+
+ /*
+ * Log visit to this page
+ */
+ trackPageView: function () {
+ getWebBug();
+ }
+ };
+ }
+
+ /************************************************************
+ * Constructor
+ ************************************************************/
+
+ // initialize the Piwik singleton
+ addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false);
+ addReadyListener();
+
+ /************************************************************
+ * Public data and methods
+ ************************************************************/
+
+ return {
+ /*
+ * Add plugin
+ */
+ addPlugin: function (pluginName, pluginObj) {
+ plugins[pluginName] = pluginObj;
+ },
+
+ /*
+ * Get Tracker
+ */
+ getTracker: function (piwikUrl, siteId) {
+ return new Tracker(piwikUrl, siteId);
+ }
+ };
+ }());
+
+ /************************************************************
+ * Deprecated functionality below
+ * - for legacy piwik.js compatibility
+ ************************************************************/
+
+ /*
+ * Piwik globals
+ *
+ * var piwik_install_tracker, piwik_tracker_pause, piwik_download_extensions, piwik_hosts_alias, piwik_ignore_classes;
+ */
+
+ /*
+ * Track click manually (function is defined below)
+ */
+ var piwik_track;
+
+ /*
+ * Track page visit
+ */
+ function piwik_log(documentTitle, siteId, piwikUrl, customData) {
+
+ function getOption(optionName) {
+ try {
+ return eval('piwik_' + optionName);
+ } catch (e) { }
+
+ return; /* undefined */
+ }
+
+ // instantiate the tracker
+ var piwikTracker = Piwik.getTracker(piwikUrl, siteId);
+
+ // initializer tracker
+ piwikTracker.setDocumentTitle(documentTitle);
+ piwikTracker.setCustomData(customData);
+
+ // handle Piwik globals
+ piwikTracker.setLinkTrackingTimer(getOption('tracker_pause'));
+ piwikTracker.setDownloadExtensions(getOption('download_extensions'));
+ piwikTracker.setDomains(getOption('hosts_alias'));
+ piwikTracker.setIgnoreClasses(getOption('ignore_classes'));
+
+ // track this page view
+ piwikTracker.trackPageView();
+
+ // default is to install the link tracker
+ if (getOption('install_tracker') !== false) {
+
+ // set-up click handler
+ piwik_track = function (sourceUrl, siteId, piwikUrl, linkType) {
+ piwikTracker.setSiteId(siteId);
+ piwikTracker.setTrackerUrl(piwikUrl);
+ piwikTracker.trackLink(sourceUrl, linkType);
+ };
+
+ // set-up link tracking
+ piwikTracker.enableLinkTracking();
+ }
+ }
+}
diff --git a/misc/testJavascriptTracker/index.php b/misc/testJavascriptTracker/index.php
deleted file mode 100644
index 1de9ff8de3..0000000000
--- a/misc/testJavascriptTracker/index.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-require_once '../../core/Url.php';
-$url = Piwik_Url::getCurrentUrlWithoutFileName();
-$urlPiwik = join("/", array_slice(split("/", $url), 0, -3));
-
-?>
-
-<div>
-You should update the piwik javascript code at the bottom of this page if needed.<br />
-And test the tracker by clicking all the links below, with several browsers...<br />
-<br />
-</div>
-
-<a href="mailto:test@test.com"> mailto test@test.com</a> <br />
-<a href="http://www.yahoo.fr"> yahoo france website</a> <br />
-<a href="http://www.yahoo.fr/index?test=test2&p_______=idugiduagi8*&*$&%(*^"> yahoo france website</a> <br />
-<a href="http://www.google.com"> google world website </a> <br />
-<a href="ftp://parcftp.xerox.com"> FTP xerox</a> <br />
-<a href="news://news.eclipse.org"> News::eclipse</a> <br />
-<a href="http://piwik.org"> piwik website </a> <br />
-<a href="http://www.testWithImageAndText.com"><img border=0 src="../../plugins/UserCountry/flags/fr.png"> Test with image + text </a> <br />
-<a href="http://www.testWithImageOnly.com"><img border=0 src="../../plugins/UserCountry/flags/pl.png"></a> <br />
-
-<br />
-<a class="piwik_ignore" href="./THIS_PDF_SHOULD_NOT_BE_COUNTED.pdf"> PDF wthdownload pdf </a> <br />
-<a href="./test.pdf"> download pdf (rel) </a> <br />
-<a href="./dir_test/test.pdf"> download pdf + directory (rel) </a> <br />
-<a href="../testJavascriptTracker/dir_test/test.pdf"> download pdf + parent directory (rel) </a> <br />
-<a href="./test.jpg"> download jpg (rel) </a> <br />
-<a href="./test.zip"> download zip (rel) </a> <br />
-<a href="./test.php?fileToDownload=test.zip"> download strange URL ?file=test.zip</a> <br />
-<a href="
-<?php echo $url; ?>
-test.rar"> download rar (abs) </a> <br />
-<br />
-<a href="./page2.php"> Next (rel)</a> <br />
-<a href="<?php echo $url; ?>page2.php"> Next (abs)</a> <br />
-
-
-
-<!-- Piwik -->
-<a href="http://piwik.org" title="Web analytics" onclick="window.open(this.href);return(false);">
-<script language="javascript" src="<?php echo $urlPiwik; ?>/piwik.js" type="text/javascript"></script>
-<script type="text/javascript">
-<!--
-piwik_action_name = '';
-piwik_idsite = 1;
-piwik_url = '<?php echo $urlPiwik; ?>/piwik.php';
-piwik_log(piwik_action_name, piwik_idsite,piwik_url);
-//-->
-</script><object>
-<noscript><p>Web analytics <img src="<?php echo $urlPiwik; ?>/piwik.php" style="border:0" alt="piwik"/></p>
-</noscript></object></a>
-<!-- /Piwik -->
diff --git a/misc/testJavascriptTracker/page2.php b/misc/testJavascriptTracker/page2.php
deleted file mode 100644
index cf53d38d11..0000000000
--- a/misc/testJavascriptTracker/page2.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-require_once '../../core/Url.php';
-$url = Piwik_Url::getCurrentUrlWithoutFileName();
-$urlPiwik = join("/", array_slice(split("/", $url), 0, -3));
-?>
-
-<div>
-You should update the piwik javascript code at the bottom of this page if needed.<br />
-And test the tracker by clicking all the links below, with several browsers...<br />
-<br />
-</div>
-
-<script type="text/javascript">
-<!--
-piwik_ignore_classes = ["no-tracking"];
-//-->
-</script>
-<h1>Ignore classes</h1>
-<a href="http://www.yahoo.com">Expecting a yahoo.com outlink</a> <br />
-<a href="http://piwik.org" class="piwik_ignore">Ignore this piwik.org outlink</a> <br />
-<a href="http://dev.piwik.org" class="no-tracking">Ignore this dev.piwik.org outlink</a> <br />
-
-<script type="text/javascript">
-<!--
-piwik_download_extensions = ".zip";
-//-->
-</script>
-<style type="text/css">
-a.boldlink {font-weight: bold}
-</style>
-<h1>Multiple classes</h1>
-<a href="./test.pdf" class="piwik_download">Track this download pdf (rel) </a> <br />
-<a href="./test.jpg" class="boldlink piwik_download">Track this download jpg (rel) </a> <br />
-<a href="./test.zip" class="boldlink no-tracking">Ignore this download zip (rel) </a> <br />
-
-<a href="./index.php"> Prev (rel)</a> <br />
-<a href="<?php echo $url; ?>index.php"> Prev (abs)</a> <br />
-
-<!-- Piwik -->
-<a href="http://piwik.org" title="Web analytics" onclick="window.open(this.href);return(false);">
-<script language="javascript" src="<?php echo $urlPiwik; ?>/piwik.js" type="text/javascript"></script>
-<script type="text/javascript">
-<!--
-piwik_action_name = '';
-piwik_idsite = 1;
-piwik_url = '<?php echo $urlPiwik; ?>/piwik.php';
-piwik_log(piwik_action_name, piwik_idsite,piwik_url);
-//-->
-</script><object>
-<noscript><p>Web analytics <img src="<?php echo $urlPiwik; ?>/piwik.php" style="border:0" alt="piwik"/></p>
-</noscript></object></a>
-<!-- /Piwik -->
-
-<script type="text/javascript">
-
-var testPkIsSiteHostname = false;
-if(testPkIsSiteHostname) {
- // automated testing
- _pk_hosts_alias = ["*.example.com"];
-
- if (_pk_is_site_hostname("localhost")) alert("failed: localhost does not match");
- if (_pk_is_site_hostname("google.com")) alert("failed: google.com does not match");
- if (!_pk_is_site_hostname("example.com")) alert("failed: example.com does match");
- if (!_pk_is_site_hostname("www.example.com")) alert("failed: www.example.com does match");
- if (!_pk_is_site_hostname("www.sub.example.com")) alert("failed: www.sub.example.com does match");
-}
-</script>
-
diff --git a/misc/testJavascriptTracker/test.jpg b/misc/testJavascriptTracker/test.jpg
deleted file mode 100644
index e69de29bb2..0000000000
--- a/misc/testJavascriptTracker/test.jpg
+++ /dev/null
diff --git a/misc/testJavascriptTracker/test.pdf b/misc/testJavascriptTracker/test.pdf
deleted file mode 100644
index e69de29bb2..0000000000
--- a/misc/testJavascriptTracker/test.pdf
+++ /dev/null
diff --git a/misc/testJavascriptTracker/test.rar b/misc/testJavascriptTracker/test.rar
deleted file mode 100644
index e69de29bb2..0000000000
--- a/misc/testJavascriptTracker/test.rar
+++ /dev/null
diff --git a/misc/testJavascriptTracker/test.zip b/misc/testJavascriptTracker/test.zip
deleted file mode 100644
index e69de29bb2..0000000000
--- a/misc/testJavascriptTracker/test.zip
+++ /dev/null
diff --git a/piwik.js b/piwik.js
index 07752e7ee2..ccc9af060a 100644
--- a/piwik.js
+++ b/piwik.js
@@ -1,250 +1,15 @@
-// Web analytics by Piwik - http://piwik.org
-// Copyleft 2007, All rights reversed.
-var _pk_use_title_as_name = 0;
-var _pk_install_tracker = 1;
-var _pk_tracker_pause = 500;
-var _pk_download_extensions = "7z|aac|avi|csv|doc|exe|flv|gif|gz|jpe?g|js|mp(3|4|e?g)|mov|pdf|phps|png|ppt|rar|sit|tar|torrent|txt|wma|wmv|xls|xml|zip";
-
-// Beginning script
-function _pk_plug_normal(_pk_pl) {
- if (_pk_tm.indexOf(_pk_pl) != -1 && (navigator.mimeTypes[_pk_pl].enabledPlugin != null))
- return '1';
- return '0';
-}
-
-function _pk_plug_ie(_pk_pl)
-{
- pk_found = false;
- document.write('<SCR' + 'IPT LANGUAGE=VBScript>\n on error resume next \n pk_found = IsObject(CreateObject("' + _pk_pl + '")) </SCR' + 'IPT>\n');
- if (pk_found) return '1';
- return '0';
-}
-
-var _pk_jav = '0'; if(navigator.javaEnabled()) _pk_jav='1';
-var _pk_agent = navigator.userAgent.toLowerCase();
-var _pk_moz = (navigator.appName.indexOf("Netscape") != -1);
-var _pk_ie = (_pk_agent.indexOf("msie") != -1);
-var _pk_win = ((_pk_agent.indexOf("win") != -1) || (_pk_agent.indexOf("32bit") != -1));
-var _pk_cookie = (navigator.cookieEnabled)? '1' : '0';
-if((typeof (navigator.cookieEnabled) == "undefined") && (_pk_cookie == '0')) {
- document.cookie="_pk_testcookie"
- _pk_cookie=(document.cookie.indexOf("_pk_testcookie")!=-1)? '1' : '0';
-}
-
-var _pk_dir='0',_pk_fla='0',_pk_pdf='0',_pk_qt = '0',_pk_rea = '0',_pk_wma='0';
-if (_pk_win && _pk_ie){
- _pk_dir = _pk_plug_ie("SWCtl.SWCtl.1");
- _pk_fla = _pk_plug_ie("ShockwaveFlash.ShockwaveFlash.1");
- if (_pk_plug_ie("PDF.PdfCtrl.1") == '1' || _pk_plug_ie('PDF.PdfCtrl.5') == '1' || _pk_plug_ie('PDF.PdfCtrl.6') == '1') _pk_pdf = '1';
- _pk_rea = _pk_plug_ie("rmocx.RealPlayer G2 Control.1");
- _pk_wma = _pk_plug_ie("wmplayer.ocx"); // Old : "MediaPlayer.MediaPlayer.1"
-} else {
- var _pk_tm = '';
- for (var i=0; i < navigator.mimeTypes.length; i++)
- _pk_tm += navigator.mimeTypes[i].type.toLowerCase();
- _pk_dir = _pk_plug_normal("application/x-director");
- _pk_fla = _pk_plug_normal("application/x-shockwave-flash");
- _pk_pdf = _pk_plug_normal("application/pdf");
- _pk_rea = _pk_plug_normal("audio/x-pn-realaudio-plugin");
- _pk_wma = _pk_plug_normal("application/x-mplayer2");
-}
-
-var _pk_rtu = '';
-try {
- _pk_rtu = top.document.referrer;
-} catch(e1) {
- if(parent){
- try{ _pk_rtu = parent.document.referrer; } catch(e2) { _pk_rtu=''; }
- }
-}
-if(_pk_rtu == '') {
- _pk_rtu = document.referrer;
-}
-
-function _pk_escape(_pk_str){
- if(typeof(encodeURIComponent) == 'function') {
- return encodeURIComponent(_pk_str);
- } else {
- return escape(_pk_str);
- }
-}
-var _pk_title = '';
-if (document.title && document.title!="") _pk_title = _pk_escape(document.title);
-
-var _pk_called;
-
-function _pk_getUrlLog( _pk_action_name, _pk_site, _pk_pkurl, _pk_custom_vars )
-{
- var _pk_custom_vars_str = '';
- if(typeof _pk_custom_vars == "undefined"){
- _pk_custom_vars = false;
- }
- if (_pk_custom_vars) {
- for (var i in _pk_custom_vars) {
- if (!Array.prototype[i]){
- _pk_custom_vars_str = _pk_custom_vars_str + '&vars['+ escape(i) + ']' + "=" + escape(_pk_custom_vars[i]);
- }
- }
- }
-
- var _pk_url = document.location.href;
- var _pk_da = new Date();
- var _pk_src = _pk_pkurl
- +'?url='+_pk_escape(document.location.href)
- +'&action_name='+_pk_escape(_pk_action_name)
- +'&idsite='+_pk_site
- +'&res='+screen.width+'x'+screen.height
- +'&h='+_pk_da.getHours()+'&m='+_pk_da.getMinutes()+'&s='+_pk_da.getSeconds()
- +'&fla='+_pk_fla+'&dir='+_pk_dir+'&realp='+_pk_rea+'&pdf='+_pk_pdf
- +'&wma='+_pk_wma+'&java='+_pk_jav+'&cookie='+_pk_cookie
- +'&title='+_pk_title
- +'&urlref='+_pk_escape(_pk_rtu)
- +_pk_custom_vars_str;
- return _pk_src;
-}
-
-function piwik_log( _pk_action_name, _pk_site, _pk_pkurl, _pk_custom_vars )
-{
- if(_pk_called && (!_pk_action_name || _pk_action_name=="")) return;
- var _pk_src = _pk_getUrlLog(_pk_action_name, _pk_site, _pk_pkurl, _pk_custom_vars );
- document.writeln('<img src="'+_pk_src+'" alt="" style="border:0" />');
- if(!_pk_action_name || _pk_action_name=="") _pk_called=1;
-
- _pk_init_tracker(_pk_site, _pk_pkurl);
-}
-
-function _pk_add_event(elm, evType, fn, useCapture)
-{
- if (elm.addEventListener) {
- elm.addEventListener(evType, fn, useCapture);
- return true;
- } else if (elm.attachEvent) {
- var r = elm.attachEvent('on' + evType, fn);
- return r;
- } else {
- elm['on' + evType] = fn;
- }
-}
-
-var _pk_tracker_site, _pk_tracker_url;
-
-function _pk_init_tracker(_pk_site, _pk_pkurl)
-{
- if( typeof(piwik_install_tracker) != "undefined" )
- _pk_install_tracker = piwik_install_tracker;
- if( typeof(piwik_tracker_pause) != "undefined" )
- _pk_tracker_pause = piwik_tracker_pause;
- if( typeof(piwik_download_extensions) != "undefined" )
- _pk_download_extensions = piwik_download_extensions;
-
- _pk_hosts_alias = ( typeof(piwik_hosts_alias) != "undefined" ? piwik_hosts_alias : new Array());
- _pk_hosts_alias[_pk_hosts_alias.length] = window.location.hostname;
-
- if( !_pk_install_tracker )
- return;
-
- _pk_tracker_site = _pk_site;
- _pk_tracker_url = _pk_pkurl;
-
- var _pk_ignore_regexp = '(?:^| )(piwik_ignore';
- if (typeof(piwik_ignore_classes) != "undefined")
- for(var i=0; i < piwik_ignore_classes.length; i++)
- _pk_ignore_regexp += '|' + piwik_ignore_classes[i];
- _pk_ignore_regexp += ')(?: |$)';
-
- var _pk_class = new RegExp(_pk_ignore_regexp);
-
- if (document.getElementsByTagName) {
- var targetTags = [ 'a', 'area' ];
- for (var j=0; j < targetTags.length; j++) {
- var linksElements = document.getElementsByTagName(targetTags[j])
- for (var i=0; i < linksElements.length; i++) {
- if( !_pk_class.exec( linksElements[i].className ) )
- _pk_add_event(linksElements[i], 'mousedown', _pk_click, false);
- }
- }
- }
-}
-
-function _pk_dummy() { return true; }
-
-function _pk_pause(_pk_time_msec) {
- var _pk_now = new Date();
- var _pk_expire = _pk_now.getTime() + _pk_time_msec;
- while(_pk_now.getTime() < _pk_expire)
- _pk_now = new Date();
-}
-
-// _pk_type only 'download' and 'link' types supported
-function piwik_track(url, _pk_site, _pk_url, _pk_type)
-{
- var _pk_image = new Image();
- _pk_image.onLoad = function() { _pk_dummy(); };
- _pk_image.src = _pk_url + '?idsite=' + _pk_site + '&' + _pk_type + '=' + escape(url) + '&rand=' + Math.random() + '&redirect=0';
- _pk_pause(_pk_tracker_pause);
-}
-
-function _pk_is_site_hostname(_pk_hostname) {
- var alias, offset;
-
- for (var i=0; i < _pk_hosts_alias.length; i++) {
- alias = _pk_hosts_alias[i];
-
- if( _pk_hostname === alias )
- return true;
-
- if ( alias.substr(0, 2) == "*." ) {
- if ((_pk_hostname) == alias.substr(2))
- return true;
-
- offset = _pk_hostname.length - alias.length + 1;
- if ((offset > 0) && (_pk_hostname.substr(offset) == alias.substr(1)))
- return true;
- }
- }
-
- return false;
-}
-
-function _pk_click(e)
-{
- var source, tag;
-
- if (typeof e == 'undefined')
- var e = window.event;
-
- if (typeof e.target != 'undefined')
- source = e.target;
- else if (typeof e.srcElement != 'undefined')
- source = e.srcElement;
- else return true;
-
- while ((tag = source.tagName) != 'A' && tag != 'AREA')
- source = source.parentNode;
-
- if( typeof source.href == 'undefined' )
- return true;
-
- var jsProtocol = 'javascript:';
- if (source.href.substr(0, jsProtocol.length) == jsProtocol)
- return true;
-
- var sourceHostName = source.hostname.toLowerCase();
- var sourceHref = source.href.replace(source.hostname, sourceHostName);
-
- var _pk_class = new RegExp('(?:^| )piwik_(download|link)(?: |$)');
- var _pk_download = new RegExp('\\.(' + _pk_download_extensions + ')$', 'i');
- var _pk_not_site_hostname = !_pk_is_site_hostname(sourceHostName);
- var _pk_link_match = _pk_class.exec( source.className);
- var _pk_link_type = _pk_link_match ? _pk_link_match[1] : 0;
-
- if (_pk_link_type == 'link')
- _pk_not_site_hostname = 1;
- else if (!_pk_link_type)
- _pk_link_type = (_pk_download.test(sourceHref) ? 'download' : 'link');
-
- if( _pk_not_site_hostname || _pk_link_type == 'download' )
- piwik_track(sourceHref, _pk_tracker_site, _pk_tracker_url, _pk_link_type);
-
- return true;
-}
+/*
+ * Piwik - Web Analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
+ * @version $Id$
+ */
+var Piwik;try{if(Piwik.getTracker){}}catch(e){Piwik=(function(){var c,j={},f=document,d=navigator,h=screen,m=window,g=false,o=[];function n(q){return typeof q!=="undefined"}function i(q,s,r,t){if(q.addEventListener){q.addEventListener(s,r,t);return true}else{if(q.attachEvent){return q.attachEvent("on"+s,r)}}q["on"+s]=r}function l(s,u){var t="",r,q;for(r in j){q=j[r][s];if(typeof q==="function"){t+=q(u)}}return t}function b(q){if(n(c)){var r=new Date();while(r.getTime()<c){r=new Date()}}l("unload")}function p(r){if(!g){g=true;l("load");for(var q=0;q<o.length;q++){o[q]()}}return true}function a(){if(f.addEventListener){i(f,"DOMContentLoaded",function(){f.removeEventListener("DOMContentLoaded",arguments.callee,false);p()})}else{if(f.attachEvent){f.attachEvent("onreadystatechange",function(){if(f.readyState==="complete"){f.detachEvent("onreadystatechange",arguments.callee);
+p()}});if(f.documentElement.doScroll&&m==m.top){(function(){if(g){return}try{f.documentElement.doScroll("left")}catch(q){setTimeout(arguments.callee,0);return}p()}())}}}i(m,"load",p,false)}function k(R,C){var E=R||"",Z=C||"",X=f.title||"",L="7z|aac|arc|arj|asf|asx|avi|bin|csv|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd||xls|xml|z|zip",A=[m.location.hostname],U=[],Q="piwik_download",q="piwik_link",D=500,N,S="0",x,O={director:["dir","application/x-director",["SWCtl.SWctl.1"],"0",""],flash:["fla","application/x-shockwave-flash",["ShockwaveFlash.ShockwaveFlash.1"],"0",""],pdf:["pdf","application/pdf",["AcroPDF.PDF.1","PDF.PdfCtrl.6","PDF.PdfCtrl.5","PDF.PdfCtrl.1"],"0",""],realplayer:["realp","audio/x-pn-realaudio-plugin",["rmocx.RealPlayer G2 Control.1","RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)"],"0",""],wma:["wma","application/x-mplayer2",["WMPlayer.OCX","MediaPlayer.MediaPlayer.1"],"0",""]},H=false,t=m.encodeURIComponent||escape,W=m.decodeURIComponent||unescape,Y=function(ac){var af=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,ad={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};
+function aa(ag){af.lastIndex=0;return af.test(ag)?'"'+ag.replace(af,function(ah){var ai=ad[ah];return typeof ai==="string"?ai:"\\u"+("0000"+ah.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+ag+'"'}function ab(ag){return ag<10?"0"+ag:ag}function ae(ak,ai){var aj,ah,ag,al,am=ai[ak];if(am===null){return"null"}if(am&&typeof am==="object"&&typeof am.toJSON==="function"){am=am.toJSON(ak)}switch(typeof am){case"string":return aa(am);case"number":return isFinite(am)?String(am):"null";case"boolean":case"null":return String(am);case"object":al=[];if(am instanceof Array){for(aj=0;aj<am.length;aj++){al[aj]=ae(aj,am)||"null"}ag=al.length===0?"[]":"["+al.join(",")+"]";return ag}if(am instanceof Date){return aa(am.getUTCFullYear()+"-"+ab(am.getUTCMonth()+1)+"-"+ab(am.getUTCDate())+"T"+ab(am.getUTCHours())+":"+ab(am.getUTCMinutes())+":"+ab(am.getUTCSeconds())+"Z")}for(ah in am){ag=ae(ah,am);if(ag){al[al.length]=aa(ah)+":"+ag}}ag=al.length===0?"{}":"{"+al.join(",")+"}";return ag}}return ae("",{"":ac})},v={};
+function K(){var aa=d.userAgent.toLowerCase();return(aa.indexOf("msie")!=-1)&&(aa.indexOf("opera")==-1)&&((aa.indexOf("win")!=-1)||(aa.indexOf("32bit")!=-1))}function z(af,ad,ab,ae,aa,ag){var ac;if(ab){ac=new Date();ac.setTime(ac.getTime()+ab*86400000)}f.cookie=af+"="+t(ad)+(ab?";expires="+ac.toGMTString():"")+";path="+(ae?ae:"/")+(aa?";domain="+aa:"")+(ag?";secure":"")}function u(ae,ad,aa){var ab=new RegExp("(^|;)[ ]*"+ae+"=([^;]*)"+(ad?"(;[ ]*expires=[^;]*)?;[ ]*path="+ad.replace("/","\\/")+"":"")+(aa?";[ ]*domain="+aa+"(;|$)":"")),ac=ab.exec(f.cookie);return ac?W(ac[2]):0}function r(ac,ab){var aa=new Date(),ad=new Image(1,1);c=aa.getTime()+ab;ad.onLoad=function(){};ad.src=ac}function y(){var ad,ac="";function ab(ae,af){if(ac.indexOf(af)!=-1&&d.mimeTypes[af].enabledPlugin!==null){O[ae][3]="1"}}function aa(ae,af){if(af!==null&&n(m.ActiveXObject)){for(var ag=0;ag<af.length;ag++){try{if(new ActiveXObject(af[ag])){O[ae][3]="1";break}}catch(ah){}}}}if(K()){for(ad in O){aa(ad,O[ad][2])}}else{for(ad=0;
+ad<d.mimeTypes.length;ad++){ac+=d.mimeTypes[ad].type.toLowerCase()}for(ad in O){ab(ad,O[ad][1])}}}function J(){var aa="";try{aa=top.document.referrer}catch(ac){if(parent){try{aa=parent.document.referrer}catch(ab){aa=""}}}if(aa===""){aa=f.referrer}return aa}function F(){var aa="_pk_testcookie";if(!n(d.cookieEnabled)){z(aa,"1");return u(aa)=="1"?"1":"0"}return d.cookieEnabled?"1":"0"}function P(){var ad,aa,af,ac,ab,ae;aa="";if(n(N)){aa="&data="+t(Y(N))}af="";for(ad in O){af+="&"+O[ad][0]+"="+O[ad][3]}ac=l("log");ab=new Date();ae=E+"?idsite="+Z+"&url="+t(f.location.href)+"&action_name="+t(X)+"&res="+h.width+"x"+h.height+"&h="+ab.getHours()+"&m="+ab.getMinutes()+"&s="+ab.getSeconds()+"&cookie="+S+"&urlref="+t(x)+af+aa+ac;r(ae,D)}function B(ac,aa,af){var ab,ad,ae;ab="";if(n(af)){ab="&data="+t(Y(N))}ad=l("click");ae=E+"?idsite="+Z+"&"+aa+"="+t(ac)+"&rand="+Math.random()+"&redirect=0"+ab+ad;r(ae,D)}function M(ac){var ab,aa,ad;for(ab=0;ab<A.length;ab++){aa=A[ab];if(ac==aa){return true}if(aa.substr(0,2)=="*."){if((ac)==aa.substr(2)){return true
+}ad=ac.length-aa.length+1;if((ad>0)&&(ac.substr(ad)==aa.substr(1))){return true}}}return false}function s(ad,ac,ab){if(!ab){return"link"}var aa=new RegExp("(^| )("+Q+"|"+q+")( |$)"),ae=aa.exec(ad),af=new RegExp("\\.("+L+")$","i");return ae?(ae[2]==Q?"download":"link"):(af.test(ac)?"download":0)}function G(ah){var af,ab,aa;if(!n(ah)){ah=m.event}if(n(ah.target)){af=ah.target}else{if(n(ah.srcElement)){af=ah.srcElement}else{return true}}while((ab=af.tagName)!="A"&&ab!="AREA"){af=af.parentNode}if(n(af.href)){var ae=af.hostname,ad=ae.toLowerCase(),ag=af.href.replace(ae,ad),ac=/^(javascript|vbscript|jscript|mocha|livescript|ecmascript):/i;if(!ac.test(ag)){aa=s(af.className,ag,M(ad));if(aa){B(ag,aa)}}}return true}function w(){var ab,aa="(^| )(piwik_ignore";if(n(U)){for(ab=0;ab<U.length;ab++){aa+="|"+U[ab]}}aa+=")( |$)";return new RegExp(aa)}function V(aa){i(aa,"click",G,false)}function I(){if(!H){H=true;var ab,aa=w(),ac=f.links;if(ac){for(ab=0;ab<ac.length;ab++){if(!aa.test(ac[ab].className)){V(ac[ab])
+}}}}}function T(aa,ac){var ab=null;if(typeof aa=="string"&&!n(v[aa])){if(typeof ac=="object"){ab=ac}else{if(typeof ac=="string"){try{eval("hookObj ="+ac)}catch(ad){}}}v[aa]=ab}return ab}x=J();S=F();y();l("run",T);return{hook:v,getHook:function(aa){return v[aa]},setTrackerUrl:function(aa){if(n(aa)){E=aa}},setSiteId:function(aa){if(n(aa)){Z=aa}},setCustomData:function(aa){if(n(aa)){N=aa}},setLinkTrackingTimer:function(aa){if(n(aa)){D=aa}},setDownloadExtensions:function(aa){if(n(aa)){L=aa}},addDownloadExtensions:function(aa){if(n(aa)){L+="|"+aa}},setDomains:function(aa){if(typeof aa=="object"&&aa instanceof Array){A=aa;A[A.length]=m.location.hostname}else{if(typeof aa=="string"){A=[aa,m.location.hostname]}}},setIgnoreClasses:function(aa){if(typeof aa=="object"&&aa instanceof Array){U=aa}else{if(typeof aa=="string"){U=[aa]}}},setDocumentTitle:function(aa){if(n(aa)){X=aa}},setDownloadClass:function(aa){if(typeof aa=="string"&&aa.length>0){Q=aa}},setLinkClass:function(aa){if(typeof aa=="string"&&aa.length>0){q=aa
+}},addListener:function(aa){if(n(aa)){V(aa)}},enableLinkTracking:function(){if(g){I()}else{o[o.length]=function(){I()}}},trackLink:function(ab,aa,ac){B(ab,aa,ac)},trackPageView:function(){P()}}}i(m,"beforeunload",b,false);a();return{addPlugin:function(q,r){j[q]=r},getTracker:function(q,r){return new k(q,r)}}}());var piwik_track;function piwik_log(c,g,a,f){function b(h){try{return eval("piwik_"+h)}catch(i){}return}var d=Piwik.getTracker(a,g);d.setDocumentTitle(c);d.setCustomData(f);d.setLinkTrackingTimer(b("tracker_pause"));d.setDownloadExtensions(b("download_extensions"));d.setDomains(b("hosts_alias"));d.setIgnoreClasses(b("ignore_classes"));d.trackPageView();if(b("install_tracker")!==false){piwik_track=function(j,k,i,h){d.setSiteId(k);d.setTrackerUrl(i);d.trackLink(j,h)};d.enableLinkTracking()}}}; \ No newline at end of file
diff --git a/tests/javascript/assets/testrunner.js b/tests/javascript/assets/testrunner.js
new file mode 100644
index 0000000000..dc190afeaf
--- /dev/null
+++ b/tests/javascript/assets/testrunner.js
@@ -0,0 +1,780 @@
+/*
+ * QUnit - jQuery unit testrunner
+ *
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2008 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Id: testrunner.js 6173 2009-02-02 20:09:32Z jeresig $
+ */
+
+(function($) {
+
+// Tests for equality any JavaScript type and structure without unexpected results.
+// Discussions and reference: http://philrathe.com/articles/equiv
+// Test suites: http://philrathe.com/tests/equiv
+// Author: Philippe Rathé <prathe@gmail.com>
+var equiv = function () {
+
+ var innerEquiv; // the real equiv function
+ var callers = []; // stack to decide between skip/abort functions
+
+ // Determine what is o.
+ function hoozit(o) {
+ if (typeof o === "string") {
+ return "string";
+
+ } else if (typeof o === "boolean") {
+ return "boolean";
+
+ } else if (typeof o === "number") {
+
+ if (isNaN(o)) {
+ return "nan";
+ } else {
+ return "number";
+ }
+
+ } else if (typeof o === "undefined") {
+ return "undefined";
+
+ // consider: typeof null === object
+ } else if (o === null) {
+ return "null";
+
+ // consider: typeof [] === object
+ } else if (o instanceof Array) {
+ return "array";
+
+ // consider: typeof new Date() === object
+ } else if (o instanceof Date) {
+ return "date";
+
+ // consider: /./ instanceof Object;
+ // /./ instanceof RegExp;
+ // typeof /./ === "function"; // => false in IE and Opera,
+ // true in FF and Safari
+ } else if (o instanceof RegExp) {
+ return "regexp";
+
+ } else if (typeof o === "object") {
+ return "object";
+
+ } else if (o instanceof Function) {
+ return "function";
+ }
+ }
+
+ // Call the o related callback with the given arguments.
+ function bindCallbacks(o, callbacks, args) {
+ var prop = hoozit(o);
+ if (prop) {
+ if (hoozit(callbacks[prop]) === "function") {
+ return callbacks[prop].apply(callbacks, args);
+ } else {
+ return callbacks[prop]; // or undefined
+ }
+ }
+ }
+
+ var callbacks = function () {
+
+ // for string, boolean, number and null
+ function useStrictEquality(b, a) {
+ return a === b;
+ }
+
+ return {
+ "string": useStrictEquality,
+ "boolean": useStrictEquality,
+ "number": useStrictEquality,
+ "null": useStrictEquality,
+ "undefined": useStrictEquality,
+
+ "nan": function (b) {
+ return isNaN(b);
+ },
+
+ "date": function (b, a) {
+ return hoozit(b) === "date" && a.valueOf() === b.valueOf();
+ },
+
+ "regexp": function (b, a) {
+ return hoozit(b) === "regexp" &&
+ a.source === b.source && // the regex itself
+ a.global === b.global && // and its modifers (gmi) ...
+ a.ignoreCase === b.ignoreCase &&
+ a.multiline === b.multiline;
+ },
+
+ // - skip when the property is a method of an instance (OOP)
+ // - abort otherwise,
+ // initial === would have catch identical references anyway
+ "function": function () {
+ var caller = callers[callers.length - 1];
+ return caller !== Object &&
+ typeof caller !== "undefined";
+ },
+
+ "array": function (b, a) {
+ var i;
+ var len;
+
+ // b could be an object literal here
+ if ( ! (hoozit(b) === "array")) {
+ return false;
+ }
+
+ len = a.length;
+ if (len !== b.length) { // safe and faster
+ return false;
+ }
+ for (i = 0; i < len; i++) {
+ if( ! innerEquiv(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "object": function (b, a) {
+ var i;
+ var eq = true; // unless we can proove it
+ var aProperties = [], bProperties = []; // collection of strings
+
+ // comparing constructors is more strict than using instanceof
+ if ( a.constructor !== b.constructor) {
+ return false;
+ }
+
+ // stack constructor before traversing properties
+ callers.push(a.constructor);
+
+ for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
+
+ aProperties.push(i); // collect a's properties
+
+ if ( ! innerEquiv(a[i], b[i])) {
+ eq = false;
+ }
+ }
+
+ callers.pop(); // unstack, we are done
+
+ for (i in b) {
+ bProperties.push(i); // collect b's properties
+ }
+
+ // Ensures identical properties name
+ return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+ }
+ };
+ }();
+
+ innerEquiv = function () { // can take multiple arguments
+ var args = Array.prototype.slice.apply(arguments);
+ if (args.length < 2) {
+ return true; // end transition
+ }
+
+ return (function (a, b) {
+ if (a === b) {
+ return true; // catch the most you can
+
+ } else if (typeof a !== typeof b || a === null || b === null || typeof a === "undefined" || typeof b === "undefined") {
+ return false; // don't lose time with error prone cases
+
+ } else {
+ return bindCallbacks(a, callbacks, [b, a]);
+ }
+
+ // apply transition with (1..n) arguments
+ })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
+ };
+
+ return innerEquiv;
+}(); // equiv
+
+var GETParams = $.map( location.search.slice(1).split('&'), decodeURIComponent ),
+ ngindex = $.inArray("noglobals", GETParams),
+ noglobals = ngindex !== -1;
+
+if( noglobals )
+ GETParams.splice( ngindex, 1 );
+
+var config = {
+ stats: {
+ all: 0,
+ bad: 0
+ },
+ queue: [],
+ // block until document ready
+ blocking: true,
+ //restrict modules/tests by get parameters
+ filters: GETParams,
+ isLocal: !!(window.location.protocol == 'file:')
+};
+
+// public API as global methods
+$.extend(window, {
+ test: test,
+ module: module,
+ expect: expect,
+ ok: ok,
+ equals: equals,
+ start: start,
+ stop: stop,
+ reset: reset,
+ isLocal: config.isLocal,
+ same: function(a, b, message) {
+ push(equiv(a, b), a, b, message);
+ },
+ QUnit: {
+ equiv: equiv,
+ ok: ok,
+ done: function(failures, total){},
+ log: function(result, message){}
+ },
+ // legacy methods below
+ isSet: isSet,
+ isObj: isObj,
+ compare: function() {
+ throw "compare is deprecated - use same() instead";
+ },
+ compare2: function() {
+ throw "compare2 is deprecated - use same() instead";
+ },
+ serialArray: function() {
+ throw "serialArray is deprecated - use jsDump.parse() instead";
+ },
+ q: q,
+ t: t,
+ url: url,
+ triggerEvent: triggerEvent
+});
+
+$(window).load(function() {
+ $('#userAgent').html(navigator.userAgent);
+ var head = $('<div class="testrunner-toolbar"><label for="filter-pass">Hide passed tests</label></div>').insertAfter("#userAgent");
+ $('<input type="checkbox" id="filter-pass" />').attr("disabled", true).prependTo(head).click(function() {
+ $('li.pass')[this.checked ? 'hide' : 'show']();
+ });
+ $('<input type="checkbox" id="filter-missing">').attr("disabled", true).appendTo(head).click(function() {
+ $("li.fail:contains('missing test - untested code is broken code')").parent('ol').parent('li.fail')[this.checked ? 'hide' : 'show']();
+ });
+ $("#filter-missing").after('<label for="filter-missing">Hide missing tests (untested code is broken code)</label>');
+ runTest();
+});
+
+function synchronize(callback) {
+ config.queue.push(callback);
+ if(!config.blocking) {
+ process();
+ }
+}
+
+function process() {
+ while(config.queue.length && !config.blocking) {
+ config.queue.shift()();
+ }
+}
+
+function stop(timeout) {
+ config.blocking = true;
+ if (timeout)
+ config.timeout = setTimeout(function() {
+ QUnit.ok( false, "Test timed out" );
+ start();
+ }, timeout);
+}
+function start() {
+ // A slight delay, to avoid any current callbacks
+ setTimeout(function() {
+ if(config.timeout)
+ clearTimeout(config.timeout);
+ config.blocking = false;
+ process();
+ }, 13);
+}
+
+function validTest( name ) {
+ var i = config.filters.length,
+ run = false;
+
+ if( !i )
+ return true;
+
+ while( i-- ){
+ var filter = config.filters[i],
+ not = filter.charAt(0) == '!';
+ if( not )
+ filter = filter.slice(1);
+ if( name.indexOf(filter) != -1 )
+ return !not;
+ if( not )
+ run = true;
+ }
+ return run;
+}
+
+function runTest() {
+ config.blocking = false;
+ var started = +new Date;
+ config.fixture = document.getElementById('main').innerHTML;
+ config.ajaxSettings = $.ajaxSettings;
+ synchronize(function() {
+ $('<p id="testresult" class="result"/>').html(['Tests completed in ',
+ +new Date - started, ' milliseconds.<br/>',
+ '<span class="bad">', config.stats.bad, '</span> tests of <span class="all">', config.stats.all, '</span> failed.']
+ .join(''))
+ .appendTo("body");
+ $("#banner").addClass(config.stats.bad ? "fail" : "pass");
+ QUnit.done( config.stats.bad, config.stats.all );
+ });
+}
+
+var pollution;
+
+function saveGlobal(){
+ pollution = [ ];
+
+ if( noglobals )
+ for( var key in window )
+ pollution.push(key);
+}
+function checkPollution( name ){
+ var old = pollution;
+ saveGlobal();
+
+ if( pollution.length > old.length ){
+ ok( false, "Introduced global variable(s): " + diff(old, pollution).join(", ") );
+ config.expected++;
+ }
+}
+
+function diff( clean, dirty ){
+ return $.grep( dirty, function(name){
+ return $.inArray( name, clean ) == -1;
+ });
+}
+
+function test(name, callback) {
+ if(config.currentModule)
+ name = config.currentModule + " module: " + name;
+ var lifecycle = $.extend({
+ setup: function() {},
+ teardown: function() {}
+ }, config.moduleLifecycle);
+
+ if ( !validTest(name) )
+ return;
+
+ synchronize(function() {
+ config.assertions = [];
+ config.expected = null;
+ try {
+ if( !pollution )
+ saveGlobal();
+ lifecycle.setup();
+ } catch(e) {
+ QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
+ }
+ })
+ synchronize(function() {
+ try {
+ callback();
+ } catch(e) {
+ if( typeof console != "undefined" && console.error && console.warn ) {
+ console.error("Test " + name + " died, exception and test follows");
+ console.error(e);
+ console.warn(callback.toString());
+ }
+ QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
+ // else next test will carry the responsibility
+ saveGlobal();
+ }
+ });
+ synchronize(function() {
+ try {
+ checkPollution();
+ lifecycle.teardown();
+ } catch(e) {
+ QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
+ }
+ })
+ synchronize(function() {
+ try {
+ reset();
+ } catch(e) {
+ if( typeof console != "undefined" && console.error && console.warn ) {
+ console.error("reset() failed, following Test " + name + ", exception and reset fn follows");
+ console.error(e);
+ console.warn(reset.toString());
+ }
+ }
+
+ if(config.expected && config.expected != config.assertions.length) {
+ QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
+ }
+
+ var good = 0, bad = 0;
+ var ol = $("<ol/>").hide();
+ config.stats.all += config.assertions.length;
+ for ( var i = 0; i < config.assertions.length; i++ ) {
+ var assertion = config.assertions[i];
+ $("<li/>").addClass(assertion.result ? "pass" : "fail").text(assertion.message || "(no message)").appendTo(ol);
+ assertion.result ? good++ : bad++;
+ }
+ config.stats.bad += bad;
+
+ var b = $("<strong/>").html(name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>")
+ .click(function(){
+ $(this).next().toggle();
+ })
+ .dblclick(function(event) {
+ var target = $(event.target).filter("strong").clone();
+ if ( target.length ) {
+ target.children().remove();
+ location.href = location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent($.trim(target.text()));
+ }
+ });
+
+ $("<li/>").addClass(bad ? "fail" : "pass").append(b).append(ol).appendTo("#tests");
+
+ if(bad) {
+ $("#filter-pass").attr("disabled", null);
+ $("#filter-missing").attr("disabled", null);
+ }
+ });
+}
+
+// call on start of module test to prepend name to all tests
+function module(name, lifecycle) {
+ config.currentModule = name;
+ config.moduleLifecycle = lifecycle;
+}
+
+/**
+ * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
+ */
+function expect(asserts) {
+ config.expected = asserts;
+}
+
+/**
+ * Resets the test setup. Useful for tests that modify the DOM.
+ */
+function reset() {
+ $("#main").html( config.fixture );
+ $.event.global = {};
+ $.ajaxSettings = $.extend({}, config.ajaxSettings);
+}
+
+/**
+ * Asserts true.
+ * @example ok( $("a").size() > 5, "There must be at least 5 anchors" );
+ */
+function ok(a, msg) {
+ QUnit.log(a, msg);
+
+ config.assertions.push({
+ result: !!a,
+ message: msg
+ });
+}
+
+/**
+ * Asserts that two arrays are the same
+ */
+function isSet(a, b, msg) {
+ function serialArray( a ) {
+ var r = [];
+
+ if ( a && a.length )
+ for ( var i = 0; i < a.length; i++ ) {
+ var str = a[i].nodeName;
+ if ( str ) {
+ str = str.toLowerCase();
+ if ( a[i].id )
+ str += "#" + a[i].id;
+ } else
+ str = a[i];
+ r.push( str );
+ }
+
+ return "[ " + r.join(", ") + " ]";
+ }
+ var ret = true;
+ if ( a && b && a.length != undefined && a.length == b.length ) {
+ for ( var i = 0; i < a.length; i++ )
+ if ( a[i] != b[i] )
+ ret = false;
+ } else
+ ret = false;
+ QUnit.ok( ret, !ret ? (msg + " expected: " + serialArray(b) + " result: " + serialArray(a)) : msg );
+}
+
+/**
+ * Asserts that two objects are equivalent
+ */
+function isObj(a, b, msg) {
+ var ret = true;
+
+ if ( a && b ) {
+ for ( var i in a )
+ if ( a[i] != b[i] )
+ ret = false;
+
+ for ( i in b )
+ if ( a[i] != b[i] )
+ ret = false;
+ } else
+ ret = false;
+
+ QUnit.ok( ret, msg );
+}
+
+/**
+ * Returns an array of elements with the given IDs, eg.
+ * @example q("main", "foo", "bar")
+ * @result [<div id="main">, <span id="foo">, <input id="bar">]
+ */
+function q() {
+ var r = [];
+ for ( var i = 0; i < arguments.length; i++ )
+ r.push( document.getElementById( arguments[i] ) );
+ return r;
+}
+
+/**
+ * Asserts that a select matches the given IDs
+ * @example t("Check for something", "//[a]", ["foo", "baar"]);
+ * @result returns true if "//[a]" return two elements with the IDs 'foo' and 'baar'
+ */
+function t(a,b,c) {
+ var f = $(b);
+ var s = "";
+ for ( var i = 0; i < f.length; i++ )
+ s += (s && ",") + '"' + f[i].id + '"';
+ isSet(f, q.apply(q,c), a + " (" + b + ")");
+}
+
+/**
+ * Add random number to url to stop IE from caching
+ *
+ * @example url("data/test.html")
+ * @result "data/test.html?10538358428943"
+ *
+ * @example url("data/test.php?foo=bar")
+ * @result "data/test.php?foo=bar&10538358345554"
+ */
+function url(value) {
+ return value + (/\?/.test(value) ? "&" : "?") + new Date().getTime() + "" + parseInt(Math.random()*100000);
+}
+
+/**
+ * Checks that the first two arguments are equal, with an optional message.
+ * Prints out both actual and expected values.
+ *
+ * Prefered to ok( actual == expected, message )
+ *
+ * @example equals( $.format("Received {0} bytes.", 2), "Received 2 bytes." );
+ *
+ * @param Object actual
+ * @param Object expected
+ * @param String message (optional)
+ */
+function equals(actual, expected, message) {
+ push(expected == actual, actual, expected, message);
+}
+
+function push(result, actual, expected, message) {
+ message = message || (result ? "okay" : "failed");
+ QUnit.ok( result, result ? message + ": " + expected : message + ", expected: " + jsDump.parse(expected) + " result: " + jsDump.parse(actual) );
+}
+
+/**
+ * Trigger an event on an element.
+ *
+ * @example triggerEvent( document.body, "click" );
+ *
+ * @param DOMElement elem
+ * @param String type
+ */
+function triggerEvent( elem, type, event ) {
+ if ( $.browser.mozilla || $.browser.opera || $.browser.safari ) {
+ event = document.createEvent("MouseEvents");
+ event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ elem.dispatchEvent( event );
+ } else if ( $.browser.msie ) {
+ elem.fireEvent("on"+type);
+ }
+}
+
+})(jQuery);
+
+/**
+ * jsDump
+ * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
+ * Date: 5/15/2008
+ * @projectDescription Advanced and extensible data dumping for Javascript.
+ * @version 1.0.0
+ * @author Ariel Flesler
+ * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ */
+(function(){
+ function quote( str ){
+ return '"' + str.toString().replace(/"/g, '\\"') + '"';
+ };
+ function literal( o ){
+ return o + '';
+ };
+ function join( pre, arr, post ){
+ var s = jsDump.separator(),
+ base = jsDump.indent();
+ inner = jsDump.indent(1);
+ if( arr.join )
+ arr = arr.join( ',' + s + inner );
+ if( !arr )
+ return pre + post;
+ return [ pre, inner + arr, base + post ].join(s);
+ };
+ function array( arr ){
+ var i = arr.length, ret = Array(i);
+ this.up();
+ while( i-- )
+ ret[i] = this.parse( arr[i] );
+ this.down();
+ return join( '[', ret, ']' );
+ };
+
+ var reName = /^function (\w+)/;
+
+ var jsDump = window.jsDump = {
+ parse:function( obj, type ){//type is used mostly internally, you can fix a (custom)type in advance
+ var parser = this.parsers[ type || this.typeOf(obj) ];
+ type = typeof parser;
+
+ return type == 'function' ? parser.call( this, obj ) :
+ type == 'string' ? parser :
+ this.parsers.error;
+ },
+ typeOf:function( obj ){
+ var type = typeof obj,
+ f = 'function';//we'll use it 3 times, save it
+ return type != 'object' && type != f ? type :
+ !obj ? 'null' :
+ obj.exec ? 'regexp' :// some browsers (FF) consider regexps functions
+ obj.getHours ? 'date' :
+ obj.scrollBy ? 'window' :
+ obj.nodeName == '#document' ? 'document' :
+ obj.nodeName ? 'node' :
+ obj.item ? 'nodelist' : // Safari reports nodelists as functions
+ obj.callee ? 'arguments' :
+ obj.call || obj.constructor != Array && //an array would also fall on this hack
+ (obj+'').indexOf(f) != -1 ? f : //IE reports functions like alert, as objects
+ 'length' in obj ? 'array' :
+ type;
+ },
+ separator:function(){
+ return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? '&nbsp;' : ' ';
+ },
+ indent:function( extra ){// extra can be a number, shortcut for increasing-calling-decreasing
+ if( !this.multiline )
+ return '';
+ var chr = this.indentChar;
+ if( this.HTML )
+ chr = chr.replace(/\t/g,' ').replace(/ /g,'&nbsp;');
+ return Array( this._depth_ + (extra||0) ).join(chr);
+ },
+ up:function( a ){
+ this._depth_ += a || 1;
+ },
+ down:function( a ){
+ this._depth_ -= a || 1;
+ },
+ setParser:function( name, parser ){
+ this.parsers[name] = parser;
+ },
+ // The next 3 are exposed so you can use them
+ quote:quote,
+ literal:literal,
+ join:join,
+ //
+ _depth_: 1,
+ // This is the list of parsers, to modify them, use jsDump.setParser
+ parsers:{
+ window: '[Window]',
+ document: '[Document]',
+ error:'[ERROR]', //when no parser is found, shouldn't happen
+ unknown: '[Unknown]',
+ 'null':'null',
+ undefined:'undefined',
+ 'function':function( fn ){
+ var ret = 'function',
+ name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
+ if( name )
+ ret += ' ' + name;
+ ret += '(';
+
+ ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
+ return join( ret, this.parse(fn,'functionCode'), '}' );
+ },
+ array: array,
+ nodelist: array,
+ arguments: array,
+ object:function( map ){
+ var ret = [ ];
+ this.up();
+ for( var key in map )
+ ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
+ this.down();
+ return join( '{', ret, '}' );
+ },
+ node:function( node ){
+ var open = this.HTML ? '&lt;' : '<',
+ close = this.HTML ? '&gt;' : '>';
+
+ var tag = node.nodeName.toLowerCase(),
+ ret = open + tag;
+
+ for( var a in this.DOMAttrs ){
+ var val = node[this.DOMAttrs[a]];
+ if( val )
+ ret += ' ' + a + '=' + this.parse( val, 'attribute' );
+ }
+ return ret + close + open + '/' + tag + close;
+ },
+ functionArgs:function( fn ){//function calls it internally, it's the arguments part of the function
+ var l = fn.length;
+ if( !l ) return '';
+
+ var args = Array(l);
+ while( l-- )
+ args[l] = String.fromCharCode(97+l);//97 is 'a'
+ return ' ' + args.join(', ') + ' ';
+ },
+ key:quote, //object calls it internally, the key part of an item in a map
+ functionCode:'[code]', //function calls it internally, it's the content of the function
+ attribute:quote, //node calls it internally, it's an html attribute value
+ string:quote,
+ date:quote,
+ regexp:literal, //regex
+ number:literal,
+ 'boolean':literal
+ },
+ DOMAttrs:{//attributes to dump from nodes, name=>realName
+ id:'id',
+ name:'name',
+ 'class':'className'
+ },
+ HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
+ indentChar:' ',//indentation unit
+ multiline:true //if true, items in a collection, are separated by a \n, else just a space.
+ };
+
+})();
diff --git a/tests/javascript/assets/testsuite.css b/tests/javascript/assets/testsuite.css
new file mode 100644
index 0000000000..dbfc43aee4
--- /dev/null
+++ b/tests/javascript/assets/testsuite.css
@@ -0,0 +1,120 @@
+body, div, h1 { font-family: 'trebuchet ms', verdana, arial; margin: 0; padding: 0 }
+body {font-size: 10pt; }
+h1 { padding: 15px; font-size: large; background-color: #06b; color: white; }
+h1 a { color: white; }
+h2 { padding: 10px; background-color: #eee; color: black; margin: 0; font-size: small; font-weight: normal }
+
+.pass { color: green; }
+.fail { color: red; }
+p.result { margin-left: 1em; }
+
+#banner { height: 2em; border-bottom: 1px solid white; }
+h2.pass { background-color: green; }
+h2.fail { background-color: red; }
+
+div.testrunner-toolbar { background: #eee; border-top: 1px solid black; padding: 10px; }
+
+ol#tests > li > strong { cursor:pointer; }
+
+div#fx-tests h4 {
+ background: red;
+}
+
+div#fx-tests h4.pass {
+ background: green;
+}
+
+div#fx-tests div.box {
+ background: red url(data/cow.jpg) no-repeat;
+ overflow: hidden;
+ border: 2px solid #000;
+}
+
+div#fx-tests div.overflow {
+ overflow: visible;
+}
+
+div.inline {
+ display: inline;
+}
+
+div.autoheight {
+ height: auto;
+}
+
+div.autowidth {
+ width: auto;
+}
+
+div.autoopacity {
+ opacity: auto;
+}
+
+div.largewidth {
+ width: 100px;
+}
+
+div.largeheight {
+ height: 100px;
+}
+
+div.largeopacity {
+ filter: progid:DXImageTransform.Microsoft.Alpha(opacity=100);
+}
+
+div.medwidth {
+ width: 50px;
+}
+
+div.medheight {
+ height: 50px;
+}
+
+div.medopacity {
+ opacity: 0.5;
+ filter: progid:DXImageTransform.Microsoft.Alpha(opacity=50);
+}
+
+div.nowidth {
+ width: 0px;
+}
+
+div.noheight {
+ height: 0px;
+}
+
+div.noopacity {
+ opacity: 0;
+ filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0);
+}
+
+div.hidden {
+ display: none;
+}
+
+div#fx-tests div.widewidth {
+ background-repeat: repeat-x;
+}
+
+div#fx-tests div.wideheight {
+ background-repeat: repeat-y;
+}
+
+div#fx-tests div.widewidth.wideheight {
+ background-repeat: repeat;
+}
+
+div#fx-tests div.noback {
+ background-image: none;
+}
+
+div.chain, div.chain div { width: 100px; height: 20px; position: relative; float: left; }
+div.chain div { position: absolute; top: 0px; left: 0px; }
+
+div.chain.test { background: red; }
+div.chain.test div { background: green; }
+
+div.chain.out { background: green; }
+div.chain.out div { background: red; display: none; }
+
+div#show-tests * { display: none; }
diff --git a/tests/javascript/index.php b/tests/javascript/index.php
new file mode 100644
index 0000000000..1d60fcd4bf
--- /dev/null
+++ b/tests/javascript/index.php
@@ -0,0 +1,295 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>piwik.js: Piwik Unit Tests</title>
+ <script src="../../js/piwik.js" type="text/javascript"></script>
+ <script src="piwiktest.js" type="text/javascript"></script>
+ <script src="../../libs/jquery/jquery.js" type="text/javascript"></script>
+ <link rel="stylesheet" href="assets/testsuite.css" type="text/css" media="screen" />
+</head>
+<body>
+<?php
+$sqlite = false;
+if (file_exists("enable_sqlite")) {
+ if (!extension_loaded('sqlite')) {
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ dl('php_sqlite.dll');
+ } else {
+ dl('sqlite.so');
+ }
+ }
+ if (extension_loaded('sqlite')) {
+ $sqlite = true;
+ }
+}
+?>
+
+
+ <h1>piwik.js: Piwik Unit Tests</h1>
+ <h2 id="banner"></h2>
+ <h2 id="userAgent"></h2>
+
+ <div class="hidden">
+ <div id="div1"></div>
+ <iframe name="iframe2"></iframe>
+ <iframe name="iframe3"></iframe>
+ <iframe name="iframe4"></iframe>
+ <iframe name="iframe5"></iframe>
+ <iframe name="iframe6"></iframe>
+ <iframe name="iframe7"></iframe>
+ <ul>
+ <li><a id="click1" href="javascript:document.getElementById('div1').innerHTML='&lt;iframe src=&quot;http://example.com&quot;&gt;&lt;/iframe&gt;';void(0)" class="clicktest">ignore: implicit (JavaScript href)</a></li>
+ <li><a id="click2" href="http://example.org" target="iframe2" class="piwik_ignore clicktest">ignore: explicit</a></li>
+ <li><a id="click3" href="example.php" target="iframe3" class="clicktest">ignore: implicit (localhost)</a></li>
+ <li><a id="click4" href="http://example.net" target="iframe4" class="clicktest">outlink: implicit (outbound URL)</a></li>
+ <li><a id="click5" href="example.html" target="iframe5" class="piwik_link clicktest">outlink: explicit (localhost)</a></li>
+ <li><a id="click6" href="example.pdf" target="iframe6" class="clicktest">download: implicit (file extension)</a></li>
+ <li><a id="click7" href="example.word" target="iframe7" class="piwik_download clicktest">download: explicit</a></li>
+ </ul>
+ </div>
+
+ <ol id="tests"></ol>
+
+ <div id="main"></div>
+
+ <script src="assets/testrunner.js" type="text/javascript"></script>
+
+ <script>
+function getToken() {
+ return "<?php $token = md5(uniqid(mt_rand(), true)); echo $token; ?>";
+}
+
+$(document).ready(function () {
+
+ test("Basic requirements", function() {
+ expect(5);
+
+ equals( typeof encodeURIComponent, 'function', 'encodeURIComponent' );
+ ok( RegExp, "RegExp" );
+ ok( Piwik, "Piwik" );
+ ok( piwik_log, "piwik_log" );
+ equals( typeof piwik_track, 'undefined', "piwk_track" );
+ });
+
+ module("piwik test");
+ test("Test API - addPlugin(), getTracker(), getHook(), and hook", function() {
+ expect(6);
+
+ ok( Piwik.addPlugin, "Piwik.addPlugin" );
+
+ var tracker = Piwik.getTracker();
+
+ equals( typeof tracker, 'object', "Piwik.getTracker()" );
+ equals( typeof tracker.getHook, 'function', "test Tracker getHook" );
+ equals( typeof tracker.hook, 'object', "test Tracker hook" );
+ equals( typeof tracker.getHook('test'), 'object', "test Tracker getHook('test')" );
+ equals( typeof tracker.hook.test, 'object', "test Tracker hook.test" );
+ });
+
+ module("piwik");
+ test("Tracker escape and unescape wrappers", function() {
+ expect(4);
+
+ var tracker = Piwik.getTracker();
+
+ equals( typeof tracker.hook.test._escape, 'function', 'escapeWrapper' );
+ equals( typeof tracker.hook.test._unescape, 'function', 'unescapeWrapper' );
+
+ equals( tracker.hook.test._escape("&=?;/#"), '%26%3D%3F%3B%2F%23', 'escapeWrapper()' );
+ equals( tracker.hook.test._unescape("%26%3D%3F%3B%2F%23"), '&=?;/#', 'unescapeWrapper()' );
+ });
+
+ test("Tracker setDomains() and isSiteHostName()", function() {
+ expect(9);
+
+ var tracker = Piwik.getTracker();
+
+ equals( typeof tracker.hook.test._isSiteHostName, 'function', "isSiteHostName" );
+
+ // test wildcards
+ tracker.setDomains( ['*.example.com'] );
+ // skip test if testing on localhost
+ ok( window.location.hostname != 'localhost' ? !tracker.hook.test._isSiteHostName('localhost') : true, '!isSiteHostName("localhost")' );
+
+ ok( !tracker.hook.test._isSiteHostName('google.com'), '!isSiteHostName("google.com")' );
+ ok( tracker.hook.test._isSiteHostName('example.com'), 'isSiteHostName("example.com")' );
+ ok( tracker.hook.test._isSiteHostName('www.example.com'), 'isSiteHostName("www.example.com")' );
+ ok( tracker.hook.test._isSiteHostName('www.sub.example.com'), 'isSiteHostName("www.sub.example.com")' );
+
+ tracker.setDomains( 'dev.piwik.org' );
+ ok( !tracker.hook.test._isSiteHostName('piwik.org'), '!isSiteHostName("dev.piwik.org")' );
+ ok( tracker.hook.test._isSiteHostName('dev.piwik.org'), 'isSiteHostName("dev.piwik.org")' );
+ ok( !tracker.hook.test._isSiteHostName('piwik.example.org'), '!isSiteHostName("piwik.example.org")');
+ });
+
+ test("Tracker setIgnoreClasses() and getIgnoreRegExp", function() {
+ expect(15);
+
+ var tracker = Piwik.getTracker();
+
+ equals( typeof tracker.hook.test._getIgnoreRegExp, 'function', "getIgnoreRegExp" );
+
+ var ignore = tracker.hook.test._getIgnoreRegExp();
+ ok( ignore.test('piwik_ignore'), '[1] piwik_ignore' );
+ ok( !ignore.test('pk_ignore'), '[1] !pk_ignore' );
+ ok( !ignore.test('apiwik_ignore'), '!apiwik_ignore' );
+ ok( !ignore.test('piwik_ignorez'), '!piwik_ignorez' );
+ ok( ignore.test('abc piwik_ignore xyz'), 'abc piwik_ignore xyz' );
+
+ tracker.setIgnoreClasses( 'my_download' );
+ ignore = tracker.hook.test._getIgnoreRegExp();
+ ok( ignore.test('piwik_ignore'), '[2] piwik_ignore' );
+ ok( !ignore.test('pk_ignore'), '[2] !pk_ignore' );
+ ok( ignore.test('my_download'), 'my_download' );
+ ok( ignore.test('abc piwik_ignore xyz'), 'abc piwik_ignore xyz' );
+ ok( ignore.test('abc my_download xyz'), 'abc my_download xyz' );
+
+ tracker.setIgnoreClasses( ['my_download', 'my_outlink'] );
+ ignore = tracker.hook.test._getIgnoreRegExp();
+ ok( ignore.test('piwik_ignore'), '[3] piwik_ignore' );
+ ok( !ignore.test('pk_ignore'), '[3] !pk_ignore' );
+ ok( ignore.test('my_download'), 'my_download' );
+ ok( ignore.test('my_outlink'), 'my_outlink' );
+ });
+
+ test("Tracker hasCookies(), getCookie(), setCookie()", function() {
+ expect(2);
+
+ var tracker = Piwik.getTracker();
+
+ ok( tracker.hook.test._hasCookies() == '1', 'hasCookies()' );
+
+ var cookieName = '_pk_test_harness' + Math.random(),
+ expectedValue = Math.random();
+ tracker.hook.test._setCookie( cookieName, expectedValue );
+ equals( tracker.hook.test._getCookie( cookieName ), expectedValue, 'getCookie(), setCookie()' );
+ });
+
+ test("Tracker setDownloadExtensions(), addDownloadExtensions(), setDownloadClass(), setLinkClass(), and getLinkType()", function() {
+ expect(19);
+
+ var tracker = Piwik.getTracker();
+
+ equals( typeof tracker.hook.test._getLinkType, 'function', 'getLinkType' );
+
+ equals( tracker.hook.test._getLinkType('something', 'goofy.html', false), 'link', 'implicit link' );
+ equals( tracker.hook.test._getLinkType('something', 'goofy.pdf', false), 'link', 'implicit link' );
+
+ equals( tracker.hook.test._getLinkType('piwik_download', 'piwiktest.ext', true), 'download', 'piwik_download' );
+ equals( tracker.hook.test._getLinkType('abc piwik_download xyz', 'piwiktest.ext', true), 'download', 'abc piwik_download xyz' );
+ equals( tracker.hook.test._getLinkType('piwik_link', 'piwiktest.asp', true), 'link', 'piwik_link' );
+ equals( tracker.hook.test._getLinkType('abc piwik_link xyz', 'piwiktest.asp', true), 'link', 'abc piwik_link xyz' );
+ equals( tracker.hook.test._getLinkType('something', 'piwiktest.txt', true), 'download', 'download extension' );
+ equals( tracker.hook.test._getLinkType('something', 'piwiktest.ext', true), 0, '[1] link (default)' );
+
+ tracker.setDownloadExtensions('pk');
+ equals( tracker.hook.test._getLinkType('something', 'piwiktest.pk', true), 'download', '[1] .pk == download extension' );
+ equals( tracker.hook.test._getLinkType('something', 'piwiktest.txt', true), 0, '.txt =! download extension' );
+
+ tracker.addDownloadExtensions('xyz');
+ equals( tracker.hook.test._getLinkType('something', 'piwiktest.pk', true), 'download', '[2] .pk == download extension' );
+ equals( tracker.hook.test._getLinkType('something', 'piwiktest.xyz', true), 'download', '.xyz == download extension' );
+
+ tracker.setDownloadClass('my_download');
+ equals( tracker.hook.test._getLinkType('my_download', 'piwiktest.ext', true), 'download', 'my_download' );
+ equals( tracker.hook.test._getLinkType('abc my_download xyz', 'piwiktest.ext', true), 'download', 'abc my_download xyz' );
+ equals( tracker.hook.test._getLinkType('piwik_download', 'piwiktest.ext', true), 0, 'piwik_download != my_download' );
+
+ tracker.setLinkClass('my_link');
+ equals( tracker.hook.test._getLinkType('my_link', 'piwiktest.ext', true), 'link', 'my_link' );
+ equals( tracker.hook.test._getLinkType('abc my_link xyz', 'piwiktest.ext', true), 'link', 'abc my_link xyz' );
+ equals( tracker.hook.test._getLinkType('piwik_link', 'piwiktest.ext', true), 0, '[2] link default' );
+ });
+
+ test("JSON", function() {
+ expect(10);
+
+ var tracker = Piwik.getTracker(), dummy;
+
+ equals( tracker.hook.test._stringify(true), 'true', 'Boolean (true)' );
+ equals( tracker.hook.test._stringify(false), 'false', 'Boolean (false)' );
+ equals( tracker.hook.test._stringify(42), '42', 'Number' );
+ equals( tracker.hook.test._stringify("ABC"), '"ABC"', 'String' );
+
+ var d = new Date();
+ d.setTime(1240013340000);
+ equals( tracker.hook.test._stringify(d), '"2009-04-18T00:09:00Z"', 'Date');
+
+ equals( tracker.hook.test._stringify(null), 'null', 'null' );
+ equals( typeof tracker.hook.test._stringify(dummy), 'undefined', 'undefined' );
+ equals( tracker.hook.test._stringify([1, 2, 3]), '[1,2,3]', 'Array of numbers' );
+ equals( tracker.hook.test._stringify({'key' : 'value'}), '{"key":"value"}', 'Object (members)' );
+ equals( tracker.hook.test._stringify(
+ [ {'domains' : ['example.com', 'example.ca']},
+ {'names' : ['Sean', 'Cathy'] } ]
+ ), '[{"domains":["example.com","example.ca"]},{"names":["Sean","Cathy"]}]', 'Nested members' );
+ });
+
+ test("Tracking", function() {
+ expect(<?php echo $sqlite ? 10 : 3; ?>);
+
+ var tracker = Piwik.getTracker();
+
+ var startTime, stopTime;
+
+ equals( typeof tracker.hook.test._beforeUnloadHandler, 'function', 'beforeUnloadHandler' );
+
+ startTime = new Date();
+ tracker.hook.test._beforeUnloadHandler();
+ stopTime = new Date();
+ ok( (stopTime.getTime() - startTime.getTime()) < 500, 'beforeUnloadHandler()' );
+
+ tracker.setLinkTrackingTimer(2000);
+ startTime = new Date();
+ tracker.trackPageView();
+ tracker.hook.test._beforeUnloadHandler();
+ stopTime = new Date();
+ ok( (stopTime.getTime() - startTime.getTime()) >= 2000, 'setLinkTrackingTimer()' );
+<?php
+if ($sqlite) {
+ echo '
+ tracker.setTrackerUrl("piwik.php");
+ tracker.setSiteId(1);
+ tracker.setCustomData({ "token" : "'. $token .'" });
+ tracker.setDocumentTitle("PiwikTest");
+
+ tracker.enableLinkTracking();
+
+ tracker.trackPageView();
+
+ tracker.trackLink("http://example.ca", "link", { "token" : "'. $token .'" });
+
+ var buttons = q("click1", "click2", "click3", "click4", "click5", "click6", "click7");
+ for (var i=0; i < buttons.length; i++) {
+ triggerEvent( buttons[i], "click" );
+ }
+
+ stop();
+ setTimeout(function() {
+ jQuery.ajax({
+ url: url("piwik.php?results='. $token .'"),
+ success: function(results) {
+//alert(results);
+ ok( /\<span\>6\<\/span\>/.test( results ), "count tracking events" );
+ ok( /PiwikTest/.test( results ), "trackPageView()" );
+ ok( /example.ca/.test( results ), "trackLink()" );
+ ok( /example.net/.test( results ), "click: implicit outlink (by outbound URL)" );
+ ok( /example.html/.test( results ), "click: explicit outlink" );
+ ok( /example.pdf/.test( results ), "click: implicit download (by file extension)" );
+ ok( /example.word/.test( results ), "click: explicit download" );
+
+ start();
+ }
+ });
+ }, 2000);
+ ';
+}
+?>
+ });
+});
+ </script>
+
+</body>
+</html>
+
diff --git a/tests/javascript/piwik.php b/tests/javascript/piwik.php
new file mode 100644
index 0000000000..2ad3d092b5
--- /dev/null
+++ b/tests/javascript/piwik.php
@@ -0,0 +1,81 @@
+<?php
+// piwik.php test harness
+
+function sendWebBug() {
+ $trans_gif_64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
+ header("Content-type: image/gif");
+ print(base64_decode($trans_gif_64));
+}
+
+if (!file_exists("enable_sqlite")) {
+ sendWebBug();
+ exit;
+}
+
+if (!extension_loaded('sqlite')) {
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ dl('php_sqlite.dll');
+ } else {
+ dl('sqlite.so');
+ }
+}
+
+if (!extension_loaded('sqlite')) {
+ sendWebBug();
+ exit;
+}
+
+$dbhandle = sqlite_open( 'unittest.dbf' );
+if ($dbhandle) {
+ // SQLite 3.3 supports CREATE TABLE IF NOT EXISTS
+
+ $result = sqlite_array_query($dbhandle, "SELECT COUNT(*) FROM requests");
+ if ($result === false) {
+ try {
+ $query = sqlite_exec( $dbhandle, 'CREATE TABLE requests (token TEXT, ip TEXT, ts TEXT, uri TEXT, referer TEXT, ua TEXT);' );
+ } catch (Exception $e) { }
+ }
+}
+
+if (isset($_GET['results'])) {
+ $token = $_GET['results'];
+ $ua = $_SERVER['HTTP_USER_AGENT'];
+
+ echo "<html><head><title>$token</title></head><body>\n";
+
+// $result = sqlite_array_query($dbhandle, "SELECT uri FROM requests");
+ $result = sqlite_array_query($dbhandle, "SELECT uri FROM requests WHERE token = \"$token\" AND ua = \"$ua\"");
+ if ($result !== false) {
+ $nofRows = count($result);
+ echo "<span>$nofRows</span>\n";
+
+ foreach ($result as $entry) {
+ echo "<span>". $entry['uri'] ."</span>\n";
+ }
+ }
+
+ echo "</body></html>\n";
+} else {
+ if (!isset($_GET['data'])) {
+ header("HTTP/1.0 400 Bad Request");
+ } else {
+ $data = json_decode($_GET['data']);
+ $token = isset($data->token) ? $data->token : '';
+
+ $ip = $_SERVER['REMOTE_ADDR'];
+ $ts = $_SERVER['REQUEST_TIME'];
+ $uri = $_SERVER['REQUEST_URI'];
+ $referer = $_SERVER['HTTP_REFERER'];
+ $ua = $_SERVER['HTTP_USER_AGENT'];
+
+ $query = sqlite_exec($dbhandle, "INSERT INTO requests (token, ip, ts, uri, referer, ua) VALUES (\"$token\", \"$ip\", \"$ts\", \"$uri\", \"$referer\", \"$ua\")", $error);
+ if (!$query) {
+ header("HTTP/1.0 500 Internal Server Error");
+ } else {
+// echo 'Number of rows modified: ', sqlite_changes($dbhandle);
+ sendWebBug();
+ }
+ }
+}
+
+sqlite_close($dbhandle);
diff --git a/tests/javascript/piwiktest.js b/tests/javascript/piwiktest.js
new file mode 100644
index 0000000000..58f2a8276c
--- /dev/null
+++ b/tests/javascript/piwiktest.js
@@ -0,0 +1,45 @@
+/*!
+ * Piwik - Web Analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
+ * @version $Id$
+ */
+
+/*global Piwik getToken */
+
+Piwik.addPlugin('testPlugin', {
+ /*
+ * called when tracker instantiated
+ * - function or string to be eval()'d
+ */
+ run: function (registerHookCallback) {
+ registerHookCallback('test', '{ _isSiteHostName : isSiteHostName, _getIgnoreRegExp : getIgnoreRegExp, _hasCookies : hasCookies, _getCookie : getCookie, _setCookie : setCookie, _escape : escapeWrapper, _unescape : unescapeWrapper, _getLinkType : getLinkType, _beforeUnloadHandler : beforeUnloadHandler, _stringify : stringify }');
+ },
+
+ /*
+ * called when DOM ready
+ */
+ load: function () { },
+
+ /*
+ * function called on trackPageView
+ * - returns URL components to be appended to tracker URL
+ */
+ log: function () {
+ return '';
+ },
+
+ /*
+ * function called on trackLin() or click event
+ * - returns URL components to be appended to tracker URL
+ */
+ click: function () {
+ return '&data=' + encodeURIComponent('{"token":"' + getToken() + '"}');
+ },
+
+ /*
+ * called before page is unloaded
+ */
+ unload: function () { }
+});