diff options
author | Mario <mario@mariovavti.com> | 2020-02-20 20:03:50 +0100 |
---|---|---|
committer | Mario <mario@mariovavti.com> | 2020-02-20 20:03:50 +0100 |
commit | b9a8b9e47b33c697ab2a5a460187ca53443bb06a (patch) | |
tree | a2eabf77fff7a9700d7deb451baccf000a86a027 | |
parent | 7d05b8e5fdcf628f6badaff60c73811dcc19e45c (diff) | |
parent | b7bac45427a400275597faa9b51c4d277fe1f5c7 (diff) | |
download | volse-hubzilla-b9a8b9e47b33c697ab2a5a460187ca53443bb06a.tar.gz volse-hubzilla-b9a8b9e47b33c697ab2a5a460187ca53443bb06a.tar.bz2 volse-hubzilla-b9a8b9e47b33c697ab2a5a460187ca53443bb06a.zip |
Merge branch 'dev' into 'dev'
Implement CardDAV address book sync with clones
See merge request hubzilla/core!1829
-rw-r--r-- | Zotlabs/Lib/Libsync.php | 5 | ||||
-rw-r--r-- | Zotlabs/Module/Cdav.php | 417 | ||||
-rw-r--r-- | include/cdav.php | 186 | ||||
-rw-r--r-- | include/import.php | 67 | ||||
-rw-r--r-- | include/zot.php | 3 |
5 files changed, 443 insertions, 235 deletions
diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php index b9e9bb38a..d1756cf47 100644 --- a/Zotlabs/Lib/Libsync.php +++ b/Zotlabs/Lib/Libsync.php @@ -244,7 +244,10 @@ class Libsync { if(array_key_exists('app',$arr) && $arr['app']) sync_apps($channel,$arr['app']); - + + if(array_key_exists('addressbook',$arr) && $arr['addressbook']) + sync_addressbook($channel,$arr['addressbook']); + if(array_key_exists('chatroom',$arr) && $arr['chatroom']) sync_chatrooms($channel,$arr['chatroom']); diff --git a/Zotlabs/Module/Cdav.php b/Zotlabs/Module/Cdav.php index af40689c1..593a78a53 100644 --- a/Zotlabs/Module/Cdav.php +++ b/Zotlabs/Module/Cdav.php @@ -10,6 +10,7 @@ require_once('include/event.php'); require_once('include/auth.php'); require_once('include/security.php'); +require_once('include/cdav.php'); class Cdav extends Controller { @@ -156,6 +157,69 @@ class Cdav extends Controller { } } + + // Track CDAV updates from remote clients + + $httpmethod = $_SERVER['REQUEST_METHOD']; + + if($httpmethod === 'PUT' || $httpmethod === 'DELETE') { + + $httpuri = $_SERVER['REQUEST_URI']; + + logger("debug: method: " . $httpmethod, LOGGER_DEBUG); + logger("debug: uri: " . $httpuri, LOGGER_DEBUG); + + // currently we process CardDAV requests only + if(strpos($httpuri, 'cdav/addressbooks')) { + + $uri = basename($httpuri); + $httpbody = file_get_contents('php://input'); + + logger("debug: body: " . $httpbody, LOGGER_DEBUG); + + if($id = get_cdav_id($principalUri, explode("/", $httpuri)[4], 'addressbooks')) { + + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); + + $etag = (isset($_SERVER['HTTP_IF_MATCH']) ? $_SERVER['HTTP_IF_MATCH'] : false); + + // delete + if($httpmethod === 'DELETE' && $cdavdata['etag'] == $etag) + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'delete_card', + 'uri' => $cdavdata['uri'], + 'carduri' => $uri + ] + ]); + else { + if($etag) { + // update + if($cdavdata['etag'] !== $etag) + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'update_card', + 'uri' => $cdavdata['uri'], + 'carduri' => $uri, + 'card' => $httpbody + ] + ]); + } + else { + // new + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'import', + 'uri' => $cdavdata['uri'], + 'ids' => [ $uri ], + 'card' => $httpbody + ] + ]); + } + } + } + } + } $principalBackend = new \Sabre\DAVACL\PrincipalBackend\PDO($pdo); @@ -523,6 +587,14 @@ class Cdav extends Controller { $properties = ['{DAV:}displayname' => $_REQUEST['{DAV:}displayname']]; $carddavBackend->createAddressBook($principalUri, $addressbookUri, $properties); + + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'create', + 'uri' => $addressbookUri, + 'properties' => $properties + ] + ]); } //edit addressbook @@ -530,6 +602,8 @@ class Cdav extends Controller { $id = $_REQUEST['id']; + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); + if(! cdav_perms($id,$addressbooks)) return; @@ -538,16 +612,24 @@ class Cdav extends Controller { ]; $patch = new \Sabre\DAV\PropPatch($mutations); - $carddavBackend->updateAddressBook($id, $patch); - $patch->commit(); + + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'edit', + 'uri' => $cdavdata['uri'], + 'mutations' => $mutations, + ] + ]); } //create addressbook card if($_REQUEST['create'] && $_REQUEST['target'] && $_REQUEST['fn']) { $id = $_REQUEST['target']; + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); + do { $duplicate = false; $uri = random_string(40) . '.vcf'; @@ -569,86 +651,22 @@ class Cdav extends Controller { 'N' => array_reverse(explode(' ', $fn)) ]); - $org = $_REQUEST['org']; - if($org) { - $vcard->ORG = $org; - } + $fields = $this->request_to_array($_REQUEST); - $title = $_REQUEST['title']; - if($title) { - $vcard->TITLE = $title; - } - - $tel = $_REQUEST['tel']; - $tel_type = $_REQUEST['tel_type']; - if($tel) { - $i = 0; - foreach($tel as $item) { - if($item) { - $vcard->add('TEL', $item, ['type' => $tel_type[$i]]); - } - $i++; - } - } - - $email = $_REQUEST['email']; - $email_type = $_REQUEST['email_type']; - if($email) { - $i = 0; - foreach($email as $item) { - if($item) { - $vcard->add('EMAIL', $item, ['type' => $email_type[$i]]); - } - $i++; - } - } - - $impp = $_REQUEST['impp']; - $impp_type = $_REQUEST['impp_type']; - if($impp) { - $i = 0; - foreach($impp as $item) { - if($item) { - $vcard->add('IMPP', $item, ['type' => $impp_type[$i]]); - } - $i++; - } - } - - $url = $_REQUEST['url']; - $url_type = $_REQUEST['url_type']; - if($url) { - $i = 0; - foreach($url as $item) { - if($item) { - $vcard->add('URL', $item, ['type' => $url_type[$i]]); - } - $i++; - } - } - - $adr = $_REQUEST['adr']; - $adr_type = $_REQUEST['adr_type']; - - if($adr) { - $i = 0; - foreach($adr as $item) { - if($item) { - $vcard->add('ADR', $item, ['type' => $adr_type[$i]]); - } - $i++; - } - } - - $note = $_REQUEST['note']; - if($note) { - $vcard->NOTE = $note; - } + process_cdav_card($fields, $vcard); $cardData = $vcard->serialize(); $carddavBackend->createCard($id, $uri, $cardData); + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'import', + 'uri' => $cdavdata['uri'], + 'ids' => [ $uri ], + 'card' => $cardData + ] + ]); } //edit addressbook card @@ -656,6 +674,8 @@ class Cdav extends Controller { $id = $_REQUEST['target']; + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); + if(!cdav_perms($id,$addressbooks)) return; @@ -670,113 +690,23 @@ class Cdav extends Controller { $vcard->N = array_reverse(explode(' ', $fn)); } - $org = $_REQUEST['org']; - if($org) { - $vcard->ORG = $org; - } - else { - unset($vcard->ORG); - } + $fields = $this->request_to_array($_REQUEST); - $title = $_REQUEST['title']; - if($title) { - $vcard->TITLE = $title; - } - else { - unset($vcard->TITLE); - } - - $tel = $_REQUEST['tel']; - $tel_type = $_REQUEST['tel_type']; - if($tel) { - $i = 0; - unset($vcard->TEL); - foreach($tel as $item) { - if($item) { - $vcard->add('TEL', $item, ['type' => $tel_type[$i]]); - } - $i++; - } - } - else { - unset($vcard->TEL); - } - - $email = $_REQUEST['email']; - $email_type = $_REQUEST['email_type']; - if($email) { - $i = 0; - unset($vcard->EMAIL); - foreach($email as $item) { - if($item) { - $vcard->add('EMAIL', $item, ['type' => $email_type[$i]]); - } - $i++; - } - } - else { - unset($vcard->EMAIL); - } - - $impp = $_REQUEST['impp']; - $impp_type = $_REQUEST['impp_type']; - if($impp) { - $i = 0; - unset($vcard->IMPP); - foreach($impp as $item) { - if($item) { - $vcard->add('IMPP', $item, ['type' => $impp_type[$i]]); - } - $i++; - } - } - else { - unset($vcard->IMPP); - } - - $url = $_REQUEST['url']; - $url_type = $_REQUEST['url_type']; - if($url) { - $i = 0; - unset($vcard->URL); - foreach($url as $item) { - if($item) { - $vcard->add('URL', $item, ['type' => $url_type[$i]]); - } - $i++; - } - } - else { - unset($vcard->URL); - } - - $adr = $_REQUEST['adr']; - $adr_type = $_REQUEST['adr_type']; - if($adr) { - $i = 0; - unset($vcard->ADR); - foreach($adr as $item) { - if($item) { - $vcard->add('ADR', $item, ['type' => $adr_type[$i]]); - } - $i++; - } - } - else { - unset($vcard->ADR); - } - - $note = $_REQUEST['note']; - if($note) { - $vcard->NOTE = $note; - } - else { - unset($vcard->NOTE); - } + process_cdav_card($fields, $vcard, true); $cardData = $vcard->serialize(); $carddavBackend->updateCard($id, $uri, $cardData); + + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'update_card', + 'uri' => $cdavdata['uri'], + 'carduri' => $uri, + 'card' => $cardData + ] + ]); + } //delete addressbook card @@ -784,12 +714,22 @@ class Cdav extends Controller { $id = $_REQUEST['target']; + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); + if(!cdav_perms($id,$addressbooks)) return; $uri = $_REQUEST['uri']; $carddavBackend->deleteCard($id, $uri); + + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'delete_card', + 'uri' => $cdavdata['uri'], + 'carduri' => $uri + ] + ]); } } @@ -799,6 +739,8 @@ class Cdav extends Controller { $src = $_FILES['userfile']['tmp_name']; if($src) { + + $carddata = @file_get_contents($src); if($_REQUEST['c_upload']) { if($_REQUEST['target'] == 'channel_calendar') { @@ -812,76 +754,39 @@ class Cdav extends Controller { return; } - $id = explode(':', $_REQUEST['target']); + $id = explode(':', $_REQUEST['target'])[0]; $ext = 'ics'; $table = 'calendarobjects'; $column = 'calendarid'; - $objects = new \Sabre\VObject\Splitter\ICalendar(@file_get_contents($src)); + $objects = new \Sabre\VObject\Splitter\ICalendar($carddata); $profile = \Sabre\VObject\Node::PROFILE_CALDAV; $backend = new \Sabre\CalDAV\Backend\PDO($pdo); } if($_REQUEST['a_upload']) { - $id[] = intval($_REQUEST['target']); + $id = intval($_REQUEST['target']); $ext = 'vcf'; $table = 'cards'; $column = 'addressbookid'; - $objects = new \Sabre\VObject\Splitter\VCard(@file_get_contents($src)); + $objects = new \Sabre\VObject\Splitter\VCard($carddata); $profile = \Sabre\VObject\Node::PROFILE_CARDDAV; $backend = new \Sabre\CardDAV\Backend\PDO($pdo); + + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); } - - while ($object = $objects->getNext()) { - - if($_REQUEST['a_upload']) { - $object = $object->convert(\Sabre\VObject\Document::VCARD40); - } - - $ret = $object->validate($profile & \Sabre\VObject\Node::REPAIR); - - //level 3 Means that the document is invalid, - //level 2 means a warning. A warning means it's valid but it could cause interopability issues, - //level 1 means that there was a problem earlier, but the problem was automatically repaired. - - if($ret[0]['level'] < 3) { - do { - $duplicate = false; - $objectUri = random_string(40) . '.' . $ext; - - $r = q("SELECT uri FROM $table WHERE $column = %d AND uri = '%s' LIMIT 1", - dbesc($id[0]), - dbesc($objectUri) - ); - - if (count($r)) - $duplicate = true; - } while ($duplicate == true); - - if($_REQUEST['c_upload']) { - $backend->createCalendarObject($id, $objectUri, $object->serialize()); - } - - if($_REQUEST['a_upload']) { - $backend->createCard($id[0], $objectUri, $object->serialize()); - } - } - else { - if($_REQUEST['c_upload']) { - notice( '<strong>' . t('INVALID EVENT DISMISSED!') . '</strong>' . EOL . - '<strong>' . t('Summary: ') . '</strong>' . (($object->VEVENT->SUMMARY) ? $object->VEVENT->SUMMARY : t('Unknown')) . EOL . - '<strong>' . t('Date: ') . '</strong>' . (($object->VEVENT->DTSTART) ? $object->VEVENT->DTSTART : t('Unknown')) . EOL . - '<strong>' . t('Reason: ') . '</strong>' . $ret[0]['message'] . EOL - ); - } - - if($_REQUEST['a_upload']) { - notice( '<strong>' . t('INVALID CARD DISMISSED!') . '</strong>' . EOL . - '<strong>' . t('Name: ') . '</strong>' . (($object->FN) ? $object->FN : t('Unknown')) . EOL . - '<strong>' . t('Reason: ') . '</strong>' . $ret[0]['message'] . EOL - ); - } - } - } + + $ids = []; + import_cdav_card($id, $ext, $table, $column, $objects, $profile, $backend, $ids, true); + + if(isset($cdavdata)) + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'import', + 'uri' => $cdavdata['uri'], + 'ids' => $ids, + 'card' => $carddata + ] + ]); } @unlink($src); } @@ -1408,7 +1313,19 @@ class Cdav extends Controller { if(! cdav_perms($id,$addressbooks)) return; + // get metadata before we delete it + $cdavdata = $this->get_cdav_data($id, 'addressbooks'); + $carddavBackend->deleteAddressBook($id); + + if($cdavdata) + build_sync_packet($channel['channel_id'], [ + 'addressbook' => [ + 'action' => 'drop', + 'uri' => $cdavdata['uri'] + ] + ]); + killme(); } @@ -1460,4 +1377,36 @@ class Cdav extends Controller { } + function get_cdav_data($id, $table) { + + $r = q("SELECT * FROM $table WHERE id = %d LIMIT 1", + intval($id) + ); + + if(! $r) + return false; + + return $r[0]; + } + + function request_to_array($req) { + + $f = []; + + $f['org'] = $req['org']; + $f['title'] = $req['title']; + $f['tel'] = $req['tel']; + $f['tel_type'] = $req['tel_type']; + $f['email'] = $req['email']; + $f['email_type'] = $req['email_type']; + $f['impp'] = $req['impp']; + $f['impp_type'] = $req['impp_type']; + $f['url'] = $req['url']; + $f['url_type'] = $req['url_type']; + $f['adr'] = $req['adr']; + $f['adr_type'] = $req['adr_type']; + $f['note'] = $req['note']; + + return $f; + } } diff --git a/include/cdav.php b/include/cdav.php new file mode 100644 index 000000000..d72caa442 --- /dev/null +++ b/include/cdav.php @@ -0,0 +1,186 @@ +<?php + +/** + * @brief Process CardDAV card + * + * @param array $f fields array + * @param obj $vcard SabreDAV object + * @param bool $edit update card + * + */ + +function process_cdav_card($f, &$vcard, $edit = false) { + + if($f['org']) + $vcard->ORG = $f['org']; + else + if($edit) + unset($vcard->ORG); + + + if($f['title']) + $vcard->TITLE = $f['title']; + else + if($edit) + unset($vcard->TITLE); + + if($edit) + unset($vcard->TEL); + if($f['tel']) { + $i = 0; + foreach($f['tel'] as $item) { + if($item) { + $vcard->add('TEL', $item, ['type' => $f['tel_type'][$i]]); + } + $i++; + } + } + + if($edit) + unset($vcard->EMAIL); + if($f['email']) { + $i = 0; + foreach($f['email'] as $item) { + if($item) { + $vcard->add('EMAIL', $item, ['type' => $f['email_type'][$i]]); + } + $i++; + } + } + + if($edit) + unset($vcard->IMPP); + if($f['impp']) { + $i = 0; + foreach($f['impp'] as $item) { + if($item) { + $vcard->add('IMPP', $item, ['type' => $f['impp_type'][$i]]); + } + $i++; + } + } + + if($edit) + unset($vcard->URL); + if($f['url']) { + $i = 0; + foreach($f['url'] as $item) { + if($item) { + $vcard->add('URL', $item, ['type' => $f['url_type'][$i]]); + } + $i++; + } + } + + if($edit) + unset($vcard->ADR); + if($f['adr']) { + $i = 0; + foreach($f['adr'] as $item) { + if($item) { + $vcard->add('ADR', $item, ['type' => $f['adr_type'][$i]]); + } + $i++; + } + } + + if($f['note']) { + $vcard->NOTE = $f['note']; + } + else + if($edit) + unset($vcard->NOTE); +} + + +/** + * @brief Import CardDAV or CalDAV card + * + * @param int $id card id + * @param str $ext card extension + * @param str $table name + * @param str $column name + * @param obj $objects + * @param str $profile + * @param obj $backend + * @param array $ids + * @param bool $notice + * + */ + +function import_cdav_card($id, $ext, $table, $column, $objects, $profile, $backend, &$ids, $notice = false) { + + $i = 0; + $newid = (count($ids) ? false : true); + + while ($object = $objects->getNext()) { + + if($_REQUEST['a_upload']) + $object = $object->convert(\Sabre\VObject\Document::VCARD40); + + $ret = $object->validate($profile & \Sabre\VObject\Node::REPAIR); + + //level 3 Means that the document is invalid, + //level 2 means a warning. A warning means it's valid but it could cause interopability issues, + //level 1 means that there was a problem earlier, but the problem was automatically repaired. + + if($ret[0]['level'] < 3) { + + if($newid) { + do { + $duplicate = false; + $objectUri = random_string(40) . '.' . $ext; + + $r = q("SELECT uri FROM $table WHERE $column = %d AND uri = '%s' LIMIT 1", + dbesc($id), + dbesc($objectUri) + ); + if (count($r)) + $duplicate = true; + } while ($duplicate == true); + $ids[$i] = $objectUri; + } + else + $objectUri = $ids[$i]; + + $i++; + + if($ext == 'ics') + $backend->createCalendarObject($id, $objectUri, $object->serialize()); + + if($ext == 'vcf') + $backend->createCard($id, $objectUri, $object->serialize()); + } + else { + if($notice && $ext == 'ics') { + notice( + '<strong>' . t('INVALID EVENT DISMISSED!') . '</strong>' . EOL . + '<strong>' . t('Summary: ') . '</strong>' . (($object->VEVENT->SUMMARY) ? $object->VEVENT->SUMMARY : t('Unknown')) . EOL . + '<strong>' . t('Date: ') . '</strong>' . (($object->VEVENT->DTSTART) ? $object->VEVENT->DTSTART : t('Unknown')) . EOL . + '<strong>' . t('Reason: ') . '</strong>' . $ret[0]['message'] . EOL + ); + } + + if($notice && $exp == 'vcf') { + notice( + '<strong>' . t('INVALID CARD DISMISSED!') . '</strong>' . EOL . + '<strong>' . t('Name: ') . '</strong>' . (($object->FN) ? $object->FN : t('Unknown')) . EOL . + '<strong>' . t('Reason: ') . '</strong>' . $ret[0]['message'] . EOL + ); + } + } + } +} + + +function get_cdav_id($principaluri, $uri, $table) { + + $r = q("SELECT id FROM $table WHERE principaluri = '%s' AND uri = '%s' LIMIT 1", + dbesc($principaluri), + dbesc($uri) + ); + if(! $r) + return false; + + return $r[0]['id']; +} diff --git a/include/import.php b/include/import.php index 6a3895b9f..0519052d8 100644 --- a/include/import.php +++ b/include/import.php @@ -1504,6 +1504,73 @@ function sync_files($channel, $files) { } /** + * @brief Synchronize addressbooks. + * + * @param array $channel + * @param array $data + */ +function sync_addressbook($channel, $data) { + + if(! \Zotlabs\Lib\Apps::system_app_installed($channel['channel_id'], 'CardDAV')) + return; + + logger("debug: " . print_r($data,true), LOGGER_DEBUG); + + require_once('include/cdav.php'); + + $principalUri = 'principals/' . $channel['channel_address']; + + if($data['action'] !== 'create') { + $id = get_cdav_id($principalUri, $data['uri'], 'addressbooks'); + if(! $id) + return; + } + + $pdo = \DBA::$dba->db; + + $carddavBackend = new \Sabre\CardDAV\Backend\PDO($pdo); + $addressbooks = $carddavBackend->getAddressBooksForUser($principalUri); + + switch($data['action']) { + + case 'create': + $carddavBackend->createAddressBook($principalUri, $data['uri'], $data['properties']); + break; + + case 'drop': + $carddavBackend->deleteAddressBook($id); + break; + + case 'edit': + $patch = new \Sabre\DAV\PropPatch($data['mutations']); + $carddavBackend->updateAddressBook($id, $patch); + $patch->commit(); + break; + + case 'delete_card': + $carddavBackend->deleteCard($id, $data['carduri']); + break; + + case 'update_card': + $vcard = \Sabre\VObject\Reader::read($data['card']); + $object = $vcard->convert(\Sabre\VObject\Document::VCARD40); + $cardData = $vcard->serialize(); + $carddavBackend->updateCard($id, $data['carduri'], $cardData); + break; + + case 'import': + $objects = new \Sabre\VObject\Splitter\VCard($data['card']); + $profile = \Sabre\VObject\Node::PROFILE_CARDDAV; + import_cdav_card($id, 'vcf', 'cards', 'addressbookid', $objects, $profile, $carddavBackend, $data['ids']); + break; + + default: + break; + } +} + + +/** * @brief Rename a key in an array. * * Replaces $old key with $new key in $arr. diff --git a/include/zot.php b/include/zot.php index d08146287..c02dab162 100644 --- a/include/zot.php +++ b/include/zot.php @@ -3611,6 +3611,9 @@ function process_channel_sync_delivery($sender, $arr, $deliveries) { if(array_key_exists('app',$arr) && $arr['app']) sync_apps($channel,$arr['app']); + if(array_key_exists('addressbook',$arr) && $arr['addressbook']) + sync_addressbook($channel,$arr['addressbook']); + if(array_key_exists('chatroom',$arr) && $arr['chatroom']) sync_chatrooms($channel,$arr['chatroom']); |