From d6f3e691ca8a31e7e0503aa26b05b3bc1cb63129 Mon Sep 17 00:00:00 2001
From: Sebastian Castro <sebastian.castro@protonmail.com>
Date: Thu, 6 May 2021 12:29:34 +0200
Subject: [PATCH] matomo: adds matomo integration

---
 .env                                          |   6 +
 assets/admin.pack.js                          |   3 +
 assets/js/admin/MatomoVisits.vue              | 111 ++++++++++++++++++
 assets/js/admin/charts.js                     |  16 +++
 config/packages/twig.yaml                     |   3 +
 config/services.yaml                          |   5 +
 docs/matomo.md                                |  20 ++++
 package.json                                  |   1 +
 src/Services/TwigHelperService.php            |  14 ++-
 templates/admin/blocks/block_charts.html.twig |  16 ++-
 templates/custom-head.html.twig               |  23 +++-
 translations/admin+intl-icu.fr.yaml           |  13 ++
 yarn.lock                                     |   5 +
 13 files changed, 231 insertions(+), 5 deletions(-)
 create mode 100644 assets/js/admin/MatomoVisits.vue
 create mode 100644 assets/js/admin/charts.js
 create mode 100644 docs/matomo.md

diff --git a/.env b/.env
index 0572ee992..303eb65a9 100755
--- a/.env
+++ b/.env
@@ -70,3 +70,9 @@ SENTRY_DSN=
 
 # Php path should be automatically detected, but if you encounter some trouble you can use following variable :
 # PHP_PATH=/usr/bin/php 
+
+###> Matomo integration - see docs/matomo.md ###
+MATOMO_URL=
+MATOMO_SITE_ID=
+MATOMO_USER_TOKEN=
+###< Matomo ###
diff --git a/assets/admin.pack.js b/assets/admin.pack.js
index b3547a089..a198fab30 100755
--- a/assets/admin.pack.js
+++ b/assets/admin.pack.js
@@ -7,5 +7,8 @@ import './js/admin/element-import/element-import'
 import './js/admin/osm-tags'
 import './js/admin/element-edit'
 import './js/admin/source-priority'
+import './js/admin/charts'
+
+
 
 import './js/i18n'
diff --git a/assets/js/admin/MatomoVisits.vue b/assets/js/admin/MatomoVisits.vue
new file mode 100644
index 000000000..0983e93c1
--- /dev/null
+++ b/assets/js/admin/MatomoVisits.vue
@@ -0,0 +1,111 @@
+<template>
+  <div class="matomo-visits">
+    <h4>{{ t('js.charts.title_visitors') }}</h4>
+    <div class="form-inline text-center">
+      <div class="form-group">
+        <label>{{ t('js.charts.display') }}</label>
+        <select class="form-control" v-model="field">
+          <option v-for="type in ['nb_visits', 'avg_time_on_page', ]" :key="type" :value="type">
+            {{ t(`js.charts.field.${type}`) }}
+          </option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label>{{ t('js.charts.last') }}</label>
+        <input class="input-last form-control" type="number" v-model="lastCount"/>
+        <select class="form-control" v-model="period">
+          <option v-for="type in ['day', 'week', 'month', 'year']" :key="type" :value="type">
+            {{ t(`js.charts.period.${type}`) }}
+          </option>
+        </select>
+      </div>      
+      <button type="button" class="btn btn-default" @click="loadData">{{ t('js.charts.reload') }}</button>
+    </div>
+    <div class="chart-container" ref="chartContainer">
+      <div class="loader"><i class="fa fa-spienner fa-spin"></i></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Highcharts from 'highcharts'
+export default {
+  props: ['baseUrl', 'siteId', 'token', 'projectUrl'],
+  data() {
+    return {
+      period: 'month',
+      lastCount: '12',
+      field: 'nb_visits',
+      data: []
+    }
+  },
+  computed: {
+    matomoUrl() {
+      return `${this.baseUrl}/index.php?module=API`+
+          `&method=Actions.getPageTitle`+
+          `&idSite=${this.siteId}`+
+          `&pageName=${this.projectUrl}`+ // we use the projectUrl as pageName in order to easily get stats for every project on saas instance
+          `&period=${this.period}`+
+          `&date=last${this.lastCount}`+
+          `&format=JSON`+
+          `&token_auth=${this.token}`
+    }
+  },
+  watch: {
+    field: function() {
+      this.drawData()
+    }
+  },
+  methods: {
+    loadData() {
+      $.getJSON(this.matomoUrl, (data) => {
+        this.data = data
+        this.drawData()
+      })
+    },
+    drawData() {
+      const chart = Highcharts.chart(this.$refs.chartContainer, {
+        chart: { type: 'spline' },
+        title: false,
+        xAxis: {
+          categories: Object.keys(this.data).map((el) => el)
+        },
+        yAxis: { title: false },
+        series: [{
+          name: t(`js.charts.field.${this.field}`),
+          data: Object.values(this.data).map((el) => el.length ? el[0][this.field] : 0)
+        }]
+      });
+    }
+  },
+  mounted() {
+    this.loadData()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .matomo-visits {
+    background-color: white;
+    padding-top: 1rem;
+    margin-bottom: 2rem;
+  }
+  h4 { 
+    text-align: center; 
+    margin-bottom: 1.5rem;
+    color: black;
+  }
+  .input-last { width: 60px; }
+  .form-group {
+    margin-right: 1rem;
+  }
+  label {
+    font-weight: normal;
+  }
+  .loader {
+    padding: 50px 0;
+    text-align: center;
+    i { font-size: 30px; }
+  }
+
+</style>
\ No newline at end of file
diff --git a/assets/js/admin/charts.js b/assets/js/admin/charts.js
new file mode 100644
index 000000000..73762c120
--- /dev/null
+++ b/assets/js/admin/charts.js
@@ -0,0 +1,16 @@
+import Highcharts from 'highcharts'
+
+// Make it availabel globally for the highcharts-bundle
+window.Highcharts = Highcharts
+
+import Vue from '../vendor/vue-custom'
+import MatomoVisits from './MatomoVisits'
+
+document.addEventListener('DOMContentLoaded', function() {
+    if ($('.matomo-visits-container').length > 0) {
+        new Vue({
+            el: '.matomo-visits-container',
+            components: { MatomoVisits }
+        })
+    }
+})
\ No newline at end of file
diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml
index 23ad18cc7..fcc29f213 100755
--- a/config/packages/twig.yaml
+++ b/config/packages/twig.yaml
@@ -13,6 +13,9 @@ twig:
         oauth_google: '%oauth_google_id%'
         oauth_facebook: '%oauth_facebook_id%'
         helper: '@App\Services\TwigHelperService'
+        matomo_url: '%env(MATOMO_URL)%'
+        matomo_site_id: '%env(MATOMO_SITE_ID)%'
+        matomo_token: '%env(MATOMO_USER_TOKEN)%'
     paths:
         "%kernel.root_dir%/../web": RootDir
     form_themes:
diff --git a/config/services.yaml b/config/services.yaml
index e1ce9f7b9..f312cd9ee 100755
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -55,6 +55,11 @@ services:
       arguments:
         $rootDB: '%root_db%'
 
+    App\Services\TwigHelperService:
+      arguments:
+        $baseUrl: '%base_url%'
+        $useAsSaas: '%use_as_saas%'
+
     # Commands
     App\Command\RemoveAbandonnedProjectsCommand:
       arguments:
diff --git a/docs/matomo.md b/docs/matomo.md
new file mode 100644
index 000000000..41bae4425
--- /dev/null
+++ b/docs/matomo.md
@@ -0,0 +1,20 @@
+# Matomo Integration
+
+If you want to track visitors the open source software Matomo, then you need to
+
+### Provide environement variable
+in `.env.local` 
+```
+MATOMO_URL=https://my_matomo_server.org/
+MATOMO_SITE_ID=12
+MATOMO_USER_TOKEN=anonymous
+```
+You can also use a dedicate user token if you prefer
+
+### Configure Cross Origin
+
+In your Matomo instance, go to Administration > System > General settings and fill the Cross Origin Section. More info at https://matomo.org/faq/how-to/faq_18694/
+
+### Allow visibility for anonymous user
+
+If you use the anonymous token, then go to Administration > System > Users, and grant anonymous user the "view" permission for the website
diff --git a/package.json b/package.json
index be1af9b1b..75956803d 100755
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
   "dependencies": {
     "core-js": "^3.8.1",
     "gogocarto-js": "^1.7.3",
+    "highcharts": "^9.1.0",
     "leaflet": "^1.7.1",
     "leaflet-shades": "^0.1.4",
     "natives": "^1.1.6",
diff --git a/src/Services/TwigHelperService.php b/src/Services/TwigHelperService.php
index 1f3c420de..d822863b9 100755
--- a/src/Services/TwigHelperService.php
+++ b/src/Services/TwigHelperService.php
@@ -7,10 +7,12 @@ use Symfony\Contracts\Translation\TranslatorInterface;
 
 class TwigHelperService
 {
-  public function __construct(DocumentManager $dm, TranslatorInterface $t)
+  public function __construct(DocumentManager $dm, TranslatorInterface $t, $baseUrl, $useAsSaas)
   {
     $this->dm = $dm;
     $this->t = $t;
+    $this->baseUrl = $baseUrl;
+    $this->useAsSaas = $useAsSaas;
   }
 
   public function config()
@@ -23,6 +25,16 @@ class TwigHelperService
     return $this->t;
   }
 
+  public function mainUrl()
+  {
+    if ($url = $this->config()->getCustomDomain())
+      return explode('://', $url)[1];
+    elseif ($this->useAsSaas)
+      return $this->config()->getDbName() . '.' . $this->baseUrl;
+    else
+      return $this->baseUrl;
+  }
+
   public function listAbouts()
   {
     return $this->dm->get('About')->findAllOrderedByPosition();
diff --git a/templates/admin/blocks/block_charts.html.twig b/templates/admin/blocks/block_charts.html.twig
index ef3bce0be..892ec6f64 100755
--- a/templates/admin/blocks/block_charts.html.twig
+++ b/templates/admin/blocks/block_charts.html.twig
@@ -2,8 +2,6 @@
 
 {% block block %}
 
-    <script src="//code.highcharts.com/4.1.8/highcharts.js"></script>
-    <script src="//code.highcharts.com/4.1.8/modules/exporting.js"></script>
     <script type="text/javascript">
         {{ chart(collabResolveChart) }}
         {{ chart(userInteractChart) }}
@@ -12,7 +10,19 @@
     </script>
 
     <div class="chart-container row">
-        <div id="userInteractChart" class="col-md-12 chart-item"></div> 
+        <div id="userInteractChart" class="col-md-12 chart-item"></div>
+
+        {% if matomo_url and matomo_site_id and matomo_token %}
+        <div class="matomo-visits-container col-md-12">
+            <matomo-visits 
+                base-url="{{ matomo_url}}" 
+                site-id="{{ matomo_site_id }}"
+                token="{{ matomo_token }}"
+                project-url="{{ helper.mainUrl }}">
+            </matomo-visits>
+        </div>
+        {% endif %}
+
         <div id="contribsAddResolvedPie" class="col-lg-4 col-md-6 chart-item small"></div>  
         <div id="contribsEditResolvedPie" class="col-lg-4 col-md-6 chart-item small"></div>  
         <div id="collabResolveChart" class="col-lg-4 col-md-12 chart-item small"></div>  
diff --git a/templates/custom-head.html.twig b/templates/custom-head.html.twig
index 1e5e1c1f7..d52035ec4 100755
--- a/templates/custom-head.html.twig
+++ b/templates/custom-head.html.twig
@@ -38,4 +38,25 @@
 <link rel="stylesheet" href="{{ asset('fonts/fontawesome-5/css/all.css') }}" />
 {{ config.iconImport|raw }}
 
-{{ config.fontImport | raw }}
\ No newline at end of file
+{{ config.fontImport | raw }}
+
+{# Matomo user tracking, if enabled in .env file #}
+{% if matomo_url and matomo_site_id and matomo_token %}
+<script>
+    var _paq = window._paq = window._paq || [];
+    /* tracker methods like "setDocumentTitle" should be called before "trackPageView" */
+    _paq.push(["setDocumentTitle", "{{ helper.mainUrl }}"]); // we use the projectUrl as pageName in order to easily get stats for every project on saas instance
+    _paq.push(["setCookieDomain", "*.{{ base_url }}"]);
+    _paq.push(["setDomains", ["*.{{ base_url }}"]]);
+    _paq.push(['trackPageView']);
+
+    _paq.push(['enableLinkTracking']);
+    (function() {
+        var u="{{ matomo_url }}";
+        _paq.push(['setTrackerUrl', u+'matomo.php']);
+        _paq.push(['setSiteId', '{{ matomo_site_id }}']);
+        var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+        g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
+    })();
+</script>
+{% endif %}
\ No newline at end of file
diff --git a/translations/admin+intl-icu.fr.yaml b/translations/admin+intl-icu.fr.yaml
index e10130303..a1fcb5bef 100755
--- a/translations/admin+intl-icu.fr.yaml
+++ b/translations/admin+intl-icu.fr.yaml
@@ -1306,6 +1306,19 @@ js: # Below keys are available to javascript
   api:
     placeholder: "Sélectionnez une ou plusieurs catégories"      
 
+  charts:
+    period:
+      day: Jour
+      week: Semaine
+      month: Mois
+      year: Année
+    field:
+      nb_visits: Nombre de visites
+      avg_time_on_page: Temps moyen sur la page (secondes)
+    title_visitors: Statistiques des visiteurs
+    display: Afficher
+    last: Les derniers
+    reload: Recharger
   mapping_ontology: # templates/admin/core_custom/custom-fields/mapping-ontology.html.twig
     id: "Identifiant unique (dans l'ancienne base de donnée)"
     name: "Titre de la fiche"
diff --git a/yarn.lock b/yarn.lock
index 55c0fb2e8..4d8c32f64 100755
--- a/yarn.lock
+++ b/yarn.lock
@@ -4444,6 +4444,11 @@ hex-color-regex@^1.1.0:
   resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
   integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
 
+highcharts@^9.1.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-9.1.0.tgz#2cdb38e2e03530b4fde022bb05fbce5b34651e39"
+  integrity sha512-K7HUuKhEylZ1pMdzGR35kPgUmpp0MDNpaWhEMkGiC5Jfzg/endtTLHJN2lsFqEO+xoN7AykBK98XaJPEpsrLyA==
+
 hmac-drbg@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
-- 
GitLab