1<?php
2
3/**
4 * Copyright Intermesh
5 *
6 * This file is part of Group-Office. You should have received a copy of the
7 * Group-Office license along with Group-Office. See the file /LICENSE.TXT
8 *
9 * If you have questions write an e-mail to info@intermesh.nl
10 *
11 *
12 * @property int $reminder The number of seconds prior to the start of the event.
13 * @property int $exception_event_id If this event is an exception it holds the id of the original event
14 * @property int $recurrence_id If this event is an exception it holds the date (not the time) of the original recurring instance. It can be used to identity it with an vcalendar file.
15 * @property boolean $is_organizer True if the owner of this event is also the organizer.
16 * @property string $owner_status The status of the owner of this event if this was an invitation
17 * @property int $exception_for_event_id
18 * @property int $sequence
19 * @property int $category_id
20 * @property boolean $read_only
21 * @property boolean $is_virtual Events that are not in the event table: (holidays, birthdays, leavedays, tasks) will have this TRUE use it to disable contextmenu
22 * @property int $files_folder_id
23 * @property string $background eg. "EBF1E2"
24 * @property string $rrule
25 * @property boolean $private
26 * @property int $resource_event_id Set this for a resource event. This is the personal event this resource belongs to.
27 * @property boolean $busy
28 * @property int $mtime
29 * @property int $ctime
30 * @property int $repeat_end_time
31 * @property string $location
32 * @property string $description
33 * @property string $name
34 * @property string $status
35 * @property boolean $all_day_event
36 * @property int $end_time
37 * @property string $timezone
38 * @property int $start_time
39 * @property int $user_id
40 * @property int $calendar_id
41 * @property string $uuid
42 *
43 * @property Participant $participants
44 * @property int $muser_id
45 *
46 * @copyright Copyright Intermesh
47 * @author Merijn Schering <mschering@intermesh.nl>
48 * @author Wesley Smits <wsmits@intermesh.nl>
49 */
50
51namespace GO\Calendar\Model;
52
53use DateInterval;
54use DateTime;
55use DateTimeZone;
56use GO\Calendar\Model\Exception;
57use GO;
58use GO\Base\Util\StringHelper;
59use go\core\model\Module;
60use Sabre;
61use Swift_Attachment;
62use Swift_Mime_ContentEncoder_PlainContentEncoder;
63
64class Event extends \GO\Base\Db\ActiveRecord {
65
66	use \go\core\orm\CustomFieldsTrait;
67
68	const STATUS_TENTATIVE = 'TENTATIVE';
69//	const STATUS_DECLINED = 'DECLINED';
70//	const STATUS_ACCEPTED = 'ACCEPTED';
71	const STATUS_CANCELLED = 'CANCELLED';
72	const STATUS_CONFIRMED = 'CONFIRMED';
73	const STATUS_NEEDS_ACTION = 'NEEDS-ACTION';
74	const STATUS_DELEGATED = 'DELEGATED';
75
76	/**
77	 * The date where the exception needs to be created. If this is set on a new event
78	 * an exception will automatically be created for the recurring series. exception_for_event_id needs to be set too.
79	 *
80	 * @var timestamp
81	 */
82	public $exception_date;
83
84	public $dontSendEmails=false;
85
86	public $sequence;
87
88
89	/**
90	 * Indicating that this is an update for a related event.
91	 * eg. The organizer modifies the event and all events for invitees.
92	 *
93	 * @var boolean
94	 */
95	public $updatingRelatedEvent=false;
96
97	/**
98	 * Flag used when importing. On import we allow participant events to be
99	 * modified even when they are not the organizer. Because a meeting request
100	 * coming from the organizer must be procesed by the participant.
101	 *
102	 * @var boolean
103	 */
104	private $_isImport=false;
105
106
107	public function getUri() {
108		if(isset($this->_setUri)) {
109			return $this->_setUri;
110		}
111
112		return str_replace('/','+',$this->uuid).'-'.$this->id;
113	}
114
115	private $_setUri;
116
117	public function setUri($uri) {
118		$this->_setUri = $uri;
119	}
120
121	public function getETag() {
122		return '"' . date('Ymd H:i:s', $this->mtime). '-'.$this->id.'"';
123	}
124
125	protected function init() {
126
127		$this->columns['calendar_id']['required']=true;
128		$this->columns['start_time']['gotype'] = 'unixtimestamp';
129		$this->columns['end_time']['greater'] = 'start_time';
130		$this->columns['end_time']['gotype'] = 'unixtimestamp';
131		$this->columns['repeat_end_time']['gotype'] = 'unixtimestamp';
132		$this->columns['repeat_end_time']['greater'] = 'start_time';
133		//$this->columns['category_id']['required'] = \GO\Calendar\CalendarModule::commentsRequired();
134
135		parent::init();
136	}
137
138	public function isValidStatus($status){
139		return ($status==self::STATUS_CANCELLED || $status==self::STATUS_CONFIRMED || $status==self::STATUS_DELEGATED || $status==self::STATUS_TENTATIVE || $status==self::STATUS_NEEDS_ACTION);
140	}
141
142	public function aclField() {
143		return 'calendar.acl_id';
144	}
145
146	public function tableName() {
147		return 'cal_events';
148	}
149
150	public function hasFiles() {
151		return true;
152	}
153
154	public function hasLinks() {
155		return true;
156	}
157//
158//	public function countLinks() {
159//		$sql = "SELECT count(*) FROM `go_links_$table` WHERE ".
160//			"`id`=".intval($this_id).";";
161//		$stmt = $this->getDbConnection()->query($sql);
162//		return !empty($stmt) ? $stmt->rowCount() : 0;
163//	}
164
165	public function countReminders() {
166
167		$modelTypeModel = \GO\Base\Model\ModelType::model()->findSingleByAttribute('model_name',$this->className());
168
169		$stmt = \GO\Base\Model\Reminder::model()->findByAttributes(array(
170			'model_id' => $this->id,
171			'model_type_id'=> $modelTypeModel->id
172		));
173
174		return !empty($stmt) ? $stmt->rowCount() : 0;
175
176	}
177
178
179	public function defaultAttributes() {
180
181
182		$defaults = array(
183			'status' => self::STATUS_CONFIRMED,
184			'start_time'=> \GO\Base\Util\Date::roundQuarters(time()),
185			'end_time' => \GO\Base\Util\Date::roundQuarters(time()+3600),
186			'timezone' => \GO::user()->timezone
187		);
188
189
190		if($this->isResource()) {
191			$defaults['status'] = self::STATUS_NEEDS_ACTION;
192		}
193
194		$settings = Settings::model()->getDefault(\GO::user());
195		if($settings){
196			$defaults = array_merge($defaults, array(
197				'reminder' => $settings->reminder,
198				'calendar_id'=>$settings->calendar_id,
199				'background'=>$settings->background
200						));
201		}
202
203		return $defaults;
204	}
205
206	public function customfieldsModel() {
207		return "GO\Calendar\Customfields\Model\Event";
208	}
209
210
211	public function relations() {
212		return array(
213				'_exceptionEvent'=>array('type' => self::BELONGS_TO, 'model' => 'GO\Calendar\Model\Event', 'field' => 'exception_for_event_id'),
214				'recurringEventException'=>array('type' => self::HAS_ONE, 'model' => 'GO\Calendar\Model\Exception', 'field' => 'exception_event_id'),//If this event is an exception for a recurring series. This relation points to the exception of the recurring series.
215				'calendar' => array('type' => self::BELONGS_TO, 'model' => 'GO\Calendar\Model\Calendar', 'field' => 'calendar_id','labelAttribute'=>function($model){return $model->calendar->name;}),
216				'category' => array('type' => self::BELONGS_TO, 'model' => 'GO\Calendar\Model\Category', 'field' => 'category_id'),
217				'participants' => array('type' => self::HAS_MANY, 'model' => 'GO\Calendar\Model\Participant', 'field' => 'event_id', 'delete' => true),
218				'exceptions' => array('type' => self::HAS_MANY, 'model' => 'GO\Calendar\Model\Exception', 'field' => 'event_id', 'delete' => true),
219				'exceptionEvents' => array('type' => self::HAS_MANY, 'model' => 'GO\Calendar\Model\Event', 'field' => 'exception_for_event_id', 'delete' => true),
220				'resources' => array('type' => self::HAS_MANY, 'model' => 'GO\Calendar\Model\Event', 'field' => 'resource_event_id', 'delete' => true)
221		);
222	}
223
224	protected function log($action, $save = true, $modifiedCustomfieldAttrs=false) {
225		if(!$this->updatingRelatedEvent) {
226			return parent::log($action, $save, $modifiedCustomfieldAttrs);
227		} else
228		{
229			return true;
230		}
231	}
232
233	protected function getCacheAttributes() {
234
235		if(GO::router()->getControllerAction()!='buildsearchcache' && !$this->isDeleted() && !$this->isModified(["calendar_id", "description", "private", "start_time", "name"])) {
236			return false;
237		}
238
239		$calendarName = empty($this->calendar) ? '' :$this->calendar->name;
240
241		$description = $calendarName;
242
243		 if(!$this->private && !empty($this->description) ){
244			$description .= ', ' . $this->description;
245		 }
246
247		return array(
248				'name' => $this->private ?  \GO::t("Private", "calendar") : $this->name,
249				'description' =>  $description,
250				'mtime'=>$this->start_time
251		);
252	}
253
254	protected function getLocalizedName() {
255		return \GO::t("Event", "calendar");
256	}
257
258	/**
259	 * The files module will use this function.
260	 */
261	public function buildFilesPath() {
262
263		return 'calendar/' . \GO\Base\Fs\Base::stripInvalidChars($this->calendar->name) . '/' . date('Y', $this->start_time) . '/' . \GO\Base\Fs\Base::stripInvalidChars($this->name).' ('.$this->id.')';
264	}
265
266	/**
267	 * Get the color for the current status of this event
268	 *
269	 * @return StringHelper
270	 */
271	public function getStatusColor(){
272
273		switch($this->status){
274			case Event::STATUS_TENTATIVE:
275				$color = 'FFFF00'; //Yellow
276			break;
277			case Event::STATUS_CANCELLED:
278				$color = 'FF0000'; //Red
279			break;
280//			case Event::STATUS_ACCEPTED:
281//				$color = '00FF00'; //Lime
282//			break;
283			case Event::STATUS_CONFIRMED:
284				$color = '32CD32'; //LimeGreen
285			break;
286			case Event::STATUS_DELEGATED:
287				$color = '0000CD'; //MediumBlue
288			break;
289
290			default:
291			case Event::STATUS_NEEDS_ACTION:
292				$color = 'FF8C00'; //DarkOrange
293			break;
294		}
295
296		return $color;
297	}
298
299	/**
300	 * Get the date interval for the event.
301	 *
302	 * @return array
303	 */
304	public function getDiff() {
305		$startDateTime = new \GO\Base\Util\Date\DateTime(date('c', $this->start_time));
306		$endDateTime = new \GO\Base\Util\Date\DateTime(date('c', $this->end_time));
307
308		return $startDateTime->diff($endDateTime);
309	}
310
311	/**
312	 * Add an Exception for the Event if it is recurring
313	 *
314	 * @param Unix Timestamp $date The date where the exception belongs to
315	 * @param Int $for_event_id The event id of the event where the exception belongs to
316	 */
317	public function addException($date, $exception_event_id=0) {
318
319		\GO::debug('Add exception '.$this->id.' '.date('c', $date));
320
321		if(!$this->isRecurring())
322			throw new \Exception("Can't add exception to non recurring event ".$this->id);
323
324		if(!($exception = $this->hasException($date))){
325			$exception = new \GO\Calendar\Model\Exception();
326		}
327
328		$exception->event_id = $this->id;
329		$exception->time = mktime(date('G',$this->start_time),date('i',$this->start_time),0,date('n',$date),date('j',$date),date('Y',$date)); // Needs to be a unix timestamp
330		$exception->exception_event_id=$exception_event_id;
331
332
333		if(!$exception->save()){
334			throw new \Exception("Event exception not saved: ".var_export($exception->getValidationErrors(), true));
335		}
336
337
338	}
339
340	/**
341	 * This Event needs to be reinitialized to become an Exception of its own on the given Unix timestamp.
342	 * It will not save the event and doesn't copy participants. Use createExcetionEvent for that.
343	 *
344	 * @param int $exceptionDate Unix timestamp
345	 */
346	public function getExceptionEvent($exceptionDate) {
347
348
349		$att['rrule'] = '';
350		$att['repeat_end_time']=0;
351		$att['exception_for_event_id'] = $this->id;
352		$att['exception_date'] = $exceptionDate;
353
354		$diff = $this->getDiff();
355
356		$d = date('Y-m-d', $exceptionDate);
357		$t = date('G:i', $this->start_time);
358
359		$att['start_time'] = strtotime($d . ' ' . $t);
360
361		$endTime = new \GO\Base\Util\Date\DateTime(date('c', $att['start_time']));
362		$endTime->add($diff);
363		$att['end_time'] = $endTime->format('U');
364
365
366
367		$duplicate =  $this->duplicate($att, false);
368
369//		$this->copyLinks($duplicate);
370
371		return $duplicate;
372	}
373
374
375	public function createException($exceptionDate){
376		$stmt = $this->getRelatedParticipantEvents(true);//A meeting can be multiple related events sharing the same uuid
377		foreach($stmt as $event){
378			$event->addException($exceptionDate, 0);
379		}
380	}
381
382	/**
383	 * Check if this model has resource conflicts.
384	 * This only works with existing events and NOT with new events.(Will allways return: false)
385	 *
386	 * @return mixed (false, Array with resource events)
387	 */
388	public function hasResourceConflicts(){
389
390		$hasConflict = false;
391		$foundConflicts = array();
392
393		if($this->isNew || $this->isResource()){
394			// Not possible to determine this when having a new model.
395			// Because the resources are not created yet.
396			return false;
397		} else {
398
399			$resources = $this->resources;
400
401			foreach($resources as $resource){
402
403				$resource->start_time = $this->start_time;
404				$resource->end_time = $this->end_time;
405
406				$conflicts = $resource->getConflictingEvents();
407
408				if(count($conflicts) > 0){
409					$foundConflicts[] = $resource;
410					$hasConflict = true;
411				}
412			}
413
414			if($hasConflict){
415				return $foundConflicts;
416			} else {
417				return false;
418			}
419		}
420	}
421
422	/**
423	 * Create an exception for a recurring series.
424	 *
425	 * @param int $exceptionDate
426	 * @return Event
427	 */
428	public function createExceptionEvent($exceptionDate, $attributes=array(), $dontSendEmails=false){
429
430
431		if(!$this->isRecurring()){
432			throw new \Exception("Can't create exception event for non recurring event ".$this->id);
433		}
434
435		$oldIgnore = \GO::setIgnoreAclPermissions();
436		$returnEvent = false;
437		if($this->isResource())
438			$stmt = array($this); //resource is never a group of events
439		else
440			$stmt = $this->getRelatedParticipantEvents(true);//A meeting can be multiple related events sharing the same uuid
441
442		$resources = array();
443
444		$freeBusyInstalled = \GO::modules()->isInstalled("freebusypermissions");
445
446		foreach($stmt as $event){
447
448			if($freeBusyInstalled && !$event->calendar->checkPermissionlevel(\GO\Base\Model\Acl::WRITE_PERMISSION) && !\GO\Freebusypermissions\FreebusypermissionsModule::hasFreebusyAccess(\GO::user()->id, $event->calendar->user_id)) {
449				//no permission to update
450				continue;
451			}
452
453			//workaround for old events that don't have the exception ID set. In this case
454			//getRelatedParticipantEvents fails. This won't happen with new events
455			if(!$event->isRecurring())
456				continue;
457
458			\GO::debug("Creating exception for related participant event ".$event->name." (".$event->id.") ".date('c', $exceptionDate));
459
460			$exceptionEvent = $event->getExceptionEvent($exceptionDate);
461			$exceptionEvent->dontSendEmails = $dontSendEmails;
462			$exceptionEvent->setAttributes($attributes);
463			if(!$exceptionEvent->save())
464				throw new \Exception("Could not create exception: ".var_export($exceptionEvent->getValidationErrors(), true));
465
466
467			$event->copyLinks($exceptionEvent);
468
469			$event->addException($exceptionDate, $exceptionEvent->id);
470
471
472
473
474
475
476			$event->duplicateRelation('participants', $exceptionEvent, array('dontCreateEvent' => true));
477
478
479			if(!$event->isResource() && $event->is_organizer){
480				$stmt = $event->resources();
481				foreach($stmt as $resource){
482					$resources[]=$resource;
483				}
484				$resourceExceptionEvent = $exceptionEvent;
485			}
486
487			if($event->id==$this->id)
488				$returnEvent=$exceptionEvent;
489		}
490
491		foreach($resources as $resource){
492			\GO::debug("Creating exception for resource: ".$resource->name);
493			$resource->createExceptionEvent($exceptionDate, array('resource_event_id'=>$resourceExceptionEvent->id), $dontSendEmails);
494		}
495
496		\GO::setIgnoreAclPermissions($oldIgnore);
497		return $returnEvent;
498	}
499
500	public function attributeLabels() {
501		$attr = parent::attributeLabels();
502		$attr['repeat_end_time']=\GO::t("Repeat until", "calendar");
503		$attr['start_time']=\GO::t("Starts at", "calendar");
504		$attr['end_time']=\GO::t("Ends at", "calendar");
505		return $attr;
506	}
507
508	public $skipValidation = false;
509
510	public function validate() {
511
512		if($this->skipValidation) {
513			return true;
514		}
515
516		if($this->rrule != ""){
517			$rrule = new \GO\Base\Util\Icalendar\Rrule();
518			$rrule->readIcalendarRruleString($this->start_time, $this->rrule);
519			$this->repeat_end_time = $rrule->until;
520		}
521
522		//ignore reminders longer than 90 days.
523		if($this->reminder > 86400 * 90){
524			\GO::debug("WARNING: Ignoring reminder that is longer than 90 days before event start");
525			$this->reminder = null;
526		}
527
528
529		if($this->exception_for_event_id != 0 && $this->exception_for_event_id == $this->id){
530			throw new \Exception("Exception event ID can't be set to ID");
531		}
532
533		if($this->exception_for_event_id != 0 && !empty($this->rrule)) {
534			throw new \Exception("Can't create exception with RRULE");
535		}
536
537		$resourceConflicts = $this->hasResourceConflicts();
538
539		if($resourceConflicts !== false){
540
541
542			$errorMessage = GO::t("Could not move event because the following resources are not available:", "calendar");
543
544			foreach ($resourceConflicts as $rc){
545				$errorMessage .= '<br />- '.$rc->calendar->name;
546			}
547
548			$this->setValidationError('start_time', $errorMessage);
549		}
550		return parent::validate();
551	}
552
553		public function getRelevantMeetingAttributes(){
554		return array("name","start_time","end_time","location","description","calendar_id","rrule","repeat_end_time");
555	}
556
557
558	public function setAttributes($attributes, $format=null){
559		parent::setAttributes($attributes, $format);
560
561		if($this->getIsNew() ) {
562			$this->reevaluateStatus();
563		}
564
565	}
566
567	private function getResourceAdminIds() {
568		$adminUserIds=array();
569		if($this->isResource()){
570
571			$groupAdminsStmt= $this->calendar->group->admins;
572			while($adminUser = $groupAdminsStmt->fetch()){
573				$adminUserIds[] = $adminUser->id;
574			}
575
576		}
577		return $adminUserIds;
578	}
579
580
581	private function isCurrentUserResourceAdmin() {
582		return in_array(\GO::user()->id, $this->getResourceAdminIds());
583	}
584
585
586	private function reevaluateStatus() {
587
588		// if is it is a resource and non resource admin set status to neet action
589		if($this->isResource()) {
590
591			if (!$this->isCurrentUserResourceAdmin()) {
592				$this->status =  self::STATUS_NEEDS_ACTION;
593			}
594		}
595
596	}
597
598	protected function beforeSave() {
599
600		GO::debug("#### EVENT BEFORE SAVE ####");
601
602		if($this->rrule != ""){
603			$rrule = new \GO\Base\Util\Icalendar\Rrule();
604			$rrule->readIcalendarRruleString($this->start_time, $this->rrule);
605			$this->repeat_end_time = intval($rrule->until);
606		}
607
608
609		if(!GO::user()->isAdmin()) {
610			//if this is not the organizer event it may only be modified by the organizer
611			if(!$this->is_organizer && !$this->updatingRelatedEvent && !$this->_isImport && !$this->isNew && $this->isModified($this->getRelevantMeetingAttributes())){
612	//			$organizerEvent = $this->getOrganizerEvent();
613	//			if($organizerEvent && !$organizerEvent->checkPermissionLevel(\GO\Base\Model\Acl::WRITE_PERMISSION) || !$organizerEvent && !$this->is_organizer){
614	//				\GO::debug($this->getModifiedAttributes());
615	//				\GO::debug($this->_attributes);
616					throw new \GO\Base\Exception\AccessDenied();
617	//			}
618			}
619		}
620
621//		//Don't set reminders for the superadmin
622//		if($this->calendar->user_id==1 && \GO::user()->id!=1 && !\GO::config()->debug)
623//			$this->reminder=0;
624
625
626		$this->reevaluateStatus();
627
628		if($this->isResource()){
629
630			if($this->status == self::STATUS_CONFIRMED){
631				$this->background='CCFFCC';
632			}else
633			{
634				$this->background='FF6666';
635			}
636		}
637
638		return parent::beforeSave();
639	}
640
641	protected function afterDbInsert() {
642		if(empty($this->uuid)){
643			$this->uuid = \GO\Base\Util\UUID::create('event', $this->id);
644			return true;
645		}else
646		{
647			return false;
648		}
649	}
650
651
652	protected function afterDelete() {
653
654		$this->deleteReminders();
655
656		if($this->is_organizer){
657//
658//	This is dangerous: when you first import the same ICS into two different calendars,
659//	and the delete one of the calendars, all the imported events in the other calendars
660//	would be deleted also.
661//
662//			$stmt = $this->getRelatedParticipantEvents();
663//
664//			foreach($stmt as $event){
665//				//prevent loop for invalid is_organizer flag
666//				$event->is_organizer=false;
667//				$event->delete(true);
668//			}
669		}else
670		{
671			$participants = $this->getParticipantsForUser();
672
673			foreach($participants as $participant){
674				$participant->updateRelatedParticipants = false;
675				$participant->dontCreateEvent = true;
676				$participant->status=Participant::STATUS_DECLINED;
677				$participant->save(true);
678			}
679		}
680
681		//for sync update master series
682		if($this->isException()) {
683			if($this->_exceptionEvent) {
684				$this->_exceptionEvent->mtime = time();
685				$this->_exceptionEvent->save(true);
686			}
687		}
688
689		return parent::afterDelete();
690	}
691
692	public static function reminderDismissed($reminder, $userId){
693
694		//this listener function is added in \GO\Calendar\CalendarModule
695
696		if($reminder->model_type_id==Event::model()->modelTypeId()){
697			$event = Event::model()->findByPk($reminder->model_id);
698			if($event && ($nextTime = $event->getNextReminderTime($reminder->time+$event->reminder))){
699				$event->addReminder($event->name, $nextTime, $userId, $nextTime+$event->reminder);
700			}
701		}
702	}
703
704	/**
705	 * Get the next reminder time of this event
706	 *
707	 * @return int
708	 */
709	public function getNextReminderTime($lastReminderTime = 0){
710
711		if($this->reminder===null)
712			return false;
713
714		if($this->isRecurring()){
715			$next = time()+$this->reminder;
716			if($next > $lastReminderTime) {
717				$lastReminderTime = $next;
718			}
719
720
721
722			$rRule = $this->getRecurrencePattern();
723			$rRule->fastForward(new \DateTime('@'.$lastReminderTime));
724			$nextTime = $rRule->current();
725			while($nextTime && $this->hasException($nextTime->getTimeStamp())){
726				$rRule->next();
727				$nextTime = $rRule->current();
728			}
729
730
731			if($nextTime && $nextTime->getTimeStamp()>time()){
732				return $nextTime->getTimeStamp()-$this->reminder;
733			}else
734				return false;
735
736		}  else {
737			$nextTime = $this->start_time-$this->reminder;
738			if($nextTime>time())
739				return $nextTime;
740			else
741				return false;
742		}
743	}
744
745	/**
746	 * Check if this event is recurring
747	 *
748	 * @return boolean
749	 */
750	public function isRecurring(){
751		return $this->rrule!="";
752	}
753
754	/**
755	 * Check if this event is a fullday event
756	 *
757	 * @return boolean
758	 */
759	public function isFullDay() {
760		return !empty($this->all_day_event);
761	}
762
763	public function hasReminders() {
764		return !is_null($this->reminder);
765	}
766
767	public function isException() {
768		return $this->exception_for_event_id != 0;
769	}
770
771	protected function afterSave($wasNew) {
772
773		//move exceptions if this event was moved in time
774		if(!$wasNew && !empty($this->rrule) && $this->isModified('start_time')){
775			$diffSeconds = $this->start_time-$this->getOldAttributeValue('start_time');
776			$stmt = $this->exceptions();
777			while($exception = $stmt->fetch()){
778				$exception->time+=$diffSeconds;
779				$exception->save();
780			}
781		}
782
783		if($this->isResource()){
784
785			if ((! $this->isCurrentUserResourceAdmin() || $this->isModified('status'))&& ($this->end_time > time() || ($this->isRecurring() && (empty($this->repeat_end_time) || $this->repeat_end_time > time())))) {
786				$this->_sendResourceNotification($wasNew);
787			}
788		}else
789		{
790			if(!$wasNew && $this->hasModificationsForParticipants())
791				$this->_updateResourceEvents();
792		}
793
794		$this->setReminder();
795
796		//update events that belong to this organizer event
797		if($this->is_organizer && !$wasNew && !$this->isResource()){
798			$updateAttr = array(
799					'name'=>$this->name,
800					'start_time'=>$this->start_time,
801					'end_time'=>$this->end_time,
802					'location'=>$this->location,
803					'description'=>$this->description,
804					'rrule'=>$this->rrule,
805					'status'=>$this->status,
806					'repeat_end_time'=>$this->repeat_end_time
807							);
808
809			if($this->isModified(array_keys($updateAttr))){
810
811				$events = $this->getRelatedParticipantEvents();
812				$freeBusyInstalled = \GO::modules()->isInstalled("freebusypermissions");
813				foreach($events as $event){
814					\GO::debug("updating related event: ".$event->id);
815
816					if($event->id!=$this->id && $this->is_organizer!=$event->is_organizer){ //this should never happen but to prevent an endless loop it's here.
817
818						if($freeBusyInstalled && ! \GO\Freebusypermissions\FreebusypermissionsModule::hasFreebusyAccess(\GO::user()->id, $event->calendar->user_id)) {
819							//no permission to update
820							continue;
821						}
822
823						$event->setAttributes($updateAttr, false);
824						$event->updatingRelatedEvent=true;
825						$event->save(true);
826
827//						$stmt = $event->participants;
828//						$stmt->callOnEach('delete');
829//
830//						$this->duplicateRelation('participants', $event);
831					}
832				}
833			}
834		}
835		if($this->isModified()) {
836			Calendar::versionUp($this->calendar_id);
837		}
838
839		//for sync update master series
840		if($this->isException() && $this->_exceptionEvent) {
841			$this->_exceptionEvent->mtime = time();
842			$this->_exceptionEvent->save(true);
843
844		}
845		return parent::afterSave($wasNew);
846	}
847
848	public function setReminder(){
849
850		if($this->reminder !== null){
851			$remindTime = $this->getNextReminderTime();
852
853			if($remindTime){
854				$this->deleteReminders();
855				$this->addReminder($this->name, $remindTime, $this->calendar->user_id, $remindTime+$this->reminder);
856			}
857		} else {
858			$this->deleteReminders();
859		}
860	}
861
862	/**
863	 * Get's all related events that are in the participant's calendars.
864	 *
865	 * @return Event
866	 */
867	public function getRelatedParticipantEvents($includeThisEvent=false){
868		$findParams = \GO\Base\Db\FindParams::newInstance()->ignoreAcl();
869
870		$start_time = $this->isModified('start_time') ? $this->getOldAttributeValue('start_time') : $this->start_time;
871
872		$findParams->getCriteria()
873						->addCondition("uuid", $this->uuid) //recurring series and participants all share the same uuid
874						->addCondition('start_time', $start_time) //make sure start time matches for recurring series
875						->addCondition("exception_for_event_id", 0, $this->exception_for_event_id==0 ? '=' : '!='); //the master event or a single occurrence can start at the same time. Therefore we must check if exception event has a value or is 0.
876
877		if(!$includeThisEvent)
878			$findParams->getCriteria()->addCondition('id', $this->id, '!=');
879
880
881		$stmt = Event::model()->find($findParams);
882
883		return $stmt;
884	}
885
886
887
888	/**
889	 * If this is a resource of the current user ignore ACL permissions when deleting
890	 */
891	public function delete($ignoreAcl=false)
892	{
893		if(!empty($this->resource_event_id) && $this->user_id == \GO::user()->id)
894			$success = parent::delete(true);
895		else
896			$success = parent::delete($ignoreAcl);
897		if($success)
898			Calendar::versionUp($this->calendar_id);
899		return $success;
900	}
901
902	public function hasModificationsForParticipants(){
903		return $this->isModified("start_time") || $this->isModified("end_time") || $this->isModified("name") || $this->isModified("location") || $this->isModified('status') || $this->isModified('rrule');
904	}
905
906	/**
907	 * Is this a private event for the current user. If the event or the calendar
908	 * is owned by the current user it will not be displayed as private.
909	 *
910	 * @param \GO\Base\Model\User $user
911	 */
912	public function isPrivate(\GO\Base\Model\User $user=null){
913		if(!isset($user))
914			$user=\GO::user();
915
916		return $this->private &&
917			($user->id != $this->user_id) &&
918			$user->id!=$this->calendar->user_id;
919	}
920
921	/**
922	 * Events may have related resource events that must be updated aftersave
923	 */
924	private function _updateResourceEvents(){
925		$stmt = $this->resources();
926
927		while($resourceEvent = $stmt->fetch()){
928
929			$resourceEvent->name=$this->name;
930			$resourceEvent->start_time=$this->start_time;
931			$resourceEvent->end_time=$this->end_time;
932			$resourceEvent->rrule=$this->rrule;
933			$resourceEvent->repeat_end_time=$this->repeat_end_time;
934			$resourceEvent->status="NEEDS-ACTION";
935			$resourceEvent->user_id=$this->user_id;
936			$resourceEvent->save(true);
937		}
938	}
939
940	private function _sendResourceNotification($wasNew){
941
942		if(!$this->dontSendEmails && $this->hasModificationsForParticipants()){
943			$url = \GO::createExternalUrl('calendar', 'showEventDialog', array('event_id' => $this->id));
944
945			//send updates to the resource admins
946			$adminUserIds=array();
947			$stmt = $this->calendar->group->admins;
948
949			while($adminUser = $stmt->fetch()){
950				$adminUserIds[] = $adminUser->id;
951				if($adminUser->id!=\GO::user()->id){
952
953
954					if($wasNew){
955
956						if ($this->status==Event::STATUS_CONFIRMED) {
957							$body = sprintf(\GO::t("%s has made a booking for the resource '%s' and confirmed the booking. You are the maintainer of this resource. Use the link below if you want to decline the booking.", "calendar"),$this->user->name,$this->calendar->name).'<br /><br />'
958											. $this->toHtml()
959											. '<br /><a href="'.$url.'">'.\GO::t("Open booking", "calendar").'</a>';
960
961							$subject = sprintf(\GO::t("Resource '%s' booked for '%s' on '%s'", "calendar"),$this->calendar->name, $this->name, \GO\Base\Util\Date::get_timestamp($this->start_time,false));
962						} else {
963							$body = sprintf(\GO::t("%s has made a booking for the resource '%s'. You are the maintainer of this resource. Please open the booking to decline or approve it.", "calendar"),$this->user->name,$this->calendar->name).'<br /><br />'
964											. $this->toHtml()
965											. '<br /><a href="'.$url.'">'.\GO::t("Open booking", "calendar").'</a>';
966
967							$subject = sprintf(\GO::t("Resource '%s' booked for '%s' on '%s'", "calendar"),$this->calendar->name, $this->name, \GO\Base\Util\Date::get_timestamp($this->start_time,false));
968						}
969					}else
970					{
971						$body = sprintf(\GO::t("%s has modified a booking for the resource '%s'. You are the maintainer of this resource. Please open the booking to decline or approve it.", "calendar"),$this->user->name,$this->calendar->name).'<br /><br />'
972										. $this->toHtml()
973										. '<br /><a href="'.$url.'">'.\GO::t("Open booking", "calendar").'</a>';
974
975						$subject = sprintf(\GO::t("Resource '%s' booking for '%s' on '%s' modified", "calendar"),$this->calendar->name, $this->name, \GO\Base\Util\Date::get_timestamp($this->start_time,false));
976					}
977
978					$message = \GO\Base\Mail\Message::newInstance(
979										$subject
980										)->setFrom(go()->getSettings()->systemEmail, go()->getSettings()->title)
981										->addTo($adminUser->email, $adminUser->name);
982
983					$message->setHtmlAlternateBody($body);
984
985					\GO\Base\Mail\Mailer::newGoInstance()->send($message);
986				}
987			}
988
989
990			//send update to user that booked the resource
991			if($this->user_id!=\GO::user()->id
992						&& in_array(\GO::user()->id,$adminUserIds)
993				) {
994				if($this->isModified('status')){
995					if($this->status==Event::STATUS_CONFIRMED){
996						$body = sprintf(\GO::t("%s has accepted your booking for the resource '%s'.", "calendar"),\GO::user()->name,$this->calendar->name).'<br /><br />'
997								. $this->toHtml();
998								//. '<br /><a href="'.$url.'">'.\GO::t("Open booking", "calendar").'</a>';
999
1000						$subject = sprintf(\GO::t("Your booking for '%s' on '%s' is accepted", "calendar"),$this->calendar->name, $this->name, \GO\Base\Util\Date::get_timestamp($this->start_time,false));
1001					}else
1002					{
1003						$body = sprintf(\GO::t("%s has declined your booking for the resource '%s'.", "calendar"),\GO::user()->name,$this->calendar->name).'<br /><br />'
1004								. $this->toHtml();
1005								//. '<br /><a href="'.$url.'">'.\GO::t("Open booking", "calendar").'</a>';
1006
1007						$subject = sprintf(\GO::t("Your booking for '%s' on '%s' is declined", "calendar"),$this->calendar->name, $this->name, \GO\Base\Util\Date::get_timestamp($this->start_time,false));
1008					}
1009				}else
1010				{
1011					$body = sprintf(\GO::t("%s has modified your booking for the resource '%s'.", "calendar"),\GO::user()->name,$this->calendar->name).'<br /><br />'
1012								. $this->toHtml();
1013//								. '<br /><a href="'.$url.'">'.\GO::t("Open booking", "calendar").'</a>';
1014					$subject = sprintf(\GO::t("Your booking for '%s' on '%s' in status '%s' is modified", "calendar"),$this->calendar->name, $this->name, \GO\Base\Util\Date::get_timestamp($this->start_time,false));
1015				}
1016
1017				$url = \GO::createExternalUrl('calendar', 'openCalendar', array(
1018					'unixtime'=>$this->start_time
1019				));
1020
1021				$body .= '<br /><a href="'.$url.'">'.\GO::t("Open calendar", "calendar").'</a>';
1022
1023				$message = \GO\Base\Mail\Message::newInstance(
1024									$subject
1025									)->setFrom(go()->getSettings()->systemEmail, go()->getSettings()->title)
1026									->addTo($this->user->email, $this->user->name);
1027
1028				$message->setHtmlAlternateBody($body);
1029
1030				\GO\Base\Mail\Mailer::newGoInstance()->send($message);
1031			}
1032
1033		}
1034	}
1035
1036	/**
1037	 *
1038	 * @var LocalEvent
1039	 */
1040	private $_calculatedEvents;
1041
1042
1043	/**
1044	 * Finds a specific occurence for a date.
1045	 *
1046	 * @param int $exceptionDate
1047	 * @return Event
1048	 * @throws Exception
1049	 */
1050	public function findException($exceptionDate) {
1051
1052		if ($this->exception_for_event_id != 0)
1053			throw new \Exception("This is not a master event");
1054
1055		$startOfDay = \GO\Base\Util\Date::clear_time($exceptionDate);
1056		$endOfDay = \GO\Base\Util\Date::date_add($startOfDay, 1);
1057
1058		$findParams = \GO\Base\Db\FindParams::newInstance();
1059
1060
1061
1062		//must be an exception and start on the must start on the exceptionTime
1063		$exceptionJoinCriteria = \GO\Base\Db\FindCriteria::newInstance()
1064						->addCondition('id', 'e.exception_event_id', '=', 't', true, true);
1065
1066		$findParams->join(Exception::model()->tableName(), $exceptionJoinCriteria, 'e');
1067
1068//			$dayStart = \GO\Base\Util\Date::clear_time($exceptionDate);
1069//			$dayEnd = \GO\Base\Util\Date::date_add($dayStart,1);
1070		$whereCriteria = \GO\Base\Db\FindCriteria::newInstance()
1071						->addCondition('exception_for_event_id', $this->id)
1072						->addCondition('time', $startOfDay, '>=', 'e')
1073						->addCondition('time', $endOfDay, '<', 'e');
1074		$findParams->criteria($whereCriteria);
1075//		$findParams->getCriteria()
1076//						->addCondition('exception_for_event_id', $this->id)
1077//						->addCondition('start_time', $startOfDay,'>=')
1078//						->addCondition('end_time', $endOfDay,'<=');
1079
1080		$event = Event::model()->findSingle($findParams);
1081
1082		return $event;
1083	}
1084
1085	/**
1086	 *
1087	 * @param int $exception_for_event_id
1088	 * @return LocalEvent
1089	 */
1090	public function getConflictingEvents($exception_for_event_id=0){
1091
1092		$conflictEvents=array();
1093
1094		$settings = Settings::model()->getDefault(GO::user());
1095		if(!$settings->check_conflict) {
1096			return $conflictEvents;
1097		}
1098
1099		$findParams = \GO\Base\Db\FindParams::newInstance();
1100		$findParams->getCriteria()->addCondition("calendar_id", $this->calendar_id);
1101		if(!$this->isNew)
1102			$findParams->getCriteria()->addCondition("resource_event_id", $this->id, '<>');
1103
1104		//find all events including repeating events that occur on that day.
1105		$conflictingEvents = Event::model()->findCalculatedForPeriod($findParams,
1106						$this->start_time,
1107						$this->end_time,
1108						true);
1109
1110		while($conflictEvent = array_shift($conflictingEvents)) {
1111			//\GO::debug("Conflict: ".$conflictEvent->getEvent()->id." ".$conflictEvent->getName()." ".\GO\Base\Util\Date::get_timestamp($conflictEvent->getAlternateStartTime())." - ".\GO\Base\Util\Date::get_timestamp($conflictEvent->getAlternateEndTime()));
1112			if($conflictEvent->getEvent()->id!=$this->id && (empty($exception_for_event_id) || $exception_for_event_id!=$conflictEvent->getEvent()->id)){
1113				$conflictEvents[]=$conflictEvent;
1114			}
1115		}
1116
1117		return $conflictEvents;
1118	}
1119
1120	/**
1121	 * Find events that occur in a given time period. They will be sorted on
1122	 * start_time and name. Recurring events are calculated and added to the array.
1123	 *
1124	 * @param \GO\Base\Db\FindParams $findParams
1125	 * @param int $periodStartTime
1126	 * @param int $periodEndTime
1127	 * @param boolean $onlyBusyEvents
1128	 *
1129	 * @return LocalEvent
1130	 */
1131	public function findCalculatedForPeriod($findParams, $periodStartTime, $periodEndTime, $onlyBusyEvents=false) {
1132
1133		$stmt = $this->findForPeriod($findParams, $periodStartTime, $periodEndTime, $onlyBusyEvents);
1134
1135		$this->_calculatedEvents = array();
1136
1137		while ($event = $stmt->fetch()) {
1138			$this->_calculateRecurrences($event, $periodStartTime, $periodEndTime);
1139		}
1140
1141		ksort($this->_calculatedEvents);
1142
1143		return array_values($this->_calculatedEvents);
1144	}
1145
1146	/**
1147	 * Find events that occur in a given time period.
1148	 *
1149	 * Recurring events are not calculated. If you need recurring events use
1150	 * findCalculatedForPeriod.
1151	 *
1152	 * @param \GO\Base\Db\FindParams $findParams extra findparmas
1153	 * @param int $periodStartTime Start time as Unix timestamp
1154	 * @param int $periodEndTime Latest start time for the selected event as Unix timestamp
1155	 * @param \GO\Base\Db\FindParams $findParams
1156
1157	 * @param boolean $onlyBusyEvents
1158	 * @return \GO\Base\Db\ActiveStatement
1159	 */
1160	public function findForPeriod($findParams, $periodStartTime, $periodEndTime=0, $onlyBusyEvents=false){
1161		if (!$findParams)
1162			$findParams = \GO\Base\Db\FindParams::newInstance();
1163
1164		// Make regular events exportable.
1165		$findParams->export('events');
1166
1167		$findParams->select("t.*");
1168
1169//		if($periodEndTime)
1170//			$findParams->getCriteria()->addCondition('start_time', $periodEndTime, '<');
1171
1172		$findParams->getCriteria()->addModel(Event::model(), "t");
1173
1174		if ($onlyBusyEvents)
1175			$findParams->getCriteria()->addCondition('busy', 1);
1176
1177		$normalEventsCriteria = \GO\Base\Db\FindCriteria::newInstance()
1178					->addModel(Event::model())
1179					->addCondition('end_time', $periodStartTime, '>');
1180
1181		if($periodEndTime)
1182			$normalEventsCriteria->addCondition('start_time', $periodEndTime, '<');
1183
1184		$recurringEventsCriteria = \GO\Base\Db\FindCriteria::newInstance()
1185					->addModel(Event::model())
1186					->addCondition('rrule', "", '!=')
1187					->mergeWith(
1188									\GO\Base\Db\FindCriteria::newInstance()
1189										->addModel(Event::model())
1190//										->addCondition('repeat_end_time', $periodStartTime, '>=')
1191										->addRawCondition('`t`.`repeat_end_time`', '('.intval($periodStartTime).'-(`t`.`end_time`-`t`.`start_time`))', '>=', true)
1192										->addCondition('repeat_end_time', 0,'=','t',false))
1193					->addCondition('start_time', $periodStartTime, '<');
1194
1195		$normalEventsCriteria->mergeWith($recurringEventsCriteria, false);
1196
1197		$findParams->getCriteria()->mergeWith($normalEventsCriteria);
1198
1199
1200
1201		return $this->find($findParams);
1202	}
1203
1204	private function _calculateRecurrences($event, $periodStartTime, $periodEndTime) {
1205
1206		$origPeriodStartTime=$periodStartTime;
1207		$origPeriodEndTime=$periodEndTime;
1208
1209		//recurrences can only be calculated correctly if we use the start of the day and the end of the day.
1210		//we'll use the original times later to check if they really overlap.
1211		$periodStartTime= \GO\Base\Util\Date::clear_time($periodStartTime)-1;
1212		$periodEndTime= \GO\Base\Util\Date::clear_time(\GO\Base\Util\Date::date_add($periodEndTime,1));
1213
1214		$localEvent = new LocalEvent($event, $origPeriodStartTime, $origPeriodEndTime);
1215
1216		if(!$localEvent->isRepeating()){
1217			$this->_calculatedEvents[$event->start_time.'-'.$event->name.'-'.$event->id] = $localEvent;
1218		} else {
1219
1220			//$rrule = new \GO\Base\Util\Icalendar\Rrule();
1221			try{
1222				$startDateTime = new \DateTime('@'.$localEvent->getEvent()->start_time, new \DateTimeZone($localEvent->getEvent()->timezone));
1223				$startDateTime->setTimezone(new \DateTimeZone($localEvent->getEvent()->timezone)); //iterate in local timezone for DST issues
1224				$rrule = new \GO\Base\Util\Icalendar\RRuleIterator($localEvent->getEvent()->rrule, $startDateTime);
1225				$rrule->fastForward(new \DateTime('@'.\GO\Base\Util\Date::date_add($periodStartTime,-ceil(( ($event->end_time-$event->start_time) /86400)))));
1226			}catch(\Exception $e) {
1227
1228				GO::debug($e->getMessage()." Event ID:".$event->id);
1229
1230//					trigger_error($e->getMessage()." Event ID:".$event->id." Cleared rrule!");
1231				return;
1232			}
1233			$origEventAttr = $localEvent->getEvent()->getAttributes('formatted');
1234			while ($occurenceStartTime = $rrule->nextRecurrence($periodEndTime)) {
1235
1236//				var_dump(\GO\Base\Util\Date::get_timestamp($occurenceStartTime));
1237
1238				if ($occurenceStartTime > $localEvent->getPeriodEndTime())
1239					break;
1240
1241				$localEvent->setAlternateStartTime($occurenceStartTime);
1242
1243				$diff = $event->getDiff();
1244
1245				$endTime = new \GO\Base\Util\Date\DateTime('@'.$occurenceStartTime);
1246				$endTime->setTimezone(new \DateTimeZone($localEvent->getEvent()->timezone));
1247				$endTime->add($diff);
1248
1249				$localEvent->setAlternateEndTime($endTime->format('U'));
1250
1251				if($localEvent->getAlternateStartTime()<$origPeriodEndTime && $localEvent->getAlternateEndTime()>$origPeriodStartTime){
1252					if(!$event->hasException($occurenceStartTime))
1253						$this->_calculatedEvents[$occurenceStartTime.'-'.$origEventAttr['name'].'-'.$origEventAttr['id']] = $localEvent;
1254				}
1255
1256				$localEvent = new LocalEvent($event, $periodStartTime, $periodEndTime);
1257			}
1258		}
1259
1260	}
1261
1262	/**
1263	 * Check if this event has an exception for a given day.
1264	 *
1265	 * @param int $time
1266	 * @return Exception
1267	 */
1268	public function hasException($time){
1269		$startDay = \GO\Base\Util\Date::clear_time($time);
1270		$endDay = \GO\Base\Util\Date::date_add($startDay, 1);
1271
1272		$findParams = \GO\Base\Db\FindParams::newInstance();
1273		$findParams->getCriteria()
1274						->addCondition('event_id', $this->id)
1275						->addCondition('time', $startDay,'>=')
1276						->addCondition('time', $endDay, '<');
1277
1278		return Exception::model()->findSingle($findParams);
1279
1280	}
1281
1282	/**
1283	 * Create a localEvent model from this event model
1284	 *
1285	 * @param Event $event
1286	 * @param StringHelper $periodStartTime
1287	 * @param StringHelper $periodEndTime
1288	 * @return LocalEvent
1289	 */
1290	public function getLocalEvent($event, $periodStartTime, $periodEndTime){
1291		$localEvent = new LocalEvent($event, $periodStartTime, $periodEndTime);
1292
1293		return $localEvent;
1294	}
1295
1296	/**
1297	 * Find an event based on uuid field for a user. Either user_id or calendar_id
1298	 * must be supplied.
1299	 *
1300	 * Optionally exceptionDate can be specified to find a specific exception.
1301	 *
1302	 * @param StringHelper $uuid
1303	 * @param int $user_id
1304	 * @param int $calendar_id
1305	 * @param int $exceptionDate
1306	 * @return Event
1307	 */
1308	public function findByUuid($uuid, $user_id, $calendar_id=0, $exceptionDate=false){
1309
1310		$whereCriteria = \GO\Base\Db\FindCriteria::newInstance()
1311										->addCondition('uuid', $uuid);
1312
1313		$params = \GO\Base\Db\FindParams::newInstance()
1314						->ignoreAcl()
1315						->single();
1316
1317		if(!$calendar_id){
1318			$joinCriteria = \GO\Base\Db\FindCriteria::newInstance()
1319							->addCondition('calendar_id', 'c.id','=','t',true, true)
1320							->addCondition('user_id', $user_id,'=','c');
1321
1322			$params->join(Calendar::model()->tableName(), $joinCriteria, 'c');
1323		}else
1324		{
1325			$whereCriteria->addCondition('calendar_id', $calendar_id);
1326		}
1327
1328		if($exceptionDate){
1329			//must be an exception and start on the must start on the exceptionTime
1330			$exceptionJoinCriteria = \GO\Base\Db\FindCriteria::newInstance()
1331							->addCondition('id', 'e.exception_event_id','=','t',true,true);
1332
1333			$params->join(Exception::model()->tableName(),$exceptionJoinCriteria,'e');
1334
1335			$dayStart = \GO\Base\Util\Date::clear_time($exceptionDate);
1336			$dayEnd = \GO\Base\Util\Date::date_add($dayStart,1);
1337
1338			$dateCriteria = \GO\Base\Db\FindCriteria::newInstance()
1339							->addCondition('time', $dayStart, '>=','e')
1340							->addCondition('time', $dayEnd, '<','e');
1341
1342			$whereCriteria->mergeWith($dateCriteria);
1343
1344//			//the code below only find exceptions on the same day which is wrong
1345//			$whereCriteria->addCondition('exception_for_event_id', 0,'>');
1346//
1347//			$dayStart = \GO\Base\Util\Date::clear_time($exceptionDate);
1348//			$dayEnd = \GO\Base\Util\Date::date_add($dayStart,1);
1349//
1350//			$dateCriteria = \GO\Base\Db\FindCriteria::newInstance()
1351//							->addCondition('start_time', $dayStart, '>=')
1352//							->addCondition('start_time', $dayEnd, '<','t',false);
1353//
1354//			$whereCriteria->mergeWith($dateCriteria);
1355
1356		}else
1357		{
1358			$whereCriteria->addCondition('exception_for_event_id', 0);
1359		}
1360
1361		$params->criteria($whereCriteria);
1362
1363		return $this->find($params);
1364	}
1365
1366//	/**
1367//	 * Find an event that belongs to a group of participant events. They all share the same uuid field.
1368//	 *
1369//	 * @param int $calendar_id
1370//	 * @param string $uuid
1371//	 * @return Event
1372//	 */
1373//	public function findParticipantEvent($calendar_id, $uuid) {
1374//		return $this->findSingleByAttributes(array('uuid' => $event->uuid, 'calendar_id' => $calendar->id));
1375//	}
1376
1377	/**
1378	 * Find the resource booking that belongs to this event
1379	 *
1380	 * @param int $event_id
1381	 * @param int $resource_calendar_id
1382	 * @return Event
1383	 */
1384	public function findResourceForEvent($event_id, $resource_calendar_id){
1385		return $this->findSingleByAttributes(array('resource_event_id' => $event_id, 'calendar_id' => $resource_calendar_id));
1386	}
1387
1388	/**
1389	 * Get the status translated into the current language setting
1390	 * @return StringHelper
1391	 */
1392	public function getLocalizedStatus(){
1393		$statuses = \GO::t("statuses", "calendar");
1394
1395		return isset($statuses[$this->status]) ? $statuses[$this->status] : $this->status;
1396
1397	}
1398
1399	/**
1400	 * Get the event in HTML markup
1401	 *
1402	 * @todo Add recurrence info
1403	 * @return StringHelper
1404	 */
1405	public function toHtml() {
1406		$html = '<table id="event-'.$this->uuid.'">' .
1407						'<tr><td>' . \GO::t("Subject", "calendar") . ':</td>' .
1408						'<td>' . $this->name . '</td></tr>';
1409
1410		if($this->calendar){
1411			$html .= '<tr><td>' . \GO::t("Calendar", "calendar") . ':</td>' .
1412						'<td>' . $this->calendar->name . '</td></tr>';
1413		}
1414
1415		$html .= '<tr><td>' . \GO::t("Starts at", "calendar") . ':</td>' .
1416						'<td>' . \GO\Base\Util\Date::get_timestamp($this->start_time, empty($this->all_day_event)) . '</td></tr>' .
1417						'<tr><td>' . \GO::t("Ends at", "calendar") . ':</td>' .
1418						'<td>' . \GO\Base\Util\Date::get_timestamp($this->end_time, empty($this->all_day_event)) . '</td></tr>';
1419
1420		$html .= '<tr><td>' . \GO::t("Status", "calendar") . ':</td>' .
1421						'<td>' . $this->getLocalizedStatus() . '</td></tr>';
1422
1423
1424		if (!empty($this->location)) {
1425			$html .= '<tr><td style="vertical-align:top">' . \GO::t("Location", "calendar") . ':</td>' .
1426							'<td>' . \GO\Base\Util\StringHelper::text_to_html($this->location) . '</td></tr>';
1427		}
1428
1429		if(!empty($this->description)){
1430			$html .= '<tr><td style="vertical-align:top">' . \GO::t("Description") . ':</td>' .
1431							'<td>' . \GO\Base\Util\StringHelper::text_to_html($this->description) . '</td></tr>';
1432		}
1433
1434		if($this->isRecurring()){
1435			$html .= '<tr><td colspan="2">' .$this->getRecurrencePattern()->getAsText().'</td></tr>';;
1436		}
1437
1438		//don't calculate timezone offset for all day events
1439//		$timezone_offset_string = \GO\Base\Util\Date::get_timezone_offset($this->start_time);
1440//
1441//		if ($timezone_offset_string > 0) {
1442//			$gmt_string = '(\G\M\T +' . $timezone_offset_string . ')';
1443//		} elseif ($timezone_offset_string < 0) {
1444//			$gmt_string = '(\G\M\T -' . $timezone_offset_string . ')';
1445//		} else {
1446//			$gmt_string = '(\G\M\T)';
1447//		}
1448
1449		//$html .= '<tr><td colspan="2">&nbsp;</td></tr>';
1450
1451		$cfRecord = $this->getCustomFields()->returnAsText(true)->toArray();
1452
1453		if (!empty($cfRecord)) {
1454		$fieldsets = \go\core\model\FieldSet::find()->filter(['entities' => ['Event']]);
1455
1456			foreach($fieldsets as $fieldset) {
1457				$html .= '<tr><td colspan="2"><b>'.($fieldset->name).'</td></tr>';
1458
1459				$fields = \go\core\model\Field::find()->where(['fieldSetId' => $fieldset->id]);
1460
1461				foreach($fields as $field) {
1462
1463					if(empty($cfRecord[$field->databaseName])) {
1464						continue;
1465					}
1466
1467					$html .= '<tr><td style="vertical-align:top">'.($field->name).'</td>'.
1468										'<td>'.$cfRecord[$field->databaseName].'</td></tr>';
1469				}
1470			}
1471		}
1472
1473		$html .= '</table>';
1474
1475		$stmt = $this->participants();
1476
1477		if($stmt->rowCount()){
1478
1479			$html .= '<table>';
1480
1481			$html .= '<tr><td colspan="3"><br /></td></tr>';
1482			$html .= '<tr><td><b>'.\GO::t("Participant", "calendar").'</b></td><td><b>'.\GO::t("Status", "calendar").'</b></td><td><b>'.\GO::t("Organizer", "calendar").'</b></td></tr>';
1483			while($participant = $stmt->fetch()){
1484				$html .= '<tr><td>'.$participant->name.'&nbsp;</td><td>'.$participant->statusName.'&nbsp;</td><td>'.($participant->is_organizer ? \GO::t("Yes") : '').'</td></tr>';
1485			}
1486			$html .='</table>';
1487		}
1488
1489
1490		return $html;
1491	}
1492
1493	/**
1494	 * Get the recurrence pattern object
1495	 *
1496	 * @return \GO\Base\Util\Icalendar\Rrule
1497	 */
1498	public function getRecurrencePattern(){
1499
1500		if(!$this->isRecurring())
1501			return false;
1502
1503		$startDateTime = new \DateTime('@'.$this->start_time, new \DateTimeZone($this->timezone));
1504		$startDateTime->setTimezone(new \DateTimeZone($this->timezone)); //iterate in local timezone for DST issues
1505		$rRule = new \GO\Base\Util\Icalendar\RRuleIterator($this->rrule, $startDateTime);
1506
1507		return $rRule;
1508	}
1509
1510
1511	/**
1512	 * Get this event as a VObject. This can be turned into a vcalendar file data.
1513	 *
1514	 * @param StringHelper $method REQUEST, REPLY or CANCEL
1515	 * @param Participant $updateByParticipant The participant that is generating this ICS for a response.
1516	 * @param int $recurrenceTime Export for a specific recurrence time for the recurrence-id.
1517	 * @param boolean $includeExdatesForMovedEvents Funambol need EXDATE lines even for appointments that have been moved. CalDAV doesn't need those lines.
1518	 *
1519	 * If this event is an occurence and has a exception_for_event_id it will automatically determine this value.
1520	 * This option is only useful for cancelling a single occurence. Because in that case there is no event model for the occurrence. There's just an exception.
1521	 *
1522	 * @return Sabre\VObject\Component
1523	 */
1524	public function toVObject($method='REQUEST', $updateByParticipant=false, $recurrenceTime=false,$includeExdatesForMovedEvents=false){
1525
1526		$calendar = new Sabre\VObject\Component\VCalendar();
1527
1528		$e=$calendar->createComponent('VEVENT');
1529
1530		if(empty($this->uuid)){
1531			$this->uuid = \GO\Base\Util\UUID::create('event', $this->id);
1532			$this->save(true);
1533		}
1534
1535		$e->uid=$this->uuid;
1536
1537		if(isset($this->sequence))
1538			$e->sequence=$this->sequence;
1539
1540
1541		$mtimeDateTime = new \DateTime('@'.$this->mtime);
1542		$mtimeDateTime->setTimezone(new \DateTimeZone('UTC'));
1543		$e->add('LAST-MODIFIED', $mtimeDateTime);
1544
1545		$ctimeDateTime = new \DateTime('@'.$this->mtime);
1546		$ctimeDateTime->setTimezone(new \DateTimeZone('UTC'));
1547		$e->add('created', $ctimeDateTime);
1548
1549    $e->summary = (string) $this->name;
1550
1551		if($this->status == "NEEDS-ACTION"){
1552			$e->status = "TENTATIVE";
1553		}else{
1554			$e->status = $this->status;
1555		}
1556
1557
1558		$dateType = $this->all_day_event ? "DATE" : "DATETIME";
1559
1560//		if($this->all_day_event){
1561//			$e->{"X-FUNAMBOL-ALLDAY"}=1;
1562//		}
1563
1564		if($this->exception_for_event_id>0){
1565			//this is an exception
1566
1567			$exception = $this->recurringEventException(); //get master event from relation
1568			if($exception){
1569				$recurrenceTime=$exception->getStartTime();
1570			}
1571		}
1572		if($recurrenceTime){
1573			$dt = \GO\Base\Util\Date\DateTime::fromUnixtime($recurrenceTime);
1574			$rId = $e->add('recurrence-id', $dt);
1575			if($this->_exceptionEvent->all_day_event){
1576				$rId['VALUE']='DATE';
1577			}
1578		}
1579
1580
1581		$dtstart = $e->add('dtstart', \GO\Base\Util\Date\DateTime::fromUnixtime($this->start_time));
1582		if($this->all_day_event){
1583			$dtstart['VALUE'] = 'DATE';
1584		}
1585
1586		if($this->all_day_event){
1587			$end_time = \GO\Base\Util\Date::clear_time($this->end_time);
1588			$end_time = \GO\Base\Util\Date::date_add($end_time,1);
1589		}else{
1590			$end_time = $this->end_time;
1591		}
1592
1593		$dtend = $e->add('dtend', \GO\Base\Util\Date\DateTime::fromUnixtime($end_time));
1594
1595		if($this->all_day_event){
1596			$dtend['VALUE'] = 'DATE';
1597		}
1598
1599		if(!empty($this->description))
1600			$e->description=$this->description;
1601
1602		if(!empty($this->location))
1603			$e->location=$this->location;
1604
1605		$rrule = str_replace('RRULE:','',$this->rrule);
1606		if(!empty($rrule)){
1607
1608//			$rRule = $this->getRecurrencePattern();
1609//			$rRule->shiftDays(false);
1610			$e->add('rrule',$rrule);
1611
1612			$findParams = \GO\Base\Db\FindParams::newInstance();
1613
1614			if(!$includeExdatesForMovedEvents)
1615				$findParams->getCriteria()->addCondition('exception_event_id', 0);
1616
1617			$stmt = $this->exceptions($findParams);
1618			while($exception = $stmt->fetch()){
1619				$dt = \GO\Base\Util\Date\DateTime::fromUnixtime($exception->getStartTime());
1620				$exdate = $e->add('exdate',$dt);
1621				if($this->all_day_event){
1622					$exdate['VALUE'] = 'DATE';
1623				}
1624
1625			}
1626		}
1627
1628
1629		$stmt = $this->participants();
1630		while($participant=$stmt->fetch()){
1631
1632			if($participant->is_organizer || $method=='REQUEST' || ($updateByParticipant && $updateByParticipant->id==$participant->id)){
1633				//If this is a meeting REQUEST then we must send all participants.
1634				//For a CANCEL or REPLY we must send the organizer and the current user.
1635				$e->add($participant->is_organizer ? 'organizer' : 'attendee', 'mailto:'.$participant->email, array(
1636						'cn'=>$participant->name,
1637						'rsvp'=>'true',
1638						'partstat'=>$this->_exportVObjectStatus($participant->status)
1639				));
1640			}
1641		}
1642
1643		if($this->category){
1644			$e->categories=$this->category->name;
1645		}
1646
1647
1648
1649
1650		if($this->reminder !== null){
1651
1652			$a=$calendar->createComponent('VALARM');
1653//			BEGIN:VALARM
1654//ACTION:DISPLAY
1655//TRIGGER;VALUE=DURATION:-PT5M
1656//DESCRIPTION:Default Mozilla Description
1657//END:VALARM
1658
1659			$a->action='DISPLAY';
1660
1661			if(empty($this->reminder)){
1662				$a->add('trigger','P0D', array('value'=>'DURATION'));
1663			} else {
1664				$a->add('trigger','-PT'.($this->reminder/60).'M', array('value'=>'DURATION'));
1665			}
1666
1667			$a->description="Alarm";
1668
1669
1670			//for funambol compatibility, the \GO\Base\VObject\Reader class use this to convert it to a vcalendar 1.0 aalarm tag.
1671			$e->{"X-GO-REMINDER-TIME"}=date('Ymd\THis', $this->start_time-$this->reminder);
1672			$e->add($a);
1673		}
1674
1675
1676		if($this->private) {
1677			$e->class='PRIVATE';
1678		}
1679
1680
1681		return $e;
1682	}
1683
1684
1685
1686	/**
1687	 * Get vcalendar data for an *.ics file.
1688	 *
1689	 * @param StringHelper $method REQUEST, REPLY or CANCEL
1690	 * @param Participant $updateByParticipant The participant that is generating this ICS for a response.
1691	 * @param int $recurrenceTime Export for a specific recurrence time for the recurrence-id.
1692	 * If this event is an occurence and has a exception_for_event_id it will automatically determine this value.
1693	 * This option is only useful for cancelling a single occurence. Because in that case there is no event model for the occurrence. There's just an exception.
1694	 *
1695	 * Set this to a unix timestamp of the start of an occurence if it's an update
1696	 * for a particular recurrence date.
1697	 *
1698	 * @return type
1699	 */
1700
1701	public function toICS($method='REQUEST', $updateByParticipant=false, $recurrenceTime=false) {
1702
1703		$c = new \GO\Base\VObject\VCalendar();
1704		$c->method=$method;
1705
1706		$c->add(new \GO\Base\VObject\VTimezone());
1707
1708		$c->add($this->toVObject($method, $updateByParticipant, $recurrenceTime));
1709		return $c->serialize();
1710	}
1711
1712	public function toVCS(){
1713		$c = new \GO\Base\VObject\VCalendar();
1714		$vobject = $this->toVObject('',false,false,true);
1715		$c->add($vobject);
1716
1717		\GO\Base\VObject\Reader::convertICalendarToVCalendar($c);
1718
1719		return $c->serialize();
1720	}
1721
1722	/**
1723	 * Check if this event is a resource booking;
1724	 *
1725	 * @return boolean
1726	 */
1727	public function isResource(){
1728		return $this->calendar && $this->calendar->group_id>1;
1729	}
1730
1731
1732	public $importedParticiants=array();
1733
1734
1735	private function _utcToLocal($date){
1736		//DateTime from SabreDav is date without time in UTC timezone. We store it in the users timezone so we must
1737		//add the timezone offset.
1738		$timezone = new DateTimeZone(\GO::user()->timezone);
1739
1740		$offset = $timezone->getOffset($date);
1741
1742\GO::debug("Offset: ".$offset);
1743
1744$sub = $offset>0;
1745		if(!$sub)
1746			$offset *= -1;
1747
1748		$interval = new DateInterval('PT'.$offset.'S');
1749		if(!$sub){
1750			$date->add($interval);
1751		}else{
1752			$date->sub($interval);
1753
1754		}
1755	}
1756
1757
1758	/**
1759	 * Import an event from a VObject
1760	 *
1761	 * @param Sabre\VObject\Component $vobject
1762	 * @param array $attributes Extra attributes to apply to the event. Raw values should be past. No input formatting is applied.
1763	 * @param boolean $dontSave. Don't save the event. WARNING. Event can't be fully imported this way because participants and exceptions need an ID. This option is useful if you want to display info about an ICS file.
1764	 * @param boolean $importExternal This should be switched on if importing happens from external ICS calendar.
1765	 * @return Event
1766	 */
1767	public function importVObject(Sabre\VObject\Component $vobject, $attributes=array(), $dontSave=false, $makeSureUserParticipantExists=false, $importExternal=false, $withCategories = true){
1768
1769		$uid = (string) $vobject->uid;
1770		if(!empty($uid))
1771			$this->uuid = $uid;
1772
1773		$this->name = (string) $vobject->summary;
1774		if(empty($this->name))
1775			$this->name = \GO::t("Unnamed");
1776
1777		$dtstart = $vobject->dtstart ? $vobject->dtstart->getDateTime() : new \DateTime();
1778		$dtend = $vobject->dtend ? $vobject->dtend->getDateTime() : new \DateTime();
1779
1780
1781		$this->all_day_event = isset($vobject->dtstart['VALUE']) && $vobject->dtstart['VALUE']=='DATE' ? 1 : 0;
1782
1783		//turn DateTimeImmutable into DateTime
1784		$dtstart = new \DateTime($dtstart->format('Y-m-d H:i'), $this->all_day_event ? null : $dtstart->getTimezone());
1785		$dtend = new \DateTime($dtend->format('Y-m-d H:i'), $this->all_day_event ? null : $dtend->getTimezone());
1786
1787
1788		//ios sends start and end date at 00:00 hour
1789		//DTEND;TZID=Europe/Amsterdam:20140121T000000
1790		//DTSTART;TZID=Europe/Amsterdam:20140120T000000
1791
1792		if($dtstart->format('Hi') == "0000" && $dtend->format('Hi') == "0000" ){
1793			$this->all_day_event=true;
1794		}
1795
1796		if($this->all_day_event){
1797// This broke all day events in thunderbird. It messed up the times.
1798//			if($dtstart->getTimezone()->getName()=='UTC' || $dtend->getTimezone()->getName()=='UTC'){
1799//				$this->timezone = 'UTC';
1800//			}
1801
1802			$this->start_time = \GO\Base\Util\Date::clear_time($dtstart->format('U'));
1803			$this->end_time = \GO\Base\Util\Date::clear_time($dtend->format('U')) - 60;
1804
1805		}else
1806		{
1807			$this->start_time =intval($dtstart->format('U'));
1808			$this->end_time = intval($dtend->format('U'));
1809		}
1810
1811
1812		if($vobject->duration){
1813			$duration = \GO\Base\VObject\Reader::parseDuration($vobject->duration);
1814			$this->end_time = $this->start_time+$duration;
1815		}
1816		if($this->end_time<=$this->start_time)
1817			$this->end_time=$this->start_time+3600;
1818
1819
1820		if($vobject->description)
1821			$this->description = (string) $vobject->description;
1822
1823
1824		if((string) $vobject->rrule != ""){
1825			// Use the RRULe as provided by the iCalendar object
1826			// When using RRuleIterator that implements all rules
1827			$this->rrule = (string) $vobject->rrule;
1828			$this->repeat_end_time = !empty($vobject->rrule->until) ? $vobject->rrule->until : null;
1829		}else
1830		{
1831			$this->rrule="";
1832			$this->repeat_end_time = 0;
1833		}
1834
1835		if($vobject->{"last-modified"})
1836			$this->mtime=intval($vobject->{"last-modified"}->getDateTime()->format('U'));
1837
1838		if($vobject->location)
1839			$this->location=(string) $vobject->location;
1840
1841		//var_dump($vobject->status);
1842		if($vobject->status){
1843			$status = (string) $vobject->status;
1844			if($this->isValidStatus($status))
1845				$this->status=$status;
1846		}
1847
1848		if(isset($vobject->class)){
1849			$this->private = strtoupper($vobject->class)!='PUBLIC';
1850		}
1851
1852		$this->reminder=null;
1853
1854//		if($vobject->valarm && $vobject->valarm->trigger){
1855//
1856//			$type = (string) $vobject->valarm->trigger["value"];
1857//
1858//
1859//			if($type == "DURATION") {
1860//				$duration = \GO\Base\VObject\Reader::parseDuration($vobject->valarm->trigger);
1861//				if($duration>0){
1862//					$this->reminder = $duration*-1;
1863//				}
1864//			}else
1865//			{
1866//				\GO::debug("WARNING: Ignoring unsupported reminder value of type: ".$type);
1867//			}
1868//
1869		// if($vobject->valarm && $vobject->valarm->trigger) {
1870		// 	$date = false;
1871		// 	try {
1872		// 		$date = $vobject->valarm->getEffectiveTriggerTime();
1873		// 	}
1874		// 	catch(\Exception $e) {
1875		// 		//invalid trigger.
1876		// 	}
1877		// 	if($date) {
1878		// 		if($this->all_day_event)
1879		// 			$this->_utcToLocal($date);
1880		// 		$this->reminder = $this->start_time-$date->format('U');
1881		// 	}
1882		// }elseif($vobject->aalarm){ //funambol sends old vcalendar 1.0 format
1883		// 	$aalarm = explode(';', (string) $vobject->aalarm);
1884		// 	if(!empty($aalarm[0])) {
1885		// 		$p = Sabre\VObject\DateTimeParser::parse($aalarm[0]);
1886		// 		$this->reminder = $this->start_time-$p->format('U');
1887		// 	}
1888		// }
1889
1890		$this->setAttributes($attributes, false);
1891
1892		$recurrenceIds = $vobject->select('recurrence-id');
1893		if(count($recurrenceIds)){
1894
1895			//this is a single instance of a recurring series.
1896			//attempt to find the exception of the recurring series event by uuid
1897			//and recurrence time so we can set the relation cal_exceptions.exception_event_id=cal_events.id
1898
1899			$firstMatch = array_shift($recurrenceIds);
1900			$recurrenceTime=$firstMatch->getDateTime()->format('U');
1901
1902			$whereCriteria = \GO\Base\Db\FindCriteria::newInstance()
1903							->addCondition('calendar_id', $this->calendar_id,'=','ev')
1904							->addCondition('uuid', $this->uuid,'=','ev')
1905							->addCondition('time', $recurrenceTime,'=','t');
1906
1907			$joinCriteria = \GO\Base\Db\FindCriteria::newInstance()
1908							->addCondition('event_id', 'ev.id','=','t',true, true);
1909
1910
1911			$findParams = \GO\Base\Db\FindParams::newInstance()
1912							->single()
1913							->criteria($whereCriteria)
1914							->join(Event::model()->tableName(),$joinCriteria,'ev');
1915
1916			$exception = Exception::model()->find($findParams);
1917			if($exception){
1918
1919
1920				$this->exception_for_event_id=$exception->event_id;
1921				if (empty($this->name) || $this->name==\GO::t("Unnamed"))
1922					$this->name = $exception->mainevent->name;
1923			}else
1924			{
1925
1926
1927				//exception was not found for this recurrence. Find the recurring series and add the exception.
1928				$recurringEvent = Event::model()->findByUuid($this->uuid, 0, $this->calendar_id);
1929				if($recurringEvent){
1930
1931					\GO::debug("Creating MISSING exception for ".date('c', $recurrenceTime));
1932					//aftersave will create Exception
1933					$this->exception_for_event_id=$recurringEvent->id;
1934
1935					//will be saved later
1936					$exception = new Exception();
1937					$exception->time=$recurrenceTime;
1938					$exception->event_id=$recurringEvent->id;
1939					if (empty($this->name) || $this->name==\GO::t("Unnamed"))
1940						$this->name = $exception->mainevent->name;
1941				}else
1942				{
1943					//ignore this because the invited participant might not be invited to the series
1944					//throw new \Exception("Could not find master event!");
1945
1946					//hack to make it be seen as an exception
1947					$this->exception_for_event_id = -1;
1948				}
1949			}
1950		}
1951
1952		if($vobject->valarm && $vobject->valarm->trigger){
1953
1954			$reminderTime = false;
1955			try {
1956				$reminderTime = $vobject->valarm->getEffectiveTriggerTime();
1957			}
1958			catch(\Exception $e) {
1959				//invalid trigger.
1960			}
1961
1962			if($reminderTime) {
1963				//echo $reminderTime->format('c');
1964				if($this->all_day_event)
1965					$this->_utcToLocal($reminderTime);
1966				$seconds = $reminderTime->format('U');
1967				$this->reminder = $this->start_time-$seconds;
1968				if($this->reminder<0)
1969					$this->reminder=0;
1970
1971			}
1972		}elseif($vobject->aalarm){ //funambol sends old vcalendar 1.0 format
1973			$aalarm = explode(';', (string) $vobject->aalarm);
1974			if(!empty($aalarm[0])) {
1975				$p = Sabre\VObject\DateTimeParser::parse($aalarm[0]);
1976				$this->reminder = $this->start_time-$p->format('U');
1977			}
1978		}
1979
1980		if($withCategories) {
1981			$cats = (string) $vobject->categories;
1982			if(!empty($cats)){
1983				//Group-Office only supports a single category.
1984				$cats = explode(',',$cats);
1985				$categoryName = array_shift($cats);
1986
1987				$category = Category::model()->findByName($this->calendar_id, $categoryName);
1988				if(!$category && !$dontSave && $this->calendar_id){
1989					$category = new Category();
1990					$category->name=$categoryName;
1991					$category->calendar_id=$this->calendar_id;
1992					$category->save(true);
1993				}
1994
1995				if($category){
1996					$this->category_id=$category->id;
1997					$this->background=$category->color;
1998				}
1999			}
2000		}
2001
2002		//set is_organizer flag
2003		if($vobject->organizer && $this->calendar){
2004			$organizerEmail = str_replace('mailto:','', strtolower((string) $vobject->organizer));
2005			$this->is_organizer=$organizerEmail == $this->calendar->user->email;
2006		}
2007
2008
2009		if(!$dontSave){
2010			$this->cutAttributeLengths();
2011//			try {
2012				$this->_isImport=true;
2013
2014				if (!$importExternal)
2015					$this->setValidationRule('uuid', 'unique', array('calendar_id','start_time', 'exception_for_event_id'));
2016
2017
2018				if(!$this->save()){
2019
2020					if ($importExternal) {
2021						$installationName = !empty(\GO::config()->title) ? \GO::config()->title : 'Group-Office';
2022						$validationErrStr = implode("\n", $this->getValidationErrors())."\n";
2023
2024						$mailSubject = str_replace(array('%cal','%event'),array($this->calendar->name,$this->name),\GO::t("Event not saved in %event calendar \"%cal\"", "calendar"));
2025						$body = \GO::t("This message is from your %goname calendar. %goname attempted to import an event called \"%event\" with start time %starttime from an external calendar into calendar \"%cal\", but that could not be done because the event contained errors. The event may still be in the external calendar.
2026
2027The following is the error message:
2028%errormessage", "calendar");
2029						$body = str_replace(
2030											array('%goname','%event','%starttime','%cal','%errormessage'),
2031											array(
2032												$installationName,
2033												$this->name,
2034												\GO\Base\Util\Date::get_timestamp($this->start_time),
2035												$this->calendar->name,
2036												$validationErrStr
2037											),
2038											$body
2039										);
2040						$message = \GO\Base\Mail\Message::newInstance(
2041														$mailSubject
2042														)->setFrom(go()->getSettings()->systemEmail, go()->getSettings()->title)
2043														->addTo($this->calendar->user->email);
2044
2045						$message->setHtmlAlternateBody(nl2br($body));
2046
2047						if (\GO\Base\Mail\Mailer::newGoInstance()->send($message))
2048							throw new \GO\Base\Exception\Validation('DUE TO ERROR, CRON SENT MAIL TO: '.$this->calendar->user->email.'. THIS IS THE EMAIL MESSAGE:'."\r\n".$body);
2049						else
2050							throw new \GO\Base\Exception\Validation('CRON COULD NOT SEND EMAIL WITH ERROR MESSAGE TO: '.$this->calendar->user->email.'. THIS IS THE EMAIL MESSAGE:'."\r\n".$body);
2051					} else {
2052						throw new \GO\Base\Exception\Validation(implode("\n", $this->getValidationErrors())."\n");
2053					}
2054
2055				}
2056				$this->_isImport=false;
2057//			} catch (\Exception $e) {
2058//				throw new \Exception($this->name.' ['.\GO\Base\Util\Date::get_timestamp($this->start_time).' - '.\GO\Base\Util\Date::get_timestamp($this->end_time).'] '.$e->getMessage());
2059//			}
2060
2061			if(!empty($exception)){
2062				//save the exception we found by recurrence-id
2063				$exception->exception_event_id=$this->id;
2064				$exception->save();
2065
2066				\GO::debug("saved exception");
2067			}
2068
2069
2070
2071			if($vobject->organizer)
2072				$p = $this->importVObjectAttendee($this, $vobject->organizer, true);
2073			else
2074				$p=false;
2075
2076			$calendarParticipantFound=!empty($p) && $p->user_id==$this->calendar->user_id;
2077
2078			$attendees = $vobject->select('attendee');
2079			foreach($attendees as $attendee){
2080				$p = $this->importVObjectAttendee($this, $attendee, false);
2081
2082				if($p->user_id==$this->calendar->user_id){
2083					$calendarParticipantFound=true;
2084				}
2085			}
2086
2087			//if the calendar owner is not in the participants then we should chnage the is_organizer flag because otherwise the event can't be opened or accepted.
2088			if(!$calendarParticipantFound){
2089
2090				if($makeSureUserParticipantExists){
2091					$participant = \GO\Calendar\Model\Participant::model()->findSingleByAttributes(array('event_id'=>$this->id,'email'=>$this->calendar->user->email));
2092
2093					if(!$participant){
2094						//this is a bad situation. The import thould have detected a user for one of the participants.
2095						//It uses the E-mail account aliases to determine a user. See GO_Calendar_Model_Event::importVObject
2096						$participant = new \GO\Calendar\Model\Participant();
2097						$participant->event_id=$this->id;
2098						$participant->user_id=$this->calendar->user_id;
2099						$participant->email=$this->calendar->user->email;
2100					} else {
2101						$participant->user_id=$this->calendar->user_id;
2102					}
2103
2104					$participant->save();
2105				}else
2106				{
2107					$this->is_organizer=true;
2108					$this->save();
2109				}
2110			}
2111
2112			//Add exception dates to Event
2113			foreach($vobject->select('EXDATE') as $i => $exdate) {
2114				try {
2115					$dts = $exdate->getDateTimes();
2116					if($dts === null) {
2117						continue;
2118					}
2119
2120					//TODO this change must be done for every participant event
2121
2122					foreach($dts as $dt) {
2123						$events = $this->getRelatedParticipantEvents(true);
2124						foreach($events as $event) {
2125							$event->addException($dt->format('U'));
2126						}
2127					}
2128				} catch (Exception $e) {
2129					trigger_error($e->getMessage(),E_USER_NOTICE);
2130				}
2131			}
2132
2133			if($importExternal && $this->isRecurring()){
2134				$exceptionEventsStmt = Event::model()->find(
2135					\GO\Base\Db\FindParams::newInstance()->criteria(
2136						\GO\Base\Db\FindCriteria::newInstance()
2137							->addCondition('calendar_id',$this->calendar_id)
2138							->addCondition('uuid',$this->uuid)
2139							->addCondition('rrule','','=')
2140					)
2141				);
2142				foreach ($exceptionEventsStmt as $exceptionEventModel) {
2143					$exceptionEventModel->exception_for_event_id=$this->id;
2144
2145					$exceptionEventModel->save();
2146					//TODO: This method only works when an exception takes place on the same day as the original occurence.
2147					//We should store the RECURRENCE-ID value so we can find it later.
2148					$this->addException($exceptionEventModel->start_time, $exceptionEventModel->id);
2149
2150
2151//					\GO::debug('=== EXCEPTION EVENT === ['.\GO\Base\Util\Date::get_timestamp($exceptionEventModel->start_time).'] '.$exceptionEventModel->name.' (\Exception for event: '.$exceptionEventModel->exception_for_event_id.')');
2152				}
2153			}
2154		}
2155
2156
2157
2158		return $this;
2159	}
2160
2161
2162	public static function eventIsFromCurrentImport(Event $eventModel, $importedEventsArray) {
2163
2164		if (!empty($importedEventsArray))
2165			foreach ($importedEventsArray as $importedEventRecord) {
2166				if ($importedEventRecord['uuid']==$eventModel->uuid && $importedEventRecord['start_time']==$eventModel->start_time)
2167					return true;
2168			}
2169
2170		return false;
2171
2172	}
2173
2174
2175	/**
2176	 * Will import an attendee from a VObject to a given event. If the attendee
2177	 * already exists it will update it.
2178	 *
2179	 * @param Event $event
2180	 * @param Sabre\VObject\Property $vattendee
2181	 * @param boolean $isOrganizer
2182	 * @return Participant
2183	 */
2184	public function importVObjectAttendee(Event $event, Sabre\VObject\Property $vattendee, $isOrganizer=false){
2185
2186		$attributes = $this->_vobjectAttendeeToParticipantAttributes($vattendee);
2187		$attributes['is_organizer']=$isOrganizer;
2188
2189		if($isOrganizer)
2190			$attributes['status']= Participant::STATUS_ACCEPTED;
2191
2192		$p= Participant::model()
2193						->findSingleByAttributes(array('event_id'=>$event->id, 'email'=>$attributes['email']));
2194		if(!$p){
2195			$p = new Participant();
2196			$p->is_organizer=$isOrganizer;
2197			$p->event_id=$event->id;
2198			if(\GO::modules()->email){
2199				$account = \GO\Email\Model\Account::model()->findByEmail($attributes['email']);
2200				if($account)
2201					$p->user_id=$account->user_id;
2202			}
2203
2204			if(!$p->user_id){
2205				$user = \GO\Base\Model\User::model()->findSingleByAttribute('email', $attributes['email']);
2206				if($user)
2207					$p->user_id=$user->id;
2208			}
2209
2210			$p->setAttributes($attributes);
2211		}else
2212		{
2213			//the organizer might be added as a participant too. We don't want to
2214			//import that a second time but we shouldn't update the is_organizer flag if
2215			//we found an existing participant.
2216			//unset($attributes['is_organizer']);
2217			$p->status = $attributes['status'];
2218		}
2219
2220
2221		$p->save();
2222
2223		return $p;
2224	}
2225
2226	private function _vobjectAttendeeToParticipantAttributes(Sabre\VObject\Property $vattendee){
2227		return array(
2228				'name'=>(string) $vattendee['CN'],
2229				'email'=>str_replace('mailto:','', strtolower((string) $vattendee)),
2230				'status'=>$this->_importVObjectStatus((string) $vattendee['PARTSTAT']),
2231				'role'=>(string) $vattendee['ROLE']
2232		);
2233	}
2234
2235	private function _importVObjectStatus($status)
2236	{
2237		$statuses = array(
2238			'NEEDS-ACTION' => Participant::STATUS_PENDING,
2239			'ACCEPTED' => Participant::STATUS_ACCEPTED,
2240			'DECLINED' => Participant::STATUS_DECLINED,
2241			'TENTATIVE' => Participant::STATUS_TENTATIVE
2242		);
2243
2244		return isset($statuses[$status]) ? $statuses[$status] : Participant::STATUS_PENDING;
2245	}
2246	private function _exportVObjectStatus($status)
2247	{
2248		$statuses = array(
2249			Participant::STATUS_PENDING=>'NEEDS-ACTION',
2250			Participant::STATUS_ACCEPTED=>'ACCEPTED',
2251			Participant::STATUS_DECLINED=>'DECLINED',
2252			Participant::STATUS_TENTATIVE=>'TENTATIVE'
2253		);
2254
2255		return isset($statuses[$status]) ? $statuses[$status] : 'NEEDS-ACTION';
2256	}
2257
2258	protected function afterDuplicate(&$duplicate) {
2259
2260		if (!$duplicate->isNew) {
2261
2262			$stmt = $duplicate->participants;
2263
2264			if (!$stmt->rowCount())
2265				$this->duplicateRelation('participants', $duplicate);
2266
2267			if($duplicate->isRecurring() && $this->isRecurring())
2268				$this->duplicateRelation('exceptions', $duplicate);
2269
2270			if($duplicate->is_organizer) {
2271				$this->duplicateRelation('resources', $duplicate, array('status'=>self::STATUS_NEEDS_ACTION));
2272			}
2273		}
2274
2275		return parent::afterDuplicate($duplicate);
2276	}
2277
2278	/**
2279	 * Add a participant to this calendar
2280	 *
2281	 * This function sets the event_id for the participant and saves it.
2282	 *
2283	 * @param Participant $participant
2284	 * @return bool Save of participant is successfull
2285	 */
2286	public function addParticipant($participant){
2287		$participant->event_id = $this->id;
2288		return $participant->save();
2289	}
2290
2291	/**
2292	 *
2293	 * @param Participant $participant
2294	 * @return Event
2295	 */
2296	public function createCopyForParticipant(Participant $participant){
2297//		$calendar = Calendar::model()->getDefault($user);
2298//
2299//		return $this->duplicate(array(
2300//			'user_id'=>$user->id,
2301//			'calendar_id'=>$calendar->id,
2302//			'is_organizer'=>false
2303//		));
2304
2305		\GO::debug("Creating event copy for ".$participant->name);
2306
2307		//create event in participant's default calendar if the current user has the permission to do that
2308		$calendar = $participant->getDefaultCalendar();
2309		if ($calendar && $calendar->userHasCreatePermission()){
2310
2311			//find if an event for this exception already exists.
2312			$exceptionDate = $this->exception_for_event_id!=0 ? $this->start_time : false;
2313			$existing = Event::model()->findByUuid($this->uuid, 0, $calendar->id, $exceptionDate);
2314
2315			if(!$existing){
2316
2317				//ignore acl permissions because we allow users to schedule events directly when they have access through
2318				//the special freebusypermissions module.
2319				$participantEvent = $this->duplicate(array(
2320						'calendar_id' => $calendar->id,
2321						'user_id'=>$participant->user_id,
2322						'is_organizer'=>false,
2323	//					'status'=>  Event::STATUS_NEEDS_ACTION
2324						),
2325								true,true);
2326				return $participantEvent;
2327			}else
2328			{
2329				\GO::debug("Found existing event: ".$existing->id.' - '.$existing->getAttribute('start_time', 'formatted'));
2330
2331
2332				//correct errors that somehow occurred.
2333				$attributes = $this->getAttributeSelection(array('name','start_time','end_time','rrule','repeat_end_time','location','description','private'), 'raw');
2334				$existing->setAttributes($attributes, false);
2335				if($existing->isModified()){
2336					$existing->updatingRelatedEvent=true;
2337					$existing->save(true);
2338				}
2339
2340				return $existing;
2341			}
2342
2343		}
2344		return false;
2345
2346	}
2347
2348	/**
2349	 * Get the default participant model for a new event.
2350	 * The default is the calendar owner except if the owner is admin. In that
2351	 * case it will default to the logged in user.
2352	 *
2353	 * @return \Participant
2354	 */
2355	public function getDefaultOrganizerParticipant(){
2356		$calendar = $this->calendar;
2357
2358		$user = $calendar->user_id==1 || !$calendar->user ? \GO::user() : $calendar->user;
2359
2360		$participant = new Participant();
2361		$participant->event_id=$this->id;
2362		$participant->user_id=$user->id;
2363
2364		$participant->name=$user->name;
2365		$participant->email=$user->email;
2366		$participant->status=Participant::STATUS_ACCEPTED;
2367		$participant->is_organizer=1;
2368
2369		return $participant;
2370	}
2371
2372	/**
2373	 * Get's the organizer's event if this event belongs to a meeting.
2374	 *
2375	 * @return Event
2376	 */
2377	public function getOrganizerEvent(){
2378		if($this->is_organizer)
2379			return false;
2380
2381		return Event::model()->findSingleByAttributes(array('uuid'=>$this->uuid, 'is_organizer'=>1, 'start_time' => $this->start_time));
2382	}
2383
2384	/**
2385	 * Check if this event has other participant then the given user id.
2386	 *
2387	 * @param int|array $user_id
2388	 * @return boolean
2389	 */
2390	public function hasOtherParticipants($user_id=0){
2391
2392		if(empty($user_id))
2393			$user_id=array($this->calendar->user_id,\GO::user()->id);
2394		elseif(!is_array($user_id))
2395			$user_id = array($user_id);
2396
2397		if(empty($this->id))
2398			return false;
2399
2400		$findParams = \GO\Base\Db\FindParams::newInstance()
2401						->single();
2402
2403		$findParams->getCriteria()
2404						->addInCondition('user_id', $user_id,'t', true, true)
2405						->addCondition('event_id', $this->id);
2406
2407
2408		$p = Participant::model()->find($findParams);
2409
2410		return $p ? true : false;
2411	}
2412
2413	/**
2414	 * When checking all Event models make sure there is a UUID if not create one
2415	 */
2416	public function checkDatabase() {
2417
2418	  if(empty($this->uuid))
2419		$this->uuid = \GO\Base\Util\UUID::create('event', $this->id);
2420
2421		//in some cases on old databases the repeat_end_time is set but the UNTIL property in the rrule is not. We correct that here.
2422		if($this->repeat_end_time>0 && strpos($this->rrule,'UNTIL=')===false){
2423			$rrule = new \GO\Base\Util\Icalendar\Rrule();
2424			$rrule->readIcalendarRruleString($this->start_time, $this->rrule);
2425			$rrule->until=$this->repeat_end_time;
2426			$this->rrule= $rrule->createRrule();
2427		}
2428
2429		parent::checkDatabase();
2430	}
2431
2432
2433	/**
2434	 * Get the organizer model of this event
2435	 *
2436	 * @return Participant
2437	 */
2438	public function getOrganizer(){
2439		return Participant::model()->findSingleByAttributes(array(
2440				'is_organizer'=>true,
2441				'event_id'=>$this->id
2442		));
2443	}
2444
2445	private static $aliases = [];
2446
2447	/**
2448	 * Get the participant model where the user matches the calendar user
2449	 *
2450	 * @return Participant
2451	 */
2452	public function getParticipantOfCalendar() {
2453
2454		if(!isset(self::$aliases[$this->calendar->user_id])) {
2455			self::$aliases[$this->calendar->user_id] = \GO\Email\Model\Alias::model()->find(
2456				GO\Base\Db\FindParams::newInstance()
2457					->select('email')
2458					->permissionLevel(GO\Base\Model\Acl::WRITE_PERMISSION, $this->calendar->user_id)
2459					->ignoreAdminGroup()
2460			)->fetchAll(\PDO::FETCH_COLUMN, 0);
2461		}
2462
2463		return Participant::model()->findSingleByAttributes(array(
2464				'email' => self::$aliases[$this->calendar->user_id],
2465				'event_id'=>$this->id
2466		));
2467	}
2468
2469	/**
2470	 * Returns all participant models for this event and all the related events for a meeting.
2471	 *
2472	 * @return Participant
2473	 */
2474	public function getParticipantsForUser(){
2475		//update all participants with this user and event uuid in the system
2476		$findParams = \GO\Base\Db\FindParams::newInstance();
2477
2478		$findParams->joinModel(array(
2479				'model'=>'GO\Calendar\Model\Event',
2480	 			'localTableAlias'=>'t', //defaults to "t"
2481	 			'localField'=>'event_id', //defaults to "id"
2482	 			'foreignField'=>'id', //defaults to primary key of the remote model
2483	 			'tableAlias'=>'e', //Optional table alias
2484	 			));
2485
2486		$findParams->getCriteria()
2487						->addCondition('user_id', $this->user_id)
2488						->addCondition('uuid', $this->uuid,'=','e')  //recurring series and participants all share the same uuid
2489						->addCondition('start_time', $this->start_time,'=','e') //make sure start time matches for recurring series
2490						->addCondition("exception_for_event_id", 0, $this->exception_for_event_id==0 ? '=' : '!=','e');//the master event or a single occurrence can start at the same time. Therefore we must check if exception event has a value or is 0.
2491
2492		return Participant::model()->find($findParams);
2493
2494	}
2495
2496
2497//	public function sendReply(){
2498//		if($this->is_organizer)
2499//			throw new \Exception("Meeting reply can only be send from the organizer's event");
2500//	}
2501
2502	/**
2503	 * Update's the participant status on all related meeting events and optionally sends a notification by e-mail to the organizer.
2504	 * This function has to be called on an event that belongs to the participant and not the organizer.
2505	 *
2506	 * @param int $status Participant status, See Participant::STATUS_*
2507	 * @param boolean $sendMessage
2508	 * @param int $recurrenceTime Export for a specific recurrence time for the recurrence-id
2509	 * @throws Exception
2510	 */
2511	public function replyToOrganizer($recurrenceTime=false, $sendingParticipant=false, $includeIcs=true){
2512
2513//		if($this->is_organizer)
2514//			throw new \Exception("Meeting reply can't be send from the organizer's event");
2515
2516
2517		//we need to pass the sending participant to the toIcs function.
2518		//Only the organizer and current participant should be included
2519		if(!$sendingParticipant)
2520			$sendingParticipant = $this->getParticipantOfCalendar();
2521
2522
2523		if(!$sendingParticipant)
2524			throw new \Exception("Could not find your participant model");
2525
2526		$organizer = $this->getOrganizer();
2527		if(!$organizer)
2528			throw new \Exception("Could not find organizer to send message to!");
2529
2530		$updateReponses = \GO::t("updateReponses", "calendar");
2531		$subject= sprintf($updateReponses[$sendingParticipant->status], $sendingParticipant->name, $this->name);
2532
2533
2534		//create e-mail message
2535		$message = \GO\Base\Mail\Message::newInstance($subject)
2536							->setFrom($sendingParticipant->email, $sendingParticipant->name)
2537							->addTo($organizer->email, $organizer->name);
2538
2539		$body = '<p>'.$subject.': </p>'.$this->toHtml();
2540
2541		$url = \GO::createExternalUrl('calendar', 'openCalendar', array(
2542					'unixtime'=>$this->start_time
2543				));
2544
2545		$body .= '<br /><a href="'.$url.'">'.\GO::t("Open calendar", "calendar").'</a>';
2546
2547//		if(!$this->getOrganizerEvent()){
2548			//organizer is not a Group-Office user with event. We must send a message to him an ICS attachment
2549		if($includeIcs){
2550			$ics=$this->toICS("REPLY", $sendingParticipant, $recurrenceTime);
2551			$a = new Swift_Attachment($ics, \GO\Base\Fs\File::stripInvalidChars($this->name) . '.ics', 'text/calendar; METHOD="REPLY"');
2552			$a->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder("8bit"));
2553			$a->setDisposition("inline");
2554			$a->setContentType("text/calendar;method=REPLY;charset=utf-8");
2555			$message->attach($a);
2556
2557			//for outlook 2003 compatibility
2558//			$a2 = new Swift_Attachment($ics, 'invite.ics', 'application/ics');
2559//			$a2->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder("8bit"));
2560//			$message->attach($a2);
2561		}
2562//		}
2563
2564		$message->setHtmlAlternateBody($body);
2565
2566		$this->getUserMailer()->send($message);
2567
2568	}
2569
2570
2571	public function sendCancelNotice(){
2572//		if(!$this->is_organizer)
2573//			throw new \Exception("Meeting request can only be send from the organizer's event");
2574
2575		$stmt = $this->participants;
2576
2577		while ($participant = $stmt->fetch()) {
2578			//don't invite organizer
2579			if($participant->is_organizer)
2580				continue;
2581
2582
2583			// Set the language of the email to the language of the participant.
2584			$language = false;
2585			if(!empty($participant->user_id)){
2586				$user = \GO\Base\Model\User::model()->findByPk($participant->user_id, false, true);
2587
2588				if($user)
2589					\GO::language()->setLanguage($user->language);
2590			}
2591
2592			$subject =  \GO::t("Cancellation", "calendar").': '.$this->name;
2593
2594			//create e-mail message
2595			$message = \GO\Base\Mail\Message::newInstance($subject)
2596								->setFrom($this->user->email, $this->user->name)
2597								->addTo($participant->email, $participant->name);
2598
2599
2600			//check if we have a Group-Office event. If so, we can handle accepting and declining in Group-Office. Otherwise we'll use ICS calendar objects by mail
2601			$participantEvent = $participant->getParticipantEvent();
2602
2603			$body = '<p>'.\GO::t("The following event has been cancelled by the organizer", "calendar").': </p>'.$this->toHtml();
2604
2605//			if(!$participantEvent){
2606
2607
2608				$ics=$this->toICS("CANCEL", $participant);
2609				$a = new \Swift_Attachment($ics, \GO\Base\Fs\File::stripInvalidChars($this->name) . '.ics', 'text/calendar; METHOD="CANCEL"');
2610				$a->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder("8bit"));
2611				$a->setDisposition("inline");
2612				$a->setContentType("text/calendar;method=CANCEL;charset=utf-8");
2613				$message->attach($a);
2614
2615//				//for outlook 2003 compatibility
2616//				$a2 = new \Swift_Attachment($ics, 'invite.ics', 'application/ics');
2617//				$a2->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder("8bit"));
2618//				$message->attach($a2);
2619
2620//			}else{
2621			if($participantEvent){
2622				$url = \GO::createExternalUrl('calendar', 'openCalendar', array(
2623				'unixtime'=>$this->start_time
2624				));
2625
2626				$body .= '<br /><a href="'.$url.'">'.\GO::t("Open calendar", "calendar").'</a>';
2627			}
2628
2629			$message->setHtmlAlternateBody($body);
2630
2631			// Set back the original language
2632			if($language !== false)
2633				\GO::language()->setLanguage($language);
2634
2635			$this->getUserMailer()->send($message);
2636		}
2637
2638		return true;
2639
2640	}
2641
2642
2643	/**
2644	 * Sends a meeting request to all participants. If the participant is not a Group-Office user
2645	 * or the organizer has no permissions to schedule an event it will include an
2646	 * icalendar attachment so the calendar software can schedule it.
2647	 *
2648	 * @return boolean
2649	 * @throws Exception
2650	 */
2651	public function sendMeetingRequest($newParticipantsOnly=false, $update=false){
2652
2653		if(!$this->is_organizer)
2654			throw new \Exception("Meeting request can only be send from the organizer's event");
2655
2656		$stmt = $this->participants;
2657
2658		//handle missing user
2659		if(!$this->user){
2660			$this->user_id=1;
2661			$this->save(true);
2662		}
2663
2664			while ($participant = $stmt->fetch()) {
2665				if (!$newParticipantsOnly || (isset(\GO::session()->values['new_participant_ids']) && in_array($participant->id,\GO::session()->values['new_participant_ids']))) {
2666
2667					//don't invite organizer
2668					if($participant->is_organizer)
2669						continue;
2670
2671					// Set the language of the email to the language of the participant.
2672					$language = false;
2673					if(!empty($participant->user_id)){
2674						$user = \GO\Base\Model\User::model()->findByPk($participant->user_id, false, true);
2675
2676						if($user)
2677							\GO::language()->setLanguage($user->language);
2678					}
2679
2680					//if participant status is pending then send a new inviation subject. Otherwise send it as update
2681					if(!$update){
2682						$subject = \GO::t("Invitation", "calendar").': '.$this->name;
2683						$bodyLine = \GO::t("You are invited for the following event", "calendar");
2684
2685					}else
2686					{
2687						$subject = \GO::t("Updated invitation", "calendar").': '.$this->name;
2688						$bodyLine = \GO::t("The following event has been updated by the organizer", "calendar");
2689					}
2690
2691					//create e-mail message
2692					$message = \GO\Base\Mail\Message::newInstance($subject)
2693										->setFrom($this->user->email, $this->user->name)
2694										->addTo($participant->email, $participant->name);
2695
2696
2697					//check if we have a Group-Office event. If so, we can handle accepting
2698					//and declining in Group-Office. Otherwise we'll use ICS calendar objects by mail
2699					$participantEvent = $participant->getParticipantEvent();
2700
2701					$body = '<p>'.$bodyLine.': </p>'.$this->toHtml();
2702
2703
2704	//				if(!$participantEvent){
2705
2706					//build message for external program
2707					$acceptUrl = \GO::url("calendar/event/invitation",array("id"=>$this->id,'accept'=>1,'email'=>$participant->email,'participantToken'=>$participant->getSecurityToken()),false, false, false);
2708					$declineUrl = \GO::url("calendar/event/invitation",array("id"=>$this->id,'accept'=>0,'email'=>$participant->email,'participantToken'=>$participant->getSecurityToken()),false, false, false);
2709
2710	//				if($participantEvent){
2711						//hide confusing buttons if user has a GO event.
2712						$body .= '<div class="go-hidden">';
2713	//				}
2714					$body .=
2715
2716							'<p><br /><b>' . \GO::t("Only use the links below if your mail client does not support calendaring functions.", "calendar") . '</b></p>' .
2717							'<p>' . \GO::t("Do you accept this event?", "calendar") . '</p>' .
2718							'<a href="'.$acceptUrl.'">'.\GO::t("Accept", "calendar") . '</a>' .
2719							'&nbsp;|&nbsp;' .
2720							'<a href="'.$declineUrl.'">'.\GO::t("Decline", "calendar") . '</a>';
2721
2722	//				if($participantEvent){
2723						$body .= '</div>';
2724	//				}
2725
2726					$ics=$this->toICS("REQUEST");
2727					$a = new \Swift_Attachment($ics, \GO\Base\Fs\File::stripInvalidChars($this->name) . '.ics', 'text/calendar; METHOD="REQUEST"');
2728					$a->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder("8bit"));
2729					$a->setDisposition("inline");
2730					$a->setContentType("text/calendar;method=REQUEST;charset=utf-8");
2731
2732					$message->attach($a);
2733
2734					//for outlook 2003 compatibility
2735//					$a2 = new \Swift_Attachment($ics, 'invite.ics', 'application/ics');
2736//					$a2->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder("8bit"));
2737//					$message->attach($a2);
2738
2739					if($participantEvent){
2740						$url = \GO::createExternalUrl('calendar', 'openCalendar', array(
2741						'unixtime'=>$this->start_time
2742						));
2743
2744						$body .= '<br /><a href="'.$url.'">'.\GO::t("Open calendar", "calendar").'</a>';
2745					}
2746
2747					$message->setHtmlAlternateBody($body);
2748
2749					// Set back the original language
2750					if($language !== false)
2751						\GO::language()->setLanguage($language);
2752
2753					if(!$this->getUserMailer()->send($message)) {
2754						throw new \Exception("Failed to send invite");
2755					}
2756
2757				}
2758
2759			}
2760
2761			unset(\GO::session()->values['new_participant_ids']);
2762
2763			return true;
2764	}
2765
2766	/**
2767	 *
2768	 * @return GO\Base\Mail\Mailer
2769	 */
2770	private function getUserMailer() {
2771
2772		if(Module::isInstalled('legacy', 'email')) {
2773			$account = GO\Email\Model\Account::model()->findByEmail($this->user->email);
2774			if($account) {
2775				$transport = GO\Email\Transport::newGoInstance($account);
2776				return \GO\Base\Mail\Mailer::newGoInstance($transport);
2777			}
2778			go()->debug("Can't find e-mail account for " . $this->user->email ." so will fall back on main SMTP configuration");
2779
2780		}
2781
2782		go()->debug("Using main SMTP configuration");
2783
2784		return \GO\Base\Mail\Mailer::newGoInstance();
2785	}
2786
2787	public function resourceGetEventCalendarName() {
2788
2789		if ($this->isResource()) {
2790			$resourcedEventModel = Event::model()->findByPk($this->resource_event_id, false , true);
2791			$calendarModel = $resourcedEventModel ? $resourcedEventModel->calendar : false;
2792			return !empty($calendarModel) ? $calendarModel->name : '';
2793		} else {
2794			return '';
2795		}
2796
2797	}
2798
2799}
2800