diff --git a/app/Console/Commands/ForumConvertPosts.php b/app/Console/Commands/ForumConvertPosts.php
new file mode 100644
index 00000000..854e53eb
--- /dev/null
+++ b/app/Console/Commands/ForumConvertPosts.php
@@ -0,0 +1,78 @@
+option('dry-run');
+ $chunk = (int)$this->option('chunk');
+ $limit = $this->option('limit') ? (int)$this->option('limit') : null;
+
+ $query = ForumPost::query()->orderBy('id');
+ $total = $limit ? min($query->count(), $limit) : $query->count();
+
+ $this->info('Converting forum posts (dry-run='.($dry ? 'yes' : 'no').')');
+ $this->info("Total posts to consider: {$total}");
+
+ $bar = $this->output->createProgressBar($total);
+ $bar->start();
+
+ $converter = new BbcodeConverter();
+ $processed = 0;
+ $changed = 0;
+
+ try {
+ $query->chunkById($chunk, function ($posts) use (&$bar, &$processed, &$changed, $dry, $limit, $converter) {
+ foreach ($posts as $post) {
+ if ($limit !== null && $processed >= $limit) {
+ throw new \RuntimeException('limit_reached');
+ }
+ $bar->advance();
+ $processed++;
+
+ $old = $post->content ?? '';
+ $new = $converter->convert($old);
+
+ if ($old === $new) {
+ continue;
+ }
+
+ $changed++;
+ if ($dry) {
+ $this->line('[dry] would update post ' . $post->id);
+ continue;
+ }
+
+ $post->content = $new;
+ $post->save();
+ }
+ });
+ } catch (\RuntimeException $e) {
+ if ($e->getMessage() !== 'limit_reached') {
+ throw $e;
+ }
+ // intentionally stop chunking when limit reached
+ }
+
+ $bar->finish();
+ $this->line('');
+
+ $this->info("Processed: {$processed} posts. Changed: {$changed} posts.");
+
+ if ($this->option('report')) {
+ $this->info('Conversion complete');
+ }
+
+ return 0;
+ }
+}
diff --git a/app/Console/Commands/ForumMigrateOld.php b/app/Console/Commands/ForumMigrateOld.php
new file mode 100644
index 00000000..18e0ead6
--- /dev/null
+++ b/app/Console/Commands/ForumMigrateOld.php
@@ -0,0 +1,388 @@
+logPath = storage_path('logs/forum_migration.log');
+ }
+
+ public function handle(): int
+ {
+ $this->info('Starting forum migration');
+ $this->log('Starting forum migration');
+
+ $dry = $this->option('dry-run');
+ $only = $this->option('only');
+ $chunk = (int)$this->option('chunk');
+
+ try {
+ if (!$only || $only === 'categories') {
+ $this->migrateCategories($dry);
+ }
+
+ if (!$only || $only === 'threads') {
+ $this->migrateThreads($dry, $chunk);
+ }
+
+ if (!$only || $only === 'posts') {
+ $this->migratePosts($dry, $chunk);
+ }
+
+ if (!$only || $only === 'gallery') {
+ $this->migrateGallery($dry, $chunk);
+ }
+
+ if ($this->option('report')) {
+ $this->generateReport();
+ }
+
+ $this->info('Forum migration finished');
+ $this->log('Forum migration finished');
+ return 0;
+ } catch (Exception $e) {
+ $this->error('Migration failed: ' . $e->getMessage());
+ $this->log('Migration failed: ' . $e->getMessage());
+ return 1;
+ }
+ }
+
+ protected function migrateCategories(bool $dry)
+ {
+ $this->info('Migrating categories');
+ $legacy = DB::connection('legacy');
+
+ $roots = $legacy->table('forum_topics')
+ ->select('root_id')
+ ->distinct()
+ ->where('root_id', '>', 0)
+ ->pluck('root_id');
+
+ $this->info('Found ' . $roots->count() . ' legacy root ids');
+
+ foreach ($roots as $rootId) {
+ $row = $legacy->table('forum_topics')->where('topic_id', $rootId)->first();
+ $name = $row->topic ?? 'Category ' . $rootId;
+ $slug = Str::slug(substr($name, 0, 150));
+
+ $this->line("-> root {$rootId}: {$name}");
+
+ if ($dry) {
+ $this->log("[dry] create category {$name} ({$slug})");
+ continue;
+ }
+
+ ForumCategory::updateOrCreate(
+ ['id' => $rootId],
+ ['name' => $name, 'slug' => $slug]
+ );
+ }
+
+ $this->info('Categories migrated');
+ }
+
+ protected function migrateThreads(bool $dry, int $chunk)
+ {
+ $this->info('Migrating threads');
+ $legacy = DB::connection('legacy');
+
+ $query = $legacy->table('forum_topics')->orderBy('topic_id');
+
+ $total = $query->count();
+ $this->info("Total threads to process: {$total}");
+
+ $bar = $this->output->createProgressBar($total);
+ $bar->start();
+
+ // chunk by legacy primary key `topic_id`
+ $query->chunkById($chunk, function ($rows) use ($dry, $bar) {
+ foreach ($rows as $r) {
+ $bar->advance();
+
+ $data = [
+ 'id' => $r->topic_id,
+ 'category_id' => $this->resolveCategoryId($r->root_id ?? null, $r->topic_id),
+ // resolve user id or assign to system user (1) when missing or not found
+ 'user_id' => $this->resolveUserId($r->user_id ?? null),
+ 'title' => $r->topic,
+ 'slug' => $this->uniqueSlug(Str::slug(substr($r->topic,0,200)) ?: 'thread-'.$r->topic_id, $r->topic_id),
+ 'content' => $r->preview ?? '',
+ 'views' => $r->views ?? 0,
+ 'is_locked' => isset($r->open) ? !((bool)$r->open) : false,
+ 'is_pinned' => false,
+ 'visibility' => $this->mapPrivilegeToVisibility($r->privilege ?? 0),
+ 'last_post_at' => $this->normalizeDate($r->last_update ?? null),
+ ];
+
+ if ($dry) {
+ $this->log('[dry] thread: ' . $r->topic_id . ' - ' . $r->topic);
+ continue;
+ }
+
+ ForumThread::updateOrCreate(['id' => $data['id']], $data);
+ }
+ }, 'topic_id');
+
+ $bar->finish();
+ $this->line('');
+ $this->info('Threads migrated');
+ }
+
+ protected function migratePosts(bool $dry, int $chunk)
+ {
+ $this->info('Migrating posts');
+ $legacy = DB::connection('legacy');
+
+ $query = $legacy->table('forum_posts')->orderBy('post_id');
+ $total = $query->count();
+ $this->info("Total posts to process: {$total}");
+
+ $bar = $this->output->createProgressBar($total);
+ $bar->start();
+
+ // legacy forum_posts uses `post_id` as primary key
+ $query->chunkById($chunk, function ($rows) use ($dry, $bar) {
+ foreach ($rows as $r) {
+ $bar->advance();
+
+ $data = [
+ 'id' => $r->post_id,
+ 'thread_id' => $r->topic_id,
+ 'user_id' => $r->user_id ?? null,
+ 'content' => $this->convertLegacyMessage($r->message ?? ''),
+ 'is_edited' => isset($r->isupdated) ? (bool)$r->isupdated : false,
+ 'edited_at' => $r->updated ?? null,
+ ];
+
+ if ($dry) {
+ $this->log('[dry] post: ' . $r->post_id);
+ continue;
+ }
+
+ ForumPost::updateOrCreate(['id' => $data['id']], $data);
+ }
+ }, 'post_id');
+
+ $bar->finish();
+ $this->line('');
+ $this->info('Posts migrated');
+ }
+
+ protected function mapPrivilegeToVisibility($priv)
+ {
+ // legacy privilege: 0 public, 1 members, 4 staff? adjust mapping conservatively
+ if ($priv >= 4) return 'staff';
+ if ($priv >= 1) return 'members';
+ return 'public';
+ }
+
+ protected function normalizeDate($val)
+ {
+ if (empty($val)) return null;
+ $s = trim((string)$val);
+ // legacy sometimes contains sentinel invalid dates like -0001-11-30 or zero dates
+ if (strpos($s, '-0001') !== false) return null;
+ if (strpos($s, '0000-00-00') !== false) return null;
+ if (strtotime($s) === false) return null;
+ return date('Y-m-d H:i:s', strtotime($s));
+ }
+
+ protected function uniqueSlug(string $base, int $id)
+ {
+ $slug = $base;
+ $i = 0;
+ while (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) {
+ $i++;
+ $slug = $base . '-' . $id;
+ // if somehow still exists, append counter
+ if (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) {
+ $slug = $base . '-' . $id . '-' . $i;
+ }
+ }
+ return $slug;
+ }
+
+ protected function resolveCategoryId($rootId, $topicId)
+ {
+ // prefer explicit rootId
+ if (!empty($rootId)) {
+ // ensure category exists
+ if (ForumCategory::where('id', $rootId)->exists()) return $rootId;
+ }
+
+ // if this topic itself is a category
+ if (ForumCategory::where('id', $topicId)->exists()) return $topicId;
+
+ // fallback: use first available category
+ $first = ForumCategory::first();
+ if ($first) return $first->id;
+
+ // as last resort, create Uncategorized
+ $cat = ForumCategory::create(['name' => 'Uncategorized', 'slug' => 'uncategorized']);
+ return $cat->id;
+ }
+
+ protected function resolveUserId($userId)
+ {
+ if (empty($userId)) {
+ return 1;
+ }
+
+ // check users table in default connection
+ if (\DB::table('users')->where('id', $userId)->exists()) {
+ return $userId;
+ }
+
+ return 1; // fallback system user
+ }
+
+ protected function convertLegacyMessage($msg)
+ {
+ $converter = new BbcodeConverter();
+ return $converter->convert($msg);
+ }
+
+ protected function generateReport()
+ {
+ $this->info('Generating migration report');
+ $legacy = DB::connection('legacy');
+
+ $legacyCounts = [
+ 'categories' => $legacy->table('forum_topics')->where('root_id','>',0)->distinct('root_id')->count('root_id'),
+ 'threads' => $legacy->table('forum_topics')->count(),
+ 'posts' => $legacy->table('forum_posts')->count(),
+ ];
+
+ $newCounts = [
+ 'categories' => ForumCategory::count(),
+ 'threads' => ForumThread::count(),
+ 'posts' => ForumPost::count(),
+ 'attachments' => \DB::table('forum_attachments')->count(),
+ ];
+
+ $this->info('Legacy counts: ' . json_encode($legacyCounts));
+ $this->info('New counts: ' . json_encode($newCounts));
+ $this->log('Report: legacy=' . json_encode($legacyCounts) . ' new=' . json_encode($newCounts));
+ }
+
+ protected function log(string $msg)
+ {
+ $line = '[' . date('c') . '] ' . $msg . "\n";
+ file_put_contents($this->logPath, $line, FILE_APPEND | LOCK_EX);
+ }
+
+ protected function migrateGallery(bool $dry, int $chunk)
+ {
+ $this->info('Migrating gallery (forum_topics_gallery → forum_attachments)');
+ $legacy = DB::connection('legacy');
+
+ if (!$legacy->getSchemaBuilder()->hasTable('forum_topics_gallery')) {
+ $this->info('No legacy forum_topics_gallery table found, skipping');
+ return;
+ }
+
+ $query = $legacy->table('forum_topics_gallery')->orderBy('id');
+ $total = $query->count();
+ $this->info("Total gallery items to process: {$total}");
+
+ $bar = $this->output->createProgressBar($total);
+ $bar->start();
+
+ $query->chunkById($chunk, function ($rows) use ($dry, $bar) {
+ foreach ($rows as $r) {
+ $bar->advance();
+
+ // expected legacy fields: id, name, category (topic id), folder, datum, description
+ $topicId = $r->category ?? ($r->topic_id ?? null);
+ $fileName = $r->name ?? null;
+ if (empty($topicId) || empty($fileName)) {
+ $this->log('Skipping gallery row with missing topic or name: ' . json_encode($r));
+ continue;
+ }
+
+ $nid = floor($topicId / 100);
+ $relativePath = "files/news/{$nid}/{$topicId}/{$fileName}";
+ $publicPath = public_path($relativePath);
+
+ $fileSize = null;
+ $mimeType = null;
+ $width = null;
+ $height = null;
+
+ if (file_exists($publicPath)) {
+ $fileSize = filesize($publicPath);
+ $img = @getimagesize($publicPath);
+ if ($img !== false) {
+ $width = $img[0];
+ $height = $img[1];
+ $mimeType = $img['mime'] ?? null;
+ } else {
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mimeType = finfo_file($finfo, $publicPath);
+ finfo_close($finfo);
+ }
+ }
+
+ // find legacy first post id for this topic
+ $legacy = DB::connection('legacy');
+ $firstPostId = $legacy->table('forum_posts')
+ ->where('topic_id', $topicId)
+ ->orderBy('post_date')
+ ->value('post_id');
+
+ // map to new forum_posts id (we preserved ids when migrating)
+ $postId = null;
+ if ($firstPostId && \App\Models\ForumPost::where('id', $firstPostId)->exists()) {
+ $postId = $firstPostId;
+ } else {
+ // fallback: find any post in new DB for thread
+ $post = \App\Models\ForumPost::where('thread_id', $topicId)->orderBy('created_at')->first();
+ if ($post) $postId = $post->id;
+ }
+
+ if (empty($postId)) {
+ $this->log('No target post found for gallery item: topic ' . $topicId . ' file ' . $fileName);
+ continue;
+ }
+
+ if ($dry) {
+ $this->log("[dry] attach {$relativePath} -> post {$postId}");
+ continue;
+ }
+
+ \App\Models\ForumAttachment::create([
+ 'post_id' => $postId,
+ 'file_path' => $relativePath,
+ 'file_size' => $fileSize ?? 0,
+ 'mime_type' => $mimeType,
+ 'width' => $width,
+ 'height' => $height,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+ }, 'id');
+
+ $bar->finish();
+ $this->line('');
+ $this->info('Gallery migrated');
+ }
+}
diff --git a/app/Http/Controllers/ArtworkController.php b/app/Http/Controllers/ArtworkController.php
index f7e8267a..408898bd 100644
--- a/app/Http/Controllers/ArtworkController.php
+++ b/app/Http/Controllers/ArtworkController.php
@@ -1,13 +1,12 @@
first();
}
+ // When the URL can represent a nested category path (e.g. /skins/audio/winamp),
+ // prefer category rendering over artwork slug collisions so same-level groups
+ // behave consistently.
+ if (! empty($artworkSlug)) {
+ $combinedPath = trim($categoryPath . '/' . $artworkSlug, '/');
+ $resolvedCategory = Category::findByPath($contentTypeSlug, $combinedPath);
+ if ($resolvedCategory) {
+ return app(BrowseGalleryController::class)->content(request(), $contentTypeSlug, $combinedPath);
+ }
+ }
+
// If no artwork was found, treat the request as a category path.
- // The route places the artwork slug in the last segment, so include it
- // when forwarding to CategoryPageController to support arbitrary-depth paths
+ // The route places the artwork slug in the last segment, so include it.
+ // Delegate to BrowseGalleryController to render the same modern gallery
+ // layout used by routes like /skins/audio.
if (! $foundArtwork) {
$combinedPath = $categoryPath;
if ($artworkSlug) {
$combinedPath = trim($categoryPath . '/' . $artworkSlug, '/');
}
- return app(CategoryPageController::class)->show(request(), $contentTypeSlug, $combinedPath);
+ return app(BrowseGalleryController::class)->content(request(), $contentTypeSlug, $combinedPath);
}
if (! $foundArtwork->is_public || ! $foundArtwork->is_approved || $foundArtwork->trashed()) {
diff --git a/app/Http/Controllers/Community/ChatController.php b/app/Http/Controllers/Community/ChatController.php
index 7cfff4bd..d8919bb1 100644
--- a/app/Http/Controllers/Community/ChatController.php
+++ b/app/Http/Controllers/Community/ChatController.php
@@ -41,4 +41,40 @@ class ChatController extends Controller
return view('community.chat', compact('page_title', 'adHtml', 'chatHtml', 'smileys'));
}
+
+ /**
+ * Handle legacy AJAX chat posts from old JS.
+ */
+ public function post(Request $request)
+ {
+ $message = $request->input('message') ?? $request->input('chat_txt') ?? null;
+ if (empty($message)) {
+ return response()->json(['ok' => false, 'error' => 'empty_message'], 400);
+ }
+
+ // Ensure legacy $_SESSION keys exist for Chat class (best-effort sync from Laravel session/auth)
+ if (empty($_SESSION['web_login']['user_id'])) {
+ $webLogin = session('web_login');
+ if ($webLogin && isset($webLogin['user_id'])) {
+ $_SESSION['web_login'] = $webLogin;
+ } elseif (auth()->check()) {
+ $user = auth()->user();
+ $_SESSION['web_login'] = [
+ 'user_id' => $user->id,
+ 'username' => $user->username ?? $user->name ?? null,
+ 'status' => true,
+ ];
+ }
+ }
+
+ $chat = new \App\Chat();
+ try {
+ $chat->StoreMessage($message);
+ $chat->UpdateChatFile('cron/chat_log.txt', 50);
+ } catch (\Throwable $e) {
+ return response()->json(['ok' => false, 'error' => 'store_failed', 'message' => $e->getMessage()], 500);
+ }
+
+ return response()->json(['ok' => true]);
+ }
}
diff --git a/app/Http/Controllers/Community/ForumController.php b/app/Http/Controllers/Community/ForumController.php
index 5ec82a4f..badca315 100644
--- a/app/Http/Controllers/Community/ForumController.php
+++ b/app/Http/Controllers/Community/ForumController.php
@@ -18,13 +18,76 @@ class ForumController extends Controller
public function index()
{
$data = $this->legacy->forumIndex();
+
+ if (empty($data['topics']) || count($data['topics']) === 0) {
+ try {
+ $categories = \App\Models\ForumCategory::query()
+ ->withCount(['threads as num_subtopics'])
+ ->orderBy('position')
+ ->orderBy('id')
+ ->get();
+
+ $topics = $categories->map(function ($category) {
+ $threadIds = \App\Models\ForumThread::where('category_id', $category->id)->pluck('id');
+
+ return (object) [
+ 'topic_id' => $category->id,
+ 'topic' => $category->name,
+ 'discuss' => null,
+ 'last_update' => \App\Models\ForumThread::where('category_id', $category->id)->max('last_post_at'),
+ 'num_posts' => $threadIds->isEmpty() ? 0 : \App\Models\ForumPost::whereIn('thread_id', $threadIds)->count(),
+ 'num_subtopics' => (int) ($category->num_subtopics ?? 0),
+ ];
+ });
+
+ $data['topics'] = $topics;
+ } catch (\Throwable $e) {
+ // keep legacy response
+ }
+ }
+
return view('community.forum.index', $data);
}
- public function topic(Request $request, $topic_id)
+ public function topic(Request $request, $topic_id, $slug = null)
{
+ // Redirect to canonical slug when possible
+ try {
+ $thread = \App\Models\ForumThread::find((int) $topic_id);
+ if ($thread && !empty($thread->slug)) {
+ $correct = $thread->slug;
+ if ($slug !== $correct) {
+ $qs = $request->getQueryString();
+ $url = route('legacy.forum.topic', ['topic_id' => $topic_id, 'slug' => $correct]);
+ if ($qs) $url .= '?' . $qs;
+ return redirect($url, 301);
+ }
+ }
+ } catch (\Throwable $e) {
+ // ignore
+ }
+
+
$data = $this->legacy->forumTopic((int) $topic_id, (int) $request->query('page', 1));
+ if (! $data) {
+ // fallback to new forum tables if migration already ran
+ try {
+ $thread = \App\Models\ForumThread::with(['posts.user'])->find((int) $topic_id);
+ if ($thread) {
+ $posts = \App\Models\ForumPost::where('thread_id', $thread->id)->orderBy('created_at')->get();
+ $data = [
+ 'type' => 'posts',
+ 'thread' => $thread,
+ 'posts' => $posts,
+ 'page_title' => $thread->title ?? 'Forum',
+ ];
+ }
+ } catch (\Throwable $e) {
+ // ignore and fall through to placeholder
+ }
+ }
+
if (! $data) {
return view('shared.placeholder');
}
diff --git a/app/Http/Controllers/Community/NewsController.php b/app/Http/Controllers/Community/NewsController.php
index cd21418f..b9d089a5 100644
--- a/app/Http/Controllers/Community/NewsController.php
+++ b/app/Http/Controllers/Community/NewsController.php
@@ -26,6 +26,19 @@ class NewsController extends Controller
return redirect('/');
}
+ // redirect to canonical slug for SEO if available
+ try {
+ $correct = \Illuminate\Support\Str::slug($news->headline ?? 'news-' . $id);
+ if ($slug !== $correct) {
+ $qs = $request->getQueryString();
+ $url = route('legacy.news.show', ['id' => $id, 'slug' => $correct]);
+ if ($qs) $url .= '?' . $qs;
+ return redirect($url, 301);
+ }
+ } catch (\Throwable $e) {
+ // ignore
+ }
+
try {
$comments = DB::table('news_comment as c')
->leftJoin('users as u', 'c.user_id', '=', 'u.user_id')
diff --git a/app/Http/Controllers/Web/ArtController.php b/app/Http/Controllers/Web/ArtController.php
index c0fd545f..f9991fa2 100644
--- a/app/Http/Controllers/Web/ArtController.php
+++ b/app/Http/Controllers/Web/ArtController.php
@@ -18,6 +18,30 @@ class ArtController extends Controller
public function show(Request $request, $id, $slug = null)
{
+ // canonicalize to new artwork route when possible
+ try {
+ $art = \App\Models\Artwork::find((int)$id);
+ if ($art && !empty($art->slug)) {
+ if ($slug !== $art->slug) {
+ // attempt to derive contentType and category for route
+ $category = $art->categories()->with('contentType')->first();
+ if ($category && $category->contentType) {
+ $contentTypeSlug = $category->contentType->slug ?? 'other';
+ $categoryPath = $category->slug ?? $category->category_name ?? 'other';
+ return redirect(route('artworks.show', [
+ 'contentTypeSlug' => $contentTypeSlug,
+ 'categoryPath' => $categoryPath,
+ 'artwork' => $art->slug,
+ ]), 301);
+ } elseif (!empty($art->slug)) {
+ // fallback: redirect to artwork slug only (may be handled by router)
+ return redirect('/' . $art->slug, 301);
+ }
+ }
+ }
+ } catch (\Throwable $e) {
+ // ignore and continue rendering legacy view
+ }
if ($request->isMethod('post') && $request->input('action') === 'store_comment') {
if (auth()->check()) {
try {
diff --git a/app/Http/Controllers/Web/BrowseGalleryController.php b/app/Http/Controllers/Web/BrowseGalleryController.php
index 1660ed4e..942576fc 100644
--- a/app/Http/Controllers/Web/BrowseGalleryController.php
+++ b/app/Http/Controllers/Web/BrowseGalleryController.php
@@ -4,10 +4,10 @@ namespace App\Http\Controllers\Web;
use App\Models\Category;
use App\Models\ContentType;
+use App\Models\Artwork;
use App\Services\ArtworkService;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
-use App\Http\Controllers\ArtworkController as ArtworkControllerAlias;
class BrowseGalleryController extends \App\Http\Controllers\Controller
{
@@ -120,13 +120,32 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
]);
}
- public function showArtwork(Request $request, string $contentTypeSlug, string $categoryPath, string $artwork)
+ public function showArtwork(...$params)
{
- return app(\App\Http\Controllers\ArtController::class)->show(
- $request,
- strtolower($contentTypeSlug),
- trim($categoryPath, '/'),
- $artwork
+ $req = request();
+ $pathSegments = array_values(array_filter(explode('/', trim($req->path(), '/'))));
+
+ $contentTypeSlug = $params[0] ?? ($pathSegments[0] ?? null);
+ $categoryPath = $params[1] ?? null;
+ $artwork = $params[2] ?? null;
+
+ // If artwork wasn't provided (some route invocations supply fewer args),
+ // derive it from the request path's last segment.
+ if ($artwork === null) {
+ $artwork = end($pathSegments) ?: null;
+ }
+
+ $contentTypeSlug = strtolower((string) $contentTypeSlug);
+ $categoryPath = $categoryPath !== null ? trim((string) $categoryPath, '/') : (isset($pathSegments[1]) ? implode('/', array_slice($pathSegments, 1, max(0, count($pathSegments) - 2))) : '');
+
+ // Normalize artwork param if route-model binding returned an Artwork model
+ $artworkSlug = $artwork instanceof Artwork ? (string) $artwork->slug : (string) $artwork;
+
+ return app(\App\Http\Controllers\ArtworkController::class)->show(
+ $req,
+ $contentTypeSlug,
+ $categoryPath,
+ $artworkSlug
);
}
diff --git a/app/Http/Controllers/Web/GalleryController.php b/app/Http/Controllers/Web/GalleryController.php
index 87945079..16cd160d 100644
--- a/app/Http/Controllers/Web/GalleryController.php
+++ b/app/Http/Controllers/Web/GalleryController.php
@@ -17,6 +17,19 @@ class GalleryController extends Controller
abort(404);
}
+ // canonicalize username in URL when possible
+ try {
+ $correctName = $user->name ?? $user->uname ?? null;
+ if ($username && $correctName && $username !== $correctName) {
+ $qs = $request->getQueryString();
+ $url = route('legacy.gallery', ['id' => $user->id, 'username' => $correctName]);
+ if ($qs) $url .= '?' . $qs;
+ return redirect($url, 301);
+ }
+ } catch (\Throwable $e) {
+ // ignore
+ }
+
$page = max(1, (int) $request->query('page', 1));
$hits = 20;
diff --git a/app/Http/Controllers/Web/HomeController.php b/app/Http/Controllers/Web/HomeController.php
index 19f02566..d09f983a 100644
--- a/app/Http/Controllers/Web/HomeController.php
+++ b/app/Http/Controllers/Web/HomeController.php
@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\ArtworkService;
use Illuminate\Support\Facades\DB;
+use Illuminate\Database\QueryException;
+use Illuminate\Support\Facades\Log;
class HomeController extends Controller
{
@@ -36,34 +38,49 @@ class HomeController extends Controller
$latestUploads = $this->artworks->getLatestArtworks(20);
// Forum news (root forum section id 2876)
- $forumNews = DB::table('forum_topics as t1')
- ->leftJoin('users as u', 't1.user_id', '=', 'u.user_id')
- ->select('t1.topic_id', 't1.topic', 'u.uname', 't1.post_date', 't1.preview')
- ->where('t1.root_id', 2876)
- ->where('t1.privilege', '<', 4)
- ->orderBy('t1.post_date', 'desc')
- ->limit(8)
- ->get();
+ try {
+ $forumNews = DB::table('forum_topics as t1')
+ ->leftJoin('users as u', 't1.user_id', '=', 'u.user_id')
+ ->select('t1.topic_id', 't1.topic', 'u.uname', 't1.post_date', 't1.preview')
+ ->where('t1.root_id', 2876)
+ ->where('t1.privilege', '<', 4)
+ ->orderBy('t1.post_date', 'desc')
+ ->limit(8)
+ ->get();
+ } catch (QueryException $e) {
+ Log::warning('Forum topics table missing or DB error when loading forum news', ['exception' => $e->getMessage()]);
+ $forumNews = collect();
+ }
// Our news (latest site news)
- $ourNews = DB::table('news as t1')
- ->join('news_categories as c', 't1.category_id', '=', 'c.category_id')
- ->join('users as u', 't1.user_id', '=', 'u.user_id')
- ->selectRaw('t1.news_id, t1.headline, t1.user_id, t1.picture, t1.preview, u.uname, t1.create_date, t1.views, c.category_name, (SELECT COUNT(*) FROM news_comments WHERE news_id = t1.news_id) AS num_comments')
- ->orderBy('t1.create_date', 'desc')
- ->limit(5)
- ->get();
+ try {
+ $ourNews = DB::table('news as t1')
+ ->join('news_categories as c', 't1.category_id', '=', 'c.category_id')
+ ->join('users as u', 't1.user_id', '=', 'u.user_id')
+ ->selectRaw('t1.news_id, t1.headline, t1.user_id, t1.picture, t1.preview, u.uname, t1.create_date, t1.views, c.category_name, (SELECT COUNT(*) FROM news_comments WHERE news_id = t1.news_id) AS num_comments')
+ ->orderBy('t1.create_date', 'desc')
+ ->limit(5)
+ ->get();
+ } catch (QueryException $e) {
+ Log::warning('News table missing or DB error when loading our news', ['exception' => $e->getMessage()]);
+ $ourNews = collect();
+ }
// Latest forum activity (exclude rootless and news root)
- $latestForumActivity = DB::table('forum_topics as t1')
- ->selectRaw('t1.topic_id, t1.topic, (SELECT COUNT(*) FROM forum_posts WHERE topic_id = t1.topic_id) AS numPosts')
- ->where('t1.root_id', '<>', 0)
- ->where('t1.root_id', '<>', 2876)
- ->where('t1.privilege', '<', 4)
- ->orderBy('t1.last_update', 'desc')
- ->orderBy('t1.post_date', 'desc')
- ->limit(10)
- ->get();
+ try {
+ $latestForumActivity = DB::table('forum_topics as t1')
+ ->selectRaw('t1.topic_id, t1.topic, (SELECT COUNT(*) FROM forum_posts WHERE topic_id = t1.topic_id) AS numPosts')
+ ->where('t1.root_id', '<>', 0)
+ ->where('t1.root_id', '<>', 2876)
+ ->where('t1.privilege', '<', 4)
+ ->orderBy('t1.last_update', 'desc')
+ ->orderBy('t1.post_date', 'desc')
+ ->limit(10)
+ ->get();
+ } catch (QueryException $e) {
+ Log::warning('Forum topics table missing or DB error when loading latest forum activity', ['exception' => $e->getMessage()]);
+ $latestForumActivity = collect();
+ }
return view('web.home', compact(
'page_title',
diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php
new file mode 100644
index 00000000..bff8757d
--- /dev/null
+++ b/app/Http/Middleware/VerifyCsrfToken.php
@@ -0,0 +1,18 @@
+
+ */
+ protected $except = [
+ 'chat_post',
+ 'chat_post/*',
+ ];
+}
diff --git a/app/Models/ForumAttachment.php b/app/Models/ForumAttachment.php
new file mode 100644
index 00000000..30537604
--- /dev/null
+++ b/app/Models/ForumAttachment.php
@@ -0,0 +1,21 @@
+belongsTo(ForumPost::class, 'post_id');
+ }
+}
diff --git a/app/Models/ForumCategory.php b/app/Models/ForumCategory.php
new file mode 100644
index 00000000..8b06c75f
--- /dev/null
+++ b/app/Models/ForumCategory.php
@@ -0,0 +1,26 @@
+belongsTo(ForumCategory::class, 'parent_id');
+ }
+
+ public function threads()
+ {
+ return $this->hasMany(ForumThread::class, 'category_id');
+ }
+}
diff --git a/app/Models/ForumPost.php b/app/Models/ForumPost.php
new file mode 100644
index 00000000..ce989c09
--- /dev/null
+++ b/app/Models/ForumPost.php
@@ -0,0 +1,33 @@
+ 'datetime',
+ ];
+
+ public function thread()
+ {
+ return $this->belongsTo(ForumThread::class, 'thread_id');
+ }
+
+ public function attachments()
+ {
+ return $this->hasMany(ForumAttachment::class, 'post_id');
+ }
+}
diff --git a/app/Models/ForumThread.php b/app/Models/ForumThread.php
new file mode 100644
index 00000000..0685bcc1
--- /dev/null
+++ b/app/Models/ForumThread.php
@@ -0,0 +1,33 @@
+ 'datetime',
+ ];
+
+ public function category()
+ {
+ return $this->belongsTo(ForumCategory::class, 'category_id');
+ }
+
+ public function posts()
+ {
+ return $this->hasMany(ForumPost::class, 'thread_id');
+ }
+}
diff --git a/app/Services/BbcodeConverter.php b/app/Services/BbcodeConverter.php
new file mode 100644
index 00000000..11252640
--- /dev/null
+++ b/app/Services/BbcodeConverter.php
@@ -0,0 +1,103 @@
+' . htmlspecialchars($m[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '';
+ return "__CODEBLOCK_{$idx}__";
+ }, $text);
+
+ // Escape remaining text to avoid XSS
+ $text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+
+ // Basic tags
+ $simple = [
+ '/\[b\](.*?)\[\/b\]/is' => '$1',
+ '/\[i\](.*?)\[\/i\]/is' => '$1',
+ '/\[u\](.*?)\[\/u\]/is' => '$1',
+ '/\[s\](.*?)\[\/s\]/is' => '$1',
+ ];
+
+ foreach ($simple as $pat => $rep) {
+ $text = preg_replace($pat, $rep, $text);
+ }
+
+ // [url=link]text[/url] and [url]link[/url]
+ $text = preg_replace_callback('/\[url=(.*?)\](.*?)\[\/url\]/is', function ($m) {
+ $url = $this->sanitizeUrl(html_entity_decode($m[1]));
+ $label = $m[2];
+ return '' . $label . '';
+ }, $text);
+ $text = preg_replace_callback('/\[url\](.*?)\[\/url\]/is', function ($m) {
+ $url = $this->sanitizeUrl(html_entity_decode($m[1]));
+ return '' . $url . '';
+ }, $text);
+
+ // [img]url[/img]
+ $text = preg_replace_callback('/\[img\](.*?)\[\/img\]/is', function ($m) {
+ $src = $this->sanitizeUrl(html_entity_decode($m[1]));
+ return '';
+ }, $text);
+
+ // [quote]...[/quote]
+ $text = preg_replace('/\[quote\](.*?)\[\/quote\]/is', '
$1', $text); + + // [list] and [*] + // Convert [list]...[*]item[*]...[/list] to
Browse forum sections and latest activity.
+| Section | +Posts | +Topics | +Last Update | +
|---|---|---|---|
|
+ {{ $topicTitle }}
+ @if (!empty($topic->discuss))
+ {!! Str::limit(strip_tags((string) $topic->discuss), 180) !!}
+ @endif
+ |
+ {{ $topic->num_posts ?? 0 }} | +{{ $topic->num_subtopics ?? 0 }} | ++ @if (!empty($topic->last_update)) + {{ Carbon::parse($topic->last_update)->format('d.m.Y H:i') }} + @else + - + @endif + | +
| No forum sections available. | +|||
{!! Str::limit(strip_tags((string) $headerDesc), 260) !!}
+ @endif +{!! Str::limit(strip_tags((string) $topic->discuss), 220) !!}
+ @endif +| Thread | +Posts | +By | +Last Update | +
|---|---|---|---|
|
+ {{ $title }}
+ @if (!empty($sub->discuss))
+ {!! Str::limit(strip_tags((string) $sub->discuss), 180) !!}
+ @endif
+ |
+ {{ $sub->num_posts ?? 0 }} | +{{ $sub->uname ?? 'Unknown' }} | ++ @if (!empty($sub->last_update)) + {{ Carbon::parse($sub->last_update)->format('d.m.Y H:i') }} + @elseif (!empty($sub->post_date)) + {{ Carbon::parse($sub->post_date)->format('d.m.Y H:i') }} + @else + - + @endif + | +
| No threads in this section yet. | +|||
-
-
+
+ Join Skinbase and be part of our community. Upload, share and explore curated photography and skins.
+ Create an account