Skip to content

Commit

Permalink
refactor: import and export vcard improvements (#6798)
Browse files Browse the repository at this point in the history
  • Loading branch information
asbiin authored Aug 21, 2023
1 parent 4e53a94 commit 4ed4536
Show file tree
Hide file tree
Showing 29 changed files with 838 additions and 497 deletions.
14 changes: 12 additions & 2 deletions app/Domains/Contact/Dav/ExportVCardResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

namespace App\Domains\Contact\Dav;

use App\Models\Contact;
use Sabre\VObject\Component\VCard;

/**
* @template T of \App\Domains\Contact\Dav\VCardResource
*/
interface ExportVCardResource
{
public function export(Contact $contact, VCard $vcard): void;
/**
* @return class-string<T>
*/
public function getType(): string;

/**
* @param T $resource
*/
public function export(mixed $resource, VCard $vcard): void;
}
10 changes: 7 additions & 3 deletions app/Domains/Contact/Dav/ImportVCardResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace App\Domains\Contact\Dav;

use App\Domains\Contact\Dav\Services\ImportVCard;
use App\Models\Contact;
use Sabre\VObject\Component\VCard;

interface ImportVCardResource
Expand All @@ -14,7 +13,12 @@ interface ImportVCardResource
public function setContext(ImportVCard $context): self;

/**
* Import Contact.
* Can import Card.
*/
public function import(?Contact $contact, VCard $vcard): Contact;
public function can(VCard $vcard): bool;

/**
* Import Card.
*/
public function import(VCard $vcard, ?VCardResource $result): ?VCardResource;
}
46 changes: 45 additions & 1 deletion app/Domains/Contact/Dav/Importer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

use App\Domains\Contact\Dav\Services\ImportVCard;
use App\Models\Account;
use App\Models\User;
use App\Models\Vault;
use Ramsey\Uuid\Uuid;
use Sabre\VObject\Component\VCard;

abstract class Importer implements ImportVCardResource
{
Expand All @@ -24,7 +28,23 @@ public function setContext(ImportVCard $context): ImportVCardResource
*/
protected function account(): Account
{
return $this->context->vault->account;
return $this->vault()->account;
}

/**
* Get the vault.
*/
protected function vault(): Vault
{
return $this->context->vault;
}

/**
* Get the author.
*/
protected function author(): User
{
return $this->context->author;
}

/**
Expand All @@ -34,4 +54,28 @@ protected function formatValue(?string $value): ?string
{
return ! empty($value) ? str_replace('\;', ';', trim($value)) : null;
}

/**
* Get uid of the card.
*/
protected function getUid(VCard $entry): ?string
{
if (! empty($uuid = (string) $entry->UID)) {
return $uuid;
}

return null;
}

/**
* Import UID.
*/
protected function importUid(array $data, VCard $entry): array
{
if (($uuid = $this->getUid($entry)) !== null && Uuid::isValid($uuid) && ! $this->context->external) {
$data['id'] = $uuid;
}

return $data;
}
}
54 changes: 19 additions & 35 deletions app/Domains/Contact/Dav/Jobs/UpdateVCard.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use App\Domains\Contact\Dav\Services\GetEtag;
use App\Domains\Contact\Dav\Services\ImportVCard;
use App\Domains\Contact\Dav\Web\Backend\CardDAV\CardDAVBackend;
use App\Interfaces\ServiceInterface;
use App\Services\QueuableService;
use Closure;
Expand All @@ -28,6 +27,7 @@ public function rules(): array
'vault_id' => 'required|uuid|exists:vaults,id',
'uri' => 'required|string',
'etag' => 'nullable|string',
'external' => 'nullable|boolean',
'card' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
Expand All @@ -48,6 +48,7 @@ public function permissions(): array
'author_must_belong_to_account',
'vault_must_belong_to_account',
'author_must_be_in_vault',
'author_must_be_vault_editor',
];
}

Expand All @@ -56,17 +57,15 @@ public function permissions(): array
*/
public function execute(array $data): void
{
if (! $this->batching()) {
return;
}
$this->data = $data;

$this->validateRules($data);

$this->withLocale($this->author->preferredLocale(), function () {
$newtag = $this->updateCard($this->data['uri'], $this->data['card']);

if (($etag = Arr::get($this->data, 'etag')) !== null && $newtag !== $etag) {
Log::warning(__CLASS__.' '.__FUNCTION__.' wrong etag when updating contact. Expected '.$etag.', get '.$newtag, [
if ($newtag !== null && ($etag = Arr::get($this->data, 'etag')) !== null && $newtag !== $etag) {
Log::warning(__CLASS__.' '.__FUNCTION__." wrong etag when updating contact. Expected [$etag], got [$newtag]", [
'contacturl' => $this->data['uri'],
'carddata' => $this->data['card'],
]);
Expand All @@ -76,48 +75,33 @@ public function execute(array $data): void

/**
* Update the contact with the carddata.
*
* @param string $cardUri
* @param string $cardData
*/
private function updateCard($cardUri, $cardData): ?string
private function updateCard(string $uri, mixed $card): ?string
{
$backend = app(CardDAVBackend::class, ['user' => $this->author]);

$contactId = null;
if ($cardUri) {
$contactObject = $backend->getObject($this->vault->id, $cardUri);

if ($contactObject) {
$contactId = $contactObject->id;
}
}

try {
$result = app(ImportVCard::class)
->execute([
'account_id' => $this->author->account_id,
'author_id' => $this->author->id,
'vault_id' => $this->vault->id,
'contact_id' => $contactId,
'entry' => $cardData,
'etag' => Arr::get($this->data, 'etag'),
'behaviour' => ImportVCard::BEHAVIOUR_REPLACE,
]);
$result = app(ImportVCard::class)->execute([
'account_id' => $this->author->account_id,
'author_id' => $this->author->id,
'vault_id' => $this->vault->id,
'entry' => $card,
'etag' => Arr::get($this->data, 'etag'),
'uri' => $uri,
'external' => Arr::get($this->data, 'external', false),
'behaviour' => ImportVCard::BEHAVIOUR_REPLACE,
]);

if (! Arr::has($result, 'error')) {
return app(GetEtag::class)->execute([
'account_id' => $this->author->account_id,
'author_id' => $this->author->id,
'vault_id' => $this->vault->id,
'contact_id' => $result['contact_id'],
'entry' => $result['entry'],
]);
}
} catch (\Exception $e) {
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
'contacturl' => $cardUri,
'contact_id' => $contactId,
'carddata' => $cardData,
'uri' => $uri,
'carddata' => $card,
$e,
]);
throw $e;
Expand Down
103 changes: 59 additions & 44 deletions app/Domains/Contact/Dav/Services/ExportVCard.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@

use App\Domains\Contact\Dav\ExportVCardResource;
use App\Domains\Contact\Dav\Order;
use App\Domains\Contact\Dav\VCardResource;
use App\Interfaces\ServiceInterface;
use App\Models\Contact;
use App\Models\Group;
use App\Services\BaseService;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use ReflectionClass;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\ParseException;
use Sabre\VObject\Reader;
use Symfony\Component\Finder\Finder;

class ExportVCard extends BaseService implements ServiceInterface
{
/** @var Collection<array-key,ExportVCardResource>|null */
private static ?Collection $exporters = null;

/**
* Get the validation rules that apply to the service.
*/
Expand All @@ -27,7 +30,8 @@ public function rules(): array
'account_id' => 'required|uuid|exists:accounts,id',
'author_id' => 'required|uuid|exists:users,id',
'vault_id' => 'required|uuid|exists:vaults,id',
'contact_id' => 'required|uuid|exists:contacts,id',
'contact_id' => 'required_if:group_id,null|uuid|exists:contacts,id',
'group_id' => 'required_if:contact_id,null|int|exists:groups,id',
];
}

Expand All @@ -41,92 +45,103 @@ public function permissions(): array
'vault_must_belong_to_account',
'author_must_be_in_vault',
'contact_must_belong_to_vault',
'group_must_belong_to_vault',
];
}

public function __construct(
private Application $app
) {
}

/**
* Export one VCard.
*/
public function execute(array $data): VCard
{
$this->validateRules($data);

$vcard = $this->export($this->contact);
if (isset($data['contact_id'])) {
$obj = $this->contact;
} elseif (isset($data['group_id'])) {
$obj = $this->group;
} else {
throw new ModelNotFoundException();
}

$this->contact->timestamps = false;
$this->contact->vcard = $vcard->serialize();
$this->contact->save();
$vcard = $this->export($obj);

$obj::withoutTimestamps(function () use ($obj, $vcard): void {
$obj->vcard = $vcard->serialize();
$obj->save();
});

return $vcard;
}

/**
* Export the contact.
*/
private function export(Contact $contact): VCard
private function export(VCardResource $resource): VCard
{
// The standard for most of these fields can be found on https://datatracker.ietf.org/doc/html/rfc6350
if ($contact->vcard) {
if ($resource->vcard) {
try {
/** @var VCard */
$vcard = Reader::read($contact->vcard, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES);
$vcard = Reader::read($resource->vcard, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES);
if (! $vcard->UID) {
$vcard->UID = $contact->id;
$vcard->UID = $resource->distant_uuid ?? $resource->uuid ?? $resource->id;
}
} catch (ParseException $e) {
// Ignore error
}
}

if (! isset($vcard)) {
// Basic information
$vcard = new VCard([
'UID' => $contact->id,
'SOURCE' => route('contact.show', [
'vault' => $contact->vault_id,
'contact' => $contact->id,
]),
'UID' => $resource->uuid ?? $resource->id,
'SOURCE' => $this->getSource($resource),
'VERSION' => '4.0',
]);
}

/** @var Collection<int, ExportVCardResource> */
$exporters = collect($this->exporters())
->sortBy(fn (ReflectionClass $exporter) => Order::get($exporter))
->map(fn (ReflectionClass $exporter): ExportVCardResource => $exporter->newInstance());
$exporters = $this->exporters($resource::class);

foreach ($exporters as $exporter) {
$exporter->export($contact, $vcard);
$exporter->export($resource, $vcard);
}

return $vcard;
}

private function getSource(VCardResource $vcard): string
{
if ($vcard instanceof Contact) {
return route('contact.show', [
'vault' => $vcard->vault,
'contact' => $vcard,
]);
} elseif ($vcard instanceof Group) {
return route('group.show', [
'vault' => $vcard->vault,
'group' => $vcard,
]);
} else {
throw new ModelNotFoundException();
}
}

/**
* Get exporters.
* Get exporter instances.
*
* @return \Generator<ReflectionClass>
* @param class-string $resourceClass
* @return Collection<array-key,ExportVCardResource>
*/
private function exporters()
private function exporters(string $resourceClass): Collection
{
$namespace = $this->app->getNamespace();
$appPath = app_path();

foreach ((new Finder)->files()->in($appPath)->name('*.php')->notName('helpers.php') as $file) {
$file = $namespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after($file->getRealPath(), realpath($appPath).DIRECTORY_SEPARATOR)
);

$class = new ReflectionClass($file);
if ($class->isSubclassOf(ExportVCardResource::class) && ! $class->isAbstract()) {
yield $class;
}
if (self::$exporters === null) {
self::$exporters = collect(subClasses(ExportVCardResource::class))
->sortBy(fn (ReflectionClass $exporter) => Order::get($exporter))
->map(fn (ReflectionClass $exporter): ExportVCardResource => $exporter->newInstance());
}

return self::$exporters
->filter(fn (ExportVCardResource $exporter): bool => $exporter->getType() === $resourceClass);
}
}
Loading

0 comments on commit 4ed4536

Please sign in to comment.