*/ // must be run within Dokuwiki if(!defined('DOKU_INC')) die(); class helper_plugin_davcard extends DokuWiki_Plugin { protected $sqlite = null; /** * Constructor to load the configuration */ public function helper_plugin_davcard() { } /** Establish and initialize the database if not already done * @return sqlite interface or false */ private function getDB() { if($this->sqlite === null) { $this->sqlite = plugin_load('helper', 'sqlite'); if(!$this->sqlite) { dbglog('This plugin requires the sqlite plugin. Please install it.'); msg('This plugin requires the sqlite plugin. Please install it.', -1); return false; } if(!$this->sqlite->init('davcard', DOKU_PLUGIN.'davcard/db/')) { $this->sqlite = null; dbglog('Error initialising the SQLite DB for davcard'); return false; } } return $this->sqlite; } /** * Retrieve a contact by specifying details like the name * * @param int $id The address book ID * @param string $type The type to look for * @param array $params The parameter array * * @return array An array containing the results */ private function getContactByDetails($id, $type, $params = array()) { $write = false; if(strpos($id, 'webdav://') === 0) { $wdc =& plugin_load('helper', 'webdavclient'); if(is_null($wdc)) return $this->getLang('no_wdc'); $connectionId = str_replace('webdav://', '', $id); $settings = $wdc->getConnection($connectionId); if($settings === false) return array('formattedname' => $this->getLang('settings_not_found'), 'result' => false); if($settings['type'] !== 'contacts') return array('formattedname' => $this->getLang('wrong_type'), 'result' => false); $entries = $wdc->getAddressbookEntries($connectionId); $write = $settings['write']; } else { $acl = auth_quickaclcheck($id); if($acl > AUTH_READ) { $write = true; } elseif($acl < AUTH_READ) { return array('formattedname' => $this->getLang('no_permission'), 'result' => false); } else { $write = false; } $addressbookid = $this->getAddressbookIdForPage($id); $entries = $this->getAddressbookEntries($addressbookid); } foreach($entries as $entry) { switch($type) { case 'structuredname': $contactdata = explode(';', strtolower($entry['structuredname'])); if(count($contactdata) < 2) // We need at least first and last name return array('formattedname' => sprintf($this->getLang('contact_not_found'), $params['firstname']. ' '.$params['lastname']), 'result' => false); if(($params['lastname'] != '') && ($contactdata[0] === $params['lastname']) || $params['lastname'] === '') { // last name matched or no last name given if(($params['firstname'] != '') && ($contactdata[1] === $params['firstname']) || $params['firstname'] === '') { // first name matched too or no first name given $info = $this->parseVcard($entry['contactdata'], $entry['uri'], $write); return $info; } } break; case 'formattedname': if(trim(strtolower($entry['formattedname'])) == $params['formattedname']) { $info = $this->parseVcard($entry['contactdata'], $entry['uri'], $write); return $info; } break; case 'email': $info = $this->parseVcard($entry['contactdata'], $entry['uri'], $write); foreach($info['mail'] as $data) { if(trim(strtolower($data['mail'])) === $params['email']) return $info; } break; } } return array('formattedname' => sprintf($this->getLang('contact_not_found'), $this->getLang('invalid_options')), 'result' => false); } /** * Retreive all address book entries * * @param int $id The addressbook ID to retrieve * * @return array All address book entries */ public function getAddressbookEntries($id) { $sqlite = $this->getDB(); if(!$sqlite) return false; $query = "SELECT contactdata, uri, formattedname, structuredname FROM addressbookobjects WHERE addressbookid = ? ORDER BY formattedname ASC"; $res = $sqlite->query($query, $id); return $sqlite->res2arr($res); } /** * Retrieve a contact by the structured name * * @param string $id The addressbook ID to work with * @param string $firstname The contact's first name * @param string $lastname The contact's last name * * @return array The contact's details */ public function getContactByStructuredName($id, $firstname = '', $lastname = '') { return $this->getContactByDetails($id, 'structuredname', array('firstname' => strtolower($firstname), 'lastname' => strtolower($lastname))); } /** * Retrieve a contact by e-mail address * * @param string $id The address book ID * @param string $email The E-Mail address * * @return array The contact's details */ public function getContactByEmail($id, $email) { // FIXME: Maybe it's a good idea to save the e-mail in the database as well! return $this->getContactByDetails($id, 'email', array('email' => strtolower($email))); } /** * Retrieve a contact by formatted name * * @param string $id The address book ID * @param string $name The contact's formatted name * * @return array The contact's details */ public function getContactByFormattedName($id, $name) { return $this->getContactByDetails($id, 'formattedname', array('formattedname' => strtolower($name))); } /** * Retrieve a contact object by its URI * * @param string $ID The address book ID * @param string $uri The object URI * * @return array An array containing the result */ public function getContactByUri($id, $uri) { $write = false; if(strpos($id, 'webdav://') === 0) { $wdc =& plugin_load('helper', 'webdavclient'); if(is_null($wdc)) return $this->getLang('no_wdc'); $connectionId = str_replace('webdav://', '', $id); $settings = $wdc->getConnection($connectionId); if($settings === false) return array('formattedname' => $this->getLang('settings_not_found'), 'result' => false); if($settings['type'] !== 'contacts') return array('formattedname' => $this->getLang('wrong_type'), 'result' => false); $row = $wdc->getAddressbookEntryByUri($connectionId, $uri); $write = $settings['write']; } else { $acl = auth_quickaclcheck($id); if($acl > AUTH_READ) { $write = true; } elseif($acl < AUTH_READ) { return array('formattedname' => $this->getLang('no_permission'), 'result' => false); } else { $write = false; } $addressbookid = $this->getAddressbookIdForPage($id); $row = $this->getAddressbookEntryByUri($addressbookid, $uri); } if($row === false) return array('formattedname' => sprintf($this->getLang('contact_not_found'), 'ID='.$id.' URI='.$uri), 'result' => false); $info = $this->parseVcard($row['contactdata'], $row['uri'], $write); $info['result'] = true; return $info; } /** * Retrieve an address book entry by URI (low-level version) * * @param int $id The address book ID * @param string $uri The object URI * * @return array The contact's details */ private function getAddressbookEntryByUri($id, $uri) { $sqlite = $this->getDB(); if(!$sqlite) return false; $query = "SELECT contactdata, addressbookid, etag, uri, formattedname, structuredname FROM addressbookobjects WHERE addressbookid = ? AND uri = ?"; $res = $sqlite->query($query, $id, $uri); return $sqlite->res2row($res); } /** * Set the addressbook name for a given page * * @param string $name The name to set * @param string $description The address book description * @param int $id (optional) The page ID * @param string $userid (optional) The user's ID * * @return boolean true on success, otherwise false */ public function setAddressbookNameForPage($name, $description, $id = null, $userid = null) { if(is_null($id)) { global $ID; $id = $ID; } if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { $userid = uniqid('davcard-'); } } $bookid = $this->getAddressbookIdForPage($id); if($bookid === false) return $this->createAddressbookForPage($name, $description, $id, $userid); $sqlite = $this->getDB(); if(!$sqlite) return false; $query = "UPDATE addressbooks SET displayname = ?, description = ? WHERE id = ?"; $res = $sqlite->query($query, $name, $description, $bookid); if($res !== false) return true; return false; } /** * Get the address book ID associated with a given page * * @param string $id (optional) The page ID * * @return mixed The address book ID or false */ public function getAddressbookIdForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $sqlite = $this->getDB(); if(!$sqlite) return false; $query = "SELECT addressbookid FROM pagetoaddressbookmapping WHERE page = ?"; $res = $sqlite->query($query, $id); $row = $sqlite->res2row($res); if(isset($row['addressbookid'])) { $addrbkid = $row['addressbookid']; return $addrbkid; } return false; } /** * Create a new address book for a given page * * @param string $name The name of the new address book * @param string $description The address book's description * @param string $id (optional) The page ID * @param string $userid (optional) The user's ID * * @return boolean True on success, otherwise false */ public function createAddressbookForPage($name, $description, $id = null, $userid = null) { if(is_null($id)) { global $ID; $id = $ID; } if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { $userid = uniqid('davcard-'); } } $sqlite = $this->getDB(); if(!$sqlite) return false; $values = array('principals/'.$userid, $name, str_replace(array('/', ' ', ':'), '_', $id), $description, 1); $query = "INSERT INTO addressbooks (principaluri, displayname, uri, description, synctoken) ". "VALUES (?, ?, ?, ?, ?)"; $res = $sqlite->query($query, $values); if($res === false) return false; // Get the new addressbook ID $query = "SELECT id FROM addressbooks WHERE principaluri = ? AND displayname = ? AND ". "uri = ? AND description = ? AND synctoken = ?"; $res = $sqlite->query($query, $values); $row = $sqlite->res2row($res); // Update the pagetocalendarmapping table with the new calendar ID if(isset($row['id'])) { $query = "INSERT INTO pagetoaddressbookmapping (page, addressbookid) VALUES (?, ?)"; $res = $sqlite->query($query, $id, $row['id']); return ($res !== false); } return false; } /** * Delete a contact entry from an address book by URI * * @param string $id The address book ID * @param string $user The user's ID * @param string $uri The object URI to delete * * @return boolean True on success, otherwise false */ public function deleteContactEntryToAddressbookForPage($id, $user, $uri) { if(strpos($id, 'webdav://') === 0) { $wdc =& plugin_load('helper', 'webdavclient'); if(is_null($wdc)) return $this->getLang('no_wdc'); $connectionId = str_replace('webdav://', '', $id); $settings = $wdc->getConnection($connectionId); if($settings === false) return array('formattedname' => $this->getLang('settings_not_found'), 'result' => false); if($settings['type'] !== 'contacts') return array('formattedname' => $this->getLang('wrong_type'), 'result' => false); return $wdc->deleteAddressbookEntry($connectionId, $uri); } $sqlite = $this->getDB(); if(!$sqlite) return false; $addressbookid = $this->getAddressbookIdForPage($id); $query = "DELETE FROM addressbookobjects WHERE uri = ? AND addressbookid = ?"; $res = $sqlite->query($query, $uri, $addressbookid); if($res !== false) { $this->updateSyncTokenLog($addressbookid, $uri, 'deleted'); return true; } return false; } /** * Edit a contact for a given address book * * @param string $id The address book ID * @param string $user The user name * @param string $uri The object URI * @param array $params The new address book parameters * * @return boolean True on success, otherwise false */ public function editContactEntryToAddressbookForPage($id, $user, $uri, $params) { require_once(DOKU_PLUGIN.'davcard/vendor/autoload.php'); if(strpos($id, 'webdav://') === 0) { $wdc =& plugin_load('helper', 'webdavclient'); if(is_null($wdc)) return $this->getLang('no_wdc'); $connectionId = str_replace('webdav://', '', $id); $settings = $wdc->getConnection($connectionId); if($settings === false) return array('formattedname' => $this->getLang('settings_not_found'), 'result' => false); if($settings['type'] !== 'contacts') return array('formattedname' => $this->getLang('wrong_type'), 'result' => false); $row = $wdc->getAddressbookEntryByUri($connectionId, $uri); } else { $addressbookid = $this->getAddressbookIdForPage($id); $row = $this->getAddressbookEntryByUri($addressbookid, $uri); } $vcard = \Sabre\VObject\Reader::read($row['contactdata']); $vcard->remove('ADR'); $vcard->remove('TEL'); $vcard->remove('EMAIL'); if(isset($params['phones'])) { foreach($params['phones'] as $data) { $vcard->add('TEL', $data['number'], array('type' => $data['type'])); } } if(isset($params['email'])) { foreach($params['email'] as $data) { $vcard->add('EMAIL', $data['mail'], array('type' => $data['type'])); } } if(isset($params['addresses'])) { foreach($params['addresses'] as $data) { $vcard->add('ADR', array('', '', $data['street'], $data['city'], '', $data['zipcode'], $data['country']), array('type' => $data['type'])); } } $structuredname = explode(';', (string)$vcard->N); $structuredname[0] = $params['lastname']; $structuredname[1] = $params['firstname']; $formattedname = $params['firstname'].' '.$params['lastname']; // FIXME: Make this configurable? $vcard->N = $structuredname; $vcard->FN = $formattedname; $contactdata = $vcard->serialize(); if(strpos($id, 'webdav://') === 0) { return $wdc->editAddressbookEntry($connectionId, $uri, $contactdata); } else { $sqlite = $this->getDB(); if(!$sqlite) return false; $now = new \DateTime(); $query = "UPDATE addressbookobjects SET contactdata = ?, lastmodified = ?, etag = ?, size = ?, formattedname = ?, structuredname = ? WHERE addressbookid = ? AND uri = ?"; $res = $sqlite->query($query, $contactdata, $now->getTimestamp(), md5($contactdata), strlen($contactdata), $formattedname, implode(';', $structuredname), $addressbookid, $uri ); if($res !== false) { $this->updateSyncTokenLog($addressbookid, $uri, 'modified'); return true; } } return false; } /** * Add a new contact entry to an address book page * * @param string $id The page ID * @param string $user The user ID * @param array $params The entry's parameters * * @return boolean True on success, otherwise false */ public function addContactEntryToAddressbookForPage($id, $user, $params) { require_once(DOKU_PLUGIN.'davcard/vendor/autoload.php'); $vcard = new \Sabre\VObject\Component\VCard(); $formattedname = $params['firstname'].' '.$params['lastname']; // FIXME: Make this configurable? $structuredname = array($params['lastname'], $params['firstname'], '', '', ''); $vcard->FN = $formattedname; $vcard->N = $structuredname; if(isset($params['phones'])) { foreach($params['phones'] as $data) { $vcard->add('TEL', $data['number'], array('type' => $data['type'])); } } if(isset($params['email'])) { foreach($params['email'] as $data) { $vcard->add('EMAIL', $data['mail'], array('type' => $data['type'])); } } if(isset($params['addresses'])) { foreach($params['addresses'] as $data) { $vcard->add('ADR', array('', '', $data['street'], $data['city'], '', $data['zipcode'], $data['country']), array('type' => $data['type'])); } } $contactdata = $vcard->serialize(); if(strpos($id, 'webdav://') === 0) { $wdc =& plugin_load('helper', 'webdavclient'); if(is_null($wdc)) return false; $connectionId = str_replace('webdav://', '', $id); return $wdc->addAddressbookEntry($connectionId, $contactdata); } else { $sqlite = $this->getDB(); if(!$sqlite) return false; $addressbookid = $this->getAddressbookIdForPage($id); $uri = uniqid('dokuwiki-').'.vcf'; $now = new \DateTime(); $query = "INSERT INTO addressbookobjects (contactdata, uri, addressbookid, lastmodified, etag, size, formattedname, structuredname) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; $res = $sqlite->query($query, $contactdata, $uri, $addressbookid, $now->getTimestamp(), md5($contactdata), strlen($contactdata), $formattedname, implode(';', $structuredname) ); // If successfully, update the sync token database if($res !== false) { $this->updateSyncTokenLog($addressbookid, $uri, 'added'); return true; } } return false; } /** * Parse a VCard and extract important contact information * * @param string $card The VCard data * @param string $uri The object URI * @param boolean $write Writable * * @return array An array with parsed data */ public function parseVcard($card, $uri, $write) { require_once(DOKU_PLUGIN.'davcard/vendor/autoload.php'); $vObject = \Sabre\VObject\Reader::read($card); $formattedname = ''; $structuredname = ''; $tel = array(); $addr = array(); $mail = array(); $photo = array(); $birthday = ''; $note = ''; $title = ''; $url = ''; if(isset($vObject->FN)) $formattedname = (string)$vObject->FN; if(isset($vObject->N)) $structuredname = join(';', $vObject->N->getParts()); if(isset($vObject->TEL)) { foreach($vObject->TEL as $number) { if(isset($number['TYPE'])) $tel[] = array('type' => strtolower((string)$number['TYPE']), 'number' => (string)$number); else $tel[] = array('number' => (string)$number); } } if(isset($vObject->ADR)) { foreach($vObject->ADR as $adr) { if(isset($adr['TYPE'])) $addr[] = array('type' => strtolower((string)$adr['TYPE']), 'address' => $adr->getParts()); else $addr[] = array('address' => $adr->getParts()); } } if(isset($vObject->EMAIL)) { foreach($vObject->EMAIL as $email) { if(isset($email['TYPE'])) $mail[] = array('type' => strtolower((string)$email['TYPE']), 'mail' => (string)$email); else $mail[] = array('mail' => (string)$email); } } if(isset($vObject->PHOTO)) { if(isset($vObject->PHOTO['TYPE'])) { $photo[] = array('type' => strtolower((string)$vObject->PHOTO['TYPE']), 'photo' => (string)$vObject->PHOTO); } else $photo[] = array('photo' => (string)$vObject->PHOTO); } if(isset($vObject->BDAY)) { $birthday = (string)$vObject->BDAY; $birthday = str_replace('-', '', $birthday); } if(isset($vObject->NOTE)) { $note = (string)$vObject->NOTE; } if(isset($vObject->TITLE)) { $title = (string)$vObject->TITLE; } if(isset($vObject->URL)) { $url = (string)$vObject->URL; } return array( 'formattedname' => $formattedname, 'structuredname' => $structuredname, 'tel' => $tel, 'mail' => $mail, 'addr' => $addr, 'uri' => $uri, 'photo' => $photo, 'birthday' => $birthday, 'note' => $note, 'title' => $title, 'url' => $url, 'result' => true, 'write' => $write ); } /** * Retrieve the settings of a given address book * * @param int $addressbookid The addressbook's ID * * @return array The settings */ public function getAddressbookSettings($addressbookid) { $sqlite = $this->getDB(); if(!$sqlite) return false; $query = "SELECT id, principaluri, displayname, uri, description, synctoken FROM addressbooks WHERE id= ? "; $res = $sqlite->query($query, $addressbookid); $row = $sqlite->res2row($res); return $row; } /** * Retrieve the current synctoken for an address book * * @param int $addressbookid The addressbook's ID * * @return string The current synctoken */ public function getSyncTokenForAddressbook($addressbookid) { $row = $this->getAddressbookSettings($addressbookid); if(isset($row['synctoken'])) return $row['synctoken']; return false; } /** * Helper function to convert the operation name to * an operation code as stored in the database * * @param string $operationName The operation name * * @return mixed The operation code or false */ public function operationNameToOperation($operationName) { switch($operationName) { case 'added': return 1; break; case 'modified': return 2; break; case 'deleted': return 3; break; } return false; } /** * Update the synctoken log for a given address book * * @param string $addressbookid The addressbook ID to work with * @param string $uri The object URI that was modified * @param string $operation The operation that was performed * * @return boolean True on success, otherwise false */ private function updateSyncTokenLog($addressbookid, $uri, $operation) { $currentToken = $this->getSyncTokenForAddressbook($addressbookid); $operationCode = $this->operationNameToOperation($operation); if(($operationCode === false) || ($currentToken === false)) return false; $values = array($uri, $currentToken, $addressbookid, $operationCode ); $sqlite = $this->getDB(); if(!$sqlite) return false; $query = "INSERT INTO addressbookchanges (uri, synctoken, addressbookid, operation) VALUES(?, ?, ?, ?)"; $res = $sqlite->query($query, $uri, $currentToken, $addressbookid, $operationCode); if($res === false) return false; $currentToken++; $query = "UPDATE addressbooks SET synctoken = ? WHERE id = ?"; $res = $sqlite->query($query, $currentToken, $addressbookid); return ($res !== false); } /** * Check the permission of a user for a given addressbook ID * * @param string $id The addressbook ID to check * @return int AUTH_* constants */ public function checkAddressbookPermission($id) { if(strpos($id, 'webdav://') === 0) { $wdc =& plugin_load('helper', 'webdavclient'); if(is_null($wdc)) return AUTH_NONE; $connectionId = str_replace('webdav://', '', $id); $settings = $wdc->getConnection($connectionId); if($settings === false) return AUTH_NONE; if($settings['write'] === '1') return AUTH_CREATE; return AUTH_READ; } else { $addr = $this->getAddressbookIdForPage($id); // We return AUTH_READ if the calendar does not exist. This makes // davcard happy when there are just included addressbooks if($addr === false) return AUTH_READ; return auth_quickaclcheck($id); } } }