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)) {
|
||||
|
||||
+179
-151
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 164 KiB |
@@ -230,4 +230,37 @@
|
||||
document.getElementById('editor-form').addEventListener('submit', function () {
|
||||
editor.save();
|
||||
});
|
||||
|
||||
// Keyboard shortcuts: Ctrl/Cmd+S saves, Ctrl/Cmd+N creates a new page
|
||||
function handleShortcut(e) {
|
||||
var mod = e.ctrlKey || e.metaKey;
|
||||
if (!mod || e.altKey) return;
|
||||
|
||||
var key = (e.key || '').toLowerCase();
|
||||
|
||||
if (key === 's') {
|
||||
e.preventDefault();
|
||||
editor.save();
|
||||
if (form) {
|
||||
if (typeof form.requestSubmit === 'function') {
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'n') {
|
||||
e.preventDefault();
|
||||
window.location.href = '/admin/content-new';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleShortcut);
|
||||
// CodeMirror swallows keystrokes inside the editor, so bind there too
|
||||
editor.setOption('extraKeys', Object.assign({}, editor.getOption('extraKeys') || {}, {
|
||||
'Ctrl-S': function () { handleShortcut({ ctrlKey: true, key: 's', preventDefault: function () {} }); },
|
||||
'Cmd-S': function () { handleShortcut({ metaKey: true, key: 's', preventDefault: function () {} }); }
|
||||
}));
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user