1<?php 2/** 3 * Kronolith_Event defines a generic API for events. 4 * 5 * Copyright 1999-2017 Horde LLC (http://www.horde.org/) 6 * 7 * See the enclosed file COPYING for license information (GPL). If you 8 * did not receive this file, see http://www.horde.org/licenses/gpl. 9 * 10 * @author Chuck Hagenbuch <chuck@horde.org> 11 * @author Jan Schneider <jan@horde.org> 12 * @package Kronolith 13 */ 14abstract class Kronolith_Event 15{ 16 /** 17 * Flag that is set to true if this event has data from either a storage 18 * backend or a form or other import method. 19 * 20 * @var boolean 21 */ 22 public $initialized = false; 23 24 /** 25 * Flag that is set to true if this event exists in a storage driver. 26 * 27 * @var boolean 28 */ 29 public $stored = false; 30 31 /** 32 * The driver unique identifier for this event. 33 * 34 * @var string 35 */ 36 protected $_id = null; 37 38 /** 39 * The UID for this event. 40 * 41 * @var string 42 */ 43 public $uid = null; 44 45 /** 46 * The iCalendar SEQUENCE for this event. 47 * 48 * @var integer 49 */ 50 public $sequence = null; 51 52 /** 53 * The user id of the creator of the event. 54 * 55 * @var string 56 */ 57 protected $_creator = null; 58 59 /** 60 * The title of this event. 61 * 62 * For displaying in the interface use getTitle() instead. 63 * 64 * @var string 65 */ 66 public $title = ''; 67 68 /** 69 * The location this event occurs at. 70 * 71 * @var string 72 */ 73 public $location = ''; 74 75 /** 76 * The timezone of this event. 77 * 78 * @var string 79 */ 80 public $timezone; 81 82 /** 83 * The status of this event. 84 * 85 * @var integer 86 */ 87 public $status = Kronolith::STATUS_CONFIRMED; 88 89 /** 90 * URL to an icon of this event. 91 * 92 * @var string 93 */ 94 public $icon = ''; 95 96 /** 97 * The description for this event. 98 * 99 * @var string 100 */ 101 public $description = ''; 102 103 /** 104 * URL of this event. 105 * 106 * @var string 107 */ 108 public $url = ''; 109 110 /** 111 * Whether the event is private. 112 * 113 * @var boolean 114 */ 115 public $private = false; 116 117 /** 118 * Event tags from the storage backend (e.g. Kolab) 119 * 120 * @var array 121 */ 122 protected $_internaltags; 123 124 /** 125 * This tag's events. 126 * 127 * @var array|string 128 */ 129 protected $_tags = null; 130 131 /** 132 * Geolocation 133 * 134 * @var array 135 */ 136 protected $_geoLocation; 137 138 /** 139 * Whether this is the event on the first day of a multi-day event. 140 * 141 * @var boolen 142 */ 143 public $first = true; 144 145 /** 146 * Whether this is the event on the last day of a multi-day event. 147 * 148 * @var boolen 149 */ 150 public $last = true; 151 152 /** 153 * All the attendees of this event. 154 * 155 * This is an associative array where the keys are the email addresses 156 * of the attendees, and the values are also associative arrays with 157 * keys 'attendance' and 'response' pointing to the attendees' attendance 158 * and response values, respectively. 159 * 160 * @var array 161 */ 162 public $attendees = array(); 163 164 /** 165 * All resources of this event. 166 * 167 * This is an associative array where keys are resource uids, values are 168 * associative arrays with keys attendance and response. 169 * 170 * @var array 171 */ 172 protected $_resources = array(); 173 174 /** 175 * The start time of the event. 176 * 177 * @var Horde_Date 178 */ 179 public $start; 180 181 /** 182 * The end time of the event. 183 * 184 * @var Horde_Date 185 */ 186 public $end; 187 188 /** 189 * The original start time of the event. 190 * 191 * This may differ from $start on multi-day events where $start is the 192 * start time on the current day. For recurring events this is the start 193 * time of the current recurrence. 194 * 195 * @var Horde_Date 196 */ 197 protected $_originalStart; 198 199 /** 200 * The original end time of the event. 201 * 202 * @see $_originalStart for details. 203 * 204 * @var Horde_Date 205 */ 206 protected $_originalEnd; 207 208 /** 209 * The duration of this event in minutes 210 * 211 * @var integer 212 */ 213 public $durMin = 0; 214 215 /** 216 * Whether this is an all-day event. 217 * 218 * @var boolean 219 */ 220 public $allday = false; 221 222 /** 223 * The creation time. 224 * 225 * @see loadHistory() 226 * @var Horde_Date 227 */ 228 public $created; 229 230 /** 231 * The creator string. 232 * 233 * @see loadHistory() 234 * @var string 235 */ 236 public $createdby; 237 238 /** 239 * The last modification time. 240 * 241 * @see loadHistory() 242 * @var Horde_Date 243 */ 244 public $modified; 245 246 /** 247 * The last-modifier string. 248 * 249 * @see loadHistory() 250 * @var string 251 */ 252 public $modifiedby; 253 254 /** 255 * Number of minutes before the event starts to trigger an alarm. 256 * 257 * @var integer 258 */ 259 public $alarm = 0; 260 261 /** 262 * Snooze minutes for this event's alarm. 263 * 264 * @see Horde_Alarm::snooze() 265 * 266 * @var integer 267 */ 268 protected $_snooze; 269 270 /** 271 * The particular alarm methods overridden for this event. 272 * 273 * @var array 274 */ 275 public $methods; 276 277 /** 278 * The identifier of the calender this event exists on. 279 * 280 * @var string 281 */ 282 public $calendar; 283 284 /** 285 * The type of the calender this event exists on. 286 * 287 * @var string 288 */ 289 public $calendarType; 290 291 /** 292 * The HTML background color to be used for this event. 293 * 294 * @var string 295 */ 296 protected $_backgroundColor = '#dddddd'; 297 298 /** 299 * The HTML foreground color to be used for this event. 300 * 301 * @var string 302 */ 303 protected $_foregroundColor = '#000000'; 304 305 /** 306 * The VarRenderer class to use for printing select elements. 307 * 308 * @var Horde_Core_Ui_VarRenderer 309 */ 310 private $_varRenderer; 311 312 /** 313 * The Horde_Date_Recurrence class for this event. 314 * 315 * @var Horde_Date_Recurrence 316 */ 317 public $recurrence; 318 319 /** 320 * Used in view renderers. 321 * 322 * @var integer 323 */ 324 protected $_overlap; 325 326 /** 327 * Used in view renderers. 328 * 329 * @var integer 330 */ 331 protected $_indent; 332 333 /** 334 * Used in view renderers. 335 * 336 * @var integer 337 */ 338 protected $_span; 339 340 /** 341 * Used in view renderers. 342 * 343 * @var integer 344 */ 345 protected $_rowspan; 346 347 /** 348 * The baseid. For events that represent exceptions this is the UID of the 349 * original, recurring event. 350 * 351 * @var string 352 */ 353 public $baseid; 354 355 /** 356 * For exceptions, the date of the original recurring event that this is an 357 * exception for. 358 * 359 * @var Horde_Date 360 */ 361 public $exceptionoriginaldate; 362 363 /** 364 * The cached event duration, split up in time units. 365 * 366 * @see getDuration() 367 * @var stdClass 368 */ 369 protected $_duration; 370 371 /** 372 * Constructor. 373 * 374 * @param Kronolith_Driver $driver The backend driver that this event is 375 * stored in. 376 * @param mixed $eventObject Backend specific event object 377 * that this will represent. 378 */ 379 public function __construct(Kronolith_Driver $driver, $eventObject = null) 380 { 381 $this->calendar = $driver->calendar; 382 list($this->_backgroundColor, $this->_foregroundColor) = $driver->colors(); 383 384 if (!is_null($eventObject)) { 385 $this->fromDriver($eventObject); 386 } 387 } 388 389 /** 390 * Retrieves history information for this event from the history backend. 391 */ 392 public function loadHistory() 393 { 394 try { 395 $log = $GLOBALS['injector']->getInstance('Horde_History') 396 ->getHistory('kronolith:' . $this->calendar . ':' . $this->uid); 397 $userId = $GLOBALS['registry']->getAuth(); 398 foreach ($log as $entry) { 399 switch ($entry['action']) { 400 case 'add': 401 $this->created = new Horde_Date($entry['ts']); 402 if ($userId != $entry['who']) { 403 $this->createdby = sprintf(_("by %s"), Kronolith::getUserName($entry['who'])); 404 } else { 405 $this->createdby = _("by me"); 406 } 407 break; 408 409 case 'modify': 410 if ($this->modified && 411 $this->modified->timestamp() >= $entry['ts']) { 412 break; 413 } 414 $this->modified = new Horde_Date($entry['ts']); 415 if ($userId != $entry['who']) { 416 $this->modifiedby = sprintf(_("by %s"), Kronolith::getUserName($entry['who'])); 417 } else { 418 $this->modifiedby = _("by me"); 419 } 420 break; 421 } 422 } 423 } catch (Horde_Exception $e) { 424 } 425 } 426 427 /** 428 * Setter. 429 * 430 * Sets the 'id' and 'creator' properties. 431 * 432 * @param string $name Property name. 433 * @param mixed $value Property value. 434 */ 435 public function __set($name, $value) 436 { 437 switch ($name) { 438 case 'id': 439 if (substr($value, 0, 10) == 'kronolith:') { 440 $value = substr($value, 10); 441 } 442 // Fall through. 443 case 'creator': 444 case 'geoLocation': 445 case 'indent': 446 case 'originalStart': 447 case 'originalEnd': 448 case 'overlap': 449 case 'rowspan': 450 case 'span': 451 case 'tags': 452 $this->{'_' . $name} = $value; 453 return; 454 } 455 $trace = debug_backtrace(); 456 trigger_error('Undefined property via __set(): ' . $name 457 . ' in ' . $trace[0]['file'] 458 . ' on line ' . $trace[0]['line'], 459 E_USER_NOTICE); 460 } 461 462 /** 463 * Getter. 464 * 465 * Returns the 'id' and 'creator' properties. 466 * 467 * @param string $name Property name. 468 * 469 * @return mixed Property value. 470 */ 471 public function __get($name) 472 { 473 switch ($name) { 474 case 'id': 475 case 'indent': 476 case 'overlap': 477 case 'rowspan': 478 case 'span': 479 return $this->{'_' . $name}; 480 case 'creator': 481 if (empty($this->_creator)) { 482 $this->_creator = $GLOBALS['registry']->getAuth(); 483 } 484 return $this->_creator; 485 break; 486 case 'originalStart': 487 if (empty($this->_originalStart)) { 488 $this->_originalStart = $this->start; 489 } 490 return $this->_originalStart; 491 break; 492 case 'originalEnd': 493 if (empty($this->_originalEnd)) { 494 $this->_originalEnd = $this->start; 495 } 496 return $this->_originalEnd; 497 break; 498 case 'tags': 499 if (!isset($this->_tags)) { 500 $this->synchronizeTags(Kronolith::getTagger()->getTags($this->uid, Kronolith_Tagger::TYPE_EVENT)); 501 } 502 return $this->_tags; 503 case 'geoLocation': 504 if (!isset($this->_geoLocation)) { 505 try { 506 $this->_geoLocation = $GLOBALS['injector']->getInstance('Kronolith_Geo')->getLocation($this->id); 507 } catch (Kronolith_Exception $e) {} 508 } 509 return $this->_geoLocation; 510 } 511 512 $trace = debug_backtrace(); 513 trigger_error('Undefined property via __set(): ' . $name 514 . ' in ' . $trace[0]['file'] 515 . ' on line ' . $trace[0]['line'], 516 E_USER_NOTICE); 517 return null; 518 } 519 520 /** 521 * Returns a reference to a driver that's valid for this event. 522 * 523 * @return Kronolith_Driver A driver that this event can use to save 524 * itself, etc. 525 */ 526 public function getDriver() 527 { 528 return Kronolith::getDriver(str_replace('Kronolith_Event_', '', get_class($this)), $this->calendar); 529 } 530 531 /** 532 * Returns the share this event belongs to. 533 * 534 * @return Horde_Share This event's share. 535 * @throws Kronolith_Exception 536 */ 537 public function getShare() 538 { 539 if ($GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_CALENDARS, $this->calendar) !== false) { 540 return $GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_CALENDARS, $this->calendar)->share(); 541 } 542 throw new LogicException('Share not found'); 543 } 544 545 /** 546 * Encapsulates permissions checking. 547 * 548 * @param integer $permission The permission to check for. 549 * @param string $user The user to check permissions for. 550 * 551 * @return boolean 552 */ 553 public function hasPermission($permission, $user = null) 554 { 555 if ($user === null) { 556 $user = $GLOBALS['registry']->getAuth(); 557 } 558 try { 559 $share = $this->getShare(); 560 } catch (Exception $e) { 561 return false; 562 } 563 return $share->hasPermission($user, $permission, $this->creator); 564 } 565 566 /** 567 * Saves changes to this event. 568 * 569 * @return integer The event id. 570 * @throws Kronolith_Exception 571 */ 572 public function save() 573 { 574 if (!$this->initialized) { 575 throw new LogicException('Event not yet initialized'); 576 } 577 578 /* Check for acceptance/denial of this event's resources. */ 579 $accepted_resources = array(); 580 $locks = $GLOBALS['injector']->getInstance('Horde_Lock'); 581 $lock = array(); 582 // Don't waste time with resource acceptance if the status is cancelled, 583 // the event will be removed from the resource calendar anyway. 584 if ($this->status != Kronolith::STATUS_CANCELLED) { 585 foreach (array_keys($this->getResources()) as $id) { 586 /* Get the resource and protect against infinite recursion in 587 * case someone is silly enough to add a resource to it's own 588 * event.*/ 589 $resource = Kronolith::getDriver('Resource')->getResource($id); 590 $rcal = $resource->get('calendar'); 591 if ($rcal == $this->calendar) { 592 continue; 593 } 594 Kronolith::getDriver('Resource')->open($rcal); 595 596 /* Lock the resource and get the response */ 597 if ($resource->get('response_type') == Kronolith_Resource::RESPONSETYPE_AUTO) { 598 $principle = 'calendar/' . $rcal; 599 $lock[$resource->getId()] = $locks->setLock($GLOBALS['registry']->getAuth(), 'kronolith', $principle, 5, Horde_Lock::TYPE_EXCLUSIVE); 600 $haveLock = true; 601 } else { 602 $haveLock = false; 603 } 604 if ($haveLock && !$lock[$resource->getId()]) { 605 // Already locked 606 // For now, just fail. Not sure how else to capture the 607 // locked resources and notify the user. 608 throw new Kronolith_Exception(sprintf(_("The resource \"%s\" was locked. Please try again."), $resource->get('name'))); 609 } else { 610 $response = $resource->getResponse($this); 611 } 612 613 /* Remember accepted resources so we can add the event to their 614 * calendars. Otherwise, clear the lock. */ 615 if ($response == Kronolith::RESPONSE_ACCEPTED) { 616 $accepted_resources[] = $resource; 617 } elseif ($haveLock) { 618 $locks->clearLock($lock[$resource->getId()]); 619 } 620 621 if ($response == Kronolith::RESPONSE_DECLINED && $this->uid) { 622 $r_driver = Kronolith::getDriver('Resource'); 623 $r_event = $r_driver->getByUID($this->uid, array($resource->get('calendar'))); 624 $r_driver->deleteEvent($r_event, true, true); 625 } 626 627 /* Add the resource to the event */ 628 $this->addResource($resource, $response); 629 } 630 } else { 631 // If event is cancelled, and actually exists, we need to mark it 632 // as cancelled in resource calendar. 633 foreach (array_keys($this->getResources()) as $id) { 634 $resource = Kronolith::getDriver('Resource')->getResource($id); 635 $rcal = $resource->get('calendar'); 636 if ($rcal == $this->calendar) { 637 continue; 638 } 639 try { 640 Kronolith::getDriver('Resource')->open($rcal); 641 $resource->addEvent($this); 642 } catch (Exception $e) { 643 } 644 } 645 } 646 647 /* Save */ 648 $result = $this->getDriver()->saveEvent($this); 649 650 /* Now that the event is definitely commited to storage, we can add 651 * the event to each resource that has accepted. Not very efficient, 652 * but this also solves the problem of not having a GUID for the event 653 * until after it's saved. If we add the event to the resources 654 * calendar before it is saved, they will have different GUIDs, and 655 * hence no longer refer to the same event. */ 656 foreach ($accepted_resources as $resource) { 657 $resource->addEvent($this); 658 if ($resource->get('response_type') == Kronolith_Resource::RESPONSETYPE_AUTO) { 659 $locks->clearLock($lock[$resource->getId()]); 660 } 661 } 662 663 $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm'); 664 if ($alarm = $this->toAlarm(new Horde_Date($_SERVER['REQUEST_TIME']))) { 665 $hordeAlarm->set($alarm); 666 if ($this->_snooze) { 667 $hordeAlarm->snooze($this->uid, $GLOBALS['registry']->getAuth(), $this->_snooze); 668 } 669 } else { 670 $hordeAlarm->delete($this->uid); 671 } 672 673 return $result; 674 } 675 676 /** 677 * Imports a backend specific event object. 678 * 679 * @param mixed $eventObject Backend specific event object that this 680 * object will represent. 681 */ 682 public function fromDriver($event) 683 { 684 } 685 686 /** 687 * Exports this event in iCalendar format. 688 * 689 * @param Horde_Icalendar $calendar A Horde_Icalendar object that acts as 690 * a container. 691 * 692 * @return array An array of Horde_Icalendar_Vevent objects for this event. 693 */ 694 public function toiCalendar($calendar) 695 { 696 $vEvent = Horde_Icalendar::newComponent('vevent', $calendar); 697 $v1 = $calendar->getAttribute('VERSION') == '1.0'; 698 $vEvents = array(); 699 700 // For certain recur types, we must output in the event's timezone 701 // so that the BYDAY values do not get out of sync with the UTC 702 // date-time. See Bug: 11339 703 if ($this->recurs()) { 704 switch ($this->recurrence->getRecurType()) { 705 case Horde_Date_Recurrence::RECUR_WEEKLY: 706 case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY: 707 case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY: 708 if (!$this->timezone) { 709 $this->timezone = date_default_timezone_get(); 710 } 711 } 712 } 713 714 if ($this->isAllDay()) { 715 $vEvent->setAttribute('DTSTART', $this->start, array('VALUE' => 'DATE')); 716 $vEvent->setAttribute('DTEND', $this->end, array('VALUE' => 'DATE')); 717 $vEvent->setAttribute('X-FUNAMBOL-ALLDAY', 1); 718 } else { 719 $this->setTimezone(true); 720 $params = array(); 721 if ($this->timezone) { 722 try { 723 if (!$this->baseid) { 724 $tz = $GLOBALS['injector']->getInstance('Horde_Timezone'); 725 $vEvents[] = $tz->getZone($this->timezone)->toVtimezone(); 726 } 727 $params['TZID'] = $this->timezone; 728 } catch (Horde_Exception $e) { 729 Horde::log('Unable to locate the tz database.', 'WARN'); 730 } 731 } 732 733 $vEvent->setAttribute('DTSTART', clone $this->start, $params); 734 $vEvent->setAttribute('DTEND', clone $this->end, $params); 735 } 736 737 $vEvent->setAttribute('DTSTAMP', $_SERVER['REQUEST_TIME']); 738 $vEvent->setAttribute('UID', $this->uid); 739 740 /* Get the event's create and last modify date. */ 741 $created = $modified = null; 742 try { 743 $history = $GLOBALS['injector']->getInstance('Horde_History'); 744 $created = $history->getActionTimestamp( 745 'kronolith:' . $this->calendar . ':' . $this->uid, 'add'); 746 $modified = $history->getActionTimestamp( 747 'kronolith:' . $this->calendar . ':' . $this->uid, 'modify'); 748 /* The history driver returns 0 for not found. If 0 or null does 749 * not matter, strip this. */ 750 if ($created == 0) { 751 $created = null; 752 } 753 if ($modified == 0) { 754 $modified = null; 755 } 756 } catch (Exception $e) { 757 } 758 if (!empty($created)) { 759 $vEvent->setAttribute($v1 ? 'DCREATED' : 'CREATED', $created); 760 if (empty($modified)) { 761 $modified = $created; 762 } 763 } 764 if (!empty($modified)) { 765 $vEvent->setAttribute('LAST-MODIFIED', $modified); 766 } 767 768 $vEvent->setAttribute('SUMMARY', $this->getTitle()); 769 770 // Organizer 771 if (count($this->attendees)) { 772 $name = Kronolith::getUserName($this->creator); 773 $email = Kronolith::getUserEmail($this->creator); 774 $params = array(); 775 if ($v1) { 776 $tmp = new Horde_Mail_Rfc822_Address($email); 777 if (!empty($name)) { 778 $tmp->personal = $name; 779 } 780 $email = strval($tmp); 781 } else { 782 if (!empty($name)) { 783 $params['CN'] = $name; 784 } 785 if (!empty($email)) { 786 $email = 'mailto:' . $email; 787 } 788 } 789 $vEvent->setAttribute('ORGANIZER', $email, $params); 790 } 791 if (!$this->isPrivate()) { 792 if (!empty($this->description)) { 793 $vEvent->setAttribute('DESCRIPTION', $this->description); 794 } 795 796 // Tags 797 if ($this->tags) { 798 $vEvent->setAttribute('CATEGORIES', '', array(), true, array_values($this->tags)); 799 } 800 801 // Location 802 if (!empty($this->location)) { 803 $vEvent->setAttribute('LOCATION', $this->location); 804 } 805 if ($this->geoLocation) { 806 $vEvent->setAttribute('GEO', array('latitude' => $this->geoLocation['lat'], 'longitude' => $this->geoLocation['lon'])); 807 } 808 809 // URL 810 if (!empty($this->url)) { 811 $vEvent->setAttribute('URL', $this->url); 812 } 813 } 814 $vEvent->setAttribute('CLASS', $this->private ? 'PRIVATE' : 'PUBLIC'); 815 816 // Status. 817 switch ($this->status) { 818 case Kronolith::STATUS_FREE: 819 // This is not an official iCalendar value, but we need it for 820 // synchronization. 821 $vEvent->setAttribute('STATUS', 'FREE'); 822 $vEvent->setAttribute('TRANSP', $v1 ? 1 : 'TRANSPARENT'); 823 break; 824 case Kronolith::STATUS_TENTATIVE: 825 $vEvent->setAttribute('STATUS', 'TENTATIVE'); 826 $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE'); 827 break; 828 case Kronolith::STATUS_CONFIRMED: 829 $vEvent->setAttribute('STATUS', 'CONFIRMED'); 830 $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE'); 831 break; 832 case Kronolith::STATUS_CANCELLED: 833 if ($v1) { 834 $vEvent->setAttribute('STATUS', 'DECLINED'); 835 $vEvent->setAttribute('TRANSP', 1); 836 } else { 837 $vEvent->setAttribute('STATUS', 'CANCELLED'); 838 $vEvent->setAttribute('TRANSP', 'TRANSPARENT'); 839 } 840 break; 841 } 842 843 // Attendees. 844 foreach ($this->attendees as $email => $status) { 845 $params = array(); 846 switch ($status['attendance']) { 847 case Kronolith::PART_REQUIRED: 848 if ($v1) { 849 $params['EXPECT'] = 'REQUIRE'; 850 } else { 851 $params['ROLE'] = 'REQ-PARTICIPANT'; 852 } 853 break; 854 855 case Kronolith::PART_OPTIONAL: 856 if ($v1) { 857 $params['EXPECT'] = 'REQUEST'; 858 } else { 859 $params['ROLE'] = 'OPT-PARTICIPANT'; 860 } 861 break; 862 863 case Kronolith::PART_NONE: 864 if ($v1) { 865 $params['EXPECT'] = 'FYI'; 866 } else { 867 $params['ROLE'] = 'NON-PARTICIPANT'; 868 } 869 break; 870 } 871 872 switch ($status['response']) { 873 case Kronolith::RESPONSE_NONE: 874 if ($v1) { 875 $params['STATUS'] = 'NEEDS ACTION'; 876 $params['RSVP'] = 'YES'; 877 } else { 878 $params['PARTSTAT'] = 'NEEDS-ACTION'; 879 $params['RSVP'] = 'TRUE'; 880 } 881 break; 882 883 case Kronolith::RESPONSE_ACCEPTED: 884 if ($v1) { 885 $params['STATUS'] = 'ACCEPTED'; 886 } else { 887 $params['PARTSTAT'] = 'ACCEPTED'; 888 } 889 break; 890 891 case Kronolith::RESPONSE_DECLINED: 892 if ($v1) { 893 $params['STATUS'] = 'DECLINED'; 894 } else { 895 $params['PARTSTAT'] = 'DECLINED'; 896 } 897 break; 898 899 case Kronolith::RESPONSE_TENTATIVE: 900 if ($v1) { 901 $params['STATUS'] = 'TENTATIVE'; 902 } else { 903 $params['PARTSTAT'] = 'TENTATIVE'; 904 } 905 break; 906 } 907 908 if (strpos($email, '@') === false) { 909 $email = ''; 910 } 911 if ($v1) { 912 if (empty($email)) { 913 if (!empty($status['name'])) { 914 $email = $status['name']; 915 } 916 } else { 917 $tmp = new Horde_Mail_Rfc822_Address($email); 918 if (!empty($status['name'])) { 919 $tmp->personal = $status['name']; 920 } 921 $email = strval($tmp); 922 } 923 } else { 924 if (!empty($status['name'])) { 925 $params['CN'] = $status['name']; 926 } 927 if (!empty($email)) { 928 $email = 'mailto:' . $email; 929 } 930 } 931 932 $vEvent->setAttribute('ATTENDEE', $email, $params); 933 } 934 935 // Alarms. 936 if (!empty($this->alarm)) { 937 if ($v1) { 938 $alarm = new Horde_Date($this->start); 939 $alarm->min -= $this->alarm; 940 $vEvent->setAttribute('AALARM', $alarm); 941 } else { 942 $vAlarm = Horde_Icalendar::newComponent('valarm', $vEvent); 943 $vAlarm->setAttribute('ACTION', 'DISPLAY'); 944 $vAlarm->setAttribute('DESCRIPTION', $this->getTitle()); 945 $vAlarm->setAttribute( 946 'TRIGGER;VALUE=DURATION', 947 ($this->alarm > 0 ? '-' : '') . 'PT' . abs($this->alarm) . 'M' 948 ); 949 $vEvent->addComponent($vAlarm); 950 } 951 $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm'); 952 if ($hordeAlarm->exists($this->uid, $GLOBALS['registry']->getAuth()) && 953 $hordeAlarm->isSnoozed($this->uid, $GLOBALS['registry']->getAuth())) { 954 $vEvent->setAttribute('X-MOZ-LASTACK', new Horde_Date($_SERVER['REQUEST_TIME'])); 955 $alarm = $hordeAlarm->get($this->uid, $GLOBALS['registry']->getAuth()); 956 if (!empty($alarm['snooze'])) { 957 $alarm['snooze']->setTimezone(date_default_timezone_get()); 958 $vEvent->setAttribute('X-MOZ-SNOOZE-TIME', $alarm['snooze']); 959 } 960 } 961 } 962 963 // Recurrence. 964 if ($this->recurs()) { 965 if ($v1) { 966 $rrule = $this->recurrence->toRRule10($calendar); 967 } else { 968 $rrule = $this->recurrence->toRRule20($calendar); 969 } 970 if (!empty($rrule)) { 971 $vEvent->setAttribute('RRULE', $rrule); 972 } 973 974 // Exceptions. An exception with no replacement event is represented 975 // by EXDATE, and those with replacement events are represented by 976 // a new vEvent element. We get all known replacement events first, 977 // then remove the exceptionoriginaldate from the list of the event 978 // exceptions. Any exceptions left should represent exceptions with 979 // no replacement. 980 $exceptions = $this->recurrence->getExceptions(); 981 $search = new stdClass(); 982 $search->baseid = $this->uid; 983 $results = $this->getDriver()->search($search); 984 foreach ($results as $days) { 985 foreach ($days as $exceptionEvent) { 986 // Need to change the UID so it links to the original 987 // recurring event, but only if not using $v1. If using $v1, 988 // we add the date to EXDATE and do NOT change the UID. 989 if (!$v1) { 990 $exceptionEvent->uid = $this->uid; 991 } 992 $vEventException = $exceptionEvent->toiCalendar($calendar); 993 994 // This should never happen, but protect against it anyway. 995 if (count($vEventException) > 2 || 996 (count($vEventException) > 1 && 997 !($vEventException[0] instanceof Horde_Icalendar_Vtimezone) && 998 !($vEventException[1] instanceof Horde_Icalendar_Vtimezone))) { 999 throw new Kronolith_Exception(_("Unable to parse event.")); 1000 } 1001 $vEventException = array_pop($vEventException); 1002 // If $v1, need to add to EXDATE 1003 if (!$this->isAllDay()) { 1004 $exceptionEvent->setTimezone(true); 1005 } 1006 if (!$v1) { 1007 $vEventException->setAttribute('RECURRENCE-ID', $exceptionEvent->exceptionoriginaldate); 1008 } else { 1009 $vEvent->setAttribute('EXDATE', array($exceptionEvent->exceptionoriginaldate), array('VALUE' => 'DATE')); 1010 } 1011 $originaldate = $exceptionEvent->exceptionoriginaldate->format('Ymd'); 1012 $key = array_search($originaldate, $exceptions); 1013 if ($key !== false) { 1014 unset($exceptions[$key]); 1015 } 1016 $vEvents[] = $vEventException; 1017 } 1018 } 1019 1020 /* The remaining exceptions represent deleted recurrences */ 1021 foreach ($exceptions as $exception) { 1022 if (!empty($exception)) { 1023 // Use multiple EXDATE attributes instead of EXDATE 1024 // attributes with multiple values to make Apple iCal 1025 // happy. 1026 list($year, $month, $mday) = sscanf($exception, '%04d%02d%02d'); 1027 if ($this->isAllDay()) { 1028 $vEvent->setAttribute('EXDATE', array(new Horde_Date($year, $month, $mday)), array('VALUE' => 'DATE')); 1029 } else { 1030 // Another Apple iCal/Calendar fix. EXDATE is only 1031 // recognized if the full datetime is present and matches 1032 // the time part given in DTSTART. 1033 $params = array(); 1034 if ($this->timezone) { 1035 $params['TZID'] = $this->timezone; 1036 } 1037 $exdate = clone $this->start; 1038 $exdate->year = $year; 1039 $exdate->month = $month; 1040 $exdate->mday = $mday; 1041 $vEvent->setAttribute('EXDATE', array($exdate), $params); 1042 } 1043 } 1044 } 1045 } 1046 array_unshift($vEvents, $vEvent); 1047 1048 $this->setTimezone(false); 1049 1050 return $vEvents; 1051 } 1052 1053 /** 1054 * Updates the properties of this event from a Horde_Icalendar_Vevent 1055 * object. 1056 * 1057 * @param Horde_Icalendar_Vevent $vEvent The iCalendar data to update 1058 * from. 1059 * @param boolean $parseAttendees Parse attendees too? 1060 * @since Kronolith 4.2 1061 */ 1062 public function fromiCalendar($vEvent, $parseAttendees = false) 1063 { 1064 // Unique ID. 1065 try { 1066 $uid = $vEvent->getAttribute('UID'); 1067 if (!empty($uid)) { 1068 $this->uid = $uid; 1069 } 1070 } catch (Horde_Icalendar_Exception $e) {} 1071 1072 // Sequence. 1073 try { 1074 $seq = $vEvent->getAttribute('SEQUENCE'); 1075 if (is_int($seq)) { 1076 $this->sequence = $seq; 1077 } 1078 } catch (Horde_Icalendar_Exception $e) {} 1079 1080 // Title, tags and description. 1081 try { 1082 $title = $this->_ensureUtf8($vEvent->getAttribute('SUMMARY')); 1083 if (!is_array($title)) { 1084 $this->title = $title; 1085 } 1086 } catch (Horde_Icalendar_Exception $e) {} 1087 1088 // Tags 1089 try { 1090 $this->_tags = $vEvent->getAttributeValues('CATEGORIES'); 1091 } catch (Horde_Icalendar_Exception $e) {} 1092 1093 // Description 1094 try { 1095 $desc = $this->_ensureUtf8($vEvent->getAttribute('DESCRIPTION')); 1096 if (!is_array($desc)) { 1097 $this->description = $desc; 1098 } 1099 } catch (Horde_Icalendar_Exception $e) {} 1100 1101 // Remote Url 1102 try { 1103 $url = $vEvent->getAttribute('URL'); 1104 if (!is_array($url)) { 1105 $this->url = $url; 1106 } 1107 } catch (Horde_Icalendar_Exception $e) {} 1108 1109 // Location 1110 try { 1111 $location = $this->_ensureUtf8($vEvent->getAttribute('LOCATION')); 1112 if (!is_array($location)) { 1113 $this->location = $location; 1114 } 1115 } catch (Horde_Icalendar_Exception $e) {} 1116 1117 try { 1118 $geolocation = $vEvent->getAttribute('GEO'); 1119 $this->geoLocation = array( 1120 'lat' => $geolocation['latitude'], 1121 'lon' => $geolocation['longitude'] 1122 ); 1123 } catch (Horde_Icalendar_Exception $e) {} 1124 1125 // Class 1126 try { 1127 $class = $vEvent->getAttribute('CLASS'); 1128 if (!is_array($class)) { 1129 $class = Horde_String::upper($class); 1130 $this->private = $class == 'PRIVATE' || $class == 'CONFIDENTIAL'; 1131 } 1132 } catch (Horde_Icalendar_Exception $e) {} 1133 1134 // Status. 1135 try { 1136 $status = $vEvent->getAttribute('STATUS'); 1137 if (!is_array($status)) { 1138 $status = Horde_String::upper($status); 1139 if ($status == 'DECLINED') { 1140 $status = 'CANCELLED'; 1141 } 1142 if (defined('Kronolith::STATUS_' . $status)) { 1143 $this->status = constant('Kronolith::STATUS_' . $status); 1144 } 1145 } 1146 } catch (Horde_Icalendar_Exception $e) {} 1147 1148 // Reset allday flag in case this has changed. Will be recalculated 1149 // next time isAllDay() is called. 1150 $this->allday = false; 1151 1152 // Start and end date. 1153 $tzid = null; 1154 try { 1155 $start = $vEvent->getAttribute('DTSTART'); 1156 $startParams = $vEvent->getAttribute('DTSTART', true); 1157 // We don't support different timezones for different attributes, 1158 // so use the DTSTART timezone for the complete event. 1159 if (isset($startParams[0]['TZID'])) { 1160 // Horde_Date supports timezone aliases, so try that first. 1161 $tz = $startParams[0]['TZID']; 1162 try { 1163 // Check if the timezone name is supported by PHP natively. 1164 new DateTimeZone($tz); 1165 $this->timezone = $tzid = $tz; 1166 } catch (Exception $e) { 1167 } 1168 } 1169 if (!is_array($start)) { 1170 // Date-Time field 1171 $this->start = new Horde_Date($start, $tzid); 1172 } else { 1173 // Date field 1174 $this->start = new Horde_Date( 1175 array('year' => (int)$start['year'], 1176 'month' => (int)$start['month'], 1177 'mday' => (int)$start['mday']), 1178 $tzid 1179 ); 1180 } 1181 } catch (Horde_Icalendar_Exception $e) { 1182 throw new Kronolith_Exception($e); 1183 } catch (Horde_Date_Exception $e) { 1184 throw new Kronolith_Exception($e); 1185 } 1186 1187 try { 1188 $end = $vEvent->getAttribute('DTEND'); 1189 if (!is_array($end)) { 1190 // Date-Time field 1191 $this->end = new Horde_Date($end, $tzid); 1192 // All day events are transferred by many device as 1193 // DSTART: YYYYMMDDT000000 DTEND: YYYYMMDDT2359(59|00) 1194 // Convert accordingly 1195 if (is_object($this->start) && $this->start->hour == 0 && 1196 $this->start->min == 0 && $this->start->sec == 0 && 1197 $this->end->hour == 23 && $this->end->min == 59) { 1198 $this->end = new Horde_Date( 1199 array('year' => (int)$this->end->year, 1200 'month' => (int)$this->end->month, 1201 'mday' => (int)$this->end->mday + 1), 1202 $tzid); 1203 } 1204 } else { 1205 // Date field 1206 $this->end = new Horde_Date( 1207 array('year' => (int)$end['year'], 1208 'month' => (int)$end['month'], 1209 'mday' => (int)$end['mday']), 1210 $tzid); 1211 } 1212 } catch (Horde_Icalendar_Exception $e) { 1213 $end = null; 1214 } 1215 1216 if (is_null($end)) { 1217 try { 1218 $duration = $vEvent->getAttribute('DURATION'); 1219 if (!is_array($duration)) { 1220 $this->end = new Horde_Date($this->start); 1221 $this->end->sec += $duration; 1222 $end = 1; 1223 } 1224 } catch (Horde_Icalendar_Exception $e) {} 1225 1226 if (is_null($end)) { 1227 // End date equal to start date as per RFC 2445. 1228 $this->end = new Horde_Date($this->start); 1229 if (is_array($start)) { 1230 // Date field 1231 $this->end->mday++; 1232 } 1233 } 1234 } 1235 1236 // vCalendar 1.0 alarms 1237 try { 1238 $alarm = $vEvent->getAttribute('AALARM'); 1239 if (!is_array($alarm) && intval($alarm)) { 1240 $this->alarm = intval(($this->start->timestamp() - $alarm) / 60); 1241 } 1242 } catch (Horde_Icalendar_Exception $e) {} 1243 1244 // vCalendar 2.0 alarms 1245 foreach ($vEvent->getComponents() as $alarm) { 1246 if (!($alarm instanceof Horde_Icalendar_Valarm)) { 1247 continue; 1248 } 1249 try { 1250 if ($alarm->getAttribute('ACTION') == 'NONE') { 1251 continue; 1252 } 1253 } catch (Horde_Icalendar_Exception $e) { 1254 } 1255 try { 1256 // @todo consider implementing different ACTION types. 1257 // $action = $alarm->getAttribute('ACTION'); 1258 $trigger = $alarm->getAttribute('TRIGGER'); 1259 $triggerParams = $alarm->getAttribute('TRIGGER', true); 1260 } catch (Horde_Icalendar_Exception $e) { 1261 continue; 1262 } 1263 if (!is_array($triggerParams)) { 1264 $triggerParams = array($triggerParams); 1265 } 1266 $haveTrigger = false; 1267 foreach ($triggerParams as $tp) { 1268 if (isset($tp['VALUE']) && 1269 $tp['VALUE'] == 'DATE-TIME') { 1270 if (isset($tp['RELATED']) && 1271 $tp['RELATED'] == 'END') { 1272 $this->alarm = intval(($this->end->timestamp() - $trigger) / 60); 1273 } else { 1274 $this->alarm = intval(($this->start->timestamp() - $trigger) / 60); 1275 } 1276 $haveTrigger = true; 1277 break; 1278 } elseif (isset($tp['RELATED']) && $tp['RELATED'] == 'END') { 1279 $this->alarm = -intval($trigger / 60); 1280 $this->alarm -= $this->durMin; 1281 $haveTrigger = true; 1282 break; 1283 } 1284 } 1285 if (!$haveTrigger) { 1286 $this->alarm = -intval($trigger / 60); 1287 } 1288 break; 1289 } 1290 1291 // Alarm snoozing/dismissal 1292 if ($this->alarm) { 1293 try { 1294 // If X-MOZ-LASTACK is set, this event is either dismissed or 1295 // snoozed. 1296 $vEvent->getAttribute('X-MOZ-LASTACK'); 1297 try { 1298 // If X-MOZ-SNOOZE-TIME is set, this event is snoozed. 1299 $snooze = $vEvent->getAttribute('X-MOZ-SNOOZE-TIME'); 1300 $this->_snooze = intval(($snooze - time()) / 60); 1301 } catch (Horde_Icalendar_Exception $e) { 1302 // If X-MOZ-SNOOZE-TIME is not set, this event is dismissed. 1303 $this->_snooze = -1; 1304 } 1305 } catch (Horde_Icalendar_Exception $e) { 1306 } 1307 } 1308 1309 // Attendance. 1310 // Importing attendance may result in confusion: editing an imported 1311 // copy of an event can cause invitation updates to be sent from 1312 // people other than the original organizer. So we don't import by 1313 // default. However to allow updates by synchronization, this behavior 1314 // can be overriden. 1315 // X-ATTENDEE is there for historical reasons. @todo remove in 1316 // Kronolith 5. 1317 $attendee = null; 1318 if ($parseAttendees) { 1319 try { 1320 $attendee = $vEvent->getAttribute('ATTENDEE'); 1321 $params = $vEvent->getAttribute('ATTENDEE', true); 1322 } catch (Horde_Icalendar_Exception $e) { 1323 try { 1324 $attendee = $vEvent->getAttribute('X-ATTENDEE'); 1325 $params = $vEvent->getAttribute('X-ATTENDEE', true); 1326 } catch (Horde_Icalendar_Exception $e) { 1327 } 1328 } 1329 } 1330 if ($attendee) { 1331 if (!is_array($attendee)) { 1332 $attendee = array($attendee); 1333 } 1334 if (!is_array($params)) { 1335 $params = array($params); 1336 } 1337 // Clear the attendees since we might be editing/replacing the event 1338 $this->attendees = array(); 1339 for ($i = 0; $i < count($attendee); ++$i) { 1340 $attendee[$i] = str_replace(array('MAILTO:', 'mailto:'), '', 1341 $attendee[$i]); 1342 $tmp = new Horde_Mail_Rfc822_Address($attendee[$i]); 1343 $email = $tmp->bare_address; 1344 // Default according to rfc2445: 1345 $attendance = Kronolith::PART_REQUIRED; 1346 // vCalendar 2.0 style: 1347 if (!empty($params[$i]['ROLE'])) { 1348 switch($params[$i]['ROLE']) { 1349 case 'OPT-PARTICIPANT': 1350 $attendance = Kronolith::PART_OPTIONAL; 1351 break; 1352 1353 case 'NON-PARTICIPANT': 1354 $attendance = Kronolith::PART_NONE; 1355 break; 1356 } 1357 } 1358 // vCalendar 1.0 style; 1359 if (!empty($params[$i]['EXPECT'])) { 1360 switch($params[$i]['EXPECT']) { 1361 case 'REQUEST': 1362 $attendance = Kronolith::PART_OPTIONAL; 1363 break; 1364 1365 case 'FYI': 1366 $attendance = Kronolith::PART_NONE; 1367 break; 1368 } 1369 } 1370 $response = Kronolith::RESPONSE_NONE; 1371 if (empty($params[$i]['PARTSTAT']) && 1372 !empty($params[$i]['STATUS'])) { 1373 $params[$i]['PARTSTAT'] = $params[$i]['STATUS']; 1374 } 1375 1376 if (!empty($params[$i]['PARTSTAT'])) { 1377 switch($params[$i]['PARTSTAT']) { 1378 case 'ACCEPTED': 1379 $response = Kronolith::RESPONSE_ACCEPTED; 1380 break; 1381 1382 case 'DECLINED': 1383 $response = Kronolith::RESPONSE_DECLINED; 1384 break; 1385 1386 case 'TENTATIVE': 1387 $response = Kronolith::RESPONSE_TENTATIVE; 1388 break; 1389 } 1390 } 1391 $name = isset($params[$i]['CN']) 1392 ? $this->_ensureUtf8($params[$i]['CN']) 1393 : null; 1394 1395 $this->addAttendee($email, $attendance, $response, $name); 1396 } 1397 } 1398 1399 $this->_handlevEventRecurrence($vEvent); 1400 1401 $this->initialized = true; 1402 } 1403 1404 /** 1405 * Handle parsing recurrence related fields. 1406 * 1407 * @param Horde_Icalendar $vEvent 1408 * @throws Kronolith_Exception 1409 */ 1410 protected function _handlevEventRecurrence($vEvent) 1411 { 1412 // Recurrence. 1413 try { 1414 $rrule = $vEvent->getAttribute('RRULE'); 1415 if (!is_array($rrule)) { 1416 $this->recurrence = new Horde_Date_Recurrence($this->start); 1417 if (strpos($rrule, '=') !== false) { 1418 $this->recurrence->fromRRule20($rrule); 1419 } else { 1420 $this->recurrence->fromRRule10($rrule); 1421 } 1422 1423 // Exceptions. EXDATE represents deleted events, just add the 1424 // exception, no new event is needed. 1425 $exdates = $vEvent->getAttributeValues('EXDATE'); 1426 if (is_array($exdates)) { 1427 foreach ($exdates as $exdate) { 1428 if (is_array($exdate)) { 1429 $this->recurrence->addException( 1430 (int)$exdate['year'], 1431 (int)$exdate['month'], 1432 (int)$exdate['mday']); 1433 } 1434 } 1435 } 1436 } 1437 } catch (Horde_Icalendar_Exception $e) {} 1438 1439 // RECURRENCE-ID indicates that this event represents an exception 1440 try { 1441 $recurrenceid = $vEvent->getAttribute('RECURRENCE-ID'); 1442 $originaldt = new Horde_Date($recurrenceid); 1443 $this->exceptionoriginaldate = $originaldt; 1444 $this->baseid = $this->uid; 1445 $this->uid = null; 1446 try { 1447 $originalEvent = $this->getDriver()->getByUID($this->baseid); 1448 if ($originalEvent->recurrence) { 1449 $originalEvent->recurrence->addException( 1450 $originaldt->format('Y'), 1451 $originaldt->format('m'), 1452 $originaldt->format('d') 1453 ); 1454 $originalEvent->save(); 1455 } 1456 } catch (Horde_Exception_NotFound $e) { 1457 throw new Kronolith_Exception(_("Unable to locate original event series.")); 1458 } 1459 } catch (Horde_Icalendar_Exception $e) {} 1460 } 1461 1462 /** 1463 * Imports the values for this event from a MS ActiveSync Message. 1464 * 1465 * @see Horde_ActiveSync_Message_Appointment 1466 */ 1467 public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message) 1468 { 1469 /* New event? */ 1470 if ($this->id === null) { 1471 $this->creator = $GLOBALS['registry']->getAuth(); 1472 } 1473 if (!$message->isGhosted('subject') && 1474 strlen($title = $message->getSubject())) { 1475 $this->title = $title; 1476 } 1477 if ($message->getProtocolVersion() == Horde_ActiveSync::VERSION_TWOFIVE && 1478 !$message->isGhosted('body') && 1479 strlen($description = $message->getBody())) { 1480 $this->description = $description; 1481 } elseif ($message->getProtocolVersion() > Horde_ActiveSync::VERSION_TWOFIVE && !$message->isGhosted('airsyncbasebody')) { 1482 if ($message->airsyncbasebody->type == Horde_ActiveSync::BODYPREF_TYPE_HTML) { 1483 $this->description = Horde_Text_Filter::filter($message->airsyncbasebody->data, 'Html2text'); 1484 } else { 1485 $this->description = $message->airsyncbasebody->data; 1486 } 1487 } 1488 1489 if (!$message->isGhosted('location') && 1490 strlen($location = $message->getLocation())) { 1491 $this->location = $location; 1492 } 1493 1494 /* Date/times */ 1495 $tz = !$message->isGhosted('timezone') 1496 ? $message->getTimezone() 1497 : $this->timezone; 1498 $dates = $message->getDatetime(); 1499 $this->start = !$message->isGhosted('starttime') 1500 ? clone($dates['start']) 1501 : $this->start; 1502 $this->start->setTimezone($tz); 1503 1504 $this->end = !$message->isGhosted('endtime') 1505 ? clone($dates['end']) 1506 : $this->end; 1507 $this->end->setTimezone($tz); 1508 1509 if (!$message->isGhosted('alldayevent')) { 1510 $this->allday = $dates['allday']; 1511 } 1512 if ($tz != date_default_timezone_get()) { 1513 $this->timezone = $tz; 1514 } 1515 1516 /* Sensitivity */ 1517 if (!$message->isGhosted('sensitivity')) { 1518 $this->private = ($message->getSensitivity() == Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE || $message->getSensitivity() == Horde_ActiveSync_Message_Appointment::SENSITIVITY_CONFIDENTIAL) ? true : false; 1519 } 1520 1521 /* Busy Status */ 1522 if (!$message->isGhosted('meetingstatus')) { 1523 if ($message->getMeetingStatus() == Horde_ActiveSync_Message_Appointment::MEETING_CANCELLED) { 1524 $status = Kronolith::STATUS_CANCELLED; 1525 } else { 1526 $status = $message->getBusyStatus(); 1527 switch ($status) { 1528 case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_BUSY: 1529 $status = Kronolith::STATUS_CONFIRMED; 1530 break; 1531 1532 case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE: 1533 $status = Kronolith::STATUS_FREE; 1534 break; 1535 1536 case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_TENTATIVE: 1537 $status = Kronolith::STATUS_TENTATIVE; 1538 break; 1539 // @TODO: not sure how "Out" should show in kronolith... 1540 case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_OUT: 1541 $status = Kronolith::STATUS_CONFIRMED; 1542 default: 1543 // EAS Specifies default should be free. 1544 $status = Kronolith::STATUS_FREE; 1545 } 1546 } 1547 $this->status = $status; 1548 } 1549 1550 /* Alarm */ 1551 if (!$message->isGhosted('reminder') && ($alarm = $message->getReminder())) { 1552 $this->alarm = $alarm; 1553 } 1554 1555 /* Recurrence */ 1556 if (!$message->isGhosted('recurrence') && ($rrule = $message->getRecurrence())) { 1557 /* Exceptions */ 1558 $kronolith_driver = $this->getDriver(); 1559 /* Since AS keeps exceptions as part of the original event, we need 1560 * to delete all existing exceptions and re-create them. The only 1561 * drawback to this is that the UIDs will change. */ 1562 $this->recurrence = $rrule; 1563 if (!empty($this->uid)) { 1564 $search = new StdClass(); 1565 $search->baseid = $this->uid; 1566 $results = $kronolith_driver->search($search); 1567 foreach ($results as $days) { 1568 foreach ($days as $exception) { 1569 $kronolith_driver->deleteEvent($exception->id); 1570 } 1571 } 1572 } 1573 1574 $erules = $message->getExceptions(); 1575 foreach ($erules as $rule){ 1576 /* Add exception to recurrence obj*/ 1577 $original = $rule->getExceptionStartTime(); 1578 $original->setTimezone($tz); 1579 $this->recurrence->addException($original->format('Y'), $original->format('m'), $original->format('d')); 1580 1581 /* Readd the exception event, if not deleted */ 1582 if (!$rule->deleted) { 1583 $event = $kronolith_driver->getEvent(); 1584 $times = $rule->getDatetime(); 1585 $event->start = $times['start']; 1586 $event->end = $times['end']; 1587 $event->start->setTimezone($tz); 1588 $event->end->setTimezone($tz); 1589 $event->allday = $times['allday']; 1590 $event->title = $rule->getSubject(); 1591 $event->title = empty($event->title) ? $this->title : $event->title; 1592 $event->description = $rule->getBody(); 1593 $event->description = empty($event->description) ? $this->description : $event->description; 1594 $event->baseid = $this->uid; 1595 $event->exceptionoriginaldate = $original; 1596 $event->initialized = true; 1597 if ($tz != date_default_timezone_get()) { 1598 $event->timezone = $tz; 1599 } 1600 $event->save(); 1601 } 1602 } 1603 } 1604 1605 /* Attendees */ 1606 if (!$message->isGhosted('attendees')) { 1607 $attendees = $message->getAttendees(); 1608 foreach ($attendees as $attendee) { 1609 switch ($attendee->status) { 1610 case Horde_ActiveSync_Message_Attendee::STATUS_ACCEPT: 1611 $response_code = Kronolith::RESPONSE_ACCEPTED; 1612 break; 1613 case Horde_ActiveSync_Message_Attendee::STATUS_DECLINE: 1614 $response_code = Kronolith::RESPONSE_DECLINED; 1615 break; 1616 case Horde_ActiveSync_Message_Attendee::STATUS_TENTATIVE: 1617 $response_code = Kronolith::RESPONSE_TENTATIVE; 1618 break; 1619 default: 1620 $response_code = Kronolith::RESPONSE_NONE; 1621 } 1622 switch ($attendee->type) { 1623 case Horde_ActiveSync_Message_Attendee::TYPE_REQUIRED: 1624 $part_type = Kronolith::PART_REQUIRED; 1625 break; 1626 case Horde_ActiveSync_Message_Attendee::TYPE_OPTIONAL: 1627 $part_type = Kronolith::PART_OPTIONAL; 1628 break; 1629 case Horde_ActiveSync_Message_Attendee::TYPE_RESOURCE: 1630 $part_type = Kronolith::PART_REQUIRED; 1631 } 1632 1633 $this->addAttendee($attendee->email, 1634 $part_type, 1635 $response_code, 1636 $attendee->name); 1637 } 1638 } 1639 1640 /* Categories (Tags) */ 1641 if (!$message->isGhosted('categories')) { 1642 $this->_tags = $message->getCategories(); 1643 } 1644 1645 // 14.1 1646 if ($message->getProtocolVersion() >= Horde_ActiveSync::VERSION_FOURTEENONE && 1647 !$message->isGhosted('onlinemeetingexternallink')) { 1648 $this->url = $message->onlinemeetingexternallink; 1649 } 1650 1651 /* Flag that we are initialized */ 1652 $this->initialized = true; 1653 } 1654 1655 /** 1656 * Export this event as a MS ActiveSync Message 1657 * 1658 * @param array $options Options: 1659 * - protocolversion: (float) The EAS version to support 1660 * DEFAULT: 2.5 1661 * - bodyprefs: (array) A BODYPREFERENCE array. 1662 * DEFAULT: none (No body prefs enforced). 1663 * - truncation: (integer) Truncate event body to this length 1664 * DEFAULT: none (No truncation). 1665 * 1666 * @return Horde_ActiveSync_Message_Appointment 1667 */ 1668 public function toASAppointment(array $options = array()) 1669 { 1670 global $prefs, $registry; 1671 1672 $message = new Horde_ActiveSync_Message_Appointment( 1673 array( 1674 'logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger'), 1675 'protocolversion' => $options['protocolversion'] 1676 ) 1677 ); 1678 1679 if (!$this->isPrivate()) { 1680 // Handle body/truncation 1681 if (!empty($options['bodyprefs'])) { 1682 if (Horde_String::length($this->description) > 0) { 1683 $bp = $options['bodyprefs']; 1684 $note = new Horde_ActiveSync_Message_AirSyncBaseBody(); 1685 // No HTML supported. Always use plaintext. 1686 $note->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN; 1687 if (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize'])) { 1688 $truncation = $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']; 1689 } elseif (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_HTML])) { 1690 $truncation = $bp[Horde_ActiveSync::BODYPREF_TYPE_HTML]['truncationsize']; 1691 $this->description = Horde_Text_Filter::filter($this->description, 'Text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO)); 1692 } else { 1693 $truncation = false; 1694 } 1695 if ($truncation && Horde_String::length($this->description) > $truncation) { 1696 $note->data = Horde_String::substr($this->description, 0, $truncation); 1697 $note->truncated = 1; 1698 } else { 1699 $note->data = $this->description; 1700 } 1701 $note->estimateddatasize = Horde_String::length($this->description); 1702 $message->airsyncbasebody = $note; 1703 } 1704 } else { 1705 $message->setBody($this->description); 1706 } 1707 $message->setLocation($this->location); 1708 } 1709 1710 $message->setSubject($this->getTitle()); 1711 $message->setDatetime(array( 1712 'start' => $this->start, 1713 'end' => $this->end, 1714 'allday' => $this->isAllDay()) 1715 ); 1716 $message->setTimezone($this->start); 1717 1718 // Organizer 1719 if (count($this->attendees)) { 1720 if ($this->creator == $registry->getAuth()) { 1721 $as_ident = $prefs->getValue('activesync_identity') == 'horde' 1722 ? $prefs->getValue('default_identity') 1723 : $prefs->getValue('activesync_identity'); 1724 1725 $name = $GLOBALS['injector'] 1726 ->getInstance('Horde_Core_Factory_Identity') 1727 ->create($this->creator)->getValue('fullname', $as_ident); 1728 $email = $GLOBALS['injector'] 1729 ->getInstance('Horde_Core_Factory_Identity') 1730 ->create($this->creator)->getValue('from_addr', $as_ident); 1731 } else { 1732 $name = Kronolith::getUserName($this->creator); 1733 $email = Kronolith::getUserEmail($this->creator); 1734 } 1735 $message->setOrganizer(array( 1736 'name' => $name, 1737 'email' => $email) 1738 ); 1739 } 1740 1741 // Privacy 1742 $message->setSensitivity($this->private ? 1743 Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE : 1744 Horde_ActiveSync_Message_Appointment::SENSITIVITY_NORMAL); 1745 1746 // Busy Status 1747 switch ($this->status) { 1748 case Kronolith::STATUS_CANCELLED: 1749 $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE; 1750 break; 1751 case Kronolith::STATUS_CONFIRMED: 1752 $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_BUSY; 1753 break; 1754 case Kronolith::STATUS_TENTATIVE: 1755 $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_TENTATIVE; 1756 case Kronolith::STATUS_FREE: 1757 case Kronolith::STATUS_NONE: 1758 $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE; 1759 } 1760 $message->setBusyStatus($status); 1761 1762 // DTStamp 1763 $message->setDTStamp($_SERVER['REQUEST_TIME']); 1764 1765 // Recurrence 1766 if ($this->recurs()) { 1767 $message->setRecurrence($this->recurrence, $GLOBALS['prefs']->getValue('week_start_monday')); 1768 1769 /* Exceptions are tricky. Exceptions, even those that represent 1770 * deleted instances of a recurring event, must be added. To do this 1771 * we query the storage for all the events that represent exceptions 1772 * (those with the baseid == $this->uid) and then remove the 1773 * exceptionoriginaldate from the list of exceptions we know about. 1774 * Any dates left in this list when we are done, must represent 1775 * deleted instances of this recurring event.*/ 1776 if (!empty($this->recurrence) && $exceptions = $this->recurrence->getExceptions()) { 1777 $results = $this->boundExceptions(); 1778 foreach ($results as $exception) { 1779 $e = new Horde_ActiveSync_Message_Exception(array( 1780 'protocolversion' => $options['protocolversion'])); 1781 $e->setDateTime(array( 1782 'start' => $exception->start, 1783 'end' => $exception->end, 1784 'allday' => $exception->isAllDay())); 1785 1786 // The start time of the *original* recurring event 1787 $e->setExceptionStartTime($exception->exceptionoriginaldate); 1788 $originaldate = $exception->exceptionoriginaldate->format('Ymd'); 1789 $key = array_search($originaldate, $exceptions); 1790 if ($key !== false) { 1791 unset($exceptions[$key]); 1792 } 1793 1794 // Remaining properties that could be different 1795 $e->setSubject($exception->getTitle()); 1796 if (!$exception->isPrivate()) { 1797 $e->setLocation($exception->location); 1798 $e->setBody($exception->description); 1799 } 1800 1801 $e->setSensitivity($exception->private ? 1802 Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE : 1803 Horde_ActiveSync_Message_Appointment::SENSITIVITY_NORMAL); 1804 $e->setReminder($exception->alarm); 1805 $e->setDTStamp($_SERVER['REQUEST_TIME']); 1806 1807 if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWELVEONE) { 1808 switch ($exception->status) { 1809 case Kronolith::STATUS_TENTATIVE; 1810 $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_TENTATIVE; 1811 break; 1812 case Kronolith::STATUS_NONE: 1813 $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NORESPONSE; 1814 break; 1815 case Kronolith::STATUS_CONFIRMED: 1816 $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_ACCEPTED; 1817 break; 1818 default: 1819 $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NONE; 1820 } 1821 } 1822 1823 // Tags/Categories 1824 if (!$exception->isPrivate()) { 1825 foreach ($exception->tags as $tag) { 1826 $e->addCategory($tag); 1827 } 1828 } 1829 1830 $message->addexception($e); 1831 } 1832 1833 // Any dates left in $exceptions must be deleted exceptions 1834 foreach ($exceptions as $deleted) { 1835 $e = new Horde_ActiveSync_Message_Exception(array( 1836 'protocolversion' => $options['protocolversion'])); 1837 // Kronolith stores the date only, but some AS clients need 1838 // the datetime. 1839 list($year, $month, $mday) = sscanf($deleted, '%04d%02d%02d'); 1840 $st = clone $this->start; 1841 $st->year = $year; 1842 $st->month = $month; 1843 $st->mday = $mday; 1844 $e->setExceptionStartTime($st); 1845 $e->deleted = true; 1846 $message->addException($e); 1847 } 1848 } 1849 } 1850 1851 // Attendees 1852 if (!$this->isPrivate() && count($this->attendees)) { 1853 $message->setMeetingStatus( 1854 $this->status == Kronolith::STATUS_CANCELLED 1855 ? Horde_ActiveSync_Message_Appointment::MEETING_CANCELLED 1856 : Horde_ActiveSync_Message_Appointment::MEETING_IS_MEETING 1857 ); 1858 foreach ($this->attendees as $email => $properties) { 1859 $attendee = new Horde_ActiveSync_Message_Attendee(array( 1860 'protocolversion' => $options['protocolversion'])); 1861 $adr_obj = new Horde_Mail_Rfc822_Address($email); 1862 $attendee->name = $adr_obj->label; 1863 $attendee->email = $adr_obj->bare_address; 1864 1865 // AS only has required or optional, and only EAS Version > 2.5 1866 if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) { 1867 $attendee->type = ($properties['attendance'] !== Kronolith::PART_REQUIRED 1868 ? Horde_ActiveSync_Message_Attendee::TYPE_OPTIONAL 1869 : Horde_ActiveSync_Message_Attendee::TYPE_REQUIRED); 1870 1871 switch ($properties['response']) { 1872 case Kronolith::RESPONSE_NONE: 1873 $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_NORESPONSE; 1874 break; 1875 case Kronolith::RESPONSE_ACCEPTED: 1876 $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_ACCEPT; 1877 break; 1878 case Kronolith::RESPONSE_DECLINED: 1879 $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_DECLINE; 1880 break; 1881 case Kronolith::RESPONSE_TENTATIVE: 1882 $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_TENTATIVE; 1883 break; 1884 default: 1885 $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_UNKNOWN; 1886 } 1887 } 1888 1889 $message->addAttendee($attendee); 1890 } 1891 } elseif ($this->status == Kronolith::STATUS_CANCELLED) { 1892 $message->setMeetingStatus(Horde_ActiveSync_Message_Appointment::MEETING_CANCELLED); 1893 } else { 1894 $message->setMeetingStatus(Horde_ActiveSync_Message_Appointment::MEETING_NOT_MEETING); 1895 } 1896 1897 // Resources 1898 if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) { 1899 $r = $this->getResources(); 1900 foreach ($r as $id => $data) { 1901 $resource = Kronolith::getDriver('Resource')->getResource($id); 1902 // EAS *REQUIRES* an email field for Resources. If it is missing 1903 // a number of clients will fail, losing push. 1904 if ($resource->get('email')) { 1905 $attendee = new Horde_ActiveSync_Message_Attendee(array( 1906 'protocolversion' => $options['protocolversion'])); 1907 $attendee->email = $resource->get('email'); 1908 $attendee->type = Horde_ActiveSync_Message_Attendee::TYPE_RESOURCE; 1909 $attendee->name = $data['name']; 1910 $attendee->status = $data['response']; 1911 $message->addAttendee($attendee); 1912 } 1913 } 1914 } 1915 1916 // Reminder 1917 if ($this->alarm) { 1918 $message->setReminder($this->alarm); 1919 } 1920 1921 // Categories (tags) 1922 if (!$this->isPrivate()) { 1923 foreach ($this->tags as $tag) { 1924 $message->addCategory($tag); 1925 } 1926 } 1927 1928 // EAS 14 1929 if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWELVEONE) { 1930 // We don't track the actual responses we sent to other's invitations. 1931 // Set this based on the status flag. 1932 switch ($this->status) { 1933 case Kronolith::STATUS_TENTATIVE; 1934 $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_TENTATIVE; 1935 break; 1936 case Kronolith::STATUS_NONE: 1937 $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NORESPONSE; 1938 break; 1939 case Kronolith::STATUS_CONFIRMED: 1940 $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_ACCEPTED; 1941 break; 1942 default: 1943 $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NONE; 1944 } 1945 } 1946 1947 // 14.1 1948 if ($options['protocolversion'] >= Horde_ActiveSync::VERSION_FOURTEENONE) { 1949 $message->onlinemeetingexternallink = $this->url; 1950 } 1951 1952 return $message; 1953 } 1954 1955 /** 1956 * Imports the values for this event from an array of values. 1957 * 1958 * @param array $hash Array containing all the values. 1959 * 1960 * @throws Kronolith_Exception 1961 */ 1962 public function fromHash($hash) 1963 { 1964 // See if it's a new event. 1965 if ($this->id === null) { 1966 $this->creator = $GLOBALS['registry']->getAuth(); 1967 } 1968 1969 if (!empty($hash['title'])) { 1970 $this->title = $hash['title']; 1971 } else { 1972 throw new Kronolith_Exception(_("Events must have a title.")); 1973 } 1974 1975 $this->start = null; 1976 if (!empty($hash['start_date'])) { 1977 $date = array_map('intval', explode('-', $hash['start_date'])); 1978 if (empty($hash['start_time'])) { 1979 $time = array(0, 0, 0); 1980 } else { 1981 $time = array_map('intval', explode(':', $hash['start_time'])); 1982 if (count($time) == 2) { 1983 $time[2] = 0; 1984 } 1985 } 1986 if (count($time) == 3 && count($date) == 3 && 1987 !empty($date[1]) && !empty($date[2])) { 1988 if ($date[0] < 100) { 1989 $date[0] += (date('Y') / 100 | 0) * 100; 1990 } 1991 $this->start = new Horde_Date( 1992 array( 1993 'year' => $date[0], 1994 'month' => $date[1], 1995 'mday' => $date[2], 1996 'hour' => $time[0], 1997 'min' => $time[1], 1998 'sec' => $time[2] 1999 ), 2000 isset($hash['timezone']) ? $hash['timezone'] : null 2001 ); 2002 } 2003 } 2004 if (!isset($this->start)) { 2005 throw new Kronolith_Exception(_("Events must have a start date.")); 2006 } 2007 2008 if (empty($hash['duration'])) { 2009 if (empty($hash['end_date'])) { 2010 $hash['end_date'] = $hash['start_date']; 2011 } 2012 if (empty($hash['end_time'])) { 2013 $hash['end_time'] = $hash['start_time']; 2014 } 2015 } else { 2016 $weeks = str_replace('W', '', $hash['duration'][1]); 2017 $days = str_replace('D', '', $hash['duration'][2]); 2018 $hours = str_replace('H', '', $hash['duration'][4]); 2019 $minutes = isset($hash['duration'][5]) ? str_replace('M', '', $hash['duration'][5]) : 0; 2020 $seconds = isset($hash['duration'][6]) ? str_replace('S', '', $hash['duration'][6]) : 0; 2021 $hash['duration'] = ($weeks * 60 * 60 * 24 * 7) + ($days * 60 * 60 * 24) + ($hours * 60 * 60) + ($minutes * 60) + $seconds; 2022 $this->end = new Horde_Date($this->start); 2023 $this->end->sec += $hash['duration']; 2024 } 2025 if (!empty($hash['end_date'])) { 2026 $date = array_map('intval', explode('-', $hash['end_date'])); 2027 if (empty($hash['end_time'])) { 2028 $time = array(0, 0, 0); 2029 } else { 2030 $time = array_map('intval', explode(':', $hash['end_time'])); 2031 if (count($time) == 2) { 2032 $time[2] = 0; 2033 } 2034 } 2035 if (count($time) == 3 && count($date) == 3 && 2036 !empty($date[1]) && !empty($date[2])) { 2037 if ($date[0] < 100) { 2038 $date[0] += (date('Y') / 100 | 0) * 100; 2039 } 2040 $this->end = new Horde_Date( 2041 array( 2042 'year' => $date[0], 2043 'month' => $date[1], 2044 'mday' => $date[2], 2045 'hour' => $time[0], 2046 'min' => $time[1], 2047 'sec' => $time[2] 2048 ), 2049 isset($hash['timezone']) ? $hash['timezone'] : null 2050 ); 2051 } 2052 } 2053 2054 if (!empty($hash['alarm'])) { 2055 $this->alarm = (int)$hash['alarm']; 2056 } elseif (!empty($hash['alarm_date']) && 2057 !empty($hash['alarm_time'])) { 2058 $date = array_map('intval', explode('-', $hash['alarm_date'])); 2059 $time = array_map('intval', explode(':', $hash['alarm_time'])); 2060 if (count($time) == 2) { 2061 $time[2] = 0; 2062 } 2063 if (count($time) == 3 && count($date) == 3 && 2064 !empty($date[1]) && !empty($date[2])) { 2065 $alarm = new Horde_Date( 2066 array( 2067 'year' => $date[0], 2068 'month' => $date[1], 2069 'mday' => $date[2], 2070 'hour' => $time[0], 2071 'min' => $time[1], 2072 'sec' => $time[2] 2073 ), 2074 isset($hash['timezone']) ? $hash['timezone'] : null 2075 ); 2076 $this->alarm = ($this->start->timestamp() - $alarm->timestamp()) / 60; 2077 } 2078 } 2079 2080 $this->allday = !empty($hash['allday']); 2081 2082 if (!empty($hash['description'])) { 2083 $this->description = $hash['description']; 2084 } 2085 2086 if (!empty($hash['location'])) { 2087 $this->location = $hash['location']; 2088 } 2089 2090 // Import once we support organizers. 2091 /* 2092 if (!empty($hash['organizer'])) { 2093 $this->organizer = $hash['organizer']; 2094 } 2095 */ 2096 2097 if (!empty($hash['private'])) { 2098 $this->private = true; 2099 } 2100 2101 if (!empty($hash['recur_type'])) { 2102 $this->recurrence = new Horde_Date_Recurrence($this->start); 2103 $this->recurrence->setRecurType($hash['recur_type']); 2104 if (!empty($hash['recur_count'])) { 2105 $this->recurrence->setRecurCount($hash['recur_count']); 2106 } elseif (!empty($hash['recur_end_date'])) { 2107 $date = array_map('intval', explode('-', $hash['recur_end_date'])); 2108 if (count($date) == 3 && !empty($date[1]) && !empty($date[2])) { 2109 $this->recurrence->setRecurEnd( 2110 new Horde_Date(array( 2111 'year' => $date[0], 2112 'month' => $date[1], 2113 'mday' => $date[2] 2114 )) 2115 ); 2116 } 2117 } 2118 if (!empty($hash['recur_interval'])) { 2119 $this->recurrence->setRecurInterval($hash['recur_interval']); 2120 } 2121 if (!empty($hash['recur_data'])) { 2122 $this->recurrence->setRecurOnDay($hash['recur_data']); 2123 } 2124 if (!empty($hash['recur_exceptions'])) { 2125 foreach ($hash['recur_exceptions'] as $exception) { 2126 $parts = explode('-', $exception); 2127 if (count($parts) == 3) { 2128 $this->recurrence->addException($parts[0], $parts[1], $parts[2]); 2129 } 2130 } 2131 } 2132 } 2133 2134 if (isset($hash['sequence'])) { 2135 $this->sequence = $hash['sequence']; 2136 } 2137 2138 if (!empty($hash['tags'])) { 2139 $this->tags = $hash['tags']; 2140 } 2141 2142 if (!empty($hash['timezone'])) { 2143 $this->timezone = $hash['timezone']; 2144 } 2145 2146 if (!empty($hash['uid'])) { 2147 $this->uid = $hash['uid']; 2148 } 2149 2150 $this->initialized = true; 2151 } 2152 2153 /** 2154 * Returns an alarm hash of this event suitable for Horde_Alarm. 2155 * 2156 * @param Horde_Date $time Time of alarm. 2157 * @param string $user The user to return alarms for. 2158 * @param Prefs $prefs A Prefs instance. 2159 * 2160 * @return array Alarm hash or null. 2161 */ 2162 public function toAlarm($time, $user = null, $prefs = null) 2163 { 2164 if (!$this->alarm || $this->status == Kronolith::STATUS_CANCELLED) { 2165 return; 2166 } 2167 2168 if ($this->recurs()) { 2169 $eventDate = $this->recurrence->nextRecurrence($time); 2170 if (!$eventDate || ($eventDate && $this->recurrence->hasException($eventDate->year, $eventDate->month, $eventDate->mday))) { 2171 return; 2172 } 2173 $start = clone $eventDate; 2174 $diff = Date_Calc::dateDiff( 2175 $this->start->mday, 2176 $this->start->month, 2177 $this->start->year, 2178 $this->end->mday, 2179 $this->end->month, 2180 $this->end->year 2181 ); 2182 if ($diff == -1) { 2183 $diff = 0; 2184 } 2185 $end = new Horde_Date(array( 2186 'year' => $start->year, 2187 'month' => $start->month, 2188 'mday' => $start->mday + $diff, 2189 'hour' => $this->end->hour, 2190 'min' => $this->end->min, 2191 'sec' => $this->end->sec) 2192 ); 2193 } else { 2194 $start = clone $this->start; 2195 $end = clone $this->end; 2196 } 2197 2198 $serverName = $_SERVER['SERVER_NAME']; 2199 $serverConf = $GLOBALS['conf']['server']['name']; 2200 if (!empty($GLOBALS['conf']['reminder']['server_name'])) { 2201 $_SERVER['SERVER_NAME'] = $GLOBALS['conf']['server']['name'] = $GLOBALS['conf']['reminder']['server_name']; 2202 } 2203 2204 if (empty($user)) { 2205 $user = $GLOBALS['registry']->getAuth(); 2206 } 2207 if (empty($prefs)) { 2208 $prefs = $GLOBALS['prefs']; 2209 } 2210 2211 $methods = !empty($this->methods) ? $this->methods : @unserialize($prefs->getValue('event_alarms')); 2212 if (isset($methods['notify'])) { 2213 $methods['notify']['show'] = array( 2214 '__app' => $GLOBALS['registry']->getApp(), 2215 'event' => $this->id, 2216 'calendar' => $this->calendar); 2217 $methods['notify']['ajax'] = 'event:' . $this->calendarType . '|' . $this->calendar . ':' . $this->id . ':' . $start->dateString(); 2218 if (!empty($methods['notify']['sound'])) { 2219 if ($methods['notify']['sound'] == 'on') { 2220 // Handle boolean sound preferences. 2221 $methods['notify']['sound'] = (string)Horde_Themes::sound('theetone.wav'); 2222 } else { 2223 // Else we know we have a sound name that can be 2224 // served from Horde. 2225 $methods['notify']['sound'] = (string)Horde_Themes::sound($methods['notify']['sound']); 2226 } 2227 } 2228 if ($this->isAllDay()) { 2229 if ($start->compareDate($end) == 0) { 2230 $methods['notify']['subtitle'] = sprintf(_("On %s"), '<strong>' . $start->strftime($prefs->getValue('date_format')) . '</strong>'); 2231 } else { 2232 $methods['notify']['subtitle'] = sprintf(_("From %s to %s"), '<strong>' . $start->strftime($prefs->getValue('date_format')) . '</strong>', '<strong>' . $end->strftime($prefs->getValue('date_format')) . '</strong>'); 2233 } 2234 } else { 2235 $methods['notify']['subtitle'] = sprintf(_("From %s at %s to %s at %s"), '<strong>' . $start->strftime($prefs->getValue('date_format')), $start->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia') . '</strong>', '<strong>' . $end->strftime($prefs->getValue('date_format')), $this->end->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia') . '</strong>'); 2236 } 2237 } 2238 if (isset($methods['mail'])) { 2239 $image = Kronolith::getImagePart('big_alarm.png'); 2240 2241 $view = new Horde_View(array('templatePath' => KRONOLITH_TEMPLATES . '/alarm', 'encoding' => 'UTF-8')); 2242 new Horde_View_Helper_Text($view); 2243 $view->event = $this; 2244 $view->imageId = $image->getContentId(); 2245 $view->user = $user; 2246 $view->dateFormat = $prefs->getValue('date_format'); 2247 $view->timeFormat = $prefs->getValue('twentyFour') ? 'H:i' : 'h:ia'; 2248 $view->start = $start; 2249 if (!$prefs->isLocked('event_reminder')) { 2250 $view->prefsUrl = Horde::url($GLOBALS['registry']->getServiceLink('prefs', 'kronolith'), true)->remove(session_name()); 2251 } 2252 if ($this->attendees) { 2253 $view->attendees = Kronolith::getAttendeeEmailList($this->attendees)->addresses; 2254 } 2255 2256 $methods['mail']['mimepart'] = Kronolith::buildMimeMessage($view, 'mail', $image); 2257 } 2258 if (isset($methods['desktop'])) { 2259 if ($this->isAllDay()) { 2260 if ($this->start->compareDate($this->end) == 0) { 2261 $methods['desktop']['subtitle'] = sprintf(_("On %s"), $start->strftime($prefs->getValue('date_format'))); 2262 } else { 2263 $methods['desktop']['subtitle'] = sprintf(_("From %s to %s"), $start->strftime($prefs->getValue('date_format')), $end->strftime($prefs->getValue('date_format'))); 2264 } 2265 } else { 2266 $methods['desktop']['subtitle'] = sprintf(_("From %s at %s to %s at %s"), $start->strftime($prefs->getValue('date_format')), $start->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia'), $end->strftime($prefs->getValue('date_format')), $this->end->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia')); 2267 } 2268 $methods['desktop']['url'] = strval($this->getViewUrl(array(), true, false)); 2269 } 2270 2271 $alarmStart = clone $start; 2272 $alarmStart->min -= $this->alarm; 2273 $alarm = array( 2274 'id' => $this->uid, 2275 'user' => $user, 2276 'start' => $alarmStart, 2277 'end' => $end, 2278 'methods' => array_keys($methods), 2279 'params' => $methods, 2280 'title' => $this->getTitle($user), 2281 'text' => $this->description, 2282 'instanceid' => $this->recurs() ? $eventDate->dateString() : null); 2283 2284 $_SERVER['SERVER_NAME'] = $serverName; 2285 $GLOBALS['conf']['server']['name'] = $serverConf; 2286 2287 return $alarm; 2288 } 2289 2290 /** 2291 * Returns a simple object suitable for json transport representing this 2292 * event. 2293 * 2294 * Possible properties are: 2295 * - t: title 2296 * - d: description 2297 * - c: calendar id 2298 * - s: start date 2299 * - e: end date 2300 * - fi: first day of a multi-day event 2301 * - la: last day of a multi-day event 2302 * - x: status (Kronolith::STATUS_* constant) 2303 * - al: all-day? 2304 * - bg: background color 2305 * - fg: foreground color 2306 * - pe: edit permissions? 2307 * - pd: delete permissions? 2308 * - vl: variable, i.e. editable length? 2309 * - a: alarm text or minutes 2310 * - r: recurrence type (Horde_Date_Recurrence::RECUR_* constant) 2311 * - bid: The baseid for an event representing an exception 2312 * - eod: The original date that an exception is replacing 2313 * - ic: icon 2314 * - ln: link 2315 * - aj: ajax link 2316 * - id: event id 2317 * - ty: calendar type (driver) 2318 * - l: location 2319 * - u: url 2320 * - sd: formatted start date 2321 * - st: formatted start time 2322 * - ed: formatted end date 2323 * - et: formatted end time 2324 * - at: attendees 2325 * - rs: resources 2326 * - tg: tag list, 2327 * - mt: meeting (Boolean true if event has attendees, false otherwise). 2328 * 2329 * @param boolean $allDay If not null, overrides whether the event is 2330 * an all-day event. 2331 * @param boolean $full Whether to return all event details. 2332 * @param string $time_format The date() format to use for time formatting. 2333 * 2334 * @return stdClass A simple object. 2335 */ 2336 public function toJson($allDay = null, $full = false, $time_format = 'H:i') 2337 { 2338 $json = new stdClass; 2339 $json->uid = $this->uid; 2340 $json->t = $this->getTitle(); 2341 $json->c = $this->calendar; 2342 $json->s = $this->start->toJson(); 2343 $json->e = $this->end->toJson(); 2344 $json->fi = $this->first; 2345 $json->la = $this->last; 2346 $json->x = (int)$this->status; 2347 $json->al = is_null($allDay) ? $this->isAllDay() : $allDay; 2348 $json->pe = $this->hasPermission(Horde_Perms::EDIT); 2349 $json->pd = $this->hasPermission(Horde_Perms::DELETE); 2350 $json->l = $this->getLocation(); 2351 $json->mt = !empty($this->attendees); 2352 $json->sort = sprintf( 2353 '%010s%06s', 2354 $this->originalStart->timestamp(), 2355 240000 - $this->end->format('His') 2356 ); 2357 2358 if ($this->icon) { 2359 $json->ic = $this->icon; 2360 } 2361 if ($this->alarm) { 2362 if ($this->alarm % 10080 == 0) { 2363 $alarm_value = $this->alarm / 10080; 2364 $json->a = sprintf(ngettext("%d week", "%d weeks", $alarm_value), $alarm_value); 2365 } elseif ($this->alarm % 1440 == 0) { 2366 $alarm_value = $this->alarm / 1440; 2367 $json->a = sprintf(ngettext("%d day", "%d days", $alarm_value), $alarm_value); 2368 } elseif ($this->alarm % 60 == 0) { 2369 $alarm_value = $this->alarm / 60; 2370 $json->a = sprintf(ngettext("%d hour", "%d hours", $alarm_value), $alarm_value); 2371 } else { 2372 $alarm_value = $this->alarm; 2373 $json->a = sprintf(ngettext("%d minute", "%d minutes", $alarm_value), $alarm_value); 2374 } 2375 } 2376 if ($this->recurs()) { 2377 $json->r = $this->recurrence->getRecurType(); 2378 } elseif ($this->baseid) { 2379 $json->bid = $this->baseid; 2380 if ($this->exceptionoriginaldate) { 2381 $json->eod = sprintf(_("%s at %s"), $this->exceptionoriginaldate->strftime($GLOBALS['prefs']->getValue('date_format')), $this->exceptionoriginaldate->strftime(($GLOBALS['prefs']->getValue('twentyFour') ? '%H:%M' : '%I:%M %p'))); 2382 } 2383 } 2384 if ($this->_resources) { 2385 $json->rs = $this->_resources; 2386 } 2387 if ($full) { 2388 $json->id = $this->id; 2389 $json->ty = $this->calendarType; 2390 $json->sd = $this->start->strftime('%x'); 2391 $json->st = $this->start->format($time_format); 2392 $json->ed = $this->end->strftime('%x'); 2393 $json->et = $this->end->format($time_format); 2394 $json->tz = $this->timezone; 2395 $json->a = $this->alarm; 2396 $json->pv = $this->private; 2397 if ($this->recurs()) { 2398 $json->r = $this->recurrence->toJson(); 2399 } 2400 if (!$this->isPrivate()) { 2401 $json->d = $this->description; 2402 $json->u = htmlentities($this->url); 2403 $json->uhl = $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter( 2404 $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($this->url, 'linkurls'), 2405 'Xss' 2406 ); 2407 $json->tg = array_values($this->tags); 2408 $json->gl = $this->geoLocation; 2409 if ($this->attendees) { 2410 $attendees = array(); 2411 foreach ($this->attendees as $email => $info) { 2412 $tmp = new Horde_Mail_Rfc822_Address($email); 2413 if (!empty($info['name'])) { 2414 $tmp->personal = $info['name']; 2415 } 2416 2417 $attendees[] = array( 2418 'a' => intval($info['attendance']), 2419 'e' => $tmp->bare_address, 2420 'r' => intval($info['response']), 2421 'l' => strval($tmp) 2422 ); 2423 $json->at = $attendees; 2424 } 2425 } 2426 } 2427 if ($this->methods) { 2428 $json->m = $this->methods; 2429 } 2430 } 2431 2432 return $json; 2433 } 2434 2435 /** 2436 * Checks if the current event is already present in the calendar. 2437 * 2438 * Does the check based on the uid. 2439 * 2440 * @return boolean True if event exists, false otherwise. 2441 */ 2442 public function exists() 2443 { 2444 if (!isset($this->uid) || !isset($this->calendar)) { 2445 return false; 2446 } 2447 try { 2448 $eventID = $this->getDriver()->exists($this->uid, $this->calendar); 2449 if (!$eventID) { 2450 return false; 2451 } 2452 } catch (Exception $e) { 2453 return false; 2454 } 2455 $this->id = $eventID; 2456 return true; 2457 } 2458 2459 /** 2460 * Converts this event between the event's and the local timezone. 2461 * 2462 * @param boolean $to_orginal If true converts to the event's timezone. 2463 */ 2464 public function setTimezone($to_original) 2465 { 2466 if (!$this->timezone || !$this->getDriver()->supportsTimezones()) { 2467 return; 2468 } 2469 $timezone = $to_original ? $this->timezone : date_default_timezone_get(); 2470 $this->start->setTimezone($timezone); 2471 $this->end->setTimezone($timezone); 2472 if ($this->recurs() && $this->recurrence->hasRecurEnd()) { 2473 /* @todo Check if have to go through all recurrence 2474 exceptions too. */ 2475 $this->recurrence->start->setTimezone($timezone); 2476 $this->recurrence->recurEnd->setTimezone($timezone); 2477 } 2478 } 2479 2480 public function getDuration() 2481 { 2482 if (isset($this->_duration)) { 2483 return $this->_duration; 2484 } 2485 2486 if ($this->start && $this->end) { 2487 $dur_day_match = Date_Calc::dateDiff($this->start->mday, 2488 $this->start->month, 2489 $this->start->year, 2490 $this->end->mday, 2491 $this->end->month, 2492 $this->end->year); 2493 $dur_hour_match = $this->end->hour - $this->start->hour; 2494 $dur_min_match = $this->end->min - $this->start->min; 2495 while ($dur_min_match < 0) { 2496 $dur_min_match += 60; 2497 --$dur_hour_match; 2498 } 2499 while ($dur_hour_match < 0) { 2500 $dur_hour_match += 24; 2501 --$dur_day_match; 2502 } 2503 } else { 2504 $dur_day_match = 0; 2505 $dur_hour_match = 1; 2506 $dur_min_match = 0; 2507 } 2508 2509 $this->_duration = new stdClass; 2510 $this->_duration->day = $dur_day_match; 2511 $this->_duration->hour = $dur_hour_match; 2512 $this->_duration->min = $dur_min_match; 2513 $this->_duration->wholeDay = $this->isAllDay(); 2514 2515 return $this->_duration; 2516 } 2517 2518 /** 2519 * Returns whether this event is a recurring event. 2520 * 2521 * @return boolean True if this is a recurring event. 2522 */ 2523 public function recurs() 2524 { 2525 return isset($this->recurrence) && 2526 !$this->recurrence->hasRecurType(Horde_Date_Recurrence::RECUR_NONE) && 2527 empty($this->baseid); 2528 } 2529 2530 /** 2531 * Returns a description of this event's recurring type. 2532 * 2533 * @return string Human readable recurring type. 2534 */ 2535 public function getRecurName() 2536 { 2537 if (empty($this->baseid)) { 2538 return $this->recurs() 2539 ? $this->recurrence->getRecurName() 2540 : _("No recurrence"); 2541 } else { 2542 return _("Exception"); 2543 } 2544 } 2545 2546 /** 2547 * Returns a correcty formatted exception date for recurring events and a 2548 * link to delete this exception. 2549 * 2550 * @param string $date Exception in the format Ymd. 2551 * 2552 * @return string The formatted date and delete link. 2553 */ 2554 public function exceptionLink($date) 2555 { 2556 if (!preg_match('/(\d{4})(\d{2})(\d{2})/', $date, $match)) { 2557 return ''; 2558 } 2559 $horde_date = new Horde_Date(array('year' => $match[1], 2560 'month' => $match[2], 2561 'mday' => $match[3])); 2562 $formatted = $horde_date->strftime($GLOBALS['prefs']->getValue('date_format')); 2563 return $formatted 2564 . Horde::url('edit.php') 2565 ->add(array('calendar' => $this->calendarType . '_' .$this->calendar, 2566 'eventID' => $this->id, 2567 'del_exception' => $date, 2568 'url' => Horde_Util::getFormData('url'))) 2569 ->link(array('title' => sprintf(_("Delete exception on %s"), $formatted))) 2570 . Horde::img('delete-small.png', _("Delete")) 2571 . '</a>'; 2572 } 2573 2574 /** 2575 * Returns a list of exception dates for recurring events including links 2576 * to delete them. 2577 * 2578 * @return string List of exception dates and delete links. 2579 */ 2580 public function exceptionsList() 2581 { 2582 $exceptions = $this->recurrence->getExceptions(); 2583 asort($exceptions); 2584 return implode(', ', array_map(array($this, 'exceptionLink'), $exceptions)); 2585 } 2586 2587 /** 2588 * Returns a list of events that represent exceptions to this event's 2589 * recurrence series, if any. If this event does not recur, an empty array 2590 * is returned. 2591 * 2592 * @param boolean $flat If true (the default), returns a flat array 2593 * containing Kronolith_Event objects. If false, 2594 * results are in the format of listEvents calls. @see 2595 * Kronolith::listEvents(). 2596 * 2597 * @return array An array of Kronolith_Event objects whose baseid property 2598 * is equal to this event's uid. I.e., it is a bound 2599 * exception. 2600 * 2601 * @since 4.2.2 2602 */ 2603 public function boundExceptions($flat = true) 2604 { 2605 if (!$this->recurrence || !$this->uid) { 2606 return array(); 2607 } 2608 $return = array(); 2609 $search = new stdClass(); 2610 $search->baseid = $this->uid; 2611 $results = $this->getDriver()->search($search); 2612 2613 if (!$flat) { 2614 return $results; 2615 } 2616 2617 foreach ($results as $days) { 2618 foreach ($days as $exception) { 2619 $return[] = $exception; 2620 } 2621 } 2622 2623 return $return; 2624 } 2625 2626 /** 2627 * Returns whether the event should be considered private. 2628 * 2629 * @param string $user The current user. If omitted, uses the current user. 2630 * 2631 * @return boolean Whether to consider the event as private. 2632 */ 2633 public function isPrivate($user = null) 2634 { 2635 global $registry; 2636 2637 if ($user === null) { 2638 $user = $registry->getAuth(); 2639 } 2640 2641 // Never private if private is not true or if the current user is the 2642 // event creator. 2643 if ((!$this->private || $this->creator == $user) && 2644 $this->hasPermission(Horde_Perms::READ, $user)) { 2645 return false; 2646 } 2647 2648 return true; 2649 } 2650 2651 /** 2652 * Returns the title of this event, considering private flags. 2653 * 2654 * @param string $user The current user. 2655 * 2656 * @return string The title of this event. 2657 */ 2658 public function getTitle($user = null) 2659 { 2660 if (!$this->initialized) { 2661 return ''; 2662 } 2663 2664 return $this->isPrivate($user) 2665 ? _("busy") 2666 : (strlen($this->title) ? $this->title : _("[Unnamed event]")); 2667 } 2668 2669 /** 2670 * Returns the location of this event, considering private flags. 2671 * 2672 * @param string $user The current user. 2673 * 2674 * @return string The location of this event. 2675 */ 2676 public function getLocation($user = null) 2677 { 2678 return $this->isPrivate($user) ? '' : $this->location; 2679 } 2680 2681 /** 2682 * Checks to see whether the specified attendee is associated with the 2683 * current event. 2684 * 2685 * @param string $email The email address of the attendee. 2686 * 2687 * @return boolean True if the specified attendee is present for this 2688 * event. 2689 */ 2690 public function hasAttendee($email) 2691 { 2692 return isset($this->attendees[Horde_String::lower($email)]); 2693 } 2694 2695 /** 2696 * Adds a new attendee to the current event. 2697 * 2698 * This will overwrite an existing attendee if one exists with the same 2699 * email address. 2700 * 2701 * @param string $email The email address of the attendee. 2702 * @param integer $attendance The attendance code of the attendee. 2703 * @param integer $response The response code of the attendee. 2704 * @param string $name The name of the attendee. 2705 */ 2706 public function addAttendee($email, $attendance, $response, $name = null) 2707 { 2708 if ($attendance == Kronolith::PART_IGNORE) { 2709 if (isset($this->attendees[$email])) { 2710 $attendance = $this->attendees[$email]['attendance']; 2711 } else { 2712 $attendance = Kronolith::PART_REQUIRED; 2713 } 2714 } 2715 if (empty($name) && isset($this->attendees[$email]) && 2716 !empty($this->attendees[$email]['name'])) { 2717 $name = $this->attendees[$email]['name']; 2718 } 2719 2720 $this->attendees[$email] = array( 2721 'attendance' => $attendance, 2722 'response' => $response, 2723 'name' => $name 2724 ); 2725 } 2726 2727 /** 2728 * Adds a single resource to this event. 2729 * 2730 * No validation or acceptence/denial is done here...it should be done 2731 * when saving the event. 2732 * 2733 * @param Kronolith_Resource $resource The resource to add. 2734 */ 2735 public function addResource($resource, $response) 2736 { 2737 $this->_resources[$resource->getId()] = array( 2738 'attendance' => Kronolith::PART_REQUIRED, 2739 'response' => $response, 2740 'name' => $resource->get('name'), 2741 'calendar' => $resource->get('calendar') 2742 ); 2743 } 2744 2745 /** 2746 * Removes a resource from this event. 2747 * 2748 * @param Kronolith_Resource $resource The resource to remove. 2749 */ 2750 public function removeResource($resource) 2751 { 2752 if (isset($this->_resources[$resource->getId()])) { 2753 unset($this->_resources[$resource->getId()]); 2754 } 2755 } 2756 2757 /** 2758 * Returns all resources. 2759 * 2760 * @return array A copy of the resources array. 2761 */ 2762 public function getResources() 2763 { 2764 return $this->_resources; 2765 } 2766 2767 /** 2768 * Set the entire resource array. Only used when copying an Event. 2769 * 2770 * @param array $resources The resource array. 2771 * @since 4.2.6 2772 */ 2773 public function setResources(array $resources) 2774 { 2775 $this->_resources = $resources; 2776 } 2777 2778 public function isAllDay() 2779 { 2780 return $this->allday || 2781 ($this->start->hour == 0 && $this->start->min == 0 && $this->start->sec == 0 && 2782 (($this->end->hour == 23 && $this->end->min == 59) || 2783 ($this->end->hour == 0 && $this->end->min == 0 && $this->end->sec == 0 && 2784 ($this->end->mday > $this->start->mday || 2785 $this->end->month > $this->start->month || 2786 $this->end->year > $this->start->year)))); 2787 } 2788 2789 /** 2790 * Syncronizes tags from the tagging backend with the task storage backend, 2791 * if necessary. 2792 * 2793 * @param array $tags Tags from the tagging backend. 2794 */ 2795 public function synchronizeTags($tags) 2796 { 2797 if (isset($this->_internaltags)) { 2798 $lower_internaltags = array_map('Horde_String::lower', $this->_internaltags); 2799 $lower_tags = array_map('Horde_String::lower', $tags); 2800 usort($lower_tags, 'strcoll'); 2801 2802 if (array_diff($lower_internaltags, $lower_tags)) { 2803 Kronolith::getTagger()->replaceTags( 2804 $this->uid, 2805 $this->_internaltags, 2806 $this->_creator, 2807 Kronolith_Tagger::TYPE_EVENT 2808 ); 2809 } 2810 $this->_tags = $this->_internaltags; 2811 } else { 2812 $this->_tags = $tags; 2813 } 2814 } 2815 2816 /** 2817 * Reads form/post data and updates this event's properties. 2818 * 2819 * @param Kronolith_Event|null $existing If this is an exception event 2820 * this is taken as the base event. 2821 * @since 4.2.6 2822 * 2823 */ 2824 public function readForm(Kronolith_Event $existing = null) 2825 { 2826 global $prefs, $session; 2827 2828 // Event owner. 2829 $targetcalendar = Horde_Util::getFormData('targetcalendar'); 2830 if (strpos($targetcalendar, '\\')) { 2831 list(, $this->creator) = explode('\\', $targetcalendar, 2); 2832 } elseif (!isset($this->_id)) { 2833 $this->creator = $GLOBALS['registry']->getAuth(); 2834 } 2835 2836 // Basic fields. 2837 $this->title = Horde_Util::getFormData('title', $this->title); 2838 $this->description = Horde_Util::getFormData('description', $this->description); 2839 $this->location = Horde_Util::getFormData('location', $this->location); 2840 $this->timezone = Horde_Util::getFormData('timezone', $this->timezone); 2841 $this->private = (bool)Horde_Util::getFormData('private'); 2842 2843 // URL. 2844 $url = Horde_Util::getFormData('eventurl', $this->url); 2845 if (strlen($url)) { 2846 // Analyze and re-construct. 2847 $url = @parse_url($url); 2848 if ($url) { 2849 if (function_exists('http_build_url')) { 2850 if (empty($url['path'])) { 2851 $url['path'] = '/'; 2852 } 2853 $url = http_build_url($url); 2854 } else { 2855 $new_url = ''; 2856 if (isset($url['scheme'])) { 2857 $new_url .= $url['scheme'] . '://'; 2858 } 2859 if (isset($url['user'])) { 2860 $new_url .= $url['user']; 2861 if (isset($url['pass'])) { 2862 $new_url .= ':' . $url['pass']; 2863 } 2864 $new_url .= '@'; 2865 } 2866 if (isset($url['host'])) { 2867 // Convert IDN hosts to ASCII. 2868 if (function_exists('idn_to_ascii')) { 2869 $url['host'] = @idn_to_ascii($url['host']); 2870 } elseif (Horde_Mime::is8bit($url['host'])) { 2871 //throw new Kronolith_Exception(_("Invalid character in URL.")); 2872 $url['host'] = ''; 2873 } 2874 $new_url .= $url['host']; 2875 } 2876 if (isset($url['path'])) { 2877 $new_url .= $url['path']; 2878 } 2879 if (isset($url['query'])) { 2880 $new_url .= '?' . $url['query']; 2881 } 2882 if (isset($url['fragment'])) { 2883 $new_url .= '#' . $url['fragment']; 2884 } 2885 $url = $new_url; 2886 } 2887 } 2888 } 2889 $this->url = $url; 2890 2891 // Status. 2892 $this->status = Horde_Util::getFormData('status', $this->status); 2893 2894 // Attendees. 2895 $attendees = $session->get('kronolith', 'attendees', Horde_Session::TYPE_ARRAY); 2896 if (!is_null($newattendees = Horde_Util::getFormData('attendees'))) { 2897 $newattendees = Kronolith::parseAttendees(trim($newattendees)); 2898 foreach ($newattendees as $email => $attendee) { 2899 if (!isset($attendees[$email])) { 2900 $attendees[$email] = $attendee; 2901 } 2902 } 2903 foreach (array_keys($attendees) as $email) { 2904 if (!isset($newattendees[$email])) { 2905 unset($attendees[$email]); 2906 } 2907 } 2908 } 2909 $this->attendees = $attendees; 2910 2911 // Event start. 2912 $allDay = Horde_Util::getFormData('whole_day'); 2913 if ($start_date = Horde_Util::getFormData('start_date')) { 2914 // From ajax interface. 2915 $this->start = Kronolith::parseDate($start_date . ' ' . Horde_Util::getFormData('start_time'), true, $this->timezone); 2916 if ($allDay) { 2917 $this->start->hour = $this->start->min = $this->start->sec = 0; 2918 } 2919 } elseif ($start = Horde_Util::getFormData('start')) { 2920 // From traditional interface. 2921 $start_year = $start['year']; 2922 $start_month = $start['month']; 2923 $start_day = $start['day']; 2924 $start_hour = Horde_Util::getFormData('start_hour'); 2925 $start_min = Horde_Util::getFormData('start_min'); 2926 $am_pm = Horde_Util::getFormData('am_pm'); 2927 2928 if (!$prefs->getValue('twentyFour')) { 2929 if ($am_pm == 'PM') { 2930 if ($start_hour != 12) { 2931 $start_hour += 12; 2932 } 2933 } elseif ($start_hour == 12) { 2934 $start_hour = 0; 2935 } 2936 } 2937 2938 if (Horde_Util::getFormData('end_or_dur') == 1) { 2939 if ($allDay) { 2940 $start_hour = 0; 2941 $start_min = 0; 2942 $dur_day = 0; 2943 $dur_hour = 24; 2944 $dur_min = 0; 2945 } else { 2946 $dur_day = (int)Horde_Util::getFormData('dur_day'); 2947 $dur_hour = (int)Horde_Util::getFormData('dur_hour'); 2948 $dur_min = (int)Horde_Util::getFormData('dur_min'); 2949 } 2950 } 2951 2952 $this->start = new Horde_Date(array('hour' => $start_hour, 2953 'min' => $start_min, 2954 'month' => $start_month, 2955 'mday' => $start_day, 2956 'year' => $start_year), 2957 $this->timezone); 2958 } 2959 2960 // Event end. 2961 if ($end_date = Horde_Util::getFormData('end_date')) { 2962 // From ajax interface. 2963 $this->end = Kronolith::parseDate($end_date . ' ' . Horde_Util::getFormData('end_time'), true, $this->timezone); 2964 if ($allDay) { 2965 $this->end->hour = $this->end->min = $this->end->sec = 0; 2966 $this->end->mday++; 2967 } 2968 } elseif (Horde_Util::getFormData('end_or_dur') == 1) { 2969 // Event duration from traditional interface. 2970 $this->end = new Horde_Date(array('hour' => $start_hour + $dur_hour, 2971 'min' => $start_min + $dur_min, 2972 'month' => $start_month, 2973 'mday' => $start_day + $dur_day, 2974 'year' => $start_year)); 2975 } elseif ($end = Horde_Util::getFormData('end')) { 2976 // From traditional interface. 2977 $end_year = $end['year']; 2978 $end_month = $end['month']; 2979 $end_day = $end['day']; 2980 $end_hour = Horde_Util::getFormData('end_hour'); 2981 $end_min = Horde_Util::getFormData('end_min'); 2982 $end_am_pm = Horde_Util::getFormData('end_am_pm'); 2983 2984 if (!$prefs->getValue('twentyFour')) { 2985 if ($end_am_pm == 'PM') { 2986 if ($end_hour != 12) { 2987 $end_hour += 12; 2988 } 2989 } elseif ($end_hour == 12) { 2990 $end_hour = 0; 2991 } 2992 } 2993 2994 $this->end = new Horde_Date(array('hour' => $end_hour, 2995 'min' => $end_min, 2996 'month' => $end_month, 2997 'mday' => $end_day, 2998 'year' => $end_year), 2999 $this->timezone); 3000 if ($this->end->compareDateTime($this->start) < 0) { 3001 $this->end = new Horde_Date($this->start); 3002 } 3003 } 3004 3005 $this->allday = false; 3006 3007 // Alarm. 3008 if (!is_null($alarm = Horde_Util::getFormData('alarm'))) { 3009 if ($alarm) { 3010 $value = Horde_Util::getFormData('alarm_value'); 3011 $unit = Horde_Util::getFormData('alarm_unit'); 3012 if ($value == 0) { 3013 $value = $unit = 1; 3014 } 3015 $this->alarm = $value * $unit; 3016 // Notification. 3017 if (Horde_Util::getFormData('alarm_change_method')) { 3018 $types = Horde_Util::getFormData('event_alarms'); 3019 $methods = array(); 3020 if (!empty($types)) { 3021 foreach ($types as $type) { 3022 $methods[$type] = array(); 3023 switch ($type){ 3024 case 'notify': 3025 $methods[$type]['sound'] = Horde_Util::getFormData('event_alarms_sound'); 3026 break; 3027 case 'mail': 3028 $methods[$type]['email'] = Horde_Util::getFormData('event_alarms_email'); 3029 break; 3030 case 'popup': 3031 break; 3032 } 3033 } 3034 } 3035 $this->methods = $methods; 3036 } else { 3037 $this->methods = array(); 3038 } 3039 } else { 3040 $this->alarm = 0; 3041 $this->methods = array(); 3042 } 3043 } 3044 3045 // Recurrence. 3046 $this->recurrence = $this->readRecurrenceForm( 3047 $this->start, $this->timezone, $this->recurrence); 3048 3049 // Convert to local timezone. 3050 $this->setTimezone(false); 3051 3052 $this->_handleResources($existing); 3053 3054 // Tags. 3055 $this->tags = Horde_Util::getFormData('tags', $this->tags); 3056 3057 // Geolocation 3058 if (Horde_Util::getFormData('lat') && Horde_Util::getFormData('lon')) { 3059 $this->geoLocation = array('lat' => Horde_Util::getFormData('lat'), 3060 'lon' => Horde_Util::getFormData('lon'), 3061 'zoom' => Horde_Util::getFormData('zoom')); 3062 } 3063 3064 $this->initialized = true; 3065 } 3066 3067 static public function readRecurrenceForm($start, $timezone, 3068 $recurrence = null) 3069 { 3070 $recur = Horde_Util::getFormData('recur'); 3071 if (!strlen($recur)) { 3072 return $recurrence; 3073 } 3074 if (!isset($recurrence)) { 3075 $recurrence = new Horde_Date_Recurrence($start); 3076 } else { 3077 $recurrence->setRecurStart($start); 3078 } 3079 if (Horde_Util::getFormData('recur_end_type') == 'date') { 3080 $end_date = Horde_Util::getFormData('recur_end_date', false); 3081 if ($end_date !== false) { 3082 // From ajax interface. 3083 if (empty($end_date)) { 3084 throw new Kronolith_Exception("Missing required end date of recurrence."); 3085 } 3086 $date_ob = Kronolith::parseDate($end_date, false); 3087 $recur_enddate = array( 3088 'year' => $date_ob->year, 3089 'month' => $date_ob->month, 3090 'day' => $date_ob->mday); 3091 } else { 3092 // From traditional interface. 3093 $recur_enddate = Horde_Util::getFormData('recur_end'); 3094 } 3095 if ($recurrence->hasRecurEnd()) { 3096 $recurEnd = $recurrence->recurEnd; 3097 $recurEnd->month = $recur_enddate['month']; 3098 $recurEnd->mday = $recur_enddate['day']; 3099 $recurEnd->year = $recur_enddate['year']; 3100 } else { 3101 $recurEnd = new Horde_Date( 3102 array('hour' => 23, 3103 'min' => 59, 3104 'sec' => 59, 3105 'month' => $recur_enddate['month'], 3106 'mday' => $recur_enddate['day'], 3107 'year' => $recur_enddate['year']), 3108 $timezone); 3109 } 3110 $recurrence->setRecurEnd($recurEnd); 3111 } elseif (Horde_Util::getFormData('recur_end_type') == 'count') { 3112 $recurrence->setRecurCount(Horde_Util::getFormData('recur_count')); 3113 } elseif (Horde_Util::getFormData('recur_end_type') == 'none') { 3114 $recurrence->setRecurCount(0); 3115 $recurrence->setRecurEnd(null); 3116 } 3117 3118 $recurrence->setRecurType($recur); 3119 switch ($recur) { 3120 case Horde_Date_Recurrence::RECUR_DAILY: 3121 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_daily_interval', 1)); 3122 break; 3123 3124 case Horde_Date_Recurrence::RECUR_WEEKLY: 3125 $weekly = Horde_Util::getFormData('weekly'); 3126 $weekdays = 0; 3127 if (is_array($weekly)) { 3128 foreach ($weekly as $day) { 3129 $weekdays |= $day; 3130 } 3131 } 3132 3133 if ($weekdays == 0) { 3134 // Sunday starts at 0. 3135 switch ($start->dayOfWeek()) { 3136 case 0: $weekdays |= Horde_Date::MASK_SUNDAY; break; 3137 case 1: $weekdays |= Horde_Date::MASK_MONDAY; break; 3138 case 2: $weekdays |= Horde_Date::MASK_TUESDAY; break; 3139 case 3: $weekdays |= Horde_Date::MASK_WEDNESDAY; break; 3140 case 4: $weekdays |= Horde_Date::MASK_THURSDAY; break; 3141 case 5: $weekdays |= Horde_Date::MASK_FRIDAY; break; 3142 case 6: $weekdays |= Horde_Date::MASK_SATURDAY; break; 3143 } 3144 } 3145 3146 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_weekly_interval', 1)); 3147 $recurrence->setRecurOnDay($weekdays); 3148 break; 3149 3150 case Horde_Date_Recurrence::RECUR_MONTHLY_DATE: 3151 switch (Horde_Util::getFormData('recur_monthly_scheme')) { 3152 case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY: 3153 $recurrence->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY); 3154 case Horde_Date_Recurrence::RECUR_MONTHLY_DATE: 3155 $recurrence->setRecurInterval( 3156 Horde_Util::getFormData('recur_monthly') 3157 ? 1 3158 : Horde_Util::getFormData('recur_monthly_interval', 1) 3159 ); 3160 break; 3161 default: 3162 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_day_of_month_interval', 1)); 3163 break; 3164 } 3165 break; 3166 3167 case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY: 3168 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_week_of_month_interval', 1)); 3169 break; 3170 3171 case Horde_Date_Recurrence::RECUR_YEARLY_DATE: 3172 switch (Horde_Util::getFormData('recur_yearly_scheme')) { 3173 case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY: 3174 case Horde_Date_Recurrence::RECUR_YEARLY_DAY: 3175 $recurrence->setRecurType(Horde_Util::getFormData('recur_yearly_scheme')); 3176 case Horde_Date_Recurrence::RECUR_YEARLY_DATE: 3177 $recurrence->setRecurInterval( 3178 Horde_Util::getFormData('recur_yearly') 3179 ? 1 3180 : Horde_Util::getFormData('recur_yearly_interval', 1) 3181 ); 3182 break; 3183 default: 3184 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_interval', 1)); 3185 break; 3186 } 3187 break; 3188 3189 case Horde_Date_Recurrence::RECUR_YEARLY_DAY: 3190 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_day_interval', $yearly_interval)); 3191 break; 3192 3193 case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY: 3194 $recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_weekday_interval', $yearly_interval)); 3195 break; 3196 } 3197 3198 foreach (array('exceptions', 'completions') as $what) { 3199 if ($data = Horde_Util::getFormData($what)) { 3200 if (!is_array($data)) { 3201 $data = explode(',', $data); 3202 } 3203 foreach ($data as $date) { 3204 list($year, $month, $mday) = sscanf($date, '%04d%02d%02d'); 3205 if ($what == 'exceptions') { 3206 $recurrence->addException($year, $month, $mday); 3207 } else { 3208 $recurrence->addCompletion($year, $month, $mday); 3209 } 3210 } 3211 } 3212 } 3213 3214 return $recurrence; 3215 } 3216 3217 /** 3218 * Handles updating/saving this event's resources. Unless this event recurs, 3219 * this will delete this event from any resource calendars that are no 3220 * longer needed (as when a resource is removed from an existing event). If 3221 * this event is an exception, i.e., contains a baseid, AND $existing is 3222 * provided, the resources from the original event are used for purposes 3223 * of determining any resources that need to be removed. 3224 * 3225 * 3226 * @param Kronolith_Event|null $existing An existing base event. 3227 * @since 4.2.6 3228 */ 3229 protected function _handleResources(Kronolith_Event $existing = null) 3230 { 3231 global $session; 3232 3233 if (Horde_Util::getFormData('isajax', false)) { 3234 $resources = array(); 3235 } else { 3236 $resources = $session->get('kronolith', 'resources', Horde_Session::TYPE_ARRAY); 3237 } 3238 3239 $existingResources = $this->_resources; 3240 $newresources = Horde_Util::getFormData('resources'); 3241 if (!empty($newresources)) { 3242 foreach (explode(',', $newresources) as $id) { 3243 try { 3244 $resource = Kronolith::getDriver('Resource')->getResource($id); 3245 } catch (Kronolith_Exception $e) { 3246 $GLOBALS['notification']->push($e->getMessage(), 'horde.error'); 3247 continue; 3248 } 3249 if (!($resource instanceof Kronolith_Resource_Group) || 3250 $resource->isFree($this)) { 3251 $resources[$resource->getId()] = array( 3252 'attendance' => Kronolith::PART_REQUIRED, 3253 'response' => Kronolith::RESPONSE_NONE, 3254 'name' => $resource->get('name') 3255 ); 3256 } else { 3257 $GLOBALS['notification']->push(_("No resources from this group were available"), 'horde.error'); 3258 } 3259 } 3260 } 3261 $this->_resources = $resources; 3262 3263 3264 // Have the base event, and this is an exception so we must 3265 // match the recurrence in the resource's copy of the base event. 3266 if (!empty($existing) && $existing->recurs() && !$this->recurs()) { 3267 foreach ($existing->getResources() as $rid => $data) { 3268 $resource = Kronolith::getDriver('Resource')->getResource($key); 3269 $r_event = Kronolith::getDriver('Resource')->getByUID($existing->uid, $resource->calendar); 3270 $r_event->recurrence = $event->recurrence; 3271 $r_event->save(); 3272 } 3273 } 3274 3275 // If we don't recur, check for removal of any resources so we can 3276 // update those resources' calendars. 3277 if (!$this->recurs()) { 3278 $merged = $existingResources + $this->_resources; 3279 $delete = array_diff(array_keys($existingResources), array_keys($this->_resources)); 3280 foreach ($delete as $key) { 3281 // Resource might be declined, in which case it won't have the event 3282 // on it's calendar. 3283 if ($merged[$key]['response'] != Kronolith::RESPONSE_DECLINED) { 3284 try { 3285 Kronolith::getDriver('Resource') 3286 ->getResource($key) 3287 ->removeEvent($this); 3288 } catch (Kronolith_Exception $e) { 3289 $GLOBALS['notification']->push('foo', 'horde.error'); 3290 } 3291 } 3292 } 3293 } 3294 } 3295 3296 public function html($property) 3297 { 3298 global $prefs; 3299 3300 $options = array(); 3301 $attributes = ''; 3302 $sel = false; 3303 $label = ''; 3304 3305 switch ($property) { 3306 case 'start[year]': 3307 return '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("Start Year") . '</label>' . 3308 '<input name="' . $property . '" value="' . $this->start->year . 3309 '" type="text"' . 3310 ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />'; 3311 3312 case 'start[month]': 3313 $sel = $this->start->month; 3314 for ($i = 1; $i < 13; ++$i) { 3315 $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1)); 3316 } 3317 $label = _("Start Month"); 3318 break; 3319 3320 case 'start[day]': 3321 $sel = $this->start->mday; 3322 for ($i = 1; $i < 32; ++$i) { 3323 $options[$i] = $i; 3324 } 3325 $label = _("Start Day"); 3326 break; 3327 3328 case 'start_hour': 3329 $sel = $this->start->format($prefs->getValue('twentyFour') ? 'G' : 'g'); 3330 $hour_min = $prefs->getValue('twentyFour') ? 0 : 1; 3331 $hour_max = $prefs->getValue('twentyFour') ? 24 : 13; 3332 for ($i = $hour_min; $i < $hour_max; ++$i) { 3333 $options[$i] = $i; 3334 } 3335 $label = _("Start Hour"); 3336 break; 3337 3338 case 'start_min': 3339 $sel = sprintf('%02d', $this->start->min); 3340 for ($i = 0; $i < 12; ++$i) { 3341 $min = sprintf('%02d', $i * 5); 3342 $options[$min] = $min; 3343 } 3344 $label = _("Start Minute"); 3345 break; 3346 3347 case 'end[year]': 3348 return '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("End Year") . '</label>' . 3349 '<input name="' . $property . '" value="' . $this->end->year . 3350 '" type="text"' . 3351 ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />'; 3352 3353 case 'end[month]': 3354 $sel = $this->end ? $this->end->month : $this->start->month; 3355 for ($i = 1; $i < 13; ++$i) { 3356 $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1)); 3357 } 3358 $label = _("End Month"); 3359 break; 3360 3361 case 'end[day]': 3362 $sel = $this->end ? $this->end->mday : $this->start->mday; 3363 for ($i = 1; $i < 32; ++$i) { 3364 $options[$i] = $i; 3365 } 3366 $label = _("End Day"); 3367 break; 3368 3369 case 'end_hour': 3370 $sel = $this->end 3371 ? $this->end->format($prefs->getValue('twentyFour') ? 'G' : 'g') 3372 : $this->start->format($prefs->getValue('twentyFour') ? 'G' : 'g') + 1; 3373 $hour_min = $prefs->getValue('twentyFour') ? 0 : 1; 3374 $hour_max = $prefs->getValue('twentyFour') ? 24 : 13; 3375 for ($i = $hour_min; $i < $hour_max; ++$i) { 3376 $options[$i] = $i; 3377 } 3378 $label = _("End Hour"); 3379 break; 3380 3381 case 'end_min': 3382 $sel = $this->end ? $this->end->min : $this->start->min; 3383 $sel = sprintf('%02d', $sel); 3384 for ($i = 0; $i < 12; ++$i) { 3385 $min = sprintf('%02d', $i * 5); 3386 $options[$min] = $min; 3387 } 3388 $label = _("End Minute"); 3389 break; 3390 3391 case 'dur_day': 3392 $dur = $this->getDuration(); 3393 return '<label for="' . $property . '" class="hidden">' . _("Duration Day") . '</label>' . 3394 '<input name="' . $property . '" value="' . $dur->day . 3395 '" type="text"' . 3396 ' id="' . $property . '" size="4" maxlength="4" />'; 3397 3398 case 'dur_hour': 3399 $dur = $this->getDuration(); 3400 $sel = $dur->hour; 3401 for ($i = 0; $i < 24; ++$i) { 3402 $options[$i] = $i; 3403 } 3404 $label = _("Duration Hour"); 3405 break; 3406 3407 case 'dur_min': 3408 $dur = $this->getDuration(); 3409 $sel = $dur->min; 3410 for ($i = 0; $i < 13; ++$i) { 3411 $min = sprintf('%02d', $i * 5); 3412 $options[$min] = $min; 3413 } 3414 $label = _("Duration Minute"); 3415 break; 3416 3417 case 'recur_end[year]': 3418 if ($this->end) { 3419 $end = ($this->recurs() && $this->recurrence->hasRecurEnd()) 3420 ? $this->recurrence->recurEnd->year 3421 : $this->end->year; 3422 } else { 3423 $end = $this->start->year; 3424 } 3425 return '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("Recurrence End Year") . '</label>' . 3426 '<input name="' . $property . '" value="' . $end . 3427 '" type="text"' . 3428 ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />'; 3429 3430 case 'recur_end[month]': 3431 if ($this->end) { 3432 $sel = ($this->recurs() && $this->recurrence->hasRecurEnd()) 3433 ? $this->recurrence->recurEnd->month 3434 : $this->end->month; 3435 } else { 3436 $sel = $this->start->month; 3437 } 3438 for ($i = 1; $i < 13; ++$i) { 3439 $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1)); 3440 } 3441 $label = _("Recurrence End Month"); 3442 break; 3443 3444 case 'recur_end[day]': 3445 if ($this->end) { 3446 $sel = ($this->recurs() && $this->recurrence->hasRecurEnd()) 3447 ? $this->recurrence->recurEnd->mday 3448 : $this->end->mday; 3449 } else { 3450 $sel = $this->start->mday; 3451 } 3452 for ($i = 1; $i < 32; ++$i) { 3453 $options[$i] = $i; 3454 } 3455 $label = _("Recurrence End Day"); 3456 break; 3457 } 3458 3459 if (!$this->_varRenderer) { 3460 $this->_varRenderer = Horde_Core_Ui_VarRenderer::factory('Html'); 3461 } 3462 3463 return '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . $label . '</label>' . 3464 '<select name="' . $property . '"' . $attributes . ' id="' . $this->_formIDEncode($property) . '">' . 3465 $this->_varRenderer->selectOptions($options, $sel) . 3466 '</select>'; 3467 } 3468 3469 /** 3470 * @param array $params 3471 * 3472 * @return Horde_Url 3473 */ 3474 public function getViewUrl($params = array(), $full = false, $encoded = true) 3475 { 3476 $params['eventID'] = $this->id; 3477 $params['calendar'] = $this->calendar; 3478 $params['type'] = $this->calendarType; 3479 3480 return Horde::url('event.php', $full)->setRaw(!$encoded)->add($params); 3481 } 3482 3483 /** 3484 * @param array $params 3485 * 3486 * @return Horde_Url 3487 */ 3488 public function getEditUrl($params = array(), $full = false) 3489 { 3490 $params['view'] = 'EditEvent'; 3491 $params['eventID'] = $this->id; 3492 $params['calendar'] = $this->calendar; 3493 $params['type'] = $this->calendarType; 3494 3495 return Horde::url('event.php', $full)->add($params); 3496 } 3497 3498 /** 3499 * @param array $params 3500 * 3501 * @return Horde_Url 3502 */ 3503 public function getDeleteUrl($params = array(), $full = false) 3504 { 3505 $params['view'] = 'DeleteEvent'; 3506 $params['eventID'] = $this->id; 3507 $params['calendar'] = $this->calendar; 3508 $params['type'] = $this->calendarType; 3509 3510 return Horde::url('event.php', $full)->add($params); 3511 } 3512 3513 /** 3514 * @param array $params 3515 * 3516 * @return Horde_Url 3517 */ 3518 public function getExportUrl($params = array(), $full = false) 3519 { 3520 $params['view'] = 'ExportEvent'; 3521 $params['eventID'] = $this->id; 3522 $params['calendar'] = $this->calendar; 3523 $params['type'] = $this->calendarType; 3524 3525 return Horde::url('event.php', $full)->add($params); 3526 } 3527 3528 public function getLink($datetime = null, $icons = true, $from_url = null, 3529 $full = false, $encoded = true) 3530 { 3531 global $prefs; 3532 3533 if (is_null($datetime)) { 3534 $datetime = $this->start; 3535 } 3536 if (is_null($from_url)) { 3537 $from_url = Horde::selfUrl(true, false, true); 3538 } 3539 3540 $event_title = $this->getTitle(); 3541 $view_url = $this->getViewUrl(array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'), 'url' => $from_url), $full, $encoded); 3542 $read_permission = $this->hasPermission(Horde_Perms::READ); 3543 3544 $link = '<span' . $this->getCSSColors() . '>'; 3545 if ($read_permission && $view_url) { 3546 $link .= Horde::linkTooltip($view_url, 3547 $event_title, 3548 $this->getStatusClass(), 3549 '', 3550 '', 3551 $this->getTooltip(), 3552 '', 3553 array('style' => $this->getCSSColors(false))); 3554 } 3555 $link .= htmlspecialchars($event_title); 3556 if ($read_permission && $view_url) { 3557 $link .= '</a>'; 3558 } 3559 3560 if ($icons && $prefs->getValue('show_icons')) { 3561 $icon_color = $this->_foregroundColor == '#000' ? '000' : 'fff'; 3562 $status = ''; 3563 if ($this->alarm) { 3564 if ($this->alarm % 10080 == 0) { 3565 $alarm_value = $this->alarm / 10080; 3566 $title = sprintf(ngettext("Alarm %d week before", "Alarm %d weeks before", $alarm_value), $alarm_value); 3567 } elseif ($this->alarm % 1440 == 0) { 3568 $alarm_value = $this->alarm / 1440; 3569 $title = sprintf(ngettext("Alarm %d day before", "Alarm %d days before", $alarm_value), $alarm_value); 3570 } elseif ($this->alarm % 60 == 0) { 3571 $alarm_value = $this->alarm / 60; 3572 $title = sprintf(ngettext("Alarm %d hour before", "Alarm %d hours before", $alarm_value), $alarm_value); 3573 } else { 3574 $alarm_value = $this->alarm; 3575 $title = sprintf(ngettext("Alarm %d minute before", "Alarm %d minutes before", $alarm_value), $alarm_value); 3576 } 3577 $status .= Horde::fullSrcImg('alarm-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconAlarm'))); 3578 } 3579 3580 if ($this->recurs()) { 3581 $title = Kronolith::recurToString($this->recurrence->getRecurType()); 3582 $status .= Horde::fullSrcImg('recur-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconRecur'))); 3583 } elseif ($this->baseid) { 3584 $title = _("Exception"); 3585 $status .= Horde::fullSrcImg('exception-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconRecur'))); 3586 } 3587 3588 if ($this->private) { 3589 $title = _("Private event"); 3590 $status .= Horde::fullSrcImg('private-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconPrivate'))); 3591 } 3592 3593 if (!empty($this->attendees)) { 3594 $status .= Horde::fullSrcImg('attendees-' . $icon_color . '.png', array('attr' => array('alt' => _("Meeting"), 'title' => _("Meeting"), 'class' => 'iconPeople'))); 3595 } 3596 3597 $space = ' '; 3598 if (!empty($this->icon)) { 3599 $link = $status . ' <img class="kronolithEventIcon" src="' . $this->icon . '" /> ' . $link; 3600 } elseif (!empty($status)) { 3601 $link .= ' ' . $status; 3602 $space = ''; 3603 } 3604 3605 if ((!$this->private || 3606 $this->creator == $GLOBALS['registry']->getAuth()) && 3607 Kronolith::getDefaultCalendar(Horde_Perms::EDIT)) { 3608 $url = $this->getEditUrl( 3609 array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'), 3610 'url' => $from_url), 3611 $full); 3612 if ($url) { 3613 $link .= $space 3614 . $url->link(array('title' => sprintf(_("Edit %s"), $event_title), 3615 'class' => 'iconEdit')) 3616 . Horde::fullSrcImg('edit-' . $icon_color . '.png', 3617 array('attr' => array('alt' => _("Edit")))) 3618 . '</a>'; 3619 $space = ''; 3620 } 3621 } 3622 if ($this->hasPermission(Horde_Perms::DELETE)) { 3623 $url = $this->getDeleteUrl( 3624 array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'), 3625 'url' => $from_url), 3626 $full); 3627 if ($url) { 3628 $link .= $space 3629 . $url->link(array('title' => sprintf(_("Delete %s"), $event_title), 3630 'class' => 'iconDelete')) 3631 . Horde::fullSrcImg('delete-' . $icon_color . '.png', 3632 array('attr' => array('alt' => _("Delete")))) 3633 . '</a>'; 3634 } 3635 } 3636 } 3637 3638 return $link . '</span>'; 3639 } 3640 3641 /** 3642 * Returns the CSS color definition for this event. 3643 * 3644 * @param boolean $with_attribute Whether to wrap the colors inside a 3645 * "style" attribute. 3646 * 3647 * @return string A CSS string with color definitions. 3648 */ 3649 public function getCSSColors($with_attribute = true) 3650 { 3651 $css = 'background-color:' . $this->_backgroundColor . ';color:' . $this->_foregroundColor; 3652 if ($with_attribute) { 3653 $css = ' style="' . $css . '"'; 3654 } 3655 return $css; 3656 } 3657 3658 /** 3659 * @return string A tooltip for quick descriptions of this event. 3660 */ 3661 public function getTooltip() 3662 { 3663 $tooltip = $this->getTimeRange() 3664 . "\n" . sprintf(_("Owner: %s"), ($this->creator == $GLOBALS['registry']->getAuth() ? 3665 _("Me") : Kronolith::getUserName($this->creator))); 3666 3667 if (!$this->isPrivate()) { 3668 if ($this->location) { 3669 $tooltip .= "\n" . _("Location") . ': ' . $this->location; 3670 } 3671 3672 if ($this->description) { 3673 $tooltip .= "\n\n" . Horde_String::wrap($this->description); 3674 } 3675 } 3676 3677 return $tooltip; 3678 } 3679 3680 /** 3681 * @return string The time range of the event ("All Day", "1:00pm-3:00pm", 3682 * "08:00-22:00"). 3683 */ 3684 public function getTimeRange() 3685 { 3686 if ($this->isAllDay()) { 3687 return _("All day"); 3688 } elseif (($cmp = $this->start->compareDate($this->end)) > 0) { 3689 $df = $GLOBALS['prefs']->getValue('date_format'); 3690 if ($cmp > 0) { 3691 return $this->end->strftime($df) . '-' 3692 . $this->start->strftime($df); 3693 } else { 3694 return $this->start->strftime($df) . '-' 3695 . $this->end->strftime($df); 3696 } 3697 } else { 3698 $twentyFour = $GLOBALS['prefs']->getValue('twentyFour'); 3699 return $this->start->format($twentyFour ? 'G:i' : 'g:ia') 3700 . '-' 3701 . $this->end->format($twentyFour ? 'G:i' : 'g:ia'); 3702 } 3703 } 3704 3705 /** 3706 * @return string The CSS class for the event based on its status. 3707 */ 3708 public function getStatusClass() 3709 { 3710 switch ($this->status) { 3711 case Kronolith::STATUS_CANCELLED: 3712 return 'kronolith-event-cancelled'; 3713 3714 case Kronolith::STATUS_TENTATIVE: 3715 case Kronolith::STATUS_FREE: 3716 return 'kronolith-event-tentative'; 3717 } 3718 } 3719 3720 protected function _formIDEncode($id) 3721 { 3722 return str_replace(array('[', ']'), 3723 array('_', ''), 3724 $id); 3725 } 3726 3727 /** 3728 * Ensure the given string is valid UTF-8. 3729 * 3730 * @param string $text The string to ensure contains no invalid UTF-8 sequences. 3731 * 3732 * @return string|boolean The valid UTF-8 string, possibly with illegal sequences removed. 3733 */ 3734 protected function _ensureUtf8($text) 3735 { 3736 if (Horde_String::validUtf8($text)) { 3737 return $text; 3738 } 3739 3740 return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $text); 3741 } 3742} 3743