v1.9.1: fix world map rendering and resolve eight small TODO items
World map: - Fix zero-padded ISO numeric ids leaving 31 countries unrendered (Brazil, Australia, Belgium, Austria, Algeria and more) - Fix Russia and Fiji smearing across the full map width at the antimeridian by unwrapping ring longitudes and drawing them at both edges - Crop to 84N-60S, add evenodd fill rule, 174 countries rendered Improvements: - Logger::tail() reads backwards in chunks instead of loading the whole file - External links get rel=noopener noreferrer in footer and Markdown content - formatDisplayName() cleaned up and guarded against empty input - Export statistics as CSV (Excel BOM) or JSON - GeoIP database auto-updates when older than 35 days - Editor shortcuts Ctrl/Cmd+S to save and Ctrl/Cmd+N for a new page - Live search filter in the admin content browser - Content versioning with timestamped .bak copies in content/-backups/ Also removes eight stale TODO entries that were already implemented
This commit is contained in:
@@ -390,6 +390,7 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
? implode(', ', array_map('trim', $_POST['plugins']))
|
||||
: '';
|
||||
$content = updateContentFrontmatter($content, 'plugins', $plugins);
|
||||
backupContentFile($filePath);
|
||||
file_put_contents($filePath, $content);
|
||||
if (!$wasRenamed) {
|
||||
adminLog($config, 'info', $user['username'] . ' sloeg ' . basename($filePath) . ' op');
|
||||
@@ -890,6 +891,45 @@ function handleStatistics(AdminAuth $auth, array $config): void
|
||||
$analytics = new Analytics($ana);
|
||||
$stats = $analytics->getStats($period);
|
||||
|
||||
// Export (CSV / JSON)
|
||||
if (isset($_GET['export'])) {
|
||||
$format = $_GET['export'] === 'json' ? 'json' : 'csv';
|
||||
$periodSlug = $period === 0 ? 'alles' : $period . 'dagen';
|
||||
$filename = 'codepress-statistieken-' . $periodSlug . '-' . date('Y-m-d');
|
||||
|
||||
if ($format === 'json') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '.json"');
|
||||
echo json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '.csv"');
|
||||
$out = fopen('php://output', 'w');
|
||||
fwrite($out, "\xEF\xBB\xBF"); // BOM so Excel reads UTF-8 correctly
|
||||
|
||||
fputcsv($out, ['Sectie', 'Sleutel', 'Waarde']);
|
||||
foreach ($stats['totals'] as $k => $v) {
|
||||
fputcsv($out, ['Totalen', $k, $v]);
|
||||
}
|
||||
foreach ($stats['countries'] as $k => $v) {
|
||||
fputcsv($out, ['Landen', $k . ' (' . GeoIP::getCountryName($k === 'UNKNOWN' ? null : $k) . ')', $v]);
|
||||
}
|
||||
foreach ($stats['pages'] as $k => $v) {
|
||||
fputcsv($out, ["Pagina's", $k, $v]);
|
||||
}
|
||||
foreach ($stats['referrers'] as $k => $v) {
|
||||
fputcsv($out, ['Verwijzers', $k, $v]);
|
||||
}
|
||||
foreach ($stats['daily_chart'] as $day) {
|
||||
fputcsv($out, ['Per dag', $day['date'], $day['views'] . ' weergaven, ' . $day['uniques'] . ' uniek']);
|
||||
}
|
||||
fclose($out);
|
||||
}
|
||||
|
||||
adminLog($config, 'info', $user['username'] . ' exporteerde statistieken (' . strtoupper($format) . ')');
|
||||
exit;
|
||||
}
|
||||
|
||||
// GeoIP database status
|
||||
$geoDir = $config['codepress_root'] . '/admin/storage/geoip';
|
||||
$geoMeta = null;
|
||||
@@ -897,6 +937,34 @@ function handleStatistics(AdminAuth $auth, array $config): void
|
||||
$geoMeta = json_decode(file_get_contents($geoDir . '/meta.json'), true);
|
||||
}
|
||||
|
||||
// Auto-update the GeoIP database when it is missing or older than 35 days
|
||||
$geoAutoUpdated = false;
|
||||
if (($ana['geoip_provider'] ?? 'local') === 'local' && !empty($ana['enabled'])) {
|
||||
$lastUpdate = !empty($geoMeta['updated']) ? strtotime($geoMeta['updated']) : 0;
|
||||
$isStale = $lastUpdate === 0 || $lastUpdate < strtotime('-35 days');
|
||||
$lockFile = $geoDir . '/.autoupdate';
|
||||
// Only retry once a day if the download keeps failing
|
||||
$recentlyTried = file_exists($lockFile) && filemtime($lockFile) > strtotime('-1 day');
|
||||
|
||||
if ($isStale && !$recentlyTried) {
|
||||
if (!is_dir($geoDir)) {
|
||||
@mkdir($geoDir, 0755, true);
|
||||
}
|
||||
@touch($lockFile);
|
||||
require_once __DIR__ . '/../cli/geoip-update.php';
|
||||
$autoResult = updateGeoIPDatabase();
|
||||
if ($autoResult['success']) {
|
||||
$geoMeta = $autoResult['meta'];
|
||||
$geoAutoUpdated = true;
|
||||
adminLog($config, 'info', 'GeoIP database automatisch bijgewerkt (was verouderd)');
|
||||
if ($message === '') {
|
||||
$message = 'GeoIP database was verouderd en is automatisch bijgewerkt.';
|
||||
$messageType = 'info';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// World map SVG
|
||||
$worldMapPath = $config['codepress_root'] . '/public/assets/img/world-map.svg';
|
||||
$worldMapSvg = file_exists($worldMapPath) ? file_get_contents($worldMapPath) : '';
|
||||
@@ -1745,6 +1813,45 @@ function handleUpdate(AdminAuth $auth, array $config): void
|
||||
|
||||
// --- Frontmatter helpers ---
|
||||
|
||||
/**
|
||||
* Keep a timestamped copy of a content file before it is overwritten.
|
||||
*
|
||||
* Backups live in a hidden -backups directory inside the content folder so the
|
||||
* CMS itself skips them, and only the newest few versions per file are kept.
|
||||
*/
|
||||
function backupContentFile(string $filePath, int $keep = 5): void
|
||||
{
|
||||
if (!is_file($filePath) || filesize($filePath) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contentRoot = realpath(dirname(__DIR__) . '/content');
|
||||
$realFile = realpath($filePath);
|
||||
if (!$contentRoot || !$realFile || strpos($realFile, $contentRoot) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backupDir = $contentRoot . '/-backups';
|
||||
if (!is_dir($backupDir) && !@mkdir($backupDir, 0755, true) && !is_dir($backupDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirror the relative path so files with the same name never collide
|
||||
$relative = ltrim(substr($realFile, strlen($contentRoot)), '/');
|
||||
$flat = str_replace('/', '__', $relative);
|
||||
|
||||
@copy($realFile, $backupDir . '/' . $flat . '.' . date('Ymd-His') . '.bak');
|
||||
|
||||
// Prune older versions of this specific file
|
||||
$existing = glob($backupDir . '/' . $flat . '.*.bak') ?: [];
|
||||
if (count($existing) > $keep) {
|
||||
sort($existing); // filenames sort chronologically
|
||||
foreach (array_slice($existing, 0, count($existing) - $keep) as $old) {
|
||||
@unlink($old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseFrontmatterField(string $content, string $key, string $default = ''): string
|
||||
{
|
||||
if (preg_match('/^---\s*\n(.*?)\n---\s*\n/s', $content, $matches)) {
|
||||
|
||||
Reference in New Issue
Block a user