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,80 @@
<?php
namespace App\Http\Controllers\DAV\Auth;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Illuminate\Support\Facades\Auth;
use Sabre\DAV\Auth\Backend\BackendInterface;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
class AuthBackend implements BackendInterface
{
/**
* Authentication Realm.
*
* The realm is often displayed by browser clients when showing the
* authentication dialog.
*
* @var string
*/
protected $realm = 'sabre/dav';
/**
* Sets the authentication realm for this backend.
*
* @param string $realm
* @return void
*/
public function setRealm($realm)
{
$this->realm = $realm;
}
/**
* Check Laravel authentication.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return array
*/
public function check(RequestInterface $request, ResponseInterface $response)
{
if (! Auth::check()) {
return [false, 'User is not authenticated'];
}
return [true, PrincipalBackend::getPrincipalUser(Auth::user())];
}
/**
* This method is called when a user could not be authenticated, and
* authentication was required for the current request.
*
* This gives you the opportunity to set authentication headers. The 401
* status code will already be set.
*
* In this case of Bearer Auth, this would for example mean that the
* following header needs to be set:
*
* $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV');
*
* Keep in mind that in the case of multiple authentication backends, other
* WWW-Authenticate headers may already have been set, and you'll want to
* append your own WWW-Authenticate header instead of overwriting the
* existing one.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return void
*/
public function challenge(RequestInterface $request, ResponseInterface $response)
{
$auth = new \Sabre\HTTP\Auth\Bearer(
$this->realm,
$request,
$response
);
$auth->requireLogin();
}
}

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);
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\CardDAV\AddressBook as BaseAddressBook;
class AddressBook extends BaseAddressBook
{
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
public function getACL()
{
return [
[
'privilege' => '{DAV:}read',
'principal' => '{DAV:}owner',
'protected' => true,
],
[
'privilege' => '{DAV:}write-content',
'principal' => '{DAV:}owner',
'protected' => true,
],
[
'privilege' => '{DAV:}bind',
'principal' => '{DAV:}owner',
'protected' => true,
],
[
'privilege' => '{DAV:}unbind',
'principal' => '{DAV:}owner',
'protected' => true,
],
[
'privilege' => '{DAV:}write-properties',
'principal' => '{DAV:}owner',
'protected' => true,
],
];
}
/**
* This method returns the ACL's for card nodes in this address book.
* The result of this method automatically gets passed to the
* card nodes in this address book.
*
* @return array
*/
public function getChildACL()
{
return $this->getACL();
}
/**
* Returns the last modification date.
*
* @return int|null
*/
public function getLastModified(): ?int
{
$carddavBackend = $this->carddavBackend;
if ($carddavBackend instanceof CardDAVBackend) {
$date = $carddavBackend->getLastModified(null);
if (! is_null($date)) {
return (int) $date->timestamp;
}
}
return null;
}
/**
* This method returns the current sync-token for this collection.
* This can be any string.
*
* If null is returned from this function, the plugin assumes there's no
* sync information available.
*
* @return string|null
*/
public function getSyncToken(): ?string
{
$carddavBackend = $this->carddavBackend;
if ($carddavBackend instanceof CardDAVBackend) {
return (string) $carddavBackend->refreshSyncToken(null)->id;
}
return null;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\CardDAV\AddressBookHome as BaseAddressBookHome;
class AddressBookHome extends BaseAddressBookHome
{
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
public function getACL()
{
return [
[
'privilege' => '{DAV:}read',
'principal' => '{DAV:}owner',
'protected' => true,
],
];
}
/**
* Returns a list of addressbooks.
*
* @return array
*/
public function getChildren()
{
$addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
return collect($addressBooks)->map(function (array $addressBook): AddressBook {
return new AddressBook($this->carddavBackend, $addressBook);
})->toArray();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\DAVACL\IACL;
use Sabre\DAVACL\ACLTrait;
use Sabre\CardDAV\AddressBookRoot as BaseAddressBookRoot;
class AddressBookRoot extends BaseAddressBookRoot implements IACL
{
use ACLTrait;
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
public function getACL()
{
return [
[
'privilege' => '{DAV:}read',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
];
}
/**
* This method returns a node for a principal.
*
* The passed array contains principal information, and is guaranteed to
* at least contain a uri item. Other properties may or may not be
* supplied by the authentication backend.
*
* @param array $principal
* @return \Sabre\DAV\INode
* @psalm-suppress ParamNameMismatch
*/
public function getChildForPrincipal(array $principal)
{
return new AddressBookHome($this->carddavBackend, $principal['uri']);
}
}

View File

@@ -0,0 +1,486 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\DAV;
use App\Jobs\Dav\UpdateVCard;
use App\Models\Contact\Contact;
use App\Services\VCard\GetEtag;
use App\Models\Account\AddressBook;
use App\Services\VCard\ExportVCard;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Sabre\DAV\Server as SabreServer;
use Sabre\CardDAV\Backend\SyncSupport;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\CardDAV\Backend\AbstractBackend;
use Sabre\CardDAV\Plugin as CardDAVPlugin;
use Sabre\DAV\Sync\Plugin as DAVSyncPlugin;
use App\Services\Contact\Contact\SetMeContact;
use App\Services\Contact\Contact\DestroyContact;
use App\Http\Controllers\DAV\Backend\IDAVBackend;
use App\Http\Controllers\DAV\Backend\SyncDAVBackend;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
use App\Services\DavClient\Utils\Model\ContactUpdateDto;
class CardDAVBackend extends AbstractBackend implements SyncSupport, IDAVBackend
{
use SyncDAVBackend;
/**
* Returns the uri for this backend.
*
* @return string
*/
public function backendUri()
{
return 'contacts';
}
/**
* Returns the list of addressbooks for a specific user.
*
* Every addressbook should have the following properties:
* id - an arbitrary unique id
* uri - the 'basename' part of the url
* principaluri - Same as the passed parameter
*
* Any additional clark-notation property may be passed besides this. Some
* common ones are :
* {DAV:}displayname
* {urn:ietf:params:xml:ns:carddav}addressbook-description
* {http://calendarserver.org/ns/}getctag
*
* @param string $principalUri
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
$result = [];
$result[] = $this->getDefaultAddressBook();
$addressBooks = AddressBook::where('account_id', $this->user->account_id)
->get();
foreach ($addressBooks as $addressBook) {
$result[] = $this->getAddressBookDetails($addressBook);
}
return $result;
}
private function getDefaultAddressBook()
{
$des = $this->getAddressBookDetails(null);
$me = auth()->user()->me;
if ($me) {
$des += [
'{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card' => '/'.config('laravelsabre.path').'/addressbooks/'.$this->user->email.'/contacts/'.$this->encodeUri($me),
];
}
return $des;
}
private function getAddressBookDetails($addressBook)
{
$id = $addressBook ? $addressBook->name : $this->backendUri();
$token = $this->getCurrentSyncToken($addressBook);
$des = [
'id' => $id,
'uri' => $id,
'principaluri' => PrincipalBackend::getPrincipalUser($this->user),
'{DAV:}displayname' => trans('app.dav_contacts'),
'{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => $addressBook ? $addressBook->description : trans('app.dav_contacts_description', ['name' => $this->user->name]),
];
if ($token) {
$des += [
'{DAV:}sync-token' => $token->id,
'{'.SabreServer::NS_SABREDAV.'}sync-token' => $token->id,
'{'.CalDAVPlugin::NS_CALENDARSERVER.'}getctag' => DAVSyncPlugin::SYNCTOKEN_PREFIX.$token->id,
];
}
return $des;
}
/**
* Extension for Calendar objects.
*
* @return string
*/
public function getExtension()
{
return '.vcf';
}
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified address book.
*
* 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 $addressBookId
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
* @return array|null
*/
public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null): ?array
{
return $this->getChanges($addressBookId, $syncToken);
}
/**
* Prepare datas for this contact.
*
* @param Contact $contact
* @return array
*/
public function prepareCard($contact): array
{
$carddata = $contact->vcard;
try {
if (empty($carddata)) {
$carddata = $this->refreshObject($contact);
}
$etag = app(GetEtag::class)->execute([
'account_id' => $this->user->account_id,
'contact_id' => $contact->id,
]);
return [
'contact_id' => $contact->id,
'uri' => $this->encodeUri($contact),
'carddata' => $carddata,
'etag' => $etag,
'distant_etag' => $contact->distant_etag,
'lastmodified' => $contact->updated_at->timestamp,
];
} catch (\Exception $e) {
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
'carddata' => $carddata,
'contact_id' => $contact->id,
$e,
]);
throw $e;
}
}
/**
* Get the new exported version of the object.
*
* @param mixed $obj contact
* @return string
*/
protected function refreshObject($obj): string
{
$vcard = app(ExportVCard::class)
->execute([
'account_id' => $this->user->account_id,
'contact_id' => $obj->id,
]);
return $vcard->serialize();
}
/**
* Returns the contact for the specific uuid.
*
* @param mixed|null $collectionId
* @param string $uuid
* @return Contact
*/
public function getObjectUuid($collectionId, $uuid)
{
$addressBook = null;
if ($collectionId && $collectionId != $this->backendUri()) {
$addressBook = AddressBook::where([
'account_id' => $this->user->account_id,
'name' => $collectionId,
])->first();
}
return Contact::where([
'account_id' => $this->user->account_id,
'uuid' => $uuid,
'address_book_id' => $addressBook ? $addressBook->id : null,
])->first();
}
/**
* Returns the collection of all active contacts.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection<array-key, Contact>
*/
public function getObjects($collectionId)
{
return $this->user->account->contacts($collectionId)
->real()
->active()
->get();
}
/**
* Returns the collection of deleted contacts.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection<array-key, Contact>
*/
public function getDeletedObjects($collectionId)
{
return $this->user->account->contacts($collectionId)
->onlyTrashed()
->get();
}
/**
* Returns all cards for a specific addressbook id.
*
* This method should return the following properties for each card:
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
*
* It's recommended to also return the following properties:
* * etag - A unique etag. This must change every time the card changes.
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
* calculating them. If they are specified, you can also ommit carddata.
* This may speed up certain requests, especially with large cards.
*
* @param mixed $addressbookId
* @return array
*/
public function getCards($addressbookId)
{
$contacts = $this->getObjects($addressbookId);
return $contacts->map(function ($contact) {
return $this->prepareCard($contact);
})->toArray();
}
/**
* Returns a specific card.
*
* The same set of properties must be returned as with getCards. The only
* exception is that 'carddata' is absolutely required.
*
* If the card does not exist, you must return false.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return array|bool
*/
public function getCard($addressBookId, $cardUri)
{
$contact = $this->getObject($addressBookId, $cardUri);
if ($contact) {
return $this->prepareCard($contact);
}
return false;
}
/**
* Creates a new card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag is for the
* newly created resource, and must be enclosed with double quotes (that
* is, the string itself must contain the double quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
public function createCard($addressBookId, $cardUri, $cardData)
{
return $this->updateCard($addressBookId, $cardUri, $cardData);
}
/**
* Updates a card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag should
* match that of the updated resource, and must be enclosed with double
* quotes (that is: the string itself must contain the actual quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string|resource $cardData
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData): ?string
{
$job = new UpdateVCard($this->user, $addressBookId, new ContactUpdateDto($cardUri, null, $cardData));
Bus::batch([$job])
->allowFailures()
->dispatch();
return null;
}
/**
* Deletes a card.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
$contact = $this->getObject($addressBookId, $cardUri);
if ($contact) {
DestroyContact::dispatch([
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
]);
return true;
}
return false;
}
/**
* Updates properties for an address book.
*
* 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 string $addressBookId
* @param \Sabre\DAV\PropPatch $propPatch
* @return bool|null
*/
public function updateAddressBook($addressBookId, DAV\PropPatch $propPatch): ?bool
{
$propPatch->handle('{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card', function ($props) use ($addressBookId) {
$contact = $this->getObject($addressBookId, $props->getHref());
$data = [
'contact_id' => $contact->id,
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
];
app(SetMeContact::class)->execute($data);
return true;
});
return null;
}
/**
* Creates a new address book.
*
* This method should return the id of the new address book. The id can be
* in any format, including ints, strings, arrays or objects.
*
* @param string $principalUri
* @param string $url Just the 'basename' of the url.
* @param array $properties
* @return int|bool
*/
public function createAddressBook($principalUri, $url, array $properties)
{
return false;
}
/**
* Deletes an entire addressbook and all its contents.
*
* @param mixed $addressBookId
* @return bool|null
*/
public function deleteAddressBook($addressBookId)
{
return false;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\DAV\Backend;
interface IDAVBackend
{
/**
* Returns the uri for this backend.
*
* @return string
*/
public function backendUri();
/**
* Returns the object for the specific uuid.
*
* @param string|null $collectionId
* @param string $uuid
* @return mixed
*/
public function getObjectUuid($collectionId, $uuid);
/**
* Returns the collection of objects.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection
*/
public function getObjects($collectionId);
/**
* Returns the extension for this backend.
*
* @return string
*/
public function getExtension();
}

View File

@@ -0,0 +1,288 @@
<?php
namespace App\Http\Controllers\DAV\Backend;
use App\Traits\WithUser;
use Illuminate\Support\Str;
use App\Models\User\SyncToken;
trait SyncDAVBackend
{
use WithUser;
/**
* This method returns a sync-token for this collection.
*
* If null is returned from this function, the plugin assumes there's no
* sync information available.
*
* @param string|null $collectionId
* @return SyncToken|null
*/
public function getCurrentSyncToken($collectionId): ?SyncToken
{
$tokens = SyncToken::where([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
'name' => $collectionId ?? $this->backendUri(),
])
->orderBy('created_at')
->get();
return $tokens->count() > 0 ? $tokens->last() : null;
}
/**
* Create or refresh the token if a change happened.
*
* @param string|null $collectionId
* @return SyncToken
*/
public function refreshSyncToken($collectionId): SyncToken
{
$token = $this->getCurrentSyncToken($collectionId);
if (! $token || $token->timestamp < $this->getLastModified($collectionId)) {
$token = $this->createSyncTokenNow($collectionId);
}
return $token;
}
/**
* Get SyncToken by token id.
*
* @param string|null $collectionId
* @param string $syncToken
* @return SyncToken|null
*/
protected function getSyncToken($collectionId, $syncToken)
{
/** @var SyncToken|null */
return SyncToken::where([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
'name' => $collectionId ?? $this->backendUri(),
])
->find($syncToken);
}
/**
* Create a token with now timestamp.
*
* @param string|null $collectionId
* @return SyncToken
*/
private function createSyncTokenNow($collectionId)
{
return SyncToken::create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
'name' => $collectionId ?? $this->backendUri(),
'timestamp' => now(),
]);
}
/**
* Returns the last modification date.
*
* @param string|null $collectionId
* @return \Carbon\Carbon|null
*/
public function getLastModified($collectionId)
{
return $this->getObjects($collectionId)
->map(function ($object) {
return $object->updated_at;
})
->max();
}
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken.
*
* 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
* , 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
* @return array|null
*/
public function getChanges($calendarId, $syncToken): ?array
{
$token = null;
$timestamp = null;
if (! empty($syncToken)) {
$token = $this->getSyncToken($calendarId, $syncToken);
if (is_null($token)) {
// syncToken is not recognized
return null;
}
$timestamp = $token->timestamp;
}
$objs = $this->getObjects($calendarId);
$modified = $objs->filter(function ($obj) use ($timestamp) {
return ! is_null($timestamp) &&
$obj->updated_at > $timestamp &&
$obj->created_at < $timestamp;
});
$added = $objs->filter(function ($obj) use ($timestamp) {
return is_null($timestamp) ||
$obj->created_at >= $timestamp;
});
$deleted = $this->getDeletedObjects($calendarId)
->filter(function ($obj) use ($timestamp) {
$d = $obj->deleted_at;
return is_null($timestamp) ||
$obj->deleted_at >= $timestamp;
});
return [
'syncToken' => $this->refreshSyncToken($calendarId)->id,
'added' => $added->map(function ($obj) {
return $this->encodeUri($obj);
})->values()->toArray(),
'modified' => $modified->map(function ($obj) {
$this->refreshObject($obj);
return $this->encodeUri($obj);
})->values()->toArray(),
'deleted' => $deleted->map(function ($obj) {
return $this->encodeUri($obj);
})->values()->toArray(),
];
}
protected function encodeUri($obj): string
{
if (empty($obj->uuid)) {
// refresh model from database
$obj->refresh();
if (empty($obj->uuid)) {
// in case uuid is still not set, do it
$obj->forceFill([
'uuid' => Str::uuid(),
])->save();
}
}
return urlencode($obj->uuid.$this->getExtension());
}
private function decodeUri($uri): string
{
return pathinfo(urldecode($uri), PATHINFO_FILENAME);
}
/**
* Returns the contact uuid for the specific uri.
*
* @param string $uri
* @return string
*/
public function getUuid($uri): string
{
return $this->decodeUri($uri);
}
/**
* Returns the contact for the specific uri.
*
* @param string|null $collectionId
* @param string $uri
* @return mixed
*/
public function getObject($collectionId, $uri)
{
try {
return $this->getObjectUuid($collectionId, $this->getUuid($uri));
} catch (\Exception $e) {
// Object not found
}
}
/**
* Returns the object for the specific uuid.
*
* @param string|null $collectionId
* @param string $uuid
* @return mixed
*/
abstract public function getObjectUuid($collectionId, $uuid);
/**
* Returns the collection of objects.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection
*/
abstract public function getObjects($collectionId);
/**
* Returns the collection of objects.
*
* @param string|null $collectionId
* @return \Illuminate\Support\Collection
*/
abstract public function getDeletedObjects($collectionId);
abstract public function getExtension();
/**
* Get the new exported version of the object.
*
* @param mixed $obj
* @return string
*/
abstract protected function refreshObject($obj): string;
}

View File

@@ -0,0 +1,203 @@
<?php
namespace App\Http\Controllers\DAV\DAVACL;
use Sabre\DAV;
use App\Traits\WithUser;
use Illuminate\Support\Str;
use Sabre\DAV\Server as SabreServer;
use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
class PrincipalBackend extends AbstractBackend
{
use WithUser;
/**
* This is the prefix that will be used to generate principal urls.
*
* @var string
*/
public const PRINCIPAL_PREFIX = 'principals/';
/**
* Get the principal for user.
*
* @return string
*/
public static function getPrincipalUser($user): string
{
return static::PRINCIPAL_PREFIX.$user->email;
}
protected function getPrincipals()
{
return [
[
'uri' => static::getPrincipalUser($this->user),
'{DAV:}displayname' => $this->user->name,
'{'.SabreServer::NS_SABREDAV.'}email-address' => $this->user->email,
],
];
}
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
* {http://sabredav.org/ns}email-address - This is a custom SabreDAV
* field that's actually injected in a number of other properties. If
* you have an email address, use this property.
*
* @param string $prefixPath
* @return array
*/
public function getPrincipalsByPrefix($prefixPath)
{
$prefixPath = Str::finish($prefixPath, '/');
return array_filter($this->getPrincipals(), function ($principal) use ($prefixPath) {
return ! $prefixPath || strpos($principal['uri'], $prefixPath) == 0;
});
}
/**
* Returns a specific principal, specified by its path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $path
* @return array
*/
public function getPrincipalByPath($path)
{
foreach ($this->getPrincipalsByPrefix(static::PRINCIPAL_PREFIX) as $principal) {
if ($principal['uri'] === $path) {
return $principal;
}
}
return [];
}
/**
* Updates one ore more webdav properties on a principal.
*
* 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 string $path
* @param \Sabre\DAV\PropPatch $propPatch
* @return void
*/
public function updatePrincipal($path, DAV\PropPatch $propPatch)
{
}
/**
* This method is used to search for principals matching a set of
* properties.
*
* This search is specifically used by RFC3744's principal-property-search
* REPORT.
*
* The actual search should be a unicode-non-case-sensitive search. The
* keys in searchProperties are the WebDAV property names, while the values
* are the property values to search on.
*
* By default, if multiple properties are submitted to this method, the
* various properties should be combined with 'AND'. If $test is set to
* 'anyof', it should be combined using 'OR'.
*
* This method should simply return an array with full principal uri's.
*
* If somebody attempted to search on a property the backend does not
* support, you should simply return 0 results.
*
* You can also just return 0 results if you choose to not support
* searching at all, but keep in mind that this may stop certain features
* from working.
*
* @param string $prefixPath
* @param array $searchProperties
* @param string $test
* @return array
*/
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
{
$result = [];
$principals = $this->getPrincipalsByPrefix($prefixPath);
if (! $principals) {
return $result;
}
foreach ($principals as $principal) {
$ok = false;
foreach ($searchProperties as $key => $value) {
if ($principal[$key] == $value) {
$ok = true;
} elseif ($test == 'allof') {
$ok = false;
break;
}
}
if ($ok) {
$result[] = $principal['uri'];
}
}
return $result;
}
/**
* Returns the list of members for a group-principal.
*
* @param string $principal
* @return array
*/
public function getGroupMemberSet($principal)
{
$principal = $this->getPrincipalByPath($principal);
if (! $principal) {
return [];
}
return [
$principal['uri'],
];
}
/**
* Returns the list of groups a principal is a member of.
*
* @param string $principal
* @return array
*/
public function getGroupMembership($principal)
{
return $this->getGroupMemberSet($principal);
}
/**
* Updates the list of group members for a group principal.
*
* The principals should be passed as a list of uri's.
*
* @param string $principal
* @param array $members
* @return void
*/
public function setGroupMemberSet($principal, array $members)
{
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\DAV;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Redirect all GET methods to settings page.
*/
class DAVRedirect extends ServerPlugin
{
public function initialize(Server $server)
{
$server->on('method:GET', [$this, 'httpGet'], 500);
}
/**
* This method intercepts GET requests to collections and returns the html.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
public function httpGet(RequestInterface $request, ResponseInterface $response)
{
$response->setStatus(302);
$response->setHeader('Location', route('settings.dav'));
return false;
}
}