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 +81 -20 1.0.58 → 1.0.64 View file →
@@ -5,8 +5,10 @@
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 {
@@ -21,8 +23,24 @@
21 23 * @param array $api_filters Additional API filters for google calendar API
22 24 *
23 25 * @return array List of calendar events.
24 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 +
25 43 public function get_events( $user_id, $api_filters = array() ) {
26 44 $access_token = timetics_get_google_access_token($user_id);
27 45
28 46 if ( ! $access_token ) {
@@ -28,16 +46,22 @@
28 46 if ( ! $access_token ) {
29 47 return ['error' => 'Access token not found or expired.'];
30 48 }
31 49
32 - // Define the time range for the last 3 months
33 - // Use gmdate() for API queries to ensure UTC timestamps.
34 - $three_months_ago = gmdate( 'c', strtotime( '-3 months' ) );
35 - $three_months_ahead = gmdate( 'c', strtotime( '+3 months' ) ); // 3 months ahead date-time in RFC3339 format
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' ) );
36 60
37 61 $filters = array(
38 - 'timeMin' => rawurlencode($three_months_ago),
39 - 'timeMax' => rawurlencode($three_months_ahead),
62 + 'timeMin' => $three_months_ago,
63 + 'timeMax' => $three_months_ahead,
40 64 'orderBy' => 'startTime',
41 65 'singleEvents' => 'true',
42 66 );
43 67
@@ -74,28 +98,60 @@
74 98 if ( empty( $event['start'] ) ) {
75 99 continue;
76 100 }
77 101
78 - $start = ! empty($event['start']['dateTime']) ? $event['start']['dateTime'] : $event['start']['date'];
79 - $end = ! empty($event['end']['dateTime']) ? $event['end']['dateTime'] : $event['end']['date'];
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'] );
80 109
81 - $timezone = $event['start']['timeZone'] ?? wp_timezone_string();
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();
82 129 $timezone = new \DateTimeZone( $timezone );
83 130
84 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 +
85 139 $start_dt->setTimezone( $timezone );
86 -
87 - $end_dt = new \DateTime( $end );
88 140 $end_dt->setTimezone( $timezone );
89 141
90 142 $filtered_events[] = [
91 - 'id' => $event['id'] ?? '',
92 - 'start_date' => $start_dt->format( 'Y-m-d' ),
93 - 'start_time' => $start_dt->format( 'H:i:s' ),
94 - 'end_date' => $end_dt->format( 'Y-m-d' ),
95 - 'end_time' => $end_dt->format( 'H:i:s' ),
96 - 'summary' => $event['summary'] ?? '',
97 - 'description'=> $event['description'] ?? '',
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'] ?? '',
98 154 ];
99 155 }
100 156
101 157 return $filtered_events;
@@ -310,9 +366,9 @@
310 366 $start_date = isset( $data['start']['date'] ) ? $data['start']['date'] : gmdate( 'Y-m-d' );
311 367 $start_time = isset( $data['start']['time'] ) ? $data['start']['time'] : gmdate( 'H:i:s' );
312 368 $end_date = isset( $data['end']['date'] ) ? $data['end']['date'] : gmdate( 'Y-m-d' );
313 369 $end_time = isset( $data['end']['time'] ) ? $data['end']['time'] : gmdate( 'H:i:s' );
314 - $timezone = isset( $data['timezone'] ) ? $data['timezone'] : wp_timezone_string();
370 + $timezone = isset( $data['timezone'] ) ? $data['timezone'] : timetics_wp_timezone_string();
315 371
316 372 // Create DateTime objects with proper timezone to avoid double conversion.
317 373 $start_datetime = new \DateTime( $start_date . ' ' . $start_time, new \DateTimeZone( $timezone ) );
318 374 $end_datetime = new \DateTime( $end_date . ' ' . $end_time, new \DateTimeZone( $timezone ) );
@@ -363,11 +419,16 @@
363 419 $args['start'] = [$date['start']];
364 420 $args['end'] = [$date['end']];
365 421
366 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 +
367 428 $args['conferenceData'] = [
368 429 'createRequest' => [
369 - 'requestId' => 'sample123',
430 + 'requestId' => $request_id,
370 431 'conferenceSolutionKey' => ['type' => 'hangoutsMeet'],
371 432 ],
372 433 ];
373 434 }