diff --git a/Changelog.md b/Changelog.md
index c79f4db..c7cb999 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,21 @@
# Changelog
+## [3.2.3] - 07.06.2026
+
+- Dashboard-Overview erweitert um KPI-Leiste für die letzten 7 Tage (Besuche, Besucher, Top-Artikel, Seiten pro Sitzung)
+- Neue klickbare Analyse-Karten auf der Statistik-Startseite, die vorhandene Lazy-Blöcke gezielt laden und anspringen
+- Restliche harte Labels aus `fragments/overview.php` in Sprachdateien ausgelagert
+
+## [3.2.2] - 07.06.2026
+
+- 404/Non-200-Aufrufe werden bei aktivierter Option "Nur 200er Aufrufe erfassen" nicht mehr als Seitenaufrufe gespeichert ([#121](https://github.com/FriendsOfREDAXO/statistics/issues/121))
+- search_it-Indexierungsaufrufe werden wieder zuverlässig ignoriert, auch wenn URL-Parameter aus Statistik-URLs entfernt werden ([#114](https://github.com/FriendsOfREDAXO/statistics/issues/114))
+
+## [3.2.1] - 07.06.2026
+
+- Datumsbereich bleibt beim Wechsel zwischen Statistik-Tabs erhalten ([#88](https://github.com/FriendsOfREDAXO/statistics/issues/88))
+- Weitere harte UI-Texte in Sprachdateien ausgelagert ([#109](https://github.com/FriendsOfREDAXO/statistics/issues/109))
+
## [3.2.0] - 29.03.2026
- Performance improvements with lazy loading
diff --git a/assets/statistics.css b/assets/statistics.css
index 1775003..f62c838 100644
--- a/assets/statistics.css
+++ b/assets/statistics.css
@@ -23,3 +23,63 @@
#rex-page-statistics-stats.modal-open {
overflow: visible;
}
+
+.statistics_week_kpis {
+ margin-top: 10px;
+}
+
+.statistics-kpi-panel .panel-body {
+ min-height: 86px;
+}
+
+.statistics_cards_row {
+ margin-bottom: 14px;
+}
+
+.statistics_cards_heading {
+ margin-top: 6px;
+ margin-bottom: 12px;
+}
+
+.statistics-analysis-card {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ border: 1px solid #d7dde3;
+ border-radius: 4px;
+ background: #ffffff;
+ padding: 12px;
+ margin-bottom: 12px;
+ text-align: left;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.statistics-analysis-card:hover,
+.statistics-analysis-card:focus {
+ border-color: #5ca2de;
+ box-shadow: 0 0 0 2px rgba(92, 162, 222, 0.15);
+ outline: none;
+}
+
+.statistics-analysis-card__icon {
+ width: 40px;
+ min-width: 40px;
+ text-align: center;
+ color: #5ca2de;
+ font-size: 24px;
+}
+
+.statistics-analysis-card__content {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.statistics-analysis-card__title {
+ font-weight: 600;
+}
+
+.statistics-analysis-card__desc {
+ color: #66717d;
+ font-size: 12px;
+}
diff --git a/assets/statistics.js b/assets/statistics.js
index 7dcd638..ebd3557 100644
--- a/assets/statistics.js
+++ b/assets/statistics.js
@@ -314,6 +314,7 @@ $(document).on("rex:ready", function (event, container) {
initPagesDomainFilter();
initLazyBlocks();
initLazyCollapses();
+ initAnalysisCards();
}
function initStatsTabHandling() {
@@ -470,6 +471,35 @@ $(document).on("rex:ready", function (event, container) {
});
}
+ function initAnalysisCards() {
+ var cards = document.querySelectorAll('[data-statistics-focus-lazy]');
+ if (!cards.length) {
+ return;
+ }
+
+ cards.forEach(function (card) {
+ if (card.dataset.statisticsBound === 'true') {
+ return;
+ }
+
+ card.dataset.statisticsBound = 'true';
+ card.addEventListener('click', function () {
+ var targetId = card.getAttribute('data-statistics-focus-lazy');
+ if (!targetId) {
+ return;
+ }
+
+ var target = document.getElementById(targetId);
+ if (!target) {
+ return;
+ }
+
+ loadLazyBlock(target);
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ });
+ }
+
function initLazyBlocks() {
var blocks = document.querySelectorAll('[data-statistics-lazy-block]');
if (!blocks.length) {
diff --git a/boot.php b/boot.php
index 15dcfaa..6440897 100644
--- a/boot.php
+++ b/boot.php
@@ -91,33 +91,16 @@
$domain = 'undefined';
}
- // page url
+ // page url (raw URL including parameters for ignore checks)
$url = $domain . rex::getRequest()->getRequestUri();
// request response code
$response_code = rex_response::getStatus();
-
- if (rex::getRequest()->getRequestUri() != "/favicon.ico") {
-
- if ($response_code == rex_response::HTTP_OK || !$addon->getConfig("statistics_rec_onlyok", false)) {
- // visitduration, number pages visited, last visited page
- $sql = rex_sql::factory();
- $sql->setQuery("INSERT INTO " . rex::getTable('pagestats_sessionstats') . " (token, lastpage, lastvisit, visitduration, pagecount) VALUES (:token, :lastpage, NOW(), 0, 1) ON DUPLICATE KEY UPDATE lastpage = VALUES(lastpage), visitduration = visitduration + (NOW() - lastvisit), lastvisit = NOW(), pagecount = pagecount + 1", [":token" => $token, ":lastpage" => $url]);
- }
- }
-
-
// get ip from visitor, set to 0.0.0.0 when ip can not be determined
$clientAddress = rex::getRequest()->getClientIp();
$clientAddress = $clientAddress ? $clientAddress : '0.0.0.0';
-
- // optionally ignore url parameters
- if ($addon->getConfig('statistics_ignore_url_params')) {
- $url = Visit::removeUrlParameters($url);
- }
-
// user agent
$userAgent = rex_server('HTTP_USER_AGENT', 'string', '');
@@ -151,10 +134,26 @@
if ($visit->shouldSaveVisit() && !$visit->DeviceDetector->isLibrary()) {
- // visits_per_url and pagestats_urlstatus must also be updated if response_code is != 200
- $visit->updateVisitsPerUrl();
+ $recordOnlyOk = (bool) $addon->getConfig('statistics_rec_onlyok', false);
+ $shouldRecordResponse = !$recordOnlyOk || $response_code === rex_response::HTTP_OK;
+
+ if ($shouldRecordResponse) {
+ // optionally ignore url parameters after ignore checks were done on the raw URL
+ if ((bool) $addon->getConfig('statistics_ignore_url_params')) {
+ $visit->setUrl(Visit::removeUrlParameters($visit->getUrl()));
+ }
+
+ // visitduration, number pages visited, last visited page
+ if (rex::getRequest()->getRequestUri() !== '/favicon.ico') {
+ $sql = rex_sql::factory();
+ $sql->setQuery(
+ 'INSERT INTO ' . rex::getTable('pagestats_sessionstats') . ' (token, lastpage, lastvisit, visitduration, pagecount) VALUES (:token, :lastpage, NOW(), 0, 1) ON DUPLICATE KEY UPDATE lastpage = VALUES(lastpage), visitduration = visitduration + (NOW() - lastvisit), lastvisit = NOW(), pagecount = pagecount + 1',
+ [':token' => $token, ':lastpage' => $visit->getUrl()]
+ );
+ }
+
+ $visit->updateVisitsPerUrl();
- if ($response_code == rex_response::HTTP_OK || !$addon->getConfig("statistics_rec_onlyok", false)) {
// visitor is human
// check hash with save_visit, if true then save visit
@@ -175,8 +174,6 @@
// save visitor
$visit->persistVisitor();
}
-
-
$visit->persist();
}
}
diff --git a/composer.json b/composer.json
index 9e76b65..32507c7 100644
--- a/composer.json
+++ b/composer.json
@@ -8,10 +8,13 @@
"matomo/network": "^2.0"
},
"replace": {
+ "composer/ca-bundle": "*",
"psr/log": "*",
"psr/container": "*",
"psr/http-message": "*",
"ankitpokhrel/tus-php": "*",
+ "symfony/deprecation-contracts": "*",
+ "symfony/service-contracts": "*",
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-intl-grapheme": "*",
@@ -23,6 +26,11 @@
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
+ "autoload": {
+ "psr-4": {
+ "AndiLeni\\Statistics\\": "lib/"
+ }
+ },
"config": {
"platform": {
"php": "7.4"
diff --git a/composer.lock b/composer.lock
index d35ee84..87450cd 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,90 +4,14 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "621c6fea5cc8098f46adc636ce82d464",
+ "content-hash": "aa9b3fc6f9a2d32cca0119af477ff995",
"packages": [
- {
- "name": "composer/ca-bundle",
- "version": "1.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/composer/ca-bundle.git",
- "reference": "b66d11b7479109ab547f9405b97205640b17d385"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385",
- "reference": "b66d11b7479109ab547f9405b97205640b17d385",
- "shasum": ""
- },
- "require": {
- "ext-openssl": "*",
- "ext-pcre": "*",
- "php": "^5.3.2 || ^7.0 || ^8.0"
- },
- "require-dev": {
- "phpstan/phpstan": "^0.12.55",
- "psr/log": "^1.0",
- "symfony/phpunit-bridge": "^4.2 || ^5",
- "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Composer\\CaBundle\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jordi Boggiano",
- "email": "j.boggiano@seld.be",
- "homepage": "http://seld.be"
- }
- ],
- "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
- "keywords": [
- "cabundle",
- "cacert",
- "certificate",
- "ssl",
- "tls"
- ],
- "support": {
- "irc": "irc://irc.freenode.org/composer",
- "issues": "https://github.com/composer/ca-bundle/issues",
- "source": "https://github.com/composer/ca-bundle/tree/1.4.0"
- },
- "funding": [
- {
- "url": "https://packagist.com",
- "type": "custom"
- },
- {
- "url": "https://github.com/composer",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
- }
- ],
- "time": "2023-12-18T12:05:55+00:00"
- },
{
"name": "geoip2/geoip2",
"version": "v2.13.0",
"source": {
"type": "git",
- "url": "git@github.com:maxmind/GeoIP2-php.git",
+ "url": "https://github.com/maxmind/GeoIP2-php.git",
"reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23"
},
"dist": {
@@ -134,27 +58,31 @@
"geolocation",
"maxmind"
],
+ "support": {
+ "issues": "https://github.com/maxmind/GeoIP2-php/issues",
+ "source": "https://github.com/maxmind/GeoIP2-php/tree/v2.13.0"
+ },
"time": "2022-08-05T20:32:58+00:00"
},
{
"name": "jaybizzle/crawler-detect",
- "version": "v1.2.116",
+ "version": "v1.3.11",
"source": {
"type": "git",
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
- "reference": "97e9fe30219e60092e107651abb379a38b342921"
+ "reference": "484792759de89fe94ea6a192065ea7cd99f1eaa2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/97e9fe30219e60092e107651abb379a38b342921",
- "reference": "97e9fe30219e60092e107651abb379a38b342921",
+ "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/484792759de89fe94ea6a192065ea7cd99f1eaa2",
+ "reference": "484792759de89fe94ea6a192065ea7cd99f1eaa2",
"shasum": ""
},
"require": {
- "php": ">=5.3.0"
+ "php": ">=7.1.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4"
+ "phpunit/phpunit": "^4.8|^5.5|^6.5|^7.5|^8.5.52|^9.4"
},
"type": "library",
"autoload": {
@@ -184,22 +112,22 @@
],
"support": {
"issues": "https://github.com/JayBizzle/Crawler-Detect/issues",
- "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.116"
+ "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.3.11"
},
- "time": "2023-07-21T15:49:49+00:00"
+ "time": "2026-05-10T14:08:06+00:00"
},
{
"name": "matomo/device-detector",
- "version": "6.2.1",
+ "version": "6.5.1",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
- "reference": "19138b0c4b9ddf4ffd8e423d6af3764b7317cb0b"
+ "reference": "f30457500c6be4c80c8830466f8a746724779fd0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/19138b0c4b9ddf4ffd8e423d6af3764b7317cb0b",
- "reference": "19138b0c4b9ddf4ffd8e423d6af3764b7317cb0b",
+ "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/f30457500c6be4c80c8830466f8a746724779fd0",
+ "reference": "f30457500c6be4c80c8830466f8a746724779fd0",
"shasum": ""
},
"require": {
@@ -213,13 +141,13 @@
"matthiasmullie/scrapbook": "^1.4.7",
"mayflower/mo4-coding-standard": "^v9.0.0",
"phpstan/phpstan": "^1.10.44",
- "phpunit/phpunit": "^8.5.8",
- "psr/cache": "^1.0.1",
- "psr/simple-cache": "^1.0.1",
- "symfony/yaml": "^5.1.7"
+ "phpunit/phpunit": "^8.5.2 || ^9 || ^10 || ^11 || ^12 || ^13",
+ "psr/cache": "^1.0.1 || ^2.0 || ^3.0",
+ "psr/simple-cache": "^1.0.1 || ^2.0 || ^3.0",
+ "slevomat/coding-standard": "<8.16.0",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.4 || ^8.0"
},
"suggest": {
- "doctrine/cache": "Can directly be used for caching purpose",
"ext-yaml": "Necessary for using the Pecl YAML parser"
},
"type": "library",
@@ -253,29 +181,29 @@
"forum": "https://forum.matomo.org/",
"issues": "https://github.com/matomo-org/device-detector/issues",
"source": "https://github.com/matomo-org/matomo",
- "wiki": "https://dev.matomo.org/"
+ "wiki": "https://developer.matomo.org/"
},
- "time": "2024-01-05T09:03:21+00:00"
+ "time": "2026-05-27T09:53:26+00:00"
},
{
"name": "matomo/network",
- "version": "2.0.1",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/component-network.git",
- "reference": "ff654b8fc7778b80279815d06a368f7b41249501"
+ "reference": "49407377fb4755cb1eb987ca827951bed732374e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/matomo-org/component-network/zipball/ff654b8fc7778b80279815d06a368f7b41249501",
- "reference": "ff654b8fc7778b80279815d06a368f7b41249501",
+ "url": "https://api.github.com/repos/matomo-org/component-network/zipball/49407377fb4755cb1eb987ca827951bed732374e",
+ "reference": "49407377fb4755cb1eb987ca827951bed732374e",
"shasum": ""
},
"require": {
- "php": ">=5.4"
+ "php": ">=7.2"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.36"
+ "phpunit/phpunit": "^8 || ^9 || ^10 || ^11 || ^12"
},
"type": "library",
"autoload": {
@@ -289,22 +217,22 @@
],
"support": {
"issues": "https://github.com/matomo-org/component-network/issues",
- "source": "https://github.com/matomo-org/component-network/tree/2.0.1"
+ "source": "https://github.com/matomo-org/component-network/tree/2.0.3"
},
- "time": "2020-10-05T06:09:39+00:00"
+ "time": "2026-04-17T10:12:27+00:00"
},
{
"name": "matomo/referrer-spam-blacklist",
- "version": "4.0.0",
+ "version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/referrer-spam-list.git",
- "reference": "afe4c1ea107ee7a8915a0d5eb0031cf0366608a8"
+ "reference": "e3254bf771f6520aa9701276a74c5a19df535d4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/matomo-org/referrer-spam-list/zipball/afe4c1ea107ee7a8915a0d5eb0031cf0366608a8",
- "reference": "afe4c1ea107ee7a8915a0d5eb0031cf0366608a8",
+ "url": "https://api.github.com/repos/matomo-org/referrer-spam-list/zipball/e3254bf771f6520aa9701276a74c5a19df535d4a",
+ "reference": "e3254bf771f6520aa9701276a74c5a19df535d4a",
"shasum": ""
},
"replace": {
@@ -318,42 +246,41 @@
"description": "Community-contributed list of referrer spammers",
"support": {
"issues": "https://github.com/matomo-org/referrer-spam-list/issues",
- "source": "https://github.com/matomo-org/referrer-spam-list/tree/4.0.0"
+ "source": "https://github.com/matomo-org/referrer-spam-list/tree/4.0.5"
},
- "time": "2020-08-10T19:54:07+00:00"
+ "time": "2026-04-15T22:44:28+00:00"
},
{
"name": "maxmind-db/reader",
- "version": "v1.11.1",
+ "version": "v1.13.1",
"source": {
"type": "git",
- "url": "git@github.com:maxmind/MaxMind-DB-Reader-php.git",
- "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7"
+ "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
+ "reference": "2194f58d0f024ce923e685cdf92af3daf9951908"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/1e66f73ffcf25e17c7a910a1317e9720a95497c7",
- "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7",
+ "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/2194f58d0f024ce923e685cdf92af3daf9951908",
+ "reference": "2194f58d0f024ce923e685cdf92af3daf9951908",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"conflict": {
- "ext-maxminddb": "<1.11.1,>=2.0.0"
+ "ext-maxminddb": "<1.11.1 || >=2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.*",
- "php-coveralls/php-coveralls": "^2.1",
"phpstan/phpstan": "*",
- "phpunit/phpcov": ">=6.0.0",
"phpunit/phpunit": ">=8.0.0,<10.0.0",
- "squizlabs/php_codesniffer": "3.*"
+ "squizlabs/php_codesniffer": "4.*"
},
"suggest": {
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
- "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups"
+ "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups",
+ "maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
},
"type": "library",
"autoload": {
@@ -381,7 +308,11 @@
"geolocation",
"maxmind"
],
- "time": "2023-12-02T00:09:23+00:00"
+ "support": {
+ "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
+ "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.13.1"
+ },
+ "time": "2025-11-21T22:24:26+00:00"
},
{
"name": "maxmind/web-service-common",
@@ -439,7 +370,7 @@
"version": "0.6.3",
"source": {
"type": "git",
- "url": "git@github.com:mustangostang/spyc.git",
+ "url": "https://github.com/mustangostang/spyc.git",
"reference": "4627c838b16550b666d15aeae1e5289dd5b77da0"
},
"dist": {
@@ -482,6 +413,10 @@
"yaml",
"yml"
],
+ "support": {
+ "issues": "https://github.com/mustangostang/spyc/issues",
+ "source": "https://github.com/mustangostang/spyc/tree/0.6.3"
+ },
"time": "2019-09-10T13:16:29+00:00"
},
{
@@ -535,16 +470,16 @@
},
{
"name": "symfony/cache",
- "version": "v5.4.34",
+ "version": "v5.4.53",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
- "reference": "b17f28169f7a2f2c0cddf2b044d729f5b75efe5a"
+ "reference": "bf581474737420d5c932ae80b868e253f465ee5b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache/zipball/b17f28169f7a2f2c0cddf2b044d729f5b75efe5a",
- "reference": "b17f28169f7a2f2c0cddf2b044d729f5b75efe5a",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/bf581474737420d5c932ae80b868e253f465ee5b",
+ "reference": "bf581474737420d5c932ae80b868e253f465ee5b",
"shasum": ""
},
"require": {
@@ -573,7 +508,7 @@
"cache/integration-tests": "dev-master",
"doctrine/cache": "^1.6|^2.0",
"doctrine/dbal": "^2.13.1|^3|^4",
- "predis/predis": "^1.1",
+ "predis/predis": "^1.1|^2.0",
"psr/simple-cache": "^1.0|^2.0",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
@@ -612,7 +547,7 @@
"psr6"
],
"support": {
- "source": "https://github.com/symfony/cache/tree/v5.4.34"
+ "source": "https://github.com/symfony/cache/tree/v5.4.53"
},
"funding": [
{
@@ -623,25 +558,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2023-12-18T14:56:06+00:00"
+ "time": "2026-05-24T08:41:21+00:00"
},
{
"name": "symfony/cache-contracts",
- "version": "v2.5.2",
+ "version": "v2.5.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache-contracts.git",
- "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc"
+ "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/64be4a7acb83b6f2bf6de9a02cee6dad41277ebc",
- "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc",
+ "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/517c3a3619dadfa6952c4651767fcadffb4df65e",
+ "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e",
"shasum": ""
},
"require": {
@@ -653,167 +592,17 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "2.5-dev"
- },
- "thanks": {
- "name": "symfony/contracts",
- "url": "https://github.com/symfony/contracts"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Contracts\\Cache\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Generic abstractions related to caching",
- "homepage": "https://symfony.com",
- "keywords": [
- "abstractions",
- "contracts",
- "decoupling",
- "interfaces",
- "interoperability",
- "standards"
- ],
- "support": {
- "source": "https://github.com/symfony/cache-contracts/tree/v2.5.2"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2022-01-02T09:53:40+00:00"
- },
- {
- "name": "symfony/deprecation-contracts",
- "version": "v2.5.2",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
- "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "2.5-dev"
- },
"thanks": {
- "name": "symfony/contracts",
- "url": "https://github.com/symfony/contracts"
- }
- },
- "autoload": {
- "files": [
- "function.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
},
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "A generic function and convention to trigger deprecation notices",
- "homepage": "https://symfony.com",
- "support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2022-01-02T09:53:40+00:00"
- },
- {
- "name": "symfony/service-contracts",
- "version": "v2.5.2",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/service-contracts.git",
- "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
- "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2.5",
- "psr/container": "^1.1",
- "symfony/deprecation-contracts": "^2.1|^3"
- },
- "conflict": {
- "ext-psr": "<1.1|>=2"
- },
- "suggest": {
- "symfony/service-implementation": ""
- },
- "type": "library",
- "extra": {
"branch-alias": {
"dev-main": "2.5-dev"
- },
- "thanks": {
- "name": "symfony/contracts",
- "url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Contracts\\Service\\": ""
+ "Symfony\\Contracts\\Cache\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -830,7 +619,7 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Generic abstractions related to writing services",
+ "description": "Generic abstractions related to caching",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
@@ -841,7 +630,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v2.5.2"
+ "source": "https://github.com/symfony/cache-contracts/tree/v2.5.4"
},
"funding": [
{
@@ -857,20 +646,20 @@
"type": "tidelift"
}
],
- "time": "2022-05-30T19:17:29+00:00"
+ "time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/var-exporter",
- "version": "v5.4.32",
+ "version": "v5.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
- "reference": "fdb022f0d3d41df240c18e2eb9a117c430f06add"
+ "reference": "862700068db0ddfd8c5b850671e029a90246ec75"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-exporter/zipball/fdb022f0d3d41df240c18e2eb9a117c430f06add",
- "reference": "fdb022f0d3d41df240c18e2eb9a117c430f06add",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/862700068db0ddfd8c5b850671e029a90246ec75",
+ "reference": "862700068db0ddfd8c5b850671e029a90246ec75",
"shasum": ""
},
"require": {
@@ -914,7 +703,7 @@
"serialize"
],
"support": {
- "source": "https://github.com/symfony/var-exporter/tree/v5.4.32"
+ "source": "https://github.com/symfony/var-exporter/tree/v5.4.45"
},
"funding": [
{
@@ -930,19 +719,19 @@
"type": "tidelift"
}
],
- "time": "2023-11-16T19:33:05+00:00"
+ "time": "2024-09-25T14:11:13+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": [],
- "platform-dev": [],
+ "platform": {},
+ "platform-dev": {},
"platform-overrides": {
"php": "7.4"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/fragments/analysis_cards.php b/fragments/analysis_cards.php
new file mode 100644
index 0000000..724a3ea
--- /dev/null
+++ b/fragments/analysis_cards.php
@@ -0,0 +1,47 @@
+ $this->i18n('statistics_analysis_card_device_title'),
+ 'description' => $this->i18n('statistics_analysis_card_device_desc'),
+ 'icon' => 'fa-mobile',
+ 'target' => 'statistics_lazy_device',
+ ],
+ [
+ 'title' => $this->i18n('statistics_analysis_card_extended_title'),
+ 'description' => $this->i18n('statistics_analysis_card_extended_desc'),
+ 'icon' => 'fa-line-chart',
+ 'target' => 'statistics_lazy_extended',
+ ],
+ [
+ 'title' => $this->i18n('statistics_analysis_card_bots_title'),
+ 'description' => $this->i18n('statistics_analysis_card_bots_desc'),
+ 'icon' => 'fa-bug',
+ 'target' => 'statistics_lazy_bots',
+ ],
+];
+
+?>
+
+
+
+
i18n('statistics_analysis_heading'); ?>
+
+
+
+
+
+
+
diff --git a/fragments/overview.php b/fragments/overview.php
index f1bfdd6..b720552 100644
--- a/fragments/overview.php
+++ b/fragments/overview.php
@@ -13,33 +13,77 @@
-
Seitenaufrufe : filtered_visits; ?>
+
i18n('statistics_overview_visits'); ?>: filtered_visits; ?>
-
Besucher : filtered_visitors; ?>
+
i18n('statistics_overview_visitors'); ?>: filtered_visitors; ?>
-
Seitenaufrufe : today_visits; ?>
+
i18n('statistics_overview_visits'); ?>: today_visits; ?>
-
Besucher : today_visitors; ?>
+
i18n('statistics_overview_visitors'); ?>: today_visitors; ?>
-
Seitenaufrufe : total_visits; ?>
+
i18n('statistics_overview_visits'); ?>: total_visits; ?>
-
Besucher : total_visitors; ?>
+
i18n('statistics_overview_visitors'); ?>: total_visitors; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
top_article_path_week, ENT_QUOTES); ?>
+
i18n('statistics_overview_views'); ?>: top_article_count_week; ?>
+
+
+
+
+
+
+
+
pages_per_session_week, 2, ',', '.'); ?>
diff --git a/lang/de_de.lang b/lang/de_de.lang
index b0614b4..61128a9 100644
--- a/lang/de_de.lang
+++ b/lang/de_de.lang
@@ -88,6 +88,14 @@ statistics_scroll_panel = Container
statistics_scroll_none = Keines von beidem
statistics_statistics_ignore_url_params = Ignoriere URL-Parameter
statistics_statistics_ignore_url_params_note = URL-Parameter werden in der Statistik nicht geloggt. Z.b. www.example.com?data=1 wird als www.example.com gespeichert.
+statistics_details_for = Details für:
+statistics_all_domains = Alle Domains
+statistics_filter_all = Alle
+statistics_filter_only_200 = Nur 200er
+statistics_filter_only_not_200 = Nur nicht 200er
+statistics_ignore_success = Es wurden %s Einträge gelöscht.
+statistics_ignore_url_future = Die URL
%s wird zukünftig ignoriert.
+statistics_deleted_campaign_entries = Es wurden %s Einträge der Kampagne
%s gelöscht.
perm_general_statistics[] = Benutzer darf das Addon "Statistiken" aufrufen.
perm_options_statistics[settings] = Benutzer darf Einstellungen des Addons "Statistiken" ändern.
statistics_default_datefilter_range = Standard Datumsbereich
@@ -132,4 +140,20 @@ statistics_api_enable_campaigns_note = Aktiviert das tracken von API Aufrufen
statistics_api_delete_api = Alle API Aufrufe löschen
statistics_api_delete_api_confirm = Möchten Sie wirklich alle API Aufrufe und Kampagnen löschen?
statistics_api_delete = Löschen
-statistics_api_delete_confirm = Wirklich alle Einträge für diese Kampagne löschen?
\ No newline at end of file
+statistics_api_delete_confirm = Wirklich alle Einträge für diese Kampagne löschen?
+statistics_overview_visits = Seitenaufrufe
+statistics_overview_visitors = Besucher
+statistics_overview_today = Heute
+statistics_overview_total = Insgesamt
+statistics_overview_week_visits = Besuche letzte 7 Tage
+statistics_overview_week_visitors = Besucher letzte 7 Tage
+statistics_overview_week_top_article = Top-Artikel letzte 7 Tage
+statistics_overview_week_pages_per_session = Seiten pro Sitzung (7 Tage)
+statistics_overview_views = Aufrufe
+statistics_analysis_heading = Detaillierte Analysen
+statistics_analysis_card_device_title = Geräte & Browser
+statistics_analysis_card_device_desc = Gerätetypen, Browser, OS, Marken und Modelle
+statistics_analysis_card_extended_title = Verhaltensdaten
+statistics_analysis_card_extended_desc = Sitzungsdauer, Seiten pro Sitzung, Ausstiegsseiten, Länder
+statistics_analysis_card_bots_title = Bots
+statistics_analysis_card_bots_desc = Erkannte Crawler, Kategorien und Hersteller
\ No newline at end of file
diff --git a/lang/en_gb.lang b/lang/en_gb.lang
index 8ade558..52afdac 100644
--- a/lang/en_gb.lang
+++ b/lang/en_gb.lang
@@ -90,6 +90,14 @@ statistics_scroll_panel = Container
statistics_scroll_none = None of both
statistics_statistics_ignore_url_params = Ignore URL parameters.
statistics_statistics_ignore_url_params_note = URL parameters are not logged in the statistics. E.g. www.example.com?data=1 is stored as www.example.com.
+statistics_details_for = Details for:
+statistics_all_domains = All domains
+statistics_filter_all = All
+statistics_filter_only_200 = Only 200
+statistics_filter_only_not_200 = Only non-200
+statistics_ignore_success = %s entries were deleted.
+statistics_ignore_url_future = The URL
%s will be ignored in the future.
+statistics_deleted_campaign_entries = %s entries of campaign
%s were deleted.
perm_general_statistics[] = User may open the "Statistics" addon.
perm_options_statistics[settings] = User may change settings of the "Statistics" addon.
statistics_default_datefilter_range = Default date range.
@@ -134,4 +142,20 @@ statistics_api_enable_campaigns_note = Enable tracking of API calls
statistics_api_delete_api = Delete all API calls
statistics_api_delete_api_confirm = Do you really want to delete all API calls and campaigns?
statistics_api_delete = Delete
-statistics_api_delete_confirm = Really delete all entries for this campaign?
\ No newline at end of file
+statistics_api_delete_confirm = Really delete all entries for this campaign?
+statistics_overview_visits = Page views
+statistics_overview_visitors = Visitors
+statistics_overview_today = Today
+statistics_overview_total = Total
+statistics_overview_week_visits = Visits last 7 days
+statistics_overview_week_visitors = Visitors last 7 days
+statistics_overview_week_top_article = Top article last 7 days
+statistics_overview_week_pages_per_session = Pages per session (7 days)
+statistics_overview_views = Views
+statistics_analysis_heading = Detailed analyses
+statistics_analysis_card_device_title = Devices and browsers
+statistics_analysis_card_device_desc = Device types, browsers, operating systems, brands and models
+statistics_analysis_card_extended_title = Behavior metrics
+statistics_analysis_card_extended_desc = Session duration, pages per session, exit pages, countries
+statistics_analysis_card_bots_title = Bots
+statistics_analysis_card_bots_desc = Detected crawlers, categories and vendors
\ No newline at end of file
diff --git a/lib/DateFilter.php b/lib/DateFilter.php
index 8c0b4ec..de5b0ab 100644
--- a/lib/DateFilter.php
+++ b/lib/DateFilter.php
@@ -4,6 +4,8 @@
use rex;
use rex_addon;
+use rex_addon_interface;
+use rex_request;
use rex_sql;
use rex_view;
use InvalidArgumentException;
@@ -22,8 +24,10 @@ class DateFilter
public DateTimeImmutable $whole_time_start;
+ /** @var non-empty-string */
private string $table;
- private rex_addon $addon;
+ private rex_addon_interface $addon;
+ private const SESSION_KEY = 'statistics_datefilter';
/**
@@ -38,37 +42,62 @@ class DateFilter
*/
function __construct(string $date_start, string $date_end, string $table)
{
+ if ('' === $table) {
+ throw new InvalidArgumentException('Table name must not be empty.');
+ }
+
+ /** @var non-empty-string $table */
$this->table = $table;
$this->addon = rex_addon::get('statistics');
- if ($date_start == '') {
-
- // prefered date range
- $date_range = $this->addon->getConfig('statistics_default_datefilter_range');
-
- if ($date_range == 'last7days') {
- $date = new DateTimeImmutable();
- $date = $date->modify("-7 day");
- $this->date_start = $date;
- } elseif ($date_range == 'last30days') {
- $date = new DateTimeImmutable();
- $date = $date->modify("-30 day");
- $this->date_start = $date;
- } elseif ($date_range == 'thisYear') {
- $date = new DateTimeImmutable();
- $date = $date->modify("-365 day");
- $this->date_start = $date;
- } else {
- $this->date_start = $this->getMinDateFromTable();
- }
- // design decision, uncomment this line to default show only timespan where data was collected
- // $this->date_end = $this->getMaxDateFromTable();
+ $hasRequestDateRange = '' !== $date_start && '' !== $date_end;
- $this->date_end = new DateTimeImmutable();
- // $this->date_end->modify('+1 day');
- } else {
+ if ($hasRequestDateRange) {
$this->date_start = new DateTimeImmutable($date_start);
$this->date_end = new DateTimeImmutable($date_end);
+
+ rex_request::setSession(self::SESSION_KEY, [
+ 'date_start' => $this->date_start->format('Y-m-d'),
+ 'date_end' => $this->date_end->format('Y-m-d'),
+ ]);
+ } else {
+ $sessionDateRange = rex_request::session(self::SESSION_KEY);
+ $hasSessionDateRange = is_array($sessionDateRange)
+ && isset($sessionDateRange['date_start'], $sessionDateRange['date_end'])
+ && is_string($sessionDateRange['date_start'])
+ && is_string($sessionDateRange['date_end'])
+ && '' !== $sessionDateRange['date_start']
+ && '' !== $sessionDateRange['date_end'];
+
+ if ($hasSessionDateRange) {
+ $this->date_start = new DateTimeImmutable($sessionDateRange['date_start']);
+ $this->date_end = new DateTimeImmutable($sessionDateRange['date_end']);
+ } else {
+
+ // prefered date range
+ $date_range = $this->addon->getConfig('statistics_default_datefilter_range');
+
+ if ($date_range === 'last7days') {
+ $date = new DateTimeImmutable();
+ $date = $date->modify('-7 day');
+ $this->date_start = $date;
+ } elseif ($date_range === 'last30days') {
+ $date = new DateTimeImmutable();
+ $date = $date->modify('-30 day');
+ $this->date_start = $date;
+ } elseif ($date_range === 'thisYear') {
+ $date = new DateTimeImmutable();
+ $date = $date->modify('-365 day');
+ $this->date_start = $date;
+ } else {
+ $this->date_start = $this->getMinDateFromTable();
+ }
+ // design decision, uncomment this line to default show only timespan where data was collected
+ // $this->date_end = $this->getMaxDateFromTable();
+
+ $this->date_end = new DateTimeImmutable();
+ // $this->date_end->modify('+1 day');
+ }
}
// set total time range to use in datefilter fragment with javascript
@@ -90,16 +119,18 @@ function __construct(string $date_start, string $date_end, string $table)
private function getMinDateFromTable(): DateTimeImmutable
{
$sql = rex_sql::factory();
- $min_date = $sql->setQuery('SELECT MIN(date) AS "date" from ' . rex::getTable($this->table));
- $min_date = $min_date->getValue('date');
+ $result = $sql->setQuery('SELECT MIN(date) AS "date" from ' . rex::getTable($this->table));
+ $minDateRaw = $result->getValue('date');
- if ($min_date === null) {
- $min_date = new DateTimeImmutable("now");
- } else {
- $min_date = DateTimeImmutable::createFromFormat('Y-m-d', $min_date);
+ if (!is_string($minDateRaw) || '' === $minDateRaw) {
+ return new DateTimeImmutable('now');
}
+ $minDate = DateTimeImmutable::createFromFormat('Y-m-d', $minDateRaw);
+ if (false === $minDate) {
+ return new DateTimeImmutable('now');
+ }
- return $min_date;
+ return $minDate;
}
}
diff --git a/lib/StatsDashboard.php b/lib/StatsDashboard.php
index 2981d2c..8c47945 100644
--- a/lib/StatsDashboard.php
+++ b/lib/StatsDashboard.php
@@ -34,10 +34,24 @@ public static function renderOverview(DateFilter $filterDateHelper, array $overv
$fragmentOverview->setVar('today_visitors', $overviewData['visitors_today']);
$fragmentOverview->setVar('total_visits', $overviewData['visits_total']);
$fragmentOverview->setVar('total_visitors', $overviewData['visitors_total']);
+ $fragmentOverview->setVar('visits_week', $overviewData['visits_week']);
+ $fragmentOverview->setVar('visitors_week', $overviewData['visitors_week']);
+ $fragmentOverview->setVar('top_article_path_week', $overviewData['top_article_path_week']);
+ $fragmentOverview->setVar('top_article_count_week', $overviewData['top_article_count_week']);
+ $fragmentOverview->setVar('pages_per_session_week', $overviewData['pages_per_session_week']);
return $fragmentOverview->parse('overview.php');
}
+ public static function renderAnalysisCards(DateFilter $filterDateHelper): string
+ {
+ $fragment = new rex_fragment();
+ $fragment->setVar('date_start', $filterDateHelper->date_start->format('Y-m-d'));
+ $fragment->setVar('date_end', $filterDateHelper->date_end->format('Y-m-d'));
+
+ return $fragment->parse('analysis_cards.php');
+ }
+
public static function renderMainChartSection(DateFilter $filterDateHelper): string
{
return StatsMainChartSection::render($filterDateHelper);
@@ -83,9 +97,9 @@ public static function renderTableLanguageConfigScript(): string
}
/**
- * @return array
|null
+ * @return array
*/
- private static function getTableLanguage(): ?array
+ private static function getTableLanguage(): array
{
$language = (string) rex::getProperty('lang');
$isGerman = 'de_de' === $language;
diff --git a/lib/Summary.php b/lib/Summary.php
index accae7b..46465cf 100644
--- a/lib/Summary.php
+++ b/lib/Summary.php
@@ -29,7 +29,7 @@ public function __construct(DateFilter $filter_date_helper)
/**
*
*
- * @return array
+ * @return array
* @throws InvalidArgumentException
* @throws rex_sql_exception
*/
@@ -37,6 +37,8 @@ public function getSummaryData(): array
{
$sql = rex_sql::factory();
$today = date('Y-m-d');
+ $weekStart = date('Y-m-d', strtotime('-6 day'));
+ $weekEnd = $today;
$visits = $sql->getArray(
'SELECT '
@@ -67,13 +69,76 @@ public function getSummaryData(): array
$visitsRow = $visits[0] ?? ['total' => 0, 'today' => 0, 'filtered' => 0];
$visitorsRow = $visitors[0] ?? ['total' => 0, 'today' => 0, 'filtered' => 0];
+ $visitsWeek = $sql->getArray(
+ 'SELECT IFNULL(SUM(count), 0) AS total '
+ . 'FROM ' . rex::getTable('pagestats_visits_per_day')
+ . ' WHERE date BETWEEN :start AND :end',
+ [
+ 'start' => $weekStart,
+ 'end' => $weekEnd,
+ ]
+ );
+
+ $visitorsWeek = $sql->getArray(
+ 'SELECT IFNULL(SUM(count), 0) AS total '
+ . 'FROM ' . rex::getTable('pagestats_visitors_per_day')
+ . ' WHERE date BETWEEN :start AND :end',
+ [
+ 'start' => $weekStart,
+ 'end' => $weekEnd,
+ ]
+ );
+
+ $topArticle = $sql->getArray(
+ 'SELECT url, SUM(count) AS total '
+ . 'FROM ' . rex::getTable('pagestats_visits_per_url')
+ . ' WHERE date BETWEEN :start AND :end '
+ . 'GROUP BY url ORDER BY total DESC LIMIT 1',
+ [
+ 'start' => $weekStart,
+ 'end' => $weekEnd,
+ ]
+ );
+
+ $weekVisits = (int) ($visitsWeek[0]['total'] ?? 0);
+ $weekVisitors = (int) ($visitorsWeek[0]['total'] ?? 0);
+ $weekPagesPerSession = $weekVisitors > 0 ? round($weekVisits / $weekVisitors, 2) : 0.0;
+
+ $topArticleUrl = (string) ($topArticle[0]['url'] ?? '');
+ $topArticleCount = (int) ($topArticle[0]['total'] ?? 0);
+ $topArticlePath = $this->extractPathFromTrackedUrl($topArticleUrl);
+
return [
- 'visits_datefilter' => $visitsRow['filtered'],
- 'visitors_datefilter' => $visitorsRow['filtered'],
- 'visits_today' => $visitsRow['today'],
- 'visitors_today' => $visitorsRow['today'],
- 'visits_total' => $visitsRow['total'],
- 'visitors_total' => $visitorsRow['total'],
+ 'visits_datefilter' => (int) $visitsRow['filtered'],
+ 'visitors_datefilter' => (int) $visitorsRow['filtered'],
+ 'visits_today' => (int) $visitsRow['today'],
+ 'visitors_today' => (int) $visitorsRow['today'],
+ 'visits_total' => (int) $visitsRow['total'],
+ 'visitors_total' => (int) $visitorsRow['total'],
+ 'visits_week' => $weekVisits,
+ 'visitors_week' => $weekVisitors,
+ 'top_article_path_week' => $topArticlePath,
+ 'top_article_count_week' => $topArticleCount,
+ 'pages_per_session_week' => $weekPagesPerSession,
];
}
+
+ private function extractPathFromTrackedUrl(string $url): string
+ {
+ if ('' === $url) {
+ return '/';
+ }
+
+ $firstSlash = strpos($url, '/');
+ if (false === $firstSlash) {
+ return '/';
+ }
+
+ $path = substr($url, $firstSlash);
+ if ('' === $path) {
+ return '/';
+ }
+
+ return $path;
+ }
}
diff --git a/lib/Visit.php b/lib/Visit.php
index e0f12ea..22688fd 100644
--- a/lib/Visit.php
+++ b/lib/Visit.php
@@ -526,6 +526,16 @@ public function isBot(): bool
return $this->DeviceDetector->isBot();
}
+ public function setUrl(string $url): void
+ {
+ $this->url = $url;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
/**
*
diff --git a/package.yml b/package.yml
index 93547c4..3cba338 100644
--- a/package.yml
+++ b/package.yml
@@ -1,5 +1,5 @@
package: statistics
-version: 3.2.0
+version: 3.2.3
author: Andreas Lenhardt
supportpage: https://github.com/AndiLeni/statistics
diff --git a/pages/events.php b/pages/events.php
index 7fc2b37..ffa65d6 100644
--- a/pages/events.php
+++ b/pages/events.php
@@ -19,14 +19,14 @@
echo StatsSubpageRenderer::renderFilter($current_backend_page, $filter_date_helper);
-if ($request_name != '' && $delete_entry === true) {
+if ($request_name !== '' && $delete_entry === true) {
$sql = rex_sql::factory();
$sql->setQuery('delete from ' . rex::getTable('pagestats_api') . ' where name = :name', ['name' => $request_name]);
- echo rex_view::success('Es wurden ' . $sql->getRows() . ' Einträge der Kampagne ' . $request_name . ' gelöscht.');
+ echo rex_view::success(sprintf($addon->i18n('statistics_deleted_campaign_entries'), (string) $sql->getRows(), htmlspecialchars($request_name, ENT_QUOTES)));
}
// details section
-if ($request_name != '' && !$delete_entry) {
+if ($request_name !== '' && !$delete_entry) {
// details section for single campaign
$pagedetails = new EventDetails($request_name, $filter_date_helper);
@@ -36,7 +36,7 @@
$content = '';
echo StatsSubpageRenderer::renderInfoSection(
- 'Details für:',
+ $addon->i18n('statistics_details_for'),
$request_name,
$content . StatsChartConfig::renderScript('chart_details', StatsChartConfig::buildTimelineOption($sum_data['labels'], $sum_data['values']))
);
diff --git a/pages/media.php b/pages/media.php
index 680957b..0950f2b 100644
--- a/pages/media.php
+++ b/pages/media.php
@@ -18,14 +18,14 @@
$filter_date_helper = new DateFilter($request_date_start, $request_date_end, 'pagestats_media');
echo StatsSubpageRenderer::renderFilter($current_backend_page, $filter_date_helper);
-if ($request_url != '' && $delete_entry === true) {
+if ($request_url !== '' && $delete_entry === true) {
$sql = rex_sql::factory();
$sql->setQuery('delete from ' . rex::getTable('pagestats_media') . ' where url = :url', ['url' => $request_url]);
- echo rex_view::success('Es wurden ' . $sql->getRows() . ' Einträge der Kampagne ' . $request_url . ' gelöscht.');
+ echo rex_view::success(sprintf($addon->i18n('statistics_deleted_campaign_entries'), (string) $sql->getRows(), htmlspecialchars($request_url, ENT_QUOTES)));
}
// details section
-if ($request_url != '' && !$delete_entry) {
+if ($request_url !== '' && !$delete_entry) {
// details section for single campaign
$pagedetails = new MediaDetails($request_url, $filter_date_helper);
@@ -34,7 +34,7 @@
$content = '';
echo StatsSubpageRenderer::renderInfoSection(
- 'Details für:',
+ $addon->i18n('statistics_details_for'),
$request_url,
$content . StatsChartConfig::renderScript('chart_details', StatsChartConfig::buildTimelineOption($sum_data['labels'], $sum_data['values']))
);
diff --git a/pages/pages.php b/pages/pages.php
index f72ef2d..e8c6c6c 100644
--- a/pages/pages.php
+++ b/pages/pages.php
@@ -29,14 +29,18 @@
// check if request is for ignoring a url
// if yes, add url to addon settings and delete all database entries of this url
-if ($request_url != '' && $ignore_page === true) {
+if ($request_url !== '' && $ignore_page === true) {
$rows = $pages_helper->ignorePage($request_url);
- echo rex_view::success('Es wurden ' . $rows . ' Einträge gelöscht. Die Url ' . $request_url . ' wird zukünftig ignoriert.');
+ echo rex_view::success(
+ sprintf($addon->i18n('statistics_ignore_success'), (string) $rows)
+ . ' '
+ . sprintf($addon->i18n('statistics_ignore_url_future'), htmlspecialchars($request_url, ENT_QUOTES))
+ );
}
// details for one url requested
-if ($request_url != '' && !$ignore_page) {
+if ($request_url !== '' && !$ignore_page) {
// details section for single page
$pagedetails = new PageDetails($request_url, $filter_date_helper);
@@ -47,7 +51,7 @@
$content .= StatsChartConfig::renderScript('chart_details', StatsChartConfig::buildTimelineOption($sum_data['labels'], $sum_data['values']));
$content .= $pagedetails->getList();
- echo StatsSubpageRenderer::renderInfoSection('Details für:', $request_url, $content);
+ echo StatsSubpageRenderer::renderInfoSection($addon->i18n('statistics_details_for'), $request_url, $content);
}
@@ -56,7 +60,7 @@
$domains = $sql->getArray('SELECT distinct domain FROM ' . rex::getTable('pagestats_visits_per_day'));
$domain_select = '