1<?php 2 3declare(strict_types=1); 4 5/** 6 * Calendar App 7 * 8 * @copyright 2021 Anna Larch <anna.larch@gmx.net> 9 * 10 * @author Anna Larch <anna.larch@gmx.net> 11 * 12 * This library is free software; you can redistribute it and/or 13 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE 14 * License as published by the Free Software Foundation; either 15 * version 3 of the License, or any later version. 16 * 17 * This library is distributed in the hope that it will be useful, 18 * but WITHOUT ANY WARRANTY; without even the implied warranty of 19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 * GNU AFFERO GENERAL PUBLIC LICENSE for more details. 21 * 22 * You should have received a copy of the GNU Affero General Public 23 * License along with this library. If not, see <http://www.gnu.org/licenses/>. 24 * 25 */ 26namespace OCA\Calendar\Controller; 27 28use DateTimeImmutable; 29use DateTimeZone; 30use InvalidArgumentException; 31use OC\URLGenerator; 32use OCA\Calendar\AppInfo\Application; 33use OCA\Calendar\Exception\ClientException; 34use OCA\Calendar\Exception\NoSlotFoundException; 35use OCA\Calendar\Exception\ServiceException; 36use OCA\Calendar\Http\JsonResponse; 37use OCA\Calendar\Service\Appointments\AppointmentConfigService; 38use OCA\Calendar\Service\Appointments\BookingService; 39use OCP\AppFramework\Controller; 40use OCP\AppFramework\Http; 41use OCP\AppFramework\Http\TemplateResponse; 42use OCP\AppFramework\Services\IInitialState; 43use OCP\AppFramework\Utility\ITimeFactory; 44use OCP\DB\Exception; 45use OCP\IRequest; 46use Psr\Log\LoggerInterface; 47 48class BookingController extends Controller { 49 50 /** @var BookingService */ 51 private $bookingService; 52 53 /** @var ITimeFactory */ 54 private $timeFactory; 55 56 /** @var AppointmentConfigService */ 57 private $appointmentConfigService; 58 59 /** @var IInitialState */ 60 private $initialState; 61 62 /** @var URLGenerator */ 63 private $urlGenerator; 64 65 /** @var LoggerInterface */ 66 private $logger; 67 68 public function __construct(string $appName, 69 IRequest $request, 70 ITimeFactory $timeFactory, 71 IInitialState $initialState, 72 BookingService $bookingService, 73 AppointmentConfigService $appointmentConfigService, 74 URLGenerator $urlGenerator, 75 LoggerInterface $logger) { 76 parent::__construct($appName, $request); 77 78 $this->bookingService = $bookingService; 79 $this->timeFactory = $timeFactory; 80 $this->appointmentConfigService = $appointmentConfigService; 81 $this->initialState = $initialState; 82 $this->urlGenerator = $urlGenerator; 83 $this->logger = $logger; 84 } 85 86 /** 87 * @NoAdminRequired 88 * @PublicPage 89 * 90 * @param int $appointmentConfigId 91 * @param int $startTime UNIX time stamp for the start time in UTC 92 * @param string $timeZone 93 * 94 * @return JsonResponse 95 */ 96 public function getBookableSlots(int $appointmentConfigId, 97 int $startTime, 98 string $timeZone): JsonResponse { 99 // Convert the timestamps to the beginning and end of the respective day in the specified timezone 100 try { 101 $tz = new DateTimeZone($timeZone); 102 } catch (Exception $e) { 103 $this->logger->error('Timezone invalid', ['exception' => $e]); 104 return JsonResponse::fail('Invalid time zone', Http::STATUS_UNPROCESSABLE_ENTITY); 105 } 106 $startTimeInTz = (new DateTimeImmutable()) 107 ->setTimestamp($startTime) 108 ->setTimezone($tz) 109 ->setTime(0, 0) 110 ->getTimestamp(); 111 $endTimeInTz = (new DateTimeImmutable()) 112 ->setTimestamp($startTime) 113 ->setTimezone($tz) 114 ->setTime(23, 59, 59) 115 ->getTimestamp(); 116 117 if ($startTimeInTz > $endTimeInTz) { 118 $this->logger->warning('Invalid time range - end time ' . $endTimeInTz . ' before start time ' . $startTimeInTz); 119 return JsonResponse::fail('Invalid time range', Http::STATUS_UNPROCESSABLE_ENTITY); 120 } 121 // rate limit this to only allow ranges between 0 and 7 days 122 if (ceil(($endTimeInTz - $startTimeInTz) / 86400) > 7) { 123 $this->logger->warning('Date range too large for start ' . $startTimeInTz . ' end ' . $endTimeInTz); 124 return JsonResponse::fail('Date Range too large.', Http::STATUS_UNPROCESSABLE_ENTITY); 125 } 126 $now = $this->timeFactory->getTime(); 127 if ($now > $endTimeInTz) { 128 $this->logger->warning('Slot time must be in the future - now ' . $now . ' end ' . $endTimeInTz); 129 return JsonResponse::fail('Slot time range must be in the future', Http::STATUS_UNPROCESSABLE_ENTITY); 130 } 131 132 try { 133 $config = $this->appointmentConfigService->findById($appointmentConfigId); 134 } catch (ServiceException $e) { 135 $this->logger->error('No appointment config found for id ' . $appointmentConfigId, ['exception' => $e]); 136 return JsonResponse::fail(null, Http::STATUS_NOT_FOUND); 137 } 138 139 return JsonResponse::success( 140 $this->bookingService->getAvailableSlots($config, $startTimeInTz, $endTimeInTz) 141 ); 142 } 143 144 /** 145 * @NoAdminRequired 146 * @PublicPage 147 * 148 * @param int $appointmentConfigId 149 * @param int $start 150 * @param int $end 151 * @param string $displayName 152 * @param string $email 153 * @param string $description 154 * @param string $timeZone 155 * @return JsonResponse 156 */ 157 public function bookSlot(int $appointmentConfigId, 158 int $start, 159 int $end, 160 string $displayName, 161 string $email, 162 string $description, 163 string $timeZone): JsonResponse { 164 if ($start > $end) { 165 return JsonResponse::fail('Invalid time range', Http::STATUS_UNPROCESSABLE_ENTITY); 166 } 167 168 try { 169 $config = $this->appointmentConfigService->findById($appointmentConfigId); 170 } catch (ServiceException $e) { 171 $this->logger->error('No appointment config found for id ' . $appointmentConfigId, ['exception' => $e]); 172 return JsonResponse::fail(null, Http::STATUS_NOT_FOUND); 173 } 174 try { 175 $booking = $this->bookingService->book($config, $start, $end, $timeZone, $displayName, $email, $description); 176 } catch (NoSlotFoundException $e) { 177 $this->logger->warning('No slot available for start: ' . $start . ', end: ' . $end . ', config id: ' . $appointmentConfigId , ['exception' => $e]); 178 return JsonResponse::fail(null, Http::STATUS_NOT_FOUND); 179 } catch (InvalidArgumentException $e) { 180 $this->logger->warning($e->getMessage(), ['exception' => $e]); 181 return JsonResponse::fail(null, Http::STATUS_UNPROCESSABLE_ENTITY); 182 } catch (ServiceException|ClientException $e) { 183 $this->logger->error($e->getMessage(), ['exception' => $e]); 184 return JsonResponse::errorFromThrowable($e, $e->getHttpCode() ?? Http::STATUS_INTERNAL_SERVER_ERROR); 185 } 186 187 return JsonResponse::success($booking); 188 } 189 190 /** 191 * @PublicPage 192 * @NoCSRFRequired 193 * 194 * @param string $token 195 * @return TemplateResponse 196 * @throws Exception 197 */ 198 public function confirmBooking(string $token): TemplateResponse { 199 try { 200 $booking = $this->bookingService->findByToken($token); 201 } catch (ClientException $e) { 202 $this->logger->warning($e->getMessage(), ['exception' => $e]); 203 return new TemplateResponse( 204 Application::APP_ID, 205 'appointments/404-booking', 206 [], 207 TemplateResponse::RENDER_AS_GUEST 208 ); 209 } 210 211 try { 212 $config = $this->appointmentConfigService->findById($booking->getApptConfigId()); 213 } catch (ServiceException $e) { 214 $this->logger->error($e->getMessage(), ['exception' => $e]); 215 return new TemplateResponse( 216 Application::APP_ID, 217 'appointments/404-booking', 218 [], 219 TemplateResponse::RENDER_AS_GUEST 220 ); 221 } 222 223 $link = $this->urlGenerator->linkToRouteAbsolute('calendar.appointment.show', [ 'token' => $config->getToken() ]); 224 try { 225 $booking = $this->bookingService->confirmBooking($booking, $config); 226 } catch (ClientException $e) { 227 $this->logger->warning($e->getMessage(), ['exception' => $e]); 228 } 229 230 $this->initialState->provideInitialState( 231 'appointment-link', 232 $link 233 ); 234 $this->initialState->provideInitialState( 235 'booking', 236 $booking 237 ); 238 239 return new TemplateResponse( 240 Application::APP_ID, 241 'appointments/booking-conflict', 242 [], 243 TemplateResponse::RENDER_AS_GUEST 244 ); 245 } 246} 247