Commit 5a63bfc2 authored by Sebastian Kummer's avatar Sebastian Kummer

Merge pull request #121 in ZP/z-push from feature/ZP-684-New-backend-CardDAV to develop

* commit '610cf709':
  ZP-684 New backend CardDAV. Released under the Affero GNU General Public License (AGPL) version 3.
parents 9eb72b3b 610cf709
This is a CardDAV backend based in the vcarddir backend.
It supports DAViCal, Sogo, OwnCloud, SabreDav... and should works with any carddav server. So if it doesn't work with your server, please open a issue.
It supports ChangesSink method that will detect and send faster changes to your device.
DAViCal implements the SYNC operation, it's a very fast method to detect changes in your vcards.
The others servers don't implement it, so the code will fallback to a slower method (suggest your carddav server developers to implement it!!).
This is controlled with a flag in the config.php file.
Also, it can autodetect multiple addressbooks and will present them to the mobile device as an unique addressbook (only iOS supports multiple addressbook).
REQUIREMENTS:
php-curl
php-xsl
CardDAV server (DAViCal, Sabredav, Sogo, Owncloud...)
\ No newline at end of file
*Drenalina SRL (www.drenalina.com)* sponsored the development of the following features in the BackendCardDAV, any existing bug it's my fault not theirs ;-)
Thank you very much for helping to improve it!!
- Autodetecting addressbooks within a DAV principal.
- Merging multiple addressbooks so the device will see a unique one. Only iOS based devices support multiple addressbooks, so we will merge them for now.
- Selecting default addressbook to store new contacts created from the device.
- GAL addressbook and GAL search.
\ No newline at end of file
<?php
/***********************************************
* File : carddav.php
* Project : Z-Push
* Descr : This backend is for carddav servers.
*
* Created : 16.03.2013
*
* Copyright 2013 - 2016 Francisco Miguel Biete
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
// config file
require_once("backend/carddav/config.php");
// TODO: remove this after ZP-682 merge
require_once("include/z_carddav.php");
class BackendCardDAV extends BackendDiff implements ISearchProvider {
private $domain = '';
private $username = '';
private $url = null;
/**
* @var carddav_backend
*/
private $server = null;
private $default_url = null;
private $gal_url = null;
// Android only supports synchronizing 1 AddressBook per account, this is the foldername for Z-Push
private $foldername = "contacts";
// We can have multiple addressbooks, but the mobile device will only see one (all of them merged)
private $addressbooks;
private $changessinkinit;
private $contactsetag;
private $sinkdata;
/**
* Constructor
*
*/
public function BackendCardDAV() {
if (!function_exists("curl_init")) {
throw new FatalException("BackendCardDAV(): php-curl is not found", 0, null, LOGLEVEL_FATAL);
}
$this->addressbooks = array();
$this->changessinkinit = false;
$this->contactsetag = array();
$this->sinkdata = array();
}
/**
* Authenticates the user - NOT EFFECTIVELY IMPLEMENTED
* Normally some kind of password check would be done here.
* Alternatively, the password could be ignored and an Apache
* authentication via mod_auth_* could be done
*
* @param string $username
* @param string $domain
* @param string $password
*
* @access public
* @return boolean
*/
public function Logon($username, $domain, $password) {
$this->url = CARDDAV_PROTOCOL . '://' . CARDDAV_SERVER . ':' . CARDDAV_PORT . str_replace("%d", $domain, str_replace("%u", $username, CARDDAV_PATH));
$this->default_url = CARDDAV_PROTOCOL . '://' . CARDDAV_SERVER . ':' . CARDDAV_PORT . str_replace("%d", $domain, str_replace("%u", $username, CARDDAV_DEFAULT_PATH));
if (defined('CARDDAV_GAL_PATH')) {
$this->gal_url = CARDDAV_PROTOCOL . '://' . CARDDAV_SERVER . ':' . CARDDAV_PORT . str_replace("%d", $domain, str_replace("%u", $username, CARDDAV_GAL_PATH));
}
else {
$this->gal_url = false;
}
$this->server = new carddav_backend($this->url, CARDDAV_URL_VCARD_EXTENSION);
$this->server->set_auth($username, $password);
if (($connected = $this->server->check_connection())) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->Logon(): User '%s' is authenticated on '%s'", $username, $this->url));
$this->username = $username;
$this->domain = $domain;
// Autodiscover all the addressbooks
$this->discoverAddressbooks();
}
else {
//TODO: get error message
$error = '';
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->Logon(): User '%s' failed to authenticate on '%s': %s", $username, $this->url, $error));
$this->server = null;
}
return $connected;
}
/**
* Logs off
*
* @access public
* @return boolean
*/
public function Logoff() {
if ($this->server != null) {
$this->server->disconnect();
unset($this->server);
}
$this->SaveStorages();
unset($this->contactsetag);
unset($this->sinkdata);
unset($this->addressbooks);
ZLog::Write(LOGLEVEL_DEBUG, "BackendCardDAV->Logoff(): disconnected from CARDDAV server");
return true;
}
/**
* Sends an e-mail
* Not implemented here
*
* @param SyncSendMail $sm SyncSendMail object
*
* @access public
* @return boolean
* @throws StatusException
*/
public function SendMail($sm) {
return false;
}
/**
* Returns the waste basket
* Not implemented here
*
* @access public
* @return string
*/
public function GetWasteBasket() {
return false;
}
/**
* Returns the content of the named attachment as stream
* Not implemented here
*
* @param string $attname
*
* @access public
* @return SyncItemOperationsAttachment
* @throws StatusException
*/
public function GetAttachmentData($attname) {
return false;
}
/**
* Indicates if the backend has a ChangesSink.
* A sink is an active notification mechanism which does not need polling.
* The CardDAV backend simulates a sink by polling revision dates from the vcards
*
* @access public
* @return boolean
*/
public function HasChangesSink() {
return true;
}
/**
* The folder should be considered by the sink.
* Folders which were not initialized should not result in a notification
* of IBackend->ChangesSink().
*
* @param string $folderid
*
* @access public
* @return boolean false if found can not be found
*/
public function ChangesSinkInitialize($folderid) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSinkInitialize(): folderid '%s'", $folderid));
// We don't need the actual cards, we only need to get the changes since this moment
$init_ok = true;
foreach ($this->addressbooks as $addressbook) {
try {
$this->server->set_url($addressbook);
$this->sinkdata[$addressbook] = $this->server->do_sync(true, false, CARDDAV_SUPPORTS_SYNC);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSinkInitialize - Error doing the initial sync for '%s': %s", $addressbook, $ex->getMessage()));
$init_ok = false;
}
if ($this->sinkdata[$addressbook] === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSinkInitialize - Error initializing the sink for '%s'", $addressbook));
$init_ok = false;
}
if (CARDDAV_SUPPORTS_SYNC) {
// we don't need to store the sinkdata if the carddav server supports native sync
unset($this->sinkdata[$addressbook]);
}
}
$this->changessinkinit = $init_ok;
return $this->changessinkinit;
}
/**
* The actual ChangesSink.
* For max. the $timeout value this method should block and if no changes
* are available return an empty array.
* If changes are available a list of folderids is expected.
*
* @param int $timeout max. amount of seconds to block
*
* @access public
* @return array
*/
public function ChangesSink($timeout = 30) {
$notifications = array();
$stopat = time() + $timeout - 1;
$changed = false;
//We can get here and the ChangesSink not be initialized yet
if (!$this->changessinkinit) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Not initialized ChangesSink, sleep and exit"));
// We sleep and do nothing else
sleep($timeout);
return $notifications;
}
// only check once to reduce pressure in the DAV server
foreach ($this->addressbooks as $addressbook) {
$vcards = false;
try {
$this->server->set_url($addressbook);
$vcards = $this->server->do_sync(false, false, CARDDAV_SUPPORTS_SYNC);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSink - Error resyncing vcards: %s", $ex->getMessage()));
}
if ($vcards === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangesSink - Error getting the changes"));
return false;
}
else {
$xml_vcards = new SimpleXMLElement($vcards);
if (CARDDAV_SUPPORTS_SYNC) {
if (count($xml_vcards->element) > 0) {
$changed = true;
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Changes detected"));
}
}
else {
$xml_sinkdata = new SimpleXMLElement($this->sinkdata[$addressbook]);
if (count($xml_vcards->element) != count($xml_sinkdata->element)) {
// If the number of cards is different, we know for sure, there are changes
$changed = true;
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Changes detected"));
}
else {
// If it's the same we need to check vcard to vcard, or the original strings
if (strcmp($this->sinkdata[$addressbook], $vcards) != 0) {
$changed = true;
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangesSink - Changes detected"));
}
}
unset($xml_sinkdata);
}
unset($vcards);
unset($xml_vcards);
}
if ($changed) {
$notifications[] = $this->foldername;
}
}
// Wait to timeout
if (empty($notifications)) {
while ($stopat > time()) {
sleep(1);
}
}
return $notifications;
}
/**----------------------------------------------------------------------------------------------------------
* implemented DiffBackend methods
*/
/**
* Returns a list (array) of folders.
* In simple implementations like this one, probably just one folder is returned.
*
* @access public
* @return array
*/
public function GetFolderList() {
ZLog::Write(LOGLEVEL_DEBUG, 'BackendCardDAV::GetFolderList()');
// The mobile will only see one
$addressbooks = array();
$addressbook = $this->StatFolder($this->foldername);
$addressbooks[] = $addressbook;
return $addressbooks;
}
/**
* Returns an actual SyncFolder object
*
* @param string $id id of the folder
*
* @access public
* @return object SyncFolder with information
*/
public function GetFolder($id) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::GetFolder('%s')", $id));
$addressbook = false;
if ($id == $this->foldername) {
$addressbook = new SyncFolder();
$addressbook->serverid = $id;
$addressbook->parentid = "0";
$addressbook->displayname = str_replace("%d", $this->domain, str_replace("%u", $this->username, CARDDAV_CONTACTS_FOLDER_NAME));
$addressbook->type = SYNC_FOLDER_TYPE_CONTACT;
}
return $addressbook;
}
/**
* Returns folder stats. An associative array with properties is expected.
*
* @param string $id id of the folder
*
* @access public
* @return array
*/
public function StatFolder($id) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::StatFolder('%s')", $id));
$addressbook = $this->GetFolder($id);
$stat = array();
$stat["id"] = $id;
$stat["parent"] = $addressbook->parentid;
$stat["mod"] = $addressbook->displayname;
return $stat;
}
/**
* Creates or modifies a folder
* Not implemented here
*
* @param string $folderid id of the parent folder
* @param string $oldid if empty -> new folder created, else folder is to be renamed
* @param string $displayname new folder name (to be created, or to be renamed to)
* @param int $type folder type
*
* @access public
* @return boolean status
* @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions
*
*/
public function ChangeFolder($folderid, $oldid, $displayname, $type) {
return false;
}
/**
* Deletes a folder
* Not implemented here
*
* @param string $id
* @param string $parent is normally false
*
* @access public
* @return boolean status - false if e.g. does not exist
* @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions
*
*/
public function DeleteFolder($id, $parentid) {
return false;
}
/**
* Returns a list (array) of messages
*
* @param string $folderid id of the parent folder
* @param long $cutoffdate timestamp in the past from which on messages should be returned
*
* @access public
* @return array/false array with messages or false if folder is not available
*/
public function GetMessageList($folderid, $cutoffdate) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetMessageList('%s', '%s')", $folderid, $cutoffdate));
$messages = array();
foreach ($this->addressbooks as $addressbook) {
$addressbookId = $this->convertAddressbookUrl($addressbook);
$vcards = false;
try {
// We don't need the actual vcards here, we only need a list of all them
// This petition is always "initial", and we don't "include_vcards"
$this->server->set_url($addressbook);
$vcards = $this->server->do_sync(true, false, CARDDAV_SUPPORTS_SYNC);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessageList - Error getting the vcards in '%s': %s", $addressbook, $ex->getMessage()));
}
if ($vcards === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessageList - Error getting the vcards"));
}
else {
$xml_vcards = new SimpleXMLElement($vcards);
foreach ($xml_vcards->element as $vcard) {
$id = $addressbookId . "-" . $vcard->id->__toString();
$this->contactsetag[$id] = $vcard->etag->__toString();
$messages[] = $this->StatMessage($folderid, $id);
}
}
}
return $messages;
}
/**
* Returns the actual SyncXXX object type.
*
* @param string $folderid id of the parent folder
* @param string $id id of the message
* @param ContentParameters $contentparameters parameters of the requested message (truncation, mimesupport etc)
*
* @access public
* @return object/false false if the message could not be retrieved
*/
public function GetMessage($folderid, $id, $contentparameters) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetMessage('%s', '%s')", $folderid, $id));
$message = false;
$addressbookId = $this->getAddressbookIdFromVcard($id);
$vcardId = $this->getVcardId($id);
$addressbookUrl = $this->getAddressbookFromId($addressbookId);
if ($addressbookUrl !== false) {
$xml_vcard = false;
try {
$this->server->set_url($addressbookUrl);
$xml_vcard = $this->server->get_xml_vcard($vcardId);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessage - Error getting vcard '%s' in '%s': %s", $vcardId, $addressbookId, $ex->getMessage()));
}
if ($xml_vcard !== false) {
$truncsize = Utils::GetTruncSize($contentparameters->GetTruncation());
$xml_data = new SimpleXMLElement($xml_vcard);
$message = $this->ParseFromVCard($xml_data->element[0]->vcard->__toString(), $truncsize);
}
}
if ($message === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetMessage(): vCard not found"));
}
return $message;
}
/**
* Returns message stats, analogous to the folder stats from StatFolder().
*
* @param string $folderid id of the folder
* @param string $id id of the message
*
* @access public
* @return array
*/
public function StatMessage($folderid, $id) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->StatMessage('%s', '%s')", $folderid, $id));
$message = array();
if (!isset($this->contactsetag[$id])) {
$addressbookId = $this->getAddressbookIdFromVcard($id);
$vcardId = $this->getVcardId($id);
$addressbookUrl = $this->getAddressbookFromId($addressbookId);
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->StatMessage - No contactsetag found, getting vcard '%s' in '%s'", $vcardId, $addressbookId));
if ($addressbookUrl !== false) {
$xml_vcard = false;
try {
$this->server->set_url($addressbookUrl);
$xml_vcard = $this->server->get_xml_vcard($vcardId);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->StatMessage - Error getting vcard '%s' in '%s': %s", $vcardId, $addressbookId, $ex->getMessage()));
}
if ($xml_vcard !== false) {
$vcard = new SimpleXMLElement($xml_vcard);
$this->contactsetag[$id] = $vcard->element[0]->etag->__toString();
unset($vcard);
}
unset($xml_vcard);
}
}
$message["mod"] = $this->contactsetag[$id];
$message["id"] = $id;
$message["flags"] = 1;
return $message;
}
/**
* Called when a message has been changed on the mobile.
* This functionality is not available for emails.
*
* @param string $folderid id of the folder
* @param string $id id of the message
* @param SyncXXX $message the SyncObject containing a message
* @param ContentParameters $contentParameters
*
* @access public
* @return array same return value as StatMessage()
* @throws StatusException could throw specific SYNC_STATUS_* exceptions
*/
public function ChangeMessage($folderid, $id, $message, $contentParameters) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangeMessage('%s', '%s')", $folderid, $id));
$vcard_text = $this->ParseToVCard($message);
if ($vcard_text === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - Error converting message to vCard"));
}
else {
ZLog::Write(LOGLEVEL_WBXML, sprintf("BackendCardDAV->ChangeMessage - vCard\n%s\n", $vcard_text));
$updated = false;
if (strlen($id) == 0) {
//no id, new vcard
try {
$addressbookId = $this->getAddressbookFromUrl($this->default_url);
if ($addressbookId === false) {
$addressbookId = $this->getAddressbookFromUrl($this->addressbooks[0]);
$this->server->set_url($this->addressbooks[0]);
}
else {
$this->server->set_url($this->default_url);
}
$updated = $this->server->add($vcard_text);
if ($updated !== false) {
$id = $addressbookId . "-" . $updated;
}
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - Error adding vcard '%s' : %s", $id, $ex->getMessage()));
}
}
else {
//id, update vcard
$vcardId = $this->getVcardId($id);
$addressbookUrl = $this->getAddressbookFromId($this->getAddressbookIdFromVcard($id));
if ($addressbookUrl !== false) {
try {
$this->server->set_url($addressbookUrl);
$updated = $this->server->update($vcard_text, $vcardId);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - Error updating vcard '%s' : %s", $id, $ex->getMessage()));
}
}
}
if ($updated !== false) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->ChangeMessage - vCard updated"));
}
else {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->ChangeMessage - vCard not updated"));
}
}
return $this->StatMessage($folderid, $id);
}
/**
* Changes the 'read' flag of a message on disk
* Not implemented here
*
* @param string $folderid id of the folder
* @param string $id id of the message
* @param int $flags read flag of the message
* @param ContentParameters $contentParameters
*
* @access public
* @return boolean status of the operation
* @throws StatusException could throw specific SYNC_STATUS_* exceptions
*/
public function SetReadFlag($folderid, $id, $flags, $contentParameters) {
return false;
}
/**
* Called when the user has requested to delete (really delete) a message
*
* @param string $folderid id of the folder
* @param string $id id of the message
* @param ContentParameters $contentParameters
*
* @access public
* @return boolean status of the operation
* @throws StatusException could throw specific SYNC_STATUS_* exceptions
*/
public function DeleteMessage($folderid, $id, $contentParameters) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->DeleteMessage('%s', '%s')", $folderid, $id));
$deleted = false;
$vcardId = $this->getVcardId($id);
$addressbookUrl = $this->getAddressbookFromId($this->getAddressbookIdFromVcard($id));
if ($addressbookUrl !== false) {
try {
$this->server->set_url($addressbookUrl);
$deleted = $this->server->delete($vcardId);
}
catch (Exception $ex) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->DeleteMessage - Error deleting vcard: %s", $ex->getMessage()));
}
}
if ($deleted) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->DeleteMessage - vCard deleted"));
}
else {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->DeleteMessage - cannot delete vCard"));
}
return $deleted;
}
/**
* Called when the user moves an item on the PDA from one folder to another
* Not implemented here
*
* @param string $folderid id of the source folder
* @param string $id id of the message
* @param string $newfolderid id of the destination folder
* @param ContentParameters $contentParameters
*
* @access public
* @return boolean status of the operation
* @throws StatusException could throw specific SYNC_MOVEITEMSSTATUS_* exceptions
*/
public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) {
return false;
}
/**
* Resolves recipients
*
* @param SyncObject $resolveRecipients
*
* @access public
* @return SyncObject $resolveRecipients
*/
public function ResolveRecipients($resolveRecipients) {
// TODO:
return false;
}
/**
* Indicates which AS version is supported by the backend.
*
* @access public
* @return string AS version constant
*/
public function GetSupportedASVersion() {
return ZPush::ASV_14;
}
/**
* Returns the BackendCardDAV as it implements the ISearchProvider interface
* This could be overwritten by the global configuration
*
* @access public
* @return object Implementation of ISearchProvider
*/
public function GetSearchProvider() {
return $this;
}
/**----------------------------------------------------------------------------------------------------------
* public ISearchProvider methods
*/
/**
* Indicates if a search type is supported by this SearchProvider
* Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented
*
* @param string $searchtype
*
* @access public
* @return boolean
*/
public function SupportsType($searchtype) {
if ($this->gal_url !== false) {
return ($searchtype == ISearchProvider::SEARCH_GAL);
}
else {
return false;
}
}
/**
* Queries the CardDAV backend
*
* @param string $searchquery string to be searched for
* @param string $searchrange specified searchrange
*
* @access public
* @return array search results
*/
public function GetGALSearchResults($searchquery, $searchrange) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults(%s, %s)", $searchquery, $searchrange));
if ($this->gal_url !== false && $this->server !== false) {
// Don't search if the length is < 5, we are typing yet
if (strlen($searchquery) < CARDDAV_GAL_MIN_LENGTH) {
return false;
}
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults searching: %s", $this->url));
try {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults server is null? %d", $this->server == null));
$this->server->set_url($this->gal_url);
$vcards = $this->server->search_vcards(str_replace("<", "", str_replace(">", "", $searchquery)), 15, true, false,
defined('CARDDAV_SUPPORTS_FN_SEARCH') ? CARDDAV_SUPPORTS_FN_SEARCH : false);
}
catch (Exception $e) {
$vcards = false;
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetGALSearchResults : Error in search %s", $e->getMessage()));
}
if ($vcards === false) {
ZLog::Write(LOGLEVEL_ERROR, "BackendCardDAV->GetGALSearchResults : Error in search query. Search aborted");
return false;
}
$xml_vcards = new SimpleXMLElement($vcards);
unset($vcards);
// range for the search results, default symbian range end is 50, wm 99,
// so we'll use that of nokia
$rangestart = 0;
$rangeend = 50;
if ($searchrange != '0') {
$pos = strpos($searchrange, '-');
$rangestart = substr($searchrange, 0, $pos);
$rangeend = substr($searchrange, ($pos + 1));
}
$items = array();
// TODO the limiting of the searchresults could be refactored into Utils as it's probably used more than once
$querycnt = $xml_vcards->count();
//do not return more results as requested in range
$querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt == 0 ? 1 : $querycnt;
$items['range'] = $rangestart.'-'.($querylimit - 1);
$items['searchtotal'] = $querycnt;
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->GetGALSearchResults : %s entries found, returning %s to %s", $querycnt, $rangestart, $querylimit));
$i = 0;
$rc = 0;
foreach ($xml_vcards->element as $xml_vcard) {
if ($i >= $rangestart && $i < $querylimit) {
$contact = $this->ParseFromVCard($xml_vcard->vcard->__toString());
if ($contact === false) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendCardDAV->GetGALSearchResults : error converting vCard to AS contact\n%s\n", $xml_vcard->vcard->__toString()));
}
else {
$items[$rc][SYNC_GAL_EMAILADDRESS] = $contact->email1address;
if (isset($contact->fileas)) {
$items[$rc][SYNC_GAL_DISPLAYNAME] = $contact->fileas;
}
else if (isset($contact->firstname) || isset($contact->middlename) || isset($contact->lastname)) {
$items[$rc][SYNC_GAL_DISPLAYNAME] = $contact->firstname . (isset($contact->middlename) ? " " . $contact->middlename : "") . (isset($contact->lastname) ? " " . $contact->lastname : "");
}
else {
$items[$rc][SYNC_GAL_DISPLAYNAME] = $contact->email1address;
}
if (isset($contact->firstname)) {
$items[$rc][SYNC_GAL_FIRSTNAME] = $contact->firstname;
}
else {
$items[$rc][SYNC_GAL_FIRSTNAME] = "";
}
if (isset($contact->lastname)) {
$items[$rc][SYNC_GAL_LASTNAME] = $contact->lastname;
}
else {
$items[$rc][SYNC_GAL_LASTNAME] = "";
}
if (isset($contact->business2phonenumber)) {
$items[$rc][SYNC_GAL_PHONE] = $contact->business2phonenumber;
}
if (isset($contact->home2phonenumber)) {
$items[$rc][SYNC_GAL_HOMEPHONE] = $contact->home2phonenumber;
}
if (isset($contact->mobilephonenumber)) {
$items[$rc][SYNC_GAL_MOBILEPHONE] = $contact->mobilephonenumber;
}
if (isset($contact->title)) {
$items[$rc][SYNC_GAL_TITLE] = $contact->title;
}
if (isset($contact->companyname)) {
$items[$rc][SYNC_GAL_COMPANY] = $contact->companyname;
}
if (isset($contact->department)) {
$items[$rc][SYNC_GAL_OFFICE] = $contact->department;
}
if (isset($contact->nickname)) {
$items[$rc][SYNC_GAL_ALIAS] = $contact->nickname;
}
unset($contact);
$rc++;
}
}
$i++;
}
unset($xml_vcards);
return $items;
}
else {
unset($xml_vcards);
return false;
}
}
/**
* Searches for the emails on the server
*
* @param ContentParameter $cpo
*
* @return array
*/
public function GetMailboxSearchResults($cpo) {
return false;
}
/**
* Terminates a search for a given PID
*
* @param int $pid
*
* @return boolean
*/
public function TerminateSearch($pid) {
return true;
}
/**
* Disconnects from CardDAV
*
* @access public
* @return boolean
*/
public function Disconnect() {
return true;
}
/**----------------------------------------------------------------------------------------------------------
* private vcard-specific internals
*/
/**
* Escapes a string
*
* @param string $data string to be escaped
*
* @access private
* @return string
*/
private function escape($data) {
if (is_array($data)) {
foreach ($data as $key => $val) {
$data[$key] = $this->escape($val);
}
return $data;
}
$data = str_replace("\r\n", "\n", $data);
$data = str_replace("\r", "\n", $data);
$data = str_replace(array('\\', ';', ',', "\n"), array('\\\\', '\\;', '\\,', '\\n'), $data);
return $data;
}
/**
* Un-escapes a string
*
* @param string $data string to be un-escaped
*
* @access private
* @return string
*/
private function unescape($data) {
$data = str_replace(array('\\\\', '\\;', '\\,', '\\n','\\N'),array('\\', ';', ',', "\n", "\n"),$data);
return $data;
}
/**
* Converts the vCard into SyncContact.
* See RFC 6350 for vCard format details.
*
* @param string $data string with the vcard
* @param int $truncsize truncate size requested
* @return SyncContact
*/
private function ParseFromVCard($data, $truncsize = -1) {
ZLog::Write(LOGLEVEL_WBXML, sprintf("BackendCardDAV->ParseFromVCard : vCard\n%s\n", $data));
$types = array ('dom' => 'type', 'intl' => 'type', 'postal' => 'type', 'parcel' => 'type', 'home' => 'type', 'work' => 'type',
'pref' => 'type', 'voice' => 'type', 'fax' => 'type', 'msg' => 'type', 'cell' => 'type', 'pager' => 'type',
'bbs' => 'type', 'modem' => 'type', 'car' => 'type', 'isdn' => 'type', 'video' => 'type',
'aol' => 'type', 'applelink' => 'type', 'attmail' => 'type', 'cis' => 'type', 'eworld' => 'type',
'internet' => 'type', 'ibmmail' => 'type', 'mcimail' => 'type',
'powershare' => 'type', 'prodigy' => 'type', 'tlx' => 'type', 'x400' => 'type',
'gif' => 'type', 'cgm' => 'type', 'wmf' => 'type', 'bmp' => 'type', 'met' => 'type', 'pmb' => 'type', 'dib' => 'type',
'pict' => 'type', 'tiff' => 'type', 'pdf' => 'type', 'ps' => 'type', 'jpeg' => 'type', 'qtime' => 'type',
'mpeg' => 'type', 'mpeg2' => 'type', 'avi' => 'type',
'wave' => 'type', 'aiff' => 'type', 'pcm' => 'type',
'x509' => 'type', 'pgp' => 'type', 'text' => 'value', 'inline' => 'value', 'url' => 'value', 'cid' => 'value', 'content-id' => 'value',
'7bit' => 'encoding', '8bit' => 'encoding', 'quoted-printable' => 'encoding', 'base64' => 'encoding',
);
// Parse the vcard
$message = new SyncContact();
$data = str_replace("\x00", '', $data);
$data = str_replace("\r\n", "\n", $data);
$data = str_replace("\r", "\n", $data);
$data = preg_replace('/(\n)([ \t])/i', '', $data);
$lines = explode("\n", $data);
$vcard = array();
foreach ($lines as $line) {
if (trim($line) == '')
continue;
$pos = strpos($line, ':');
if ($pos === false)
continue;
$field = trim(substr($line, 0, $pos));
$value = trim(substr($line, $pos + 1));
$fieldparts = preg_split('/(?<!\\\\)(\;)/i', $field, -1, PREG_SPLIT_NO_EMPTY);
// The base type
$type = strtolower(array_shift($fieldparts));
// We do not care about visually grouping properties together, so strip groups off (see RFC 6350 § 3.3)
if (preg_match('#^[a-z0-9\\-]+\\.(.+)$#i', $type, $matches)) {
$type = $matches[1];
}
// Parse all field values
$fieldvalue = array();
foreach ($fieldparts as $fieldpart) {
if (preg_match('/([^=]+)=(.+)/', $fieldpart, $matches)) {
$fieldName = strtolower($matches[1]);
if (!in_array($fieldName, array('value', 'type', 'encoding', 'language')))
continue;
if (isset($fieldvalue[$fieldName]) && is_array($fieldvalue[$fieldName])) {
if ($fieldName == 'type') {
$fieldvalue[$fieldName] = array_merge($fieldvalue[$fieldName], array_map('strtolower', preg_split('/(?<!\\\\)(\,)/i', $matches[2], -1, PREG_SPLIT_NO_EMPTY)));
} else {
$fieldvalue[$fieldName] = array_merge($fieldvalue[$fieldName], preg_split('/(?<!\\\\)(\,)/i', $matches[2], -1, PREG_SPLIT_NO_EMPTY));
}
} else {
if ($fieldName == 'type') {
$fieldvalue[$fieldName] = array_map('strtolower', preg_split('/(?<!\\\\)(\,)/i', $matches[2], -1, PREG_SPLIT_NO_EMPTY));
} else {
$fieldvalue[$fieldName] = preg_split('/(?<!\\\\)(\,)/i', $matches[2], -1, PREG_SPLIT_NO_EMPTY);
}
}
} else {
if (!isset($types[strtolower($fieldpart)]))
continue;
$fieldvalue[$types[strtolower($fieldpart)]][] = $fieldpart;
}
}
//
switch ($type) {
case 'categories':
//case 'nickname':
$val = preg_split('/(\s)*(\\\)?\,(\s)*/i', $value);
break;
default:
$val = preg_split('/(?<!\\\\)(\;)/i', $value);
break;
}
if (isset($fieldvalue['encoding'][0])) {
switch (strtolower($fieldvalue['encoding'][0])) {
case 'q':
case 'quoted-printable':
foreach ($val as $i => $v) {
$val[$i] = quoted_printable_decode($v);
}
break;
case 'b':
case 'base64':
foreach ($val as $i => $v) {
$val[$i] = base64_decode($v);
}
break;
}
} else {
foreach ($val as $i => $v) {
$val[$i] = $this->unescape($v);
}
}
$fieldvalue['val'] = $val;
$vcard[$type][] = $fieldvalue;
}
if (isset($vcard['email'][0]['val'][0]))
$message->email1address = $vcard['email'][0]['val'][0];
if (isset($vcard['email'][1]['val'][0]))
$message->email2address = $vcard['email'][1]['val'][0];
if (isset($vcard['email'][2]['val'][0]))
$message->email3address = $vcard['email'][2]['val'][0];
if (isset($vcard['tel'])) {
foreach ($vcard['tel'] as $tel) {
if (!isset($tel['type'])) {
$tel['type'] = array();
}
if (in_array('car', $tel['type'])) {
$message->carphonenumber = $tel['val'][0];
}
elseif (in_array('pager', $tel['type'])) {
$message->pagernumber = $tel['val'][0];
}
elseif (in_array('cell', $tel['type'])) {
$message->mobilephonenumber = $tel['val'][0];
}
elseif (in_array('home', $tel['type'])) {
if (in_array('fax', $tel['type'])) {
$message->homefaxnumber = $tel['val'][0];
}
elseif (empty($message->homephonenumber)) {
$message->homephonenumber = $tel['val'][0];
}
else {
$message->home2phonenumber = $tel['val'][0];
}
}
elseif (in_array('work', $tel['type'])) {
if (in_array('fax', $tel['type'])) {
$message->businessfaxnumber = $tel['val'][0];
}
elseif (empty($message->businessphonenumber)) {
$message->businessphonenumber = $tel['val'][0];
}
else {
$message->business2phonenumber = $tel['val'][0];
}
}
elseif (empty($message->homephonenumber)) {
$message->homephonenumber = $tel['val'][0];
}
elseif (empty($message->home2phonenumber)) {
$message->home2phonenumber = $tel['val'][0];
}
else {
$message->radiophonenumber = $tel['val'][0];
}
}
}
//;;street;city;state;postalcode;country
if (isset($vcard['adr'])) {
foreach ($vcard['adr'] as $adr) {
if (empty($adr['type'])) {
$a = 'other';
}
elseif (in_array('home', $adr['type'])) {
$a = 'home';
}
elseif (in_array('work', $adr['type'])) {
$a = 'business';
}
else {
$a = 'other';
}
if (!empty($adr['val'][2])) {
$b=$a.'street';
$message->$b = $adr['val'][2];
}
if (!empty($adr['val'][3])) {
$b=$a.'city';
$message->$b = $adr['val'][3];
}
if (!empty($adr['val'][4])) {
$b=$a.'state';
$message->$b = $adr['val'][4];
}
if (!empty($adr['val'][5])) {
$b=$a.'postalcode';
$message->$b = $adr['val'][5];
}
if (!empty($adr['val'][6])) {
$b=$a.'country';
$message->$b = $adr['val'][6];
}
}
}
if (!empty($vcard['fn'][0]['val'][0]))
$message->fileas = $vcard['fn'][0]['val'][0];
if (!empty($vcard['n'][0]['val'][0]))
$message->lastname = $vcard['n'][0]['val'][0];
if (!empty($vcard['n'][0]['val'][1]))
$message->firstname = $vcard['n'][0]['val'][1];
if (!empty($vcard['n'][0]['val'][2]))
$message->middlename = $vcard['n'][0]['val'][2];
if (!empty($vcard['n'][0]['val'][3]))
$message->title = $vcard['n'][0]['val'][3];
if (!empty($vcard['n'][0]['val'][4]))
$message->suffix = $vcard['n'][0]['val'][4];
if (!empty($vcard['nickname'][0]['val'][0]))
$message->nickname = $vcard['nickname'][0]['val'][0];
if (!empty($vcard['bday'][0]['val'][0])) {
$tz = date_default_timezone_get();
date_default_timezone_set('UTC');
$message->birthday = strtotime($vcard['bday'][0]['val'][0]);
date_default_timezone_set($tz);
}
if (!empty($vcard['org'][0]['val'][0]))
$message->companyname = $vcard['org'][0]['val'][0];
if (!empty($vcard['note'][0]['val'][0])) {
if (Request::GetProtocolVersion() >= 12.0) {
$message->asbody = new SyncBaseBody();
$message->asbody->type = SYNC_BODYPREFERENCE_PLAIN;
$message->asbody->data = $vcard['note'][0]['val'][0];
if ($truncsize > 0 && $truncsize < strlen($message->asbody->data)) {
$message->asbody->truncated = 1;
$message->asbody->data = Utils::Utf8_truncate($message->asbody->data, $truncsize);
}
else {
$message->asbody->truncated = 0;
}
$message->asbody->estimatedDataSize = strlen($message->asbody->data);
}
else {
$message->body = $vcard['note'][0]['val'][0];
if ($truncsize > 0 && $truncsize < strlen($message->body)) {
$message->bodytruncated = 1;
$message->body = Utils::Utf8_truncate($message->body, $truncsize);
}
else {
$message->bodytruncated = 0;
}
$message->bodysize = strlen($message->body);
}
}
// Support both ROLE and TITLE (RFC 6350 § 6.6.1 / § 6.6.2) as mapped to JobTitle
if (!empty($vcard['role'][0]['val'][0]))
$message->jobtitle = $vcard['role'][0]['val'][0];
if (!empty($vcard['title'][0]['val'][0]))
$message->jobtitle = $vcard['title'][0]['val'][0];
if (!empty($vcard['url'][0]['val'][0]))
$message->webpage = $vcard['url'][0]['val'][0];
if (!empty($vcard['categories'][0]['val']))
$message->categories = $vcard['categories'][0]['val'];
if (!empty($vcard['photo'][0]['val'][0]))
$message->picture = base64_encode($vcard['photo'][0]['val'][0]);
return $message;
}
/**
* Convert a SyncObject into vCard.
*
* @param SyncContact $message AS Contact
* @return string vcard text
*/
private function ParseToVCard($message) {
// http://tools.ietf.org/html/rfc6350
$mapping = array(
'fileas' => 'FN',
'lastname;firstname;middlename;title;suffix' => 'N',
'email1address' => 'EMAIL;PREF=1',
'email2address' => 'EMAIL;PREF=2',
'email3address' => 'EMAIL;PREF=3',
'businessphonenumber' => 'TEL;TYPE=WORK,VOICE',
'business2phonenumber' => 'TEL;TYPE=WORK,VOICE',
'businessfaxnumber' => 'TEL;TYPE=WORK,FAX',
'homephonenumber' => 'TEL;TYPE=HOME,VOICE',
'home2phonenumber' => 'TEL;TYPE=HOME,VOICE',
'homefaxnumber' => 'TEL;TYPE=HOME,FAX',
'mobilephonenumber' => 'TEL;TYPE=CELL',
'carphonenumber' => 'TEL;TYPE=VOICE',
'pagernumber' => 'TEL;TYPE=PAGER',
';;businessstreet;businesscity;businessstate;businesspostalcode;businesscountry' => 'ADR;TYPE=WORK',
';;homestreet;homecity;homestate;homepostalcode;homecountry' => 'ADR;TYPE=HOME',
';;otherstreet;othercity;otherstate;otherpostalcode;othercountry' => 'ADR',
'companyname' => 'ORG',
'body' => 'NOTE',
'jobtitle' => 'ROLE',
'webpage' => 'URL',
'nickname' => 'NICKNAME'
);
$data = "BEGIN:VCARD\nVERSION:3.0\nPRODID:Z-Push\n";
foreach ($mapping as $k => $v) {
$val = '';
$ks = explode(';', $k);
foreach ($ks as $i) {
if (!empty($message->$i))
$val .= $this->escape($message->$i);
$val.=';';
}
if ($k == 'body' && isset($message->asbody)) {
$val = $message->asbody->data;
}
if (empty($val) || preg_match('/^(\;)+$/', $val) == 1)
continue;
// Remove trailing ;
$val = substr($val, 0, -1);
if (strlen($val) > 50) {
$data .= $v.":\n\t".substr(chunk_split($val, 50, "\n\t"), 0, -1);
}
else {
$data .= $v.':'.$val."\n";
}
}
if (!empty($message->categories))
$data .= 'CATEGORIES:'.implode(',', $message->categories)."\n";
if (!empty($message->picture))
$data .= 'PHOTO;ENCODING=BASE64;TYPE=JPEG:'."\n\t".substr(chunk_split($message->picture, 50, "\n\t"), 0, -1);
if (isset($message->birthday))
$data .= 'BDAY:'.date('Y-m-d', $message->birthday)."\n";
$data .= "END:VCARD";
// http://en.wikipedia.org/wiki/VCard
// TODO: add support for v4.0
// not supported: anniversary, assistantname, assistnamephonenumber, children, department, officelocation, radiophonenumber, spouse, rtf
return $data;
}
/**
* Discover all the addressbooks collections for a user under a root.
*
*/
private function discoverAddressbooks() {
unset($this->addressbooks);
$this->addressbooks = array();
$raw = $this->server->get(false, false, true);
if ($raw !== false) {
$xml = new SimpleXMLElement($raw);
foreach ($xml->addressbook_element as $response) {
if ($this->gal_url !== false) {
if (strcmp(urldecode($response->url), $this->gal_url) == 0) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::discoverAddressbooks() Ignoring GAL addressbook '%s'", $this->gal_url));
continue;
}
}
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::discoverAddressbooks() Found addressbook '%s'", urldecode($response->url)));
$this->addressbooks[] = urldecode($response->url);
}
unset($xml);
}
}
/**
* Returns de addressbookId of a vcard.
* The vcardId sent to the device is formed as [addressbookId]-[vcardId]
*
* @param string $vcardId vcard ID in device.
* @return addressbookId
*/
private function getAddressbookIdFromVcard($vcardId) {
$parts = explode("-", $vcardId);
return $parts[0];
}
/**
* Returns de vcard id stored in the carddav server.
*
* @param string $vcardId vcard ID in device
* @return vcard id in carddav server
*/
private function getVcardId($vcardId) {
$parts = explode("-", $vcardId);
$id = "";
for ($i = 1; $i < count($parts); $i++) {
if ($i > 1) {
$id .= "-";
}
$id .= $parts[$i];
}
return $id;
}
/**
* Convert an addressbook url into a zpush id.
*
* @param string $addressbookUrl AddressBook URL
* @return id or false
*/
private function convertAddressbookUrl($addressbookUrl) {
$this->InitializePermanentStorage();
// check if this addressbookUrl was converted before
$addressbookId = $this->getAddressbookFromUrl($addressbookUrl);
// nothing found, so generate a new id and put it in the cache
if ($addressbookId === false) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::convertAddressbookUrl('%s') New addressbook", $addressbookUrl));
// generate addressbookId and add it to the mapping
$addressbookId = sprintf('%04x%04x', mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ));
// addressbookId to addressbookUrl mapping
if (!isset($this->permanentStorage->fmAidAurl))
$this->permanentStorage->fmAidAurl = array();
$a = $this->permanentStorage->fmAidAurl;
$a[$addressbookId] = $addressbookUrl;
$this->permanentStorage->fmAidAurl = $a;
// addressbookUrl to addressbookId mapping
if (!isset($this->permanentStorage->fmAurlAid))
$this->permanentStorage->fmAurlAid = array();
$b = $this->permanentStorage->fmAurlAid;
$b[$addressbookUrl] = $addressbookId;
$this->permanentStorage->fmAurlAid = $b;
}
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::convertAddressbookUrl('%s') = %s", $addressbookUrl, $addressbookId));
return $addressbookId;
}
/**
* Get the URL of an addressbook zpush id.
*
* @param string $addressbookId AddressBook Z-Push based ID
* @return url or false
*/
private function getAddressbookFromId($addressbookId) {
$this->InitializePermanentStorage();
$addressbookUrl = false;
if (isset($this->permanentStorage->fmAidAurl)) {
if (isset($this->permanentStorage->fmAidAurl[$addressbookId])) {
$addressbookUrl = $this->permanentStorage->fmAidAurl[$addressbookId];
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromId('%s') = %s", $addressbookId, $addressbookUrl));
}
else {
ZLog::Write(LOGLEVEL_WARN, sprintf("BackendCardDAV::getAddressbookFromId('%s') = %s", $addressbookId, 'not found'));
}
}
else {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromId('%s') = %s", $addressbookId, 'not initialized!'));
}
return $addressbookUrl;
}
/**
* Get the zpush id of an addressbook.
*
* @param string $addressbookUrl AddressBook URL
* @return id or false
*/
private function getAddressbookFromUrl($addressbookUrl) {
$this->InitializePermanentStorage();
$addressbookId = false;
if (isset($this->permanentStorage->fmAurlAid)) {
if (isset($this->permanentStorage->fmAurlAid[$addressbookUrl])) {
$addressbookId = $this->permanentStorage->fmAurlAid[$addressbookUrl];
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromUrl('%s') = %s", $addressbookUrl, $addressbookId));
}
else {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromUrl('%s') = %s", $addressbookUrl, 'not found'));
}
}
else {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV::getAddressbookFromUrl('%s') = %s", $addressbookUrl, 'not initialized!'));
}
return $addressbookId;
}
}
\ No newline at end of file
<?php
/***********************************************
* File : config.php
* Project : Z-Push
* Descr : CardDAV backend configuration file
*
* Created : 16.03.2013
*
* Copyright 2013 - 2016 Francisco Miguel Biete
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
// ************************
// BackendCardDAV settings
// ************************
// Server protocol: http or https
define('CARDDAV_PROTOCOL', 'https');
// Server name
define('CARDDAV_SERVER', 'localhost');
// Server port
define('CARDDAV_PORT', '443');
// Server path to the addressbook, or the principal with the addressbooks
// If your user has more than 1 addressbook point it to the principal.
// Example: user test@domain.com will have 2 addressbooks
// http://localhost/caldav.php/test@domain.com/addresses/personal
// http://localhost/caldav.php/test@domain.com/addresses/work
// You set the CARDDAV_PATH to '/caldav.php/%u/addresses/' and personal and work will be autodiscovered
// %u: replaced with the username
// %d: replaced with the domain
// Add the trailing /
define('CARDDAV_PATH', '/caldav.php/%u/');
// Server path to the default addressbook
// Mobile device will create new contacts here. It must be under CARDDAV_PATH
// %u: replaced with the username
// %d: replaced with the domain
// Add the trailing /
define('CARDDAV_DEFAULT_PATH', '/caldav.php/%u/addresses/');
// Server path to the GAL addressbook. This addressbook is readonly and searchable by the user, but it will NOT be synced.
// If you don't want GAL, comment it
// %u: replaced with the username
// %d: replaced with the domain
// Add the trailing /
define('CARDDAV_GAL_PATH', '/caldav.php/%d/GAL/');
// Minimal length for the search pattern to do the real search.
define('CARDDAV_GAL_MIN_LENGTH', 5);
// Addressbook display name, the name showed in the mobile device
// %u: replaced with the username
// %d: replaced with the domain
define('CARDDAV_CONTACTS_FOLDER_NAME', '%u Addressbook');
// If the CardDAV server supports the sync-collection operation
// DAViCal and SabreDav support it, but Owncloud, SOGo don't
// SabreDav version must be at least 1.9.0, otherwise set this to false
// Setting this to false will work with most servers, but it will be slower: 1 petition for the href of vcards, and 1 petition for each vcard
define('CARDDAV_SUPPORTS_SYNC', false);
// If the CardDAV server supports the FN attribute for searches
// DAViCal supports it, but SabreDav, Owncloud and SOGo don't
// Setting this to true will search by FN. If false will search by sn, givenName and email
// It's safe to leave it as false
define('CARDDAV_SUPPORTS_FN_SEARCH', false);
// If your carddav server needs to use file extension to recover a vcard.
// Davical needs it
// SOGo official demo online needs it, but some SOGo installation don't need it, so test it
define('CARDDAV_URL_VCARD_EXTENSION', '.vcf');
\ No newline at end of file
...@@ -74,6 +74,9 @@ class BackendCombinedConfig { ...@@ -74,6 +74,9 @@ class BackendCombinedConfig {
'v' => array( 'v' => array(
'name' => 'BackendVCardDir', 'name' => 'BackendVCardDir',
), ),
'd' => array(
'name' => 'BackendCardDAV',
),
'c' => array( 'c' => array(
'name' => 'BackendCalDAV', 'name' => 'BackendCalDAV',
), ),
......
<?php
/**
* CardDAV PHP
*
* Simple CardDAV query
* --------------------
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* echo $carddav->get();
*
*
* Simple vCard query
* ------------------
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* echo $carddav->get_vcard('0126FFB4-2EB74D0A-302EA17F');
*
*
* XML vCard query
* ------------------
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* echo $carddav->get_xml_vcard('0126FFB4-2EB74D0A-302EA17F');
*
*
* Check CardDAV server connection
* -------------------------------
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* var_dump($carddav->check_connection());
*
*
* CardDAV delete query
* --------------------
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* $carddav->delete('0126FFB4-2EB74D0A-302EA17F');
*
*
* CardDAV add query
* --------------------
* $vcard = 'BEGIN:VCARD
* VERSION:3.0
* UID:1f5ea45f-b28a-4b96-25as-ed4f10edf57b
* FN:Christian Putzke
* N:Christian;Putzke;;;
* EMAIL;TYPE=OTHER:christian.putzke@graviox.de
* END:VCARD';
*
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* $vcard_id = $carddav->add($vcard);
*
*
* CardDAV update query
* --------------------
* $vcard = 'BEGIN:VCARD
* VERSION:3.0
* UID:1f5ea45f-b28a-4b96-25as-ed4f10edf57b
* FN:Christian Putzke
* N:Christian;Putzke;;;
* EMAIL;TYPE=OTHER:christian.putzke@graviox.de
* END:VCARD';
*
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->set_auth('username', 'password');
* $carddav->update($vcard, '0126FFB4-2EB74D0A-302EA17F');
*
*
* CardDAV debug
* -------------
* $carddav = new carddav_backend('https://davical.example.com/user/contacts/');
* $carddav->enable_debug();
* $carddav->set_auth('username', 'password');
* $carddav->get();
* var_dump($carddav->get_debug());
*
*
* CardDAV server list
* -------------------
* DAViCal: https://example.com/{resource|principal|username}/{collection}/
* Apple Addressbook Server: https://example.com/addressbooks/users/{resource|principal|username}/{collection}/
* memotoo: https://sync.memotoo.com/cardDAV/
* SabreDAV: https://example.com/addressbooks/{resource|principal|username}/{collection}/
* ownCloud: https://example.com/apps/contacts/carddav.php/addressbooks/{resource|principal|username}/{collection}/
* SOGo: https://example.com/SOGo/dav/{resource|principal|username}/Contacts/{collection}/
*
*
* @author Christian Putzke <christian.putzke@graviox.de>
* @copyright Christian Putzke
* @link http://www.graviox.de/
* @link https://twitter.com/cputzke/
* @since 20.07.2011
* @version 0.6
* @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*
*/
class carddav_backend
{
/**
* CardDAV PHP Version
*
* @constant string
*/
const VERSION = '0.6.c';
/**
* User agent displayed in http requests
*
* @constant string
*/
const USERAGENT = 'Z-Push CardDAV/';
/**
* CardDAV server url
*
* @var string
*/
private $url = null;
/**
* CardDAV server url_parts
*
* @var array
*/
private $url_parts = null;
/**
* Authentication string
*
* @var string
*/
private $auth = null;
/**
* Authentication: username
*
* @var string
*/
private $username = null;
/**
* Authentication: password
*
* @var string
*/
private $password = null;
/**
* Characters used for vCard id generation
*
* @var array
*/
private $vcard_id_chars = array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 'C', 'D', 'E', 'F');
/**
* CardDAV server connection (curl handle)
*
* @var resource
*/
private $curl = false;
/**
* Debug on or off
*
* @var boolean
*/
private $debug = false;
/**
* All available debug information
*
* @var array
*/
private $debug_information = array();
/**
* Sync-token for sync-collection operations.
*
* @var array[string]
*/
private $synctoken = array();
/* VCard File URL Extension
*
* @var string
*/
private $url_vcard_extension;
/**
* Exception codes
*/
const EXCEPTION_WRONG_HTTP_STATUS_CODE_GET = 1000;
const EXCEPTION_WRONG_HTTP_STATUS_CODE_GET_VCARD = 1001;
const EXCEPTION_WRONG_HTTP_STATUS_CODE_GET_XML_VCARD = 1002;
const EXCEPTION_WRONG_HTTP_STATUS_CODE_DELETE = 1003;
const EXCEPTION_WRONG_HTTP_STATUS_CODE_ADD = 1004;
const EXCEPTION_WRONG_HTTP_STATUS_CODE_UPDATE = 1005;
const EXCEPTION_MALFORMED_XML_RESPONSE = 1006;
const EXCEPTION_COULD_NOT_GENERATE_NEW_VCARD_ID = 1007;
const EXCEPTION_COULD_NOT_FIND_VCARD_HREF = 1008;
/**
* Constructor
* Sets the CardDAV server url
*
* @param string $url CardDAV server url
* @param string $url_vcard_extension extension needed to recover the vcard, it could be empty
*/
public function __construct($url = null, $url_vcard_extension = '.vcf') {
if ($url !== null) {
$this->set_url($url);
}
$this->url_vcard_extension = $url_vcard_extension;
}
/**
* Sets debug information
*
* @param array $debug_information Debug information
* @return void
*/
public function set_debug(array $debug_information) {
$this->debug_information[] = $debug_information;
}
/**
* Sets the CardDAV server url
*
* @param string $url CardDAV server url
* @return void
*/
public function set_url($url) {
$this->url = $url;
// Url always end with trailing /
if (substr($this->url, -1, 1) !== '/') {
$this->url .= '/';
}
$this->url_parts = parse_url($this->url);
}
/**
* Sets authentication information
*
* @param string $username CardDAV server username
* @param string $password CardDAV server password
* @return void
*/
public function set_auth($username, $password) {
$this->username = $username;
$this->password = $password;
$this->auth = $username . ':' . $password;
}
/**
* Gets all available debug information
*
* @return array $this->debug_information All available debug information
*/
public function get_debug() {
return $this->debug_information;
}
/**
* Sets the CardDAV vcard url extension
*
* Most providers do requests handling Vcards with .vcf, however
* this isn't always the case and some providers (such as Google)
* returned a 404 if the .vcf extension is used - or the other
* way around, returning 404 unless .vcf is used.
*
* Both approaches are technically correct, see rfc635
* http://tools.ietf.org/html/rfc6352
*
*
* @param string $extension File extension
* @return void
*/
public function set_vcard_extension($extension) {
$this->url_vcard_extension = $extension;
}
/**
* Gets all vCards including additional information from the CardDAV server.
* This operation could be slow if you have a lot of vcards.
*
* @param boolean $include_vcards Include vCards within the response (simplified only)
* @param boolean $raw Get response raw or simplified
* @params boolean $discover Only discover addressbooks
* @return string Raw or simplified XML response
*/
public function get($include_vcards = true, $raw = false, $discover = false) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->get"));
if ($discover) {
$result = $this->query($this->url, 'PROPFIND', null, null, '1');
}
else {
$result = $this->query($this->url, 'PROPFIND');
}
switch ($result['http_code']) {
case 200:
case 207:
if ($raw === true) {
return $result['response'];
}
else {
return $this->simplify($result['response'], $include_vcards);
}
break;
default:
throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_GET);
break;
}
}
/**
* Get all vcards matching a full name or mail.
*
* @param string $pattern Pattern to search
* @param integer $limit Return only N vcards
* @param boolean $include_vcards Include vCards within the response (simplified only)
* @param boolean $raw Get response raw or simplified
* @param boolean $support_fn_search If the server supports searchs by fn
* @return string Raw or simplified XML response
*/
public function search_vcards($pattern, $limit, $include_vcards = true, $raw = false, $support_fn_search = false) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->search_vcards"));
if ($support_fn_search) {
$xml = <<<EOFCONTENTSEARCH
<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<C:address-data>
<C:allprop/>
</C:address-data>
</D:prop>
<C:filter test="anyof">
<C:prop-filter name="FN">
<C:text-match collation="i;unicode-casemap" negate-condition="no" match-type="contains">$pattern</C:text-match>
</C:prop-filter>
</C:filter>
<C:limit>
<C:nresults>$limit</C:nresults>
</C:limit>
</C:addressbook-query>
EOFCONTENTSEARCH;
}
else {
$xml = <<<EOFCONTENTSEARCH
<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<C:address-data>
<C:allprop/>
</C:address-data>
</D:prop>
<C:filter test="anyof">
<C:prop-filter name="sn">
<C:text-match collation="i;unicode-casemap" negate-condition="no" match-type="contains">$pattern</C:text-match>
</C:prop-filter>
<C:prop-filter name="givenname">
<C:text-match collation="i;unicode-casemap" negate-condition="no" match-type="contains">$pattern</C:text-match>
</C:prop-filter>
<C:prop-filter name="email">
<C:text-match collation="i;unicode-casemap" negate-condition="no" match-type="contains">$pattern</C:text-match>
</C:prop-filter>
</C:filter>
<C:limit>
<C:nresults>$limit</C:nresults>
</C:limit>
</C:addressbook-query>
EOFCONTENTSEARCH;
}
return $this->do_query_report($xml, $include_vcards, $raw, true);
}
/**
* Get all vcards or changes since the last sync.
*
* @param boolean $initial If the sync should be full
* @param boolean $include_vcards If the vCards should be included within the response
* @param boolean $support_carddav_sync If the cardDAV server supports sync-collection operations (DAViCal supports it)
* @return string Simplified XML response
*/
public function do_sync($initial = true, $include_vcards = false, $support_carddav_sync = false) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->do_sync"));
if ($support_carddav_sync) {
if ($initial) {
$token = "";
}
else {
$token = $this->synctoken[$this->url];
}
$xml = <<<EOFXMLINITIALSYNC
<?xml version="1.0" encoding="utf-8"?>
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>$token</D:sync-token>
<D:sync-level>1</D:sync-level>
<D:prop>
<D:getetag/>
<D:getlastmodified/>
</D:prop>
</D:sync-collection>
EOFXMLINITIALSYNC;
return $this->do_query_report($xml, $include_vcards, false);
}
else {
return $this->get($include_vcards, false);
}
}
/**
* Do a REPORT query against the server
*
* @param string $xml XML body request
* @param boolean $include_vcards If the vCards should be included within the response
* @param boolean $raw If the response should be raw or XML simplified
* @param boolean $remove_duplicates If we will apply uniqness to the response vcards
* @return string
*/
private function do_query_report($xml, $include_vcards = true, $raw = false, $remove_duplicates = false) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->do_query_report"));
$result = $this->query($this->url, 'REPORT', $xml, 'text/xml');
try {
switch ($result['http_code']) {
case 200:
case 207:
if ($raw === true) {
return $result['response'];
}
else {
return $this->simplify($result['response'], $include_vcards, $remove_duplicates);
}
break;
default:
throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_GET);
break;
}
}
catch(Exception $ex) {
// vcard not found
if ($ex->getCode() == self::EXCEPTION_COULD_NOT_FIND_VCARD_HREF) {
if (strlen($this->url_vcard_extension) == 0 || stripos($xml, $this->url_vcard_extension) === FALSE) {
throw $ex;
}
else {
// try to do the same without the $this->url_vcard_extension
return $this->do_query_report(str_ireplace($this->url_vcard_extension, "", $xml), $include_vcards, $raw, $remove_duplicates);
}
}
else {
throw $ex;
}
}
}
/**
* Gets a clean vCard from the CardDAV server
*
* @param string $vcard_href vCard href on the CardDAV server
* @return string vCard (text/vcard)
*/
private function get_vcard($vcard_href) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->get_vcard"));
$url = $this->url_parts['scheme'] . '://' . $this->url_parts['host'] . ':' . $this->url_parts['port'] . $vcard_href;
$result = $this->query($url, 'GET');
switch ($result['http_code']) {
case 200:
case 207:
return $result['response'];
break;
default:
throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_GET_VCARD);
break;
}
}
/**
* Gets a vCard + XML from the CardDAV Server
*
* @param string $vcard_id vCard id on the CardDAV Server
* @return string Raw or simplified vCard (text/xml)
*/
public function get_xml_vcard($vcard_id) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->get_xml_vcard"));
$href = $this->url_parts['path'] . str_replace($this->url_vcard_extension, null, $vcard_id) . $this->url_vcard_extension;
// If we don't ask for allprop, SOGo doesn't return the content_type
$xml = <<<EOFXMLGETXMLVCARD
<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<D:getlastmodified/>
<C:address-data>
<C:allprop/>
</C:address-data>
</D:prop>
<D:href>$href</D:href>
</C:addressbook-multiget>
EOFXMLGETXMLVCARD;
return $this->do_query_report($xml);
}
/**
* Enables the debug mode
*
* @return void
*/
public function enable_debug() {
$this->debug = true;
}
/**
* Checks if the CardDAV server is reachable
*
* @return boolean
*/
public function check_connection() {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->check_connection"));
$result = $this->query($this->url, 'OPTIONS');
$status = false;
switch($result['http_code']) {
case 200:
case 207:
$status = true;
break;
}
return $status;
}
/**
* Deletes an entry from the CardDAV server
*
* @param string $vcard_id vCard id on the CardDAV server
* @return boolean
*/
public function delete($vcard_id) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->delete"));
$result = $this->query($this->url . $vcard_id . $this->url_vcard_extension, 'DELETE');
switch ($result['http_code']) {
case 204:
return true;
break;
default:
throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_DELETE);
break;
}
}
/**
* Adds an entry to the CardDAV server
*
* @param string $vcard vCard
* @param string $vcard_id vCard id on the CardDAV server
* @return string The new vCard id
*/
public function add($vcard, $vcard_id = null) {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->add"));
if ($vcard_id === null) {
$vcard_id = $this->generate_vcard_id();
}
$vcard = str_replace("\nEND:VCARD","\nUID:" . $vcard_id . "\r\nEND:VCARD", $vcard);
$result = $this->query($this->url . $vcard_id . $this->url_vcard_extension, 'PUT', $vcard, 'text/vcard');
switch($result['http_code']) {
case 201:
case 204:
return $vcard_id;
break;
default:
throw new Exception('Woops, something\'s gone wrong! The CardDAV server returned the http status code ' . $result['http_code'] . '.', self::EXCEPTION_WRONG_HTTP_STATUS_CODE_ADD);
break;
}
}
/**
* Updates an entry to the CardDAV server
*
* @param string $vcard vCard
* @param string $vcard_id vCard id on the CardDAV server
* @return boolean
*/
public function update($vcard, $vcard_id) {
try {
return $this->add($vcard, $vcard_id);
}
catch (Exception $e) {
throw new Exception($e->getMessage(), self::EXCEPTION_WRONG_HTTP_STATUS_CODE_UPDATE);
}
}
/**
* Simplify CardDAV XML response
*
* @param string $response CardDAV XML response
* @param boolean $include_vcards Include vCards or not
* @param boolean $remove_duplicates If we will apply uniqness to the response vcards
* @return string Simplified CardDAV XML response
*/
private function simplify($response, $include_vcards = true, $remove_duplicates = false) {
$response = $this->remove_namespaces($response);
try {
$xml = new SimpleXMLElement($response);
}
catch(Exception $e) {
throw new Exception('The XML response seems to be malformed and can\'t be simplified!', self::EXCEPTION_MALFORMED_XML_RESPONSE, $e);
}
if (!empty($xml->{'sync-token'})) {
$this->synctoken[$this->url] = $xml->{'sync-token'};
}
$simplified_xml = new XMLWriter();
$simplified_xml->openMemory();
$simplified_xml->setIndent(4);
$simplified_xml->startDocument('1.0', 'utf-8');
$simplified_xml->startElement('response');
if (!empty($xml->response)) {
$unique_etags = array();
foreach ($xml->response as $response) {
if (isset($response->propstat)) {
if ((strlen($this->url_vcard_extension) > 0 && preg_match('/'.$this->url_vcard_extension.'/', $response->href) &&
!(isset($response->propstat->prop->resourcetype) && isset($response->propstat->prop->resourcetype->addressbook)))
|| preg_match('/vcard/', $response->propstat->prop->getcontenttype) || isset($response->propstat->prop->{'address-data'}) || isset($response->propstat->prop->{'addressbook-data'})) {
// It's a vcard
$id = basename($response->href);
$id = str_replace($this->url_vcard_extension, null, $id);
if (!empty($id)) {
$simplified_xml->startElement('element');
$simplified_xml->writeElement('id', $id);
$simplified_xml->writeElement('etag', str_replace('"', null, $response->propstat->prop->getetag));
$simplified_xml->writeElement('last_modified', $response->propstat->prop->getlastmodified);
if ($include_vcards === true) {
if (isset($response->propstat->prop->{'address-data'})) {
// We already have the full vcard
$simplified_xml->writeElement('vcard', $response->propstat->prop->{'address-data'});
}
else if (isset($response->propstat->prop->{'addressbook-data'})) {
// We already have the full vcard, also
$simplified_xml->writeElement('vcard', $response->propstat->prop->{'addressbook-data'});
}
else {
// We don't have the vcard, we need to get it. We never should hit here, it would mean a buggy server
$simplified_xml->writeElement('vcard', $this->get_vcard($response->href));
}
}
$simplified_xml->endElement();
}
}
else if (isset($response->propstat->prop->resourcetype->addressbook)) {
// It's an addressbook
if (isset($response->propstat->prop->href)) {
$href = $response->propstat->prop->href;
}
else if (isset($response->href)) {
$href = $response->href;
}
else {
$href = null;
}
$url = str_replace($this->url_parts['path'], null, $this->url) . $href;
$simplified_xml->startElement('addressbook_element');
$simplified_xml->writeElement('display_name', $response->propstat->prop->displayname);
$simplified_xml->writeElement('url', $url);
$simplified_xml->writeElement('last_modified', $response->propstat->prop->getlastmodified);
$simplified_xml->endElement();
}
}
else {
// We don't have a propstat node, so it will be an error answer
if (isset($response->status) && preg_match('/404 Not Found/', $response->status)) {
throw new Exception('Not found!', self::EXCEPTION_COULD_NOT_FIND_VCARD_HREF);
}
else {
throw new Exception('The XML response is an error message and can\'t be simplified!', self::EXCEPTION_MALFORMED_XML_RESPONSE);
}
}
}
unset($unique_etags);
}
$simplified_xml->endElement();
$simplified_xml->endDocument();
return $simplified_xml->outputMemory();
}
/**
* Cleans CardDAV XML response
*
* @param string $response CardDAV XML response
* @return string $response Cleaned CardDAV XML response
*/
private function remove_namespaces($response) {
// $response = preg_replace('/<[a-z0-9]+:(.*)/i', '<$1', $response);
// $response = preg_replace('/<\/[a-z0-9]+:(.*)/i', '</$1', $response);
// Removing namespace it's pretty hard with regex.
// Also, each server uses different namespaces, so using namespaces it's impossible
// This uses DOMDocument, so it won't be the fastest or least memory-using way.
// Feel free to suggest or improve it
// http://stackoverflow.com/questions/15634291/remove-name-space-from-xml-file-and-save-as-new-xml
$xsl = <<<EOFXSL
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="xml" version="1.0" encoding="UTF-8" />
<xsl:template match="*">
<xsl:element name="{local-name()}">
<xsl:apply-templates select="@* | node()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
EOFXSL;
$dom = new DOMDocument();
$dom->loadXML($response);
$stylesheet = new DOMDocument();
$stylesheet->loadXML($xsl);
$xsltprocessor = new XSLTProcessor();
$xsltprocessor->importStylesheet($stylesheet);
$response = $xsltprocessor->transformToXML($dom);
$dom = null;
$stylesheet = null;
$xsltprocessor = null;
return $response;
}
/**
* Curl initialization
*
* @return void
*/
public function curl_init() {
if ($this->curl === false) {
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_HEADER, true);
curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_USERAGENT, self::USERAGENT.self::VERSION);
if ($this->auth !== null) {
curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt($this->curl, CURLOPT_USERPWD, $this->auth);
}
}
}
/**
* Query the CardDAV server via curl and returns the response
*
* @param string $url CardDAV server URL
* @param string $method HTTP method like (OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE)
* @param string $content Content for CardDAV queries
* @param string $content_type Set content type
* @param string $depth Set Depth
* @return array Raw CardDAV Response and http status code
*/
private function query($url, $method, $content = null, $content_type = null, $depth = "infinity") {
// ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCardDAV->carddav_backend->query - '%s' '%s' '%s' '%s'", $url, $method, $content, $content_type));
$this->curl_init();
curl_setopt($this->curl, CURLOPT_URL, $url);
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method);
if ($content !== null) {
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, $content);
}
else {
curl_setopt($this->curl, CURLOPT_POST, false);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, null);
}
if ($content_type !== null) {
curl_setopt($this->curl, CURLOPT_HTTPHEADER, array('Content-type: '.$content_type. '; charset=utf-8', 'Depth: '.$depth));
}
else {
curl_setopt($this->curl, CURLOPT_HTTPHEADER, array('Depth: '.$depth));
}
$complete_response = curl_exec($this->curl);
$header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE);
$http_code = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
$header = trim(substr($complete_response, 0, $header_size));
$response = substr($complete_response, $header_size);
$return = array(
'response' => $response,
'http_code' => $http_code
);
if ($this->debug === true) {
$debug = $return;
$debug['url'] = $url;
$debug['method'] = $method;
$debug['content'] = $content;
$debug['content_type'] = $content_type;
$debug['header'] = $header;
$this->set_debug($debug);
}
return $return;
}
/**
* Returns a valid and unused vCard id
*
* @return string $vcard_id Valid vCard id
*/
private function generate_vcard_id() {
$vcard_id = null;
for ($number = 0; $number <= 25; $number ++) {
if ($number == 8 || $number == 17) {
$vcard_id .= '-';
}
else {
$vcard_id .= $this->vcard_id_chars[mt_rand(0, (count($this->vcard_id_chars) - 1))];
}
}
try {
$carddav = new carddav_backend($this->url);
$carddav->set_auth($this->username, $this->password);
$result = $carddav->query($this->url . $vcard_id . $this->url_vcard_extension, 'GET');
if ($result['http_code'] !== 404) {
$vcard_id = $this->generate_vcard_id();
}
return $vcard_id;
}
catch (Exception $e) {
throw new Exception($e->getMessage(), self::EXCEPTION_COULD_NOT_GENERATE_NEW_VCARD_ID);
}
}
/**
* Destructor
* Close curl connection if it's open
*
* @return void
*/
public function __destruct() {
$this->disconnect();
}
/**
* Disconnect curl connection
*
*/
public function disconnect() {
if ($this->curl !== false) {
curl_close($this->curl);
$this->curl = false;
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment