$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; } }