From fdbf9a32107318fe52f51733c8dc7e8bcda83fcc Mon Sep 17 00:00:00 2001 From: Ruben Date: Sun, 1 Feb 2026 20:27:34 +0100 Subject: [PATCH 1/2] Add manual signature confirmation by email address Add new menu option to manually confirm signatures by email address Search through all petition files to find matching emails Update status from pending to confirmed when found Display results with confirmation status and petition IDs Handle multiple emails via comma-separated input Maintain proper file locking to prevent race conditions --- custom/petition-cli.php | 154 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/custom/petition-cli.php b/custom/petition-cli.php index f409ca5..f49985a 100755 --- a/custom/petition-cli.php +++ b/custom/petition-cli.php @@ -407,7 +407,8 @@ function showMenu(): void { echo " 3) Send e-post pa nytt til mislykkede\n"; echo " 4) Send bekreftelse pa nytt til ubekreftede\n"; echo " 5) Marker oppforinger som ignorert\n"; - echo " 6) Avslutt\n"; + echo " 6) Manuell bekreftelse av signaturer\n"; + echo " 7) Avslutt\n"; echo "\n"; } @@ -642,6 +643,152 @@ function resendToUnconfirmed(): void { printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); } +/** + * Manually confirm signatures by email address + */ +function manuallyConfirmSignatures(): void { + echo "\n"; + printColor("Manuell bekreftelse av 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 = []; + $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; + } + + $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); + + // 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, ',', '"', ''); + } + } + + flock($fp, LOCK_UN); + fclose($fp); + } + + // Determine which emails were not found + $foundEmails = array_map(fn($f) => strtolower($f['email']), $found); + foreach ($emails as $email) { + if (!in_array($email, $foundEmails)) { + $notFound[] = $email; + } + } + + // Display results + if (!empty($found)) { + printColor("Funnet:\n", 'green'); + foreach ($found as $entry) { + $status = $entry['was_pending'] + ? "BEKREFTET" + : "allerede " . $entry['status']; + echo " - {$entry['email']} ({$entry['name']}) - {$entry['petition_id']}: "; + if ($entry['was_pending']) { + printColor("{$status}\n", 'green'); + } else { + printColor("{$status}\n", 'yellow'); + } + } + } + + if (!empty($notFound)) { + echo "\n"; + printColor("Ikke funnet:\n", 'red'); + foreach ($notFound as $email) { + echo " - {$email}\n"; + } + } + + $confirmedCount = count(array_filter($found, fn($f) => $f['was_pending'])); + echo "\n"; + printColor("Ferdig: {$confirmedCount} signatur(er) bekreftet\n", 'green'); +} + /** * Interactive ignore marking */ @@ -823,13 +970,16 @@ function main(): void { markAsIgnored(); break; case '6': + manuallyConfirmSignatures(); + break; + case '7': case 'q': case 'quit': case 'exit': echo "Ha det!\n"; exit(0); default: - printColor("Ugyldig valg. Velg 1-6.\n", 'red'); + printColor("Ugyldig valg. Velg 1-7.\n", 'red'); } } } From fffd51422c0ddb7a6e29adff874b6a39c1d3d9e8 Mon Sep 17 00:00:00 2001 From: Ruben Date: Sun, 1 Feb 2026 20:49:06 +0100 Subject: [PATCH 2/2] 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'); } } }