diff --git a/custom/petition-cli.php b/custom/petition-cli.php index f409ca5..1f50051 100755 --- a/custom/petition-cli.php +++ b/custom/petition-cli.php @@ -407,7 +407,9 @@ 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) Slett signaturer\n"; + echo " 8) Avslutt\n"; echo "\n"; } @@ -642,6 +644,313 @@ 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 + */ +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 = []; + + // Search through all petition files + foreach (getPetitionFiles() as $petitionId) { + $result = modifyPetitionFile($petitionId, function(&$rows, $petitionId) use ($emails) { + $found = []; + $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); + + return ['found' => $found, 'modified' => $modified]; + }); + + if ($result && !empty($result['found'])) { + $found = array_merge($found, $result['found']); + } + } + + // 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; + } + } + + // 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'); +} + +/** + * 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 */ @@ -823,13 +1132,19 @@ function main(): void { markAsIgnored(); break; case '6': + 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-6.\n", 'red'); + printColor("Ugyldig valg. Velg 1-8.\n", 'red'); } } }