Remove the automated calendar detection from the CalDAV sync as it will not work with all CalDAV providers (some use different URL structures than Baikal)

This commit is contained in:
Alex Tselegidis 2024-06-14 18:49:41 +02:00
parent dd34836c88
commit f398d18bbc
6 changed files with 121 additions and 269 deletions

View file

@ -11,6 +11,8 @@
* @since v1.5.0
* ---------------------------------------------------------------------------- */
use GuzzleHttp\Exception\GuzzleException;
/**
* Caldav controller.
*
@ -54,19 +56,7 @@ class Caldav extends EA_Controller
$caldav_username = request('caldav_username');
$caldav_password = request('caldav_password');
$default_caldav_calendar = $this->caldav_sync->get_default_calendar(
$caldav_url,
$caldav_username,
$caldav_password,
);
if (!$default_caldav_calendar) {
json_response([
'success' => false,
]);
return;
}
$this->caldav_sync->test_connection($caldav_url, $caldav_username, $caldav_password);
$provider = $this->providers_model->find($provider_id);
@ -74,13 +64,17 @@ class Caldav extends EA_Controller
$provider['settings']['caldav_url'] = $caldav_url;
$provider['settings']['caldav_username'] = $caldav_username;
$provider['settings']['caldav_password'] = $caldav_password;
$provider['settings']['caldav_calendar'] = $default_caldav_calendar;
$this->providers_model->save($provider);
json_response([
'success' => true,
]);
} catch (GuzzleException | InvalidArgumentException $e) {
json_response([
'success' => false,
'message' => $e->getMessage(),
]);
} catch (Throwable $e) {
json_exception($e);
}
@ -277,62 +271,6 @@ class Caldav extends EA_Controller
]);
}
/**
* Get CalDAV Calendars
*
* This method will return a list of the available CalDAV Calendars.
*
* @return void
*/
public function get_caldav_calendars(): void
{
try {
$provider_id = (int) request('provider_id');
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$calendars = $this->caldav_sync->get_caldav_calendars($provider_id);
json_response($calendars);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Select a specific caldav calendar for a provider.
*
* All the appointments will be synced with this particular calendar.
*
* @return void
*/
public function select_caldav_calendar(): void
{
try {
$provider_id = (int) request('provider_id');
$user_id = session('user_id');
if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) {
throw new RuntimeException('You do not have the required permissions for this task.');
}
$calendar_id = request('calendar_id');
$this->providers_model->set_setting($provider_id, 'caldav_calendar', $calendar_id);
json_response([
'success' => true,
]);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Disable a providers sync setting.
*
@ -361,7 +299,6 @@ class Caldav extends EA_Controller
$provider['settings']['caldav_url'] = null;
$provider['settings']['caldav_username'] = null;
$provider['settings']['caldav_password'] = null;
$provider['settings']['caldav_calendar'] = null;
$this->providers_model->save($provider);

View file

@ -14,6 +14,7 @@
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Reader;
@ -68,12 +69,10 @@ class Caldav_sync
$client = $this->get_http_client_by_provider_id($provider['id']);
$caldav_calendar = $provider['settings']['caldav_calendar'];
$caldav_event_id =
$appointment['id_caldav_calendar'] ?: $this->CI->ics_file->generate_uid($appointment['id']);
$uri = $this->get_caldav_event_uri($caldav_calendar, $caldav_event_id);
$uri = $this->get_caldav_event_uri($caldav_event_id);
$client->request('PUT', $uri, [
'headers' => [
@ -106,12 +105,10 @@ class Caldav_sync
$client = $this->get_http_client_by_provider_id($provider['id']);
$caldav_calendar = $provider['settings']['caldav_calendar'];
$caldav_event_id =
$unavailability['id_caldav_calendar'] ?: $this->CI->ics_file->generate_uid($unavailability['id']);
$uri = $this->get_caldav_event_uri($caldav_calendar, $caldav_event_id);
$uri = $this->get_caldav_event_uri($caldav_event_id);
$client->request('PUT', $uri, [
'headers' => [
@ -138,9 +135,7 @@ class Caldav_sync
try {
$client = $this->get_http_client_by_provider_id($provider['id']);
$caldav_calendar = $provider['settings']['caldav_calendar'];
$uri = $this->get_caldav_event_uri($caldav_calendar, $caldav_event_id);
$uri = $this->get_caldav_event_uri($caldav_event_id);
$client->request('DELETE', $uri);
} catch (GuzzleException $e) {
@ -162,9 +157,7 @@ class Caldav_sync
try {
$client = $this->get_http_client_by_provider_id($provider['id']);
$caldav_calendar = $provider['settings']['caldav_calendar'];
$uri = $this->get_caldav_event_uri($caldav_calendar, $caldav_event_id);
$uri = $this->get_caldav_event_uri($caldav_event_id);
$response = $client->request('GET', $uri);
@ -194,41 +187,7 @@ class Caldav_sync
try {
$client = $this->get_http_client_by_provider_id($provider['id']);
$caldav_calendar = $provider['settings']['caldav_calendar'];
$uri = $this->get_caldav_event_uri($caldav_calendar);
$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');
$response = $client->request('REPORT', $uri, [
'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>
',
]);
$response = $this->fetch_events($client, $start_date_time, $end_date_time);
$xml = new SimpleXMLElement($response->getBody(), 0, false, 'd', true);
@ -249,94 +208,6 @@ class Caldav_sync
}
}
/**
* Return available Caldav Calendars for specific user.
*
* The given user's token must already exist in db in order to get access to his
* Caldav Calendar account.
*
* @return array Returns an array with the available calendars.
*
* @throws Exception
*/
public function get_caldav_calendars(int $provider_id): array
{
try {
$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'];
$client = $this->get_http_client($caldav_url, $caldav_username, $caldav_password);
$caldav_calendars = [];
$response = $client->request('PROPFIND', 'calendars/' . $caldav_username);
$xml = new SimpleXMLElement($response->getBody(), 0, false, 'd', true);
foreach ($xml->children('d', true) as $response) {
$resource_type = $response->propstat->prop->resourcetype->children('cal', true)->getName();
if ($resource_type !== 'calendar') {
continue;
}
$caldav_calendars[] = [
'id' => $this->sanitize_href($caldav_url, $response->href),
'summary' => (string) $response->propstat->prop->displayname,
];
}
return $caldav_calendars;
} catch (GuzzleException $e) {
$this->handle_guzzle_exception($e, 'Failed to get the CalDAV calendars');
return [];
}
}
/**
* Connect to the remote CalDAV server and get the default calendar URL.
*
* @param string $caldav_url
* @param string $caldav_username
* @param string $caldav_password
*
* @return string|null
*
* @throws Exception
*/
public function get_default_calendar(string $caldav_url, string $caldav_username, string $caldav_password): ?string
{
try {
$client = $this->get_http_client($caldav_url, $caldav_username, $caldav_password);
$response = $client->request('PROPFIND', 'calendars/' . $caldav_username);
$xml = new SimpleXMLElement($response->getBody(), 0, false, 'd', true);
foreach ($xml->children('d', true) as $response) {
$resource_type = $response->propstat->prop->resourcetype->children('cal', true)->getName();
if ($resource_type === 'calendar') {
return $this->sanitize_href($caldav_url, $response->href); // Use this response as the default calendar
}
}
return null;
} catch (GuzzleException $e) {
$this->handle_guzzle_exception($e, 'Failed to check CalDAV credentials');
return null;
}
}
/**
* Common error handling for the CalDAV requests.
*
@ -390,11 +261,24 @@ class Caldav_sync
]);
}
private function sanitize_href(string $caldav_url, string $href): string
/**
* @throws Exception
* @throws GuzzleException
*/
public function test_connection(string $caldav_url, string $caldav_username, string $caldav_password): void
{
$parts = parse_url($caldav_url);
try {
// Fetch some events to see if the connection is valid
$client = $this->get_http_client($caldav_url, $caldav_username, $caldav_password);
return str_replace($parts['path'], '', $href);
$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 save CalDAV event');
throw $e;
}
}
private function get_http_client_by_provider_id(int $provider_id): Client
@ -415,14 +299,13 @@ class Caldav_sync
/**
* 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
private function get_caldav_event_uri(?string $caldav_event_id = null): string
{
return trim($caldav_calendar, '/') . ($caldav_event_id ? '/' . $caldav_event_id . '.ics' : '');
return $caldav_event_id ? '/' . $caldav_event_id . '.ics' : '';
}
/**
@ -475,4 +358,43 @@ class Caldav_sync
'location' => (string) $vevent->LOCATION,
];
}
/**
* @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>
',
]);
}
}

View file

@ -0,0 +1,44 @@
<?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
* ---------------------------------------------------------------------------- */
class Migration_Drop_caldav_calendar_column_from_user_settings_table extends EA_Migration
{
/**
* Upgrade method.
*/
public function up()
{
if ($this->db->field_exists('caldav_calendar', 'user_settings')) {
$this->dbforge->drop_column('user_settings', 'caldav_calendar');
}
}
/**
* Downgrade method.
*/
public function down()
{
if (!$this->db->field_exists('caldav_calendar', 'user_settings')) {
$fields = [
'caldav_calendar' => [
'type' => 'VARCHAR',
'constraint' => '256',
'null' => true,
'after' => 'caldav_password',
],
];
$this->dbforge->add_column('user_settings', $fields);
}
}
}

View file

@ -799,9 +799,6 @@ class Providers_model extends EA_Model
'caldavPassword' => array_key_exists('caldav_password', $provider['settings'])
? $provider['settings']['caldav_password']
: null,
'caldavCalendar' => array_key_exists('caldav_calendar', $provider['settings'])
? $provider['settings']['caldav_calendar']
: null,
'syncFutureDays' => array_key_exists('sync_future_days', $provider['settings'])
? (int) $provider['settings']['sync_future_days']
: null,
@ -949,10 +946,6 @@ class Providers_model extends EA_Model
$decoded_resource['settings']['caldav_password'] = $provider['settings']['caldavPassword'];
}
if (array_key_exists('caldavCalendar', $provider['settings'])) {
$decoded_resource['settings']['caldav_calendar'] = $provider['settings']['caldavCalendar'];
}
if (array_key_exists('syncFutureDays', $provider['settings'])) {
$decoded_resource['settings']['sync_future_days'] = $provider['settings']['syncFutureDays'];
}

View file

@ -171,26 +171,26 @@ App.Utils.CalendarSync = (function () {
});
}
function enableCaldavSync() {
function enableCaldavSync(defaultCaldavUrl = '', defaultCaldavUsername = '', defaultCaldavPassword = '') {
const $container = $(`
<div>
<div class="mb-3">
<label for="caldav-url" class="form-label">
${lang('calendar_url')}
</label>
<input type="text" class="form-control" id="caldav-url"/>
<input type="text" class="form-control" id="caldav-url" value="${defaultCaldavUrl}"/>
</div>
<div class="mb-3">
<label for="caldav-username" class="form-label">
${lang('username')}
</label>
<input type="text" class="form-control" id="caldav-username"/>
<input type="text" class="form-control" id="caldav-username" value="${defaultCaldavUsername}"/>
</div>
<div class="mb-3">
<label for="caldav-password" class="form-label">
${lang('password')}
</label>
<input type="password" class="form-control" id="caldav-password"/>
<input type="password" class="form-control" id="caldav-password" value="${defaultCaldavPassword}"/>
</div>
<div class="alert alert-danger" hidden>
@ -247,7 +247,7 @@ App.Utils.CalendarSync = (function () {
$caldavUsername.addClass('is-invalid');
$caldavPassword.addClass('is-invalid');
$alert.text(lang('login_failed')).prop('hidden', false);
$alert.text(lang('login_failed') + ' ' + response.message).prop('hidden', false);
return;
}
@ -258,7 +258,7 @@ App.Utils.CalendarSync = (function () {
updateSyncButtons();
selectCaldavCalendar();
App.Layouts.Backend.displayNotification(lang('sync_calendar_selected'));
messageModal.hide();
},
@ -296,7 +296,6 @@ App.Utils.CalendarSync = (function () {
provider.settings.caldav_url = null;
provider.settings.caldav_username = null;
provider.settings.caldav_password = null;
provider.settings.caldav_calendar = null;
App.Http.Caldav.disableProviderSync(provider.id);
@ -312,43 +311,6 @@ App.Utils.CalendarSync = (function () {
]);
}
function selectCaldavCalendar() {
const providerId = $selectFilterItem.val();
App.Http.Caldav.getCaldavCalendars(providerId).done((caldavCalendars) => {
const $selectCaldavCalendar = $(`
<select class="form-control">
<!-- JS -->
</select>
`);
caldavCalendars.forEach((caldavCalendar) => {
$selectCaldavCalendar.append(new Option(caldavCalendar.summary, caldavCalendar.id));
});
const $messageModal = App.Utils.Message.show(
lang('select_sync_calendar'),
lang('select_sync_calendar_prompt'),
[
{
text: lang('select'),
click: (event, messageModal) => {
const caldavCalendarId = $selectCaldavCalendar.val();
App.Http.Caldav.selectCaldavCalendar(providerId, caldavCalendarId).done(() => {
App.Layouts.Backend.displayNotification(lang('sync_calendar_selected'));
});
messageModal.hide();
},
},
],
);
$selectCaldavCalendar.appendTo($messageModal.find('.modal-body'));
});
}
function triggerCaldavSync() {
const providerId = $selectFilterItem.val();

View file

@ -2280,8 +2280,6 @@ components:
type: string
caldavPassword:
type: string
caldavCalendar:
type: string
syncFutureDays:
type: string
syncPastDays:
@ -2316,7 +2314,6 @@ components:
caldavUrl: null
caldavUsername: null
caldavPassword: null
caldavCalendar: null
syncFutureDays: 90
syncPastDays: 30
workingPlan:
@ -2401,8 +2398,6 @@ components:
type: string
caldavPassword:
type: string
caldavCalendar:
type: string
syncFutureDays:
type: string
syncPastDays:
@ -2436,7 +2431,6 @@ components:
caldavUrl: null
caldavUsername: null
caldavPassword: null
caldavCalendar: null
syncFutureDays: 90
syncPastDays: 30
workingPlan: