Andrew's Web Libraries (AWL)
 All Classes Namespaces Functions Variables Pages
iCalendar.php
1 <?php
49 require_once('XMLElement.php');
50 require_once('AwlQuery.php');
51 
57 class iCalProp {
67  var $name;
68 
74  var $parameters;
75 
81  var $content;
82 
88  var $rendered;
89 
100  function __construct( $propstring = null ) {
101  $this->name = "";
102  $this->content = "";
103  $this->parameters = array();
104  unset($this->rendered);
105  if ( $propstring != null && gettype($propstring) == 'string' ) {
106  $this->ParseFrom($propstring);
107  }
108  }
109 
110 
119  function ParseFrom( $propstring ) {
120  $this->rendered = (strlen($propstring) < 72 ? $propstring : null); // Only pre-rendered if we didn't unescape it
121 
122  // Unescape newlines
123  $unescaped = preg_replace('{\\\\[nN]}', "\n", $propstring);
124 
125  /*
126  * Split propname with params from propvalue. Searches for the first unquoted COLON.
127  *
128  * RFC5545 3.2
129  *
130  * Property parameter values that contain the COLON, SEMICOLON, or COMMA
131  * character separators MUST be specified as quoted-string text values.
132  * Property parameter values MUST NOT contain the DQUOTE character.
133  */
134  $split = $this->SplitQuoted($unescaped, ':', 2);
135  if (count($split) != 2) {
136  // Bad things happended...
137  dbg_error_log('ERROR', "iCalendar::ParseFrom(): Couldn't parse property from string: `%s`, skipping", $unescaped);
138  return;
139  }
140  list($prop, $value) = $split;
141 
142  // Unescape ESCAPED-CHAR
143  $this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $value);
144 
145  // Split property name and parameters
146  $parameters = $this->SplitQuoted($prop, ';');
147  $this->name = array_shift($parameters);
148  $this->parameters = array();
149  foreach ($parameters AS $k => $v) {
150  $pos = strpos($v, '=');
151  $name = substr($v, 0, $pos);
152  $value = substr($v, $pos + 1);
153  $this->parameters[$name] = preg_replace('/^"(.+)"$/', '$1', $value); // Removes DQUOTE on demand
154  }
155 // dbg_error_log('iCalendar', " iCalProp::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
156  }
157 
166  function SplitQuoted($str, $sep = ',', $limit = 0) {
167  $result = array();
168  $cursor = 0;
169  $inquote = false;
170  $num = 0;
171  for($i = 0, $len = strlen($str); $i < $len; ++$i) {
172  $ch = $str[$i];
173  if ($ch == '"') {
174  $inquote = !$inquote;
175  }
176  if (!$inquote && $ch == $sep) {
177  //var_dump("Found sep `$sep` - Splitting from $cursor to $i from $len.");
178  // If we reached the maximal number of splits, we cut till the end and stop here.
179  ++$num;
180  if ($limit > 0 && $num == $limit) {
181  $result[] = substr($str, $cursor);
182  break;
183  }
184  $result[] = substr($str, $cursor, $i - $cursor);
185  $cursor = $i + 1;
186  }
187  // Add rest of string on end reached
188  if ($i + 1 == $len) {
189  //var_dump("Reached end - Splitting from $cursor to $len.");
190  $result[] = substr($str, $cursor);
191  }
192  }
193 
194  return $result;
195  }
196 
204  function Name( $newname = null ) {
205  if ( $newname != null ) {
206  $this->name = $newname;
207  if ( isset($this->rendered) ) unset($this->rendered);
208 // dbg_error_log('iCalendar', " iCalProp::Name(%s)", $this->name );
209  }
210  return $this->name;
211  }
212 
213 
221  function Value( $newvalue = null ) {
222  if ( $newvalue != null ) {
223  $this->content = $newvalue;
224  if ( isset($this->rendered) ) unset($this->rendered);
225  }
226  return $this->content;
227  }
228 
229 
237  function Parameters( $newparams = null ) {
238  if ( $newparams != null ) {
239  $this->parameters = $newparams;
240  if ( isset($this->rendered) ) unset($this->rendered);
241  }
242  return $this->parameters;
243  }
244 
245 
253  function TextMatch( $search ) {
254  if ( isset($this->content) ) {
255  return (stristr( $this->content, $search ) !== false);
256  }
257  return false;
258  }
259 
260 
268  function GetParameterValue( $name ) {
269  if ( isset($this->parameters[$name]) ) return $this->parameters[$name];
270  }
271 
279  function SetParameterValue( $name, $value ) {
280  if ( isset($this->rendered) ) unset($this->rendered);
281  $this->parameters[$name] = $value;
282  }
283 
288  function RenderParameters() {
289  $rendered = "";
290  foreach( $this->parameters AS $k => $v ) {
291  $escaped = preg_replace( "/([;:])/", '\\\\$1', $v);
292  $rendered .= sprintf( ";%s=%s", $k, $escaped );
293  }
294  return $rendered;
295  }
296 
297 
301  function Render() {
302  // If we still have the string it was parsed in from, it hasn't been screwed with
303  // and we can just return that without modification.
304  if ( isset($this->rendered) ) return $this->rendered;
305 
306  $property = preg_replace( '/[;].*$/', '', $this->name );
307  $escaped = $this->content;
308  switch( $property ) {
310  case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
311  case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
312  case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
313  case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
314  case 'RRULE': case 'REPEAT': case 'TRIGGER':
315  break;
316 
317  case 'COMPLETED': case 'DTEND':
318  case 'DUE': case 'DTSTART':
319  case 'DTSTAMP': case 'LAST-MODIFIED':
320  case 'CREATED': case 'EXDATE':
321  case 'RDATE':
322  if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) {
323  $escaped = substr( $escaped, 0, 8);
324  }
325  break;
326 
328  default:
329  $escaped = str_replace( '\\', '\\\\', $escaped);
330  $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
331  $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
332  }
333  $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
334  if ( (strlen($property) + strlen($escaped)) <= 72 ) {
335  $this->rendered = $property . $escaped;
336  }
337  else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) {
338  $this->rendered = $property . "\r\n " . $escaped;
339  }
340  else {
341  $this->rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property . $escaped );
342  }
343  return $this->rendered;
344  }
345 
346 }
347 
348 
364  var $type;
365 
371  var $properties;
372 
378  var $components;
379 
385  var $rendered;
386 
392  function __construct( $content = null ) {
393  $this->type = "";
394  $this->properties = array();
395  $this->components = array();
396  $this->rendered = "";
397  if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) {
398  $this->ParseFrom($content);
399  }
400  }
401 
402 
407  function VCalendar( $extra_properties = null ) {
408  $this->SetType('VCALENDAR');
409  $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN');
410  $this->AddProperty('VERSION', '2.0');
411  $this->AddProperty('CALSCALE', 'GREGORIAN');
412  if ( is_array($extra_properties) ) {
413  foreach( $extra_properties AS $k => $v ) {
414  $this->AddProperty($k,$v);
415  }
416  }
417  }
418 
423  function CollectParameterValues( $parameter_name ) {
424  $values = array();
425  foreach( $this->components AS $k => $v ) {
426  $also = $v->CollectParameterValues($parameter_name);
427  $values = array_merge( $values, $also );
428  }
429  foreach( $this->properties AS $k => $v ) {
430  $also = $v->GetParameterValue($parameter_name);
431  if ( isset($also) && $also != "" ) {
432 // dbg_error_log( 'iCalendar', "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also);
433  $values[$also] = 1;
434  }
435  }
436  return $values;
437  }
438 
439 
444  function ParseFrom( $content ) {
445  $this->rendered = $content;
446  $content = $this->UnwrapComponent($content);
447 
448  $type = false;
449  $subtype = false;
450  $finish = null;
451  $subfinish = null;
452 
453  $length = strlen($content);
454  $linefrom = 0;
455  while( $linefrom < $length ) {
456  $lineto = strpos( $content, "\n", $linefrom );
457  if ( $lineto === false ) {
458  $lineto = strpos( $content, "\r", $linefrom );
459  }
460  if ( $lineto > 0 ) {
461  $line = substr( $content, $linefrom, $lineto - $linefrom);
462  $linefrom = $lineto + 1;
463  }
464  else {
465  $line = substr( $content, $linefrom );
466  $linefrom = $length;
467  }
468  if ( preg_match('/^\s*$/', $line ) ) continue;
469  $line = rtrim( $line, "\r\n" );
470 // dbg_error_log( 'iCalendar', "::ParseFrom: Parsing line: $line");
471 
472  if ( $type === false ) {
473  if ( preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
474  // We have found the start of the main component
475  $type = $matches[1];
476  $finish = "END:$type";
477  $this->type = $type;
478  dbg_error_log( 'iCalendar', "::ParseFrom: Start component of type '%s'", $type);
479  }
480  else {
481  dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap before start of component: $line");
482  // unset($lines[$k]); // The content has crap before the start
483  if ( $line != "" ) $this->rendered = null;
484  }
485  }
486  else if ( $type == null ) {
487  dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap after end of component");
488  if ( $line != "" ) $this->rendered = null;
489  }
490  else if ( $line == $finish ) {
491  dbg_error_log( 'iCalendar', "::ParseFrom: End of component");
492  $type = null; // We have reached the end of our component
493  }
494  else {
495  if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
496  // We have found the start of a sub-component
497  $subtype = $matches[1];
498  $subfinish = "END:$subtype";
499  $subcomponent = $line . "\r\n";
500  dbg_error_log( 'iCalendar', "::ParseFrom: Found a subcomponent '%s'", $subtype);
501  }
502  else if ( $subtype ) {
503  // We are inside a sub-component
504  $subcomponent .= $this->WrapComponent($line);
505  if ( $line == $subfinish ) {
506  dbg_error_log( 'iCalendar', "::ParseFrom: End of subcomponent '%s'", $subtype);
507  // We have found the end of a sub-component
508  $this->components[] = new iCalComponent($subcomponent);
509  $subtype = false;
510  }
511 // else
512 // dbg_error_log( 'iCalendar', "::ParseFrom: Inside a subcomponent '%s'", $subtype );
513  }
514  else {
515 // dbg_error_log( 'iCalendar', "::ParseFrom: Parse property of component");
516  // It must be a normal property line within a component.
517  $this->properties[] = new iCalProp($line);
518  }
519  }
520  }
521  }
522 
523 
529  function UnwrapComponent( $content ) {
530  return preg_replace('/\r?\n[ \t]/', '', $content );
531  }
532 
541  function WrapComponent( $content ) {
542  $strs = preg_split( "/\r?\n/", $content );
543  $wrapped = "";
544  foreach ($strs as $str) {
545  $wrapped .= preg_replace( '/(.{72})/u', '$1'."\r\n ", $str ) ."\r\n";
546  }
547  return $wrapped;
548  }
549 
553  function GetType() {
554  return $this->type;
555  }
556 
557 
561  function SetType( $type ) {
562  if ( isset($this->rendered) ) unset($this->rendered);
563  $this->type = $type;
564  return $this->type;
565  }
566 
567 
571  function GetProperties( $type = null ) {
572  $properties = array();
573  foreach( $this->properties AS $k => $v ) {
574  if ( $type == null || $v->Name() == $type ) {
575  $properties[$k] = $v;
576  }
577  }
578  return $properties;
579  }
580 
581 
589  function GetPValue( $type ) {
590  foreach( $this->properties AS $k => $v ) {
591  if ( $v->Name() == $type ) return $v->Value();
592  }
593  return null;
594  }
595 
596 
605  function GetPParamValue( $type, $parameter_name ) {
606  foreach( $this->properties AS $k => $v ) {
607  if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name);
608  }
609  return null;
610  }
611 
612 
617  function ClearProperties( $type = null ) {
618  if ( $type != null ) {
619  // First remove all the existing ones of that type
620  foreach( $this->properties AS $k => $v ) {
621  if ( $v->Name() == $type ) {
622  unset($this->properties[$k]);
623  if ( isset($this->rendered) ) unset($this->rendered);
624  }
625  }
626  $this->properties = array_values($this->properties);
627  }
628  else {
629  if ( isset($this->rendered) ) unset($this->rendered);
630  $this->properties = array();
631  }
632  }
633 
634 
638  function SetProperties( $new_properties, $type = null ) {
639  if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered);
640  $this->ClearProperties($type);
641  foreach( $new_properties AS $k => $v ) {
642  $this->AddProperty($v);
643  }
644  }
645 
646 
654  function AddProperty( $new_property, $value = null, $parameters = null ) {
655  if ( isset($this->rendered) ) unset($this->rendered);
656  if ( isset($value) && gettype($new_property) == 'string' ) {
657  $new_prop = new iCalProp();
658  $new_prop->Name($new_property);
659  $new_prop->Value($value);
660  if ( $parameters != null ) $new_prop->Parameters($parameters);
661  dbg_error_log('iCalendar'," Adding new property '%s'", $new_prop->Render() );
662  $this->properties[] = $new_prop;
663  }
664  else if ( gettype($new_property) ) {
665  $this->properties[] = $new_property;
666  }
667  }
668 
669 
674  function &FirstNonTimezone( $type = null ) {
675  foreach( $this->components AS $k => $v ) {
676  if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k];
677  }
678  $result = false;
679  return $result;
680  }
681 
682 
689  function IsOrganizer( $email ) {
690  if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
691  $props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER');
692  foreach( $props AS $k => $prop ) {
693  if ( $prop->Value() == $email ) return true;
694  }
695  return false;
696  }
697 
698 
705  function IsAttendee( $email ) {
706  if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
707  if ( $this->IsOrganizer($email) ) return true;
708  $props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE');
709  foreach( $props AS $k => $prop ) {
710  if ( $prop->Value() == $email ) return true;
711  }
712  return false;
713  }
714 
715 
724  function GetComponents( $type = null, $normal_match = true ) {
725  $components = $this->components;
726  if ( $type != null ) {
727  foreach( $components AS $k => $v ) {
728  if ( ($v->GetType() != $type) === $normal_match ) {
729  unset($components[$k]);
730  }
731  }
732  $components = array_values($components);
733  }
734  return $components;
735  }
736 
737 
742  function ClearComponents( $type = null ) {
743  if ( $type != null ) {
744  // First remove all the existing ones of that type
745  foreach( $this->components AS $k => $v ) {
746  if ( $v->GetType() == $type ) {
747  unset($this->components[$k]);
748  if ( isset($this->rendered) ) unset($this->rendered);
749  }
750  else {
751  if ( ! $this->components[$k]->ClearComponents($type) ) {
752  if ( isset($this->rendered) ) unset($this->rendered);
753  }
754  }
755  }
756  return isset($this->rendered);
757  }
758  else {
759  if ( isset($this->rendered) ) unset($this->rendered);
760  $this->components = array();
761  }
762  }
763 
764 
771  function SetComponents( $new_component, $type = null ) {
772  if ( isset($this->rendered) ) unset($this->rendered);
773  if ( count($new_component) > 0 ) $this->ClearComponents($type);
774  foreach( $new_component AS $k => $v ) {
775  $this->components[] = $v;
776  }
777  }
778 
779 
785  function AddComponent( $new_component ) {
786  if ( is_array($new_component) && count($new_component) == 0 ) return;
787  if ( isset($this->rendered) ) unset($this->rendered);
788  if ( is_array($new_component) ) {
789  foreach( $new_component AS $k => $v ) {
790  $this->components[] = $v;
791  }
792  }
793  else {
794  $this->components[] = $new_component;
795  }
796  }
797 
798 
803  function MaskComponents( $keep ) {
804  foreach( $this->components AS $k => $v ) {
805  if ( ! in_array( $v->GetType(), $keep ) ) {
806  unset($this->components[$k]);
807  if ( isset($this->rendered) ) unset($this->rendered);
808  }
809  else {
810  $v->MaskComponents($keep);
811  }
812  }
813  }
814 
815 
821  function MaskProperties( $keep, $component_list=null ) {
822  foreach( $this->components AS $k => $v ) {
823  $v->MaskProperties($keep, $component_list);
824  }
825 
826  if ( !isset($component_list) || in_array($this->GetType(), $component_list) ) {
827  foreach( $this->properties AS $k => $v ) {
828  if ( ! in_array( $v->name, $keep ) ) {
829  unset($this->properties[$k]);
830  if ( isset($this->rendered) ) unset($this->rendered);
831  }
832  }
833  }
834  }
835 
836 
842  function CloneConfidential() {
843  $confidential = clone($this);
844  $keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'DUE', 'UID', 'CLASS', 'TRANSP', 'CREATED', 'LAST-MODIFIED' );
845  $resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' );
846  $confidential->MaskComponents(array( 'VTIMEZONE', 'STANDARD', 'DAYLIGHT', 'VEVENT', 'VTODO', 'VJOURNAL' ));
847  $confidential->MaskProperties($keep_properties, $resource_components );
848 
849  if ( isset($confidential->rendered) )
850  unset($confidential->rendered); // we need to re-render the whole object
851 
852  if ( in_array( $confidential->GetType(), $resource_components ) ) {
853  $confidential->AddProperty( 'SUMMARY', translate('Busy') );
854  }
855  foreach( $confidential->components AS $k => $v ) {
856  if ( in_array( $v->GetType(), $resource_components ) ) {
857  $v->AddProperty( 'SUMMARY', translate('Busy') );
858  }
859  }
860 
861  return $confidential;
862  }
863 
872  function RenderWithoutWrap($restricted_properties = null){
873  // substr - remove new line of end, because new line
874  // are handled in vComponent::RenderWithoutWrap
875  return substr($this->Render($restricted_properties), 0 , -2);
876  }
877 
878 
882  function Render( $restricted_properties = null) {
883 
884  $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0);
885 
886  if ( isset($this->rendered) && $unrestricted )
887  return $this->rendered;
888 
889  $rendered = "BEGIN:$this->type\r\n";
890  foreach( $this->properties AS $k => $v ) {
891  if ( method_exists($v, 'Render') ) {
892  if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n";
893  }
894  }
895  foreach( $this->components AS $v ) { $rendered .= $v->Render(); }
896  $rendered .= "END:$this->type\r\n";
897 
898  $rendered = preg_replace('{(?<!\r)\n}', "\r\n", $rendered);
899  if ( $unrestricted ) $this->rendered = $rendered;
900 
901  return $rendered;
902  }
903 
904 
914  function GetPropertiesByPath( $path ) {
915  $properties = array();
916  dbg_error_log( 'iCalendar', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path );
917  if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties;
918 
919  $adrift = ($matches[1] == '');
920  $normal = ($matches[2] == '');
921  $ourtest = $matches[3];
922  $therest = $matches[4];
923  dbg_error_log( 'iCalendar', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] );
924  if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) {
925  if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) {
926  $normmatch = ($matches[1] =='');
927  $proptest = $matches[2];
928  foreach( $this->properties AS $k => $v ) {
929  if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) {
930  $properties[] = $v;
931  }
932  }
933  }
934  else {
938  foreach( $this->components AS $k => $v ) {
939  $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) );
940  }
941  }
942  }
943 
944  if ( $adrift ) {
948  foreach( $this->components AS $k => $v ) {
949  $properties = array_merge( $properties, $v->GetPropertiesByPath($path) );
950  }
951  }
952  dbg_error_log('iCalendar', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path );
953  return $properties;
954  }
955 
956 }
__construct($propstring=null)
Definition: iCalendar.php:100
ClearComponents($type=null)
Definition: iCalendar.php:742
AddProperty($new_property, $value=null, $parameters=null)
Definition: iCalendar.php:654
GetComponents($type=null, $normal_match=true)
Definition: iCalendar.php:724
Name($newname=null)
Definition: iCalendar.php:204
IsAttendee($email)
Definition: iCalendar.php:705
WrapComponent($content)
Definition: iCalendar.php:541
UnwrapComponent($content)
Definition: iCalendar.php:529
SplitQuoted($str, $sep= ',', $limit=0)
Definition: iCalendar.php:166
Parameters($newparams=null)
Definition: iCalendar.php:237
ClearProperties($type=null)
Definition: iCalendar.php:617
Value($newvalue=null)
Definition: iCalendar.php:221
SetParameterValue($name, $value)
Definition: iCalendar.php:279
RenderWithoutWrap($restricted_properties=null)
Definition: iCalendar.php:872
TextMatch($search)
Definition: iCalendar.php:253
& FirstNonTimezone($type=null)
Definition: iCalendar.php:674
GetProperties($type=null)
Definition: iCalendar.php:571
VCalendar($extra_properties=null)
Definition: iCalendar.php:407
SetProperties($new_properties, $type=null)
Definition: iCalendar.php:638
MaskProperties($keep, $component_list=null)
Definition: iCalendar.php:821
ParseFrom($content)
Definition: iCalendar.php:444
IsOrganizer($email)
Definition: iCalendar.php:689
GetPValue($type)
Definition: iCalendar.php:589
GetPropertiesByPath($path)
Definition: iCalendar.php:914
MaskComponents($keep)
Definition: iCalendar.php:803
ParseFrom($propstring)
Definition: iCalendar.php:119
CollectParameterValues($parameter_name)
Definition: iCalendar.php:423
SetComponents($new_component, $type=null)
Definition: iCalendar.php:771
GetPParamValue($type, $parameter_name)
Definition: iCalendar.php:605
Render($restricted_properties=null)
Definition: iCalendar.php:882
AddComponent($new_component)
Definition: iCalendar.php:785
RenderParameters()
Definition: iCalendar.php:288
SetType($type)
Definition: iCalendar.php:561
GetParameterValue($name)
Definition: iCalendar.php:268
__construct($content=null)
Definition: iCalendar.php:392