#!/usr/bin/env php = $min && $val <= $max) return $val; echo " Please enter a number between $min and $max.\n"; } } function promptFloat(string $question, float $default, float $min = 0.0, float $max = 1.0): float { while (true) { $val = (float)prompt($question, (string)$default); if ($val >= $min && $val <= $max) return $val; echo " Please enter a number between $min and $max.\n"; } } // ------- Distribution helpers ------- /** * Generate a biased region distribution. * * @param int $n Total signatures to distribute * @param float $variance 0.0 = perfectly even, 1.0 = highly concentrated (Oslo-heavy) * @return array [region => count] */ function generateRegionDistribution(int $n, float $variance, array $regions): array { // Base weights (approx. Norwegian population distribution, normalized) $baseWeights = [ 'oslo' => 12.0, 'akershus' => 11.5, 'vestland' => 9.0, 'rogaland' => 8.0, 'trondelag' => 7.5, 'innlandet' => 5.5, 'buskerud' => 5.0, 'vestfold' => 4.5, 'ostfold' => 4.5, 'more_og_romsdal'=> 4.5, 'agder' => 4.0, 'telemark' => 3.5, 'nordland' => 4.0, 'troms' => 3.5, 'finnmark' => 1.5, ]; // Blend between uniform (variance=0) and biased (variance=1) $uniform = 1.0 / count($regions); $total = array_sum($baseWeights); $weights = []; foreach ($regions as $r) { $biased = ($baseWeights[$r] ?? 1.0) / $total; $weights[$r] = (1 - $variance) * $uniform + $variance * $biased; } // Normalise $wsum = array_sum($weights); foreach ($weights as &$w) $w /= $wsum; // Allocate counts $counts = []; $remaining = $n; $keys = array_keys($weights); foreach ($keys as $i => $r) { if ($i === count($keys) - 1) { $counts[$r] = $remaining; } else { $c = (int)round($n * $weights[$r]); $counts[$r] = $c; $remaining -= $c; } } return $counts; } // ------- CSV writer ------- function appendSignature(string $csvPath, array $data): void { $fp = fopen($csvPath, 'a'); if (!$fp) { echo "ERROR: Cannot open $csvPath\n"; exit(1); } fputcsv($fp, $data, ',', '"', ''); fclose($fp); } function initCsv(string $csvPath): void { $fp = fopen($csvPath, 'w'); if (!$fp) { echo "ERROR: Cannot create $csvPath\n"; exit(1); } fputcsv($fp, ['timestamp', 'email', 'firstname', 'surname', 'region', 'display', 'status', 'token', 'token_created', 'ip_hash'], ',', '"', ''); fclose($fp); } // ------- Main ------- echo "\n"; echo "╔══════════════════════════════════════════════╗\n"; echo "║ Mock Petition Data Generator — DEV ONLY ║\n"; echo "╚══════════════════════════════════════════════╝\n\n"; $existingCount = 0; if (file_exists($csvPath)) { $fp = fopen($csvPath, 'r'); fgetcsv($fp, null, ',', '"', ''); // skip header while (fgetcsv($fp, null, ',', '"', '')) $existingCount++; fclose($fp); echo " Current CSV: $csvPath\n"; echo " Existing rows: $existingCount\n\n"; } else { echo " CSV does not exist yet, will create it.\n\n"; } $action = prompt(" Action — [a]ppend or [r]eplace (wipes existing data)", 'a'); $replace = strtolower($action[0] ?? 'a') === 'r'; if ($replace && $existingCount > 0) { $confirm = prompt(" ⚠️ This will delete $existingCount existing rows. Type 'yes' to confirm", ''); if (strtolower($confirm) !== 'yes') { echo " Aborted.\n"; exit(0); } } echo "\n"; $count = promptInt(" How many mock petitioners to generate", 1000, 1, 100000); echo "\n Regional distribution:\n"; echo " 0.0 = perfectly even across all fylker\n"; echo " 1.0 = highly concentrated (Oslo/Akershus heavy, matches real population)\n"; $variance = promptFloat(" Variance (0.0 – 1.0)", 0.7, 0.0, 1.0); echo "\n"; // Generate distribution $dist = generateRegionDistribution($count, $variance, $regions); echo " Distribution preview:\n"; arsort($dist); foreach ($dist as $r => $c) { $bar = str_repeat('█', (int)round($c / $count * 40)); printf(" %-20s %4d %s\n", $r, $c, $bar); } echo "\n"; $go = prompt(" Generate $count signatures? [y/n]", 'y'); if (strtolower($go[0] ?? 'n') !== 'y') { echo " Aborted.\n"; exit(0); } // Prepare CSV if ($replace || !file_exists($csvPath)) { initCsv($csvPath); } // Generate signatures $fp = fopen($csvPath, 'a'); if (!$fp) { echo "ERROR: Cannot open $csvPath for writing\n"; exit(1); } $now = time(); $generated = 0; $usedEmails = []; foreach ($dist as $region => $regionCount) { for ($i = 0; $i < $regionCount; $i++) { $firstname = $GLOBALS['firstNames'][array_rand($GLOBALS['firstNames'])]; $surname = $GLOBALS['surnames'][array_rand($GLOBALS['surnames'])]; $display = $GLOBALS['displayOptions'][array_rand($GLOBALS['displayOptions'])]; // Unique-ish email $slug = strtolower($firstname . '.' . $surname . '.' . $generated); $email = $slug . '@mock.test'; // Timestamp spread over last 60 days $timestamp = $now - random_int(0, 60 * 86400); $token = bin2hex(random_bytes(32)); $ipHash = bin2hex(random_bytes(32)); fputcsv($fp, [ $timestamp, $email, $firstname, $surname, $region, $display, 'confirmed', // all mock sigs are confirmed $token, $timestamp, $ipHash, ], ',', '"', ''); $generated++; } } fclose($fp); echo "\n ✓ Generated $generated mock signatures.\n"; echo " ✓ Written to: $csvPath\n\n"; // Invalidate map cache (both private cache and public static file) foreach ([ '/var/www/custom/data/petition-map-cache.json', '/var/www/custom/assets/petition-map-data.json', ] as $cacheFile) { if (file_exists($cacheFile)) { unlink($cacheFile); echo " ✓ Cleared: $cacheFile\n"; } } echo "\n";