diff --git a/config/config.ini.example b/config/config.ini.example
index 31ba57a..15348e5 100644
--- a/config/config.ini.example
+++ b/config/config.ini.example
@@ -134,3 +134,9 @@ views.root = "themes/twentyfifteen"
; Framework config. No need to edit.
views.layout = "layout"
+
+; Matomo Config
+matomo.url = ""
+matomo.token = ""
+matomo.id = ""
+matomo.tracking = "disable"
\ No newline at end of file
diff --git a/lang/de_DE.ini b/lang/de_DE.ini
index d1dfd3e..61bd714 100644
--- a/lang/de_DE.ini
+++ b/lang/de_DE.ini
@@ -26,7 +26,7 @@ Filename = "Datei Name"
Follow = "Folgen"
Image_post = "Bild-Beitrag"
Image_post_comment = "Erstelle einen Beitrag mit hervorgehobenem Bild"
-Import = "Importieren"
+Import = "Import"
Link_post = "Link-Beitrag"
Link_post_comment = "Erstelle einen Beitrag mit hervorgehobenem Link"
Login = "Anmelden"
@@ -69,12 +69,12 @@ Search_for = "Suche nach"
Static_page = "Statische Seite"
Static_page_comment = "Erstelle eine statische Seite"
Static_pages = "Statische Seiten"
-Tag = "Etikett"
-Tags = "Stichworte"
+Tag = "Tag"
+Tags = "Tags"
Title = "Titel"
Uncategorized = "Sonstiges"
Uncategorized_comment = "Themen die keine Kategorie brauchen oder nicht in andere existierende Kategorien passen."
-Update = "Aktualisieren"
+Update = "Update"
Update_draft = "Draft aktualisieren"
Update_post = "Beitrag aktualisieren"
Video_post = "Video-Beitrag"
@@ -84,36 +84,36 @@ Views = "Ansichten"
Your_recent_posts = "Deine letzten Beiträge"
by = "von"
read_more = "weiterlesen"
-Dashboard="Instrumententafel"
-Posts_list="Beitragsliste"
-Posts_draft="Beiträge Entwurf"
-Menus="Menü-Editor"
-Settings="die Einstellungen"
-Tools="Werkzeuge"
-Check_update="Aktualisierung überprüfen"
-Import_RSS="RSS importieren"
-User="Benutzer"
-Proudly_powered_by="Stolz angetrieben von"
-Home="Zuhause"
-Type_to_search="Tippe um zu suchen"
-Admin_panel_style_based_on="Admin-Panel-Stil basierend auf"
-Sign_in_to_start_your_session="Melden Sie sich an, um Ihre Sitzung zu starten"
-Back_to="Zurück zu"
-Login="Anmeldung"
-User="Benutzer"
-Password="Passwort"
-Comma_separated_values="Komma-getrennte Werte"
-If_leave_empty_we_will_excerpt_it_from_the_content_below="Wenn Sie das Feld leer lassen, wird es aus dem folgenden Inhalt extrahiert"
+Dashboard="Dashboard"
+Posts_list="Posts list"
+Posts_draft="Posts draft"
+Menus="Menu Editor"
+Settings="Settings"
+Tools="Tools"
+Check_update="Check update"
+Import_RSS="Import RSS"
+User="User"
+Proudly_powered_by="Proudly powered by"
+Home="Home"
+Type_to_search="Type to search"
+Admin_panel_style_based_on="Admin panel style based on"
+Sign_in_to_start_your_session="Sign in to start your session"
+Back_to="Back to"
+Login="Login"
+User="User"
+Password="Password"
+Comma_separated_values="Comma separated values"
+If_leave_empty_we_will_excerpt_it_from_the_content_below="If leave empty we will excerpt it from the content below"
optional="optional"
-If_the_url_leave_empty_we_will_use_the_post_title="Wenn die URL leer bleibt, verwenden wir den Post-Titel"
-If_the_url_leave_empty_we_will_use_the_page_title="Wenn die URL leer bleibt, verwenden wir den Seitentitel"
-Only="Nur"
-Featured_Audio="Empfohlenes Audio"
-Featured_Video="Empfohlenes Video"
-Featured_Image="Ausgewähltes Bild"
-Featured_Quote="Ausgewähltes Zitat"
-Featured_Link="Ausgewählter Link"
-Content="Inhalt"
+If_the_url_leave_empty_we_will_use_the_post_title="If the url leave empty we will use the post title"
+If_the_url_leave_empty_we_will_use_the_page_title="If the url leave empty we will use the page title"
+Only="Only"
+Featured_Audio="Featured Audio"
+Featured_Video="Featured Video"
+Featured_Image="Featured Image"
+Featured_Quote="Featured Quote"
+Featured_Link="Featured Link"
+Content="Content"
Preview="Preview"
Enter_image_URL="Enter image URL"
Upload="Upload"
@@ -122,71 +122,71 @@ This_page_doesnt_exist="This page doesn't exist !"
Would_you_like_to_try_our="Would you like to try our "
homepage="homepage"
instead="instead"
-Your_backups="Ihre Backups"
-Create_backup="Ein Backup erstellen"
-All_cache_has_been_deleted="Der gesamte Cache wurde gelöscht!"
-Edit_category="Kategorie bearbeiten"
+Your_backups="Your backups"
+Create_backup="Create backup"
+All_cache_has_been_deleted="All cache has been deleted !"
+Edit_category="Edit category"
Date="Date"
-Time="Zeit"
-No_available_backup="Derzeit ist kein Backup verfügbar."
-You_dont_have_permission_to_access_this_page="Sie haben keine Berechtigung, auf diese Seite zuzugreifen."
-Save_category="Kategorie speichern"
-Import_RSS_Feed_2.0="RSS-Feed 2.0 importieren"
-By_using_this_importer_you_are_agree_if_the_feed_is_yours_or_at_least_you_have_the_authority_to_publish_it="Durch die Verwendung dieses Importeurs stimmen Sie zu, ob der Feed Ihnen gehört oder zumindest die Berechtigung hat, ihn zu veröffentlichen."
+Time="Time"
+No_available_backup="No available backup at this time."
+You_dont_have_permission_to_access_this_page="You don't have permission to access this page"
+Save_category="Save category"
+Import_RSS_Feed_2.0="Import RSS Feed 2.0"
+By_using_this_importer_you_are_agree_if_the_feed_is_yours_or_at_least_you_have_the_authority_to_publish_it="By using this importer you are agree if the feed is yours or at least you have the authority to publish it."
Feed_Url="Feed URL"
-Add_source_link_optional="Quelllink hinzufügen (optional)"
-Import_Feed="Importieren Feed starten"
-At_the_moment_you_are_using_auto_generated_menu="Im Moment verwenden Sie ein automatisch generiertes Menü. "
-Add_menu="Menü hinzufügen"
+Add_source_link_optional="Add source link (optional)"
+Import_Feed="Start Import Feed"
+At_the_moment_you_are_using_auto_generated_menu="At the moment you are using auto generated menu."
+Add_menu="Add menu"
Name="Name"
-Link_name="Linkname"
+Link_name="Link name"
Slug="Slug"
-item_slug="Link URL einfügen"
-CSS_Class_Optional="CSS-Klasse (optional) "
-item_class="CSS Klasse einfügen"
-Add_link="Link hinzufügen"
-Save_Edit="Speichern Bearbeiten"
-Save_Menu="Menü speichern"
-Add_new_post="Neuen Beitrag hinzufügen"
-Add_new_page="Neue Seite hinzufügen"
-Update_Available="Update verfügbar"
-Congrats_You_have_the_latest_version_of_HTMLy="Herzlichen Glückwunsch! Sie haben die neueste Version von HTMLy."
-Update_to="Aktualisieren zu"
-now="jetzt"
-Performance_Settings="Leistungseinstellungen"
-General="Allgemeines"
-Reading="Lesen"
+item_slug="Insert Link URL"
+CSS_Class_Optional="CSS Class (optional)"
+item_class="Insert CSS class"
+Add_link="Add link"
+Save_Edit="Save Edit"
+Save_Menu="Save menu"
+Add_new_post="Add new post"
+Add_new_page="Add new page"
+Update_Available="Update Available"
+Congrats_You_have_the_latest_version_of_HTMLy="Congrats! You have the latest version of HTMLy."
+Update_to="Update to"
+now="now"
+Performance_Settings="Performance Settings"
+General="General"
+Reading="Reading"
Widget="Widget"
Metatags="Metatags"
Performance="Performance"
-Custom="Benutzerdefiniert"
-General_Settings="Allgemeine Einstellungen"
-Reading_Settings="Leseeinstellungen"
-Widget_Settings="Widget Einstellungen"
-Metatags_Settings="Metatags Einstellungen"
-Custom_Settings="Benutzerdefinierte Einstellungen"
-Address_URL="Adresse (URL)"
-Blog_Title="Blog Titel"
-Blog_Title_Placeholder="Mein HTMLy Blog "
+Custom="Custom"
+General_Settings="General Settings"
+Reading_Settings="Reading Settings"
+Widget_Settings="Widget Settings"
+Metatags_Settings="Metatags Settings"
+Custom_Settings="Custom Settings"
+Address_URL="Address (URL)"
+Blog_Title="Blog title"
+Blog_Title_Placeholder="My HTMLy Blog"
Tagline="Tagline"
-Tagline_Placeholder="Datenbanklose PHP Blogging Plattform"
-Tagline_description="Erklären Sie in wenigen Worten, worum es in diesem Blog geht."
-Blog_Description="Erzählen Sie uns in einem Absatz mehr über Ihr Blog."
-Language="Systemsprache"
-Timezone="Zeitzone"
-Date_Format="Datumsformat"
-Blog_Theme="Blog Thema"
-Copyright_Line="Copyright Zeile"
-Copyright_Line_Placeholder="(c) Dein Name."
-Save_Config="Konfiguration speichern"
-Front_page_displays="Titelseite wird angezeigt"
-Your_latest_blog_posts="Deine neuesten Blog-Beiträge"
-Enable_blog_URL="URL aktivieren / bloggen"
-Enable="Aktivieren"
-Disable="Deaktivieren"
-Posts_in_front_page_show_at_most="Beiträge auf der Titelseite werden höchstens angezeigt"
-Blog_posts_displayed_as="Blog-Beiträge angezeigt als"
-Full_post="Vollständiger Beitrag"
+Tagline_Placeholder="Databaseless PHP Blogging Platform"
+Tagline_description="In a few words, explain what this blog is about."
+Blog_Description="In one paragraph, tell us more about your blog."
+Language="System Language"
+Timezone="Timezone"
+Date_Format="Date Format"
+Blog_Theme="Blog Theme"
+Copyright_Line="Copyright line"
+Copyright_Line_Placeholder="(c) Your name."
+Save_Config="Save config"
+Front_page_displays="Front page displays"
+Your_latest_blog_posts="Your latest blog posts"
+Enable_blog_URL="Enable /blog URL"
+Enable="Enable"
+Disable="Disable"
+Posts_in_front_page_show_at_most="Posts in front page show at most"
+Blog_posts_displayed_as="Blog posts displayed as"
+Full_post="Full post"
Summary="Summary"
Summary_character="Summary character"
Read_more_text="Read more text"
@@ -248,3 +248,8 @@ Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Seiten-ID"
+Matomo_Tracking="Tracking Methode"
\ No newline at end of file
diff --git a/lang/en_US.ini b/lang/en_US.ini
index e377334..2a9948d 100644
--- a/lang/en_US.ini
+++ b/lang/en_US.ini
@@ -247,4 +247,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/es_ES.ini b/lang/es_ES.ini
index 77e7a7c..1a7d554 100644
--- a/lang/es_ES.ini
+++ b/lang/es_ES.ini
@@ -246,4 +246,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/fr_FR.ini b/lang/fr_FR.ini
index 41a5c9e..140bc07 100644
--- a/lang/fr_FR.ini
+++ b/lang/fr_FR.ini
@@ -248,3 +248,8 @@ Github_pre_release="Pré-release Github"
Pre_release="Pré-release"
Yes_Im_in="Oui, je suis prêt"
Nope="Non !"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/hu_HU.ini b/lang/hu_HU.ini
index af86782..52c80da 100644
--- a/lang/hu_HU.ini
+++ b/lang/hu_HU.ini
@@ -247,4 +247,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/id_ID.ini b/lang/id_ID.ini
index fed3640..0160c2d 100644
--- a/lang/id_ID.ini
+++ b/lang/id_ID.ini
@@ -248,3 +248,8 @@ Github_pre_release="Pra-rilis Github"
Pre_release="Pra-rilis"
Yes_Im_in="Ya, saya ikut"
Nope="Nggak"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/it_IT.ini b/lang/it_IT.ini
index 5e6275a..a4dd987 100644
--- a/lang/it_IT.ini
+++ b/lang/it_IT.ini
@@ -247,4 +247,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/ko_KO.ini b/lang/ko_KO.ini
index feacf4c..abde346 100644
--- a/lang/ko_KO.ini
+++ b/lang/ko_KO.ini
@@ -248,3 +248,8 @@ Github_pre_release = "Github에서의 시험판"
Pre_release = "사전 출시"
Yes_Im_in = "그래, 난에있어"
Nope = "아니"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/ms_MY.ini b/lang/ms_MY.ini
index 5cc9f0d..3cea591 100644
--- a/lang/ms_MY.ini
+++ b/lang/ms_MY.ini
@@ -248,3 +248,8 @@ Github_pre_release = "Github pra keluaran"
Pre_release = "Pra-release"
Yes_Im_in = "Ya saya setuju"
Nope = "Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/nl_NL.ini b/lang/nl_NL.ini
index 996dbd1..9dc2de3 100644
--- a/lang/nl_NL.ini
+++ b/lang/nl_NL.ini
@@ -207,7 +207,7 @@ post_your_post_slug="/bericht/uw-bericht-slug"
Meta_description_character="Meta beschrijving"
Breadcrumb_home_text="Breadcrumb home tekst"
Sitemap="Sitemap"
-Valid_values_range_from_0_to_1.0._See="Geldige waarden variren van 0,0 tot 1,0. Zie"
+Valid_values_range_from_0_to_1.0._See="Geldige waarden variëren van 0,0 tot 1,0. Zie"
hint_Use_CtrlCMDF_to_search_for_your_config_key_or_value="hint: Use Ctrl/CMD + F to search for your config key or value."
pro_tips_You_can_creating_custom_config_key_and_print_out_your_config_key_value_anywhere_in_your_template="pro tips: You can creating custom config key and print out your config key value anywhere in your template."
your_key="your.key"
@@ -247,4 +247,9 @@ Page_generation_time="Generatietijd pagina"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Ja, ik doe mee"
-Nope="Nietes"
\ No newline at end of file
+Nope="Nietes"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/pl_PL.ini b/lang/pl_PL.ini
index 1791c0a..9189360 100644
--- a/lang/pl_PL.ini
+++ b/lang/pl_PL.ini
@@ -248,4 +248,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/pt_BR.ini b/lang/pt_BR.ini
index 91d7a8d..1e0ee3c 100644
--- a/lang/pt_BR.ini
+++ b/lang/pt_BR.ini
@@ -247,4 +247,9 @@ Page_generation_time="Tempo de geração da página"
Github_pre_release="Pré-lançamento do Github"
Pre_release="Pré-lançamento"
Yes_Im_in="Sim, estou dentro"
-Nope="Não"
\ No newline at end of file
+Nope="Não"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/ru_RU.ini b/lang/ru_RU.ini
index 9569d35..26beca5 100644
--- a/lang/ru_RU.ini
+++ b/lang/ru_RU.ini
@@ -248,4 +248,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/sv_SE.ini b/lang/sv_SE.ini
index b189b31..9fc9625 100644
--- a/lang/sv_SE.ini
+++ b/lang/sv_SE.ini
@@ -265,4 +265,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/tr_TR.ini b/lang/tr_TR.ini
index 618bca3..f87a1a8 100644
--- a/lang/tr_TR.ini
+++ b/lang/tr_TR.ini
@@ -247,4 +247,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/lang/zh_TW.ini b/lang/zh_TW.ini
index 31705f7..e35b63a 100644
--- a/lang/zh_TW.ini
+++ b/lang/zh_TW.ini
@@ -247,4 +247,9 @@ Page_generation_time="Page generation time"
Github_pre_release="Github pre-release"
Pre_release="Pre-release"
Yes_Im_in="Yes I'm in"
-Nope="Nope"
\ No newline at end of file
+Nope="Nope"
+Matomo="Matomo"
+Matomo_URL="Server"
+Matomo_Token="Token"
+Matomo_ID="Page ID"
+Matomo_Tracking="Tracking Method"
\ No newline at end of file
diff --git a/system/admin/views/config-widget.html.php b/system/admin/views/config-widget.html.php
index 8365ea2..72256f0 100644
--- a/system/admin/views/config-widget.html.php
+++ b/system/admin/views/config-widget.html.php
@@ -150,6 +150,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ checked>
+
+
+
+ checked>
+
+
+
+ checked>
+
+
+
+
+
+
@@ -170,4 +217,5 @@
+
diff --git a/system/includes/functions.php b/system/includes/functions.php
index 26f3613..dc56c95 100644
--- a/system/includes/functions.php
+++ b/system/includes/functions.php
@@ -5,6 +5,7 @@ use \Michelf\MarkdownExtra;
use \Suin\RSSWriter\Feed;
use \Suin\RSSWriter\Channel;
use \Suin\RSSWriter\Item;
+use function Composer\Autoload\includeFile;
// Get blog post path. Unsorted. Mostly used on widget.
function get_post_unsorted()
@@ -2021,6 +2022,119 @@ EOF;
}
}
+// Matomo
+function matomo($title)
+{
+ $matomoURL = config('matomo.url');
+ $matomoToken = config('matomo.token');
+ $matomoID = config('matomo.id');
+ $matomoTracking = config('matomo.tracking');
+
+ if($matomoTracking == 'disable')
+ {
+ return;
+ }
+
+ if(empty($matomoURL) || empty($matomoTracking) || empty($matomoID))
+ {
+ error_log("Please set all Matomo configuration values", 0);
+ return;
+ }
+
+
+ if($matomoTracking == 'javascript')
+ {
+ $jsURL = $matomoURL;
+ $imageURL = $matomoURL;
+
+ // remove http | https
+ if (startsWith($matomoURL, 'https'))
+ {
+ $jsURL = replace_first_str('https', '', $matomoURL);
+ }
+ else if (startsWith($matomoURL, 'http'))
+ {
+ $jsURL = replace_first_str('http', '', $matomoURL);
+ }
+
+ if(!endsWith($matomoURL, '/'))
+ {
+ $jsURL = $jsURL . '/';
+ $imageURL = $imageURL . "/";
+ }
+
+ $script = <<
+ var _paq = window._paq || [];
+ _paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
+ _paq.push(['trackPageView']);
+ _paq.push(['enableLinkTracking']);
+ (function() {
+ var u="{$jsURL}";
+ _paq.push(['setTrackerUrl', u+'matomo.php']);
+ _paq.push(['setSiteId', '{$matomoID}']);
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+ g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
+ })();
+
+
+EOF;
+
+ return $script;
+ }
+ else if($matomoTracking == 'php')
+ {
+ if(empty($matomoToken))
+ {
+ error_log("Please set Matomo Token for PHP Tracking Method", 0);
+ return;
+ }
+
+ $jsURL = 'system/plugins/matomo/MatomoTracker.php';
+ require_once ($jsURL);
+
+ MatomoTracker::$URL = $matomoURL;
+
+ // Matomo object
+ $matomoTracker = new MatomoTracker((int) $matomoID, $matomoURL);
+
+ // do not wait
+ $matomoTracker->setRequestTimeout(1);
+
+ // Set authentication token
+ $matomoTracker->setTokenAuth($matomoToken);
+
+ // Track page view
+ $matomoTracker->doTrackPageView($title);
+ }
+}
+
+
+function startsWith($haystack, $needle, $case = true)
+{
+ if ($case)
+ {
+ return (strcmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
+ }
+
+ return (strcasecmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
+}
+
+function endsWith($haystack, $needle, $case = true)
+{
+ if ($case)
+ {
+ return (strcmp(substr($haystack, strlen($haystack) - strlen($needle)), $needle) === 0);
+ }
+
+ return (strcasecmp(substr($haystack, strlen($haystack) - strlen($needle)), $needle) === 0);
+}
+
+function replace_first_str($search_str, $replacement_str, $src_str)
+{
+ return (false !== ($pos = strpos($src_str, $search_str))) ? substr_replace($src_str, $replacement_str, $pos, strlen($search_str)) : $src_str;
+}
+
function slashUrl($url) {
return rtrim($url, '/') . '/';
}
diff --git a/system/plugins/matomo/MatomoTracker.php b/system/plugins/matomo/MatomoTracker.php
new file mode 100644
index 0000000..5894692
--- /dev/null
+++ b/system/plugins/matomo/MatomoTracker.php
@@ -0,0 +1,2201 @@
+ecommerceItems = array();
+ $this->attributionInfo = false;
+ $this->eventCustomVar = false;
+ $this->forcedDatetime = false;
+ $this->forcedNewVisit = false;
+ $this->networkTime = false;
+ $this->serverTime = false;
+ $this->transferTime = false;
+ $this->domProcessingTime = false;
+ $this->domCompletionTime = false;
+ $this->onLoadTime = false;
+ $this->pageCustomVar = false;
+ $this->ecommerceView = array();
+ $this->customParameters = array();
+ $this->customDimensions = array();
+ $this->customData = false;
+ $this->hasCookies = false;
+ $this->token_auth = false;
+ $this->userAgent = false;
+ $this->country = false;
+ $this->region = false;
+ $this->city = false;
+ $this->lat = false;
+ $this->long = false;
+ $this->width = false;
+ $this->height = false;
+ $this->plugins = false;
+ $this->localHour = false;
+ $this->localMinute = false;
+ $this->localSecond = false;
+ $this->idPageview = false;
+
+ $this->idSite = $idSite;
+ $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false;
+ $this->pageCharset = self::DEFAULT_CHARSET_PARAMETER_VALUES;
+ $this->pageUrl = self::getCurrentUrl();
+ $this->ip = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false;
+ $this->acceptLanguage = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : false;
+ $this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : false;
+ if (!empty($apiUrl)) {
+ self::$URL = $apiUrl;
+ }
+
+ // Life of the visitor cookie (in sec)
+ $this->configVisitorCookieTimeout = 33955200; // 13 months (365 + 28 days)
+ // Life of the session cookie (in sec)
+ $this->configSessionCookieTimeout = 1800; // 30 minutes
+ // Life of the session cookie (in sec)
+ $this->configReferralCookieTimeout = 15768000; // 6 months
+
+ // Visitor Ids in order
+ $this->userId = false;
+ $this->forcedVisitorId = false;
+ $this->cookieVisitorId = false;
+ $this->randomVisitorId = false;
+
+ $this->setNewVisitorId();
+
+ $this->configCookiesDisabled = false;
+ $this->configCookiePath = self::DEFAULT_COOKIE_PATH;
+ $this->configCookieDomain = '';
+ $this->configCookieSameSite = '';
+ $this->configCookieSecure = false;
+ $this->configCookieHTTPOnly = false;
+
+ $this->currentTs = time();
+ $this->createTs = $this->currentTs;
+
+ // Allow debug while blocking the request
+ $this->requestTimeout = 600;
+ $this->doBulkRequests = false;
+ $this->storedTrackingActions = array();
+
+ $this->sendImageResponse = true;
+
+ $this->visitorCustomVar = $this->getCustomVariablesFromCookie();
+
+ $this->outgoingTrackerCookies = array();
+ $this->incomingTrackerCookies = array();
+ }
+
+ /**
+ * By default, Matomo expects utf-8 encoded values, for example
+ * for the page URL parameter values, Page Title, etc.
+ * It is recommended to only send UTF-8 data to Matomo.
+ * If required though, you can also specify another charset using this function.
+ *
+ * @param string $charset
+ * @return $this
+ */
+ public function setPageCharset($charset = '')
+ {
+ $this->pageCharset = $charset;
+ return $this;
+ }
+
+ /**
+ * Sets the current URL being tracked
+ *
+ * @param string $url Raw URL (not URL encoded)
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->pageUrl = $url;
+ return $this;
+ }
+
+ /**
+ * Sets the URL referrer used to track Referrers details for new visits.
+ *
+ * @param string $url Raw URL (not URL encoded)
+ * @return $this
+ */
+ public function setUrlReferrer($url)
+ {
+ $this->urlReferrer = $url;
+ return $this;
+ }
+
+ /**
+ * This method is deprecated and does nothing. It used to set the time that it took to generate the document on the server side.
+ *
+ * @param int $timeMs Generation time in ms
+ * @return $this
+ *
+ * @deprecated this metric is deprecated please use performance timings instead
+ * @see setPerformanceTimings
+ */
+ public function setGenerationTime($timeMs)
+ {
+ return $this;
+ }
+
+ /**
+ * Sets timings for various browser performance metrics.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
+ *
+ * @param null|int $network Network time in ms (connectEnd fetchStart)
+ * @param null|int $server Server time in ms (responseStart requestStart)
+ * @param null|int $transfer Transfer time in ms (responseEnd responseStart)
+ * @param null|int $domProcessing DOM Processing to Interactive time in ms (domInteractive domLoading)
+ * @param null|int $domCompletion DOM Interactive to Complete time in ms (domComplete domInteractive)
+ * @param null|int $onload Onload time in ms (loadEventEnd loadEventStart)
+ * @return $this
+ */
+ public function setPerformanceTimings($network = null, $server = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null)
+ {
+ $this->networkTime = $network;
+ $this->serverTime = $server;
+ $this->transferTime = $transfer;
+ $this->domProcessingTime = $domProcessing;
+ $this->domCompletionTime = $domCompletion;
+ $this->onLoadTime = $onload;
+ return $this;
+ }
+
+ /**
+ * Clear / reset all previously set performance metrics.
+ */
+ public function clearPerformanceTimings()
+ {
+ $this->networkTime = false;
+ $this->serverTime = false;
+ $this->transferTime = false;
+ $this->domProcessingTime = false;
+ $this->domCompletionTime = false;
+ $this->onLoadTime = false;
+ }
+
+ /**
+ * @deprecated
+ * @ignore
+ */
+ public function setUrlReferer($url)
+ {
+ $this->setUrlReferrer($url);
+ return $this;
+ }
+
+ /**
+ * Sets the attribution information to the visit, so that subsequent Goal conversions are
+ * properly attributed to the right Referrer URL, timestamp, Campaign Name & Keyword.
+ *
+ * This must be a JSON encoded string that would typically be fetched from the JS API:
+ * matomoTracker.getAttributionInfo() and that you have JSON encoded via JSON2.stringify()
+ *
+ * If you call enableCookies() then these referral attribution values will be set
+ * to the 'ref' first party cookie storing referral information.
+ *
+ * @param string $jsonEncoded JSON encoded array containing Attribution info
+ * @return $this
+ * @throws Exception
+ * @see function getAttributionInfo() in https://github.com/matomo-org/matomo/blob/master/js/matomo.js
+ */
+ public function setAttributionInfo($jsonEncoded)
+ {
+ $decoded = json_decode($jsonEncoded, $assoc = true);
+ if (!is_array($decoded)) {
+ throw new Exception("setAttributionInfo() is expecting a JSON encoded string, $jsonEncoded given");
+ }
+ $this->attributionInfo = $decoded;
+ return $this;
+ }
+
+ /**
+ * Sets Visit Custom Variable.
+ * See https://matomo.org/docs/custom-variables/
+ *
+ * @param int $id Custom variable slot ID from 1-5
+ * @param string $name Custom variable name
+ * @param string $value Custom variable value
+ * @param string $scope Custom variable scope. Possible values: visit, page, event
+ * @return $this
+ * @throws Exception
+ */
+ public function setCustomVariable($id, $name, $value, $scope = 'visit')
+ {
+ if (!is_int($id)) {
+ throw new Exception("Parameter id to setCustomVariable should be an integer");
+ }
+ if ($scope == 'page') {
+ $this->pageCustomVar[$id] = array($name, $value);
+ } elseif ($scope == 'event') {
+ $this->eventCustomVar[$id] = array($name, $value);
+ } elseif ($scope == 'visit') {
+ $this->visitorCustomVar[$id] = array($name, $value);
+ } else {
+ throw new Exception("Invalid 'scope' parameter value");
+ }
+ return $this;
+ }
+
+ /**
+ * Returns the currently assigned Custom Variable.
+ *
+ * If scope is 'visit', it will attempt to read the value set in the first party cookie created by Matomo Tracker
+ * ($_COOKIE array).
+ *
+ * @param int $id Custom Variable integer index to fetch from cookie. Should be a value from 1 to 5
+ * @param string $scope Custom variable scope. Possible values: visit, page, event
+ *
+ * @throws Exception
+ * @return mixed An array with this format: array( 0 => CustomVariableName, 1 => CustomVariableValue ) or false
+ * @see matomo.js getCustomVariable()
+ */
+ public function getCustomVariable($id, $scope = 'visit')
+ {
+ if ($scope == 'page') {
+ return isset($this->pageCustomVar[$id]) ? $this->pageCustomVar[$id] : false;
+ } elseif ($scope == 'event') {
+ return isset($this->eventCustomVar[$id]) ? $this->eventCustomVar[$id] : false;
+ } else {
+ if ($scope != 'visit') {
+ throw new Exception("Invalid 'scope' parameter value");
+ }
+ }
+ if (!empty($this->visitorCustomVar[$id])) {
+ return $this->visitorCustomVar[$id];
+ }
+ $cookieDecoded = $this->getCustomVariablesFromCookie();
+ if (!is_int($id)) {
+ throw new Exception("Parameter to getCustomVariable should be an integer");
+ }
+ if (!is_array($cookieDecoded)
+ || !isset($cookieDecoded[$id])
+ || !is_array($cookieDecoded[$id])
+ || count($cookieDecoded[$id]) != 2
+ ) {
+ return false;
+ }
+
+ return $cookieDecoded[$id];
+ }
+
+ /**
+ * Clears any Custom Variable that may be have been set.
+ *
+ * This can be useful when you have enabled bulk requests,
+ * and you wish to clear Custom Variables of 'visit' scope.
+ */
+ public function clearCustomVariables()
+ {
+ $this->visitorCustomVar = array();
+ $this->pageCustomVar = array();
+ $this->eventCustomVar = array();
+ }
+
+ /**
+ * Sets a specific custom dimension
+ *
+ * @param int $id id of custom dimension
+ * @param string $value value for custom dimension
+ * @return $this
+ */
+ public function setCustomDimension($id, $value)
+ {
+ $this->customDimensions['dimension'.(int)$id] = $value;
+ return $this;
+ }
+
+ /**
+ * Clears all previously set custom dimensions
+ */
+ public function clearCustomDimensions()
+ {
+ $this->customDimensions = [];
+ }
+
+ /**
+ * Returns the value of the custom dimension with the given id
+ *
+ * @param int $id id of custom dimension
+ * @return string|null
+ */
+ public function getCustomDimension($id)
+ {
+ return $this->customDimensions['dimension'.(int)$id] ?? null;
+ }
+
+ /**
+ * Sets a custom tracking parameter. This is useful if you need to send any tracking parameters for a 3rd party
+ * plugin that is not shipped with Matomo itself. Please note that custom parameters are cleared after each
+ * tracking request.
+ *
+ * @param string $trackingApiParameter The name of the tracking API parameter, eg 'bw_bytes'
+ * @param string $value Tracking parameter value that shall be sent for this tracking parameter.
+ * @return $this
+ * @throws Exception
+ */
+ public function setCustomTrackingParameter($trackingApiParameter, $value)
+ {
+ $matches = [];
+
+ if (preg_match('/^dimension([0-9]+)$/', $trackingApiParameter, $matches)) {
+ $this->setCustomDimension($matches[1], $value);
+ return $this;
+ }
+
+ $this->customParameters[$trackingApiParameter] = $value;
+ return $this;
+ }
+
+ /**
+ * Clear / reset all previously set custom tracking parameters.
+ */
+ public function clearCustomTrackingParameters()
+ {
+ $this->customParameters = array();
+ }
+
+ /**
+ * Sets the current visitor ID to a random new one.
+ * @return $this
+ */
+ public function setNewVisitorId()
+ {
+ $this->randomVisitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID);
+ $this->forcedVisitorId = false;
+ $this->cookieVisitorId = false;
+ return $this;
+ }
+
+ /**
+ * Sets the current site ID.
+ *
+ * @param int $idSite
+ * @return $this
+ */
+ public function setIdSite($idSite)
+ {
+ $this->idSite = $idSite;
+ return $this;
+ }
+
+ /**
+ * Sets the Browser language. Used to guess visitor countries when GeoIP is not enabled
+ *
+ * @param string $acceptLanguage For example "fr-fr"
+ * @return $this
+ */
+ public function setBrowserLanguage($acceptLanguage)
+ {
+ $this->acceptLanguage = $acceptLanguage;
+ return $this;
+ }
+
+ /**
+ * Sets the user agent, used to detect OS and browser.
+ * If this function is not called, the User Agent will default to the current user agent.
+ *
+ * @param string $userAgent
+ * @return $this
+ */
+ public function setUserAgent($userAgent)
+ {
+ $this->userAgent = $userAgent;
+ return $this;
+ }
+
+ /**
+ * Sets the country of the visitor. If not used, Matomo will try to find the country
+ * using either the visitor's IP address or language.
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth().
+ * @param string $country
+ * @return $this
+ */
+ public function setCountry($country)
+ {
+ $this->country = $country;
+ return $this;
+ }
+
+ /**
+ * Sets the region of the visitor. If not used, Matomo may try to find the region
+ * using the visitor's IP address (if configured to do so).
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth().
+ * @param string $region
+ * @return $this
+ */
+ public function setRegion($region)
+ {
+ $this->region = $region;
+ return $this;
+ }
+
+ /**
+ * Sets the city of the visitor. If not used, Matomo may try to find the city
+ * using the visitor's IP address (if configured to do so).
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth().
+ * @param string $city
+ * @return $this
+ */
+ public function setCity($city)
+ {
+ $this->city = $city;
+ return $this;
+ }
+
+ /**
+ * Sets the latitude of the visitor. If not used, Matomo may try to find the visitor's
+ * latitude using the visitor's IP address (if configured to do so).
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth().
+ * @param float $lat
+ * @return $this
+ */
+ public function setLatitude($lat)
+ {
+ $this->lat = $lat;
+ return $this;
+ }
+
+ /**
+ * Sets the longitude of the visitor. If not used, Matomo may try to find the visitor's
+ * longitude using the visitor's IP address (if configured to do so).
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth().
+ * @param float $long
+ * @return $this
+ */
+ public function setLongitude($long)
+ {
+ $this->long = $long;
+ return $this;
+ }
+
+ /**
+ * Enables the bulk request feature. When used, each tracking action is stored until the
+ * doBulkTrack method is called. This method will send all tracking data at once.
+ *
+ */
+ public function enableBulkTracking()
+ {
+ $this->doBulkRequests = true;
+ }
+
+ /**
+ * Enable Cookie Creation - this will cause a first party VisitorId cookie to be set when the VisitorId is set or reset
+ *
+ * @param string $domain (optional) Set first-party cookie domain.
+ * Accepted values: example.com, *.example.com (same as .example.com) or subdomain.example.com
+ * @param string $path (optional) Set first-party cookie path
+ * @param bool $secure (optional) Set secure flag for cookies
+ * @param bool $httpOnly (optional) Set HTTPOnly flag for cookies
+ * @param string $sameSite (optional) Set SameSite flag for cookies
+ */
+ public function enableCookies($domain = '', $path = '/', $secure = false, $httpOnly = false, $sameSite = '')
+ {
+ $this->configCookiesDisabled = false;
+ $this->configCookieDomain = self::domainFixup($domain);
+ $this->configCookiePath = $path;
+ $this->configCookieSecure = $secure;
+ $this->configCookieHTTPOnly = $httpOnly;
+ $this->configCookieSameSite = $sameSite;
+ }
+
+ /**
+ * If image response is disabled Matomo will respond with a HTTP 204 header instead of responding with a gif.
+ */
+ public function disableSendImageResponse()
+ {
+ $this->sendImageResponse = false;
+ }
+
+ /**
+ * Fix-up domain
+ */
+ protected static function domainFixup($domain)
+ {
+ if (strlen($domain) > 0) {
+ $dl = strlen($domain) - 1;
+ // remove trailing '.'
+ if ($domain[$dl] === '.') {
+ $domain = substr($domain, 0, $dl);
+ }
+ // remove leading '*'
+ if (substr($domain, 0, 2) === '*.') {
+ $domain = substr($domain, 1);
+ }
+ }
+
+ return $domain;
+ }
+
+ /**
+ * Get cookie name with prefix and domain hash
+ * @param string $cookieName
+ * @return string
+ */
+ protected function getCookieName($cookieName)
+ {
+ // NOTE: If the cookie name is changed, we must also update the method in matomo.js with the same name.
+ $hash = substr(
+ sha1(
+ ($this->configCookieDomain == '' ? self::getCurrentHost() : $this->configCookieDomain) . $this->configCookiePath
+ ),
+ 0,
+ 4
+ );
+
+ return self::FIRST_PARTY_COOKIES_PREFIX . $cookieName . '.' . $this->idSite . '.' . $hash;
+ }
+
+ /**
+ * Tracks a page view
+ *
+ * @param string $documentTitle Page title as it will appear in the Actions > Page titles report
+ * @return mixed Response string or true if using bulk requests.
+ */
+ public function doTrackPageView($documentTitle)
+ {
+ $this->generateNewPageviewId();
+
+ $url = $this->getUrlTrackPageView($documentTitle);
+
+ return $this->sendRequest($url);
+ }
+
+ private function generateNewPageviewId()
+ {
+ $this->idPageview = substr(md5(uniqid(rand(), true)), 0, 6);
+ }
+
+ /**
+ * Tracks an event
+ *
+ * @param string $category The Event Category (Videos, Music, Games...)
+ * @param string $action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...)
+ * @param string|bool $name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...)
+ * @param float|bool $value (optional) The Event's value
+ * @return mixed Response string or true if using bulk requests.
+ */
+ public function doTrackEvent($category, $action, $name = false, $value = false)
+ {
+ $url = $this->getUrlTrackEvent($category, $action, $name, $value);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Tracks a content impression
+ *
+ * @param string $contentName The name of the content. For instance 'Ad Foo Bar'
+ * @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
+ * @param string|bool $contentTarget (optional) The target of the content. For instance the URL of a landing page.
+ * @return mixed Response string or true if using bulk requests.
+ */
+ public function doTrackContentImpression($contentName, $contentPiece = 'Unknown', $contentTarget = false)
+ {
+ $url = $this->getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Tracks a content interaction. Make sure you have tracked a content impression using the same content name and
+ * content piece, otherwise it will not count. To do so you should call the method doTrackContentImpression();
+ *
+ * @param string $interaction The name of the interaction with the content. For instance a 'click'
+ * @param string $contentName The name of the content. For instance 'Ad Foo Bar'
+ * @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
+ * @param string|bool $contentTarget (optional) The target the content leading to when an interaction occurs. For instance the URL of a landing page.
+ * @return mixed Response string or true if using bulk requests.
+ */
+ public function doTrackContentInteraction(
+ $interaction,
+ $contentName,
+ $contentPiece = 'Unknown',
+ $contentTarget = false
+ )
+ {
+ $url = $this->getUrlTrackContentInteraction($interaction, $contentName, $contentPiece, $contentTarget);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Tracks an internal Site Search query, and optionally tracks the Search Category, and Search results Count.
+ * These are used to populate reports in Actions > Site Search.
+ *
+ * @param string $keyword Searched query on the site
+ * @param string $category (optional) Search engine category if applicable
+ * @param bool|int $countResults (optional) results displayed on the search result page. Used to track "zero result" keywords.
+ *
+ * @return mixed Response or true if using bulk requests.
+ */
+ public function doTrackSiteSearch($keyword, $category = '', $countResults = false)
+ {
+ $url = $this->getUrlTrackSiteSearch($keyword, $category, $countResults);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Records a Goal conversion
+ *
+ * @param int $idGoal Id Goal to record a conversion
+ * @param float $revenue Revenue for this conversion
+ * @return mixed Response or true if using bulk request
+ */
+ public function doTrackGoal($idGoal, $revenue = 0.0)
+ {
+ $url = $this->getUrlTrackGoal($idGoal, $revenue);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Tracks a download or outlink
+ *
+ * @param string $actionUrl URL of the download or outlink
+ * @param string $actionType Type of the action: 'download' or 'link'
+ * @return mixed Response or true if using bulk request
+ */
+ public function doTrackAction($actionUrl, $actionType)
+ {
+ // Referrer could be udpated to be the current URL temporarily (to mimic JS behavior)
+ $url = $this->getUrlTrackAction($actionUrl, $actionType);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Adds an item in the Ecommerce order.
+ *
+ * This should be called before doTrackEcommerceOrder(), or before doTrackEcommerceCartUpdate().
+ * This function can be called for all individual products in the cart (or order).
+ * SKU parameter is mandatory. Other parameters are optional (set to false if value not known).
+ * Ecommerce items added via this function are automatically cleared when doTrackEcommerceOrder() or getUrlTrackEcommerceOrder() is called.
+ *
+ * @param string $sku (required) SKU, Product identifier
+ * @param string $name (optional) Product name
+ * @param string|array $category (optional) Product category, or array of product categories (up to 5 categories can be specified for a given product)
+ * @param float|int $price (optional) Individual product price (supports integer and decimal prices)
+ * @param int $quantity (optional) Product quantity. If not specified, will default to 1 in the Reports
+ * @throws Exception
+ * @return $this
+ */
+ public function addEcommerceItem($sku, $name = '', $category = '', $price = 0.0, $quantity = 1)
+ {
+ if (empty($sku)) {
+ throw new Exception("You must specify a SKU for the Ecommerce item");
+ }
+
+ $price = $this->forceDotAsSeparatorForDecimalPoint($price);
+
+ $this->ecommerceItems[] = array($sku, $name, $category, $price, $quantity);
+ return $this;
+ }
+
+ /**
+ * Tracks a Cart Update (add item, remove item, update item).
+ *
+ * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart,
+ * including the items that haven't been updated since the last cart update.
+ * Items which were in the previous cart and are not sent in later Cart updates will be deleted from the cart (in the database).
+ *
+ * @param float $grandTotal Cart grandTotal (typically the sum of all items' prices)
+ * @return mixed Response or true if using bulk request
+ */
+ public function doTrackEcommerceCartUpdate($grandTotal)
+ {
+ $url = $this->getUrlTrackEcommerceCartUpdate($grandTotal);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Sends all stored tracking actions at once. Only has an effect if bulk tracking is enabled.
+ *
+ * To enable bulk tracking, call enableBulkTracking().
+ *
+ * @throws Exception
+ * @return string Response
+ */
+ public function doBulkTrack()
+ {
+ if (empty($this->storedTrackingActions)) {
+ throw new Exception(
+ "Error: you must call the function doTrackPageView or doTrackGoal from this class,
+ before calling this method doBulkTrack()"
+ );
+ }
+
+ $data = array('requests' => $this->storedTrackingActions);
+
+ // token_auth is not required by default, except if bulk_requests_require_authentication=1
+ if (!empty($this->token_auth)) {
+ $data['token_auth'] = $this->token_auth;
+ }
+
+ $postData = json_encode($data);
+ $response = $this->sendRequest($this->getBaseUrl(), 'POST', $postData, $force = true);
+
+ $this->storedTrackingActions = array();
+
+ return $response;
+ }
+
+ /**
+ * Tracks an Ecommerce order.
+ *
+ * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order.
+ * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Matomo reports.
+ * Only the parameters $orderId and $grandTotal are required.
+ *
+ * @param string|int $orderId (required) Unique Order ID.
+ * This will be used to count this order only once in the event the order page is reloaded several times.
+ * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Matomo.
+ * @param float $grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.)
+ * @param float $subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied)
+ * @param float $tax (optional) Tax amount for this order
+ * @param float $shipping (optional) Shipping amount for this order
+ * @param float $discount (optional) Discounted amount in this order
+ * @return mixed Response or true if using bulk request
+ */
+ public function doTrackEcommerceOrder(
+ $orderId,
+ $grandTotal,
+ $subTotal = 0.0,
+ $tax = 0.0,
+ $shipping = 0.0,
+ $discount = 0.0
+ )
+ {
+ $url = $this->getUrlTrackEcommerceOrder($orderId, $grandTotal, $subTotal, $tax, $shipping, $discount);
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Sends a ping request.
+ *
+ * Ping requests do not track new actions. If they are sent within the standard visit length (see global.ini.php),
+ * they will extend the existing visit and the current last action for the visit. If after the standard visit length,
+ * ping requests will create a new visit using the last action in the last known visit.
+ *
+ * @return mixed Response or true if using bulk request
+ */
+ public function doPing()
+ {
+ $url = $this->getRequest($this->idSite);
+ $url .= '&ping=1';
+
+ return $this->sendRequest($url);
+ }
+
+ /**
+ * Sets the current page view as an item (product) page view, or an Ecommerce Category page view.
+ *
+ * This must be called before doTrackPageView() on this product/category page.
+ *
+ * On a category page, you may set the parameter $category only and set the other parameters to false.
+ *
+ * Tracking Product/Category page views will allow Matomo to report on Product & Categories
+ * conversion rates (Conversion rate = Ecommerce orders containing this product or category / Visits to the product or category)
+ *
+ * @param string $sku Product SKU being viewed
+ * @param string $name Product Name being viewed
+ * @param string|array $category Category being viewed. On a Product page, this is the product's category.
+ * You can also specify an array of up to 5 categories for a given page view.
+ * @param float $price Specify the price at which the item was displayed
+ * @return $this
+ */
+ public function setEcommerceView($sku = '', $name = '', $category = '', $price = 0.0)
+ {
+ $this->ecommerceView = [];
+
+ if (!empty($category)) {
+ if (is_array($category)) {
+ $category = json_encode($category);
+ }
+ } else {
+ $category = "";
+ }
+ $this->ecommerceView['_pkc'] = $category;
+
+ if (!empty($price)) {
+ $price = (float)$price;
+ $price = $this->forceDotAsSeparatorForDecimalPoint($price);
+ $this->ecommerceView['_pkp'] = $price;
+ }
+
+ // On a category page, do not record "Product name not defined"
+ if (empty($sku) && empty($name)) {
+ return $this;
+ }
+ if (!empty($sku)) {
+ $this->ecommerceView['_pks'] = $sku;
+ }
+ if (empty($name)) {
+ $name = "";
+ }
+ $this->ecommerceView['_pkn'] = $name;
+ return $this;
+ }
+
+ /**
+ * Force the separator for decimal point to be a dot. See https://github.com/matomo-org/matomo/issues/6435
+ * If for instance a German locale is used it would be a comma otherwise.
+ *
+ * @param float|string $value
+ * @return string
+ */
+ private function forceDotAsSeparatorForDecimalPoint($value)
+ {
+ if (null === $value || false === $value) {
+ return $value;
+ }
+
+ return str_replace(',', '.', $value);
+ }
+
+ /**
+ * Returns URL used to track Ecommerce Cart updates
+ * Calling this function will reinitializes the property ecommerceItems to empty array
+ * so items will have to be added again via addEcommerceItem()
+ * @ignore
+ */
+ public function getUrlTrackEcommerceCartUpdate($grandTotal)
+ {
+ $url = $this->getUrlTrackEcommerce($grandTotal);
+
+ return $url;
+ }
+
+ /**
+ * Returns URL used to track Ecommerce Orders
+ * Calling this function will reinitializes the property ecommerceItems to empty array
+ * so items will have to be added again via addEcommerceItem()
+ * @ignore
+ */
+ public function getUrlTrackEcommerceOrder(
+ $orderId,
+ $grandTotal,
+ $subTotal = 0.0,
+ $tax = 0.0,
+ $shipping = 0.0,
+ $discount = 0.0
+ )
+ {
+ if (empty($orderId)) {
+ throw new Exception("You must specifiy an orderId for the Ecommerce order");
+ }
+ $url = $this->getUrlTrackEcommerce($grandTotal, $subTotal, $tax, $shipping, $discount);
+ $url .= '&ec_id=' . urlencode($orderId);
+
+ return $url;
+ }
+
+ /**
+ * Returns URL used to track Ecommerce orders
+ *
+ * Calling this function will reinitializes the property ecommerceItems to empty array
+ * so items will have to be added again via addEcommerceItem()
+ *
+ * @ignore
+ */
+ protected function getUrlTrackEcommerce($grandTotal, $subTotal = 0.0, $tax = 0.0, $shipping = 0.0, $discount = 0.0)
+ {
+ if (!is_numeric($grandTotal)) {
+ throw new Exception("You must specifiy a grandTotal for the Ecommerce order (or Cart update)");
+ }
+
+ $url = $this->getRequest($this->idSite);
+ $url .= '&idgoal=0';
+ if (!empty($grandTotal)) {
+ $grandTotal = $this->forceDotAsSeparatorForDecimalPoint($grandTotal);
+ $url .= '&revenue=' . $grandTotal;
+ }
+ if (!empty($subTotal)) {
+ $subTotal = $this->forceDotAsSeparatorForDecimalPoint($subTotal);
+ $url .= '&ec_st=' . $subTotal;
+ }
+ if (!empty($tax)) {
+ $tax = $this->forceDotAsSeparatorForDecimalPoint($tax);
+ $url .= '&ec_tx=' . $tax;
+ }
+ if (!empty($shipping)) {
+ $shipping = $this->forceDotAsSeparatorForDecimalPoint($shipping);
+ $url .= '&ec_sh=' . $shipping;
+ }
+ if (!empty($discount)) {
+ $discount = $this->forceDotAsSeparatorForDecimalPoint($discount);
+ $url .= '&ec_dt=' . $discount;
+ }
+ if (!empty($this->ecommerceItems)) {
+ $url .= '&ec_items=' . urlencode(json_encode($this->ecommerceItems));
+ }
+ $this->ecommerceItems = array();
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a page view.
+ *
+ * @see doTrackPageView()
+ * @param string $documentTitle Page view name as it will appear in Matomo reports
+ * @return string URL to matomo.php with all parameters set to track the pageview
+ */
+ public function getUrlTrackPageView($documentTitle = '')
+ {
+ $url = $this->getRequest($this->idSite);
+ if (strlen($documentTitle) > 0) {
+ $url .= '&action_name=' . urlencode($documentTitle);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a custom event.
+ *
+ * @see doTrackEvent()
+ * @param string $category The Event Category (Videos, Music, Games...)
+ * @param string $action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...)
+ * @param string|bool $name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...)
+ * @param float|bool $value (optional) The Event's value
+ * @return string URL to matomo.php with all parameters set to track the pageview
+ * @throws
+ */
+ public function getUrlTrackEvent($category, $action, $name = false, $value = false)
+ {
+ $url = $this->getRequest($this->idSite);
+ if (strlen($category) == 0) {
+ throw new Exception("You must specify an Event Category name (Music, Videos, Games...).");
+ }
+ if (strlen($action) == 0) {
+ throw new Exception("You must specify an Event action (click, view, add...).");
+ }
+
+ $url .= '&e_c=' . urlencode($category);
+ $url .= '&e_a=' . urlencode($action);
+
+ if (strlen($name) > 0) {
+ $url .= '&e_n=' . urlencode($name);
+ }
+ if (strlen($value) > 0) {
+ $value = $this->forceDotAsSeparatorForDecimalPoint($value);
+ $url .= '&e_v=' . $value;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a content impression.
+ *
+ * @see doTrackContentImpression()
+ * @param string $contentName The name of the content. For instance 'Ad Foo Bar'
+ * @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
+ * @param string|false $contentTarget (optional) The target of the content. For instance the URL of a landing page.
+ * @throws Exception In case $contentName is empty
+ * @return string URL to matomo.php with all parameters set to track the pageview
+ */
+ public function getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget)
+ {
+ $url = $this->getRequest($this->idSite);
+
+ if (strlen($contentName) == 0) {
+ throw new Exception("You must specify a content name");
+ }
+
+ $url .= '&c_n=' . urlencode($contentName);
+
+ if (!empty($contentPiece) && strlen($contentPiece) > 0) {
+ $url .= '&c_p=' . urlencode($contentPiece);
+ }
+ if (!empty($contentTarget) && strlen($contentTarget) > 0) {
+ $url .= '&c_t=' . urlencode($contentTarget);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a content impression.
+ *
+ * @see doTrackContentInteraction()
+ * @param string $interaction The name of the interaction with the content. For instance a 'click'
+ * @param string $contentName The name of the content. For instance 'Ad Foo Bar'
+ * @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
+ * @param string|false $contentTarget (optional) The target the content leading to when an interaction occurs. For instance the URL of a landing page.
+ * @throws Exception In case $interaction or $contentName is empty
+ * @return string URL to matomo.php with all parameters set to track the pageview
+ */
+ public function getUrlTrackContentInteraction($interaction, $contentName, $contentPiece, $contentTarget)
+ {
+ $url = $this->getRequest($this->idSite);
+
+ if (strlen($interaction) == 0) {
+ throw new Exception("You must specify a name for the interaction");
+ }
+
+ if (strlen($contentName) == 0) {
+ throw new Exception("You must specify a content name");
+ }
+
+ $url .= '&c_i=' . urlencode($interaction);
+ $url .= '&c_n=' . urlencode($contentName);
+
+ if (!empty($contentPiece) && strlen($contentPiece) > 0) {
+ $url .= '&c_p=' . urlencode($contentPiece);
+ }
+ if (!empty($contentTarget) && strlen($contentTarget) > 0) {
+ $url .= '&c_t=' . urlencode($contentTarget);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a site search.
+ *
+ * @see doTrackSiteSearch()
+ * @param string $keyword
+ * @param string $category
+ * @param int $countResults
+ * @return string
+ */
+ public function getUrlTrackSiteSearch($keyword, $category, $countResults)
+ {
+ $url = $this->getRequest($this->idSite);
+ $url .= '&search=' . urlencode($keyword);
+ if (strlen($category) > 0) {
+ $url .= '&search_cat=' . urlencode($category);
+ }
+ if (!empty($countResults) || $countResults === 0) {
+ $url .= '&search_count=' . (int)$countResults;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a goal with idGoal and revenue.
+ *
+ * @see doTrackGoal()
+ * @param int $idGoal Id Goal to record a conversion
+ * @param float $revenue Revenue for this conversion
+ * @return string URL to matomo.php with all parameters set to track the goal conversion
+ */
+ public function getUrlTrackGoal($idGoal, $revenue = 0.0)
+ {
+ $url = $this->getRequest($this->idSite);
+ $url .= '&idgoal=' . $idGoal;
+ if (!empty($revenue)) {
+ $revenue = $this->forceDotAsSeparatorForDecimalPoint($revenue);
+ $url .= '&revenue=' . $revenue;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Builds URL to track a new action.
+ *
+ * @see doTrackAction()
+ * @param string $actionUrl URL of the download or outlink
+ * @param string $actionType Type of the action: 'download' or 'link'
+ * @return string URL to matomo.php with all parameters set to track an action
+ */
+ public function getUrlTrackAction($actionUrl, $actionType)
+ {
+ $url = $this->getRequest($this->idSite);
+ $url .= '&' . $actionType . '=' . urlencode($actionUrl);
+
+ return $url;
+ }
+
+ /**
+ * Overrides server date and time for the tracking requests.
+ * By default Matomo will track requests for the "current datetime" but this function allows you
+ * to track visits in the past. All times are in UTC.
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth()
+ * @see setTokenAuth()
+ * @param string $dateTime Date with the format 'Y-m-d H:i:s', or a UNIX timestamp.
+ * If the datetime is older than one day (default value for tracking_requests_require_authentication_when_custom_timestamp_newer_than), then you must call setTokenAuth() with a valid Admin/Super user token.
+ * @return $this
+ */
+ public function setForceVisitDateTime($dateTime)
+ {
+ $this->forcedDatetime = $dateTime;
+ return $this;
+ }
+
+ /**
+ * Forces Matomo to create a new visit for the tracking request.
+ *
+ * By default, Matomo will create a new visit if the last request by this user was more than 30 minutes ago.
+ * If you call setForceNewVisit() before calling doTrack*, then a new visit will be created for this request.
+ * @return $this
+ */
+ public function setForceNewVisit()
+ {
+ $this->forcedNewVisit = true;
+ return $this;
+ }
+
+ /**
+ * Overrides IP address
+ *
+ * Allowed only for Admin/Super User, must be used along with setTokenAuth()
+ * @see setTokenAuth()
+ * @param string $ip IP string, eg. 130.54.2.1
+ * @return $this
+ */
+ public function setIp($ip)
+ {
+ $this->ip = $ip;
+ return $this;
+ }
+
+ /**
+ * Force the action to be recorded for a specific User. The User ID is a string representing a given user in your system.
+ *
+ * A User ID can be a username, UUID or an email address, or any number or string that uniquely identifies a user or client.
+ *
+ * @param string $userId Any user ID string (eg. email address, ID, username). Must be non empty. Set to false to de-assign a user id previously set.
+ * @return $this
+ * @throws Exception
+ */
+ public function setUserId($userId)
+ {
+ if ($userId === '') {
+ throw new Exception("User ID cannot be empty.");
+ }
+ $this->userId = $userId;
+ return $this;
+ }
+
+ /**
+ * Hash function used internally by Matomo to hash a User ID into the Visitor ID.
+ *
+ * Note: matches implementation of Tracker\Request->getUserIdHashed()
+ *
+ * @param $id
+ * @return string
+ */
+ public static function getUserIdHashed($id)
+ {
+ return substr(sha1($id), 0, 16);
+ }
+
+ /**
+ * Forces the requests to be recorded for the specified Visitor ID.
+ *
+ * Rather than letting Matomo attribute the user with a heuristic based on IP and other user fingeprinting attributes,
+ * force the action to be recorded for a particular visitor.
+ *
+ * If not set, the visitor ID will be fetched from the 1st party cookie, or will be set to a random UUID.
+ *
+ * @param string $visitorId 16 hexadecimal characters visitor ID, eg. "33c31e01394bdc63"
+ * @return $this
+ * @throws Exception
+ */
+ public function setVisitorId($visitorId)
+ {
+ $hexChars = '01234567890abcdefABCDEF';
+ if (strlen($visitorId) != self::LENGTH_VISITOR_ID
+ || strspn($visitorId, $hexChars) !== strlen($visitorId)
+ ) {
+ throw new Exception(
+ "setVisitorId() expects a "
+ . self::LENGTH_VISITOR_ID
+ . " characters hexadecimal string (containing only the following: "
+ . $hexChars
+ . ")"
+ );
+ }
+ $this->forcedVisitorId = $visitorId;
+ return $this;
+ }
+
+ /**
+ * If the user initiating the request has the Matomo first party cookie,
+ * this function will try and return the ID parsed from this first party cookie (found in $_COOKIE).
+ *
+ * If you call this function from a server, where the call is triggered by a cron or script
+ * not initiated by the actual visitor being tracked, then it will return
+ * the random Visitor ID that was assigned to this visit object.
+ *
+ * This can be used if you wish to record more visits, actions or goals for this visitor ID later on.
+ *
+ * @return string 16 hex chars visitor ID string
+ */
+ public function getVisitorId()
+ {
+ if (!empty($this->forcedVisitorId)) {
+ return $this->forcedVisitorId;
+ }
+ if ($this->loadVisitorIdCookie()) {
+ return $this->cookieVisitorId;
+ }
+
+ return $this->randomVisitorId;
+ }
+
+ /**
+ * Returns the currently set user agent.
+ * @return string
+ */
+ public function getUserAgent()
+ {
+ return $this->userAgent;
+ }
+
+ /**
+ * Returns the currently set IP address.
+ * @return string
+ */
+ public function getIp()
+ {
+ return $this->ip;
+ }
+
+ /**
+ * Returns the User ID string, which may have been set via:
+ * $v->setUserId('username@example.org');
+ *
+ * @return bool
+ */
+ public function getUserId()
+ {
+ return $this->userId;
+ }
+
+ /**
+ * Loads values from the VisitorId Cookie
+ *
+ * @return bool True if cookie exists and is valid, False otherwise
+ */
+ protected function loadVisitorIdCookie()
+ {
+ $idCookie = $this->getCookieMatchingName('id');
+ if ($idCookie === false) {
+ return false;
+ }
+ $parts = explode('.', $idCookie);
+ if (strlen($parts[0]) != self::LENGTH_VISITOR_ID) {
+ return false;
+ }
+
+ /* $this->cookieVisitorId provides backward compatibility since getVisitorId()
+didn't change any existing VisitorId value */
+ $this->cookieVisitorId = $parts[0];
+ $this->createTs = $parts[1];
+
+ return true;
+ }
+
+ /**
+ * Deletes all first party cookies from the client
+ */
+ public function deleteCookies()
+ {
+ $cookies = array('id', 'ses', 'cvar', 'ref');
+ foreach ($cookies as $cookie) {
+ $this->setCookie($cookie, '', -86400);
+ }
+ }
+
+ /**
+ * Returns the currently assigned Attribution Information stored in a first party cookie.
+ *
+ * This function will only work if the user is initiating the current request, and his cookies
+ * can be read by PHP from the $_COOKIE array.
+ *
+ * @return string JSON Encoded string containing the Referrer information for Goal conversion attribution.
+ * Will return false if the cookie could not be found
+ * @see matomo.js getAttributionInfo()
+ */
+ public function getAttributionInfo()
+ {
+ if (!empty($this->attributionInfo)) {
+ return json_encode($this->attributionInfo);
+ }
+
+ return $this->getCookieMatchingName('ref');
+ }
+
+ /**
+ * Some Tracking API functionality requires express authentication, using either the
+ * Super User token_auth, or a user with 'admin' access to the website.
+ *
+ * The following features require access:
+ * - force the visitor IP
+ * - force the date & time of the tracking requests rather than track for the current datetime
+ *
+ * @param string $token_auth token_auth 32 chars token_auth string
+ * @return $this
+ */
+ public function setTokenAuth($token_auth)
+ {
+ $this->token_auth = $token_auth;
+ return $this;
+ }
+
+ /**
+ * Sets local visitor time
+ *
+ * @param string $time HH:MM:SS format
+ * @return $this
+ */
+ public function setLocalTime($time)
+ {
+ list($hour, $minute, $second) = explode(':', $time);
+ $this->localHour = (int)$hour;
+ $this->localMinute = (int)$minute;
+ $this->localSecond = (int)$second;
+ return $this;
+ }
+
+ /**
+ * Sets user resolution width and height.
+ *
+ * @param int $width
+ * @param int $height
+ * @return $this
+ */
+ public function setResolution($width, $height)
+ {
+ $this->width = $width;
+ $this->height = $height;
+ return $this;
+ }
+
+ /**
+ * Sets if the browser supports cookies
+ * This is reported in "List of plugins" report in Matomo.
+ *
+ * @param bool $bool
+ * @return $this
+ */
+ public function setBrowserHasCookies($bool)
+ {
+ $this->hasCookies = $bool;
+ return $this;
+ }
+
+ /**
+ * Will append a custom string at the end of the Tracking request.
+ * @param string $string
+ * @return $this
+ */
+ public function setDebugStringAppend($string)
+ {
+ $this->DEBUG_APPEND_URL = '&' . $string;
+ return $this;
+ }
+
+ /**
+ * Sets visitor browser supported plugins
+ *
+ * @param bool $flash
+ * @param bool $java
+ * @param bool $quickTime
+ * @param bool $realPlayer
+ * @param bool $pdf
+ * @param bool $windowsMedia
+ * @param bool $silverlight
+ * @return $this
+ */
+ public function setPlugins(
+ $flash = false,
+ $java = false,
+ $quickTime = false,
+ $realPlayer = false,
+ $pdf = false,
+ $windowsMedia = false,
+ $silverlight = false
+ )
+ {
+ $this->plugins =
+ '&fla=' . (int)$flash .
+ '&java=' . (int)$java .
+ '&qt=' . (int)$quickTime .
+ '&realp=' . (int)$realPlayer .
+ '&pdf=' . (int)$pdf .
+ '&wma=' . (int)$windowsMedia .
+ '&ag=' . (int)$silverlight;
+ return $this;
+ }
+
+ /**
+ * By default, MatomoTracker will read first party cookies
+ * from the request and write updated cookies in the response (using setrawcookie).
+ * This can be disabled by calling this function.
+ */
+ public function disableCookieSupport()
+ {
+ $this->configCookiesDisabled = true;
+ }
+
+ /**
+ * Returns the maximum number of seconds the tracker will spend waiting for a response
+ * from Matomo. Defaults to 600 seconds.
+ */
+ public function getRequestTimeout()
+ {
+ return $this->requestTimeout;
+ }
+
+ /**
+ * Sets the maximum number of seconds that the tracker will spend waiting for a response
+ * from Matomo.
+ *
+ * @param int $timeout
+ * @return $this
+ * @throws Exception
+ */
+ public function setRequestTimeout($timeout)
+ {
+ if (!is_int($timeout) || $timeout < 0) {
+ throw new Exception("Invalid value supplied for request timeout: $timeout");
+ }
+
+ $this->requestTimeout = $timeout;
+ return $this;
+ }
+
+ /**
+ * Sets the request method to POST, which is recommended when using setTokenAuth()
+ * to prevent the token from being recorded in server logs. Avoid using redirects
+ * when using POST to prevent the loss of POST values. When using Log Analytics,
+ * be aware that POST requests are not parseable/replayable.
+ *
+ * @param string $method Either 'POST' or 'GET'
+ * @return $this
+ */
+ public function setRequestMethodNonBulk($method)
+ {
+ $this->requestMethod = strtoupper($method) === 'POST' ? 'POST' : 'GET';
+ return $this;
+ }
+
+ /**
+ * If a proxy is needed to look up the address of the Matomo site, set it with this
+ * @param string $proxy IP as string, for example "173.234.92.107"
+ * @param int $proxyPort
+ */
+ public function setProxy($proxy, $proxyPort = 80)
+ {
+ $this->proxy = $proxy;
+ $this->proxyPort = $proxyPort;
+ }
+
+ /**
+ * If the proxy IP and the proxy port have been set, with the setProxy() function
+ * returns a string, like "173.234.92.107:80"
+ */
+ private function getProxy()
+ {
+ if (isset($this->proxy) && isset($this->proxyPort)) {
+ return $this->proxy.":".$this->proxyPort;
+ }
+ return null;
+ }
+
+ /**
+ * Used in tests to output useful error messages.
+ *
+ * @ignore
+ */
+ static public $DEBUG_LAST_REQUESTED_URL = false;
+
+ /**
+ * @ignore
+ */
+ protected function sendRequest($url, $method = 'GET', $data = null, $force = false)
+ {
+ self::$DEBUG_LAST_REQUESTED_URL = $url;
+
+ // if doing a bulk request, store the url
+ if ($this->doBulkRequests && !$force) {
+ $this->storedTrackingActions[]
+ = $url
+ . (!empty($this->userAgent) ? ('&ua=' . urlencode($this->userAgent)) : '')
+ . (!empty($this->acceptLanguage) ? ('&lang=' . urlencode($this->acceptLanguage)) : '');
+
+ // Clear custom variables & dimensions so they don't get copied over to other users in the bulk request
+ $this->clearCustomVariables();
+ $this->clearCustomDimensions();
+ $this->clearCustomTrackingParameters();
+ $this->userAgent = false;
+ $this->acceptLanguage = false;
+
+ return true;
+ }
+
+ $forcePostUrlEncoded = false;
+ if (!$this->doBulkRequests) {
+ if (strtoupper($this->requestMethod) === 'POST') {
+ // POST ALL parameters and have no GET parameters
+ $urlParts = explode('?', $url);
+
+ $url = $urlParts[0];
+ $data = $urlParts[1];
+ $forcePostUrlEncoded = true;
+
+ $method = 'POST';
+ }
+
+ if (!empty($this->token_auth)) {
+ $appendTokenString = '&token_auth=' . urlencode($this->token_auth);
+
+ if (empty($this->requestMethod) || $method === 'POST') {
+ // Only post token_auth but use GET URL parameters for everything else
+ $forcePostUrlEncoded = true;
+ if (empty($data)) {
+ $data = '';
+ }
+ $data .= $appendTokenString;
+ $data = ltrim($data, '&'); // when no request method set we don't want it to start with '&'
+ } elseif (!empty($this->token_auth)) {
+ // Use GET for all URL parameters
+ $url .= $appendTokenString;
+ }
+ }
+ }
+
+ $proxy = $this->getProxy();
+
+ if (function_exists('curl_init') && function_exists('curl_exec')) {
+ $options = array(
+ CURLOPT_URL => $url,
+ CURLOPT_USERAGENT => $this->userAgent,
+ CURLOPT_HEADER => true,
+ CURLOPT_TIMEOUT => $this->requestTimeout,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HTTPHEADER => array(
+ 'Accept-Language: ' . $this->acceptLanguage,
+ ),
+ );
+
+ if ($method === 'GET') {
+ $options[CURLOPT_FOLLOWLOCATION] = true;
+ }
+
+ if (defined('PATH_TO_CERTIFICATES_FILE')) {
+ $options[CURLOPT_CAINFO] = PATH_TO_CERTIFICATES_FILE;
+ }
+
+ if (isset($proxy)) {
+ $options[CURLOPT_PROXY] = $proxy;
+ }
+
+ switch ($method) {
+ case 'POST':
+ $options[CURLOPT_POST] = true;
+ break;
+ default:
+ break;
+ }
+
+ // only supports JSON data
+ if (!empty($data) && $forcePostUrlEncoded) {
+ $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
+ $options[CURLOPT_POSTFIELDS] = $data;
+ $options[CURLOPT_POST] = true;
+ if (defined('CURL_REDIR_POST_ALL')) {
+ $options[CURLOPT_POSTREDIR] = CURL_REDIR_POST_ALL;
+ $options[CURLOPT_FOLLOWLOCATION] = true;
+ }
+ } elseif (!empty($data)) {
+ $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
+ $options[CURLOPT_HTTPHEADER][] = 'Expect:';
+ $options[CURLOPT_POSTFIELDS] = $data;
+ }
+
+ if (!empty($this->outgoingTrackerCookies)) {
+ $options[CURLOPT_COOKIE] = http_build_query($this->outgoingTrackerCookies);
+ $this->outgoingTrackerCookies = array();
+ }
+
+ $ch = curl_init();
+ curl_setopt_array($ch, $options);
+ ob_start();
+ $response = @curl_exec($ch);
+ ob_end_clean();
+ $header = '';
+ $content = '';
+
+ if ($response === false) {
+ throw new \RuntimeException(curl_error($ch));
+ }
+
+ if (!empty($response)) {
+ list($header, $content) = explode("\r\n\r\n", $response, $limitCount = 2);
+ }
+
+ $this->parseIncomingCookies(explode("\r\n", $header));
+
+ } elseif (function_exists('stream_context_create')) {
+ $stream_options = array(
+ 'http' => array(
+ 'method' => $method,
+ 'user_agent' => $this->userAgent,
+ 'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n",
+ 'timeout' => $this->requestTimeout,
+ ),
+ );
+
+ if (isset($proxy)) {
+ $stream_options['http']['proxy'] = $proxy;
+ }
+
+ // only supports JSON data
+ if (!empty($data) && $forcePostUrlEncoded) {
+ $stream_options['http']['header'] .= "Content-Type: application/x-www-form-urlencoded \r\n";
+ $stream_options['http']['content'] = $data;
+ } elseif (!empty($data)) {
+ $stream_options['http']['header'] .= "Content-Type: application/json \r\n";
+ $stream_options['http']['content'] = $data;
+ }
+
+ if (!empty($this->outgoingTrackerCookies)) {
+ $stream_options['http']['header'] .= 'Cookie: ' . http_build_query($this->outgoingTrackerCookies) . "\r\n";
+ $this->outgoingTrackerCookies = array();
+ }
+
+ $ctx = stream_context_create($stream_options);
+ $response = file_get_contents($url, 0, $ctx);
+ $content = $response;
+
+ $this->parseIncomingCookies($http_response_header);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Returns current timestamp, or forced timestamp/datetime if it was set
+ * @return string|int
+ */
+ protected function getTimestamp()
+ {
+ return !empty($this->forcedDatetime)
+ ? strtotime($this->forcedDatetime)
+ : time();
+ }
+
+ /**
+ * Returns the base URL for the Matomo server.
+ */
+ protected function getBaseUrl()
+ {
+ if (empty(self::$URL)) {
+ throw new Exception(
+ 'You must first set the Matomo Tracker URL by calling
+ MatomoTracker::$URL = \'http://your-website.org/matomo/\';'
+ );
+ }
+ if (strpos(self::$URL, '/matomo.php') === false
+ && strpos(self::$URL, '/proxy-matomo.php') === false
+ ) {
+ self::$URL = rtrim(self::$URL, '/');
+ self::$URL .= '/matomo.php';
+ }
+
+ return self::$URL;
+ }
+
+ /**
+ * @ignore
+ */
+ protected function getRequest($idSite)
+ {
+ $this->setFirstPartyCookies();
+
+ $customFields = '';
+ if (!empty($this->customParameters)) {
+ $customFields = '&' . http_build_query($this->customParameters, '', '&');
+ }
+
+ $customDimensions = '';
+ if (!empty($this->customDimensions)) {
+ $customDimensions = '&' . http_build_query($this->customDimensions, '', '&');
+ }
+
+ $baseUrl = $this->getBaseUrl();
+ $start = '?';
+ if (strpos($baseUrl, '?') !== false) {
+ $start = '&';
+ }
+
+ $url = $baseUrl . $start .
+ 'idsite=' . $idSite .
+ '&rec=1' .
+ '&apiv=' . self::VERSION .
+ '&r=' . substr(strval(mt_rand()), 2, 6) .
+
+ // XDEBUG_SESSIONS_START and KEY are related to the PHP Debugger, this can be ignored in other languages
+ (!empty($_GET['XDEBUG_SESSION_START']) ?
+ '&XDEBUG_SESSION_START=' . @urlencode($_GET['XDEBUG_SESSION_START']) : '') .
+ (!empty($_GET['KEY']) ? '&KEY=' . @urlencode($_GET['KEY']) : '') .
+
+ // Only allowed for Admin/Super User, token_auth required,
+ ((!empty($this->ip) && !empty($this->token_auth)) ? '&cip=' . $this->ip : '') .
+ (!empty($this->userId) ? '&uid=' . urlencode($this->userId) : '') .
+ (!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') .
+ (!empty($this->forcedNewVisit) ? '&new_visit=1' : '') .
+
+ // Values collected from cookie
+ '&_idts=' . $this->createTs .
+
+ // These parameters are set by the JS, but optional when using API
+ (!empty($this->plugins) ? $this->plugins : '') .
+ (($this->localHour !== false && $this->localMinute !== false && $this->localSecond !== false) ?
+ '&h=' . $this->localHour . '&m=' . $this->localMinute . '&s=' . $this->localSecond : '') .
+ (!empty($this->width) && !empty($this->height) ? '&res=' . $this->width . 'x' . $this->height : '') .
+ (!empty($this->hasCookies) ? '&cookie=' . $this->hasCookies : '') .
+
+ // Various important attributes
+ (!empty($this->customData) ? '&data=' . $this->customData : '') .
+ (!empty($this->visitorCustomVar) ? '&_cvar=' . urlencode(json_encode($this->visitorCustomVar)) : '') .
+ (!empty($this->pageCustomVar) ? '&cvar=' . urlencode(json_encode($this->pageCustomVar)) : '') .
+ (!empty($this->eventCustomVar) ? '&e_cvar=' . urlencode(json_encode($this->eventCustomVar)) : '') .
+ (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) .
+
+ // URL parameters
+ '&url=' . urlencode($this->pageUrl) .
+ '&urlref=' . urlencode($this->urlReferrer) .
+ ((!empty($this->pageCharset) && $this->pageCharset != self::DEFAULT_CHARSET_PARAMETER_VALUES) ?
+ '&cs=' . $this->pageCharset : '') .
+
+ // unique pageview id
+ (!empty($this->idPageview) ? '&pv_id=' . urlencode($this->idPageview) : '') .
+
+ // Attribution information, so that Goal conversions are attributed to the right referrer or campaign
+ // Campaign name
+ (!empty($this->attributionInfo[0]) ? '&_rcn=' . urlencode($this->attributionInfo[0]) : '') .
+ // Campaign keyword
+ (!empty($this->attributionInfo[1]) ? '&_rck=' . urlencode($this->attributionInfo[1]) : '') .
+ // Timestamp at which the referrer was set
+ (!empty($this->attributionInfo[2]) ? '&_refts=' . $this->attributionInfo[2] : '') .
+ // Referrer URL
+ (!empty($this->attributionInfo[3]) ? '&_ref=' . urlencode($this->attributionInfo[3]) : '') .
+
+ // custom location info
+ (!empty($this->country) ? '&country=' . urlencode($this->country) : '') .
+ (!empty($this->region) ? '®ion=' . urlencode($this->region) : '') .
+ (!empty($this->city) ? '&city=' . urlencode($this->city) : '') .
+ (!empty($this->lat) ? '&lat=' . urlencode($this->lat) : '') .
+ (!empty($this->long) ? '&long=' . urlencode($this->long) : '') .
+ $customFields . $customDimensions .
+ (!$this->sendImageResponse ? '&send_image=0' : '') .
+
+ // DEBUG
+ $this->DEBUG_APPEND_URL;
+
+ if (!empty($this->idPageview)) {
+ $url .=
+ ($this->networkTime !== false ? '&pf_net=' . ((int)$this->networkTime) : '') .
+ ($this->serverTime !== false ? '&pf_srv=' . ((int)$this->serverTime) : '') .
+ ($this->transferTime !== false ? '&pf_tfr=' . ((int)$this->transferTime) : '') .
+ ($this->domProcessingTime !== false ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') .
+ ($this->domCompletionTime !== false ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') .
+ ($this->onLoadTime !== false ? '&pf_onl=' . ((int)$this->onLoadTime) : '');
+ $this->clearPerformanceTimings();
+ }
+
+ foreach ($this->ecommerceView as $param => $value) {
+ $url .= '&' . $param . '=' . urlencode($value);
+ }
+
+ // Reset page level custom variables after this page view
+ $this->ecommerceView = array();
+ $this->pageCustomVar = array();
+ $this->eventCustomVar = array();
+ $this->clearCustomDimensions();
+ $this->clearCustomTrackingParameters();
+
+ // force new visit only once, user must call again setForceNewVisit()
+ $this->forcedNewVisit = false;
+
+ return $url;
+ }
+
+
+ /**
+ * Returns a first party cookie which name contains $name
+ *
+ * @param string $name
+ * @return string String value of cookie, or false if not found
+ * @ignore
+ */
+ protected function getCookieMatchingName($name)
+ {
+ if ($this->configCookiesDisabled) {
+ return false;
+ }
+ if (!is_array($_COOKIE)) {
+ return false;
+ }
+ $name = $this->getCookieName($name);
+
+ // Matomo cookie names use dots separators in matomo.js,
+ // but PHP Replaces . with _ http://www.php.net/manual/en/language.variables.predefined.php#72571
+ $name = str_replace('.', '_', $name);
+ foreach ($_COOKIE as $cookieName => $cookieValue) {
+ if (strpos($cookieName, $name) !== false) {
+ return $cookieValue;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
+ * will return "/dir1/dir2/index.php"
+ *
+ * @return string
+ * @ignore
+ */
+ protected static function getCurrentScriptName()
+ {
+ $url = '';
+ if (!empty($_SERVER['PATH_INFO'])) {
+ $url = $_SERVER['PATH_INFO'];
+ } else {
+ if (!empty($_SERVER['REQUEST_URI'])) {
+ if (($pos = strpos($_SERVER['REQUEST_URI'], '?')) !== false) {
+ $url = substr($_SERVER['REQUEST_URI'], 0, $pos);
+ } else {
+ $url = $_SERVER['REQUEST_URI'];
+ }
+ }
+ }
+ if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) {
+ $url = $_SERVER['SCRIPT_NAME'];
+ } elseif (empty($url)) {
+ $url = '/';
+ }
+
+ if (!empty($url) && $url[0] !== '/') {
+ $url = '/' . $url;
+ }
+
+ return $url;
+ }
+
+ /**
+ * If the current URL is 'http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
+ * will return 'http'
+ *
+ * @return string 'https' or 'http'
+ * @ignore
+ */
+ protected static function getCurrentScheme()
+ {
+ if (isset($_SERVER['HTTPS'])
+ && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true)
+ ) {
+ return 'https';
+ }
+
+ return 'http';
+ }
+
+ /**
+ * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
+ * will return "http://example.org"
+ *
+ * @return string
+ * @ignore
+ */
+ protected static function getCurrentHost()
+ {
+ if (isset($_SERVER['HTTP_HOST'])) {
+ return $_SERVER['HTTP_HOST'];
+ }
+
+ return 'unknown';
+ }
+
+ /**
+ * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
+ * will return "?param1=value1¶m2=value2"
+ *
+ * @return string
+ * @ignore
+ */
+ protected static function getCurrentQueryString()
+ {
+ $url = '';
+ if (isset($_SERVER['QUERY_STRING'])
+ && !empty($_SERVER['QUERY_STRING'])
+ ) {
+ $url .= '?' . $_SERVER['QUERY_STRING'];
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns the current full URL (scheme, host, path and query string.
+ *
+ * @return string
+ * @ignore
+ */
+ protected static function getCurrentUrl()
+ {
+ return self::getCurrentScheme() . '://'
+ . self::getCurrentHost()
+ . self::getCurrentScriptName()
+ . self::getCurrentQueryString();
+ }
+
+ /**
+ * Sets the first party cookies as would the matomo.js
+ * All cookies are supported: 'id' and 'ses' and 'ref' and 'cvar' cookies.
+ * @return $this
+ */
+ protected function setFirstPartyCookies()
+ {
+ if ($this->configCookiesDisabled) {
+ return $this;
+ }
+
+ if (empty($this->cookieVisitorId)) {
+ $this->loadVisitorIdCookie();
+ }
+
+ // Set the 'ref' cookie
+ $attributionInfo = $this->getAttributionInfo();
+ if (!empty($attributionInfo)) {
+ $this->setCookie('ref', $attributionInfo, $this->configReferralCookieTimeout);
+ }
+
+ // Set the 'ses' cookie
+ $this->setCookie('ses', '*', $this->configSessionCookieTimeout);
+
+ // Set the 'id' cookie
+ $cookieValue = $this->getVisitorId() . '.' . $this->createTs;
+ $this->setCookie('id', $cookieValue, $this->configVisitorCookieTimeout);
+
+ // Set the 'cvar' cookie
+ $this->setCookie('cvar', json_encode($this->visitorCustomVar), $this->configSessionCookieTimeout);
+ return $this;
+ }
+
+ /**
+ * Sets a first party cookie to the client to improve dual JS-PHP tracking.
+ *
+ * This replicates the matomo.js tracker algorithms for consistency and better accuracy.
+ *
+ * @param $cookieName
+ * @param $cookieValue
+ * @param $cookieTTL
+ * @return $this
+ */
+ protected function setCookie($cookieName, $cookieValue, $cookieTTL)
+ {
+ $cookieExpire = $this->currentTs + $cookieTTL;
+ if (!headers_sent()) {
+ $header = 'Set-Cookie: ' . rawurlencode($this->getCookieName($cookieName)) . '=' . rawurlencode($cookieValue)
+ . (empty($cookieExpire) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $cookieExpire) . ' GMT')
+ . (empty($this->configCookiePath) ? '' : '; path=' . $this->configCookiePath)
+ . (empty($this->configCookieDomain) ? '' : '; domain=' . rawurlencode($this->configCookieDomain))
+ . (!$this->configCookieSecure ? '' : '; secure')
+ . (!$this->configCookieHTTPOnly ? '' : '; HttpOnly')
+ . (!$this->configCookieSameSite ? '' : '; SameSite=' . rawurlencode($this->configCookieSameSite));
+
+ header($header, false);
+ }
+ return $this;
+ }
+
+ /**
+ * @return bool|mixed
+ */
+ protected function getCustomVariablesFromCookie()
+ {
+ $cookie = $this->getCookieMatchingName('cvar');
+ if (!$cookie) {
+ return false;
+ }
+
+ return json_decode($cookie, $assoc = true);
+ }
+
+ /**
+ * Sets a cookie to be sent to the tracking server.
+ *
+ * @param $name
+ * @param $value
+ */
+ public function setOutgoingTrackerCookie($name, $value)
+ {
+ if ($value === null) {
+ unset($this->outgoingTrackerCookies[$name]);
+ }
+ else {
+ $this->outgoingTrackerCookies[$name] = $value;
+ }
+ }
+
+ /**
+ * Gets a cookie which was set by the tracking server.
+ *
+ * @param $name
+ *
+ * @return bool|string
+ */
+ public function getIncomingTrackerCookie($name)
+ {
+ if (isset($this->incomingTrackerCookies[$name])) {
+ return $this->incomingTrackerCookies[$name];
+ }
+
+ return false;
+ }
+
+ /**
+ * Reads incoming tracking server cookies.
+ *
+ * @param array $headers Array with HTTP response headers as values
+ */
+ protected function parseIncomingCookies($headers)
+ {
+ $this->incomingTrackerCookies = array();
+
+ if (!empty($headers)) {
+ $headerName = 'set-cookie:';
+ $headerNameLength = strlen($headerName);
+
+ foreach($headers as $header) {
+ if (strpos(strtolower($header), $headerName) !== 0) {
+ continue;
+ }
+ $cookies = trim(substr($header, $headerNameLength));
+ $posEnd = strpos($cookies, ';');
+ if ($posEnd !== false) {
+ $cookies = substr($cookies, 0, $posEnd);
+ }
+ parse_str($cookies, $this->incomingTrackerCookies);
+ }
+ }
+ }
+}
+
+/**
+ * Helper function to quickly generate the URL to track a page view.
+ *
+ * @param $idSite
+ * @param string $documentTitle
+ * @return string
+ */
+function Matomo_getUrlTrackPageView($idSite, $documentTitle = '')
+{
+ $tracker = new MatomoTracker($idSite);
+
+ return $tracker->getUrlTrackPageView($documentTitle);
+}
+
+/**
+ * Helper function to quickly generate the URL to track a goal.
+ *
+ * @param $idSite
+ * @param $idGoal
+ * @param float $revenue
+ * @return string
+ */
+function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0)
+{
+ $tracker = new MatomoTracker($idSite);
+
+ return $tracker->getUrlTrackGoal($idGoal, $revenue);
+}
\ No newline at end of file
diff --git a/themes/blog/layout.html.php b/themes/blog/layout.html.php
index 77a6340..b531f94 100644
--- a/themes/blog/layout.html.php
+++ b/themes/blog/layout.html.php
@@ -192,5 +192,6 @@
+