101 lines
3.3 KiB
PHP
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');
|
|
}
|
|
}
|