From b837a33d500b1495b34fd6a3e31419baaac8f4f8 Mon Sep 17 00:00:00 2001 From: Guajiro <276488307+Guajir0-code@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:48:12 -0300 Subject: [PATCH] fix(resources): stop exposing author email in page props WpPostResource serialized the whole author model into the Inertia payload. The author relation selects user_email (needed by WpAuthorService::getAvatar for the Gravatar hash), so every author address was shipped to the browser on every post of every listing. Replace it with the three fields the frontend declares in PostAuthor, and add $hidden on WpUser so an accidental full-model serialization cannot leak credentials again. Attribute access is unaffected, so getAvatar still works. phpunit.xml now pins sqlite/:memory: so the suite can boot at all. Co-Authored-By: Claude Opus 5 --- app/Http/Resources/WpPostResource.php | 24 ++++- app/Models/WpUser.php | 15 +++ phpunit.xml | 3 +- tests/Feature/WpPostResourceTest.php | 126 ++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/WpPostResourceTest.php diff --git a/app/Http/Resources/WpPostResource.php b/app/Http/Resources/WpPostResource.php index 88891d4..6b6f80f 100644 --- a/app/Http/Resources/WpPostResource.php +++ b/app/Http/Resources/WpPostResource.php @@ -37,7 +37,7 @@ public function toArray(Request $request): array $postData = [ 'id' => $this->ID, - 'author' => $this->author, + 'author' => $this->getAuthor(), 'title' => $this->post_title, 'excerpt' => $this->post_excerpt, 'slug' => $this->post_name, @@ -64,6 +64,28 @@ public function toArray(Request $request): array return $postData; } + /** + * Only the author fields the frontend actually consumes. + * + * The author relation also selects user_email, which WpAuthorService::getAvatar() + * needs to build the Gravatar hash. Serializing the whole model would ship that + * address to the browser on every post of every listing. + * + * @return array|null + */ + private function getAuthor(): ?array + { + if (! $this->author) { + return null; + } + + return [ + 'ID' => $this->author->ID, + 'display_name' => $this->author->display_name, + 'user_nicename' => $this->author->user_nicename, + ]; + } + private function getCategories() { return $this->terms diff --git a/app/Models/WpUser.php b/app/Models/WpUser.php index 4136f71..0ab11ce 100644 --- a/app/Models/WpUser.php +++ b/app/Models/WpUser.php @@ -14,6 +14,21 @@ class WpUser extends Model { protected $table = 'wp_users'; + /** + * Never serialize credentials or contact data. + * + * Attribute access still works (getAvatar() reads user_email), this only + * affects toArray()/toJson() — so an accidental `'author' => $model` cannot + * leak these fields into a response again. + * + * @var list + */ + protected $hidden = [ + 'user_pass', + 'user_email', + 'user_activation_key', + ]; + public function metadata(): HasMany { return $this->hasMany(WpUserMeta::class, 'user_id', 'ID'); diff --git a/phpunit.xml b/phpunit.xml index c09b5bc..61c031c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,8 @@ - + + diff --git a/tests/Feature/WpPostResourceTest.php b/tests/Feature/WpPostResourceTest.php new file mode 100644 index 0000000..404114c --- /dev/null +++ b/tests/Feature/WpPostResourceTest.php @@ -0,0 +1,126 @@ +id('ID'); + $table->string('display_name'); + $table->string('user_nicename'); + $table->string('user_email'); + $table->string('user_pass'); + }); + + Schema::create('wp_posts', function (Blueprint $table) { + $table->id('ID'); + $table->unsignedBigInteger('post_author')->default(0); + $table->string('post_title'); + $table->text('post_excerpt'); + $table->text('post_content'); + $table->string('post_name'); + $table->string('post_type')->default('post'); + $table->string('post_status')->default('publish'); + $table->dateTime('post_date'); + $table->string('guid')->default(''); + }); + + Schema::create('wp_postmeta', function (Blueprint $table) { + $table->id('meta_id'); + $table->unsignedBigInteger('post_id'); + $table->string('meta_key'); + $table->text('meta_value'); + }); +}); + +/** + * Builds a post with its relations pre-set, so the assertions below exercise the + * resource and nothing else. + */ +function makePost(?WpUser $author): WpPost +{ + // forceFill: the Wp* models declare no $fillable + $post = (new WpPost)->forceFill([ + 'ID' => 1, + 'post_title' => 'Um post', + 'post_excerpt' => 'resumo', + 'post_content' => 'conteudo', + 'post_name' => 'um-post', + 'post_date' => '2026-07-01 10:00:00', + ]); + + $post->exists = true; + + $post->setRelation('author', $author); + $post->setRelation('terms', new Collection); + $post->setRelation('metadata', new Collection); + + return $post; +} + +function authorPayload(?WpUser $author): array +{ + $resource = WpPostResource::make(makePost($author)); + + return $resource->toArray(Request::create('/')); +} + +it('does not expose the author email in the payload', function () { + $author = (new WpUser)->forceFill([ + 'ID' => 2, + 'display_name' => 'Mayron Câmara', + 'user_nicename' => 'mayron', + 'user_email' => 'autor@example.com', + ]); + + $payload = authorPayload($author); + + expect($payload['author'])->toBe([ + 'ID' => 2, + 'display_name' => 'Mayron Câmara', + 'user_nicename' => 'mayron', + ]); + + expect(json_encode($payload))->not->toContain('autor@example.com'); + expect(json_encode($payload))->not->toContain('user_email'); +}); + +it('keeps the fields the frontend consumes', function () { + $author = (new WpUser)->forceFill([ + 'ID' => 2, + 'display_name' => 'Mayron Câmara', + 'user_nicename' => 'mayron', + 'user_email' => 'autor@example.com', + ]); + + // types/index.d.ts declares PostAuthor as { ID, display_name, user_nicename } + expect(authorPayload($author)['author']) + ->toHaveKeys(['ID', 'display_name', 'user_nicename']); +}); + +it('returns null when the post has no author, as before', function () { + expect(authorPayload(null)['author'])->toBeNull(); +}); + +it('hides credentials when a WpUser is serialized directly', function () { + $user = (new WpUser)->forceFill([ + 'display_name' => 'Mayron Câmara', + 'user_nicename' => 'mayron', + 'user_email' => 'autor@example.com', + 'user_pass' => '$P$Bxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + ]); + + expect($user->toArray()) + ->not->toHaveKey('user_email') + ->not->toHaveKey('user_pass'); + + // attribute access still works — getAvatar() depends on it + expect($user->user_email)->toBe('autor@example.com'); +});