Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Please note that this version is still in its early stages.

The following data types are currently supported by the JMAP Plugin for Nextcloud:

* Contacts over the JMAP for Contacts protocol ([RFC 9610](https://datatracker.ietf.org/doc/html/rfc9610))
* Contacts over the JMAP for Contacts protocol ([RFC 9553](https://datatracker.ietf.org/doc/rfc9553/))
* Calendars over the JMAP for Calendars protocol ([draft-ietf-jmap-calendars-22](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-calendars-22)), built on top of the JSCalendar ([RFC 8984](https://datatracker.ietf.org/doc/html/rfc8984)) format

## 🏗 Installation
Expand Down
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
],
"require": {
"php": ">=5.6.0",
"audriga/jmap-openxport": "~1",
"audriga/jmap-icalendar_vcard": ">=0.0.1",
"audriga/jmap-openxport": "dev-7676-ietf_implementation",
"audriga/jmap-icalendar_vcard": "dev-7676-support_new_ietf",
"sabre/xml": "~2"
},
"require-dev": {
"christophwurst/nextcloud_testing": "^1.0.0",
"squizlabs/php_codesniffer": "^3"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"classmap": ["lib/"]
},
Expand All @@ -40,4 +42,4 @@
"archive": {
"exclude": ["/tests", "/build", "/README.md"]
}
}
}
276 changes: 164 additions & 112 deletions composer.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion config/config.default.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
'adminGroups' => array(''),

// Enabled capabilities for this endpoint
'capabilities' => array('jscontact', 'calendars'),
'capabilities' => array('calendars', 'contactCard'),

// ********************** //
/// Logging configuration
Expand Down
6 changes: 3 additions & 3 deletions lib/Controller/JmapController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private function init()
}

$accessors = array(
"Contacts" => new \OpenXPort\DataAccess\NextcloudContactDataAccess(
"ContactCard" => new \OpenXPort\DataAccess\NextcloudContactDataAccess(
$this->cardDavBackend,
$this->userSession
),
Expand All @@ -80,7 +80,7 @@ private function init()
);

$adapters = array(
"Contacts" => new \OpenXPort\Adapter\NextcloudJSContactVCardAdapter(),
"ContactCard" => new \OpenXPort\Adapter\NextcloudJSContactVCardAdapter(),
"AddressBooks" => new \OpenXPort\Adapter\NextcloudAddressbookAdapter(),
"Calendars" => new \OpenXPort\Adapter\NextcloudCalendarAdapter(),
"CalendarEvents" => new \OpenXPort\Adapter\JSCalendarICalendarAdapter(),
Expand All @@ -94,7 +94,7 @@ private function init()
);

$mappers = array(
"Contacts" => new \OpenXPort\Mapper\VCardMapper(),
"ContactCard" => new \OpenXPort\Mapper\JSContactVCardMapper(),
"AddressBooks" => new \OpenXPort\Mapper\NextcloudAddressbookMapper(),
"Calendars" => new \OpenXPort\Mapper\NextcloudCalendarMapper(),
"CalendarEvents" => new \OpenXPort\Mapper\JSCalendarICalendarMapper(),
Expand Down
3 changes: 2 additions & 1 deletion lib/jmap/adapter/NextcloudAddressbookAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ public function getId()

public function getName()
{
return $this->addressbook['uri'];
return $this->addressbook['{DAV:}displayname'] ?? $this->addressbook['uri'];
}


public function setName($name)
{
$this->addressbook['uri'] = $name;
Expand Down
4 changes: 2 additions & 2 deletions lib/jmap/adapter/NextcloudCalendarAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function getId()

public function getName()
{
return $this->calendar["uri"];
return $this->calendar["{DAV:}displayname"] ?? $this->calendar["uri"];
}

public function setName($name)
Expand All @@ -55,4 +55,4 @@ public function setColor($color)
{
$this->calendar["color"] = $color;
}
}
}
160 changes: 158 additions & 2 deletions lib/jmap/data_access/NextcloudAddressbookDataAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,26 @@ public function getAll($accountId = null)

public function get($ids, $accountId = null)
{
// TODO: Implement me
if (is_null($ids) || empty($ids)) {
$this->logger->warning("No IDs provided for get operation");
return [];
}

$this->logger->info("Getting " . count($ids) . " address books for user " . $this->principalUri);

$result = [];

foreach ($ids as $id) {
$addressbook = $this->backend->getAddressBookById($id);

if ($addressbook && $addressbook['principaluri'] === $this->principalUri) {
$result[$id] = $addressbook;
} else {
$this->logger->warning("Address book not found or access denied: " . $id);
}
}

return $result;
}

/**
Expand Down Expand Up @@ -116,6 +135,143 @@ public function destroy($ids, $accountId = null)

public function query($accountId, $filter = null)
{
// TODO: Implement me
$db = \OC::$server->getDatabaseConnection();

$sql = 'SELECT id FROM `oc_addressbooks` WHERE `principaluri` = ?';
$queryParams = array($this->principalUri);

if (!is_null($filter)) {
if (is_object($filter)) {
$filter = json_decode(json_encode($filter), true);
}

if (is_array($filter)) {
// Filter by name (displayname in database)
if (isset($filter['name'])) {
$sql .= ' AND `displayname` = ?';
array_push($queryParams, $filter['name']);
}

// Filter by URI
if (isset($filter['uri'])) {
$sql .= ' AND `uri` = ?';
array_push($queryParams, $filter['uri']);
}
}
}

$result = $db->executeQuery($sql, $queryParams);
$addressbooks = $result->fetchAll();

$ids = array_column($addressbooks, 'id');

return $ids;
}

public function update($addressbooksToUpdate, $accountId = null)
{
if (is_null($addressbooksToUpdate)) {
return [];
}

$this->logger->info("Updating " . count($addressbooksToUpdate) . " address books");
$addressbookMap = [];
// Map JMAP property names to WebDAV/CardDAV property names with XML namespaces
// as defined in RFC 6352 (CardDAV)
$propertyMap = [
'name' => '{DAV:}displayname',
'description' => '{urn:ietf:params:xml:ns:carddav}addressbook-description'
];

foreach ($addressbooksToUpdate as $id => $data) {
try {
$addressbook = $this->backend->getAddressBookById($id);

if (!$addressbook || $addressbook['principaluri'] !== $this->principalUri) {
$this->logger->error("Address book not found or access denied: $id");
$addressbookMap[$id] = false;
continue;
}
if (is_object($data)) {
$data = json_decode(json_encode($data), true);
}

$mutations = [];

foreach ($propertyMap as $jmapKey => $caldavKey) {
if (isset($data[$jmapKey])) {
$mutations[$caldavKey] = $data[$jmapKey];
}
}

if (!empty($mutations)) {
$propPatch = new \Sabre\DAV\PropPatch($mutations);
$this->backend->updateAddressBook($id, $propPatch);
$propPatch->commit();

$propPatchResult = $propPatch->getResult();
$allSucceeded = true;
foreach ($propPatchResult as $prop => $code) {
if ($code !== 200 && $code !== 204) {
$allSucceeded = false;
}
}

$addressbookMap[$id] = $allSucceeded;
} else {
$addressbookMap[$id] = false;
}
} catch (\Exception $e) {
$this->logger->error("Failed to update address book $id: " . $e->getMessage());
$addressbookMap[$id] = false;
}
}

return $addressbookMap;
}

/**
* Get changes for address books
*
* Note: Nextcloud does not track address book metadata changes in a separate table.
* This only detects if the state changed, not which address books were affected.
* Returns empty arrays for created/updated/destroyed.
*/
public function getChanges($sinceState, $maxChanges = 500, $accountId = null)
{
$currentState = $this->getCurrentState($accountId);

return [
'newState' => $currentState,
'hasMoreChanges' => false,
'created' => [],
'updated' => [],
'destroyed' => []
];
}

/**
* Get current state for address books
* Returns the maximum synctoken from user's address books
*/
public function getCurrentState($accountId = null)
{
try {
$db = \OC::$server->getDatabaseConnection();

$query = "SELECT MAX(synctoken) as current_state
FROM oc_addressbooks
WHERE principaluri = ?";

$stmt = $db->prepare($query);
$stmt->execute([$this->principalUri]);
$result = $stmt->fetch();

return $result && $result['current_state'] ? (string)$result['current_state'] : "0";

} catch (\Exception $e) {
$this->logger->error("Failed to get current state: " . $e->getMessage());
return "0";
}
}
}
Loading