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(); + } +}