feat: support CalDAV for birthdays and tasks (#2304)
This commit is contained in:
Binary file not shown.
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-3
@@ -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);
|
||||
}
|
||||
+4
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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']);
|
||||
}
|
||||
}
|
||||
+241
-314
@@ -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();
|
||||
}
|
||||
+2
-2
@@ -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.
|
||||
+28
-17
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\CardDAV;
|
||||
namespace App\Http\Controllers\DAV;
|
||||
|
||||
use Sabre\HTTP\Sapi;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
@@ -23,6 +23,7 @@ class SyncToken extends Model
|
||||
protected $fillable = [
|
||||
'account_id',
|
||||
'user_id',
|
||||
'name',
|
||||
'timestamp',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* Carddav enabled
|
||||
*/
|
||||
|
||||
'enabled' => (bool) env('CARDDAV_ENABLED', false),
|
||||
|
||||
];
|
||||
@@ -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',
|
||||
|
||||
];
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'comment' => 'Comment: :comment',
|
||||
'footer_contact_info' => 'Add, view, complete, and change information about this contact:',
|
||||
'footer_contact_info2' => 'See :name’s profile',
|
||||
'footer_contact_info2_link' => 'See :name’s profile: :url',
|
||||
|
||||
'notification_subject_line' => 'You have an upcoming event',
|
||||
'notification_description' => 'In :count days (on :date), the following event will happen:',
|
||||
|
||||
@@ -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', '(.)*');
|
||||
});
|
||||
@@ -14,4 +14,4 @@ QUEUE_DRIVER=sync
|
||||
LOG_CHANNEL=testing
|
||||
|
||||
MFA_ENABLED=true
|
||||
CARDDAV_ENABLED=true
|
||||
DAV_ENABLED=true
|
||||
|
||||
@@ -14,4 +14,4 @@ QUEUE_DRIVER=sync
|
||||
LOG_CHANNEL=testing
|
||||
|
||||
MFA_ENABLED=true
|
||||
CARDDAV_ENABLED=true
|
||||
DAV_ENABLED=true
|
||||
|
||||
@@ -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>"");
|
||||
$response->assertSee(""</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>"");
|
||||
$response->assertSee(""</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>");
|
||||
}
|
||||
}
|
||||
@@ -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>"{$this->getEtag($specialDate)}"</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>"{$this->getEtag($specialDate)}"</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>");
|
||||
}
|
||||
}
|
||||
@@ -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>"{$this->getEtag($task)}"</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>"{$this->getEtag($task)}"</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>");
|
||||
}
|
||||
}
|
||||
@@ -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>"{$this->getEtag($contact)}"</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>"{$this->getEtag($contact)}"</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>");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>"');
|
||||
$response->assertSee('"</d:getetag>'.
|
||||
"<d:getetag>"{$this->getEtag($contact)}"</d:getetag>".
|
||||
"<card:address-data>BEGIN:VCARD \n".
|
||||
"VERSION:4.0 \n".
|
||||
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN \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>"{$this->getEtag($contact)}"</d:getetag>".
|
||||
"<card:address-data>BEGIN:VCARD \n".
|
||||
"VERSION:3.0 \n".
|
||||
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN \n".
|
||||
"UID:{$contact->uuid} \n".
|
||||
"SOURCE:{$peopleurl} \n".
|
||||
"FN:John Doe \n".
|
||||
"N:Doe;John;;; \n".
|
||||
"GENDER:O; \n".
|
||||
"END:VCARD \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>"');
|
||||
$response->assertSee('"</d:getetag>'.
|
||||
"<d:getetag>"{$this->getEtag($contact1)}"</d:getetag>".
|
||||
"<card:address-data>BEGIN:VCARD \n".
|
||||
"VERSION:4.0 \n".
|
||||
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN \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>"');
|
||||
$response->assertSee('"</d:getetag>'.
|
||||
"<d:getetag>"{$this->getEtag($contact2)}"</d:getetag>".
|
||||
"<card:address-data>BEGIN:VCARD \n".
|
||||
"VERSION:4.0 \n".
|
||||
"PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN \n".
|
||||
@@ -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>"{$this->getEtag($specialDate)}"</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>"{$this->getEtag($specialDate1)}"</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>"{$this->getEtag($specialDate2)}"</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>');
|
||||
}
|
||||
}
|
||||
@@ -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>"{$this->getEtag($task)}"</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>"{$this->getEtag($task1)}"</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>"{$this->getEtag($task2)}"</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>');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user