| 1: | <?php |
| 2: | |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | if (!defined('DEBUG_MODE')) { die(); } |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | class Hm_Carddav { |
| 15: | |
| 16: | public $addresses = array(); |
| 17: | private $src; |
| 18: | private $url; |
| 19: | private $user; |
| 20: | private $pass; |
| 21: | private $principal_url; |
| 22: | private $address_url; |
| 23: | private $principal_path = '//response/propstat/prop/current-user-principal/href'; |
| 24: | private $addressbook_path = '//response/propstat/prop/addressbook-home-set/href'; |
| 25: | private $addr_list_path = '//response/href'; |
| 26: | private $addr_detail_path = '//response/propstat/prop/address-data'; |
| 27: | private $card_flds = array( |
| 28: | 'carddav_email' => 'email', |
| 29: | 'carddav_phone' => 'tel', |
| 30: | 'carddav_fn' => 'fn' |
| 31: | ); |
| 32: | private $api; |
| 33: | |
| 34: | public function __construct($src, $url, $user, $pass) { |
| 35: | $this->user = $user; |
| 36: | $this->src = $src; |
| 37: | $this->pass = $pass; |
| 38: | $this->url = $url; |
| 39: | $this->api = new Hm_API_Curl('xml'); |
| 40: | } |
| 41: | |
| 42: | public function get_vcards() { |
| 43: | if (!$this->discover()) { |
| 44: | return; |
| 45: | } |
| 46: | $res = array(); |
| 47: | $parser = new Hm_VCard(); |
| 48: | $count = 0; |
| 49: | foreach ($this->xml_find($this->list_addressbooks(), $this->addr_list_path, true) as $url) { |
| 50: | $url = $this->url_concat($url); |
| 51: | if ($url == $this->address_url) { |
| 52: | continue; |
| 53: | } |
| 54: | $count++; |
| 55: | Hm_Debug::add(sprintf('CARDDAV: Trying contacts url %s', $url)); |
| 56: | foreach ($this->xml_find($this->report($url), $this->addr_detail_path, true) as $addr) { |
| 57: | $parser->import($addr); |
| 58: | foreach ($this->convert_to_contact($parser) as $contact) { |
| 59: | $contact['src_url'] = $url; |
| 60: | $res[] = $contact; |
| 61: | } |
| 62: | } |
| 63: | } |
| 64: | Hm_Debug::add(sprintf('CARDDAV: %s contact urls found', $count)); |
| 65: | $this->addresses = $res; |
| 66: | } |
| 67: | |
| 68: | public function add_contact($form) { |
| 69: | if (!$this->discover()) { |
| 70: | return false; |
| 71: | } |
| 72: | $filename = sha1(time().json_encode($form)); |
| 73: | $uid = sha1($filename); |
| 74: | $card = array('BEGIN:VCARD', 'VERSION:3', sprintf('UID:%s', $uid)); |
| 75: | foreach ($this->card_flds as $name => $cname) { |
| 76: | if (array_key_exists($name, $form) && trim($form[$name])) { |
| 77: | $card[] = sprintf('%s:%s', mb_strtoupper($cname), $form[$name]); |
| 78: | } |
| 79: | } |
| 80: | $card[] = 'END:VCARD'; |
| 81: | $url = sprintf('%s%s.vcf', $this->address_url, $filename); |
| 82: | $card = implode("\n", $card); |
| 83: | return $this->update_server_contact($url, $card); |
| 84: | } |
| 85: | |
| 86: | public function delete_contact($contact) { |
| 87: | return $this->delete_server_contact($contact->value('src_url')); |
| 88: | } |
| 89: | |
| 90: | public function update_contact($contact, $form) { |
| 91: | $parsed = $contact->value('carddav_parsed'); |
| 92: | $parsed = $this->update_or_add('carddav_email', $form, $parsed); |
| 93: | $parsed = $this->update_or_add('carddav_fn', $form, $parsed); |
| 94: | $parsed = $this->update_or_add('carddav_phone', $form, $parsed); |
| 95: | $new_card = $this->convert_to_card($parsed); |
| 96: | return $this->update_server_contact($contact->value('src_url'), $new_card); |
| 97: | } |
| 98: | |
| 99: | private function find_by_id($type, $form, $data) { |
| 100: | if (!array_key_exists($type, $form) || !trim($form[$type])) { |
| 101: | return false; |
| 102: | } |
| 103: | $id = $form[$type]; |
| 104: | foreach ($data as $name => $entry) { |
| 105: | if (!is_array($entry)) { |
| 106: | continue; |
| 107: | } |
| 108: | foreach ($entry as $index => $vals) { |
| 109: | if (array_key_exists('id', $vals) && $id == $vals['id']) { |
| 110: | return array($name, $index); |
| 111: | } |
| 112: | } |
| 113: | } |
| 114: | return false; |
| 115: | } |
| 116: | |
| 117: | private function update_or_add($type, $form, $parsed) { |
| 118: | $path = $this->find_by_id($type.'_id', $form, $parsed); |
| 119: | if ($path === false && trim($form[$type])) { |
| 120: | $start = array_splice($parsed, 0, 2); |
| 121: | $start[$this->card_flds[$type]] = array(array('values' => $form[$type])); |
| 122: | $parsed = array_merge($start, $parsed); |
| 123: | } |
| 124: | elseif (trim($form[$type])) { |
| 125: | $parsed[$path[0]][$path[1]]['values'] = $form[$type]; |
| 126: | } |
| 127: | return $parsed; |
| 128: | } |
| 129: | |
| 130: | private function convert_to_card($parsed) { |
| 131: | $parser = new Hm_VCard(); |
| 132: | $parser->import_parsed($parsed); |
| 133: | return $parser->build_card(); |
| 134: | } |
| 135: | |
| 136: | private function get_phone($parser) { |
| 137: | $res = $parser->fld_val('tel', false, false, true); |
| 138: | if ($res === false) { |
| 139: | return array('', ''); |
| 140: | } |
| 141: | return array($res[0]['values'], $res[0]['id']); |
| 142: | } |
| 143: | |
| 144: | private function get_display_name($parser) { |
| 145: | $res = $parser->fld_val('fn', false, false, true); |
| 146: | if ($res === false) { |
| 147: | return array('', ''); |
| 148: | } |
| 149: | return array($res[0]['values'], $res[0]['id']); |
| 150: | } |
| 151: | |
| 152: | private function convert_to_contact($parser) { |
| 153: | $res = array(); |
| 154: | $emails = $parser->fld_val('email', false, array(), true); |
| 155: | if (count($emails) == 0) { |
| 156: | return $res; |
| 157: | } |
| 158: | |
| 159: | list($fn, $fn_id) = $this->get_display_name($parser); |
| 160: | list($phone, $phone_id) = $this->get_phone($parser); |
| 161: | |
| 162: | $all_flds = $this->parse_extr_flds($parser); |
| 163: | foreach ($emails as $email) { |
| 164: | $res[] = array( |
| 165: | 'source' => $this->src, |
| 166: | 'type' => 'carddav', |
| 167: | 'fn' => $fn, |
| 168: | 'carddav_fn_id' => $fn_id, |
| 169: | 'phone_number' => $phone, |
| 170: | 'email_address' => $email['values'], |
| 171: | 'carddav_phone_id' => $phone_id, |
| 172: | 'carddav_email_id' => $email['id'], |
| 173: | 'carddav_parsed' => $parser->parsed_data(), |
| 174: | 'all_fields' => $all_flds |
| 175: | ); |
| 176: | } |
| 177: | return $res; |
| 178: | } |
| 179: | |
| 180: | private function parse_extr_flds($parser) { |
| 181: | $all_flds = array(); |
| 182: | foreach (array_keys($parser->parsed_data()) as $name) { |
| 183: | if (in_array($name, array('begin', 'end', 'n', 'fn', 'tel', 'email', 'raw'))) { |
| 184: | continue; |
| 185: | } |
| 186: | $all_flds[$name] = $parser->fld_val($name); |
| 187: | } |
| 188: | return $all_flds; |
| 189: | } |
| 190: | |
| 191: | private function discover() { |
| 192: | $path = $this->xml_find($this->principal_discover(), $this->principal_path); |
| 193: | if ($path === false) { |
| 194: | Hm_Debug::add('CARDDAV: No principal path discovered'); |
| 195: | return false; |
| 196: | } |
| 197: | Hm_Debug::add(sprintf('CARDDAV: Found %s principal path', $path)); |
| 198: | $this->principal_url = $this->url_concat($path); |
| 199: | $address_path = $this->xml_find($this->addressbook_discover(), $this->addressbook_path); |
| 200: | if ($address_path === false) { |
| 201: | Hm_Debug::add('CARDDAV: No address path discovered'); |
| 202: | return false; |
| 203: | } |
| 204: | Hm_Debug::add(sprintf('CARDDAV: Found %s address path', $address_path), 'info'); |
| 205: | $this->address_url = $this->url_concat($address_path); |
| 206: | return true; |
| 207: | } |
| 208: | |
| 209: | private function parse_xml($xml) { |
| 210: | if (mb_substr((string) $this->api->last_status, 0, 1) != '2') { |
| 211: | Hm_Debug::add(sprintf('ERRUnable to access CardDav server (%d)', $this->api->last_status)); |
| 212: | return false; |
| 213: | } |
| 214: | $xml = preg_replace("/<[a-zA-Z]+:/Um", "<", $xml); |
| 215: | $xml = preg_replace("/<\/[a-zA-Z]+:/Um", "</", $xml); |
| 216: | $xml = str_replace('xmlns=', 'ns=', $xml); |
| 217: | try { |
| 218: | $data = new SimpleXMLElement($xml); |
| 219: | return $data; |
| 220: | } |
| 221: | catch (Exception $oops) { |
| 222: | Hm_Msgs::add('Unable to access CardDav server', 'warning'); |
| 223: | Hm_Debug::add(sprintf('CARDDAV: Could not parse XML: %s', $xml)); |
| 224: | } |
| 225: | return false; |
| 226: | } |
| 227: | |
| 228: | private function xml_find($xml, $path, $multi=false) { |
| 229: | $data = $this->parse_xml($xml); |
| 230: | if (!$data) { |
| 231: | return false; |
| 232: | } |
| 233: | $res = array(); |
| 234: | foreach ($data->xpath($path) as $node) { |
| 235: | if (!$multi) { |
| 236: | return (string) $node; |
| 237: | } |
| 238: | $res[] = (string) $node; |
| 239: | } |
| 240: | if ($multi) { |
| 241: | if (count($res) == 0) { |
| 242: | Hm_Debug::add(sprintf('CARDDAV: find for %s failed in xml: %s', $path, $xml)); |
| 243: | } |
| 244: | return $res; |
| 245: | } |
| 246: | Hm_Debug::add(sprintf('CARDDAV: find for %s failed in xml: %s', $path, $xml)); |
| 247: | return false; |
| 248: | } |
| 249: | |
| 250: | private function url_concat($path) { |
| 251: | $parsed = parse_url($this->url); |
| 252: | return sprintf('%s://%s/%s', $parsed['scheme'], $parsed['host'], preg_replace('#^/#', '', $path)); |
| 253: | } |
| 254: | |
| 255: | private function auth_headers() { |
| 256: | return array('Authorization: Basic '. base64_encode(sprintf('%s:%s', $this->user, $this->pass))); |
| 257: | } |
| 258: | |
| 259: | private function addressbook_discover() { |
| 260: | $req_xml = '<d:propfind xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"><d:prop>'. |
| 261: | '<card:addressbook-home-set /></d:prop></d:propfind>'; |
| 262: | return $this->api->command($this->principal_url, $this->auth_headers(), array(), $req_xml, 'PROPFIND'); |
| 263: | } |
| 264: | |
| 265: | private function principal_discover() { |
| 266: | $req_xml = '<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal /></d:prop></d:propfind>'; |
| 267: | Hm_Debug::add(sprintf('CARDDAV: Sending discover XML: %s', $req_xml), 'info'); |
| 268: | return $this->api->command($this->url, $this->auth_headers(), array(), $req_xml, 'PROPFIND'); |
| 269: | } |
| 270: | |
| 271: | private function list_addressbooks() { |
| 272: | $headers = $this->auth_headers(); |
| 273: | $headers[] = 'Depth: 1'; |
| 274: | $req_xml = '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"><d:prop>'. |
| 275: | '<d:resourcetype /><d:displayname /><cs:getctag /></d:prop></d:propfind>'; |
| 276: | Hm_Debug::add(sprintf('CARDDAV: Sending addressbook XML: %s', $req_xml), 'info'); |
| 277: | return $this->api->command($this->address_url, $headers, array(), $req_xml, 'PROPFIND'); |
| 278: | } |
| 279: | |
| 280: | private function report($url) { |
| 281: | $headers = $this->auth_headers(); |
| 282: | $headers[] = 'Depth: 1'; |
| 283: | $req_xml = '<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">'. |
| 284: | '<d:prop><d:getetag /><card:address-data /></d:prop></card:addressbook-query>'; |
| 285: | Hm_Debug::add(sprintf('CARDDAV: Sending contacts XML: %s', $req_xml), 'info'); |
| 286: | return $this->api->command($url, $headers, array(), $req_xml, 'REPORT'); |
| 287: | } |
| 288: | |
| 289: | private function delete_server_contact($url) { |
| 290: | $headers = $this->auth_headers(); |
| 291: | $this->api->command($url, $headers, array(), '', 'DELETE'); |
| 292: | return $this->api->last_status == 200; |
| 293: | } |
| 294: | private function update_server_contact($url, $card) { |
| 295: | $headers = $this->auth_headers(); |
| 296: | $headers[] = 'Content-Type: text/vcard; charset=utf-8'; |
| 297: | $this->api->command($url, $headers, array(), $card, 'PUT'); |
| 298: | return $this->api->last_status == 200 || $this->api->last_status == 201; |
| 299: | } |
| 300: | } |
| 301: | |