diff options
Diffstat (limited to 'js/piwik.js')
-rw-r--r-- | js/piwik.js | 200 |
1 files changed, 182 insertions, 18 deletions
diff --git a/js/piwik.js b/js/piwik.js index d15eac4b1b..e6db6e4848 100644 --- a/js/piwik.js +++ b/js/piwik.js @@ -36,7 +36,8 @@ /*members Piwik, Matomo, encodeURIComponent, decodeURIComponent, getElementsByTagName, shift, unshift, piwikAsyncInit, matomoAsyncInit, matomoPluginAsyncInit , frameElement, self, hasFocus, createElement, appendChild, characterSet, charset, all, - addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies, + addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies, setCookieConsentGiven, + areCookiesEnabled, getRememberedCookieConsent, rememberCookieConsentGiven, forgetCookieConsentGiven, requireCookieConsent, cookie, domain, readyState, documentElement, doScroll, title, text, contentWindow, postMessage, location, top, onerror, document, referrer, parent, links, href, protocol, name, performance, mozPerformance, msPerformance, webkitPerformance, timing, connectEnd, requestStart, responseStart, @@ -2136,6 +2137,7 @@ if (typeof window.Matomo !== 'object') { // constants CONSENT_COOKIE_NAME = 'mtm_consent', + COOKIE_CONSENT_COOKIE_NAME = 'mtm_cookie_consent', CONSENT_REMOVED_COOKIE_NAME = 'mtm_consent_removed', // Current URL and Referrer URL @@ -2370,7 +2372,10 @@ if (typeof window.Matomo !== 'object') { consentRequestsQueue = [], // a unique ID for this tracker during this request - uniqueTrackerId = trackerIdCounter++; + uniqueTrackerId = trackerIdCounter++, + + // whether a tracking request has been sent yet during this page view + hasSentTrackingRequestYet = false; // Document title try { @@ -2952,6 +2957,9 @@ if (typeof window.Matomo !== 'object') { consentRequestsQueue.push(request); return; } + + hasSentTrackingRequestYet = true; + if (!configDoNotTrack && request) { if (configConsentRequired && configHasConsent) { // send a consent=1 when explicit consent is given for the apache logs request += '&consent=1'; @@ -3018,6 +3026,8 @@ if (typeof window.Matomo !== 'object') { return; } + hasSentTrackingRequestYet = true; + makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () { var chunks = arrayChunk(requests, 50); @@ -3126,8 +3136,12 @@ if (typeof window.Matomo !== 'object') { browserFeatures.java = '1'; } - // other browser features - browserFeatures.cookie = hasCookies(); + if (!isDefined(windowAlias.showModalDialog) && isDefined(navigatorAlias.cookieEnabled)) { + browserFeatures.cookie = navigatorAlias.cookieEnabled ? '1' : '0'; + } else { + // Eg IE11 ... prevent error when cookieEnabled is requested within modal dialog. see #11507 + browserFeatures.cookie = hasCookies(); + } } var width = parseInt(screenAlias.width, 10); @@ -3432,7 +3446,6 @@ if (typeof window.Matomo !== 'object') { function setSiteId(siteId) { configTrackerSiteId = siteId; - setVisitorIdCookie(); } function sortObjectByKeys(value) { @@ -4818,9 +4831,6 @@ if (typeof window.Matomo !== 'object') { this.getContent = function () { return content; }; - this.setVisitorId = function (visitorId) { - visitorUUID = visitorId; - }; this.buildContentImpressionRequest = buildContentImpressionRequest; this.buildContentInteractionRequest = buildContentInteractionRequest; @@ -5048,6 +5058,22 @@ if (typeof window.Matomo !== 'object') { }; /** + * Sets a Visitor ID to this visitor. Should be a 16 digit hex string. + * The visitorId won't be persisted in a cookie or something similar and needs to be set every time. + * + * @param string User ID + */ + this.setVisitorId = function (visitorId) { + var validation = /[0-9A-Fa-f]{16}/g; + + if (isString(visitorId) && validation.test(visitorId)) { + visitorUUID = visitorId; + } else { + logConsoleError('Invalid visitorId set' + visitorId); + } + }; + + /** * Gets the User ID if set. * * @returns string User ID @@ -5755,7 +5781,6 @@ if (typeof window.Matomo !== 'object') { */ this.disableCookies = function () { configCookiesDisabled = true; - browserFeatures.cookie = '0'; if (configTrackerSiteId) { deleteCookies(); @@ -5763,6 +5788,115 @@ if (typeof window.Matomo !== 'object') { }; /** + * Detects if cookies are enabled or not + * @returns {boolean} + */ + this.areCookiesEnabled = function () { + return !configCookiesDisabled; + }; + + /** + * Enables cookies if they were disabled previously. + */ + this.setCookieConsentGiven = function () { + if (configCookiesDisabled && !configDoNotTrack) { + configCookiesDisabled = false; + if (configTrackerSiteId && hasSentTrackingRequestYet) { + setVisitorIdCookie(); + + // sets attribution cookie, and updates visitorId in the backend + // because hasSentTrackingRequestYet=true we assume there might not be another tracking + // request within this page view so we trigger one ourselves. + // if no tracking request has been sent yet, we don't set the attribution cookie cause Matomo + // sets the cookie only when there is a tracking request. It'll be set if the user sends + // a tracking request afterwards + var request = getRequest('ping=1', null, 'ping'); + sendRequest(request, configTrackerPause); + } + } + }; + + /** + * When called, no cookies will be set until you have called `setCookieConsentGiven()` + * unless consent was given previously AND you called {@link rememberCookieConsentGiven()} when the user + * gave consent. + * + * This may be useful when you want to implement for example a popup to ask for cookie consent. + * Once the user has given consent, you should call {@link setCookieConsentGiven()} + * or {@link rememberCookieConsentGiven()}. + * + * If you require tracking consent for example because you are tracking personal data and GDPR applies to you, + * then have a look at `_paq.push(['requireConsent'])` instead. + * + * If the user has already given consent in the past, you can either decide to not call `requireCookieConsent` at all + * or call `_paq.push(['setCookieConsentGiven'])` on each page view at any time after calling `requireCookieConsent`. + * + * When the user gives you the consent to set cookies, you can also call `_paq.push(['rememberCookieConsentGiven', optionalTimeoutInHours])` + * and for the duration while the cookie consent is remembered, any call to `requireCoookieConsent` will be automatically ignored + * until you call `forgetCookieConsentGiven`. + * `forgetCookieConsentGiven` needs to be called when the user removes consent for using cookies. This means if you call `rememberCookieConsentGiven` at the + * time the user gives you consent, you do not need to ever call `_paq.push(['setCookieConsentGiven'])` as the consent + * will be detected automatically through cookies. + */ + this.requireCookieConsent = function() { + if (this.getRememberedCookieConsent()) { + return false; + } + this.disableCookies(); + return true; + }; + + /** + * If the user has given cookie consent previously and this consent was remembered, it will return the number + * in milliseconds since 1970/01/01 which is the date when the user has given cookie consent. Please note that + * the returned time depends on the users local time which may not always be correct. + * + * @returns number|string + */ + this.getRememberedCookieConsent = function () { + return getCookie(COOKIE_CONSENT_COOKIE_NAME); + }; + + /** + * Calling this method will remove any previously given cookie consent and it disables cookies for subsequent + * page views. You may call this method if the user removes cookie consent manually, or if you + * want to re-ask for cookie consent after a specific time period. + */ + this.forgetCookieConsentGiven = function () { + deleteCookie(COOKIE_CONSENT_COOKIE_NAME, configCookiePath, configCookieDomain); + this.disableCookies(); + }; + + /** + * Calling this method will remember that the user has given cookie consent across multiple requests by setting + * a cookie named "mtm_cookie_consent". You can optionally define the lifetime of that cookie in hours + * using a parameter. + * + * When you call this method, we imply that the user has given cookie consent for this page view, and will also + * imply consent for all future page views unless the cookie expires or the user + * deletes all her or his cookies. Remembering cookie consent means even if you call {@link disableCookies()}, + * then cookies will still be enabled and it won't disable cookies since the user has given consent for cookies. + * + * Please note that this feature requires you to set the `cookieDomain` and `cookiePath` correctly. Please + * also note that when you call this method, consent will be implied for all sites that match the configured + * cookieDomain and cookiePath. Depending on your website structure, you may need to restrict or widen the + * scope of the cookie domain/path to ensure the consent is applied to the sites you want. + * + * @param int hoursToExpire After how many hours the cookie consent should expire. By default the consent is valid + * for 30 years unless cookies are deleted by the user or the browser prior to this + */ + this.rememberCookieConsentGiven = function (hoursToExpire) { + if (hoursToExpire) { + hoursToExpire = hoursToExpire * 60 * 60 * 1000; + } else { + hoursToExpire = 30 * 365 * 24 * 60 * 60 * 1000; + } + this.setCookieConsentGiven(); + var now = new Date().getTime(); + setCookie(COOKIE_CONSENT_COOKIE_NAME, now, hoursToExpire, configCookiePath, configCookieDomain, configCookieIsSecure); + }; + + /** * 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. */ @@ -6556,10 +6690,6 @@ if (typeof window.Matomo !== 'object') { * This may be useful when you want to implement for example a popup to ask for consent before tracking the user. * Once the user has given consent, you should call {@link setConsentGiven()} or {@link rememberConsentGiven()}. * - * Please note that when consent is required, we will temporarily set cookies but not track any data. Those - * cookies will only exist during this page view and deleted as soon as the user navigates to a different page - * or closes the browser. - * * If you require consent for tracking personal data for example, you should first call * `_paq.push(['requireConsent'])`. * @@ -6574,6 +6704,11 @@ if (typeof window.Matomo !== 'object') { this.requireConsent = function () { configConsentRequired = true; configHasConsent = this.hasRememberedConsent(); + if (!configHasConsent) { + // we won't call this.disableCookies() since we don't want to delete any cookies just yet + // user might call `setConsentGiven` next + configCookiesDisabled = true; + } // Matomo.addPlugin might not be defined at this point, we add the plugin directly also to make JSLint happy // We also want to make sure to define an unload listener for each tracker, not only one tracker. coreConsentCounter++; @@ -6591,10 +6726,16 @@ if (typeof window.Matomo !== 'object') { * Call this method once the user has given consent. This will cause all tracking requests from this * page view to be sent. Please note that the given consent won't be remembered across page views. If you * want to remember consent across page views, call {@link rememberConsentGiven()} instead. + * + * It will also automatically enable cookies if they were disabled previously. + * + * @param bool [setCookieConsent=true] Internal parameter. Defines whether cookies should be enabled or not. */ - this.setConsentGiven = function () { + this.setConsentGiven = function (setCookieConsent) { configHasConsent = true; + deleteCookie(CONSENT_REMOVED_COOKIE_NAME, configCookiePath, configCookieDomain); + var i, requestType; for (i = 0; i < consentRequestsQueue.length; i++) { requestType = typeof consentRequestsQueue[i]; @@ -6605,11 +6746,23 @@ if (typeof window.Matomo !== 'object') { } } consentRequestsQueue = []; + + // we need to enable cookies after sending the previous requests as it will make sure that we send + // a ping request if needed. Cookies are only set once we call `getRequest`. Above only calls sendRequest + // meaning no cookies will be created unless we called enableCookies after at least one request has been sent. + // this will cause a ping request to be sent that sets the cookies and also updates the newly generated visitorId + // on the server. + // If the user calls setConsentGiven before sending any tracking request (which usually is the case) then + // nothing will need to be done as it only enables cookies and the next tracking request will set the cookies + // etc. + if (!isDefined(setCookieConsent) || setCookieConsent) { + this.setCookieConsentGiven(); + } }; /** * Calling this method will remember that the user has given consent across multiple requests by setting - * a cookie. You can optionally define the lifetime of that cookie in milliseconds using a parameter. + * a cookie. You can optionally define the lifetime of that cookie in hours using a parameter. * * When you call this method, we imply that the user has given consent for this page view, and will also * imply consent for all future page views unless the cookie expires (if timeout defined) or the user @@ -6621,6 +6774,9 @@ if (typeof window.Matomo !== 'object') { * for all sites that match the configured cookieDomain and cookiePath. Depending on your website structure, * you may need to restrict or widen the scope of the cookie domain/path to ensure the consent is applied * to the sites you want. + * + * @param int hoursToExpire After how many hours the consent should expire. By default the consent is valid + * for 30 years unless cookies are deleted by the user or the browser prior to this */ this.rememberConsentGiven = function (hoursToExpire) { if (hoursToExpire) { @@ -6628,7 +6784,10 @@ if (typeof window.Matomo !== 'object') { } else { hoursToExpire = 30 * 365 * 24 * 60 * 60 * 1000; } - this.setConsentGiven(); + var setCookieConsent = true; + // we currently always enable cookies if we remember consent cause we don't store across requests whether + // cookies should be automatically enabled or not. + this.setConsentGiven(setCookieConsent); var now = new Date().getTime(); setCookie(CONSENT_COOKIE_NAME, now, hoursToExpire, configCookiePath, configCookieDomain, configCookieIsSecure); }; @@ -6644,6 +6803,7 @@ if (typeof window.Matomo !== 'object') { deleteCookie(CONSENT_COOKIE_NAME, configCookiePath, configCookieDomain); setCookie(CONSENT_REMOVED_COOKIE_NAME, new Date().getTime(), thirtyYears, configCookiePath, configCookieDomain, configCookieIsSecure); + this.forgetCookieConsentGiven(); this.requireConsent(); }; @@ -6665,7 +6825,11 @@ if (typeof window.Matomo !== 'object') { /** * Alias for rememberConsentGiven(). After calling this function, the current user will be tracked. */ - this.forgetUserOptOut = this.rememberConsentGiven; + this.forgetUserOptOut = function () { + // we can't automatically enable cookies here as we don't know if user actually gave consent for cookies + var setCookieConsent = false; + this.rememberConsentGiven(0, setCookieConsent); + }; /** * Mark performance metrics as available, once onload event has finished @@ -6728,7 +6892,7 @@ if (typeof window.Matomo !== 'object') { * Constructor ************************************************************/ - var applyFirst = ['addTracker', 'disableCookies', 'setTrackerUrl', 'setAPIUrl', 'enableCrossDomainLinking', 'setCrossDomainLinkingTimeout', 'setSessionCookieTimeout', 'setVisitorCookieTimeout', 'setCookieNamePrefix', 'setSecureCookie', 'setCookiePath', 'setCookieDomain', 'setDomains', 'setUserId', 'setSiteId', 'alwaysUseSendBeacon', 'enableLinkTracking', 'requireConsent', 'setConsentGiven']; + var applyFirst = ['addTracker', 'forgetCookieConsentGiven', 'requireCookieConsent', 'disableCookies', 'setTrackerUrl', 'setAPIUrl', 'enableCrossDomainLinking', 'setCrossDomainLinkingTimeout', 'setSessionCookieTimeout', 'setVisitorCookieTimeout', 'setCookieNamePrefix', 'setSecureCookie', 'setCookiePath', 'setCookieDomain', 'setDomains', 'setUserId', 'setVisitorId', 'setSiteId', 'alwaysUseSendBeacon', 'enableLinkTracking', 'setCookieConsentGiven', 'requireConsent', 'setConsentGiven']; function createFirstTracker(matomoUrl, siteId) { |