diff options
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 "token_auth" 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+=">_ms="+cg}else{if(i&&i.timing&&i.timing.requestStart&&i.timing.responseEnd){c9+=">_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 |