From fdb8198ecdfd5320f28a540016747a1e5e3e8383 Mon Sep 17 00:00:00 2001 From: gakigaki Date: Fri, 3 Apr 2026 16:55:37 +0900 Subject: [PATCH] fix(core): restore upload page context fallback --- app/Http/Middleware/ConnectPage.php | 69 +++- .../Core/ConnectPageUploadFallbackTest.php | 306 ++++++++++++++++++ 2 files changed, 367 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/Core/ConnectPageUploadFallbackTest.php diff --git a/app/Http/Middleware/ConnectPage.php b/app/Http/Middleware/ConnectPage.php index 18014fdf9..18e7413bd 100644 --- a/app/Http/Middleware/ConnectPage.php +++ b/app/Http/Middleware/ConnectPage.php @@ -59,14 +59,7 @@ public function handle($request, Closure $next) } // ページの特定 - $route_page_id = $request->route('page_id'); - if (!empty($route_page_id)) { - // ページID が渡ってきた場合 - $this->page = Page::where('id', $route_page_id)->first(); - } else { - // ページID が渡されなかった場合、URL から取得 - $this->page = $this->getCurrentPage(); - } + $this->page = $this->resolveRequestPage($request); // 下層ページへ自動転送 if ($this->page && $this->page->transfer_lower_page_flag) { @@ -207,6 +200,66 @@ private function getCurrentPage() return $page; } + /** + * リクエストから現在ページを特定する。 + */ + private function resolveRequestPage($request) + { + $route_page_id = $request->route('page_id'); + if ($this->isValidPageId($route_page_id)) { + return Page::where('id', (int)$route_page_id)->first(); + } + + $upload_page_id = $this->getUploadFallbackPageId($request); + if (!is_null($upload_page_id)) { + return Page::where('id', $upload_page_id)->first(); + } + + return $this->getCurrentPage(); + } + + /** + * /upload/{method?} 限定で body の page_id を補完する。 + */ + private function getUploadFallbackPageId($request): ?int + { + if (!$this->isUploadPostRoute($request)) { + return null; + } + + $page_id = $request->input('page_id'); + if (!$this->isValidPageId($page_id)) { + return null; + } + + return (int)$page_id; + } + + /** + * body の page_id fallback を許可する upload POST か判定する。 + */ + private function isUploadPostRoute($request): bool + { + if (!$request->isMethod('post')) { + return false; + } + + $route = $request->route(); + return !is_null($route) && $route->getName() === 'post_upload'; + } + + /** + * page_id として扱える正の整数か判定する。 + */ + private function isValidPageId($page_id): bool + { + if (filter_var($page_id, FILTER_VALIDATE_INT) === false) { + return false; + } + + return (int)$page_id > 0; + } + /** * 404 判定 * (ConnectController から移動してきた) diff --git a/tests/Feature/Core/ConnectPageUploadFallbackTest.php b/tests/Feature/Core/ConnectPageUploadFallbackTest.php new file mode 100644 index 000000000..748493cb2 --- /dev/null +++ b/tests/Feature/Core/ConnectPageUploadFallbackTest.php @@ -0,0 +1,306 @@ +seed(); + } + + /** + * ページツリー検証に使うページを、必要最小限の属性で生成する。 + */ + private function createPage(string $page_name, string $permanent_link, ?Page $parent = null): Page + { + $page = new Page([ + 'page_name' => $page_name, + 'permanent_link' => $permanent_link, + 'base_display_flag' => 1, + ]); + + if (is_null($parent)) { + $page->save(); + } else { + $page->appendToNode($parent)->save(); + } + + return $page->fresh(); + } + + /** + * seed 済みのトップページを取得し、子ページ追加の起点にする。 + */ + private function getRootPage(): Page + { + return Page::where('permanent_link', '/')->firstOrFail(); + } + + /** + * ベース権限を持たず、指定ページのページ権限だけを持つユーザーを生成する。 + */ + private function createPageRoleUser(Page $page, string $role_name = 'role_reporter'): User + { + $user = User::factory()->create(); + $group = Group::factory()->create(); + + GroupUser::factory()->create([ + 'group_id' => $group->id, + 'user_id' => $user->id, + ]); + + PageRole::factory()->create([ + 'page_id' => $page->id, + 'group_id' => $group->id, + 'target' => 'base', + 'role_name' => $role_name, + 'role_value' => 1, + ]); + + return $user->fresh(); + } + + /** + * middleware が参照する route / request 情報を isolated Router 上で組み立てる。 + */ + private function createRequest(string $uri, string $method, string $route_uri, string $route_name, array $parameters = []): Request + { + $request = Request::create($uri, $method, $parameters); + + $router = new Router($this->app['events'], $this->app); + $router->match([strtoupper($method)], $route_uri, function () { + return response('prepared', 200); + })->name($route_name); + $router->dispatchToRoute($request); + + $this->app->instance('request', $request); + $this->app->instance(Request::class, $request); + $this->app->instance(Router::class, $router); + + return $request; + } + + /** + * ConnectPage の公開入口 handle() を通し、処理後の request を取得する。 + */ + private function handleRequest(Request $request): Request + { + $handled_request = null; + $middleware = new ConnectPage(); + + $response = $middleware->handle($request, function ($request_after_handle) use (&$handled_request) { + $handled_request = $request_after_handle; + return response('ok', 200); + }); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertInstanceOf(Request::class, $handled_request); + + return $handled_request; + } + + /** + * /upload のテスト用に postInvoke() を差し替え、既存の isCan() 判定と保存値を検証可能にする。 + */ + private function bindUploadControllerForFileTest(): void + { + $this->app->bind(UploadController::class, function () { + return new class extends UploadController { + public function postInvoke(Request $request, $method = null) + { + $can_upload = $this->isCan('role_reporter') || $this->isCan('role_article'); + if (!$can_upload) { + return [ + 'location' => 'error', + 'resolved_page_id' => optional($request->attributes->get('page'))->id, + 'can_upload' => false, + ]; + } + + $upload = Uploads::create([ + 'client_original_name' => $request->file('file')->getClientOriginalName(), + 'mimetype' => $request->file('file')->getClientMimeType(), + 'extension' => $request->file('file')->getClientOriginalExtension(), + 'size' => $request->file('file')->getSize(), + 'page_id' => $request->page_id, + 'plugin_name' => $request->plugin_name, + ]); + + return [ + 'location' => url('/') . '/file/' . $upload->id, + 'resolved_page_id' => optional($request->attributes->get('page'))->id, + 'saved_page_id' => $upload->page_id, + 'can_upload' => true, + ]; + } + }; + }); + } + + /** + * /upload/face のテスト用に postInvoke() を差し替え、既存の isCan() 判定と保存値を検証可能にする。 + */ + private function bindUploadControllerForFaceTest(): void + { + $this->app->bind(UploadController::class, function () { + return new class extends UploadController { + public function postInvoke(Request $request, $method = null) + { + $can_upload = $this->isCan('role_reporter') || $this->isCan('role_article'); + if (!$can_upload) { + return [ + 'location' => 'error', + 'resolved_page_id' => optional($request->attributes->get('page'))->id, + 'can_upload' => false, + ]; + } + + $upload = Uploads::create([ + 'client_original_name' => $request->file('photo')->getClientOriginalName(), + 'mimetype' => $request->file('photo')->getClientMimeType(), + 'extension' => $request->file('photo')->getClientOriginalExtension(), + 'size' => $request->file('photo')->getSize(), + 'page_id' => $request->page_id, + 'plugin_name' => $request->plugin_name, + ]); + + return [ + 'location' => url('/') . '/file/' . $upload->id, + 'resolved_page_id' => optional($request->attributes->get('page'))->id, + 'saved_page_id' => $upload->page_id, + 'can_upload' => true, + ]; + } + }; + }); + } + + /** + * テストの意図: + * /upload は route に page_id がなくても、body の page_id からページ文脈を復元し、ページ権限だけのユーザーでも保存できることを守る。 + */ + public function testUploadUsesBodyPageIdFallbackForPageRoleUser(): void + { + Storage::fake('local'); + + $root = $this->getRootPage(); + $page = $this->createPage('target', '/target', $root); + $user = $this->createPageRoleUser($page); + + $this->bindUploadControllerForFileTest(); + + $response = $this->actingAs($user)->post('/upload', [ + 'page_id' => $page->id, + 'plugin_name' => 'contents', + 'file' => UploadedFile::fake()->create('sample.txt', 10, 'text/plain'), + ]); + + $response->assertStatus(200); + $response->assertJsonMissing(['location' => 'error']); + + $response_json = $response->json(); + $upload = Uploads::query()->latest('id')->first(); + + $this->assertTrue($response_json['can_upload']); + $this->assertSame($page->id, $response_json['resolved_page_id']); + $this->assertSame($page->id, $response_json['saved_page_id']); + $this->assertSame($page->id, $upload->page_id); + } + + /** + * テストの意図: + * /upload/face も /upload と同じ fallback 対象であり、ページ権限だけのユーザーで認可と page 文脈復元が通ることを守る。 + */ + public function testFaceUploadUsesBodyPageIdFallbackForPageRoleUser(): void + { + $root = $this->getRootPage(); + $page = $this->createPage('target', '/target', $root); + $user = $this->createPageRoleUser($page); + + $this->bindUploadControllerForFaceTest(); + + $response = $this->actingAs($user)->post('/upload/face', [ + 'page_id' => $page->id, + 'plugin_name' => 'contents', + 'image_size' => 120, + 'mosaic_fineness' => 10, + 'photo' => UploadedFile::fake()->image('face.jpg', 120, 120), + ]); + + $response->assertStatus(200); + $response->assertJsonMissing(['location' => 'error']); + + $response_json = $response->json(); + + $this->assertTrue($response_json['can_upload']); + $this->assertSame($page->id, $response_json['resolved_page_id']); + $this->assertSame($page->id, $response_json['saved_page_id']); + } + + /** + * テストの意図: + * upload 以外の POST では body の page_id を現在ページ解決に使わず、fallback が /upload 系に閉じていることを守る。 + */ + public function testBodyPageIdDoesNotFallbackOutsideUploadRoutes(): void + { + $root = $this->getRootPage(); + $page = $this->createPage('target', '/target', $root); + + $request = $this->createRequest('/not-upload', 'POST', '/not-upload', 'post_dummy', [ + 'page_id' => $page->id, + ]); + + $handled_request = $this->handleRequest($request); + + $this->assertFalse($handled_request->attributes->get('page')); + } + + /** + * テストの意図: + * route に page_id がある場合は upload 系でも body より route を優先し、意図しないページ権限へのすり替わりを防ぐことを守る。 + */ + public function testRoutePageIdTakesPriorityOverBodyPageId(): void + { + $root = $this->getRootPage(); + $route_page = $this->createPage('route target', '/route-target', $root); + $body_page = $this->createPage('body target', '/body-target', $root); + + $request = $this->createRequest('/upload/' . $route_page->id, 'POST', '/upload/{page_id}/{method?}', 'post_upload', [ + 'page_id' => $body_page->id, + ]); + + $handled_request = $this->handleRequest($request); + $resolved_page = $handled_request->attributes->get('page'); + + $this->assertInstanceOf(Page::class, $resolved_page); + $this->assertSame($route_page->id, $resolved_page->id); + } +}