f4a96ba59e1e4a7b7f7444bdbaac7b750510c40d
[tine20] / tine20 / Calendar / Import / CalDav / Client.php
1 <?php
2 //./tine20.php --username unittest --method Calendar.importCalDav url="https://osx-testfarm-mavericks-server.hh.metaways.de:8443" caldavuserfile=caldavuserfile.csv
3
4 /**
5  * Tine 2.0
6  * 
7  * @package     Calendar
8  * @subpackage  Import
9  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
10  * @author      Paul Mehrer <p.mehrer@metaways.de>
11  * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
12  */
13
14 /**
15  * Calendar_Import_CalDAV
16  * 
17  * @package     Calendar
18  * @subpackage  Import
19  */
20 class Calendar_Import_CalDav_Client extends Tinebase_Import_CalDav_Client
21 {
22     protected $calendars = array();
23     protected $calendarICSs = array();
24     protected $maxBulkRequest = 20;
25     protected $mapToDefaultContainer = 'calendar';
26     protected $decorator = null;
27     
28     const findAllCalendarsRequest =
29 '<?xml version="1.0"?>
30 <d:propfind xmlns:d="DAV:">
31   <d:prop>
32     <d:resourcetype />
33     <d:acl />
34     <d:displayname />
35     <x:supported-calendar-component-set xmlns:x="urn:ietf:params:xml:ns:caldav"/>
36   </d:prop>
37 </d:propfind>';
38     
39     const findAllCalendarICSsRequest = 
40 '<?xml version="1.0"?>
41 <d:propfind xmlns:d="DAV:">
42   <d:prop>
43     <x:calendar-data xmlns:x="urn:ietf:params:xml:ns:caldav"/>
44   </d:prop>
45 </d:propfind>';
46     
47     const getAllCalendarDataRequest =
48 '<?xml version="1.0"?>
49 <b:calendar-multiget xmlns:a="DAV:" xmlns:b="urn:ietf:params:xml:ns:caldav">
50   <a:prop>
51     <b:calendar-data />
52     <a:getetag />
53   </a:prop>
54 ';
55     
56     const getEventETagsRequest =
57 '<?xml version="1.0"?>
58 <b:calendar-multiget xmlns:a="DAV:" xmlns:b="urn:ietf:params:xml:ns:caldav">
59   <a:prop>
60     <a:getetag />
61   </a:prop>
62 ';
63     
64     public function __construct(array $a, $flavor)
65     {
66         parent::__construct($a);
67         
68         $flavor = 'Calendar_Import_CalDav_Decorator_'.$flavor;
69         $this->decorator = new $flavor($this);
70     }
71     
72     public function findAllCalendars()
73     {
74         if ('' == $this->calendarHomeSet && ! $this->findCalendarHomeSet())
75             return false;
76         
77         //issue with follow location in curl!?!?
78         if ($this->calendarHomeSet[strlen($this->calendarHomeSet)-1] !== '/')
79             $this->calendarHomeSet .= '/';
80         
81         $result = $this->calDavRequest('PROPFIND', $this->calendarHomeSet, $this->decorator->preparefindAllCalendarsRequest(self::findAllCalendarsRequest), 1);
82         
83         foreach ($result as $uri => $response) {
84             if (isset($response['{DAV:}resourcetype']) && isset($response['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']) && 
85                     $response['{DAV:}resourcetype']->is('{urn:ietf:params:xml:ns:caldav}calendar') &&
86                     in_array('VEVENT', $response['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue())) {
87                 $this->calendars[$uri]['acl'] = $response['{DAV:}acl'];
88                 $this->calendars[$uri]['displayname'] = $response['{DAV:}displayname'];
89                 $this->decorator->processAdditionalCalendarProperties($this->calendars[$uri], $response);
90                 $this->resolvePrincipals($this->calendars[$uri]['acl']->getPrivileges());
91             }
92         }
93         
94         if (count($this->calendars) > 0) {
95             return true;
96         } else {
97             if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
98                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' couldn\'t find a calendar');
99             return false;
100         }
101     }
102     
103     public function findAllCalendarICSs()
104     {
105         if (count($this->calendars) < 1 && ! $this->findAllCalendars())
106             return false;
107         
108         foreach ($this->calendars as $calUri => $calendar) {
109             $result = $this->calDavRequest('PROPFIND', $calUri, self::findAllCalendarICSsRequest, 1);
110             foreach ($result as $ics => $value) {
111                 if (strpos($ics, '.ics') !== FALSE)
112                     $this->calendarICSs[$calUri][] = $ics;
113             }
114         }
115         
116         if (count($this->calendarICSs) > 0) {
117             return true;
118         } else {
119             if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
120                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' all found calendars are empty');
121             return false;
122         }
123     }
124     
125     protected function findContainerForCalendar($calendarUri, $displayname, $defaultCalendarsName, $type, $application_id, $modelName)
126     {
127         $uuid = basename($calendarUri);
128         
129         $filter = new Tinebase_Model_ContainerFilter(array(
130             array(
131                 'field' => 'uuid', 
132                 'operator' => 'equals', 
133                 'value' => $uuid
134             ),
135             array(
136                 'field' => 'model', 
137                 'operator' => 'equals', 
138                 'value' => 'Calendar_Model_Event'
139             ),
140         ));
141         $existingCalendar = Tinebase_Container::getInstance()->search($filter, null, false, false, 'sync')->getFirstRecord();
142         if($existingCalendar) {
143             return $existingCalendar;
144         }
145         
146         $counter = '';
147         
148         if ($defaultCalendarsName == $displayname) {
149             $existingCalendar = Tinebase_Container::getInstance()->getDefaultContainer('Calendar_Model_Event');
150             if (! $existingCalendar->uuid) {
151                 $existingCalendar->uuid = $uuid;
152                 return $existingCalendar;
153             }
154             $existingCalendar = null;
155             $counter = 1;
156         }
157         
158         try {
159             while (true) {
160                 $existingCalendar = Tinebase_Container::getInstance()->getContainerByName('Calendar', $displayname . $counter, $type, Tinebase_Core::getUser());
161                 if (! $existingCalendar->uuid) {
162                     $existingCalendar->uuid = $uuid;
163                     return $existingCalendar;
164                 }
165                 $counter += 1;
166             }
167         } catch (Tinebase_Exception_NotFound $e) {
168             $newContainer = new Tinebase_Model_Container(array(
169                 'name'              => $displayname . $counter,
170                 'type'              => $type,
171                 'backend'           => 'Sql',
172                 'application_id'    => $application_id,
173                 'model'             => $modelName,
174                 'uuid'              => $uuid
175             ));
176             return Tinebase_Container::getInstance()->addContainer($newContainer);
177         }
178     }
179     
180     public function importAllCalendars()
181     {
182         if (count($this->calendars) < 1 && ! $this->findAllCalendars()) {
183             return false;
184         }
185         
186         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__ 
187             . ' Importing all calendars for user ' . $this->userName);
188         
189         Calendar_Controller_Event::getInstance()->sendNotifications(false);
190         Sabre\VObject\Component\VCalendar::$propertyMap['ATTACH'] = '\\Calendar_Import_CalDav_SabreAttachProperty';
191         
192         $this->decorator->initCalendarImport();
193         
194         $modelName = Tinebase_Core::getApplicationInstance('Calendar')->getDefaultModel();
195         $application_id = Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId();
196         $type = Tinebase_Model_Container::TYPE_PERSONAL; //Tinebase_Model_Container::TYPE_SHARED;
197         $defaultContainer = Tinebase_Container::getInstance()->getDefaultContainer('Calendar_Model_Event');
198         
199         //decide which calendar to use as default calendar
200         //if there is a remote default calendar, use that. If not, use the first we find
201         $defaultCalendarsName = '';
202         foreach ($this->calendarICSs as $calUri => $calICSs) {
203             if ($this->mapToDefaultContainer == $this->calendars[$calUri]['displayname']) {
204                 $container = Tinebase_Container::getInstance()->getDefaultContainer('Calendar_Model_Event');
205             } elseif ($defaultsCalendarsName === '') {
206                 $defaultCalendarsName = $this->calendars[$calUri]['displayname'];
207             }
208         }
209         
210         foreach ($this->calendars as $calUri => $cal) {
211             $container = $this->findContainerForCalendar($calUri, $cal['displayname'], $defaultCalendarsName,
212                     $type, $application_id, $modelName);
213             
214             $this->decorator->setCalendarProperties($container, $this->calendars[$calUri]);
215             
216             $grants = $this->getCalendarGrants($calUri);
217             Tinebase_Container::getInstance()->setGrants($container->getId(), $grants, TRUE, FALSE);
218         }
219     }
220     
221     public function updateAllCalendarData($onlyCurrentUserOrganizer = false)
222     {
223         if (count($this->calendarICSs) < 1 && ! $this->findAllCalendarICSs())
224             return false;
225         
226         $newICSs = array();
227         $calendarEventBackend = Calendar_Controller_Event::getInstance()->getBackend();
228         
229         foreach ($this->calendarICSs as $calUri => $calICSs) {
230             $start = 0;
231             $max = count($calICSs);
232             $etags = array();
233             do {
234                 $requestEnd = '';
235                 for ($i = $start; $i < $max && $i < ($this->maxBulkRequest+$start); ++$i) {
236                     $requestEnd .= '  <a:href>' . $calICSs[$i] . "</a:href>\n";
237                 }
238                 $start = $i;
239                 $requestEnd .= '</b:calendar-multiget>';
240                 $result = $this->calDavRequest('REPORT', $calUri, self::getEventETagsRequest . $requestEnd, 1);
241                 
242                 foreach ($result as $key => $value) {
243                     if (isset($value['{DAV:}getetag'])) {
244                         $name = explode('/', $key);
245                         $name = end($name);
246                         $id = ($pos = strpos($name, '.')) === false ? $name : substr($name, 0, $pos);
247                         $etags[$key] = array( 'id' => $id, 'etag' => $value['{DAV:}getetag']);
248                     }
249                 }
250             } while($start < $max);
251             
252             //check etags
253             foreach ($etags as $ics => $data) {
254                 if (! $calendarEventBackend->checkETag($data['id'], $data['etag'])) {
255                     if (!isset($newICSs[$calUri])) {
256                         $newICSs[$calUri] = array();
257                     }
258                     $newICSs[$calUri][] = $ics;
259                 }
260             }
261         }
262         
263         if (($count = count($newICSs)) > 0) {
264             if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
265                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $count . ' calendar(s) changed for: ' . $this->userName);
266             $this->calendarICSs = $newICSs;
267             $this->importAllCalendarData($onlyCurrentUserOrganizer);
268         } else {
269             if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
270                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' no changes found for: ' . $this->userName);
271             
272         }
273     }
274     
275     public function importAllCalendarData($onlyCurrentUserOrganizer = false)
276     {
277         if (count($this->calendarICSs) < 1 && ! $this->findAllCalendarICSs()) {
278             return false;
279         }
280         
281         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__
282             . ' Importing all calendar data for user ' . $this->userName);
283         
284         Calendar_Controller_Event::getInstance()->sendNotifications(false);
285         Sabre\VObject\Component\VCalendar::$propertyMap['ATTACH'] = '\\Calendar_Import_CalDav_SabreAttachProperty';
286         
287         $this->decorator->initCalendarImport();
288         
289         $modelName = Tinebase_Core::getApplicationInstance('Calendar')->getDefaultModel();
290         $application_id = Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId();
291         $type = Tinebase_Model_Container::TYPE_PERSONAL; //Tinebase_Model_Container::TYPE_SHARED;
292         $defaultContainer = Tinebase_Container::getInstance()->getDefaultContainer('Calendar_Model_Event');
293         $calendarEventBackend = Calendar_Controller_Event::getInstance()->getBackend();
294         
295         //decide which calendar to use as default calendar
296         //if there is a remote default calendar, use that. If not, use the first we find
297         $defaultCalendarsName = '';
298         foreach ($this->calendarICSs as $calUri => $calICSs) {
299             if ($this->mapToDefaultContainer == $this->calendars[$calUri]['displayname']) {
300                 $container = Tinebase_Container::getInstance()->getDefaultContainer('Calendar_Model_Event');
301             } elseif ($defaultsCalendarsName === '') {
302                 $defaultCalendarsName = $this->calendars[$calUri]['displayname'];
303             }
304         }
305         
306         foreach ($this->calendarICSs as $calUri => $calICSs) {
307             $container = $this->findContainerForCalendar($calUri, $this->calendars[$calUri]['displayname'], $defaultCalendarsName,
308                     $type, $application_id, $modelName);
309             
310             $this->decorator->setCalendarProperties($container, $this->calendars[$calUri]);
311             
312             // we shouldnt do the grants here as the caldav user file may not contain all users, so setting the grants wont work properly!
313             // use importAllCalendars to have the grants set
314             //$grants = $this->getCalendarGrants($calUri);
315             //Tinebase_Container::getInstance()->setGrants($container->getId(), $grants, TRUE, FALSE);
316             
317             $start = 0;
318             $max = count($calICSs);
319             do {
320                 $etags = array();
321                 $requestEnd = '';
322                 for ($i = $start; $i < $max && $i < ($this->maxBulkRequest+$start); ++$i) {
323                     $requestEnd .= '  <a:href>' . $calICSs[$i] . "</a:href>\n";
324                 }
325                 $start = $i;
326                 $requestEnd .= '</b:calendar-multiget>';
327                 $result = $this->calDavRequest('REPORT', $calUri, self::getAllCalendarDataRequest . $requestEnd, 1);
328                 
329                 foreach ($result as $key => $value) {
330                     if (isset($value['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
331                         $data = $value['{urn:ietf:params:xml:ns:caldav}calendar-data'];
332                         $name = explode('/', $key);
333                         $name = end($name);
334                         try {
335                             $event = Calendar_Frontend_WebDAV_Event::create(
336                                 $container,
337                                 $name,
338                                 $data,
339                                 $onlyCurrentUserOrganizer
340                             );
341                             if ($event) {
342                                 $etags[$event->getRecord()->getId()] = $value['{DAV:}getetag'];
343                             }
344                         } catch (Exception $e) {
345                             // don't warn on VTODOs
346                             if (strpos($data, 'BEGIN:VTODO') !== false) {
347                                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
348                                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Skipping VTODO');
349                             } else {
350                                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
351                                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not create event from data: ' . $data);
352                                 Tinebase_Exception::log($e);
353                             }
354                         }
355                     }
356                 }
357                 
358                 $calendarEventBackend->setETags($etags);
359             } while($start < $max);
360         }
361         return true;
362     }
363     
364     public function getCalendarGrants($calUri)
365     {
366         $grants = array();
367         $user = array();
368         $type = array();
369         $privilege = array();
370         foreach ($this->calendars[$calUri]['acl']->getPrivileges() as $ace)
371         {
372             if ('{DAV:}authenticated' == $ace['principal']) {
373                 $user[] = 0;
374                 $type[] = Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE;
375                 $privilege[] = $ace['privilege'];
376             } elseif (isset($this->principals[$ace['principal']])) {
377                 $user[] = $this->principals[$ace['principal']]->getId();
378                 $type[] = Tinebase_Acl_Rights::ACCOUNT_TYPE_USER;
379                 $privilege[] = $ace['privilege'];
380             } elseif (isset($this->principalGroups[$ace['principal']])) {
381                 foreach($this->principalGroups[$ace['principal']] as $principal) {
382                     if ('{DAV:}authenticated' == $principal) {
383                         $user[] = 0;
384                         $type[] = Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE;
385                         $privilege[] = $ace['privilege'];
386                     } elseif (isset($this->principals[$principal])) {
387                         $user[] = $this->principals[$principal]->getId();
388                         $type[] = Tinebase_Acl_Rights::ACCOUNT_TYPE_USER;
389                         $privilege[] = $ace['privilege'];
390                     } else {
391                         if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
392                             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' there is an unresolved principal: ' . $principal . ' in group: ' . $ace['principal']);
393                     }
394                 }
395             } else {
396                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
397                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' couldn\'t resolve principal: '.$ace['principal']);
398             }
399         }
400         for ($i=0; $i<count($user); ++$i) {
401             switch ($privilege[$i]) {
402                 case '{DAV:}all':
403                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_READ ] = true;
404                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_ADD] = true;
405                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_EDIT] = true;
406                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_DELETE] = true;
407                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_EXPORT] = true;
408                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_SYNC] = true;
409                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_ADMIN] = true;
410                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_FREEBUSY] = true;
411                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_PRIVATE] = true;
412                     break;
413                 case '{urn:ietf:params:xml:ns:caldav}read-free-busy':
414                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_FREEBUSY] = true;
415                     break;
416                 case '{DAV:}read':
417                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_READ] = true;
418                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_EXPORT] = true;
419                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_SYNC] = true;
420                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_FREEBUSY] = true;
421                     break;
422                 case '{DAV:}write':
423                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_ADD] = true;
424                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_EDIT] = true;
425                     $grants[$user[$i]][Tinebase_Model_Grants::GRANT_DELETE] = true;
426                     break;
427                 case '{DAV:}read-current-user-privilege-set':
428                     continue;
429                 default:
430                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
431                         Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' unknown privilege: ' . $privilege[$i]);
432                     continue;
433             }
434             $grants[$user[$i]]['account_id'] = $user[$i];
435             $grants[$user[$i]]['account_type'] = $type[$i];
436         }
437         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
438             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' found grants: ' . print_r($grants, true) . ' for calendar: ' . $calUri);
439         
440         return new Tinebase_Record_RecordSet('Tinebase_Model_Grants', $grants, TRUE);
441     }
442     
443     public function updateAllCalendarDataForUsers(array $users)
444     {
445         // first only update/import events where the current user is also the organizer
446         $result = true;
447         foreach ($users as $username => $pwd) {
448             $this->clearCurrentUserCalendarData();
449             $this->userName = $username;
450             $this->password = $pwd;
451             if (!$this->updateAllCalendarData(true)) {
452                 $result = false;
453             }
454         }
455         // then update all events again
456         foreach ($users as $username => $pwd) {
457             $this->clearCurrentUserCalendarData();
458             $this->userName = $username;
459             $this->password = $pwd;
460             if (!$this->updateAllCalendarData(false)) {
461                 $result = false;
462             }
463         }
464         return $result;
465     }
466     
467     public function importAllCalendarDataForUsers(array $users)
468     {
469         $result = true;
470         foreach ($users as $username => $pwd) {
471             $this->clearCurrentUserCalendarData();
472             $this->userName = $username;
473             $this->password = $pwd;
474             if (!$this->updateAllCalendarData()) {
475                 $result = false;
476             }
477         }
478         return $result;
479     }
480     
481     public function importAllCalendarDataForUsers(array $users)
482     {
483         if (!$this->findCurrentUserPrincipalForUsers($users)) {
484             return false;
485         }
486         
487         $result = true;
488         // first only import events where the current user is also the organizer
489         foreach ($users as $username => $pwd) {
490             $this->clearCurrentUserCalendarData();
491             $this->userName = $username;
492             $this->password = $pwd;
493             if (!$this->importAllCalendarData(true)) {
494                 $result = false;
495             }
496         }
497         // then import all events again
498         foreach ($users as $username => $pwd) {
499             $this->clearCurrentUserCalendarData();
500             $this->userName = $username;
501             $this->password = $pwd;
502             if (!$this->importAllCalendarData(false)) {
503                 $result = false;
504             }
505         }
506         return $result;
507     }
508     
509     public function importAllCalendarsForUsers(array $users)
510     {
511         if (!$this->findCurrentUserPrincipalForUsers($users)) {
512             return false;
513         }
514         
515         $result = true;
516         foreach ($users as $username => $pwd) {
517             $this->clearCurrentUserCalendarData();
518             $this->userName = $username;
519             $this->password = $pwd;
520             if (!$this->importAllCalendars()) {
521                 $result = false;
522             }
523         }
524         return $result;
525     }
526     
527     public function clearCurrentUserCalendarData()
528     {
529         $this->clearCurrentUserData();
530         $this->calendars = array();
531         $this->calendarICSs = array();
532     }
533 }