From 1d232faab183c4b0b9ec9c15321c6fb52a24227d Mon Sep 17 00:00:00 2001 From: lucs7 Date: Sun, 11 Oct 2026 10:31:16 +0200 Subject: [PATCH] feat(privacy): add view.public.reservation.details setting Public (not logged-in) users could see reservation labels on the schedule when privacy.view.reservations is true, but opening a reservation failed with "no permissions to any of the resources". Both the popup and view-reservation.php check permission rows by UserId, and the public session (UserId 0) has none. Add privacy.view.public.reservation.details (default false). When it and privacy.view.reservations are both true, the popup and the reservation detail page grant a public user the resources of the reservation they opened. The shared condition lives in ReservationDetailsFilter::CanPublicViewDetails(). Existing installs are unchanged because privacy.view.reservations keeps its meaning. Builds on the approach of #1331 by Norbert Krings, reworked to sit behind a new setting as suggested in the review. Related: LibreBooking/app#444, #1331 Assisted-by: Claude:claude-sonnet-5-5 --- .env.example | 4 ++ Pages/Ajax/ReservationPopupPage.php | 13 ++++- config/config.dist.php | 4 ++ docs/source/ADMINISTRATION.rst | 4 ++ docs/source/BASIC-CONFIGURATION.rst | 5 ++ .../ReservationComponentBinder.php | 8 +++ .../Reservation/ReservationDetailsFilter.php | 15 ++++++ lib/Config/ConfigKeys.php | 9 ++++ .../ReservationDetailsFilterTest.php | 47 ++++++++++++++++ .../Ajax/ReservationPopupPresenterTest.php | 53 +++++++++++++++++++ 10 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 tests/Application/Reservation/ReservationDetailsFilterTest.php create mode 100644 tests/Pages/Ajax/ReservationPopupPresenterTest.php diff --git a/.env.example b/.env.example index 8afdd9b88e..8c6ac3ff96 100644 --- a/.env.example +++ b/.env.example @@ -497,6 +497,10 @@ LB_PRIVACY_VIEW_SCHEDULES=true # When false (default), public views show only that a slot is occupied with no content. (true/false) LB_PRIVACY_VIEW_RESERVATIONS=false +# When true, public (unauthenticated) users can open reservation details from public views. +# Requires privacy.view.reservations. (true/false) +LB_PRIVACY_VIEW_PUBLIC_RESERVATION_DETAILS=false + # When true, non-owner/non-admin users see "Private" instead of the organizer name and email (true/false) LB_PRIVACY_HIDE_USER_DETAILS=false diff --git a/Pages/Ajax/ReservationPopupPage.php b/Pages/Ajax/ReservationPopupPage.php index 9b53bd2047..88c3a8c85b 100644 --- a/Pages/Ajax/ReservationPopupPage.php +++ b/Pages/Ajax/ReservationPopupPage.php @@ -403,7 +403,7 @@ public function PageLoad() $user = $this->_userRepository->LoadById(ServiceLocator::GetServer()->GetUserSession()->UserId); $owner = $this->_userRepository->LoadById($reservation->OwnerId); - $this->UserResourcePermissions(ServiceLocator::GetServer()->GetUserSession()->UserId); + $this->UserResourcePermissions(ServiceLocator::GetServer()->GetUserSession()->UserId, $reservation); $this->_page->SetCurrentUserParticipating($this->IsCurrentUserParticipating(ServiceLocator::GetServer()->GetUserSession()->UserId)); $this->_page->SetCurrentUserInvited($this->IsCurrentUserInvited(ServiceLocator::GetServer()->GetUserSession()->UserId)); @@ -426,9 +426,18 @@ public function PageLoad() /** * Gets the resources the user has permissions (full access and view only permissions) * This is used to block a user from seeing reservation details if he has no permissions to it's resources + * Public (not logged-in) users have no permission rows, so when public details are enabled + * they may see the resources of the reservation they opened. */ - private function UserResourcePermissions($userId) + private function UserResourcePermissions($userId, $reservation) { + if (ReservationDetailsFilter::CanPublicViewDetails()) { + $this->_page->BindViewableResourceReservations( + array_map(fn ($resource) => $resource->GetResourceId(), $reservation->Resources) + ); + return; + } + $resourceRepo = new ResourceRepository(); $resourceIds = []; diff --git a/config/config.dist.php b/config/config.dist.php index 4d7117ad03..8cf16e96ff 100644 --- a/config/config.dist.php +++ b/config/config.dist.php @@ -545,6 +545,10 @@ # When false (default), public views show only that a slot is occupied with no content. (true/false) 'view.reservations' => false, + # When true, public (unauthenticated) users can open reservation details from public views. + # Requires privacy.view.reservations. (true/false) + 'view.public.reservation.details' => false, + # When true, non-owner/non-admin users see "Private" instead of the organizer name and email (true/false) 'hide.user.details' => false, diff --git a/docs/source/ADMINISTRATION.rst b/docs/source/ADMINISTRATION.rst index 41a3b148b5..68f0d22047 100644 --- a/docs/source/ADMINISTRATION.rst +++ b/docs/source/ADMINISTRATION.rst @@ -144,6 +144,10 @@ read-only schedule. By default, only available and unavailable times are shown. Setting this to ``true`` will show full reservation details to unauthenticated users. +``view.public.reservation.details`` additionally lets unauthenticated users +open the reservation popup from the read-only schedule. It has no effect +unless ``view.reservations`` is ``true``. + Resources --------- diff --git a/docs/source/BASIC-CONFIGURATION.rst b/docs/source/BASIC-CONFIGURATION.rst index 80a84fc376..7763d1d6b1 100644 --- a/docs/source/BASIC-CONFIGURATION.rst +++ b/docs/source/BASIC-CONFIGURATION.rst @@ -342,6 +342,7 @@ Basic Privacy Settings 'privacy' => [ 'view.schedules' => true, 'view.reservations' => false, + 'view.public.reservation.details' => false, 'allow.guest.reservations' => false, ], @@ -351,6 +352,10 @@ Basic Privacy Settings **privacy.view.reservations** Allow users to view reservations. +**privacy.view.public.reservation.details** + Allow public (not logged-in) users to open reservation details from the + public schedule and calendar. Requires ``privacy.view.reservations``. + **privacy.allow.guest.reservations** Allow guests to make reservations. diff --git a/lib/Application/Reservation/ReservationComponentBinder.php b/lib/Application/Reservation/ReservationComponentBinder.php index f2bca6955b..3009b1a299 100644 --- a/lib/Application/Reservation/ReservationComponentBinder.php +++ b/lib/Application/Reservation/ReservationComponentBinder.php @@ -382,9 +382,17 @@ private function SetAutoReleaseMinutes() /** * Gets the resources the user has permissions (full access and view only permissions) * This is used to block a user from seeing reservation details if he has no permissions to it's resources + * Public users may see the resources of the reservation when public details are enabled. */ private function UserResourcePermissions($userId) { + if (ReservationDetailsFilter::CanPublicViewDetails()) { + $this->page->BindViewableResourceReservations( + array_map(fn ($resource) => $resource->GetResourceId(), $this->reservationView->Resources) + ); + return; + } + $resourceRepo = new ResourceRepository(); $resourceIds = []; diff --git a/lib/Application/Reservation/ReservationDetailsFilter.php b/lib/Application/Reservation/ReservationDetailsFilter.php index 0821102ab2..a3fc10057a 100644 --- a/lib/Application/Reservation/ReservationDetailsFilter.php +++ b/lib/Application/Reservation/ReservationDetailsFilter.php @@ -24,4 +24,19 @@ public static function HideReservationDetails($reservationStart = null, $reserva $converter = new BooleanConverter(); return $converter->Convert($hideReservationDetails); } + + /** + * Public (not logged-in) users have no resource permission rows, so + * they may open reservation details only when both settings allow it. + * @return bool + */ + public static function CanPublicViewDetails() + { + $converter = new BooleanConverter(); + $config = Configuration::Instance(); + + return !ServiceLocator::GetServer()->GetUserSession()->IsLoggedIn() + && $config->GetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, $converter) + && $config->GetKey(ConfigKeys::PRIVACY_VIEW_PUBLIC_RESERVATION_DETAILS, $converter); + } } diff --git a/lib/Config/ConfigKeys.php b/lib/Config/ConfigKeys.php index db117238e8..c16bcfb3a8 100644 --- a/lib/Config/ConfigKeys.php +++ b/lib/Config/ConfigKeys.php @@ -1235,6 +1235,15 @@ class ConfigKeys extends AbstractConfigKeys 'config_file_comment' => "When true, public (unauthenticated) views show reservation details.\nWhen false (default), public views show only that a slot is occupied with no content.", 'section' => 'privacy' ]; + public const PRIVACY_VIEW_PUBLIC_RESERVATION_DETAILS = [ + 'key' => 'privacy.view.public.reservation.details', + 'type' => 'boolean', + 'default' => false, + 'label' => 'View Public Reservation Details', + 'description' => 'When true, public (not logged-in) users can open the reservation popup from the public schedule and calendar to see reservation details. Requires privacy.view.reservations to be true.', + 'config_file_comment' => "When true, public (unauthenticated) users can open reservation details from public views.\nRequires privacy.view.reservations.", + 'section' => 'privacy' + ]; public const PRIVACY_HIDE_USER_DETAILS = [ 'key' => 'privacy.hide.user.details', 'type' => 'boolean', diff --git a/tests/Application/Reservation/ReservationDetailsFilterTest.php b/tests/Application/Reservation/ReservationDetailsFilterTest.php new file mode 100644 index 0000000000..abf93206e5 --- /dev/null +++ b/tests/Application/Reservation/ReservationDetailsFilterTest.php @@ -0,0 +1,47 @@ +fakeServer->SetUserSession(new NullUserSession()); + } + + public function testPublicUserCanViewDetailsWhenBothSettingsAreEnabled() + { + $this->SetSettings(true, true); + + $this->assertTrue(ReservationDetailsFilter::CanPublicViewDetails()); + } + + public function testPublicUserCannotViewDetailsWithoutViewReservations() + { + $this->SetSettings(false, true); + + $this->assertFalse(ReservationDetailsFilter::CanPublicViewDetails()); + } + + public function testPublicUserCannotViewDetailsWithoutPublicDetailsSetting() + { + $this->SetSettings(true, false); + + $this->assertFalse(ReservationDetailsFilter::CanPublicViewDetails()); + } + + public function testLoggedInUserIsNotTreatedAsPublic() + { + $this->SetSettings(true, true); + $this->fakeServer->SetUserSession(new FakeUserSession()); + + $this->assertFalse(ReservationDetailsFilter::CanPublicViewDetails()); + } + + private function SetSettings(bool $viewReservations, bool $publicDetails) + { + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, $viewReservations); + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_PUBLIC_RESERVATION_DETAILS, $publicDetails); + } +} diff --git a/tests/Pages/Ajax/ReservationPopupPresenterTest.php b/tests/Pages/Ajax/ReservationPopupPresenterTest.php new file mode 100644 index 0000000000..34d1767035 --- /dev/null +++ b/tests/Pages/Ajax/ReservationPopupPresenterTest.php @@ -0,0 +1,53 @@ +page = $this->createMock(IReservationPopupPage::class); + $this->page->method('GetReservationId')->willReturn(1); + + $view = new ReservationView(); + $view->StartDate = Date::Now(); + $view->EndDate = Date::Now()->AddDays(1); + $view->Resources = [ + new ReservationResourceView(10, 'r1', null, 1, null, false, null), + new ReservationResourceView(11, 'r2', null, 1, null, false, null), + ]; + + $this->reservationRepository = $this->createMock(IReservationViewRepository::class); + $this->reservationRepository->method('GetReservationForEditing')->willReturn($view); + + $userRepository = new FakeUserRepository(); + $userRepository->_User = new FakeUser(0); + + $this->presenter = new ReservationPopupPresenter( + $this->page, + $this->reservationRepository, + $this->createMock(IReservationAuthorization::class), + new FakeAttributeService(), + $userRepository + ); + } + + public function testPublicUserSeesReservationResourcesWhenPublicDetailsEnabled() + { + $this->fakeServer->SetUserSession(new NullUserSession()); + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, true); + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_PUBLIC_RESERVATION_DETAILS, true); + + $this->page->expects($this->once())->method('BindViewableResourceReservations')->with([10, 11]); + + $this->presenter->PageLoad(); + } +}