normalizeDomain($domain); if ($normalized === null) { return ModerationDomainStatus::Neutral; } $record = ContentModerationDomain::query()->where('domain', $normalized)->first(); if ($record !== null) { return $record->status; } if ($this->matchesAnyPattern($normalized, (array) \app('config')->get('content_moderation.allowed_domains', []))) { return ModerationDomainStatus::Allowed; } if ($this->matchesAnyPattern($normalized, (array) \app('config')->get('content_moderation.blacklisted_domains', []))) { return ModerationDomainStatus::Blocked; } if ($this->matchesAnyPattern($normalized, (array) \app('config')->get('content_moderation.suspicious_domains', []))) { return ModerationDomainStatus::Suspicious; } return ModerationDomainStatus::Neutral; } /** * @param array $domains * @return array */ public function trackDomains(array $domains, bool $flagged = false, bool $confirmedSpam = false): array { $normalized = \collect($domains) ->map(fn (?string $domain): ?string => $this->normalizeDomain($domain)) ->filter() ->unique() ->values(); if ($normalized->isEmpty()) { return []; } $existing = ContentModerationDomain::query() ->whereIn('domain', $normalized->all()) ->get() ->keyBy('domain'); $records = []; $now = \now(); foreach ($normalized as $domain) { $defaultStatus = $this->statusForDomain($domain); $record = $existing[$domain] ?? new ContentModerationDomain([ 'domain' => $domain, 'status' => $defaultStatus, 'first_seen_at' => $now, ]); $record->forceFill([ 'status' => $record->status ?? $defaultStatus, 'times_seen' => ((int) $record->times_seen) + 1, 'times_flagged' => ((int) $record->times_flagged) + ($flagged ? 1 : 0), 'times_confirmed_spam' => ((int) $record->times_confirmed_spam) + ($confirmedSpam ? 1 : 0), 'first_seen_at' => $record->first_seen_at ?? $now, 'last_seen_at' => $now, ])->save(); $records[] = $record->fresh(); } return $records; } public function updateStatus(string $domain, ModerationDomainStatus $status, ?User $actor = null, ?string $notes = null): ContentModerationDomain { $normalized = $this->normalizeDomain($domain); \abort_unless($normalized !== null, 422, 'Invalid domain.'); $record = ContentModerationDomain::query()->firstOrNew(['domain' => $normalized]); $previous = $record->status?->value; $record->forceFill([ 'status' => $status, 'first_seen_at' => $record->first_seen_at ?? \now(), 'last_seen_at' => \now(), 'notes' => $notes !== null && trim($notes) !== '' ? trim($notes) : $record->notes, ])->save(); ContentModerationActionLog::query()->create([ 'target_type' => 'domain', 'target_id' => $record->id, 'action_type' => match ($status) { ModerationDomainStatus::Blocked => ModerationActionType::BlockDomain->value, ModerationDomainStatus::Suspicious => ModerationActionType::MarkDomainSuspicious->value, ModerationDomainStatus::Escalated => ModerationActionType::Escalate->value, ModerationDomainStatus::ReviewRequired => ModerationActionType::MarkDomainSuspicious->value, ModerationDomainStatus::Allowed, ModerationDomainStatus::Neutral => ModerationActionType::AllowDomain->value, }, 'actor_type' => $actor ? 'admin' : 'system', 'actor_id' => $actor?->id, 'notes' => $notes, 'old_status' => $previous, 'new_status' => $status->value, 'meta_json' => ['domain' => $normalized], 'created_at' => \now(), ]); $this->intelligence->refreshDomain($normalized); return $record->fresh(); } /** * @return array */ public function shortenerDomains(): array { return \collect((array) \app('config')->get('content_moderation.shortener_domains', [])) ->map(fn (string $domain): ?string => $this->normalizeDomain($domain)) ->filter() ->values() ->all(); } public function attachDomainIds(ContentModerationFinding $finding): void { $domains = \collect((array) $finding->matched_domains_json) ->map(fn (?string $domain): ?string => $this->normalizeDomain($domain)) ->filter() ->unique() ->values(); if ($domains->isEmpty()) { $finding->forceFill(['domain_ids_json' => []])->save(); return; } $ids = ContentModerationDomain::query() ->whereIn('domain', $domains->all()) ->pluck('id') ->map(static fn (int $id): int => $id) ->values() ->all(); $finding->forceFill(['domain_ids_json' => $ids])->save(); foreach ($domains as $domain) { $this->intelligence->refreshDomain((string) $domain); } } private function matchesAnyPattern(string $domain, array $patterns): bool { foreach ($patterns as $pattern) { $pattern = trim(mb_strtolower((string) $pattern)); if ($pattern === '') { continue; } $quoted = preg_quote($pattern, '/'); $regex = '/^' . str_replace('\\*', '.*', $quoted) . '$/i'; if (preg_match($regex, $domain) === 1) { return true; } } return false; } }