Add signature deletion functionality to petition CLI

Introduce new modifyPetitionFile helper function for atomic CSV
operations
Refactor manual confirmation to use the new helper function
Add new manuallyDeleteSignatures function with confirmation flow
Update menu to include deletion option and adjust numbering
This commit is contained in:
Ruben 2026-02-01 20:49:06 +01:00
parent fdbf9a3210
commit fffd51422c

View file

@ -408,7 +408,8 @@ function showMenu(): void {
echo " 4) Send bekreftelse pa nytt til ubekreftede\n";
echo " 5) Marker oppforinger som ignorert\n";
echo " 6) Manuell bekreftelse av signaturer\n";
echo " 7) Avslutt\n";
echo " 7) Slett signaturer\n";
echo " 8) Avslutt\n";
echo "\n";
}
@ -643,6 +644,73 @@ function resendToUnconfirmed(): void {
printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green');
}
/**
* Atomically modify petition CSV file with a callback function.
* The callback receives the rows array (without header) by reference and can modify it.
* Returns the result from the callback, or null on file errors.
*
* @param string $petitionId The petition ID
* @param callable $callback Function that receives (&$rows, $petitionId) and returns a result
* @return mixed|null The callback's return value, or null on error
*/
function modifyPetitionFile(string $petitionId, callable $callback): mixed {
$csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv';
if (!file_exists($csvPath)) {
return null;
}
// Open with r+ to allow reading and writing, acquire exclusive lock immediately
// This matches the pattern used in petition-form.php to prevent race conditions
$fp = fopen($csvPath, 'r+');
if (!$fp) {
printColor(" Advarsel: Kunne ikke apne {$petitionId}.csv\n", 'yellow');
return null;
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
printColor(" Advarsel: Kunne ikke lase {$petitionId}.csv\n", 'yellow');
return null;
}
// Read all rows while holding the lock
$header = fgetcsv($fp, null, ',', '"', '');
if ($header === false) {
flock($fp, LOCK_UN);
fclose($fp);
return null;
}
$rows = [];
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
$rows[] = $row;
}
// Call the callback to modify rows
$originalCount = count($rows);
$result = $callback($rows, $petitionId);
// Write back if rows were modified (count changed or callback signals modification)
$modified = (count($rows) !== $originalCount);
if (!$modified && is_array($result) && isset($result['modified'])) {
$modified = $result['modified'];
}
if ($modified) {
rewind($fp);
ftruncate($fp, 0);
fputcsv($fp, $header, ',', '"', '');
foreach ($rows as $row) {
fputcsv($fp, $row, ',', '"', '');
}
}
flock($fp, LOCK_UN);
fclose($fp);
return $result;
}
/**
* Manually confirm signatures by email address
*/
@ -671,89 +739,53 @@ function manuallyConfirmSignatures(): void {
echo "Soker etter " . count($emails) . " e-postadresse(r)...\n\n";
$found = [];
$notFound = [];
// Search through all petition files
foreach (getPetitionFiles() as $petitionId) {
$csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv';
if (!file_exists($csvPath)) {
continue;
}
// Open with r+ to allow reading and writing, acquire exclusive lock immediately
// This matches the pattern used in petition-form.php to prevent race conditions
$fp = fopen($csvPath, 'r+');
if (!$fp) {
printColor(" Advarsel: Kunne ikke apne {$petitionId}.csv\n", 'yellow');
continue;
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
printColor(" Advarsel: Kunne ikke lase {$petitionId}.csv\n", 'yellow');
continue;
}
// Read all rows while holding the lock
$rows = [];
$header = fgetcsv($fp, null, ',', '"', '');
if ($header === false) {
// Empty file, skip
flock($fp, LOCK_UN);
fclose($fp);
continue;
}
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
$rows[] = $row;
}
$modified = false;
foreach ($rows as &$row) {
if (!isset($row[1]) || !isset($row[6])) {
continue;
}
$result = modifyPetitionFile($petitionId, function(&$rows, $petitionId) use ($emails) {
$found = [];
$modified = false;
$rowEmail = strtolower($row[1]);
if (in_array($rowEmail, $emails)) {
if ($row[6] === 'pending') {
$row[6] = 'confirmed';
$modified = true;
$found[] = [
'email' => $row[1],
'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''),
'petition_id' => $petitionId,
'was_pending' => true
];
} else {
$found[] = [
'email' => $row[1],
'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''),
'petition_id' => $petitionId,
'was_pending' => false,
'status' => $row[6]
];
foreach ($rows as &$row) {
if (!isset($row[1]) || !isset($row[6])) {
continue;
}
$rowEmail = strtolower($row[1]);
if (in_array($rowEmail, $emails)) {
if ($row[6] === 'pending') {
$row[6] = 'confirmed';
$modified = true;
$found[] = [
'email' => $row[1],
'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''),
'petition_id' => $petitionId,
'was_pending' => true
];
} else {
$found[] = [
'email' => $row[1],
'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''),
'petition_id' => $petitionId,
'was_pending' => false,
'status' => $row[6]
];
}
}
}
}
unset($row);
unset($row);
return ['found' => $found, 'modified' => $modified];
});
// Write back if modified (still holding the lock)
if ($modified) {
rewind($fp);
ftruncate($fp, 0);
fputcsv($fp, $header, ',', '"', '');
foreach ($rows as $row) {
fputcsv($fp, $row, ',', '"', '');
}
if ($result && !empty($result['found'])) {
$found = array_merge($found, $result['found']);
}
flock($fp, LOCK_UN);
fclose($fp);
}
// Determine which emails were not found
$foundEmails = array_map(fn($f) => strtolower($f['email']), $found);
$notFound = [];
foreach ($emails as $email) {
if (!in_array($email, $foundEmails)) {
$notFound[] = $email;
@ -789,6 +821,136 @@ function manuallyConfirmSignatures(): void {
printColor("Ferdig: {$confirmedCount} signatur(er) bekreftet\n", 'green');
}
/**
* Manually delete signatures by email address
*/
function manuallyDeleteSignatures(): void {
echo "\n";
printColor("Slett signaturer\n", 'cyan');
echo "\n";
$input = prompt("Skriv inn e-postadresse(r) (kommaseparert): ");
if (empty(trim($input))) {
echo "Ingen e-postadresser oppgitt.\n";
return;
}
// Parse and clean email addresses
$emails = array_map('trim', explode(',', $input));
$emails = array_filter($emails, fn($e) => !empty($e));
$emails = array_map('strtolower', $emails);
if (empty($emails)) {
echo "Ingen gyldige e-postadresser oppgitt.\n";
return;
}
echo "\n";
echo "Soker etter " . count($emails) . " e-postadresse(r)...\n\n";
$found = [];
// First pass: find all matching signatures (read-only)
foreach (getPetitionFiles() as $petitionId) {
$csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv';
if (!file_exists($csvPath)) {
continue;
}
$fp = fopen($csvPath, 'r');
if (!$fp) {
continue;
}
if (!flock($fp, LOCK_SH)) {
fclose($fp);
continue;
}
fgetcsv($fp, null, ',', '"', ''); // Skip header
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (!isset($row[1])) {
continue;
}
$rowEmail = strtolower($row[1]);
if (in_array($rowEmail, $emails)) {
$found[] = [
'email' => $row[1],
'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''),
'petition_id' => $petitionId,
'status' => $row[6] ?? 'unknown',
'timestamp' => (int)($row[0] ?? 0)
];
}
}
flock($fp, LOCK_UN);
fclose($fp);
}
// Determine which emails were not found
$foundEmails = array_map(fn($f) => strtolower($f['email']), $found);
$notFound = [];
foreach ($emails as $email) {
if (!in_array($email, $foundEmails)) {
$notFound[] = $email;
}
}
if (!empty($notFound)) {
printColor("Ikke funnet:\n", 'red');
foreach ($notFound as $email) {
echo " - {$email}\n";
}
echo "\n";
}
if (empty($found)) {
echo "Ingen signaturer a slette.\n";
return;
}
// Show what will be deleted
printColor("Folgende signaturer vil bli slettet:\n", 'yellow');
foreach ($found as $entry) {
$date = formatDate($entry['timestamp']);
echo " - {$entry['email']} ({$entry['name']}) - {$entry['petition_id']} [{$entry['status']}] {$date}\n";
}
echo "\n";
if (!confirm("Er du sikker pa at du vil slette " . count($found) . " signatur(er)?")) {
echo "Avbrutt.\n";
return;
}
// Second pass: delete the signatures
$deleted = 0;
$emailsToDelete = array_map(fn($f) => strtolower($f['email']), $found);
foreach (getPetitionFiles() as $petitionId) {
$result = modifyPetitionFile($petitionId, function(&$rows, $petitionId) use ($emailsToDelete) {
$deletedCount = 0;
$rows = array_filter($rows, function($row) use ($emailsToDelete, &$deletedCount) {
if (isset($row[1]) && in_array(strtolower($row[1]), $emailsToDelete)) {
$deletedCount++;
return false; // Remove this row
}
return true; // Keep this row
});
$rows = array_values($rows); // Re-index
return ['deleted' => $deletedCount];
});
if ($result && isset($result['deleted'])) {
$deleted += $result['deleted'];
}
}
echo "\n";
printColor("Ferdig: {$deleted} signatur(er) slettet\n", 'green');
}
/**
* Interactive ignore marking
*/
@ -973,13 +1135,16 @@ function main(): void {
manuallyConfirmSignatures();
break;
case '7':
manuallyDeleteSignatures();
break;
case '8':
case 'q':
case 'quit':
case 'exit':
echo "Ha det!\n";
exit(0);
default:
printColor("Ugyldig valg. Velg 1-7.\n", 'red');
printColor("Ugyldig valg. Velg 1-8.\n", 'red');
}
}
}