Browse Source

Add cookie banner (#6627)

* Add cookie banner

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
Co-authored-by: Miguel Serrano <mserranom@users.noreply.github.com>
GitOrigin-RevId: a3625d4e6357ff58c7c47532901c382bedbe07e0
Thomas 4 years ago
parent
commit
76beba4393

+ 9 - 10
services/web/app/src/infrastructure/ExpressLocals.js

@@ -277,16 +277,6 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
     next()
   })
 
-  webRouter.use(function (req, res, next) {
-    res.locals.gaToken =
-      Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.token
-    res.locals.gaTokenV4 =
-      Settings.analytics &&
-      Settings.analytics.ga &&
-      Settings.analytics.ga.tokenV4
-    next()
-  })
-
   webRouter.use(function (req, res, next) {
     res.locals.getReqQueryParam = field =>
       req.query != null ? req.query[field] : undefined
@@ -387,6 +377,15 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
       sentryEnvironment: Settings.sentry.environment,
       sentryRelease: Settings.sentry.release,
       enableSubscriptions: Settings.enableSubscriptions,
+      gaToken:
+        Settings.analytics &&
+        Settings.analytics.ga &&
+        Settings.analytics.ga.token,
+      gaTokenV4:
+        Settings.analytics &&
+        Settings.analytics.ga &&
+        Settings.analytics.ga.tokenV4,
+      cookieDomain: Settings.cookieDomain,
     }
     next()
   })

+ 5 - 0
services/web/app/views/_cookie_banner.pug

@@ -0,0 +1,5 @@
+.cookie-banner.hidden-print.hidden
+    .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our <a href="/legal#Cookies">cookie policy</a>.
+    .cookie-banner-actions
+        button(type="button" class="btn btn-link btn-sm" data-ol-cookie-banner-set-consent="essential") Essential cookies only
+        button(type="button" class="btn btn-info btn-sm" data-ol-cookie-banner-set-consent="all") Accept all cookies

+ 50 - 0
services/web/app/views/_google_analytics.pug

@@ -0,0 +1,50 @@
+if (typeof(ExposedSettings.gaTokenV4) != "undefined" || typeof(ExposedSettings.gaToken) != "undefined")
+    script(type="text/javascript", nonce=scriptNonce, id="ga-loader" data-ga-token=ExposedSettings.gaToken data-ga-token-v4=ExposedSettings.gaTokenV4 data-cookie-domain=ExposedSettings.cookieDomain).
+        var gaSettings = document.querySelector('#ga-loader').dataset;
+        var gaid = gaSettings.gaTokenV4;
+        var gaToken = gaSettings.gaToken;
+        var cookieDomain = gaSettings.cookieDomain;
+        if(gaid) {
+            window.dataLayer = window.dataLayer || [];
+            function gtag(){
+                dataLayer.push(arguments);
+            }
+            gtag('js', new Date());
+            gtag('config', gaid, { 'anonymize_ip': true });
+        }
+        if (gaToken) {
+            window.ga = window.ga || function () {
+                (window.ga.q = window.ga.q || []).push(arguments);
+            }, window.ga.l = 1 * new Date();
+        }
+        var loadGA = window.olLoadGA = function() {
+            if (gaid) {
+                var s = document.createElement('script');
+                s.setAttribute('async', 'async');
+                s.setAttribute('src', 'https://www.googletagmanager.com/gtag/js?id=' + gaid);
+                document.querySelector('head').append(s);
+            } 
+            if (gaToken) {
+                (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+                (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+                m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+                })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+                ga('create', gaToken, cookieDomain.replace(/^\./, ""));
+                ga('set', 'anonymizeIp', true);
+                ga('send', 'pageview');
+            }
+        };
+        // Check if consent given (features/cookie-banner)
+        var oaCookie = document.cookie.split('; ').find(function(cookie) {
+            return cookie.startsWith('oa=');
+        });
+        if(oaCookie) {
+            var oaCookieValue = oaCookie.split('=')[1];
+            if(oaCookieValue === '1') {
+                loadGA();
+            }
+        }
+else 
+    script(type="text/javascript", nonce=scriptNonce).
+        window.ga = function() { console.log("would send to GA", arguments) };
+        window.gtag = function() { console.log("would send to GA4", arguments) };

+ 1 - 37
services/web/app/views/layout-base.pug

@@ -25,43 +25,7 @@ html(
 					link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode)
 
 		//- Scripts
-
-		//- Google Analytics
-		if (typeof(gaToken) != "undefined")
-			script(type="text/javascript", nonce=scriptNonce).
-				(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-				(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-				m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-				})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-			script(type="text/javascript", nonce=scriptNonce).
-				ga('create', '#{gaToken}', '#{settings.cookieDomain.replace(/^\./, "")}');
-				ga('set', 'anonymizeIp', true);
-				ga('send', 'pageview');
-
-				try {
-					ga.isBlocked = localStorage.getItem('gaBlocked') === 'true'
-					if (!ga.isBlocked) {
-						window.addEventListener('load', function () {
-							setTimeout(function () {
-								if (!ga.loaded) localStorage.setItem('gaBlocked', 'true')
-							}, 4000)
-						})
-					}
-				} catch (e) {}
-		else
-			script(type="text/javascript", nonce=scriptNonce).
-				window.ga = function() { console.log("would send to GA", arguments) };
-
-		if (typeof(gaTokenV4) != "undefined")
-			script(async, nonce=scriptNonce, src='https://www.googletagmanager.com/gtag/js?id=' + gaTokenV4)
-			script(type="text/javascript", nonce=scriptNonce).
-				window.dataLayer = window.dataLayer || [];
-				function gtag(){dataLayer.push(arguments);}
-				gtag('js', new Date());
-				gtag('config', '#{gaTokenV4}');
-		else
-			script(type = "text/javascript", nonce = scriptNonce).
-				window.gtag = function() { console.log("would send to GA4", arguments) };
+		include _google_analytics
 
 		block meta
 			meta(name="ol-csrfToken" content=csrfToken)

+ 2 - 0
services/web/app/views/layout-marketing.pug

@@ -17,4 +17,6 @@ block body
 		else
 			include layout/fat-footer
 
+		include _cookie_banner
+
 	!= moduleIncludes("contactModal-marketing", locals)

+ 2 - 0
services/web/app/views/layout.pug

@@ -15,4 +15,6 @@ block body
 		else
 			include layout/fat-footer
 
+		include _cookie_banner
+
 	!= moduleIncludes("contactModal", locals)

+ 43 - 0
services/web/frontend/js/features/cookie-banner/index.js

@@ -0,0 +1,43 @@
+function loadGA() {
+  if (window.olLoadGA) {
+    window.olLoadGA()
+  }
+}
+
+function setConsent(value) {
+  document.querySelector('.cookie-banner').classList.add('hidden')
+  const cookieDomain = window.ExposedSettings.cookieDomain
+  const oneYearInSeconds = 60 * 60 * 24 * 365
+  const cookieAttributes =
+    '; domain=' +
+    cookieDomain +
+    '; max-age=' +
+    oneYearInSeconds +
+    '; SameSite=Lax; Secure'
+  if (value === 'all') {
+    document.cookie = 'oa=1' + cookieAttributes
+    loadGA()
+  } else {
+    document.cookie = 'oa=0' + cookieAttributes
+  }
+}
+
+if (window.ExposedSettings.gaToken || window.ExposedSettings.gaTokenV4) {
+  document
+    .querySelectorAll('[data-ol-cookie-banner-set-consent]')
+    .forEach(el => {
+      el.addEventListener('click', function (e) {
+        e.preventDefault()
+        const consentType = el.getAttribute('data-ol-cookie-banner-set-consent')
+        setConsent(consentType)
+      })
+    })
+
+  const oaCookie = document.cookie.split('; ').find(c => c.startsWith('oa='))
+  if (!oaCookie) {
+    const cookieBannerEl = document.querySelector('.cookie-banner')
+    if (cookieBannerEl) {
+      cookieBannerEl.classList.remove('hidden')
+    }
+  }
+}

+ 1 - 0
services/web/frontend/js/main.js

@@ -51,6 +51,7 @@ import './services/queued-http'
 import './services/validateCaptcha'
 import './services/validateCaptchaV3'
 import './filters/formatDate'
+import './features/cookie-banner'
 import '../../modules/modules-main.js'
 import './cdn-load-test'
 angular.module('SharelatexApp').config(function ($locationProvider) {

+ 1 - 0
services/web/frontend/js/marketing.js

@@ -10,6 +10,7 @@ import './features/contact-form'
 import './features/event-tracking'
 import './features/fallback-image'
 import './features/multi-submit'
+import './features/cookie-banner'
 
 $('[data-ol-lang-selector-tooltip]').tooltip({ trigger: 'hover' })
 $('[data-toggle="tooltip"]').tooltip()

+ 34 - 0
services/web/frontend/stylesheets/components/footer.less

@@ -262,3 +262,37 @@ footer.site-footer {
     }
   }
 }
+
+.cookie-banner {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  padding: 10px 20px;
+  font-size: 0.9rem;
+  line-height: 1;
+  position: fixed;
+  bottom: 0px;
+  left: 0;
+  right: 0;
+  z-index: 100;
+  color: @text-color;
+  background: @ol-blue-gray-0;
+  box-shadow: 0 -2px 1px 0px #2c36455c;
+
+  a {
+    color: @footer-link-color;
+    &:hover,
+    &:focus {
+      color: @footer-link-hover-color;
+    }
+  }
+}
+
+.cookie-banner-content {
+  flex: 1;
+}
+
+.cookie-banner-actions {
+  flex-shrink: 0;
+  white-space: nowrap;
+}