From fffd51422c0ddb7a6e29adff874b6a39c1d3d9e8 Mon Sep 17 00:00:00 2001 From: Ruben Date: Sun, 1 Feb 2026 20:49:06 +0100 Subject: [PATCH] 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 --- custom/petition-cli.php | 311 ++++++++++++++++++++++++++++++---------- 1 file changed, 238 insertions(+), 73 deletions(-) diff --git a/custom/petition-cli.php b/custom/petition-cli.php index f49985a..1f50051 100755 --- a/custom/petition-cli.php +++ b/custom/petition-cli.php @@ -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'); } } }