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"> </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.' </td><td>'.$participant->statusName.' </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 ' | ' . 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