This commit is contained in:
184
application/libraries/Accounts.php
Normal file
184
application/libraries/Accounts.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.5.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Accounts library.
|
||||
*
|
||||
* Handles account related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Accounts
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Accounts constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('users_model');
|
||||
$this->CI->load->model('roles_model');
|
||||
|
||||
$this->CI->load->library('timezones');
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the provided credentials.
|
||||
*
|
||||
* @param string $username Username.
|
||||
* @param string $password Password (non-hashed).
|
||||
*
|
||||
* @return array|null Returns an associative array with the PHP session data or NULL on failure.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function check_login(string $username, string $password): ?array
|
||||
{
|
||||
$salt = $this->get_salt_by_username($username);
|
||||
|
||||
$password = hash_password($salt, $password);
|
||||
|
||||
$user_settings = $this->CI->db
|
||||
->get_where('user_settings', [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
])
|
||||
->row_array();
|
||||
|
||||
if (empty($user_settings)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $this->CI->users_model->find($user_settings['id_users']);
|
||||
|
||||
$role = $this->CI->roles_model->find($user['id_roles']);
|
||||
|
||||
$default_timezone = $this->CI->timezones->get_default_timezone();
|
||||
|
||||
return [
|
||||
'user_id' => $user['id'],
|
||||
'user_email' => $user['email'],
|
||||
'username' => $username,
|
||||
'timezone' => !empty($user['timezone']) ? $user['timezone'] : $default_timezone,
|
||||
'language' => !empty($user['language']) ? $user['language'] : Config::LANGUAGE,
|
||||
'role_slug' => $role['slug'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's salt value.
|
||||
*
|
||||
* @param string $username Username.
|
||||
*
|
||||
* @return string Returns the salt value.
|
||||
*/
|
||||
public function get_salt_by_username(string $username): string
|
||||
{
|
||||
$user_settings = $this->CI->db->get_where('user_settings', ['username' => $username])->row_array();
|
||||
|
||||
return $user_settings['salt'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user full name.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string Returns the user full name.
|
||||
*/
|
||||
public function get_user_display_name(int $user_id): string
|
||||
{
|
||||
$user = $this->CI->users_model->find($user_id);
|
||||
|
||||
return $user['first_name'] . ' ' . $user['last_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the password of the user that matches the provided username and email.
|
||||
*
|
||||
* @param string $username Username.
|
||||
* @param string $email Email.
|
||||
*
|
||||
* @return string Returns the new password on success or FALSE on failure.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function regenerate_password(string $username, string $email): string
|
||||
{
|
||||
$query = $this->CI->db
|
||||
->select('users.id')
|
||||
->from('users')
|
||||
->join('user_settings', 'user_settings.id_users = users.id', 'inner')
|
||||
->where('users.email', $email)
|
||||
->where('user_settings.username', $username)
|
||||
->get();
|
||||
|
||||
if (!$query->num_rows()) {
|
||||
throw new RuntimeException('The user was not found in the database with the provided info.');
|
||||
}
|
||||
|
||||
$user = $query->row_array();
|
||||
|
||||
// Generate a new password for the user.
|
||||
$new_password = random_string('alnum', 12);
|
||||
|
||||
$salt = $this->get_salt_by_username($username);
|
||||
|
||||
$hash_password = hash_password($salt, $new_password);
|
||||
|
||||
$this->CI->users_model->set_setting($user['id'], 'password', $hash_password);
|
||||
|
||||
return $new_password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user account exists or not.
|
||||
*
|
||||
* @param int $user_id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function does_account_exist(int $user_id): bool
|
||||
{
|
||||
return $this->CI->users_model
|
||||
->query()
|
||||
->where(['id' => $user_id])
|
||||
->get()
|
||||
->num_rows() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user record based on the provided username value
|
||||
*
|
||||
* @param string $username
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_user_by_username(string $username): ?array
|
||||
{
|
||||
$user_settings = $this->CI->db->get_where('user_settings', ['username' => $username])->row_array();
|
||||
|
||||
if (!$user_settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user_id = $user_settings['id_users'];
|
||||
|
||||
return $this->CI->users_model->find($user_id);
|
||||
}
|
||||
}
|
||||
261
application/libraries/Api.php
Normal file
261
application/libraries/Api.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php use JetBrains\PhpStorm\NoReturn;
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.5.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Api library.
|
||||
*
|
||||
* Handles API related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Api
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $default_length = 20;
|
||||
|
||||
/**
|
||||
* @var EA_Model
|
||||
*/
|
||||
protected EA_Model $model;
|
||||
|
||||
/**
|
||||
* Api constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->library('accounts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and use the provided model class.
|
||||
*
|
||||
* @param string $model
|
||||
*/
|
||||
public function model(string $model): void
|
||||
{
|
||||
$this->CI->load->model($model);
|
||||
|
||||
$this->model = $this->CI->{$model};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize the API request (Basic Auth or Bearer Token supported).
|
||||
*/
|
||||
public function auth(): void
|
||||
{
|
||||
try {
|
||||
// Bearer token.
|
||||
$api_token = setting('api_token');
|
||||
|
||||
if (!empty($api_token) && $api_token === $this->get_bearer_token()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic auth.
|
||||
$username = $_SERVER['PHP_AUTH_USER'] ?? null;
|
||||
|
||||
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
|
||||
|
||||
$user_data = $this->CI->accounts->check_login($username, $password);
|
||||
|
||||
if (empty($user_data['role_slug']) || $user_data['role_slug'] !== DB_SLUG_ADMIN) {
|
||||
throw new RuntimeException(
|
||||
'The provided credentials do not match any admin user!',
|
||||
401,
|
||||
'Unauthorized',
|
||||
);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
$this->request_authentication();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bearer token value.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function get_bearer_token(): ?string
|
||||
{
|
||||
$headers = $this->get_authorization_header();
|
||||
|
||||
// HEADER: Get the access token from the header
|
||||
|
||||
if (!empty($headers)) {
|
||||
if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization header.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function get_authorization_header(): ?string
|
||||
{
|
||||
$headers = null;
|
||||
|
||||
if (isset($_SERVER['Authorization'])) {
|
||||
$headers = trim($_SERVER['Authorization']);
|
||||
} else {
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
// Nginx or fast CGI
|
||||
$headers = trim($_SERVER['HTTP_AUTHORIZATION']);
|
||||
} elseif (function_exists('apache_request_headers')) {
|
||||
$requestHeaders = apache_request_headers();
|
||||
|
||||
// Server-side fix for bug in old Android versions (a nice side effect of this fix means we don't care
|
||||
// about capitalization for Authorization).
|
||||
$requestHeaders = array_combine(
|
||||
array_map('ucwords', array_keys($requestHeaders)),
|
||||
array_values($requestHeaders),
|
||||
);
|
||||
|
||||
if (isset($requestHeaders['Authorization'])) {
|
||||
$headers = trim($requestHeaders['Authorization']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets request authentication headers.
|
||||
*/
|
||||
#[NoReturn]
|
||||
public function request_authentication(): void
|
||||
{
|
||||
header('WWW-Authenticate: Basic realm="Easy!Appointments"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
exit('You are not authorized to use the API.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search keyword value of the current request.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function request_keyword(): ?string
|
||||
{
|
||||
return request('q');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limit value of the current request.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function request_limit(): ?int
|
||||
{
|
||||
return request('length', $this->default_length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limit value of the current request.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function request_offset(): ?int
|
||||
{
|
||||
$page = request('page', 1);
|
||||
|
||||
$length = request('length', $this->default_length);
|
||||
|
||||
return ($page - 1) * $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order by value of the current request.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function request_order_by(): ?string
|
||||
{
|
||||
$sort = request('sort');
|
||||
|
||||
if (!$sort) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sort_tokens = array_map('trim', explode(',', $sort));
|
||||
|
||||
$order_by = [];
|
||||
|
||||
foreach ($sort_tokens as $sort_token) {
|
||||
$api_field = substr($sort_token, 1);
|
||||
|
||||
$direction_operator = substr($sort_token, 0, 1);
|
||||
|
||||
if (!in_array($direction_operator, ['-', '+'])) {
|
||||
$direction_operator = '+';
|
||||
$api_field = $sort_token;
|
||||
}
|
||||
|
||||
$db_field = $this->model->db_field($api_field);
|
||||
|
||||
$direction = $direction_operator === '-' ? 'DESC' : 'ASC';
|
||||
|
||||
$order_by[] = $db_field . ' ' . $direction;
|
||||
}
|
||||
|
||||
return implode(', ', $order_by);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chosen "fields" array of the current request.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function request_fields(): ?array
|
||||
{
|
||||
$fields = request('fields');
|
||||
|
||||
if (!$fields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_map('trim', explode(',', $fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provided "with" array of the current request.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function request_with(): ?array
|
||||
{
|
||||
$with = request('with');
|
||||
|
||||
if (!$with) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_map('trim', explode(',', $with));
|
||||
}
|
||||
}
|
||||
642
application/libraries/Availability.php
Normal file
642
application/libraries/Availability.php
Normal file
@@ -0,0 +1,642 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Availability library.
|
||||
*
|
||||
* Handles availability related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Availability
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Availability constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('admins_model');
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('settings_model');
|
||||
$this->CI->load->model('unavailabilities_model');
|
||||
$this->CI->load->model('blocked_periods_model');
|
||||
|
||||
$this->CI->load->library('ics_file');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available hours of a provider.
|
||||
*
|
||||
* @param string $date Selected date (Y-m-d).
|
||||
* @param array $service Service data.
|
||||
* @param array $provider Provider data.
|
||||
* @param int|null $exclude_appointment_id Exclude an appointment from the availability generation.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function get_available_hours(
|
||||
string $date,
|
||||
array $service,
|
||||
array $provider,
|
||||
?int $exclude_appointment_id = null,
|
||||
): array {
|
||||
if ($this->CI->blocked_periods_model->is_entire_date_blocked($date)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($service['attendants_number'] > 1) {
|
||||
$available_hours = $this->consider_multiple_attendants($date, $service, $provider, $exclude_appointment_id);
|
||||
} else {
|
||||
$available_periods = $this->get_available_periods($date, $provider, $exclude_appointment_id);
|
||||
|
||||
$available_hours = $this->generate_available_hours($date, $service, $available_periods);
|
||||
}
|
||||
|
||||
$available_hours = $this->consider_book_advance_timeout($date, $available_hours, $provider);
|
||||
|
||||
return $this->consider_future_booking_limit($date, $available_hours, $provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple attendants hours.
|
||||
*
|
||||
* This method will add the additional appointment hours whenever a service accepts multiple attendants.
|
||||
*
|
||||
* @param string $date Selected date (Y-m-d).
|
||||
* @param array $service Service data.
|
||||
* @param array $provider Provider data.
|
||||
* @param int|null $exclude_appointment_id Exclude an appointment from the availability generation.
|
||||
*
|
||||
* @return array Returns the available hours array.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function consider_multiple_attendants(
|
||||
string $date,
|
||||
array $service,
|
||||
array $provider,
|
||||
?int $exclude_appointment_id = null,
|
||||
): array {
|
||||
$unavailability_events = $this->CI->unavailabilities_model->get([
|
||||
'is_unavailability' => true,
|
||||
'DATE(start_datetime) <=' => $date,
|
||||
'DATE(end_datetime) >=' => $date,
|
||||
'id_users_provider' => $provider['id'],
|
||||
]);
|
||||
|
||||
$working_plan = json_decode($provider['settings']['working_plan'], true);
|
||||
|
||||
$working_plan_exceptions = json_decode($provider['settings']['working_plan_exceptions'], true);
|
||||
|
||||
$working_day = strtolower(date('l', strtotime($date)));
|
||||
|
||||
$date_working_plan = $working_plan[$working_day] ?? null;
|
||||
|
||||
// Search if the $date is a custom availability period added outside the normal working plan.
|
||||
if (array_key_exists($date, $working_plan_exceptions)) {
|
||||
$date_working_plan = $working_plan_exceptions[$date];
|
||||
}
|
||||
|
||||
if (!$date_working_plan) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$periods = [
|
||||
[
|
||||
'start' => new DateTime($date . ' ' . $date_working_plan['start']),
|
||||
'end' => new DateTime($date . ' ' . $date_working_plan['end']),
|
||||
],
|
||||
];
|
||||
|
||||
$blocked_periods = $this->CI->blocked_periods_model->get_for_period($date, $date);
|
||||
|
||||
$periods = $this->remove_breaks($date, $periods, $date_working_plan['breaks']);
|
||||
$periods = $this->remove_unavailability_events($periods, $unavailability_events);
|
||||
$periods = $this->remove_unavailability_events($periods, $blocked_periods);
|
||||
|
||||
$hours = [];
|
||||
|
||||
$interval_value = $service['availabilities_type'] == AVAILABILITIES_TYPE_FIXED ? $service['duration'] : '15';
|
||||
$interval = new DateInterval('PT' . (int) $interval_value . 'M');
|
||||
$duration = new DateInterval('PT' . (int) $service['duration'] . 'M');
|
||||
|
||||
foreach ($periods as $period) {
|
||||
$slot_start = clone $period['start'];
|
||||
$slot_end = clone $slot_start;
|
||||
$slot_end->add($duration);
|
||||
|
||||
while ($slot_end <= $period['end']) {
|
||||
// Make sure there is no other service appointment for this time slot.
|
||||
$other_service_attendants_number = $this->CI->appointments_model->get_other_service_attendants_number(
|
||||
$slot_start,
|
||||
$slot_end,
|
||||
$service['id'],
|
||||
$provider['id'],
|
||||
$exclude_appointment_id,
|
||||
);
|
||||
|
||||
if ($other_service_attendants_number > 0) {
|
||||
$slot_start->add($interval);
|
||||
$slot_end->add($interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check reserved attendants for this time slot and see if current attendants fit.
|
||||
$appointment_attendants_number = $this->CI->appointments_model->get_attendants_number_for_period(
|
||||
$slot_start,
|
||||
$slot_end,
|
||||
$service['id'],
|
||||
$provider['id'],
|
||||
$exclude_appointment_id,
|
||||
);
|
||||
|
||||
if ($appointment_attendants_number < $service['attendants_number']) {
|
||||
$hours[] = $slot_start->format('H:i');
|
||||
}
|
||||
|
||||
$slot_start->add($interval);
|
||||
$slot_end->add($interval);
|
||||
}
|
||||
}
|
||||
|
||||
return $hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove breaks from available time periods.
|
||||
*
|
||||
* @param string $date Selected date (Y-m-d).
|
||||
* @param array $periods Empty periods.
|
||||
* @param array $breaks Array of breaks.
|
||||
*
|
||||
* @return array Returns the available time periods without the breaks.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function remove_breaks(string $date, array $periods, array $breaks): array
|
||||
{
|
||||
if (!$breaks) {
|
||||
return $periods;
|
||||
}
|
||||
|
||||
foreach ($breaks as $break) {
|
||||
$break_start = new DateTime($date . ' ' . $break['start']);
|
||||
|
||||
$break_end = new DateTime($date . ' ' . $break['end']);
|
||||
|
||||
foreach ($periods as &$period) {
|
||||
$period_start = $period['start'];
|
||||
|
||||
$period_end = $period['end'];
|
||||
|
||||
if ($break_start <= $period_start && $break_end >= $period_start && $break_end <= $period_end) {
|
||||
// left
|
||||
$period['start'] = $break_end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$break_start >= $period_start &&
|
||||
$break_start <= $period_end &&
|
||||
$break_end >= $period_start &&
|
||||
$break_end <= $period_end
|
||||
) {
|
||||
// middle
|
||||
$period['end'] = $break_start;
|
||||
$periods[] = [
|
||||
'start' => $break_end,
|
||||
'end' => $period_end,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($break_start >= $period_start && $break_start <= $period_end && $break_end >= $period_end) {
|
||||
// right
|
||||
$period['end'] = $break_start;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($break_start <= $period_start && $break_end >= $period_end) {
|
||||
// break contains period
|
||||
$period['start'] = $break_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $periods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the unavailability entries from the available time periods of the selected date.
|
||||
*
|
||||
* @param array $periods Available time periods.
|
||||
* @param array $unavailability_events Unavailability events of the current date.
|
||||
*
|
||||
* @return array Returns the available time periods without the unavailability events.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function remove_unavailability_events(array $periods, array $unavailability_events): array
|
||||
{
|
||||
foreach ($unavailability_events as $unavailability_event) {
|
||||
$unavailability_start = new DateTime($unavailability_event['start_datetime']);
|
||||
|
||||
$unavailability_end = new DateTime($unavailability_event['end_datetime']);
|
||||
|
||||
foreach ($periods as &$period) {
|
||||
$period_start = $period['start'];
|
||||
|
||||
$period_end = $period['end'];
|
||||
|
||||
if (
|
||||
$unavailability_start <= $period_start &&
|
||||
$unavailability_end >= $period_start &&
|
||||
$unavailability_end <= $period_end
|
||||
) {
|
||||
// Left
|
||||
$period['start'] = $unavailability_end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$unavailability_start >= $period_start &&
|
||||
$unavailability_start <= $period_end &&
|
||||
$unavailability_end >= $period_start &&
|
||||
$unavailability_end <= $period_end
|
||||
) {
|
||||
// Middle
|
||||
$period['end'] = $unavailability_start;
|
||||
$periods[] = [
|
||||
'start' => $unavailability_end,
|
||||
'end' => $period_end,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$unavailability_start >= $period_start &&
|
||||
$unavailability_start <= $period_end &&
|
||||
$unavailability_end >= $period_end
|
||||
) {
|
||||
// Right
|
||||
$period['end'] = $unavailability_start;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($unavailability_start <= $period_start && $unavailability_end >= $period_end) {
|
||||
// Unavailability contains period
|
||||
$period['start'] = $unavailability_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $periods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array containing the free time periods (start - end) of a selected date.
|
||||
*
|
||||
* This method is very important because there are many cases where the system needs to know when a provider is
|
||||
* available for an appointment. It will return an array that belongs to the selected date and contains values that
|
||||
* have the start and the end time of an available time period.
|
||||
*
|
||||
* @param string $date Selected date (Y-m-d).
|
||||
* @param array $provider Provider data.
|
||||
* @param int|null $exclude_appointment_id Exclude an appointment from the availability generation.
|
||||
*
|
||||
* @return array Returns an array with the available time periods of the provider.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function get_available_periods(string $date, array $provider, ?int $exclude_appointment_id = null): array
|
||||
{
|
||||
// Get the service, provider's working plan and provider appointments.
|
||||
$working_plan = json_decode($provider['settings']['working_plan'], true);
|
||||
|
||||
// Get the provider's working plan exceptions.
|
||||
$working_plan_exceptions_json = $provider['settings']['working_plan_exceptions'];
|
||||
|
||||
$working_plan_exceptions = $working_plan_exceptions_json
|
||||
? json_decode($provider['settings']['working_plan_exceptions'], true)
|
||||
: [];
|
||||
|
||||
$escaped_provider_id = $this->CI->db->escape($provider['id']);
|
||||
|
||||
$escaped_date = $this->CI->db->escape($date);
|
||||
|
||||
$where =
|
||||
'id_users_provider = ' .
|
||||
$escaped_provider_id .
|
||||
' AND DATE(start_datetime) <= ' .
|
||||
$escaped_date .
|
||||
' AND DATE(end_datetime) >= ' .
|
||||
$escaped_date;
|
||||
|
||||
// Sometimes it might be necessary to exclude an appointment from the calculation (e.g. when editing an
|
||||
// existing appointment).
|
||||
if ($exclude_appointment_id) {
|
||||
$escaped_exclude_appointment_id = $this->CI->db->escape($exclude_appointment_id);
|
||||
$where .= ' AND id != ' . $escaped_exclude_appointment_id;
|
||||
}
|
||||
|
||||
$appointments = array_values(
|
||||
array_merge(
|
||||
$this->CI->appointments_model->get($where),
|
||||
$this->CI->unavailabilities_model->get($where),
|
||||
$this->CI->blocked_periods_model->get_for_period($date, $date),
|
||||
),
|
||||
);
|
||||
|
||||
// Find the empty spaces on the plan. The first split between the plan is due to a break (if any). After that
|
||||
// every reserved appointment is considered to be a taken space in the plan.
|
||||
$working_day = strtolower(date('l', strtotime($date)));
|
||||
|
||||
$date_working_plan = $working_plan[$working_day] ?? null;
|
||||
|
||||
// Search if the $date is a custom availability period added outside the normal working plan.
|
||||
if (array_key_exists($date, $working_plan_exceptions)) {
|
||||
$date_working_plan = $working_plan_exceptions[$date];
|
||||
}
|
||||
|
||||
if (!$date_working_plan) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$periods = [];
|
||||
|
||||
if (isset($date_working_plan['breaks'])) {
|
||||
$periods[] = [
|
||||
'start' => $date_working_plan['start'],
|
||||
'end' => $date_working_plan['end'],
|
||||
];
|
||||
|
||||
$day_start = new DateTime($date_working_plan['start']);
|
||||
$day_end = new DateTime($date_working_plan['end']);
|
||||
|
||||
// Split the working plan to available time periods that do not contain the breaks in them.
|
||||
foreach ($date_working_plan['breaks'] as $break) {
|
||||
$break_start = new DateTime($break['start']);
|
||||
$break_end = new DateTime($break['end']);
|
||||
|
||||
if ($break_start < $day_start) {
|
||||
$break_start = $day_start;
|
||||
}
|
||||
|
||||
if ($break_end > $day_end) {
|
||||
$break_end = $day_end;
|
||||
}
|
||||
|
||||
if ($break_start >= $break_end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($periods as $key => $period) {
|
||||
$period_start = new DateTime($period['start']);
|
||||
$period_end = new DateTime($period['end']);
|
||||
|
||||
$remove_current_period = false;
|
||||
|
||||
if ($break_start > $period_start && $break_start < $period_end && $break_end > $period_start) {
|
||||
$periods[] = [
|
||||
'start' => $period_start->format('H:i'),
|
||||
'end' => $break_start->format('H:i'),
|
||||
];
|
||||
|
||||
$remove_current_period = true;
|
||||
}
|
||||
|
||||
if ($break_start < $period_end && $break_end > $period_start && $break_end < $period_end) {
|
||||
$periods[] = [
|
||||
'start' => $break_end->format('H:i'),
|
||||
'end' => $period_end->format('H:i'),
|
||||
];
|
||||
|
||||
$remove_current_period = true;
|
||||
}
|
||||
|
||||
if ($break_start == $period_start && $break_end == $period_end) {
|
||||
$remove_current_period = true;
|
||||
}
|
||||
|
||||
if ($remove_current_period) {
|
||||
unset($periods[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Break the empty periods with the reserved appointments.
|
||||
foreach ($appointments as $appointment) {
|
||||
foreach ($periods as $index => &$period) {
|
||||
$appointment_start = new DateTime($appointment['start_datetime']);
|
||||
$appointment_end = new DateTime($appointment['end_datetime']);
|
||||
|
||||
if ($appointment_start >= $appointment_end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$period_start = new DateTime($date . ' ' . $period['start']);
|
||||
$period_end = new DateTime($date . ' ' . $period['end']);
|
||||
|
||||
if (
|
||||
$appointment_start <= $period_start &&
|
||||
$appointment_end <= $period_end &&
|
||||
$appointment_end <= $period_start
|
||||
) {
|
||||
// The appointment does not belong in this time period, so we will not change anything.
|
||||
continue;
|
||||
} else {
|
||||
if (
|
||||
$appointment_start <= $period_start &&
|
||||
$appointment_end <= $period_end &&
|
||||
$appointment_end >= $period_start
|
||||
) {
|
||||
// The appointment starts before the period and finishes somewhere inside. We will need to break
|
||||
// this period and leave the available part.
|
||||
$period['start'] = $appointment_end->format('H:i');
|
||||
} else {
|
||||
if ($appointment_start >= $period_start && $appointment_end < $period_end) {
|
||||
// The appointment is inside the time period, so we will split the period into two new
|
||||
// others.
|
||||
unset($periods[$index]);
|
||||
|
||||
$periods[] = [
|
||||
'start' => $period_start->format('H:i'),
|
||||
'end' => $appointment_start->format('H:i'),
|
||||
];
|
||||
|
||||
$periods[] = [
|
||||
'start' => $appointment_end->format('H:i'),
|
||||
'end' => $period_end->format('H:i'),
|
||||
];
|
||||
} elseif ($appointment_start == $period_start && $appointment_end == $period_end) {
|
||||
unset($periods[$index]); // The whole period is blocked so remove it from the available periods array.
|
||||
} else {
|
||||
if (
|
||||
$appointment_start >= $period_start &&
|
||||
$appointment_end >= $period_start &&
|
||||
$appointment_start <= $period_end
|
||||
) {
|
||||
// The appointment starts in the period and finishes out of it. We will need to remove
|
||||
// the time that is taken from the appointment.
|
||||
$period['end'] = $appointment_start->format('H:i');
|
||||
} else {
|
||||
if (
|
||||
$appointment_start >= $period_start &&
|
||||
$appointment_end >= $period_end &&
|
||||
$appointment_start >= $period_end
|
||||
) {
|
||||
// The appointment does not belong in the period so do not change anything.
|
||||
continue;
|
||||
} else {
|
||||
if (
|
||||
$appointment_start <= $period_start &&
|
||||
$appointment_end >= $period_end &&
|
||||
$appointment_start <= $period_end
|
||||
) {
|
||||
// The appointment is bigger than the period, so this period needs to be removed.
|
||||
unset($periods[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the available appointment hours.
|
||||
*
|
||||
* Calculate the available appointment hours for the given date. The empty spaces are broken down to 15 min and if
|
||||
* the service fit in each quarter then a new available hour is added to the "$available_hours" array.
|
||||
*
|
||||
* @param string $date Selected date (Y-m-d).
|
||||
* @param array $service Service data.
|
||||
* @param array $empty_periods Empty periods array.
|
||||
*
|
||||
* @return array Returns an array with the available hours for the appointment.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function generate_available_hours(string $date, array $service, array $empty_periods): array
|
||||
{
|
||||
$available_hours = [];
|
||||
|
||||
foreach ($empty_periods as $period) {
|
||||
$start_hour = new DateTime($date . ' ' . $period['start']);
|
||||
|
||||
$end_hour = new DateTime($date . ' ' . $period['end']);
|
||||
|
||||
$interval = $service['availabilities_type'] === AVAILABILITIES_TYPE_FIXED ? (int) $service['duration'] : 15;
|
||||
|
||||
$current_hour = $start_hour;
|
||||
|
||||
$diff = $current_hour->diff($end_hour);
|
||||
|
||||
while ($diff->h * 60 + $diff->i >= (int) $service['duration'] && $diff->invert === 0) {
|
||||
$available_hours[] = $current_hour->format('H:i');
|
||||
|
||||
$current_hour->add(new DateInterval('PT' . $interval . 'M'));
|
||||
|
||||
$diff = $current_hour->diff($end_hour);
|
||||
}
|
||||
}
|
||||
|
||||
return $available_hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider the book advance timeout and remove available hours that have passed the threshold.
|
||||
*
|
||||
* If the selected date is today, remove past hours. It is important include the timeout before booking
|
||||
* that is set in the back-office the system. Normally we might want the customer to book an appointment
|
||||
* that is at least half or one hour from now. The setting is stored in minutes.
|
||||
*
|
||||
* @param string $date The selected date.
|
||||
* @param array $available_hours Already generated available hours.
|
||||
* @param array $provider Provider information.
|
||||
*
|
||||
* @return array Returns the updated available hours.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function consider_book_advance_timeout(string $date, array $available_hours, array $provider): array
|
||||
{
|
||||
$provider_timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$book_advance_timeout = setting('book_advance_timeout');
|
||||
|
||||
$threshold = new DateTime('+' . $book_advance_timeout . ' minutes', $provider_timezone);
|
||||
|
||||
foreach ($available_hours as $index => $value) {
|
||||
$available_hour = new DateTime($date . ' ' . $value, $provider_timezone);
|
||||
|
||||
if ($available_hour->getTimestamp() <= $threshold->getTimestamp()) {
|
||||
unset($available_hours[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
$available_hours = array_values($available_hours);
|
||||
|
||||
sort($available_hours, SORT_STRING);
|
||||
|
||||
return array_values($available_hours);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove times if succeed the future booking limit.
|
||||
*
|
||||
* @param string $selected_date
|
||||
* @param array $available_hours
|
||||
* @param array $provider
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function consider_future_booking_limit(
|
||||
string $selected_date,
|
||||
array $available_hours,
|
||||
array $provider,
|
||||
): array {
|
||||
$provider_timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$future_booking_limit = setting('future_booking_limit'); // in days
|
||||
|
||||
$threshold = new DateTime('+' . $future_booking_limit . ' days', $provider_timezone);
|
||||
|
||||
$selected_date_time = new DateTime($selected_date);
|
||||
|
||||
if ($threshold < $selected_date_time) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $threshold > $selected_date_time ? $available_hours : [];
|
||||
}
|
||||
}
|
||||
585
application/libraries/Caldav_sync.php
Normal file
585
application/libraries/Caldav_sync.php
Normal file
@@ -0,0 +1,585 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.5.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Jsvrcek\ICS\Exception\CalendarEventException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
/**
|
||||
* Caldav sync library.
|
||||
*
|
||||
* Handles CalDAV related operations using Guzzle.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Caldav_sync
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Caldav_sync constructor.
|
||||
*
|
||||
* This method initializes the Caldav client class and the Calendar service class so that they can be used by the
|
||||
* other methods.
|
||||
*
|
||||
* @throws Exception If there is an issue with the initialization.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('customers_model');
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('services_model');
|
||||
|
||||
$this->CI->load->library('ics_file');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an appointment record to the connected CalDAV calendar.
|
||||
*
|
||||
* @param array $appointment Appointment record.
|
||||
* @param array $service Service record.
|
||||
* @param array $provider Provider record.
|
||||
* @param array $customer Customer record.
|
||||
*
|
||||
* @return string|null Returns the event ID
|
||||
*
|
||||
* @throws CalendarEventException If there's an issue generating the ICS file.
|
||||
*/
|
||||
public function save_appointment(array $appointment, array $service, array $provider, array $customer): ?string
|
||||
{
|
||||
try {
|
||||
$ics_file = $this->get_appointment_ics_file($appointment, $service, $provider, $customer);
|
||||
|
||||
$client = $this->get_http_client_by_provider_id($provider['id']);
|
||||
|
||||
$caldav_event_id =
|
||||
$appointment['id_caldav_calendar'] ?: $this->CI->ics_file->generate_uid($appointment['id']);
|
||||
|
||||
$uri = $this->get_caldav_event_uri($provider['settings']['caldav_url'], $caldav_event_id);
|
||||
|
||||
$client->request('PUT', $uri, [
|
||||
'headers' => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
],
|
||||
'body' => $ics_file,
|
||||
]);
|
||||
|
||||
return $caldav_event_id;
|
||||
} catch (GuzzleException $e) {
|
||||
$this->handle_guzzle_exception($e, 'Failed to save CalDAV appointment event');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an unavailability record to the connected CalDAV calendar.
|
||||
*
|
||||
* @param array $unavailability Appointment record.
|
||||
* @param array $provider Provider record.
|
||||
*
|
||||
* @return string|null Returns the event ID
|
||||
*
|
||||
* @throws CalendarEventException If there's an issue generating the ICS file.
|
||||
*/
|
||||
public function save_unavailability(array $unavailability, array $provider): ?string
|
||||
{
|
||||
try {
|
||||
if (str_contains((string) $unavailability['id_caldav_calendar'], 'RECURRENCE')) {
|
||||
return $unavailability['id_caldav_calendar'] ?? null; // Do not sync recurring unavailabilities
|
||||
}
|
||||
|
||||
$ics_file = $this->get_unavailability_ics_file($unavailability, $provider);
|
||||
|
||||
$client = $this->get_http_client_by_provider_id($provider['id']);
|
||||
|
||||
$caldav_event_id =
|
||||
$unavailability['id_caldav_calendar'] ?: $this->CI->ics_file->generate_uid($unavailability['id']);
|
||||
|
||||
$uri = $this->get_caldav_event_uri($provider['settings']['caldav_url'], $caldav_event_id);
|
||||
|
||||
$client->request('PUT', $uri, [
|
||||
'headers' => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
],
|
||||
'body' => $ics_file,
|
||||
]);
|
||||
|
||||
return $caldav_event_id;
|
||||
} catch (GuzzleException $e) {
|
||||
$this->handle_guzzle_exception($e, 'Failed to save CalDAV unavailability event');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing appointment from Caldav Calendar.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param string $caldav_event_id The Caldav Calendar event ID to be removed.
|
||||
*/
|
||||
public function delete_event(array $provider, string $caldav_event_id): void
|
||||
{
|
||||
try {
|
||||
$client = $this->get_http_client_by_provider_id($provider['id']);
|
||||
|
||||
$uri = $this->get_caldav_event_uri($provider['settings']['caldav_url'], $caldav_event_id);
|
||||
|
||||
$client->request('DELETE', $uri);
|
||||
} catch (GuzzleException $e) {
|
||||
$this->handle_guzzle_exception($e, 'Failed to delete CalDAV event');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Caldav Calendar event.
|
||||
*
|
||||
* @param array $provider Provider Data.
|
||||
* @param string $caldav_event_id CalDAV calendar event ID.
|
||||
*
|
||||
* @return array|null
|
||||
* @throws Exception If there’s an issue parsing the ICS data.
|
||||
*/
|
||||
public function get_event(array $provider, string $caldav_event_id): ?array
|
||||
{
|
||||
try {
|
||||
$client = $this->get_http_client_by_provider_id($provider['id']);
|
||||
|
||||
$provider_timezone_object = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$uri = $this->get_caldav_event_uri($provider['settings']['caldav_url'], $caldav_event_id);
|
||||
|
||||
$response = $client->request('GET', $uri);
|
||||
|
||||
$ics_file = $response->getBody()->getContents();
|
||||
|
||||
$vcalendar = Reader::read($ics_file);
|
||||
|
||||
return $this->convert_caldav_event_to_array_event($vcalendar->VEVENT, $provider_timezone_object);
|
||||
} catch (GuzzleException $e) {
|
||||
$this->handle_guzzle_exception($e, 'Failed to get CalDAV event');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the events between the sync period.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param string $start_date_time The start date of sync period.
|
||||
* @param string $end_date_time The end date of sync period.
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception If there's an issue with event fetching or parsing.
|
||||
*/
|
||||
public function get_sync_events(array $provider, string $start_date_time, string $end_date_time): array
|
||||
{
|
||||
try {
|
||||
$client = $this->get_http_client_by_provider_id($provider['id']);
|
||||
$provider_timezone_object = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$response = $this->fetch_events($client, $start_date_time, $end_date_time);
|
||||
|
||||
if (!$response->getBody()) {
|
||||
log_message('error', 'No response body from fetch_events' . PHP_EOL);
|
||||
return [];
|
||||
}
|
||||
|
||||
$xml = new SimpleXMLElement($response->getBody(), 0, false, 'd', true);
|
||||
|
||||
if ($xml->children('d', true)) {
|
||||
return $this->parse_xml_events($xml, $start_date_time, $end_date_time, $provider_timezone_object);
|
||||
}
|
||||
|
||||
$ics_file_urls = $this->extract_ics_file_urls($response->getBody());
|
||||
return $this->fetch_and_parse_ics_files(
|
||||
$client,
|
||||
$ics_file_urls,
|
||||
$start_date_time,
|
||||
$end_date_time,
|
||||
$provider_timezone_object,
|
||||
);
|
||||
} catch (GuzzleException $e) {
|
||||
$this->handle_guzzle_exception($e, 'Failed to get CalDAV sync events');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function parse_xml_events(
|
||||
SimpleXMLElement $xml,
|
||||
string $start_date_time,
|
||||
string $end_date_time,
|
||||
DateTimeZone $timezone,
|
||||
): array {
|
||||
$events = [];
|
||||
|
||||
foreach ($xml->children('d', true) as $response) {
|
||||
$ics_contents = (string) $response->propstat->prop->children('cal', true);
|
||||
|
||||
$events = array_merge(
|
||||
$events,
|
||||
$this->expand_ics_content($ics_contents, $start_date_time, $end_date_time, $timezone),
|
||||
);
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
private function extract_ics_file_urls(string $body): array
|
||||
{
|
||||
$ics_files = [];
|
||||
$lines = explode("\n", $body);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/\/calendars\/.*?\.ics/', $line, $matches)) {
|
||||
$ics_files[] = $matches[0];
|
||||
}
|
||||
}
|
||||
return $ics_files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and parse the ICS files from the remote server
|
||||
*
|
||||
* @param Client $client
|
||||
* @param array $ics_file_urls
|
||||
* @param string $start_date_time
|
||||
* @param string $end_date_time
|
||||
* @param DateTimeZone $timezone_OBJECT
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function fetch_and_parse_ics_files(
|
||||
Client $client,
|
||||
array $ics_file_urls,
|
||||
string $start_date_time,
|
||||
string $end_date_time,
|
||||
DateTimeZone $timezone_OBJECT,
|
||||
): array {
|
||||
$events = [];
|
||||
|
||||
foreach ($ics_file_urls as $ics_file_url) {
|
||||
try {
|
||||
$ics_response = $client->request('GET', $ics_file_url);
|
||||
|
||||
$ics_contents = $ics_response->getBody()->getContents();
|
||||
|
||||
if (empty($ics_contents)) {
|
||||
log_message('error', 'ICS file data is empty for URL: ' . $ics_file_url . PHP_EOL);
|
||||
continue;
|
||||
}
|
||||
|
||||
$events = array_merge(
|
||||
$events,
|
||||
$this->expand_ics_content($ics_contents, $start_date_time, $end_date_time, $timezone_OBJECT),
|
||||
);
|
||||
} catch (GuzzleException $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Failed to fetch ICS content from ' . $ics_file_url . ': ' . $e->getMessage() . PHP_EOL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
private function expand_ics_content(
|
||||
string $ics_contents,
|
||||
string $start_date_time,
|
||||
string $end_date_time,
|
||||
DateTimeZone $timezone_object,
|
||||
): array {
|
||||
$events = [];
|
||||
|
||||
try {
|
||||
$vcalendar = Reader::read($ics_contents);
|
||||
|
||||
$expanded_vcalendar = $vcalendar->expand(new DateTime($start_date_time), new DateTime($end_date_time));
|
||||
|
||||
foreach ($expanded_vcalendar->VEVENT as $event) {
|
||||
$events[] = $this->convert_caldav_event_to_array_event($event, $timezone_object);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message('error', 'Failed to parse or expand calendar data: ' . $e->getMessage() . PHP_EOL);
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common error handling for the CalDAV requests.
|
||||
*
|
||||
* @param GuzzleException $e
|
||||
* @param string $message
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_guzzle_exception(GuzzleException $e, string $message): void
|
||||
{
|
||||
// Guzzle throws a RequestException for HTTP errors
|
||||
|
||||
if ($e instanceof RequestException && $e->hasResponse()) {
|
||||
// Handle HTTP error response
|
||||
|
||||
$response = $e->getResponse();
|
||||
|
||||
$status_code = $response->getStatusCode();
|
||||
|
||||
$guzzle_info = '(Status Code: ' . $status_code . '): ' . $response->getBody()->getContents() . PHP_EOL;
|
||||
} else {
|
||||
// Handle other request errors
|
||||
|
||||
$guzzle_info = $e->getMessage();
|
||||
}
|
||||
|
||||
log_message('error', $message . ' ' . $guzzle_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception If there is an invalid CalDAV URL or credentials.
|
||||
* @throws GuzzleException If there’s an issue with the HTTP request.
|
||||
*/
|
||||
private function get_http_client(string $caldav_url, string $caldav_username, string $caldav_password): Client
|
||||
{
|
||||
if (!filter_var($caldav_url, FILTER_VALIDATE_URL)) {
|
||||
throw new InvalidArgumentException('Invalid CalDAV URL provided: ' . $caldav_url);
|
||||
}
|
||||
|
||||
if (!$caldav_username) {
|
||||
throw new InvalidArgumentException('Missing CalDAV username');
|
||||
}
|
||||
|
||||
if (!$caldav_password) {
|
||||
throw new InvalidArgumentException('Missing CalDAV password');
|
||||
}
|
||||
|
||||
return new Client([
|
||||
'base_uri' => rtrim($caldav_url, '/') . '/',
|
||||
'connect_timeout' => 15,
|
||||
'headers' => [
|
||||
'Content-Type' => 'text/xml',
|
||||
],
|
||||
'auth' => [$caldav_username, $caldav_password],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function test_connection(string $caldav_url, string $caldav_username, string $caldav_password): void
|
||||
{
|
||||
try {
|
||||
// Fetch some events to see if the connection is valid
|
||||
$client = $this->get_http_client($caldav_url, $caldav_username, $caldav_password);
|
||||
|
||||
$start_date_time = date('Y-m-d 00:00:00');
|
||||
$end_date_time = date('Y-m-d 23:59:59');
|
||||
|
||||
$this->fetch_events($client, $start_date_time, $end_date_time);
|
||||
} catch (GuzzleException $e) {
|
||||
$this->handle_guzzle_exception($e, 'Failed to test CalDAV connection');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function get_http_client_by_provider_id(int $provider_id): Client
|
||||
{
|
||||
$provider = $this->CI->providers_model->find($provider_id);
|
||||
|
||||
if (!$provider['settings']['caldav_sync']) {
|
||||
throw new RuntimeException('The selected provider does not have the CalDAV sync enabled: ' . $provider_id);
|
||||
}
|
||||
|
||||
$caldav_url = $provider['settings']['caldav_url'];
|
||||
$caldav_username = $provider['settings']['caldav_username'];
|
||||
$caldav_password = $provider['settings']['caldav_password'];
|
||||
|
||||
return $this->get_http_client($caldav_url, $caldav_username, $caldav_password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the event URI, used in various requests.
|
||||
*
|
||||
* @param string $caldav_calendar
|
||||
* @param string|null $caldav_event_id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_caldav_event_uri(string $caldav_calendar, ?string $caldav_event_id = null): string
|
||||
{
|
||||
return $caldav_event_id ? rtrim($caldav_calendar, '/') . '/' . $caldav_event_id . '.ics' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarEventException
|
||||
*/
|
||||
private function get_appointment_ics_file(
|
||||
array $appointment,
|
||||
array $service,
|
||||
array $provider,
|
||||
array $customer,
|
||||
): string {
|
||||
$ics_file = $this->CI->ics_file->get_stream($appointment, $service, $provider, $customer);
|
||||
|
||||
return str_replace('METHOD:PUBLISH', '', $ics_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarEventException
|
||||
*/
|
||||
private function get_unavailability_ics_file(array $unavailability, array $provider): string
|
||||
{
|
||||
$ics_file = $this->CI->ics_file->get_unavailability_stream($unavailability, $provider);
|
||||
|
||||
return str_replace('METHOD:PUBLISH', '', $ics_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the CalDAV event date-time value with the right timezone.
|
||||
*
|
||||
* @throws DateMalformedStringException
|
||||
* @throws DateInvalidTimeZoneException
|
||||
*/
|
||||
private function parse_date_time_object(string $caldav_date_time, DateTimeZone $default_timezone_object): DateTime
|
||||
{
|
||||
try {
|
||||
if (str_contains($caldav_date_time, 'TZID=')) {
|
||||
// Extract the TZID and use it
|
||||
preg_match('/TZID=([^:]+):/', $caldav_date_time, $matches);
|
||||
$parsed_timezone = $matches[1];
|
||||
$parsed_timezone_object = new DateTimeZone($parsed_timezone);
|
||||
$date_time = preg_replace('/TZID=[^:]+:/', '', $caldav_date_time);
|
||||
$date_time_object = new DateTime($date_time, $parsed_timezone_object);
|
||||
} elseif (str_ends_with($caldav_date_time, 'Z')) {
|
||||
// Handle UTC timestamps
|
||||
$date_time_object = new DateTime($caldav_date_time, new DateTimeZone('UTC'));
|
||||
} else {
|
||||
// Default to the provided timezone
|
||||
$date_time_object = new DateTime($caldav_date_time, $default_timezone_object);
|
||||
}
|
||||
|
||||
return $date_time_object;
|
||||
} catch (Throwable $e) {
|
||||
error_log('Error parsing date-time value (' . $caldav_date_time . ') with timezone: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the VEvent object to an associative array
|
||||
*
|
||||
* @link https://sabre.io/vobject/icalendar
|
||||
*
|
||||
* @param VEvent $vevent Holds the VEVENT information
|
||||
* @param DateTimeZone $timezone_object The date timezone values
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function convert_caldav_event_to_array_event(VEvent $vevent, DateTimeZone $timezone_object): array
|
||||
{
|
||||
try {
|
||||
$caldav_start_date_time = (string) $vevent->DTSTART;
|
||||
$start_date_time_object = $this->parse_date_time_object($caldav_start_date_time, $timezone_object);
|
||||
$start_date_time_object->setTimezone($timezone_object); // Convert to the provider timezone
|
||||
|
||||
$caldav_end_date_time = (string) $vevent->DTEND;
|
||||
$end_date_time_object = $this->parse_date_time_object($caldav_end_date_time, $timezone_object);
|
||||
$end_date_time_object->setTimezone($timezone_object); // Convert to the provider timezone
|
||||
|
||||
// Check if the event is recurring
|
||||
|
||||
$is_recurring_event =
|
||||
isset($vevent->RRULE) ||
|
||||
isset($vevent->RDATE) ||
|
||||
isset($vevent->{'RECURRENCE-ID'}) ||
|
||||
isset($vevent->EXDATE);
|
||||
|
||||
// Generate ID based on recurrence status
|
||||
|
||||
$event_id = (string) $vevent->UID;
|
||||
|
||||
if ($is_recurring_event) {
|
||||
$event_id .= '-RECURRENCE-' . random_string();
|
||||
}
|
||||
|
||||
// Return the converted event
|
||||
|
||||
return [
|
||||
'id' => $event_id,
|
||||
'summary' => (string) $vevent->SUMMARY ?? null ?: '',
|
||||
'start_datetime' => $start_date_time_object->format('Y-m-d H:i:s'),
|
||||
'end_datetime' => $end_date_time_object->format('Y-m-d H:i:s'),
|
||||
'description' => (string) $vevent->DESCRIPTION ?? null ?: '',
|
||||
'status' => (string) $vevent->STATUS ?? null ?: 'CONFIRMED',
|
||||
'location' => (string) $vevent->LOCATION ?? null ?: '',
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
error_log('Error parsing CalDAV event object (' . var_export($vevent, true) . '): ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GuzzleException
|
||||
* @throws Exception
|
||||
*/
|
||||
private function fetch_events(Client $client, string $start_date_time, string $end_date_time): ResponseInterface
|
||||
{
|
||||
$start_date_time_object = new DateTime($start_date_time);
|
||||
$formatted_start_date_time = $start_date_time_object->format('Ymd\THis\Z');
|
||||
$end_date_time_object = new DateTime($end_date_time);
|
||||
$formatted_end_date_time = $end_date_time_object->format('Ymd\THis\Z');
|
||||
|
||||
return $client->request('REPORT', '', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/xml',
|
||||
'Depth' => '1',
|
||||
],
|
||||
'body' =>
|
||||
'
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:getetag />
|
||||
<c:calendar-data />
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
<c:comp-filter name="VCALENDAR">
|
||||
<c:comp-filter name="VEVENT">
|
||||
<c:time-range start="' .
|
||||
$formatted_start_date_time .
|
||||
'" end="' .
|
||||
$formatted_end_date_time .
|
||||
'"/>
|
||||
</c:comp-filter>
|
||||
</c:comp-filter>
|
||||
</c:filter>
|
||||
</c:calendar-query>
|
||||
',
|
||||
]);
|
||||
}
|
||||
}
|
||||
741
application/libraries/Captcha_builder.php
Normal file
741
application/libraries/Captcha_builder.php
Normal file
@@ -0,0 +1,741 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Open Source Web Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
|
||||
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link http://easyappointments.org
|
||||
* @since v1.4.3
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use Gregwar\Captcha\PhraseBuilder;
|
||||
use Gregwar\Captcha\PhraseBuilderInterface;
|
||||
|
||||
/**
|
||||
* Class Captcha_builder
|
||||
*
|
||||
* This class replaces the Gregwar\Captcha\CaptchaBuilder so that it becomes PHP 8.1 compatible.
|
||||
*/
|
||||
class Captcha_builder
|
||||
{
|
||||
/**
|
||||
* Temporary dir, for OCR check
|
||||
*/
|
||||
public $tempDir = 'temp/';
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fingerprint = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $useFingerprint = false;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $textColor = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $lineColor = null;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $background = null;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $backgroundColor = null;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $backgroundImages = [];
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
protected $contents = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $phrase = null;
|
||||
/**
|
||||
* @var PhraseBuilderInterface
|
||||
*/
|
||||
protected $builder;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $distortion = true;
|
||||
/**
|
||||
* The maximum number of lines to draw in front of
|
||||
* the image. null - use default algorithm
|
||||
*/
|
||||
protected $maxFrontLines = null;
|
||||
/**
|
||||
* The maximum number of lines to draw behind
|
||||
* the image. null - use default algorithm
|
||||
*/
|
||||
protected $maxBehindLines = null;
|
||||
/**
|
||||
* The maximum angle of char
|
||||
*/
|
||||
protected $maxAngle = 8;
|
||||
/**
|
||||
* The maximum offset of char
|
||||
*/
|
||||
protected $maxOffset = 5;
|
||||
/**
|
||||
* Is the interpolation enabled ?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $interpolation = true;
|
||||
/**
|
||||
* Ignore all effects
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $ignoreAllEffects = false;
|
||||
/**
|
||||
* Allowed image types for the background images
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $allowedBackgroundImageTypes = ['image/png', 'image/jpeg', 'image/gif'];
|
||||
|
||||
public function __construct($phrase = null, ?PhraseBuilderInterface $builder = null)
|
||||
{
|
||||
if ($builder === null) {
|
||||
$this->builder = new PhraseBuilder();
|
||||
} else {
|
||||
$this->builder = $builder;
|
||||
}
|
||||
|
||||
$this->phrase = is_string($phrase) ? $phrase : $this->builder->build($phrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the image
|
||||
* @throws Exception
|
||||
*/
|
||||
public function build($width = 150, $height = 40, $font = null, $fingerprint = null)
|
||||
{
|
||||
if (null !== $fingerprint) {
|
||||
$this->fingerprint = $fingerprint;
|
||||
$this->useFingerprint = true;
|
||||
} else {
|
||||
$this->fingerprint = [];
|
||||
$this->useFingerprint = false;
|
||||
}
|
||||
|
||||
if ($font === null) {
|
||||
$font =
|
||||
__DIR__ . '/../../vendor/gregwar/captcha/src/Gregwar/Captcha/Font/captcha' . $this->rand(0, 5) . '.ttf';
|
||||
}
|
||||
|
||||
$bg = null;
|
||||
|
||||
if (empty($this->backgroundImages)) {
|
||||
// if background images list is not set, use a color fill as a background
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
if ($this->backgroundColor == null) {
|
||||
$bg = imagecolorallocate($image, $this->rand(200, 255), $this->rand(200, 255), $this->rand(200, 255));
|
||||
} else {
|
||||
$color = $this->backgroundColor;
|
||||
$bg = imagecolorallocate($image, $color[0], $color[1], $color[2]);
|
||||
}
|
||||
$this->background = $bg;
|
||||
imagefill($image, 0, 0, $bg);
|
||||
} else {
|
||||
// use a random background image
|
||||
$randomBackgroundImage = $this->backgroundImages[rand(0, count($this->backgroundImages) - 1)];
|
||||
|
||||
$imageType = $this->validateBackgroundImage($randomBackgroundImage);
|
||||
|
||||
$image = $this->createBackgroundImageFromType($randomBackgroundImage, $imageType);
|
||||
}
|
||||
|
||||
// Apply effects
|
||||
if (!$this->ignoreAllEffects) {
|
||||
$square = $width * $height;
|
||||
$effects = $this->rand($square / 3000, $square / 2000);
|
||||
|
||||
// set the maximum number of lines to draw in front of the text
|
||||
if ($this->maxBehindLines != null && $this->maxBehindLines > 0) {
|
||||
$effects = min($this->maxBehindLines, $effects);
|
||||
}
|
||||
|
||||
if ($this->maxBehindLines !== 0) {
|
||||
for ($e = 0; $e < $effects; $e++) {
|
||||
$this->drawLine($image, $width, $height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write CAPTCHA text
|
||||
$color = $this->writePhrase($image, $this->phrase, $font, $width, $height);
|
||||
|
||||
// Apply effects
|
||||
if (!$this->ignoreAllEffects) {
|
||||
$square = $width * $height;
|
||||
$effects = $this->rand($square / 3000, $square / 2000);
|
||||
|
||||
// set the maximum number of lines to draw in front of the text
|
||||
if ($this->maxFrontLines != null && $this->maxFrontLines > 0) {
|
||||
$effects = min($this->maxFrontLines, $effects);
|
||||
}
|
||||
|
||||
if ($this->maxFrontLines !== 0) {
|
||||
for ($e = 0; $e < $effects; $e++) {
|
||||
$this->drawLine($image, $width, $height, $color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distort the image
|
||||
if ($this->distortion && !$this->ignoreAllEffects) {
|
||||
$image = $this->distort($image, $width, $height, $bg);
|
||||
}
|
||||
|
||||
// Post effects
|
||||
if (!$this->ignoreAllEffects) {
|
||||
$this->postEffect($image);
|
||||
}
|
||||
|
||||
$this->contents = $image;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random number or the next number in the
|
||||
* fingerprint
|
||||
*/
|
||||
protected function rand($min, $max)
|
||||
{
|
||||
if (!is_array($this->fingerprint)) {
|
||||
$this->fingerprint = [];
|
||||
}
|
||||
|
||||
if ($this->useFingerprint) {
|
||||
$value = current($this->fingerprint);
|
||||
next($this->fingerprint);
|
||||
} else {
|
||||
$value = mt_rand((int) $min, (int) $max);
|
||||
$this->fingerprint[] = $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the background image path. Return the image type if valid
|
||||
*
|
||||
* @param string $backgroundImage
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function validateBackgroundImage($backgroundImage)
|
||||
{
|
||||
// check if file exists
|
||||
if (!file_exists($backgroundImage)) {
|
||||
$backgroundImageExploded = explode('/', $backgroundImage);
|
||||
$imageFileName =
|
||||
count($backgroundImageExploded) > 1
|
||||
? $backgroundImageExploded[count($backgroundImageExploded) - 1]
|
||||
: $backgroundImage;
|
||||
|
||||
throw new Exception('Invalid background image: ' . $imageFileName);
|
||||
}
|
||||
|
||||
// check image type
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
|
||||
$imageType = finfo_file($finfo, $backgroundImage);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($imageType, $this->allowedBackgroundImageTypes)) {
|
||||
throw new Exception(
|
||||
'Invalid background image type! Allowed types are: ' . join(', ', $this->allowedBackgroundImageTypes),
|
||||
);
|
||||
}
|
||||
|
||||
return $imageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create background image from type
|
||||
*
|
||||
* @param string $backgroundImage
|
||||
* @param string $imageType
|
||||
* @return resource
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function createBackgroundImageFromType($backgroundImage, $imageType)
|
||||
{
|
||||
switch ($imageType) {
|
||||
case 'image/jpeg':
|
||||
$image = imagecreatefromjpeg($backgroundImage);
|
||||
break;
|
||||
case 'image/png':
|
||||
$image = imagecreatefrompng($backgroundImage);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$image = imagecreatefromgif($backgroundImage);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Not supported file type for background image!');
|
||||
break;
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw lines over the image
|
||||
*/
|
||||
protected function drawLine($image, $width, $height, $tcol = null)
|
||||
{
|
||||
if ($this->lineColor === null) {
|
||||
$red = $this->rand(100, 255);
|
||||
$green = $this->rand(100, 255);
|
||||
$blue = $this->rand(100, 255);
|
||||
} else {
|
||||
$red = $this->lineColor[0];
|
||||
$green = $this->lineColor[1];
|
||||
$blue = $this->lineColor[2];
|
||||
}
|
||||
|
||||
if ($tcol === null) {
|
||||
$tcol = imagecolorallocate($image, $red, $green, $blue);
|
||||
}
|
||||
|
||||
if ($this->rand(0, 1)) {
|
||||
// Horizontal
|
||||
$Xa = $this->rand(0, $width / 2);
|
||||
$Ya = $this->rand(0, $height);
|
||||
$Xb = $this->rand($width / 2, $width);
|
||||
$Yb = $this->rand(0, $height);
|
||||
} else {
|
||||
// Vertical
|
||||
$Xa = $this->rand(0, $width);
|
||||
$Ya = $this->rand(0, $height / 2);
|
||||
$Xb = $this->rand(0, $width);
|
||||
$Yb = $this->rand($height / 2, $height);
|
||||
}
|
||||
imagesetthickness($image, $this->rand(1, 3));
|
||||
imageline($image, $Xa, $Ya, $Xb, $Yb, $tcol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the phrase on the image
|
||||
*/
|
||||
protected function writePhrase($image, $phrase, $font, $width, $height)
|
||||
{
|
||||
$length = mb_strlen($phrase);
|
||||
if ($length === 0) {
|
||||
return imagecolorallocate($image, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Gets the text size and start position
|
||||
$size = (int) round($width / $length) - $this->rand(0, 3) - 1;
|
||||
$box = imagettfbbox($size, 0, $font, $phrase);
|
||||
$textWidth = $box[2] - $box[0];
|
||||
$textHeight = $box[1] - $box[7];
|
||||
$x = (int) round(($width - $textWidth) / 2);
|
||||
$y = (int) round(($height - $textHeight) / 2) + $size;
|
||||
|
||||
if (!$this->textColor) {
|
||||
$textColor = [$this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)];
|
||||
} else {
|
||||
$textColor = $this->textColor;
|
||||
}
|
||||
$col = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]);
|
||||
|
||||
// Write the letters one by one, with random angle
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$symbol = mb_substr($phrase, $i, 1);
|
||||
$box = imagettfbbox($size, 0, $font, $symbol);
|
||||
$w = $box[2] - $box[0];
|
||||
$angle = $this->rand(-$this->maxAngle, $this->maxAngle);
|
||||
$offset = $this->rand(-$this->maxOffset, $this->maxOffset);
|
||||
imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $symbol);
|
||||
$x += $w;
|
||||
}
|
||||
|
||||
return $col;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distorts the image
|
||||
*/
|
||||
public function distort($image, $width, $height, $bg)
|
||||
{
|
||||
$contents = imagecreatetruecolor($width, $height);
|
||||
$X = $this->rand(0, $width);
|
||||
$Y = $this->rand(0, $height);
|
||||
$phase = $this->rand(0, 10);
|
||||
$scale = 1.1 + $this->rand(0, 10000) / 30000;
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
$Vx = $x - $X;
|
||||
$Vy = $y - $Y;
|
||||
$Vn = sqrt($Vx * $Vx + $Vy * $Vy);
|
||||
|
||||
if ($Vn != 0) {
|
||||
$Vn2 = $Vn + 4 * sin($Vn / 30);
|
||||
$nX = $X + ($Vx * $Vn2) / $Vn;
|
||||
$nY = $Y + ($Vy * $Vn2) / $Vn;
|
||||
} else {
|
||||
$nX = $X;
|
||||
$nY = $Y;
|
||||
}
|
||||
$nY = $nY + $scale * sin($phase + $nX * 0.2);
|
||||
|
||||
if ($this->interpolation) {
|
||||
$p = $this->interpolate(
|
||||
$nX - floor($nX),
|
||||
$nY - floor($nY),
|
||||
$this->getCol($image, floor($nX), floor($nY), $bg),
|
||||
$this->getCol($image, ceil($nX), floor($nY), $bg),
|
||||
$this->getCol($image, floor($nX), ceil($nY), $bg),
|
||||
$this->getCol($image, ceil($nX), ceil($nY), $bg),
|
||||
);
|
||||
} else {
|
||||
$p = $this->getCol($image, round($nX), round($nY), $bg);
|
||||
}
|
||||
|
||||
if ($p == 0) {
|
||||
$p = $bg;
|
||||
}
|
||||
|
||||
imagesetpixel($contents, $x, $y, $p);
|
||||
}
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $x
|
||||
* @param $y
|
||||
* @param $nw
|
||||
* @param $ne
|
||||
* @param $sw
|
||||
* @param $se
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function interpolate($x, $y, $nw, $ne, $sw, $se)
|
||||
{
|
||||
[$r0, $g0, $b0] = $this->getRGB($nw);
|
||||
[$r1, $g1, $b1] = $this->getRGB($ne);
|
||||
[$r2, $g2, $b2] = $this->getRGB($sw);
|
||||
[$r3, $g3, $b3] = $this->getRGB($se);
|
||||
|
||||
$cx = 1.0 - $x;
|
||||
$cy = 1.0 - $y;
|
||||
|
||||
$m0 = $cx * $r0 + $x * $r1;
|
||||
$m1 = $cx * $r2 + $x * $r3;
|
||||
$r = (int) ($cy * $m0 + $y * $m1);
|
||||
|
||||
$m0 = $cx * $g0 + $x * $g1;
|
||||
$m1 = $cx * $g2 + $x * $g3;
|
||||
$g = (int) ($cy * $m0 + $y * $m1);
|
||||
|
||||
$m0 = $cx * $b0 + $x * $b1;
|
||||
$m1 = $cx * $b2 + $x * $b3;
|
||||
$b = (int) ($cy * $m0 + $y * $m1);
|
||||
|
||||
return ($r << 16) | ($g << 8) | $b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $col
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getRGB($col)
|
||||
{
|
||||
return [(int) ($col >> 16) & 0xff, (int) ($col >> 8) & 0xff, (int) $col & 0xff];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $image
|
||||
* @param $x
|
||||
* @param $y
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getCol($image, $x, $y, $background)
|
||||
{
|
||||
$L = imagesx($image);
|
||||
$H = imagesy($image);
|
||||
if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) {
|
||||
return $background;
|
||||
}
|
||||
|
||||
return imagecolorat($image, $x, $y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply some post effects
|
||||
*/
|
||||
protected function postEffect($image)
|
||||
{
|
||||
if (!function_exists('imagefilter')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->backgroundColor != null || $this->textColor != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Negate ?
|
||||
if ($this->rand(0, 1) == 0) {
|
||||
imagefilter($image, IMG_FILTER_NEGATE);
|
||||
}
|
||||
|
||||
// Edge ?
|
||||
if ($this->rand(0, 10) == 0) {
|
||||
imagefilter($image, IMG_FILTER_EDGEDETECT);
|
||||
}
|
||||
|
||||
// Contrast
|
||||
imagefilter($image, IMG_FILTER_CONTRAST, $this->rand(-50, 10));
|
||||
|
||||
// Colorize
|
||||
if ($this->rand(0, 5) == 0) {
|
||||
imagefilter($image, IMG_FILTER_COLORIZE, $this->rand(-80, 50), $this->rand(-80, 50), $this->rand(-80, 50));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiation
|
||||
*/
|
||||
public static function create($phrase = null)
|
||||
{
|
||||
return new self($phrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* The image contents
|
||||
*/
|
||||
public function getContents()
|
||||
{
|
||||
return $this->contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disables the interpolation
|
||||
*
|
||||
* @param $interpolate bool True to enable, false to disable
|
||||
*
|
||||
* @return Captcha_builder
|
||||
*/
|
||||
public function setInterpolation($interpolate = true)
|
||||
{
|
||||
$this->interpolation = $interpolate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/disable distortion
|
||||
*/
|
||||
public function setDistortion($distortion)
|
||||
{
|
||||
$this->distortion = (bool) $distortion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaxBehindLines($maxBehindLines)
|
||||
{
|
||||
$this->maxBehindLines = $maxBehindLines;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaxFrontLines($maxFrontLines)
|
||||
{
|
||||
$this->maxFrontLines = $maxFrontLines;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaxAngle($maxAngle)
|
||||
{
|
||||
$this->maxAngle = $maxAngle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaxOffset($maxOffset)
|
||||
{
|
||||
$this->maxOffset = $maxOffset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text color to use
|
||||
*/
|
||||
public function setTextColor($r, $g, $b)
|
||||
{
|
||||
$this->textColor = [$r, $g, $b];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background color to use
|
||||
*/
|
||||
public function setBackgroundColor($r, $g, $b)
|
||||
{
|
||||
$this->backgroundColor = [$r, $g, $b];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLineColor($r, $g, $b)
|
||||
{
|
||||
$this->lineColor = [$r, $g, $b];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ignoreAllEffects value
|
||||
*
|
||||
* @param bool $ignoreAllEffects
|
||||
* @return Captcha_builder
|
||||
*/
|
||||
public function setIgnoreAllEffects($ignoreAllEffects)
|
||||
{
|
||||
$this->ignoreAllEffects = $ignoreAllEffects;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of background images to use (one image is randomly selected)
|
||||
*/
|
||||
public function setBackgroundImages(array $backgroundImages)
|
||||
{
|
||||
$this->backgroundImages = $backgroundImages;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds while the code is readable against an OCR
|
||||
*/
|
||||
public function buildAgainstOCR($width = 150, $height = 40, $font = null, $fingerprint = null)
|
||||
{
|
||||
do {
|
||||
$this->build($width, $height, $font, $fingerprint);
|
||||
} while ($this->isOCRReadable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to read the code against an OCR
|
||||
*/
|
||||
public function isOCRReadable()
|
||||
{
|
||||
if (!is_dir($this->tempDir)) {
|
||||
@mkdir($this->tempDir, 0755, true);
|
||||
}
|
||||
|
||||
$tempj = $this->tempDir . uniqid('captcha', true) . '.jpg';
|
||||
$tempp = $this->tempDir . uniqid('captcha', true) . '.pgm';
|
||||
|
||||
$this->save($tempj);
|
||||
shell_exec("convert $tempj $tempp");
|
||||
$value = trim(strtolower(shell_exec("ocrad $tempp")));
|
||||
|
||||
@unlink($tempj);
|
||||
@unlink($tempp);
|
||||
|
||||
return $this->testPhrase($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the Captcha to a jpeg file
|
||||
*/
|
||||
public function save($filename, $quality = 90)
|
||||
{
|
||||
imagejpeg($this->contents, $filename, $quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given phrase is good
|
||||
*/
|
||||
public function testPhrase($phrase)
|
||||
{
|
||||
return $this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the captcha phrase
|
||||
*/
|
||||
public function getPhrase()
|
||||
{
|
||||
return $this->phrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting the phrase
|
||||
*/
|
||||
public function setPhrase($phrase)
|
||||
{
|
||||
$this->phrase = (string) $phrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image GD
|
||||
*/
|
||||
public function getGd()
|
||||
{
|
||||
return $this->contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTML inline base64
|
||||
*/
|
||||
public function inline($quality = 90)
|
||||
{
|
||||
return 'data:image/jpeg;base64,' . base64_encode($this->get($quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image contents
|
||||
*/
|
||||
public function get($quality = 90)
|
||||
{
|
||||
ob_start();
|
||||
$this->output($quality);
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the image
|
||||
*/
|
||||
public function output($quality = 90)
|
||||
{
|
||||
imagejpeg($this->contents, null, $quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFingerprint()
|
||||
{
|
||||
return $this->fingerprint;
|
||||
}
|
||||
}
|
||||
267
application/libraries/Email_messages.php
Normal file
267
application/libraries/Email_messages.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
/**
|
||||
* Email messages library.
|
||||
*
|
||||
* Handles the email messaging related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Email_messages
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Email_messages constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('admins_model');
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('settings_model');
|
||||
|
||||
$this->CI->load->library('email');
|
||||
$this->CI->load->library('ics_file');
|
||||
$this->CI->load->library('timezones');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email with the appointment details.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $service Service data.
|
||||
* @param array $customer Customer data.
|
||||
* @param array $settings App settings.
|
||||
* @param string $subject Email subject.
|
||||
* @param string $message Email message.
|
||||
* @param string $appointment_link Appointment unique URL.
|
||||
* @param string $recipient_email Recipient email address.
|
||||
* @param string $ics_stream ICS file contents.
|
||||
* @param string|null $timezone Custom timezone.
|
||||
*
|
||||
* @throws DateInvalidTimeZoneException
|
||||
* @throws DateMalformedStringException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function send_appointment_saved(
|
||||
array $appointment,
|
||||
array $provider,
|
||||
array $service,
|
||||
array $customer,
|
||||
array $settings,
|
||||
string $subject,
|
||||
string $message,
|
||||
string $appointment_link,
|
||||
string $recipient_email,
|
||||
string $ics_stream,
|
||||
?string $timezone = null,
|
||||
): void {
|
||||
$appointment_timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$appointment_start = new DateTime($appointment['start_datetime'], $appointment_timezone);
|
||||
|
||||
$appointment_end = new DateTime($appointment['end_datetime'], $appointment_timezone);
|
||||
|
||||
if ($timezone && $timezone !== $provider['timezone']) {
|
||||
$custom_timezone = new DateTimeZone($timezone);
|
||||
|
||||
$appointment_start->setTimezone($custom_timezone);
|
||||
$appointment['start_datetime'] = $appointment_start->format('Y-m-d H:i:s');
|
||||
|
||||
$appointment_end->setTimezone($custom_timezone);
|
||||
$appointment['end_datetime'] = $appointment_end->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$html = $this->CI->load->view(
|
||||
'emails/appointment_saved_email',
|
||||
[
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'appointment' => $appointment,
|
||||
'service' => $service,
|
||||
'provider' => $provider,
|
||||
'customer' => $customer,
|
||||
'settings' => $settings,
|
||||
'timezone' => $timezone,
|
||||
'appointment_link' => $appointment_link,
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
$php_mailer = $this->get_php_mailer($recipient_email, $subject, $html);
|
||||
|
||||
$php_mailer->addStringAttachment($ics_stream, 'invitation.ics', PHPMailer::ENCODING_BASE64, 'text/calendar');
|
||||
|
||||
$php_mailer->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email with the appointment removal details.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $service Service data.
|
||||
* @param array $customer Customer data.
|
||||
* @param array $settings App settings.
|
||||
* @param string $recipient_email Recipient email address.
|
||||
* @param string|null $reason Removal reason.
|
||||
* @param string|null $timezone Custom timezone.
|
||||
*
|
||||
* @throws DateInvalidTimeZoneException
|
||||
* @throws DateMalformedStringException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function send_appointment_deleted(
|
||||
array $appointment,
|
||||
array $provider,
|
||||
array $service,
|
||||
array $customer,
|
||||
array $settings,
|
||||
string $recipient_email,
|
||||
?string $reason = null,
|
||||
?string $timezone = null,
|
||||
): void {
|
||||
$appointment_timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$appointment_start = new DateTime($appointment['start_datetime'], $appointment_timezone);
|
||||
|
||||
$appointment_end = new DateTime($appointment['end_datetime'], $appointment_timezone);
|
||||
|
||||
if ($timezone && $timezone !== $provider['timezone']) {
|
||||
$custom_timezone = new DateTimeZone($timezone);
|
||||
|
||||
$appointment_start->setTimezone($custom_timezone);
|
||||
$appointment['start_datetime'] = $appointment_start->format('Y-m-d H:i:s');
|
||||
|
||||
$appointment_end->setTimezone($custom_timezone);
|
||||
$appointment['end_datetime'] = $appointment_end->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$html = $this->CI->load->view(
|
||||
'emails/appointment_deleted_email',
|
||||
[
|
||||
'appointment' => $appointment,
|
||||
'service' => $service,
|
||||
'provider' => $provider,
|
||||
'customer' => $customer,
|
||||
'settings' => $settings,
|
||||
'timezone' => $timezone,
|
||||
'reason' => $reason,
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
$subject = lang('appointment_cancelled_title');
|
||||
|
||||
$php_mailer = $this->get_php_mailer($recipient_email, $subject, $html);
|
||||
|
||||
$php_mailer->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the account recovery details.
|
||||
*
|
||||
* @param string $password New password.
|
||||
* @param string $recipient_email Recipient email address.
|
||||
* @param array $settings App settings.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function send_password(string $password, string $recipient_email, array $settings): void
|
||||
{
|
||||
$html = $this->CI->load->view(
|
||||
'emails/account_recovery_email',
|
||||
[
|
||||
'subject' => lang('new_account_password'),
|
||||
'message' => str_replace('$password', '<strong>' . $password . '</strong>', lang('new_password_is')),
|
||||
'settings' => $settings,
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
$subject = lang('new_account_password');
|
||||
|
||||
$php_mailer = $this->get_php_mailer($recipient_email, $subject, $html);
|
||||
|
||||
$php_mailer->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PHP Mailer instance based on the email configuration.
|
||||
*
|
||||
* @param string|null $recipient_email
|
||||
* @param string|null $subject
|
||||
* @param string|null $html
|
||||
*
|
||||
* @return PHPMailer
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_php_mailer(
|
||||
?string $recipient_email = null,
|
||||
?string $subject = null,
|
||||
?string $html = null,
|
||||
): PHPMailer {
|
||||
$php_mailer = new PHPMailer(true);
|
||||
|
||||
$php_mailer->isHTML();
|
||||
$php_mailer->CharSet = 'UTF-8';
|
||||
$php_mailer->SMTPDebug = config('smtp_debug') ? SMTP::DEBUG_SERVER : null;
|
||||
|
||||
if (config('protocol') === 'smtp') {
|
||||
$php_mailer->isSMTP();
|
||||
$php_mailer->Host = config('smtp_host');
|
||||
$php_mailer->SMTPAuth = config('smtp_auth');
|
||||
$php_mailer->Username = config('smtp_user');
|
||||
$php_mailer->Password = config('smtp_pass');
|
||||
$php_mailer->SMTPSecure = config('smtp_crypto');
|
||||
$php_mailer->Port = config('smtp_port');
|
||||
}
|
||||
|
||||
$from_name = config('from_name') ?: setting('company_name');
|
||||
$from_address = config('from_address') ?: setting('company_email');
|
||||
$reply_to_address = config('reply_to') ?: setting('company_email');
|
||||
|
||||
$php_mailer->setFrom($from_address, $from_name);
|
||||
$php_mailer->addReplyTo($reply_to_address);
|
||||
|
||||
if ($recipient_email) {
|
||||
$php_mailer->addAddress($recipient_email);
|
||||
}
|
||||
|
||||
if ($subject) {
|
||||
$php_mailer->Subject = $subject;
|
||||
}
|
||||
|
||||
if ($html) {
|
||||
$php_mailer->Body = $html;
|
||||
$php_mailer->AltBody = $html;
|
||||
}
|
||||
|
||||
return $php_mailer;
|
||||
}
|
||||
}
|
||||
471
application/libraries/Google_sync.php
Normal file
471
application/libraries/Google_sync.php
Normal file
@@ -0,0 +1,471 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.0.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use Google\Service\Calendar\Event;
|
||||
use Google\Service\Calendar\Events;
|
||||
|
||||
/**
|
||||
* Google sync library.
|
||||
*
|
||||
* Handles Google Calendar API related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Google_sync
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* @var Google_Client
|
||||
*/
|
||||
protected Google_Client $client;
|
||||
|
||||
/**
|
||||
* @var Google_Service_Calendar
|
||||
*/
|
||||
protected Google_Service_Calendar $service;
|
||||
|
||||
/**
|
||||
* Google_sync constructor.
|
||||
*
|
||||
* This method initializes the Google client class and the Calendar service class so that they can be used by the
|
||||
* other methods.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('customers_model');
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('services_model');
|
||||
|
||||
$this->initialize_clients();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the client, so that existing execution errors are not passed from one provider to another.
|
||||
*/
|
||||
public function initialize_clients(): void
|
||||
{
|
||||
$http = new GuzzleHttp\Client([
|
||||
'verify' => false,
|
||||
]);
|
||||
|
||||
$this->client = new Google_Client();
|
||||
$this->client->setHttpClient($http);
|
||||
$this->client->setApplicationName('Easy!Appointments');
|
||||
$this->client->setClientId(config('google_client_id'));
|
||||
$this->client->setClientSecret(config('google_client_secret'));
|
||||
$this->client->setRedirectUri(site_url('google/oauth_callback'));
|
||||
$this->client->setPrompt('consent');
|
||||
$this->client->setAccessType('offline');
|
||||
$this->client->addScope([Google_Service_Calendar::CALENDAR]);
|
||||
|
||||
$this->service = new Google_Service_Calendar($this->client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google OAuth authorization url.
|
||||
*
|
||||
* This url must be used to redirect the user to the Google user consent page,
|
||||
* where the user grants access to his data for the Easy!Appointments app.
|
||||
*/
|
||||
public function get_auth_url(): string
|
||||
{
|
||||
// The "max_auth_age" is needed because the user needs to always log in and not use an existing session.
|
||||
return $this->client->createAuthUrl() . '&max_auth_age=0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the Google API usage.
|
||||
*
|
||||
* When the user grants consent for his data usage, Google is going to redirect the browser back to the given
|
||||
* redirect URL. There an authentication code is provided. Using this code, we can authenticate the API usage and
|
||||
* store the token information to the database.
|
||||
*
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function authenticate(string $code): array
|
||||
{
|
||||
$response = $this->client->fetchAccessTokenWithAuthCode($code);
|
||||
|
||||
if (isset($response['error'])) {
|
||||
throw new RuntimeException(
|
||||
'Google Authentication Error (' . $response['error'] . '): ' . $response['error_description'],
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the Google Client access token.
|
||||
*
|
||||
* This method must be executed every time we need to make actions on a provider's Google Calendar account. A new
|
||||
* token is necessary and the only way to get it is to use the stored refresh token that was provided when the
|
||||
* provider granted consent to Easy!Appointments for use his Google Calendar account.
|
||||
*
|
||||
* @param string $refresh_token The provider's refresh token. This value is stored in the database and used every
|
||||
* time we need to make actions to his Google Calendar account.
|
||||
*/
|
||||
public function refresh_token(string $refresh_token): void
|
||||
{
|
||||
$this->initialize_clients();
|
||||
|
||||
$this->client->refreshToken($refresh_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an appointment record to its providers Google Calendar account.
|
||||
*
|
||||
* This method checks whether the appointment's provider has enabled the Google Sync utility of Easy!Appointments
|
||||
* and the stored access token is still valid. If yes, the selected appointment record is going to be added to the
|
||||
* Google Calendar account.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $service Service data.
|
||||
* @param array $customer Customer data.
|
||||
* @param array $settings Required settings.
|
||||
*
|
||||
* @return Event Returns the Google_Event class object.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function add_appointment(
|
||||
array $appointment,
|
||||
array $provider,
|
||||
array $service,
|
||||
array $customer,
|
||||
array $settings,
|
||||
): Event {
|
||||
$event = new Google_Service_Calendar_Event();
|
||||
$event->setSummary(!empty($service) ? $service['name'] : 'Unavailable');
|
||||
$event->setDescription($appointment['notes']);
|
||||
$event->setLocation($appointment['location'] ?? $settings['company_name']);
|
||||
|
||||
$timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$start = new Google_Service_Calendar_EventDateTime();
|
||||
$start->setDateTime(
|
||||
(new DateTime($appointment['start_datetime'], $timezone))->format(DateTimeInterface::RFC3339),
|
||||
);
|
||||
$event->setStart($start);
|
||||
|
||||
$end = new Google_Service_Calendar_EventDateTime();
|
||||
$end->setDateTime((new DateTime($appointment['end_datetime'], $timezone))->format(DateTimeInterface::RFC3339));
|
||||
$event->setEnd($end);
|
||||
|
||||
$event->attendees = [];
|
||||
|
||||
$event_provider = new Google_Service_Calendar_EventAttendee();
|
||||
$event_provider->setDisplayName($provider['first_name'] . ' ' . $provider['last_name']);
|
||||
$event_provider->setEmail($provider['email']);
|
||||
$event->attendees[] = $event_provider;
|
||||
|
||||
if (!empty($customer['first_name']) && !empty($customer['last_name']) && !empty($customer['email'])) {
|
||||
$event_customer = new Google_Service_Calendar_EventAttendee();
|
||||
$event_customer->setDisplayName($customer['first_name'] . ' ' . $customer['last_name']);
|
||||
$event_customer->setEmail($customer['email']);
|
||||
$event->attendees[] = $event_customer;
|
||||
}
|
||||
|
||||
// Add the new event to the Google Calendar.
|
||||
return $this->service->events->insert($provider['settings']['google_calendar'], $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing appointment that is already synced with Google Calendar.
|
||||
*
|
||||
* This method updates the Google Calendar event item that is connected with the provided appointment record of
|
||||
* Easy!Appointments.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $service Service data.
|
||||
* @param array $customer Customer data.
|
||||
* @parma array $settings Required settings.
|
||||
*
|
||||
* @return Event Returns the Google_Service_Calendar_Event class object.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update_appointment(
|
||||
array $appointment,
|
||||
array $provider,
|
||||
array $service,
|
||||
array $customer,
|
||||
array $settings,
|
||||
): Event {
|
||||
$event = $this->service->events->get(
|
||||
$provider['settings']['google_calendar'],
|
||||
$appointment['id_google_calendar'],
|
||||
);
|
||||
|
||||
$event->setSummary($service['name']);
|
||||
$event->setDescription($appointment['notes']);
|
||||
$event->setLocation($appointment['location'] ?? $settings['company_name']);
|
||||
|
||||
$timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$start = new Google_Service_Calendar_EventDateTime();
|
||||
$start->setDateTime(
|
||||
(new DateTime($appointment['start_datetime'], $timezone))->format(DateTimeInterface::RFC3339),
|
||||
);
|
||||
$event->setStart($start);
|
||||
|
||||
$end = new Google_Service_Calendar_EventDateTime();
|
||||
$end->setDateTime((new DateTime($appointment['end_datetime'], $timezone))->format(DateTimeInterface::RFC3339));
|
||||
$event->setEnd($end);
|
||||
|
||||
$event->attendees = [];
|
||||
|
||||
$event_provider = new Google_Service_Calendar_EventAttendee();
|
||||
$event_provider->setDisplayName($provider['first_name'] . ' ' . $provider['last_name']);
|
||||
$event_provider->setEmail($provider['email']);
|
||||
$event->attendees[] = $event_provider;
|
||||
|
||||
if (!empty($customer['first_name']) && !empty($customer['last_name']) && !empty($customer['email'])) {
|
||||
$event_customer = new Google_Service_Calendar_EventAttendee();
|
||||
$event_customer->setDisplayName($customer['first_name'] . ' ' . $customer['last_name']);
|
||||
$event_customer->setEmail($customer['email']);
|
||||
$event->attendees[] = $event_customer;
|
||||
}
|
||||
|
||||
return $this->service->events->update($provider['settings']['google_calendar'], $event->getId(), $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing appointment from Google Calendar.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param string $google_event_id The Google Calendar event ID to be removed.
|
||||
*
|
||||
* @throws \Google\Service\Exception
|
||||
*/
|
||||
public function delete_appointment(array $provider, string $google_event_id): void
|
||||
{
|
||||
$this->service->events->delete($provider['settings']['google_calendar'], $google_event_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add unavailability period event to Google Calendar.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param array $unavailability Unavailable data.
|
||||
*
|
||||
* @return Google_Service_Calendar_Event Returns the Google event.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function add_unavailability(array $provider, array $unavailability): Google_Service_Calendar_Event
|
||||
{
|
||||
$event = new Google_Service_Calendar_Event();
|
||||
$event->setSummary('Unavailable');
|
||||
$event->setDescription($unavailability['notes']);
|
||||
|
||||
$timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$start = new Google_Service_Calendar_EventDateTime();
|
||||
$start->setDateTime(
|
||||
(new DateTime($unavailability['start_datetime'], $timezone))->format(DateTimeInterface::RFC3339),
|
||||
);
|
||||
$event->setStart($start);
|
||||
|
||||
$end = new Google_Service_Calendar_EventDateTime();
|
||||
$end->setDateTime(
|
||||
(new DateTime($unavailability['end_datetime'], $timezone))->format(DateTimeInterface::RFC3339),
|
||||
);
|
||||
$event->setEnd($end);
|
||||
|
||||
// Add the new event to the Google Calendar.
|
||||
return $this->service->events->insert($provider['settings']['google_calendar'], $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Google Calendar unavailability period event.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param array $unavailability Unavailability data.
|
||||
*
|
||||
* @return Google_Service_Calendar_Event Returns the Google_Service_Calendar_Event object.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update_unavailability(array $provider, array $unavailability): Google_Service_Calendar_Event
|
||||
{
|
||||
$event = $this->service->events->get(
|
||||
$provider['settings']['google_calendar'],
|
||||
$unavailability['id_google_calendar'],
|
||||
);
|
||||
|
||||
$event->setSummary('Unavailable');
|
||||
$event->setDescription($unavailability['notes']);
|
||||
|
||||
$timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$start = new Google_Service_Calendar_EventDateTime();
|
||||
$start->setDateTime(
|
||||
(new DateTime($unavailability['start_datetime'], $timezone))->format(DateTimeInterface::RFC3339),
|
||||
);
|
||||
$event->setStart($start);
|
||||
|
||||
$end = new Google_Service_Calendar_EventDateTime();
|
||||
$end->setDateTime(
|
||||
(new DateTime($unavailability['end_datetime'], $timezone))->format(DateTimeInterface::RFC3339),
|
||||
);
|
||||
$event->setEnd($end);
|
||||
|
||||
return $this->service->events->update($provider['settings']['google_calendar'], $event->getId(), $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete unavailability period event from Google Calendar.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param string $google_event_id Google Calendar event ID to be removed.
|
||||
*
|
||||
* @throws \Google\Service\Exception
|
||||
*/
|
||||
public function delete_unavailability(array $provider, string $google_event_id): void
|
||||
{
|
||||
$this->service->events->delete($provider['settings']['google_calendar'], $google_event_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Google Calendar event.
|
||||
*
|
||||
* @param array $provider Provider data.
|
||||
* @param string $google_event_id Google Calendar event ID.
|
||||
*
|
||||
* @return Event Returns the Google Calendar event.
|
||||
*
|
||||
* @throws \Google\Service\Exception
|
||||
*/
|
||||
public function get_event(array $provider, string $google_event_id): Event
|
||||
{
|
||||
return $this->service->events->get($provider['settings']['google_calendar'], $google_event_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the events between the sync period.
|
||||
*
|
||||
* @param string $google_calendar The name of the Google Calendar to be used.
|
||||
* @param string $start The start date of sync period.
|
||||
* @param string $end The end date of sync period.
|
||||
*
|
||||
* @return Events Returns a collection of events.
|
||||
*
|
||||
* @throws \Google\Service\Exception
|
||||
*/
|
||||
public function get_sync_events(string $google_calendar, string $start, string $end): Events
|
||||
{
|
||||
$params = [
|
||||
'timeMin' => date(DateTimeInterface::RFC3339, $start),
|
||||
'timeMax' => date(DateTimeInterface::RFC3339, $end),
|
||||
'singleEvents' => true,
|
||||
];
|
||||
|
||||
return $this->service->events->listEvents($google_calendar, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return available Google Calendars for specific user.
|
||||
*
|
||||
* The given user's token must already exist in db in order to get access to his
|
||||
* Google Calendar account.
|
||||
*
|
||||
* @return array Returns an array with the available calendars.
|
||||
*
|
||||
* @throws \Google\Service\Exception
|
||||
*/
|
||||
public function get_google_calendars(): array
|
||||
{
|
||||
$calendar_list = $this->service->calendarList->listCalendarList();
|
||||
|
||||
$calendars = [];
|
||||
|
||||
foreach ($calendar_list->getItems() as $google_calendar) {
|
||||
if ($google_calendar->getAccessRole() === 'reader') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$calendars[] = [
|
||||
'id' => $google_calendar->getId(),
|
||||
'summary' => $google_calendar->getSummary(),
|
||||
];
|
||||
}
|
||||
|
||||
return $calendars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Add-To-Google-URL, that can be used by anyone to quickly add the event to Google Calendar (no API needed).
|
||||
*
|
||||
* @param int $appointment_id
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function get_add_to_google_url(int $appointment_id): string
|
||||
{
|
||||
$appointment = $this->CI->appointments_model->find($appointment_id);
|
||||
|
||||
$service = $this->CI->services_model->find($appointment['id_services']);
|
||||
|
||||
$provider = $this->CI->providers_model->find($appointment['id_users_provider']);
|
||||
|
||||
$customer = $this->CI->customers_model->find($appointment['id_users_customer']);
|
||||
|
||||
$provider_timezone_instance = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$utc_timezone_instance = new DateTimeZone('UTC');
|
||||
|
||||
$appointment_start_instance = new DateTime($appointment['start_datetime'], $provider_timezone_instance);
|
||||
|
||||
$appointment_start_instance->setTimezone($utc_timezone_instance);
|
||||
|
||||
$appointment_end_instance = new DateTime($appointment['end_datetime'], $provider_timezone_instance);
|
||||
|
||||
$appointment_end_instance->setTimezone($utc_timezone_instance);
|
||||
|
||||
$add = [$provider['email']];
|
||||
|
||||
if (!empty($customer['email'])) {
|
||||
$add[] = $customer['email'];
|
||||
}
|
||||
|
||||
$add_to_google_url_params = [
|
||||
'action' => 'TEMPLATE',
|
||||
'text' => $service['name'],
|
||||
'dates' =>
|
||||
$appointment_start_instance->format('Ymd\THis\Z') .
|
||||
'/' .
|
||||
$appointment_end_instance->format('Ymd\THis\Z'),
|
||||
'location' => setting('company_name'),
|
||||
'details' => 'View/Change Appointment: ' . site_url('booking/reschedule/' . $appointment['hash']),
|
||||
'add' => implode(', ', $add),
|
||||
];
|
||||
|
||||
return 'https://calendar.google.com/calendar/render?' . http_build_query($add_to_google_url_params);
|
||||
}
|
||||
}
|
||||
415
application/libraries/Ics_calendar.php
Normal file
415
application/libraries/Ics_calendar.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Open Source Web Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
|
||||
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link http://easyappointments.org
|
||||
* @since v1.4.3
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use Jsvrcek\ICS\Model\Calendar;
|
||||
use Jsvrcek\ICS\Model\CalendarEvent;
|
||||
use Jsvrcek\ICS\Model\CalendarFreeBusy;
|
||||
use Jsvrcek\ICS\Model\CalendarTodo;
|
||||
use Jsvrcek\ICS\Utility\Provider;
|
||||
|
||||
/**
|
||||
* Class Ics_calendar
|
||||
*
|
||||
* This class replaces the Jsvrcek\ICS\Model\Calendar so that it uses the new Ics_provider instances, which is
|
||||
* compatible to PHP 8.1.
|
||||
*
|
||||
* There is no other change to the original file.
|
||||
*/
|
||||
class Ics_calendar extends Calendar
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $version = '2.0';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $prodId = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $calendarScale = 'GREGORIAN';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $method = 'PUBLISH';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $image = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $customHeaders = [];
|
||||
|
||||
/**
|
||||
* @var DateTimeZone
|
||||
*/
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* @var Provider
|
||||
*/
|
||||
private $events;
|
||||
|
||||
/**
|
||||
* @var Provider
|
||||
*/
|
||||
private $todos;
|
||||
|
||||
/**
|
||||
* @var Provider
|
||||
*/
|
||||
private $freeBusy;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $color;
|
||||
|
||||
/**
|
||||
* Calendar constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->timezone = new DateTimeZone('America/New_York');
|
||||
$this->events = new Ics_provider();
|
||||
$this->todos = new Ics_provider();
|
||||
$this->freeBusy = new Ics_provider();
|
||||
}
|
||||
|
||||
/**
|
||||
* For use if you want CalendarExport::getStream to get events in batches from a database during
|
||||
* the output of the ics feed, instead of adding all events to the Calendar object before outputting
|
||||
* the ics feed.
|
||||
* - CalendarExport::getStream iterates through the Calendar::$events internal data array. The $eventsProvider
|
||||
* closure will be called every time this data array reaches its end during iteration, and the closure should
|
||||
* return the next batch of events
|
||||
* - A $startKey argument with the current key of the data array will be passed to the $eventsProvider closure
|
||||
* - The $eventsProvider must return an array of CalendarEvent objects
|
||||
*
|
||||
* Example: Calendar::setEventsProvider(function($startKey){
|
||||
* //get database rows starting with $startKey
|
||||
* //return an array of CalendarEvent objects
|
||||
* })
|
||||
*
|
||||
* @param Closure $eventsProvider
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setEventsProvider(Closure $eventsProvider)
|
||||
{
|
||||
$this->events = new Ics_provider($eventsProvider);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setVersion($version)
|
||||
{
|
||||
$this->version = $version;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getProdId()
|
||||
{
|
||||
return $this->prodId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prodId
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setProdId($prodId)
|
||||
{
|
||||
$this->prodId = $prodId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the RFC-7986 "Name" field for the calendar
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCalendarScale()
|
||||
{
|
||||
return $this->calendarScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $calendarScale
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setCalendarScale($calendarScale)
|
||||
{
|
||||
$this->calendarScale = $calendarScale;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setMethod($method)
|
||||
{
|
||||
$this->method = $method;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getImage()
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Images can come in one of two formats:
|
||||
* 1: URI - where a URI to the relevant image is provided
|
||||
* 2: BINARY - Where a Binary representation of the image is provided, normally Base64 Encoded.
|
||||
*
|
||||
* If sending a URI for the image, set the "VALUE" key to be "URI" and provide a URI key with the relevant URI.
|
||||
* IE:
|
||||
* $calendar->setImage(
|
||||
* 'VALUE' => 'URL',
|
||||
* 'URI' => 'https://some.domain.com/path/to/image.jpg'
|
||||
* );
|
||||
* It is optional to add a FMTTYPE key as well in the array, to indicate relevant mime type.
|
||||
* IE: 'FMTTYPE' => 'image/jpg'
|
||||
*
|
||||
* When sending Binary version, you must provide the encoding type of the image, as well as the encoded string.
|
||||
* IE:
|
||||
* $calendar->setImage(
|
||||
* 'VALUE' => 'BINARY',
|
||||
* 'ENCODING' => 'BASE64',
|
||||
* 'BINARY' => $base64_encoded_string
|
||||
* );
|
||||
* For Binary, it is RECOMMENDED to add the FMTTYPE as well, but still not REQUIRED
|
||||
*
|
||||
* @param array $image
|
||||
*/
|
||||
public function setImage($image)
|
||||
{
|
||||
// Do some validation on provided data.
|
||||
if (array_key_exists('VALUE', $image) && in_array($image['VALUE'], ['URI', 'BINARY'])) {
|
||||
if ($image['VALUE'] == 'URI' && $image['URI']) {
|
||||
$new_image = [
|
||||
'VALUE' => 'URI',
|
||||
'URI' => $image['URI'],
|
||||
];
|
||||
} elseif ($image['VALUE'] == 'BINARY' && $image['ENCODING'] && $image['BINARY']) {
|
||||
$new_image = [
|
||||
'VALUE' => 'BINARY',
|
||||
'ENCODING' => $image['ENCODING'],
|
||||
'BINARY' => $image['BINARY'],
|
||||
];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
$new_image['DISPLAY'] = isset($image['DISPLAY']) ? $image['DISPLAY'] : '';
|
||||
$new_image['FMTTYPE'] = isset($image['FMTTYPE']) ? $image['FMTTYPE'] : '';
|
||||
$this->image = $new_image;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomHeaders()
|
||||
{
|
||||
return $this->customHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* use to add custom headers as array key-value pairs<br>
|
||||
* <strong>Example:</strong> $customHeaders = array('X-WR-TIMEZONE' => 'America/New_York')
|
||||
*
|
||||
* @param array $customHeaders
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setCustomHeaders(array $customHeaders)
|
||||
{
|
||||
$this->customHeaders = $customHeaders;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return Calendar
|
||||
*/
|
||||
public function addCustomHeader($key, $value)
|
||||
{
|
||||
$this->customHeaders[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeZone
|
||||
*/
|
||||
public function getTimezone()
|
||||
{
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTimeZone $timezone
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setTimezone(DateTimeZone $timezone)
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Provider
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalendarEvent $event
|
||||
* @return Calendar
|
||||
*/
|
||||
public function addEvent(CalendarEvent $event)
|
||||
{
|
||||
$this->events->add($event);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Provider returs array of CalendarTodo objects
|
||||
*/
|
||||
public function getTodos()
|
||||
{
|
||||
return $this->todos;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $todos
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setTodos(array $todos)
|
||||
{
|
||||
$this->todos = $todos;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalendarTodo $todo
|
||||
* @return Calendar
|
||||
*/
|
||||
public function addTodo(CalendarTodo $todo)
|
||||
{
|
||||
$this->todos[] = $todo;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Provider returs array of CalendarFreeBusy objects
|
||||
*/
|
||||
public function getFreeBusy()
|
||||
{
|
||||
return $this->freeBusy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $freeBusy
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setFreeBusy(array $freeBusy)
|
||||
{
|
||||
$this->freeBusy = $freeBusy;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalendarFreeBusy $todo
|
||||
* @return Calendar
|
||||
*/
|
||||
public function addFreeBusy(CalendarFreeBusy $todo)
|
||||
{
|
||||
$this->freeBusy[] = $todo;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor()
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color as CSS3 string
|
||||
*
|
||||
* @param string $color
|
||||
* @return Calendar
|
||||
*/
|
||||
public function setColor($color)
|
||||
{
|
||||
$this->color = $color;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
231
application/libraries/Ics_file.php
Normal file
231
application/libraries/Ics_file.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.3.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use Jsvrcek\ICS\CalendarExport;
|
||||
use Jsvrcek\ICS\CalendarStream;
|
||||
use Jsvrcek\ICS\Exception\CalendarEventException;
|
||||
use Jsvrcek\ICS\Model\CalendarAlarm;
|
||||
use Jsvrcek\ICS\Model\CalendarEvent;
|
||||
use Jsvrcek\ICS\Model\Description\Location;
|
||||
use Jsvrcek\ICS\Model\Relationship\Attendee;
|
||||
use Jsvrcek\ICS\Model\Relationship\Organizer;
|
||||
use Jsvrcek\ICS\Utility\Formatter;
|
||||
|
||||
/**
|
||||
* Ics file library.
|
||||
*
|
||||
* Handle ICS related functionality.
|
||||
*
|
||||
* An ICS file is a calendar file saved in a universal calendar format used by many email and calendar programs,
|
||||
* including Microsoft Outlook, Google Calendar, and Apple Calendar.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Ics_file
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Availability constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->library('ics_provider');
|
||||
$this->CI->load->library('ics_calendar');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ICS file contents for the provided arguments.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $service Service data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $customer Customer data.
|
||||
*
|
||||
* @return string Returns the contents of the ICS file.
|
||||
*
|
||||
* @throws CalendarEventException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function get_stream(array $appointment, array $service, array $provider, array $customer): string
|
||||
{
|
||||
$appointment_timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$appointment_start = new DateTime($appointment['start_datetime'], $appointment_timezone);
|
||||
|
||||
$appointment_end = new DateTime($appointment['end_datetime'], $appointment_timezone);
|
||||
|
||||
// Set up the event.
|
||||
$event = new CalendarEvent();
|
||||
|
||||
$event
|
||||
->setStart($appointment_start)
|
||||
->setEnd($appointment_end)
|
||||
->setStatus('CONFIRMED')
|
||||
->setSummary($service['name'])
|
||||
->setUid($appointment['id_caldav_calendar'] ?: $this->generate_uid($appointment['id']));
|
||||
|
||||
if (!empty($service['location'])) {
|
||||
$location = new Location();
|
||||
$location->setName((string) $service['location']);
|
||||
$event->addLocation($location);
|
||||
}
|
||||
|
||||
$description = [
|
||||
'',
|
||||
lang('provider'),
|
||||
'',
|
||||
lang('name') . ': ' . $provider['first_name'] . ' ' . $provider['last_name'],
|
||||
lang('email') . ': ' . $provider['email'],
|
||||
lang('phone_number') . ': ' . $provider['phone_number'],
|
||||
lang('address') . ': ' . $provider['address'],
|
||||
lang('city') . ': ' . $provider['city'],
|
||||
lang('zip_code') . ': ' . $provider['zip_code'],
|
||||
'',
|
||||
lang('customer'),
|
||||
'',
|
||||
lang('name') . ': ' . $customer['first_name'] . ' ' . $customer['last_name'],
|
||||
lang('email') . ': ' . $customer['email'],
|
||||
lang('phone_number') . ': ' . ($customer['phone_number'] ?? '-'),
|
||||
lang('address') . ': ' . $customer['address'],
|
||||
lang('city') . ': ' . $customer['city'],
|
||||
lang('zip_code') . ': ' . $customer['zip_code'],
|
||||
'',
|
||||
lang('notes'),
|
||||
'',
|
||||
$appointment['notes'],
|
||||
];
|
||||
|
||||
$event->setDescription(implode("\\n", $description));
|
||||
|
||||
$attendee = new Attendee(new Formatter());
|
||||
|
||||
if (isset($customer['email']) && !empty($customer['email'])) {
|
||||
$attendee->setValue($customer['email']);
|
||||
}
|
||||
|
||||
// Add the event attendees.
|
||||
$attendee->setName($customer['first_name'] . ' ' . $customer['last_name']);
|
||||
$attendee
|
||||
->setCalendarUserType('INDIVIDUAL')
|
||||
->setRole('REQ-PARTICIPANT')
|
||||
->setParticipationStatus('NEEDS-ACTION')
|
||||
->setRsvp('TRUE');
|
||||
$event->addAttendee($attendee);
|
||||
|
||||
$alarm = new CalendarAlarm();
|
||||
$alarm_datetime = clone $appointment_start;
|
||||
$alarm->setTrigger($alarm_datetime->modify('-15 minutes'));
|
||||
$alarm->setSummary('Alarm notification');
|
||||
$alarm->setDescription('This is an event reminder');
|
||||
$alarm->setAction('EMAIL');
|
||||
$alarm->addAttendee($attendee);
|
||||
$event->addAlarm($alarm);
|
||||
|
||||
$alarm = new CalendarAlarm();
|
||||
$alarm_datetime = clone $appointment_start;
|
||||
$alarm->setTrigger($alarm_datetime->modify('-60 minutes'));
|
||||
$alarm->setSummary('Alarm notification');
|
||||
$alarm->setDescription('This is an event reminder');
|
||||
$alarm->setAction('EMAIL');
|
||||
$alarm->addAttendee($attendee);
|
||||
$event->addAlarm($alarm);
|
||||
|
||||
$attendee = new Attendee(new Formatter());
|
||||
|
||||
if (isset($provider['email']) && !empty($provider['email'])) {
|
||||
$attendee->setValue($provider['email']);
|
||||
}
|
||||
|
||||
$attendee->setName($provider['first_name'] . ' ' . $provider['last_name']);
|
||||
$attendee
|
||||
->setCalendarUserType('INDIVIDUAL')
|
||||
->setRole('REQ-PARTICIPANT')
|
||||
->setParticipationStatus('ACCEPTED')
|
||||
->setRsvp('FALSE');
|
||||
$event->addAttendee($attendee);
|
||||
|
||||
// Set the organizer.
|
||||
$organizer = new Organizer(new Formatter());
|
||||
|
||||
$organizer->setValue($provider['email'])->setName($provider['first_name'] . ' ' . $provider['last_name']);
|
||||
|
||||
$event->setOrganizer($organizer);
|
||||
|
||||
// Setup calendar.
|
||||
$calendar = new Ics_calendar();
|
||||
|
||||
$calendar
|
||||
->setProdId('-//EasyAppointments//Open Source Web Scheduler//EN')
|
||||
->setTimezone(new DateTimeZone($provider['timezone']))
|
||||
->addEvent($event);
|
||||
|
||||
// Setup exporter.
|
||||
$calendarExport = new CalendarExport(new CalendarStream(), new Formatter());
|
||||
$calendarExport->addCalendar($calendar);
|
||||
|
||||
return $calendarExport->getStream();
|
||||
}
|
||||
|
||||
public function get_unavailability_stream(array $unavailability, array $provider): string
|
||||
{
|
||||
$unavailability_timezone = new DateTimeZone($provider['timezone']);
|
||||
|
||||
$unavailability_start = new DateTime($unavailability['start_datetime'], $unavailability_timezone);
|
||||
|
||||
$unavailability_end = new DateTime($unavailability['end_datetime'], $unavailability_timezone);
|
||||
|
||||
// Set up the event.
|
||||
$event = new CalendarEvent();
|
||||
|
||||
$event
|
||||
->setStart($unavailability_start)
|
||||
->setEnd($unavailability_end)
|
||||
->setStatus('CONFIRMED')
|
||||
->setSummary('Unavailability')
|
||||
->setUid($unavailability['id_caldav_calendar'] ?: $this->generate_uid($unavailability['id']));
|
||||
|
||||
$event->setDescription(str_replace("\n", "\\n", (string) $unavailability['notes']));
|
||||
|
||||
// Set the organizer.
|
||||
$organizer = new Organizer(new Formatter());
|
||||
|
||||
$organizer->setValue($provider['email'])->setName($provider['first_name'] . ' ' . $provider['last_name']);
|
||||
|
||||
$event->setOrganizer($organizer);
|
||||
|
||||
// Setup calendar.
|
||||
$calendar = new Ics_calendar();
|
||||
|
||||
$calendar
|
||||
->setProdId('-//EasyAppointments//Open Source Web Scheduler//EN')
|
||||
->setTimezone(new DateTimeZone($provider['timezone']))
|
||||
->addEvent($event);
|
||||
|
||||
// Setup exporter.
|
||||
$calendarExport = new CalendarExport(new CalendarStream(), new Formatter());
|
||||
$calendarExport->addCalendar($calendar);
|
||||
|
||||
return $calendarExport->getStream();
|
||||
}
|
||||
|
||||
public function generate_uid(int $db_record_id): string
|
||||
{
|
||||
return 'ea-' . md5($db_record_id);
|
||||
}
|
||||
}
|
||||
162
application/libraries/Ics_provider.php
Normal file
162
application/libraries/Ics_provider.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Open Source Web Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
|
||||
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link http://easyappointments.org
|
||||
* @since v1.4.3
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Class Ics_calendar
|
||||
*
|
||||
* This class replaces the Jsvrcek\ICS\Utility\Provider so that it becomes a PHP 8.1 compatible Iterator class.
|
||||
*
|
||||
* Since the method signatures changed in PHP 8.1, the ReturnTypeWillChange attribute allows us to keep compatibility
|
||||
* between different PHP versions.
|
||||
*/
|
||||
class Ics_provider implements Iterator
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $manuallyAddedData = [];
|
||||
/**
|
||||
* @var Closure
|
||||
*/
|
||||
private $provider;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $key;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
private $first;
|
||||
|
||||
/**
|
||||
* @param Closure $provider An optional closure for adding items in batches during iteration. The closure will be
|
||||
* called each time the end of the internal data array is reached during iteration, and the current data
|
||||
* array key value will be passed as an argument. The closure should return an array containing the next
|
||||
* batch of items.
|
||||
*/
|
||||
public function __construct(?Closure $provider = null)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* for manually adding items, rather than using a provider closure to add items in batches during iteration
|
||||
* Cannot be used in conjunction with a provider closure!
|
||||
*
|
||||
* @param mixed $item
|
||||
* @return void
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function add($item)
|
||||
{
|
||||
$this->manuallyAddedData[] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|mixed
|
||||
* @see Iterator::current()
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function current()
|
||||
{
|
||||
return current($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int|null
|
||||
* @see Iterator::key()
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function key()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @see Iterator::next()
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function next()
|
||||
{
|
||||
array_shift($this->data);
|
||||
$this->key++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @see Iterator::rewind()
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function rewind()
|
||||
{
|
||||
$this->data = [];
|
||||
$this->key = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first event
|
||||
*
|
||||
* @return false|mixed
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function first()
|
||||
{
|
||||
if (isset($this->first)) {
|
||||
return $this->first;
|
||||
}
|
||||
|
||||
if ($this->provider instanceof Closure) {
|
||||
if ($this->valid()) {
|
||||
return $this->first;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($this->manuallyAddedData[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->manuallyAddedData[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* get next batch from provider if data array is at the end
|
||||
*
|
||||
* @return bool
|
||||
* @see Iterator::valid()
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function valid()
|
||||
{
|
||||
if (count($this->data) < 1) {
|
||||
if ($this->provider instanceof Closure) {
|
||||
$this->data = $this->provider->__invoke($this->key);
|
||||
if (isset($this->data[0])) {
|
||||
$this->first = $this->data[0];
|
||||
}
|
||||
} else {
|
||||
$this->data = $this->manuallyAddedData;
|
||||
$this->manuallyAddedData = [];
|
||||
}
|
||||
}
|
||||
|
||||
return count($this->data) > 0;
|
||||
}
|
||||
}
|
||||
183
application/libraries/Instance.php
Normal file
183
application/libraries/Instance.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
require_once __DIR__ . '/../core/EA_Migration.php';
|
||||
|
||||
/**
|
||||
* Instance library.
|
||||
*
|
||||
* Handles all Easy!Appointments instance related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Instance
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Installation constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('admins_model');
|
||||
$this->CI->load->model('services_model');
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('customers_model');
|
||||
|
||||
$this->CI->load->library('timezones');
|
||||
$this->CI->load->library('migration');
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the database to the latest state.
|
||||
*
|
||||
* @param string $type Provide "fresh" to revert previous migrations and start from the beginning or "up"/"down" to step.
|
||||
*/
|
||||
public function migrate(string $type = ''): void
|
||||
{
|
||||
$current_version = $this->CI->migration->current_version();
|
||||
|
||||
if ($type === 'up') {
|
||||
if (!$this->CI->migration->version($current_version + 1)) {
|
||||
show_error($this->CI->migration->error_string());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'down') {
|
||||
if (!$this->CI->migration->version($current_version - 1)) {
|
||||
show_error($this->CI->migration->error_string());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'fresh' && !$this->CI->migration->version(0)) {
|
||||
show_error($this->CI->migration->error_string());
|
||||
}
|
||||
|
||||
if ($this->CI->migration->latest() === false) {
|
||||
show_error($this->CI->migration->error_string());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the database with test data.
|
||||
*
|
||||
* @return string Return's the administrator user password.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function seed(): string
|
||||
{
|
||||
// Settings
|
||||
|
||||
setting([
|
||||
'company_name' => 'Company Name',
|
||||
'company_email' => 'info@example.org',
|
||||
'company_link' => 'https://example.org',
|
||||
]);
|
||||
|
||||
// Admin
|
||||
|
||||
$password = 'administrator';
|
||||
|
||||
$this->CI->admins_model->save([
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'john@example.org',
|
||||
'phone_number' => '+10000000000',
|
||||
'settings' => [
|
||||
'username' => 'administrator',
|
||||
'password' => $password,
|
||||
'notifications' => true,
|
||||
'calendar_view' => CALENDAR_VIEW_DEFAULT,
|
||||
],
|
||||
]);
|
||||
|
||||
// Service
|
||||
|
||||
$service_id = $this->CI->services_model->save([
|
||||
'name' => 'Service',
|
||||
'duration' => '30',
|
||||
'price' => '0',
|
||||
'currency' => '',
|
||||
'availabilities_type' => 'flexible',
|
||||
'attendants_number' => '1',
|
||||
]);
|
||||
|
||||
// Provider
|
||||
|
||||
$this->CI->providers_model->save([
|
||||
'first_name' => 'Jane',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'jane@example.org',
|
||||
'phone_number' => '+10000000000',
|
||||
'services' => [$service_id],
|
||||
'settings' => [
|
||||
'username' => 'janedoe',
|
||||
'password' => random_string(),
|
||||
'working_plan' => setting('company_working_plan'),
|
||||
'working_plan_exceptions' => '{}',
|
||||
'notifications' => true,
|
||||
'google_sync' => false,
|
||||
'sync_past_days' => 30,
|
||||
'sync_future_days' => 90,
|
||||
'calendar_view' => CALENDAR_VIEW_DEFAULT,
|
||||
],
|
||||
]);
|
||||
|
||||
// Customer
|
||||
|
||||
$this->CI->customers_model->save([
|
||||
'first_name' => 'James',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'james@example.org',
|
||||
'phone_number' => '+10000000000',
|
||||
]);
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a database backup file.
|
||||
*
|
||||
* @param string|null $path Override the default backup path (storage/backups/*).
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function backup(string $path = null): void
|
||||
{
|
||||
$path = $path ?? APPPATH . '/../storage/backups';
|
||||
|
||||
if (!file_exists($path)) {
|
||||
throw new InvalidArgumentException('The backup path does not exist: ' . $path);
|
||||
}
|
||||
|
||||
if (!is_writable($path)) {
|
||||
throw new RuntimeException('The backup path is not writable: ' . $path);
|
||||
}
|
||||
|
||||
$contents = $this->CI->dbutil->backup();
|
||||
|
||||
$filename = 'easyappointments-backup-' . date('Y-m-d-His') . '.gz';
|
||||
|
||||
write_file(rtrim($path, '/') . '/' . $filename, $contents);
|
||||
}
|
||||
}
|
||||
187
application/libraries/Ldap_client.php
Normal file
187
application/libraries/Ldap_client.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.5.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Ldap_client library.
|
||||
*
|
||||
* Handles LDAP related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Ldap_client
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Ldap_client constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('roles_model');
|
||||
|
||||
$this->CI->load->library('timezones');
|
||||
$this->CI->load->library('accounts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try authenticating the user with LDAP
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function check_login(string $username, string $password): ?array
|
||||
{
|
||||
if (!extension_loaded('ldap')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($username)) {
|
||||
throw new InvalidArgumentException('No username value provided.');
|
||||
}
|
||||
|
||||
$ldap_is_active = setting('ldap_is_active');
|
||||
|
||||
if (!$ldap_is_active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Match user by username
|
||||
|
||||
$user = $this->CI->accounts->get_user_by_username($username);
|
||||
|
||||
if (empty($user['ldap_dn'])) {
|
||||
return null; // User does not exist in Easy!Appointments
|
||||
}
|
||||
|
||||
// Connect to LDAP server
|
||||
|
||||
$ldap_host = setting('ldap_host');
|
||||
$ldap_port = (int) setting('ldap_port');
|
||||
|
||||
$connection = @ldap_connect($ldap_host, $ldap_port);
|
||||
@ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||
$user_bind = @ldap_bind($connection, $user['ldap_dn'], $password);
|
||||
|
||||
if ($user_bind) {
|
||||
$role = $this->CI->roles_model->find($user['id_roles']);
|
||||
|
||||
$default_timezone = $this->CI->timezones->get_default_timezone();
|
||||
|
||||
return [
|
||||
'user_id' => $user['id'],
|
||||
'user_email' => $user['email'],
|
||||
'username' => $username,
|
||||
'timezone' => !empty($user['timezone']) ? $user['timezone'] : $default_timezone,
|
||||
'language' => !empty($user['language']) ? $user['language'] : Config::LANGUAGE,
|
||||
'role_slug' => $role['slug'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the LDAP server based on the provided keyword and configuration.
|
||||
*
|
||||
* @param string $keyword
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function search(string $keyword): array
|
||||
{
|
||||
$this->check_environment();
|
||||
|
||||
$host = setting('ldap_host');
|
||||
$port = (int) setting('ldap_port');
|
||||
$user_dn = setting('ldap_user_dn');
|
||||
$password = setting('ldap_password');
|
||||
$base_dn = setting('ldap_base_dn');
|
||||
$filter = setting('ldap_filter');
|
||||
|
||||
$connection = @ldap_connect($host, $port);
|
||||
|
||||
if (!$connection) {
|
||||
throw new Exception('Could not connect to LDAP server: ' . @ldap_error($connection));
|
||||
}
|
||||
|
||||
@ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||
@ldap_set_option($connection, LDAP_OPT_REFERRALS, 0); // We need this for doing an LDAP search.
|
||||
|
||||
$bind = @ldap_bind($connection, $user_dn, $password);
|
||||
|
||||
if (!$bind) {
|
||||
throw new Exception('LDAP bind failed: ' . @ldap_error($connection));
|
||||
}
|
||||
|
||||
$wildcard_keyword = !empty($keyword) ? '*' . $keyword . '*' : '*';
|
||||
|
||||
$interpolated_filter = str_replace('{{KEYWORD}}', $wildcard_keyword, $filter);
|
||||
|
||||
$result = @ldap_search($connection, $base_dn, $interpolated_filter);
|
||||
|
||||
if (!$result) {
|
||||
throw new Exception('Search failed: ' . @ldap_error($connection));
|
||||
}
|
||||
|
||||
$ldap_entries = @ldap_get_entries($connection, $result);
|
||||
|
||||
// Flatten the LDAP entries so that they become easier to import
|
||||
|
||||
$entries = [];
|
||||
|
||||
foreach ($ldap_entries as $ldap_entry) {
|
||||
if (!is_array($ldap_entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'dn' => $ldap_entry['dn'],
|
||||
];
|
||||
|
||||
foreach ($ldap_entry as $key => $value) {
|
||||
if (!is_array($value) || !in_array($key, LDAP_WHITELISTED_ATTRIBUTES)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry[$key] = $value[0] ?? null;
|
||||
}
|
||||
|
||||
$entries[] = $entry;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ldap extension is installed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function check_environment(): void
|
||||
{
|
||||
if (!extension_loaded('ldap')) {
|
||||
throw new RuntimeException('The LDAP extension is not loaded.');
|
||||
}
|
||||
}
|
||||
}
|
||||
324
application/libraries/Notifications.php
Normal file
324
application/libraries/Notifications.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Notifications library.
|
||||
*
|
||||
* Handles the notifications related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Notifications
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Notifications constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('admins_model');
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('settings_model');
|
||||
|
||||
$this->CI->load->library('email_messages');
|
||||
$this->CI->load->library('ics_file');
|
||||
$this->CI->load->library('timezones');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the required notifications, related to an appointment creation/modification.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $service Service data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $customer Customer data.
|
||||
* @param array $settings Required settings.
|
||||
* @param bool|false $manage_mode Manage mode.
|
||||
*/
|
||||
public function notify_appointment_saved(
|
||||
array $appointment,
|
||||
array $service,
|
||||
array $provider,
|
||||
array $customer,
|
||||
array $settings,
|
||||
bool $manage_mode = false,
|
||||
): void {
|
||||
try {
|
||||
$current_language = config('language');
|
||||
|
||||
$customer_link = site_url('booking/reschedule/' . $appointment['hash']);
|
||||
|
||||
$provider_link = site_url('calendar/reschedule/' . $appointment['hash']);
|
||||
|
||||
$ics_stream = $this->CI->ics_file->get_stream($appointment, $service, $provider, $customer);
|
||||
|
||||
// Notify customer.
|
||||
$send_customer =
|
||||
!empty($customer['email']) && filter_var(setting('customer_notifications'), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if ($send_customer === true) {
|
||||
config(['language' => $customer['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
$subject = $manage_mode ? lang('appointment_details_changed') : lang('appointment_booked');
|
||||
$message = $manage_mode ? '' : lang('thank_you_for_appointment');
|
||||
|
||||
$this->CI->email_messages->send_appointment_saved(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$subject,
|
||||
$message,
|
||||
$customer_link,
|
||||
$customer['email'],
|
||||
$ics_stream,
|
||||
$customer['timezone'],
|
||||
);
|
||||
}
|
||||
|
||||
// Notify provider.
|
||||
$send_provider = filter_var(
|
||||
$this->CI->providers_model->get_setting($provider['id'], 'notifications'),
|
||||
FILTER_VALIDATE_BOOLEAN,
|
||||
);
|
||||
|
||||
if ($send_provider === true) {
|
||||
config(['language' => $provider['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
$subject = $manage_mode ? lang('appointment_details_changed') : lang('appointment_added_to_your_plan');
|
||||
$message = $manage_mode ? '' : lang('appointment_link_description');
|
||||
|
||||
$this->CI->email_messages->send_appointment_saved(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$subject,
|
||||
$message,
|
||||
$provider_link,
|
||||
$provider['email'],
|
||||
$ics_stream,
|
||||
$provider['timezone'],
|
||||
);
|
||||
}
|
||||
|
||||
// Notify admins.
|
||||
$admins = $this->CI->admins_model->get();
|
||||
|
||||
foreach ($admins as $admin) {
|
||||
if ($admin['settings']['notifications'] === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
config(['language' => $admin['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
$subject = $manage_mode ? lang('appointment_details_changed') : lang('appointment_added_to_your_plan');
|
||||
$message = $manage_mode ? '' : lang('appointment_link_description');
|
||||
|
||||
$this->CI->email_messages->send_appointment_saved(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$subject,
|
||||
$message,
|
||||
$provider_link,
|
||||
$admin['email'],
|
||||
$ics_stream,
|
||||
$admin['timezone'],
|
||||
);
|
||||
}
|
||||
|
||||
// Notify secretaries.
|
||||
$secretaries = $this->CI->secretaries_model->get();
|
||||
|
||||
foreach ($secretaries as $secretary) {
|
||||
if ($secretary['settings']['notifications'] === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($provider['id'], $secretary['providers'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
config(['language' => $secretary['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
$subject = $manage_mode ? lang('appointment_details_changed') : lang('appointment_added_to_your_plan');
|
||||
$message = $manage_mode ? '' : lang('appointment_link_description');
|
||||
|
||||
$this->CI->email_messages->send_appointment_saved(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$subject,
|
||||
$message,
|
||||
$provider_link,
|
||||
$secretary['email'],
|
||||
$ics_stream,
|
||||
$secretary['timezone'],
|
||||
);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Notifications - Could not email confirmation details of appointment (' .
|
||||
($appointment['id'] ?? '-') .
|
||||
') : ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
log_message('error', $e->getTraceAsString());
|
||||
} finally {
|
||||
config(['language' => $current_language ?? 'english']);
|
||||
$this->CI->lang->load('translations');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the required notifications, related to an appointment removal.
|
||||
*
|
||||
* @param array $appointment Appointment data.
|
||||
* @param array $service Service data.
|
||||
* @param array $provider Provider data.
|
||||
* @param array $customer Customer data.
|
||||
* @param array $settings Required settings.
|
||||
*/
|
||||
public function notify_appointment_deleted(
|
||||
array $appointment,
|
||||
array $service,
|
||||
array $provider,
|
||||
array $customer,
|
||||
array $settings,
|
||||
string $cancellation_reason = '',
|
||||
): void {
|
||||
try {
|
||||
$current_language = config('language');
|
||||
|
||||
// Notify provider.
|
||||
$send_provider = filter_var(
|
||||
$this->CI->providers_model->get_setting($provider['id'], 'notifications'),
|
||||
FILTER_VALIDATE_BOOLEAN,
|
||||
);
|
||||
|
||||
if ($send_provider === true) {
|
||||
config(['language' => $provider['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
|
||||
$this->CI->email_messages->send_appointment_deleted(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$provider['email'],
|
||||
$cancellation_reason,
|
||||
$provider['timezone'],
|
||||
);
|
||||
}
|
||||
|
||||
// Notify customer.
|
||||
$send_customer =
|
||||
!empty($customer['email']) && filter_var(setting('customer_notifications'), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if ($send_customer === true) {
|
||||
config(['language' => $customer['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
|
||||
$this->CI->email_messages->send_appointment_deleted(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$customer['email'],
|
||||
$cancellation_reason,
|
||||
$customer['timezone'],
|
||||
);
|
||||
}
|
||||
|
||||
// Notify admins.
|
||||
$admins = $this->CI->admins_model->get();
|
||||
|
||||
foreach ($admins as $admin) {
|
||||
if ($admin['settings']['notifications'] === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
config(['language' => $admin['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
|
||||
$this->CI->email_messages->send_appointment_deleted(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$admin['email'],
|
||||
$cancellation_reason,
|
||||
$admin['timezone'],
|
||||
);
|
||||
}
|
||||
|
||||
// Notify secretaries.
|
||||
$secretaries = $this->CI->secretaries_model->get();
|
||||
|
||||
foreach ($secretaries as $secretary) {
|
||||
if ($secretary['settings']['notifications'] === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($provider['id'], $secretary['providers'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
config(['language' => $secretary['language']]);
|
||||
$this->CI->lang->load('translations');
|
||||
|
||||
$this->CI->email_messages->send_appointment_deleted(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
$secretary['email'],
|
||||
$cancellation_reason,
|
||||
$secretary['timezone'],
|
||||
);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Notifications - Could not email cancellation details of appointment (' .
|
||||
($appointment['id'] ?? '-') .
|
||||
') : ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
log_message('error', $e->getTraceAsString());
|
||||
} finally {
|
||||
config(['language' => $current_language ?? 'english']);
|
||||
$this->CI->lang->load('translations');
|
||||
}
|
||||
}
|
||||
}
|
||||
96
application/libraries/Permissions.php
Normal file
96
application/libraries/Permissions.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.5.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Permissions library.
|
||||
*
|
||||
* Handles permission related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Permissions
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Permissions constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('roles_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('users_model');
|
||||
|
||||
$this->CI->load->library('timezones');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is allowed to manage the provided customer.
|
||||
*
|
||||
* The "limit_customer_access" setting changes the access permissions to customer entries. In order for a provider
|
||||
* or a secretary to be able to make changes to a customer, they will first need to at least have a single
|
||||
* appointment with them.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $customer_id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_customer_access(int $user_id, int $customer_id): bool
|
||||
{
|
||||
$role_id = $this->CI->users_model->value($user_id, 'id_roles');
|
||||
|
||||
$role_slug = $this->CI->roles_model->value($role_id, 'slug');
|
||||
|
||||
$limit_customer_access = setting('limit_customer_access');
|
||||
|
||||
if ($role_slug === DB_SLUG_ADMIN || !$limit_customer_access) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($role_slug === DB_SLUG_PROVIDER) {
|
||||
return $this->CI->appointments_model
|
||||
->query()
|
||||
->where(['id_users_provider' => $user_id, 'id_users_customer' => $customer_id])
|
||||
->get()
|
||||
->num_rows() > 0;
|
||||
}
|
||||
|
||||
if ($role_slug === DB_SLUG_SECRETARY) {
|
||||
$secretary = $this->CI->secretaries_model->find($user_id);
|
||||
|
||||
foreach ($secretary['providers'] as $secretary_provider_id) {
|
||||
$has_appointments_with_customer =
|
||||
$this->CI->appointments_model
|
||||
->query()
|
||||
->where(['id_users_provider' => $secretary_provider_id, 'id_users_customer' => $customer_id])
|
||||
->get()
|
||||
->num_rows() > 0;
|
||||
|
||||
if ($has_appointments_with_customer) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
270
application/libraries/Synchronization.php
Normal file
270
application/libraries/Synchronization.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Synchronization library.
|
||||
*
|
||||
* Handles external calendar synchronization functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Synchronization
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Synchronization constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('appointments_model');
|
||||
|
||||
$this->CI->load->library('google_sync');
|
||||
$this->CI->load->library('caldav_sync');
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize changes made to the appointment with external calendars.
|
||||
*
|
||||
* @param array $appointment Appointment record.
|
||||
* @param array $service Service record.
|
||||
* @param array $provider Provider record.
|
||||
* @param array $customer Customer record.
|
||||
* @param array $settings Required settings for the notification content.
|
||||
*/
|
||||
public function sync_appointment_saved(
|
||||
array $appointment,
|
||||
array $service,
|
||||
array $provider,
|
||||
array $customer,
|
||||
array $settings,
|
||||
): void {
|
||||
try {
|
||||
// Google
|
||||
|
||||
if ($provider['settings']['google_sync']) {
|
||||
if (empty($provider['settings']['google_token'])) {
|
||||
throw new RuntimeException('No google token available for the provider: ' . $provider['id']);
|
||||
}
|
||||
|
||||
$google_token = json_decode($provider['settings']['google_token'], true);
|
||||
|
||||
$this->CI->google_sync->refresh_token($google_token['refresh_token']);
|
||||
|
||||
if (empty($appointment['id_google_calendar'])) {
|
||||
$google_event = $this->CI->google_sync->add_appointment(
|
||||
$appointment,
|
||||
$provider,
|
||||
$service,
|
||||
$customer,
|
||||
$settings,
|
||||
);
|
||||
|
||||
$appointment['id_google_calendar'] = $google_event->getId();
|
||||
|
||||
$this->CI->appointments_model->save($appointment);
|
||||
} else {
|
||||
$this->CI->google_sync->update_appointment($appointment, $provider, $service, $customer, $settings);
|
||||
}
|
||||
}
|
||||
|
||||
// CalDAV
|
||||
|
||||
if ($provider['settings']['caldav_sync']) {
|
||||
$appointment['id_caldav_calendar'] = $this->CI->caldav_sync->save_appointment(
|
||||
$appointment,
|
||||
$service,
|
||||
$provider,
|
||||
$customer,
|
||||
);
|
||||
|
||||
$this->CI->appointments_model->save($appointment);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Synchronization - Could not sync confirmation details of appointment (' .
|
||||
($appointment['id'] ?? '-') .
|
||||
') : ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
log_message('error', $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize removal of an appointment with external calendars.
|
||||
*
|
||||
* @param array $appointment Appointment record.
|
||||
* @param array $provider Provider record.
|
||||
*/
|
||||
public function sync_appointment_deleted(array $appointment, array $provider): void
|
||||
{
|
||||
try {
|
||||
// Google
|
||||
|
||||
if ($provider['settings']['google_sync'] && !empty($appointment['id_google_calendar'])) {
|
||||
if (empty($provider['settings']['google_token'])) {
|
||||
throw new RuntimeException('No google token available for the provider: ' . $provider['id']);
|
||||
}
|
||||
|
||||
$google_token = json_decode($provider['settings']['google_token'], true);
|
||||
|
||||
$this->CI->google_sync->refresh_token($google_token['refresh_token']);
|
||||
|
||||
$this->CI->google_sync->delete_appointment($provider, $appointment['id_google_calendar']);
|
||||
}
|
||||
|
||||
// CalDAV
|
||||
|
||||
if ($provider['settings']['caldav_sync'] && !empty($appointment['id_caldav_calendar'])) {
|
||||
$this->CI->caldav_sync->delete_event($provider, $appointment['id_caldav_calendar']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Synchronization - Could not sync cancellation details of appointment (' .
|
||||
($appointment['id'] ?? '-') .
|
||||
') : ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
log_message('error', $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize changes made to the unavailability with external calendars.
|
||||
*
|
||||
* @param array $unavailability Unavailability record.
|
||||
* @param array $provider Provider record.
|
||||
*/
|
||||
public function sync_unavailability_saved(array $unavailability, array $provider): void
|
||||
{
|
||||
try {
|
||||
// Google
|
||||
|
||||
if ($provider['settings']['google_sync']) {
|
||||
if (empty($provider['settings']['google_token'])) {
|
||||
throw new RuntimeException('No google token available for the provider: ' . $provider['id']);
|
||||
}
|
||||
|
||||
$google_token = json_decode($provider['settings']['google_token'], true);
|
||||
|
||||
$this->CI->google_sync->refresh_token($google_token['refresh_token']);
|
||||
|
||||
if (empty($unavailability['id_google_calendar'])) {
|
||||
$google_event = $this->CI->google_sync->add_unavailability($provider, $unavailability);
|
||||
|
||||
$unavailability['id_google_calendar'] = $google_event->getId();
|
||||
|
||||
$this->CI->unavailabilities_model->save($unavailability);
|
||||
} else {
|
||||
$this->CI->google_sync->update_unavailability($provider, $unavailability);
|
||||
}
|
||||
}
|
||||
|
||||
// CalDAV
|
||||
|
||||
if ($provider['settings']['caldav_sync']) {
|
||||
$unavailability['id_caldav_calendar'] = $this->CI->caldav_sync->save_unavailability(
|
||||
$unavailability,
|
||||
$provider,
|
||||
);
|
||||
|
||||
$this->CI->unavailabilities_model->save($unavailability);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Synchronization - Could not sync cancellation details of unavailability (' .
|
||||
($appointment['id'] ?? '-') .
|
||||
') : ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
log_message('error', $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize removal of an unavailability with external calendars.
|
||||
*
|
||||
* @param array $unavailability Unavailability record.
|
||||
* @param array $provider Provider record.
|
||||
*/
|
||||
public function sync_unavailability_deleted(array $unavailability, array $provider): void
|
||||
{
|
||||
try {
|
||||
// Google
|
||||
|
||||
if ($provider['settings']['google_sync'] && !empty($unavailability['id_google_calendar'])) {
|
||||
if (empty($provider['settings']['google_token'])) {
|
||||
throw new RuntimeException('No google token available for the provider: ' . $provider['id']);
|
||||
}
|
||||
|
||||
$google_token = json_decode($provider['settings']['google_token'], true);
|
||||
|
||||
$this->CI->google_sync->refresh_token($google_token['refresh_token']);
|
||||
|
||||
$this->CI->google_sync->delete_unavailability($provider, $unavailability['id_google_calendar']);
|
||||
}
|
||||
|
||||
// CalDAV
|
||||
|
||||
if ($provider['settings']['caldav_sync'] && !empty($unavailability['id_caldav_calendar'])) {
|
||||
$this->CI->caldav_sync->delete_event($provider, $unavailability['id_caldav_calendar']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Synchronization - Could not sync cancellation details of unavailability (' .
|
||||
($appointment['id'] ?? '-') .
|
||||
') : ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
log_message('error', $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure a synced appointment is removed from Google/CalDAV Calendar, if its provider is changed.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function remove_appointment_on_provider_change($appointment_id): void
|
||||
{
|
||||
$existing_appointment = $this->CI->appointments_model->find($appointment_id);
|
||||
|
||||
$existing_google_id = $existing_appointment['id_google_calendar'];
|
||||
$existing_caldav_id = $existing_appointment['id_caldav_calendar'];
|
||||
|
||||
$existing_provider_id = $existing_appointment['id_users_provider'];
|
||||
|
||||
if (
|
||||
(!empty($existing_google_id) || !empty($existing_caldav_id)) &&
|
||||
(int) $existing_provider_id !== (int) $existing_appointment['id_users_provider']
|
||||
) {
|
||||
$existing_provider = $this->CI->providers_model->find($existing_provider_id);
|
||||
|
||||
if ($existing_provider['settings']['google_sync'] || $existing_provider['settings']['caldav_sync']) {
|
||||
$this->sync_appointment_deleted($existing_appointment, $existing_provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
587
application/libraries/Timezones.php
Normal file
587
application/libraries/Timezones.php
Normal file
@@ -0,0 +1,587 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Online Appointment Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) Alex Tselegidis
|
||||
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link https://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Timezones library.
|
||||
*
|
||||
* Handles timezone related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Timezones
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $default = 'UTC';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $timezones = [
|
||||
'UTC' => [
|
||||
'UTC' => 'UTC',
|
||||
],
|
||||
'America' => [
|
||||
'America/Adak' => 'Adak (-10:00)',
|
||||
'America/Atka' => 'Atka (-10:00)',
|
||||
'America/Anchorage' => 'Anchorage (-9:00)',
|
||||
'America/Juneau' => 'Juneau (-9:00)',
|
||||
'America/Nome' => 'Nome (-9:00)',
|
||||
'America/Yakutat' => 'Yakutat (-9:00)',
|
||||
'America/Dawson' => 'Dawson (-8:00)',
|
||||
'America/Ensenada' => 'Ensenada (-8:00)',
|
||||
'America/Los_Angeles' => 'Los_Angeles (-8:00)',
|
||||
'America/Tijuana' => 'Tijuana (-8:00)',
|
||||
'America/Vancouver' => 'Vancouver (-8:00)',
|
||||
'America/Whitehorse' => 'Whitehorse (-8:00)',
|
||||
'America/Boise' => 'Boise (-7:00)',
|
||||
'America/Cambridge_Bay' => 'Cambridge_Bay (-7:00)',
|
||||
'America/Chihuahua' => 'Chihuahua (-7:00)',
|
||||
'America/Dawson_Creek' => 'Dawson_Creek (-7:00)',
|
||||
'America/Denver' => 'Denver (-7:00)',
|
||||
'America/Edmonton' => 'Edmonton (-7:00)',
|
||||
'America/Hermosillo' => 'Hermosillo (-7:00)',
|
||||
'America/Inuvik' => 'Inuvik (-7:00)',
|
||||
'America/Mazatlan' => 'Mazatlan (-7:00)',
|
||||
'America/Phoenix' => 'Phoenix (-7:00)',
|
||||
'America/Shiprock' => 'Shiprock (-7:00)',
|
||||
'America/Yellowknife' => 'Yellowknife (-7:00)',
|
||||
'America/Belize' => 'Belize (-6:00)',
|
||||
'America/Cancun' => 'Cancun (-6:00)',
|
||||
'America/Chicago' => 'Chicago (-6:00)',
|
||||
'America/Costa_Rica' => 'Costa_Rica (-6:00)',
|
||||
'America/El_Salvador' => 'El_Salvador (-6:00)',
|
||||
'America/Guatemala' => 'Guatemala (-6:00)',
|
||||
'America/Knox_IN' => 'Knox_IN (-6:00)',
|
||||
'America/Managua' => 'Managua (-6:00)',
|
||||
'America/Menominee' => 'Menominee (-6:00)',
|
||||
'America/Merida' => 'Merida (-6:00)',
|
||||
'America/Mexico_City' => 'Mexico_City (-6:00)',
|
||||
'America/Monterrey' => 'Monterrey (-6:00)',
|
||||
'America/Rainy_River' => 'Rainy_River (-6:00)',
|
||||
'America/Rankin_Inlet' => 'Rankin_Inlet (-6:00)',
|
||||
'America/Regina' => 'Regina (-6:00)',
|
||||
'America/Swift_Current' => 'Swift_Current (-6:00)',
|
||||
'America/Tegucigalpa' => 'Tegucigalpa (-6:00)',
|
||||
'America/Winnipeg' => 'Winnipeg (-6:00)',
|
||||
'America/Atikokan' => 'Atikokan (-5:00)',
|
||||
'America/Bogota' => 'Bogota (-5:00)',
|
||||
'America/Cayman' => 'Cayman (-5:00)',
|
||||
'America/Coral_Harbour' => 'Coral_Harbour (-5:00)',
|
||||
'America/Detroit' => 'Detroit (-5:00)',
|
||||
'America/Fort_Wayne' => 'Fort_Wayne (-5:00)',
|
||||
'America/Grand_Turk' => 'Grand_Turk (-5:00)',
|
||||
'America/Guayaquil' => 'Guayaquil (-5:00)',
|
||||
'America/Havana' => 'Havana (-5:00)',
|
||||
'America/Indianapolis' => 'Indianapolis (-5:00)',
|
||||
'America/Iqaluit' => 'Iqaluit (-5:00)',
|
||||
'America/Jamaica' => 'Jamaica (-5:00)',
|
||||
'America/Lima' => 'Lima (-5:00)',
|
||||
'America/Louisville' => 'Louisville (-5:00)',
|
||||
'America/Montreal' => 'Montreal (-5:00)',
|
||||
'America/Nassau' => 'Nassau (-5:00)',
|
||||
'America/New_York' => 'New_York (-5:00)',
|
||||
'America/Nipigon' => 'Nipigon (-5:00)',
|
||||
'America/Panama' => 'Panama (-5:00)',
|
||||
'America/Pangnirtung' => 'Pangnirtung (-5:00)',
|
||||
'America/Port-au-Prince' => 'Port-au-Prince (-5:00)',
|
||||
'America/Resolute' => 'Resolute (-5:00)',
|
||||
'America/Thunder_Bay' => 'Thunder_Bay (-5:00)',
|
||||
'America/Toronto' => 'Toronto (-5:00)',
|
||||
'America/Caracas' => 'Caracas (-4:-30)',
|
||||
'America/Anguilla' => 'Anguilla (-4:00)',
|
||||
'America/Antigua' => 'Antigua (-4:00)',
|
||||
'America/Aruba' => 'Aruba (-4:00)',
|
||||
'America/Asuncion' => 'Asuncion (-4:00)',
|
||||
'America/Barbados' => 'Barbados (-4:00)',
|
||||
'America/Blanc-Sablon' => 'Blanc-Sablon (-4:00)',
|
||||
'America/Boa_Vista' => 'Boa_Vista (-4:00)',
|
||||
'America/Campo_Grande' => 'Campo_Grande (-4:00)',
|
||||
'America/Cuiaba' => 'Cuiaba (-4:00)',
|
||||
'America/Curacao' => 'Curacao (-4:00)',
|
||||
'America/Dominica' => 'Dominica (-4:00)',
|
||||
'America/Eirunepe' => 'Eirunepe (-4:00)',
|
||||
'America/Glace_Bay' => 'Glace_Bay (-4:00)',
|
||||
'America/Goose_Bay' => 'Goose_Bay (-4:00)',
|
||||
'America/Grenada' => 'Grenada (-4:00)',
|
||||
'America/Guadeloupe' => 'Guadeloupe (-4:00)',
|
||||
'America/Guyana' => 'Guyana (-4:00)',
|
||||
'America/Halifax' => 'Halifax (-4:00)',
|
||||
'America/La_Paz' => 'La_Paz (-4:00)',
|
||||
'America/Manaus' => 'Manaus (-4:00)',
|
||||
'America/Marigot' => 'Marigot (-4:00)',
|
||||
'America/Martinique' => 'Martinique (-4:00)',
|
||||
'America/Moncton' => 'Moncton (-4:00)',
|
||||
'America/Montserrat' => 'Montserrat (-4:00)',
|
||||
'America/Port_of_Spain' => 'Port_of_Spain (-4:00)',
|
||||
'America/Porto_Acre' => 'Porto_Acre (-4:00)',
|
||||
'America/Porto_Velho' => 'Porto_Velho (-4:00)',
|
||||
'America/Puerto_Rico' => 'Puerto_Rico (-4:00)',
|
||||
'America/Rio_Branco' => 'Rio_Branco (-4:00)',
|
||||
'America/Santiago' => 'Santiago (-4:00)',
|
||||
'America/Santo_Domingo' => 'Santo_Domingo (-4:00)',
|
||||
'America/St_Barthelemy' => 'St_Barthelemy (-4:00)',
|
||||
'America/St_Kitts' => 'St_Kitts (-4:00)',
|
||||
'America/St_Lucia' => 'St_Lucia (-4:00)',
|
||||
'America/St_Thomas' => 'St_Thomas (-4:00)',
|
||||
'America/St_Vincent' => 'St_Vincent (-4:00)',
|
||||
'America/Thule' => 'Thule (-4:00)',
|
||||
'America/Tortola' => 'Tortola (-4:00)',
|
||||
'America/Virgin' => 'Virgin (-4:00)',
|
||||
'America/St_Johns' => 'St_Johns (-3:-30)',
|
||||
'America/Araguaina' => 'Araguaina (-3:00)',
|
||||
'America/Bahia' => 'Bahia (-3:00)',
|
||||
'America/Belem' => 'Belem (-3:00)',
|
||||
'America/Buenos_Aires' => 'Buenos_Aires (-3:00)',
|
||||
'America/Catamarca' => 'Catamarca (-3:00)',
|
||||
'America/Cayenne' => 'Cayenne (-3:00)',
|
||||
'America/Cordoba' => 'Cordoba (-3:00)',
|
||||
'America/Fortaleza' => 'Fortaleza (-3:00)',
|
||||
'America/Godthab' => 'Godthab (-3:00)',
|
||||
'America/Jujuy' => 'Jujuy (-3:00)',
|
||||
'America/Maceio' => 'Maceio (-3:00)',
|
||||
'America/Mendoza' => 'Mendoza (-3:00)',
|
||||
'America/Miquelon' => 'Miquelon (-3:00)',
|
||||
'America/Montevideo' => 'Montevideo (-3:00)',
|
||||
'America/Paramaribo' => 'Paramaribo (-3:00)',
|
||||
'America/Recife' => 'Recife (-3:00)',
|
||||
'America/Rosario' => 'Rosario (-3:00)',
|
||||
'America/Santarem' => 'Santarem (-3:00)',
|
||||
'America/Sao_Paulo' => 'Sao_Paulo (-3:00)',
|
||||
'America/Noronha' => 'Noronha (-2:00)',
|
||||
'America/Scoresbysund' => 'Scoresbysund (-1:00)',
|
||||
'America/Danmarkshavn' => 'Danmarkshavn (+0:00)',
|
||||
],
|
||||
'Canada' => [
|
||||
'Canada/Pacific' => 'Pacific (-8:00)',
|
||||
'Canada/Yukon' => 'Yukon (-8:00)',
|
||||
'Canada/Mountain' => 'Mountain (-7:00)',
|
||||
'Canada/Central' => 'Central (-6:00)',
|
||||
'Canada/East-Saskatchewan' => 'East-Saskatchewan (-6:00)',
|
||||
'Canada/Saskatchewan' => 'Saskatchewan (-6:00)',
|
||||
'Canada/Eastern' => 'Eastern (-5:00)',
|
||||
'Canada/Atlantic' => 'Atlantic (-4:00)',
|
||||
'Canada/Newfoundland' => 'Newfoundland (-3:-30)',
|
||||
],
|
||||
'Mexico' => [
|
||||
'Mexico/BajaNorte' => 'BajaNorte (-8:00)',
|
||||
'Mexico/BajaSur' => 'BajaSur (-7:00)',
|
||||
'Mexico/General' => 'General (-6:00)',
|
||||
],
|
||||
'Chile' => [
|
||||
'Chile/EasterIsland' => 'EasterIsland (-6:00)',
|
||||
'Chile/Continental' => 'Continental (-4:00)',
|
||||
],
|
||||
'Antarctica' => [
|
||||
'Antarctica/Palmer' => 'Palmer (-4:00)',
|
||||
'Antarctica/Rothera' => 'Rothera (-3:00)',
|
||||
'Antarctica/Syowa' => 'Syowa (+3:00)',
|
||||
'Antarctica/Mawson' => 'Mawson (+6:00)',
|
||||
'Antarctica/Vostok' => 'Vostok (+6:00)',
|
||||
'Antarctica/Davis' => 'Davis (+7:00)',
|
||||
'Antarctica/Casey' => 'Casey (+8:00)',
|
||||
'Antarctica/DumontDUrville' => 'DumontDUrville (+10:00)',
|
||||
'Antarctica/McMurdo' => 'McMurdo (+12:00)',
|
||||
'Antarctica/South_Pole' => 'South_Pole (+12:00)',
|
||||
],
|
||||
'Atlantic' => [
|
||||
'Atlantic/Bermuda' => 'Bermuda (-4:00)',
|
||||
'Atlantic/Stanley' => 'Stanley (-4:00)',
|
||||
'Atlantic/South_Georgia' => 'South_Georgia (-2:00)',
|
||||
'Atlantic/Azores' => 'Azores (-1:00)',
|
||||
'Atlantic/Cape_Verde' => 'Cape_Verde (-1:00)',
|
||||
'Atlantic/Canary' => 'Canary (+0:00)',
|
||||
'Atlantic/Faeroe' => 'Faeroe (+0:00)',
|
||||
'Atlantic/Faroe' => 'Faroe (+0:00)',
|
||||
'Atlantic/Madeira' => 'Madeira (+0:00)',
|
||||
'Atlantic/Reykjavik' => 'Reykjavik (+0:00)',
|
||||
'Atlantic/St_Helena' => 'St_Helena (+0:00)',
|
||||
'Atlantic/Jan_Mayen' => 'Jan_Mayen (+1:00)',
|
||||
],
|
||||
'Brazil' => [
|
||||
'Brazil/Acre' => 'Acre (-4:00)',
|
||||
'Brazil/West' => 'West (-4:00)',
|
||||
'Brazil/East' => 'East (-3:00)',
|
||||
'Brazil/DeNoronha' => 'DeNoronha (-2:00)',
|
||||
],
|
||||
'Africa' => [
|
||||
'Africa/Abidjan' => 'Abidjan (+0:00)',
|
||||
'Africa/Accra' => 'Accra (+0:00)',
|
||||
'Africa/Bamako' => 'Bamako (+0:00)',
|
||||
'Africa/Banjul' => 'Banjul (+0:00)',
|
||||
'Africa/Bissau' => 'Bissau (+0:00)',
|
||||
'Africa/Casablanca' => 'Casablanca (+0:00)',
|
||||
'Africa/Conakry' => 'Conakry (+0:00)',
|
||||
'Africa/Dakar' => 'Dakar (+0:00)',
|
||||
'Africa/El_Aaiun' => 'El_Aaiun (+0:00)',
|
||||
'Africa/Freetown' => 'Freetown (+0:00)',
|
||||
'Africa/Lome' => 'Lome (+0:00)',
|
||||
'Africa/Monrovia' => 'Monrovia (+0:00)',
|
||||
'Africa/Nouakchott' => 'Nouakchott (+0:00)',
|
||||
'Africa/Ouagadougou' => 'Ouagadougou (+0:00)',
|
||||
'Africa/Sao_Tome' => 'Sao_Tome (+0:00)',
|
||||
'Africa/Timbuktu' => 'Timbuktu (+0:00)',
|
||||
'Africa/Algiers' => 'Algiers (+1:00)',
|
||||
'Africa/Bangui' => 'Bangui (+1:00)',
|
||||
'Africa/Brazzaville' => 'Brazzaville (+1:00)',
|
||||
'Africa/Ceuta' => 'Ceuta (+1:00)',
|
||||
'Africa/Douala' => 'Douala (+1:00)',
|
||||
'Africa/Kinshasa' => 'Kinshasa (+1:00)',
|
||||
'Africa/Lagos' => 'Lagos (+1:00)',
|
||||
'Africa/Libreville' => 'Libreville (+1:00)',
|
||||
'Africa/Luanda' => 'Luanda (+1:00)',
|
||||
'Africa/Malabo' => 'Malabo (+1:00)',
|
||||
'Africa/Ndjamena' => 'Ndjamena (+1:00)',
|
||||
'Africa/Niamey' => 'Niamey (+1:00)',
|
||||
'Africa/Porto-Novo' => 'Porto-Novo (+1:00)',
|
||||
'Africa/Tunis' => 'Tunis (+1:00)',
|
||||
'Africa/Windhoek' => 'Windhoek (+1:00)',
|
||||
'Africa/Blantyre' => 'Blantyre (+2:00)',
|
||||
'Africa/Bujumbura' => 'Bujumbura (+2:00)',
|
||||
'Africa/Cairo' => 'Cairo (+2:00)',
|
||||
'Africa/Gaborone' => 'Gaborone (+2:00)',
|
||||
'Africa/Harare' => 'Harare (+2:00)',
|
||||
'Africa/Johannesburg' => 'Johannesburg (+2:00)',
|
||||
'Africa/Kigali' => 'Kigali (+2:00)',
|
||||
'Africa/Lubumbashi' => 'Lubumbashi (+2:00)',
|
||||
'Africa/Lusaka' => 'Lusaka (+2:00)',
|
||||
'Africa/Maputo' => 'Maputo (+2:00)',
|
||||
'Africa/Maseru' => 'Maseru (+2:00)',
|
||||
'Africa/Mbabane' => 'Mbabane (+2:00)',
|
||||
'Africa/Tripoli' => 'Tripoli (+2:00)',
|
||||
'Africa/Addis_Ababa' => 'Addis_Ababa (+3:00)',
|
||||
'Africa/Asmara' => 'Asmara (+3:00)',
|
||||
'Africa/Asmera' => 'Asmera (+3:00)',
|
||||
'Africa/Dar_es_Salaam' => 'Dar_es_Salaam (+3:00)',
|
||||
'Africa/Djibouti' => 'Djibouti (+3:00)',
|
||||
'Africa/Kampala' => 'Kampala (+3:00)',
|
||||
'Africa/Khartoum' => 'Khartoum (+3:00)',
|
||||
'Africa/Mogadishu' => 'Mogadishu (+3:00)',
|
||||
'Africa/Nairobi' => 'Nairobi (+3:00)',
|
||||
],
|
||||
'Europe' => [
|
||||
'Europe/Belfast' => 'Belfast (+0:00)',
|
||||
'Europe/Dublin' => 'Dublin (+0:00)',
|
||||
'Europe/Guernsey' => 'Guernsey (+0:00)',
|
||||
'Europe/Isle_of_Man' => 'Isle_of_Man (+0:00)',
|
||||
'Europe/Jersey' => 'Jersey (+0:00)',
|
||||
'Europe/Lisbon' => 'Lisbon (+0:00)',
|
||||
'Europe/London' => 'London (+0:00)',
|
||||
'Europe/Amsterdam' => 'Amsterdam (+1:00)',
|
||||
'Europe/Andorra' => 'Andorra (+1:00)',
|
||||
'Europe/Belgrade' => 'Belgrade (+1:00)',
|
||||
'Europe/Berlin' => 'Berlin (+1:00)',
|
||||
'Europe/Bratislava' => 'Bratislava (+1:00)',
|
||||
'Europe/Brussels' => 'Brussels (+1:00)',
|
||||
'Europe/Budapest' => 'Budapest (+1:00)',
|
||||
'Europe/Copenhagen' => 'Copenhagen (+1:00)',
|
||||
'Europe/Gibraltar' => 'Gibraltar (+1:00)',
|
||||
'Europe/Ljubljana' => 'Ljubljana (+1:00)',
|
||||
'Europe/Luxembourg' => 'Luxembourg (+1:00)',
|
||||
'Europe/Madrid' => 'Madrid (+1:00)',
|
||||
'Europe/Malta' => 'Malta (+1:00)',
|
||||
'Europe/Monaco' => 'Monaco (+1:00)',
|
||||
'Europe/Oslo' => 'Oslo (+1:00)',
|
||||
'Europe/Paris' => 'Paris (+1:00)',
|
||||
'Europe/Podgorica' => 'Podgorica (+1:00)',
|
||||
'Europe/Prague' => 'Prague (+1:00)',
|
||||
'Europe/Rome' => 'Rome (+1:00)',
|
||||
'Europe/San_Marino' => 'San_Marino (+1:00)',
|
||||
'Europe/Sarajevo' => 'Sarajevo (+1:00)',
|
||||
'Europe/Skopje' => 'Skopje (+1:00)',
|
||||
'Europe/Stockholm' => 'Stockholm (+1:00)',
|
||||
'Europe/Tirane' => 'Tirane (+1:00)',
|
||||
'Europe/Vaduz' => 'Vaduz (+1:00)',
|
||||
'Europe/Vatican' => 'Vatican (+1:00)',
|
||||
'Europe/Vienna' => 'Vienna (+1:00)',
|
||||
'Europe/Warsaw' => 'Warsaw (+1:00)',
|
||||
'Europe/Zagreb' => 'Zagreb (+1:00)',
|
||||
'Europe/Zurich' => 'Zurich (+1:00)',
|
||||
'Europe/Athens' => 'Athens (+2:00)',
|
||||
'Europe/Bucharest' => 'Bucharest (+2:00)',
|
||||
'Europe/Chisinau' => 'Chisinau (+2:00)',
|
||||
'Europe/Helsinki' => 'Helsinki (+2:00)',
|
||||
'Europe/Kaliningrad' => 'Kaliningrad (+2:00)',
|
||||
'Europe/Kiev' => 'Kiev (+2:00)',
|
||||
'Europe/Mariehamn' => 'Mariehamn (+2:00)',
|
||||
'Europe/Minsk' => 'Minsk (+2:00)',
|
||||
'Europe/Nicosia' => 'Nicosia (+2:00)',
|
||||
'Europe/Riga' => 'Riga (+2:00)',
|
||||
'Europe/Simferopol' => 'Simferopol (+2:00)',
|
||||
'Europe/Sofia' => 'Sofia (+2:00)',
|
||||
'Europe/Tallinn' => 'Tallinn (+2:00)',
|
||||
'Europe/Tiraspol' => 'Tiraspol (+2:00)',
|
||||
'Europe/Uzhgorod' => 'Uzhgorod (+2:00)',
|
||||
'Europe/Vilnius' => 'Vilnius (+2:00)',
|
||||
'Europe/Zaporozhye' => 'Zaporozhye (+2:00)',
|
||||
'Europe/Istanbul' => 'Istanbul (+3:00)',
|
||||
'Europe/Moscow' => 'Moscow (+3:00)',
|
||||
'Europe/Volgograd' => 'Volgograd (+3:00)',
|
||||
'Europe/Samara' => 'Samara (+4:00)',
|
||||
],
|
||||
'Arctic' => [
|
||||
'Arctic/Longyearbyen' => 'Longyearbyen (+1:00)',
|
||||
],
|
||||
'Asia' => [
|
||||
'Asia/Amman' => 'Amman (+2:00)',
|
||||
'Asia/Beirut' => 'Beirut (+2:00)',
|
||||
'Asia/Damascus' => 'Damascus (+2:00)',
|
||||
'Asia/Gaza' => 'Gaza (+2:00)',
|
||||
'Asia/Jerusalem' => 'Jerusalem (+2:00)',
|
||||
'Asia/Nicosia' => 'Nicosia (+2:00)',
|
||||
'Asia/Tel_Aviv' => 'Tel_Aviv (+2:00)',
|
||||
'Asia/Aden' => 'Aden (+3:00)',
|
||||
'Asia/Baghdad' => 'Baghdad (+3:00)',
|
||||
'Asia/Bahrain' => 'Bahrain (+3:00)',
|
||||
'Asia/Istanbul' => 'Istanbul (+3:00)',
|
||||
'Asia/Kuwait' => 'Kuwait (+3:00)',
|
||||
'Asia/Qatar' => 'Qatar (+3:00)',
|
||||
'Asia/Tehran' => 'Tehran (+3:30)',
|
||||
'Asia/Baku' => 'Baku (+4:00)',
|
||||
'Asia/Dubai' => 'Dubai (+4:00)',
|
||||
'Asia/Muscat' => 'Muscat (+4:00)',
|
||||
'Asia/Tbilisi' => 'Tbilisi (+4:00)',
|
||||
'Asia/Yerevan' => 'Yerevan (+4:00)',
|
||||
'Asia/Kabul' => 'Kabul (+4:30)',
|
||||
'Asia/Aqtau' => 'Aqtau (+5:00)',
|
||||
'Asia/Aqtobe' => 'Aqtobe (+5:00)',
|
||||
'Asia/Ashgabat' => 'Ashgabat (+5:00)',
|
||||
'Asia/Ashkhabad' => 'Ashkhabad (+5:00)',
|
||||
'Asia/Dushanbe' => 'Dushanbe (+5:00)',
|
||||
'Asia/Karachi' => 'Karachi (+5:00)',
|
||||
'Asia/Oral' => 'Oral (+5:00)',
|
||||
'Asia/Samarkand' => 'Samarkand (+5:00)',
|
||||
'Asia/Tashkent' => 'Tashkent (+5:00)',
|
||||
'Asia/Yekaterinburg' => 'Yekaterinburg (+5:00)',
|
||||
'Asia/Calcutta' => 'Calcutta (+5:30)',
|
||||
'Asia/Colombo' => 'Colombo (+5:30)',
|
||||
'Asia/Kolkata' => 'Kolkata (+5:30)',
|
||||
'Asia/Katmandu' => 'Katmandu (+5:45)',
|
||||
'Asia/Almaty' => 'Almaty (+6:00)',
|
||||
'Asia/Bishkek' => 'Bishkek (+6:00)',
|
||||
'Asia/Dacca' => 'Dacca (+6:00)',
|
||||
'Asia/Dhaka' => 'Dhaka (+6:00)',
|
||||
'Asia/Novosibirsk' => 'Novosibirsk (+6:00)',
|
||||
'Asia/Omsk' => 'Omsk (+6:00)',
|
||||
'Asia/Qyzylorda' => 'Qyzylorda (+6:00)',
|
||||
'Asia/Thimbu' => 'Thimbu (+6:00)',
|
||||
'Asia/Thimphu' => 'Thimphu (+6:00)',
|
||||
'Asia/Rangoon' => 'Rangoon (+6:30)',
|
||||
'Asia/Bangkok' => 'Bangkok (+7:00)',
|
||||
'Asia/Ho_Chi_Minh' => 'Ho_Chi_Minh (+7:00)',
|
||||
'Asia/Hovd' => 'Hovd (+7:00)',
|
||||
'Asia/Jakarta' => 'Jakarta (+7:00)',
|
||||
'Asia/Krasnoyarsk' => 'Krasnoyarsk (+7:00)',
|
||||
'Asia/Phnom_Penh' => 'Phnom_Penh (+7:00)',
|
||||
'Asia/Pontianak' => 'Pontianak (+7:00)',
|
||||
'Asia/Saigon' => 'Saigon (+7:00)',
|
||||
'Asia/Vientiane' => 'Vientiane (+7:00)',
|
||||
'Asia/Brunei' => 'Brunei (+8:00)',
|
||||
'Asia/Choibalsan' => 'Choibalsan (+8:00)',
|
||||
'Asia/Chongqing' => 'Chongqing (+8:00)',
|
||||
'Asia/Chungking' => 'Chungking (+8:00)',
|
||||
'Asia/Harbin' => 'Harbin (+8:00)',
|
||||
'Asia/Hong_Kong' => 'Hong_Kong (+8:00)',
|
||||
'Asia/Irkutsk' => 'Irkutsk (+8:00)',
|
||||
'Asia/Kashgar' => 'Kashgar (+8:00)',
|
||||
'Asia/Kuala_Lumpur' => 'Kuala_Lumpur (+8:00)',
|
||||
'Asia/Kuching' => 'Kuching (+8:00)',
|
||||
'Asia/Macao' => 'Macao (+8:00)',
|
||||
'Asia/Macau' => 'Macau (+8:00)',
|
||||
'Asia/Makassar' => 'Makassar (+8:00)',
|
||||
'Asia/Manila' => 'Manila (+8:00)',
|
||||
'Asia/Shanghai' => 'Shanghai (+8:00)',
|
||||
'Asia/Singapore' => 'Singapore (+8:00)',
|
||||
'Asia/Taipei' => 'Taipei (+8:00)',
|
||||
'Asia/Ujung_Pandang' => 'Ujung_Pandang (+8:00)',
|
||||
'Asia/Ulaanbaatar' => 'Ulaanbaatar (+8:00)',
|
||||
'Asia/Ulan_Bator' => 'Ulan_Bator (+8:00)',
|
||||
'Asia/Urumqi' => 'Urumqi (+8:00)',
|
||||
'Asia/Dili' => 'Dili (+9:00)',
|
||||
'Asia/Jayapura' => 'Jayapura (+9:00)',
|
||||
'Asia/Pyongyang' => 'Pyongyang (+9:00)',
|
||||
'Asia/Seoul' => 'Seoul (+9:00)',
|
||||
'Asia/Tokyo' => 'Tokyo (+9:00)',
|
||||
'Asia/Yakutsk' => 'Yakutsk (+9:00)',
|
||||
'Asia/Sakhalin' => 'Sakhalin (+10:00)',
|
||||
'Asia/Vladivostok' => 'Vladivostok (+10:00)',
|
||||
'Asia/Magadan' => 'Magadan (+11:00)',
|
||||
'Asia/Anadyr' => 'Anadyr (+12:00)',
|
||||
'Asia/Kamchatka' => 'Kamchatka (+12:00)',
|
||||
],
|
||||
'Indian' => [
|
||||
'Indian/Antananarivo' => 'Antananarivo (+3:00)',
|
||||
'Indian/Comoro' => 'Comoro (+3:00)',
|
||||
'Indian/Mayotte' => 'Mayotte (+3:00)',
|
||||
'Indian/Mahe' => 'Mahe (+4:00)',
|
||||
'Indian/Mauritius' => 'Mauritius (+4:00)',
|
||||
'Indian/Reunion' => 'Reunion (+4:00)',
|
||||
'Indian/Kerguelen' => 'Kerguelen (+5:00)',
|
||||
'Indian/Maldives' => 'Maldives (+5:00)',
|
||||
'Indian/Chagos' => 'Chagos (+6:00)',
|
||||
'Indian/Cocos' => 'Cocos (+6:30)',
|
||||
'Indian/Christmas' => 'Christmas (+7:00)',
|
||||
],
|
||||
'Australia' => [
|
||||
'Australia/Perth' => 'Perth (+8:00)',
|
||||
'Australia/West' => 'West (+8:00)',
|
||||
'Australia/Eucla' => 'Eucla (+8:45)',
|
||||
'Australia/Adelaide' => 'Adelaide (+9:30)',
|
||||
'Australia/Broken_Hill' => 'Broken_Hill (+9:30)',
|
||||
'Australia/Darwin' => 'Darwin (+9:30)',
|
||||
'Australia/North' => 'North (+9:30)',
|
||||
'Australia/South' => 'South (+9:30)',
|
||||
'Australia/Yancowinna' => 'Yancowinna (+9:30)',
|
||||
'Australia/ACT' => 'ACT (+10:00)',
|
||||
'Australia/Brisbane' => 'Brisbane (+10:00)',
|
||||
'Australia/Canberra' => 'Canberra (+10:00)',
|
||||
'Australia/Currie' => 'Currie (+10:00)',
|
||||
'Australia/Hobart' => 'Hobart (+10:00)',
|
||||
'Australia/Lindeman' => 'Lindeman (+10:00)',
|
||||
'Australia/Melbourne' => 'Melbourne (+10:00)',
|
||||
'Australia/NSW' => 'NSW (+10:00)',
|
||||
'Australia/Queensland' => 'Queensland (+10:00)',
|
||||
'Australia/Sydney' => 'Sydney (+10:00)',
|
||||
'Australia/Tasmania' => 'Tasmania (+10:00)',
|
||||
'Australia/Victoria' => 'Victoria (+10:00)',
|
||||
'Australia/LHI' => 'LHI (+10:30)',
|
||||
'Australia/Lord_Howe' => 'Lord_Howe (+10:30)',
|
||||
],
|
||||
'Pacific' => [
|
||||
'Pacific/Apia' => 'Apia (+13:00)',
|
||||
'Pacific/Auckland' => 'Auckland (+12:00)',
|
||||
'Pacific/Bougainville' => 'Bougainville (+11:00)',
|
||||
'Pacific/Chatham' => 'Chatham (+12:45)',
|
||||
'Pacific/Chuuk' => 'Chuuk (+10:00)',
|
||||
'Pacific/Easter' => 'Easter (−06:00)',
|
||||
'Pacific/Efate' => 'Efate (+11:00)',
|
||||
'Pacific/Enderbury' => 'Enderbury (+13:00)',
|
||||
'Pacific/Fakaofo' => 'Fakaofo (+13:00)',
|
||||
'Pacific/Fiji' => 'Fiji (+12:00)',
|
||||
'Pacific/Funafuti' => 'Funafuti (+12:00)',
|
||||
'Pacific/Galapagos' => 'Galapagos (−06:00)',
|
||||
'Pacific/Gambier' => 'Gambier (−09:00)',
|
||||
'Pacific/Guadalcanal' => 'Guadalcanal (+11:00)',
|
||||
'Pacific/Guam' => 'Guam (+10:00)',
|
||||
'Pacific/Honolulu' => 'Honolulu (−10:00)',
|
||||
'Pacific/Kiritimati' => 'Kiritimati (+14:00)',
|
||||
'Pacific/Kosrae' => 'Kosrae (+11:00)',
|
||||
'Pacific/Kwajalein' => 'Kwajalein (+12:00)',
|
||||
'Pacific/Majuro' => 'Majuro (+12:00)',
|
||||
'Pacific/Marquesas' => 'Marquesas (−09:30)',
|
||||
'Pacific/Nauru' => 'Nauru (+12:00)',
|
||||
'Pacific/Niue' => 'Niue (−11:00)',
|
||||
'Pacific/Norfolk' => 'Norfolk (+11:00)',
|
||||
'Pacific/Noumea' => 'Noumea (+11:00)',
|
||||
'Pacific/Pago_Pago' => 'Pago_Pago (−11:00)',
|
||||
'Pacific/Palau' => 'Palau (+09:00)',
|
||||
'Pacific/Pitcairn' => 'Pitcairn (−08:00)',
|
||||
'Pacific/Pohnpei' => 'Pohnpei (+11:00)',
|
||||
'Pacific/Port_Moresby' => 'Port_Moresby (+10:00)',
|
||||
'Pacific/Rarotonga' => 'Rarotonga (−10:00)',
|
||||
'Pacific/Tahiti' => 'Tahiti (−10:00)',
|
||||
'Pacific/Tarawa' => 'Tarawa (+12:00)',
|
||||
'Pacific/Tongatapu' => 'Tongatapu (+13:00)',
|
||||
'Pacific/Wake' => 'Wake (+12:00)',
|
||||
'Pacific/Wallis' => 'Wallis (+12:00)',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Timezones constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('users_model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all timezones to a grouped array (by continent).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_grouped_array(): array
|
||||
{
|
||||
return $this->timezones;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default timezone value of the current system.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_timezone(): string
|
||||
{
|
||||
return date_default_timezone_get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date time value to a new timezone.
|
||||
*
|
||||
* @param string $value Provide a date time value as a string (format Y-m-d H:i:s).
|
||||
* @param string $from_timezone From timezone value.
|
||||
* @param string $to_timezone To timezone value.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function convert(string $value, string $from_timezone, string $to_timezone): string
|
||||
{
|
||||
if (!$to_timezone || $from_timezone === $to_timezone) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$from = new DateTimeZone($from_timezone);
|
||||
|
||||
$to = new DateTimeZone($to_timezone);
|
||||
|
||||
$result = new DateTime($value, $from);
|
||||
|
||||
$result->setTimezone($to);
|
||||
|
||||
return $result->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timezone name for the provided value.
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_timezone_name(string $value): ?string
|
||||
{
|
||||
$timezones = $this->to_array();
|
||||
|
||||
return $timezones[$value] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all timezones to a flat array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array
|
||||
{
|
||||
return array_merge(...array_values($this->timezones));
|
||||
}
|
||||
}
|
||||
104
application/libraries/Webhooks_client.php
Normal file
104
application/libraries/Webhooks_client.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Easy!Appointments - Open Source Web Scheduler
|
||||
*
|
||||
* @package EasyAppointments
|
||||
* @author A.Tselegidis <alextselegidis@gmail.com>
|
||||
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
|
||||
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
|
||||
* @link http://easyappointments.org
|
||||
* @since v1.4.0
|
||||
* ---------------------------------------------------------------------------- */
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
/**
|
||||
* Webhooks client library.
|
||||
*
|
||||
* Handles the webhook HTTP related functionality.
|
||||
*
|
||||
* @package Libraries
|
||||
*/
|
||||
class Webhooks_client
|
||||
{
|
||||
/**
|
||||
* @var EA_Controller|CI_Controller
|
||||
*/
|
||||
protected EA_Controller|CI_Controller $CI;
|
||||
|
||||
/**
|
||||
* Webhook client constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
$this->CI->load->model('providers_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('secretaries_model');
|
||||
$this->CI->load->model('admins_model');
|
||||
$this->CI->load->model('appointments_model');
|
||||
$this->CI->load->model('settings_model');
|
||||
$this->CI->load->model('webhooks_model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the registered webhooks for the provided action.
|
||||
*
|
||||
* @param string $action Webhook action.
|
||||
* @param array $payload Payload data.
|
||||
*
|
||||
* @return void|null
|
||||
*/
|
||||
public function trigger(string $action, array $payload)
|
||||
{
|
||||
$webhooks = $this->CI->webhooks_model->get();
|
||||
|
||||
foreach ($webhooks as $webhook) {
|
||||
if (str_contains($webhook['actions'], $action)) {
|
||||
$this->call($webhook, $action, $payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the provided webhook.
|
||||
*
|
||||
* @param array $webhook
|
||||
* @param string $action
|
||||
* @param array $payload
|
||||
*/
|
||||
private function call(array $webhook, string $action, array $payload): void
|
||||
{
|
||||
try {
|
||||
$client = new Client();
|
||||
|
||||
$headers = [];
|
||||
|
||||
if (!empty($webhook['secret_header']) && !empty($webhook['secret_token'])) {
|
||||
$headers[$webhook['secret_header']] = $webhook['secret_token'];
|
||||
}
|
||||
|
||||
$response = $client->post($webhook['url'], [
|
||||
'verify' => $webhook['is_ssl_verified'],
|
||||
'headers' => $headers,
|
||||
'json' => [
|
||||
'action' => $action,
|
||||
'payload' => $payload,
|
||||
],
|
||||
]);
|
||||
|
||||
echo $response->getBody()->getContents(); // Use this for quick debugging
|
||||
} catch (Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'Webhooks Client - The webhook (' .
|
||||
($webhook['id'] ?? null) .
|
||||
') request received an unexpected exception: ' .
|
||||
$e->getMessage(),
|
||||
);
|
||||
log_message('error', $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
10
application/libraries/index.html
Normal file
10
application/libraries/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Directory access is forbidden.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user