PriviMetrics
Version 1.0.9 • 43 files • 278.98 KB
Files
.last_check
admin.php
assets/.htaccess
assets/dashboard-chart.php
assets/dashboard-logic.php
assets/dashboard-modals.php
assets/dashboard-tables.php
assets/dashboard-template.php
assets/trends-template.php
chosen-limits.php
dashboard.php
data/.htaccess
extensions-load.php
extensions.php
extensions.xml
extensions/.htaccess
extensions/extensions_off.txt
functions.php
getCountryFrom/db-ip.php
getCountryFrom/geo-lite.php
getCountryFrom/ip-api-com.php
getCountryFrom/ip-info.php
getCountryFrom/ip-stack.php
getCountryFrom/ip2location-io.php
getCountryFrom/privacy-friendly.php
index.html
install.php
limits-options.php
new_version.php
privimetrics-div.js
privimetrics.php
public.php
scripts.js
settings-config.php
settings.php
signup.php
storage.php
styles-mobile.css
styles.css
trends.css
trends.php
updater/index.php
version.txt
public.php
<?php
// ===============================================================================
// PriviMetrics - Public Statistics Page (Read-Only)
// ===============================================================================
if (!file_exists('config.php')) {
die('Configuration file not found.');
}
require_once 'config.php';
if (!file_exists('functions.php')) {
die('Functions file not found.');
}
require_once 'functions.php';
if (!file_exists('storage.php')) {
die('Storage file not found.');
}
require_once 'storage.php';
// Security: Rate limiting to prevent abuse
$clientIP = getClientIP();
if (!checkRateLimit('public_stats_' . $clientIP, 30, 60)) {
http_response_code(429);
die('Too many requests. Please try again later.');
}
// Get tracking code from URL parameter
$trackingCode = sanitize($_GET['site'] ?? '');
if (empty($trackingCode)) {
http_response_code(400);
die('Invalid request. Site parameter is required.');
}
// Configurable days - can be overridden per site or globally
$publicDays = (int)($config['public_stats_days'] ?? 30);
$publicDays = max(1, min(90, $publicDays)); // Limit between 1-90 days
// Load sites and find the matching site by tracking code
$sites = loadXMLFile($config['sites_file'], 'sites');
$currentSite = null;
foreach ($sites->site as $site) {
if ((string)$site->tracking_code === $trackingCode && (string)$site->active === 'true') {
$currentSite = $site;
break;
}
}
if (!$currentSite) {
http_response_code(404);
die('Site not found or statistics not available.');
}
// Check if public stats are enabled for this site (optional feature)
$publicStatsEnabled = !isset($currentSite->public_stats) || (string)$currentSite->public_stats !== 'false';
if (!$publicStatsEnabled) {
http_response_code(403);
die('Public statistics are not enabled for this site.');
}
// Get date range (last X days)
$endTime = time();
$startTime = strtotime("-" . ($publicDays - 1) . " days 00:00:00");
$dateRange = ['start' => $startTime, 'end' => $endTime];
// Load analytics data securely through storage manager
$storageType = (string)($currentSite->storage ?? 'xml');
$storageManager = new StorageManager($config);
$siteData = [
'id' => (string)$currentSite->id,
'name' => (string)$currentSite->name
];
$analyticsData = $storageManager->loadAnalytics($storageType, $siteData, $dateRange);
// Process analytics data - NO SENSITIVE DATA
$stats = [
'total_visits' => count($analyticsData),
'unique_pages' => 0,
'top_pages' => [],
'top_countries' => [],
'top_referrers' => [],
];
$pages = [];
$countries = [];
$referrers = [];
$visitsByDate = [];
foreach ($analyticsData as $visit) {
// Pages
$url = (string)$visit['page_url'];
if (!isset($pages[$url])) {
$pages[$url] = ['url' => $url, 'title' => (string)$visit['page_title'], 'count' => 0];
}
$pages[$url]['count']++;
// Countries
$country = (string)$visit['country'];
$countryCode = (string)$visit['country_code'];
if (!empty($country) && $country !== 'Unknown') {
$countryKey = $country . '|' . $countryCode;
if (!isset($countries[$countryKey])) {
$countries[$countryKey] = ['name' => $country, 'code' => $countryCode, 'count' => 0];
}
$countries[$countryKey]['count']++;
}
// Referrers (external only)
$ref = (string)$visit['referrer'];
if (!empty($ref) && $ref !== 'direct') {
if (!isset($referrers[$ref])) {
$referrers[$ref] = 0;
}
$referrers[$ref]++;
}
// Visits by date
$date = (string)$visit['date'];
if (!isset($visitsByDate[$date])) {
$visitsByDate[$date] = 0;
}
$visitsByDate[$date]++;
}
// Sort and prepare data
$pagesList = array_values($pages);
usort($pagesList, function($a, $b) {
return $b['count'] - $a['count'];
});
$countriesList = array_values($countries);
usort($countriesList, function($a, $b) {
return $b['count'] - $a['count'];
});
arsort($referrers);
$stats['unique_pages'] = count($pagesList);
$stats['top_pages'] = array_slice($pagesList, 0, 10);
$stats['top_countries'] = array_slice($countriesList, 0, 10);
$stats['top_referrers'] = array_slice($referrers, 0, 10, true);
// Fill in missing dates
$allDates = [];
$current = $startTime;
while ($current <= $endTime) {
$dateKey = gmdate('Y-m-d', $current);
$allDates[$dateKey] = $visitsByDate[$dateKey] ?? 0;
$current = strtotime('+1 day', $current);
}
ksort($allDates);
$theme = sanitize($_GET['theme'] ?? 'dark');
$theme = in_array($theme, ['dark', 'light']) ? $theme : 'dark';
$siteName = sanitize((string)$currentSite->name);
$siteLogo = (string)($config['site_logo'] ?? 'PM');
?>
<!DOCTYPE html>
<html lang="en" data-theme="<?= $theme ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Public Statistics - <?= $siteName ?></title>
<meta name="robots" content="index,follow">
<style>
:root[data-theme="dark"] {
--bg-primary: #0a0a0a;
--bg-secondary: #151515;
--bg-tertiary: #1a1a1a;
--border-color: #252525;
--text-primary: #e5e5e5;
--text-secondary: #a0a0a0;
--text-tertiary: #707070;
--accent: #f1484e;
--accent-hover: #d53b40;
}
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-tertiary: #f3f4f6;
--border-color: #e5e7eb;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--accent: #f1484e;
--accent-hover: #d53b40;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 20px 0;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-section {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 40px;
height: 40px;
border-radius: 8px;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
}
.logo-text {
font-size: 18px;
font-weight: 600;
}
.badge {
background: var(--bg-tertiary);
color: var(--text-secondary);
padding: 4px 12px;
border-radius: 6px;
font-size: 13px;
border: 1px solid var(--border-color);
}
.theme-toggle {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
color: var(--text-primary);
text-decoration: none;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.theme-toggle:hover {
background: var(--bg-secondary);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 32px 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
}
.stat-label {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 8px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: var(--accent);
}
.chart-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.chart-header {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: var(--text-primary);
}
.chart {
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 200px;
gap: 4px;
padding: 10px 0;
}
.bar {
flex: 1;
background: var(--accent);
border-radius: 4px 4px 0 0;
min-height: 2px;
transition: opacity 0.2s;
cursor: pointer;
}
.bar:hover {
opacity: 0.8;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
border-bottom: 2px solid var(--border-color);
}
th {
text-align: left;
padding: 12px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 16px 12px;
border-bottom: 1px solid var(--border-color);
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: var(--bg-tertiary);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
}
.footer {
text-align: center;
padding: 32px 24px;
color: var(--text-tertiary);
font-size: 13px;
border-top: 1px solid var(--border-color);
margin-top: 48px;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: 1fr;
}
.chart {
height: 150px;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo-section">
<div class="logo-icon">
<?php
if (str_starts_with($siteLogo, 'img:')) {
$logoFile = substr($siteLogo, 4);
echo '<img src="' . htmlspecialchars($logoFile) . '" alt="' . $siteName . '" style="max-width:100%; max-height:100%;">';
} else {
echo htmlspecialchars($siteLogo);
}
?>
</div>
<div>
<div class="logo-text">
<?= $siteName ?>
</div>
<div class="badge">Public Statistics</div>
</div>
</div>
<a href="?site=<?= htmlspecialchars($trackingCode) ?>&theme=<?= $theme === 'dark' ? 'light' : 'dark' ?>"
class="btn btn-secondary" style="width:40px; height:40px; display:flex; align-items:center; justify-content:center; border-radius:8px;">
<?= $theme === 'dark' ? '<svg class="icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="5" /> <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" /></svg>' : '<svg class="icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" /></svg>' ?>
</a>
</div>
</header>
<main class="container">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Total Visits (Last
<?= $publicDays ?> Days)
</div>
<div class="stat-value">
<?= number_format($stats['total_visits']) ?>
</div>
</div>
</div>
<?php if ($stats['total_visits'] > 0): ?>
<div class="chart-card">
<div class="chart-header">Visits Over Time (Last
<?= $publicDays ?> Days)
</div>
<div class="chart">
<?php
$maxVisits = max(array_values($allDates));
if ($maxVisits === 0) $maxVisits = 1;
foreach ($allDates as $date => $count):
$heightPercent = ($count / $maxVisits) * 100;
if ($heightPercent < 2 && $count > 0) $heightPercent = 2;
?>
<div class="bar" title="<?= htmlspecialchars($date) ?>: <?= $count ?> visits"
style="height: <?= $heightPercent ?>%;"></div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="chart-card">
<div class="empty-state">
<div class="empty-state-icon">📊</div>
<h3>No Data Available</h3>
<p>No statistics have been recorded for the last
<?= $publicDays ?> days.
</p>
</div>
</div>
<?php endif; ?>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<div class="footer">
Powered by PriviMetrics
<br>
Data shown: Last
<?= $publicDays ?> days
</div>
</main>
</body>
</html>