* * Data flow: * CSV -> petitionMapBuildData() -> cache (custom/data/) + public JSON (custom/assets/) * Public JSON is served at /petition-map-data.json by the router's asset mechanism. * JS fetches it and renders dots on a Leaflet map. * * Public JSON format: * { "generated": int, "total": int, "regions": { "oslo": { "count": 42, "signers": [{"n":"Kari","a":false}] } } } * n = first name (null for anonymous), a = anonymous (bool) * No emails, surnames, tokens, or IP hashes ever leave the server. */ define('PETITION_MAP_CACHE_TTL', 60); define('PETITION_MAP_CACHE_PATH', dirname(__DIR__, 2) . '/data/petition-map-cache.json'); define('PETITION_MAP_PUBLIC_PATH', dirname(__DIR__, 2) . '/assets/petition-map-data.json'); /** * Build public-safe map data from the petition CSV. */ function petitionMapBuildData(string $csvPath): array { $regions = []; $total = 0; if (!file_exists($csvPath)) { return ['generated' => time(), 'total' => 0, 'regions' => (object)[]]; } $fp = fopen($csvPath, 'r'); if (!$fp) { return ['generated' => time(), 'total' => 0, 'regions' => (object)[]]; } if (flock($fp, LOCK_SH)) { fgetcsv($fp, null, ',', '"', ''); while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { if (!isset($row[6]) || $row[6] !== 'confirmed') continue; $region = $row[4] ?? ''; $display = $row[5] ?? 'semi'; if (empty($region)) continue; $anonymous = ($display === 'anonymous'); $name = $anonymous ? null : trim($row[2] ?? ''); if ($name !== null) { $name = mb_substr($name, 0, 50); if ($name === '') $name = null; } if (!isset($regions[$region])) { $regions[$region] = ['count' => 0, 'signers' => []]; } $regions[$region]['count']++; $regions[$region]['signers'][] = ['n' => $name, 'a' => $anonymous]; $total++; } flock($fp, LOCK_UN); } fclose($fp); return [ 'generated' => time(), 'total' => $total, 'regions' => empty($regions) ? (object)[] : $regions, ]; } function petitionMapReadCache(): ?array { $path = PETITION_MAP_CACHE_PATH; if (!file_exists($path)) return null; if ((time() - filemtime($path)) > PETITION_MAP_CACHE_TTL) return null; $json = file_get_contents($path); if (!$json) return null; $data = json_decode($json, true); return is_array($data) ? $data : null; } function petitionMapWriteCache(array $data): void { $json = json_encode($data, JSON_UNESCAPED_UNICODE); if ($json === false) return; foreach ([PETITION_MAP_CACHE_PATH, PETITION_MAP_PUBLIC_PATH] as $path) { $dir = dirname($path); if (!is_dir($dir)) { mkdir($dir, 0750, true); } $tmp = $path . '.tmp.' . getmypid(); if (file_put_contents($tmp, $json, LOCK_EX) !== false) { rename($tmp, $path); } } } function petitionMapGetData(string $csvPath): array { $cached = petitionMapReadCache(); if ($cached !== null) return $cached; $data = petitionMapBuildData($csvPath); petitionMapWriteCache($data); return $data; } /** * Render the map widget HTML block. */ function petitionMapRenderWidget(Context $ctx): string { $langPrefix = $ctx->get('langPrefix', ''); // CSS hash for cache busting $cssPath = dirname(__DIR__, 2) . '/assets/petition-map.css'; $cssHash = file_exists($cssPath) ? substr(hash_file('md5', $cssPath), 0, 8) : ''; $jsPath = dirname(__DIR__, 2) . '/assets/petition-map.js'; $jsHash = file_exists($jsPath) ? substr(hash_file('md5', $jsPath), 0, 8) : ''; $dataJsonPath = PETITION_MAP_PUBLIC_PATH; $dataHash = file_exists($dataJsonPath) ? substr(hash_file('md5', $dataJsonPath), 0, 8) : ''; $html = ''; // Leaflet CSS $html .= ''; // Map CSS $html .= ''; // Map container $html .= '
'; $html .= '
'; $html .= '
Laster kart…
'; $html .= '
'; $html .= '
'; // Config for JS $html .= ''; // Leaflet JS + map JS $html .= ''; $html .= ''; return $html; } // --- Hook --- Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) { // Resolve page directory and metadata (reuse petition-form helpers if available) if (!function_exists('petitionResolvePageDir')) return $vars; $pageDir = petitionResolvePageDir($ctx); if (!$pageDir) return $vars; $metadata = loadMetadata($pageDir); $plugins = $metadata['plugins'] ?? ''; if (strpos($plugins, 'petition-map') === false) return $vars; $petitionId = $metadata['petition_id'] ?? (function_exists('petitionGetIdFromPath') ? petitionGetIdFromPath($pageDir) : null); if (!$petitionId) return $vars; $petitionId = preg_replace('/[^a-z0-9\-_]/i', '', $petitionId); if (empty($petitionId)) return $vars; $csvPath = dirname(__DIR__, 2) . "/data/petitions/{$petitionId}.csv"; // Refresh cache + public JSON if stale petitionMapGetData($csvPath); // Inject widget HTML as template variable $vars['petition_map'] = petitionMapRenderWidget($ctx); return $vars; });