/home/edulekha/crm.edulekha.com/modules/appointly/models/Googlecalendar.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Googlecalendar extends App_Model
{
public $table;
protected $googleReminders;
public $unauthorized_url_settings;
public $calendar;
public function __construct()
{
parent::__construct();
$this->table = db_prefix() . 'appointly_google';
// Ensure database structure is correct for all tenants (auto-migration for multi-tenant systems)
$this->ensureGoogleTokenColumnsAreLongtext();
$this->load->library('appointly/googleplus');
$this->unauthorized_url_settings = '<h3>Seems that something is wrong, please check that your Client Secret is correctly inserted in <a href="' . admin_url('settings?group=appointly-settings') . '">Setup->Settings->Appointments</a></h3>';
$this->googleReminders = [
'useDefault' => false,
'overrides' => [
['method' => 'email', 'minutes' => 24 * 60],
['method' => 'popup', 'minutes' => 60],
],
];
/**
* Init Google Calendar Service instance
*/
$this->calendar = new Google\Service\Calendar($this->googleplus->client());
}
/**
* Auto-migrate token columns to LONGTEXT for multi-tenant systems
*
* Modern OAuth tokens can exceed VARCHAR(191) limit causing "Data too long" errors.
* This method automatically upgrades existing installations without requiring manual intervention.
*
* @return void
*/
private function ensureGoogleTokenColumnsAreLongtext()
{
try {
if ($this->db->table_exists($this->table)) {
$columnInfo = $this->db->query("SHOW COLUMNS FROM {$this->table} LIKE 'access_token'")->row_array();
// Check if we need to upgrade from VARCHAR to LONGTEXT
if ($columnInfo && stripos($columnInfo['Type'], 'varchar') !== false) {
$this->db->query("ALTER TABLE {$this->table}
MODIFY COLUMN access_token LONGTEXT,
MODIFY COLUMN refresh_token LONGTEXT");
log_message('info', 'GoogleCalendar: Successfully upgraded token columns to LONGTEXT for ' . $this->table);
}
}
} catch (Exception $e) {
log_message('error', 'GoogleCalendar: Failed to ensure LONGTEXT columns for ' . $this->table . ' — ' . $e->getMessage());
}
}
/**
* Google OAuth Login Url
*
* @return void
*/
public function loginUrl()
{
return $this->googleplus
->loginUrl();
}
/**
* Google OAuth Login validation
*
* @return bool
*/
public function login($code)
{
try {
$login = $this->googleplus
->client
->authenticate($code);
} catch (GuzzleHttp\Exception\ClientException $e) {
if ($e->getCode() === 401) {
die($this->unauthorized_url_settings);
}
}
if (isset($login['error_description']) && $login['error_description'] == 'Unauthorized') {
die($this->unauthorized_url_settings);
}
if ($login) {
// Safely trim long tokens to prevent data truncation errors
// LONGTEXT supports up to 4GB, but we limit to 65,000 chars for safety
$access_token = isset($login['access_token']) ? substr($login['access_token'], 0, 65000) : null;
$refresh_token = isset($login['refresh_token']) ? substr($login['refresh_token'], 0, 65000) : null;
$expires_in = isset($login['expires_in']) ? time() + (int)$login['expires_in'] : time();
$this->db->where('staff_id', get_staff_user_id());
$result = $this->db->get($this->table)->row_array();
if ($result) {
// Update existing record
$this->db->where('staff_id', get_staff_user_id());
$this->db->update($this->table, [
'access_token' => $access_token,
'expires_in' => $expires_in,
'refresh_token' => $refresh_token,
]);
} else {
// Insert new record
$insertData = [
'staff_id' => get_staff_user_id(),
'access_token' => $access_token,
'expires_in' => $expires_in,
];
// Only add refresh_token if it exists
if ($refresh_token) {
$insertData['refresh_token'] = $refresh_token;
}
$this->db->insert($this->table, $insertData);
}
$token = $this->googleplus
->client
->getAccessToken();
$this->googleplus
->client
->setAccessToken($token);
$this->saveNewTokenValues($token);
return true;
}
return false;
}
/**
* Google get email user info
*
* @return array
*/
public function getUserInfo()
{
return $this->googleplus->getUser();
}
/**
* Google Calendar delete event
*
* @param $eventid
* @return bool
*/
public function deleteEvent($eventid)
{
try {
// Check if we need to refresh the token
if ($this->googleplus->isAccessTokenExpired()) {
$refreshToken = $this->googleplus->getTokenType('refresh_token');
if ($refreshToken) {
$newToken = $this->googleplus->refreshToken($refreshToken);
$this->googleplus->setAccessToken($newToken);
} else {
log_message('error', 'Google Calendar: No refresh token available for token refresh');
return false;
}
}
// First check if the event exists
try {
$event = $this->calendar->events->get('primary', $eventid);
// Log successful retrieval
log_message('debug', 'Google Calendar: Successfully found event ' . $eventid . ' before deletion');
// Set sendUpdates parameter based on settings
// Default to 'none' to prevent duplicate emails (Perfex already sends notifications)
$params = [];
if (get_option('appointly_disable_google_meeting_emails') == '1' || get_option('appointly_disable_google_meeting_emails') === false || get_option('appointly_disable_google_meeting_emails') === '') {
$params['sendUpdates'] = 'none';
} else {
$params['sendUpdates'] = 'all';
}
// If we get here, the event exists, so try to delete it
$deleteResult = $this->calendar->events->delete('primary', $eventid, $params);
// Log success
log_message('debug', 'Google Calendar: Successfully deleted event ' . $eventid);
return true;
} catch (Exception $innerException) {
// If event not found
if ($innerException->getCode() === 404) {
log_message('debug', 'Google Calendar: Event not found when trying to delete: ' . $eventid);
// Consider returning true here instead of false, since the end result is the same - event doesn't exist
return true;
}
// Log other exceptions
log_message('error', 'Google Calendar inner exception: ' . $innerException->getMessage());
throw $innerException;
}
} catch (Exception $e) {
log_message('error', 'Google Calendar: Error deleting event: ' . $e->getMessage());
return false;
}
}
/**
* Google Calendar get events
*
* @param string $calendarId
* @param bool $timeMin
* @param bool $timeMax
* @param int $maxResults
* @return array
*/
public function getEvents($calendarId = 'primary', $timeMin = false, $timeMax = false, $maxResults = 200)
{
// Respect the date range filter setting
$dateRangeSetting = get_option('appointments_googlesync_show_from', 'last_3_months');
if (! $timeMin) {
switch ($dateRangeSetting) {
case 'today':
$timeMin = date("c", strtotime(date('Y-m-d') . ' 00:00:00'));
break;
case 'last_month':
$timeMin = date("c", strtotime("-1 month", strtotime(date('Y-m-d') . ' 00:00:00')));
break;
case 'last_3_months':
$timeMin = date("c", strtotime("-3 months", strtotime(date('Y-m-d') . ' 00:00:00')));
break;
case 'all':
// Fetch all events (up to API limits) - go back 5 years
$timeMin = date("c", strtotime("-5 years", strtotime(date('Y-m-d') . ' 00:00:00')));
break;
default:
// Default to 3 months
$timeMin = date("c", strtotime("-3 months", strtotime(date('Y-m-d') . ' 00:00:00')));
break;
}
} else {
$timeMin = date("c", strtotime($timeMin));
}
if (! $timeMax) {
// Always fetch future events up to 2 years ahead
$timeMax = date("c", strtotime("+2 years", strtotime(date('Y-m-d') . ' 23:59:59')));
} else {
$timeMax = date("c", strtotime($timeMax));
}
// Increase maxResults for 'all' setting to fetch more events
if ($dateRangeSetting === 'all' && $maxResults === 200) {
$maxResults = 2500; // Google Calendar API max is 2500
}
$optParams = [
'maxResults' => $maxResults,
'orderBy' => 'startTime',
'singleEvents' => true,
'timeMin' => $timeMin,
'timeMax' => $timeMax,
'timeZone' => get_option('default_timezone'),
];
try {
$results = $this->calendar->events->listEvents($calendarId, $optParams);
$data = [];
foreach ($results->getItems() as $item) {
// Get the correct date/time format
$start = $item->getStart();
$end = $item->getEnd();
// Make sure we can safely access dateTime
$startDateTime = isset($start->dateTime) ? $start->dateTime : (isset($start->date) ? $start->date : null);
$endDateTime = isset($end->dateTime) ? $end->dateTime : (isset($end->date) ? $end->date : null);
if ($startDateTime) {
array_push($data, [
'id' => $item->getId(),
'summary' => $item->getSummary() ?? 'Untitled Event',
'description' => $item->getDescription() ?? '',
'creator' => $item->getCreator(),
'start' => $startDateTime,
'end' => $endDateTime,
'htmlLink' => $item->getHtmlLink(),
'recurringEventId' => $item->getRecurringEventId(),
'status' => $item->getStatus(),
'eventType' => $item->getEventType(),
'location' => $item->getLocation()
]);
}
}
return $data;
} catch (Exception $e) {
log_message('error', 'Google Calendar: Error fetching events: ' . $e->getMessage());
return [];
}
}
/**
* Google Calendar add new event
*
* @param string $calendarId
* @param $data
* @return Google_Service_Calendar_Event
*/
public function addEvent($data, $calendarId = 'primary')
{
$event = $this->fillGoogleCalendarEvent($data);
// Set sendUpdates parameter in the API call based on settings
// Default to 'none' to prevent duplicate emails (Perfex already sends notifications)
$params = ['conferenceDataVersion' => 1];
if (get_option('appointly_disable_google_meeting_emails') == '1' || get_option('appointly_disable_google_meeting_emails') === false || get_option('appointly_disable_google_meeting_emails') === '') {
$params['sendUpdates'] = 'none';
} else {
$params['sendUpdates'] = 'all';
}
return $this->calendar->events->insert($calendarId, $event, $params);
}
/**
* Google Calendar update existing event
*
* @param $eventid
* @param $data
* @return Google\Service\Calendar\Event
*/
public function updateEvent($eventid, $data)
{
$event = $this->fillGoogleCalendarEvent($data);
// Set sendUpdates parameter in the API call based on settings
// Default to 'none' to prevent duplicate emails (Perfex already sends notifications)
$params = [];
if (get_option('appointly_disable_google_meeting_emails') == '1' || get_option('appointly_disable_google_meeting_emails') === false || get_option('appointly_disable_google_meeting_emails') === '') {
$params['sendUpdates'] = 'none';
} else {
$params['sendUpdates'] = 'all';
}
return $this->calendar->events->update('primary', $eventid, $event, $params);
}
/**
* Manage data and fill google calendar event array
*
* @param $data
* @return Google\Service\Calendar\Event
*/
public function fillGoogleCalendarEvent($data)
{
$event = new Google\Service\Calendar\Event(
[
'summary' => $data['summary'],
'description' => $data['description'],
'location' => ($data['location']) ? $data['location'] : '',
'start' => $data['start'],
'end' => $data['end'],
'attendees' => (array) $data['attendees'],
'reminders' => $this->googleReminders,
]
);
if (get_option('appointly_auto_enable_google_meet') == '1') {
$event->setConferenceData(new Google\Service\Calendar\ConferenceData([
'createRequest' => new Google\Service\Calendar\CreateConferenceRequest([
'requestId' => uniqid(),
'conferenceSolutionKey' => new Google\Service\Calendar\ConferenceSolutionKey([
'type' => 'hangoutsMeet'
])
])
]));
}
return $event;
}
/**
* Get logged in Google account details
*
* @return mixed
*/
public function getAccountDetails()
{
$this->db->select();
$this->db->where('staff_id', get_staff_user_id());
$result = $this->db->get($this->table)->result();
if ($result) {
return $result;
}
return false;
}
/**
* Google save / update new token values in database
*
* @param $data
* @return void
*/
public function saveNewTokenValues($data)
{
// Safely trim long tokens to prevent data truncation errors
$access_token = isset($data['access_token']) ? substr($data['access_token'], 0, 65000) : null;
$refresh_token = isset($data['refresh_token']) ? substr($data['refresh_token'], 0, 65000) : null;
$expires_in = isset($data['expires_in']) ? time() + (int)$data['expires_in'] : time();
$this->db->where('staff_id', get_staff_user_id());
$this->db->update(
$this->table,
[
'refresh_token' => $refresh_token,
'access_token' => $access_token,
'expires_in' => $expires_in
]
);
}
}