Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
<?php
namespace App\Polls;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\Poll;
use App\Models\PollLedgerEntry;
use App\Models\Post;
use App\Models\PostTranslation;
use App\Models\User;
use Illuminate\Support\Carbon;
/**
* Poll definitions in post backups (BackupPosts / RestorePosts / Posts\BackupRestore).
* Only the definition travels: no votes and no ledger. A restored poll is a draft.
* Location targeting is left out: location ids differ between databases.
*/
class PollBackup
{
private const OWNER_TYPES = [User::class, Organization::class, Bank::class, Admin::class];
public static function export(Poll $poll): array
{
return [
'owner_type' => $poll->pollable_type,
'owner_name' => $poll->pollable?->name,
'opens_at' => $poll->opens_at?->toDateTimeString(),
'closes_at' => $poll->closes_at?->toDateTimeString(),
'vote_scope' => $poll->vote_scope,
'member_scope' => $poll->member_scope,
'allow_vote_change' => (bool) $poll->allow_vote_change,
'anonymous' => (bool) $poll->anonymous,
'results_visibility' => $poll->results_visibility,
'verification' => $poll->verification,
'target_profile_types' => $poll->target_profile_types,
'last_login_from' => $poll->last_login_from?->toDateString(),
'last_login_until' => $poll->last_login_until?->toDateString(),
'registered_from' => $poll->registered_from?->toDateString(),
'registered_until' => $poll->registered_until?->toDateString(),
'questions' => $poll->questions->map(fn ($question) => [
'position' => $question->position,
'type' => $question->type,
'label' => $question->label,
'settings' => $question->settings,
'required' => (bool) $question->required,
])->values()->all(),
];
}
/**
* Recreate a poll from a backup as a draft. The owner is looked up by type and name.
*/
public static function import(Post $post, array $data): Poll
{
$owner = null;
$ownerType = $data['owner_type'] ?? null;
// Whitelist: never instantiate an arbitrary class from a backup file
if (in_array($ownerType, self::OWNER_TYPES, true) && !empty($data['owner_name'])) {
$owner = $ownerType::where('name', $data['owner_name'])->first();
}
$pick = fn (string $key, array $allowed, string $default) => in_array($data[$key] ?? null, $allowed, true) ? $data[$key] : $default;
$profileTypes = array_values(array_intersect((array) ($data['target_profile_types'] ?? []), Poll::PROFILE_TYPES));
$poll = Poll::create([
'post_id' => $post->id,
'pollable_id' => $owner?->getKey(),
'pollable_type' => $owner ? $ownerType : null,
'status' => Poll::STATUS_DRAFT,
'opens_at' => self::date($data['opens_at'] ?? null),
'closes_at' => self::date($data['closes_at'] ?? null),
'vote_scope' => $pick('vote_scope', [Poll::VOTE_SCOPE_PER_PROFILE, Poll::VOTE_SCOPE_PER_USER], Poll::VOTE_SCOPE_PER_PROFILE),
'member_scope' => $pick('member_scope', [Poll::MEMBER_SCOPE_ALL_USERS, Poll::MEMBER_SCOPE_MANAGERS_COORDINATORS], Poll::MEMBER_SCOPE_ALL_USERS),
'allow_vote_change' => (bool) ($data['allow_vote_change'] ?? false),
'anonymous' => (bool) ($data['anonymous'] ?? false),
'results_visibility' => $pick('results_visibility', [Poll::RESULTS_NEVER, Poll::RESULTS_AFTER_VOTE, Poll::RESULTS_AFTER_CLOSE], Poll::RESULTS_NEVER),
'verification' => $pick('verification', [Poll::VERIFICATION_OFF, Poll::VERIFICATION_RECEIPTS, Poll::VERIFICATION_PUBLIC], Poll::VERIFICATION_PUBLIC),
'filter_by_location' => false,
'target_profile_types' => $profileTypes ?: null,
'last_login_from' => self::date($data['last_login_from'] ?? null),
'last_login_until' => self::date($data['last_login_until'] ?? null),
'registered_from' => self::date($data['registered_from'] ?? null),
'registered_until' => self::date($data['registered_until'] ?? null),
]);
foreach ((array) ($data['questions'] ?? []) as $index => $question) {
if (!is_array($question) || !is_string($question['type'] ?? null)) {
continue;
}
$poll->questions()->create([
'position' => (int) ($question['position'] ?? $index + 1),
'type' => $question['type'],
'label' => is_array($question['label'] ?? null) ? $question['label'] : [],
'settings' => is_array($question['settings'] ?? null) ? $question['settings'] : [],
'required' => (bool) ($question['required'] ?? true),
]);
}
return $poll;
}
/**
* Whether overwriting the posts with these slugs would destroy a poll's votes: deleting a post
* cascades to its poll, its votes and its ballot ledger.
*/
public static function protectsSlugs(array $slugs): bool
{
$postIds = PostTranslation::withTrashed()->whereIn('slug', $slugs)->pluck('post_id');
return PollLedgerEntry::whereIn('poll_id', Poll::withTrashed()->whereIn('post_id', $postIds)->select('id'))->exists();
}
private static function date(?string $value): ?Carbon
{
return $value ? Carbon::parse($value) : null;
}
}