# timetics/1.0.63/core/integrations/google/service/calendar.php

Timetics – Appointment Booking Calendar &amp; Scheduling, version 1.0.63. 457 lines.

- Page: https://pluginprobe.com/plugins/timetics/1.0.63/code/core/integrations/google/service/calendar.php
- Raw: https://pluginprobe.com/plugins/timetics/1.0.63/raw/core/integrations/google/service/calendar.php
- Modified: 2026-09-21T08:26:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/timetics/1.0.63/code/core/integrations/google/service/calendar.php#L10-L20`.

```php
<?php
/**
 * Google Calendar Class
 *
 * @package Timetics
 */
namespace Timetics\Core\Integrations\Google\Service;

defined( 'ABSPATH' ) || exit;

/**
 * Class Calendar
 */
class Calendar {
    const TIMETICS_TIMEZONE_URI   = 'https://www.googleapis.com/calendar/v3/users/me/settings/timezone';
    const TIMETICS_CALENDAR_EVENT = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';


    /**
     * Get events from the calendar for the last 3 months.
     *
     * @param int $user_id Team member ID.
     * @param array $api_filters Additional API filters for google calendar API
     *
     * @return array List of calendar events.
     */
    /**
     * Format an instant as RFC3339 in UTC ("...Z").
     *
     * Google rejects bounds whose "+hh:mm" offset reaches it unescaped, so
     * every timeMin/timeMax the plugin sends goes through here.
     *
     * @param int|\DateTimeInterface $when Timestamp or date object.
     *
     * @return string
     */
    public static function to_rfc3339_utc( $when ) {
        $timestamp = $when instanceof \DateTimeInterface ? $when->getTimestamp() : (int) $when;

        return gmdate( 'Y-m-d\TH:i:s\Z', $timestamp );
    }

    public function get_events( $user_id, $api_filters = array() ) {
        $access_token = timetics_get_google_access_token($user_id);

        if ( ! $access_token ) {
            return ['error' => 'Access token not found or expired.'];
        }

        // Define the time range for the last 3 months.
        //
        // Always formatted as UTC with a trailing "Z" rather than an offset:
        // an offset like "+06:00" carries a plus sign that survives into the
        // query string, where Google reads it as a space. The bounds then fail
        // to parse and the API answers with no items at all — which this class
        // cannot distinguish from "no events", so every event silently
        // disappeared. "Z" sidesteps the escaping problem entirely.
        $three_months_ago   = self::to_rfc3339_utc( strtotime( '-3 months' ) );
        $three_months_ahead = self::to_rfc3339_utc( strtotime( '+3 months' ) );

        $filters = array(
            'timeMin' => $three_months_ago,
            'timeMax' => $three_months_ahead,
            'orderBy' => 'startTime',
            'singleEvents' => 'true',
        );

        // add additional api filters if needed
        $filters = array_merge( $filters, $api_filters );

        // API URL with time range filter
        $api_url = add_query_arg( $filters, self::TIMETICS_CALENDAR_EVENT);

        // Set headers
        $args = [
            'headers' => [
                'Authorization' => 'Bearer ' . $access_token,
                'Content-Type'  => 'application/json',
            ],
        ];

        // Fetch data from Google Calendar API
        $response = wp_remote_get( $api_url, $args );
        if ( is_wp_error( $response ) ) {
            return [];
        }

        $body   = wp_remote_retrieve_body( $response );
        $events = json_decode($body, true);

        if ( empty( $events['items'] ) ) {
            return [];
        }

        // Filter required fields
        $filtered_events = [];
        foreach ( $events['items'] as $event ) {
            if ( empty( $event['start'] ) ) {
                continue;
            }
            
            // Google sends `dateTime` for timed events and a date-only `date`
            // for all-day ones. All-day bounds have no time and no timezone, so
            // they must not be run through setTimezone() — that shifts the
            // wall-clock and used to collapse them to 00:00:00-00:00:00, which
            // blocked nothing at all. Note Google's all-day end date is
            // EXCLUSIVE: a single day off is 08-10 to 08-11.
            $all_day = empty( $event['start']['dateTime'] );

            $start = $all_day ? $event['start']['date'] : $event['start']['dateTime'];
            $end   = $all_day ? $event['end']['date'] : $event['end']['dateTime'];

            if ( $all_day ) {
                $filtered_events[] = [
                    'id'          => $event['id'] ?? '',
                    'all_day'     => true,
                    'start_date'  => $start,
                    'start_time'  => '00:00:00',
                    'end_date'    => $end,
                    'end_time'    => '00:00:00',
                    'summary'     => $event['summary'] ?? '',
                    'description' => $event['description'] ?? '',
                ];

                continue;
            }

            $timezone = $event['start']['timeZone'] ?? timetics_wp_timezone_string();
            $timezone = new \DateTimeZone( $timezone );

            $start_dt = new \DateTime( $start );
            $end_dt   = new \DateTime( $end );

            // Absolute instants, captured before the display conversion below,
            // so overlap maths never has to reason about wall-clock strings.
            $start_timestamp = $start_dt->getTimestamp();
            $end_timestamp   = $end_dt->getTimestamp();

            $start_dt->setTimezone( $timezone );
            $end_dt->setTimezone( $timezone );

            $filtered_events[] = [
                'id'              => $event['id'] ?? '',
                'all_day'         => false,
                'start_date'      => $start_dt->format( 'Y-m-d' ),
                'start_time'      => $start_dt->format( 'H:i:s' ),
                'end_date'        => $end_dt->format( 'Y-m-d' ),
                'end_time'        => $end_dt->format( 'H:i:s' ),
                'start_timestamp' => $start_timestamp,
                'end_timestamp'   => $end_timestamp,
                'timezone'        => $timezone->getName(),
                'summary'         => $event['summary'] ?? '',
                'description'     => $event['description'] ?? '',
            ];
        }

        return $filtered_events;
    }

    /**
     * Get event by ID
     *
     * @param   string  $event_id
     *
     * @return JSON | WP_Error
     */
    public function get_event( $event_id , $user_id = null ) {
        if ( ! $user_id ) {
            $user_id = get_current_user_id();
        }

        $access_token = timetics_get_google_access_token( $user_id );

        $data = [
            'headers' => [
                'Authorization' => 'Bearer ' . $access_token,
            ],
        ];

        $response = wp_remote_get(self::TIMETICS_CALENDAR_EVENT . '/' . $event_id, $data);

        if ( is_wp_error( $response ) ) {
            return ['error' => $response->get_error_message()];
        }

        $body   = wp_remote_retrieve_body( $response );
        $event  = json_decode($body, true);

        return $event;
    }

    /**
     * Create event
     *
     * @param   array  $args  Event data
     *
     * @return JSON | WP_Error
     */
    public function create_event( $args = [] ) {
        $defaults = [
            'summary'      => '',
            'description'  => '',
            'location'     => '',
            'start'        => '',
            'end'          => '',
            'attendees'    => [],
            'google_meet'  => true,
            'access_token' => '',
        ];

        $args = apply_filters( 'timetics/booking/create/google-event', $args);

        $args = wp_parse_args( $args, $defaults );
        $data = $this->prepare_request_data( $args );

        $query_params = build_query( [
            'conferenceDataVersion' => '1',
            // 'sendUpdates'           => 'all',
        ] );

        $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '?' . $query_params, $data );

        if ( is_wp_error( $response ) ) {
            return false;
        }

        $status_code = wp_remote_retrieve_response_code( $response );

        if ( 200 != $status_code ) {
            return false;
        }

        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        return $data;
    }

    /**
     * Update calender event
     *
     * @param   array  $args
     *
     * @return array
     */
    public function update_event( $event_id, $args ) {
        $defaults = [
            'summary'      => '',
            'description'  => '',
            'location'     => '',
            'start'        => '',
            'end'          => '',
            'attendees'    => [],
            'google_meet'  => true,
            'access_token' => '',
            'method'       => 'PUT',
        ];

        $args         = wp_parse_args( $args, $defaults );
        $query_params = build_query( [
            'conferenceDataVersion' => '1',
            'sendUpdates'           => 'all',
        ] );

        $data           = $this->prepare_request_data( $args );
        $data['method'] = 'PUT';

        $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, $data );

        if ( is_wp_error( $response ) ) {
            return false;
        }

        $status_code = wp_remote_retrieve_response_code( $response );

        if ( 200 != $status_code ) {
            return false;
        }

        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        return $data;
    }

    /**
     * Get timzeson
     *
     * @return  string | WP_Error
     */
    public function get_timezone( $access_token ) {
        $data = [
            'headers' => [
                'Authorization' => 'Bearer ' . $access_token,
            ],
        ];

        $response = wp_remote_get( self::TIMETICS_TIMEZONE_URI, $data );

        if ( ! is_wp_error( $response ) ) {
            $data = json_decode( wp_remote_retrieve_body( $response ), true );
            return $data['value'];
        }

        return $response;
    }

    /**
     * Delete google calendar event
     *
     * @param   string  $event_id
     *
     * @return array
     */
    public function delete_event( $event_id, $access_token ) {
        $query_params = build_query( [
            'conferenceDataVersion' => '1',
            'sendUpdates'           => 'all',
        ] );

        $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, [
            'headers' => [
                'Authorization' => 'Bearer ' . $access_token,
                'Content-Type'  => 'application/json; charset=utf-8',
            ],
            'method'  => 'DELETE',
        ] );

        if ( is_wp_error( $response ) ) {
            return false;
        }

        $status_code = wp_remote_retrieve_response_code( $response );

        if ( 200 != $status_code ) {
            return false;
        }

        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        return $data;
    }

    /**
     * Get timezone offset
     *
     * @param   string  $timezone
     *
     * @return string
     */
    public function get_timezone_offset( $timezone ) {
        $current       = timezone_open( $timezone );
        $utc_time      = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
        $offset_insecs = timezone_offset_get( $current, $utc_time );
        $hours_and_sec = gmdate( 'H:i', abs( $offset_insecs ) );

        return stripos( $offset_insecs, '-' ) === false ? "+{$hours_and_sec}" : "-{$hours_and_sec}";
    }

    /**
     * Prepare time for calendar event
     *
     * @param   array  $data
     *
     * @return array
     */
    private function prepare_time( $data, $access_token ) {
        $start_date = isset( $data['start']['date'] ) ? $data['start']['date'] : gmdate( 'Y-m-d' );
        $start_time = isset( $data['start']['time'] ) ? $data['start']['time'] : gmdate( 'H:i:s' );
        $end_date   = isset( $data['end']['date'] ) ? $data['end']['date'] : gmdate( 'Y-m-d' );
        $end_time   = isset( $data['end']['time'] ) ? $data['end']['time'] : gmdate( 'H:i:s' );
        $timezone   = isset( $data['timezone'] ) ? $data['timezone'] : timetics_wp_timezone_string();

        // Create DateTime objects with proper timezone to avoid double conversion.
        $start_datetime = new \DateTime( $start_date . ' ' . $start_time, new \DateTimeZone( $timezone ) );
        $end_datetime   = new \DateTime( $end_date . ' ' . $end_time, new \DateTimeZone( $timezone ) );

        return [
            'start' => [
                'dateTime' => $start_datetime->format( \DateTime::RFC3339 ),
                'timeZone' => $timezone,
            ],
            'end'   => [
                'dateTime' => $end_datetime->format( \DateTime::RFC3339 ),
                'timeZone' => $timezone,
            ],
        ];
    }

    /**
     * Convet 12 hours format to 24 hours format
     *
     * @param   string  $time
     *
     * @return string
     */
    public function convertTo24HourFormat( $time ) {
        // Use gmdate() instead of wp_date() to avoid timezone conversion
        // since we're building an RFC3339 datetime string with explicit timezone offset.
        return gmdate( 'H:i:s', strtotime( $time ) );
    }

    /**
     * Prepare event create requested data
     *
     * @param   array  $args
     *
     * @return array
     */
    private function prepare_request_data( $args = [] ) {
        $access_token = $args['access_token'];
        $date         = $this->prepare_time(
            [
                'start'    => $args['start'],
                'end'      => $args['end'],
                'timezone' => $args['timezone'],
            ],
            $access_token
        );

        $args['start'] = [$date['start']];
        $args['end']   = [$date['end']];

        if ( $args['google_meet'] ) {
            // requestId must be unique per request; Google ignores the
            // conference create request (no Meet link generated) if it
            // matches a previously used id.
            $request_id = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'tt-meet-', true );

            $args['conferenceData'] = [
                'createRequest' => [
                    'requestId'             => $request_id,
                    'conferenceSolutionKey' => ['type' => 'hangoutsMeet'],
                ],
            ];
        }

        unset( $args['access_token'] );
        $data = [
            'headers' => [
                'Authorization' => 'Bearer ' . $access_token,
                'Content-Type'  => 'application/json; charset=utf-8',
            ],
            'body'    => wp_json_encode( $args ),
        ];

        return $data;
    }

    /**
     * Get google calendar auth scope
     *
     * @return  string
     */
    public static function scope() {
        return 'https://www.googleapis.com/auth/calendar';
    }
}

```
