feat: CardDAV support (#1284)

This commit is contained in:
Samuel Vandamme
2018-10-29 23:25:47 +01:00
committed by Alexis Saettler
parent 3ee534ae06
commit c7eea2b8c5
23 changed files with 1340 additions and 101 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ APP_TRUSTED_PROXIES=
# Frequency of creation of new log files. Logs are written when an error occurs.
# Refer to config/logging.php for the possible values.
LOG_CHANNEL=syslog
LOG_CHANNEL=single
SENTRY_SUPPORT=false
SENTRY_LARAVEL_DSN=
+3
View File
@@ -116,6 +116,9 @@ AWS_SERVER=
# Allow Two Factor Authentication feature on your instance
MFA_ENABLED=false
# Enable CardDAV support (beta feature)
CARDDAV_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
# access the route to let your users sign in with their credentials
+1
View File
@@ -18,6 +18,7 @@ UNRELEASED CHANGES:
* API breaking change: Remove 'PUT /contacts/:contact_id/pets/:id' in favor of 'PUT /pets/:id' with a 'contact_id'
* API breaking change: Every validator fails now send a HTTP 400 code (was 200) with the error 32
* API breaking change: Every Invald Parameters errors now send a HTTP 400 (was 500 or 200) code with the error 41
* Add CardDAV support — disabled by default. To enable it, toggle the `CARDDAV_ENABLED` env variable.
* Use Laravel email verification, and remove the old module used for that
RELEASED VERSIONS:
@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Barryvdh\Debugbar\Facade as Debugbar;
use App\Models\CardDAV\MonicaAddressBookRoot;
use App\Models\CardDAV\Backends\MonicaSabreBackend;
use App\Models\CardDAV\Backends\MonicaCardDAVBackend;
use App\Models\CardDAV\Backends\MonicaPrincipleBackend;
class CardDAVController extends Controller
{
/**
* Display the specified resource.
*/
public function init(Request $request)
{
// Disable debugger for caldav output
Debugbar::disable();
// Initiate custom backends for link between Sabre and Monica
$authBackend = new MonicaSabreBackend(); // Authentication
$principalBackend = new MonicaPrincipleBackend(); // User rights
$carddavBackend = new MonicaCardDAVBackend(); // Contacts
$nodes = [
new \Sabre\DAVACL\PrincipalCollection($principalBackend),
new MonicaAddressBookRoot($principalBackend, $carddavBackend),
];
// Initiate Sabre server
$server = new \Sabre\DAV\Server($nodes);
$server->setBaseUri('/carddav');
$server->debugExceptions = true;
// Modify Laravel request to include trailing slash. Laravel removes it by default, Sabre needs it.
$server->httpRequest->setUrl(str_replace('/carddav', '/carddav/', $request->getRequestUri()));
$server->httpRequest->setBaseUrl('/carddav/');
// Testing needs method verb to be set manually
if (App::environment('testing')) {
$server->httpRequest->setMethod($request->method());
}
// Add required plugins
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'SabreDAV'));
// CardDAV plugin
$server->addPlugin(new \Sabre\CardDAV\Plugin());
// ACL plugnin
$aclPlugin = new \Sabre\DAVACL\Plugin();
$aclPlugin->allowUnauthenticatedAccess = false;
$server->addPlugin($aclPlugin);
// In debug mode add browser plugin
if (App::environment('local')) {
$server->addPlugin(new \Sabre\DAV\Browser\Plugin());
}
// Execute requests and catch output
// We do this because laravel always sends a 200 back, but we need to use the StatusCode and of Sabre
ob_start();
$server->exec();
$status = $server->httpResponse->getStatus();
$content = ob_get_contents();
$headers = $server->httpResponse->getHeaders();
ob_end_clean();
// Return response through laravel
Log::debug(__CLASS__.' init', [
'status' => $status,
'content' => $content,
]);
return response($content, $status)
->withHeaders($headers);
}
}
+1
View File
@@ -58,6 +58,7 @@ class Kernel extends HttpKernel
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.tokenonbasic' => \App\Http\Middleware\AuthenticateWithTokenOnBasicAuth::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \App\Http\Middleware\ThrottleRequestsMiddleware::class,
@@ -0,0 +1,74 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\AuthManager;
/**
* Authenticate user with Basic Authentication, with two methods:
* - Basic auth: login + password
* - Bearer on basic: login + api token.
*/
class AuthenticateWithTokenOnBasicAuth
{
/**
* The guard factory instance.
*
* @var AuthManager
*/
protected $auth;
/**
* Create a new middleware instance.
*
* @param AuthManager $auth
* @return void
*/
public function __construct(AuthManager $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$this->authenticate($request);
return $next($request);
}
/**
* Handle authentication.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
private function authenticate($request)
{
if ($this->auth->check()) {
return;
}
// Try Bearer authentication, with token in 'password' field on basic auth
if (! $request->bearerToken()) {
$password = $request->getPassword();
$request->headers->set('Authorization', 'Bearer '.$password);
}
$user = $this->auth->guard('api')->user($request);
if ($user && (! $request->getUser() || $request->getUser() === $user->email)) {
$this->auth->setUser($user);
} else {
// Basic authentication
$this->auth->onceBasic();
}
}
}
@@ -0,0 +1,314 @@
<?php
namespace App\Models\CardDAV\Backends;
use Sabre\DAV;
use App\Models\Contact\Contact;
use Sabre\VObject\Component\VCard;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class MonicaCardDAVBackend implements \Sabre\CardDAV\Backend\BackendInterface
{
/**
* Returns the list of addressbooks for a specific user.
*
* Every addressbook should have the following properties:
* id - an arbitrary unique id
* uri - the 'basename' part of the url
* principaluri - Same as the passed parameter
*
* Any additional clark-notation property may be passed besides this. Some
* common ones are :
* {DAV:}displayname
* {urn:ietf:params:xml:ns:carddav}addressbook-description
* {http://calendarserver.org/ns/}getctag
*
* @param string $principalUri
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
Log::debug(__CLASS__.' getAddressBooksForUser', func_get_args());
return [
[
'id' => '0',
'uri' => 'Contacts',
'principaluri' => 'principals/'.Auth::user()->email,
],
];
}
/**
* Updates properties for an address book.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
* you're going to process with the handle() method.
*
* Calling the handle method is like telling the PropPatch object "I
* promise I can handle updating this property".
*
* Read the PropPatch documentation for more info and examples.
*
* @param string $addressBookId
* @param \Sabre\DAV\PropPatch $propPatch
* @return void|bool
*/
public function updateAddressBook($addressBookId, DAV\PropPatch $propPatch)
{
Log::debug(__CLASS__.' updateAddressBook', func_get_args());
return false;
}
/**
* Creates a new address book.
*
* This method should return the id of the new address book. The id can be
* in any format, including ints, strings, arrays or objects.
*
* @param string $principalUri
* @param string $url Just the 'basename' of the url.
* @param array $properties
* @return int|bool
*/
public function createAddressBook($principalUri, $url, array $properties)
{
Log::debug(__CLASS__.' createAddressBook', func_get_args());
return false;
}
/**
* Deletes an entire addressbook and all its contents.
*
* @param mixed $addressBookId
* @return void|bool
*/
public function deleteAddressBook($addressBookId)
{
Log::debug(__CLASS__.' deleteAddressBook', func_get_args());
return false;
}
private function prepareCard($contact)
{
// The standard for most of these fields can be found on https://tools.ietf.org/html/rfc6350
// Basic information
$vcard = new VCard([
'FN' => $contact->name,
'N' => [$contact->first_name, $contact->last_name],
'UID' => $contact->hashid(),
]);
// Picture
$picture = $contact->getAvatarURL();
if (! is_null($picture)) {
$vcard->add('PHOTO', $picture);
}
// Gender
switch ($contact->gender->name) {
case 'Man':
$gender = 'M';
break;
case 'Woman':
$gender = 'F';
break;
default:
$gender = 'O';
break;
}
$vcard->add('GENDER', $gender.';'.$contact->gender->name);
// Birthday
if (! is_null($contact->birthdate)) {
$date = $contact->birthdate->date->format('Ymd');
$vcard->add('BDAY', $date);
}
// Contactfields
foreach ($contact->contactFields as $contactField) {
switch ($contactField->contactFieldType->name) {
case 'Phone':
$vcard->add('TEL', $contactField->data);
break;
case 'Email':
$vcard->add('EMAIL', $contactField->data);
break;
case 'Facebook':
$vcard->add('socialProfile', $contactField->data, ['type' => 'facebook']);
break;
// ... Twitter, Whatsapp, Telegram, other
default:
break;
}
}
return [
'id' => $contact->hashid(),
'etag' => '"'.md5($vcard->serialize()).'"',
'uri' => $contact->id,
'lastmodified' => $contact->updated_at->timestamp,
'carddata' => $vcard->serialize(),
];
}
private function prepareCards($contacts)
{
$results = [];
foreach ($contacts as $contact) {
$results[] = $this->prepareCard($contact);
}
Log::debug(__CLASS__.' prepareCards', ['count' => count($results)]);
return $results;
}
/**
* 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)
{
Log::debug(__CLASS__.' getCards', func_get_args());
$contacts = Auth::user()->account->contacts()->real()->get();
return $this->prepareCards($contacts);
}
/**
* Returns a specfic card.
*
* The same set of prope
* @param mixed $addressBookId
* @param string $cardUri
* @return array
*/
public function getCard($addressBookId, $cardUri)
{
Log::debug(__CLASS__.' getCard', func_get_args());
$contact = Contact::where('account_id', Auth::user()->account_id)
->findOrFail($cardUri);
return $this->prepareCard($contact);
}
/**
* Returns a list of cards.
*
* This method should work identical to getCard, but instead return all the
* cards in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
* @param array $uris
* @return array
*/
public function getMultipleCards($addressBookId, array $uris)
{
Log::debug(__CLASS__.' getMultipleCards', func_get_args());
$contacts = Contact::where('account_id', Auth::user()->account_id)
->whereIn('id', $uris)
->get();
return $this->prepareCards($contacts);
}
/**
* 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)
{
Log::debug(__CLASS__.' createCard', func_get_args());
}
/**
* 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)
{
Log::debug(__CLASS__.' updateCard', func_get_args());
}
/**
* Deletes a card.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
Log::debug(__CLASS__.' deleteCard', func_get_args());
}
}
@@ -0,0 +1,194 @@
<?php
namespace App\Models\CardDAV\Backends;
use Sabre\DAV;
use Sabre\CalDAV;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class MonicaPrincipleBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
{
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
* {http://sabredav.org/ns}email-address - This is a custom SabreDAV
* field that's actually injected in a number of other properties. If
* you have an email address, use this property.
*
* @param string $prefixPath
* @return array
*/
public function getPrincipalsByPrefix($prefixPath)
{
Log::debug(__CLASS__.' getPrincipalsByPrefix', func_get_args());
$principals = [
[
'uri' => 'principals/'.Auth::user()->email,
'{http://sabredav.org/ns}email-address' => Auth::user()->email,
'{DAV:}displayname' => Auth::user()->name,
],
];
$prefixPath = trim($prefixPath, '/');
if ($prefixPath) {
$prefixPath .= '/';
}
$return = [];
foreach ($principals as $principal) {
if ($prefixPath && strpos($principal['uri'], $prefixPath) !== 0) {
continue;
}
$return[] = $principal;
}
return $return;
}
/**
* Returns a specific principal, specified by its path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $path
* @return array
*/
public function getPrincipalByPath($path)
{
Log::debug(__CLASS__.' getPrincipalByPath', func_get_args());
foreach ($this->getPrincipalsByPrefix('principals') as $principal) {
if ($principal['uri'] === $path) {
return $principal;
}
}
}
/**
* Updates one ore more webdav properties on a principal.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
* you're going to process with the handle() method.
*
* Calling the handle method is like telling the PropPatch object "I
* promise I can handle updating this property".
*
* Read the PropPatch documentation for more info and examples.
*
* @param string $path
* @param \Sabre\DAV\PropPatch $propPatch
* @return void
*/
public function updatePrincipal($path, DAV\PropPatch $propPatch)
{
Log::debug(__CLASS__.' updatePrincipal', func_get_args());
}
/**
* This method is used to search for principals matching a set of
* properties.
*
* This search is specifically used by RFC3744's principal-property-search
* REPORT.
*
* The actual search should be a unicode-non-case-sensitive search. The
* keys in searchProperties are the WebDAV property names, while the values
* are the property values to search on.
*
* By default, if multiple properties are submitted to this method, the
* various properties should be combined with 'AND'. If $test is set to
* 'anyof', it should be combined using 'OR'.
*
* This method should simply return an array with full principal uri's.
*
* If somebody attempted to search on a property the backend does not
* support, you should simply return 0 results.
*
* You can also just return 0 results if you choose to not support
* searching at all, but keep in mind that this may stop certain features
* from working.
*
* @param string $prefixPath
* @param array $searchProperties
* @param string $test
* @return array
*/
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
{
Log::debug(__CLASS__.' searchPrincipals', func_get_args());
}
/**
* Finds a principal by its URI.
*
* This method may receive any type of uri, but mailto: addresses will be
* the most common.
*
* Implementation of this API is optional. It is currently used by the
* CalDAV system to find principals based on their email addresses. If this
* API is not implemented, some features may not work correctly.
*
* This method must return a relative principal path, or null, if the
* principal was not found or you refuse to find it.
*
* @param string $uri
* @param string $principalPrefix
* @return string
*/
public function findByUri($uri, $principalPrefix)
{
Log::debug(__CLASS__.' searchPrincipals', func_get_args());
}
/**
* Returns the list of members for a group-principal.
*
* @param string $principal
* @return array
*/
public function getGroupMemberSet($principal)
{
debug('getGroupMemberSet');
return [
'principals/'.Auth::user()->email,
];
}
/**
* Returns the list of groups a principal is a member of.
*
* @param string $principal
* @return array
*/
public function getGroupMembership($principal)
{
debug('getGroupMembership');
return [
'principals/'.Auth::user()->email,
];
}
/**
* Updates the list of group members for a group principal.
*
* The principals should be passed as a list of uri's.
*
* @param string $principal
* @param array $members
* @return void
*/
public function setGroupMemberSet($principal, array $members)
{
dd('setGroupMemberSet');
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Models\CardDAV\Backends;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use Sabre\DAV\Auth\Backend\BackendInterface;
class MonicaSabreBackend implements BackendInterface
{
/**
* Authentication Realm.
*
* The realm is often displayed by browser clients when showing the
* authentication dialog.
*
* @var string
*/
protected $realm = 'sabre/dav';
/**
* This is the prefix that will be used to generate principal urls.
*
* @var string
*/
protected $principalPrefix = 'principals/';
/**
* Sets the authentication realm for this backend.
*
* @param string $realm
* @return void
*/
public function setRealm($realm)
{
$this->realm = $realm;
}
/**
* Check Laravel authentication.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return array
*/
public function check(RequestInterface $request, ResponseInterface $response)
{
if (! Auth::check()) {
return [false, 'User is not authenticated'];
}
Log::debug(__CLASS__.' validateUserPass', [Auth::user()->name]);
return [true, $this->principalPrefix.Auth::user()->email];
}
/**
* This method is called when a user could not be authenticated, and
* authentication was required for the current request.
*
* This gives you the opportunity to set authentication headers. The 401
* status code will already be set.
*
* In this case of Bearer Auth, this would for example mean that the
* following header needs to be set:
*
* $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV');
*
* Keep in mind that in the case of multiple authentication backends, other
* WWW-Authenticate headers may already have been set, and you'll want to
* append your own WWW-Authenticate header instead of overwriting the
* existing one.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return void
*/
public function challenge(RequestInterface $request, ResponseInterface $response)
{
$auth = new \Sabre\HTTP\Auth\Bearer(
$this->realm,
$request,
$response
);
$auth->requireLogin();
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Models\CardDAV;
use Sabre\CardDAV\AddressBook;
class MonicaAddressBook extends AddressBook
{
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
public function getACL()
{
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
];
}
/**
* This method returns the ACL's for card nodes in this address book.
* The result of this method automatically gets passed to the
* card nodes in this address book.
*
* @return array
*/
public function getChildACL()
{
return $this->getACL();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Models\CardDAV;
use Sabre\CardDAV\AddressBookHome;
class MonicaAddressBookHome extends AddressBookHome
{
/**
* Returns a list of addressbooks.
*
* @return array
*/
public function getChildren()
{
$addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
$objs = [];
foreach ($addressbooks as $addressbook) {
$objs[] = new MonicaAddressBook($this->carddavBackend, $addressbook);
}
return $objs;
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Models\CardDAV;
use Sabre\CardDAV\AddressBookRoot;
class MonicaAddressBookRoot extends AddressBookRoot
{
/**
* This method returns a node for a principal.
*
* The passed array contains principal information, and is guaranteed to
* at least contain a uri item. Other properties may or may not be
* supplied by the authentication backend.
*
* @param array $principal
* @return \Sabre\DAV\INode
*/
public function getChildForPrincipal(array $principal)
{
return new MonicaAddressBookHome($this->carddavBackend, $principal['uri']);
}
}
+24 -2
View File
@@ -81,6 +81,10 @@ class RouteServiceProvider extends ServiceProvider
$this->mapOAuthRoutes($router);
if (config('carddav.enabled')) {
$this->mapCardDAVRoutes($router);
}
$this->mapSpecialRoutes($router);
}
@@ -131,11 +135,29 @@ class RouteServiceProvider extends ServiceProvider
'prefix' => 'api',
'middleware' => 'api',
'namespace' => $this->namespace,
], function ($router) {
], function () {
require base_path('routes/api.php');
});
}
/**
* Define the "carddav" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapCardDAVRoutes(Router $router)
{
$router->group([
'prefix' => 'carddav',
'middleware' => 'api',
'namespace' => $this->namespace,
], function () {
require base_path('routes/carddav.php');
});
}
/**
* Define the "special" routes for the application.
*
@@ -148,7 +170,7 @@ class RouteServiceProvider extends ServiceProvider
$router->group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
], function () {
require base_path('routes/special.php');
});
}
+2 -2
View File
@@ -39,7 +39,7 @@
"pragmarx/recovery": "^0.1",
"predis/predis": "^1.1",
"ralouphie/mimey": "^2.0",
"sabre/vobject": "^4.1",
"sabre/dav": "^3.2",
"sentry/sentry-laravel": "^0.10",
"stevebauman/location": "^3.0",
"symfony/translation": "^4.0",
@@ -61,7 +61,7 @@
"roave/security-advisories": "dev-master",
"symfony/css-selector": "~4.0",
"symfony/dom-crawler": "~4.0",
"vimeo/psalm": "^2.0"
"vimeo/psalm": "~2.0"
},
"suggest": {
"ext-apcu": "*"
Generated
+222 -93
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "251e221e6b645534037348feff30e0d1",
"content-hash": "652aed71d76e7dd519a8e64001933836",
"packages": [
{
"name": "aws/aws-sdk-php",
@@ -5158,24 +5158,220 @@
"time": "2016-07-19T19:14:21+00:00"
},
{
"name": "sabre/uri",
"version": "2.1.1",
"name": "sabre/dav",
"version": "3.2.3",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/uri.git",
"reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c"
"url": "https://github.com/sabre-io/dav.git",
"reference": "a9780ce4f35560ecbd0af524ad32d9d2c8954b80"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/uri/zipball/a42126042c7dcb53e2978dadb6d22574d1359b4c",
"reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c",
"url": "https://api.github.com/repos/sabre-io/dav/zipball/a9780ce4f35560ecbd0af524ad32d9d2c8954b80",
"reference": "a9780ce4f35560ecbd0af524ad32d9d2c8954b80",
"shasum": ""
},
"require": {
"php": ">=7"
"ext-ctype": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-iconv": "*",
"ext-mbstring": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"ext-spl": "*",
"lib-libxml": ">=2.7.0",
"php": ">=5.5.0",
"psr/log": "^1.0",
"sabre/event": ">=2.0.0, <4.0.0",
"sabre/http": "^4.2.1",
"sabre/uri": "^1.0.1",
"sabre/vobject": "^4.1.0",
"sabre/xml": "^1.4.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"evert/phpdoc-md": "~0.1.0",
"monolog/monolog": "^1.18",
"phpunit/phpunit": "> 4.8, <6.0.0",
"sabre/cs": "^1.0.0"
},
"suggest": {
"ext-curl": "*",
"ext-pdo": "*"
},
"bin": [
"bin/sabredav",
"bin/naturalselection"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1.0-dev"
}
},
"autoload": {
"psr-4": {
"Sabre\\DAV\\": "lib/DAV/",
"Sabre\\DAVACL\\": "lib/DAVACL/",
"Sabre\\CalDAV\\": "lib/CalDAV/",
"Sabre\\CardDAV\\": "lib/CardDAV/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "WebDAV Framework for PHP",
"homepage": "http://sabre.io/",
"keywords": [
"CalDAV",
"CardDAV",
"WebDAV",
"framework",
"iCalendar"
],
"time": "2018-10-19T09:58:27+00:00"
},
{
"name": "sabre/event",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/event.git",
"reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/event/zipball/831d586f5a442dceacdcf5e9c4c36a4db99a3534",
"reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "*",
"sabre/cs": "~0.0.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Sabre\\Event\\": "lib/"
},
"files": [
"lib/coroutine.php",
"lib/Loop/functions.php",
"lib/Promise/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "sabre/event is a library for lightweight event-based programming",
"homepage": "http://sabre.io/event/",
"keywords": [
"EventEmitter",
"async",
"events",
"hooks",
"plugin",
"promise",
"signal"
],
"time": "2015-11-05T20:14:39+00:00"
},
{
"name": "sabre/http",
"version": "v4.2.4",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/http.git",
"reference": "acccec4ba863959b2d10c1fa0fb902736c5c8956"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/http/zipball/acccec4ba863959b2d10c1fa0fb902736c5c8956",
"reference": "acccec4ba863959b2d10c1fa0fb902736c5c8956",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-mbstring": "*",
"php": ">=5.4",
"sabre/event": ">=1.0.0,<4.0.0",
"sabre/uri": "~1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.3",
"sabre/cs": "~0.0.1"
},
"suggest": {
"ext-curl": " to make http requests with the Client class"
},
"type": "library",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\HTTP\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
"homepage": "https://github.com/fruux/sabre-http",
"keywords": [
"http"
],
"time": "2018-02-23T11:10:29+00:00"
},
{
"name": "sabre/uri",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/uri.git",
"reference": "ada354d83579565949d80b2e15593c2371225e61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/uri/zipball/ada354d83579565949d80b2e15593c2371225e61",
"reference": "ada354d83579565949d80b2e15593c2371225e61",
"shasum": ""
},
"require": {
"php": ">=5.4.7"
},
"require-dev": {
"phpunit/phpunit": ">=4.0,<6.0",
"sabre/cs": "~1.0.0"
},
"type": "library",
@@ -5206,7 +5402,7 @@
"uri",
"url"
],
"time": "2017-02-20T20:02:35+00:00"
"time": "2017-02-20T19:59:28+00:00"
},
{
"name": "sabre/vobject",
@@ -5307,16 +5503,16 @@
},
{
"name": "sabre/xml",
"version": "2.1.1",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/xml.git",
"reference": "c5f510f8d0fa8135de9495145e2758f56037aa1e"
"reference": "59b20e5bbace9912607481634f97d05a776ffca7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/xml/zipball/c5f510f8d0fa8135de9495145e2758f56037aa1e",
"reference": "c5f510f8d0fa8135de9495145e2758f56037aa1e",
"url": "https://api.github.com/repos/sabre-io/xml/zipball/59b20e5bbace9912607481634f97d05a776ffca7",
"reference": "59b20e5bbace9912607481634f97d05a776ffca7",
"shasum": ""
},
"require": {
@@ -5324,11 +5520,12 @@
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"lib-libxml": ">=2.6.20",
"php": ">=7.0",
"php": ">=5.5.5",
"sabre/uri": ">=1.0,<3.0.0"
},
"require-dev": {
"phpunit/phpunit": "*"
"phpunit/phpunit": "*",
"sabre/cs": "~1.0.0"
},
"type": "library",
"autoload": {
@@ -5365,7 +5562,7 @@
"dom",
"xml"
],
"time": "2018-10-09T11:41:10+00:00"
"time": "2016-10-09T22:57:52+00:00"
},
{
"name": "sentry/sentry",
@@ -9067,66 +9264,6 @@
"description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
"time": "2018-10-02T16:19:22+00:00"
},
{
"name": "sabre/event",
"version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/event.git",
"reference": "f5cf802d240df1257866d8813282b98aee3bc548"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/event/zipball/f5cf802d240df1257866d8813282b98aee3bc548",
"reference": "f5cf802d240df1257866d8813282b98aee3bc548",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": ">=6",
"sabre/cs": "~1.0.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Sabre\\Event\\": "lib/"
},
"files": [
"lib/coroutine.php",
"lib/Loop/functions.php",
"lib/Promise/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "sabre/event is a library for lightweight event-based programming",
"homepage": "http://sabre.io/event/",
"keywords": [
"EventEmitter",
"async",
"coroutine",
"eventloop",
"events",
"hooks",
"plugin",
"promise",
"reactor",
"signal"
],
"time": "2018-03-05T13:55:47+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.1",
@@ -9868,32 +10005,25 @@
},
{
"name": "vimeo/psalm",
"version": "2.0.15",
"version": "2.0.14",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
"reference": "8b525a066b107b9936f338ec7c2c5618cf9a1764"
"reference": "792144def40d678b7fa7c8a52da6cdbd34dd415a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/8b525a066b107b9936f338ec7c2c5618cf9a1764",
"reference": "8b525a066b107b9936f338ec7c2c5618cf9a1764",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/792144def40d678b7fa7c8a52da6cdbd34dd415a",
"reference": "792144def40d678b7fa7c8a52da6cdbd34dd415a",
"shasum": ""
},
"require": {
"composer/xdebug-handler": "^1.1",
"felixfbecker/advanced-json-rpc": "^3.0.3",
"felixfbecker/language-server-protocol": "^1.2",
"muglug/package-versions-56": "1.2.4",
"netresearch/jsonmapper": "^1.0",
"nikic/php-parser": "^4.0.3 || ^4.1",
"nikic/php-parser": "^4.0",
"openlss/lib-array2xml": "^0.0.10||^0.5.1",
"php": "^7.0",
"php-cs-fixer/diff": "^1.2",
"sabre/event": "^5.0.1",
"sabre/uri": "^2.0",
"webmozart/glob": "^4.1",
"webmozart/path-util": "^2.3"
"php-cs-fixer/diff": "^1.2"
},
"provide": {
"psalm/psalm": "self.version"
@@ -9909,8 +10039,7 @@
},
"bin": [
"psalm",
"psalter",
"psalm-language-server"
"psalter"
],
"type": "library",
"extra": {
@@ -9939,7 +10068,7 @@
"inspection",
"php"
],
"time": "2018-10-19T18:04:22+00:00"
"time": "2018-10-10T18:22:06+00:00"
},
{
"name": "webmozart/assert",
+11
View File
@@ -0,0 +1,11 @@
<?php
return [
/*
* Carddav enabled
*/
'enabled' => (bool) env('CARDDAV_ENABLED', false),
];
+1 -1
View File
@@ -27,10 +27,10 @@ return [
*/
'languages' => parse_langs_to_array(
env('LANG_DETECTOR_LANGUAGES', [
'en',
'ar',
'cs',
'de',
'en',
'es',
'fr',
'he',
+4
View File
@@ -76,5 +76,9 @@ return [
'driver' => 'errorlog',
'level' => 'debug',
],
'testing' => [
'driver' => 'errorlog',
'level' => 'info',
],
],
];
+2 -2
View File
@@ -4,7 +4,7 @@ services:
build:
context: .
dockerfile: Dockerfile.dev
links:
depends_on:
- mysql
ports:
- 8080:80
@@ -30,7 +30,7 @@ services:
phpmyadmin:
image: phpmyadmin/phpmyadmin
links:
depends_on:
- mysql
environment:
PMA_HOST: mysql
+1
View File
@@ -41,5 +41,6 @@
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="APP_DEFAULT_LOCALE" value="en"/>
<env name="LOG_CHANNEL" value="testing"/>
</php>
</phpunit>
+27
View File
@@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Route;
$verbs = [
'GET',
'HEAD',
'POST',
'PUT',
'PATCH',
'DELETE',
'PROPFIND',
'PROPPATCH',
'MKCOL',
'COPY',
'MOVE',
'LOCK',
'UNLOCK',
'OPTIONS',
'REPORT',
];
Illuminate\Routing\Router::$verbs = $verbs;
Route::group(['middleware' => ['auth.tokenonbasic']], function () use ($verbs) {
Route::match($verbs, '{path?}', 'CardDAVController@init')->where('path', '(.)*');
});
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace Tests\Api\Carddav;
use Tests\ApiTestCase;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @runTestsInSeparateProcesses
*/
class CarddavContact extends ApiTestCase
{
use DatabaseTransactions;
public function setUp()
{
if (! (bool) env('CARDDAV_ENABLED', false)) {
$this->markTestSkipped('carddav disabled');
}
}
/**
* @group carddav
*/
public function test_carddav_get_one_contact()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->get("/carddav/addressbooks/{$user->email}/Contacts/{$contact->id}");
$response->assertStatus(200);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('PRODID:-//Sabre//Sabre VObject');
$response->assertSee('FN:'.$contact->name);
$response->assertSee('N:'.$contact->first_name.';'.$contact->last_name);
$response->assertSee('GENDER:O\;');
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace Tests\Api\Carddav;
use Tests\ApiTestCase;
use App\Models\Contact\Contact;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @runTestsInSeparateProcesses
*/
class CarddavServer extends ApiTestCase
{
use DatabaseTransactions;
public function setUp()
{
if (! (bool) env('CARDDAV_ENABLED', false)) {
$this->markTestSkipped('carddav disabled');
}
}
/**
* @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>");
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/Contacts/{$contact->id}</d:href>");
}
/**
* @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->id}");
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee("<d:response><d:href>/carddav/addressbooks/{$user->email}/Contacts/{$contact->id}</d:href>");
}
}