Add laravel sabre

This commit is contained in:
philipp lang 2022-10-06 20:58:58 +02:00
parent ddfcb04989
commit 5ed486559a
5 changed files with 581 additions and 0 deletions

View File

@ -0,0 +1,256 @@
<?php
namespace App\Dav;
use App\Member\Member;
use App\User;
use Sabre\CardDAV\Backend\AbstractBackend;
use Sabre\DAV\PropPatch;
use Sabre\VObject\Component\VCard;
/**
* @template M as array{lastmodified: int, etag: string, uri: string, id: int, size: int}
*/
class AddressBookBackend extends AbstractBackend
{
/**
* 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<int, array<string, string>>
*/
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<string, string> $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<int, M>
*/
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()),
];
}
}

178
app/Dav/Principal.php Normal file
View File

@ -0,0 +1,178 @@
<?php
namespace App\Dav;
use App\User;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL\PrincipalBackend\BackendInterface as PrincipalBackendInterface;
class Principal implements PrincipalBackendInterface
{
/**
* 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<int, array<string, string>>
*/
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<string, string>
*/
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<string, string> $searchProperties
* @param string $test
*
* @return array<int, string>
*/
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<string, string>
*/
private function userToPrincipal(User $user): array
{
return [
'{DAV:}displayname' => $user->name,
'uri' => '/principals/'.$user->email,
'{http://sabredav.org/ns}email-address' => $user->email,
];
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Dav;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use LaravelSabre\Http\Auth\AuthBackend;
use LaravelSabre\LaravelSabre;
use Sabre\CardDAV\AddressBookRoot;
use Sabre\CardDAV\Plugin as CardDAVPlugin;
use Sabre\DAV\Auth\Plugin as AuthPlugin;
use Sabre\DAV\Browser\Plugin as BrowserPlugin;
use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\PrincipalCollection;
class ServiceProvider extends BaseServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
LaravelSabre::nodes(function () {
return $this->nodes();
});
LaravelSabre::plugins(fn () => $this->plugins());
LaravelSabre::auth(function () {
auth()->onceBasic();
return true;
});
}
/**
* List of nodes for DAV Collection.
*
* @return array<int, AbstractPrincipalCollection>
*/
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(),
];
}
}

57
config/laravelsabre.php Normal file
View File

@ -0,0 +1,57 @@
<?php
use LaravelSabre\Http\Middleware\Authorize;
return [
/*
|--------------------------------------------------------------------------
| LaravelSabre Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where LaravelSabre will be accessible from. If the
| setting is null, LaravelSabre will reside under the same domain as the
| application. Otherwise, this value will be used as the subdomain.
|
*/
'domain' => 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,
],
];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('members', function (Blueprint $table) {
$table->string('slug', 100);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
};