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