Просмотр исходного кода

fix(library): 阅读页目标不存在时返回 404 而不是 500

/library/tipitaka/{book}-{para}/read 以下情况原来都直接 500:
- channel 不存在或不是 uuid:ChannelApi::getById() 返回 false 后仍读 studio_id
- 段落不是 chapter 节点(如正文段落 level=100)或书/段落不存在:
  resolveDisplayRange() 里 $curr / $currBook 为 null
- id 不是 {book}-{para} 格式

channel 查询提前到入口,后面复用结果。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 5 дней назад
Родитель
Сommit
883990ff9a

+ 7 - 1
api-v13/app/Http/Controllers/Library/BookController.php

@@ -108,10 +108,14 @@ class BookController extends Controller
             $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
         }
 
+        abort_unless(preg_match('/^\d+-\d+$/', $id), 404);
         [$bookId, $paraId] = explode('-', $id);
         $bookId = (int) $bookId;
         $paraId = (int) $paraId;
 
+        $channel = ChannelApi::getById($channelId);
+        abort_unless($channel, 404);
+
         // 解析选中 chapter 的实际显示区间(起始/终止段落号),再按段落号聚合 HTML,
         // 避免选中父 chapter 时仅显示一个标题。
         $range = $this->resolveDisplayRange($bookId, $paraId);
@@ -131,7 +135,6 @@ class BookController extends Controller
         $book = [];
 
         $book['toc'] = $this->getBookToc($bookId, $paraId, $channelId, 2, 7, [$range['start'], $range['end']]);
-        $channel = ChannelApi::getById($channelId);
         $studio = StudioApi::getById($channel['studio_id']);
         $book['categories'] = $chapter['category'];
         $book['title'] = $chapter['title'];
@@ -180,6 +183,7 @@ class BookController extends Controller
     private function resolveDisplayRange(int $book, int $para): array
     {
         $currBook = $this->bookStart($book, $para);
+        abort_if($currBook === null, 404);
         $bookStart = $currBook->paragraph;
         $bookEnd = $currBook->paragraph + $currBook->chapter_len - 1;
 
@@ -191,6 +195,8 @@ class BookController extends Controller
             ->get();
 
         $curr = $paragraphs->firstWhere('paragraph', $para);
+        // 选中的不是可导航的 chapter 节点(如正文段落 level=100)
+        abort_if($curr === null, 404);
         $endParagraph = $curr->paragraph + $curr->chapter_len - 1;
 
         if ($curr->chapter_strlen > $this->maxChapterStrlen) {

+ 29 - 0
api-v13/tests/Feature/LibraryTipitakaReadTest.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Str;
+
+uses(RefreshDatabase::class);
+
+/**
+ * 阅读页 /library/tipitaka/{book}-{para}/read 的目标不存在时返回 404,而不是 500。
+ */
+it('returns 404 when the channel does not exist', function () {
+    $this->get('/library/tipitaka/165-123/read?channel='.Str::uuid())
+        ->assertNotFound();
+});
+
+it('returns 404 when the channel is not a uuid', function () {
+    $this->get('/library/tipitaka/165-123/read?channel=not-a-uuid')
+        ->assertNotFound();
+});
+
+it('returns 404 when the id is malformed', function () {
+    $this->get('/library/tipitaka/abc/read?channel='.makeChannel(makeStudio('me'), 'c'))
+        ->assertNotFound();
+});
+
+it('returns 404 when the paragraph is not a chapter', function () {
+    $this->get('/library/tipitaka/99999-1/read?channel='.makeChannel(makeStudio('me'), 'c'))
+        ->assertNotFound();
+});