1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48:
49: require_once('XMLElement.php');
50: require_once('AwlQuery.php');
51:
52: 53: 54: 55: 56:
57: class iCalProp {
58: 59: 60:
61:
62: 63: 64: 65: 66:
67: var $name;
68:
69: 70: 71: 72: 73:
74: var $parameters;
75:
76: 77: 78: 79: 80:
81: var $content;
82:
83: 84: 85: 86: 87:
88: var $rendered;
89:
90:
91:
92: 93: 94: 95: 96: 97: 98: 99:
100: function iCalProp( $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:
111: 112: 113: 114: 115: 116: 117: 118:
119: function ParseFrom( $propstring ) {
120: $this->rendered = (strlen($propstring) < 72 ? $propstring : null);
121:
122:
123: $unescaped = preg_replace('{\\\\[nN]}', "\n", $propstring);
124:
125: 126: 127: 128: 129: 130: 131: 132: 133:
134: $split = $this->SplitQuoted($unescaped, ':', 2);
135: if (count($split) != 2) {
136:
137: throw new \RuntimeException(sprintf("Couldn't parse property from string: `%s`", $propstring));
138: }
139: list($prop, $value) = $split;
140:
141:
142: $this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $value);
143:
144:
145: $parameters = $this->SplitQuoted($prop, ';');
146: $this->name = array_shift($parameters);
147: $this->parameters = array();
148: foreach ($parameters AS $k => $v) {
149: $pos = strpos($v, '=');
150: $name = substr($v, 0, $pos);
151: $value = substr($v, $pos + 1);
152: $this->parameters[$name] = preg_replace('/^"(.+)"$/', '$1', $value);
153: }
154:
155: }
156:
157: 158: 159: 160: 161: 162: 163: 164:
165: function SplitQuoted($str, $sep = ',', $limit = 0) {
166: $result = array();
167: $cursor = 0;
168: $inquote = false;
169: $num = 0;
170: for($i = 0, $len = strlen($str); $i < $len; ++$i) {
171: $ch = $str[$i];
172: if ($ch == '"') {
173: $inquote = !$inquote;
174: }
175: if (!$inquote && $ch == $sep) {
176:
177:
178: ++$num;
179: if ($limit > 0 && $num == $limit) {
180: $result[] = substr($str, $cursor);
181: break;
182: }
183: $result[] = substr($str, $cursor, $i - $cursor);
184: $cursor = $i + 1;
185: }
186:
187: if ($i + 1 == $len) {
188:
189: $result[] = substr($str, $cursor);
190: }
191: }
192:
193: return $result;
194: }
195:
196: 197: 198: 199: 200: 201: 202:
203: function Name( $newname = null ) {
204: if ( $newname != null ) {
205: $this->name = $newname;
206: if ( isset($this->rendered) ) unset($this->rendered);
207:
208: }
209: return $this->name;
210: }
211:
212:
213: 214: 215: 216: 217: 218: 219:
220: function Value( $newvalue = null ) {
221: if ( $newvalue != null ) {
222: $this->content = $newvalue;
223: if ( isset($this->rendered) ) unset($this->rendered);
224: }
225: return $this->content;
226: }
227:
228:
229: 230: 231: 232: 233: 234: 235:
236: function Parameters( $newparams = null ) {
237: if ( $newparams != null ) {
238: $this->parameters = $newparams;
239: if ( isset($this->rendered) ) unset($this->rendered);
240: }
241: return $this->parameters;
242: }
243:
244:
245: 246: 247: 248: 249: 250: 251:
252: function TextMatch( $search ) {
253: if ( isset($this->content) ) {
254: return (stristr( $this->content, $search ) !== false);
255: }
256: return false;
257: }
258:
259:
260: 261: 262: 263: 264: 265: 266:
267: function GetParameterValue( $name ) {
268: if ( isset($this->parameters[$name]) ) return $this->parameters[$name];
269: }
270:
271: 272: 273: 274: 275: 276: 277:
278: function SetParameterValue( $name, $value ) {
279: if ( isset($this->rendered) ) unset($this->rendered);
280: $this->parameters[$name] = $value;
281: }
282:
283: 284: 285: 286:
287: function RenderParameters() {
288: $rendered = "";
289: foreach( $this->parameters AS $k => $v ) {
290: $escaped = preg_replace( "/([;:])/", '\\\\$1', $v);
291: $rendered .= sprintf( ";%s=%s", $k, $escaped );
292: }
293: return $rendered;
294: }
295:
296:
297: 298: 299:
300: function Render() {
301:
302:
303: if ( isset($this->rendered) ) return $this->rendered;
304:
305: $property = preg_replace( '/[;].*$/', '', $this->name );
306: $escaped = $this->content;
307: switch( $property ) {
308:
309: case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
310: case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
311: case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
312: case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
313: case 'RRULE': case 'REPEAT': case 'TRIGGER':
314: break;
315:
316: case 'COMPLETED': case 'DTEND':
317: case 'DUE': case 'DTSTART':
318: case 'DTSTAMP': case 'LAST-MODIFIED':
319: case 'CREATED': case 'EXDATE':
320: case 'RDATE':
321: if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) {
322: $escaped = substr( $escaped, 0, 8);
323: }
324: break;
325:
326:
327: default:
328: $escaped = str_replace( '\\', '\\\\', $escaped);
329: $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
330: $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
331: }
332: $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
333: if ( (strlen($property) + strlen($escaped)) <= 72 ) {
334: $this->rendered = $property . $escaped;
335: }
336: else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) {
337: $this->rendered = $property . "\r\n " . $escaped;
338: }
339: else {
340: $this->rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property . $escaped );
341: }
342: return $this->rendered;
343: }
344:
345: }
346:
347:
348: 349: 350: 351: 352:
353: class iCalComponent {
354: 355: 356:
357:
358: 359: 360: 361: 362:
363: var $type;
364:
365: 366: 367: 368: 369:
370: var $properties;
371:
372: 373: 374: 375: 376:
377: var $components;
378:
379: 380: 381: 382: 383:
384: var $rendered;
385:
386:
387:
388: 389: 390:
391: function iCalComponent( $content = null ) {
392: $this->type = "";
393: $this->properties = array();
394: $this->components = array();
395: $this->rendered = "";
396: if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) {
397: $this->ParseFrom($content);
398: }
399: }
400:
401:
402: 403: 404: 405:
406: function VCalendar( $extra_properties = null ) {
407: $this->SetType('VCALENDAR');
408: $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN');
409: $this->AddProperty('VERSION', '2.0');
410: $this->AddProperty('CALSCALE', 'GREGORIAN');
411: if ( is_array($extra_properties) ) {
412: foreach( $extra_properties AS $k => $v ) {
413: $this->AddProperty($k,$v);
414: }
415: }
416: }
417:
418: 419: 420: 421:
422: function CollectParameterValues( $parameter_name ) {
423: $values = array();
424: foreach( $this->components AS $k => $v ) {
425: $also = $v->CollectParameterValues($parameter_name);
426: $values = array_merge( $values, $also );
427: }
428: foreach( $this->properties AS $k => $v ) {
429: $also = $v->GetParameterValue($parameter_name);
430: if ( isset($also) && $also != "" ) {
431:
432: $values[$also] = 1;
433: }
434: }
435: return $values;
436: }
437:
438:
439: 440: 441: 442:
443: function ParseFrom( $content ) {
444: $this->rendered = $content;
445: $content = $this->UnwrapComponent($content);
446:
447: $type = false;
448: $subtype = false;
449: $finish = null;
450: $subfinish = null;
451:
452: $length = strlen($content);
453: $linefrom = 0;
454: while( $linefrom < $length ) {
455: $lineto = strpos( $content, "\n", $linefrom );
456: if ( $lineto === false ) {
457: $lineto = strpos( $content, "\r", $linefrom );
458: }
459: if ( $lineto > 0 ) {
460: $line = substr( $content, $linefrom, $lineto - $linefrom);
461: $linefrom = $lineto + 1;
462: }
463: else {
464: $line = substr( $content, $linefrom );
465: $linefrom = $length;
466: }
467: if ( preg_match('/^\s*$/', $line ) ) continue;
468: $line = rtrim( $line, "\r\n" );
469:
470:
471: if ( $type === false ) {
472: if ( preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
473:
474: $type = $matches[1];
475: $finish = "END:$type";
476: $this->type = $type;
477: dbg_error_log( 'iCalendar', "::ParseFrom: Start component of type '%s'", $type);
478: }
479: else {
480: dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap before start of component: $line");
481:
482: if ( $line != "" ) $this->rendered = null;
483: }
484: }
485: else if ( $type == null ) {
486: dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap after end of component");
487: if ( $line != "" ) $this->rendered = null;
488: }
489: else if ( $line == $finish ) {
490: dbg_error_log( 'iCalendar', "::ParseFrom: End of component");
491: $type = null;
492: }
493: else {
494: if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
495:
496: $subtype = $matches[1];
497: $subfinish = "END:$subtype";
498: $subcomponent = $line . "\r\n";
499: dbg_error_log( 'iCalendar', "::ParseFrom: Found a subcomponent '%s'", $subtype);
500: }
501: else if ( $subtype ) {
502:
503: $subcomponent .= $this->WrapComponent($line);
504: if ( $line == $subfinish ) {
505: dbg_error_log( 'iCalendar', "::ParseFrom: End of subcomponent '%s'", $subtype);
506:
507: $this->components[] = new iCalComponent($subcomponent);
508: $subtype = false;
509: }
510:
511:
512: }
513: else {
514:
515:
516: $this->properties[] = new iCalProp($line);
517: }
518: }
519: }
520: }
521:
522:
523: 524: 525: 526: 527:
528: function UnwrapComponent( $content ) {
529: return preg_replace('/\r?\n[ \t]/', '', $content );
530: }
531:
532: 533: 534: 535: 536: 537: 538: 539:
540: function WrapComponent( $content ) {
541: $strs = preg_split( "/\r?\n/", $content );
542: $wrapped = "";
543: foreach ($strs as $str) {
544: $wrapped .= preg_replace( '/(.{72})/u', '$1'."\r\n ", $str ) ."\r\n";
545: }
546: return $wrapped;
547: }
548:
549: 550: 551:
552: function GetType() {
553: return $this->type;
554: }
555:
556:
557: 558: 559:
560: function SetType( $type ) {
561: if ( isset($this->rendered) ) unset($this->rendered);
562: $this->type = $type;
563: return $this->type;
564: }
565:
566:
567: 568: 569:
570: function GetProperties( $type = null ) {
571: $properties = array();
572: foreach( $this->properties AS $k => $v ) {
573: if ( $type == null || $v->Name() == $type ) {
574: $properties[$k] = $v;
575: }
576: }
577: return $properties;
578: }
579:
580:
581: 582: 583: 584: 585: 586: 587:
588: function GetPValue( $type ) {
589: foreach( $this->properties AS $k => $v ) {
590: if ( $v->Name() == $type ) return $v->Value();
591: }
592: return null;
593: }
594:
595:
596: 597: 598: 599: 600: 601: 602: 603:
604: function GetPParamValue( $type, $parameter_name ) {
605: foreach( $this->properties AS $k => $v ) {
606: if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name);
607: }
608: return null;
609: }
610:
611:
612: 613: 614: 615:
616: function ClearProperties( $type = null ) {
617: if ( $type != null ) {
618:
619: foreach( $this->properties AS $k => $v ) {
620: if ( $v->Name() == $type ) {
621: unset($this->properties[$k]);
622: if ( isset($this->rendered) ) unset($this->rendered);
623: }
624: }
625: $this->properties = array_values($this->properties);
626: }
627: else {
628: if ( isset($this->rendered) ) unset($this->rendered);
629: $this->properties = array();
630: }
631: }
632:
633:
634: 635: 636:
637: function SetProperties( $new_properties, $type = null ) {
638: if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered);
639: $this->ClearProperties($type);
640: foreach( $new_properties AS $k => $v ) {
641: $this->AddProperty($v);
642: }
643: }
644:
645:
646: 647: 648: 649: 650: 651: 652:
653: function AddProperty( $new_property, $value = null, $parameters = null ) {
654: if ( isset($this->rendered) ) unset($this->rendered);
655: if ( isset($value) && gettype($new_property) == 'string' ) {
656: $new_prop = new iCalProp();
657: $new_prop->Name($new_property);
658: $new_prop->Value($value);
659: if ( $parameters != null ) $new_prop->Parameters($parameters);
660: dbg_error_log('iCalendar'," Adding new property '%s'", $new_prop->Render() );
661: $this->properties[] = $new_prop;
662: }
663: else if ( gettype($new_property) ) {
664: $this->properties[] = $new_property;
665: }
666: }
667:
668:
669: 670: 671: 672:
673: function &FirstNonTimezone( $type = null ) {
674: foreach( $this->components AS $k => $v ) {
675: if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k];
676: }
677: $result = false;
678: return $result;
679: }
680:
681:
682: 683: 684: 685: 686: 687:
688: function IsOrganizer( $email ) {
689: if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
690: $props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER');
691: foreach( $props AS $k => $prop ) {
692: if ( $prop->Value() == $email ) return true;
693: }
694: return false;
695: }
696:
697:
698: 699: 700: 701: 702: 703:
704: function IsAttendee( $email ) {
705: if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
706: if ( $this->IsOrganizer($email) ) return true;
707: $props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE');
708: foreach( $props AS $k => $prop ) {
709: if ( $prop->Value() == $email ) return true;
710: }
711: return false;
712: }
713:
714:
715: 716: 717: 718: 719: 720: 721: 722:
723: function GetComponents( $type = null, $normal_match = true ) {
724: $components = $this->components;
725: if ( $type != null ) {
726: foreach( $components AS $k => $v ) {
727: if ( ($v->GetType() != $type) === $normal_match ) {
728: unset($components[$k]);
729: }
730: }
731: $components = array_values($components);
732: }
733: return $components;
734: }
735:
736:
737: 738: 739: 740:
741: function ClearComponents( $type = null ) {
742: if ( $type != null ) {
743:
744: foreach( $this->components AS $k => $v ) {
745: if ( $v->GetType() == $type ) {
746: unset($this->components[$k]);
747: if ( isset($this->rendered) ) unset($this->rendered);
748: }
749: else {
750: if ( ! $this->components[$k]->ClearComponents($type) ) {
751: if ( isset($this->rendered) ) unset($this->rendered);
752: }
753: }
754: }
755: return isset($this->rendered);
756: }
757: else {
758: if ( isset($this->rendered) ) unset($this->rendered);
759: $this->components = array();
760: }
761: }
762:
763:
764: 765: 766: 767: 768: 769:
770: function SetComponents( $new_component, $type = null ) {
771: if ( isset($this->rendered) ) unset($this->rendered);
772: if ( count($new_component) > 0 ) $this->ClearComponents($type);
773: foreach( $new_component AS $k => $v ) {
774: $this->components[] = $v;
775: }
776: }
777:
778:
779: 780: 781: 782: 783:
784: function AddComponent( $new_component ) {
785: if ( is_array($new_component) && count($new_component) == 0 ) return;
786: if ( isset($this->rendered) ) unset($this->rendered);
787: if ( is_array($new_component) ) {
788: foreach( $new_component AS $k => $v ) {
789: $this->components[] = $v;
790: }
791: }
792: else {
793: $this->components[] = $new_component;
794: }
795: }
796:
797:
798: 799: 800: 801:
802: function MaskComponents( $keep ) {
803: foreach( $this->components AS $k => $v ) {
804: if ( ! in_array( $v->GetType(), $keep ) ) {
805: unset($this->components[$k]);
806: if ( isset($this->rendered) ) unset($this->rendered);
807: }
808: else {
809: $v->MaskComponents($keep);
810: }
811: }
812: }
813:
814:
815: 816: 817: 818: 819:
820: function MaskProperties( $keep, $component_list=null ) {
821: foreach( $this->components AS $k => $v ) {
822: $v->MaskProperties($keep, $component_list);
823: }
824:
825: if ( !isset($component_list) || in_array($this->GetType(), $component_list) ) {
826: foreach( $this->properties AS $k => $v ) {
827: if ( ! in_array( $v->name, $keep ) ) {
828: unset($this->properties[$k]);
829: if ( isset($this->rendered) ) unset($this->rendered);
830: }
831: }
832: }
833: }
834:
835:
836: 837: 838: 839: 840:
841: function CloneConfidential() {
842: $confidential = clone($this);
843: $keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'DUE', 'UID', 'CLASS', 'TRANSP', 'CREATED', 'LAST-MODIFIED' );
844: $resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' );
845: $confidential->MaskComponents(array( 'VTIMEZONE', 'STANDARD', 'DAYLIGHT', 'VEVENT', 'VTODO', 'VJOURNAL' ));
846: $confidential->MaskProperties($keep_properties, $resource_components );
847:
848: if ( isset($confidential->rendered) )
849: unset($confidential->rendered);
850:
851: if ( in_array( $confidential->GetType(), $resource_components ) ) {
852: $confidential->AddProperty( 'SUMMARY', translate('Busy') );
853: }
854: foreach( $confidential->components AS $k => $v ) {
855: if ( in_array( $v->GetType(), $resource_components ) ) {
856: $v->AddProperty( 'SUMMARY', translate('Busy') );
857: }
858: }
859:
860: return $confidential;
861: }
862:
863: 864: 865: 866: 867: 868: 869: 870:
871: function RenderWithoutWrap($restricted_properties = null){
872:
873:
874: return substr($this->Render($restricted_properties), 0 , -2);
875: }
876:
877:
878: 879: 880:
881: function Render( $restricted_properties = null) {
882:
883: $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0);
884:
885: if ( isset($this->rendered) && $unrestricted )
886: return $this->rendered;
887:
888: $rendered = "BEGIN:$this->type\r\n";
889: foreach( $this->properties AS $k => $v ) {
890: if ( method_exists($v, 'Render') ) {
891: if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n";
892: }
893: }
894: foreach( $this->components AS $v ) { $rendered .= $v->Render(); }
895: $rendered .= "END:$this->type\r\n";
896:
897: $rendered = preg_replace('{(?<!\r)\n}', "\r\n", $rendered);
898: if ( $unrestricted ) $this->rendered = $rendered;
899:
900: return $rendered;
901: }
902:
903:
904: 905: 906: 907: 908: 909: 910: 911: 912:
913: function GetPropertiesByPath( $path ) {
914: $properties = array();
915: dbg_error_log( 'iCalendar', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path );
916: if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties;
917:
918: $adrift = ($matches[1] == '');
919: $normal = ($matches[2] == '');
920: $ourtest = $matches[3];
921: $therest = $matches[4];
922: dbg_error_log( 'iCalendar', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] );
923: if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) {
924: if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) {
925: $normmatch = ($matches[1] =='');
926: $proptest = $matches[2];
927: foreach( $this->properties AS $k => $v ) {
928: if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) {
929: $properties[] = $v;
930: }
931: }
932: }
933: else {
934: 935: 936:
937: foreach( $this->components AS $k => $v ) {
938: $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) );
939: }
940: }
941: }
942:
943: if ( $adrift ) {
944: 945: 946:
947: foreach( $this->components AS $k => $v ) {
948: $properties = array_merge( $properties, $v->GetPropertiesByPath($path) );
949: }
950: }
951: dbg_error_log('iCalendar', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path );
952: return $properties;
953: }
954:
955: }
956: