easyappointments/application/controllers/Google.php

437 lines
16 KiB
PHP
Raw Permalink Normal View History

<?php defined('BASEPATH') or exit('No direct script access allowed');
2015-10-11 23:21:45 +03:00
/* ----------------------------------------------------------------------------
2022-01-18 15:05:42 +03:00
* Easy!Appointments - Online Appointment Scheduler
2015-10-11 23:21:45 +03:00
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
2021-12-18 19:43:45 +03:00
* @copyright Copyright (c) Alex Tselegidis
2020-11-14 22:36:25 +03:00
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
2015-10-11 23:21:45 +03:00
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
2021-11-06 18:21:27 +03:00
* Google controller.
2015-10-11 23:21:45 +03:00
*
* Handles the Google Calendar synchronization related operations.
2015-12-30 13:02:14 +02:00
*
2015-10-11 23:21:45 +03:00
* @package Controllers
*/
class Google extends EA_Controller
{
/**
* Google constructor.
*/
public function __construct()
{
parent::__construct();
$this->load->library('google_sync');
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('roles_model');
}
2015-10-11 23:21:45 +03:00
/**
* Complete synchronization of appointments between Google Calendar and Easy!Appointments.
*
* This method will completely sync the appointments of a provider with his Google Calendar account. The sync period
* needs to be relatively small, because a lot of API calls might be necessary and this will lead to consuming the
* Google limit for the Calendar API usage.
2015-10-11 23:21:45 +03:00
*/
public static function sync(string $provider_id = null): void
{
try {
/** @var EA_Controller $CI */
$CI = get_instance();
$CI->load->library('google_sync');
2015-10-11 23:21:45 +03:00
2023-10-23 12:21:30 +03:00
// Load the libraries as this method is called statically from the CLI command
$CI->load->model('appointments_model');
$CI->load->model('unavailabilities_model');
$CI->load->model('providers_model');
$CI->load->model('services_model');
$CI->load->model('customers_model');
$CI->load->model('settings_model');
2015-10-11 23:21:45 +03:00
$user_id = session('user_id');
if (!$user_id && !is_cli()) {
return;
}
if (!$provider_id) {
throw new InvalidArgumentException('No provider ID provided.');
}
2015-10-11 23:21:45 +03:00
$provider = $CI->providers_model->find($provider_id);
// Check whether the selected provider has the Google Sync enabled.
$google_sync = $CI->providers_model->get_setting($provider['id'], 'google_sync');
if (!$google_sync) {
return; // The selected provider does not have the Google Sync enabled.
2015-10-11 23:21:45 +03:00
}
$google_token = json_decode($provider['settings']['google_token'], true);
$CI->google_sync->refresh_token($google_token['refresh_token']);
2015-10-11 23:21:45 +03:00
// Fetch provider's appointments that belong to the sync time period.
$sync_past_days = $provider['settings']['sync_past_days'];
$sync_future_days = $provider['settings']['sync_future_days'];
2015-10-11 23:21:45 +03:00
$start = strtotime('-' . $sync_past_days . ' days', strtotime(date('Y-m-d')));
2015-10-11 23:21:45 +03:00
$end = strtotime('+' . $sync_future_days . ' days', strtotime(date('Y-m-d')));
$where = [
2015-10-11 23:21:45 +03:00
'start_datetime >=' => date('Y-m-d H:i:s', $start),
'end_datetime <=' => date('Y-m-d H:i:s', $end),
'id_users_provider' => $provider['id'],
];
2015-10-11 23:21:45 +03:00
$appointments = $CI->appointments_model->get($where);
2015-10-11 23:21:45 +03:00
$unavailabilities = $CI->unavailabilities_model->get($where);
$local_events = [...$appointments, ...$unavailabilities];
$settings = [
'company_name' => setting('company_name'),
'company_link' => setting('company_link'),
'company_email' => setting('company_email'),
];
2015-10-11 23:21:45 +03:00
$provider_timezone = new DateTimeZone($provider['timezone']);
// Sync each appointment with Google Calendar by following the project's sync protocol (see documentation).
foreach ($local_events as $local_event) {
if (!$local_event['is_unavailability']) {
$service = $CI->services_model->find($local_event['id_services']);
$customer = $CI->customers_model->find($local_event['id_users_customer']);
$events_model = $CI->appointments_model;
} else {
$service = null;
$customer = null;
$events_model = $CI->unavailabilities_model;
2015-10-11 23:21:45 +03:00
}
// If current appointment not synced yet, add to Google Calendar.
if (!$local_event['id_google_calendar']) {
if (!$local_event['is_unavailability']) {
$google_event = $CI->google_sync->add_appointment(
$local_event,
$provider,
$service,
$customer,
$settings,
);
} else {
$google_event = $CI->google_sync->add_unavailability($provider, $local_event);
}
$local_event['id_google_calendar'] = $google_event->getId();
$events_model->save($local_event); // Save the Google Calendar ID.
continue;
2018-01-23 12:08:37 +03:00
}
// Appointment is synced with Google Calendar.
try {
$google_event = $CI->google_sync->get_event($provider, $local_event['id_google_calendar']);
2015-10-11 23:21:45 +03:00
if ($google_event->getStatus() == 'cancelled') {
throw new Exception('Event is cancelled, remove the record from Easy!Appointments.');
}
// If Google Calendar event is different from Easy!Appointments appointment then update Easy!Appointments record.
$local_event_start = strtotime($local_event['start_datetime']);
$local_event_end = strtotime($local_event['end_datetime']);
$google_event_start = new DateTime(
$google_event->getStart()->getDateTime() ?? $google_event->getEnd()->getDate(),
);
$google_event_start->setTimezone($provider_timezone);
$google_event_end = new DateTime(
$google_event->getEnd()->getDateTime() ?? $google_event->getEnd()->getDate(),
);
$google_event_end->setTimezone($provider_timezone);
$google_event_notes = $local_event['is_unavailability']
? $google_event->getSummary() . ' ' . $google_event->getDescription()
: $google_event->getDescription();
$is_different =
$local_event_start !== $google_event_start->getTimestamp() ||
$local_event_end !== $google_event_end->getTimestamp() ||
$local_event['notes'] !== $google_event_notes;
if ($is_different) {
$local_event['start_datetime'] = $google_event_start->format('Y-m-d H:i:s');
$local_event['end_datetime'] = $google_event_end->format('Y-m-d H:i:s');
$local_event['notes'] = $google_event_notes;
$events_model->save($local_event);
2015-10-11 23:21:45 +03:00
}
} catch (Throwable) {
// Appointment not found on Google Calendar, delete from Easy!Appointments.
$events_model->delete($local_event['id']);
$local_event['id_google_calendar'] = null;
2015-10-11 23:21:45 +03:00
}
}
// Add Google Calendar events that do not exist in Easy!Appointments.
2015-10-11 23:21:45 +03:00
$google_calendar = $provider['settings']['google_calendar'];
try {
$google_events = $CI->google_sync->get_sync_events($google_calendar, $start, $end);
} catch (Throwable $e) {
if ($e->getCode() === 404) {
log_message('error', 'Google - Remote Calendar not found for provider ID: ' . $provider_id);
return; // The remote calendar was not found.
} else {
throw $e;
}
}
2015-10-11 23:21:45 +03:00
foreach ($google_events->getItems() as $google_event) {
if ($google_event->getStatus() === 'cancelled') {
continue;
}
if ($google_event->getStart() === null || $google_event->getEnd() === null) {
continue;
}
if ($google_event->getStart()->getDateTime() === $google_event->getEnd()->getDateTime()) {
continue;
}
$google_event_start = new DateTime($google_event->getStart()->getDateTime());
$google_event_start->setTimezone($provider_timezone);
$google_event_end = new DateTime($google_event->getEnd()->getDateTime());
$google_event_end->setTimezone($provider_timezone);
$appointment_results = $CI->appointments_model->get(['id_google_calendar' => $google_event->getId()]);
if (!empty($appointment_results)) {
continue;
}
$unavailability_results = $CI->unavailabilities_model->get([
'id_google_calendar' => $google_event->getId(),
]);
if (!empty($unavailability_results)) {
continue;
2015-10-11 23:21:45 +03:00
}
// Record doesn't exist in the Easy!Appointments, so add the event now.
$local_event = [
'start_datetime' => $google_event_start->format('Y-m-d H:i:s'),
'end_datetime' => $google_event_end->format('Y-m-d H:i:s'),
'is_unavailability' => true,
'location' => $google_event->getLocation(),
'notes' => $google_event->getSummary() . ' ' . $google_event->getDescription(),
'id_users_provider' => $provider_id,
'id_google_calendar' => $google_event->getId(),
'id_users_customer' => null,
'id_services' => null,
];
$CI->unavailabilities_model->save($local_event);
2015-10-11 23:21:45 +03:00
}
json_response([
'success' => true,
]);
} catch (Throwable $e) {
log_message(
'error',
'Google - Sync completed with an error (provider ID "' . $provider_id . '"): ' . $e->getMessage(),
);
json_exception($e);
2015-10-11 23:21:45 +03:00
}
}
2020-12-09 15:17:45 +03:00
/**
* Authorize Google Calendar API usage for a specific provider.
*
* Since it is required to follow the web application flow, in order to retrieve a refresh token from the Google API
* service, this method is going to authorize the given provider.
*
* @param string $provider_id The provider id, for whom the sync authorization is made.
2020-12-09 15:17:45 +03:00
*/
public function oauth(string $provider_id): void
2020-12-09 15:17:45 +03:00
{
if (!$this->session->userdata('user_id')) {
show_error('Forbidden', 403);
}
2020-12-09 15:17:45 +03:00
// Store the provider id for use on the callback function.
session(['oauth_provider_id' => $provider_id]);
2020-12-09 15:17:45 +03:00
// Redirect browser to google user content page.
header('Location: ' . $this->google_sync->get_auth_url());
}
/**
* Callback method for the Google Calendar API authorization process.
*
* Once the user grants consent with his Google Calendar data usage, the Google OAuth service will redirect him back
* in this page. Here we are going to store the refresh token, because this is what will be used to generate access
* tokens in the future.
*
* IMPORTANT: Because it is necessary to authorize the application using the web server flow (see official
* documentation of OAuth), every Easy!Appointments installation should use its own calendar api key. So in every
2023-10-23 12:21:30 +03:00
* api console account, the "http://path-to-Easy!Appointments/google/oauth_callback" should be included in an
2023-03-13 11:06:18 +03:00
* allowed redirect URL.
2023-10-23 12:21:30 +03:00
*
2023-03-13 11:06:18 +03:00
* @throws Exception
2020-12-09 15:17:45 +03:00
*/
public function oauth_callback(): void
2020-12-09 15:17:45 +03:00
{
if (!session('user_id')) {
abort(403, 'Forbidden');
}
$code = request('code');
2020-12-09 15:17:45 +03:00
if (empty($code)) {
response('Code authorization failed.');
2020-12-09 15:17:45 +03:00
return;
}
$token = $this->google_sync->authenticate($code);
if (empty($token)) {
response('Token authorization failed.');
2020-12-09 15:17:45 +03:00
return;
}
// Store the token into the database for future reference.
$oauth_provider_id = session('oauth_provider_id');
2020-12-09 15:17:45 +03:00
if ($oauth_provider_id) {
$this->providers_model->set_setting($oauth_provider_id, 'google_sync', true);
$this->providers_model->set_setting($oauth_provider_id, 'google_token', json_encode($token));
$this->providers_model->set_setting($oauth_provider_id, 'google_calendar', 'primary');
} else {
response('Sync provider id not specified.');
2020-12-09 15:17:45 +03:00
}
}
/**
* This method will return a list of the available Google Calendars.
*
* The user will need to select a specific calendar from this list to sync his appointments with. Google access must
* be already granted for the specific provider.
*/
public function get_google_calendars(): void
{
try {
$provider_id = (int) request('provider_id');
if (empty($provider_id)) {
throw new Exception('Provider id is required in order to fetch the google calendars.');
}
// Check if selected provider has sync enabled.
$google_sync = $this->providers_model->get_setting($provider_id, 'google_sync');
if (!$google_sync) {
json_response([
'success' => false,
]);
return;
}
$google_token = json_decode($this->providers_model->get_setting($provider_id, 'google_token'), true);
$this->google_sync->refresh_token($google_token['refresh_token']);
$calendars = $this->google_sync->get_google_calendars();
json_response($calendars);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Select a specific google calendar for a provider.
*
* All the appointments will be synced with this particular calendar.
*/
public function select_google_calendar(): void
{
try {
$provider_id = request('provider_id');
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$calendar_id = request('calendar_id');
$this->providers_model->set_setting($provider_id, 'google_calendar', $calendar_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Disable a providers sync setting.
*
* This method deletes the "google_sync" and "google_token" settings from the database.
*
* After that the provider's appointments will be no longer synced with Google Calendar.
*/
public function disable_provider_sync(): void
{
try {
$provider_id = request('provider_id');
if (!$provider_id) {
throw new Exception('Provider id not specified.');
}
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$this->providers_model->set_setting($provider_id, 'google_sync', false);
2023-03-13 11:06:18 +03:00
$this->providers_model->set_setting($provider_id, 'google_token');
$this->appointments_model->clear_google_sync_ids($provider_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
2015-10-11 23:21:45 +03:00
}