PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.62
Timetics – Appointment Booking Calendar & Scheduling v1.0.62
1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 All 64 releases
timetics / core / integrations / google / service / calendar.php

calendar.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.62, at core/integrations/google/service/calendar.php

457 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Google Calendar Class
4 *
5 * @package Timetics
6 */
7 namespace Timetics\Core\Integrations\Google\Service;
8
9 defined( 'ABSPATH' ) || exit;
10
11 /**
12 * Class Calendar
13 */
14 class Calendar {
15 const TIMETICS_TIMEZONE_URI = 'https://www.googleapis.com/calendar/v3/users/me/settings/timezone';
16 const TIMETICS_CALENDAR_EVENT = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
17
18
19 /**
20 * Get events from the calendar for the last 3 months.
21 *
22 * @param int $user_id Team member ID.
23 * @param array $api_filters Additional API filters for google calendar API
24 *
25 * @return array List of calendar events.
26 */
27 /**
28 * Format an instant as RFC3339 in UTC ("...Z").
29 *
30 * Google rejects bounds whose "+hh:mm" offset reaches it unescaped, so
31 * every timeMin/timeMax the plugin sends goes through here.
32 *
33 * @param int|\DateTimeInterface $when Timestamp or date object.
34 *
35 * @return string
36 */
37 public static function to_rfc3339_utc( $when ) {
38 $timestamp = $when instanceof \DateTimeInterface ? $when->getTimestamp() : (int) $when;
39
40 return gmdate( 'Y-m-d\TH:i:s\Z', $timestamp );
41 }
42
43 public function get_events( $user_id, $api_filters = array() ) {
44 $access_token = timetics_get_google_access_token($user_id);
45
46 if ( ! $access_token ) {
47 return ['error' => 'Access token not found or expired.'];
48 }
49
50 // Define the time range for the last 3 months.
51 //
52 // Always formatted as UTC with a trailing "Z" rather than an offset:
53 // an offset like "+06:00" carries a plus sign that survives into the
54 // query string, where Google reads it as a space. The bounds then fail
55 // to parse and the API answers with no items at all — which this class
56 // cannot distinguish from "no events", so every event silently
57 // disappeared. "Z" sidesteps the escaping problem entirely.
58 $three_months_ago = self::to_rfc3339_utc( strtotime( '-3 months' ) );
59 $three_months_ahead = self::to_rfc3339_utc( strtotime( '+3 months' ) );
60
61 $filters = array(
62 'timeMin' => $three_months_ago,
63 'timeMax' => $three_months_ahead,
64 'orderBy' => 'startTime',
65 'singleEvents' => 'true',
66 );
67
68 // add additional api filters if needed
69 $filters = array_merge( $filters, $api_filters );
70
71 // API URL with time range filter
72 $api_url = add_query_arg( $filters, self::TIMETICS_CALENDAR_EVENT);
73
74 // Set headers
75 $args = [
76 'headers' => [
77 'Authorization' => 'Bearer ' . $access_token,
78 'Content-Type' => 'application/json',
79 ],
80 ];
81
82 // Fetch data from Google Calendar API
83 $response = wp_remote_get( $api_url, $args );
84 if ( is_wp_error( $response ) ) {
85 return [];
86 }
87
88 $body = wp_remote_retrieve_body( $response );
89 $events = json_decode($body, true);
90
91 if ( empty( $events['items'] ) ) {
92 return [];
93 }
94
95 // Filter required fields
96 $filtered_events = [];
97 foreach ( $events['items'] as $event ) {
98 if ( empty( $event['start'] ) ) {
99 continue;
100 }
101
102 // Google sends `dateTime` for timed events and a date-only `date`
103 // for all-day ones. All-day bounds have no time and no timezone, so
104 // they must not be run through setTimezone() — that shifts the
105 // wall-clock and used to collapse them to 00:00:00-00:00:00, which
106 // blocked nothing at all. Note Google's all-day end date is
107 // EXCLUSIVE: a single day off is 08-10 to 08-11.
108 $all_day = empty( $event['start']['dateTime'] );
109
110 $start = $all_day ? $event['start']['date'] : $event['start']['dateTime'];
111 $end = $all_day ? $event['end']['date'] : $event['end']['dateTime'];
112
113 if ( $all_day ) {
114 $filtered_events[] = [
115 'id' => $event['id'] ?? '',
116 'all_day' => true,
117 'start_date' => $start,
118 'start_time' => '00:00:00',
119 'end_date' => $end,
120 'end_time' => '00:00:00',
121 'summary' => $event['summary'] ?? '',
122 'description' => $event['description'] ?? '',
123 ];
124
125 continue;
126 }
127
128 $timezone = $event['start']['timeZone'] ?? timetics_wp_timezone_string();
129 $timezone = new \DateTimeZone( $timezone );
130
131 $start_dt = new \DateTime( $start );
132 $end_dt = new \DateTime( $end );
133
134 // Absolute instants, captured before the display conversion below,
135 // so overlap maths never has to reason about wall-clock strings.
136 $start_timestamp = $start_dt->getTimestamp();
137 $end_timestamp = $end_dt->getTimestamp();
138
139 $start_dt->setTimezone( $timezone );
140 $end_dt->setTimezone( $timezone );
141
142 $filtered_events[] = [
143 'id' => $event['id'] ?? '',
144 'all_day' => false,
145 'start_date' => $start_dt->format( 'Y-m-d' ),
146 'start_time' => $start_dt->format( 'H:i:s' ),
147 'end_date' => $end_dt->format( 'Y-m-d' ),
148 'end_time' => $end_dt->format( 'H:i:s' ),
149 'start_timestamp' => $start_timestamp,
150 'end_timestamp' => $end_timestamp,
151 'timezone' => $timezone->getName(),
152 'summary' => $event['summary'] ?? '',
153 'description' => $event['description'] ?? '',
154 ];
155 }
156
157 return $filtered_events;
158 }
159
160 /**
161 * Get event by ID
162 *
163 * @param string $event_id
164 *
165 * @return JSON | WP_Error
166 */
167 public function get_event( $event_id , $user_id = null ) {
168 if ( ! $user_id ) {
169 $user_id = get_current_user_id();
170 }
171
172 $access_token = timetics_get_google_access_token( $user_id );
173
174 $data = [
175 'headers' => [
176 'Authorization' => 'Bearer ' . $access_token,
177 ],
178 ];
179
180 $response = wp_remote_get(self::TIMETICS_CALENDAR_EVENT . '/' . $event_id, $data);
181
182 if ( is_wp_error( $response ) ) {
183 return ['error' => $response->get_error_message()];
184 }
185
186 $body = wp_remote_retrieve_body( $response );
187 $event = json_decode($body, true);
188
189 return $event;
190 }
191
192 /**
193 * Create event
194 *
195 * @param array $args Event data
196 *
197 * @return JSON | WP_Error
198 */
199 public function create_event( $args = [] ) {
200 $defaults = [
201 'summary' => '',
202 'description' => '',
203 'location' => '',
204 'start' => '',
205 'end' => '',
206 'attendees' => [],
207 'google_meet' => true,
208 'access_token' => '',
209 ];
210
211 $args = apply_filters( 'timetics/booking/create/google-event', $args);
212
213 $args = wp_parse_args( $args, $defaults );
214 $data = $this->prepare_request_data( $args );
215
216 $query_params = build_query( [
217 'conferenceDataVersion' => '1',
218 // 'sendUpdates' => 'all',
219 ] );
220
221 $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '?' . $query_params, $data );
222
223 if ( is_wp_error( $response ) ) {
224 return false;
225 }
226
227 $status_code = wp_remote_retrieve_response_code( $response );
228
229 if ( 200 != $status_code ) {
230 return false;
231 }
232
233 $data = json_decode( wp_remote_retrieve_body( $response ), true );
234
235 return $data;
236 }
237
238 /**
239 * Update calender event
240 *
241 * @param array $args
242 *
243 * @return array
244 */
245 public function update_event( $event_id, $args ) {
246 $defaults = [
247 'summary' => '',
248 'description' => '',
249 'location' => '',
250 'start' => '',
251 'end' => '',
252 'attendees' => [],
253 'google_meet' => true,
254 'access_token' => '',
255 'method' => 'PUT',
256 ];
257
258 $args = wp_parse_args( $args, $defaults );
259 $query_params = build_query( [
260 'conferenceDataVersion' => '1',
261 'sendUpdates' => 'all',
262 ] );
263
264 $data = $this->prepare_request_data( $args );
265 $data['method'] = 'PUT';
266
267 $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, $data );
268
269 if ( is_wp_error( $response ) ) {
270 return false;
271 }
272
273 $status_code = wp_remote_retrieve_response_code( $response );
274
275 if ( 200 != $status_code ) {
276 return false;
277 }
278
279 $data = json_decode( wp_remote_retrieve_body( $response ), true );
280
281 return $data;
282 }
283
284 /**
285 * Get timzeson
286 *
287 * @return string | WP_Error
288 */
289 public function get_timezone( $access_token ) {
290 $data = [
291 'headers' => [
292 'Authorization' => 'Bearer ' . $access_token,
293 ],
294 ];
295
296 $response = wp_remote_get( self::TIMETICS_TIMEZONE_URI, $data );
297
298 if ( ! is_wp_error( $response ) ) {
299 $data = json_decode( wp_remote_retrieve_body( $response ), true );
300 return $data['value'];
301 }
302
303 return $response;
304 }
305
306 /**
307 * Delete google calendar event
308 *
309 * @param string $event_id
310 *
311 * @return array
312 */
313 public function delete_event( $event_id, $access_token ) {
314 $query_params = build_query( [
315 'conferenceDataVersion' => '1',
316 'sendUpdates' => 'all',
317 ] );
318
319 $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, [
320 'headers' => [
321 'Authorization' => 'Bearer ' . $access_token,
322 'Content-Type' => 'application/json; charset=utf-8',
323 ],
324 'method' => 'DELETE',
325 ] );
326
327 if ( is_wp_error( $response ) ) {
328 return false;
329 }
330
331 $status_code = wp_remote_retrieve_response_code( $response );
332
333 if ( 200 != $status_code ) {
334 return false;
335 }
336
337 $data = json_decode( wp_remote_retrieve_body( $response ), true );
338
339 return $data;
340 }
341
342 /**
343 * Get timezone offset
344 *
345 * @param string $timezone
346 *
347 * @return string
348 */
349 public function get_timezone_offset( $timezone ) {
350 $current = timezone_open( $timezone );
351 $utc_time = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
352 $offset_insecs = timezone_offset_get( $current, $utc_time );
353 $hours_and_sec = gmdate( 'H:i', abs( $offset_insecs ) );
354
355 return stripos( $offset_insecs, '-' ) === false ? "+{$hours_and_sec}" : "-{$hours_and_sec}";
356 }
357
358 /**
359 * Prepare time for calendar event
360 *
361 * @param array $data
362 *
363 * @return array
364 */
365 private function prepare_time( $data, $access_token ) {
366 $start_date = isset( $data['start']['date'] ) ? $data['start']['date'] : gmdate( 'Y-m-d' );
367 $start_time = isset( $data['start']['time'] ) ? $data['start']['time'] : gmdate( 'H:i:s' );
368 $end_date = isset( $data['end']['date'] ) ? $data['end']['date'] : gmdate( 'Y-m-d' );
369 $end_time = isset( $data['end']['time'] ) ? $data['end']['time'] : gmdate( 'H:i:s' );
370 $timezone = isset( $data['timezone'] ) ? $data['timezone'] : timetics_wp_timezone_string();
371
372 // Create DateTime objects with proper timezone to avoid double conversion.
373 $start_datetime = new \DateTime( $start_date . ' ' . $start_time, new \DateTimeZone( $timezone ) );
374 $end_datetime = new \DateTime( $end_date . ' ' . $end_time, new \DateTimeZone( $timezone ) );
375
376 return [
377 'start' => [
378 'dateTime' => $start_datetime->format( \DateTime::RFC3339 ),
379 'timeZone' => $timezone,
380 ],
381 'end' => [
382 'dateTime' => $end_datetime->format( \DateTime::RFC3339 ),
383 'timeZone' => $timezone,
384 ],
385 ];
386 }
387
388 /**
389 * Convet 12 hours format to 24 hours format
390 *
391 * @param string $time
392 *
393 * @return string
394 */
395 public function convertTo24HourFormat( $time ) {
396 // Use gmdate() instead of wp_date() to avoid timezone conversion
397 // since we're building an RFC3339 datetime string with explicit timezone offset.
398 return gmdate( 'H:i:s', strtotime( $time ) );
399 }
400
401 /**
402 * Prepare event create requested data
403 *
404 * @param array $args
405 *
406 * @return array
407 */
408 private function prepare_request_data( $args = [] ) {
409 $access_token = $args['access_token'];
410 $date = $this->prepare_time(
411 [
412 'start' => $args['start'],
413 'end' => $args['end'],
414 'timezone' => $args['timezone'],
415 ],
416 $access_token
417 );
418
419 $args['start'] = [$date['start']];
420 $args['end'] = [$date['end']];
421
422 if ( $args['google_meet'] ) {
423 // requestId must be unique per request; Google ignores the
424 // conference create request (no Meet link generated) if it
425 // matches a previously used id.
426 $request_id = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'tt-meet-', true );
427
428 $args['conferenceData'] = [
429 'createRequest' => [
430 'requestId' => $request_id,
431 'conferenceSolutionKey' => ['type' => 'hangoutsMeet'],
432 ],
433 ];
434 }
435
436 unset( $args['access_token'] );
437 $data = [
438 'headers' => [
439 'Authorization' => 'Bearer ' . $access_token,
440 'Content-Type' => 'application/json; charset=utf-8',
441 ],
442 'body' => wp_json_encode( $args ),
443 ];
444
445 return $data;
446 }
447
448 /**
449 * Get google calendar auth scope
450 *
451 * @return string
452 */
453 public static function scope() {
454 return 'https://www.googleapis.com/auth/calendar';
455 }
456 }
457