PageRenderTime 76ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/inc/vCalendar.php

https://gitlab.com/karora/awl
PHP | 416 lines | 274 code | 43 blank | 99 comment | 55 complexity | 7220a72a2350a2509fa4471a7cedb1d9 MD5 | raw file
  1. <?php
  2. /**
  3. * A Class for handling vCalendar data.
  4. *
  5. * When parsed the underlying structure is roughly as follows:
  6. *
  7. * vCalendar( array(vComponent), array(vProperty), array(vTimezone) )
  8. *
  9. * with the TIMEZONE data still currently included in the component array (likely
  10. * to change in the future) and the timezone array only containing vComponent objects
  11. * (which is also likely to change).
  12. *
  13. * @package awl
  14. * @subpackage vCalendar
  15. * @author Andrew McMillan <andrew@mcmillan.net.nz>
  16. * @copyright Morphoss Ltd <http://www.morphoss.com/>
  17. * @license http://gnu.org/copyleft/lgpl.html GNU LGPL v3 or later
  18. *
  19. */
  20. require_once('vComponent.php');
  21. class vCalendar extends vComponent {
  22. /**
  23. * These variables are mostly used to improve efficiency by caching values as they are
  24. * retrieved to speed any subsequent access.
  25. * @var string $contained_type
  26. * @var vComponent $primary_component
  27. * @var array $timezones
  28. * @var string $organizer
  29. * @var array $attendees
  30. */
  31. private $contained_type;
  32. private $primary_component;
  33. private $timezones;
  34. private $organizer;
  35. private $attendees;
  36. private $schedule_agent;
  37. /**
  38. * Constructor. If a string is passed it will be parsed as if it was an iCalendar object,
  39. * otherwise a new vCalendar will be initialised with basic content. If an array of key value
  40. * pairs is provided they will also be used as top-level properties.
  41. *
  42. * Typically this will be used to set a METHOD property on the VCALENDAR as something like:
  43. * $shinyCalendar = new vCalendar( array('METHOD' => 'REQUEST' ) );
  44. *
  45. * @param mixed $content Can be a string to be parsed, or an array of key value pairs.
  46. */
  47. function __construct($content=null) {
  48. $this->contained_type = null;
  49. $this->primary_component = null;
  50. $this->timezones = array();
  51. if ( empty($content) || is_array($content) ) {
  52. parent::__construct();
  53. $this->SetType('VCALENDAR');
  54. $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN');
  55. $this->AddProperty('VERSION', '2.0');
  56. $this->AddProperty('CALSCALE', 'GREGORIAN');
  57. if ( !empty($content) ) {
  58. foreach( $content AS $k => $v ) {
  59. $this->AddProperty($k,$v);
  60. }
  61. }
  62. }
  63. else {
  64. parent::__construct($content);
  65. $components = $this->GetComponents();
  66. if(isset($components) && count($components) > 0){
  67. foreach( $components AS $k => $comp ) {
  68. if ( $comp->GetType() == 'VTIMEZONE' ) {
  69. $this->AddTimeZone($comp, true);
  70. }
  71. else if ( empty($this->contained_type) ) {
  72. $this->contained_type = $comp->GetType();
  73. $this->primary_component = $comp;
  74. }
  75. }
  76. }
  77. if ( !isset($this->contained_type) && !empty($this->timezones) ) {
  78. $this->contained_type = 'VTIMEZONE';
  79. $this->primary_component = reset($this->timezones);
  80. }
  81. }
  82. }
  83. /**
  84. * Add a timezone component to this vCalendar.
  85. */
  86. function AddTimeZone(vComponent $vtz, $in_components=false) {
  87. $tzid = $vtz->GetPValue('TZID');
  88. if ( empty($tzid) ) {
  89. dbg_error_log('ERROR','Ignoring invalid VTIMEZONE with no TZID parameter!');
  90. dbg_log_array('LOG', 'vTimezone', $vtz, true);
  91. return;
  92. }
  93. $this->timezones[$tzid] = $vtz;
  94. if ( !$in_components ) $this->AddComponent($vtz);
  95. }
  96. /**
  97. * Get a timezone component for a specific TZID in this calendar.
  98. * @param string $tzid The TZID for the timezone to be retrieved.
  99. * @return vComponent The timezone as a vComponent.
  100. */
  101. function GetTimeZone( $tzid ) {
  102. if ( empty($this->timezones[$tzid]) ) return null;
  103. return $this->timezones[$tzid];
  104. }
  105. /**
  106. * Get the organizer of this VEVENT/VTODO
  107. * @return vProperty The Organizer property.
  108. */
  109. function GetOrganizer() {
  110. if ( !isset($this->organizer) ) {
  111. $organizers = $this->GetPropertiesByPath('/VCALENDAR/*/ORGANIZER');
  112. $organizer = (count($organizers) > 0 ? $organizers[0] : false);
  113. $this->organizer = (empty($organizer) ? false : $organizer );
  114. if ( $this->organizer ) {
  115. $this->schedule_agent = $organizer->GetParameterValue('SCHEDULE-AGENT');
  116. if ( empty($schedule_agent) ) $this->schedule_agent = 'SERVER';
  117. }
  118. }
  119. return $this->organizer;
  120. }
  121. /**
  122. * Get the schedule-agent from the organizer
  123. * @return vProperty The schedule-agent parameter
  124. */
  125. function GetScheduleAgent() {
  126. if ( !isset($this->schedule_agent) ) $this->GetOrganizer();
  127. return $this->schedule_agent;
  128. }
  129. /**
  130. * Get the attendees of this VEVENT/VTODO
  131. */
  132. function GetAttendees() {
  133. if ( !isset($this->attendees) ) {
  134. $this->attendees = array();
  135. $attendees = $this->GetPropertiesByPath('/VCALENDAR/*/ATTENDEE');
  136. $wr_attendees = $this->GetPropertiesByPath('/VCALENDAR/*/X-WR-ATTENDEE');
  137. if ( count ( $wr_attendees ) > 0 ) {
  138. dbg_error_log( 'PUT', 'Non-compliant iCal request. Using X-WR-ATTENDEE property' );
  139. foreach( $wr_attendees AS $k => $v ) {
  140. $attendees[] = $v;
  141. }
  142. }
  143. $this->attendees = $attendees;
  144. }
  145. return $this->attendees;
  146. }
  147. /**
  148. * Update the attendees of this VEVENT/VTODO
  149. * @param string $email The e-mail address of the attendee to be updated.
  150. * @param vProperty $statusProperty A replacement property.
  151. */
  152. function UpdateAttendeeStatus( $email, vProperty $statusProperty ) {
  153. foreach($this->GetComponents() AS $ck => $v ) {
  154. if ($v->GetType() == 'VEVENT' || $v->GetType() == 'VTODO' ) {
  155. $new_attendees = array();
  156. foreach( $v->GetProperties() AS $p ) {
  157. if ( $p->Name() == 'ATTENDEE' ) {
  158. if ( $p->Value() == $email || $p->Value() == 'mailto:'.$email ) {
  159. $new_attendees[] = $statusProperty;
  160. }
  161. else {
  162. $new_attendees[] = clone($p);
  163. }
  164. }
  165. }
  166. $v->SetProperties($new_attendees,'ATTENDEE');
  167. $this->attendees = null;
  168. $this->rendered = null;
  169. }
  170. }
  171. }
  172. /**
  173. * Update the ORGANIZER of this VEVENT/VTODO
  174. * @param vProperty $statusProperty A replacement property.
  175. */
  176. function UpdateOrganizerStatus( vProperty $statusProperty ) {
  177. $this->rendered = null;
  178. foreach($this->GetComponents() AS $ck => $v ) {
  179. if ($v->GetType() == 'VEVENT' || $v->GetType() == 'VTODO' ) {
  180. $v->SetProperties(array($statusProperty), 'ORGANIZER');
  181. $this->organizer = null;
  182. $this->rendered = null;
  183. }
  184. }
  185. }
  186. /**
  187. * Test a PROP-FILTER or COMP-FILTER and return a true/false
  188. * COMP-FILTER (is-defined | is-not-defined | (time-range?, prop-filter*, comp-filter*))
  189. * PROP-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*))
  190. *
  191. * @param array $filter An array of XMLElement defining the filter
  192. *
  193. * @return boolean Whether or not this vCalendar passes the test
  194. */
  195. function StartFilter( $filters ) {
  196. dbg_error_log('vCalendar', ':StartFilter we have %d filters to test', count($filters) );
  197. if ( count($filters) != 1 ) return false;
  198. $tag = $filters[0]->GetNSTag();
  199. $name = $filters[0]->GetAttribute("name");
  200. if ( $tag != "urn:ietf:params:xml:ns:caldav:comp-filter" || $name != 'VCALENDAR' ) return false;
  201. return $this->TestFilter($filters[0]->GetContent());
  202. }
  203. /**
  204. * Work out what Olson timezone this VTIMEZONE really is. Perhaps we should put this
  205. * into a vTimezone class.
  206. * @param vComponent $vtz The VTIMEZONE component.
  207. * @return string The Olson name for the timezone.
  208. */
  209. function GetOlsonName( vComponent $vtz ) {
  210. $tzstring = $vtz->GetPValue('TZID');
  211. $tzid = olson_from_tzstring($tzstring);
  212. if ( !empty($tzid) ) return $tzid;
  213. $tzstring = $vtz->GetPValue('X-LIC-LOCATION');
  214. $tzid = olson_from_tzstring($tzstring);
  215. if ( !empty($tzid) ) return $tzid;
  216. $tzcdo = $vtz->GetPValue('X-MICROSOFT-CDO-TZID');
  217. if ( empty($tzcdo) ) return null;
  218. switch( $tzcdo ) {
  219. /**
  220. * List of Microsoft CDO Timezone IDs from here:
  221. * http://msdn.microsoft.com/en-us/library/aa563018%28loband%29.aspx
  222. */
  223. case 0: return('UTC');
  224. case 1: return('Europe/London');
  225. case 2: return('Europe/Lisbon');
  226. case 3: return('Europe/Paris');
  227. case 4: return('Europe/Berlin');
  228. case 5: return('Europe/Bucharest');
  229. case 6: return('Europe/Prague');
  230. case 7: return('Europe/Athens');
  231. case 8: return('America/Brasilia');
  232. case 9: return('America/Halifax');
  233. case 10: return('America/New_York');
  234. case 11: return('America/Chicago');
  235. case 12: return('America/Denver');
  236. case 13: return('America/Los_Angeles');
  237. case 14: return('America/Anchorage');
  238. case 15: return('Pacific/Honolulu');
  239. case 16: return('Pacific/Apia');
  240. case 17: return('Pacific/Auckland');
  241. case 18: return('Australia/Brisbane');
  242. case 19: return('Australia/Adelaide');
  243. case 20: return('Asia/Tokyo');
  244. case 21: return('Asia/Singapore');
  245. case 22: return('Asia/Bangkok');
  246. case 23: return('Asia/Kolkata');
  247. case 24: return('Asia/Muscat');
  248. case 25: return('Asia/Tehran');
  249. case 26: return('Asia/Baghdad');
  250. case 27: return('Asia/Jerusalem');
  251. case 28: return('America/St_Johns');
  252. case 29: return('Atlantic/Azores');
  253. case 30: return('America/Noronha');
  254. case 31: return('Africa/Casablanca');
  255. case 32: return('America/Argentina/Buenos_Aires');
  256. case 33: return('America/La_Paz');
  257. case 34: return('America/Indiana/Indianapolis');
  258. case 35: return('America/Bogota');
  259. case 36: return('America/Regina');
  260. case 37: return('America/Tegucigalpa');
  261. case 38: return('America/Phoenix');
  262. case 39: return('Pacific/Kwajalein');
  263. case 40: return('Pacific/Fiji');
  264. case 41: return('Asia/Magadan');
  265. case 42: return('Australia/Hobart');
  266. case 43: return('Pacific/Guam');
  267. case 44: return('Australia/Darwin');
  268. case 45: return('Asia/Shanghai');
  269. case 46: return('Asia/Novosibirsk');
  270. case 47: return('Asia/Karachi');
  271. case 48: return('Asia/Kabul');
  272. case 49: return('Africa/Cairo');
  273. case 50: return('Africa/Harare');
  274. case 51: return('Europe/Moscow');
  275. case 53: return('Atlantic/Cape_Verde');
  276. case 54: return('Asia/Yerevan');
  277. case 55: return('America/Panama');
  278. case 56: return('Africa/Nairobi');
  279. case 58: return('Asia/Yekaterinburg');
  280. case 59: return('Europe/Helsinki');
  281. case 60: return('America/Godthab');
  282. case 61: return('Asia/Rangoon');
  283. case 62: return('Asia/Kathmandu');
  284. case 63: return('Asia/Irkutsk');
  285. case 64: return('Asia/Krasnoyarsk');
  286. case 65: return('America/Santiago');
  287. case 66: return('Asia/Colombo');
  288. case 67: return('Pacific/Tongatapu');
  289. case 68: return('Asia/Vladivostok');
  290. case 69: return('Africa/Ndjamena');
  291. case 70: return('Asia/Yakutsk');
  292. case 71: return('Asia/Dhaka');
  293. case 72: return('Asia/Seoul');
  294. case 73: return('Australia/Perth');
  295. case 74: return('Asia/Riyadh');
  296. case 75: return('Asia/Taipei');
  297. case 76: return('Australia/Sydney');
  298. case 57: // null
  299. case 52: // null
  300. default: // null
  301. }
  302. return null;
  303. }
  304. /**
  305. * Morph this component (and subcomponents) into a confidential version of it. A confidential
  306. * event will be scrubbed of any identifying characteristics other than time/date, repeat, uid
  307. * and a summary which is just a translated 'Busy'.
  308. */
  309. function Confidential() {
  310. static $keep_properties = array( 'DTSTAMP'=>1, 'DTSTART'=>1, 'RRULE'=>1, 'DURATION'=>1, 'DTEND'=>1, 'DUE'=>1, 'UID'=>1, 'CLASS'=>1, 'TRANSP'=>1, 'CREATED'=>1, 'LAST-MODIFIED'=>1 );
  311. static $resource_components = array( 'VEVENT'=>1, 'VTODO'=>1, 'VJOURNAL'=>1 );
  312. $this->MaskComponents(array( 'VTIMEZONE'=>1, 'VEVENT'=>1, 'VTODO'=>1, 'VJOURNAL'=>1 ), false);
  313. $this->MaskProperties($keep_properties, $resource_components );
  314. if ( isset($this->rendered) ) unset($this->rendered);
  315. foreach( $this->GetComponents() AS $comp ) {
  316. if ( isset($resource_components[$comp->GetType()] ) ) {
  317. if ( isset($comp->rendered) ) unset($comp->rendered);
  318. $comp->AddProperty( 'SUMMARY', translate('Busy') );
  319. }
  320. }
  321. return $this;
  322. }
  323. /**
  324. * Clone this component (and subcomponents) into a minimal iTIP version of it.
  325. */
  326. function GetItip($method, $attendee_value ) {
  327. $iTIP = clone($this);
  328. static $keep_properties = array( 'DTSTART'=>1, 'DURATION'=>1, 'DTEND'=>1, 'DUE'=>1, 'UID'=>1,
  329. 'SEQUENCE'=>1, 'ORGANIZER'=>1, 'ATTENDEE'=>1 );
  330. static $resource_components = array( 'VEVENT'=>1, 'VTODO'=>1, 'VJOURNAL'=>1 );
  331. $iTIP->MaskComponents($resource_components, false);
  332. $iTIP->MaskProperties($keep_properties, $resource_components );
  333. $iTIP->AddProperty('METHOD',$method);
  334. if ( isset($iTIP->rendered) ) unset($iTIP->rendered);
  335. if ( !empty($attendee_value) ) {
  336. $iTIP->attendees = array();
  337. foreach( $iTIP->GetComponents() AS $comp ) {
  338. if ( isset($resource_components[$comp->GetType()] ) ) {
  339. foreach( $comp->GetProperties() AS $k=> $property ) {
  340. switch( $property->Name() ) {
  341. case 'ATTENDEE':
  342. if ( $property->Value() == $attendee_value )
  343. $iTIP->attendees[] = $property->ClearParameters(array('CUTYPE'=>true, 'SCHEDULE-STATUS'=>true));
  344. else
  345. $comp->clearPropertyAt($k);
  346. break;
  347. case 'SEQUENCE':
  348. $property->Value( $property->Value() + 1);
  349. break;
  350. }
  351. }
  352. $comp->AddProperty('DTSTAMP', date('Ymd\THis\Z'));
  353. }
  354. }
  355. }
  356. return $iTIP;
  357. }
  358. /**
  359. * Get the UID from the primary component.
  360. */
  361. function GetUID() {
  362. if ( empty($this->primary_component) ) return null;
  363. return $this->primary_component->GetPValue('UID');
  364. }
  365. /**
  366. * Set the UID on the primary component.
  367. * @param string newUid
  368. */
  369. function SetUID( $newUid ) {
  370. if ( empty($this->primary_component) ) return;
  371. $this->primary_component->SetProperties( array( new vProperty('UID', $newUid) ), 'UID');
  372. }
  373. }