1<?php
2
3class goCalendar extends GoBaseBackendDiff {
4
5
6	public function DeleteMessage($folderid, $id, $contentparameters) {
7		ZLog::Write(LOGLEVEL_DEBUG, 'goCalendar->DeleteMessage('.$folderid.','.$id.')');
8
9		$event = \GO\Calendar\Model\Event::model()->findByPk($id);
10
11//		if(!$event->is_organizer) {
12//
13//			//iphone uses delete to decline!
14//			if($this->MeetingResponse($id, "a/GroupOfficeCalendar", 4)) {
15//				return true;
16//			}
17//		}
18
19		// Only delete from GO when you have the right permissions for it.
20		if ($event && $event->checkPermissionLevel(\GO\Base\Model\Acl::DELETE_PERMISSION)) {
21			//HTC deletes old appointments. We don't like that so we refuse to delete appointments older then 7 days.
22			if($event->start_time<\GO\Base\Util\Date::date_add(time(), -7)){
23				return true;
24			}  else {
25				return $event->delete();
26			}
27		} else {
28			return true;
29		}
30	}
31
32	/**
33	 * Get the item object that needs to be synced to the phone.
34	 * This information will be send to the phone.
35	 *
36	 * Direction: SERVER -> PHONE
37	 *
38	 * @param StringHelper $folderid
39	 * @param int $id
40	 * @param array $contentparameters
41	 * @return \SyncAppointment
42	 */
43	public function GetMessage($folderid, $id, $contentparameters) {
44		$event = \GO\Calendar\Model\Event::model()->findByPk($id);
45		if($event)
46			return $this->_handleEvent($event,$contentparameters);
47		else
48			return false;
49	}
50
51	/**
52	 * Handle the event request
53	 *
54	 * Direction: SERVER -> PHONE
55	 *
56	 * @param \GO\Calendar\Model\Event $event
57	 * @return \SyncAppointment
58	 */
59	private function _handleEvent($event,$contentparameters, $exception=false) {
60		$message = $exception ?  new SyncAppointmentException() : new SyncAppointment();
61
62		$message->timezone = GoSyncUtils::getTimeZoneForClient(); //xP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAEAAAAAAAAAxP///w==";
63		$message->starttime = $event->start_time;
64		$message->subject = $event->name;
65		$message->uid = $event->uuid;
66		$message->location = $event->location;
67		$message->endtime = $event->end_time;
68
69		if($event->all_day_event) {
70			//correction because GO saves till 23:59
71			$message->endtime += 60;
72		}
73
74		$message->busystatus = $event->busy == 1 ? "2" : "0";
75
76		if(!$event->is_organizer)
77		{
78			$participant = GO\Calendar\Model\Participant::model()->findSingleByAttributes(array('event_id' => $event->id, 'user_id' => GO::user()->id));
79
80			if($participant) {
81				//iphone uses busy status for events.
82				switch($participant->status) {
83					case GO\Calendar\Model\Participant::STATUS_ACCEPTED:
84						$message->busystatus = 2;
85						break;
86					case GO\Calendar\Model\Participant::STATUS_TENTATIVE:
87						$message->busystatus = 1;
88						break;
89					case GO\Calendar\Model\Participant::STATUS_DECLINED:
90						$message->busystatus = 3;
91						break;
92				}
93
94			}
95		}
96
97		$bpReturnType = GoSyncUtils::getBodyPreferenceMatch($contentparameters->GetBodyPreference());
98
99		if (Request::GetProtocolVersion() >= 12.0) {
100			$message->asbody = GoSyncUtils::createASBodyForMessage($event,'description',$bpReturnType);
101		} else {
102			$message->body = \GO\Base\Util\StringHelper::normalizeCrlf($event->description);
103			$message->bodysize = strlen($message->body);
104			$message->bodytruncated = 0;
105		}
106
107//			$message->sensitivity;
108//			$message->deleted;
109//			$message->categories;
110		$message->dtstamp = $event->ctime;
111//			$message->rtf;
112		// AS 12.0 props
113//			$message->nativebodytype;
114		// AS 14.0 props
115//			$message->disallownewtimeprop;
116//			$message->responsetype;
117//			$message->responserequested;
118
119		$message->alldayevent = $event->all_day_event;
120
121		if (!empty($event->rrule))
122			$message->recurrence = GoSyncUtils::exportRecurrence($event);
123
124		if ($event->reminder !== null){
125
126			if(empty($event->reminder)){ // 0
127				$message->reminder = 0;
128			} else {
129				$message->reminder = $event->reminder / 60;
130			}
131		}
132
133		$exceptions = $this->_handleExceptions($event,$contentparameters);
134		if ($exceptions !== false)
135			$message->exceptions = $exceptions;
136
137		$this->_handleParticipants($event, $message);
138
139		//ZLog::Write(LOGLEVEL_DEBUG, 'MESSAGE '.var_export($message,true));
140		return $message;
141	}
142
143	private function _handleExceptions($event,$contentparameters) {
144		$stmt = $event->exceptions();
145		$exceptions = false;
146
147		if ($stmt->rowCount() > 0) {
148
149			Zlog::Write(LOGLEVEL_DEBUG, "Found exceptions");
150			$exceptions = array();
151			while ($exception = $stmt->fetch()) {
152				if ($exception->event) {
153					$exceptionEvent = $exception->event;
154
155					$xcp = $this->_handleEvent($exceptionEvent,$contentparameters, true);
156					$xcp->exceptionstarttime = $exception->getStartTime();
157					$exceptions[] = $xcp;
158				} else {
159
160					$xcp = new SyncAppointmentException();
161					$xcp->exceptionstarttime = $exception->getStartTime();
162					$xcp->deleted = 1;
163					$exceptions[] = $xcp;
164				}
165			}
166		}
167
168		return $exceptions;
169	}
170
171	/**
172	 *
173	 * Direction: SERVER -> PHONE
174	 *
175	 * @param \GO\Calendar\Model\Event $event
176	 * @param type $message
177	 */
178	private function _handleParticipants($event, &$message) {
179		// Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded
180		$message->meetingstatus = 0;
181
182		$organizer = $event->getOrganizer();
183//		ZLog::Write(LOGLEVEL_DEBUG, '[SERVER -> PHONE]'.var_export($organizer,true));
184		if(!empty($organizer)) {
185			$message->organizername = $organizer->name;
186			$message->organizeremail = $organizer->email;
187			ZLog::Write(LOGLEVEL_DEBUG, '[SERVER -> PHONE] ORGANIZER '.$organizer->name.' : '.$organizer->email);
188
189			$stmt = $event->participants();
190			ZLog::Write(LOGLEVEL_DEBUG, '[SERVER -> PHONE] _handleParticipants rows:'.$stmt->rowCount());
191			if ($stmt->rowCount() > 0) {
192
193
194				$message->responsetype = $event->is_organizer ? 1 : 0;
195
196				if($event->status == \GO\Calendar\Model\Event::STATUS_CANCELLED) {
197					$message->meetingstatus = $event->is_organizer ? 5 : 7;
198				}else
199				{
200					$message->meetingstatus = $event->is_organizer ? 1 : 3;
201				}
202
203				while ($participant = $stmt->fetch()) {
204					ZLog::Write(LOGLEVEL_DEBUG, '[SERVER -> PHONE] PARTICIPANT'.$participant->is_organizer?'(organizer)':''.' '.$participant->name.' : '.$participant->email);
205					if (!$participant->is_organizer) {
206
207
208
209						$att = new SyncAttendee();
210						$att->name = $participant->name;
211						$att->email = $participant->email;
212						$att->attendeetype = 1; //not supported by z-push 1
213						switch ($participant->status) { //not supported by z-push 1
214							case \GO\Calendar\Model\Participant::STATUS_ACCEPTED:
215								$att->attendeestatus = 3;
216								break;
217							case \GO\Calendar\Model\Participant::STATUS_DECLINED:
218								$att->attendeestatus = 4;
219								break;
220							case \GO\Calendar\Model\Participant::STATUS_TENTATIVE:
221								$att->attendeestatus = 2;
222								break;
223							default:
224								$att->attendeestatus = 0;
225								break;
226							}
227							$message->attendees[] = $att;
228
229							if($participant->user_id == GO::user()->id) {
230								$message->responsetype = $att->attendeestatus;
231							}
232					}
233				}
234			}
235		}
236	}
237
238	/**
239	 * Handle the participants of the incoming appointment
240	 *
241	 * Direction: PHONE -> SERVER
242	 *
243	 * @param \SyncAppointment $message
244	 * @param \GO\Calendar\Model\Event $event
245	 */
246	private function _handleAppointmentParticipants(SyncAppointment $message, $event) {
247
248		// Remove existing participants
249		// this function is not called on updates because it's unreliable
250		if (isset($message->attendees)) {
251			$stmt = $event->participants();
252
253			$existingParticipants = array();
254			$hasOrganizer=false;
255			foreach($stmt as $participant){
256				if($participant->is_organizer){
257					$hasOrganizer = true;
258				}else
259				{
260					$existingParticipants[$participant->email]=$participant;
261				}
262			}
263
264			///ZLog::Write(LOGLEVEL_DEBUG, '[PHONE -> SERVER] goCalendar->handleAppointmentParticipants('.  var_export($message,true).','.  var_export($event,true).')');
265
266
267
268			if(isset($message->organizeremail)){
269
270				if(!$hasOrganizer){
271					$organizer = $event->getOrganizer();
272
273					if($organizer){
274
275						$organizer->email = $message->organizeremail;
276
277						if(isset($message->organizername))
278							$organizer->name = $message->organizername;
279
280					} else {
281
282						$organizer = new \GO\Calendar\Model\Participant();
283
284						$organizer->email = $message->organizeremail;
285						$organizer->is_organizer = true;
286
287						if(isset($message->organizername))
288							$organizer->name = $message->organizername;
289
290						if(!$event->addParticipant($organizer))
291							ZLog::Write(LOGLEVEL_ERROR, '[PHONE -> SERVER] Could not add the organizer('.$message->organizeremail.') to the event('.$event->name.')!');
292					}
293				}
294			}elseif(!$hasOrganizer){
295				$organizer = $event->getDefaultOrganizerParticipant();
296				$organizer->save();
297			}
298
299			foreach ($message->attendees as $attendee) {
300
301
302				if(isset($existingParticipants[$attendee->email])){
303					$participant = $existingParticipants[$attendee->email];
304					unset($existingParticipants[$attendee->email]);
305				}  else {
306					$participant = new \GO\Calendar\Model\Participant();
307				}
308
309				$participant->event_id = $event->id;
310				$participant->email = $attendee->email;
311				$participant->name = $attendee->name;
312				$participant->status = \GO\Calendar\Model\Participant::STATUS_PENDING;
313
314				if(isset($attendee->attendeestatus)) {
315					switch($attendee->attendeestatus) {
316						case 2:
317							$participant->status = \GO\Calendar\Model\Participant::STATUS_TENTATIVE;
318							break;
319						case 3:
320							$participant->status = \GO\Calendar\Model\Participant::STATUS_ACCEPTED;
321							break;
322						case 4:
323							$participant->status = \GO\Calendar\Model\Participant::STATUS_DECLINED;
324							break;
325					}
326				}
327				ZLog::Write(LOGLEVEL_DEBUG, '[PHONE -> SERVER] PARTICIPANT '.$participant->name.' : '.$participant->email);
328				$success = $participant->save();
329				ZLog::Write(LOGLEVEL_DEBUG, '[PHONE -> SERVER] PARTICIPANT SAVE ~~ '.$success?'OK':'ERROR');
330			}
331
332
333			foreach($existingParticipants as $notIncludedParticipant){
334
335				ZLog::Write(LOGLEVEL_DEBUG, "DELETE participant: ".$notIncludedParticipant->email);
336				$notIncludedParticipant->delete();
337			}
338		}
339	}
340
341	private $timezone;
342	private function getDefaultTimeZone() {
343		if(!isset($this->timezone)) {
344			$this->timezone = go()->getAuthState()->getUser(['timezone'])->timezone;
345		}
346		return $this->timezone;
347	}
348
349	private function importAllDayTime($time) {
350		$dt = new \DateTime('@'.$time, new \DateTimeZone("UTC"));
351		$dt->setTimezone(new \DateTimeZone($this->getDefaultTimeZone()));
352		$dt->setTime(0, 0);
353		$newTime = $dt->format("U");
354
355		return $newTime;
356	}
357
358	/**
359	 *
360	 * Direction: PHONE -> SERVER
361	 *
362	 * @param \SyncAppointment $message
363	 * @param \GO\Calendar\Model\Event $event
364	 * @return type
365	 */
366	private function _handleAppointment($message, $event) {
367		ZLog::Write(LOGLEVEL_DEBUG, 'goCalendar->_handleAppointment() MESSAGE ~~~ ');
368
369
370//		$message->timezone;
371//    $message->dtstamp;
372//    $message->sensitivity;
373//    $message->rtf;
374//    $message->meetingstatus;
375//    $message->attendees;
376//    $message->bodytruncated;
377//    $message->exception;
378//    $message->deleted;
379//    $message->exceptionstarttime;
380//    $message->categories;
381//
382//    // AS 12.0 props
383//    $message->nativebodytype;
384//
385//    // AS 14.0 props
386//    $message->disallownewtimeprop;
387//    $message->responsetype;
388//    $message->responserequested;
389
390		if (isset($message->uid))
391			$event->uuid = $message->uid;
392		if (isset($message->starttime)) {
393			$event->start_time = $message->starttime;
394			if($message->alldayevent) {
395				$event->start_time = $this->importAllDayTime($event->start_time);
396			}
397		}
398		if (isset($message->endtime)){
399			$event->end_time = $message->endtime;
400			if($message->alldayevent) {
401				$event->end_time = $this->importAllDayTime($event->end_time);
402			}
403		}
404		if (isset($message->location))
405			$event->location = $message->location;
406		if (isset($message->reminder))
407			$event->reminder = $message->reminder * 60;
408		if (isset($message->busystatus))
409			$event->busy = !empty($message->busystatus) ? 2 : 0;
410		if (isset($message->sensitivity))
411			$event->private = empty($message->sensitivity) ? 0 : 1;
412		if (!empty($message->alldayevent)) {
413			$event->end_time -= 60;
414		}
415		$event->all_day_event = $message->alldayevent;
416		$event->name = !empty($message->subject) ? $message->subject : "No subject";
417
418		$event->description = GoSyncUtils::getBodyFromMessage($message);
419
420		if (isset($message->recurrence)) {
421			if (!empty($message->recurrence->until))
422				$event->repeat_end_time = $message->recurrence->until;
423			$event->rrule = GoSyncUtils::importRecurrence($message->recurrence, $event->start_time);
424		}
425
426		$new = $event->isNew;
427
428		$event->cutAttributeLengths();
429		if(!$event->save()){
430			ZLog::Write(LOGLEVEL_WARN, 'ZPUSH2EVENT::Could not save ' . $event->id);
431			ZLog::Write(LOGLEVEL_WARN, var_export($event->getValidationErrors(), true));
432			return false;
433		}
434
435		$event->exceptions()->callOnEach('delete');
436		$event->exceptionEvents()->callOnEach('delete');
437
438
439
440		//don't update existing participants because it is unreliable data from the phone
441		if($event->is_organizer)
442			$this->_handleAppointmentParticipants($message, $event);
443		else
444		{
445			//iphone sends busy status for tentative
446			if($message->busystatus == 1 || $message->busystatus == 2) //tentative
447			{
448				$this->MeetingResponse($event->id, 'a/GroupOfficeCalendar', $message->busystatus == 1 ? 2 : 3);
449			}
450		}
451
452		if (isset($message->exceptions)) {
453			foreach ($message->exceptions as $k => $v) {
454				if (!$v->deleted) {
455					$e = $event->createExceptionEvent($v->exceptionstarttime, array(), true);
456					$e->calendar_id = $event->calendar_id;
457					$e->exception_for_event_id = $event->id;
458					$e->uuid = $event->uuid;
459					// Recursive add the appointment exceptions
460					ZLog::Write(LOGLEVEL_DEBUG, "Creating exception");
461					$e = $this->_handleAppointment($v, $e);
462				} else {
463					$event->createException($v->exceptionstarttime);
464				}
465			}
466		}
467		return $event;
468	}
469
470	/**
471	 * Save the information from the phone to Group-Office.
472	 *
473	 * Direction: PHONE -> SERVER
474	 *
475	 * @param StringHelper $folderid
476	 * @param int $id
477	 * @param \SyncAppointment $message
478	 * @return array
479	 */
480	public function ChangeMessage($folderid, $id, $message,$contentParameters) {
481		ZLog::Write(LOGLEVEL_DEBUG, 'goCalendar->ChangeMessage('.$folderid.','.$id.',)');
482		try {
483
484			$event = \GO\Calendar\Model\Event::model()->findByPk($id);
485
486			if ($event) {
487				if ($event->permissionLevel < \GO\Base\Model\Acl::WRITE_PERMISSION) {
488					ZLog::Write(LOGLEVEL_DEBUG, "Skipping update of read-only event " . $event->name);
489					return $this->StatMessage($folderid, $id);
490				}
491			} else {
492				$calendar = GoSyncUtils::getUserSettings()->getDefaultCalendar();
493
494				if (!$calendar)
495					throw new \Exception("FATAL: No default calendar configured");
496
497				$event = new \GO\Calendar\Model\Event();
498				$event->calendar_id = $calendar->id;
499			}
500
501			$event = $this->_handleAppointment($message, $event);
502			if(!$event)
503				return false;
504
505			$id = $event->id;
506		} catch (\Exception $e) {
507			ZLog::Write(LOGLEVEL_FATAL, 'ZPUSH2CALENDAR::EXCEPTION ~~ ' . (string) $e);
508		}
509
510		return $this->StatMessage($folderid, $id);
511	}
512
513	/**
514	 * Get the status of an item
515	 *
516	 * @param StringHelper $folderid
517	 * @param int $id
518	 * @return array
519	 */
520	public function StatMessage($folderid, $id) {
521
522		$event = \GO\Calendar\Model\Event::model()->findByPk($id);
523		$stat = false;
524		if ($event) {
525			$stat = array();
526			$stat["id"] = $event->id;
527			$stat["flags"] = 1;
528			$stat["mod"] = $event->mtime;
529		}
530
531		return $stat;
532	}
533
534
535	/**
536     * Processes a response to a meeting request.
537     * CalendarID is a reference and has to be set if a new calendar item is created
538     *
539     * @param string        $requestid      id of the object containing the request
540     * @param string        $folderid       id of the parent folder of $requestid
541     * @param string        $response
542     *
543     * @access public
544     * @return string       id of the created/updated calendar obj
545     * @throws StatusException
546     */
547    public function MeetingResponse($requestid, $folderid, $response) {
548
549			ZLog::Write(LOGLEVEL_DEBUG, 'goCalendar->MeetingResponse('.$requestid.', '.$folderid.', '.$response.')');
550
551			$event = \GO\Calendar\Model\Event::model()->findByPk($requestid);
552
553			$participant = GO\Calendar\Model\Participant::model()->findSingleByAttributes(array('user_id' => GO::user()->id, 'event_id' => $requestid));
554			if(!$participant) {
555				throw new StatusException("Participant not found!");
556			}
557
558			switch($response) {
559				case 2:
560					$participant->status = \GO\Calendar\Model\Participant::STATUS_TENTATIVE;
561					break;
562				case 1: //???
563				case 3:
564					$participant->status = \GO\Calendar\Model\Participant::STATUS_ACCEPTED;
565					break;
566				case 4:
567					$participant->status = \GO\Calendar\Model\Participant::STATUS_DECLINED;
568					break;
569			}
570
571			if(!$participant->save(false)) {
572				throw new StatusException("Failed to save participant");
573			}
574
575			ZLog::Write(LOGLEVEL_DEBUG, 'Participant '.$participant->id.' set to status '.$participant->status);
576
577			return $requestid;
578	}
579
580	/**
581	 * Get the list of the items that need to be synced
582	 *
583	 * @param StringHelper $folderid
584	 * @param type $cutoffdate
585	 * @return array
586	 */
587	public function GetMessageList($folderid, $cutoffdate) {
588
589		$messages = array();
590		if (!\GO::modules()->calendar) {
591			return $messages;
592		}
593
594		$params = \GO\Base\Db\FindParams::newInstance()
595						->ignoreAcl()
596						->select('t.id,t.mtime,t.private,t.calendar_id')
597						->joinModel(array(
598								'model' => 'GO\Sync\Model\UserCalendar',
599								'tableAlias' => 'ua',
600								'localTableAlias' => 't',
601								'localField' => 'calendar_id',
602								'foreignField' => 'calendar_id'
603						))
604						->criteria(
605						\GO\Base\Db\FindCriteria::newInstance()
606						->addCondition('user_id', \GO::user()->id, '=', 'ua')
607						->addCondition('exception_for_event_id', 0)
608										);
609
610		if (!empty($cutoffdate)) {
611			ZLog::Write(LOGLEVEL_DEBUG, 'Client sent cutoff date for calendar: ' . \GO\Base\Util\Date::get_timestamp($cutoffdate));
612
613			$params->getCriteria()->mergeWith(\GO\Base\Db\FindCriteria::newInstance()
614							->addCondition('end_time', $cutoffdate, '>=')
615							->mergeWith(
616											\GO\Base\Db\FindCriteria::newInstance()
617											->addCondition('rrule', '', '!=')
618											->mergeWith(
619															\GO\Base\Db\FindCriteria::newInstance()
620															->addCondition('repeat_end_time', 0)
621															->addCondition('repeat_end_time', $cutoffdate, '>=', 't', false))
622											, false)
623			);
624		}
625
626		$stmt = \GO\Calendar\Model\Event::model()->find($params);
627
628		while ($event = $stmt->fetch()) {
629
630			if(!$event->private || $event->calendar->user_id == \GO::user()->id){
631				$message = array();
632				$message['id'] = $event->id;
633				$message['mod'] = $event->mtime;
634				$message['flags'] = 1;
635				$messages[] = $message;
636			}
637		}
638
639		return $messages;
640	}
641
642	/**
643	 * Get the syncFolder that is attached to the given id
644	 *
645	 * @param StringHelper $id
646	 * @return \SyncFolder
647	 */
648	public function GetFolder($id) {
649
650		if ($id != BackendGoConfig::CALENDARBACKENDFOLDER) {
651			return false;
652		}
653
654		$folder = new SyncFolder();
655		$folder->serverid = $id;
656		$folder->parentid = "0";
657		$folder->displayname = 'Calendar';
658		$folder->type = SYNC_FOLDER_TYPE_APPOINTMENT;
659
660		return $folder;
661	}
662
663	/**
664	 * Get a list of folders that are located in the current folder
665	 *
666	 * @return array
667	 */
668	public function GetFolderList() {
669		$folders = array();
670		$folder = $this->StatFolder(BackendGoConfig::CALENDARBACKENDFOLDER);
671		$folders[] = $folder;
672
673		return $folders;
674	}
675
676
677	public function getNotification($folder=null) {
678
679
680		$params = \GO\Base\Db\FindParams::newInstance()
681						->ignoreAcl()->debugSql()
682						->single(true, true)
683						->select('count(*) AS count, max(mtime) AS lastmtime')
684						->join(\GO\Sync\Model\UserCalendar::model()->tableName(), \GO\Base\Db\FindCriteria::newInstance()
685						->addCondition('calendar_id', 's.calendar_id', '=', 't', true, true)
686						->addCondition('user_id', \GO::user()->id, '=', 's')
687						, 's');
688
689
690		$record = \GO\Calendar\Model\Event::model()->find($params);
691
692		$lastmtime = isset($record->lastmtime) ? $record->lastmtime : 0;
693		$newstate = 'M'.$lastmtime.':C'.$record->count;
694
695		ZLog::Write(LOGLEVEL_DEBUG,'goCalendar->getNotification() State: '.$newstate);
696
697		return $newstate;
698	}
699
700}
701