e607f346232f4839ea5375a205c5d061df773764
[tine20] / tine20 / Calendar / Frontend / Json.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Calendar
6  * @subpackage  Frontend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * json interface for calendar
14  * 
15  * @package     Calendar
16  * @subpackage  Frontend
17  */
18 class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
19 {
20     /**
21      * app name
22      * 
23      * @var string
24      */
25     protected $_applicationName = 'Calendar';
26     
27     /**
28      * creates an exception instance of a recurring event
29      *
30      * NOTE: deleting persistent exceptions is done via a normal delete action
31      *       and handled in the controller
32      * 
33      * @param  array       $recordData
34      * @param  bool        $deleteInstance
35      * @param  bool        $deleteAllFollowing
36      * @param  bool        $checkBusyConflicts
37      * @return array       exception Event | updated baseEvent
38      * 
39      * @todo replace $_allFollowing param with $range
40      * @deprecated replace with create/update/delete
41      */
42     public function createRecurException($recordData, $deleteInstance, $deleteAllFollowing, $checkBusyConflicts = FALSE)
43     {
44         $event = new Calendar_Model_Event(array(), TRUE);
45         $event->setFromJsonInUsersTimezone($recordData);
46
47         /** @noinspection PhpDeprecationInspection */
48         $returnEvent = Calendar_Controller_Event::getInstance()->createRecurException($event, $deleteInstance, $deleteAllFollowing, $checkBusyConflicts);
49         
50         return $this->getEvent($returnEvent->getId());
51     }
52     
53     /**
54      * deletes existing events
55      *
56      * @param array $ids
57      * @param string $range
58      * @return string
59      */
60     public function deleteEvents($ids, $range = Calendar_Model_Event::RANGE_THIS)
61     {
62         return $this->_delete($ids, Calendar_Controller_Event::getInstance(), array($range));
63     }
64     
65     /**
66      * deletes existing resources
67      *
68      * @param array $ids
69      * @return string
70      */
71     public function deleteResources($ids)
72     {
73         return $this->_delete($ids, Calendar_Controller_Resource::getInstance());
74     }
75     
76     /**
77      * deletes a recur series
78      *
79      * @param  array $recordData
80      * @return array
81      */
82     public function deleteRecurSeries($recordData)
83     {
84         $event = new Calendar_Model_Event(array(), TRUE);
85         $event->setFromJsonInUsersTimezone($recordData);
86         
87         Calendar_Controller_Event::getInstance()->deleteRecurSeries($event);
88         return array('success' => true);
89     }
90     
91     /**
92      * Return a single event
93      *
94      * @param   string $id
95      * @return  array record data
96      */
97     public function getEvent($id)
98     {
99         return $this->_get($id, Calendar_Controller_Event::getInstance());
100     }
101     
102     /**
103      * Returns registry data of the calendar.
104      *
105      * @return mixed array 'variable name' => 'data'
106      * 
107      * @todo move exception handling (no default calender found) to another place?
108      */
109     public function getRegistryData()
110     {
111         $defaultCalendarId = Tinebase_Core::getPreference('Calendar')->getValue(Calendar_Preference::DEFAULTCALENDAR);
112         try {
113             $defaultCalendarArray = Tinebase_Container::getInstance()->getContainerById($defaultCalendarId)->toArray();
114             $defaultCalendarArray['account_grants'] = Tinebase_Container::getInstance()->getGrantsOfAccount(Tinebase_Core::getUser(), $defaultCalendarId)->toArray();
115             if ($defaultCalendarArray['type'] != Tinebase_Model_Container::TYPE_SHARED) {
116                 $defaultCalendarArray['ownerContact'] = Addressbook_Controller_Contact::getInstance()->getContactByUserId($defaultCalendarArray['owner_id'])->toArray();
117             }
118         } catch (Exception $e) {
119             // remove default cal pref
120             Tinebase_Core::getPreference('Calendar')->deleteUserPref(Calendar_Preference::DEFAULTCALENDAR);
121             $defaultCalendarArray = array();
122         }
123         
124         $importDefinitions = $this->_getImportDefinitions();
125         $allCalendarResources = Calendar_Controller_Resource::getInstance()->getAll()->toArray();
126         
127         $registryData = array(
128             'defaultContainer'          => $defaultCalendarArray,
129             'defaultImportDefinition'   => $importDefinitions['default'],
130             'importDefinitions'         => $importDefinitions,
131             'calendarResources'         => $allCalendarResources
132         );
133         
134         return $registryData;
135     }
136     
137     /**
138      * get default addressbook
139      * 
140      * @return array
141      */
142     public function getDefaultCalendar() 
143    {
144         $defaultCalendar = Calendar_Controller_Event::getInstance()->getDefaultCalendar();
145         $defaultCalendarArray = $defaultCalendar->toArray();
146         $defaultCalendarArray['account_grants'] = Tinebase_Container::getInstance()->getGrantsOfAccount(Tinebase_Core::getUser(), $defaultCalendar->getId())->toArray();
147         Tinebase_Core::getLogger()->notice(print_r($defaultCalendar, true));
148         return $defaultCalendarArray;
149     }
150     
151     /**
152      * import contacts
153      * 
154      * @param string $tempFileId to import
155      * @param string $definitionId
156      * @param array $importOptions
157      * @param array $clientRecordData
158      * @return array
159      */
160     public function importEvents($tempFileId, $definitionId, $importOptions, $clientRecordData = array())
161     {
162         return $this->_import($tempFileId, $definitionId, $importOptions, $clientRecordData);
163     }
164     
165     /**
166      * creates a scheduled import
167      * 
168      * @param string $remoteUrl
169      * @param string $interval
170      * @param string $importOptions
171      * @return array
172      */
173     public function importRemoteEvents($remoteUrl, $interval, $importOptions)
174     {
175         // Determine which plugin should be used to import
176         switch ($importOptions['sourceType']) {
177             case 'remote_caldav':
178                 $plugin = 'Calendar_Import_CalDAV';
179                 break;
180             default:
181                 $plugin = 'Calendar_Import_Ical';
182         }
183
184         $record = Tinebase_Controller_ScheduledImport::getInstance()->create( new Tinebase_Model_Import(array(
185             'source'            => $remoteUrl,
186             'sourcetype'        => Tinebase_Model_Import::SOURCETYPE_REMOTE,
187             'interval'          => $interval,
188             'options'           => array_replace($importOptions, array(
189                 'plugin' => $plugin,
190                 'importFileByScheduler' => $importOptions['sourceType'] != 'remote_caldav',
191             )),
192             'model'             => 'Calendar_Model_Event',
193             'application_id'    => Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId(),
194         ), true));
195
196         $result = $this->_recordToJson($record);
197
198         return $result;
199     }
200     
201     /**
202      * get addressbook import definitions
203      * 
204      * @return array
205      * 
206      * @todo generalize this
207      */
208     protected function _getImportDefinitions()
209     {
210         $filter = new Tinebase_Model_ImportExportDefinitionFilter(array(
211             array('field' => 'application_id',  'operator' => 'equals', 'value' => Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId()),
212             array('field' => 'type',            'operator' => 'equals', 'value' => 'import'),
213         ));
214         
215         $definitionConverter = new Tinebase_Convert_ImportExportDefinition_Json();
216         
217         try {
218             $importDefinitions = Tinebase_ImportExportDefinition::getInstance()->search($filter);
219             $defaultDefinition = $this->_getDefaultImportDefinition($importDefinitions);
220             $result = array(
221                 'results'               => $definitionConverter->fromTine20RecordSet($importDefinitions),
222                 'totalcount'            => count($importDefinitions),
223                 'default'               => ($defaultDefinition) ? $definitionConverter->fromTine20Model($defaultDefinition) : array(),
224             );
225         } catch (Exception $e) {
226             Tinebase_Exception::log($e);
227             $result = array(
228                 array(
229                     'results'               => array(),
230                     'totalcount'            => 0,
231                     'default'               => array(),
232                 )
233             );
234         }
235         
236         return $result;
237     }
238     
239     /**
240      * get default definition
241      * 
242      * @param Tinebase_Record_RecordSet $_importDefinitions
243      * @return Tinebase_Model_ImportExportDefinition
244      * 
245      * @todo generalize this
246      */
247     protected function _getDefaultImportDefinition($_importDefinitions)
248     {
249         try {
250             $defaultDefinition = Tinebase_ImportExportDefinition::getInstance()->getByName('cal_import_ical');
251         } catch (Tinebase_Exception_NotFound $tenf) {
252             if (count($_importDefinitions) > 0) {
253                 $defaultDefinition = $_importDefinitions->getFirstRecord();
254             } else {
255                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' No import definitions found for Calendar');
256                 $defaultDefinition = NULL;
257             }
258         }
259         
260         return $defaultDefinition;
261     }
262     
263     /**
264      * Return a single resouece
265      *
266      * @param   string $id
267      * @return  array record data
268      */
269     public function getResource($id)
270     {
271         return $this->_get($id, Calendar_Controller_Resource::getInstance());
272     }
273
274     /**
275      * @param array $_event
276      *   attendee to find free timeslot for
277      *   dtstart, dtend -> to calculate duration
278      *   rrule optional
279      * @param array $_options
280      *  'from'         datetime (optional, defaults event->dtstart) from where to start searching
281      *  'until'        datetime (optional, defaults 2 years) until when to giveup searching
282      *  'constraints'  array    (optional, defaults 8-20 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR') array of timespecs to limit the search with
283      *     timespec:
284      *       dtstart,
285      *       dtend,
286      *       rrule ... for example "work days" -> 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR'
287      * @return array
288      */
289     public function searchFreeTime($_event, $_options)
290     {
291         $eventRecord = new Calendar_Model_Event(array(), TRUE);
292         $eventRecord->setFromJsonInUsersTimezone($_event);
293
294         if (isset($_options['from']) || isset($_options['until'])) {
295             $tmpData = array();
296             if (isset($_options['from'])) {
297                 $tmpData['dtstart'] = $_options['from'];
298             }
299             if (isset($_options['until'])) {
300                 $tmpData['dtend'] = $_options['until'];
301             }
302             $tmpEvent = new Calendar_Model_Event(array(), TRUE);
303             $tmpEvent->setFromJsonInUsersTimezone($tmpData);
304             if (isset($_options['from'])) {
305                 $_options['from'] = $tmpEvent->dtstart;
306             }
307             if (isset($_options['until'])) {
308                 $_options['until'] = $tmpEvent->dtend;
309             }
310         }
311
312         $timeSearchStopped = null;
313         try {
314             $records = Calendar_Controller_Event::getInstance()->searchFreeTime($eventRecord, $_options);
315         } catch (Calendar_Exception_AttendeeBusy $ceab) {
316             $event = $this->_recordToJson($ceab->getEvent());
317             $timeSearchStopped = $event['dtend'];
318             $records = new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
319         }
320
321         $records->attendee = array();
322         $result = $this->_multipleRecordsToJson($records, null, null);
323
324         return array(
325             'results'           => $result,
326             'totalcount'        => count($result),
327             'filter'            => array(),
328             'timeSearchStopped' => $timeSearchStopped,
329         );
330     }
331     
332     /**
333      * Search for events matching given arguments
334      *
335      * @param  array $filter
336      * @param  array $paging
337      * @return array
338      */
339     public function searchEvents($filter, $paging)
340     {
341         $controller = Calendar_Controller_Event::getInstance();
342         
343         $decodedPagination = $this->_prepareParameter($paging);
344         $pagination = new Tinebase_Model_Pagination($decodedPagination);
345         $clientFilter = $filter = $this->_decodeFilter($filter, 'Calendar_Model_EventFilter');
346
347         // find out if fixed calendars should be used
348         $fixedCalendarIds = Calendar_Controller_Event::getInstance()->getFixedCalendarIds();
349         $useFixedCalendars = is_array($fixedCalendarIds) && ! empty($fixedCalendarIds);
350         
351         $periodFilter = $filter->getFilter('period');
352         
353         // add period filter per default to prevent endless search
354         if (! $periodFilter) {
355             $periodFilter = $this->_getDefaultPeriodFilter();
356             // periodFilter will be added to fixed filter when using fixed calendars
357             if (! $useFixedCalendars) {
358                 $filter->addFilter($periodFilter);
359             }
360         }
361         
362         // add fixed calendar on demand
363         if ($useFixedCalendars) {
364             $fixed = new Calendar_Model_EventFilter(array(), 'AND');
365             $fixed->addFilter( new Tinebase_Model_Filter_Text('container_id', 'in', $fixedCalendarIds));
366             
367             $fixed->addFilter($periodFilter);
368             
369             $og = new Calendar_Model_EventFilter(array(), 'OR');
370             $og->addFilterGroup($fixed);
371             $og->addFilterGroup($clientFilter);
372             
373             $filter = new Calendar_Model_EventFilter(array(), 'AND');
374             $filter->addFilterGroup($og);
375         }
376         
377         $records = $controller->search($filter, $pagination, FALSE);
378         
379         $result = $this->_multipleRecordsToJson($records, $clientFilter, $pagination);
380         
381         return array(
382             'results'       => $result,
383             'totalcount'    => count($result),
384             'filter'        => $clientFilter->toArray(TRUE),
385         );
386     }
387
388     /**
389      * get default period filter
390      * 
391      * @return Calendar_Model_PeriodFilter
392      */
393     protected function _getDefaultPeriodFilter()
394     {
395         $now = Tinebase_DateTime::now()->setTime(0,0,0);
396         
397         $from = $now->getClone()->subMonth(Calendar_Config::getInstance()->get(Calendar_Config::MAX_JSON_DEFAULT_FILTER_PERIOD_FROM, 0));
398         $until = $now->getClone()->addMonth(Calendar_Config::getInstance()->get(Calendar_Config::MAX_JSON_DEFAULT_FILTER_PERIOD_UNTIL, 1));
399         $periodFilter = new Calendar_Model_PeriodFilter(array(
400             'field' => 'period',
401             'operator' => 'within',
402             'value' => array("from" => $from, "until" => $until)
403         ));
404         
405         return $periodFilter;
406     }
407     
408     /**
409      * Search for resources matching given arguments
410      *
411      * @param  array $filter
412      * @param  array $paging
413      * @return array
414      */
415     public function searchResources($filter, $paging)
416     {
417         return $this->_search($filter, $paging, Calendar_Controller_Resource::getInstance(), 'Calendar_Model_ResourceFilter', true);
418     }
419     
420     /**
421      * creates/updates an event / recur
422      *
423      * @param   array   $recordData
424      * @param   bool    $checkBusyConflicts
425      * @param   string  $range
426      * @return  array   created/updated event
427      */
428     public function saveEvent($recordData, $checkBusyConflicts = FALSE, $range = Calendar_Model_Event::RANGE_THIS)
429     {
430         return $this->_save($recordData, Calendar_Controller_Event::getInstance(), 'Event', 'id', array($checkBusyConflicts, $range));
431     }
432     
433     /**
434      * creates/updates a Resource
435      *
436      * @param   array   $recordData
437      * @return  array   created/updated Resource
438      */
439     public function saveResource($recordData)
440     {
441         $recordData['grants'] = new Tinebase_Record_RecordSet('Tinebase_Model_Grants', $recordData['grants']);
442         if(array_key_exists ('max_number_of_people', $recordData) && $recordData['max_number_of_people'] == '') {
443            $recordData['max_number_of_people'] = null;
444         }
445         
446         return $this->_save($recordData, Calendar_Controller_Resource::getInstance(), 'Resource');
447     }
448     
449     /**
450      * sets attendee status for an attender on the given event
451      * 
452      * NOTE: for recur events we implicitly create an exceptions on demand
453      *
454      * @param  array         $eventData
455      * @param  array         $attenderData
456      * @param  string        $authKey
457      * @return array         complete event
458      */
459     public function setAttenderStatus($eventData, $attenderData, $authKey)
460     {
461         $event    = new Calendar_Model_Event($eventData);
462         $attender = new Calendar_Model_Attender($attenderData);
463         
464         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($event, $attender, $authKey);
465         
466         return $this->getEvent($event->getId());
467     }
468     
469     /**
470      * updated a recur series
471      *
472      * @param  array $recordData
473      * @param  bool  $checkBusyConflicts
474      * @noparamyet  JSONstring $returnPeriod NOT IMPLEMENTED YET
475      * @return array 
476      */
477     public function updateRecurSeries($recordData, $checkBusyConflicts = FALSE /*, $returnPeriod*/)
478     {
479         $recurInstance = new Calendar_Model_Event(array(), TRUE);
480         $recurInstance->setFromJsonInUsersTimezone($recordData);
481         
482         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(print_r($recurInstance->toArray(), true));
483         
484         $baseEvent = Calendar_Controller_Event::getInstance()->updateRecurSeries($recurInstance, $checkBusyConflicts);
485         
486         return $this->getEvent($baseEvent->getId());
487     }
488     
489     /**
490      * prepares an iMIP (RFC 6047) Message
491      * 
492      * @param array|Calendar_Model_iMIP $iMIP
493      * @return array prepared iMIP part
494      */
495     public function iMIPPrepare($iMIP)
496     {
497         $iMIPMessage = $iMIP instanceof Calendar_Model_iMIP ? $iMIP : new Calendar_Model_iMIP($iMIP);
498         $iMIPFrontend = new Calendar_Frontend_iMIP();
499         
500         $iMIPMessage->preconditionsChecked = FALSE;
501         $iMIPFrontend->prepareComponent($iMIPMessage);
502         $iMIPMessage->setTimezone(Tinebase_Core::getUserTimezone());
503         return $iMIPMessage->toArray();
504     }
505     
506     /**
507      * process an iMIP (RFC 6047) Message
508      * 
509      * @param array  $iMIP
510      * @param string $status
511      * @return array prepared iMIP part
512      */
513     public function iMIPProcess($iMIP, $status=null)
514     {
515         $iMIPMessage = new Calendar_Model_iMIP($iMIP);
516         $iMIPFrontend = new Calendar_Frontend_iMIP();
517         
518         $iMIPFrontend->process($iMIPMessage, $status);
519         
520         return $this->iMIPPrepare($iMIPMessage);
521     }
522
523     /**
524      * @param array $attendee
525      * @return array
526      */
527     public function resolveGroupMembers($attendee)
528     {
529         $attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $attendee);
530         Calendar_Model_Attender::resolveGroupMembers($attendee);
531         Calendar_Model_Attender::resolveAttendee($attendee,false);
532
533         return $attendee->toArray();
534     }
535
536     /**
537      * @param array $attendee
538      * @param array $events single event or set of events
539      * @param array $ignoreUIDs
540      * @return array single fbInfo or array of eventid => fbinfo
541      */
542     public function getFreeBusyInfo($attendee, $events, $ignoreUIDs = array())
543     {
544         $attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $attendee);
545         $calendarController = Calendar_Controller_Event::getInstance();
546         $fbInfo = [];
547
548         foreach($events as $event) {
549             $eventRecord = new Calendar_Model_Event(array(), TRUE);
550             $eventRecord->setFromJsonInUsersTimezone($event);
551
552             $periods = $calendarController->getBlockingPeriods($eventRecord, array(
553                 'from'  => $eventRecord->dtstart,
554                 'until' => $eventRecord->dtstart->getClone()->addMonth(2)
555             ));
556
557             $fbInfo[$eventRecord->getId()] = $calendarController->getFreeBusyInfo($periods, $attendee, $ignoreUIDs)->toArray();
558         }
559
560         return $fbInfo;
561     }
562
563     /**
564      * @param array $filter
565      * @param array $paging
566      * @param array $event
567      * @param array $ignoreUIDs
568      * @return array
569      */
570     public function searchAttenders($filter, $paging, $events, $ignoreUIDs)
571     {
572         $filters = array();
573         foreach($filter as $f) {
574             switch($f['field']) {
575                 case 'query':
576                     $filters['query'] = $f;
577                     break;
578                 default:
579                     $filters[$f['field']] = $f['value'];
580                     break;
581             }
582         }
583
584         $result = array();
585         $addressBookFE = new Addressbook_Frontend_Json();
586
587         if (!isset($filters['type']) || in_array(Calendar_Model_Attender::USERTYPE_USER, $filters['type'])) {
588             $contactFilter = array(array('condition' => 'OR', 'filters' => array(
589                 $filters['query'],
590                 array('field' => 'path', 'operator' => 'contains', 'value' => $filters['query']['value'])
591             )));
592             if (isset($filters['userFilter'])) {
593                 $contactFilter[] = $filters['userFilter'];
594             }
595             $contactPaging = $paging;
596             $contactPaging['sort'] = 'type';
597             $result[Calendar_Model_Attender::USERTYPE_USER] = $addressBookFE->searchContacts($contactFilter, $contactPaging);
598         }
599
600         if (!isset($filters['type']) || in_array(Calendar_Model_Attender::USERTYPE_GROUP, $filters['type'])) {
601             $groupFilter = array(array('condition' => 'OR', 'filters' => array(
602                 $filters['query'],
603                 array('field' => 'path', 'operator' => 'contains', 'value' => $filters['query']['value'])
604             )));
605             if (isset($filters['groupFilter'])) {
606                 $groupFilter[] = $filters['groupFilter'];
607             }
608             $groupFilter[] = array('field' => 'type', 'operator' => 'contains', 'value' => 'group');
609             $result[Calendar_Model_Attender::USERTYPE_GROUP] = $addressBookFE->searchLists($groupFilter, $paging);
610         }
611
612         if (!isset($filters['type']) || in_array(Calendar_Model_Attender::USERTYPE_RESOURCE, $filters['type'])) {
613             $resourceFilter = array($filters['query']);
614             if (isset($filters['resourceFilter'])) {
615                 $resourceFilter[] = $filters['resourceFilter'];
616             }
617             $result[Calendar_Model_Attender::USERTYPE_RESOURCE] = $this->searchResources($resourceFilter, $paging);
618         }
619
620         if (empty($events)) {
621             $result['freeBusyInfo'] = array();
622         } else {
623             $attendee = array();
624             foreach ($result as $type => $res) {
625                 foreach ($res['results'] as $r) {
626                     $attendee[] = array(
627                         'user_id' => $type === 'group' ? $r['group_id'] : $r['id'],
628                         'user_type' => $type
629                     );
630                 }
631             }
632
633             $result['freeBusyInfo'] = $this->getFreeBusyInfo($attendee, $events, $ignoreUIDs);
634         }
635
636         return $result;
637     }
638 }