refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s

This commit is contained in:
Kroonk
2026-05-22 16:19:55 +02:00
parent 38cc4c09ca
commit a213a1dba0
2215 changed files with 242567 additions and 6015 deletions

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Sabre\DAV\Server as SabreServer;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\DAV\Sync\Plugin as DAVSyncPlugin;
use App\Http\Controllers\DAV\Backend\IDAVBackend;
use App\Http\Controllers\DAV\Backend\SyncDAVBackend;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
abstract class AbstractCalDAVBackend implements ICalDAVBackend, IDAVBackend
{
use SyncDAVBackend;
/**
* Get description array.
*
* @return array
*/
public function getDescription()
{
$token = DAVSyncPlugin::SYNCTOKEN_PREFIX.$this->refreshSyncToken(null)->id;
return [
'id' => $this->backendUri(),
'uri' => $this->backendUri(),
'principaluri' => PrincipalBackend::getPrincipalUser($this->user),
'{DAV:}sync-token' => $token,
'{'.SabreServer::NS_SABREDAV.'}sync-token' => $token,
'{'.CalDAVPlugin::NS_CALENDARSERVER.'}getctag' => $token,
];
}
/**
* Get the new exported version of the object.
*
* @param mixed $obj
* @return string
*/
abstract protected function refreshObject($obj): string;
}

View File

@@ -0,0 +1,356 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Sabre\DAV;
use App\Traits\WithUser;
use Sabre\CalDAV\Backend\SyncSupport;
use Sabre\CalDAV\Backend\AbstractBackend;
class CalDAVBackend extends AbstractBackend implements SyncSupport
{
use WithUser;
/**
* Set the Calendar backends.
*
* @return array
*/
private function getBackends(): array
{
return [
app(CalDAVBirthdays::class)->init($this->user),
app(CalDAVTasks::class)->init($this->user),
];
}
/**
* Get the backend for this id.
*
* @return AbstractCalDAVBackend|null
*/
private function getBackend($id)
{
return collect($this->getBackends())->first(function ($backend) use ($id) {
return $backend->backendUri() === $id;
});
}
/**
* Returns a list of calendars for a principal.
*
* Every project is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* calendar. This can be the same as the uri or a database key.
* * uri, which is the basename of the uri with which the calendar is
* accessed.
* * principaluri. The owner of the calendar. Almost always the same as
* principalUri passed to this method.
*
* Furthermore it can contain webdav properties in clark notation. A very
* common one is '{DAV:}displayname'.
*
* Many clients also require:
* {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
* For this property, you can just return an instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet.
*
* If you return {http://sabredav.org/ns}read-only and set the value to 1,
* ACL will automatically be put in read-only mode.
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
return array_map(function ($backend) {
return $backend->getDescription();
}, $this->getBackends());
}
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified calendar.
*
* This function should return an array, such as the following:
*
* [
* 'syncToken' => 'The current synctoken',
* 'added' => [
* 'new.txt',
* ],
* 'modified' => [
* 'modified.txt',
* ],
* 'deleted' => [
* 'foo.php.bak',
* 'old.txt'
* ]
* );
*
* The returned syncToken property should reflect the *current* syncToken
* of the calendar, as reported in the {http://sabredav.org/ns}sync-token
* property This is * needed here too, to ensure the operation is atomic.
*
* If the $syncToken argument is specified as null, this is an initial
* sync, and all members should be reported.
*
* The modified property is an array of nodenames that have changed since
* the last token.
*
* The deleted property is an array with nodenames, that have been deleted
* from collection.
*
* The $syncLevel argument is basically the 'depth' of the report. If it's
* 1, you only have to report changes that happened only directly in
* immediate descendants. If it's 2, it should also include changes from
* the nodes below the child collections. (grandchildren)
*
* The $limit argument allows a client to specify how many results should
* be returned at most. If the limit is not specified, it should be treated
* as infinite.
*
* If the limit (infinite or not) is higher than you're willing to return,
* you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
*
* If the syncToken is expired (due to data cleanup) or unknown, you must
* return null.
*
* The limit is 'suggestive'. You are free to ignore it.
*
* @param string $calendarId
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
* @return array
*/
public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null)
{
$backend = $this->getBackend($calendarId);
if ($backend) {
return $backend->getChanges($calendarId, $syncToken);
}
return [];
}
/**
* Returns all calendar objects within a calendar.
*
* Every item contains an array with the following keys:
* * calendardata - The iCalendar-compatible calendar data
* * uri - a unique key which will be used to construct the uri. This can
* be any arbitrary string, but making sure it ends with '.ics' is a
* good idea. This is only the basename, or filename, not the full
* path.
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
* '"abcdef"')
* * size - The size of the calendar objects, in bytes.
* * component - optional, a string containing the type of object, such
* as 'vevent' or 'vtodo'. If specified, this will be used to populate
* the Content-Type header.
*
* Note that the etag is optional, but it's highly encouraged to return for
* speed reasons.
*
* The calendardata is also optional. If it's not returned
* 'getCalendarObject' will be called later, which *is* expected to return
* calendardata.
*
* If neither etag or size are specified, the calendardata will be
* used/fetched to determine these numbers. If both are specified the
* amount of times this is needed is reduced by a great degree.
*
* @param mixed $calendarId
* @return array
*/
public function getCalendarObjects($calendarId)
{
$backend = $this->getBackend($calendarId);
if ($backend) {
$objs = $backend->getObjects($calendarId);
return $objs
->map(function ($date) use ($backend) {
return $backend->prepareData($date);
})
->filter(function ($event) {
return $event !== null;
})
->toArray();
}
return [];
}
/**
* Returns information from a single calendar object, based on it's object
* uri.
*
* The object uri is only the basename, or filename and not a full path.
*
* The returned array must have the same keys as getCalendarObjects. The
* 'calendardata' object is required here though, while it's not required
* for getCalendarObjects.
*
* This method must return null if the object did not exist.
*
* @param mixed $calendarId
* @param string $objectUri
* @return array|null
*/
public function getCalendarObject($calendarId, $objectUri)
{
$backend = $this->getBackend($calendarId);
if ($backend) {
$obj = $backend->getObject($calendarId, $objectUri);
if ($obj) {
return $backend->prepareData($obj);
}
}
return [];
}
/**
* Creates a new calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* It is possible to return an etag from this function, which will be used
* in the response to this PUT request. Note that the ETag must be
* surrounded by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData)
{
return $this->updateCalendarObject($calendarId, $objectUri, $calendarData);
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* The object uri is only the basename, or filename and not a full path.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData): ?string
{
$backend = $this->getBackend($calendarId);
return $backend ?
$backend->updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData)
: null;
}
/**
* Deletes an existing calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* @param mixed $calendarId
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri)
{
$backend = $this->getBackend($calendarId);
if ($backend) {
$backend->deleteCalendarObject($objectUri);
}
}
/**
* Creates a new calendar for a principal.
*
* If the creation was a success, an id must be returned that can be used to
* reference this calendar in other methods, such as updateCalendar.
*
* The id can be any type, including ints, strings, objects or array.
*
* @param string $principalUri
* @param string $calendarUri
* @param array $properties
* @return void
*/
public function createCalendar($principalUri, $calendarUri, array $properties): void
{
}
/**
* Delete a calendar and all its objects.
*
* @param mixed $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
}
/**
* Creates a new subscription for a principal.
*
* If the creation was a success, an id must be returned that can be used to reference
* this subscription in other methods, such as updateSubscription.
*
* @param string $principalUri
* @param string $uri
* @param array $properties
* @return mixed
*/
public function createSubscription($principalUri, $uri, array $properties)
{
return false;
}
/**
* Updates a subscription.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
* you're going to process with the handle() method.
*
* Calling the handle method is like telling the PropPatch object "I
* promise I can handle updating this property".
*
* Read the PropPatch documentation for more info and examples.
*
* @param mixed $subscriptionId
* @param \Sabre\DAV\PropPatch $propPatch
* @return void
*/
public function updateSubscription($subscriptionId, DAV\PropPatch $propPatch)
{
}
/**
* Deletes a subscription.
*
* @param mixed $subscriptionId
* @return void
*/
public function deleteSubscription($subscriptionId)
{
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Illuminate\Support\Facades\Log;
use App\Models\Instance\SpecialDate;
use Sabre\DAV\Server as SabreServer;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use App\Services\VCalendar\ExportVCalendar;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
class CalDAVBirthdays extends AbstractCalDAVBackend
{
/**
* Returns the uri for this backend.
*
* @return string
*/
public function backendUri()
{
return 'birthdays';
}
public function getDescription()
{
return parent::getDescription()
+ [
'{DAV:}displayname' => trans('app.dav_birthdays'),
'{'.SabreServer::NS_SABREDAV.'}read-only' => true,
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_birthdays_description', ['name' => $this->user->name]),
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => $this->user->timezone,
'{'.CalDAVPlugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']),
'{'.CalDAVPlugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT),
];
}
/**
* Extension for Calendar objects.
*
* @return string
*/
public function getExtension()
{
return '.ics';
}
/**
* Datas for this date.
*
* @param mixed $obj
* @return array
*/
public function prepareData($obj)
{
$calendardata = null;
if ($obj instanceof SpecialDate) {
try {
$calendardata = $this->refreshObject($obj);
return [
'id' => $obj->id,
'uri' => $this->encodeUri($obj),
'calendardata' => $calendardata,
'etag' => '"'.sha1($calendardata).'"',
'lastmodified' => $obj->updated_at->timestamp,
];
} catch (\Exception $e) {
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
'calendardata' => $calendardata,
$e,
]);
}
}
return [];
}
/**
* Get the new exported version of the object.
*
* @param mixed $obj date
* @return string
*/
protected function refreshObject($obj): string
{
$vcal = app(ExportVCalendar::class)
->execute([
'account_id' => $this->user->account_id,
'special_date_id' => $obj->id,
]);
return $vcal->serialize();
}
private function hasBirthday($contact)
{
if (! $contact || ! $contact->birthdate) {
return false;
}
$birthdayState = $contact->getBirthdayState();
if ($birthdayState != 'almost' && $birthdayState != 'exact') {
return false;
}
return true;
}
/**
* Returns the date for the specific uuid.
*
* @param string|null $collectionId
* @param string $uuid
* @return mixed
*/
public function getObjectUuid($collectionId, $uuid)
{
return SpecialDate::where([
'account_id' => $this->user->account_id,
'uuid' => $uuid,
])->first();
}
/**
* Returns the collection of contact's birthdays.
*
* @return \Illuminate\Support\Collection
*/
public function getObjects($collectionId)
{
// We only return the birthday of default addressBook
$contacts = $this->user->account->contacts()
->real()
->active()
->get();
return $contacts->filter(function ($contact) {
return $this->hasBirthday($contact);
})
->map(function ($contact) {
return $contact->birthdate;
});
}
/**
* Returns the collection of deleted birthdays.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection
*/
public function getDeletedObjects($collectionId)
{
return collect();
}
/**
* @return string|null
*/
public function updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData): ?string
{
return null;
}
public function deleteCalendarObject($objectUri)
{
// Not implemented
}
}

View File

@@ -0,0 +1,219 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Illuminate\Support\Arr;
use App\Models\Contact\Task;
use App\Services\Task\DestroyTask;
use Illuminate\Support\Facades\Log;
use App\Services\VCalendar\ExportTask;
use App\Services\VCalendar\ImportTask;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
class CalDAVTasks extends AbstractCalDAVBackend
{
/**
* Returns the uri for this backend.
*
* @return string
*/
public function backendUri()
{
return 'tasks';
}
public function getDescription()
{
return parent::getDescription()
+ [
'{DAV:}displayname' => trans('app.dav_tasks'),
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_tasks_description', ['name' => $this->user->name]),
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => $this->user->timezone,
'{'.CalDAVPlugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']),
'{'.CalDAVPlugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT),
];
}
/**
* Returns the collection of all tasks.
*
* @param mixed|null $collectionId
* @return \Illuminate\Support\Collection
*/
public function getObjects($collectionId)
{
return $this->user->account
->tasks()
->get();
}
/**
* Returns the collection of deleted tasks.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection
*/
public function getDeletedObjects($collectionId)
{
return collect();
}
/**
* Returns the contact for the specific uuid.
*
* @param mixed|null $collectionId
* @param string $uuid
* @return mixed
*/
public function getObjectUuid($collectionId, $uuid)
{
return Task::where([
'account_id' => $this->user->account_id,
'uuid' => $uuid,
])->first();
}
/**
* Extension for Calendar objects.
*
* @return string
*/
public function getExtension()
{
return '.ics';
}
/**
* Datas for this task.
*
* @param mixed $obj
* @return array
*/
public function prepareData($obj)
{
$calendardata = null;
if ($obj instanceof Task) {
try {
$calendardata = $this->refreshObject($obj);
return [
'id' => $obj->id,
'uri' => $this->encodeUri($obj),
'calendardata' => $calendardata,
'etag' => '"'.sha1($calendardata).'"',
'lastmodified' => $obj->updated_at->timestamp,
];
} catch (\Exception $e) {
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
'calendardata' => $calendardata,
$e,
]);
}
}
return [];
}
/**
* Get the new exported version of the object.
*
* @param mixed $obj task
* @return string
*/
protected function refreshObject($obj): string
{
$vcal = app(ExportTask::class)
->execute([
'account_id' => $this->user->account_id,
'task_id' => $obj->id,
]);
return $vcal->serialize();
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* The object uri is only the basename, or filename and not a full path.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData): ?string
{
$task_id = null;
if ($objectUri) {
$task = $this->getObject($this->backendUri(), $objectUri);
if ($task) {
$task_id = $task->id;
}
}
try {
$result = app(ImportTask::class)
->execute([
'account_id' => $this->user->account_id,
'task_id' => $task_id,
'entry' => $calendarData,
]);
if (! Arr::has($result, 'error')) {
$task = Task::where('account_id', $this->user->account_id)
->find($result['task_id']);
$calendar = $this->prepareData($task);
return $calendar['etag'];
}
} catch (\Exception $e) {
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
'calendarId' => $calendarId,
'objectUri' => $objectUri,
'calendarData' => $calendarData,
$e,
]);
}
return null;
}
/**
* Deletes an existing calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($objectUri)
{
$task = $this->getObject($this->backendUri(), $objectUri);
if ($task) {
try {
app(DestroyTask::class)
->execute([
'account_id' => $this->user->account_id,
'task_id' => $task->id,
]);
} catch (\Exception $e) {
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
'objectUri' => $objectUri,
$e,
]);
}
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
interface ICalDAVBackend
{
/**
* Returns a list of properties for a principal.
*
* Every project is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* calendar. This can be the same as the uri or a database key.
* * uri, which is the basename of the uri with which the calendar is
* accessed.
* * principaluri. The owner of the calendar. Almost always the same as
* principalUri passed to this method.
*
* Furthermore it can contain webdav properties in clark notation. A very
* common one is '{DAV:}displayname'.
*
* Many clients also require:
* {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
* For this property, you can just return an instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet.
*
* If you return {http://sabredav.org/ns}read-only and set the value to 1,
* ACL will automatically be put in read-only mode.
*
************************
* == From Subscription :
* Furthermore, all the subscription info must be returned too:
*
* 1. {DAV:}displayname
* 2. {http://apple.com/ns/ical/}refreshrate
* 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
* should not be stripped).
* 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
* should not be stripped).
* 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
* attachments should not be stripped).
* 6. {http://calendarserver.org/ns/}source (Must be a
* Sabre\DAV\Property\Href).
* 7. {http://apple.com/ns/ical/}calendar-color
* 8. {http://apple.com/ns/ical/}calendar-order
* 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
* (should just be an instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
* default components).
*
* @return array
*/
public function getDescription();
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified calendar.
*
* @param string|null $calendarId
* @param string $syncToken
* @return array
*/
public function getChanges($calendarId, $syncToken);
/**
* Returns calendar object.
*
* It returns an array with the following keys:
* * calendardata - The iCalendar-compatible calendar data
* * uri - a unique key which will be used to construct the uri. This can
* be any arbitrary string, but making sure it ends with '.ics' is a
* good idea. This is only the basename, or filename, not the full
* path.
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
* '"abcdef"')
* * size - The size of the calendar objects, in bytes.
* * component - optional, a string containing the type of object, such
* as 'vevent' or 'vtodo'. If specified, this will be used to populate
* the Content-Type header.
*
* Note that the etag is optional, but it's highly encouraged to return for
* speed reasons.
*
* @param mixed $obj
* @return array
*/
public function prepareData($obj);
/**
* Updates an existing calendarobject, based on it's uri.
*
* The object uri is only the basename, or filename and not a full path.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param string|null $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData): ?string;
/**
* Deletes an existing calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($objectUri);
}