PageRenderTime 50ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Calendar/ICalendar.php

http://github.com/modolabs/Kurogo-Mobile-Web
PHP | 1118 lines | 858 code | 140 blank | 120 comment | 118 complexity | 484fd5ab197e55f25ec2abe12a12ba06 MD5 | raw file
Possible License(s): LGPL-3.0, LGPL-2.1
  1. <?php
  2. /*
  3. * Copyright © 2009 - 2010 Massachusetts Institute of Technology
  4. * Copyright © 2010 - 2013 Modo Labs Inc. All rights reserved.
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. /**
  25. * ICalendar
  26. * The ICal* classes in this file together partially implement RFC 2445.
  27. * @package ExternalData
  28. * @subpackage Calendar
  29. */
  30. /**
  31. * ICalendar
  32. * @package Exceptions
  33. */
  34. class ICalendarException extends KurogoDataException {
  35. }
  36. /**
  37. * @package ExternalData
  38. * @subpackage Calendar
  39. */
  40. abstract class ICalObject {
  41. protected $classname;
  42. public function get_name() {
  43. return $this->classname;
  44. }
  45. public function set_attribute($attr, $value, $params=null) {
  46. }
  47. public function get_attribute($attr) {
  48. }
  49. }
  50. /**
  51. * @package ExternalData
  52. * @subpackage Calendar
  53. */
  54. class ICalTodo extends ICalObject {
  55. public function __construct() {
  56. $this->classname = 'VTODO';
  57. }
  58. }
  59. /**
  60. * @package ExternalData
  61. * @subpackage Calendar
  62. */
  63. class ICalJournal extends ICalObject {
  64. public function __construct() {
  65. $this->classname = 'VJOURNAL';
  66. }
  67. }
  68. /**
  69. * @package ExternalData
  70. * @subpackage Calendar
  71. */
  72. class ICalFreeBusy extends ICalObject {
  73. public function __construct() {
  74. $this->classname = 'VFREEBUSY';
  75. }
  76. }
  77. /**
  78. * @package ExternalData
  79. * @subpackage Calendar
  80. */
  81. class ICalTimeZone extends ICalObject implements CalendarTimeZone {
  82. protected $tzid;
  83. public function getTZID() {
  84. return $this->tzid;
  85. }
  86. public function __construct() {
  87. $this->classname = 'VTIMEZONE';
  88. }
  89. public function set_attribute($attr, $value, $params=NULL) {
  90. switch ($attr) {
  91. case 'TZID':
  92. $this->tzid = $value;
  93. break;
  94. }
  95. }
  96. }
  97. /**
  98. * @package ExternalData
  99. * @subpackage Calendar
  100. */
  101. class ICalDaylight extends ICalTimeZone {
  102. public function __construct() {
  103. $this->classname = 'DAYLIGHT';
  104. }
  105. }
  106. /**
  107. * @package ExternalData
  108. * @subpackage Calendar
  109. */
  110. class ICalStandard extends ICalTimeZone {
  111. public function __construct() {
  112. $this->classname = 'STANDARD';
  113. }
  114. }
  115. /**
  116. * @package ExternalData
  117. * @subpackage Calendar
  118. */
  119. class ICalAlarm extends ICalObject {
  120. public function __construct() {
  121. $this->classname = 'VALARM';
  122. }
  123. }
  124. /**
  125. * @package ExternalData
  126. * @subpackage Calendar
  127. */
  128. class ICalEvent extends ICalObject implements KurogoObject, CalendarEvent {
  129. protected $uid;
  130. protected $sequence;
  131. protected $recurid = NULL;
  132. protected $range;
  133. protected $starttime;
  134. protected $summary;
  135. protected $description;
  136. protected $location;
  137. protected $geo;
  138. protected $tzid;
  139. protected $timezone;
  140. protected $url;
  141. protected $created;
  142. protected $updated;
  143. protected $dtstamp;
  144. protected $status;
  145. protected $transparency;
  146. protected $categories=array();
  147. protected $properties=array();
  148. protected $rrules=array();
  149. protected $exdates = array();
  150. protected $recurrence_exceptions = array();
  151. public function getEventCategories(TimeRange $range = null) {
  152. if($range && $this->range->contained_by($range)) {
  153. return $this->categories;
  154. }else {
  155. return $this->categories;
  156. }
  157. return array();
  158. }
  159. protected function standardAttributes() {
  160. return array(
  161. 'summary',
  162. 'location',
  163. 'geo',
  164. 'description',
  165. 'uid',
  166. 'start',
  167. 'end',
  168. 'url',
  169. 'categories',
  170. 'datetime',
  171. );
  172. }
  173. public function filterItem($filters) {
  174. foreach ($filters as $filter=>$value) {
  175. switch ($filter)
  176. {
  177. case 'search': //case insensitive
  178. return (stripos($this->getTitle(), $value)!==FALSE);
  179. break;
  180. case 'category': //case insensitive
  181. if (!in_array(strtolower($value), array_map('strtolower', $this->categories))) {
  182. return false;
  183. }
  184. break;
  185. }
  186. }
  187. return true;
  188. }
  189. public function get_tzid() {
  190. return $this->tzid;
  191. }
  192. public function get_timezone() {
  193. return $this->timezone;
  194. }
  195. public function getID() {
  196. return $this->uid;
  197. }
  198. public function get_uid() {
  199. return $this->uid;
  200. }
  201. public function get_recurid() {
  202. return $this->recurid;
  203. }
  204. public function isAllDay() {
  205. return $this->range instanceOf DayRange;
  206. }
  207. public function getRange() {
  208. return $this->range;
  209. }
  210. public function get_range() {
  211. return $this->range;
  212. }
  213. public function get_series_range() {
  214. return new TimeRange($this->get_start(), $this->get_end());
  215. }
  216. public function get_start() {
  217. return $this->range->get_start();
  218. }
  219. public function set_start($timestamp, $dayOnly=false) {
  220. if (!$this->range) {
  221. throw new KurogoDataException("set_start called before range is initialized");
  222. }
  223. $this->range->set_start($timestamp);
  224. $this->starttime = $timestamp;
  225. }
  226. public function get_end() {
  227. return $this->range->get_end();
  228. }
  229. public function set_end($timestamp, $dayOnly=false) {
  230. if (!$this->range) {
  231. throw new KurogoDataException("set_end called before range is initialized");
  232. }
  233. $start = $this->range->get_start();
  234. if ($timestamp < $start) {
  235. // if the end time is an all day time then just use the start time.
  236. if ($dayOnly) {
  237. $timestamp = $start;
  238. } else {
  239. //ignore end time if it's later
  240. return false;
  241. }
  242. }
  243. $this->range->set_end($timestamp);
  244. }
  245. public function get_summary() {
  246. return $this->summary;
  247. }
  248. public function getTitle() {
  249. return $this->summary;
  250. }
  251. public function get_url() {
  252. return $this->url;
  253. }
  254. public function get_description() {
  255. return $this->description;
  256. }
  257. public function get_location() {
  258. return $this->location;
  259. }
  260. public function get_location_coordinates() {
  261. $coords = false;
  262. $parts = explode(';', $this->geo);
  263. if (count($parts) == 2) {
  264. $coords = array(
  265. 'lat' => floatval($parts[0]),
  266. 'lon' => floatval($parts[1]),
  267. );
  268. }
  269. return $coords;
  270. }
  271. public function get_categories() {
  272. return $this->categories;
  273. }
  274. public function is_recurring() {
  275. return count($this->rrules)>0;
  276. }
  277. /* returns an array of occurrences that occur in the given range */
  278. public function getOccurrencesInRange(TimeRange $range, $limit=null) {
  279. $occurrences = array();
  280. /* check the "base" event */
  281. if ($this->range->overlaps($range)) {
  282. $occurrences[$this->get_start()] = $this;
  283. }
  284. foreach ($this->rrules as $rrule) {
  285. foreach ($rrule->occurrences($this, $range, $limit) as $occurrence) {
  286. if (!in_array($occurrence->get_start(), $this->exdates)) {
  287. $occurrences[$occurrence->get_start()] = $occurrence;
  288. }
  289. }
  290. }
  291. ksort($occurrences);
  292. return array_values($occurrences);
  293. }
  294. public function overlaps(TimeRange $range) {
  295. return $this->range->overlaps($range);
  296. }
  297. public function contains(TimeRange $range) {
  298. return $this->range->contains($range);
  299. }
  300. public function contained_by(TimeRange $range) {
  301. return $this->range->contained_by($range);
  302. }
  303. public function get_attribute($attr) {
  304. if (in_array($attr, $this->standardAttributes())) {
  305. if ($attr == 'datetime') {
  306. return $this->range;
  307. } else {
  308. $method = "get_$attr";
  309. return $this->$method();
  310. }
  311. } else {
  312. return isset($this->properties[$attr]) ? $this->properties[$attr] : null;
  313. }
  314. }
  315. public function get_all_attributes() {
  316. return array_merge($this->standardAttributes(), array_keys($this->properties));
  317. }
  318. public function setRange(TimeRange $range) {
  319. $this->range = $range;
  320. $this->starttime = $range->get_start();
  321. }
  322. public function setSummary($summary) {
  323. $this->summary = $summary;
  324. }
  325. public function setDescription($description) {
  326. $this->description = TextFormatter::linkify($description);
  327. }
  328. public function setUID($uid) {
  329. $this->uid = $uid;
  330. }
  331. public function setLocation($location) {
  332. $this->location = $location;
  333. }
  334. public function setLocationCoordinates($coordinates) {
  335. if (count($coordinates) == 2) {
  336. $this->geo = implode(';', array_values($coordinates));
  337. }
  338. }
  339. private static function timezoneFilter($tzid) {
  340. static $timezoneMap;
  341. if (!$timezoneMap) {
  342. $timezoneMap = Kurogo::getSiteSection('timezones');
  343. }
  344. return Kurogo::arrayVal($timezoneMap, $tzid, $tzid);
  345. }
  346. private static function getTimezoneForID($tzid) {
  347. $tzid = self::timezoneFilter($tzid);
  348. try {
  349. $timezone = new DateTimeZone($tzid);
  350. } catch (Exception $e) {
  351. Kurogo::log(LOG_WARNING, "Invalid timezone $tzid found when processing calendar", 'data');
  352. $timezone = null;
  353. }
  354. return $timezone;
  355. }
  356. /**
  357. * reset the end time when the start time is less than the start time
  358. */
  359. protected function rectifyRange() {
  360. if ($this->range) {
  361. $start = $this->range->get_start();
  362. $end = $this->range->get_end();
  363. if ($end < $start) {
  364. $this->range->set_end($start);
  365. }
  366. }
  367. }
  368. public function set_attribute($attr, $value, $params=NULL) {
  369. switch ($attr) {
  370. case 'UID':
  371. $this->setUID($value);
  372. break;
  373. case 'RECURRENCE-ID':
  374. $this->recurid = $value;
  375. break;
  376. case 'DESCRIPTION':
  377. $this->setDescription(iCalendar::ical_unescape_text($value));
  378. break;
  379. case 'LOCATION':
  380. $this->setLocation(iCalendar::ical_unescape_text($value));
  381. break;
  382. case 'GEO':
  383. if (is_array($value)) {
  384. $this->setLocationCoordinates($value);
  385. } else if (is_string($value)) {
  386. $this->geo = $value;
  387. }
  388. break;
  389. case 'SUMMARY':
  390. $this->setSummary(iCalendar::ical_unescape_text($value));
  391. break;
  392. case 'CATEGORIES':
  393. $categories = explode(',', $value);
  394. $this->categories = array();
  395. foreach ($categories as $category) {
  396. $this->categories[] = trim(iCalendar::ical_unescape_text($category));
  397. }
  398. break;
  399. case 'URL':
  400. $this->url = iCalendar::ical_unescape_text($value);
  401. break;
  402. case 'SEQUENCE':
  403. $this->sequence = $value;
  404. break;
  405. case 'STATUS':
  406. $this->status = $value;
  407. break;
  408. case 'CREATED':
  409. if (array_key_exists('TZID', $params)) {
  410. $timezone = self::getTimezoneForID($params['TZID']);
  411. $datetime = new DateTime($value, $timezone);
  412. } else {
  413. $datetime = new DateTime($value);
  414. }
  415. $this->created = $datetime->format('U');
  416. break;
  417. case 'LAST-MODIFIED':
  418. if (array_key_exists('TZID', $params)) {
  419. $timezone = self::getTimezoneForID($params['TZID']);
  420. $datetime = new DateTime($value, $timezone);
  421. } else {
  422. $datetime = new DateTime($value);
  423. }
  424. $this->updated = $datetime->format('U');
  425. break;
  426. case 'DTSTAMP':
  427. if (array_key_exists('TZID', $params)) {
  428. $timezone = self::getTimezoneForID($params['TZID']);
  429. $datetime = new DateTime($value, $timezone);
  430. } else {
  431. $datetime = new DateTime($value);
  432. }
  433. $this->dtstamp = $datetime->format('U');
  434. break;
  435. case 'DTSTART':
  436. // set the event timezone if it's present in the start time
  437. if (array_key_exists('TZID', $params)) {
  438. if ($timezone = self::getTimezoneForID($params['TZID'])) {
  439. $this->timezone = $timezone;
  440. $this->tzid = $timezone->getName();
  441. }
  442. }
  443. case 'DTEND':
  444. $dayOnly = false;
  445. case 'DTEND':
  446. //reset the system timezone to calculate the timestamp
  447. if ($this->tzid) {
  448. $old_timezone = date_default_timezone_get();
  449. date_default_timezone_set($this->tzid);
  450. $datetime = new DateTime($value, $this->timezone);
  451. } else {
  452. $datetime = new DateTime($value);
  453. }
  454. $t = strpos($value, 'T');
  455. // if there is no "T" or if the "T" is at the end
  456. if ( ($t === FALSE) || ($t == (strlen($value)-1))) {
  457. $dayOnly = true;
  458. }
  459. $timestamp = $datetime->format('U');
  460. if ($this->tzid) {
  461. date_default_timezone_set($old_timezone);
  462. }
  463. if ($attr=='DTEND') {
  464. if ($dayOnly) {
  465. // make all day events end at 11:59:59 so they don't overlap next day
  466. $timestamp -= 1;
  467. }
  468. }
  469. if (!$this->range) {
  470. $range = $dayOnly ? new DayRange($timestamp, null, $this->tzid) : new TimeRange($timestamp, null, $this->tzid);
  471. $this->setRange($range);
  472. if (isset($this->properties['duration'])) {
  473. $this->range->set_icalendar_duration($this->properties['duration']);
  474. unset($this->properties['duration']);
  475. }
  476. }
  477. switch ($attr)
  478. {
  479. case 'DTSTART':
  480. $this->set_start($timestamp, $dayOnly);
  481. break;
  482. case 'DTEND':
  483. $this->set_end($timestamp, $dayOnly);
  484. break;
  485. }
  486. break;
  487. case 'TRANSP':
  488. $this->transparency = $value;
  489. break;
  490. case 'DURATION':
  491. if ($this->range) {
  492. $this->range->set_icalendar_duration($value);
  493. } else {
  494. $this->properties['duration'] = $value;
  495. }
  496. break;
  497. case 'RRULE':
  498. $this->add_rrule($value);
  499. break;
  500. case 'EXDATE':
  501. if (array_key_exists('TZID', $params)) {
  502. $timezone = self::getTimezoneForID($params['TZID']);
  503. $datetime = new DateTime($value, $timezone);
  504. } else {
  505. $datetime = new DateTime($value);
  506. }
  507. $this->exdates[] = $datetime->format('U'); // start time
  508. break;
  509. case 'TZID': // this only gets called by ICalendar::__construct
  510. if ($timezone = self::getTimezoneForID($value)) {
  511. $this->timezone = $timezone;
  512. $this->tzid = $timezone->getName();
  513. }
  514. break;
  515. default:
  516. $this->properties[$attr] = iCalendar::ical_unescape_text($value);
  517. break;
  518. }
  519. }
  520. protected function increment_set($set) {
  521. return array_map(
  522. $this->incrementor,
  523. $set,
  524. array_fill(0, count($set), $this->interval)
  525. );
  526. }
  527. public function clear_rrules() {
  528. $this->rrules = array();
  529. }
  530. protected function add_rrule($rrule_string) {
  531. $rrule = new ICalRecurrenceRule($rrule_string);
  532. $this->rrules[] = $rrule;
  533. return;
  534. }
  535. private function addLine(&$string, $prop, $value) {
  536. $string .= sprintf("%s:%s\n", $prop, iCalendar::ical_escape_text($value));
  537. }
  538. /**
  539. * Add an ICalEvent as an Exception to the recurrence pattern of a repeating
  540. * event.
  541. */
  542. public function addRecurenceException(ICalEvent $recurrence_exception) {
  543. $this->recurrence_exceptions[] = $recurrence_exception;
  544. }
  545. /**
  546. * Answer an ICalEvent that is an exception to the normal recurrence pattern
  547. * if one exists for the start-time given. null if none match.
  548. * @param int $time
  549. * @return mixed ICalEvent or null
  550. */
  551. public function getRecurrenceException($time) {
  552. $recurrence_id = strftime("%Y%m%dT%H%M%S",$time);
  553. foreach ($this->recurrence_exceptions as $exception) {
  554. // Some ical feeds only have the %Y%m%d portion for DTSTART/DTEND (Google Calendar feeds),
  555. // so first check for an exact match, then try adding a T000000 time and check again.
  556. if (($exception->get_recurid() == $recurrence_id) || ($exception->get_recurid().'T000000' == $recurrence_id)){
  557. return $exception;
  558. }
  559. }
  560. return null;
  561. }
  562. public function outputICS() {
  563. $output_string = '';
  564. $this->addLine($output_string, "BEGIN", 'VEVENT');
  565. if ($this->uid) {
  566. $this->addLine($output_string, "UID", $this->uid);
  567. }
  568. if ($this->summary) {
  569. $this->addLine($output_string, "SUMMARY", $this->summary);
  570. }
  571. if ($this->location) {
  572. $this->addLine($output_string, "LOCATION", $this->location);
  573. }
  574. if ($this->geo) {
  575. $this->addLine($output_string, "GEO", $this->geo);
  576. }
  577. if ($this->description) {
  578. $this->addLine($output_string, "DESCRIPTION", $this->description);
  579. }
  580. if ($this->range) {
  581. if ($this->range instanceOf DayRange) {
  582. $this->addLine($output_string, "DTSTART", date('Ymd', $this->range->get_start()));
  583. $this->addLine($output_string, "DTEND", date('Ymd', $this->range->get_end()));
  584. } else {
  585. $this->addLine($output_string, "DTSTART", strftime('%Y%m%dT%H%M%S', $this->range->get_start()));
  586. $this->addLine($output_string, "DTEND", strftime('%Y%m%dT%H%M%S', $this->range->get_end()));
  587. }
  588. }
  589. $this->addLine($output_string, 'END', 'VEVENT');
  590. return $output_string;
  591. }
  592. public function init($args) {
  593. }
  594. public function __construct($summary=NULL, TimeRange $range=NULL) {
  595. $this->classname = 'VEVENT';
  596. if ($summary !== NULL) {
  597. $this->summary = $summary;
  598. }
  599. if ($range !== NULL) {
  600. $this->range = $range;
  601. }
  602. }
  603. }
  604. /**
  605. * @package ExternalData
  606. * @subpackage Calendar
  607. */
  608. class ICalRecurrenceRule extends ICalObject {
  609. const MAX_OCCURRENCES = PHP_INT_MAX; // provided as a safety net
  610. protected $classname = 'RECURRENCE';
  611. protected $type;
  612. protected $limit = -1;
  613. protected $limitType = 'COUNT';
  614. protected $interval = 1;
  615. protected $occurs_by_list = array();
  616. protected $occurs_by_day = array();
  617. protected $wkst;
  618. private static $dayIndex = Array('SU'=>0, 'MO'=>1, 'TU'=>2, 'WE'=>3, 'TH'=>4, 'FR'=>5, 'SA'=>6 );
  619. private $dayString = array(
  620. 'SU' => 'Sunday',
  621. 'MO' => 'Monday',
  622. 'TU' => 'Tuesday',
  623. 'WE' => 'Wednesday',
  624. 'TH' => 'Thursday',
  625. 'FR' => 'Friday',
  626. 'SA' => 'Saturday'
  627. );
  628. private $frequencies = Array(
  629. 'SECONDLY',
  630. 'MINUTELY',
  631. 'HOURLY',
  632. 'DAILY',
  633. 'WEEKLY',
  634. 'MONTHLY',
  635. 'YEARLY'
  636. );
  637. function __construct($rule_string) {
  638. $rules = explode(';', $rule_string);
  639. foreach ($rules as $rule) {
  640. $namevalue = explode('=', $rule);
  641. $rulename = $namevalue[0];
  642. $rulevalue = $namevalue[1];
  643. switch ($rulename) {
  644. case 'FREQ': // always present
  645. if (in_array($rulevalue, $this->frequencies)) {
  646. $this->type = $rulevalue;
  647. } else {
  648. throw new ICalendarException("Invalid frequency $rulevalue");
  649. }
  650. break;
  651. case 'INTERVAL':
  652. $this->interval = $rulevalue;
  653. break;
  654. case 'UNTIL':
  655. $this->limitType = 'UNTIL';
  656. $datetime = new DateTime($rulevalue);
  657. $this->limit = $datetime->format('U');
  658. break;
  659. case 'COUNT':
  660. $limitType = 'COUNT';
  661. $this->limit = $rulevalue;
  662. break;
  663. case 'BYDAY':
  664. if ($this->type == 'WEEKLY') {
  665. $this->type = 'WEEKLY-BYDAY';
  666. $this->occurs_by_day = array();
  667. foreach (explode(',', $rulevalue) as $day) {
  668. $this->occurs_by_day[self::$dayIndex[$day]] = $day;
  669. }
  670. ksort($this->occurs_by_day);
  671. break;
  672. } else if($this->type == 'MONTHLY'){
  673. $this->occurs_by_list = array('BYDAY'=>$rulevalue);
  674. }
  675. case 'WKST':
  676. $this->wkst = array_key_exists($rulevalue, self::$dayIndex) ? $rulevalue : '';
  677. break;
  678. case (substr($rulename, 0, 2) == 'BY'):
  679. $this->occurs_by_list[$rulename] = $rulevalue;
  680. break;
  681. default:
  682. throw new ICalendarException("Unknown recurrence rule property $rulename found");
  683. break;
  684. }
  685. }
  686. if (empty($this->type)) {
  687. throw new ICalendarException("Invalid Frequency");
  688. }
  689. }
  690. private function nextIncrement($time, $type, $interval = 1, $tzid = null) {
  691. //remember the initial time
  692. $startTime = $time;
  693. //keep the current timezone
  694. if ($tzid) {
  695. $old_timezone = date_default_timezone_get();
  696. date_default_timezone_set($tzid);
  697. }
  698. switch ($type) {
  699. case 'SECONDLY':
  700. $time += $interval;
  701. break;
  702. case 'MINUTELY':
  703. $time += ($interval * 60);
  704. break;
  705. case 'HOURLY' :
  706. $time += ($interval * 3600);
  707. break;
  708. case 'DAILY':
  709. $hour = date('H', $time);
  710. $minute = date('i', $time);
  711. $second = date('s', $time);
  712. for ($i=0; $i<$interval; $i++) {
  713. //can't assume 24 "hours" in a day due to daylight savings. start at midnight and add 28 hours to be in the next day
  714. $timestamp = mktime(0,0,0, date('m', $time), date('d', $time), date('Y', $time)) + 100800;
  715. $time = mktime($hour, $minute, $second, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
  716. }
  717. break;
  718. case 'WEEKLY':
  719. $time = self::nextIncrement($time, 'DAILY', 7*$interval, $tzid);
  720. break;
  721. case 'WEEKLY-BYDAY':
  722. $current_day = strtoupper(substr(date('D', $time), 0,2));
  723. // Loop through the days and find the next one.
  724. reset($this->occurs_by_day);
  725. //check the wkst to resort the occurs_by_day
  726. if ($interval > 1 && $this->wkst && self::$dayIndex[$this->wkst] > 0 && isset($this->occurs_by_day[0])) {
  727. array_shift($this->occurs_by_day);
  728. $this->occurs_by_day[7] = 'SU';
  729. }
  730. $day = current($this->occurs_by_day);
  731. while ($day) {
  732. if ($day == $current_day) {
  733. $next_day = next($this->occurs_by_day);
  734. if ($next_day) {
  735. $offset = self::$dayIndex[$next_day] - self::$dayIndex[$current_day];
  736. $time = self::nextIncrement($time, 'DAILY', $offset, $tzid);
  737. }
  738. // If we have reached the end of the sequence, use the beginning and add 7
  739. else {
  740. reset($this->occurs_by_day);
  741. $next_day = current($this->occurs_by_day);
  742. $offset = 7 + self::$dayIndex[$next_day] - self::$dayIndex[$current_day];
  743. $time = self::nextIncrement($time, 'DAILY', $offset + (($interval - 1) * 7), $tzid);
  744. }
  745. break;
  746. }
  747. $day = next($this->occurs_by_day);
  748. }
  749. //$time = self::nextIncrement($time, 'DAILY', $offset*$interval, $tzid);
  750. break;
  751. case 'MONTHLY':
  752. $time = mktime(date('H', $time), date('i', $time), date('s', $time), date('m', $time)+$interval, date('d', $time), date('Y', $time));
  753. break;
  754. case 'YEARLY':
  755. $time = mktime(date('H', $time), date('i', $time), date('s', $time), date('m', $time), date('d', $time), date('Y', $time)+$interval);
  756. break;
  757. default:
  758. throw new ICalendarException("Invalid type $type");
  759. }
  760. //restore the old timezone
  761. if ($tzid) {
  762. date_default_timezone_set($old_timezone);
  763. }
  764. //ensure that the time has changed
  765. if ($time == $startTime) {
  766. throw new KurogoDataException("nextIncrement was the same when parsing a recurring event rule. There is likely a bug in the iCalendar code. Please report this behavior");
  767. }
  768. return $this->affectRules($time, $tzid);
  769. }
  770. private function affectRules($time, $tzid) {
  771. if(empty($this->occurs_by_list)) {
  772. return $time;
  773. }
  774. //keep the current timezone
  775. if ($tzid) {
  776. $old_timezone = date_default_timezone_get();
  777. date_default_timezone_set($tzid);
  778. }
  779. foreach($this->occurs_by_list as $rule => $val) {
  780. switch($rule) {
  781. case 'BYDAY':
  782. $n = substr($val, 0, -2);
  783. $day = substr($val, -2);
  784. if ($n < 0) {
  785. $firstday = mktime(date('H', $time), date('i', $time), date('s', $time), date('m', $time) + 1, 1, date('Y', $time));
  786. $time = strtotime($n . " " . $this->dayString[$day], $firstday);
  787. } else {
  788. $firstday = mktime(date('H', $time), date('i', $time), date('s', $time), date('m', $time), 1, date('Y', $time));
  789. $time = strtotime($n . " " . $this->dayString[$day], $firstday);
  790. }
  791. break;
  792. case 'BYMONTH':
  793. $time = mktime(date('H', $time), date('i', $time), date('s', $time), $val, date('d', $time), date('Y', $time));
  794. break;
  795. case 'BYSECOND':
  796. $time = mktime(date('H', $time), date('i', $time), $val, date('m', $time), date('d', $time), date('Y', $time));
  797. break;
  798. case 'BYMINUTE':
  799. $time = mktime(date('H', $time), $val, date('s', $time), date('m', $time), date('d', $time), date('Y', $time));
  800. break;
  801. case 'BYHOUR':
  802. $time = mktime($val, date('i', $time), date('s', $time), date('m', $time), date('d', $time), date('Y', $time));
  803. break;
  804. case 'BYMONTHDAY':
  805. $time = mktime(date('H', $time), date('i', $time), date('s', $time), date('m', $time), $val, date('Y', $time));
  806. break;
  807. case 'BYYEARDAY':
  808. $firstday = mktime(date('H', $time), date('i', $time), date('s', $time), 1, 1, date('Y', $time));
  809. $time = strtotime($val . " day", $firstday);
  810. break;
  811. case 'BYWEEKNO':
  812. case 'BYSETPOS':
  813. case 'WKST':
  814. throw new ICalendarException("$rule Not handled yet");
  815. break;
  816. default:
  817. }
  818. }
  819. //restore the timezone
  820. if ($tzid) {
  821. date_default_timezone_set($old_timezone);
  822. }
  823. return $time;
  824. }
  825. /* takes an event and range as parmeters and returns an array of occurrences DOES NOT include the original event */
  826. function occurrences(ICalEvent $event, TimeRange $range=null, $max=null) {
  827. $occurrences = array();
  828. $time = $event->get_start();
  829. $diff = $event->get_end()-$event->get_start();
  830. $limitType = $this->limitType;
  831. $limit = $this->limit;
  832. $count = 0;
  833. // echo date('m/d/Y H:i:s', $time) . "<br>\n";
  834. $time = $this->nextIncrement($time, $this->type, $this->interval, $event->get_tzid());
  835. while ($time <= $range->get_end()) {
  836. // echo date('m/d/Y H:i:s', $time) . "<br>\n";
  837. if ( ($limitType=='UNTIL') && ($time > $limit) ) {
  838. break;
  839. }
  840. $occurrence_range = new TimeRange($time, $time + $diff);
  841. if ($occurrence_range->overlaps($range)) {
  842. if ($recurrence_exception = $event->getRecurrenceException($time)) {
  843. $occurrence = clone $recurrence_exception;
  844. } else {
  845. $occurrence = clone $event;
  846. $occurrence->setRange($occurrence_range);
  847. $occurrence->clear_rrules();
  848. $recurrence_id = strftime("%Y%m%dT%H%M%S",$time);
  849. if ($tzid = $occurrence->get_tzid()) {
  850. $recurrence_id = sprintf("TZID=%s:%s", $tzid, $recurrence_id);
  851. }
  852. $occurrence->set_attribute('RECURRENCE-ID', $recurrence_id);
  853. }
  854. $occurrences[] = $occurrence;
  855. }
  856. if ( ($limitType=='COUNT') && ($count < $limit) ) {
  857. break;
  858. }
  859. if ( $count > ICalRecurrenceRule::MAX_OCCURRENCES) {
  860. break;
  861. }
  862. if ( !is_null($max) && count($occurrences)>=$max) {
  863. break;
  864. }
  865. $time = $this->nextIncrement($time, $this->type, $this->interval, $event->get_tzid());
  866. $count++;
  867. }
  868. return $occurrences;
  869. }
  870. }
  871. /**
  872. * @package ExternalData
  873. * @subpackage Calendar
  874. */
  875. class ICalendar extends ICalObject implements CalendarInterface {
  876. protected $properties;
  877. protected $timezone = NULL;
  878. protected $events=array();
  879. protected $eventStartTimes=array();
  880. protected $recurrence_exceptions = array();
  881. protected $initArgs=array();
  882. public function setTimezone(CalendarTimeZone $timezone) {
  883. $this->timezone = $timezone;
  884. }
  885. public function getTimezone() {
  886. return $this->timezone;
  887. }
  888. public function add_event(CalendarEvent $event) {
  889. if (!$event instanceOf ICalEvent) {
  890. throw new KurogoConfigurationException(gettype($event) . " must be a subclass of ICalEvent");
  891. }
  892. $uid = $event->get_uid();
  893. if (is_null($event->get_recurid())) {
  894. $this->events[$uid] = $event;
  895. // use event start times so we can return events in starting order
  896. $this->eventStartTimes[$uid] = $event->get_start();
  897. // Add any stored exceptions to the event.
  898. if (isset($this->recurrence_exceptions[$uid])) {
  899. foreach ($this->recurrence_exceptions[$uid] as $exception) {
  900. $this->events[$uid]->addRecurenceException($exception);
  901. }
  902. }
  903. } else {
  904. // If the event already exists, add the exception to it.
  905. if (isset($this->events[$uid])) {
  906. $this->events[$uid]->addRecurenceException($event);
  907. }
  908. // Otherwise, store up a list of exceptions for addition to the event
  909. // when its added.
  910. else {
  911. if (!isset($this->recurrence_exceptions[$uid]))
  912. $this->recurrence_exceptions[$uid] = array();
  913. $this->recurrence_exceptions[$uid][] = $event;
  914. }
  915. }
  916. }
  917. public function getEvents() {
  918. return $this->events;
  919. }
  920. public function getEvent($id) {
  921. return isset($this->events[$id]) ? $this->events[$id] : null;
  922. }
  923. /* returns an array of events keyed by uid containing an array of occurrences keyed by start time */
  924. public function getEventsInRange(TimeRange $range=null, $limit=null, $filters=null) {
  925. $occurrences = array();
  926. $filters = is_array($filters) ? $filters : array();
  927. foreach ($this->eventStartTimes as $id => $startTime) {
  928. $event = $this->events[$id];
  929. if ($event->filterItem($filters)) {
  930. $occurrences = array_merge($occurrences, $event->getOccurrencesInRange($range, $limit));
  931. }
  932. }
  933. usort($occurrences, array($this, "sort_events"));
  934. // in some case, it doesn't work properly if we just sort $this->eventStartTimes
  935. return $occurrences;
  936. }
  937. private function sort_events($a, $b) {
  938. $startA = $a->get_start();
  939. $startB = $b->get_start();
  940. if ($startA == $startB) {
  941. return 0;
  942. }
  943. return ($startA < $startB) ? -1 : 1;
  944. }
  945. public function set_attribute($attr, $value, $params=null) {
  946. $this->properties[$attr] = $value;
  947. }
  948. public function init($args) {
  949. $this->initArgs = $args;
  950. }
  951. public function __construct($url=FALSE) {
  952. $this->properties = Array();
  953. $this->events = Array();
  954. $this->classname = 'VCALENDAR';
  955. }
  956. private function addLine(&$string, $prop, $value) {
  957. $string .= sprintf("%s:%s\n", $prop, self::ical_escape_text($value));
  958. }
  959. public function ical_escape_text($text) {
  960. $text = str_replace(array("\"","\\",",",";","\n"), array("DQUOTE","\\\\", "\,","\;","\\n"), $text);
  961. return $text;
  962. }
  963. public static function ical_unescape_text($text) {
  964. $text = str_replace(array("DQUOTE","\\\\", "\,","\;","\\n","\\r"), array("\"","\\",",",";","\n",""), $text);
  965. return $text;
  966. }
  967. public function outputICS() {
  968. $output_string = '';
  969. $this->addLine($output_string, 'BEGIN','VCALENDAR');
  970. $this->addLine($output_string, 'CALSCALE','GREGORIAN');
  971. foreach ($this->events as $event) {
  972. $output_string .= $event->outputICS();
  973. }
  974. $output_string .= 'END:VCALENDAR';
  975. return $output_string;
  976. }
  977. }