Skip to content
Merged

3.6.0 #154

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- Unbenutzte Abhängigkeiten und Frontend-Assets bereinigt: `exceljs.min.js` und `jspdf.umd.min.js` entfernt sowie die nicht mehr benötigten Composer-Abhängigkeiten `phpoffice/phpspreadsheet` und `matomo/referrer-spam-blacklist` aus dem Addon entfernt
- Release-Paketierung ergänzt: Entwicklungsordner `.tools` und `.github` werden über `installer_ignore` sowie per `.gitattributes` (`export-ignore`) aus Release-Archiven ausgeschlossen
- Frontend-Vendor-Assets aktualisiert (DataTables auf 1.13.11, ECharts auf 5.6.1) und automatischen GitHub-Workflow für regelmäßige Asset-Update-PRs ergänzt (`.github/workflows/update-frontend-assets.yml`)
- Tracking-SQL gehärtet und vereinheitlicht: bisherige String-SQL mit `addslashes` in `Visit` auf parametrisierte Upsert-Queries umgestellt, wiederholte Counter-Upserts in `Visit`/`EventRequest` über interne Helper konsolidiert und den `pagestats_data`-Write-Path per Bulk-Upsert reduziert
- Data-Aggregationen für Browser/Brand/Browsertype/OS/Model/Country/Hour/Weekday zusammengeführt: statt mehrerer Einzelabfragen je Klasse werden die Typen zentral über eine gemeinsame Query geladen und intern wiederverwendet
- REDAXO-Härtung ergänzt: direkte `$_SERVER`-Zugriffe in `Visit`/`EventRequest` durch REDAXO-Serverzugriff ersetzt (u. a. Client-Hints und `HTTP_VIA`)
- Wartungs- und Analyseballast reduziert: veraltete `psalm.xml` entfernt und README um eine Maintainer-Sektion für `.tools` erweitert

## [3.5.2] - 04.08.2026

Expand Down
60 changes: 60 additions & 0 deletions lib/DataTypeAggregationRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace AndiLeni\Statistics;

use rex;
use rex_sql;

final class DataTypeAggregationRepository
{
/**
* @var null|array<string, array<int, array{name: string, count: int}>>
*/
private static ?array $cache = null;

/**
* @param string $type
* @return array<int, array{name: string, count: int}>
*/
public static function getRowsByType(string $type): array
{
self::ensureLoaded();

return self::$cache[$type] ?? [];
}

private static function ensureLoaded(): void
{
if (null !== self::$cache) {
return;
}

self::$cache = [];
$types = ['browser', 'brand', 'browsertype', 'os', 'model', 'country', 'hour', 'weekday'];
$quotedTypes = array_map(static fn(string $type): string => '"' . $type . '"', $types);

$sql = rex_sql::factory();
$rows = $sql->getArray(
'SELECT type, name, count'
. ' FROM ' . rex::getTable('pagestats_data')
. ' WHERE type IN (' . implode(', ', $quotedTypes) . ')'
. ' ORDER BY type ASC, count DESC'
);

foreach ($types as $type) {
self::$cache[$type] = [];
}

foreach ($rows as $row) {
$type = (string) ($row['type'] ?? '');
if (!isset(self::$cache[$type])) {
continue;
}

self::$cache[$type][] = [
'name' => (string) ($row['name'] ?? ''),
'count' => (int) ($row['count'] ?? 0),
];
}
}
}
186 changes: 132 additions & 54 deletions lib/Visit.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,10 @@ private function normalizePathFromUrl(string $rawUrl): string
public function isChromeDataSaverUsed(IP $ip): bool
{
// see https://github.com/piwik/piwik/issues/7733
return !empty($_SERVER['HTTP_VIA'])
&& false !== strpos(strtolower($_SERVER['HTTP_VIA']), 'chrome-compression-proxy')
$httpVia = rex_server('HTTP_VIA', 'string', '');

return '' !== $httpVia
&& false !== strpos(strtolower($httpVia), 'chrome-compression-proxy')
&& $ip->isInRanges(self::BOTIPS);
}

Expand Down Expand Up @@ -475,28 +477,26 @@ public function persist(): void
$this->brand = trim($brandInfo) != '' ? ucfirst($brandInfo) : 'Undefiniert';
$this->model = trim($modelInfo) != '' ? ucfirst($modelInfo) : 'Undefiniert';


$sql = rex_sql::factory();

$sql_insert = 'INSERT INTO ' . rex::getTable('pagestats_data') . ' (type,name,count) VALUES
("browser","' . addslashes($this->browser) . '",1),
("os","' . addslashes($this->os) . ' ' . addslashes($this->osVer) . '",1),
("browsertype","' . addslashes($this->device_type) . '",1),
("brand","' . addslashes($this->brand) . '",1),
("model","' . addslashes($this->brand) . ' - ' . addslashes($this->model) . '",1),
("hour","' . $this->datetime_now->format('H') . '",1),
("weekday","' . $this->datetime_now->format('N') . '",1),
("country","' . $this->country . '",1)
ON DUPLICATE KEY UPDATE count = count + 1;';

$sql->setQuery($sql_insert);


$sql_insert = 'INSERT INTO ' . rex::getTable('pagestats_visits_per_day') . ' (date,domain,count) VALUES
("' . $this->datetime_now->format('Y-m-d') . '","' . addslashes($this->domain) . '",1)
ON DUPLICATE KEY UPDATE count = count + 1;';

$sql->setQuery($sql_insert);
$counterRows = [
['type' => 'browser', 'name' => $this->browser],
['type' => 'os', 'name' => $this->os . ' ' . $this->osVer],
['type' => 'browsertype', 'name' => $this->device_type],
['type' => 'brand', 'name' => $this->brand],
['type' => 'model', 'name' => $this->brand . ' - ' . $this->model],
['type' => 'hour', 'name' => $this->datetime_now->format('H')],
['type' => 'weekday', 'name' => $this->datetime_now->format('N')],
['type' => 'country', 'name' => $this->country],
];

$this->incrementCounterRows(rex::getTable('pagestats_data'), $counterRows);

$this->incrementCounterRow(
rex::getTable('pagestats_visits_per_day'),
[
'date' => $this->datetime_now->format('Y-m-d'),
'domain' => $this->domain,
]
);
}


Expand All @@ -509,11 +509,14 @@ public function persist(): void
*/
public function updateVisitsPerUrl(): void
{
$sql_insert = 'INSERT INTO ' . rex::getTable('pagestats_visits_per_url') . ' (hash,date,url,count) VALUES
("' . md5($this->datetime_now->format('Y-m-d') . $this->url) . '","' . $this->datetime_now->format('Y-m-d') . '","' . addslashes($this->url) . '",1)
ON DUPLICATE KEY UPDATE count = count + 1;';

$this->executeWriteWithRetry($sql_insert);
$this->incrementCounterRow(
rex::getTable('pagestats_visits_per_url'),
[
'hash' => md5($this->datetime_now->format('Y-m-d') . $this->url),
'date' => $this->datetime_now->format('Y-m-d'),
'url' => $this->url,
]
);


// save url http status
Expand Down Expand Up @@ -590,13 +593,13 @@ public function getCountry(): void
*/
public function persistVisitor(): void
{
$sql = rex_sql::factory();

$sql_insert = 'INSERT INTO ' . rex::getTable('pagestats_visitors_per_day') . ' (date,domain,count) VALUES
("' . $this->datetime_now->format('Y-m-d') . '","' . addslashes($this->domain) . '",1)
ON DUPLICATE KEY UPDATE count = count + 1;';

$sql->setQuery($sql_insert);
$this->incrementCounterRow(
rex::getTable('pagestats_visitors_per_day'),
[
'date' => $this->datetime_now->format('Y-m-d'),
'domain' => $this->domain,
]
);
}

/**
Expand Down Expand Up @@ -725,14 +728,44 @@ public function shouldSaveVisitor(): bool
public function parseUA(): void
{
$cache = new StaticCache();
$clientHints = ClientHints::factory($_SERVER);
$clientHints = ClientHints::factory(self::buildClientHintsServerBag());
$this->DeviceDetector = new DeviceDetector($this->userAgent, $clientHints);
// $this->DeviceDetector = new DeviceDetector($this->userAgent);
$this->DeviceDetector->setYamlParser(new DeviceDetectorSymfonyYamlParser());
$this->DeviceDetector->setCache($cache);
$this->DeviceDetector->parse();
}

/**
* @return array<string, string>
*/
private static function buildClientHintsServerBag(): array
{
$keys = [
'HTTP_USER_AGENT',
'HTTP_SEC_CH_UA',
'HTTP_SEC_CH_UA_MOBILE',
'HTTP_SEC_CH_UA_PLATFORM',
'HTTP_SEC_CH_UA_PLATFORM_VERSION',
'HTTP_SEC_CH_UA_MODEL',
'HTTP_SEC_CH_UA_FULL_VERSION',
'HTTP_SEC_CH_UA_FULL_VERSION_LIST',
'HTTP_SEC_CH_UA_ARCH',
'HTTP_SEC_CH_UA_BITNESS',
];

$server = [];

foreach ($keys as $key) {
$value = rex_server($key, 'string', '');
if ('' !== $value) {
$server[$key] = $value;
}
}

return $server;
}


/**
*
Expand All @@ -749,12 +782,14 @@ public function saveBot(): void
$botcategory = $botInfo['category'] ?? '-';
$botproducer = $botInfo['producer']['name'] ?? '-';

$sql = rex_sql::factory();

$sql->setQuery('
INSERT INTO ' . rex::getTable('pagestats_bot') . ' (name,category,producer,count) VALUES
(:botname,:botcategory,:botproducer,1)
ON DUPLICATE KEY UPDATE count = count + 1;', ['botname' => $botname, 'botcategory' => $botcategory, 'botproducer' => $botproducer]);
$this->incrementCounterRow(
rex::getTable('pagestats_bot'),
[
'name' => $botname,
'category' => $botcategory,
'producer' => $botproducer,
]
);
}


Expand All @@ -767,12 +802,14 @@ public function saveBot(): void
*/
public function saveCrawlerDetect(string $name): void
{
$sql = rex_sql::factory();

$sql->setQuery('
INSERT INTO ' . rex::getTable('pagestats_bot') . ' (name,category,producer,count) VALUES
(:botname,:botcategory,:botproducer,1)
ON DUPLICATE KEY UPDATE count = count + 1;', ['botname' => $name, 'botcategory' => "Crawler", 'botproducer' => "-"]);
$this->incrementCounterRow(
rex::getTable('pagestats_bot'),
[
'name' => $name,
'category' => 'Crawler',
'producer' => '-',
]
);
}


Expand All @@ -786,12 +823,53 @@ public function saveCrawlerDetect(string $name): void
*/
public function saveReferer(string $referer): void
{
$sql = rex_sql::factory();
$this->incrementCounterRow(
rex::getTable('pagestats_referer'),
[
'hash' => md5($this->datetime_now->format('Y-m-d') . $referer),
'referer' => $referer,
'date' => $this->datetime_now->format('Y-m-d'),
]
);
}

/**
* @param array<string, scalar|null> $keyValues
*/
private function incrementCounterRow(string $table, array $keyValues): void
{
$this->incrementCounterRows($table, [$keyValues]);
}

/**
* @param list<array<string, scalar|null>> $rows
*/
private function incrementCounterRows(string $table, array $rows): void
{
if ([] === $rows) {
return;
}

$columns = array_keys($rows[0]);
$params = [];
$valueGroups = [];

foreach ($rows as $index => $row) {
$rowPlaceholders = [];
foreach ($columns as $column) {
$placeholder = ':' . $column . '_' . $index;
$rowPlaceholders[] = $placeholder;
$params[$placeholder] = $row[$column] ?? null;
}
$valueGroups[] = '(' . implode(',', $rowPlaceholders) . ',1)';
}

$query = 'INSERT INTO ' . $table
. ' (' . implode(',', $columns) . ',count) VALUES '
. implode(',', $valueGroups)
. ' ON DUPLICATE KEY UPDATE count = count + 1;';

$sql->setQuery('
INSERT INTO ' . rex::getTable('pagestats_referer') . ' (hash,referer,date,count) VALUES
(:hash,:referer,:date,1)
ON DUPLICATE KEY UPDATE count = count + 1;', ['hash' => md5($this->datetime_now->format('Y-m-d') . $referer), 'referer' => $referer, 'date' => $this->datetime_now->format('Y-m-d')]);
$this->executeWriteWithRetry($query, $params);
}


Expand Down
Loading