Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <tsteur@users.noreply.github.com>2018-12-06 08:44:01 +0300
committerdiosmosis <diosmosis@users.noreply.github.com>2018-12-06 08:44:01 +0300
commit7b6a14626960795b0acdf4a064cd3982d7704f2b (patch)
tree1f8b5fc7d7869c77e91a1f8dc2ab592b3f8de4ca
parent1fe8a6ee582c2f61dc209b7df856695378007c23 (diff)
Report tracking into wrong Site ID and missing token auth (#13493)
* log tracking failures * added page * tweak UI * use a db column instead of option table to simplify code * add system summary, notifiy super users by email, fixes, update, ... * more fixes, needs tests next * add widget for tracking failures * ensure to not log any failure when visit is excluded * some tests and fixes * added tests * added missing test * apply review feedback * fix tests * trying to fix test * fix tests * fix update names * fix tests * Fix another test.
-rw-r--r--core/Db/Schema/Mysql.php8
-rw-r--r--core/Tracker/Failures.php197
-rw-r--r--core/Tracker/Request.php30
-rw-r--r--core/Tracker/Visit.php17
-rw-r--r--core/Tracker/VisitExcluded.php52
-rw-r--r--core/Updates/3.8.0-b3.php (renamed from core/Updates/3.8.0_b3.php)0
-rw-r--r--core/Updates/3.8.0-b4.php46
-rw-r--r--core/Version.php2
-rw-r--r--plugins/BulkTracking/tests/Integration/RequestsTest.php2
-rw-r--r--plugins/CoreAdminHome/.gitignore1
-rw-r--r--plugins/CoreAdminHome/API.php59
-rw-r--r--plugins/CoreAdminHome/Controller.php9
-rw-r--r--plugins/CoreAdminHome/CoreAdminHome.php41
-rw-r--r--plugins/CoreAdminHome/Emails/TrackingFailuresEmail.php106
-rw-r--r--plugins/CoreAdminHome/Menu.php6
-rw-r--r--plugins/CoreAdminHome/Tasks.php46
-rw-r--r--plugins/CoreAdminHome/Widgets/GetTrackingFailures.php39
-rw-r--r--plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.controller.js59
-rw-r--r--plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.html59
-rw-r--r--plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.js25
-rw-r--r--plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.less9
-rw-r--r--plugins/CoreAdminHome/config/test.php30
-rw-r--r--plugins/CoreAdminHome/lang/en.json21
-rw-r--r--plugins/CoreAdminHome/templates/_trackingFailuresEmail.twig3
-rw-r--r--plugins/CoreAdminHome/templates/getTrackingFailures.twig13
-rw-r--r--plugins/CoreAdminHome/templates/home.twig7
-rw-r--r--plugins/CoreAdminHome/templates/trackingFailures.twig8
-rw-r--r--plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php46
-rw-r--r--plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php77
-rw-r--r--plugins/CoreAdminHome/tests/Integration/APITest.php134
-rw-r--r--plugins/CoreAdminHome/tests/Integration/TasksTest.php36
-rw-r--r--plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php63
-rw-r--r--plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml27
-rw-r--r--plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js94
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png3
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png3
-rw-r--r--plugins/CoreHome/CoreHome.php2
-rw-r--r--plugins/CoreHome/Tracker/VisitRequestProcessor.php5
-rw-r--r--plugins/CustomVariables/tests/UI/expected-screenshots/CustomVariables_link_in_menu.png4
m---------plugins/QueuedTracking0
-rw-r--r--plugins/SitesManager/SitesManager.php15
-rw-r--r--plugins/Widgetize/tests/System/WidgetTest.php8
-rw-r--r--tests/PHPUnit/Fixtures/InvalidVisits.php5
-rw-r--r--tests/PHPUnit/Integration/Tracker/FailuresTest.php329
-rw-r--r--tests/PHPUnit/Integration/Tracker/RequestSetTest.php61
-rw-r--r--tests/PHPUnit/Integration/Tracker/RequestTest.php76
-rw-r--r--tests/PHPUnit/Integration/Tracker/VisitTest.php8
-rw-r--r--tests/PHPUnit/Integration/WidgetsListTest.php8
-rw-r--r--tests/PHPUnit/System/TrackerResponseTest.php9
-rw-r--r--tests/PHPUnit/System/expected/test_ImportLogs__CoreAdminHome.getTrackingFailures.xml2
-rw-r--r--tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.original1
-rw-r--r--tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.xml2
-rw-r--r--tests/PHPUnit/System/expected/test_OneVisitorTwoVisits_withCookieSupport__CoreAdminHome.getTrackingFailures.xml2
-rw-r--r--tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml19
-rw-r--r--tests/PHPUnit/System/expected/test_noVisit_PeriodIsLast__CoreAdminHome.getTrackingFailures.xml2
-rw-r--r--tests/PHPUnit/System/expected/test_noVisit__CoreAdminHome.getTrackingFailures.xml2
-rw-r--r--tests/PHPUnit/Unit/Tracker/RequestSetTest.php26
-rw-r--r--tests/PHPUnit/Unit/Tracker/RequestTest.php78
-rw-r--r--tests/PHPUnit/piwik.js77
-rw-r--r--tests/UI/expected-screenshots/Menus_admin_changed.png4
-rw-r--r--tests/UI/expected-screenshots/Menus_admin_loaded.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png4
69 files changed, 2014 insertions, 139 deletions
diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php
index 557ec9795d..4fe9e6b560 100644
--- a/core/Db/Schema/Mysql.php
+++ b/core/Db/Schema/Mysql.php
@@ -292,6 +292,14 @@ class Mysql implements SchemaInterface
PRIMARY KEY(`name`)
) ENGINE=$engine DEFAULT CHARSET=utf8
",
+ 'tracking_failure' => "CREATE TABLE {$prefixTables}tracking_failure (
+ `idsite` BIGINT(20) UNSIGNED NOT NULL ,
+ `idfailure` SMALLINT UNSIGNED NOT NULL ,
+ `date_first_occurred` DATETIME NOT NULL ,
+ `request_url` MEDIUMTEXT NOT NULL ,
+ PRIMARY KEY(`idsite`, `idfailure`)
+ ) ENGINE=$engine DEFAULT CHARSET=utf8
+ ",
);
return $tables;
diff --git a/core/Tracker/Failures.php b/core/Tracker/Failures.php
new file mode 100644
index 0000000000..5756b85ed1
--- /dev/null
+++ b/core/Tracker/Failures.php
@@ -0,0 +1,197 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Tracker;
+
+use Piwik\Common;
+use Piwik\Date;
+use Piwik\Exception\InvalidRequestParameterException;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
+use Piwik\Piwik;
+use Piwik\Site;
+use Piwik\Db as PiwikDb;
+
+class Failures
+{
+ const CLEANUP_OLD_FAILURES_DAYS = 2;
+ const FAILURE_ID_INVALID_SITE = 1;
+ const FAILURE_ID_NOT_AUTHENTICATED = 2;
+
+ private $table = 'tracking_failure';
+ private $tablePrefixed;
+ private $now;
+
+ public function __construct()
+ {
+ $this->tablePrefixed = Common::prefixTable($this->table);
+ }
+
+ public function setNow(Date $now)
+ {
+ $this->now = $now;
+ }
+
+ private function getNow()
+ {
+ if (isset($this->now)) {
+ return $this->now;
+ }
+ return Date::now();
+ }
+
+ public function logFailure($idFailure, Request $request)
+ {
+ $isVisitExcluded = $request->getMetadata('CoreHome', 'isVisitExcluded');
+
+ if ($isVisitExcluded === null) {
+ try {
+ $visitExcluded = new VisitExcluded($request);
+ $isVisitExcluded = $visitExcluded->isExcluded();
+ } catch (InvalidRequestParameterException $e) {
+ // we ignore this error and assume visit is not excluded... happens eg when using `cip` and request was
+ // not authenticated...
+ $isVisitExcluded = false;
+ }
+ }
+
+ if ($isVisitExcluded) {
+ return;
+ }
+
+ $idSite = (int) $request->getIdSiteUnverified();
+ $idFailure = (int) $idFailure;
+
+ if ($idSite > 9999999 || $idSite < 0 || $this->hasLoggedFailure($idSite, $idFailure)) {
+ return; // we prevent creating huge amount of entries in the cache
+ }
+
+ $params = $this->getParamsWithTokenAnonymized($request);
+ $sql = sprintf('INSERT INTO %s (`idsite`, `idfailure`, `date_first_occurred`, `request_url`) VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE idsite=idsite;', $this->tablePrefixed);
+
+ PiwikDb::get()->query($sql, array($idSite, $idFailure, $this->getNow()->getDatetime(), http_build_query($params)));
+ }
+
+ private function hasLoggedFailure($idSite, $idFailure)
+ {
+ $sql = sprintf('SELECT idsite FROM %s WHERE idsite = ? and idfailure = ?', $this->tablePrefixed);
+ $row = PiwikDb::fetchRow($sql, array($idSite, $idFailure));
+
+ return !empty($row);
+ }
+
+ private function getParamsWithTokenAnonymized(Request $request)
+ {
+ // eg if there is a typo in the token auth we want to replace it as well to not accidentally leak a token
+ // eg imagine a super user tries to issue an API request for a site and sending the wrong parameter for a token...
+ // an admin may have view access for this and can see the super users token
+ $token = $request->getTokenAuth();
+ $params = $request->getRawParams();
+ foreach (array('token_auth', 'token', 'tokenauth', 'token__auth') as $key) {
+ if (isset($params[$key])) {
+ $params[$key] = '__TOKEN_AUTH__';
+ }
+ }
+ foreach ($params as $key => $value) {
+ if (!empty($token) && $value === $token) {
+ $params[$key] = '__TOKEN_AUTH__'; // user accidentally posted the token in a wrong field
+ } elseif (!empty($value) && is_string($value)
+ && Common::mb_strlen($value) >= 29 && Common::mb_strlen($value) <= 36
+ && ctype_xdigit($value)) {
+ $params[$key] = '__TOKEN_AUTH__'; // user maybe posted a token in a different field... it looks like it might be a token
+ }
+ }
+
+ return $params;
+ }
+
+ public function removeFailuresOlderThanDays($days)
+ {
+ $minutesAgo = $this->getNow()->subDay($days)->getDatetime();
+
+ PiwikDb::query(sprintf('DELETE FROM %s WHERE date_first_occurred < ?', $this->tablePrefixed), array($minutesAgo));
+ }
+
+ public function getAllFailures()
+ {
+ $failures = PiwikDb::fetchAll(sprintf('SELECT * FROM %s', $this->tablePrefixed));
+ return $this->enrichFailures($failures);
+ }
+
+ public function getFailuresForSites($idSites)
+ {
+ if (empty($idSites)) {
+ return array();
+ }
+ $idSites = array_map('intval', $idSites);
+ $idSites = implode(',', $idSites);
+ $failures = PiwikDb::fetchAll(sprintf('SELECT * FROM %s WHERE idsite IN (%s)', $this->tablePrefixed, $idSites));
+ return $this->enrichFailures($failures);
+ }
+
+ public function deleteTrackingFailure($idSite, $idFailure)
+ {
+ PiwikDb::query(sprintf('DELETE FROM %s WHERE idsite = ? and idfailure = ?', $this->tablePrefixed), array($idSite, $idFailure));
+ }
+
+ public function deleteTrackingFailures($idSites)
+ {
+ if (!empty($idSites)) {
+ $idSites = array_map('intval', $idSites);
+ $idSites = implode(',', $idSites);
+ PiwikDb::query(sprintf('DELETE FROM %s WHERE idsite IN(%s)', $this->tablePrefixed, $idSites));
+ }
+ }
+
+ public function deleteAllTrackingFailures()
+ {
+ PiwikDb::query(sprintf('DELETE FROM %s', $this->tablePrefixed));
+ }
+
+ private function enrichFailures($failures)
+ {
+ foreach ($failures as &$failure) {
+ try {
+ $failure['site_name'] = Site::getNameFor($failure['idsite']);
+ } catch (UnexpectedWebsiteFoundException $e) {
+ $failure['site_name'] = Piwik::translate('General_Unknown');
+ }
+ $failure['pretty_date_first_occurred'] = Date::factory($failure['date_first_occurred'])->getLocalized(Date::DATETIME_FORMAT_SHORT);
+ parse_str($failure['request_url'], $params);
+ if (empty($params['url'])) {
+ $params['url'] = ' ';// workaround it using the default provider in request constructor
+ }
+ $request = new Request($params);
+ $failure['url'] = trim($request->getParam('url'));
+ $failure['problem'] = '';
+ $failure['solution'] = '';
+ $failure['solution_url'] = '';
+
+ switch ($failure['idfailure']) {
+ case self::FAILURE_ID_INVALID_SITE:
+ $failure['problem'] = Piwik::translate('CoreAdminHome_TrackingFailureInvalidSiteProblem');
+ $failure['solution'] = Piwik::translate('CoreAdminHome_TrackingFailureInvalidSiteSolution');
+ $failure['solution_url'] = 'https://matomo.org/faq/how-to/faq_30838/';
+ break;
+ case self::FAILURE_ID_NOT_AUTHENTICATED:
+ $failure['problem'] = Piwik::translate('CoreAdminHome_TrackingFailureAuthenticationProblem');
+ $failure['solution'] = Piwik::translate('CoreAdminHome_TrackingFailureAuthenticationSolution');
+ $failure['solution_url'] = 'https://matomo.org/faq/how-to/faq_30835/';
+ break;
+ }
+ }
+
+ /**
+ * @ignore
+ * internal use only
+ */
+ Piwik::postEvent('Tracking.makeFailuresHumanReadable', array(&$failures));
+
+ return $failures;
+ }
+}
diff --git a/core/Tracker/Request.php b/core/Tracker/Request.php
index 3dcb078060..5e56e2185c 100644
--- a/core/Tracker/Request.php
+++ b/core/Tracker/Request.php
@@ -174,6 +174,8 @@ class Request
if ($this->isAuthenticated) {
Common::printDebug("token_auth is authenticated!");
+ } else {
+ StaticContainer::get('Piwik\Tracker\Failures')->logFailure(Failures::FAILURE_ID_NOT_AUTHENTICATED, $this);
}
} else {
$this->isAuthenticated = true;
@@ -513,12 +515,12 @@ class Request
&& $time > $now - 10 * 365 * 86400;
}
- public function getIdSite()
+ /**
+ * @internal
+ * @ignore
+ */
+ public function getIdSiteUnverified()
{
- if (isset($this->idSiteCache)) {
- return $this->idSiteCache;
- }
-
$idSite = Common::getRequestVar('idsite', 0, 'int', $this->params);
/**
@@ -534,11 +536,29 @@ class Request
* request.
*/
Piwik::postEvent('Tracker.Request.getIdSite', array(&$idSite, $this->params));
+ return $idSite;
+ }
+
+ public function getIdSite()
+ {
+ if (isset($this->idSiteCache)) {
+ return $this->idSiteCache;
+ }
+
+ $idSite = $this->getIdSiteUnverified();
if ($idSite <= 0) {
throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
}
+ // check site actually exists, should throw UnexpectedWebsiteFoundException directly
+ $site = Cache::getCacheWebsiteAttributes($idSite);
+
+ if (empty($site)) {
+ // fallback just in case exception wasn't thrown...
+ throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
+ }
+
$this->idSiteCache = $idSite;
return $idSite;
diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php
index c48f065dc7..f768ed6190 100644
--- a/core/Tracker/Visit.php
+++ b/core/Tracker/Visit.php
@@ -16,8 +16,6 @@ use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Network\IPUtils;
-use Piwik\Piwik;
-use Piwik\Plugin;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Tracker;
use Piwik\Tracker\Visit\VisitProperties;
@@ -87,6 +85,19 @@ class Visit implements VisitInterface
$this->request = $request;
}
+ private function checkSiteExists(Request $request)
+ {
+ try {
+ $this->request->getIdSite();
+ } catch (UnexpectedWebsiteFoundException $e) {
+ // we allow 0... the request will fail anyway as the site won't exist... allowing 0 will help us
+ // reporting this tracking problem as it is a common issue. Otherwise we would not be able to report
+ // this problem in tracking failures
+ StaticContainer::get(Failures::class)->logFailure(Failures::FAILURE_ID_INVALID_SITE, $request);
+ throw $e;
+ }
+ }
+
/**
* Main algorithm to handle the visit.
*
@@ -109,6 +120,8 @@ class Visit implements VisitInterface
*/
public function handle()
{
+ $this->checkSiteExists($this->request);
+
foreach ($this->requestProcessors as $processor) {
Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()...");
diff --git a/core/Tracker/VisitExcluded.php b/core/Tracker/VisitExcluded.php
index 9ae5dd5727..eabef88193 100644
--- a/core/Tracker/VisitExcluded.php
+++ b/core/Tracker/VisitExcluded.php
@@ -11,6 +11,7 @@ namespace Piwik\Tracker;
use Piwik\Cache as PiwikCache;
use Piwik\Common;
use Piwik\DeviceDetectorFactory;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Network\IP;
use Piwik\Piwik;
use Piwik\Plugins\SitesManager\SiteUrls;
@@ -26,15 +27,23 @@ class VisitExcluded
*/
private $spamFilter;
+ private $siteCache = array();
+
/**
* @param Request $request
*/
public function __construct(Request $request)
{
$this->spamFilter = new ReferrerSpamFilter();
-
$this->request = $request;
- $this->idSite = $request->getIdSite();
+
+ try {
+ $this->idSite = $request->getIdSite();
+ } catch (UnexpectedWebsiteFoundException $e){
+ // most checks will still work on a global scope and we still want to be able to test if this is a valid
+ // visit or not
+ $this->idSite = 0;
+ }
$userAgent = $request->getUserAgent();
$this->userAgent = Common::unsanitizeInputValue($userAgent);
$this->ip = $request->getIp();
@@ -263,11 +272,11 @@ class VisitExcluded
*/
protected function isVisitorIpExcluded()
{
- $websiteAttributes = Cache::getCacheWebsiteAttributes($this->idSite);
+ $excludedIps = $this->getAttributes('excluded_ips', 'global_excluded_ips');
- if (!empty($websiteAttributes['excluded_ips'])) {
+ if (!empty($excludedIps)) {
$ip = IP::fromBinaryIP($this->ip);
- if ($ip->isInRanges($websiteAttributes['excluded_ips'])) {
+ if ($ip->isInRanges($excludedIps)) {
Common::printDebug('Visitor IP ' . $ip->toString() . ' is excluded from being tracked');
return true;
}
@@ -276,20 +285,41 @@ class VisitExcluded
return false;
}
+ private function getAttributes($siteAttribute, $globalAttribute)
+ {
+ if (!isset($this->siteCache[$this->idSite])) {
+ $this->siteCache[$this->idSite] = array();
+ }
+ try {
+ if (empty($this->siteCache[$this->idSite])) {
+ $this->siteCache[$this->idSite] = Cache::getCacheWebsiteAttributes($this->idSite);
+ }
+ if (isset($this->siteCache[$this->idSite][$siteAttribute])) {
+ return $this->siteCache[$this->idSite][$siteAttribute];
+ }
+ } catch (UnexpectedWebsiteFoundException $e) {
+ $cached = Cache::getCacheGeneral();
+ if ($globalAttribute && isset($cached[$globalAttribute])) {
+ return $cached[$globalAttribute];
+ }
+ }
+ }
+
/**
* Checks if request URL is excluded
* @return bool
*/
protected function isUrlExcluded()
{
- $site = Cache::getCacheWebsiteAttributes($this->idSite);
+ $excludedUrls = $this->getAttributes('exclude_unknown_urls', null);
+ $siteUrls = $this->getAttributes('urls', null);
- if (!empty($site['exclude_unknown_urls']) && !empty($site['urls'])) {
+ if (!empty($excludedUrls) && !empty($siteUrls)) {
$url = $this->request->getParam('url');
$parsedUrl = parse_url($url);
$trackingUrl = new SiteUrls();
- $urls = $trackingUrl->groupUrlsByHost(array($this->idSite => $site['urls']));
+ $urls = $trackingUrl->groupUrlsByHost(array($this->idSite => $siteUrls));
$idSites = $trackingUrl->getIdSitesMatchingUrl($parsedUrl, $urls);
$isUrlExcluded = !isset($idSites) || !in_array($this->idSite, $idSites);
@@ -311,10 +341,10 @@ class VisitExcluded
*/
protected function isUserAgentExcluded()
{
- $websiteAttributes = Cache::getCacheWebsiteAttributes($this->idSite);
+ $excludedAgents = $this->getAttributes('excluded_user_agents', 'global_excluded_user_agents');
- if (!empty($websiteAttributes['excluded_user_agents'])) {
- foreach ($websiteAttributes['excluded_user_agents'] as $excludedUserAgent) {
+ if (!empty($excludedAgents)) {
+ foreach ($excludedAgents as $excludedUserAgent) {
// if the excluded user agent string part is in this visit's user agent, this visit should be excluded
if (stripos($this->userAgent, $excludedUserAgent) !== false) {
return true;
diff --git a/core/Updates/3.8.0_b3.php b/core/Updates/3.8.0-b3.php
index 83cc13e4fb..83cc13e4fb 100644
--- a/core/Updates/3.8.0_b3.php
+++ b/core/Updates/3.8.0-b3.php
diff --git a/core/Updates/3.8.0-b4.php b/core/Updates/3.8.0-b4.php
new file mode 100644
index 0000000000..822d207d29
--- /dev/null
+++ b/core/Updates/3.8.0-b4.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Updates;
+
+use Piwik\Updater;
+use Piwik\Updates as PiwikUpdates;
+use Piwik\Updater\Migration\Factory as MigrationFactory;
+
+class Updates_3_8_0_b4 extends PiwikUpdates
+{
+ /**
+ * @var MigrationFactory
+ */
+ private $migration;
+
+ public function __construct(MigrationFactory $factory)
+ {
+ $this->migration = $factory;
+ }
+
+ public function getMigrations(Updater $updater)
+ {
+ $trackingFailureTable = $this->migration->db->createTable('tracking_failure',
+ array('idsite' => 'BIGINT(20) UNSIGNED NOT NULL',
+ 'idfailure' => 'SMALLINT UNSIGNED NOT NULL',
+ 'date_first_occurred' => 'DATETIME NOT NULL',
+ 'request_url' => 'MEDIUMTEXT NOT NULL'),
+ array('idsite', 'idfailure'));
+
+ return array(
+ $trackingFailureTable
+ );
+ }
+
+ public function doUpdate(Updater $updater)
+ {
+ $updater->executeMigrations(__FILE__, $this->getMigrations($updater));
+ }
+}
diff --git a/core/Version.php b/core/Version.php
index d5dfb53b70..e554c08219 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -20,7 +20,7 @@ final class Version
* The current Matomo version.
* @var string
*/
- const VERSION = '3.8.0-b3';
+ const VERSION = '3.8.0-b4';
public function isStableVersion($version)
{
diff --git a/plugins/BulkTracking/tests/Integration/RequestsTest.php b/plugins/BulkTracking/tests/Integration/RequestsTest.php
index 7f6236d82c..e0832770d7 100644
--- a/plugins/BulkTracking/tests/Integration/RequestsTest.php
+++ b/plugins/BulkTracking/tests/Integration/RequestsTest.php
@@ -33,6 +33,8 @@ class RequestsTest extends IntegrationTestCase
{
parent::setUp();
+ Fixture::createWebsite('2014-01-02 03:04:05');
+
$this->requests = new Requests();
}
diff --git a/plugins/CoreAdminHome/.gitignore b/plugins/CoreAdminHome/.gitignore
new file mode 100644
index 0000000000..c8c9480010
--- /dev/null
+++ b/plugins/CoreAdminHome/.gitignore
@@ -0,0 +1 @@
+tests/System/processed/*xml \ No newline at end of file
diff --git a/plugins/CoreAdminHome/API.php b/plugins/CoreAdminHome/API.php
index 6fc3313696..19eef3d946 100644
--- a/plugins/CoreAdminHome/API.php
+++ b/plugins/CoreAdminHome/API.php
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\CoreAdminHome;
use Exception;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
+use Piwik\Access;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Config;
use Piwik\Container\StaticContainer;
@@ -22,6 +23,7 @@ use Piwik\Piwik;
use Piwik\Segment;
use Piwik\Scheduler\Scheduler;
use Piwik\Site;
+use Piwik\Tracker\Failures;
use Piwik\Url;
/**
@@ -39,10 +41,16 @@ class API extends \Piwik\Plugin\API
*/
private $invalidator;
- public function __construct(Scheduler $scheduler, ArchiveInvalidator $invalidator)
+ /**
+ * @var Failures
+ */
+ private $trackingFailures;
+
+ public function __construct(Scheduler $scheduler, ArchiveInvalidator $invalidator, Failures $trackingFailures)
{
$this->scheduler = $scheduler;
$this->invalidator = $invalidator;
+ $this->trackingFailures = $trackingFailures;
}
/**
@@ -180,6 +188,55 @@ class API extends \Piwik\Plugin\API
}
/**
+ * Deletes all tracking failures this user has at least admin access to.
+ * A super user will also delete tracking failures for sites that don't exist.
+ */
+ public function deleteAllTrackingFailures()
+ {
+ if (Piwik::hasUserSuperUserAccess()) {
+ $this->trackingFailures->deleteAllTrackingFailures();
+ } else {
+ Piwik::checkUserHasSomeAdminAccess();
+ $idSites = Access::getInstance()->getSitesIdWithAdminAccess();
+ Piwik::checkUserHasAdminAccess($idSites);
+ $this->trackingFailures->deleteTrackingFailures($idSites);
+ }
+ }
+
+ /**
+ * Deletes a specific tracking failure
+ * @param int $idSite
+ * @param int $idFailure
+ */
+ public function deleteTrackingFailure($idSite, $idFailure)
+ {
+ $idSite = (int) $idSite;
+ Piwik::checkUserHasAdminAccess($idSite);
+
+ $this->trackingFailures->deleteTrackingFailure($idSite, $idFailure);
+ }
+
+ /**
+ * Get all tracking failures. A user retrieves only tracking failures for sites with at least admin access.
+ * A super user will also retrieve failed requests for sites that don't exist.
+ * @return array
+ */
+ public function getTrackingFailures()
+ {
+ if (Piwik::hasUserSuperUserAccess()) {
+ $failures = $this->trackingFailures->getAllFailures();
+ } else {
+ Piwik::checkUserHasSomeAdminAccess();
+ $idSites = Access::getInstance()->getSitesIdWithAdminAccess();
+ Piwik::checkUserHasAdminAccess($idSites);
+
+ $failures = $this->trackingFailures->getFailuresForSites($idSites);
+ }
+
+ return $failures;
+ }
+
+ /**
* Ensure the specified dates are valid.
* Store invalid date so we can log them
* @param array $dates
diff --git a/plugins/CoreAdminHome/Controller.php b/plugins/CoreAdminHome/Controller.php
index bee4b57cac..32441e0d39 100644
--- a/plugins/CoreAdminHome/Controller.php
+++ b/plugins/CoreAdminHome/Controller.php
@@ -60,6 +60,7 @@ class Controller extends ControllerAdmin
$hasPremiumFeatures = $widgetsList->isDefined('Marketplace', 'getPremiumFeatures');
$hasNewPlugins = $widgetsList->isDefined('Marketplace', 'getNewPlugins');
$hasDiagnostics = $widgetsList->isDefined('Installation', 'getSystemCheck');
+ $hasTrackingFailures = $widgetsList->isDefined('CoreAdminHome', 'getTrackingFailures');
return $this->renderTemplate('home', array(
'isInternetEnabled' => $isInternetEnabled,
@@ -70,6 +71,7 @@ class Controller extends ControllerAdmin
'hasDonateForm' => $hasDonateForm,
'hasPiwikBlog' => $hasPiwikBlog,
'hasDiagnostics' => $hasDiagnostics,
+ 'hasTrackingFailures' => $hasTrackingFailures,
));
}
@@ -79,6 +81,13 @@ class Controller extends ControllerAdmin
return;
}
+ public function trackingFailures()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ return $this->renderTemplate('trackingFailures');
+ }
+
public function generalSettings()
{
Piwik::checkUserHasSuperUserAccess();
diff --git a/plugins/CoreAdminHome/CoreAdminHome.php b/plugins/CoreAdminHome/CoreAdminHome.php
index a87c6da948..3e0d0e4912 100644
--- a/plugins/CoreAdminHome/CoreAdminHome.php
+++ b/plugins/CoreAdminHome/CoreAdminHome.php
@@ -8,10 +8,10 @@
*/
namespace Piwik\Plugins\CoreAdminHome;
-use Piwik\Db;
+use Piwik\API\Request;
use Piwik\Piwik;
use Piwik\ProxyHttp;
-use Piwik\Settings\Plugin\UserSetting;
+use Piwik\Plugins\CoreHome\SystemSummary;
use Piwik\Settings\Storage\Backend\PluginSettingsTable;
/**
@@ -20,7 +20,7 @@ use Piwik\Settings\Storage\Backend\PluginSettingsTable;
class CoreAdminHome extends \Piwik\Plugin
{
/**
- * @see Piwik\Plugin::registerEvents
+ * @see \Piwik\Plugin::registerEvents
*/
public function registerEvents()
{
@@ -30,10 +30,24 @@ class CoreAdminHome extends \Piwik\Plugin
'UsersManager.deleteUser' => 'cleanupUser',
'API.DocumentationGenerator.@hideExceptForSuperUser' => 'displayOnlyForSuperUser',
'Template.jsGlobalVariables' => 'addJsGlobalVariables',
- 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys'
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'System.addSystemSummaryItems' => 'addSystemSummaryItems',
);
}
+ public function addSystemSummaryItems(&$systemSummary)
+ {
+ if (Piwik::isUserHasSomeAdminAccess()) {
+ $failures = Request::processRequest('CoreAdminHome.getTrackingFailures', [], []);
+ $numFailures = count($failures);
+ $icon = 'icon-error';
+ if ($numFailures === 0) {
+ $icon = 'icon-ok';
+ }
+ $systemSummary[] = new SystemSummary\Item($key = 'trackingfailures', Piwik::translate('CoreAdminHome_NTrackingFailures', $numFailures), $value = null, array('module' => 'CoreAdminHome', 'action' => 'trackingFailures'), $icon, $order = 9);
+ }
+ }
+
public function cleanupUser($userLogin)
{
PluginSettingsTable::removeAllUserSettingsForUser($userLogin);
@@ -45,6 +59,7 @@ class CoreAdminHome extends \Piwik\Plugin
$stylesheets[] = "plugins/Morpheus/stylesheets/base.less";
$stylesheets[] = "plugins/Morpheus/stylesheets/main.less";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/generalSettings.less";
+ $stylesheets[] = "plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.less";
}
public function getJsFiles(&$jsFiles)
@@ -82,5 +97,23 @@ class CoreAdminHome extends \Piwik\Plugin
$translationKeys[] = 'CoreAdminHome_ProtocolNotDetectedCorrectlySolution';
$translationKeys[] = 'CoreAdminHome_SettingsSaveSuccess';
$translationKeys[] = 'UserCountryMap_None';
+ $translationKeys[] = 'Actions_ColumnPageURL';
+ $translationKeys[] = 'General_Date';
+ $translationKeys[] = 'General_Measurable';
+ $translationKeys[] = 'General_Action';
+ $translationKeys[] = 'General_Delete';
+ $translationKeys[] = 'General_Id';
+ $translationKeys[] = 'CoreHome_ClickToSeeFullInformation';
+ $translationKeys[] = 'CoreAdminHome_LearnMore';
+ $translationKeys[] = 'CoreAdminHome_ConfirmDeleteAllTrackingFailures';
+ $translationKeys[] = 'CoreAdminHome_ConfirmDeleteThisTrackingFailure';
+ $translationKeys[] = 'CoreAdminHome_DeleteAllFailures';
+ $translationKeys[] = 'CoreAdminHome_NTrackingFailures';
+ $translationKeys[] = 'CoreAdminHome_Problem';
+ $translationKeys[] = 'CoreAdminHome_Solution';
+ $translationKeys[] = 'CoreAdminHome_TrackingFailures';
+ $translationKeys[] = 'CoreAdminHome_TrackingFailuresIntroduction';
+ $translationKeys[] = 'CoreAdminHome_TrackingURL';
+ $translationKeys[] = 'CoreAdminHome_NoKnownFailures';
}
}
diff --git a/plugins/CoreAdminHome/Emails/TrackingFailuresEmail.php b/plugins/CoreAdminHome/Emails/TrackingFailuresEmail.php
new file mode 100644
index 0000000000..92d3ee2c16
--- /dev/null
+++ b/plugins/CoreAdminHome/Emails/TrackingFailuresEmail.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\Emails;
+
+use Piwik\Access;
+use Piwik\Mail;
+use Piwik\Piwik;
+use Piwik\SettingsPiwik;
+use Piwik\Url;
+use Piwik\View;
+
+class TrackingFailuresEmail extends Mail
+{
+ /**
+ * @var string
+ */
+ private $login;
+
+ /**
+ * @var string
+ */
+ private $emailAddress;
+
+ /**
+ * @var int
+ */
+ private $numFailures;
+
+ public function __construct($login, $emailAddress, $numFailures)
+ {
+ parent::__construct();
+
+ $this->login = $login;
+ $this->emailAddress = $emailAddress;
+ $this->numFailures = (int)$numFailures;
+
+ $this->setUpEmail();
+ }
+
+ /**
+ * @return string
+ */
+ public function getLogin()
+ {
+ return $this->login;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEmailAddress()
+ {
+ return $this->emailAddress;
+ }
+
+ /**
+ * @return int
+ */
+ public function getNumFailures()
+ {
+ return $this->numFailures;
+ }
+
+ private function setUpEmail()
+ {
+ $this->setDefaultFromPiwik();
+ $this->addTo($this->emailAddress);
+ $this->setSubject($this->getDefaultSubject());
+ $this->setReplyTo($this->getFrom());
+ $this->setWrappedHtmlBody($this->getDefaultBodyView());
+ }
+
+ private function getDefaultSubject()
+ {
+ return Piwik::translate('CoreAdminHome_TrackingFailuresEmailSubject');
+ }
+
+ private function getDefaultBodyView()
+ {
+ $view = new View('@CoreAdminHome/_trackingFailuresEmail.twig');
+ $view->login = $this->login;
+ $view->emailAddress = $this->emailAddress;
+ $view->numFailures = $this->numFailures;
+
+ $sitesId = Access::getInstance()->getSitesIdWithAtLeastViewAccess();
+ $idSite = false;
+ if (!empty($sitesId)) {
+ $idSite = array_shift($sitesId);
+ }
+ $view->trackingFailuresUrl = SettingsPiwik::getPiwikUrl() . 'index.php?' . Url::getQueryStringFromParameters([
+ 'module' => 'CoreAdminHome',
+ 'action' => 'trackingFailures',
+ 'period' => 'day',
+ 'date' => 'yesterday',
+ 'idSite' => $idSite
+ ]);
+ return $view;
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/Menu.php b/plugins/CoreAdminHome/Menu.php
index 49a5579636..8ec288ec6c 100644
--- a/plugins/CoreAdminHome/Menu.php
+++ b/plugins/CoreAdminHome/Menu.php
@@ -36,6 +36,12 @@ class Menu extends \Piwik\Plugin\Menu
$this->urlForAction('trackingCodeGenerator'),
$order = 12);
}
+
+ if (Piwik::isUserHasSomeAdminAccess()) {
+ $menu->addDiagnosticItem('CoreAdminHome_TrackingFailures',
+ $this->urlForAction('trackingFailures'),
+ $order = 2);
+ }
}
public function configureTopMenu(MenuTop $menu)
diff --git a/plugins/CoreAdminHome/Tasks.php b/plugins/CoreAdminHome/Tasks.php
index 80560dde48..e6c7d1b242 100644
--- a/plugins/CoreAdminHome/Tasks.php
+++ b/plugins/CoreAdminHome/Tasks.php
@@ -18,13 +18,14 @@ use Piwik\Date;
use Piwik\Db;
use Piwik\Http;
use Piwik\Option;
+use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\Emails\JsTrackingCodeMissingEmail;
+use Piwik\Plugins\CoreAdminHome\Emails\TrackingFailuresEmail;
use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
use Piwik\Plugins\SitesManager\SitesManager;
-use Piwik\Scheduler\Schedule\Daily;
-use Piwik\Scheduler\Schedule\Monthly;
use Piwik\Scheduler\Schedule\SpecificTime;
use Piwik\Settings\Storage\Backend\MeasurableSettingsTable;
+use Piwik\Tracker\Failures;
use Piwik\Site;
use Piwik\Tracker\Visit\ReferrerSpamFilter;
use Psr\Log\LoggerInterface;
@@ -43,10 +44,16 @@ class Tasks extends \Piwik\Plugin\Tasks
*/
private $logger;
- public function __construct(ArchivePurger $archivePurger, LoggerInterface $logger)
+ /**
+ * @var Failures
+ */
+ private $trackingFailures;
+
+ public function __construct(ArchivePurger $archivePurger, LoggerInterface $logger, Failures $failures)
{
$this->archivePurger = $archivePurger;
$this->logger = $logger;
+ $this->trackingFailures = $failures;
}
public function schedule()
@@ -60,6 +67,9 @@ class Tasks extends \Piwik\Plugin\Tasks
// lowest priority since tables should be optimized after they are modified
$this->daily('optimizeArchiveTable', null, self::LOWEST_PRIORITY);
+ $this->daily('cleanupTrackingFailures', null, self::LOWEST_PRIORITY);
+ $this->weekly('notifyTrackingFailures', null, self::LOWEST_PRIORITY);
+
if(SettingsPiwik::isInternetEnabled() === true){
$this->weekly('updateSpammerBlacklist');
}
@@ -140,6 +150,36 @@ class Tasks extends \Piwik\Plugin\Tasks
}
/**
+ * To test execute the following command:
+ * `./console core:run-scheduled-tasks "Piwik\Plugins\CoreAdminHome\Tasks.cleanupTrackingFailures"`
+ *
+ * @throws \Exception
+ */
+ public function cleanupTrackingFailures()
+ {
+ // we remove possibly outdated/fixed tracking failures that have not occurred again recently
+ $this->trackingFailures->removeFailuresOlderThanDays(Failures::CLEANUP_OLD_FAILURES_DAYS);
+ }
+
+ /**
+ * To test execute the following command:
+ * `./console core:run-scheduled-tasks "Piwik\Plugins\CoreAdminHome\Tasks.notifyTrackingFailures"`
+ *
+ * @throws \Exception
+ */
+ public function notifyTrackingFailures()
+ {
+ $failures = $this->trackingFailures->getAllFailures();
+ if (!empty($failures)) {
+ $superUsers = Piwik::getAllSuperUserAccessEmailAddresses();
+ foreach ($superUsers as $login => $email) {
+ $email = new TrackingFailuresEmail($login, $email, count($failures));
+ $email->send();
+ }
+ }
+ }
+
+ /**
* @return bool `true` if the purge was executed, `false` if it was skipped.
* @throws \Exception
*/
diff --git a/plugins/CoreAdminHome/Widgets/GetTrackingFailures.php b/plugins/CoreAdminHome/Widgets/GetTrackingFailures.php
new file mode 100644
index 0000000000..c251c3c92f
--- /dev/null
+++ b/plugins/CoreAdminHome/Widgets/GetTrackingFailures.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\CoreAdminHome\Widgets;
+
+use Piwik\API\Request;
+use Piwik\Piwik;
+use Piwik\Widget\Widget;
+use Piwik\Widget\WidgetConfig;
+
+class GetTrackingFailures extends Widget
+{
+ public static function configure(WidgetConfig $config)
+ {
+ $config->setCategoryId('About Matomo');
+ $config->setName('CoreAdminHome_TrackingFailures');
+ $config->setOrder(5);
+
+ if (!Piwik::isUserHasSomeAdminAccess()) {
+ $config->disable();
+ }
+ }
+
+ public function render()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+ $failures = Request::processRequest('CoreAdminHome.getTrackingFailures');
+ $numFailures = count($failures);
+
+ return $this->renderTemplate('getTrackingFailures', array(
+ 'numFailures' => $numFailures
+ ));
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.controller.js b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.controller.js
new file mode 100644
index 0000000000..e007fae6c3
--- /dev/null
+++ b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.controller.js
@@ -0,0 +1,59 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp').controller('TrackingFailuresController', TrackingFailuresController);
+
+ TrackingFailuresController.$inject = ['piwikApi', 'piwik'];
+
+ function TrackingFailuresController(piwikApi, piwik){
+ var self = this;
+ this.failures = [];
+ this.sortColumn = 'idsite';
+ this.sortReverse = false;
+ this.isLoading = false;
+
+ this.changeSortOrder = function (columnToSort) {
+ if (this.sortColumn === columnToSort) {
+ this.sortReverse = !this.sortReverse;
+ } else {
+ this.sortColumn = columnToSort;
+ }
+ };
+
+ this.fetchAll = function () {
+ this.failures = [];
+ this.isLoading = true;
+ piwikApi.fetch({method: 'CoreAdminHome.getTrackingFailures', filter_limit: '-1'}).then(function (failures) {
+ self.failures = failures;
+ self.isLoading = false;
+ }, function () {
+ self.isLoading = false;
+ });
+ };
+
+ this.deleteAll = function () {
+ piwik.helper.modalConfirm('#confirmDeleteAllTrackingFailures', {yes: function () {
+ self.failures = [];
+ piwikApi.fetch({method: 'CoreAdminHome.deleteAllTrackingFailures'}).then(function () {
+ self.fetchAll();
+ });
+ }});
+ };
+
+ this.deleteFailure = function (idSite, idFailure) {
+ piwik.helper.modalConfirm('#confirmDeleteThisTrackingFailure', {yes: function () {
+ self.failures = [];
+ piwikApi.fetch({method: 'CoreAdminHome.deleteTrackingFailure', idSite: idSite, idFailure: idFailure}).then(function () {
+ self.fetchAll();
+ });
+ }});
+ };
+
+ this.fetchAll();
+ }
+
+})();
diff --git a/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.html b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.html
new file mode 100644
index 0000000000..07e4baa895
--- /dev/null
+++ b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.html
@@ -0,0 +1,59 @@
+<div piwik-content-block
+ content-title="{{ 'CoreAdminHome_TrackingFailures'|translate }}"
+ class="matomoTrackingFailures">
+ <p>
+ {{ 'CoreAdminHome_TrackingFailuresIntroduction'|translate:2 }}
+ <br /><br />
+ <input class="btn deleteAllFailures"
+ ng-show="!trackingFailures.isLoading && trackingFailures.failures.length > 0"
+ type="button" ng-click="trackingFailures.deleteAll();"
+ value="{{'CoreAdminHome_DeleteAllFailures'|translate}}">
+ </p>
+
+ <div piwik-activity-indicator loading="trackingFailures.isLoading"></div>
+
+ <table piwik-content-table>
+ <thead>
+ <tr>
+ <th ng-click="trackingFailures.changeSortOrder('idsite')">{{ 'General_Measurable'|translate }}</th>
+ <th ng-click="trackingFailures.changeSortOrder('problem')">{{ 'CoreAdminHome_Problem'|translate }}</th>
+ <th ng-click="trackingFailures.changeSortOrder('solution')">{{ 'CoreAdminHome_Solution'|translate }}</th>
+ <th ng-click="trackingFailures.changeSortOrder('date_first_occurred')">{{ 'General_Date'|translate }}</th>
+ <th ng-click="trackingFailures.changeSortOrder('url')">{{ 'Actions_ColumnPageURL'|translate }}</th>
+ <th ng-click="trackingFailures.changeSortOrder('request_url')">{{ 'CoreAdminHome_TrackingURL'|translate }}</th>
+ <th class="action">{{ 'General_Action'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr><td colspan="7" ng-show="!trackingFailures.isLoading && trackingFailures.failures.length == 0">{{'CoreAdminHome_NoKnownFailures'|translate}} <span class="icon-ok"></span></td></tr>
+ <tr ng-repeat="failure in trackingFailures.failures | orderBy:trackingFailures.sortColumn:trackingFailures.sortReverse">
+ <td>{{ failure.site_name }} ({{'General_Id'|translate}} {{ failure.idsite }})</td>
+ <td>{{ failure.problem }}</td>
+ <td>{{ failure.solution }} <a ng-show="failure.solution_url" rel="noopener noreferrer" ng-href="{{ failure.solution_url }}">{{'CoreAdminHome_LearnMore'|translate }}</a></td>
+ <td class="datetime">{{ failure.pretty_date_first_occurred }}</td>
+ <td>{{ failure.url }}</td>
+ <td><span ng-show="!failure.showFullRequestUrl" title="{{'CoreHome_ClickToSeeFullInformation'|translate}}"
+ ng-click="failure.showFullRequestUrl = true">{{ failure.request_url|limitTo:100 }}...</span>
+ <span ng-show="failure.showFullRequestUrl">{{ failure.request_url }}</span></td>
+ <td><span class="table-action icon-delete"
+ title="{{'General_Delete'|translate}}"
+ ng-click="trackingFailures.deleteFailure(failure.idsite, failure.idfailure)"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="ui-confirm" id="confirmDeleteAllTrackingFailures">
+ <h2>{{ 'CoreAdminHome_ConfirmDeleteAllTrackingFailures'|translate }}</h2>
+
+ <input type="button" value="{{ 'General_Yes'|translate }}" role="yes"/>
+ <input type="button" value="{{ 'General_No'|translate }}" role="no" />
+ </div>
+
+ <div class="ui-confirm" id="confirmDeleteThisTrackingFailure">
+ <h2>{{ 'CoreAdminHome_ConfirmDeleteThisTrackingFailure'|translate }}</h2>
+
+ <input type="button" value="{{ 'General_Yes'|translate }}" role="yes"/>
+ <input type="button" value="{{ 'General_No'|translate }}" role="no" />
+ </div>
+
+</div> \ No newline at end of file
diff --git a/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.js b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.js
new file mode 100644
index 0000000000..910e5278d7
--- /dev/null
+++ b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.js
@@ -0,0 +1,25 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <div matomo-tracking-failures>
+ */
+(function () {
+ angular.module('piwikApp').directive('matomoTrackingFailures', matomoTrackingFailures);
+
+ matomoTrackingFailures.$inject = ['piwik'];
+
+ function matomoTrackingFailures(piwik){
+ return {
+ restrict: 'A',
+ templateUrl: 'plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.html?cb=' + piwik.cacheBuster,
+ controller: 'TrackingFailuresController',
+ controllerAs: 'trackingFailures'
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.less b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.less
new file mode 100644
index 0000000000..505b106431
--- /dev/null
+++ b/plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.less
@@ -0,0 +1,9 @@
+.matomoTrackingFailures {
+ .icon-delete,
+ th:not(.action) {
+ cursor: pointer;
+ }
+ th.action {
+ width: 60px;
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/config/test.php b/plugins/CoreAdminHome/config/test.php
new file mode 100644
index 0000000000..13a2080dae
--- /dev/null
+++ b/plugins/CoreAdminHome/config/test.php
@@ -0,0 +1,30 @@
+<?php
+use \Piwik\Tracker\Request;
+use \Piwik\Tracker\Failures;
+
+return array(
+
+ 'Piwik\Tracker\Failures' => DI\decorate(function ($previous) {
+ /** @var Failures $previous */
+
+ $generate = \Piwik\Container\StaticContainer::get('test.vars.generateTrackingFailures');
+ if ($generate) {
+ $previous->setNow(\Piwik\Date::factory('2018-07-07 01:02:03'));
+ $previous->logFailure(Failures::FAILURE_ID_INVALID_SITE, new Request(array(
+ 'idsite' => 998, 'rec' => '1'
+ )));
+ $previous->logFailure(Failures::FAILURE_ID_NOT_AUTHENTICATED, new Request(array(
+ 'idsite' => 1,
+ 'url' => 'https://www.example.com/foo/bar?x=1',
+ 'action_name' => 'foobar',
+ 'rec' => '1'
+ )));
+ $previous->logFailure(Failures::FAILURE_ID_INVALID_SITE, new Request(array(
+ 'idsite' => 999, 'rec' => '1'
+ )));
+ }
+
+ return $previous;
+ }),
+
+); \ No newline at end of file
diff --git a/plugins/CoreAdminHome/lang/en.json b/plugins/CoreAdminHome/lang/en.json
index 25ae8e094f..ac3328f395 100644
--- a/plugins/CoreAdminHome/lang/en.json
+++ b/plugins/CoreAdminHome/lang/en.json
@@ -82,6 +82,7 @@
"PluginSettingsValueNotAllowed": "The value for field \"%1$s\" in plugin \"%2$s\" is not allowed",
"PluginSettingsSaveFailed": "Failed to save plugin settings",
"PluginSettingsSaveSuccess": "Plugin settings updated.",
+ "TrackingFailures": "Tracking failures",
"SettingsSaveSuccess": "Settings updated.",
"SendPluginUpdateCommunication": "Send an email when a plugin update is available",
"SendPluginUpdateCommunicationHelp": "An email will be sent to Super Users when there is a new version available for a plugin.",
@@ -108,6 +109,24 @@
"MissingTrackingCodeEmailSubject": "No traffic for %s recorded in Matomo Analytics, get started now",
"JsTrackingCodeMissingEmail1": "A few days ago you added the website '%s' to your Matomo Analytics. We just checked and your Matomo doesn't seem to have any recorded traffic for this website.",
"JsTrackingCodeMissingEmail2": "To begin tracking data and getting insights into your users, you'll need to setup tracking in your website or mobile app. For websites simply embed the tracking code right before the %s tag.",
- "JsTrackingCodeMissingEmail3": "To find and customize your tracking code, %1$sclick here%2$s (or have a look at the %3$sJavaScript Tracking Client guide%4$s)."
+ "JsTrackingCodeMissingEmail3": "To find and customize your tracking code, %1$sclick here%2$s (or have a look at the %3$sJavaScript Tracking Client guide%4$s).",
+ "TrackingFailuresIntroduction": "This page lists tracking failures that happened during the last %s days. Please note that only the most common kind of tracking failures are recorded and not all of them.",
+ "NoKnownFailures": "There are no known tracking failures.",
+ "Problem": "Problem",
+ "Solution": "Solution",
+ "TrackingURL": "Tracking URL",
+ "LearnMore": "Learn more",
+ "DeleteAllFailures": "Delete all failures",
+ "NTrackingFailures": "%s tracking failures",
+ "ViewAllTrackingFailures": "View all tracking failures",
+ "TrackingFailureInvalidSiteProblem": "The site does not exist.",
+ "TrackingFailureInvalidSiteSolution": "Update the configured idSite in the tracker.",
+ "TrackingFailureAuthenticationProblem": "Request was not authenticated but authentication was required.",
+ "TrackingFailureAuthenticationSolution": "Set or correct a \"token_auth\" in your tracking request.",
+ "ConfirmDeleteAllTrackingFailures": "Are you sure you want to delete all tracking failures?",
+ "ConfirmDeleteThisTrackingFailure": "Are you sure you want to delete this tracking failure?",
+ "TrackingFailuresEmailSubject": "Tracking failures in your Matomo Analytics",
+ "TrackingFailuresEmail1": "This is just to let you know that %s different kinds of tracking failures have occurred in the last days.",
+ "TrackingFailuresEmail2": "To view all the failed tracking requests %1$sclick here%2$s."
}
}
diff --git a/plugins/CoreAdminHome/templates/_trackingFailuresEmail.twig b/plugins/CoreAdminHome/templates/_trackingFailuresEmail.twig
new file mode 100644
index 0000000000..8be51c0e0d
--- /dev/null
+++ b/plugins/CoreAdminHome/templates/_trackingFailuresEmail.twig
@@ -0,0 +1,3 @@
+<p>{{ 'General_HelloUser'|translate(login) }}</p>
+<p>{{ 'CoreAdminHome_TrackingFailuresEmail1'|translate('<strong>'~numFailures~'</strong>')|raw }}</p>
+<p>{{ 'CoreAdminHome_TrackingFailuresEmail2'|translate('<a href="'~trackingFailuresUrl~'">', '</a>')|raw }}</p>
diff --git a/plugins/CoreAdminHome/templates/getTrackingFailures.twig b/plugins/CoreAdminHome/templates/getTrackingFailures.twig
new file mode 100644
index 0000000000..3096e65c2f
--- /dev/null
+++ b/plugins/CoreAdminHome/templates/getTrackingFailures.twig
@@ -0,0 +1,13 @@
+<div class="widgetBody system-check">
+ {% if numFailures == 0 %}
+ <p class="system-success"><span class="icon-ok"></span> {{ 'CoreAdminHome_NoKnownFailures'|translate }}</p>
+ {% else %}
+ <p class="system-errors">
+ <span style="font-size: 16px;"><span class="icon-error"></span> {{ 'CoreAdminHome_NTrackingFailures'|translate(numFailures) }}</span>
+ </p>
+ <p>
+ <a href="{{ linkTo({'module': 'CoreAdminHome', 'action': 'trackingFailures'}) }}"
+ >{{ 'CoreAdminHome_ViewAllTrackingFailures'|translate }}</a>
+ </p>
+ {% endif %}
+</div> \ No newline at end of file
diff --git a/plugins/CoreAdminHome/templates/home.twig b/plugins/CoreAdminHome/templates/home.twig
index 58c3288c19..0c52fdf9c1 100644
--- a/plugins/CoreAdminHome/templates/home.twig
+++ b/plugins/CoreAdminHome/templates/home.twig
@@ -21,9 +21,14 @@
<div class="col s12 {% if isFeedbackEnabled %}m4{% else %}m6{% endif %}">
<div piwik-widget-loader='{"module":"CoreHome","action":"getSystemSummary"}'></div>
</div>
- {% if hasDiagnostics %}
+ {% if hasDiagnostics or hasTrackingFailures %}
<div class="col s12 {% if isFeedbackEnabled %}m4{% else %}m6{% endif %}">
+ {% if hasDiagnostics %}
<div piwik-widget-loader='{"module":"Installation","action":"getSystemCheck"}'></div>
+ {% endif %}
+ {% if hasTrackingFailures %}
+ <div piwik-widget-loader='{"module":"CoreAdminHome","action":"getTrackingFailures"}'></div>
+ {% endif %}
</div>
{% endif %}
{% if isFeedbackEnabled %}
diff --git a/plugins/CoreAdminHome/templates/trackingFailures.twig b/plugins/CoreAdminHome/templates/trackingFailures.twig
new file mode 100644
index 0000000000..36c2347e2e
--- /dev/null
+++ b/plugins/CoreAdminHome/templates/trackingFailures.twig
@@ -0,0 +1,8 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'CoreAdminHome_TrackingFailures'|translate }}{% endset %}
+
+{% block content %}
+ <div matomo-tracking-failures>
+ </div>
+{% endblock %}
diff --git a/plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php b/plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php
new file mode 100644
index 0000000000..f8ab0cb808
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreAdminHome\tests\Fixture;
+
+use Piwik\Date;
+use Piwik\Tests\Framework\Fixture;
+
+class TrackingFailures extends Fixture
+{
+ public $idSite = 1;
+ public $dateTime = '2013-01-02 03:04:05';
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ Fixture::createSuperUser();
+ if (!self::siteCreated($this->idSite)) {
+ Fixture::createWebsite('2014-01-02 03:04:05');
+ }
+ $this->trackData();
+ }
+
+ private function trackData()
+ {
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+ self::checkResponse($t->doTrackPageView('Valid Site'));
+
+ $t = self::getTracker(99999, Date::now()->getDatetime(), $defaultInit = true);
+
+ for ($i = 0; $i < 2; $i++) {
+ // we trigger it multiple times to test it will be inserted only once
+ $t->doTrackPageView('Invalid Site');
+ }
+
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+ $t->setIp('10.11.12.13');
+ $t->setTokenAuth('foobar'); // wrong token
+ $t->doTrackPageView('Invalid Token');
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php b/plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php
new file mode 100644
index 0000000000..b5c7fcf711
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreAdminHome\tests\Fixtures;
+
+use Piwik\Date;
+use Piwik\Tests\Framework\Fixture;
+
+/**
+ * Generates tracker testing data for our TrackingFailuresTest
+ *
+ * This Simple fixture adds one website and tracks one visit with couple pageviews and an ecommerce conversion
+ */
+class SimpleFixtureTrackFewVisits extends Fixture
+{
+ public $dateTime = '2013-01-23 01:23:45';
+ public $idSite = 1;
+
+ public function setUp()
+ {
+ $this->setUpWebsite();
+ $this->trackFirstVisit();
+ $this->trackSecondVisit();
+ }
+
+ public function tearDown()
+ {
+ // empty
+ }
+
+ private function setUpWebsite()
+ {
+ if (!self::siteCreated($this->idSite)) {
+ $idSite = self::createWebsite($this->dateTime, $ecommerce = 1);
+ $this->assertSame($this->idSite, $idSite);
+ }
+ }
+
+ protected function trackFirstVisit()
+ {
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+ $t->setUrl('http://example.com/');
+ self::checkResponse($t->doTrackPageView('Viewing homepage'));
+
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime());
+ $t->setUrl('http://example.com/sub/page');
+ self::checkResponse($t->doTrackPageView('Second page view'));
+
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.25)->getDatetime());
+ $t->addEcommerceItem($sku = 'SKU_ID', $name = 'Test item!', $category = 'Test & Category', $price = 777, $quantity = 33);
+ self::checkResponse($t->doTrackEcommerceOrder('TestingOrder', $grandTotal = 33 * 77));
+ }
+
+ protected function trackSecondVisit()
+ {
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+ $t->setIp('56.11.55.73');
+
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+ $t->setUrl('http://example.com/sub/page');
+ self::checkResponse($t->doTrackPageView('Viewing homepage'));
+
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime());
+ $t->setUrl('http://example.com/?search=this is a site search query');
+ self::checkResponse($t->doTrackPageView('Site search query'));
+
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.3)->getDatetime());
+ $t->addEcommerceItem($sku = 'SKU_ID2', $name = 'A durable item', $category = 'Best seller', $price = 321);
+ self::checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 33 * 77));
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/Integration/APITest.php b/plugins/CoreAdminHome/tests/Integration/APITest.php
new file mode 100644
index 0000000000..a3ddf2a182
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Integration/APITest.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\tests\Integration;
+
+use Piwik\Plugins\CoreAdminHome\API;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\Mock\FakeAccess;
+
+/**
+ * @group CoreAdminHome
+ * @group APITest
+ * @group API
+ * @group Plugins
+ */
+class APITest extends \Piwik\Tests\Framework\TestCase\IntegrationTestCase
+{
+ /**
+ * @var int
+ */
+ private $idSite;
+
+ /**
+ * @var API
+ */
+ private $api;
+
+ public function setUp()
+ {
+ parent::setUp();
+ $this->api = API::getInstance();
+ for ($i = 0; $i < 5; $i++) {
+ Fixture::createWebsite('2014-01-02 03:04:05');
+ }
+ }
+
+ /**
+ * @expectedException \Piwik\NoAccessException
+ * @expectedExceptionMessage checkUserHasSomeAdminAccess
+ */
+ public function test_getTrackingFailures_failsForViewUser()
+ {
+ $this->setUser();
+ $this->api->getTrackingFailures();
+ }
+
+ public function test_getTrackingFailures_WorksForAdminAndSuperuser()
+ {
+ $this->setAdminUser();
+ $this->assertSame(array(), $this->api->getTrackingFailures());
+ $this->setSuperUser();
+ $this->api->getTrackingFailures();
+ $this->assertSame(array(), $this->api->getTrackingFailures());
+ }
+
+ /**
+ * @expectedException \Piwik\NoAccessException
+ * @expectedExceptionMessage checkUserHasSomeAdminAccess
+ */
+ public function test_deleteAllTrackingFailures_failsForViewUser()
+ {
+ $this->setUser();
+ $this->api->deleteAllTrackingFailures();
+ }
+
+ public function test_deleteAllTrackingFailures_WorksForAdminAndSuperuser()
+ {
+ $this->setAdminUser();
+ $this->api->deleteAllTrackingFailures();
+ $this->setSuperUser();
+ $this->api->deleteAllTrackingFailures();
+ }
+
+ /**
+ * @expectedException \Piwik\NoAccessException
+ * @expectedExceptionMessage checkUserHasAdminAccess
+ */
+ public function test_deleteTrackingFailure_failsForViewUser()
+ {
+ $this->setUser();
+ $this->api->deleteTrackingFailure(1, 2);
+ }
+
+ /**
+ * @expectedException \Piwik\NoAccessException
+ * @expectedExceptionMessage checkUserHasAdminAccess
+ */
+ public function test_deleteTrackingFailure_failsForAdminUserIfNotAdminAccessToThatSite()
+ {
+ $this->setAdminUser();
+ $this->api->deleteTrackingFailure(2, 2);
+ }
+
+ public function test_deleteTrackingFailure_WorksForAdminAndSuperuser()
+ {
+ $this->setAdminUser();
+ $this->api->deleteTrackingFailure(1, 2);
+ $this->setSuperUser();
+ $this->api->deleteTrackingFailure(1, 2);
+ }
+
+ protected function setSuperUser()
+ {
+ FakeAccess::clearAccess(true);
+ }
+
+ protected function setUser()
+ {
+ FakeAccess::clearAccess(false);
+ FakeAccess::$identity = 'testUser';
+ FakeAccess::$idSitesView = array(1,3, $this->idSite);
+ FakeAccess::$idSitesAdmin = array();
+ }
+
+ protected function setAdminUser()
+ {
+ FakeAccess::clearAccess(false);
+ FakeAccess::$identity = 'testUser';
+ FakeAccess::$idSitesView = array();
+ FakeAccess::$idSitesAdmin = array(1,3, $this->idSite);
+ }
+
+ public function provideContainerConfig()
+ {
+ return array(
+ 'Piwik\Access' => new FakeAccess()
+ );
+ }
+}
diff --git a/plugins/CoreAdminHome/tests/Integration/TasksTest.php b/plugins/CoreAdminHome/tests/Integration/TasksTest.php
index d3ffc66518..ffab94c01d 100644
--- a/plugins/CoreAdminHome/tests/Integration/TasksTest.php
+++ b/plugins/CoreAdminHome/tests/Integration/TasksTest.php
@@ -14,12 +14,15 @@ use Piwik\Date;
use Piwik\Db;
use Piwik\Mail;
use Piwik\Plugins\CoreAdminHome\Emails\JsTrackingCodeMissingEmail;
+use Piwik\Plugins\CoreAdminHome\Emails\TrackingFailuresEmail;
use Piwik\Plugins\CoreAdminHome\Tasks;
use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
use Piwik\Scheduler\Task;
use Piwik\Tests\Fixtures\RawArchiveDataWithTempAndInvalidated;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Tracker\Failures;
+use Piwik\Tracker\Request;
use Psr\Log\NullLogger;
/**
@@ -66,7 +69,7 @@ class TasksTest extends IntegrationTestCase
$archivePurger->setYesterdayDate(Date::factory('2015-02-26'));
$archivePurger->setNow(Date::factory('2015-02-27 08:00:00')->getTimestamp());
- $this->tasks = new Tasks($archivePurger, new NullLogger());
+ $this->tasks = new Tasks($archivePurger, new NullLogger(), new Failures());
$this->mail = null;
}
@@ -129,6 +132,8 @@ class TasksTest extends IntegrationTestCase
'purgeOutdatedArchives.',
'purgeInvalidatedArchives.',
'optimizeArchiveTable.',
+ 'cleanupTrackingFailures.',
+ 'notifyTrackingFailures.',
'updateSpammerBlacklist.',
'checkSiteHasTrackedVisits.2',
'checkSiteHasTrackedVisits.3',
@@ -181,6 +186,35 @@ class TasksTest extends IntegrationTestCase
$this->assertEquals($mail->getIdSite(), $idSite);
}
+ public function test_cleanupTrackingFailures_doesNotCauseAnyException()
+ {
+ // it is only calling one method which is already tested... no need to write complex tests for it
+ $this->tasks->cleanupTrackingFailures();
+ $this->assertTrue(true);
+ }
+
+ public function test_notifyTrackingFailures_doesNotSendAnyMailWhenThereAreNoTrackingRequests()
+ {
+ $this->tasks->notifyTrackingFailures();
+ $this->assertNull($this->mail);
+ }
+
+ public function test_notifyTrackingFailures_sendsMailWhenThereAreTrackingFailures()
+ {
+ $failures = new Failures();
+ $failures->logFailure(1, new Request(array('idsite' => 9999, 'rec' => 1)));
+ $failures->logFailure(1, new Request(array('idsite' => 9998, 'rec' => 1)));
+ Fixture::createSuperUser(false);
+ $this->tasks->notifyTrackingFailures();
+
+ /** @var TrackingFailuresEmail $mail */
+ $mail = $this->mail;
+ $this->assertInstanceOf(TrackingFailuresEmail::class, $mail);
+ $this->assertEquals('superUserLogin', $mail->getLogin());
+ $this->assertEquals('hello@example.org', $mail->getEmailAddress());
+ $this->assertEquals(2, $mail->getNumFailures());
+ }
+
/**
* @param Date[] $dates
*/
diff --git a/plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php b/plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php
new file mode 100644
index 0000000000..061c819e60
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\tests\System;
+
+use Piwik\Plugins\CoreAdminHome\tests\Fixture\TrackingFailures;
+use Piwik\Tests\Framework\TestCase\SystemTestCase;
+
+/**
+ * @group CoreAdminHome
+ * @group TrackingFailuresTest
+ * @group Plugins
+ */
+class TrackingFailuresTest extends SystemTestCase
+{
+ /**
+ * @var TrackingFailures
+ */
+ public static $fixture = null; // initialized below class definition
+
+ /**
+ * @dataProvider getApiForTesting
+ */
+ public function testApi($api, $params)
+ {
+ $params['xmlFieldsToRemove'] = array('date_first_occurred', 'pretty_date_first_occurred', 'request_url');
+ $this->runApiTests($api, $params);
+ }
+
+ public function getApiForTesting()
+ {
+ $api = array(
+ 'CoreAdminHome.getTrackingFailures',
+ );
+
+ $apiToTest = array();
+ $apiToTest[] = array($api,
+ array(
+ 'testSuffix' => ''
+ )
+ );
+
+ return $apiToTest;
+ }
+
+ public static function getOutputPrefix()
+ {
+ return '';
+ }
+
+ public static function getPathToTestDirectory()
+ {
+ return dirname(__FILE__);
+ }
+
+}
+
+TrackingFailuresTest::$fixture = new TrackingFailures(); \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml b/plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml
new file mode 100644
index 0000000000..511339901f
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <idsite>1</idsite>
+ <idfailure>2</idfailure>
+
+
+ <site_name>Piwik test</site_name>
+
+ <url>http://example.com/piwik/</url>
+ <problem>Request was not authenticated but authentication was required.</problem>
+ <solution>Set or correct a &quot;token_auth&quot; in your tracking request.</solution>
+ <solution_url>https://matomo.org/faq/how-to/faq_30835/</solution_url>
+ </row>
+ <row>
+ <idsite>99999</idsite>
+ <idfailure>1</idfailure>
+
+
+ <site_name>Unknown</site_name>
+
+ <url>http://example.com/piwik/</url>
+ <problem>The site does not exist.</problem>
+ <solution>Update the configured idSite in the tracker.</solution>
+ <solution_url>https://matomo.org/faq/how-to/faq_30838/</solution_url>
+ </row>
+</result> \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js b/plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js
new file mode 100644
index 0000000000..f98f1c0646
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js
@@ -0,0 +1,94 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("TrackingFailures", function () {
+ this.timeout(0);
+
+ var manageUrl = '?module=CoreAdminHome&action=trackingFailures&idSite=1&period=day&date=today';
+ var widgetUrl = '?module=Widgetize&action=iframe&moduleToWidgetize=CoreAdminHome&actionToWidgetize=getTrackingFailures&idSite=1&period=day&date=today&widget=1';
+
+ function captureScreen(done, screenshotName, theTest)
+ {
+ expect.screenshot(screenshotName).to.be.captureSelector('.matomoTrackingFailures', theTest, done);
+ }
+
+ function captureModal(done, screenshotName, theTest)
+ {
+ expect.screenshot(screenshotName).to.be.captureSelector('.modal.open', theTest, done);
+ }
+
+ function generateTrackingFailures()
+ {
+ testEnvironment.generateTrackingFailures = 1;
+ testEnvironment.save();
+ }
+
+ function confirmModal(page)
+ {
+ page.click('.modal.open .modal-footer a:contains(Yes)');
+ }
+
+ afterEach(function () {
+ delete testEnvironment.generateTrackingFailures;
+ testEnvironment.save();
+ });
+
+ it('should show widget with no failures', function (done) {
+ captureScreen(done, 'widget_no_failures', function (page) {
+ page.load(widgetUrl);
+ });
+ });
+
+ it('should show manage page with no failures', function (done) {
+ captureScreen(done, 'manage_no_failures', function (page) {
+ page.load(manageUrl);
+ });
+ });
+
+ it('should show widget with failures', function (done) {
+ generateTrackingFailures();
+ captureScreen(done, 'widget_with_failures', function (page) {
+ generateTrackingFailures();
+ page.load(widgetUrl);
+ });
+ });
+
+ it('should show manage page with failures', function (done) {
+ generateTrackingFailures();
+ captureScreen(done, 'manage_with_failures', function (page) {
+ generateTrackingFailures();
+ page.load(manageUrl);
+ });
+ });
+
+ it('should show ask to confirm delete one', function (done) {
+ captureModal(done, 'manage_with_failures_delete_one_ask_confirmation', function (page) {
+ page.evaluate(function () {
+ $('.matomoTrackingFailures table tbody tr:nth-child(2) .icon-delete').click()
+ });
+ });
+ });
+
+ it('should show delete when confirmed', function (done) {
+ captureScreen(done, 'manage_with_failures_delete_one_confirmed', function (page) {
+ confirmModal(page);
+ });
+ });
+
+ it('should show ask to confirm delete all', function (done) {
+ captureModal(done, 'manage_with_failures_delete_all_ask_confirmation', function (page) {
+ page.click('.matomoTrackingFailures .deleteAllFailures');
+ });
+ });
+
+ it('should show ask to confirm delete one', function (done) {
+ captureScreen(done, 'manage_with_failures_delete_all_confirmed', function (page) {
+ confirmModal(page);
+ });
+ });
+
+}); \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png
new file mode 100644
index 0000000000..3652d932ca
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:555956386a3960079e98fd0ed955ccf836d5aef1e9883f330ef08e43c496f3ff
+size 25706
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png
new file mode 100644
index 0000000000..3920b896ea
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:164fc892e74d90ff316f76cc424dbb00e8af9f8ff5c487deaca4df67fb3b058d
+size 75105
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png
new file mode 100644
index 0000000000..d087655199
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7a8c736bf8efa2dde7c14609ae473fddd4c09f616cbfa1d794bddcc01d12b707
+size 9554
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png
new file mode 100644
index 0000000000..3652d932ca
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:555956386a3960079e98fd0ed955ccf836d5aef1e9883f330ef08e43c496f3ff
+size 25706
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png
new file mode 100644
index 0000000000..372005bf86
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3a773a094fe4a81c031a71ad279ccda03cee6f3b8139ed6bbaac27c6ea8ff7c4
+size 9585
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png
new file mode 100644
index 0000000000..237b4c9a7a
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:32619750eb571199b8fb5b0761efe64d46f9db3dd090b7f8bbc7a1151aa5e295
+size 48513
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png
new file mode 100644
index 0000000000..c610efccb2
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8151ae81cc83466e763346f02b272dc7ec480832d49807f59cfb32a53630da6a
+size 11165
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png
new file mode 100644
index 0000000000..fc40fbe9b1
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1a98049c33bb5cab09436985b5cf062a931560028014f8c9f8b1ccc0d50c3af8
+size 12805
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php
index 76e933ae51..ab11a468eb 100644
--- a/plugins/CoreHome/CoreHome.php
+++ b/plugins/CoreHome/CoreHome.php
@@ -287,6 +287,8 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CoreAdminHome/angularjs/trackingcode/jstrackingcode.controller.js";
$jsFiles[] = "plugins/CoreAdminHome/angularjs/trackingcode/imagetrackingcode.controller.js";
$jsFiles[] = "plugins/CoreAdminHome/angularjs/archiving/archiving.controller.js";
+ $jsFiles[] = "plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.controller.js";
+ $jsFiles[] = "plugins/CoreAdminHome/angularjs/trackingfailures/trackingfailures.directive.js";
// we have to load these CorePluginsAdmin files here. If we loaded them in CorePluginsAdmin,
// there would be JS errors as CorePluginsAdmin is loaded first. Meaning it is loaded before
diff --git a/plugins/CoreHome/Tracker/VisitRequestProcessor.php b/plugins/CoreHome/Tracker/VisitRequestProcessor.php
index 033399e196..c2a88e93d4 100644
--- a/plugins/CoreHome/Tracker/VisitRequestProcessor.php
+++ b/plugins/CoreHome/Tracker/VisitRequestProcessor.php
@@ -88,7 +88,10 @@ class VisitRequestProcessor extends RequestProcessor
$visitProperties->setProperty('location_ip', $request->getIp());
$excluded = new VisitExcluded($request);
- if ($excluded->isExcluded()) {
+ $isExcluded = $excluded->isExcluded();
+ $request->setMetadata('CoreHome', 'isVisitExcluded', $isExcluded);
+
+ if ($isExcluded) {
return true;
}
diff --git a/plugins/CustomVariables/tests/UI/expected-screenshots/CustomVariables_link_in_menu.png b/plugins/CustomVariables/tests/UI/expected-screenshots/CustomVariables_link_in_menu.png
index 62e1cb3583..ea9a6fe395 100644
--- a/plugins/CustomVariables/tests/UI/expected-screenshots/CustomVariables_link_in_menu.png
+++ b/plugins/CustomVariables/tests/UI/expected-screenshots/CustomVariables_link_in_menu.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:08217344adea43733531372ee479b5f11dc6f67d7ac40f07284e8feaae5b3c8a
-size 14959
+oid sha256:f7798c4e443bd838de905511c77f82899e44d4d2e990e5623ee2fd0b2a09c79d
+size 16695
diff --git a/plugins/QueuedTracking b/plugins/QueuedTracking
-Subproject 49e30794fcf0a77bac7cef9e5c779e855453549
+Subproject 7609e1bd46b5da8f8e638c01dc71fb782dffdb9
diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php
index 76576f84ed..054ecc2fce 100644
--- a/plugins/SitesManager/SitesManager.php
+++ b/plugins/SitesManager/SitesManager.php
@@ -8,9 +8,11 @@
*/
namespace Piwik\Plugins\SitesManager;
+use Piwik\Access;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Container\StaticContainer;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Piwik;
use Piwik\Plugins\CoreHome\SystemSummary;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
@@ -36,7 +38,8 @@ class SitesManager extends \Piwik\Plugin
return array(
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
- 'Tracker.Cache.getSiteAttributes' => 'recordWebsiteDataInCache',
+ 'Tracker.Cache.getSiteAttributes' => array('function' => 'recordWebsiteDataInCache', 'before' => true),
+ 'Tracker.setTrackerCacheGeneral' => 'setTrackerCacheGeneral',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
'SitesManager.deleteSite.end' => 'onSiteDeleted',
'System.addSystemSummaryItems' => 'addSystemSummaryItems',
@@ -145,13 +148,13 @@ class SitesManager extends \Piwik\Plugin
{
$idSite = (int) $idSite;
+ $website = API::getInstance()->getSiteFromId($idSite);
$urls = API::getInstance()->getSiteUrlsFromId($idSite);
// add the 'hosts' entry in the website array
$array['urls'] = $urls;
$array['hosts'] = $this->getTrackerHosts($urls);
- $website = API::getInstance()->getSiteFromId($idSite);
$array['exclude_unknown_urls'] = $website['exclude_unknown_urls'];
$array['excluded_ips'] = $this->getTrackerExcludedIps($website);
$array['excluded_parameters'] = self::getTrackerExcludedQueryParameters($website);
@@ -165,6 +168,14 @@ class SitesManager extends \Piwik\Plugin
$array['type'] = $website['type'];
}
+ public function setTrackerCacheGeneral(&$cache)
+ {
+ Access::doAsSuperUser(function () use (&$cache) {
+ $cache['global_excluded_user_agents'] = self::filterBlankFromCommaSepList(API::getInstance()->getExcludedUserAgentsGlobal());
+ $cache['global_excluded_ips'] = self::filterBlankFromCommaSepList(API::getInstance()->getExcludedIpsGlobal());
+ });
+ }
+
/**
* Returns whether we should keep URL fragments for a specific site.
*
diff --git a/plugins/Widgetize/tests/System/WidgetTest.php b/plugins/Widgetize/tests/System/WidgetTest.php
index cdf8fe54ff..96b78dd70b 100644
--- a/plugins/Widgetize/tests/System/WidgetTest.php
+++ b/plugins/Widgetize/tests/System/WidgetTest.php
@@ -1016,6 +1016,14 @@ class WidgetTest extends SystemTestCase
'action' => 'getSystemCheck',
),
), array (
+ 'name' => 'Tracking failures',
+ 'uniqueId' => 'widgetCoreAdminHomegetTrackingFailures',
+ 'parameters' =>
+ array (
+ 'module' => 'CoreAdminHome',
+ 'action' => 'getTrackingFailures',
+ ),
+ ), array (
'name' => 'System Summary',
'uniqueId' => 'widgetCoreHomegetSystemSummary',
'parameters' =>
diff --git a/tests/PHPUnit/Fixtures/InvalidVisits.php b/tests/PHPUnit/Fixtures/InvalidVisits.php
index 5888bbeb7a..6f3f44c961 100644
--- a/tests/PHPUnit/Fixtures/InvalidVisits.php
+++ b/tests/PHPUnit/Fixtures/InvalidVisits.php
@@ -53,6 +53,7 @@ class InvalidVisits extends Fixture
API::getInstance()->setSiteSpecificUserAgentExcludeEnabled(true);
API::getInstance()->setGlobalExcludedUserAgents('globalexcludeduseragent');
+ Cache::regenerateCacheWebsiteAttributes([1]);
// Trigger empty request
$trackerUrl = self::getTrackerUrl();
@@ -69,6 +70,7 @@ class InvalidVisits extends Fixture
foreach (array(false, true) as $enable) {
$excludedIp = '154.1.12.34';
API::getInstance()->updateSite($idSite, 'new site name', $url = array('http://site.com'), $ecommerce = 0, $ss = 1, $ss_kwd = '', $ss_cat = '', $excludedIp . ',1.2.3.4', $excludedQueryParameters = null, $timezone = null, $currency = null, $group = null, $startDate = null, $excludedUserAgents = 'excludeduseragentstring');
+ Cache::regenerateCacheWebsiteAttributes([1]);
// Enable IP Anonymization
$t->DEBUG_APPEND_URL = '&forceIpAnonymization=' . (int)$enable;
@@ -105,6 +107,7 @@ class InvalidVisits extends Fixture
$searchKeywordParameters = null, $searchCategoryParameters = null, $excludedIps = null, $excludedQueryParams = null,
$timezone = null, $currency = null, $group = null, $startDate = null, $excludedUserAgents = null,
$keepUrlFragments = null, $type = null, $settings = null, $excludeUnknownUrls = 1);
+ Cache::regenerateCacheWebsiteAttributes([1]);
$t->setIp("125.4.5.6");
@@ -114,11 +117,13 @@ class InvalidVisits extends Fixture
$t->setUrl("http://their.stuff.com/back/to/the/future");
$t->doTrackPageView("ignored, not from my.stuff.com");
+
// undo exclude unknown urls change (important when multiple fixtures are setup together, as is done in OmniFixture)
API::getInstance()->updateSite($idSite, $siteName = null, $urls, $ecommerce = null, $siteSearch = null,
$searchKeywordParameters = null, $searchCategoryParameters = null, $excludedIps = null, $excludedQueryParams = null,
$timezone = null, $currency = null, $group = null, $startDate = null, $excludedUserAgents = null,
$keepUrlFragments = null, $type = null, $settings = null, $excludeUnknownUrls = 0);
+ Cache::regenerateCacheWebsiteAttributes([1]);
try {
@$t->setAttributionInfo(array());
diff --git a/tests/PHPUnit/Integration/Tracker/FailuresTest.php b/tests/PHPUnit/Integration/Tracker/FailuresTest.php
new file mode 100644
index 0000000000..308dfc18ab
--- /dev/null
+++ b/tests/PHPUnit/Integration/Tracker/FailuresTest.php
@@ -0,0 +1,329 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Integration\Tracker;
+
+use Piwik\Date;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Tracker\Failures;
+use Piwik\Tracker\Request;
+
+/**
+ * @group Failures
+ * @group FailuresTest
+ */
+class FailuresTest extends IntegrationTestCase
+{
+ /**
+ * @var Failures
+ */
+ private $failures;
+ private $idSite;
+ /**
+ * @var Date
+ */
+ private $now;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->idSite = Fixture::createWebsite('2018-01-02 03:04:05');
+ Fixture::createWebsite('2018-01-02 03:04:05');
+ $this->now = Date::factory('2018-09-07 01:02:03');
+ $this->failures = new Failures();
+ $this->failures->setNow($this->now);
+ }
+
+ public function test_logFailure_getAllFailures()
+ {
+ $this->logFailure(1, array());
+ $this->logFailure(1, array('idsite' => 9999999)); // unknown idsite
+ $this->logFailure(9999, array()); // unknown failure
+ $this->logFailure(2, array('url' => ''));
+ $this->logFailure(1, array('url' => 'https://www.example.com/page'));
+ $failures = $this->failures->getAllFailures();
+ $this->assertEquals(array (
+ array (
+ 'idsite' => '1',
+ 'idfailure' => '1',
+ 'date_first_occurred' => '2018-09-07 01:02:03',
+ 'request_url' => 'rec=1&idsite=1',
+ 'site_name' => 'Piwik test',
+ 'pretty_date_first_occurred' => 'Intl_1or02Intl_Time_AMt_250Intl_Time_AMtTi02_S1ort',
+ 'url' => '',
+ 'solution_url' => 'https://matomo.org/faq/how-to/faq_30838/',
+ 'problem' => 'CoreAdminHome_TrackingFailureInvalidSiteProblem',
+ 'solution' => 'CoreAdminHome_TrackingFailureInvalidSiteSolution',
+ ),
+ array (
+ 'idsite' => '1',
+ 'idfailure' => '2',
+ 'date_first_occurred' => '2018-09-07 01:02:03',
+ 'request_url' => 'url=&rec=1&idsite=1',
+ 'site_name' => 'Piwik test',
+ 'pretty_date_first_occurred' => 'Intl_1or02Intl_Time_AMt_250Intl_Time_AMtTi02_S1ort',
+ 'url' => '',
+ 'solution_url' => 'https://matomo.org/faq/how-to/faq_30835/',
+ 'problem' => 'CoreAdminHome_TrackingFailureAuthenticationProblem',
+ 'solution' => 'CoreAdminHome_TrackingFailureAuthenticationSolution',
+ ),
+ array (
+ 'idsite' => '1',
+ 'idfailure' => '9999',
+ 'date_first_occurred' => '2018-09-07 01:02:03',
+ 'request_url' => 'rec=1&idsite=1',
+ 'site_name' => 'Piwik test',
+ 'pretty_date_first_occurred' => 'Intl_1or02Intl_Time_AMt_250Intl_Time_AMtTi02_S1ort',
+ 'url' => '',
+ 'problem' => '',
+ 'solution' => '',
+ 'solution_url' => '',
+ ),
+ array (
+ 'idsite' => '9999999',
+ 'idfailure' => '1',
+ 'date_first_occurred' => '2018-09-07 01:02:03',
+ 'request_url' => 'idsite=9999999&rec=1',
+ 'site_name' => 'General_Unknown',
+ 'pretty_date_first_occurred' => 'Intl_1or02Intl_Time_AMt_250Intl_Time_AMtTi02_S1ort',
+ 'url' => '',
+ 'solution_url' => 'https://matomo.org/faq/how-to/faq_30838/',
+ 'problem' => 'CoreAdminHome_TrackingFailureInvalidSiteProblem',
+ 'solution' => 'CoreAdminHome_TrackingFailureInvalidSiteSolution',
+ ),
+ ), $failures);
+ }
+
+ public function test_logFailure_doesNotLogSameFailureTwice()
+ {
+ $expected = array (
+ array (
+ 'idsite' => '1',
+ 'idfailure' => '1',
+ 'date_first_occurred' => '2018-09-07 01:02:03',
+ 'request_url' => 'rec=1&idsite=1',
+ 'site_name' => 'Piwik test',
+ 'pretty_date_first_occurred' => 'Intl_1or02Intl_Time_AMt_250Intl_Time_AMtTi02_S1ort',
+ 'url' => '',
+ 'solution_url' => 'https://matomo.org/faq/how-to/faq_30838/',
+ 'problem' => 'CoreAdminHome_TrackingFailureInvalidSiteProblem',
+ 'solution' => 'CoreAdminHome_TrackingFailureInvalidSiteSolution',
+ )
+ );
+
+ $this->logFailure(1, array());
+ $failures = $this->failures->getAllFailures();
+ $this->assertEquals($expected, $failures);
+
+ $this->logFailure(1, array());
+ $failures = $this->failures->getAllFailures();
+ $this->assertEquals($expected, $failures);
+
+ // does log a different problem for same site
+ $this->logFailure(2, array());
+ $failures = $this->failures->getAllFailures();
+ $this->assertCount(2, $failures);
+
+ // does log a same problem for different site
+ $this->logFailure(1, array('idsite' => 999));
+ $failures = $this->failures->getAllFailures();
+ $this->assertCount(3, $failures);
+ }
+
+ public function test_logFailure_anonymizesTokenWhenParamUsed()
+ {
+ $this->logFailure(1, array('token_auth' => 'foobar', 'token' => 'bar', 'tokenauth' => 'baz'));
+ $failures = $this->failures->getAllFailures();
+ $this->assertEquals('token_auth=__TOKEN_AUTH__&token=__TOKEN_AUTH__&tokenauth=__TOKEN_AUTH__&rec=1&idsite=1', $failures[0]['request_url']);
+ }
+
+ public function test_logFailure_anonymizesTokenWhenMd5ValueUsed()
+ {
+ $this->logFailure(1, array('foo' => md5('foo')));
+ $failures = $this->failures->getAllFailures();
+ $this->assertEquals('foo=__TOKEN_AUTH__&rec=1&idsite=1', $failures[0]['request_url']);
+ }
+
+ public function test_logFailure_anonymizesTokenWhenMd5SimilarValueUsed()
+ {
+ $this->logFailure(1, array('foo' => md5('foo') .'ff'));
+ $failures = $this->failures->getAllFailures();
+ $this->assertEquals('foo=__TOKEN_AUTH__&rec=1&idsite=1', $failures[0]['request_url']);
+ }
+
+ public function test_logFailure_doesNotLogExcludedRequest()
+ {
+ $this->logFailure(1, array('rec' => '0'));
+ $this->assertEquals(array(), $this->failures->getAllFailures());
+ }
+
+ public function test_logFailure_doesNotLogAnyUnusualHighSiteId()
+ {
+ $this->logFailure(1, array('idsite' => '99999999999'));
+ $this->assertEquals(array(), $this->failures->getAllFailures());
+ }
+
+ public function test_logFailure_doesNotLogAnyUnusualLowSiteId()
+ {
+ try {
+ $this->logFailure(1, array('idsite' => '-1'));
+ } catch (UnexpectedWebsiteFoundException $e) {
+ // triggered by $request->getIdSite() in visits excluded... we ignore this error in this test
+ // as it is fine to have this error as long as the failure is not recorded
+ }
+ $this->assertEquals(array(), $this->failures->getAllFailures());
+ }
+
+ public function test_logFailure_canLogEntryForIdSite0()
+ {
+ $this->logFailure(1, array('idsite' => '0'));
+ $this->assertCount(1, $this->failures->getAllFailures());
+ }
+
+ public function test_getAllFailures_noFailuresByDefault()
+ {
+ $this->assertSame(array(), $this->failures->getAllFailures());
+ }
+
+ public function test_getFailuresForSites_noFailuresByDefault()
+ {
+ $this->assertSame(array(), $this->failures->getAllFailures());
+ }
+
+ public function test_getFailuresForSites_returnsOnlyFailuresForGivenSite()
+ {
+ $this->logFailure(1, array('idsite' => 2));
+ $this->logFailure(2, array('idsite' => 2));
+ $this->logFailure(1, array('idsite' => 3));
+ $this->logFailure(2, array('idsite' => 3));
+ $this->logFailure(3, array('idsite' => 3));
+ $this->logFailure(1, array('idsite' => 4));
+ $this->logFailure(2, array('idsite' => 4));
+ $this->logFailure(3, array('idsite' => 4));
+ $this->logFailure(4, array('idsite' => 4));
+ $this->logFailure(1, array('idsite' => 5));
+ $this->logFailure(2, array('idsite' => 5));
+ $this->logFailure(3, array('idsite' => 5));
+ $this->logFailure(4, array('idsite' => 5));
+ $this->logFailure(5, array('idsite' => 5));
+ $this->assertSame(array(), $this->failures->getFailuresForSites(array()));
+ $this->assertCount(2, $this->failures->getFailuresForSites(array(2)));
+ $this->assertCount(3, $this->failures->getFailuresForSites(array(3)));
+ $this->assertCount(7, $this->failures->getFailuresForSites(array(2,5)));
+ $this->assertCount(12, $this->failures->getFailuresForSites(array(4,3,5)));
+ }
+
+ public function test_deleteTrackingFailure()
+ {
+ $this->logFailure(1, array('idsite' => 2));
+ $this->logFailure(2, array('idsite' => 2));
+ $this->logFailure(1, array('idsite' => 3));
+ $this->logFailure(2, array('idsite' => 3));
+ $this->logFailure(3, array('idsite' => 3));
+ $this->assertCount(5, $this->failures->getAllFailures());
+
+ $this->failures->deleteTrackingFailure(3, 2);
+
+ $summary = $this->getFailureSummary();
+ $this->assertEquals(array(
+ array(2,1), array(2,2), array(3,1), array(3,3), // 3,2 is not returned
+ ), $summary);
+ }
+
+ public function test_deleteTrackingFailureWhenWrongIdAllAreKept()
+ {
+ $this->logFailure(1, array('idsite' => 2));
+ $this->logFailure(2, array('idsite' => 2));
+ $this->logFailure(1, array('idsite' => 3));
+ $this->logFailure(2, array('idsite' => 3));
+ $this->logFailure(3, array('idsite' => 3));
+ $this->assertCount(5, $this->failures->getAllFailures());
+
+ $this->failures->deleteTrackingFailure(99999, 2);
+ $this->assertCount(5, $this->failures->getAllFailures());
+ $this->failures->deleteTrackingFailure(2, 9999);
+ $this->assertCount(5, $this->failures->getAllFailures());
+ }
+
+ public function test_deleteAllTrackingFailures()
+ {
+ $this->logFailure(1, array('idsite' => 2));
+ $this->logFailure(2, array('idsite' => 2));
+ $this->logFailure(1, array('idsite' => 3));
+ $this->logFailure(2, array('idsite' => 3));
+ $this->logFailure(3, array('idsite' => 3));
+ $this->assertCount(5, $this->failures->getAllFailures());
+
+ $this->failures->deleteAllTrackingFailures();
+ $this->assertSame([], $this->failures->getAllFailures());
+ }
+
+ public function test_deleteTrackingFailures()
+ {
+ $this->logFailure(1, array('idsite' => 1));
+ $this->logFailure(1, array('idsite' => 2));
+ $this->logFailure(2, array('idsite' => 2));
+ $this->logFailure(1, array('idsite' => 3));
+ $this->logFailure(2, array('idsite' => 3));
+ $this->logFailure(3, array('idsite' => 3));
+ $this->assertCount(6, $this->failures->getAllFailures());
+
+ $this->failures->deleteTrackingFailures(array(1,3));
+ $this->assertEquals([array(2,1), array(2,2)], $this->getFailureSummary());
+ }
+
+ public function test_removeFailuresOlderThanDays()
+ {
+ $this->logFailure(1, array('idsite' => 2));
+ $this->logFailure(2, array('idsite' => 2));
+ $this->logFailure(3, array('idsite' => 2), 1);
+ $this->logFailure(1, array('idsite' => 3), 2);
+ $this->logFailure(2, array('idsite' => 3), 2);
+ $this->logFailure(3, array('idsite' => 3), 3);
+ $this->logFailure(4, array('idsite' => 3), 3);
+ $this->logFailure(5, array('idsite' => 3), 3);
+ $this->logFailure(6, array('idsite' => 3), 4);
+
+ $this->failures->removeFailuresOlderThanDays(2);
+
+ $summary = $this->getFailureSummary();
+ $this->assertEquals(array(
+ array(2,1), array(2,2), array(2,3), array(3,1), array(3,2)
+ ), $summary);
+ }
+
+ private function getFailureSummary()
+ {
+ $failures = $this->failures->getAllFailures();
+
+ $summary = array();
+ foreach ($failures as $failure) {
+ $summary[] = array($failure['idsite'], $failure['idfailure']);
+ }
+ return $summary;
+ }
+
+ private function logFailure($idFailure, $params, $daysAgo = null)
+ {
+ if (!isset($params['rec'])) {
+ $params['rec'] = 1;
+ }
+ if (!isset($params['idsite'])) {
+ $params['idsite'] = $this->idSite;
+ }
+ $request = new Request($params);
+ if (isset($daysAgo)) {
+ $this->failures->setNow($this->now->subDay($daysAgo)->addPeriod(1, 'minute'));
+ }
+ $this->failures->logFailure($idFailure, $request);
+ $this->failures->setNow($this->now);
+ }
+
+} \ No newline at end of file
diff --git a/tests/PHPUnit/Integration/Tracker/RequestSetTest.php b/tests/PHPUnit/Integration/Tracker/RequestSetTest.php
index 6049ad6245..c6f09460b3 100644
--- a/tests/PHPUnit/Integration/Tracker/RequestSetTest.php
+++ b/tests/PHPUnit/Integration/Tracker/RequestSetTest.php
@@ -11,6 +11,7 @@ namespace Piwik\Tests\Integration\Tracker;
use Piwik\EventDispatcher;
use Piwik\Piwik;
use Piwik\Tests\Framework\Fixture;
+use Piwik\Tracker\Request;
use Piwik\Tracker\RequestSet;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
@@ -23,6 +24,11 @@ class TestRequestSet extends RequestSet {
$this->redirectUrl = $url;
}
+ public function getAllSiteIdsWithinRequest()
+ {
+ return parent::getAllSiteIdsWithinRequest();
+ }
+
public function getRedirectUrl()
{
return $this->redirectUrl;
@@ -41,6 +47,7 @@ class RequestSetTest extends IntegrationTestCase
private $requestSet;
private $get;
private $post;
+ private $time;
public function setUp()
{
@@ -49,12 +56,18 @@ class RequestSetTest extends IntegrationTestCase
Fixture::createWebsite('2014-01-01 00:00:00');
Fixture::createWebsite('2014-01-01 00:00:00', 0, false, 'http://www.example.com');
+ foreach (range(3,10) as $idSite) {
+ Fixture::createWebsite('2014-01-01 00:00:00');
+ }
+
$this->requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
$this->requestSet->setRequests(array(array('idsite' => 1), array('idsite' => 2)));
$this->get = $_GET;
$this->post = $_POST;
+ $this->time = time();
+
$_GET = array();
$_POST = array();
}
@@ -67,6 +80,54 @@ class RequestSetTest extends IntegrationTestCase
parent::tearDown();
}
+ public function test_getAllSiteIdsWithinRequest_ShouldReturnEmptyArray_IfNoRequestsSet()
+ {
+ $this->requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
+ $this->assertEquals(array(), $this->requestSet->getAllSiteIdsWithinRequest());
+ }
+
+ public function test_getAllSiteIdsWithinRequest_ShouldReturnTheSiteIds_FromRequests()
+ {
+ $this->requestSet->setRequests($this->buildRequests(3));
+
+ $this->assertEquals(array(1, 2, 3), $this->requestSet->getAllSiteIdsWithinRequest());
+ }
+
+ public function test_getAllSiteIdsWithinRequest_ShouldReturnUniqueSiteIds_Unordered()
+ {
+ $this->requestSet->setRequests(array(
+ $this->buildRequest(1),
+ $this->buildRequest(5),
+ $this->buildRequest(1),
+ $this->buildRequest(2),
+ $this->buildRequest(2),
+ $this->buildRequest(9),
+ ));
+
+ $this->assertEquals(array(1, 5, 2, 9), $this->requestSet->getAllSiteIdsWithinRequest());
+ }
+
+ /**
+ * @param int $numRequests
+ * @return Request[]
+ */
+ private function buildRequests($numRequests)
+ {
+ $requests = array();
+ for ($index = 1; $index <= $numRequests; $index++) {
+ $requests[] = $this->buildRequest($index);
+ }
+ return $requests;
+ }
+
+ private function buildRequest($idsite)
+ {
+ $request = new Request(array('idsite' => ('' . $idsite)));
+ $request->setCurrentTimestamp($this->time);
+
+ return $request;
+ }
+
public function test_shouldPerformRedirectToUrl_shouldNotRedirect_IfNoUrlIsSet()
{
$this->assertFalse($this->requestSet->shouldPerformRedirectToUrl());
diff --git a/tests/PHPUnit/Integration/Tracker/RequestTest.php b/tests/PHPUnit/Integration/Tracker/RequestTest.php
index 647acf1100..1498cb0c90 100644
--- a/tests/PHPUnit/Integration/Tracker/RequestTest.php
+++ b/tests/PHPUnit/Integration/Tracker/RequestTest.php
@@ -8,6 +8,7 @@
namespace Piwik\Tests\Integration\Tracker;
+use Piwik\Network\IPUtils;
use Piwik\Piwik;
use Piwik\Plugins\CustomVariables\CustomVariables;
use Piwik\Plugins\UsersManager\API;
@@ -37,11 +38,69 @@ class RequestTest extends IntegrationTestCase
Fixture::createWebsite('2014-01-01 00:00:00');
Fixture::createWebsite('2014-01-01 00:00:00');
+ foreach (range(3,14) as $idSite) {
+ Fixture::createWebsite('2014-01-01 00:00:00');
+ }
+
Cache::deleteTrackerCache();
$this->request = $this->buildRequest(array('idsite' => '1'));
}
+ public function test_getIdSite()
+ {
+ $request = $this->buildRequest(array('idsite' => '14'));
+ $this->assertSame(14, $request->getIdSite());
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
+ * @expectedExceptionMessage Invalid idSite: '0'
+ */
+ public function test_getIdSite_shouldNotThrowException_IfValueIsZero()
+ {
+ $request = $this->buildRequest(array('idsite' => '0'));
+ $request->getIdSite();
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
+ * @expectedExceptionMessage Invalid idSite: '-1'
+ */
+ public function test_getIdSite_shouldThrowException_IfValueIsLowerThanZero()
+ {
+ $request = $this->buildRequest(array('idsite' => '-1'));
+ $request->getIdSite();
+ }
+
+ public function test_getIpString_ShouldDefaultToServerAddress()
+ {
+ $this->assertEquals($_SERVER['REMOTE_ADDR'], $this->request->getIpString());
+ }
+
+ public function test_getIpString_ShouldReturnCustomIp_IfAuthenticated()
+ {
+ $request = $this->buildRequest(array('cip' => '192.192.192.192'));
+ $request->setIsAuthenticated();
+ $this->assertEquals('192.192.192.192', $request->getIpString());
+ }
+
+ public function test_getIp()
+ {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ $this->assertEquals(IPUtils::stringToBinaryIP($ip), $this->request->getIp());
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\InvalidRequestParameterException
+ * @expectedException requires valid token_auth
+ */
+ public function test_getIpString_ShouldDefaultToServerAddress_IfCustomIpIsSetButNotAuthenticated()
+ {
+ $request = $this->buildRequest(array('cip' => '192.192.192.192'));
+ $this->assertEquals($_SERVER['REMOTE_ADDR'], $request->getIpString());
+ }
+
public function test_getCustomVariablesInVisitScope_ShouldReturnNoCustomVars_IfNoWerePassedInParams()
{
$this->assertEquals(array(), $this->request->getCustomVariablesInVisitScope());
@@ -339,6 +398,7 @@ class RequestTest extends IntegrationTestCase
$this->assertSame(12, $request->getIdSite());
}
+
/**
* @group invalidChars
* @dataProvider getInvalidCharacterUrls
@@ -366,6 +426,22 @@ class RequestTest extends IntegrationTestCase
);
}
+ /**
+ * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
+ * @expectedExceptionMessage An unexpected website was found in the request: website id was set to '155'
+ */
+ public function test_getIdSite_shouldTriggerExceptionWhenSiteNotExists()
+ {
+ $self = $this;
+ Piwik::addAction('Tracker.Request.getIdSite', function (&$idSite, $params) use ($self) {
+ $self->assertSame(14, $idSite);
+ $self->assertEquals(array('idsite' => '14'), $params);
+ $idSite = 155;
+ });
+
+ $this->buildRequest(array('idsite' => '14'))->getIdSite();
+ }
+
private function assertCustomVariablesInVisitScope($expectedCvars, $cvarsJsonEncoded)
{
$request = $this->buildRequest(array('_cvar' => $cvarsJsonEncoded));
diff --git a/tests/PHPUnit/Integration/Tracker/VisitTest.php b/tests/PHPUnit/Integration/Tracker/VisitTest.php
index ae7b070b9f..2700b674d6 100644
--- a/tests/PHPUnit/Integration/Tracker/VisitTest.php
+++ b/tests/PHPUnit/Integration/Tracker/VisitTest.php
@@ -87,6 +87,14 @@ class VisitTest extends IntegrationTestCase
);
}
+ public function test_worksWhenSiteDoesNotExist()
+ {
+ $request = new RequestAuthenticated(array('idsite' => 99999999, 'rec' => 1));
+
+ $excluded = new VisitExcluded($request);
+ $this->assertSame(false, $excluded->isExcluded());
+ }
+
/**
* @dataProvider getExcludedIpTestData
*/
diff --git a/tests/PHPUnit/Integration/WidgetsListTest.php b/tests/PHPUnit/Integration/WidgetsListTest.php
index 632e176189..e69fc2d6d6 100644
--- a/tests/PHPUnit/Integration/WidgetsListTest.php
+++ b/tests/PHPUnit/Integration/WidgetsListTest.php
@@ -1,8 +1,8 @@
<?php
/**
- * Piwik - free/libre analytics platform
+ * Matomo - free/libre analytics platform
*
- * @link http://piwik.org
+ * @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
@@ -10,8 +10,8 @@ namespace Piwik\Tests\Integration;
use Piwik\Widget\WidgetConfig;
use Piwik\Plugins\Goals\API;
-use Piwik\Tests\Framework\Mock\FakeAccess;
use Piwik\Translate;
+use Piwik\Tests\Framework\Mock\FakeAccess;
use Piwik\Widget\WidgetsList;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
@@ -49,7 +49,7 @@ class WidgetsListTest extends IntegrationTestCase
'Insights_WidgetCategory' => 2,
'ExampleUI_UiFramework' => 8,
'Referrers_Referrers' => 10,
- 'About Matomo' => 10,
+ 'About Matomo' => 11,
);
// number of main categories
$this->assertEquals(count($numberOfWidgets), count($widgetsPerCategory));
diff --git a/tests/PHPUnit/System/TrackerResponseTest.php b/tests/PHPUnit/System/TrackerResponseTest.php
index 35d9a548ce..fcfee8200f 100644
--- a/tests/PHPUnit/System/TrackerResponseTest.php
+++ b/tests/PHPUnit/System/TrackerResponseTest.php
@@ -85,6 +85,15 @@ class TrackerResponseTest extends SystemTestCase
$this->assertEquals(400, $response['status']);
}
+ public function test_response_ShouldSend400ResponseCode_IfSiteIdIsNegative()
+ {
+ $url = $this->tracker->getUrlTrackPageView('Test');
+ $url .= '&idsite=-1';
+
+ $response = $this->sendHttpRequest($url);
+ $this->assertEquals(400, $response['status']);
+ }
+
public function test_response_ShouldSend400ResponseCode_IfSiteIdIsZero()
{
$url = $this->tracker->getUrlTrackPageView('Test');
diff --git a/tests/PHPUnit/System/expected/test_ImportLogs__CoreAdminHome.getTrackingFailures.xml b/tests/PHPUnit/System/expected/test_ImportLogs__CoreAdminHome.getTrackingFailures.xml
new file mode 100644
index 0000000000..c234bed59e
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_ImportLogs__CoreAdminHome.getTrackingFailures.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result /> \ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.original b/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.original
new file mode 100644
index 0000000000..c856afcf97
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.original
@@ -0,0 +1 @@
+a:0:{} \ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.xml b/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.xml
new file mode 100644
index 0000000000..c234bed59e
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits__CoreAdminHome.getTrackingFailures.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result /> \ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits_withCookieSupport__CoreAdminHome.getTrackingFailures.xml b/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits_withCookieSupport__CoreAdminHome.getTrackingFailures.xml
new file mode 100644
index 0000000000..c234bed59e
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_OneVisitorTwoVisits_withCookieSupport__CoreAdminHome.getTrackingFailures.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result /> \ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml
index a947977d29..04c461dfa4 100644
--- a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml
+++ b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml
@@ -3259,6 +3259,25 @@
<isReport>1</isReport>
</row>
<row>
+ <name>Tracking failures</name>
+ <category>
+ <id>About Matomo</id>
+ <name>About Matomo</name>
+ <order>99</order>
+ <icon />
+ </category>
+ <subcategory />
+ <module>CoreAdminHome</module>
+ <action>getTrackingFailures</action>
+ <order>5</order>
+ <parameters>
+ <module>CoreAdminHome</module>
+ <action>getTrackingFailures</action>
+ </parameters>
+ <uniqueId>widgetCoreAdminHomegetTrackingFailures</uniqueId>
+ <isWide>0</isWide>
+ </row>
+ <row>
<name>Support Matomo!</name>
<category>
<id>About Matomo</id>
diff --git a/tests/PHPUnit/System/expected/test_noVisit_PeriodIsLast__CoreAdminHome.getTrackingFailures.xml b/tests/PHPUnit/System/expected/test_noVisit_PeriodIsLast__CoreAdminHome.getTrackingFailures.xml
new file mode 100644
index 0000000000..c234bed59e
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_noVisit_PeriodIsLast__CoreAdminHome.getTrackingFailures.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result /> \ No newline at end of file
diff --git a/tests/PHPUnit/System/expected/test_noVisit__CoreAdminHome.getTrackingFailures.xml b/tests/PHPUnit/System/expected/test_noVisit__CoreAdminHome.getTrackingFailures.xml
new file mode 100644
index 0000000000..c234bed59e
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_noVisit__CoreAdminHome.getTrackingFailures.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result /> \ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Tracker/RequestSetTest.php b/tests/PHPUnit/Unit/Tracker/RequestSetTest.php
index 94e58a28d1..871582c915 100644
--- a/tests/PHPUnit/Unit/Tracker/RequestSetTest.php
+++ b/tests/PHPUnit/Unit/Tracker/RequestSetTest.php
@@ -398,32 +398,6 @@ class RequestSetTest extends \PHPUnit_Framework_TestCase
unset($_POST['redirecturl']);
}
- public function test_getAllSiteIdsWithinRequest_ShouldReturnEmptyArray_IfNoRequestsSet()
- {
- $this->assertEquals(array(), $this->requestSet->getAllSiteIdsWithinRequest());
- }
-
- public function test_getAllSiteIdsWithinRequest_ShouldReturnTheSiteIds_FromRequests()
- {
- $this->requestSet->setRequests($this->buildRequests(3));
-
- $this->assertEquals(array(1, 2, 3), $this->requestSet->getAllSiteIdsWithinRequest());
- }
-
- public function test_getAllSiteIdsWithinRequest_ShouldReturnUniqueSiteIds_Unordered()
- {
- $this->requestSet->setRequests(array(
- $this->buildRequest(1),
- $this->buildRequest(5),
- $this->buildRequest(1),
- $this->buildRequest(2),
- $this->buildRequest(2),
- $this->buildRequest(9),
- ));
-
- $this->assertEquals(array(1, 5, 2, 9), $this->requestSet->getAllSiteIdsWithinRequest());
- }
-
/**
* @param int $numRequests
* @return Request[]
diff --git a/tests/PHPUnit/Unit/Tracker/RequestTest.php b/tests/PHPUnit/Unit/Tracker/RequestTest.php
index be05e8a1bf..5b125559f1 100644
--- a/tests/PHPUnit/Unit/Tracker/RequestTest.php
+++ b/tests/PHPUnit/Unit/Tracker/RequestTest.php
@@ -38,18 +38,6 @@ class RequestTest extends UnitTestCase
$this->request = $this->buildRequest(array('idsite' => '1'));
}
- public function test_getCurrentTimestamp_ShouldReturnTheSetTimestamp_IfNoCustomValueGiven()
- {
- $this->assertSame($this->time, $this->request->getCurrentTimestamp());
- }
-
- public function test_getCurrentTimestamp_ShouldReturnTheCurrentTimestamp_IfTimestampIsInvalid()
- {
- $request = $this->buildRequest(array('cdt' => '' . 5));
- $request->setIsAuthenticated();
- $this->assertSame($this->time, $request->getCurrentTimestamp());
- }
-
/**
* @expectedException \Exception
* @expectedExceptionMessage Custom timestamp is 86500 seconds old
@@ -88,6 +76,18 @@ class RequestTest extends UnitTestCase
$this->assertNotEmpty($request->getCurrentTimestamp());
}
+ public function test_getCurrentTimestamp_ShouldReturnTheSetTimestamp_IfNoCustomValueGiven()
+ {
+ $this->assertSame($this->time, $this->request->getCurrentTimestamp());
+ }
+
+ public function test_getCurrentTimestamp_ShouldReturnTheCurrentTimestamp_IfTimestampIsInvalid()
+ {
+ $request = $this->buildRequest(array('cdt' => '' . 5));
+ $request->setIsAuthenticated();
+ $this->assertSame($this->time, $request->getCurrentTimestamp());
+ }
+
public function test_isEmptyRequest_ShouldReturnTrue_InCaseNoParamsSet()
{
$request = $this->buildRequest(array());
@@ -494,60 +494,6 @@ class RequestTest extends UnitTestCase
$this->assertSame('00:00:00', $request->getLocalTime());
}
- public function test_getIdSite()
- {
- $request = $this->buildRequest(array('idsite' => '14'));
- $this->assertSame(14, $request->getIdSite());
- }
-
- /**
- * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
- * @expectedExceptionMessage Invalid idSite: '0'
- */
- public function test_getIdSite_shouldThrowException_IfValueIsZero()
- {
- $request = $this->buildRequest(array('idsite' => '0'));
- $request->getIdSite();
- }
-
- /**
- * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
- * @expectedExceptionMessage Invalid idSite: '-1'
- */
- public function test_getIdSite_shouldThrowException_IfValueIsLowerThanZero()
- {
- $request = $this->buildRequest(array('idsite' => '-1'));
- $request->getIdSite();
- }
-
- public function test_getIpString_ShouldDefaultToServerAddress()
- {
- $this->assertEquals($_SERVER['REMOTE_ADDR'], $this->request->getIpString());
- }
-
- /**
- * @expectedException \Piwik\Exception\InvalidRequestParameterException
- * @expectedException requires valid token_auth
- */
- public function test_getIpString_ShouldDefaultToServerAddress_IfCustomIpIsSetButNotAuthenticated()
- {
- $request = $this->buildRequest(array('cip' => '192.192.192.192'));
- $this->assertEquals($_SERVER['REMOTE_ADDR'], $request->getIpString());
- }
-
- public function test_getIpString_ShouldReturnCustomIp_IfAuthenticated()
- {
- $request = $this->buildRequest(array('cip' => '192.192.192.192'));
- $request->setIsAuthenticated();
- $this->assertEquals('192.192.192.192', $request->getIpString());
- }
-
- public function test_getIp()
- {
- $ip = $_SERVER['REMOTE_ADDR'];
- $this->assertEquals(IPUtils::stringToBinaryIP($ip), $this->request->getIp());
- }
-
public function test_getCookieName_ShouldReturnConfigValue()
{
$this->assertEquals('_pk_uid', $this->request->getCookieName());
diff --git a/tests/PHPUnit/piwik.js b/tests/PHPUnit/piwik.js
new file mode 100644
index 0000000000..43069ac865
--- /dev/null
+++ b/tests/PHPUnit/piwik.js
@@ -0,0 +1,77 @@
+/*!!
+ * Piwik - free/libre analytics platform
+ *
+ * JavaScript tracking client
+ *
+ * @link https://piwik.org
+ * @source https://github.com/matomo-org/matomo/blob/master/js/piwik.js
+ * @license https://piwik.org/free-software/bsd/ BSD-3 Clause (also in js/LICENSE.txt)
+ * @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
+ */
+;if(typeof JSON_PIWIK!=="object"&&typeof window.JSON==="object"&&window.JSON.stringify&&window.JSON.parse){JSON_PIWIK=window.JSON}else{(function(){var a={};
+/*!! JSON v3.3.2 | http://bestiejs.github.io/json3 | Copyright 2012-2014, Kit Cambridge | http://kit.mit-license.org */
+(function(){var c=typeof define==="function"&&define.amd;var e={"function":true,object:true};var h=e[typeof a]&&a&&!a.nodeType&&a;var i=e[typeof window]&&window||this,b=h&&e[typeof module]&&module&&!module.nodeType&&typeof global=="object"&&global;if(b&&(b.global===b||b.window===b||b.self===b)){i=b}function j(ab,V){ab||(ab=i.Object());V||(V=i.Object());
+var K=ab.Number||i.Number,R=ab.String||i.String,x=ab.Object||i.Object,S=ab.Date||i.Date,T=ab.SyntaxError||i.SyntaxError,aa=ab.TypeError||i.TypeError,J=ab.Math||i.Math,Y=ab.JSON||i.JSON;if(typeof Y=="object"&&Y){V.stringify=Y.stringify;V.parse=Y.parse}var n=x.prototype,u=n.toString,r,m,L;var B=new S(-3509827334573292);try{B=B.getUTCFullYear()==-109252&&B.getUTCMonth()===0&&B.getUTCDate()===1&&B.getUTCHours()==10&&B.getUTCMinutes()==37&&B.getUTCSeconds()==6&&B.getUTCMilliseconds()==708}catch(v){}function o(ac){if(o[ac]!==L){return o[ac]}var ad;if(ac=="bug-string-char-index"){ad="a"[0]!="a"}else{if(ac=="json"){ad=o("json-stringify")&&o("json-parse")}else{var ak,ah='{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}';if(ac=="json-stringify"){var ai=V.stringify,aj=typeof ai=="function"&&B;if(aj){(ak=function(){return 1}).toJSON=ak;try{aj=ai(0)==="0"&&ai(new K())==="0"&&ai(new R())=='""'&&ai(u)===L&&ai(L)===L&&ai()===L&&ai(ak)==="1"&&ai([ak])=="[1]"&&ai([L])=="[null]"&&ai(null)=="null"&&ai([L,u,null])=="[null,null,null]"&&ai({a:[ak,true,false,null,"\x00\b\n\f\r\t"]})==ah&&ai(null,ak)==="1"&&ai([1,2],null,1)=="[\n 1,\n 2\n]"&&ai(new S(-8640000000000000))=='"-271821-04-20T00:00:00.000Z"'&&ai(new S(8640000000000000))=='"+275760-09-13T00:00:00.000Z"'&&ai(new S(-62198755200000))=='"-000001-01-01T00:00:00.000Z"'&&ai(new S(-1))=='"1969-12-31T23:59:59.999Z"'
+}catch(ae){aj=false}}ad=aj}if(ac=="json-parse"){var ag=V.parse;if(typeof ag=="function"){try{if(ag("0")===0&&!ag(false)){ak=ag(ah);var af=ak.a.length==5&&ak.a[0]===1;if(af){try{af=!ag('"\t"')}catch(ae){}if(af){try{af=ag("01")!==1}catch(ae){}}if(af){try{af=ag("1.")!==1}catch(ae){}}}}}catch(ae){af=false}}ad=af}}}return o[ac]=!!ad}if(!o("json")){var U="[object Function]",Q="[object Date]",N="[object Number]",O="[object String]",E="[object Array]",A="[object Boolean]";var F=o("bug-string-char-index");if(!B){var s=J.floor;var Z=[0,31,59,90,120,151,181,212,243,273,304,334];var D=function(ac,ad){return Z[ad]+365*(ac-1970)+s((ac-1969+(ad=+(ad>1)))/4)-s((ac-1901+ad)/100)+s((ac-1601+ad)/400)}}if(!(r=n.hasOwnProperty)){r=function(ae){var ac={},ad;if((ac.__proto__=null,ac.__proto__={toString:1},ac).toString!=u){r=function(ah){var ag=this.__proto__,af=ah in (this.__proto__=null,this);this.__proto__=ag;return af}}else{ad=ac.constructor;r=function(ag){var af=(this.constructor||ad).prototype;return ag in this&&!(ag in af&&this[ag]===af[ag])
+}}ac=null;return r.call(this,ae)}}m=function(ae,ah){var af=0,ac,ad,ag;(ac=function(){this.valueOf=0}).prototype.valueOf=0;ad=new ac();for(ag in ad){if(r.call(ad,ag)){af++}}ac=ad=null;if(!af){ad=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];m=function(aj,an){var am=u.call(aj)==U,al,ak;var ai=!am&&typeof aj.constructor!="function"&&e[typeof aj.hasOwnProperty]&&aj.hasOwnProperty||r;for(al in aj){if(!(am&&al=="prototype")&&ai.call(aj,al)){an(al)}}for(ak=ad.length;al=ad[--ak];ai.call(aj,al)&&an(al)){}}}else{if(af==2){m=function(aj,am){var ai={},al=u.call(aj)==U,ak;for(ak in aj){if(!(al&&ak=="prototype")&&!r.call(ai,ak)&&(ai[ak]=1)&&r.call(aj,ak)){am(ak)}}}}else{m=function(aj,am){var al=u.call(aj)==U,ak,ai;for(ak in aj){if(!(al&&ak=="prototype")&&r.call(aj,ak)&&!(ai=ak==="constructor")){am(ak)}}if(ai||r.call(aj,(ak="constructor"))){am(ak)}}}}return m(ae,ah)};if(!o("json-stringify")){var q={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};
+var I="000000";var t=function(ac,ad){return(I+(ad||0)).slice(-ac)};var z="\\u00";var C=function(ai){var ad='"',ag=0,ah=ai.length,ac=!F||ah>10;var af=ac&&(F?ai.split(""):ai);for(;ag<ah;ag++){var ae=ai.charCodeAt(ag);switch(ae){case 8:case 9:case 10:case 12:case 13:case 34:case 92:ad+=q[ae];break;default:if(ae<32){ad+=z+t(2,ae.toString(16));break}ad+=ac?af[ag]:ai.charAt(ag)}}return ad+'"'};var p=function(ai,aA,ag,al,ax,ac,aj){var at,ae,ap,az,ay,ak,aw,au,aq,an,ar,ad,ah,af,av,ao;try{at=aA[ai]}catch(am){}if(typeof at=="object"&&at){ae=u.call(at);if(ae==Q&&!r.call(at,"toJSON")){if(at>-1/0&&at<1/0){if(D){ay=s(at/86400000);for(ap=s(ay/365.2425)+1970-1;D(ap+1,0)<=ay;ap++){}for(az=s((ay-D(ap,0))/30.42);D(ap,az+1)<=ay;az++){}ay=1+ay-D(ap,az);ak=(at%86400000+86400000)%86400000;aw=s(ak/3600000)%24;au=s(ak/60000)%60;aq=s(ak/1000)%60;an=ak%1000}else{ap=at.getUTCFullYear();az=at.getUTCMonth();ay=at.getUTCDate();aw=at.getUTCHours();au=at.getUTCMinutes();aq=at.getUTCSeconds();an=at.getUTCMilliseconds()}at=(ap<=0||ap>=10000?(ap<0?"-":"+")+t(6,ap<0?-ap:ap):t(4,ap))+"-"+t(2,az+1)+"-"+t(2,ay)+"T"+t(2,aw)+":"+t(2,au)+":"+t(2,aq)+"."+t(3,an)+"Z"
+}else{at=null}}else{if(typeof at.toJSON=="function"&&((ae!=N&&ae!=O&&ae!=E)||r.call(at,"toJSON"))){at=at.toJSON(ai)}}}if(ag){at=ag.call(aA,ai,at)}if(at===null){return"null"}ae=u.call(at);if(ae==A){return""+at}else{if(ae==N){return at>-1/0&&at<1/0?""+at:"null"}else{if(ae==O){return C(""+at)}}}if(typeof at=="object"){for(af=aj.length;af--;){if(aj[af]===at){throw aa()}}aj.push(at);ar=[];av=ac;ac+=ax;if(ae==E){for(ah=0,af=at.length;ah<af;ah++){ad=p(ah,at,ag,al,ax,ac,aj);ar.push(ad===L?"null":ad)}ao=ar.length?(ax?"[\n"+ac+ar.join(",\n"+ac)+"\n"+av+"]":("["+ar.join(",")+"]")):"[]"}else{m(al||at,function(aC){var aB=p(aC,at,ag,al,ax,ac,aj);if(aB!==L){ar.push(C(aC)+":"+(ax?" ":"")+aB)}});ao=ar.length?(ax?"{\n"+ac+ar.join(",\n"+ac)+"\n"+av+"}":("{"+ar.join(",")+"}")):"{}"}aj.pop();return ao}};V.stringify=function(ac,ae,af){var ad,al,aj,ai;if(e[typeof ae]&&ae){if((ai=u.call(ae))==U){al=ae}else{if(ai==E){aj={};for(var ah=0,ag=ae.length,ak;ah<ag;ak=ae[ah++],((ai=u.call(ak)),ai==O||ai==N)&&(aj[ak]=1)){}}}}if(af){if((ai=u.call(af))==N){if((af-=af%1)>0){for(ad="",af>10&&(af=10);
+ad.length<af;ad+=" "){}}}else{if(ai==O){ad=af.length<=10?af:af.slice(0,10)}}}return p("",(ak={},ak[""]=ac,ak),al,aj,ad,"",[])}}if(!o("json-parse")){var M=R.fromCharCode;var l={92:"\\",34:'"',47:"/",98:"\b",116:"\t",110:"\n",102:"\f",114:"\r"};var G,X;var H=function(){G=X=null;throw T()};var y=function(){var ah=X,af=ah.length,ag,ae,ac,ai,ad;while(G<af){ad=ah.charCodeAt(G);switch(ad){case 9:case 10:case 13:case 32:G++;break;case 123:case 125:case 91:case 93:case 58:case 44:ag=F?ah.charAt(G):ah[G];G++;return ag;case 34:for(ag="@",G++;G<af;){ad=ah.charCodeAt(G);if(ad<32){H()}else{if(ad==92){ad=ah.charCodeAt(++G);switch(ad){case 92:case 34:case 47:case 98:case 116:case 110:case 102:case 114:ag+=l[ad];G++;break;case 117:ae=++G;for(ac=G+4;G<ac;G++){ad=ah.charCodeAt(G);if(!(ad>=48&&ad<=57||ad>=97&&ad<=102||ad>=65&&ad<=70)){H()}}ag+=M("0x"+ah.slice(ae,G));break;default:H()}}else{if(ad==34){break}ad=ah.charCodeAt(G);ae=G;while(ad>=32&&ad!=92&&ad!=34){ad=ah.charCodeAt(++G)}ag+=ah.slice(ae,G)}}}if(ah.charCodeAt(G)==34){G++;
+return ag}H();default:ae=G;if(ad==45){ai=true;ad=ah.charCodeAt(++G)}if(ad>=48&&ad<=57){if(ad==48&&((ad=ah.charCodeAt(G+1)),ad>=48&&ad<=57)){H()}ai=false;for(;G<af&&((ad=ah.charCodeAt(G)),ad>=48&&ad<=57);G++){}if(ah.charCodeAt(G)==46){ac=++G;for(;ac<af&&((ad=ah.charCodeAt(ac)),ad>=48&&ad<=57);ac++){}if(ac==G){H()}G=ac}ad=ah.charCodeAt(G);if(ad==101||ad==69){ad=ah.charCodeAt(++G);if(ad==43||ad==45){G++}for(ac=G;ac<af&&((ad=ah.charCodeAt(ac)),ad>=48&&ad<=57);ac++){}if(ac==G){H()}G=ac}return +ah.slice(ae,G)}if(ai){H()}if(ah.slice(G,G+4)=="true"){G+=4;return true}else{if(ah.slice(G,G+5)=="false"){G+=5;return false}else{if(ah.slice(G,G+4)=="null"){G+=4;return null}}}H()}}return"$"};var W=function(ad){var ac,ae;if(ad=="$"){H()}if(typeof ad=="string"){if((F?ad.charAt(0):ad[0])=="@"){return ad.slice(1)}if(ad=="["){ac=[];for(;;ae||(ae=true)){ad=y();if(ad=="]"){break}if(ae){if(ad==","){ad=y();if(ad=="]"){H()}}else{H()}}if(ad==","){H()}ac.push(W(ad))}return ac}else{if(ad=="{"){ac={};for(;;ae||(ae=true)){ad=y();
+if(ad=="}"){break}if(ae){if(ad==","){ad=y();if(ad=="}"){H()}}else{H()}}if(ad==","||typeof ad!="string"||(F?ad.charAt(0):ad[0])!="@"||y()!=":"){H()}ac[ad.slice(1)]=W(y())}return ac}}H()}return ad};var P=function(ae,ad,af){var ac=w(ae,ad,af);if(ac===L){delete ae[ad]}else{ae[ad]=ac}};var w=function(af,ae,ag){var ad=af[ae],ac;if(typeof ad=="object"&&ad){if(u.call(ad)==E){for(ac=ad.length;ac--;){P(ad,ac,ag)}}else{m(ad,function(ah){P(ad,ah,ag)})}}return ag.call(af,ae,ad)};V.parse=function(ae,af){var ac,ad;G=0;X=""+ae;ac=W(y());if(y()!="$"){H()}G=X=null;return af&&u.call(af)==U?w((ad={},ad[""]=ac,ad),"",af):ac}}}V.runInContext=j;return V}if(h&&!c){j(i,h)}else{var f=i.JSON,k=i.JSON3,d=false;var g=j(i,(i.JSON3={noConflict:function(){if(!d){d=true;i.JSON=f;i.JSON3=k;f=k=null}return g}}));i.JSON={parse:g.parse,stringify:g.stringify}}if(c){define(function(){return g})}}).call(this);JSON_PIWIK=a})()}if(typeof _paq!=="object"){_paq=[]}if(typeof window.Piwik!=="object"){window.Matomo=window.Piwik=(function(){var r,b={},y={},G=document,h=navigator,X=screen,T=window,i=T.performance||T.mozPerformance||T.msPerformance||T.webkitPerformance,t=T.encodeURIComponent,S=T.decodeURIComponent,l=unescape,I=[],E,e,ae=[],x=0,U=0,m=false;
+function p(al){try{return S(al)}catch(am){return unescape(al)}}function J(am){var al=typeof am;return al!=="undefined"}function A(al){return typeof al==="function"}function W(al){return typeof al==="object"}function w(al){return typeof al==="string"||al instanceof String}function B(am){if(!am){return true}var al;var an=true;for(al in am){if(Object.prototype.hasOwnProperty.call(am,al)){an=false}}return an}function ah(al){var am=typeof console;if(am!=="undefined"&&console&&console.error){console.error(al)}}function ad(){var aq,ap,at,am,al;for(aq=0;aq<arguments.length;aq+=1){al=null;if(arguments[aq]&&arguments[aq].slice){al=arguments[aq].slice()}am=arguments[aq];at=am.shift();var ar,an;var ao=w(at)&&at.indexOf("::")>0;if(ao){ar=at.split("::");an=ar[0];at=ar[1];if("object"===typeof e[an]&&"function"===typeof e[an][at]){e[an][at].apply(e[an],am)}else{if(al){ae.push(al)}}}else{for(ap=0;ap<I.length;ap++){if(w(at)){an=I[ap];var au=at.indexOf(".")>0;if(au){ar=at.split(".");if(an&&"object"===typeof an[ar[0]]){an=an[ar[0]];
+at=ar[1]}else{if(al){ae.push(al);break}}}if(an[at]){an[at].apply(an,am)}else{var av="The method '"+at+'\' was not found in "_paq" variable. Please have a look at the Piwik tracker documentation: https://developer.piwik.org/api-reference/tracking-javascript';ah(av);if(!au){throw new TypeError(av)}}if(at==="addTracker"){break}if(at==="setTrackerUrl"||at==="setSiteId"){break}}else{at.apply(I[ap],am)}}}}}function ak(ao,an,am,al){if(ao.addEventListener){ao.addEventListener(an,am,al);return true}if(ao.attachEvent){return ao.attachEvent("on"+an,am)}ao["on"+an]=am}function n(al){if(G.readyState==="complete"){al()}else{if(T.addEventListener){T.addEventListener("load",al,false)}else{if(T.attachEvent){T.attachEvent("onload",al)}}}}function q(ao){var al=false;if(G.attachEvent){al=G.readyState==="complete"}else{al=G.readyState!=="loading"}if(al){ao();return}var an;if(G.addEventListener){ak(G,"DOMContentLoaded",function am(){G.removeEventListener("DOMContentLoaded",am,false);if(!al){al=true;ao()}})}else{if(G.attachEvent){G.attachEvent("onreadystatechange",function am(){if(G.readyState==="complete"){G.detachEvent("onreadystatechange",am);
+if(!al){al=true;ao()}}});if(G.documentElement.doScroll&&T===T.top){(function am(){if(!al){try{G.documentElement.doScroll("left")}catch(ap){setTimeout(am,0);return}al=true;ao()}}())}}}ak(T,"load",function(){if(!al){al=true;ao()}},false)}function aa(am,ar,at){if(!am){return""}var al="",ao,an,ap,aq;for(ao in b){if(Object.prototype.hasOwnProperty.call(b,ao)){aq=b[ao]&&"function"===typeof b[ao][am];if(aq){an=b[ao][am];ap=an(ar||{},at);if(ap){al+=ap}}}}return al}function af(){var al;m=true;aa("unload");if(r){do{al=new Date()}while(al.getTimeAlias()<r)}}function o(an,am){var al=G.createElement("script");al.type="text/javascript";al.src=an;if(al.readyState){al.onreadystatechange=function(){var ao=this.readyState;if(ao==="loaded"||ao==="complete"){al.onreadystatechange=null;am()}}}else{al.onload=am}G.getElementsByTagName("head")[0].appendChild(al)}function K(){var al="";try{al=T.top.document.referrer}catch(an){if(T.parent){try{al=T.parent.document.referrer}catch(am){al=""}}}if(al===""){al=G.referrer
+}return al}function s(al){var an=new RegExp("^([a-z]+):"),am=an.exec(al);return am?am[1]:null}function d(al){var an=new RegExp("^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)"),am=an.exec(al);return am?am[1]:al}function ag(am,al){am=String(am);return am.lastIndexOf(al,0)===0}function R(am,al){am=String(am);return am.indexOf(al,am.length-al.length)!==-1}function z(am,al){am=String(am);return am.indexOf(al)!==-1}function g(am,al){am=String(am);return am.substr(0,am.length-al)}function F(ao,an,aq){ao=String(ao);if(!aq){aq=""}var al=ao.indexOf("#");var ar=ao.length;if(al===-1){al=ar}var ap=ao.substr(0,al);var am=ao.substr(al,ar-al);if(ap.indexOf("?")===-1){ap+="?"}else{if(!R(ap,"?")){ap+="&"}}return ap+t(an)+"="+t(aq)+am}function k(am,an){am=String(am);if(am.indexOf("?"+an+"=")===-1&&am.indexOf("&"+an+"=")===-1){return am}var ao=am.indexOf("?");if(ao===-1){return am}var al=am.substr(ao+1);var at=am.substr(0,ao);if(al){var au="";var aw=al.indexOf("#");if(aw!==-1){au=al.substr(aw+1);al=al.substr(0,aw)
+}var ap;var ar=al.split("&");var aq=ar.length-1;for(aq;aq>=0;aq--){ap=ar[aq].split("=")[0];if(ap===an){ar.splice(aq,1)}}var av=ar.join("&");if(av){at=at+"?"+av}if(au){at+="#"+au}}return at}function f(an,am){var al="[\\?&#]"+am+"=([^&#]*)";var ap=new RegExp(al);var ao=ap.exec(an);return ao?S(ao[1]):""}function a(al){if(al&&String(al)===al){return al.replace(/^\s+|\s+$/g,"")}return al}function D(al){return unescape(t(al))}function aj(aB){var an=function(aH,aG){return(aH<<aG)|(aH>>>(32-aG))},aC=function(aJ){var aH="",aI,aG;for(aI=7;aI>=0;aI--){aG=(aJ>>>(aI*4))&15;aH+=aG.toString(16)}return aH},aq,aE,aD,am=[],av=1732584193,at=4023233417,ar=2562383102,ap=271733878,ao=3285377520,aA,az,ay,ax,aw,aF,al,au=[];aB=D(aB);al=aB.length;for(aE=0;aE<al-3;aE+=4){aD=aB.charCodeAt(aE)<<24|aB.charCodeAt(aE+1)<<16|aB.charCodeAt(aE+2)<<8|aB.charCodeAt(aE+3);au.push(aD)}switch(al&3){case 0:aE=2147483648;break;case 1:aE=aB.charCodeAt(al-1)<<24|8388608;break;case 2:aE=aB.charCodeAt(al-2)<<24|aB.charCodeAt(al-1)<<16|32768;
+break;case 3:aE=aB.charCodeAt(al-3)<<24|aB.charCodeAt(al-2)<<16|aB.charCodeAt(al-1)<<8|128;break}au.push(aE);while((au.length&15)!==14){au.push(0)}au.push(al>>>29);au.push((al<<3)&4294967295);for(aq=0;aq<au.length;aq+=16){for(aE=0;aE<16;aE++){am[aE]=au[aq+aE]}for(aE=16;aE<=79;aE++){am[aE]=an(am[aE-3]^am[aE-8]^am[aE-14]^am[aE-16],1)}aA=av;az=at;ay=ar;ax=ap;aw=ao;for(aE=0;aE<=19;aE++){aF=(an(aA,5)+((az&ay)|(~az&ax))+aw+am[aE]+1518500249)&4294967295;aw=ax;ax=ay;ay=an(az,30);az=aA;aA=aF}for(aE=20;aE<=39;aE++){aF=(an(aA,5)+(az^ay^ax)+aw+am[aE]+1859775393)&4294967295;aw=ax;ax=ay;ay=an(az,30);az=aA;aA=aF}for(aE=40;aE<=59;aE++){aF=(an(aA,5)+((az&ay)|(az&ax)|(ay&ax))+aw+am[aE]+2400959708)&4294967295;aw=ax;ax=ay;ay=an(az,30);az=aA;aA=aF}for(aE=60;aE<=79;aE++){aF=(an(aA,5)+(az^ay^ax)+aw+am[aE]+3395469782)&4294967295;aw=ax;ax=ay;ay=an(az,30);az=aA;aA=aF}av=(av+aA)&4294967295;at=(at+az)&4294967295;ar=(ar+ay)&4294967295;ap=(ap+ax)&4294967295;ao=(ao+aw)&4294967295}aF=aC(av)+aC(at)+aC(ar)+aC(ap)+aC(ao);
+return aF.toLowerCase()}function Z(an,al,am){if(!an){an=""}if(!al){al=""}if(an==="translate.googleusercontent.com"){if(am===""){am=al}al=f(al,"u");an=d(al)}else{if(an==="cc.bingj.com"||an==="webcache.googleusercontent.com"||an.slice(0,5)==="74.6."){al=G.links[0].href;an=d(al)}}return[an,al,am]}function L(am){var al=am.length;if(am.charAt(--al)==="."){am=am.slice(0,al)}if(am.slice(0,2)==="*."){am=am.slice(1)}if(am.indexOf("/")!==-1){am=am.substr(0,am.indexOf("/"))}return am}function ai(am){am=am&&am.text?am.text:am;if(!w(am)){var al=G.getElementsByTagName("title");if(al&&J(al[0])){am=al[0].text}}return am}function P(al){if(!al){return[]}if(!J(al.children)&&J(al.childNodes)){return al.children}if(J(al.children)){return al.children}return[]}function V(am,al){if(!am||!al){return false}if(am.contains){return am.contains(al)}if(am===al){return true}if(am.compareDocumentPosition){return !!(am.compareDocumentPosition(al)&16)}return false}function M(an,ao){if(an&&an.indexOf){return an.indexOf(ao)
+}if(!J(an)||an===null){return -1}if(!an.length){return -1}var al=an.length;if(al===0){return -1}var am=0;while(am<al){if(an[am]===ao){return am}am++}return -1}function j(an){if(!an){return false}function al(ap,aq){if(T.getComputedStyle){return G.defaultView.getComputedStyle(ap,null)[aq]}if(ap.currentStyle){return ap.currentStyle[aq]}}function ao(ap){ap=ap.parentNode;while(ap){if(ap===G){return true}ap=ap.parentNode}return false}function am(ar,ay,ap,av,at,aw,au){var aq=ar.parentNode,ax=1;if(!ao(ar)){return false}if(9===aq.nodeType){return true}if("0"===al(ar,"opacity")||"none"===al(ar,"display")||"hidden"===al(ar,"visibility")){return false}if(!J(ay)||!J(ap)||!J(av)||!J(at)||!J(aw)||!J(au)){ay=ar.offsetTop;at=ar.offsetLeft;av=ay+ar.offsetHeight;ap=at+ar.offsetWidth;aw=ar.offsetWidth;au=ar.offsetHeight}if(an===ar&&(0===au||0===aw)&&"hidden"===al(ar,"overflow")){return false}if(aq){if(("hidden"===al(aq,"overflow")||"scroll"===al(aq,"overflow"))){if(at+ax>aq.offsetWidth+aq.scrollLeft||at+aw-ax<aq.scrollLeft||ay+ax>aq.offsetHeight+aq.scrollTop||ay+au-ax<aq.scrollTop){return false
+}}if(ar.offsetParent===aq){at+=aq.offsetLeft;ay+=aq.offsetTop}return am(aq,ay,ap,av,at,aw,au)}return true}return am(an)}var ac={htmlCollectionToArray:function(an){var al=[],am;if(!an||!an.length){return al}for(am=0;am<an.length;am++){al.push(an[am])}return al},find:function(al){if(!document.querySelectorAll||!al){return[]}var am=document.querySelectorAll(al);return this.htmlCollectionToArray(am)},findMultiple:function(an){if(!an||!an.length){return[]}var am,ao;var al=[];for(am=0;am<an.length;am++){ao=this.find(an[am]);al=al.concat(ao)}al=this.makeNodesUnique(al);return al},findNodesByTagName:function(am,al){if(!am||!al||!am.getElementsByTagName){return[]}var an=am.getElementsByTagName(al);return this.htmlCollectionToArray(an)},makeNodesUnique:function(al){var aq=[].concat(al);al.sort(function(at,ar){if(at===ar){return 0}var av=M(aq,at);var au=M(aq,ar);if(av===au){return 0}return av>au?-1:1});if(al.length<=1){return al}var am=0;var ao=0;var ap=[];var an;an=al[am++];while(an){if(an===al[am]){ao=ap.push(am)
+}an=al[am++]||null}while(ao--){al.splice(ap[ao],1)}return al},getAttributeValueFromNode:function(ap,an){if(!this.hasNodeAttribute(ap,an)){return}if(ap&&ap.getAttribute){return ap.getAttribute(an)}if(!ap||!ap.attributes){return}var ao=(typeof ap.attributes[an]);if("undefined"===ao){return}if(ap.attributes[an].value){return ap.attributes[an].value}if(ap.attributes[an].nodeValue){return ap.attributes[an].nodeValue}var am;var al=ap.attributes;if(!al){return}for(am=0;am<al.length;am++){if(al[am].nodeName===an){return al[am].nodeValue}}return null},hasNodeAttributeWithValue:function(am,al){var an=this.getAttributeValueFromNode(am,al);return !!an},hasNodeAttribute:function(an,al){if(an&&an.hasAttribute){return an.hasAttribute(al)}if(an&&an.attributes){var am=(typeof an.attributes[al]);return"undefined"!==am}return false},hasNodeCssClass:function(an,al){if(an&&al&&an.className){var am=typeof an.className==="string"?an.className.split(" "):[];if(-1!==M(am,al)){return true}}return false},findNodesHavingAttribute:function(ap,an,al){if(!al){al=[]
+}if(!ap||!an){return al}var ao=P(ap);if(!ao||!ao.length){return al}var am,aq;for(am=0;am<ao.length;am++){aq=ao[am];if(this.hasNodeAttribute(aq,an)){al.push(aq)}al=this.findNodesHavingAttribute(aq,an,al)}return al},findFirstNodeHavingAttribute:function(an,am){if(!an||!am){return}if(this.hasNodeAttribute(an,am)){return an}var al=this.findNodesHavingAttribute(an,am);if(al&&al.length){return al[0]}},findFirstNodeHavingAttributeWithValue:function(ao,an){if(!ao||!an){return}if(this.hasNodeAttributeWithValue(ao,an)){return ao}var al=this.findNodesHavingAttribute(ao,an);if(!al||!al.length){return}var am;for(am=0;am<al.length;am++){if(this.getAttributeValueFromNode(al[am],an)){return al[am]}}},findNodesHavingCssClass:function(ap,ao,al){if(!al){al=[]}if(!ap||!ao){return al}if(ap.getElementsByClassName){var aq=ap.getElementsByClassName(ao);return this.htmlCollectionToArray(aq)}var an=P(ap);if(!an||!an.length){return[]}var am,ar;for(am=0;am<an.length;am++){ar=an[am];if(this.hasNodeCssClass(ar,ao)){al.push(ar)
+}al=this.findNodesHavingCssClass(ar,ao,al)}return al},findFirstNodeHavingClass:function(an,am){if(!an||!am){return}if(this.hasNodeCssClass(an,am)){return an}var al=this.findNodesHavingCssClass(an,am);if(al&&al.length){return al[0]}},isLinkElement:function(am){if(!am){return false}var al=String(am.nodeName).toLowerCase();var ao=["a","area"];var an=M(ao,al);return an!==-1},setAnyAttribute:function(am,al,an){if(!am||!al){return}if(am.setAttribute){am.setAttribute(al,an)}else{am[al]=an}}};var v={CONTENT_ATTR:"data-track-content",CONTENT_CLASS:"piwikTrackContent",CONTENT_NAME_ATTR:"data-content-name",CONTENT_PIECE_ATTR:"data-content-piece",CONTENT_PIECE_CLASS:"piwikContentPiece",CONTENT_TARGET_ATTR:"data-content-target",CONTENT_TARGET_CLASS:"piwikContentTarget",CONTENT_IGNOREINTERACTION_ATTR:"data-content-ignoreinteraction",CONTENT_IGNOREINTERACTION_CLASS:"piwikContentIgnoreInteraction",location:undefined,findContentNodes:function(){var am="."+this.CONTENT_CLASS;var al="["+this.CONTENT_ATTR+"]";
+var an=ac.findMultiple([am,al]);return an},findContentNodesWithinNode:function(ao){if(!ao){return[]}var am=ac.findNodesHavingCssClass(ao,this.CONTENT_CLASS);var al=ac.findNodesHavingAttribute(ao,this.CONTENT_ATTR);if(al&&al.length){var an;for(an=0;an<al.length;an++){am.push(al[an])}}if(ac.hasNodeAttribute(ao,this.CONTENT_ATTR)){am.push(ao)}else{if(ac.hasNodeCssClass(ao,this.CONTENT_CLASS)){am.push(ao)}}am=ac.makeNodesUnique(am);return am},findParentContentNode:function(am){if(!am){return}var an=am;var al=0;while(an&&an!==G&&an.parentNode){if(ac.hasNodeAttribute(an,this.CONTENT_ATTR)){return an}if(ac.hasNodeCssClass(an,this.CONTENT_CLASS)){return an}an=an.parentNode;if(al>1000){break}al++}},findPieceNode:function(am){var al;al=ac.findFirstNodeHavingAttribute(am,this.CONTENT_PIECE_ATTR);if(!al){al=ac.findFirstNodeHavingClass(am,this.CONTENT_PIECE_CLASS)}if(al){return al}return am},findTargetNodeNoDefault:function(al){if(!al){return}var am=ac.findFirstNodeHavingAttributeWithValue(al,this.CONTENT_TARGET_ATTR);
+if(am){return am}am=ac.findFirstNodeHavingAttribute(al,this.CONTENT_TARGET_ATTR);if(am){return am}am=ac.findFirstNodeHavingClass(al,this.CONTENT_TARGET_CLASS);if(am){return am}},findTargetNode:function(al){var am=this.findTargetNodeNoDefault(al);if(am){return am}return al},findContentName:function(am){if(!am){return}var ap=ac.findFirstNodeHavingAttributeWithValue(am,this.CONTENT_NAME_ATTR);if(ap){return ac.getAttributeValueFromNode(ap,this.CONTENT_NAME_ATTR)}var al=this.findContentPiece(am);if(al){return this.removeDomainIfIsInLink(al)}if(ac.hasNodeAttributeWithValue(am,"title")){return ac.getAttributeValueFromNode(am,"title")}var an=this.findPieceNode(am);if(ac.hasNodeAttributeWithValue(an,"title")){return ac.getAttributeValueFromNode(an,"title")}var ao=this.findTargetNode(am);if(ac.hasNodeAttributeWithValue(ao,"title")){return ac.getAttributeValueFromNode(ao,"title")}},findContentPiece:function(am){if(!am){return}var ao=ac.findFirstNodeHavingAttributeWithValue(am,this.CONTENT_PIECE_ATTR);
+if(ao){return ac.getAttributeValueFromNode(ao,this.CONTENT_PIECE_ATTR)}var al=this.findPieceNode(am);var an=this.findMediaUrlInNode(al);if(an){return this.toAbsoluteUrl(an)}},findContentTarget:function(an){if(!an){return}var ao=this.findTargetNode(an);if(ac.hasNodeAttributeWithValue(ao,this.CONTENT_TARGET_ATTR)){return ac.getAttributeValueFromNode(ao,this.CONTENT_TARGET_ATTR)}var am;if(ac.hasNodeAttributeWithValue(ao,"href")){am=ac.getAttributeValueFromNode(ao,"href");return this.toAbsoluteUrl(am)}var al=this.findPieceNode(an);if(ac.hasNodeAttributeWithValue(al,"href")){am=ac.getAttributeValueFromNode(al,"href");return this.toAbsoluteUrl(am)}},isSameDomain:function(al){if(!al||!al.indexOf){return false}if(0===al.indexOf(this.getLocation().origin)){return true}var am=al.indexOf(this.getLocation().host);if(8>=am&&0<=am){return true}return false},removeDomainIfIsInLink:function(an){var am="^https?://[^/]+";var al="^.*//[^/]+";if(an&&an.search&&-1!==an.search(new RegExp(am))&&this.isSameDomain(an)){an=an.replace(new RegExp(al),"");
+if(!an){an="/"}}return an},findMediaUrlInNode:function(ap){if(!ap){return}var an=["img","embed","video","audio"];var al=ap.nodeName.toLowerCase();if(-1!==M(an,al)&&ac.findFirstNodeHavingAttributeWithValue(ap,"src")){var ao=ac.findFirstNodeHavingAttributeWithValue(ap,"src");return ac.getAttributeValueFromNode(ao,"src")}if(al==="object"&&ac.hasNodeAttributeWithValue(ap,"data")){return ac.getAttributeValueFromNode(ap,"data")}if(al==="object"){var aq=ac.findNodesByTagName(ap,"param");if(aq&&aq.length){var am;for(am=0;am<aq.length;am++){if("movie"===ac.getAttributeValueFromNode(aq[am],"name")&&ac.hasNodeAttributeWithValue(aq[am],"value")){return ac.getAttributeValueFromNode(aq[am],"value")}}}var ar=ac.findNodesByTagName(ap,"embed");if(ar&&ar.length){return this.findMediaUrlInNode(ar[0])}}},trim:function(al){return a(al)},isOrWasNodeInViewport:function(aq){if(!aq||!aq.getBoundingClientRect||aq.nodeType!==1){return true}var ap=aq.getBoundingClientRect();var ao=G.documentElement||{};var an=ap.top<0;
+if(an&&aq.offsetTop){an=(aq.offsetTop+ap.height)>0}var am=ao.clientWidth;if(T.innerWidth&&am>T.innerWidth){am=T.innerWidth}var al=ao.clientHeight;if(T.innerHeight&&al>T.innerHeight){al=T.innerHeight}return((ap.bottom>0||an)&&ap.right>0&&ap.left<am&&((ap.top<al)||an))},isNodeVisible:function(am){var al=j(am);var an=this.isOrWasNodeInViewport(am);return al&&an},buildInteractionRequestParams:function(al,am,an,ao){var ap="";if(al){ap+="c_i="+t(al)}if(am){if(ap){ap+="&"}ap+="c_n="+t(am)}if(an){if(ap){ap+="&"}ap+="c_p="+t(an)}if(ao){if(ap){ap+="&"}ap+="c_t="+t(ao)}return ap},buildImpressionRequestParams:function(al,am,an){var ao="c_n="+t(al)+"&c_p="+t(am);if(an){ao+="&c_t="+t(an)}return ao},buildContentBlock:function(an){if(!an){return}var al=this.findContentName(an);var am=this.findContentPiece(an);var ao=this.findContentTarget(an);al=this.trim(al);am=this.trim(am);ao=this.trim(ao);return{name:al||"Unknown",piece:am||"Unknown",target:ao||""}},collectContent:function(ao){if(!ao||!ao.length){return[]
+}var an=[];var al,am;for(al=0;al<ao.length;al++){am=this.buildContentBlock(ao[al]);if(J(am)){an.push(am)}}return an},setLocation:function(al){this.location=al},getLocation:function(){var al=this.location||T.location;if(!al.origin){al.origin=al.protocol+"//"+al.hostname+(al.port?":"+al.port:"")}return al},toAbsoluteUrl:function(am){if((!am||String(am)!==am)&&am!==""){return am}if(""===am){return this.getLocation().href}if(am.search(/^\/\//)!==-1){return this.getLocation().protocol+am}if(am.search(/:\/\//)!==-1){return am}if(0===am.indexOf("#")){return this.getLocation().origin+this.getLocation().pathname+am}if(0===am.indexOf("?")){return this.getLocation().origin+this.getLocation().pathname+am}if(0===am.search("^[a-zA-Z]{2,11}:")){return am}if(am.search(/^\//)!==-1){return this.getLocation().origin+am}var al="(.*/)";var an=this.getLocation().origin+this.getLocation().pathname.match(new RegExp(al))[0];return an+am},isUrlToCurrentDomain:function(am){var an=this.toAbsoluteUrl(am);if(!an){return false
+}var al=this.getLocation().origin;if(al===an){return true}if(0===String(an).indexOf(al)){if(":"===String(an).substr(al.length,1)){return false}return true}return false},setHrefAttribute:function(am,al){if(!am||!al){return}ac.setAnyAttribute(am,"href",al)},shouldIgnoreInteraction:function(an){var am=ac.hasNodeAttribute(an,this.CONTENT_IGNOREINTERACTION_ATTR);var al=ac.hasNodeCssClass(an,this.CONTENT_IGNOREINTERACTION_CLASS);return am||al}};function O(am,ap){if(ap){return ap}am=v.toAbsoluteUrl(am);if(z(am,"?")){var ao=am.indexOf("?");am=am.slice(0,ao)}if(R(am,"matomo.php")){am=g(am,"matomo.php".length)}else{if(R(am,"piwik.php")){am=g(am,"piwik.php".length)}else{if(R(am,".php")){var al=am.lastIndexOf("/");var an=1;am=am.slice(0,al+an)}}}if(R(am,"/js/")){am=g(am,"js/".length)}return am}function N(ar){var au="Piwik_Overlay";var am=new RegExp("index\\.php\\?module=Overlay&action=startOverlaySession&idSite=([0-9]+)&period=([^&]+)&date=([^&]+)(&segment=.*)?$");var an=am.exec(G.referrer);if(an){var ap=an[1];
+if(ap!==String(ar)){return false}var aq=an[2],al=an[3],ao=an[4];if(!ao){ao=""}else{if(ao.indexOf("&segment=")===0){ao=ao.substr("&segment=".length)}}T.name=au+"###"+aq+"###"+al+"###"+ao}var at=T.name.split("###");return at.length===4&&at[0]===au}function Y(am,at,ao){var ar=T.name.split("###"),aq=ar[1],al=ar[2],ap=ar[3],an=O(am,at);o(an+"plugins/Overlay/client/client.js?v=1",function(){Piwik_Overlay_Client.initialize(an,ao,aq,al,ap)})}function u(){var an;try{an=T.frameElement}catch(am){return true}if(J(an)){return(an&&String(an.nodeName).toLowerCase()==="iframe")?true:false}try{return T.self!==T.top}catch(al){return true}}function Q(b7,b2){var bB=this,a8="mtm_consent",cE="mtm_consent_removed",bX=Z(G.domain,T.location.href,K()),cM=L(bX[0]),bG=p(bX[1]),bh=p(bX[2]),cK=false,cb="GET",cZ=cb,aE="application/x-www-form-urlencoded; charset=UTF-8",cq=aE,aA=b7||"",bA="",cQ="",bZ=b2||"",bs="",bH="",aZ,bd="",cW=["7z","aac","apk","arc","arj","asf","asx","avi","azw3","bin","csv","deb","dmg","doc","docx","epub","exe","flv","gif","gz","gzip","hqx","ibooks","jar","jpg","jpeg","js","mobi","mp2","mp3","mp4","mpg","mpeg","mov","movie","msi","msp","odb","odf","odg","ods","odt","ogg","ogv","pdf","phps","png","ppt","pptx","qt","qtm","ra","ram","rar","rpm","sea","sit","tar","tbz","tbz2","bz","bz2","tgz","torrent","txt","wav","wma","wmv","wpd","xls","xlsx","xml","z","zip"],au=[cM],bt=[],bE=[],a3=[],bC=500,cA,a0,bK,bI,al,ck=["pk_campaign","piwik_campaign","utm_campaign","utm_source","utm_medium"],bz=["pk_kwd","piwik_kwd","utm_term"],be="_pk_",ar="pk_vid",aU=180,cO,bj,bL=false,bf=false,cI,a9,bp,cB=33955200000,ci=1800000,cV=15768000000,aX=true,cg=0,bJ=false,aL=false,b4,bP={},cf={},bg={},bn=200,cR={},cX={},b3=[],b8=false,cu=false,am=false,cY=false,cF=false,aJ=false,a7=u(),cP=null,b5,aM,bu,b0=aj,bi,aG,cl=0,bo=["id","ses","cvar","ref"],ct=false,bv=null,cC=[],at=U++;
+try{bd=G.title}catch(cr){bd=""}function c2(dd,db,da,dc,c9,c8){if(bf){return}var c7;if(da){c7=new Date();c7.setTime(c7.getTime()+da)}G.cookie=dd+"="+t(db)+(da?";expires="+c7.toGMTString():"")+";path="+(dc||"/")+(c9?";domain="+c9:"")+(c8?";secure":"")}function az(c9){if(bf){return 0}var c7=new RegExp("(^|;)[ ]*"+c9+"=([^;]*)"),c8=c7.exec(G.cookie);return c8?S(c8[2]):0}bv=!az(cE);function bV(c7){var c8;c7=k(c7,ar);if(bI){c8=new RegExp("#.*");return c7.replace(c8,"")}return c7}function bO(c9,c7){var da=s(c7),c8;if(da){return c7}if(c7.slice(0,1)==="/"){return s(c9)+"://"+d(c9)+c7}c9=bV(c9);c8=c9.indexOf("?");if(c8>=0){c9=c9.slice(0,c8)}c8=c9.lastIndexOf("/");if(c8!==c9.length-1){c9=c9.slice(0,c8+1)}return c9+c7}function cz(c9,c7){var c8;c9=String(c9).toLowerCase();c7=String(c7).toLowerCase();if(c9===c7){return true}if(c7.slice(0,1)==="."){if(c9===c7.slice(1)){return true}c8=c9.length-c7.length;if((c8>0)&&(c9.slice(c8)===c7)){return true}}return false}function ce(c7){var c8=document.createElement("a");
+if(c7.indexOf("//")!==0&&c7.indexOf("http")!==0){if(c7.indexOf("*")===0){c7=c7.substr(1)}if(c7.indexOf(".")===0){c7=c7.substr(1)}c7="http://"+c7}c8.href=v.toAbsoluteUrl(c7);if(c8.pathname){return c8.pathname}return""}function aY(c8,c7){if(!ag(c7,"/")){c7="/"+c7}if(!ag(c8,"/")){c8="/"+c8}var c9=(c7==="/"||c7==="/*");if(c9){return true}if(c8===c7){return true}c7=String(c7).toLowerCase();c8=String(c8).toLowerCase();if(R(c7,"*")){c7=c7.slice(0,-1);c9=(!c7||c7==="/");if(c9){return true}if(c8===c7){return true}return c8.indexOf(c7)===0}if(!R(c8,"/")){c8+="/"}if(!R(c7,"/")){c7+="/"}return c8.indexOf(c7)===0}function ao(db,dd){var c8,c7,c9,da,dc;for(c8=0;c8<au.length;c8++){da=L(au[c8]);dc=ce(au[c8]);if(cz(db,da)&&aY(dd,dc)){return true}}return false}function aQ(da){var c8,c7,c9;for(c8=0;c8<au.length;c8++){c7=L(au[c8].toLowerCase());if(da===c7){return true}if(c7.slice(0,1)==="."){if(da===c7.slice(1)){return true}c9=da.length-c7.length;if((c9>0)&&(da.slice(c9)===c7)){return true}}}return false}function cj(c7,c9){c7=c7.replace("send_image=0","send_image=1");
+var c8=new Image(1,1);c8.onload=function(){E=0;if(typeof c9==="function"){c9()}};c8.src=aA+(aA.indexOf("?")<0?"?":"&")+c7}function a1(c8){var dc="object"===typeof h&&"function"===typeof h.sendBeacon&&"function"===typeof Blob;if(!dc){return false}var db={type:"application/x-www-form-urlencoded; charset=UTF-8"};var da=false;try{var c7=new Blob([c8],db);da=h.sendBeacon(aA,c7)}catch(c9){return false}return da}function cU(c8,c9,c7){if(!J(c7)||null===c7){c7=true}if(m&&a1(c8)){return}setTimeout(function(){if(m&&a1(c8)){return}var dc;try{var db=T.XMLHttpRequest?new T.XMLHttpRequest():T.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):null;db.open("POST",aA,true);db.onreadystatechange=function(){if(this.readyState===4&&!(this.status>=200&&this.status<300)){var dd=m&&a1(c8);if(!dd&&c7){cj(c8,c9)}}else{if(this.readyState===4&&(typeof c9==="function")){c9()}}};db.setRequestHeader("Content-Type",cq);db.send(c8)}catch(da){dc=m&&a1(c8);if(!dc&&c7){cj(c8,c9)}}},50)}function b9(c8){var c7=new Date();
+var c9=c7.getTime()+c8;if(!r||c9>r){r=c9}}function ch(c7){if(b5||!a0||!bv){return}b5=setTimeout(function c8(){b5=null;if(!a7){a7=(!G.hasFocus||G.hasFocus())}if(!a7){ch(a0);return}if(bK()){return}var c9=new Date(),da=a0-(c9.getTime()-cP);da=Math.min(a0,da);ch(da)},c7||a0)}function bD(){if(!b5){return}clearTimeout(b5);b5=null}function a5(){a7=true;if(bK()){return}ch()}function av(){bD()}function c4(){if(aJ||!a0){return}aJ=true;ak(T,"focus",a5);ak(T,"blur",av);ch()}function cv(db){var c8=new Date();var c7=c8.getTime();cP=c7;if(cu&&c7<cu){var c9=cu-c7;setTimeout(db,c9);b9(c9+50);cu+=50;return}if(cu===false){var da=800;cu=c7+da}db()}function by(c8,c7,c9){if(!bv){cC.push(c8);return}if(!cI&&c8){if(ct&&bv){c8+="&consent=1"}cv(function(){if(cZ==="POST"||String(c8).length>2000){cU(c8,c9)}else{cj(c8,c9)}b9(c7)})}if(!aJ){c4()}else{ch()}}function cd(c7){if(cI){return false}return(c7&&c7.length)}function c3(c9,c7){if(!cd(c9)){return}if(!bv){cC.push(c9);return}var c8='{"requests":["?'+c9.join('","?')+'"]}';
+cv(function(){cU(c8,null,false);b9(c7)})}function aO(c7){return be+c7+"."+bZ+"."+bi}function bY(){if(bf){return"0"}if(!J(h.cookieEnabled)){var c7=aO("testcookie");c2(c7,"1");return az(c7)==="1"?"1":"0"}return h.cookieEnabled?"1":"0"}function bc(){bi=b0((cO||cM)+(bj||"/")).slice(0,4)}function bQ(){var c8=aO("cvar"),c7=az(c8);if(c7.length){c7=JSON_PIWIK.parse(c7);if(W(c7)){return c7}}return{}}function cw(){if(aL===false){aL=bQ()}}function cJ(){return b0((h.userAgent||"")+(h.platform||"")+JSON_PIWIK.stringify(cX)+(new Date()).getTime()+Math.random()).slice(0,16)}function aw(){return b0((h.userAgent||"")+(h.platform||"")+JSON_PIWIK.stringify(cX)).slice(0,6)}function ba(){return Math.floor((new Date()).getTime()/1000)}function aF(){var c8=ba();var c9=aw();var c7=String(c8)+c9;return c7}function cT(c9){c9=String(c9);var dc=aw();var da=dc.length;var db=c9.substr(-1*da,da);var c8=parseInt(c9.substr(0,c9.length-da),10);if(c8&&db&&db===dc){var c7=ba();if(aU<=0){return true}if(c7>=c8&&c7<=(c8+aU)){return true
+}}return false}function c5(c7){if(!cF){return""}var db=f(c7,ar);if(!db){return""}db=String(db);var c9=new RegExp("^[a-zA-Z0-9]+$");if(db.length===32&&c9.test(db)){var c8=db.substr(16,32);if(cT(c8)){var da=db.substr(0,16);return da}}return""}function cG(){if(!bH){bH=c5(bG)}var c9=new Date(),c7=Math.round(c9.getTime()/1000),c8=aO("id"),dc=az(c8),db,da;if(dc){db=dc.split(".");db.unshift("0");if(bH.length){db[1]=bH}return db}if(bH.length){da=bH}else{if("0"===bY()){da=""}else{da=cJ()}}db=["1",da,c7,0,c7,"",""];return db}function aT(){var de=cG(),da=de[0],db=de[1],c8=de[2],c7=de[3],dc=de[4],c9=de[5];if(!J(de[6])){de[6]=""}var dd=de[6];return{newVisitor:da,uuid:db,createTs:c8,visitCount:c7,currentVisitTs:dc,lastVisitTs:c9,lastEcommerceOrderTs:dd}}function aD(){var da=new Date(),c8=da.getTime(),db=aT().createTs;var c7=parseInt(db,10);var c9=(c7*1000)+cB-c8;return c9}function aH(c7){if(!bZ){return}var c9=new Date(),c8=Math.round(c9.getTime()/1000);if(!J(c7)){c7=aT()}var da=c7.uuid+"."+c7.createTs+"."+c7.visitCount+"."+c8+"."+c7.lastVisitTs+"."+c7.lastEcommerceOrderTs;
+c2(aO("id"),da,aD(),bj,cO,bL)}function bF(){var c7=az(aO("ref"));if(c7.length){try{c7=JSON_PIWIK.parse(c7);if(W(c7)){return c7}}catch(c8){}}return["","",0,""]}function bR(c9,c8,c7){c2(c9,"",-86400,c8,c7)}function bq(c8){var c7="testvalue";c2("test",c7,10000,null,c8);if(az("test")===c7){bR("test",null,c8);return true}return false}function aB(){var c8=bf;bf=false;var c7,c9;for(c7=0;c7<bo.length;c7++){c9=aO(bo[c7]);if(c9!==cE&&c9!==a8&&0!==az(c9)){bR(c9,bj,cO)}}bf=c8}function bW(c7){bZ=c7;aH()}function c6(db){if(!db||!W(db)){return}var da=[];var c9;for(c9 in db){if(Object.prototype.hasOwnProperty.call(db,c9)){da.push(c9)}}var dc={};da.sort();var c7=da.length;var c8;for(c8=0;c8<c7;c8++){dc[da[c8]]=db[da[c8]]}return dc}function b6(){c2(aO("ses"),"*",ci,bj,cO,bL)}function bb(){var da="";var c8="abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";var c9=c8.length;var c7;for(c7=0;c7<6;c7++){da+=c8.charAt(Math.floor(Math.random()*c9))}return da}function cm(c9,dv,dw,da){var du,c8=new Date(),dh=Math.round(c8.getTime()/1000),de,dt,db=1024,dB,di,dr=aL,dc=aO("ses"),dp=aO("ref"),dl=aO("cvar"),dm=az(dc),ds=bF(),dy=aZ||bG,df,c7;
+if(bf){aB()}if(cI){return""}var dn=aT();if(!J(da)){da=""}var dk=G.characterSet||G.charset;if(!dk||dk.toLowerCase()==="utf-8"){dk=null}df=ds[0];c7=ds[1];de=ds[2];dt=ds[3];if(!dm){var dx=ci/1000;if(!dn.lastVisitTs||(dh-dn.lastVisitTs)>dx){dn.visitCount++;dn.lastVisitTs=dn.currentVisitTs}if(!bp||!df.length){for(du in ck){if(Object.prototype.hasOwnProperty.call(ck,du)){df=f(dy,ck[du]);if(df.length){break}}}for(du in bz){if(Object.prototype.hasOwnProperty.call(bz,du)){c7=f(dy,bz[du]);if(c7.length){break}}}}dB=d(bh);di=dt.length?d(dt):"";if(dB.length&&!aQ(dB)&&(!bp||!di.length||aQ(di))){dt=bh}if(dt.length||df.length){de=dh;ds=[df,c7,de,bV(dt.slice(0,db))];c2(dp,JSON_PIWIK.stringify(ds),cV,bj,cO)}}c9+="&idsite="+bZ+"&rec=1&r="+String(Math.random()).slice(2,8)+"&h="+c8.getHours()+"&m="+c8.getMinutes()+"&s="+c8.getSeconds()+"&url="+t(bV(dy))+(bh.length?"&urlref="+t(bV(bh)):"")+((bs&&bs.length)?"&uid="+t(bs):"")+"&_id="+dn.uuid+"&_idts="+dn.createTs+"&_idvc="+dn.visitCount+"&_idn="+dn.newVisitor+(df.length?"&_rcn="+t(df):"")+(c7.length?"&_rck="+t(c7):"")+"&_refts="+de+"&_viewts="+dn.lastVisitTs+(String(dn.lastEcommerceOrderTs).length?"&_ects="+dn.lastEcommerceOrderTs:"")+(String(dt).length?"&_ref="+t(bV(dt.slice(0,db))):"")+(dk?"&cs="+t(dk):"")+"&send_image=0";
+for(du in cX){if(Object.prototype.hasOwnProperty.call(cX,du)){c9+="&"+du+"="+cX[du]}}var dA=[];if(dv){for(du in dv){if(Object.prototype.hasOwnProperty.call(dv,du)&&/^dimension\d+$/.test(du)){var dd=du.replace("dimension","");dA.push(parseInt(dd,10));dA.push(String(dd));c9+="&"+du+"="+dv[du];delete dv[du]}}}if(dv&&B(dv)){dv=null}for(du in bg){if(Object.prototype.hasOwnProperty.call(bg,du)){var dj=(-1===M(dA,du));if(dj){c9+="&dimension"+du+"="+bg[du]}}}if(dv){c9+="&data="+t(JSON_PIWIK.stringify(dv))}else{if(al){c9+="&data="+t(JSON_PIWIK.stringify(al))}}function dg(dC,dD){var dE=JSON_PIWIK.stringify(dC);if(dE.length>2){return"&"+dD+"="+t(dE)}return""}var dz=c6(bP);var dq=c6(cf);c9+=dg(dz,"cvar");c9+=dg(dq,"e_cvar");if(aL){c9+=dg(aL,"_cvar");for(du in dr){if(Object.prototype.hasOwnProperty.call(dr,du)){if(aL[du][0]===""||aL[du][1]===""){delete aL[du]}}}if(bJ){c2(dl,JSON_PIWIK.stringify(aL),ci,bj,cO)}}if(aX){if(cg){c9+="&gt_ms="+cg}else{if(i&&i.timing&&i.timing.requestStart&&i.timing.responseEnd){c9+="&gt_ms="+(i.timing.responseEnd-i.timing.requestStart)
+}}}if(aG){c9+="&pv_id="+aG}dn.lastEcommerceOrderTs=J(da)&&String(da).length?da:dn.lastEcommerceOrderTs;aH(dn);b6();c9+=aa(dw,{tracker:bB,request:c9});if(cQ.length){c9+="&"+cQ}if(A(b4)){c9=b4(c9)}return c9}bK=function a2(){var c7=new Date();if(cP+a0<=c7.getTime()){var c8=cm("ping=1",null,"ping");by(c8,bC);return true}return false};function bk(da,c9,df,db,c7,di){var dd="idgoal=0",de,c8=new Date(),dg=[],dh,dc=String(da).length;if(dc){dd+="&ec_id="+t(da);de=Math.round(c8.getTime()/1000)}dd+="&revenue="+c9;if(String(df).length){dd+="&ec_st="+df}if(String(db).length){dd+="&ec_tx="+db}if(String(c7).length){dd+="&ec_sh="+c7}if(String(di).length){dd+="&ec_dt="+di}if(cR){for(dh in cR){if(Object.prototype.hasOwnProperty.call(cR,dh)){if(!J(cR[dh][1])){cR[dh][1]=""}if(!J(cR[dh][2])){cR[dh][2]=""}if(!J(cR[dh][3])||String(cR[dh][3]).length===0){cR[dh][3]=0}if(!J(cR[dh][4])||String(cR[dh][4]).length===0){cR[dh][4]=1}dg.push(cR[dh])}}dd+="&ec_items="+t(JSON_PIWIK.stringify(dg))}dd=cm(dd,al,"ecommerce",de);
+by(dd,bC);if(dc){cR={}}}function bS(c7,db,da,c9,c8,dc){if(String(c7).length&&J(db)){bk(c7,db,da,c9,c8,dc)}}function bm(c7){if(J(c7)){bk("",c7,"","","","")}}function bT(c8,da,c9){aG=bb();var c7=cm("action_name="+t(ai(c8||bd)),da,"log");by(c7,bC,c9)}function aV(c9,c8){var da,c7="(^| )(piwik[_-]"+c8;if(c9){for(da=0;da<c9.length;da++){c7+="|"+c9[da]}}c7+=")( |$)";return new RegExp(c7)}function aP(c7){return(aA&&c7&&0===String(c7).indexOf(aA))}function cn(db,c7,dc,c8){if(aP(c7)){return 0}var da=aV(bE,"download"),c9=aV(a3,"link"),dd=new RegExp("\\.("+cW.join("|")+")([?&#]|$)","i");if(c9.test(db)){return"link"}if(c8||da.test(db)||dd.test(c7)){return"download"}if(dc){return 0}return"link"}function aq(c8){var c7;c7=c8.parentNode;while(c7!==null&&J(c7)){if(ac.isLinkElement(c8)){break}c8=c7;c7=c8.parentNode}return c8}function c1(dc){dc=aq(dc);if(!ac.hasNodeAttribute(dc,"href")){return}if(!J(dc.href)){return}var db=ac.getAttributeValueFromNode(dc,"href");if(aP(db)){return}var c8=dc.pathname||ce(dc.href);
+var dd=dc.hostname||d(dc.href);var de=dd.toLowerCase();var c9=dc.href.replace(dd,de);var da=new RegExp("^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto|tel):","i");if(!da.test(c9)){var c7=cn(dc.className,c9,ao(de,c8),ac.hasNodeAttribute(dc,"download"));if(c7){return{type:c7,href:c9}}}}function aK(c7,c8,c9,da){var db=v.buildInteractionRequestParams(c7,c8,c9,da);if(!db){return}return cm(db,null,"contentInteraction")}function cD(c9,da,de,c7,c8){if(!J(c9)){return}if(aP(c9)){return c9}var dc=v.toAbsoluteUrl(c9);var db="redirecturl="+t(dc)+"&";db+=aK(da,de,c7,(c8||c9));var dd="&";if(aA.indexOf("?")<0){dd="?"}return aA+dd+db}function a6(c7,c8){if(!c7||!c8){return false}var c9=v.findTargetNode(c7);if(v.shouldIgnoreInteraction(c9)){return false}c9=v.findTargetNodeNoDefault(c7);if(c9&&!V(c9,c8)){return false}return true}function co(c9,c8,db){if(!c9){return}var c7=v.findParentContentNode(c9);if(!c7){return}if(!a6(c7,c9)){return}var da=v.buildContentBlock(c7);if(!da){return}if(!da.target&&db){da.target=db
+}return v.buildInteractionRequestParams(c8,da.name,da.piece,da.target)}function aR(c8){if(!b3||!b3.length){return false}var c7,c9;for(c7=0;c7<b3.length;c7++){c9=b3[c7];if(c9&&c9.name===c8.name&&c9.piece===c8.piece&&c9.target===c8.target){return true}}return false}function bx(da){if(!da){return false}var dd=v.findTargetNode(da);if(!dd||v.shouldIgnoreInteraction(dd)){return false}var de=c1(dd);if(cY&&de&&de.type){return false}if(ac.isLinkElement(dd)&&ac.hasNodeAttributeWithValue(dd,"href")){var c7=String(ac.getAttributeValueFromNode(dd,"href"));if(0===c7.indexOf("#")){return false}if(aP(c7)){return true}if(!v.isUrlToCurrentDomain(c7)){return false}var db=v.buildContentBlock(da);if(!db){return}var c9=db.name;var df=db.piece;var dc=db.target;if(!ac.hasNodeAttributeWithValue(dd,v.CONTENT_TARGET_ATTR)||dd.wasContentTargetAttrReplaced){dd.wasContentTargetAttrReplaced=true;dc=v.toAbsoluteUrl(c7);ac.setAnyAttribute(dd,v.CONTENT_TARGET_ATTR,dc)}var c8=cD(c7,"click",c9,df,dc);v.setHrefAttribute(dd,c8);
+return true}return false}function aI(c8){if(!c8||!c8.length){return}var c7;for(c7=0;c7<c8.length;c7++){bx(c8[c7])}}function aS(c7){return function(c8){if(!c7){return}var db=v.findParentContentNode(c7);var dc;if(c8){dc=c8.target||c8.srcElement}if(!dc){dc=c7}if(!a6(db,dc)){return}b9(bC);if(ac.isLinkElement(c7)&&ac.hasNodeAttributeWithValue(c7,"href")&&ac.hasNodeAttributeWithValue(c7,v.CONTENT_TARGET_ATTR)){var c9=ac.getAttributeValueFromNode(c7,"href");if(!aP(c9)&&c7.wasContentTargetAttrReplaced){ac.setAnyAttribute(c7,v.CONTENT_TARGET_ATTR,"")}}var dg=c1(c7);if(am&&dg&&dg.type){return dg.type}if(bx(db)){return"href"}var dd=v.buildContentBlock(db);if(!dd){return}var da=dd.name;var dh=dd.piece;var df=dd.target;var de=aK("click",da,dh,df);by(de,bC);return de}}function bU(c9){if(!c9||!c9.length){return}var c7,c8;for(c7=0;c7<c9.length;c7++){c8=v.findTargetNode(c9[c7]);if(c8&&!c8.contentInteractionTrackingSetupDone){c8.contentInteractionTrackingSetupDone=true;ak(c8,"click",aS(c8))}}}function br(c9,da){if(!c9||!c9.length){return[]
+}var c7,c8;for(c7=0;c7<c9.length;c7++){if(aR(c9[c7])){c9.splice(c7,1);c7--}else{b3.push(c9[c7])}}if(!c9||!c9.length){return[]}aI(da);bU(da);var db=[];for(c7=0;c7<c9.length;c7++){c8=cm(v.buildImpressionRequestParams(c9[c7].name,c9[c7].piece,c9[c7].target),undefined,"contentImpressions");if(c8){db.push(c8)}}return db}function cs(c8){var c7=v.collectContent(c8);return br(c7,c8)}function a4(c8){if(!c8||!c8.length){return[]}var c7;for(c7=0;c7<c8.length;c7++){if(!v.isNodeVisible(c8[c7])){c8.splice(c7,1);c7--}}if(!c8||!c8.length){return[]}return cs(c8)}function aC(c9,c7,c8){var da=v.buildImpressionRequestParams(c9,c7,c8);return cm(da,null,"contentImpression")}function c0(da,c8){if(!da){return}var c7=v.findParentContentNode(da);var c9=v.buildContentBlock(c7);if(!c9){return}if(!c8){c8="Unknown"}return aK(c8,c9.name,c9.piece,c9.target)}function cH(c8,da,c7,c9){return"e_c="+t(c8)+"&e_a="+t(da)+(J(c7)?"&e_n="+t(c7):"")+(J(c9)?"&e_v="+t(c9):"")}function ap(c9,db,c7,da,dd,dc){if(a(String(c9)).length===0||a(String(db)).length===0){ah("Error while logging event: Parameters `category` and `action` must not be empty or filled with whitespaces");
+return false}var c8=cm(cH(c9,db,c7,da),dd,"event");by(c8,bC,dc)}function b1(c7,da,c8,db){var c9=cm("search="+t(c7)+(da?"&search_cat="+t(da):"")+(J(c8)?"&search_count="+c8:""),db,"sitesearch");by(c9,bC)}function cL(c7,db,da,c9){var c8=cm("idgoal="+c7+(db?"&revenue="+db:""),da,"goal");by(c8,bC,c9)}function cS(da,c7,de,dd,c9){var dc=c7+"="+t(bV(da));var c8=co(c9,"click",da);if(c8){dc+="&"+c8}var db=cm(dc,de,"link");by(db,bC,dd)}function bN(c8,c7){if(c8!==""){return c8+c7.charAt(0).toUpperCase()+c7.slice(1)}return c7}function ca(dc){var db,c7,da=["","webkit","ms","moz"],c9;if(!a9){for(c7=0;c7<da.length;c7++){c9=da[c7];if(Object.prototype.hasOwnProperty.call(G,bN(c9,"hidden"))){if(G[bN(c9,"visibilityState")]==="prerender"){db=true}break}}}if(db){ak(G,c9+"visibilitychange",function c8(){G.removeEventListener(c9+"visibilitychange",c8,false);dc()});return}dc()}function bl(){var c8=aT().uuid;var c7=aF();return c8+c7}function cc(c7){if(!c7){return}if(!ac.hasNodeAttribute(c7,"href")){return}var c8=ac.getAttributeValueFromNode(c7,"href");
+if(!c8||aP(c8)){return}c8=k(c8,ar);if(c8.indexOf("?")>0){c8+="&"}else{c8+="?"}var c9=bl();c8=F(c8,ar,c9);ac.setAnyAttribute(c7,"href",c8)}function ax(da){var db=ac.getAttributeValueFromNode(da,"href");if(!db){return false}db=String(db);var c8=db.indexOf("//")===0||db.indexOf("http://")===0||db.indexOf("https://")===0;if(!c8){return false}var c7=da.pathname||ce(da.href);var c9=(da.hostname||d(da.href)).toLowerCase();if(ao(c9,c7)){if(!cz(cM,L(c9))){return true}return false}return false}function cy(c7){var c8=c1(c7);if(c8&&c8.type){c8.href=p(c8.href);cS(c8.href,c8.type,undefined,null,c7);return}if(cF){c7=aq(c7);if(ax(c7)){cc(c7)}}}function cp(){return G.all&&!G.addEventListener}function cN(c7){var c9=c7.which;var c8=(typeof c7.button);if(!c9&&c8!=="undefined"){if(cp()){if(c7.button&1){c9=1}else{if(c7.button&2){c9=3}else{if(c7.button&4){c9=2}}}}else{if(c7.button===0||c7.button==="0"){c9=1}else{if(c7.button&1){c9=2}else{if(c7.button&2){c9=3}}}}}return c9}function bM(c7){switch(cN(c7)){case 1:return"left";
+case 2:return"middle";case 3:return"right"}}function aW(c7){return c7.target||c7.srcElement}function ay(c7){return function(da){da=da||T.event;var c9=bM(da);var db=aW(da);if(da.type==="click"){var c8=false;if(c7&&c9==="middle"){c8=true}if(db&&!c8){cy(db)}}else{if(da.type==="mousedown"){if(c9==="middle"&&db){aM=c9;bu=db}else{aM=bu=null}}else{if(da.type==="mouseup"){if(c9===aM&&db===bu){cy(db)}aM=bu=null}else{if(da.type==="contextmenu"){cy(db)}}}}}}function an(c9,c8){var c7=typeof c8;if(c7==="undefined"){c8=true}ak(c9,"click",ay(c8),false);if(c8){ak(c9,"mouseup",ay(c8),false);ak(c9,"mousedown",ay(c8),false);ak(c9,"contextmenu",ay(c8),false)}}function bw(c9,db){am=true;var da,c8=aV(bt,"ignore"),dc=G.links,c7=null,dd=null;if(dc){for(da=0;da<dc.length;da++){c7=dc[da];if(!c8.test(c7.className)){dd=typeof c7.piwikTrackers;if("undefined"===dd){c7.piwikTrackers=[]}if(-1===M(c7.piwikTrackers,db)){c7.piwikTrackers.push(db);an(c7,c9)}}}}}function aN(c8,db,dc){if(b8){return true}b8=true;var dd=false;
+var da,c9;function c7(){dd=true}n(function(){function de(dg){setTimeout(function(){if(!b8){return}dd=false;dc.trackVisibleContentImpressions();de(dg)},dg)}function df(dg){setTimeout(function(){if(!b8){return}if(dd){dd=false;dc.trackVisibleContentImpressions()}df(dg)},dg)}if(c8){da=["scroll","resize"];for(c9=0;c9<da.length;c9++){if(G.addEventListener){G.addEventListener(da[c9],c7,false)}else{T.attachEvent("on"+da[c9],c7)}}df(100)}if(db&&db>0){db=parseInt(db,10);de(db)}})}function cx(){var c8,da,db={pdf:"application/pdf",qt:"video/quicktime",realp:"audio/x-pn-realaudio-plugin",wma:"application/x-mplayer2",dir:"application/x-director",fla:"application/x-shockwave-flash",java:"application/x-java-vm",gears:"application/x-googlegears",ag:"application/x-silverlight"};if(!((new RegExp("MSIE")).test(h.userAgent))){if(h.mimeTypes&&h.mimeTypes.length){for(c8 in db){if(Object.prototype.hasOwnProperty.call(db,c8)){da=h.mimeTypes[db[c8]];cX[c8]=(da&&da.enabledPlugin)?"1":"0"}}}if(!((new RegExp("Edge[ /](\\d+[\\.\\d]+)")).test(h.userAgent))&&typeof navigator.javaEnabled!=="unknown"&&J(h.javaEnabled)&&h.javaEnabled()){cX.java="1"
+}if(A(T.GearsFactory)){cX.gears="1"}cX.cookie=bY()}var c9=parseInt(X.width,10);var c7=parseInt(X.height,10);cX.res=parseInt(c9,10)+"x"+parseInt(c7,10)}cx();bc();aH();this.getVisitorId=function(){return aT().uuid};this.getVisitorInfo=function(){return cG()};this.getAttributionInfo=function(){return bF()};this.getAttributionCampaignName=function(){return bF()[0]};this.getAttributionCampaignKeyword=function(){return bF()[1]};this.getAttributionReferrerTimestamp=function(){return bF()[2]};this.getAttributionReferrerUrl=function(){return bF()[3]};this.setTrackerUrl=function(c7){aA=c7};this.getTrackerUrl=function(){return aA};this.getPiwikUrl=function(){return O(this.getTrackerUrl(),bA)};this.addTracker=function(c7,c9){if(!c9){throw new Error("A siteId must be given to add a new tracker")}if(!J(c7)||null===c7){c7=this.getTrackerUrl()}var c8=new Q(c7,c9);I.push(c8);return c8};this.getSiteId=function(){return bZ};this.setSiteId=function(c7){bW(c7)};this.resetUserId=function(){bs=""};this.setUserId=function(c7){if(!J(c7)||!c7.length){return
+}bs=c7};this.getUserId=function(){return bs};this.setCustomData=function(c7,c8){if(W(c7)){al=c7}else{if(!al){al={}}al[c7]=c8}};this.getCustomData=function(){return al};this.setCustomRequestProcessing=function(c7){b4=c7};this.appendToTrackingUrl=function(c7){cQ=c7};this.getRequest=function(c7){return cm(c7)};this.addPlugin=function(c7,c8){b[c7]=c8};this.setCustomDimension=function(c7,c8){c7=parseInt(c7,10);if(c7>0){if(!J(c8)){c8=""}if(!w(c8)){c8=String(c8)}bg[c7]=c8}};this.getCustomDimension=function(c7){c7=parseInt(c7,10);if(c7>0&&Object.prototype.hasOwnProperty.call(bg,c7)){return bg[c7]}};this.deleteCustomDimension=function(c7){c7=parseInt(c7,10);if(c7>0){delete bg[c7]}};this.setCustomVariable=function(c8,c7,db,c9){var da;if(!J(c9)){c9="visit"}if(!J(c7)){return}if(!J(db)){db=""}if(c8>0){c7=!w(c7)?String(c7):c7;db=!w(db)?String(db):db;da=[c7.slice(0,bn),db.slice(0,bn)];if(c9==="visit"||c9===2){cw();aL[c8]=da}else{if(c9==="page"||c9===3){bP[c8]=da}else{if(c9==="event"){cf[c8]=da}}}}};this.getCustomVariable=function(c8,c9){var c7;
+if(!J(c9)){c9="visit"}if(c9==="page"||c9===3){c7=bP[c8]}else{if(c9==="event"){c7=cf[c8]}else{if(c9==="visit"||c9===2){cw();c7=aL[c8]}}}if(!J(c7)||(c7&&c7[0]==="")){return false}return c7};this.deleteCustomVariable=function(c7,c8){if(this.getCustomVariable(c7,c8)){this.setCustomVariable(c7,"","",c8)}};this.deleteCustomVariables=function(c7){if(c7==="page"||c7===3){bP={}}else{if(c7==="event"){cf={}}else{if(c7==="visit"||c7===2){aL={}}}}};this.storeCustomVariablesInCookie=function(){bJ=true};this.setLinkTrackingTimer=function(c7){bC=c7};this.getLinkTrackingTimer=function(){return bC};this.setDownloadExtensions=function(c7){if(w(c7)){c7=c7.split("|")}cW=c7};this.addDownloadExtensions=function(c8){var c7;if(w(c8)){c8=c8.split("|")}for(c7=0;c7<c8.length;c7++){cW.push(c8[c7])}};this.removeDownloadExtensions=function(c9){var c8,c7=[];if(w(c9)){c9=c9.split("|")}for(c8=0;c8<cW.length;c8++){if(M(c9,cW[c8])===-1){c7.push(cW[c8])}}cW=c7};this.setDomains=function(c7){au=w(c7)?[c7]:c7;var db=false,c9=0,c8;
+for(c9;c9<au.length;c9++){c8=String(au[c9]);if(cz(cM,L(c8))){db=true;break}var da=ce(c8);if(da&&da!=="/"&&da!=="/*"){db=true;break}}if(!db){au.push(cM)}};this.enableCrossDomainLinking=function(){cF=true};this.disableCrossDomainLinking=function(){cF=false};this.isCrossDomainLinkingEnabled=function(){return cF};this.setCrossDomainLinkingTimeout=function(c7){aU=c7};this.getCrossDomainLinkingUrlParameter=function(){return t(ar)+"="+t(bl())};this.setIgnoreClasses=function(c7){bt=w(c7)?[c7]:c7};this.setRequestMethod=function(c7){cZ=c7||cb};this.setRequestContentType=function(c7){cq=c7||aE};this.setReferrerUrl=function(c7){bh=c7};this.setCustomUrl=function(c7){aZ=bO(bG,c7)};this.getCurrentUrl=function(){return aZ||bG};this.setDocumentTitle=function(c7){bd=c7};this.setAPIUrl=function(c7){bA=c7};this.setDownloadClasses=function(c7){bE=w(c7)?[c7]:c7};this.setLinkClasses=function(c7){a3=w(c7)?[c7]:c7};this.setCampaignNameKey=function(c7){ck=w(c7)?[c7]:c7};this.setCampaignKeywordKey=function(c7){bz=w(c7)?[c7]:c7
+};this.discardHashTag=function(c7){bI=c7};this.setCookieNamePrefix=function(c7){be=c7;aL=bQ()};this.setCookieDomain=function(c7){var c8=L(c7);if(bq(c8)){cO=c8;bc()}};this.getCookieDomain=function(){return cO};this.hasCookies=function(){return"1"===bY()};this.setSessionCookie=function(c9,c8,c7){if(!c9){throw new Error("Missing cookie name")}if(!J(c7)){c7=ci}bo.push(c9);c2(aO(c9),c8,c7,bj,cO)};this.getCookie=function(c8){var c7=az(aO(c8));if(c7===0){return null}return c7};this.setCookiePath=function(c7){bj=c7;bc()};this.getCookiePath=function(c7){return bj};this.setVisitorCookieTimeout=function(c7){cB=c7*1000};this.setSessionCookieTimeout=function(c7){ci=c7*1000};this.getSessionCookieTimeout=function(){return ci};this.setReferralCookieTimeout=function(c7){cV=c7*1000};this.setConversionAttributionFirstReferrer=function(c7){bp=c7};this.setSecureCookie=function(c7){bL=c7};this.disableCookies=function(){bf=true;cX.cookie="0";if(bZ){aB()}};this.deleteCookies=function(){aB()};this.setDoNotTrack=function(c8){var c7=h.doNotTrack||h.msDoNotTrack;
+cI=c8&&(c7==="yes"||c7==="1");if(cI){this.disableCookies()}};this.addListener=function(c8,c7){an(c8,c7)};this.enableLinkTracking=function(c8){cY=true;var c7=this;ca(function(){q(function(){bw(c8,c7)})})};this.enableJSErrorTracking=function(){if(cK){return}cK=true;var c7=T.onerror;T.onerror=function(dc,da,c9,db,c8){ca(function(){var dd="JavaScript Errors";var de=da+":"+c9;if(db){de+=":"+db}ap(dd,de,dc)});if(c7){return c7(dc,da,c9,db,c8)}return false}};this.disablePerformanceTracking=function(){aX=false};this.setGenerationTimeMs=function(c7){cg=parseInt(c7,10)};this.enableHeartBeatTimer=function(c7){c7=Math.max(c7,1);a0=(c7||15)*1000;if(cP!==null){c4()}};this.disableHeartBeatTimer=function(){bD();if(a0||aJ){if(T.removeEventListener){T.removeEventListener("focus",a5,true);T.removeEventListener("blur",av,true)}else{if(T.detachEvent){T.detachEvent("onfocus",a5);T.detachEvent("onblur",av)}}}a0=null;aJ=false};this.killFrame=function(){if(T.location!==T.top.location){T.top.location=T.location}};
+this.redirectFile=function(c7){if(T.location.protocol==="file:"){T.location=c7}};this.setCountPreRendered=function(c7){a9=c7};this.trackGoal=function(c7,da,c9,c8){ca(function(){cL(c7,da,c9,c8)})};this.trackLink=function(c8,c7,da,c9){ca(function(){cS(c8,c7,da,c9)})};this.getNumTrackedPageViews=function(){return cl};this.trackPageView=function(c7,c9,c8){b3=[];cC=[];if(N(bZ)){ca(function(){Y(aA,bA,bZ)})}else{ca(function(){cl++;bT(c7,c9,c8)})}};this.trackAllContentImpressions=function(){if(N(bZ)){return}ca(function(){q(function(){var c7=v.findContentNodes();var c8=cs(c7);c3(c8,bC)})})};this.trackVisibleContentImpressions=function(c7,c8){if(N(bZ)){return}if(!J(c7)){c7=true}if(!J(c8)){c8=750}aN(c7,c8,this);ca(function(){n(function(){var c9=v.findContentNodes();var da=a4(c9);c3(da,bC)})})};this.trackContentImpression=function(c9,c7,c8){if(N(bZ)){return}c9=a(c9);c7=a(c7);c8=a(c8);if(!c9){return}c7=c7||"Unknown";ca(function(){var da=aC(c9,c7,c8);by(da,bC)})};this.trackContentImpressionsWithinNode=function(c7){if(N(bZ)||!c7){return
+}ca(function(){if(b8){n(function(){var c8=v.findContentNodesWithinNode(c7);var c9=a4(c8);c3(c9,bC)})}else{q(function(){var c8=v.findContentNodesWithinNode(c7);var c9=cs(c8);c3(c9,bC)})}})};this.trackContentInteraction=function(c9,da,c7,c8){if(N(bZ)){return}c9=a(c9);da=a(da);c7=a(c7);c8=a(c8);if(!c9||!da){return}c7=c7||"Unknown";ca(function(){var db=aK(c9,da,c7,c8);by(db,bC)})};this.trackContentInteractionNode=function(c8,c7){if(N(bZ)||!c8){return}ca(function(){var c9=c0(c8,c7);by(c9,bC)})};this.logAllContentBlocksOnPage=function(){var c9=v.findContentNodes();var c7=v.collectContent(c9);var c8=typeof console;if(c8!=="undefined"&&console&&console.log){console.log(c7)}};this.trackEvent=function(c8,da,c7,c9,dc,db){ca(function(){ap(c8,da,c7,c9,dc,db)})};this.trackSiteSearch=function(c7,c9,c8,da){ca(function(){b1(c7,c9,c8,da)})};this.setEcommerceView=function(da,c7,c9,c8){if(!J(c9)||!c9.length){c9=""}else{if(c9 instanceof Array){c9=JSON_PIWIK.stringify(c9)}}bP[5]=["_pkc",c9];if(J(c8)&&String(c8).length){bP[2]=["_pkp",c8]
+}if((!J(da)||!da.length)&&(!J(c7)||!c7.length)){return}if(J(da)&&da.length){bP[3]=["_pks",da]}if(!J(c7)||!c7.length){c7=""}bP[4]=["_pkn",c7]};this.addEcommerceItem=function(db,c7,c9,c8,da){if(db.length){cR[db]=[db,c7,c9,c8,da]}};this.removeEcommerceItem=function(c7){if(c7.length){delete cR[c7]}};this.clearEcommerceCart=function(){cR={}};this.trackEcommerceOrder=function(c7,db,da,c9,c8,dc){bS(c7,db,da,c9,c8,dc)};this.trackEcommerceCartUpdate=function(c7){bm(c7)};this.trackRequest=function(c8,da,c9,c7){ca(function(){var db=cm(c8,da,c7);by(db,bC,c9)})};this.queueRequest=function(c7){ca(function(){var c8=cm(c7);requestQueue.push(c8)})};this.getRememberedConsent=function(){var c7=az(a8);if(az(cE)){if(c7){bR(a8,bj,cO)}return null}if(!c7||c7===0){return null}return c7};this.hasRememberedConsent=function(){return !!this.getRememberedConsent()};this.requireConsent=function(){ct=true;bv=this.hasRememberedConsent();x++;b["CoreConsent"+x]={unload:function(){if(!bv){aB()}}}};this.setConsentGiven=function(){bv=true;
+bR(cE,bj,cO);var c8,c7;for(c8=0;c8<cC.length;c8++){c7=typeof cC[c8];if(c7==="string"){by(cC[c8],bC)}else{if(c7==="object"){c3(cC[c8],bC)}}}cC=[]};this.rememberConsentGiven=function(c8){if(bf){ah("rememberConsentGiven is called but cookies are disabled, consent will not be remembered");return}if(c8){c8=c8*60*60*1000}this.setConsentGiven();var c7=new Date().getTime();c2(a8,c7,c8,bj,cO,bL)};this.forgetConsentGiven=function(){if(bf){ah("forgetConsentGiven is called but cookies are disabled, consent will not be forgotten");return}bR(a8,bj,cO);c2(cE,new Date().getTime(),0,bj,cO,bL);this.requireConsent()};this.isUserOptedOut=function(){return !bv};this.optUserOut=this.forgetConsentGiven;this.forgetUserOptOut=this.rememberConsentGiven;e.trigger("TrackerSetup",[this])}function H(){return{push:ad}}function c(aq,ap){var ar={};var an,ao;for(an=0;an<ap.length;an++){var al=ap[an];ar[al]=1;for(ao=0;ao<aq.length;ao++){if(aq[ao]&&aq[ao][0]){var am=aq[ao][0];if(al===am){ad(aq[ao]);delete aq[ao];if(ar[am]>1&&am!=="addTracker"){ah("The method "+am+' is registered more than once in "_paq" variable. Only the last call has an effect. Please have a look at the multiple Piwik trackers documentation: https://developer.piwik.org/guides/tracking-javascript-guide#multiple-piwik-trackers')
+}ar[am]++}}}}return aq}var C=["addTracker","disableCookies","setTrackerUrl","setAPIUrl","enableCrossDomainLinking","setCrossDomainLinkingTimeout","setSecureCookie","setCookiePath","setCookieDomain","setDomains","setUserId","setSiteId","enableLinkTracking","requireConsent","setConsentGiven"];function ab(al,an){var am=new Q(al,an);I.push(am);_paq=c(_paq,C);for(E=0;E<_paq.length;E++){if(_paq[E]){ad(_paq[E])}}_paq=new H();return am}ak(T,"beforeunload",af,false);Date.prototype.getTimeAlias=Date.prototype.getTime;e={initialized:false,JSON:JSON_PIWIK,DOM:{addEventListener:function(ao,an,am,al){var ap=typeof al;if(ap==="undefined"){al=false}ak(ao,an,am,al)},onLoad:n,onReady:q,isNodeVisible:j,isOrWasNodeVisible:v.isNodeVisible},on:function(am,al){if(!y[am]){y[am]=[]}y[am].push(al)},off:function(an,am){if(!y[an]){return}var al=0;for(al;al<y[an].length;al++){if(y[an][al]===am){y[an].splice(al,1)}}},trigger:function(an,ao,am){if(!y[an]){return}var al=0;for(al;al<y[an].length;al++){y[an][al].apply(am||T,ao)
+}},addPlugin:function(al,am){b[al]=am},getTracker:function(al,am){if(!J(am)){am=this.getAsyncTracker().getSiteId()}if(!J(al)){al=this.getAsyncTracker().getTrackerUrl()}return new Q(al,am)},getAsyncTrackers:function(){return I},addTracker:function(al,an){var am;if(!I.length){am=ab(al,an)}else{am=I[0].addTracker(al,an)}return am},getAsyncTracker:function(am,ap){var ao;if(I&&I.length&&I[0]){ao=I[0]}else{return ab(am,ap)}if(!ap&&!am){return ao}if((!J(ap)||null===ap)&&ao){ap=ao.getSiteId()}if((!J(am)||null===am)&&ao){am=ao.getTrackerUrl()}var an,al=0;for(al;al<I.length;al++){an=I[al];if(an&&String(an.getSiteId())===String(ap)&&an.getTrackerUrl()===am){return an}}},retryMissedPluginCalls:function(){var am=ae;ae=[];var al=0;for(al;al<am.length;al++){ad(am[al])}}};if(typeof define==="function"&&define.amd){define("piwik",[],function(){return e});define("matomo",[],function(){return e})}return e}())}
+/*!!! pluginTrackerHook */
+(function(){function b(){if("object"!==typeof _paq){return false}var c=typeof _paq.length;
+if("undefined"===c){return false}return !!_paq.length}if(window&&"object"===typeof window.piwikPluginAsyncInit&&window.piwikPluginAsyncInit.length){var a=0;for(a;a<window.piwikPluginAsyncInit.length;a++){if(typeof window.piwikPluginAsyncInit[a]==="function"){window.piwikPluginAsyncInit[a]()}}}if(window&&window.piwikAsyncInit){window.piwikAsyncInit()}if(!window.Piwik.getAsyncTrackers().length){if(b()){window.Piwik.addTracker()}else{_paq={push:function(c){var d=typeof console;if(d!=="undefined"&&console&&console.error){console.error("_paq.push() was used but Matomo tracker was not initialized before the matomo.js file was loaded. Make sure to configure the tracker via _paq.push before loading matomo.js. Alternatively, you can create a tracker via Matomo.addTracker() manually and then use _paq.push but it may not fully work as tracker methods may not be executed in the correct order.",c)}}}}}window.Piwik.trigger("PiwikInitialized",[]);window.Piwik.initialized=true}());(function(){var a=(typeof AnalyticsTracker);
+if(a==="undefined"){AnalyticsTracker=window.Piwik}}());if(typeof piwik_log!=="function"){piwik_log=function(b,f,d,g){function a(h){try{if(window["piwik_"+h]){return window["piwik_"+h]}}catch(i){}return}var c,e=window.Piwik.getTracker(d,f);e.setDocumentTitle(b);e.setCustomData(g);c=a("tracker_pause");if(c){e.setLinkTrackingTimer(c)}c=a("download_extensions");if(c){e.setDownloadExtensions(c)}c=a("hosts_alias");if(c){e.setDomains(c)}c=a("ignore_classes");if(c){e.setIgnoreClasses(c)}e.trackPageView();if(a("install_tracker")){piwik_track=function(i,k,j,h){e.setSiteId(k);e.setTrackerUrl(j);e.trackLink(i,h)};e.enableLinkTracking()}}}
+/*!! @license-end */;
diff --git a/tests/UI/expected-screenshots/Menus_admin_changed.png b/tests/UI/expected-screenshots/Menus_admin_changed.png
index 02846b4f21..9ebe576a61 100644
--- a/tests/UI/expected-screenshots/Menus_admin_changed.png
+++ b/tests/UI/expected-screenshots/Menus_admin_changed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5424b7cc9ef643785fb01432be95797150775a5ce4265bfa3e32f4f8b2ecfc4e
-size 54155
+oid sha256:1e361ddbac4575f41ddb46bed9988764be79af09feb792cabb73af3223ef06bc
+size 55902
diff --git a/tests/UI/expected-screenshots/Menus_admin_loaded.png b/tests/UI/expected-screenshots/Menus_admin_loaded.png
index 9fd17c7347..03ebe91612 100644
--- a/tests/UI/expected-screenshots/Menus_admin_loaded.png
+++ b/tests/UI/expected-screenshots/Menus_admin_loaded.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:083a99fc385b54acef5793db5c1ecc12fde3bfae253691dc43547bf7ead28153
-size 54150
+oid sha256:a7d83c2a832a4953a5751067c867c2f5d8281bc1a06af0ceeb3521715c4dd6cf
+size 55895
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png
index 0fd37f1f05..547ba75371 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fa241b21bef2984cb3c87f70d68af0823355573f306220f7943a4705294bc11f
-size 1542273
+oid sha256:90ee0e072293768cb4b2fa0266e9d94f59d3d32b130d10d2d75bb6dbec396d95
+size 1597892
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png
index cef8a9b33f..be9bb1b3fc 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fad6ab8b806b9a177b3c43644ed59b5444084b59a977084a50ed553fce8557d0
-size 292066
+oid sha256:c9b62b944f5d1b4f62eef2c716bbe9e166dc2f7e9123c0fab83a079fc2840dd2
+size 298660