Skip to content

Commit 8790a7f

Browse files
committed
feat: import contacts from files via ocs
Signed-off-by: Richard Steinmetz <[email protected]>
1 parent 487b023 commit 8790a7f

File tree

2 files changed

+543
-0
lines changed

2 files changed

+543
-0
lines changed

lib/Controller/ImportController.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Contacts\Controller;
11+
12+
use OCA\Contacts\AppInfo\Application;
13+
use OCP\AppFramework\Http;
14+
use OCP\AppFramework\Http\Attribute\ApiRoute;
15+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
16+
use OCP\AppFramework\Http\DataResponse;
17+
use OCP\AppFramework\OCSController;
18+
use OCP\Contacts\IManager as IContactsManager;
19+
use OCP\Files\File;
20+
use OCP\Files\IRootFolder;
21+
use OCP\ICreateContactFromString;
22+
use OCP\IRequest;
23+
use OCP\IUserSession;
24+
use OCP\Security\ISecureRandom;
25+
26+
class ImportController extends OCSController {
27+
private const UID_PREFIX = 'UID:';
28+
29+
public function __construct(
30+
IRequest $request,
31+
private readonly IUserSession $userSession,
32+
private readonly IContactsManager $contactsManager,
33+
private readonly IRootFolder $rootFolder,
34+
private readonly ISecureRandom $random,
35+
) {
36+
parent::__construct(Application::APP_ID, $request);
37+
}
38+
39+
/**
40+
* Import the given vCard file (by id) into the given address book of the current user.
41+
* Exactly one of $addressBookKey or $addressBookUri is expected.
42+
*
43+
* @param int $fileId The id of a vCard file to import
44+
* @param ?string $addressBookKey The key or id of the address book - {@see \OCP\IAddressBook::getKey}
45+
* @param ?string $addressBookUri The uri of the address book - {@see \OCP\IAddressBook::getUri}
46+
* @return DataResponse
47+
*
48+
* 200: Contacts were imported successfully
49+
* 400: Not a vCard file or given both $addressBookKey and $addressBookUri
50+
* 401: User is not logged in
51+
* 404: File or address book was not found
52+
*/
53+
#[NoAdminRequired]
54+
#[ApiRoute('POST', '/api/v1/import')]
55+
public function import(
56+
int $fileId,
57+
?string $addressBookKey = null,
58+
?string $addressBookUri = null,
59+
): DataResponse {
60+
if (empty($addressBookKey) === empty($addressBookUri)) {
61+
return new DataResponse(
62+
'Expected one of addressBookKey or addressBookUri',
63+
Http::STATUS_BAD_REQUEST,
64+
);
65+
}
66+
67+
$user = $this->userSession->getUser();
68+
if ($user === null) {
69+
return new DataResponse('Not logged in', Http::STATUS_UNAUTHORIZED);
70+
}
71+
72+
$addressBook = $this->findUserAddressBook($addressBookKey, $addressBookUri);
73+
if ($addressBook === null) {
74+
return new DataResponse('Address book not found', Http::STATUS_NOT_FOUND);
75+
}
76+
77+
$userRoot = $this->rootFolder->getUserFolder($user->getUID());
78+
$file = $userRoot->getFirstNodeById($fileId);
79+
if ($file === null) {
80+
return new DataResponse('File not found', Http::STATUS_NOT_FOUND);
81+
}
82+
83+
if (!($file instanceof File)) {
84+
return new DataResponse('Not a file', Http::STATUS_BAD_REQUEST);
85+
}
86+
87+
if ($file->getMimetype() !== 'text/vcard' && $file->getExtension() !== 'vcf') {
88+
return new DataResponse('Not a vCard file', Http::STATUS_BAD_REQUEST);
89+
}
90+
91+
$contacts = [];
92+
$currentContact = null;
93+
94+
// The vcf file might contain multiple contacts -> split each vcard
95+
$vcf = $file->getContent();
96+
$vcfLines = explode("\n", $vcf);
97+
foreach ($vcfLines as $line) {
98+
$line = rtrim($line, "\r");
99+
100+
if ($line === 'BEGIN:VCARD') {
101+
$currentContact = [$line];
102+
continue;
103+
}
104+
105+
if ($line === 'END:VCARD') {
106+
$currentContact[] = $line;
107+
$contacts[] = implode("\n", $currentContact);
108+
$currentContact = null;
109+
continue;
110+
}
111+
112+
if ($currentContact === null) {
113+
continue;
114+
}
115+
116+
$currentContact[] = $line;
117+
}
118+
119+
$imported = [];
120+
foreach ($contacts as $contact) {
121+
$uid = $this->random->generate(
122+
32,
123+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
124+
);
125+
$name = "$uid.vcf";
126+
try {
127+
$addressBook->createFromString($name, $contact);
128+
} catch (\Exception $e) {
129+
continue;
130+
}
131+
132+
$imported[] = $name;
133+
}
134+
135+
return new DataResponse([
136+
'importedContactUris' => $imported,
137+
]);
138+
}
139+
140+
private function findUserAddressBook(?string $key, ?string $uri): ?ICreateContactFromString {
141+
$addressBooks = $this->contactsManager->getUserAddressBooks();
142+
foreach ($addressBooks as $addressBook) {
143+
if (!($addressBook instanceof ICreateContactFromString)) {
144+
continue;
145+
}
146+
147+
if ($addressBook->getKey() === $key || $addressBook->getUri() === $uri) {
148+
return $addressBook;
149+
}
150+
}
151+
152+
return null;
153+
}
154+
}

0 commit comments

Comments
 (0)