1: <?php
2:
3: /**
4: * Carddav modules
5: * @package modules
6: * @subpackage carddav_contacts
7: */
8:
9: if (!defined('DEBUG_MODE')) { die(); }
10:
11: /**
12: * @subpackage carddav_contacts/lib
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: