From 6c920ee31871cabdc5012aa101fd0ee3701d9679 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 12:43:51 +0200 Subject: [PATCH 01/15] info.xml: allow usage with nextcloud v30 --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 965a4d5..19ae806 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,6 +12,6 @@ customization http://example.com/foo - + From 4af5bc290c508ca9611dcdbb5ec20a0e737924cb Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 12:52:37 +0200 Subject: [PATCH 02/15] info.xml: set version to 0.0.2 --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 19ae806..49258f7 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ ENC Analytics GA4 Analytics enabler for Nextcloud - 0.0.1 + 0.0.2 agpl Mikhailo Matiyenko-Kupriyanov EncAnalytics From db18df58582434ac52f8e5c584fa0621418e0857 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 12:45:32 +0200 Subject: [PATCH 03/15] package.json: remove stylelint since we have no css in project --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 31d2b51..297a443 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,7 @@ "private": true, "scripts": { "lint": "eslint --ext .js,.vue src", - "lint:fix": "eslint --ext .js,.vue src --fix", - "stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue", - "stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix" + "lint:fix": "eslint --ext .js,.vue src --fix" }, "dependencies": { "@nextcloud/axios": "^1.10.0", From 5c0c161f4fb3a1e3ed03ca841833300f47c7115c Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 13:18:43 +0200 Subject: [PATCH 04/15] package.json: add noop build script in order to have same "signature" for nextcloud projects --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 297a443..8e29c18 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "agpl", "private": true, "scripts": { + "build": "echo 'Nothing to build'", "lint": "eslint --ext .js,.vue src", "lint:fix": "eslint --ext .js,.vue src --fix" }, From 534f967cf1edd6dde8459768f591fc1c3b694e8d Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 13:20:48 +0200 Subject: [PATCH 05/15] info.xml: add link to bugtracker --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 49258f7..859cc7b 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -10,7 +10,7 @@ Mikhailo Matiyenko-Kupriyanov EncAnalytics customization - http://example.com/foo + https://github.com/IONOS-Productivity/enc_analytics/issues From 3cd3572b6af7f7a6b00a5b39c588d3277f5544f0 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 15:53:16 +0200 Subject: [PATCH 06/15] reuse: add project files to AGPL-3.0-or-later license --- .reuse/dep5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.reuse/dep5 b/.reuse/dep5 index 07fa543..6c57f18 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -3,7 +3,7 @@ Upstream-Name: ENC Analytics Upstream-Contact: Mikhailo Matiyenko-Kupriyanov Source: https://github.com/nextcloud/profiler -Files: package-lock.json package.json composer.json composer.lock +Files: package-lock.json package.json composer.json composer.lock lib/*.php lib/**/*.php appinfo/* js/*.js .gitlab-ci.yml README.md Copyright: Mikhailo Matiyenko-Kupriyanov License: AGPL-3.0-or-later From 3a0f4aabc764ff220f176949b45465998ce65279 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 15:12:17 +0200 Subject: [PATCH 07/15] reuse: add composer.phar license --- .reuse/dep5 | 4 ++++ LICENSES/MIT.txt | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 LICENSES/MIT.txt diff --git a/.reuse/dep5 b/.reuse/dep5 index 6c57f18..280b8fd 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -10,3 +10,7 @@ License: AGPL-3.0-or-later Files: l10n/*.js l10n/*.json Copyright: Nextcloud translators License: AGPL-3.0-or-later + +Files: composer.phar +Copyright: Nils Adermann, Jordi Boggiano +License: MIT diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From b427666cb55702044f432440439870d65e775aed Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 15:39:01 +0200 Subject: [PATCH 08/15] reuse: add phpunit.phar license --- .reuse/dep5 | 4 ++++ LICENSES/BSD-3-Clause.txt | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 LICENSES/BSD-3-Clause.txt diff --git a/.reuse/dep5 b/.reuse/dep5 index 280b8fd..e3ee9df 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -14,3 +14,7 @@ License: AGPL-3.0-or-later Files: composer.phar Copyright: Nils Adermann, Jordi Boggiano License: MIT + +Files: phpunit.phar +Copyright: Sebastian Bergmann +License: BSD-3-Clause diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..ea890af --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) . + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 04739fee246077b25d2f73c6327814b28932d0f8 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 15:58:50 +0200 Subject: [PATCH 09/15] replace deprecated node v16 with v20 --- .github/workflows/lint-eslint.yml | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint-eslint.yml b/.github/workflows/lint-eslint.yml index 11b0b51..e689ae7 100644 --- a/.github/workflows/lint-eslint.yml +++ b/.github/workflows/lint-eslint.yml @@ -29,11 +29,11 @@ jobs: uses: skjnldsv/read-package-engines-version-actions@v1.2 id: versions with: - fallbackNode: '^16' + fallbackNode: '^20' fallbackNpm: '^7' - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ steps.versions.outputs.nodeVersion }} diff --git a/package.json b/package.json index 8e29c18..7c82858 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "extends @nextcloud/browserslist-config" ], "engines": { - "node": "^16.0.0", + "node": "^20.0.0", "npm": "^7.0.0 || ^8.0.0" }, "devDependencies": { From 3018f1c1bcdafc9488cbff15f340f0eb8fa64ffe Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 7 May 2024 16:58:50 +0200 Subject: [PATCH 10/15] track.js: GTM deny consent by default https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced --- js/track.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/track.js b/js/track.js index c0ef618..aeac44a 100644 --- a/js/track.js +++ b/js/track.js @@ -1,3 +1,16 @@ +window.dataLayer = window.dataLayer || []; + +function gtag() { + dataLayer.push(arguments); +} + +gtag('consent', 'default', { + 'ad_storage': 'denied', + 'ad_user_data': 'denied', + 'ad_personalization': 'denied', + 'analytics_storage': 'denied' +}); + (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], From 055fbbe5cd1b0fad4039f31cf4fd7638c9e542ca Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 8 May 2024 11:53:29 +0200 Subject: [PATCH 11/15] track.js: updateConsent depending on IONOS privacy cookie https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced --- js/track.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/js/track.js b/js/track.js index aeac44a..5b4df2b 100644 --- a/js/track.js +++ b/js/track.js @@ -11,10 +11,59 @@ gtag('consent', 'default', { 'analytics_storage': 'denied' }); +/** + * Update the consent for Google Analytics depending on IONOS consent cookie + */ +function updateConsent() { + function getCookie(cname) { + let name = cname + "="; + let ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ""; + } + + /** + * @typedef {Object} PrivacyConsent + * @property {boolean} technical + * @property {boolean} statistics + * @property {boolean} marketing + * @property {boolean} partnerships + */ + let privacyConsentDecoded = {}; + + try { + const cookie = getCookie('PRIVACY_CONSENT'); + if (cookie === '' || !cookie) { + throw new Error('No privacy consent cookie found') + } + + const cookieDecoded = atob(cookie); + privacyConsentDecoded = JSON.parse(cookieDecoded); + } catch (e) { + console.warn('Failed to parse privacy consent cookie', e); + } + + if (privacyConsentDecoded['marketing'] === true) { + gtag('consent', 'update', { + 'ad_user_data': 'granted', + 'ad_personalization': 'granted', + }); + } +} + (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + updateConsent(); })(window,document,'script','dataLayer', '%GTM_ID%'); From 52ba9b007ce9aecd2929c8779875e55ca462329e Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 8 May 2024 12:36:17 +0200 Subject: [PATCH 12/15] track.js: restrict referrer https://developers.google.com/tag-platform/tag-manager/restrict --- js/track.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/track.js b/js/track.js index 5b4df2b..2048894 100644 --- a/js/track.js +++ b/js/track.js @@ -1,4 +1,7 @@ window.dataLayer = window.dataLayer || []; +dataLayer.push({ + 'gtm.blocklist': ['f'] +}); function gtag() { dataLayer.push(arguments); From 6c8f662e010b234b503fb2e610b6b10a7aa391c1 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 8 May 2024 12:51:48 +0200 Subject: [PATCH 13/15] track.js: restrict url url won't be passed to GTM https://developers.google.com/tag-platform/tag-manager/restrict --- js/track.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/track.js b/js/track.js index 2048894..d10e398 100644 --- a/js/track.js +++ b/js/track.js @@ -1,6 +1,6 @@ window.dataLayer = window.dataLayer || []; dataLayer.push({ - 'gtm.blocklist': ['f'] + 'gtm.blocklist': ['f', 'u'] }); function gtag() { From b92dbc124ca85a68e1b1c170742131e3da30ae86 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Fri, 10 May 2024 11:47:20 +0200 Subject: [PATCH 14/15] Application.php: allow GA4 CSP https://developers.google.com/tag-platform/security/guides/csp --- lib/AppInfo/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 4d263ac..02e4f31 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -39,7 +39,7 @@ public function addTrackingScript(IURLGenerator $urlGenerator, ContentSecurityPo } public function addContentSecurityPolicy(ContentSecurityPolicyManager $policyManager): void { - $allowedUrl = "www.googletagmanager.com"; + $allowedUrl = "*.googletagmanager.com tagmanager.google.com *.google-analytics.com"; $policy = new ContentSecurityPolicy(); $policy->addAllowedScriptDomain($allowedUrl); From 0b63cadb523ba859848bd2522f2f633af6aeaadb Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 13 May 2024 11:05:33 +0200 Subject: [PATCH 15/15] POC sanitization --- js/track.js | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/js/track.js b/js/track.js index d10e398..d64187e 100644 --- a/js/track.js +++ b/js/track.js @@ -1,9 +1,104 @@ +const ENC_TRACKING = { + privacyConsentCookie: "PRIVACY_CONSENT", + pathsToSanitize: [ + /^((\/index\.php|)\/apps\/files\/personal)(.*)$/, + /^((\/index\.php|)\/apps\/files\/personal\/)(.*)$/, + /^((\/index\.php|)\/apps\/files\/resent)$/, + /^((\/index\.php|)\/apps\/files\/resent\/)(.*)$/, + /^((\/index\.php|)\/apps\/files\/favorites)$/, + /^((\/index\.php|)\/apps\/files\/favorites\/)(.*)$/, + /^((\/index\.php|)\/apps\/files\/trashbin)$/, + /^((\/index\.php|)\/apps\/files\/trashbin\/)(.*)$/, + /^((\/index\.php|)\/apps\/files)$/, + /^((\/index\.php|)\/apps\/files\/)(.*)$/, + ], + lastEventId: 0, + eventCounter: 0, + /** + * Array of paths as regex to sanitize + * @param {string} url + * @returns {string} + */ + sanitizeUrl: (url) => { + console.group('sanitizeUrl'); + const urlObj = new URL(url); + + console.log('URL:before', url); + + for (const path of ENC_TRACKING.pathsToSanitize) { + if (path.test(urlObj.pathname)) { + const matched = urlObj.pathname.match(path); + + let pathParametersHash = ''; + + if (matched[1].endsWith('/')) { + pathParametersHash = '~sanitized~'; + } + + const pathnameSanitized = `${matched[1]}${pathParametersHash}`; + urlObj.pathname = `${pathnameSanitized}`; + if (urlObj.search) { + urlObj.search = ''; + } + console.log('URL:after', urlObj.toString()); + console.groupEnd(); + return urlObj.toString(); + } + } + console.groupEnd(); + return url; + }, + + /** + * Sanitize GTM events + * @param {IArguments} args arguments passed to GTM event + * @returns {IArguments} sanitized arguments + */ + sanitizeGTMEvent: (args) => { + if (args.length === 0) { + return args; + } + + const event = args[0]; + if (event.event === undefined) { + return args; + } + + let sanitizedArgs = args; + ENC_TRACKING.eventCounter++; + if (event.event === 'gtm.historyChange-v2') { + console.trace('historyChange-v2 event', event); + sanitizedArgs[0]['gtm.oldUrl'] = `${ENC_TRACKING.sanitizeUrl(sanitizedArgs[0]['gtm.oldUrl'])}?_=${ENC_TRACKING.lastEventId}`; //sanitizeUrl(sanitizedArgs[0]['gtm.oldUrl']); + sanitizedArgs[0]['gtm.newUrl'] = `${ENC_TRACKING.sanitizeUrl(document.location.href)}?_=${ENC_TRACKING.eventCounter}`; //sanitizeUrl(document.location.href); + + ENC_TRACKING.lastEventId = ENC_TRACKING.eventCounter; + } else if (event.event.startsWith('gtm.historyChange')) { + console.warn('Unsupported historyChange event', event); + } + + return sanitizedArgs; + } +}; + +console.log('ENC_TRACKING', ENC_TRACKING); + window.dataLayer = window.dataLayer || []; dataLayer.push({ 'gtm.blocklist': ['f', 'u'] }); +const dataLayerPush = window.dataLayer.push; +window.dataLayer.push = function () { + console.group('dataLayer.push'); + console.log('dataLayer.push:before', arguments[0]); + const sanitized = ENC_TRACKING.sanitizeGTMEvent(arguments); + console.log('dataLayer.push:sanitized', sanitized); + dataLayerPush.apply(window.dataLayer, sanitized); + console.groupEnd(); +} + function gtag() { + console.log('gtag', arguments); dataLayer.push(arguments); } @@ -43,7 +138,7 @@ function updateConsent() { let privacyConsentDecoded = {}; try { - const cookie = getCookie('PRIVACY_CONSENT'); + const cookie = getCookie(ENC_TRACKING.privacyConsentCookie); if (cookie === '' || !cookie) { throw new Error('No privacy consent cookie found') }