Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,100 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user