273 lines
9.3 KiB
PHP
273 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Actions\BbcodeFormatter;
|
|
use App\Models\Post;
|
|
use App\Models\Thread;
|
|
use App\Models\Setting;
|
|
use App\Services\AuditLogger;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class PostController extends Controller
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$query = Post::query()->withoutTrashed()->with([
|
|
'user' => fn ($query) => $query
|
|
->withCount(['posts', 'threads', 'thanksGiven', 'thanksReceived'])
|
|
->with(['rank', 'roles']),
|
|
'attachments.extension',
|
|
'attachments.group',
|
|
]);
|
|
|
|
$threadParam = $request->query('thread');
|
|
if (is_string($threadParam)) {
|
|
$threadId = $this->parseIriId($threadParam);
|
|
if ($threadId !== null) {
|
|
$query->where('thread_id', $threadId);
|
|
}
|
|
}
|
|
|
|
$posts = $query
|
|
->oldest('created_at')
|
|
->get()
|
|
->map(fn (Post $post) => $this->serializePost($post));
|
|
|
|
return response()->json($posts);
|
|
}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$data = $request->validate([
|
|
'body' => ['required', 'string'],
|
|
'thread' => ['required', 'string'],
|
|
]);
|
|
|
|
$threadId = $this->parseIriId($data['thread']);
|
|
$thread = Thread::findOrFail($threadId);
|
|
|
|
$post = Post::create([
|
|
'thread_id' => $thread->id,
|
|
'user_id' => $request->user()?->id,
|
|
'body' => $data['body'],
|
|
]);
|
|
|
|
app(AuditLogger::class)->log($request, 'post.created', $post, [
|
|
'thread_id' => $thread->id,
|
|
]);
|
|
|
|
$post->loadMissing([
|
|
'user' => fn ($query) => $query
|
|
->withCount(['posts', 'threads', 'thanksGiven', 'thanksReceived'])
|
|
->with(['rank', 'roles']),
|
|
'attachments.extension',
|
|
'attachments.group',
|
|
]);
|
|
|
|
return response()->json($this->serializePost($post), 201);
|
|
}
|
|
|
|
public function destroy(Request $request, Post $post): JsonResponse
|
|
{
|
|
$reason = $request->input('reason');
|
|
$reasonText = $request->input('reason_text');
|
|
app(AuditLogger::class)->log($request, 'post.deleted', $post, [
|
|
'thread_id' => $post->thread_id,
|
|
'reason' => $reason,
|
|
'reason_text' => $reasonText,
|
|
]);
|
|
$post->deleted_by = $request->user()?->id;
|
|
$post->save();
|
|
$post->delete();
|
|
|
|
return response()->json(null, 204);
|
|
}
|
|
|
|
public function update(Request $request, Post $post): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
if (!$user) {
|
|
return response()->json(['message' => 'Unauthorized.'], 401);
|
|
}
|
|
|
|
$isAdmin = $user->roles()->where('name', 'ROLE_ADMIN')->exists();
|
|
if (!$isAdmin && $post->user_id !== $user->id) {
|
|
return response()->json(['message' => 'Not authorized to edit posts.'], 403);
|
|
}
|
|
|
|
$data = $request->validate([
|
|
'body' => ['required', 'string'],
|
|
]);
|
|
|
|
$post->body = $data['body'];
|
|
$post->save();
|
|
$post->refresh();
|
|
|
|
app(AuditLogger::class)->log($request, 'post.edited', $post, [
|
|
'thread_id' => $post->thread_id,
|
|
]);
|
|
|
|
$post->loadMissing([
|
|
'user' => fn ($query) => $query
|
|
->withCount(['posts', 'threads', 'thanksGiven', 'thanksReceived'])
|
|
->with(['rank', 'roles']),
|
|
'attachments.extension',
|
|
'attachments.group',
|
|
]);
|
|
|
|
return response()->json($this->serializePost($post));
|
|
}
|
|
|
|
private function parseIriId(?string $value): ?int
|
|
{
|
|
if (!$value) {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('#/threads/(\d+)$#', $value, $matches)) {
|
|
return (int) $matches[1];
|
|
}
|
|
|
|
if (is_numeric($value)) {
|
|
return (int) $value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function serializePost(Post $post): array
|
|
{
|
|
$attachments = $post->relationLoaded('attachments') ? $post->attachments : collect();
|
|
$bodyHtml = $this->renderBody($post->body, $attachments);
|
|
return [
|
|
'id' => $post->id,
|
|
'body' => $post->body,
|
|
'body_html' => $bodyHtml,
|
|
'thread' => "/api/threads/{$post->thread_id}",
|
|
'user_id' => $post->user_id,
|
|
'user_name' => $post->user?->name,
|
|
'user_avatar_url' => $post->user?->avatar_path
|
|
? Storage::url($post->user->avatar_path)
|
|
: null,
|
|
'user_posts_count' => ($post->user?->posts_count ?? 0) + ($post->user?->threads_count ?? 0),
|
|
'user_created_at' => $post->user?->created_at?->toIso8601String(),
|
|
'user_location' => $post->user?->location,
|
|
'user_thanks_given_count' => $post->user?->thanks_given_count ?? 0,
|
|
'user_thanks_received_count' => $post->user?->thanks_received_count ?? 0,
|
|
'user_rank_name' => $post->user?->rank?->name,
|
|
'user_rank_badge_type' => $post->user?->rank?->badge_type,
|
|
'user_rank_badge_text' => $post->user?->rank?->badge_text,
|
|
'user_rank_badge_url' => $post->user?->rank?->badge_image_path
|
|
? Storage::url($post->user->rank->badge_image_path)
|
|
: null,
|
|
'user_rank_color' => $post->user?->rank?->color,
|
|
'user_group_color' => $this->resolveGroupColor($post->user),
|
|
'created_at' => $post->created_at?->toIso8601String(),
|
|
'updated_at' => $post->updated_at?->toIso8601String(),
|
|
'attachments' => $post->relationLoaded('attachments')
|
|
? $attachments
|
|
->map(fn ($attachment) => [
|
|
'id' => $attachment->id,
|
|
'group' => $attachment->group ? [
|
|
'id' => $attachment->group->id,
|
|
'name' => $attachment->group->name,
|
|
] : null,
|
|
'original_name' => $attachment->original_name,
|
|
'extension' => $attachment->extension,
|
|
'mime_type' => $attachment->mime_type,
|
|
'size_bytes' => $attachment->size_bytes,
|
|
'download_url' => "/api/attachments/{$attachment->id}/download",
|
|
'thumbnail_url' => $attachment->thumbnail_path
|
|
? "/api/attachments/{$attachment->id}/thumbnail"
|
|
: null,
|
|
'is_image' => str_starts_with((string) $attachment->mime_type, 'image/'),
|
|
'created_at' => $attachment->created_at?->toIso8601String(),
|
|
])
|
|
->values()
|
|
: [],
|
|
];
|
|
}
|
|
|
|
private function renderBody(string $body, $attachments): string
|
|
{
|
|
$replaced = $this->replaceAttachmentTags($body, $attachments);
|
|
return BbcodeFormatter::format($replaced);
|
|
}
|
|
|
|
private function replaceAttachmentTags(string $body, $attachments): string
|
|
{
|
|
if (!$attachments || count($attachments) === 0) {
|
|
return $body;
|
|
}
|
|
|
|
$map = [];
|
|
foreach ($attachments as $attachment) {
|
|
$name = strtolower($attachment->original_name ?? '');
|
|
if ($name !== '') {
|
|
$map[$name] = [
|
|
'url' => "/api/attachments/{$attachment->id}/download",
|
|
'mime' => $attachment->mime_type ?? '',
|
|
'thumb' => $attachment->thumbnail_path
|
|
? "/api/attachments/{$attachment->id}/thumbnail"
|
|
: null,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!$map) {
|
|
return $body;
|
|
}
|
|
|
|
return preg_replace_callback('/\\[attachment\\](.+?)\\[\\/attachment\\]/i', function ($matches) use ($map) {
|
|
$rawName = trim($matches[1]);
|
|
$key = strtolower($rawName);
|
|
if (!array_key_exists($key, $map)) {
|
|
return $matches[0];
|
|
}
|
|
$entry = $map[$key];
|
|
$url = $entry['url'];
|
|
$mime = $entry['mime'] ?? '';
|
|
if (str_starts_with($mime, 'image/') && $this->displayImagesInline()) {
|
|
if (!empty($entry['thumb'])) {
|
|
$thumb = $entry['thumb'];
|
|
return "[url={$url}][img]{$thumb}[/img][/url]";
|
|
}
|
|
return "[img]{$url}[/img]";
|
|
}
|
|
return "[url={$url}]{$rawName}[/url]";
|
|
}, $body) ?? $body;
|
|
}
|
|
|
|
private function displayImagesInline(): bool
|
|
{
|
|
$value = Setting::query()->where('key', 'attachments.display_images_inline')->value('value');
|
|
if ($value === null) {
|
|
return true;
|
|
}
|
|
return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
|
|
}
|
|
|
|
private function resolveGroupColor(?\App\Models\User $user): ?string
|
|
{
|
|
if (!$user) {
|
|
return null;
|
|
}
|
|
|
|
$roles = $user->roles;
|
|
if (!$roles) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($roles->sortBy('name') as $role) {
|
|
if (!empty($role->color)) {
|
|
return $role->color;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|