PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 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 All 65 releases
← All changes | core/integrations/google/service/calendar.php +205 -34 1.0.16 → 1.0.64 View file →
@@ -5,16 +5,192 @@
5 5 * @package Timetics
6 6 */
7 7 namespace Timetics\Core\Integrations\Google\Service;
8 8
9 +defined( 'ABSPATH' ) || exit;
10 +
9 11 /**
10 12 * Class Calendar
11 13 */
12 14 class Calendar {
13 15 const TIMETICS_TIMEZONE_URI = 'https://www.googleapis.com/calendar/v3/users/me/settings/timezone';
14 - const TIMETICS_CALENDAR_EVENT = 'https://www.googleapis.com/calendar/v3/calendars/primary/events/';
16 + const TIMETICS_CALENDAR_EVENT = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
15 17
18 +
16 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 + /**
17 193 * Create event
18 194 *
19 195 * @param array $args Event data
20 196 *
@@ -31,8 +207,10 @@
31 207 'google_meet' => true,
32 208 'access_token' => '',
33 209 ];
34 210
211 + $args = apply_filters( 'timetics/booking/create/google-event', $args);
212 +
35 213 $args = wp_parse_args( $args, $defaults );
36 214 $data = $this->prepare_request_data( $args );
37 215
38 216 $query_params = build_query( [
@@ -85,9 +263,9 @@
85 263
86 264 $data = $this->prepare_request_data( $args );
87 265 $data['method'] = 'PUT';
88 266
89 - $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . $event_id . '?' . $query_params, $data );
267 + $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, $data );
90 268
91 269 if ( is_wp_error( $response ) ) {
92 270 return false;
93 271 }
@@ -137,9 +315,9 @@
137 315 'conferenceDataVersion' => '1',
138 316 'sendUpdates' => 'all',
139 317 ] );
140 318
141 - $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . $event_id . '?' . $query_params, [
319 + $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, [
142 320 'headers' => [
143 321 'Authorization' => 'Bearer ' . $access_token,
144 322 'Content-Type' => 'application/json; charset=utf-8',
145 323 ],
@@ -184,30 +362,28 @@
184 362 *
185 363 * @return array
186 364 */
187 365 private function prepare_time( $data, $access_token ) {
188 - $start_date = isset( $data['start']['date'] ) ? $data['start']['date'] : gmdate( 'Y-m-d' );
189 - $start_time = isset( $data['start']['time'] ) ? $data['start']['time'] : gmdate( 'H:i:s' );
190 - $end_date = isset( $data['end']['date'] ) ? $data['end']['date'] : gmdate( 'Y-m-d' );
191 - $end_time = isset( $data['end']['time'] ) ? $data['end']['time'] : gmdate( 'H:i:s' );
192 - $timezone = $this->get_timezone( $access_token );
193 - $timezone_offset = $this->get_timezone_offset( $timezone );
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();
194 371
195 - $start_date_time = $start_date . 'T' . $this->convertTo24HourFormat( $start_time ) . $timezone_offset;
196 - $end_date_time = $end_date . 'T' . $this->convertTo24HourFormat( $end_time ) . $timezone_offset;
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 ) );
197 375
198 - $date_time = [
376 + return [
199 377 'start' => [
200 - 'dateTime' => $start_date_time,
378 + 'dateTime' => $start_datetime->format( \DateTime::RFC3339 ),
201 379 'timeZone' => $timezone,
202 380 ],
203 381 'end' => [
204 - 'dateTime' => $end_date_time,
382 + 'dateTime' => $end_datetime->format( \DateTime::RFC3339 ),
205 383 'timeZone' => $timezone,
206 384 ],
207 385 ];
208 -
209 - return $date_time;
210 386 }
211 387
212 388 /**
213 389 * Convet 12 hours format to 24 hours format
@@ -216,22 +392,11 @@
216 392 *
217 393 * @return string
218 394 */
219 395 public function convertTo24HourFormat( $time ) {
220 - $hours = intval( substr( $time, 0, 2 ) );
221 - $minutes = intval( substr( $time, 3, 2 ) );
222 - $seconds = intval( substr( $time, 6, 2 ) );
223 - $ampm = substr( $time, 9 );
224 -
225 - if ( strtolower( $ampm ) == 'pm' && $hours < 12 ) {
226 - $hours += 12;
227 - }
228 -
229 - if ( strtolower( $ampm ) == "am" && $hours == 12 ) {
230 - $hours = 0;
231 - }
232 -
233 - return sprintf( "%02d:%02d:%02d", $hours, $minutes, $seconds );
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 ) );
234 399 }
235 400
236 401 /**
237 402 * Prepare event create requested data
@@ -243,10 +408,11 @@
243 408 private function prepare_request_data( $args = [] ) {
244 409 $access_token = $args['access_token'];
245 410 $date = $this->prepare_time(
246 411 [
247 - 'start' => $args['start'],
248 - 'end' => $args['end'],
412 + 'start' => $args['start'],
413 + 'end' => $args['end'],
414 + 'timezone' => $args['timezone'],
249 415 ],
250 416 $access_token
251 417 );
252 418
@@ -253,11 +419,16 @@
253 419 $args['start'] = [$date['start']];
254 420 $args['end'] = [$date['end']];
255 421
256 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 +
257 428 $args['conferenceData'] = [
258 429 'createRequest' => [
259 - 'requestId' => 'sample123',
430 + 'requestId' => $request_id,
260 431 'conferenceSolutionKey' => ['type' => 'hangoutsMeet'],
261 432 ],
262 433 ];
263 434 }
@@ -267,9 +438,9 @@
267 438 'headers' => [
268 439 'Authorization' => 'Bearer ' . $access_token,
269 440 'Content-Type' => 'application/json; charset=utf-8',
270 441 ],
271 - 'body' => json_encode( $args ),
442 + 'body' => wp_json_encode( $args ),
272 443 ];
273 444
274 445 return $data;
275 446 }