| @@ -5,10 +5,8 @@ | ||
| 5 | 5 | * @package Timetics |
| 6 | 6 | */ |
| 7 | 7 | namespace Timetics\Core\Integrations\Google\Service; |
| 8 | 8 | |
| 9 | -defined( 'ABSPATH' ) || exit; | |
| 10 | - | |
| 11 | 9 | /** |
| 12 | 10 | * Class Calendar |
| 13 | 11 | */ |
| 14 | 12 | class Calendar { |
| @@ -23,24 +21,8 @@ | ||
| 23 | 21 | * @param array $api_filters Additional API filters for google calendar API |
| 24 | 22 | * |
| 25 | 23 | * @return array List of calendar events. |
| 26 | 24 | */ |
| 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 | 25 | public function get_events( $user_id, $api_filters = array() ) { |
| 44 | 26 | $access_token = timetics_get_google_access_token($user_id); |
| 45 | 27 | |
| 46 | 28 | if ( ! $access_token ) { |
| @@ -46,22 +28,15 @@ | ||
| 46 | 28 | if ( ! $access_token ) { |
| 47 | 29 | return ['error' => 'Access token not found or expired.']; |
| 48 | 30 | } |
| 49 | 31 | |
| 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' ) ); | |
| 32 | + // Define the time range for the last 3 months | |
| 33 | + $three_months_ago = date('c', strtotime('-3 months')); | |
| 34 | + $three_months_ahead = date('c', strtotime('+3 months')); // 3 months ahead date-time in RFC3339 format | |
| 60 | 35 | |
| 61 | 36 | $filters = array( |
| 62 | - 'timeMin' => $three_months_ago, | |
| 63 | - 'timeMax' => $three_months_ahead, | |
| 37 | + 'timeMin' => rawurlencode($three_months_ago), | |
| 38 | + 'timeMax' => rawurlencode($three_months_ahead), | |
| 64 | 39 | 'orderBy' => 'startTime', |
| 65 | 40 | 'singleEvents' => 'true', |
| 66 | 41 | ); |
| 67 | 42 | |
| @@ -98,60 +73,28 @@ | ||
| 98 | 73 | if ( empty( $event['start'] ) ) { |
| 99 | 74 | continue; |
| 100 | 75 | } |
| 101 | 76 | |
| 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'] ); | |
| 77 | + $start = ! empty($event['start']['dateTime']) ? $event['start']['dateTime'] : $event['start']['date']; | |
| 78 | + $end = ! empty($event['end']['dateTime']) ? $event['end']['dateTime'] : $event['end']['date']; | |
| 109 | 79 | |
| 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(); | |
| 80 | + $timezone = $event['start']['timeZone'] ?? wp_timezone_string(); | |
| 129 | 81 | $timezone = new \DateTimeZone( $timezone ); |
| 130 | 82 | |
| 131 | 83 | $start_dt = new \DateTime( $start ); |
| 132 | - $end_dt = new \DateTime( $end ); | |
| 84 | + $start_dt->setTimezone( $timezone ); | |
| 133 | 85 | |
| 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 ); | |
| 86 | + $end_dt = new \DateTime( $end ); | |
| 140 | 87 | $end_dt->setTimezone( $timezone ); |
| 141 | 88 | |
| 142 | 89 | $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'] ?? '', | |
| 90 | + 'id' => $event['id'] ?? '', | |
| 91 | + 'start_date' => $start_dt->format( 'Y-m-d' ), | |
| 92 | + 'start_time' => $start_dt->format( 'H:i:s' ), | |
| 93 | + 'end_date' => $end_dt->format( 'Y-m-d' ), | |
| 94 | + 'end_time' => $end_dt->format( 'H:i:s' ), | |
| 95 | + 'summary' => $event['summary'] ?? '', | |
| 96 | + 'description'=> $event['description'] ?? '', | |
| 154 | 97 | ]; |
| 155 | 98 | } |
| 156 | 99 | |
| 157 | 100 | return $filtered_events; |
| @@ -366,24 +309,26 @@ | ||
| 366 | 309 | $start_date = isset( $data['start']['date'] ) ? $data['start']['date'] : gmdate( 'Y-m-d' ); |
| 367 | 310 | $start_time = isset( $data['start']['time'] ) ? $data['start']['time'] : gmdate( 'H:i:s' ); |
| 368 | 311 | $end_date = isset( $data['end']['date'] ) ? $data['end']['date'] : gmdate( 'Y-m-d' ); |
| 369 | 312 | $end_time = isset( $data['end']['time'] ) ? $data['end']['time'] : gmdate( 'H:i:s' ); |
| 370 | - $timezone = isset( $data['timezone'] ) ? $data['timezone'] : timetics_wp_timezone_string(); | |
| 313 | + $timezone = isset( $data['timezone'] ) ? $data['timezone'] : 'Asia/Dhaka'; | |
| 314 | + $timezone_offset = $this->get_timezone_offset( $timezone ); | |
| 371 | 315 | |
| 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 ) ); | |
| 316 | + $start_date_time = $start_date . 'T' . $this->convertTo24HourFormat( $start_time ) . $timezone_offset; | |
| 317 | + $end_date_time = $end_date . 'T' . $this->convertTo24HourFormat( $end_time ) . $timezone_offset; | |
| 375 | 318 | |
| 376 | - return [ | |
| 319 | + $date_time = [ | |
| 377 | 320 | 'start' => [ |
| 378 | - 'dateTime' => $start_datetime->format( \DateTime::RFC3339 ), | |
| 321 | + 'dateTime' => $start_date_time, | |
| 379 | 322 | 'timeZone' => $timezone, |
| 380 | 323 | ], |
| 381 | 324 | 'end' => [ |
| 382 | - 'dateTime' => $end_datetime->format( \DateTime::RFC3339 ), | |
| 325 | + 'dateTime' => $end_date_time, | |
| 383 | 326 | 'timeZone' => $timezone, |
| 384 | 327 | ], |
| 385 | 328 | ]; |
| 329 | + | |
| 330 | + return $date_time; | |
| 386 | 331 | } |
| 387 | 332 | |
| 388 | 333 | /** |
| 389 | 334 | * Convet 12 hours format to 24 hours format |
| @@ -392,11 +337,9 @@ | ||
| 392 | 337 | * |
| 393 | 338 | * @return string |
| 394 | 339 | */ |
| 395 | 340 | 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 ) ); | |
| 341 | + return date('H:i:s', strtotime( $time ) ); | |
| 399 | 342 | } |
| 400 | 343 | |
| 401 | 344 | /** |
| 402 | 345 | * Prepare event create requested data |
| @@ -419,16 +362,11 @@ | ||
| 419 | 362 | $args['start'] = [$date['start']]; |
| 420 | 363 | $args['end'] = [$date['end']]; |
| 421 | 364 | |
| 422 | 365 | 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 | 366 | $args['conferenceData'] = [ |
| 429 | 367 | 'createRequest' => [ |
| 430 | - 'requestId' => $request_id, | |
| 368 | + 'requestId' => 'sample123', | |
| 431 | 369 | 'conferenceSolutionKey' => ['type' => 'hangoutsMeet'], |
| 432 | 370 | ], |
| 433 | 371 | ]; |
| 434 | 372 | } |
| @@ -438,9 +376,9 @@ | ||
| 438 | 376 | 'headers' => [ |
| 439 | 377 | 'Authorization' => 'Bearer ' . $access_token, |
| 440 | 378 | 'Content-Type' => 'application/json; charset=utf-8', |
| 441 | 379 | ], |
| 442 | - 'body' => wp_json_encode( $args ), | |
| 380 | + 'body' => json_encode( $args ), | |
| 443 | 381 | ]; |
| 444 | 382 | |
| 445 | 383 | return $data; |
| 446 | 384 | } |