Andrew's Web Libraries (AWL)
iCalendar.php
1 <?php
2 
3 require_once('XMLElement.php');
4 require_once('AwlQuery.php');
5 
15 class iCalProp {
25  var $name;
26 
32  var $parameters;
33 
39  var $content;
40 
46  var $rendered;
47 
58  function __construct( $propstring = null ) {
59  $this->name = "";
60  $this->content = "";
61  $this->parameters = array();
62  unset($this->rendered);
63  if ( $propstring != null && gettype($propstring) == 'string' ) {
64  $this->ParseFrom($propstring);
65  }
66  }
67 
68 
77  function ParseFrom( $propstring ) {
78  $this->rendered = (strlen($propstring) < 72 ? $propstring : null); // Only pre-rendered if we didn't unescape it
79 
80  // Unescape newlines
81  $unescaped = preg_replace('{\\\\[nN]}', "\n", $propstring);
82 
83  /*
84  * Split propname with params from propvalue. Searches for the first unquoted COLON.
85  *
86  * RFC5545 3.2
87  *
88  * Property parameter values that contain the COLON, SEMICOLON, or COMMA
89  * character separators MUST be specified as quoted-string text values.
90  * Property parameter values MUST NOT contain the DQUOTE character.
91  */
92  $split = $this->SplitQuoted($unescaped, ':', 2);
93  if (count($split) != 2) {
94  // Bad things happended...
95  dbg_error_log('ERROR', "iCalendar::ParseFrom(): Couldn't parse property from string: `%s`, skipping", $unescaped);
96  return;
97  }
98  list($prop, $value) = $split;
99 
100  // Unescape ESCAPED-CHAR
101  $this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $value);
102 
103  // Split property name and parameters
104  $parameters = $this->SplitQuoted($prop, ';');
105  $this->name = array_shift($parameters);
106  $this->parameters = array();
107  foreach ($parameters AS $k => $v) {
108  $pos = strpos($v, '=');
109  $name = substr($v, 0, $pos);
110  $value = substr($v, $pos + 1);
111  $this->parameters[$name] = preg_replace('/^"(.+)"$/', '$1', $value); // Removes DQUOTE on demand
112  }
113 // dbg_error_log('iCalendar', " iCalProp::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
114  }
115 
124  function SplitQuoted($str, $sep = ',', $limit = 0) {
125  $result = array();
126  $cursor = 0;
127  $inquote = false;
128  $num = 0;
129  for($i = 0, $len = strlen($str); $i < $len; ++$i) {
130  $ch = $str[$i];
131  if ($ch == '"') {
132  $inquote = !$inquote;
133  }
134  if (!$inquote && $ch == $sep) {
135  //var_dump("Found sep `$sep` - Splitting from $cursor to $i from $len.");
136  // If we reached the maximal number of splits, we cut till the end and stop here.
137  ++$num;
138  if ($limit > 0 && $num == $limit) {
139  $result[] = substr($str, $cursor);
140  break;
141  }
142  $result[] = substr($str, $cursor, $i - $cursor);
143  $cursor = $i + 1;
144  }
145  // Add rest of string on end reached
146  if ($i + 1 == $len) {
147  //var_dump("Reached end - Splitting from $cursor to $len.");
148  $result[] = substr($str, $cursor);
149  }
150  }
151 
152  return $result;
153  }
154 
162  function Name( $newname = null ) {
163  if ( $newname != null ) {
164  $this->name = $newname;
165  if ( isset($this->rendered) ) unset($this->rendered);
166 // dbg_error_log('iCalendar', " iCalProp::Name(%s)", $this->name );
167  }
168  return $this->name;
169  }
170 
171 
179  function Value( $newvalue = null ) {
180  if ( $newvalue != null ) {
181  $this->content = $newvalue;
182  if ( isset($this->rendered) ) unset($this->rendered);
183  }
184  return $this->content;
185  }
186 
187 
195  function Parameters( $newparams = null ) {
196  if ( $newparams != null ) {
197  $this->parameters = $newparams;
198  if ( isset($this->rendered) ) unset($this->rendered);
199  }
200  return $this->parameters;
201  }
202 
203 
211  function TextMatch( $search ) {
212  if ( isset($this->content) ) {
213  return (stristr( $this->content, $search ) !== false);
214  }
215  return false;
216  }
217 
218 
226  function GetParameterValue( $name ) {
227  if ( isset($this->parameters[$name]) ) return $this->parameters[$name];
228  }
229 
237  function SetParameterValue( $name, $value ) {
238  if ( isset($this->rendered) ) unset($this->rendered);
239  $this->parameters[$name] = $value;
240  }
241 
246  function RenderParameters() {
247  $rendered = "";
248  foreach( $this->parameters AS $k => $v ) {
249  $escaped = preg_replace( "/([;:])/", '\\\\$1', $v);
250  $rendered .= sprintf( ";%s=%s", $k, $escaped );
251  }
252  return $rendered;
253  }
254 
255 
259  function Render() {
260  // If we still have the string it was parsed in from, it hasn't been screwed with
261  // and we can just return that without modification.
262  if ( isset($this->rendered) ) return $this->rendered;
263 
264  $property = preg_replace( '/[;].*$/', '', $this->name );
265  $escaped = $this->content;
266  switch( $property ) {
268  case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
269  case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
270  case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
271  case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
272  case 'RRULE': case 'REPEAT': case 'TRIGGER':
273  break;
274 
275  case 'COMPLETED': case 'DTEND':
276  case 'DUE': case 'DTSTART':
277  case 'DTSTAMP': case 'LAST-MODIFIED':
278  case 'CREATED': case 'EXDATE':
279  case 'RDATE':
280  if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) {
281  $escaped = substr( $escaped, 0, 8);
282  }
283  break;
284 
286  default:
287  $escaped = str_replace( '\\', '\\\\', $escaped);
288  $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
289  $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
290  }
291  $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
292  if ( (strlen($property) + strlen($escaped)) <= 72 ) {
293  $this->rendered = $property . $escaped;
294  }
295  else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) {
296  $this->rendered = $property . "\r\n " . $escaped;
297  }
298  else {
299  $this->rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property . $escaped );
300  }
301  return $this->rendered;
302  }
303 
304 }
305 
306 
326  var $type;
327 
333  var $properties;
334 
340  var $components;
341 
347  var $rendered;
348 
354  function __construct( $content = null ) {
355  $this->type = "";
356  $this->properties = array();
357  $this->components = array();
358  $this->rendered = "";
359  if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) {
360  $this->ParseFrom($content);
361  }
362  }
363 
364 
369  function VCalendar( $extra_properties = null ) {
370  $this->SetType('VCALENDAR');
371  $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN');
372  $this->AddProperty('VERSION', '2.0');
373  $this->AddProperty('CALSCALE', 'GREGORIAN');
374  if ( is_array($extra_properties) ) {
375  foreach( $extra_properties AS $k => $v ) {
376  $this->AddProperty($k,$v);
377  }
378  }
379  }
380 
385  function CollectParameterValues( $parameter_name ) {
386  $values = array();
387  foreach( $this->components AS $k => $v ) {
388  $also = $v->CollectParameterValues($parameter_name);
389  $values = array_merge( $values, $also );
390  }
391  foreach( $this->properties AS $k => $v ) {
392  $also = $v->GetParameterValue($parameter_name);
393  if ( isset($also) && $also != "" ) {
394 // dbg_error_log( 'iCalendar', "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also);
395  $values[$also] = 1;
396  }
397  }
398  return $values;
399  }
400 
401 
406  function ParseFrom( $content ) {
407  $this->rendered = $content;
408  $content = $this->UnwrapComponent($content);
409 
410  $type = false;
411  $subtype = false;
412  $finish = null;
413  $subfinish = null;
414 
415  $length = strlen($content);
416  $linefrom = 0;
417  while( $linefrom < $length ) {
418  $lineto = strpos( $content, "\n", $linefrom );
419  if ( $lineto === false ) {
420  $lineto = strpos( $content, "\r", $linefrom );
421  }
422  if ( $lineto > 0 ) {
423  $line = substr( $content, $linefrom, $lineto - $linefrom);
424  $linefrom = $lineto + 1;
425  }
426  else {
427  $line = substr( $content, $linefrom );
428  $linefrom = $length;
429  }
430  if ( preg_match('/^\s*$/', $line ) ) continue;
431  $line = rtrim( $line, "\r\n" );
432 // dbg_error_log( 'iCalendar', "::ParseFrom: Parsing line: $line");
433 
434  if ( $type === false ) {
435  if ( preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
436  // We have found the start of the main component
437  $type = $matches[1];
438  $finish = "END:$type";
439  $this->type = $type;
440  dbg_error_log( 'iCalendar', "::ParseFrom: Start component of type '%s'", $type);
441  }
442  else {
443  dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap before start of component: $line");
444  // unset($lines[$k]); // The content has crap before the start
445  if ( $line != "" ) $this->rendered = null;
446  }
447  }
448  else if ( $type == null ) {
449  dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap after end of component");
450  if ( $line != "" ) $this->rendered = null;
451  }
452  else if ( $line == $finish ) {
453  dbg_error_log( 'iCalendar', "::ParseFrom: End of component");
454  $type = null; // We have reached the end of our component
455  }
456  else {
457  if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
458  // We have found the start of a sub-component
459  $subtype = $matches[1];
460  $subfinish = "END:$subtype";
461  $subcomponent = $line . "\r\n";
462  dbg_error_log( 'iCalendar', "::ParseFrom: Found a subcomponent '%s'", $subtype);
463  }
464  else if ( $subtype ) {
465  // We are inside a sub-component
466  $subcomponent .= $this->WrapComponent($line);
467  if ( $line == $subfinish ) {
468  dbg_error_log( 'iCalendar', "::ParseFrom: End of subcomponent '%s'", $subtype);
469  // We have found the end of a sub-component
470  $this->components[] = new iCalComponent($subcomponent);
471  $subtype = false;
472  }
473 // else
474 // dbg_error_log( 'iCalendar', "::ParseFrom: Inside a subcomponent '%s'", $subtype );
475  }
476  else {
477 // dbg_error_log( 'iCalendar', "::ParseFrom: Parse property of component");
478  // It must be a normal property line within a component.
479  $this->properties[] = new iCalProp($line);
480  }
481  }
482  }
483  }
484 
485 
491  function UnwrapComponent( $content ) {
492  return preg_replace('/\r?\n[ \t]/', '', $content );
493  }
494 
503  function WrapComponent( $content ) {
504  $strs = preg_split( "/\r?\n/", $content );
505  $wrapped = "";
506  foreach ($strs as $str) {
507  $wrapped .= preg_replace( '/(.{72})/u', '$1'."\r\n ", $str ) ."\r\n";
508  }
509  return $wrapped;
510  }
511 
515  function GetType() {
516  return $this->type;
517  }
518 
519 
523  function SetType( $type ) {
524  if ( isset($this->rendered) ) unset($this->rendered);
525  $this->type = $type;
526  return $this->type;
527  }
528 
529 
533  function GetProperties( $type = null ) {
534  $properties = array();
535  foreach( $this->properties AS $k => $v ) {
536  if ( $type == null || $v->Name() == $type ) {
537  $properties[$k] = $v;
538  }
539  }
540  return $properties;
541  }
542 
543 
551  function GetPValue( $type ) {
552  foreach( $this->properties AS $k => $v ) {
553  if ( $v->Name() == $type ) return $v->Value();
554  }
555  return null;
556  }
557 
558 
567  function GetPParamValue( $type, $parameter_name ) {
568  foreach( $this->properties AS $k => $v ) {
569  if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name);
570  }
571  return null;
572  }
573 
574 
579  function ClearProperties( $type = null ) {
580  if ( $type != null ) {
581  // First remove all the existing ones of that type
582  foreach( $this->properties AS $k => $v ) {
583  if ( $v->Name() == $type ) {
584  unset($this->properties[$k]);
585  if ( isset($this->rendered) ) unset($this->rendered);
586  }
587  }
588  $this->properties = array_values($this->properties);
589  }
590  else {
591  if ( isset($this->rendered) ) unset($this->rendered);
592  $this->properties = array();
593  }
594  }
595 
596 
600  function SetProperties( $new_properties, $type = null ) {
601  if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered);
602  $this->ClearProperties($type);
603  foreach( $new_properties AS $k => $v ) {
604  $this->AddProperty($v);
605  }
606  }
607 
608 
616  function AddProperty( $new_property, $value = null, $parameters = null ) {
617  if ( isset($this->rendered) ) unset($this->rendered);
618  if ( isset($value) && gettype($new_property) == 'string' ) {
619  $new_prop = new iCalProp();
620  $new_prop->Name($new_property);
621  $new_prop->Value($value);
622  if ( $parameters != null ) $new_prop->Parameters($parameters);
623  dbg_error_log('iCalendar'," Adding new property '%s'", $new_prop->Render() );
624  $this->properties[] = $new_prop;
625  }
626  else if ( gettype($new_property) ) {
627  $this->properties[] = $new_property;
628  }
629  }
630 
631 
636  function &FirstNonTimezone( $type = null ) {
637  foreach( $this->components AS $k => $v ) {
638  if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k];
639  }
640  $result = false;
641  return $result;
642  }
643 
644 
651  function IsOrganizer( $email ) {
652  if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
653  $props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER');
654  foreach( $props AS $k => $prop ) {
655  if ( $prop->Value() == $email ) return true;
656  }
657  return false;
658  }
659 
660 
667  function IsAttendee( $email ) {
668  if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
669  if ( $this->IsOrganizer($email) ) return true;
670  $props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE');
671  foreach( $props AS $k => $prop ) {
672  if ( $prop->Value() == $email ) return true;
673  }
674  return false;
675  }
676 
677 
686  function GetComponents( $type = null, $normal_match = true ) {
687  $components = $this->components;
688  if ( $type != null ) {
689  foreach( $components AS $k => $v ) {
690  if ( ($v->GetType() != $type) === $normal_match ) {
691  unset($components[$k]);
692  }
693  }
694  $components = array_values($components);
695  }
696  return $components;
697  }
698 
699 
704  function ClearComponents( $type = null ) {
705  if ( $type != null ) {
706  // First remove all the existing ones of that type
707  foreach( $this->components AS $k => $v ) {
708  if ( $v->GetType() == $type ) {
709  unset($this->components[$k]);
710  if ( isset($this->rendered) ) unset($this->rendered);
711  }
712  else {
713  if ( ! $this->components[$k]->ClearComponents($type) ) {
714  if ( isset($this->rendered) ) unset($this->rendered);
715  }
716  }
717  }
718  return isset($this->rendered);
719  }
720  else {
721  if ( isset($this->rendered) ) unset($this->rendered);
722  $this->components = array();
723  }
724  }
725 
726 
733  function SetComponents( $new_component, $type = null ) {
734  if ( isset($this->rendered) ) unset($this->rendered);
735  if ( count($new_component) > 0 ) $this->ClearComponents($type);
736  foreach( $new_component AS $k => $v ) {
737  $this->components[] = $v;
738  }
739  }
740 
741 
747  function AddComponent( $new_component ) {
748  if ( is_array($new_component) && count($new_component) == 0 ) return;
749  if ( isset($this->rendered) ) unset($this->rendered);
750  if ( is_array($new_component) ) {
751  foreach( $new_component AS $k => $v ) {
752  $this->components[] = $v;
753  }
754  }
755  else {
756  $this->components[] = $new_component;
757  }
758  }
759 
760 
765  function MaskComponents( $keep ) {
766  foreach( $this->components AS $k => $v ) {
767  if ( ! in_array( $v->GetType(), $keep ) ) {
768  unset($this->components[$k]);
769  if ( isset($this->rendered) ) unset($this->rendered);
770  }
771  else {
772  $v->MaskComponents($keep);
773  }
774  }
775  }
776 
777 
783  function MaskProperties( $keep, $component_list=null ) {
784  foreach( $this->components AS $k => $v ) {
785  $v->MaskProperties($keep, $component_list);
786  }
787 
788  if ( !isset($component_list) || in_array($this->GetType(), $component_list) ) {
789  foreach( $this->properties AS $k => $v ) {
790  if ( ! in_array( $v->name, $keep ) ) {
791  unset($this->properties[$k]);
792  if ( isset($this->rendered) ) unset($this->rendered);
793  }
794  }
795  }
796  }
797 
798 
804  function CloneConfidential() {
805  $confidential = clone($this);
806  $keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'DUE', 'UID', 'CLASS', 'TRANSP', 'CREATED', 'LAST-MODIFIED' );
807  $resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' );
808  $confidential->MaskComponents(array( 'VTIMEZONE', 'STANDARD', 'DAYLIGHT', 'VEVENT', 'VTODO', 'VJOURNAL' ));
809  $confidential->MaskProperties($keep_properties, $resource_components );
810 
811  if ( isset($confidential->rendered) )
812  unset($confidential->rendered); // we need to re-render the whole object
813 
814  if ( in_array( $confidential->GetType(), $resource_components ) ) {
815  $confidential->AddProperty( 'SUMMARY', translate('Busy') );
816  }
817  foreach( $confidential->components AS $k => $v ) {
818  if ( in_array( $v->GetType(), $resource_components ) ) {
819  $v->AddProperty( 'SUMMARY', translate('Busy') );
820  }
821  }
822 
823  return $confidential;
824  }
825 
834  function RenderWithoutWrap($restricted_properties = null){
835  // substr - remove new line of end, because new line
836  // are handled in vComponent::RenderWithoutWrap
837  return substr($this->Render($restricted_properties), 0 , -2);
838  }
839 
840 
844  function Render( $restricted_properties = null) {
845 
846  $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0);
847 
848  if ( isset($this->rendered) && $unrestricted )
849  return $this->rendered;
850 
851  $rendered = "BEGIN:$this->type\r\n";
852  foreach( $this->properties AS $k => $v ) {
853  if ( method_exists($v, 'Render') ) {
854  if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n";
855  }
856  }
857  foreach( $this->components AS $v ) { $rendered .= $v->Render(); }
858  $rendered .= "END:$this->type\r\n";
859 
860  $rendered = preg_replace('{(?<!\r)\n}', "\r\n", $rendered);
861  if ( $unrestricted ) $this->rendered = $rendered;
862 
863  return $rendered;
864  }
865 
866 
876  function GetPropertiesByPath( $path ) {
877  $properties = array();
878  dbg_error_log( 'iCalendar', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path );
879  if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties;
880 
881  $adrift = ($matches[1] == '');
882  $normal = ($matches[2] == '');
883  $ourtest = $matches[3];
884  $therest = $matches[4];
885  dbg_error_log( 'iCalendar', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] );
886  if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) {
887  if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) {
888  $normmatch = ($matches[1] =='');
889  $proptest = $matches[2];
890  foreach( $this->properties AS $k => $v ) {
891  if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) {
892  $properties[] = $v;
893  }
894  }
895  }
896  else {
900  foreach( $this->components AS $k => $v ) {
901  $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) );
902  }
903  }
904  }
905 
906  if ( $adrift ) {
910  foreach( $this->components AS $k => $v ) {
911  $properties = array_merge( $properties, $v->GetPropertiesByPath($path) );
912  }
913  }
914  dbg_error_log('iCalendar', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path );
915  return $properties;
916  }
917 
918 }
IsAttendee( $email)
Definition: iCalendar.php:667
VCalendar( $extra_properties=null)
Definition: iCalendar.php:369
ParseFrom( $content)
Definition: iCalendar.php:406
SetType( $type)
Definition: iCalendar.php:523
CollectParameterValues( $parameter_name)
Definition: iCalendar.php:385
WrapComponent( $content)
Definition: iCalendar.php:503
IsOrganizer( $email)
Definition: iCalendar.php:651
AddProperty( $new_property, $value=null, $parameters=null)
Definition: iCalendar.php:616
MaskProperties( $keep, $component_list=null)
Definition: iCalendar.php:783
ClearComponents( $type=null)
Definition: iCalendar.php:704
__construct( $content=null)
Definition: iCalendar.php:354
SetComponents( $new_component, $type=null)
Definition: iCalendar.php:733
MaskComponents( $keep)
Definition: iCalendar.php:765
UnwrapComponent( $content)
Definition: iCalendar.php:491
& FirstNonTimezone( $type=null)
Definition: iCalendar.php:636
RenderWithoutWrap($restricted_properties=null)
Definition: iCalendar.php:834
GetPParamValue( $type, $parameter_name)
Definition: iCalendar.php:567
GetProperties( $type=null)
Definition: iCalendar.php:533
Render( $restricted_properties=null)
Definition: iCalendar.php:844
ClearProperties( $type=null)
Definition: iCalendar.php:579
SetProperties( $new_properties, $type=null)
Definition: iCalendar.php:600
GetPropertiesByPath( $path)
Definition: iCalendar.php:876
GetPValue( $type)
Definition: iCalendar.php:551
AddComponent( $new_component)
Definition: iCalendar.php:747
GetComponents( $type=null, $normal_match=true)
Definition: iCalendar.php:686
__construct( $propstring=null)
Definition: iCalendar.php:58
SetParameterValue( $name, $value)
Definition: iCalendar.php:237
Value( $newvalue=null)
Definition: iCalendar.php:179
SplitQuoted($str, $sep=',', $limit=0)
Definition: iCalendar.php:124
Parameters( $newparams=null)
Definition: iCalendar.php:195
ParseFrom( $propstring)
Definition: iCalendar.php:77
Name( $newname=null)
Definition: iCalendar.php:162
TextMatch( $search)
Definition: iCalendar.php:211
GetParameterValue( $name)
Definition: iCalendar.php:226
RenderParameters()
Definition: iCalendar.php:246