From 5ed486559ae653c3773b3b0ef46bf7d8572e5b1c Mon Sep 17 00:00:00 2001 From: philipp lang Date: Thu, 6 Oct 2022 20:58:58 +0200 Subject: [PATCH] Add laravel sabre --- app/Dav/AddressBookBackend.php | 256 ++++++++++++++++++ app/Dav/Principal.php | 178 ++++++++++++ app/Dav/ServiceProvider.php | 62 +++++ config/laravelsabre.php | 57 ++++ ...0_05_171451_create_members_slug_column.php | 28 ++ 5 files changed, 581 insertions(+) create mode 100644 app/Dav/AddressBookBackend.php create mode 100644 app/Dav/Principal.php create mode 100644 app/Dav/ServiceProvider.php create mode 100644 config/laravelsabre.php create mode 100644 database/migrations/2022_10_05_171451_create_members_slug_column.php diff --git a/app/Dav/AddressBookBackend.php b/app/Dav/AddressBookBackend.php new file mode 100644 index 00000000..aa1901f8 --- /dev/null +++ b/app/Dav/AddressBookBackend.php @@ -0,0 +1,256 @@ +> + */ + public function getAddressBooksForUser($principalUri) + { + if (1 !== preg_match('/^\/principals\/(.*)$/', $principalUri, $matches)) { + return []; + } + + $user = User::where('email', $matches[1])->firstOrFail(); + + return [ + [ + 'id' => 'contacts', + 'principaluri' => $principalUri, + 'uri' => 'contacts', + '{DAV:}displayname' => 'Kontakte', + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Alle Adressen', + ], + ]; + } + + /** + * 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 + * + * @return void + */ + public function updateAddressBook($addressBookId, PropPatch $propPatch) + { + } + + /** + * 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 mixed + */ + public function createAddressBook($principalUri, $url, array $properties) + { + } + + /** + * Deletes an entire addressbook and all its contents. + * + * @param mixed $addressBookId + * + * @return void + */ + public function deleteAddressBook($addressBookId) + { + } + + /** + * 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): array + { + return Member::get()->map(fn ($member) => $this->cardMeta($member))->toArray(); + } + + /** + * Returns a specfic 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 M + */ + public function getCard($addressBookId, $cardUri) + { + $member = Member::where('slug', $cardUri)->firstOrFail(); + + return [ + ...$this->cardMeta($member), + 'carddata' => $member->toVcard()->serialize(), + ]; + } + + /** + * 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 + * + * @return array + */ + public function getMultipleCards($addressBookId, array $uris) + { + return Member::whereIn('slug', $uris)->get()->map(fn ($member) => [ + ...$this->cardMeta($member), + 'carddata' => $member->toVcard()->serialize(), + ])->toArray(); + } + + /** + * 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 null; + } + + /** + * 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 null; + } + + /** + * Deletes a card. + * + * @param mixed $addressBookId + * @param string $cardUri + * + * @return bool + */ + public function deleteCard($addressBookId, $cardUri) + { + return false; + } + + /** + * @return M + */ + private function cardMeta(Member $member): array + { + return [ + 'lastmodified' => $member->updated_at->timestamp, + 'etag' => '"'.$member->etag.'"', + 'uri' => $member->slug, + 'id' => $member->id, + 'size' => strlen($member->toVcard()->serialize()), + ]; + } +} diff --git a/app/Dav/Principal.php b/app/Dav/Principal.php new file mode 100644 index 00000000..a221ea15 --- /dev/null +++ b/app/Dav/Principal.php @@ -0,0 +1,178 @@ +> + */ + public function getPrincipalsByPrefix($prefixPath) + { + if ('principals' !== $prefixPath) { + return []; + } + + return User::get()->map(fn ($user) => $this->userToPrincipal($user))->toArray(); + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * + * @return array + */ + public function getPrincipalByPath($path) + { + if (1 !== preg_match('/^principals\/(.*)$/', $path, $matches)) { + return []; + } + + $user = User::where('email', $matches[1])->firstOrFail(); + + return $this->userToPrincipal($user); + } + + /** + * 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 + * + * @return void + */ + public function updatePrincipal($path, PropPatch $propPatch) + { + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * + * @return array + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + return []; + } + + /** + * 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|null + */ + public function findByUri($uri, $principalPrefix) + { + } + + /** + * Returns the list of members for a group-principal. + * + * @param string $principal + * + * @return array + */ + public function getGroupMemberSet($principal) + { + } + + /** + * Returns the list of groups a principal is a member of. + * + * @param string $principal + * + * @return array + */ + public function getGroupMembership($principal) + { + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + */ + public function setGroupMemberSet($principal, array $members) + { + } + + /** + * @return array + */ + private function userToPrincipal(User $user): array + { + return [ + '{DAV:}displayname' => $user->name, + 'uri' => '/principals/'.$user->email, + '{http://sabredav.org/ns}email-address' => $user->email, + ]; + } +} diff --git a/app/Dav/ServiceProvider.php b/app/Dav/ServiceProvider.php new file mode 100644 index 00000000..3a94c8f8 --- /dev/null +++ b/app/Dav/ServiceProvider.php @@ -0,0 +1,62 @@ +nodes(); + }); + LaravelSabre::plugins(fn () => $this->plugins()); + LaravelSabre::auth(function () { + auth()->onceBasic(); + + return true; + }); + } + + /** + * List of nodes for DAV Collection. + * + * @return array + */ + private function nodes(): array + { + $principalBackend = new Principal(); + $addressBookBackend = new AddressBookBackend(); + + // Directory tree + return [ + new PrincipalCollection($principalBackend), + new AddressBookRoot($principalBackend, $addressBookBackend), + ]; + } + + private function plugins(): array + { + $authBackend = new AuthBackend(); + + return [ + new BrowserPlugin(), + new AuthPlugin($authBackend), + new CardDAVPlugin(), + ]; + } +} diff --git a/config/laravelsabre.php b/config/laravelsabre.php new file mode 100644 index 00000000..02916085 --- /dev/null +++ b/config/laravelsabre.php @@ -0,0 +1,57 @@ + null, + + /* + |-------------------------------------------------------------------------- + | LaravelSabre Path + |-------------------------------------------------------------------------- + | + | This is the URI path where LaravelSabre will be accessible from. Feel free + | to change this path to anything you like. + | + */ + + 'path' => 'dav', + + /* + |-------------------------------------------------------------------------- + | LaravelSabre Master Switch + |-------------------------------------------------------------------------- + | + | This option may be used to disable LaravelSabre. + | + */ + + 'enabled' => env('LARAVELSABRE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | LaravelSabre Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every LaravelSabre route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'api', + Authorize::class, + ], +]; diff --git a/database/migrations/2022_10_05_171451_create_members_slug_column.php b/database/migrations/2022_10_05_171451_create_members_slug_column.php new file mode 100644 index 00000000..4be42c61 --- /dev/null +++ b/database/migrations/2022_10_05_171451_create_members_slug_column.php @@ -0,0 +1,28 @@ +string('slug', 100); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + } +};