Files

101 lines
3.3 KiB
PHP

<?php
namespace App\Http\Livewire\Polls;
use App\Models\Poll;
use App\Models\PollAnswer;
use App\Models\PollResponse;
use App\Polls\Ledger;
use App\Polls\QuestionTypeRegistry;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Attributes\Locked;
use Livewire\Component;
/**
* "Check my receipt" on the verification page: shows the ballot behind a receipt code
* and whether it counts, was replaced by a changed vote, or was withdrawn.
*/
class ReceiptCheck extends Component
{
#[Locked]
public int $pollId;
public string $code = '';
#[Locked]
public ?array $result = null;
#[Locked]
public bool $notFound = false;
public function mount(int $pollId): void
{
$this->pollId = $pollId;
}
public function check(Ledger $ledger): void
{
$this->validate(['code' => ['required', 'string', 'max:40']], [], ['code' => __('Receipt code')]);
// Codes cannot be guessed (about 99 bits), but checks are limited anyway (security audit F6)
$limiterKey = 'poll-receipt-check:' . (auth('web')->id() ?? request()->ip());
if (RateLimiter::tooManyAttempts($limiterKey, 20)) {
$this->addError('code', __('Too many checks. Please try again in a minute.'));
return;
}
RateLimiter::hit($limiterKey, 60);
$poll = Poll::findOrFail($this->pollId);
$this->result = null;
$this->notFound = false;
$found = $poll->verification !== Poll::VERIFICATION_OFF ? $ledger->lookupReceipt($poll, $this->code) : null;
if (!$found) {
$this->notFound = true;
return;
}
$entry = $found['entry'];
$questions = $poll->questions()->get()->keyBy('id');
$registry = app(QuestionTypeRegistry::class);
// Texts are in poll_answers only while this ballot is the voter's current one
$texts = [];
$response = PollResponse::where('poll_id', $poll->id)->where('ledger_sequence', $entry->sequence)->first();
if ($response) {
$texts = PollAnswer::where('poll_response_id', $response->id)->pluck('text', 'poll_question_id')->all();
}
$answers = [];
foreach ($entry->payload['answers'] ?? [] as $answer) {
$question = $questions->get($answer['question_id']);
if (!$question || !$registry->has($question->type)) {
continue;
}
$text = $texts[$question->id] ?? null;
$textHash = $answer['text_sha256'] ?? null;
$textMatches = $textHash && $text !== null && hash_equals($textHash, hash('sha256', $text));
$shownText = $textMatches ? $text : ($textHash ? '[' . __('text') . ']' : null);
$answers[] = [
'question' => $question->labelFor(App::getLocale()),
'answer' => $question->questionType()->answerToText($answer['value'] ?? null, $shownText, $question->settings ?? [], App::getLocale()),
'text_checked' => $textMatches,
];
}
$this->result = [
'sequence' => $entry->sequence,
'status' => $found['status'],
'recorded_at' => $entry->recorded_at,
'answers' => $answers,
];
}
public function render()
{
return view('livewire.polls.receipt-check');
}
}