diff --git a/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java b/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
index 958e82179fda774cc75c16bca6a2b05d05f29103..cced372b598d7e5caaa9c88f4a7ce4540ab5e1ce 100644
--- a/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
+++ b/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
@@ -372,7 +372,9 @@ public class BaseFullWebappController extends BasicController implements DTabs,
 		// Add JS analytics code, e.g. for google analytics
 		if (analyticsModule.isAnalyticsEnabled()) {
 			AnalyticsSPI analyticsSPI = analyticsModule.getAnalyticsProvider();
-			mainVc.contextPut("analytics",analyticsSPI.analyticsInitPageJavaScript());			
+			if(analyticsSPI != null) {
+				mainVc.contextPut("analytics",analyticsSPI.analyticsInitPageJavaScript());
+			}
 		}
 		
 		// content panel
diff --git a/src/main/java/org/olat/core/commons/modules/bc/commands/CmdServeResource.java b/src/main/java/org/olat/core/commons/modules/bc/commands/CmdServeResource.java
index d5b2f8f5862ac10da82b95a190d79c4447053879..d3ee2bfb1b8f42f784da1cb6dd150db96cd0ceb7 100644
--- a/src/main/java/org/olat/core/commons/modules/bc/commands/CmdServeResource.java
+++ b/src/main/java/org/olat/core/commons/modules/bc/commands/CmdServeResource.java
@@ -26,6 +26,7 @@
 
 package org.olat.core.commons.modules.bc.commands;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -40,6 +41,8 @@ import org.olat.core.gui.media.MediaResource;
 import org.olat.core.gui.media.NotFoundMediaResource;
 import org.olat.core.gui.media.StringMediaResource;
 import org.olat.core.gui.translator.Translator;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
 import org.olat.core.logging.activity.CoreLoggingResourceable;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
 import org.olat.core.util.FileUtils;
@@ -52,6 +55,8 @@ import org.olat.core.util.vfs.callbacks.VFSSecurityCallback;
 import org.olat.core.util.vfs.meta.MetaInfo;
 
 public class CmdServeResource implements FolderCommand {
+	
+	private static final OLog log = Tracing.createLoggerFor(CmdServeResource.class);
 
 	private static final String DEFAULT_ENCODING = "iso-8859-1";
 	private static final Pattern PATTERN_ENCTYPE = Pattern.compile("<meta.*charset=([^\"]*)\"", Pattern.CASE_INSENSITIVE);
@@ -102,51 +107,54 @@ public class CmdServeResource implements FolderCommand {
 			if (path.toLowerCase().endsWith(".html") || path.toLowerCase().endsWith(".htm")) {
 				// set the http content-type and the encoding
 				// try to load in iso-8859-1
-				InputStream is = vfsfile.getInputStream();
-				if(is == null) {
-					mr = new NotFoundMediaResource();
-				} else {
-					String page = FileUtils.load(is, DEFAULT_ENCODING);
-					// search for the <meta content="text/html; charset=utf-8"
-					// http-equiv="Content-Type" /> tag
-					// if none found, assume iso-8859-1
-					String enc = DEFAULT_ENCODING;
-					boolean useLoaded = false;
-					// <meta.*charset=([^"]*)"
-					Matcher m = PATTERN_ENCTYPE.matcher(page);
-					boolean found = m.find();
-					if (found) {
-						String htmlcharset = m.group(1);
-						enc = htmlcharset;
-						if (htmlcharset.equals(DEFAULT_ENCODING)) {
-							useLoaded = true;
-						}
+				try(InputStream is = vfsfile.getInputStream()) {
+					if(is == null) {
+						mr = new NotFoundMediaResource();
 					} else {
-						useLoaded = true;
-					}
-					// set the new encoding to remember for any following .js file loads
-					g_encoding = enc;
-					if (useLoaded) {
-						StringMediaResource smr = new StringMediaResource();
-						String mimetype = forceDownload ? VFSMediaResource.MIME_TYPE_FORCE_DOWNLOAD : "text/html;charset=" + enc;
-						smr.setContentType(mimetype);
-						smr.setEncoding(enc);
-						smr.setData(page);
-						if(forceDownload) {
-							smr.setDownloadable(true, vfsfile.getName());
+						String page = FileUtils.load(is, DEFAULT_ENCODING);
+						// search for the <meta content="text/html; charset=utf-8"
+						// http-equiv="Content-Type" /> tag
+						// if none found, assume iso-8859-1
+						String enc = DEFAULT_ENCODING;
+						boolean useLoaded = false;
+						// <meta.*charset=([^"]*)"
+						Matcher m = PATTERN_ENCTYPE.matcher(page);
+						boolean found = m.find();
+						if (found) {
+							String htmlcharset = m.group(1);
+							enc = htmlcharset;
+							if (htmlcharset.equals(DEFAULT_ENCODING)) {
+								useLoaded = true;
+							}
+						} else {
+							useLoaded = true;
 						}
-						mr = smr;
-					} else {
-						// found a new charset other than iso-8859-1 -> let it load again
-						// as bytes (so we do not need to convert to string and back
-						// again)
-						VFSMediaResource vmr = new VFSMediaResource(vfsfile);
-						vmr.setEncoding(enc);
-						if(forceDownload) {
-							vmr.setDownloadable(true);
+						// set the new encoding to remember for any following .js file loads
+						g_encoding = enc;
+						if (useLoaded) {
+							StringMediaResource smr = new StringMediaResource();
+							String mimetype = forceDownload ? VFSMediaResource.MIME_TYPE_FORCE_DOWNLOAD : "text/html;charset=" + enc;
+							smr.setContentType(mimetype);
+							smr.setEncoding(enc);
+							smr.setData(page);
+							if(forceDownload) {
+								smr.setDownloadable(true, vfsfile.getName());
+							}
+							mr = smr;
+						} else {
+							// found a new charset other than iso-8859-1 -> let it load again
+							// as bytes (so we do not need to convert to string and back
+							// again)
+							VFSMediaResource vmr = new VFSMediaResource(vfsfile);
+							vmr.setEncoding(enc);
+							if(forceDownload) {
+								vmr.setDownloadable(true);
+							}
+							mr = vmr;
 						}
-						mr = vmr;
 					}
+				} catch (IOException e) {
+					log.error("", e);
 				}
 			} else if (path.endsWith(".js")) { // a javascript library
 				VFSMediaResource vmr = new VFSMediaResource(vfsfile);
diff --git a/src/main/java/org/olat/core/commons/modules/bc/components/FolderComponent.java b/src/main/java/org/olat/core/commons/modules/bc/components/FolderComponent.java
index f84e04866a90da08556ca9e521d012194b0cd2b9..697d570fe3a645294081d7573a13fd895121632d 100644
--- a/src/main/java/org/olat/core/commons/modules/bc/components/FolderComponent.java
+++ b/src/main/java/org/olat/core/commons/modules/bc/components/FolderComponent.java
@@ -34,11 +34,14 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 
+import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.controllers.linkchooser.CustomLinkTreeModel;
 import org.olat.core.commons.modules.bc.FolderLoggingAction;
 import org.olat.core.commons.modules.bc.FolderRunController;
 import org.olat.core.commons.modules.bc.commands.FolderCommandFactory;
 import org.olat.core.commons.modules.bc.comparators.LockComparator;
+import org.olat.core.commons.services.analytics.AnalyticsModule;
+import org.olat.core.commons.services.analytics.AnalyticsSPI;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.AbstractComponent;
 import org.olat.core.gui.components.ComponentRenderer;
@@ -99,6 +102,8 @@ public class FolderComponent extends AbstractComponent {
 	private VFSItemExcludePrefixFilter exclFilter;
 	private CustomLinkTreeModel customLinkTreeModel;
 	private final VFSContainer externContainerForCopy;
+	
+	private final AnalyticsSPI analyticsSpi;
 
 	/**
 	 * Wraps the folder module as a component.
@@ -138,6 +143,8 @@ public class FolderComponent extends AbstractComponent {
 		setCurrentContainerPath("/");
 		
 		dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
+		
+		analyticsSpi = CoreSpringFactory.getImpl(AnalyticsModule.class).getAnalyticsProvider();
 	}
 	
 	@Override
@@ -200,6 +207,10 @@ public class FolderComponent extends AbstractComponent {
 	public void setCanMail(boolean canMail) {
 		this.canMail = canMail;
 	}
+	
+	public AnalyticsSPI getAnalyticsSPI() {
+		return analyticsSpi;
+	}
 
 	/**
 	 * Sorts the bc folder components table
diff --git a/src/main/java/org/olat/core/commons/modules/bc/components/ListRenderer.java b/src/main/java/org/olat/core/commons/modules/bc/components/ListRenderer.java
index 55a441ce039cb9d2cc412b0f170b287813e2f07a..96690738428976cc75fada9f6af26ff82f225b86 100644
--- a/src/main/java/org/olat/core/commons/modules/bc/components/ListRenderer.java
+++ b/src/main/java/org/olat/core/commons/modules/bc/components/ListRenderer.java
@@ -267,6 +267,11 @@ public class ListRenderer {
 				} else {					
 					sb.append(" target=\"_blank\"");
 				}
+				if(fc.getAnalyticsSPI() != null) {
+					sb.append(" onclick=\"");
+					fc.getAnalyticsSPI().analyticsCountOnclickJavaScript(sb);
+					sb.append("\"");
+				}
 			}
 			sb.append(">");
 
diff --git a/src/main/java/org/olat/core/commons/services/analytics/AnalyticsSPI.java b/src/main/java/org/olat/core/commons/services/analytics/AnalyticsSPI.java
index d8d01855fc762a5cf6139104a7dcea48ecb80e11..914219f14e2799a4945fbe0c132dacee45de4f49 100644
--- a/src/main/java/org/olat/core/commons/services/analytics/AnalyticsSPI.java
+++ b/src/main/java/org/olat/core/commons/services/analytics/AnalyticsSPI.java
@@ -22,6 +22,7 @@ package org.olat.core.commons.services.analytics;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.render.StringOutput;
 
 /**
  * The AnalyticsSPI offers methods to analyse site usage and patterns based on a
@@ -84,5 +85,12 @@ public interface AnalyticsSPI {
 	 *            The location as an URL part
 	 */
 	public void analyticsCountPageJavaScript(StringBuilder sb, String title, String url);
+	
+	/**
+	 * The script can rely on the download attribute.
+	 * 
+	 * @param sb The string builder
+	 */
+	public void analyticsCountOnclickJavaScript(StringOutput sb);
 
 }
diff --git a/src/main/java/org/olat/core/commons/services/analytics/spi/GoogleAnalyticsSPI.java b/src/main/java/org/olat/core/commons/services/analytics/spi/GoogleAnalyticsSPI.java
index 268482fb3abd32d7a981475a35b27a79ede766e2..f70f86046734174b2054c0e9e6fc85b6cc3c46ba 100644
--- a/src/main/java/org/olat/core/commons/services/analytics/spi/GoogleAnalyticsSPI.java
+++ b/src/main/java/org/olat/core/commons/services/analytics/spi/GoogleAnalyticsSPI.java
@@ -25,9 +25,11 @@ import org.olat.core.configuration.AbstractSpringModule;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.render.StringOutput;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 /**
@@ -54,6 +56,7 @@ public class GoogleAnalyticsSPI extends AbstractSpringModule implements Analytic
 	 * Constructor, used by spring. Implements the module interface to store configuration
 	 * @param coordinatorManager
 	 */
+	@Autowired
 	public GoogleAnalyticsSPI(CoordinatorManager coordinatorManager) {
 		super(coordinatorManager);
 	}
@@ -140,6 +143,14 @@ public class GoogleAnalyticsSPI extends AbstractSpringModule implements Analytic
 		}
 	}
 
+	@Override
+	public void analyticsCountOnclickJavaScript(StringOutput sb) {
+		if (isValid()) {
+			// Currently only send page views with url and title. No support for tags so far
+			sb.append("ga('send', 'pageview', { page: o_info.businessPath, title: jQuery(this).attr('download') });");
+		}
+	}
+
 	/**
 	 * Helper method to build the tracker page initialization code. Does not
 	 * change often, thus store in variable for reuse
diff --git a/src/main/java/org/olat/core/commons/services/analytics/spi/MatomoSPI.java b/src/main/java/org/olat/core/commons/services/analytics/spi/MatomoSPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0cff536836862e3ac0ccac67b17865da0610f9b
--- /dev/null
+++ b/src/main/java/org/olat/core/commons/services/analytics/spi/MatomoSPI.java
@@ -0,0 +1,170 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.commons.services.analytics.spi;
+
+import org.olat.core.commons.services.analytics.AnalyticsSPI;
+import org.olat.core.commons.services.analytics.ui.MatomoConfigFormController;
+import org.olat.core.configuration.AbstractSpringModule;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.render.StringOutput;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.coordinate.CoordinatorManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 25 janv. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class MatomoSPI extends AbstractSpringModule implements AnalyticsSPI {
+	
+	private static final String TRACKER_CODE = "matomoTrackerCode";
+	private static final String SITE_ID = "matomoSiteId";
+	private static final String TRACKER_URL = "matomoTrackerurl";
+
+	private String siteId;
+	private String trackerUrl;
+	private String trackerJsCode;
+	
+	@Autowired
+	public MatomoSPI(CoordinatorManager coordinatorManager) {
+		super(coordinatorManager);
+	}
+
+	@Override
+	public void init() {
+		updateProperties();
+	}
+
+	@Override
+	protected void initFromChangedProperties() {
+		updateProperties();
+	}
+	
+	private void updateProperties() {
+		String trackerObj = getStringPropertyValue(TRACKER_CODE, true);
+		if (StringHelper.containsNonWhitespace(trackerObj)) {
+			trackerJsCode = trackerObj;
+		}
+		
+		String siteIdObj = getStringPropertyValue(SITE_ID, true);
+		if (StringHelper.containsNonWhitespace(siteIdObj)) {
+			siteId = siteIdObj;
+		}
+		
+		String trackerUrlObj = getStringPropertyValue(TRACKER_URL, true);
+		if (StringHelper.containsNonWhitespace(trackerUrlObj)) {
+			trackerUrl = trackerUrlObj;
+		}
+	}
+
+	@Override
+	public String getId() {
+		return "matomo";
+	}
+
+	@Override
+	public String getName() {
+		return "Matomo (Piwik)";
+	}
+
+	public String getTrackerJsCode() {
+		return trackerJsCode;
+	}
+
+	public void setTrackerJsCode(String trackerJsCode) {
+		this.trackerJsCode = trackerJsCode;
+		setStringProperty(TRACKER_CODE, trackerJsCode, true);
+	}
+
+	public String getSiteId() {
+		return siteId;
+	}
+
+	public void setSiteId(String siteId) {
+		this.siteId = siteId;
+		setStringProperty(SITE_ID, siteId, true);
+	}
+
+	public String getTrackerUrl() {
+		return trackerUrl;
+	}
+
+	public void setTrackerUrl(String trackerUrl) {
+		this.trackerUrl = trackerUrl;
+		setStringProperty(TRACKER_URL, trackerUrl, true);
+	}
+
+	@Override
+	public Controller createAdminController(UserRequest ureq, WindowControl wControl) {
+		return new MatomoConfigFormController(ureq, wControl);
+	}
+
+	@Override
+	public boolean isValid() {
+		return StringHelper.isLong(siteId) && StringHelper.containsNonWhitespace(trackerUrl);
+	}
+
+	@Override
+	public String analyticsInitPageJavaScript() {
+		StringBuilder sb = new StringBuilder();
+		if(isValid()) {
+			sb.append("var _paq = window._paq || [];\n")
+			  .append("_paq.push(['trackPageView']);\n")
+			  .append("_paq.push(['enableLinkTracking']);\n")
+			  .append("  (function() {\n")
+			  .append("    var u='").append(trackerUrl).append("';\n")
+			  .append("   _paq.push(['setTrackerUrl', u+'matomo.php']);\n")
+	          .append("   _paq.push(['setSiteId', '").append(siteId).append("']);\n")
+	          .append("    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];\n")
+	          .append("    g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);\n")
+	          .append("  })();\n");
+		}
+		return sb.toString();
+	}
+	
+	@Override
+	public void analyticsCountOnclickJavaScript(StringOutput sb) {
+		if(!isValid()) return;
+
+		sb.append("try{")
+		  .append("_paq.push(['setDocumentTitle', jQuery(this).attr('download')]);")
+		  .append("_paq.push(['setCustomUrl', o_info.businessPath]);")
+		  .append("_paq.push(['trackPageView']);")
+		  .append("} catch(e) { if(window.console) console.log(e) }");
+	}
+
+	@Override
+	public void analyticsCountPageJavaScript(StringBuilder sb, String title, String url) {
+		if(!isValid()) return;
+
+		sb.append("try{\n")
+		  .append("_paq.push([\"setDocumentTitle\", \"").append(Formatter.escapeDoubleQuotes(title)).append("\"]);\n")
+		  .append("_paq.push([\"setCustomUrl\", \"").append(url).append("\"]);\n")
+		  .append("_paq.push([\"trackPageView\"]);\n")
+		  .append("} catch(e) { if(window.console) console.log(e) }");
+	}
+}
diff --git a/src/main/java/org/olat/core/commons/services/analytics/ui/GoogleAnalyticsConfigFormController.java b/src/main/java/org/olat/core/commons/services/analytics/ui/GoogleAnalyticsConfigFormController.java
index 980a7378e27fc643df34a37463d0a73d2f484411..401f6989780adb2cdbbc386bb8fb8e60969c000b 100644
--- a/src/main/java/org/olat/core/commons/services/analytics/ui/GoogleAnalyticsConfigFormController.java
+++ b/src/main/java/org/olat/core/commons/services/analytics/ui/GoogleAnalyticsConfigFormController.java
@@ -75,19 +75,19 @@ public class GoogleAnalyticsConfigFormController extends FormBasicController {
 		
 		FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
 		formLayout.add(buttonsCont);
-		uifactory.addFormSubmitButton("save", buttonsCont);
 		uifactory.addFormResetButton("reset", "reset", buttonsCont);
+		uifactory.addFormSubmitButton("save", buttonsCont);
 	}
 
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) {
-		boolean allOk = true;		
+		boolean allOk = super.validateFormLogic(ureq);		
 		analyticsTrackingIdEl.clearError();
 		if(!StringHelper.containsNonWhitespace(analyticsTrackingIdEl.getValue())) {
 			analyticsTrackingIdEl.setErrorKey("form.legende.mandatory", null);
 			allOk &= false;
 		}
-		return allOk & super.validateFormLogic(ureq);
+		return allOk;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/core/commons/services/analytics/ui/MatomoConfigFormController.java b/src/main/java/org/olat/core/commons/services/analytics/ui/MatomoConfigFormController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed58c9481ba4ca431273e8bb72930134d06bbd58
--- /dev/null
+++ b/src/main/java/org/olat/core/commons/services/analytics/ui/MatomoConfigFormController.java
@@ -0,0 +1,100 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.commons.services.analytics.ui;
+
+import org.olat.core.commons.services.analytics.spi.MatomoSPI;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 25 janv. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class MatomoConfigFormController extends FormBasicController {
+	
+	private TextElement siteIdEl;
+	private TextElement trackerUrlEl;
+	
+	@Autowired
+	private MatomoSPI matomoModule;
+	
+	public MatomoConfigFormController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		setFormTitle("matomo.title");		
+		setFormDescription("matomo.desc");
+		
+		String siteId = matomoModule.getSiteId();
+		siteIdEl = uifactory.addTextElement("matomo.site.id", 6, siteId, formLayout);
+		String trackerUrl = matomoModule.getTrackerUrl();
+		trackerUrlEl = uifactory.addTextElement("matomo.tracker.url", 128, trackerUrl, flc);
+		
+		FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add(buttonsCont);
+		uifactory.addFormSubmitButton("save", buttonsCont);
+	}
+
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = super.validateFormLogic(ureq);
+		
+		siteIdEl.clearError();
+		if(!StringHelper.containsNonWhitespace(siteIdEl.getValue())) {
+			siteIdEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		} else if(!StringHelper.isLong(siteIdEl.getValue())) {
+			siteIdEl.setErrorKey("form.error.nointeger", null);
+			allOk &= false;
+		}
+		
+		trackerUrlEl.clearError();
+		if(!StringHelper.containsNonWhitespace(trackerUrlEl.getValue())) {
+			trackerUrlEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		matomoModule.setSiteId(siteIdEl.getValue());
+		matomoModule.setTrackerUrl(trackerUrlEl.getValue());
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_de.properties
index 4a37012ef8f7bb4a057922d577a9eeb597f8f28f..af50aabdcd72d43d39e21fdd23d6f3cbb6394105 100644
--- a/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_de.properties
@@ -1,12 +1,15 @@
 admin.menu.title=Analytics
 admin.menu.title.alt=Analyse des Benutzeverhaltens
-
 analytics.title=Analytics Modul
 analytics.desc=Wählen Sie einen optionalen Analytics Service aus um das Benutzerverhalten auf einem externen Analytics Server auszuwerten. 
 analytics.privacy=Wir weisen Sie darauf hin, dass Sie als Betreiberin der Plattform verpflichtet sind Ihre Benutzer auf die Verwendung eines Analytics Services hinzuweisen. 
 analytics.disabled=Analytics Module nicht verwenden
 analytics.service=Analytics Service
-
 analytics.google.title=Google Analytics Konfiguration
 analytics.google.desc=Wenn Sie ein Google Analytics Konto besitzen können Sie hier die Google Tracking ID eingeben um detaillierte statistische und real-time Auswertungen über die Nutzung Ihrer OpenOLAT Installation zu erhalten. 
 analytics.google.tracking.id=Tracking ID
+matomo.title=Matomo (Piwik)
+matomo.desc=Matomo Beschreibung
+matomo.site.id=Site ID
+matomo.tracker.code=JavaScript-Tracking-Code
+matomo.tracker.url=Matomo URL
diff --git a/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_en.properties
index 0ec1dfaaff97adcff6c7a6bf537afc0bdeea0631..5b080b39ed5d14ca6f9d37383efc2357050fd500 100644
--- a/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_en.properties
@@ -1,12 +1,15 @@
 admin.menu.title=Analytics
 admin.menu.title.alt=User behavior analytics
-
 analytics.title=Analytics module
 analytics.desc=Select an optional analytics service to analyse user behavior using an external analytics server. 
 analytics.privacy=Please note that you as the operator of the platform are legally obliged to inform your users about the usage of google analytics. 
 analytics.disabled=Disable analytics module
 analytics.service=Analytics service
-
 analytics.google.title=Google analytics configuration
 analytics.google.desc=If you have a google analytics account you can configure your google Tracking ID to perform detailed statistical and realt-time data about the usage of your OpenOLAT installation.
-analytics.google.tracking.id=Tracking ID
\ No newline at end of file
+analytics.google.tracking.id=Tracking ID
+matomo.title=Matomo (Piwik)
+matomo.desc=Matomo description
+matomo.site.id=Site ID
+matomo.tracker.code=JavaScript-Tracking-Code
+matomo.tracker.url=Matomo URL
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_fr.properties
index 1d9492b58bc2ab2ee824eddf81b6c29208b4bbad..44d06e96f3e8bf99fe8782a5066d260d4c7b610b 100644
--- a/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_fr.properties
+++ b/src/main/java/org/olat/core/commons/services/analytics/ui/_i18n/LocalStrings_fr.properties
@@ -9,3 +9,8 @@ analytics.google.tracking.id=Tracking ID
 analytics.privacy=Nous vous rendons attentif au fait qu'en tant qu'op\u00E9rateur de la plateforme vous devez vous engag\u00E9 \u00E0 informer vos utilisateurs de l'emploi de services d'analyse d'audience.
 analytics.service=Service d'analyses
 analytics.title=Module d'analyse d'audience
+matomo.title=Matomo (Piwik)
+matomo.desc=Matomo description
+matomo.site.id=Site ID
+matomo.tracker.code=JavaScript-Tracking-Code
+matomo.tracker.url=Matomo URL
diff --git a/src/main/java/org/olat/core/servlets/HeadersFilter.java b/src/main/java/org/olat/core/servlets/HeadersFilter.java
index b0df9635b57b369851ddd93a539256b6507aa371..b0ecc8c371101c8a2d2202d2951035efb003ce4c 100644
--- a/src/main/java/org/olat/core/servlets/HeadersFilter.java
+++ b/src/main/java/org/olat/core/servlets/HeadersFilter.java
@@ -20,7 +20,9 @@ import javax.servlet.http.HttpServletResponse;
 
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.analytics.AnalyticsModule;
+import org.olat.core.commons.services.analytics.AnalyticsSPI;
 import org.olat.core.commons.services.analytics.spi.GoogleAnalyticsSPI;
+import org.olat.core.commons.services.analytics.spi.MatomoSPI;
 import org.olat.core.commons.services.csp.CSPModule;
 import org.olat.core.helpers.Settings;
 import org.olat.core.logging.OLog;
@@ -167,7 +169,7 @@ public class HeadersFilter implements Filter {
 			sb.append(" ").append(securityModule.getContentSecurityPolicyConnectSrc());
 		}
 		
-		appendGoogleAnalyticsUrl(sb);
+		appendAnalyticsUrl(sb);
 		appendEdusharingUrl(sb);
 		sb.append(";");
 	}
@@ -180,7 +182,7 @@ public class HeadersFilter implements Filter {
 		}
 		
 		appendMathJaxUrl(sb);
-		appendGoogleAnalyticsUrl(sb);
+		appendAnalyticsUrl(sb);
 		appendEdusharingUrl(sb);
 		sb.append(";");
 	}
@@ -191,7 +193,7 @@ public class HeadersFilter implements Filter {
 		if(!standard && StringHelper.containsNonWhitespace(securityModule.getContentSecurityPolicyImgSrc())) {
 			sb.append(" ").append(securityModule.getContentSecurityPolicyImgSrc());
 		}
-		appendGoogleAnalyticsUrl(sb);
+		appendAnalyticsUrl(sb);
 		appendEdubaseUrl(sb);
 		appendEdusharingUrl(sb);
 		sb.append(";");
@@ -258,9 +260,17 @@ public class HeadersFilter implements Filter {
 		}
 	}
 	
-	private void appendGoogleAnalyticsUrl(StringBuilder sb) {
-		if(analyticsModule != null && analyticsModule.getAnalyticsProvider() instanceof GoogleAnalyticsSPI) {
-			sb.append(" ").append("https://www.google-analytics.com");
+	private void appendAnalyticsUrl(StringBuilder sb) {
+		if(analyticsModule != null) {
+			AnalyticsSPI spi = analyticsModule.getAnalyticsProvider();
+			if(spi instanceof GoogleAnalyticsSPI) {
+				sb.append(" ").append("https://www.google-analytics.com");
+			} else if(spi instanceof MatomoSPI) {
+				String trackerUrl = ((MatomoSPI)spi).getTrackerUrl();
+				if(StringHelper.containsNonWhitespace(trackerUrl)) {
+					sb.append(" ").append(trackerUrl);
+				}
+			}
 		}
 	}
 	
diff --git a/src/main/java/org/olat/core/util/URIHelper.java b/src/main/java/org/olat/core/util/URIHelper.java
deleted file mode 100644
index 999f4e0a1469c6adf31dd1a1abe57181688afae7..0000000000000000000000000000000000000000
--- a/src/main/java/org/olat/core/util/URIHelper.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
-* OLAT - Online Learning and Training<br>
-* http://www.olat.org
-* <p>
-* Licensed under the Apache License, Version 2.0 (the "License"); <br>
-* you may not use this file except in compliance with the License.<br>
-* You may obtain a copy of the License at
-* <p>
-* http://www.apache.org/licenses/LICENSE-2.0
-* <p>
-* Unless required by applicable law or agreed to in writing,<br>
-* software distributed under the License is distributed on an "AS IS" BASIS, <br>
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
-* See the License for the specific language governing permissions and <br>
-* limitations under the License.
-* <p>
-* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
-* University of Zurich, Switzerland.
-* <hr>
-* <a href="http://www.openolat.org">
-* OpenOLAT - Online Learning and Training</a><br>
-* This file has been modified by the OpenOLAT community. Changes are licensed
-* under the Apache 2.0 license as the original file.  
-* <p>
-*/ 
-
-package org.olat.core.util;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.StringTokenizer;
-
-/**
- * With the URIHelper it is very simple to modify URL query parameters.
- * <P>
- * Initial Date: 11.07.2006 <br>
- * 
- * @author Carsten Weisse
- */
-public class URIHelper {
-
-	private String encoding;
-
-	private URI uri;
-	private String string;
-	private String query;
-	private Map<String, String> params;
-	private boolean modified;
-
-	public URIHelper(String str) throws URISyntaxException {
-		this(str, "UTF-8");
-	}
-
-	private URIHelper(String str, String enc) throws URISyntaxException {
-		this.uri = new URI(str);
-		this.encoding = enc;
-		this.modified = true;
-		parseQuery();
-	}
-
-	/**
-	 * Remove a single parameter, if exists.
-	 */
-	public URIHelper removeParameter(String name) {
-		if (params != null && !params.isEmpty()) {
-			// don't reset the modification state, because of initial construction
-			modified |= (params.remove(name) != null);
-		}
-		return this;
-	}
-
-	/**
-	 * Return the value of a single parameter.
-	 * @return value; may be <code>null</code> if the parameter doesn't exist.
-	 */
-	public String getParameter(String name) {
-		if (params == null || params.isEmpty()) {
-			return null;
-		} else {
-			return params.get(name);
-		}
-	}
-
-	public String toString() {
-		if (modified) {
-			updateQuery();
-			updateString();
-			modified = false;
-		}
-		return string;
-	}
-
-	private void updateString() {
-		StringBuilder sb = new StringBuilder();
-		if (uri.getScheme() != null) {
-			sb.append(uri.getScheme());
-			sb.append(':');
-		}
-		if (uri.isOpaque()) {
-			sb.append(uri.getRawSchemeSpecificPart());
-		} else {
-			String host = uri.getHost();
-			if (host != null) {
-				sb.append("//");
-				if (uri.getRawUserInfo() != null) {
-					sb.append(uri.getRawUserInfo());
-					sb.append('@');
-				}
-				boolean needBrackets = ((host.indexOf(':') >= 0) && !host.startsWith("[") && !host.endsWith("]"));
-				if (needBrackets) sb.append('[');
-				sb.append(host);
-				if (needBrackets) sb.append(']');
-				if (uri.getPort() != -1) {
-					sb.append(':');
-					sb.append(uri.getPort());
-				}
-			} else if (uri.getRawAuthority() != null) {
-				sb.append("//");
-				sb.append(uri.getRawAuthority());
-			}
-		}
-		if (uri.getRawPath() != null) sb.append(uri.getRawPath());
-		if (query != null) {
-			sb.append('?');
-			sb.append(query);
-		}
-		if (uri.getRawFragment() != null) {
-			sb.append('#');
-			sb.append(uri.getRawFragment());
-		}
-		string = sb.toString();
-	}
-
-	private void parseQuery() {
-		query = uri.getRawQuery();
-		if (query == null) return;
-		// build the map
-		modified = true;
-		params = new HashMap<String,String>();
-
-		// Split off the given URL from its query string
-		StringTokenizer pairParser = new StringTokenizer(query, "&");
-
-		while (pairParser.hasMoreTokens()) {
-			try {
-				String pair = pairParser.nextToken();
-				StringTokenizer valueParser = new StringTokenizer(pair, "=");
-
-				String name = valueParser.nextToken();
-				String value = valueParser.nextToken();
-
-				params.put(decode(name), decode(value));
-			} catch (Throwable t) {
-				// If we cannot parse a parameter, ignore it
-			}
-		}
-	}
-
-	private void updateQuery() {
-		// delete query if there are no parameters 
-		if (params == null || params.isEmpty()) {
-			query = null;
-			return;
-		}
-
-		// build the query string from parameter map
-		StringBuilder sb = new StringBuilder();
-		for (Iterator<String> it = params.keySet().iterator(); it.hasNext();) {
-			String name = it.next();
-			String value = params.get(name);
-			if (value.length() == 0) continue;
-			sb.append(encode(name)).append('=').append(encode(value));
-			sb.append('&');
-		}
-		// remove the last '&'
-		sb.deleteCharAt(sb.length() - 1);
-		query = sb.toString();
-	}
-
-	private String encode(String orig) {
-		try {
-			return URLEncoder.encode(orig, encoding);
-		} catch (UnsupportedEncodingException e) {
-			// can't be but return the orig
-			return orig;
-		}
-	}
-
-	private String decode(String orig) {
-		try {
-			return URLDecoder.decode(orig, encoding);
-		} catch (UnsupportedEncodingException e) {
-			// can't be but return the orig
-			return orig;
-		}
-	}
-}