feat: support CalDAV for birthdays and tasks (#2304)

This commit is contained in:
Alexis Saettler
2019-01-23 14:39:44 +01:00
committed by GitHub
parent 9b8d5a9c1c
commit ba236dd718
50 changed files with 3524 additions and 881 deletions
Binary file not shown.
+2 -2
View File
@@ -120,8 +120,8 @@ AWS_SERVER=
# Allow Two Factor Authentication feature on your instance
MFA_ENABLED=false
# Enable CardDAV support (beta feature)
CARDDAV_ENABLED=false
# Enable DAV support (beta feature)
DAV_ENABLED=false
# CLIENT ID and SECRET used for the official mobile application
# This is to make sure that only the mobile application that you approve can
+1
View File
@@ -2,6 +2,7 @@ UNRELEASED CHANGES:
New features:
* Support CalDAV to export the collection of birthdays (breaking change: url of CardDAV is '/dav' now)
* Add Laravel Telescope (deactivated by default)
* Add notion of instance administrator for a user
* Add ability to name u2f security keys and to delete register ones
@@ -1,13 +1,14 @@
<?php
namespace App\Models\CardDAV\Backends;
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 MonicaAuthBackend implements BackendInterface
class AuthBackend implements BackendInterface
{
/**
* Authentication Realm.
@@ -43,7 +44,7 @@ class MonicaAuthBackend implements BackendInterface
return [false, 'User is not authenticated'];
}
return [true, MonicaPrincipalBackend::getPrincipalUser()];
return [true, PrincipalBackend::getPrincipalUser()];
}
/**
@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use App\Http\Controllers\DAV\Backend\IDAVBackend;
use App\Http\Controllers\DAV\Backend\SyncDAVBackend;
abstract class AbstractCalDAVBackend implements ICalDAVBackend, IDAVBackend
{
use SyncDAVBackend;
}
@@ -0,0 +1,305 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Sabre\DAV;
use App\Models\User\SyncToken;
use Sabre\CalDAV\Backend\SyncSupport;
use Sabre\CalDAV\Backend\AbstractBackend;
class CalDAVBackend extends AbstractBackend implements SyncSupport
{
/**
* Set the Calendar backends.
*
* @return array
*/
private function getBackends(): array
{
return [
new CalDAVBirthdays(),
new CalDAVTasks(),
];
}
/**
* Get the backend for this id.
*
* @return AbstractCalDAVBackend
*/
private function getBackend($id): AbstractCalDAVBackend
{
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()
+ [
'id' => $backend->backendUri(),
'uri' => $backend->backendUri(),
];
}, $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($syncToken);
}
}
/**
* 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();
return $objs
->map(function ($date) use ($backend) {
return $backend->prepareData($date);
})
->filter(function ($event) {
return ! is_null($event);
})
->toArray();
}
}
/**
* 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($objectUri);
if ($obj) {
return $backend->prepareData($obj);
}
}
}
/**
* 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)
{
$backend = $this->getBackend($calendarId);
if ($backend) {
return $backend->updateOrCreateCalendarObject($objectUri, $calendarData);
}
}
/**
* 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) {
return $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 mixed
*/
public function createCalendar($principalUri, $calendarUri, array $properties)
{
}
/**
* Delete a calendar and all its objects.
*
* @param mixed $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
}
}
@@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Sabre\DAV;
use App\Models\Contact\Contact;
use Illuminate\Support\Facades\Log;
use App\Models\Instance\SpecialDate;
use Illuminate\Support\Facades\Auth;
use Sabre\DAV\Server as SabreServer;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use App\Services\VCalendar\ExportVCalendar;
use Sabre\DAV\Sync\Plugin as DAVSyncPlugin;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
class CalDAVBirthdays extends AbstractCalDAVBackend
{
/**
* Returns the uri for this backend.
*
* @return string
*/
public function backendUri()
{
return 'birthdays';
}
public function getDescription()
{
$name = Auth::user()->name;
$token = $this->getCurrentSyncToken();
$des = [
'principaluri' => PrincipalBackend::getPrincipalUser(),
'{DAV:}displayname' => $name,
'{'.SabreServer::NS_SABREDAV.'}read-only' => 1,
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => 'Birthdays',
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => Auth::user()->timezone,
];
if ($token) {
$token = DAVSyncPlugin::SYNCTOKEN_PREFIX.$token->id;
$des += [
'{DAV:}sync-token' => $token,
'{'.SabreServer::NS_SABREDAV.'}sync-token' => $token,
'{'.CalDAVPlugin::NS_CALENDARSERVER.'}getctag' => $token,
];
}
return $des;
}
/**
* Extension for Calendar objects.
*
* @var string
*/
public function getExtension()
{
return '.ics';
}
/**
* Datas for this date.
*
* @param mixed $date
* @return array
*/
public function prepareData($date)
{
if ($date instanceof SpecialDate) {
try {
$vcal = (new ExportVCalendar())
->execute([
'account_id' => Auth::user()->account_id,
'special_date_id' => $date->id,
]);
$calendardata = $vcal->serialize();
return [
'id' => $date->id,
'uri' => $this->encodeUri($date),
'calendardata' => $calendardata,
'etag' => '"'.md5($calendardata).'"',
'lastmodified' => $date->updated_at->timestamp,
];
} catch (\Exception $e) {
Log::debug(__CLASS__.' prepareData: '.(string) $e);
}
}
}
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 uri.
*
* @param string $uri
* @return mixed
*/
public function getObjectUuid($uuid)
{
return SpecialDate::where([
'account_id' => Auth::user()->account_id,
'uuid' => $uuid,
])->first();
}
/**
* Returns the collection of contact's birthdays.
*
* @return \Illuminate\Support\Collection
*/
public function getObjects()
{
$contacts = Auth::user()->account
->contacts()
->real()
->active()
->get();
return $contacts->filter(function ($contact) {
return $this->hasBirthday($contact);
})
->map(function ($contact) {
return $contact->birthdate;
});
}
public function updateOrCreateCalendarObject($objectUri, $calendarData)
{
}
public function deleteCalendarObject($objectUri)
{
}
}
@@ -0,0 +1,192 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
use Sabre\DAV;
use App\Models\Contact\Task;
use App\Services\Task\DestroyTask;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use Sabre\DAV\Server as SabreServer;
use App\Services\VCalendar\ExportTask;
use App\Services\VCalendar\ImportTask;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\DAV\Sync\Plugin as DAVSyncPlugin;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
class CalDAVTasks extends AbstractCalDAVBackend
{
/**
* Returns the uri for this backend.
*
* @return string
*/
public function backendUri()
{
return 'tasks';
}
public function getDescription()
{
$name = Auth::user()->name;
$token = $this->getCurrentSyncToken();
$des = [
'principaluri' => PrincipalBackend::getPrincipalUser(),
'{DAV:}displayname' => $name,
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => 'Tasks',
'{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => Auth::user()->timezone,
];
if ($token) {
$token = DAVSyncPlugin::SYNCTOKEN_PREFIX.$token->id;
$des += [
'{DAV:}sync-token' => $token,
'{'.SabreServer::NS_SABREDAV.'}sync-token' => $token,
'{'.CalDAVPlugin::NS_CALENDARSERVER.'}getctag' => $token,
];
}
return $des;
}
/**
* Returns the collection of all tasks.
*
* @return \Illuminate\Support\Collection
*/
public function getObjects()
{
return Auth::user()->account
->tasks()
->get();
}
/**
* Returns the contact for the specific uri.
*
* @param string $uri
* @return mixed
*/
public function getObjectUuid($uuid)
{
return Task::where([
'account_id' => Auth::user()->account_id,
'uuid' => $uuid,
])->first();
}
/**
* Extension for Calendar objects.
*
* @var string
*/
public function getExtension()
{
return '.ics';
}
/**
* Datas for this task.
*
* @param mixed $task
* @return array
*/
public function prepareData($task)
{
if ($task instanceof Task) {
try {
$vcal = (new ExportTask())
->execute([
'account_id' => Auth::user()->account_id,
'task_id' => $task->id,
]);
$calendardata = $vcal->serialize();
return [
'id' => $task->id,
'uri' => $this->encodeUri($task),
'calendardata' => $calendardata,
'etag' => '"'.md5($calendardata).'"',
'lastmodified' => $task->updated_at->timestamp,
];
} catch (\Exception $e) {
Log::debug(__CLASS__.' prepareData: '.(string) $e);
}
}
}
/**
* 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($objectUri, $calendarData)
{
$task_id = null;
if ($objectUri) {
$task = $this->getObject($objectUri);
if ($task) {
$task_id = $task->id;
}
}
try {
$result = (new ImportTask())
->execute([
'account_id' => Auth::user()->account_id,
'task_id' => $task_id,
'entry' => $calendarData,
]);
} catch (\Exception $e) {
Log::debug(__CLASS__.' updateOrCreateCalendarObject: '.(string) $e);
}
if (! array_has($result, 'error')) {
$task = Task::where('account_id', Auth::user()->account_id)
->find($result['task_id']);
$calendar = $this->prepareData($task);
return $calendar['etag'];
}
}
/**
* 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($objectUri);
if ($task) {
try {
(new DestroyTask)
->execute([
'account_id' => Auth::user()->account_id,
'task_id' => $task->id,
]);
} catch (\Exception $e) {
Log::debug(__CLASS__.' deleteCalendarObject: '.(string) $e);
}
}
}
}
@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers\DAV\Backend\CalDAV;
interface ICalDAVBackend
{
/**
* 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 getDescription();
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified calendar.
*
* @param string $syncToken
* @return array
*/
public function getChanges($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 mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateOrCreateCalendarObject($objectUri, $calendarData);
/**
* 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($objectUri);
}
@@ -1,11 +1,10 @@
<?php
namespace App\Models\CardDAV;
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\CardDAV\AddressBook;
use App\Models\CardDAV\Backends\MonicaCardDAVBackend;
use Sabre\CardDAV\AddressBook as BaseAddressBook;
class MonicaAddressBook extends AddressBook
class AddressBook extends BaseAddressBook
{
/**
* Returns a list of ACE's for this node.
@@ -64,7 +63,7 @@ class MonicaAddressBook extends AddressBook
*/
public function getLastModified()
{
if ($this->carddavBackend instanceof MonicaCardDAVBackend) {
if ($this->carddavBackend instanceof CardDAVBackend) {
return $this->carddavBackend->getLastModified();
}
}
@@ -1,10 +1,10 @@
<?php
namespace App\Models\CardDAV;
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\CardDAV\AddressBookHome;
use Sabre\CardDAV\AddressBookHome as BaseAddressBookHome;
class MonicaAddressBookHome extends AddressBookHome
class AddressBookHome extends BaseAddressBookHome
{
/**
* Returns a list of ACE's for this node.
@@ -39,7 +39,7 @@ class MonicaAddressBookHome extends AddressBookHome
$addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
return collect($addressbooks)->map(function ($addressbook) {
return new MonicaAddressBook($this->carddavBackend, $addressbook);
return new AddressBook($this->carddavBackend, $addressbook);
});
}
}
@@ -1,12 +1,12 @@
<?php
namespace App\Models\CardDAV;
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\DAVACL\IACL;
use Sabre\DAVACL\ACLTrait;
use Sabre\CardDAV\AddressBookRoot;
use Sabre\CardDAV\AddressBookRoot as BaseAddressBookRoot;
class MonicaAddressBookRoot extends AddressBookRoot implements IACL
class AddressBookRoot extends BaseAddressBookRoot implements IACL
{
use ACLTrait;
@@ -45,6 +45,6 @@ class MonicaAddressBookRoot extends AddressBookRoot implements IACL
*/
public function getChildForPrincipal(array $principal)
{
return new MonicaAddressBookHome($this->carddavBackend, $principal['uri']);
return new AddressBookHome($this->carddavBackend, $principal['uri']);
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Models\CardDAV\Backends;
namespace App\Http\Controllers\DAV\Backend\CardDAV;
use Sabre\DAV;
use App\Models\User\SyncToken;
@@ -12,11 +12,28 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
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\Http\Controllers\DAV\Backend\IDAVBackend;
use App\Http\Controllers\DAV\Backend\SyncDAVBackend;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
class MonicaCardDAVBackend extends AbstractBackend implements SyncSupport
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.
*
@@ -37,76 +54,36 @@ class MonicaCardDAVBackend extends AbstractBackend implements SyncSupport
public function getAddressBooksForUser($principalUri)
{
$name = Auth::user()->name;
$token = $this->getSyncToken();
$token = $this->getCurrentSyncToken();
$des = [
'id' => $this->backendUri(),
'uri' => $this->backendUri(),
'principaluri' => PrincipalBackend::getPrincipalUser(),
'{DAV:}displayname' => $name,
'{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => $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 [
[
'id' => '0',
'uri' => 'contacts',
'principaluri' => MonicaPrincipalBackend::getPrincipalUser(),
'{DAV:}sync-token' => $token->id,
'{DAV:}displayname' => $name,
'{'.SabreServer::NS_SABREDAV.'}sync-token' => $token->id,
'{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => $name,
],
$des,
];
}
/**
* This method returns a sync-token for this collection.
* Extension for Calendar objects.
*
* If null is returned from this function, the plugin assumes there's no
* sync information available.
*
* @return SyncToken
* @var string
*/
private function getSyncToken()
public function getExtension()
{
$tokens = SyncToken::where([
['account_id', Auth::user()->account_id],
['user_id', Auth::user()->id],
])
->orderBy('created_at')
->get();
if ($tokens->count() <= 0) {
$token = $this->createSyncToken();
} else {
$token = $tokens->last();
if ($token->timestamp < $this->getLastModified()) {
$token = $this->createSyncToken();
}
}
return $token;
}
/**
* Create a token.
*
* @return SyncToken
*/
private function createSyncToken()
{
$max = $this->getLastModified();
return SyncToken::create([
'account_id' => Auth::user()->account_id,
'user_id' => Auth::user()->id,
'timestamp' => $max,
]);
}
/**
* Returns the last modification date.
*
* @return \Carbon\Carbon
*/
public function getLastModified()
{
return $this->getContacts()
->max('updated_at');
return '.vcf';
}
/**
@@ -167,48 +144,213 @@ class MonicaCardDAVBackend extends AbstractBackend implements SyncSupport
*/
public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null)
{
$token = null;
$timestamp = null;
if (! empty($syncToken)) {
$token = SyncToken::where([
'account_id' => Auth::user()->account_id,
'user_id' => Auth::user()->id,
])->find($syncToken);
return $this->getChanges($syncToken);
}
if (is_null($token)) {
// syncToken is not recognized
return;
}
/**
* Prepare datas for this contact.
*
* @param Contact $contact
* @return array|null
*/
private function prepareCard($contact)
{
try {
$vcard = (new ExportVCard())
->execute([
'account_id' => Auth::user()->account_id,
'contact_id' => $contact->id,
]);
$timestamp = $token->timestamp;
$token = $this->getSyncToken();
} else {
$token = $this->createSyncToken();
$timestamp = null;
$carddata = $vcard->serialize();
return [
'id' => $contact->hashID(),
'uri' => $this->encodeUri($contact),
'carddata' => $carddata,
'etag' => '"'.md5($carddata).'"',
'lastmodified' => $contact->updated_at->timestamp,
];
} catch (\Exception $e) {
Log::debug(__CLASS__.' prepareCard: '.(string) $e);
}
}
/**
* Returns the contact for the specific uri.
*
* @param string $uri
* @return Contact
*/
public function getObjectUuid($uuid)
{
return Contact::where([
'account_id' => Auth::user()->account_id,
'uuid' => $uuid,
])->first();
}
/**
* Returns the collection of all active contacts.
*
* @return \Illuminate\Support\Collection
*/
public function getObjects()
{
return Auth::user()->account
->contacts()
->real()
->active()
->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();
return $contacts->map(function ($contact) {
return $this->prepareCard($contact);
});
}
/**
* 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($cardUri);
if ($contact) {
return $this->prepareCard($contact);
}
$contacts = $this->getContacts();
return false;
}
$modified = $contacts->filter(function ($contact) use ($timestamp) {
return ! is_null($timestamp) &&
$contact->updated_at > $timestamp &&
$contact->created_at < $timestamp;
});
$added = $contacts->filter(function ($contact) use ($timestamp) {
return is_null($timestamp) ||
$contact->created_at >= $timestamp;
});
/**
* 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);
}
return [
'syncToken' => $token->id,
'added' => $added->map(function ($contact) {
return $this->encodeUri($contact);
})->toArray(),
'modified' => $modified->map(function ($contact) {
return $this->encodeUri($contact);
})->toArray(),
'deleted' => [],
];
/**
* 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 $cardData
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData)
{
$contact_id = null;
if ($cardUri) {
$contact = $this->getObject($cardUri);
if ($contact) {
$contact_id = $contact->id;
}
}
try {
$result = (new ImportVCard(Auth::user()->account_id))
->execute([
'contact_id' => $contact_id,
'entry' => $cardData,
'behaviour' => ImportVCard::BEHAVIOUR_REPLACE,
]);
} catch (\Exception $e) {
Log::debug(__CLASS__.' updateCard: '.(string) $e);
}
if (! array_has($result, 'error')) {
$contact = Contact::where('account_id', Auth::user()->account_id)
->find($result['contact_id']);
$card = $this->prepareCard($contact);
return $card['etag'];
}
}
/**
* Deletes a card.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
return false;
}
/**
@@ -258,219 +400,4 @@ class MonicaCardDAVBackend extends AbstractBackend implements SyncSupport
{
return false;
}
private function prepareCard($contact)
{
if (! $contact) {
return;
}
try {
$vcard = (new ExportVCard())
->execute([
'account_id' => Auth::user()->account_id,
'contact_id' => $contact->id,
]);
} catch (\Exception $e) {
Log::debug(__CLASS__.' prepareCard: '.(string) $e);
}
$carddata = $vcard->serialize();
return [
'id' => $contact->hashID(),
'uri' => $this->encodeUri($contact),
'carddata' => $carddata,
'etag' => '"'.md5($carddata).'"',
'lastmodified' => $contact->updated_at->timestamp,
];
}
private function encodeUri($contact)
{
return urlencode($contact->uuid.'.vcf');
}
private function decodeUri($uri)
{
return pathinfo(urldecode($uri), PATHINFO_FILENAME);
}
/**
* Returns the contact for the specific uri.
*
* @param string $uri
* @return Contact
*/
private function getContact($uri)
{
try {
return Contact::where([
'account_id' => Auth::user()->account_id,
'uuid' => $this->decodeUri($uri),
])->first();
} catch (\Exception $e) {
return;
}
}
/**
* Returns the collection of all active contacts.
*
* @return \Illuminate\Support\Collection
*/
private function getContacts()
{
return Auth::user()->account
->contacts()
->real()
->active()
->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->getContacts();
return $contacts->map(function ($contact) {
return $this->prepareCard($contact);
});
}
/**
* Returns a specific card.
*
* The same set of prope
* @param mixed $addressBookId
* @param string $cardUri
* @return array
*/
public function getCard($addressBookId, $cardUri)
{
$contact = $this->getContact($cardUri);
return $this->prepareCard($contact);
}
/**
* 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->importCard(null, $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 $cardData
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData)
{
return $this->importCard($cardUri, $cardData);
}
private function importCard($cardUri, $cardData)
{
$contact_id = null;
if ($cardUri) {
$contact = $this->getContact($cardUri);
if ($contact) {
$contact_id = $contact->id;
}
}
try {
$result = (new ImportVCard(Auth::user()->account_id))
->execute([
'contact_id' => $contact_id,
'entry' => $cardData,
'behaviour' => ImportVCard::BEHAVIOUR_REPLACE,
]);
} catch (\Exception $e) {
Log::debug(__CLASS__.' importCard: '.(string) $e);
}
if (! array_has($result, 'error')) {
$contact = Contact::where('account_id', Auth::user()->account_id)
->find($result['contact_id']);
$card = $this->prepareCard($contact);
return $card['etag'];
}
}
/**
* Deletes a card.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
return false;
}
}
@@ -0,0 +1,35 @@
<?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 $uri
* @return mixed
*/
public function getObjectUuid($uuid);
/**
* Returns the collection of objects.
*
* @return \Illuminate\Support\Collection
*/
public function getObjects();
/**
* Returns the extension for this backend.
*
* @return string
*/
public function getExtension();
}
@@ -0,0 +1,251 @@
<?php
namespace App\Http\Controllers\DAV\Backend;
use Illuminate\Support\Str;
use App\Models\User\SyncToken;
use App\Models\Contact\Contact;
use Illuminate\Support\Facades\Auth;
trait SyncDAVBackend
{
/**
* 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.
*
* @return SyncToken
*/
protected function getCurrentSyncToken()
{
$tokens = SyncToken::where([
'account_id' => Auth::user()->account_id,
'user_id' => Auth::user()->id,
'name' => $this->backendUri(),
])
->orderBy('created_at')
->get();
if ($tokens->count() <= 0) {
$token = $this->createSyncToken();
} else {
$token = $tokens->last();
if ($token->timestamp < $this->getLastModified()) {
$token = $this->createSyncToken();
}
}
return $token;
}
/**
* Get SyncToken by token id.
*
* @return SyncToken
*/
protected function getSyncToken($syncToken)
{
return SyncToken::where([
'account_id' => Auth::user()->account_id,
'user_id' => Auth::user()->id,
'name' => $this->backendUri(),
])
->find($syncToken);
}
/**
* Create a token.
*
* @return SyncToken|null
*/
private function createSyncToken()
{
$max = $this->getLastModified();
if ($max) {
return SyncToken::create([
'account_id' => Auth::user()->account_id,
'user_id' => Auth::user()->id,
'name' => $this->backendUri(),
'timestamp' => $max,
]);
}
}
/**
* Create a token with now timestamp.
*
* @return SyncToken
*/
private function createSyncTokenNow()
{
return SyncToken::create([
'account_id' => Auth::user()->account_id,
'user_id' => Auth::user()->id,
'name' => $this->backendUri(),
'timestamp' => now(),
]);
}
/**
* Returns the last modification date.
*
* @return \Carbon\Carbon
*/
public function getLastModified()
{
return $this->getObjects()
->max('updated_at');
}
/**
* 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 $syncToken
* @return array
*/
public function getChanges($syncToken)
{
$token = null;
$timestamp = null;
if (! empty($syncToken)) {
$token = $this->getSyncToken($syncToken);
if (is_null($token)) {
// syncToken is not recognized
return;
}
$timestamp = $token->timestamp;
} else {
$token = $this->createSyncTokenNow();
$timestamp = null;
}
$objs = $this->getObjects();
$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;
});
return [
'syncToken' => $token->id,
'added' => $added->map(function ($obj) {
return $this->encodeUri($obj);
})->toArray(),
'modified' => $modified->map(function ($obj) {
return $this->encodeUri($obj);
})->toArray(),
'deleted' => [],
];
}
protected function encodeUri($obj)
{
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)
{
return pathinfo(urldecode($uri), PATHINFO_FILENAME);
}
/**
* Returns the contact for the specific uri.
*
* @param string $uri
* @return mixed
*/
public function getObject($uri)
{
try {
return $this->getObjectUuid($this->decodeUri($uri));
} catch (\Exception $e) {
// Object not found
}
}
/**
* Returns the object for the specific uuid.
*
* @param string $uri
* @return mixed
*/
abstract public function getObjectUuid($uuid);
/**
* Returns the collection of objects.
*
* @return \Illuminate\Support\Collection
*/
abstract public function getObjects();
abstract public function getExtension();
}
@@ -1,13 +1,13 @@
<?php
namespace App\Models\CardDAV\Backends;
namespace App\Http\Controllers\DAV\DAVACL;
use Sabre\DAV;
use Illuminate\Support\Facades\Auth;
use Sabre\DAV\Server as SabreServer;
use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
class MonicaPrincipalBackend extends AbstractBackend
class PrincipalBackend extends AbstractBackend
{
/**
* This is the prefix that will be used to generate principal urls.
@@ -1,34 +1,36 @@
<?php
namespace App\Http\Controllers\CardDAV;
namespace App\Http\Controllers\DAV;
use Illuminate\Http\Request;
use Sabre\CalDAV\CalendarRoot;
use Sabre\CalDAV\ICSExportPlugin;
use Sabre\CardDAV\VCFExportPlugin;
use Illuminate\Support\Facades\App;
use App\Http\Controllers\Controller;
use Sabre\DAV\Server as SabreServer;
use Sabre\DAVACL\Plugin as AclPlugin;
use Sabre\DAVACL\PrincipalCollection;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\DAV\Auth\Plugin as AuthPlugin;
use Sabre\DAV\Sync\Plugin as SyncPlugin;
use Barryvdh\Debugbar\Facade as Debugbar;
use Sabre\CardDAV\Plugin as CardDAVPlugin;
use App\Models\CardDAV\MonicaAddressBookRoot;
use App\Http\Controllers\DAV\Auth\AuthBackend;
use Sabre\DAV\Browser\Plugin as BrowserPlugin;
use App\Models\CardDAV\Backends\MonicaAuthBackend;
use App\Models\CardDAV\Backends\MonicaCardDAVBackend;
use App\Models\CardDAV\Backends\MonicaPrincipalBackend;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
use App\Http\Controllers\DAV\Backend\CalDAV\CalDAVBackend;
use App\Http\Controllers\DAV\Backend\CardDAV\CardDAVBackend;
use App\Http\Controllers\DAV\Backend\CardDAV\AddressBookRoot;
class CardDAVController extends Controller
class DAVController extends Controller
{
private const BASE_URI = '/carddav/';
/**
* Display the specified resource.
*/
public function init(Request $request)
{
if (! config('carddav.enabled')) {
if (! config('dav.enabled')) {
abort(404);
}
@@ -47,12 +49,14 @@ class CardDAVController extends Controller
private function getNodes() : array
{
// Initiate custom backends for link between Sabre and Monica
$principalBackend = new MonicaPrincipalBackend(); // User rights
$carddavBackend = new MonicaCardDAVBackend(); // Contacts
$principalBackend = new PrincipalBackend(); // User rights
$carddavBackend = new CardDAVBackend(); // Contacts
$caldavBackend = new CalDAVBackend(); // Calendar
return [
new PrincipalCollection($principalBackend),
new MonicaAddressBookRoot($principalBackend, $carddavBackend),
new AddressBookRoot($principalBackend, $carddavBackend),
new CalendarRoot($principalBackend, $caldavBackend),
];
}
@@ -65,7 +69,7 @@ class CardDAVController extends Controller
$server->sapi = new SapiServerMock();
// Base Uri of carddav
$server->setBaseUri(self::BASE_URI);
$server->setBaseUri($this->getBaseUri());
// Set Url with trailing slash
$server->httpRequest->setUrl($this->fullUrl($request));
@@ -102,11 +106,16 @@ class CardDAVController extends Controller
private function addPlugins(SabreServer $server)
{
// Authentication backend
$authBackend = new MonicaAuthBackend();
$authBackend = new AuthBackend();
$server->addPlugin(new AuthPlugin($authBackend, 'SabreDAV'));
// CardDAV plugin
$server->addPlugin(new CardDAVPlugin());
$server->addPlugin(new VCFExportPlugin());
// CalDAV plugin
$server->addPlugin(new CalDAVPlugin());
$server->addPlugin(new ICSExportPlugin());
// Sync Plugin - rfc6578
$server->addPlugin(new SyncPlugin());
@@ -117,9 +126,6 @@ class CardDAVController extends Controller
$aclPlugin->hideNodesFromListings = true;
$server->addPlugin($aclPlugin);
// VCFExport
$server->addPlugin(new VCFExportPlugin());
// In local environment add browser plugin
if (App::environment('local')) {
$server->addPlugin(new BrowserPlugin());
@@ -139,4 +145,9 @@ class CardDAVController extends Controller
return response($content, $status)
->withHeaders($headers);
}
private function getBaseUri()
{
return str_start(str_finish(config('dav.path'), '/'), '/');
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\CardDAV;
namespace App\Http\Controllers\DAV;
use Sabre\HTTP\Sapi;
use Sabre\HTTP\ResponseInterface;
+1
View File
@@ -23,6 +23,7 @@ class SyncToken extends Model
protected $fillable = [
'account_id',
'user_id',
'name',
'timestamp',
];
}
+6 -6
View File
@@ -71,7 +71,7 @@ class RouteServiceProvider extends ServiceProvider
$this->mapApiRoutes($router);
$this->mapWebRoutes($router);
$this->mapOAuthRoutes($router);
$this->mapCardDAVRoutes($router);
$this->mapDAVRoutes($router);
$this->mapSpecialRoutes($router);
}
@@ -88,7 +88,7 @@ class RouteServiceProvider extends ServiceProvider
$router->group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
], function () {
require base_path('routes/web.php');
});
}
@@ -128,20 +128,20 @@ class RouteServiceProvider extends ServiceProvider
}
/**
* Define the "carddav" routes for the application.
* Define the DAV routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapCardDAVRoutes(Router $router)
protected function mapDAVRoutes(Router $router)
{
$router->group([
'prefix' => 'carddav',
'prefix' => config('dav.path'),
'middleware' => 'api',
'namespace' => $this->namespace,
], function () {
require base_path('routes/carddav.php');
require base_path('routes/dav.php');
});
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace App\Services\VCalendar;
use Illuminate\Support\Str;
use App\Models\Contact\Task;
use App\Services\BaseService;
use Illuminate\Support\Facades\Auth;
use Sabre\VObject\Component\VCalendar;
class ExportTask extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'task_id' => 'required|integer|exists:tasks,id',
];
}
/**
* Export one VCalendar.
*
* @param array $data
* @return VCalendar
*/
public function execute(array $data) : VCalendar
{
$this->validate($data);
$task = Task::where('account_id', $data['account_id'])
->findOrFail($data['task_id']);
return $this->export($task);
}
/**
* @param Task $task
* @return VCalendar
*/
private function export(Task $task) : VCalendar
{
// The standard for most of these fields can be found on https://tools.ietf.org/html/rfc5545
if (! $task->uuid) {
$task->forceFill([
'uuid' => Str::uuid(),
])->save();
}
// Basic information
$vcal = new VCalendar([
//'UID' => $task->uuid,
]);
$this->exportTimezone($vcal);
$this->exportVTodo($task, $vcal);
return $vcal;
}
/**
* @param VCalendar $vcard
*/
private function exportTimezone(VCalendar $vcal)
{
$vcal->add('VTIMEZONE', [
'TZID' => Auth::user()->timezone,
]);
}
/**
* @param Task $task
* @param VCalendar $vcard
*/
private function exportVTodo(Task $task, VCalendar $vcal)
{
$contact = $task->contact;
$vcal->add('VTODO', [
'UID' => $task->uuid,
'SUMMARY' => $task->title,
]);
if ($task->created_at) {
$vcal->VTODO->add('CREATED', $task->created_at);
}
if (! empty($task->description)) {
$vcal->VTODO->add('DESCRIPTION', $task->description);
}
if ($contact) {
$vcal->VTODO->add('ATTACH', route('people.show', $contact));
}
if ($task->completed) {
$vcal->VTODO->add('STATUS', 'COMPLETED');
}
if ($task->completed_at) {
$vcal->VTODO->add('COMPLETED', $task->completed_at);
}
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Services\VCalendar;
use App\Helpers\DateHelper;
use Illuminate\Support\Str;
use App\Services\BaseService;
use Sabre\VObject\Component\VEvent;
use App\Models\Instance\SpecialDate;
use Illuminate\Support\Facades\Auth;
use Sabre\VObject\Component\VCalendar;
class ExportVCalendar extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'special_date_id' => 'required|integer|exists:special_dates,id',
];
}
/**
* Export one VCalendar.
*
* @param array $data
* @return VCalendar
*/
public function execute(array $data) : VCalendar
{
$this->validate($data);
$date = SpecialDate::where('account_id', $data['account_id'])
->findOrFail($data['special_date_id']);
return $this->export($date);
}
/**
* @param SpecialDate $date
* @return VCalendar
*/
private function export(SpecialDate $date) : VCalendar
{
// The standard for most of these fields can be found on https://tools.ietf.org/html/rfc5545
if (! $date->uuid) {
$date->forceFill([
'uuid' => Str::uuid(),
])->save();
}
// Basic information
$vcal = new VCalendar([
'UID' => $date->uuid,
]);
$this->exportTimezone($vcal);
$this->exportBirthday($date, $vcal);
return $vcal;
}
/**
* @param VCalendar $vcard
* @return void
*/
private function exportTimezone(VCalendar $vcal)
{
$vcal->add('VTIMEZONE', [
'TZID' => Auth::user()->timezone,
]);
}
/**
* @param SpecialDate $date
* @param VCalendar $vcard
* @return void
*/
private function exportBirthday(SpecialDate $date, VCalendar $vcal)
{
$contact = $date->contact;
$name = $contact->name;
$vcal->add('VEVENT', [
'UID' => $date->uuid,
'SUMMARY' => trans('people.reminders_birthday', ['name' => $name]),
'DTSTART' => $date->date->format('Ymd'),
'DTEND' => $date->date->addDays(1)->format('Ymd'),
'RRULE' => 'FREQ=YEARLY',
'CREATED' => DateHelper::parseDateTime($date->created_at, Auth::user()->timezone),
'DTSTAMP' => DateHelper::parseDateTime($date->created_at),
'DESCRIPTION' => trans('mail.footer_contact_info2_link', [
'name' => $name,
'url' => route('people.show', $contact),
]),
]);
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
namespace App\Services\VCalendar;
use Ramsey\Uuid\Uuid;
use App\Traits\DAVFormat;
use Sabre\VObject\Reader;
use App\Helpers\DateHelper;
use App\Models\Contact\Task;
use App\Services\BaseService;
use Sabre\VObject\ParseException;
use Sabre\VObject\Component\VCalendar;
class ImportTask extends BaseService
{
use DAVFormat;
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'task_id' => 'nullable|integer|exists:tasks,id',
'entry' => 'required|string',
];
}
/**
* Export one VCalendar.
*
* @param array $data
* @return array
*/
public function execute(array $data) : array
{
$this->validate($data);
if (array_has($data, 'task_id') && ! is_null($data['task_id'])) {
$task = Task::where('account_id', $data['account_id'])
->findOrFail($data['task_id']);
} else {
$task = new Task(['account_id' => $data['account_id']]);
}
return $this->process($data, $task);
}
/**
* Import one VCalendar.
*
* @param array $data
* @param Task $task
* @return array
*/
private function process(array $data, Task $task) : array
{
$entry = $this->getEntry($data);
if (! $entry) {
return [
'error' => '0',
];
}
if (! $this->canImportCurrentEntry($entry)) {
return [
'error' => '1',
];
}
$task = $this->importEntry($task, $entry);
return [
'task_id' => $task->id,
];
}
/**
* Check whether this entry contains a VTODO. If not, it
* can not be imported.
*
* @param VCalendar $entry
* @return bool
*/
private function canImportCurrentEntry(VCalendar $entry) : bool
{
return ! is_null($entry->VTODO);
}
/**
* Create the Task object matching the current entry.
*
* @param Task $task
* @param VCalendar $entry
* @return Task
*/
private function importEntry($task, VCalendar $entry): Task
{
$this->importUid($task, $entry);
$this->importSummary($task, $entry);
$this->importCompleted($task, $entry);
$this->importTimestamp($task, $entry);
$task->save();
return $task;
}
/**
* @param array $data
* @return VCalendar
*/
private function getEntry($data) : VCalendar
{
try {
$entry = Reader::read($data['entry'], Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES);
} catch (ParseException $e) {
return null;
}
return $entry;
}
/**
* Import uid.
*
* @param Task $contact
* @param VCalendar $entry
* @return void
*/
private function importUid(Task $task, VCalendar $entry): void
{
if (empty($task->uuid) && Uuid::isValid((string) $entry->VTODO->UID)) {
$task->uuid = (string) $entry->VTODO->UID;
}
}
/**
* Import uid.
*
* @param Task $contact
* @param VCalendar $entry
* @return void
*/
private function importTimestamp(Task $task, VCalendar $entry): void
{
if (empty($task->created_at) && $entry->VTODO->CREATED) {
$task->created_at = DateHelper::parseDateTime((string) $entry->VTODO->CREATED);
}
}
/**
* @param Task $task
* @param VCalendar $vcard
*/
private function importSummary(Task $task, VCalendar $entry)
{
$task->title = $this->formatValue($entry->VTODO->SUMMARY);
if ($entry->VTODO->DESCRIPTION) {
$task->description = $this->formatValue($entry->VTODO->DESCRIPTION);
}
}
/**
* @param Task $task
* @param VCalendar $vcard
*/
private function importCompleted(Task $task, VCalendar $entry)
{
$task->completed = ((string) $entry->VTODO->STATUS) == 'COMPLETED';
if (! $task->completed) {
$task->completed_at = null;
} elseif ($entry->VTODO->COMPLETED) {
$task->completed_at = DateHelper::parseDateTime((string) $entry->VTODO->COMPLETED);
}
}
}
+2 -2
View File
@@ -19,12 +19,12 @@ class ExportVCard extends BaseService
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'contact_id' => 'nullable|integer',
'contact_id' => 'required|integer|exists:contacts,id',
];
}
/**
* Import one VCard.
* Export one VCard.
*
* @param array $data
* @return VCard
+7 -12
View File
@@ -2,6 +2,8 @@
namespace App\Services\VCard;
use Ramsey\Uuid\Uuid;
use App\Traits\DAVFormat;
use Sabre\VObject\Reader;
use App\Helpers\DateHelper;
use App\Helpers\VCardHelper;
@@ -21,6 +23,8 @@ use App\Services\Contact\Reminder\CreateReminder;
class ImportVCard extends BaseService
{
use DAVFormat;
public const BEHAVIOUR_ADD = 'behaviour_add';
public const BEHAVIOUR_REPLACE = 'behaviour_replace';
@@ -345,17 +349,6 @@ class ImportVCard extends BaseService
])->first();
}
/**
* Formats and returns a string for the contact.
*
* @param null|string $value
* @return null|string
*/
private function formatValue($value)
{
return ! empty($value) ? str_replace('\;', ';', trim((string) $value)) : null;
}
/**
* Create the Contact object matching the current entry.
*
@@ -507,7 +500,9 @@ class ImportVCard extends BaseService
*/
private function importUid(Contact $contact, VCard $entry): void
{
$contact->uuid = (string) $entry->UID;
if (empty($contact->uuid) && Uuid::isValid((string) $entry->UID)) {
$contact->uuid = (string) $entry->UID;
}
}
/**
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Traits;
trait DAVFormat
{
/**
* Formats and returns a string for DAV Card/Cal.
*
* @param null|string $value
* @return null|string
*/
private function formatValue($value)
{
return ! empty($value) ? str_replace('\;', ';', trim((string) $value)) : null;
}
}
-11
View File
@@ -1,11 +0,0 @@
<?php
return [
/*
* Carddav enabled
*/
'enabled' => (bool) env('CARDDAV_ENABLED', false),
];
+25
View File
@@ -0,0 +1,25 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enable DAV feature
|--------------------------------------------------------------------------
|
| Enable or disable DAV completely.
|
*/
'enabled' => (bool) env('DAV_ENABLED', (bool) env('CARDDAV_ENABLED', false)),
/*
|--------------------------------------------------------------------------
| DAV Path
|--------------------------------------------------------------------------
|
| Path for all DAV requests.
|
*/
'path' => 'dav',
];
+1
View File
@@ -168,6 +168,7 @@ $factory->define(App\Models\Contact\Task::class, function (Faker\Generator $fake
'description' => $faker->word,
'completed' => 0,
'created_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()),
'uuid' => Str::uuid(),
];
});
@@ -0,0 +1,56 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddDavUuid extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('contacts', function (Blueprint $table) {
$table->index(['account_id', 'uuid']);
});
Schema::table('special_dates', function (Blueprint $table) {
$table->uuid('uuid')->after('contact_id')->nullable();
$table->index(['account_id', 'uuid']);
});
Schema::table('tasks', function (Blueprint $table) {
$table->uuid('uuid')->after('contact_id')->nullable();
$table->index(['account_id', 'uuid']);
});
Schema::table('synctoken', function (Blueprint $table) {
$table->string('name')->after('user_id')->default('contacts');
$table->index(['account_id', 'user_id', 'name']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('contacts', function (Blueprint $table) {
$table->dropIndex(['account_id', 'uuid']);
});
Schema::table('special_dates', function (Blueprint $table) {
$table->dropIndex(['account_id', 'uuid']);
$table->dropColumn('uuid');
});
Schema::table('tasks', function (Blueprint $table) {
$table->dropIndex(['account_id', 'uuid']);
$table->dropColumn('uuid');
});
Schema::table('synctoken', function (Blueprint $table) {
$table->dropIndex(['account_id', 'user_id', 'name']);
$table->dropColumn('name');
});
}
}
+5 -1
View File
@@ -5,9 +5,13 @@ location / {
location @rewriteapp {
# Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers)
rewrite .well-known/carddav /carddav/ permanent;
rewrite .well-known/carddav /dav/ permanent;
rewrite .well-known/caldav /dav/ permanent;
rewrite .well-known/security.txt$ /security.txt permanent;
# Old carddav url
rewrite carddav/(.*) /dav/$1 permanent;
# rewrite all to app.php
rewrite ^(.*)$ /index.php/$1 last;
}
+1 -1
View File
@@ -42,6 +42,6 @@
<env name="QUEUE_DRIVER" value="sync"/>
<env name="APP_DEFAULT_LOCALE" value="en"/>
<env name="LOG_CHANNEL" value="testing"/>
<env name="CARDDAV_ENABLED" value="true"/>
<env name="DAV_ENABLED" value="true"/>
</php>
</phpunit>
+6 -2
View File
@@ -11,13 +11,17 @@
# Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers)
RewriteCond %{REQUEST_URI} .well-known/carddav
RewriteRule ^ /carddav/ [L,R=301]
RewriteCond %{REQUEST_URI} .well-known/caldav
RewriteRule ^ /dav/ [L,R=301]
RewriteCond %{REQUEST_URI} .well-known/security.txt
RewriteRule ^ /security.txt [L,R=301]
# old carddav url
RewriteCond %{REQUEST_URI} /carddav/(.+)
RewriteRule ^ /dav/%1 [L,R=301]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !carddav/*
RewriteCond %{REQUEST_FILENAME} !dav/*
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/app.js": "/js/app.js?id=0b2b2062ed908133529e",
"/js/app.js": "/js/app.js?id=e93e1335c28da7ce937c",
"/css/app-ltr.css": "/css/app-ltr.css?id=9d5c67e058757ccc7b5f",
"/css/app-rtl.css": "/css/app-rtl.css?id=26961bfc239e7ce6ea27",
"/css/stripe.css": "/css/stripe.css?id=2de4e0ce557016a0327e",
+1
View File
@@ -9,6 +9,7 @@ return [
'comment' => 'Comment: :comment',
'footer_contact_info' => 'Add, view, complete, and change information about this contact:',
'footer_contact_info2' => 'See :names profile',
'footer_contact_info2_link' => 'See :names profile: :url',
'notification_subject_line' => 'You have an upcoming event',
'notification_description' => 'In :count days (on :date), the following event will happen:',
+1 -1
View File
@@ -23,5 +23,5 @@ $verbs = [
Illuminate\Routing\Router::$verbs = $verbs;
Route::group(['middleware' => ['auth.tokenonbasic', 'limitations']], function () use ($verbs) {
Route::match($verbs, '{path?}', 'CardDAV\\CardDAVController@init')->where('path', '(.)*');
Route::match($verbs, '{path?}', 'DAV\\DAVController@init')->where('path', '(.)*');
});
+1 -1
View File
@@ -14,4 +14,4 @@ QUEUE_DRIVER=sync
LOG_CHANNEL=testing
MFA_ENABLED=true
CARDDAV_ENABLED=true
DAV_ENABLED=true
+1 -1
View File
@@ -14,4 +14,4 @@ QUEUE_DRIVER=sync
LOG_CHANNEL=testing
MFA_ENABLED=true
CARDDAV_ENABLED=true
DAV_ENABLED=true
-446
View File
@@ -1,446 +0,0 @@
<?php
namespace Tests\Api\Carddav;
use Tests\ApiTestCase;
use App\Models\User\SyncToken;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class CarddavServerTest extends ApiTestCase
{
use DatabaseTransactions;
/**
* @group carddav
*/
public function test_carddav_propfind_base()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/carddav');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/carddav/</d:href>');
$response->assertSee('<d:response><d:href>/carddav/principals/</d:href>');
$response->assertSee('<d:response><d:href>/carddav/addressbooks/</d:href>');
}
/**
* @group carddav
*/
public function test_carddav_propfind_principals()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/carddav/principals');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/carddav/principals/</d:href>');
$response->assertSee("<d:response><d:href>/carddav/principals/{$user->email}/</d:href>");
}
/**
* @group carddav
*/
public function test_carddav_propfind_principals_user()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/carddav/principals/{$user->email}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/carddav/principals/{$user->email}/</d:href>");
}
/**
* @group carddav
*/
public function test_carddav_ensure_browser_plugin_not_enabled()
{
$user = $this->signin();
$response = $this->call('GET', '/carddav');
$response->assertStatus(501);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('There was no plugin in the system that was willing to handle this GET method. Enable the Browser plugin to get a better result here.');
}
/**
* @group carddav
*/
public function test_carddav_propfind_addressbooks()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/carddav/addressbooks');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/carddav/addressbooks/</d:href>');
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/</d:href>");
}
/**
* @group carddav
*/
public function test_carddav_propfind_addressbooks_user()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/carddav/addressbooks/{$user->email}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/</d:href>");
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/contacts/</d:href>");
}
/**
* @group carddav
*/
public function test_carddav_propfind_contacts()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/carddav/addressbooks/{$user->email}/contacts");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/contacts/</d:href>");
$contactId = urlencode($contact->uuid);
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/contacts/{$contactId}.vcf</d:href>");
}
public function test_carddav_propfind_contacts_with_props()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/carddav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => 0,
],
'<d:propfind xmlns:d="DAV:">
<d:prop>
<d:displayname />
</d:prop>
</d:propfind>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav">'.
'<d:response>'.
"<d:href>/carddav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:displayname>{$user->name}</d:displayname>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus');
}
/**
* @group carddav
*/
public function test_carddav_propfind_one_contact()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/carddav/addressbooks/{$user->email}/contacts/{$contact->uuid}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact->uuid}</d:href>");
}
/**
* @group carddav
*/
public function test_carddav_propfind_groupmemberset()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/carddav/principals/{$user->email}/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
'<propfind xmlns="DAV:"
xmlns:CAL="urn:ietf:params:xml:ns:caldav"
xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<prop>
<CARD:addressbook-home-set />
<group-member-set />
</prop>
</propfind>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav">'.
'<d:response>'.
"<d:href>/carddav/principals/{$user->email}/</d:href>".
'<d:propstat>'.
'<d:prop>'.
'<card:addressbook-home-set>'.
"<d:href>/carddav/addressbooks/{$user->email}/</d:href>".
'</card:addressbook-home-set>'.
'<d:group-member-set>'.
"<d:href>/carddav/principals/{$user->email}/</d:href>".
'</d:group-member-set>'.
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
/**
* @group carddav
*/
public function test_carddav_report_propertysearch()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('REPORT', '/carddav/principals/', [], [], [],
[
'HTTP_DEPTH' => '0',
'content-type' => 'application/xml; charset=utf-8',
],
"<principal-property-search xmlns=\"DAV:\">
<property-search>
<match>{$user->name}</match>
<prop>
<displayname/>
</prop>
</property-search>
<prop>
<displayname/>
</prop>
</principal-property-search>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav">'.
'<d:response>'.
"<d:href>/carddav/principals/{$user->email}/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:displayname>{$user->name}</d:displayname>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
public function test_carddav_getctag()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/carddav/addressbooks/{$user->email}/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/'>
<prop>
<cs:getctag />
<sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
['account_id', $user->account_id],
['user_id', $user->id],
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:response>'.
"<d:href>/carddav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<x1:getctag xmlns:x1=\"http://calendarserver.org/ns/\">http://sabre.io/ns/sync/{$token->id}</x1:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_carddav_getctag_contacts()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/carddav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => '0',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/'>
<prop>
<cs:getctag />
<sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
['account_id', $user->account_id],
['user_id', $user->id],
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:response>'.
"<d:href>/carddav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<x1:getctag xmlns:x1=\"http://calendarserver.org/ns/\">http://sabre.io/ns/sync/{$token->id}</x1:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_carddav_sync_collection_with_token()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$token = factory(SyncToken::class)->create([
'account_id' => $user->account->id,
'user_id' => $user->id,
'timestamp' => \App\Helpers\DateHelper::parseDateTime(now()),
]);
$response = $this->call('REPORT', "/carddav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token>http://sabre.io/ns/sync/{$token->id}</sync-token>
<sync-level>1</sync-level>
<prop>
<getetag/>
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\">
<d:response>
<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;");
$response->assertSee("&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
public function test_carddav_sync_collection_init()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('REPORT', "/carddav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token />
<sync-level>1</sync-level>
<prop>
<getetag/>
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
['account_id', $user->account_id],
['user_id', $user->id],
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\">
<d:response>
<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;");
$response->assertSee("&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
}
+283
View File
@@ -0,0 +1,283 @@
<?php
namespace Tests\Api\DAV;
use Tests\ApiTestCase;
use Illuminate\Support\Str;
use App\Models\User\SyncToken;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class CalDAVBirthdaysTest extends ApiTestCase
{
use DatabaseTransactions, CardEtag;
/**
* @group dav
*/
public function test_caldav_birthdays_propfind()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/birthdays/</d:href>");
$specialDate->refresh();
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics</d:href>");
}
public function test_caldav_birthdays_propfind_with_props()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays/", [], [], [],
[
'HTTP_DEPTH' => 0,
],
'<d:propfind xmlns:d="DAV:">
<d:prop>
<d:displayname />
</d:prop>
</d:propfind>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/birthdays/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:displayname>{$user->name}</d:displayname>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus');
}
/**
* @group dav
*/
public function test_caldav_birthdays_propfind_one_birthday()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics</d:href>");
}
public function test_caldav_birthdays_getctag()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:s='http://sabredav.org/ns'>
<prop>
<cs:getctag />
<sync-token />
<s:sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'birthdays',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/calendars/{$user->email}/birthdays/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:getctag>http://sabre.io/ns/sync/{$token->id}</cs:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
"<s:sync-token>http://sabre.io/ns/sync/{$token->id}</s:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_caldav_birthdays_getctag_birthday()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays/", [], [], [],
[
'HTTP_DEPTH' => '0',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:s='http://sabredav.org/ns'>
<prop>
<cs:getctag />
<sync-token />
<s:sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'birthdays',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/calendars/{$user->email}/birthdays/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:getctag>http://sabre.io/ns/sync/{$token->id}</cs:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
"<s:sync-token>http://sabre.io/ns/sync/{$token->id}</s:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_caldav_birthdays_sync_collection_with_token()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$token = factory(SyncToken::class)->create([
'account_id' => $user->account->id,
'user_id' => $user->id,
'name' => 'birthdays',
'timestamp' => \App\Helpers\DateHelper::parseDateTime(now()),
]);
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token>http://sabre.io/ns/sync/{$token->id}</sync-token>
<sync-level>1</sync-level>
<prop>
<getetag />
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\">
<d:response>
<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;{$this->getEtag($specialDate)}&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
public function test_caldav_birthdays_sync_collection_init()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token />
<sync-level>1</sync-level>
<prop>
<getetag />
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'birthdays',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\">
<d:response>
<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;{$this->getEtag($specialDate)}&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
}
+292
View File
@@ -0,0 +1,292 @@
<?php
namespace Tests\Api\DAV;
use Tests\ApiTestCase;
use App\Models\Contact\Task;
use App\Models\User\SyncToken;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class CalDAVTasksTest extends ApiTestCase
{
use DatabaseTransactions, CardEtag;
/**
* @group dav
*/
public function test_caldav_tasks_propfind()
{
$user = $this->signin();
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => null,
'created_at' => now(),
'updated_at' => now(),
]);
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/tasks/</d:href>");
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics</d:href>");
}
public function test_caldav_tasks_propfind_with_props()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks/", [], [], [],
[
'HTTP_DEPTH' => 0,
],
'<d:propfind xmlns:d="DAV:">
<d:prop>
<d:displayname />
</d:prop>
</d:propfind>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/tasks/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:displayname>{$user->name}</d:displayname>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus');
}
/**
* @group dav
*/
public function test_caldav_tasks_propfind_one_task()
{
$user = $this->signin();
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'created_at' => now(),
'updated_at' => now(),
]);
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics</d:href>");
}
public function test_caldav_tasks_getctag()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => $contact->id,
]);
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:s='http://sabredav.org/ns'>
<prop>
<cs:getctag />
<sync-token />
<s:sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'tasks',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/calendars/{$user->email}/tasks/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:getctag>http://sabre.io/ns/sync/{$token->id}</cs:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
"<s:sync-token>http://sabre.io/ns/sync/{$token->id}</s:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_caldav_tasks_getctag_task()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => $contact->id,
'created_at' => now(),
'updated_at' => now(),
]);
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks/", [], [], [],
[
'HTTP_DEPTH' => '0',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:s='http://sabredav.org/ns'>
<prop>
<cs:getctag />
<sync-token />
<s:sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'tasks',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/calendars/{$user->email}/tasks/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:getctag>http://sabre.io/ns/sync/{$token->id}</cs:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
"<s:sync-token>http://sabre.io/ns/sync/{$token->id}</s:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_caldav_tasks_sync_collection_with_token()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => $contact->id,
'created_at' => now(),
'updated_at' => now(),
]);
$token = factory(SyncToken::class)->create([
'account_id' => $user->account->id,
'user_id' => $user->id,
'name' => 'tasks',
'timestamp' => \App\Helpers\DateHelper::parseDateTime(now()),
]);
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token>http://sabre.io/ns/sync/{$token->id}</sync-token>
<sync-level>1</sync-level>
<prop>
<getetag />
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\">
<d:response>
<d:href>/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;{$this->getEtag($task)}&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
public function test_caldav_tasks_sync_collection_init()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => $contact->id,
'created_at' => now(),
'updated_at' => now(),
]);
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token />
<sync-level>1</sync-level>
<prop>
<getetag />
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'tasks',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\">
<d:response>
<d:href>/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;{$this->getEtag($task)}&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
}
+312
View File
@@ -0,0 +1,312 @@
<?php
namespace Tests\Api\DAV;
use Tests\ApiTestCase;
use App\Models\User\SyncToken;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class CardDAVTest extends ApiTestCase
{
use DatabaseTransactions, CardEtag;
/**
* @group dav
*/
public function test_carddav_propfind_addressbooks()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/dav/addressbooks');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/dav/addressbooks/</d:href>');
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/</d:href>");
}
/**
* @group dav
*/
public function test_carddav_propfind_addressbooks_user()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/</d:href>");
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/contacts/</d:href>");
}
/**
* @group dav
*/
public function test_carddav_propfind_contacts()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/contacts/</d:href>");
$contactId = urlencode($contact->uuid);
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/contacts/{$contactId}.vcf</d:href>");
}
public function test_carddav_propfind_contacts_with_props()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => 0,
],
'<d:propfind xmlns:d="DAV:">
<d:prop>
<d:displayname />
</d:prop>
</d:propfind>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:displayname>{$user->name}</d:displayname>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus');
}
/**
* @group dav
*/
public function test_carddav_propfind_one_contact()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>");
}
/**
* @group dav
*/
public function test_carddav_propfind_one_contact_without_extension()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}</d:href>");
}
public function test_carddav_getctag()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/'>
<prop>
<cs:getctag />
<sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'contacts',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:getctag>http://sabre.io/ns/sync/{$token->id}</cs:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_carddav_getctag_contacts()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => '0',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/'>
<prop>
<cs:getctag />
<sync-token />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'contacts',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:getctag>http://sabre.io/ns/sync/{$token->id}</cs:getctag>".
"<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_carddav_sync_collection_with_token()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$token = factory(SyncToken::class)->create([
'account_id' => $user->account->id,
'user_id' => $user->id,
'name' => 'contacts',
'timestamp' => \App\Helpers\DateHelper::parseDateTime(now()),
]);
$response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token>http://sabre.io/ns/sync/{$token->id}</sync-token>
<sync-level>1</sync-level>
<prop>
<getetag />
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\">
<d:response>
<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;{$this->getEtag($contact)}&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
public function test_carddav_sync_collection_init()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<sync-collection xmlns='DAV:'>
<sync-token />
<sync-level>1</sync-level>
<prop>
<getetag />
</prop>
</sync-collection>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$tokens = SyncToken::where([
'account_id' => $user->account_id,
'user_id' => $user->id,
'name' => 'contacts',
])->orderBy('created_at')->get();
$this->assertGreaterThan(0, $tokens->count());
$token = $tokens->last();
$response->assertSee("<d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\">
<d:response>
<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;{$this->getEtag($contact)}&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:sync-token>http://sabre.io/ns/sync/{$token->id}</d:sync-token>
</d:multistatus>");
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace Tests\Api\DAV;
use App\Models\Contact\Task;
use App\Models\Contact\Contact;
use App\Models\Instance\SpecialDate;
trait CardEtag
{
protected function getEtag($obj)
{
$data = '';
if ($obj instanceof Contact) {
$data = $this->getCard($obj, true);
} elseif ($obj instanceof SpecialDate) {
$data = $this->getCal($obj, true);
} elseif ($obj instanceof Task) {
$data = $this->getVTodo($obj, true);
}
return md5($data);
}
protected function getCard(Contact $contact, bool $realFormat = false): string
{
$url = route('people.show', $contact);
$sabreversion = \Sabre\VObject\Version::VERSION;
$data = "BEGIN:VCARD
VERSION:4.0
PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN
UID:{$contact->uuid}
SOURCE:{$url}
FN:{$contact->name}
N:{$contact->last_name};{$contact->first_name};{$contact->middle_name};;
GENDER:O;
END:VCARD
";
if ($realFormat) {
$data = mb_ereg_replace("\n", "\r\n", $data);
}
return $data;
}
protected function getCal(SpecialDate $specialDate, bool $realFormat = false): string
{
$contact = $specialDate->contact;
$url = route('people.show', $contact);
$description = "See {$contact->name}s profile: {$url}";
$description1 = mb_substr($description, 0, 61);
$description2 = mb_substr($description, 61);
$sabreversion = \Sabre\VObject\Version::VERSION;
$timestamp = $specialDate->created_at->format('Ymd\THis\Z');
$start = $specialDate->date->format('Ymd');
$end = $specialDate->date->addDays(1)->format('Ymd');
$data = "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN
CALSCALE:GREGORIAN
UID:{$specialDate->uuid}
BEGIN:VTIMEZONE
TZID:UTC
END:VTIMEZONE
BEGIN:VEVENT
UID:{$specialDate->uuid}
DTSTAMP:{$timestamp}
SUMMARY:Birthday of {$contact->name}
DTSTART:{$start}
DTEND:{$end}
RRULE:FREQ=YEARLY
CREATED:{$timestamp}
DESCRIPTION:{$description1}
{$description2}
END:VEVENT
END:VCALENDAR
";
if ($realFormat) {
$data = mb_ereg_replace("\n", "\r\n", $data);
}
return $data;
}
protected function getVTodo(Task $task, bool $realFormat = false): string
{
$sabreversion = \Sabre\VObject\Version::VERSION;
$timestamp = $task->created_at->format('Ymd\THis\Z');
$contact = $task->contact;
$data = "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:UTC
END:VTIMEZONE
BEGIN:VTODO
UID:{$task->uuid}
DTSTAMP:{$timestamp}
SUMMARY:{$task->title}
CREATED:{$timestamp}
DESCRIPTION:{$task->description}
";
if ($contact) {
$url = route('people.show', $contact);
$data .= "ATTACH:{$url}
";
}
$data .= 'END:VTODO
END:VCALENDAR
';
if ($realFormat) {
$data = mb_ereg_replace("\n", "\r\n", $data);
}
return $data;
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php
namespace Tests\Api\DAV;
use Tests\ApiTestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class DAVServerTest extends ApiTestCase
{
use DatabaseTransactions;
/**
* @group dav
*/
public function test_dav_propfind_base()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/dav');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/dav/</d:href>');
$response->assertSee('<d:response><d:href>/dav/principals/</d:href>');
$response->assertSee('<d:response><d:href>/dav/addressbooks/</d:href>');
$response->assertSee('<d:response><d:href>/dav/calendars/</d:href>');
}
/**
* @group dav
*/
public function test_dav_propfind_principals()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/dav/principals');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/dav/principals/</d:href>');
$response->assertSee("<d:response><d:href>/dav/principals/{$user->email}/</d:href>");
}
/**
* @group dav
*/
public function test_dav_propfind_principals_user()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/principals/{$user->email}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/principals/{$user->email}/</d:href>");
}
/**
* @group dav
*/
public function test_dav_ensure_browser_plugin_not_enabled()
{
$user = $this->signin();
$response = $this->call('GET', '/dav');
$response->assertStatus(501);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('There was no plugin in the system that was willing to handle this GET method. Enable the Browser plugin to get a better result here.');
}
/**
* @group dav
*/
public function test_carddav_propfind_groupmemberset()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/principals/{$user->email}/", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
'<propfind xmlns="DAV:"
xmlns:CAL="urn:ietf:params:xml:ns:caldav"
xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<prop>
<CARD:addressbook-home-set />
<group-member-set />
</prop>
</propfind>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/principals/{$user->email}/</d:href>".
'<d:propstat>'.
'<d:prop>'.
'<card:addressbook-home-set>'.
"<d:href>/dav/addressbooks/{$user->email}/</d:href>".
'</card:addressbook-home-set>'.
'<d:group-member-set>'.
"<d:href>/dav/principals/{$user->email}/</d:href>".
'</d:group-member-set>'.
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
/**
* @group dav
*/
public function test_carddav_report_propertysearch()
{
$user = $this->signin();
$response = $this->call('REPORT', '/dav/principals/', [], [], [],
[
'HTTP_DEPTH' => '0',
'content-type' => 'application/xml; charset=utf-8',
],
"<principal-property-search xmlns=\"DAV:\">
<property-search>
<match>{$user->name}</match>
<prop>
<displayname/>
</prop>
</property-search>
<prop>
<displayname/>
</prop>
</principal-property-search>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/principals/{$user->email}/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:displayname>{$user->name}</d:displayname>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
/**
* @group dav
*/
public function test_caldav_propfind()
{
$user = $this->signin();
$response = $this->call('PROPFIND', '/dav/calendars');
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response><d:href>/dav/calendars/</d:href>');
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/</d:href>");
}
/**
* @group dav
*/
public function test_caldav_propfind_calendars_user()
{
$user = $this->signin();
$response = $this->call('PROPFIND', "/dav/calendars/{$user->email}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/</d:href>");
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/birthdays/</d:href>");
$response->assertSee("<d:response><d:href>/dav/calendars/{$user->email}/tasks/</d:href>");
}
}
@@ -1,17 +1,17 @@
<?php
namespace Tests\Api\Carddav;
namespace Tests\Api\DAV;
use Tests\ApiTestCase;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class CarddavContactTest extends ApiTestCase
class VCardContactTest extends ApiTestCase
{
use DatabaseTransactions;
use DatabaseTransactions, CardEtag;
/**
* @group carddav
* @group dav
*/
public function test_carddav_get_one_contact()
{
@@ -20,25 +20,24 @@ class CarddavContactTest extends ApiTestCase
'account_id' => $user->account->id,
]);
$response = $this->get("/carddav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf");
$response = $this->get("/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf", [
'HTTP_ACCEPT' => 'text/vcard; version=4.0',
]);
$response->assertStatus(200);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('PRODID:-//Sabre//Sabre VObject');
$response->assertSee('FN:John Doe');
$response->assertSee('N:Doe;John;;;');
$response->assertSee('GENDER:O;');
$this->assertEquals($this->getCard($contact, true), $response->getContent());
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_put_one_contact()
{
$user = $this->signin();
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/single_vcard_stub.vcf", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/single_vcard_stub.vcf", [], [], [],
['content-type' => 'application/xml; charset=utf-8'],
"BEGIN:VCARD\nVERSION:4.0\nFN:John Doe\nN:Doe;John;;;\nEND:VCARD"
);
@@ -55,7 +54,7 @@ class CarddavContactTest extends ApiTestCase
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_update_existing_contact()
{
@@ -63,9 +62,8 @@ class CarddavContactTest extends ApiTestCase
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$filename = urlencode($contact->uuid.'.vcf');
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf", [], [], [],
['content-type' => 'application/xml; charset=utf-8'],
"BEGIN:VCARD\nVERSION:4.0\nFN:John Doex\nN:Doex;John;;;\nEND:VCARD"
);
@@ -82,7 +80,7 @@ class CarddavContactTest extends ApiTestCase
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_update_existing_contact_if_modified()
{
@@ -92,7 +90,7 @@ class CarddavContactTest extends ApiTestCase
]);
$filename = urlencode($contact->uuid.'.vcf');
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
[
'HTTP_If-Modified-Since' => $contact->updated_at->addDays(-1)->toRfc7231String(),
'content-type' => 'application/xml; charset=utf-8',
@@ -112,7 +110,7 @@ class CarddavContactTest extends ApiTestCase
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_update_existing_contact_if_modified_not_modified()
{
@@ -122,7 +120,7 @@ class CarddavContactTest extends ApiTestCase
]);
$filename = urlencode($contact->uuid.'.vcf');
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
[
'HTTP_If-Modified-Since' => $contact->updated_at->addDays(1)->toRfc7231String(),
'content-type' => 'application/xml; charset=utf-8',
@@ -142,7 +140,7 @@ class CarddavContactTest extends ApiTestCase
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_update_existing_contact_if_unmodified()
{
@@ -152,7 +150,7 @@ class CarddavContactTest extends ApiTestCase
]);
$filename = urlencode($contact->uuid.'.vcf');
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
[
'HTTP_If-Unmodified-Since' => $contact->updated_at->addDays(1)->toRfc7231String(),
'content-type' => 'application/xml; charset=utf-8',
@@ -172,7 +170,7 @@ class CarddavContactTest extends ApiTestCase
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_update_existing_contact_if_unmodified_error()
{
@@ -182,7 +180,7 @@ class CarddavContactTest extends ApiTestCase
]);
$filename = urlencode($contact->uuid.'.vcf');
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
[
'HTTP_If-Unmodified-Since' => $contact->updated_at->addDays(-1)->toRfc7231String(),
'content-type' => 'application/xml; charset=utf-8',
@@ -203,7 +201,7 @@ class CarddavContactTest extends ApiTestCase
}
/**
* @group carddav
* @group dav
*/
public function test_carddav_update_existing_contact_no_modify()
{
@@ -213,10 +211,10 @@ class CarddavContactTest extends ApiTestCase
]);
$filename = urlencode($contact->uuid.'.vcf');
$response = $this->get("/carddav/addressbooks/{$user->email}/contacts/{$filename}");
$response = $this->get("/dav/addressbooks/{$user->email}/contacts/{$filename}");
$data = $response->getContent();
$response = $this->call('PUT', "/carddav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
$response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [],
['content-type' => 'application/xml; charset=utf-8'],
$data
);
@@ -226,14 +224,14 @@ class CarddavContactTest extends ApiTestCase
$response->assertHeader('ETag');
}
public function test_carddav_contacts_report()
public function test_carddav_contacts_report_version4()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('REPORT', "/carddav/addressbooks/{$user->email}/contacts/", [], [], [],
$response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
@@ -252,13 +250,12 @@ class CarddavContactTest extends ApiTestCase
$peopleurl = route('people.show', $contact);
$sabreversion = \Sabre\VObject\Version::VERSION;
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav">'.
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>".
"<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>".
'<d:propstat>'.
'<d:prop>'.
'<d:getetag>&quot;');
$response->assertSee('&quot;</d:getetag>'.
"<d:getetag>&quot;{$this->getEtag($contact)}&quot;</d:getetag>".
"<card:address-data>BEGIN:VCARD&#13;\n".
"VERSION:4.0&#13;\n".
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN&#13;\n".
@@ -276,6 +273,55 @@ class CarddavContactTest extends ApiTestCase
'</d:multistatus>');
}
public function test_carddav_contacts_report_version3()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
'<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
<d:prop>
<d:getetag />
<card:address-data content-type="text/vcard" version="3.0" />
</d:prop>
</card:addressbook-query>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$peopleurl = route('people.show', $contact);
$sabreversion = \Sabre\VObject\Version::VERSION;
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($contact)}&quot;</d:getetag>".
"<card:address-data>BEGIN:VCARD&#13;\n".
"VERSION:3.0&#13;\n".
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN&#13;\n".
"UID:{$contact->uuid}&#13;\n".
"SOURCE:{$peopleurl}&#13;\n".
"FN:John Doe&#13;\n".
"N:Doe;John;;;&#13;\n".
"GENDER:O;&#13;\n".
"END:VCARD&#13;\n".
'</card:address-data>'.
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
public function test_carddav_contacts_report_multiget()
{
$user = $this->signin();
@@ -286,7 +332,7 @@ class CarddavContactTest extends ApiTestCase
'account_id' => $user->account->id,
]);
$response = $this->call('REPORT', "/carddav/addressbooks/{$user->email}/contacts/", [], [], [],
$response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [],
[
'HTTP_DEPTH' => '1',
],
@@ -295,8 +341,8 @@ class CarddavContactTest extends ApiTestCase
<d:getetag />
<card:address-data content-type=\"text/vcard\" version=\"4.0\" />
</d:prop>
<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact1->uuid}.vcf</d:href>
<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact2->uuid}.vcf</d:href>
<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact1->uuid}.vcf</d:href>
<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact2->uuid}.vcf</d:href>
</card:addressbook-multiget>"
);
@@ -307,13 +353,12 @@ class CarddavContactTest extends ApiTestCase
$people2url = route('people.show', $contact2);
$sabreversion = \Sabre\VObject\Version::VERSION;
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav">'.
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact1->uuid}.vcf</d:href>".
"<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact1->uuid}.vcf</d:href>".
'<d:propstat>'.
'<d:prop>'.
'<d:getetag>&quot;');
$response->assertSee('&quot;</d:getetag>'.
"<d:getetag>&quot;{$this->getEtag($contact1)}&quot;</d:getetag>".
"<card:address-data>BEGIN:VCARD&#13;\n".
"VERSION:4.0&#13;\n".
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN&#13;\n".
@@ -330,11 +375,10 @@ class CarddavContactTest extends ApiTestCase
'</d:response>');
$response->assertSee(
'<d:response>'.
"<d:href>/carddav/addressbooks/{$user->email}/contacts/{$contact2->uuid}.vcf</d:href>".
"<d:href>/dav/addressbooks/{$user->email}/contacts/{$contact2->uuid}.vcf</d:href>".
'<d:propstat>'.
'<d:prop>'.
'<d:getetag>&quot;');
$response->assertSee('&quot;</d:getetag>'.
"<d:getetag>&quot;{$this->getEtag($contact2)}&quot;</d:getetag>".
"<card:address-data>BEGIN:VCARD&#13;\n".
"VERSION:4.0&#13;\n".
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN&#13;\n".
+137
View File
@@ -0,0 +1,137 @@
<?php
namespace Tests\Api\DAV;
use Tests\ApiTestCase;
use Illuminate\Support\Str;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class VEventBirthdayTest extends ApiTestCase
{
use DatabaseTransactions, CardEtag;
/**
* @group dav
*/
public function test_caldav_get_one_birthday()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$response = $this->get("/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics");
$response->assertStatus(200);
$response->assertHeader('X-Sabre-Version');
$this->assertEquals($this->getCal($specialDate, true), $response->getContent());
}
public function test_caldav_birthdays_report()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate->uuid = Str::uuid();
$specialDate->save();
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
'<cal:calendar-query xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:getetag />
<cal:calendar-data content-type="text/calendar" version="2.0" />
</d:prop>
<cal:filter>
<cal:comp-filter name="VCALENDAR" />
</cal:filter>
</cal:calendar-query>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($specialDate)}&quot;</d:getetag>".
"<cal:calendar-data>{$this->getCal($specialDate)}</cal:calendar-data>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
public function test_caldav_birthdays_report_multiget()
{
$user = $this->signin();
$contact1 = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$specialDate1 = $contact1->setSpecialDate('birthdate', 1983, 03, 04);
$specialDate1->uuid = Str::uuid();
$specialDate1->save();
$contact2 = factory(Contact::class)->create([
'account_id' => $user->account->id,
'firstname' => 'Jane',
]);
$specialDate2 = $contact2->setSpecialDate('birthdate', 1980, 05, 01);
$specialDate2->uuid = Str::uuid();
$specialDate2->save();
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [],
[
'HTTP_DEPTH' => '1',
],
"<cal:calendar-multiget xmlns:d=\"DAV:\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<d:getetag />
<cal:calendar-data content-type=\"text/calendar\" version=\"2.0\" />
</d:prop>
<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate1->uuid}.ics</d:href>
<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate2->uuid}.ics</d:href>
</cal:calendar-multiget>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate1->uuid}.ics</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($specialDate1)}&quot;</d:getetag>".
"<cal:calendar-data>{$this->getCal($specialDate1)}</cal:calendar-data>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>');
$response->assertSee(
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/birthdays/{$specialDate2->uuid}.ics</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($specialDate2)}&quot;</d:getetag>".
"<cal:calendar-data>{$this->getCal($specialDate2)}</cal:calendar-data>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
}
+243
View File
@@ -0,0 +1,243 @@
<?php
namespace Tests\Api\DAV;
use Carbon\Carbon;
use Tests\ApiTestCase;
use Illuminate\Support\Str;
use App\Models\Contact\Task;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class VTodoTaskTest extends ApiTestCase
{
use DatabaseTransactions, CardEtag;
/**
* @group dav
*/
public function test_caldav_get_one_task()
{
$user = $this->signin();
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => null,
'created_at' => now(),
'updated_at' => now(),
]);
$response = $this->get("/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics");
$response->assertStatus(200);
$response->assertHeader('X-Sabre-Version');
$this->assertEquals($this->getVTodo($task, true), $response->getContent());
}
/**
* @group dav
*/
public function test_caldav_put_one_task()
{
$user = $this->signin();
$uuid = Str::uuid();
$response = $this->call('PUT', "/dav/calendars/{$user->email}/tasks/{$uuid}.ics", [], [], [],
['content-type' => 'application/xml; charset=utf-8'],
"BEGIN:VCALENDAR
BEGIN:VTODO
UID:{$uuid}
SUMMARY:title
DESCRIPTION:description
END:VTODO
END:VCALENDAR
"
);
$response->assertStatus(201);
$response->assertHeader('X-Sabre-Version');
$response->assertHeaderMissing('ETag');
$this->assertDatabaseHas('tasks', [
'account_id' => $user->account->id,
'contact_id' => null,
'uuid' => $uuid,
'title' => 'title',
'description' => 'description',
]);
}
/**
* @group dav
*/
public function test_caldav_update_existing_task()
{
$user = $this->signin();
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => null,
]);
$response = $this->call('PUT', "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics", [], [], [],
['content-type' => 'application/xml; charset=utf-8'],
"BEGIN:VCALENDAR
BEGIN:VTODO
UID:{$task->uuid}
SUMMARY:new title
DESCRIPTION:new description
END:VTODO
END:VCALENDAR
"
);
$response->assertStatus(204);
$response->assertHeader('X-Sabre-Version');
$response->assertHeaderMissing('ETag');
$this->assertDatabaseHas('tasks', [
'account_id' => $user->account->id,
'uuid' => $task->uuid,
'title' => 'new title',
'description' => 'new description',
]);
}
/**
* @group dav
*/
public function test_caldav_update_task_complete()
{
$user = $this->signin();
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'contact_id' => null,
]);
$response = $this->call('PUT', "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics", [], [], [],
['content-type' => 'application/xml; charset=utf-8'],
"BEGIN:VCALENDAR
BEGIN:VTODO
UID:{$task->uuid}
SUMMARY:{$task->title}
DESCRIPTION:{$task->description}
STATUS:COMPLETED
COMPLETED:20190121T182800Z
END:VTODO
END:VCALENDAR
"
);
$response->assertStatus(204);
$response->assertHeader('X-Sabre-Version');
$response->assertHeaderMissing('ETag');
$this->assertDatabaseHas('tasks', [
'account_id' => $user->account->id,
'uuid' => $task->uuid,
'completed' => true,
'completed_at' => Carbon::create(2019, 01, 21, 18, 28, 00),
]);
}
public function test_caldav_tasks_report()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$task = factory(Task::class)->create([
'account_id' => $user->account->id,
'created_at' => now(),
]);
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
'<cal:calendar-query xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:getetag />
<cal:calendar-data content-type="text/calendar" version="2.0" />
</d:prop>
<cal:filter>
<cal:comp-filter name="VCALENDAR" />
</cal:filter>
</cal:calendar-query>'
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$peopleurl = route('people.show', $contact);
$sabreversion = \Sabre\VObject\Version::VERSION;
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($task)}&quot;</d:getetag>".
"<cal:calendar-data>{$this->getVTodo($task)}</cal:calendar-data>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
public function test_caldav_tasks_report_multiget()
{
$user = $this->signin();
$task1 = factory(Task::class)->create([
'account_id' => $user->account->id,
'created_at' => now(),
]);
$task2 = factory(Task::class)->create([
'account_id' => $user->account->id,
'created_at' => now(),
]);
$response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [],
[
'HTTP_DEPTH' => '1',
],
"<cal:calendar-multiget xmlns:d=\"DAV:\" xmlns:cal=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<d:getetag />
<cal:calendar-data content-type=\"text/calendar\" version=\"2.0\" />
</d:prop>
<d:href>/dav/calendars/{$user->email}/tasks/{$task1->uuid}.ics</d:href>
<d:href>/dav/calendars/{$user->email}/tasks/{$task2->uuid}.ics</d:href>
</cal:calendar-multiget>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">'.
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/tasks/{$task1->uuid}.ics</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($task1)}&quot;</d:getetag>".
"<cal:calendar-data>{$this->getVTodo($task1)}</cal:calendar-data>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>');
$response->assertSee(
'<d:response>'.
"<d:href>/dav/calendars/{$user->email}/tasks/{$task2->uuid}.ics</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<d:getetag>&quot;{$this->getEtag($task2)}&quot;</d:getetag>".
"<cal:calendar-data>{$this->getVTodo($task2)}</cal:calendar-data>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'.
'</d:multistatus>');
}
}
+3 -1
View File
@@ -26,7 +26,9 @@ class SpecialDateTest extends FeatureTestCase
public function test_it_belongs_to_a_contact()
{
$account = factory(Account::class)->create([]);
$contact = factory(Contact::class)->create([]);
$contact = factory(Contact::class)->create([
'account_id' => $account->id,
]);
$specialDate = factory(SpecialDate::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,