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,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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user