fix image text
[pear] / iCal / Vcalendar.php
1 <?php
2 /*********************************************************************************/
3 /**
4  * iCalcreator v2.10.15
5  * copyright (c) 2007-2011 Kjell-Inge Gustafsson kigkonsult
6  * kigkonsult.se/iCalcreator/index.php
7  * ical@kigkonsult.se
8  *
9  * Description:
10  * This file is a PHP implementation of RFC 2445.
11  *
12  * This library is free software; you can redistribute it and/or
13  * modify it under the terms of the GNU Lesser General Public
14  * License as published by the Free Software Foundation; either
15  * version 2.1 of the License, or (at your option) any later version.
16  *
17  * This library is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20  * Lesser General Public License for more details.
21  *
22  * You should have received a copy of the GNU Lesser General Public
23  * License along with this library; if not, write to the Free Software
24  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25  */
26 /*********************************************************************************/
27 /*********************************************************************************/
28 /*         A little setup                                                        */
29 /*********************************************************************************/
30             /* your local language code */
31 // define( 'ICAL_LANG', 'sv' );
32             // alt. autosetting
33 /*
34 $langstr     = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
35 $pos         = strpos( $langstr, ';' );
36 if ($pos   !== false) {
37   $langstr   = substr( $langstr, 0, $pos );
38   $pos       = strpos( $langstr, ',' );
39   if ($pos !== false) {
40     $pos     = strpos( $langstr, ',' );
41     $langstr = substr( $langstr, 0, $pos );
42   }
43   define( 'ICAL_LANG', $langstr );
44 }
45 */
46 /*********************************************************************************/
47 /*         only for phpversion 5.1 and later,                                    */
48 /*         date management, default timezone setting                             */
49 /*         since 2.6.36 - 2010-12-31 */
50 if( substr( phpversion(), 0, 3 ) >= '5.1' )
51   // && ( 'UTC' == date_default_timezone_get()))
52   date_default_timezone_set( 'Asia/Hong_Kong' );
53 /*********************************************************************************/
54 /*         since 2.6.22 - 2010-09-25, do NOT remove!!                            */
55 require_once 'UtilityFunctions.php';
56 require_once 'calendarComponent.php';
57 require_once 'valarm.php';
58 require_once 'vevent.php';
59 require_once 'vfreebusy.php';
60 require_once 'vjournal.php';
61 require_once 'vtimezone.php';
62 require_once 'vtodo.php';
63
64
65
66 /*********************************************************************************/
67 /*         version, do NOT remove!!                                              */
68 define( 'ICALCREATOR_VERSION', 'iCalcreator 2.10.15' );
69 /*********************************************************************************/
70 /*********************************************************************************/
71 /**
72  * vcalendar class
73  *
74  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
75  * @since 2.9.6 - 2011-05-14
76  */
77 class iCal_Vcalendar {
78             //  calendar property variables
79   var $calscale;
80   var $method;
81   var $prodid;
82   var $version;
83   var $xprop;
84             //  container for calendar components
85   var $components;
86             //  component config variables
87   var $allowEmpty;
88   var $unique_id;
89   var $language;
90   var $directory;
91   var $filename;
92   var $url;
93   var $delimiter;
94   var $nl;
95   var $format;
96   var $dtzid;
97             //  component internal variables
98   var $attributeDelimiter;
99   var $valueInit;
100             //  component xCal declaration container
101   var $xcaldecl;
102 /**
103  * constructor for calendar object
104  *
105  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
106  * @since 2.9.6 - 2011-05-14
107  * @param array $config
108  * @return void
109  */
110   function iCal_Vcalendar ( $config = array()) {
111     $this->_makeVersion();
112     $this->calscale   = null;
113     $this->method     = null;
114     $this->_makeUnique_id();
115     $this->prodid     = null;
116     $this->xprop      = array();
117     $this->language   = null;
118     $this->directory  = null;
119     $this->filename   = null;
120     $this->url        = null;
121     $this->dtzid      = null;
122 /**
123  *   language = <Text identifying a language, as defined in [RFC 1766]>
124  */
125     if( defined( 'ICAL_LANG' ) && !isset( $config['language'] ))
126                                           $config['language']   = ICAL_LANG;
127     if( !isset( $config['allowEmpty'] ))  $config['allowEmpty'] = TRUE;
128     if( !isset( $config['nl'] ))          $config['nl']         = "\r\n";
129     if( !isset( $config['format'] ))      $config['format']     = 'iCal';
130     if( !isset( $config['delimiter'] ))   $config['delimiter']  = DIRECTORY_SEPARATOR;
131     $this->setConfig( $config );
132
133     $this->xcaldecl   = array();
134     $this->components = array();
135   }
136 /*********************************************************************************/
137 /**
138  * Property Name: CALSCALE
139  */
140 /**
141  * creates formatted output for calendar property calscale
142  *
143  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
144  * @since 2.4.8 - 2008-10-21
145  * @return string
146  */
147   function createCalscale() {
148     if( empty( $this->calscale )) return FALSE;
149     switch( $this->format ) {
150       case 'xcal':
151         return ' calscale="'.$this->calscale.'"'.$this->nl;
152         break;
153       default:
154         return 'CALSCALE:'.$this->calscale.$this->nl;
155         break;
156     }
157   }
158 /**
159  * set calendar property calscale
160  *
161  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
162  * @since 2.4.8 - 2008-10-21
163  * @param string $value
164  * @return void
165  */
166   function setCalscale( $value ) {
167     if( empty( $value )) return FALSE;
168     $this->calscale = $value;
169   }
170 /*********************************************************************************/
171 /**
172  * Property Name: METHOD
173  */
174 /**
175  * creates formatted output for calendar property method
176  *
177  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
178  * @since 0.9.7 - 2006-11-20
179  * @return string
180  */
181   function createMethod() {
182     if( empty( $this->method )) return FALSE;
183     switch( $this->format ) {
184       case 'xcal':
185         return ' method="'.$this->method.'"'.$this->nl;
186         break;
187       default:
188         return 'METHOD:'.$this->method.$this->nl;
189         break;
190     }
191   }
192 /**
193  * set calendar property method
194  *
195  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
196  * @since 2.4.8 - 2008-20-23
197  * @param string $value
198  * @return bool
199  */
200   function setMethod( $value ) {
201     if( empty( $value )) return FALSE;
202     $this->method = $value;
203     return TRUE;
204   }
205 /*********************************************************************************/
206 /**
207  * Property Name: PRODID
208  *
209  *  The identifier is RECOMMENDED to be the identical syntax to the
210  * [RFC 822] addr-spec. A good method to assure uniqueness is to put the
211  * domain name or a domain literal IP address of the host on which.. .
212  */
213 /**
214  * creates formatted output for calendar property prodid
215  *
216  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
217  * @since 0.9.7 - 2006-11-20
218  * @return string
219  */
220   function createProdid() {
221     if( !isset( $this->prodid ))
222       $this->_makeProdid();
223     switch( $this->format ) {
224       case 'xcal':
225         return ' prodid="'.$this->prodid.'"'.$this->nl;
226         break;
227       default:
228         return 'PRODID:'.$this->prodid.$this->nl;
229         break;
230     }
231   }
232 /**
233  * make default value for calendar prodid
234  *
235  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
236  * @since 2.6.8 - 2009-12-30
237  * @return void
238  */
239   function _makeProdid() {
240     $this->prodid  = '-//'.$this->unique_id.'//NONSGML kigkonsult.se '.ICALCREATOR_VERSION.'//'.strtoupper( $this->language );
241   }
242 /**
243  * Conformance: The property MUST be specified once in an iCalendar object.
244  * Description: The vendor of the implementation SHOULD assure that this
245  * is a globally unique identifier; using some technique such as an FPI
246  * value, as defined in [ISO 9070].
247  */
248 /**
249  * make default unique_id for calendar prodid
250  *
251  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
252  * @since 0.3.0 - 2006-08-10
253  * @return void
254  */
255   function _makeUnique_id() {
256     $this->unique_id  = ( isset( $_SERVER['SERVER_NAME'] )) ? gethostbyname( $_SERVER['SERVER_NAME'] ) : 'localhost';
257   }
258 /*********************************************************************************/
259 /**
260  * Property Name: VERSION
261  *
262  * Description: A value of "2.0" corresponds to this memo.
263  */
264 /**
265  * creates formatted output for calendar property version
266
267  *
268  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
269  * @since 0.9.7 - 2006-11-20
270  * @return string
271  */
272   function createVersion() {
273     if( empty( $this->version ))
274       $this->_makeVersion();
275     switch( $this->format ) {
276       case 'xcal':
277         return ' version="'.$this->version.'"'.$this->nl;
278         break;
279       default:
280         return 'VERSION:'.$this->version.$this->nl;
281         break;
282     }
283   }
284 /**
285  * set default calendar version
286  *
287  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
288  * @since 0.3.0 - 2006-08-10
289  * @return void
290  */
291   function _makeVersion() {
292     $this->version = '2.0';
293   }
294 /**
295  * set calendar version
296  *
297  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
298  * @since 2.4.8 - 2008-10-23
299  * @param string $value
300  * @return void
301  */
302   function setVersion( $value ) {
303     if( empty( $value )) return FALSE;
304     $this->version = $value;
305     return TRUE;
306   }
307 /*********************************************************************************/
308 /**
309  * Property Name: x-prop
310  */
311 /**
312  * creates formatted output for calendar property x-prop, iCal format only
313  *
314  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
315  * @since 2.9.3 - 2011-05-14
316  * @return string
317  */
318   function createXprop() {
319     if( 'xcal' == $this->format )
320       return false;
321     if( empty( $this->xprop ) || !is_array( $this->xprop )) return FALSE;
322     $output = null;
323     $toolbox = new calendarComponent();
324     $toolbox->setConfig( $this->getConfig());
325     foreach( $this->xprop as $label => $xpropPart ) {
326       if( !isset($xpropPart['value']) || ( empty( $xpropPart['value'] ) && !is_numeric( $xpropPart['value'] ))) {
327         $output  .= $toolbox->_createElement( $label );
328         continue;
329       }
330       $attributes = $toolbox->_createParams( $xpropPart['params'], array( 'LANGUAGE' ));
331       if( is_array( $xpropPart['value'] )) {
332         foreach( $xpropPart['value'] as $pix => $theXpart )
333           $xpropPart['value'][$pix] = $toolbox->_strrep( $theXpart );
334         $xpropPart['value']  = implode( ',', $xpropPart['value'] );
335       }
336       else
337         $xpropPart['value'] = $toolbox->_strrep( $xpropPart['value'] );
338       $output    .= $toolbox->_createElement( $label, $attributes, $xpropPart['value'] );
339     }
340     return $output;
341   }
342 /**
343  * set calendar property x-prop
344  *
345  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
346  * @since 2.9.3 - 2011-05-14
347  * @param string $label
348  * @param string $value
349  * @param array $params optional
350  * @return bool
351  */
352   function setXprop( $label, $value, $params=FALSE ) {
353     if( empty( $label )) return FALSE;
354     if( empty( $value ) && !is_numeric( $value )) if( $this->getConfig( 'allowEmpty' )) $value = null; else return FALSE;
355     $xprop           = array( 'value' => $value );
356     $xprop['params'] = iCal_UtilityFunctions::_setParams( $params );
357     if( !is_array( $this->xprop )) $this->xprop = array();
358     $this->xprop[strtoupper( $label )] = $xprop;
359     return TRUE;
360   }
361 /*********************************************************************************/
362 /**
363  * delete calendar property value
364  *
365  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
366  * @since 2.8.8 - 2011-03-15
367  * @param mixed $propName, bool FALSE => X-property
368  * @param int $propix, optional, if specific property is wanted in case of multiply occurences
369  * @return bool, if successfull delete
370  */
371   function deleteProperty( $propName=FALSE, $propix=FALSE ) {
372     $propName = ( $propName ) ? strtoupper( $propName ) : 'X-PROP';
373     if( !$propix )
374       $propix = ( isset( $this->propdelix[$propName] ) && ( 'X-PROP' != $propName )) ? $this->propdelix[$propName] + 2 : 1;
375     $this->propdelix[$propName] = --$propix;
376     $return = FALSE;
377     switch( $propName ) {
378       case 'CALSCALE':
379         if( isset( $this->calscale )) {
380           $this->calscale = null;
381           $return = TRUE;
382         }
383         break;
384       case 'METHOD':
385         if( isset( $this->method )) {
386           $this->method   = null;
387           $return = TRUE;
388         }
389         break;
390       default:
391         $reduced = array();
392         if( $propName != 'X-PROP' ) {
393           if( !isset( $this->xprop[$propName] )) { unset( $this->propdelix[$propName] ); return FALSE; }
394           foreach( $this->xprop as $k => $a ) {
395             if(( $k != $propName ) && !empty( $a ))
396               $reduced[$k] = $a;
397           }
398         }
399         else {
400           if( count( $this->xprop ) <= $propix )  return FALSE;
401           $xpropno = 0;
402           foreach( $this->xprop as $xpropkey => $xpropvalue ) {
403             if( $propix != $xpropno )
404               $reduced[$xpropkey] = $xpropvalue;
405             $xpropno++;
406           }
407         }
408         $this->xprop = $reduced;
409         if( empty( $this->xprop )) {
410           unset( $this->propdelix[$propName] );
411           return FALSE;
412         }
413         return TRUE;
414     }
415     return $return;
416   }
417 /**
418  * get calendar property value/params
419  *
420  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
421  * @since 2.8.8 - 2011-04-16
422  * @param string $propName, optional
423  * @param int $propix, optional, if specific property is wanted in case of multiply occurences
424  * @param bool $inclParam=FALSE
425  * @return mixed
426  */
427   function getProperty( $propName=FALSE, $propix=FALSE, $inclParam=FALSE ) {
428     $propName = ( $propName ) ? strtoupper( $propName ) : 'X-PROP';
429     if( 'X-PROP' == $propName ) {
430       if( !$propix )
431         $propix = ( isset( $this->propix[$propName] )) ? $this->propix[$propName] + 2 : 1;
432       $this->propix[$propName] = --$propix;
433     }
434     switch( $propName ) {
435       case 'ATTENDEE':
436       case 'CATEGORIES':
437       case 'DTSTART':
438       case 'LOCATION':
439       case 'ORGANIZER':
440       case 'PRIORITY':
441       case 'RESOURCES':
442       case 'STATUS':
443       case 'SUMMARY':
444       case 'RECURRENCE-ID-UID':
445       case 'R-UID':
446       case 'UID':
447         $output = array();
448         foreach ( $this->components as $cix => $component) {
449           if( !in_array( $component->objName, array('vevent', 'vtodo', 'vjournal', 'vfreebusy' )))
450             continue;
451           if(( 'ATTENDEE' == $propName ) || ( 'CATEGORIES' == $propName ) || ( 'RESOURCES' == $propName )) {
452             $component->_getProperties( $propName, $output );
453             continue;
454           }
455           elseif(( 3 < strlen( $propName )) && ( 'UID' == substr( $propName, -3 ))) {
456             if( FALSE !== ( $content = $component->getProperty( 'RECURRENCE-ID' )))
457               $content = $component->getProperty( 'UID' );
458           }
459           elseif( FALSE === ( $content = $component->getProperty( $propName )))
460             continue;
461           if( FALSE === $content )
462             continue;
463           elseif( is_array( $content )) {
464             if( isset( $content['year'] )) {
465               $key  = sprintf( '%04d%02d%02d', $content['year'], $content['month'], $content['day'] );
466               if( !isset( $output[$key] ))
467                 $output[$key] = 1;
468               else
469                 $output[$key] += 1;
470             }
471             else {
472               foreach( $content as $partValue => $partCount ) {
473                 if( !isset( $output[$partValue] ))
474                   $output[$partValue] = $partCount;
475                 else
476                   $output[$partValue] += $partCount;
477               }
478             }
479           } // end elseif( is_array( $content )) {
480           elseif( !isset( $output[$content] ))
481             $output[$content] = 1;
482           else
483             $output[$content] += 1;
484         } // end foreach ( $this->components as $cix => $component)
485         if( !empty( $output ))
486           ksort( $output );
487         return $output;
488         break;
489
490       case 'CALSCALE':
491         return ( !empty( $this->calscale )) ? $this->calscale : FALSE;
492         break;
493       case 'METHOD':
494         return ( !empty( $this->method )) ? $this->method : FALSE;
495         break;
496       case 'PRODID':
497         if( empty( $this->prodid ))
498           $this->_makeProdid();
499         return $this->prodid;
500         break;
501       case 'VERSION':
502         return ( !empty( $this->version )) ? $this->version : FALSE;
503         break;
504       default:
505         if( $propName != 'X-PROP' ) {
506           if( !isset( $this->xprop[$propName] )) return FALSE;
507           return ( $inclParam ) ? array( $propName, $this->xprop[$propName] )
508                                 : array( $propName, $this->xprop[$propName]['value'] );
509         }
510         else {
511           if( empty( $this->xprop )) return FALSE;
512           $xpropno = 0;
513           foreach( $this->xprop as $xpropkey => $xpropvalue ) {
514             if( $propix == $xpropno )
515               return ( $inclParam ) ? array( $xpropkey, $this->xprop[$xpropkey] )
516                                     : array( $xpropkey, $this->xprop[$xpropkey]['value'] );
517             else
518               $xpropno++;
519           }
520           unset( $this->propix[$propName] );
521           return FALSE; // not found ??
522         }
523     }
524     return FALSE;
525   }
526 /**
527  * general iCal_Vcalendar property setting
528  *
529  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
530  * @since 2.2.13 - 2007-11-04
531  * @param mixed $args variable number of function arguments,
532  *                    first argument is ALWAYS component name,
533  *                    second ALWAYS component value!
534  * @return bool
535  */
536   function setProperty () {
537     $numargs    = func_num_args();
538     if( 1 > $numargs )
539       return FALSE;
540     $arglist    = func_get_args();
541     $arglist[0] = strtoupper( $arglist[0] );
542     switch( $arglist[0] ) {
543       case 'CALSCALE':
544         return $this->setCalscale( $arglist[1] );
545       case 'METHOD':
546         return $this->setMethod( $arglist[1] );
547       case 'VERSION':
548         return $this->setVersion( $arglist[1] );
549       default:
550         if( !isset( $arglist[1] )) $arglist[1] = null;
551         if( !isset( $arglist[2] )) $arglist[2] = null;
552         return $this->setXprop( $arglist[0], $arglist[1], $arglist[2] );
553     }
554     return FALSE;
555   }
556 /*********************************************************************************/
557 /**
558  * get iCal_Vcalendar config values or * calendar components
559  *
560  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
561  * @since 2.9.6 - 2011-05-14
562  * @param mixed $config
563  * @return value
564  */
565   function getConfig( $config = FALSE ) {
566     if( !$config ) {
567       $return = array();
568       $return['ALLOWEMPTY']  = $this->getConfig( 'ALLOWEMPTY' );
569       $return['DELIMITER']   = $this->getConfig( 'DELIMITER' );
570       $return['DIRECTORY']   = $this->getConfig( 'DIRECTORY' );
571       $return['FILENAME']    = $this->getConfig( 'FILENAME' );
572       $return['DIRFILE']     = $this->getConfig( 'DIRFILE' );
573       $return['FILESIZE']    = $this->getConfig( 'FILESIZE' );
574       $return['FORMAT']      = $this->getConfig( 'FORMAT' );
575       if( FALSE !== ( $lang  = $this->getConfig( 'LANGUAGE' )))
576         $return['LANGUAGE']  = $lang;
577       $return['NEWLINECHAR'] = $this->getConfig( 'NEWLINECHAR' );
578       $return['UNIQUE_ID']   = $this->getConfig( 'UNIQUE_ID' );
579       if( FALSE !== ( $url   = $this->getConfig( 'URL' )))
580         $return['URL']       = $url;
581       $return['TZID']        = $this->getConfig( 'TZID' );
582       return $return;
583     }
584     switch( strtoupper( $config )) {
585       case 'ALLOWEMPTY':
586         return $this->allowEmpty;
587         break;
588       case 'COMPSINFO':
589         unset( $this->compix );
590         $info = array();
591         foreach( $this->components as $cix => $component ) {
592           if( empty( $component )) continue;
593           $info[$cix]['ordno'] = $cix + 1;
594           $info[$cix]['type']  = $component->objName;
595           $info[$cix]['uid']   = $component->getProperty( 'uid' );
596           $info[$cix]['props'] = $component->getConfig( 'propinfo' );
597           $info[$cix]['sub']   = $component->getConfig( 'compsinfo' );
598         }
599         return $info;
600         break;
601       case 'DELIMITER':
602         return $this->delimiter;
603         break;
604       case 'DIRECTORY':
605         if( empty( $this->directory ))
606           $this->directory = '.';
607         return $this->directory;
608         break;
609       case 'DIRFILE':
610         return $this->getConfig( 'directory' ).$this->getConfig( 'delimiter' ).$this->getConfig( 'filename' );
611         break;
612       case 'FILEINFO':
613         return array( $this->getConfig( 'directory' )
614                     , $this->getConfig( 'filename' )
615                     , $this->getConfig( 'filesize' ));
616         break;
617       case 'FILENAME':
618         if( empty( $this->filename )) {
619           if( 'xcal' == $this->format )
620             $this->filename = date( 'YmdHis' ).'.xml'; // recommended xcs.. .
621           else
622             $this->filename = date( 'YmdHis' ).'.ics';
623         }
624         return $this->filename;
625         break;
626       case 'FILESIZE':
627         $size    = 0;
628         if( empty( $this->url )) {
629           $dirfile = $this->getConfig( 'dirfile' );
630           if( !is_file( $dirfile ) || ( FALSE === ( $size = filesize( $dirfile ))))
631             $size = 0;
632           clearstatcache();
633         }
634         return $size;
635         break;
636       case 'FORMAT':
637         return ( $this->format == 'xcal' ) ? 'xCal' : 'iCal';
638         break;
639       case 'LANGUAGE':
640          /* get language for calendar component as defined in [RFC 1766] */
641         return $this->language;
642         break;
643       case 'NL':
644       case 'NEWLINECHAR':
645         return $this->nl;
646         break;
647       case 'TZID':
648         return $this->dtzid;
649         break;
650       case 'UNIQUE_ID':
651         return $this->unique_id;
652         break;
653       case 'URL':
654         if( !empty( $this->url ))
655           return $this->url;
656         else
657           return FALSE;
658         break;
659     }
660   }
661 /**
662  * general iCal_Vcalendar config setting
663  *
664  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
665  * @since 2.10.10 - 2011-09-19
666  * @param mixed  $config
667  * @param string $value
668  * @return void
669  */
670   function setConfig( $config, $value = FALSE) {
671     if( is_array( $config )) {
672       $ak = array_keys( $config );
673       foreach( $ak as $k ) {
674         if( 'DIRECTORY' == strtoupper( $k )) {
675           if( FALSE === $this->setConfig( 'DIRECTORY', $config[$k] ))
676             return FALSE;
677           unset( $config[$k] );
678           break;
679         }
680       }
681       foreach( $config as $cKey => $cValue ) {
682         if( FALSE === $this->setConfig( $cKey, $cValue ))
683           return FALSE;
684       }
685       return TRUE;
686     }
687     $res = FALSE;
688     switch( strtoupper( $config )) {
689       case 'ALLOWEMPTY':
690         $this->allowEmpty = $value;
691         $subcfg  = array( 'ALLOWEMPTY' => $value );
692         $res = TRUE;
693         break;
694       case 'DELIMITER':
695         $this->delimiter = $value;
696         return TRUE;
697         break;
698       case 'DIRECTORY':
699         $value   = trim( $value );
700         $del     = $this->getConfig('delimiter');
701         if( $del == substr( $value, ( 0 - strlen( $del ))))
702           $value = substr( $value, 0, ( strlen( $value ) - strlen( $del )));
703         if( is_dir( $value )) {
704             /* local directory */
705           clearstatcache();
706           $this->directory = $value;
707           $this->url       = null;
708           return TRUE;
709         }
710         else
711           return FALSE;
712         break;
713       case 'FILENAME':
714         $value   = trim( $value );
715         if( !empty( $this->url )) {
716             /* remote directory+file -> URL */
717           $this->filename = $value;
718           return TRUE;
719         }
720         $dirfile = $this->getConfig( 'directory' ).$this->getConfig( 'delimiter' ).$value;
721         if( file_exists( $dirfile )) {
722             /* local file exists */
723           if( is_readable( $dirfile ) || is_writable( $dirfile )) {
724             clearstatcache();
725             $this->filename = $value;
726             return TRUE;
727           }
728           else
729             return FALSE;
730         }
731         elseif( is_readable($this->getConfig( 'directory' ) ) || is_writable( $this->getConfig( 'directory' ) )) {
732             /* read- or writable directory */
733           $this->filename = $value;
734           return TRUE;
735         }
736         else
737           return FALSE;
738         break;
739       case 'FORMAT':
740         $value   = trim( strtolower( $value ));
741         if( 'xcal' == $value ) {
742           $this->format             = 'xcal';
743           $this->attributeDelimiter = $this->nl;
744           $this->valueInit          = null;
745         }
746         else {
747           $this->format             = null;
748           $this->attributeDelimiter = ';';
749           $this->valueInit          = ':';
750         }
751         $subcfg  = array( 'FORMAT' => $value );
752         $res = TRUE;
753         break;
754       case 'LANGUAGE':
755          // set language for calendar component as defined in [RFC 1766]
756         $value   = trim( $value );
757         $this->language = $value;
758         $subcfg  = array( 'LANGUAGE' => $value );
759         $res = TRUE;
760         break;
761       case 'NL':
762       case 'NEWLINECHAR':
763         $this->nl = $value;
764         $subcfg  = array( 'NL' => $value );
765         $res = TRUE;
766         break;
767       case 'TZID':
768         $this->dtzid = $value;
769         $subcfg  = array( 'TZID' => $value );
770         $res = TRUE;
771         break;
772       case 'UNIQUE_ID':
773         $value   = trim( $value );
774         $this->unique_id = $value;
775         $subcfg  = array( 'UNIQUE_ID' => $value );
776         $res = TRUE;
777         break;
778       case 'URL':
779             /* remote file - URL */
780         $value     = trim( $value );
781         $value     = str_replace( 'HTTP://',   'http://', $value );
782         $value     = str_replace( 'WEBCAL://', 'http://', $value );
783         $value     = str_replace( 'webcal://', 'http://', $value );
784         $this->url = $value;
785         $this->directory = null;
786         $parts     = pathinfo( $value );
787         return $this->setConfig( 'filename',  $parts['basename'] );
788         break;
789       default:  // any unvalid config key.. .
790         return TRUE;
791     }
792     if( !$res ) return FALSE;
793     if( isset( $subcfg ) && !empty( $this->components )) {
794       foreach( $subcfg as $cfgkey => $cfgvalue ) {
795         foreach( $this->components as $cix => $component ) {
796           $res = $component->setConfig( $cfgkey, $cfgvalue, TRUE );
797           if( !$res )
798             break 2;
799           $this->components[$cix] = $component->copy(); // PHP4 compliant
800         }
801       }
802     }
803     return $res;
804   }
805 /*********************************************************************************/
806 /**
807  * add calendar component to container
808  *
809  * alias to setComponent
810  *
811  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
812  * @since 1.x.x - 2007-04-24
813  * @param object $component calendar component
814  * @return void
815  */
816   function addComponent( $component ) {
817     $this->setComponent( $component );
818   }
819 /**
820  * delete calendar component from container
821  *
822  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
823  * @since 2.8.8 - 2011-03-15
824  * @param mixed $arg1 ordno / component type / component uid
825  * @param mixed $arg2 optional, ordno if arg1 = component type
826  * @return void
827  */
828   function deleteComponent( $arg1, $arg2=FALSE  ) {
829     $argType = $index = null;
830     if ( ctype_digit( (string) $arg1 )) {
831       $argType = 'INDEX';
832       $index   = (int) $arg1 - 1;
833     }
834     elseif(( strlen( $arg1 ) <= strlen( 'vfreebusy' )) && ( FALSE === strpos( $arg1, '@' ))) {
835       $argType = strtolower( $arg1 );
836       $index   = ( !empty( $arg2 ) && ctype_digit( (string) $arg2 )) ? (( int ) $arg2 - 1 ) : 0;
837     }
838     $cix1dC = 0;
839     foreach ( $this->components as $cix => $component) {
840       if( empty( $component )) continue;
841       if(( 'INDEX' == $argType ) && ( $index == $cix )) {
842         unset( $this->components[$cix] );
843         return TRUE;
844       }
845       elseif( $argType == $component->objName ) {
846         if( $index == $cix1dC ) {
847           unset( $this->components[$cix] );
848           return TRUE;
849         }
850         $cix1dC++;
851       }
852       elseif( !$argType && ($arg1 == $component->getProperty( 'uid' ))) {
853         unset( $this->components[$cix] );
854         return TRUE;
855       }
856     }
857     return FALSE;
858   }
859 /**
860  * get calendar component from container
861  *
862  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
863  * @since 2.9.1 - 2011-04-16
864  * @param mixed $arg1 optional, ordno/component type/ component uid
865  * @param mixed $arg2 optional, ordno if arg1 = component type
866  * @return object
867  */
868   function getComponent( $arg1=FALSE, $arg2=FALSE ) {
869     $index = $argType = null;
870     if ( !$arg1 ) { // first or next in component chain
871       $argType = 'INDEX';
872       $index   = $this->compix['INDEX'] = ( isset( $this->compix['INDEX'] )) ? $this->compix['INDEX'] + 1 : 1;
873     }
874     elseif ( ctype_digit( (string) $arg1 )) { // specific component in chain
875       $argType = 'INDEX';
876       $index   = (int) $arg1;
877       unset( $this->compix );
878     }
879     elseif( is_array( $arg1 )) { // array( *[propertyName => propertyValue] )
880       $arg2  = implode( '-', array_keys( $arg1 ));
881       $index = $this->compix[$arg2] = ( isset( $this->compix[$arg2] )) ? $this->compix[$arg2] + 1 : 1;
882       $dateProps  = array( 'DTSTART', 'DTEND', 'DUE', 'CREATED', 'COMPLETED', 'DTSTAMP', 'LAST-MODIFIED', 'RECURRENCE-ID' );
883       $otherProps = array( 'ATTENDEE', 'CATEGORIES', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'RESOURCES', 'STATUS', 'SUMMARY', 'UID' );
884     }
885     elseif(( strlen( $arg1 ) <= strlen( 'vfreebusy' )) && ( FALSE === strpos( $arg1, '@' ))) { // object class name
886       unset( $this->compix['INDEX'] );
887       $argType = strtolower( $arg1 );
888       if( !$arg2 )
889         $index = $this->compix[$argType] = ( isset( $this->compix[$argType] )) ? $this->compix[$argType] + 1 : 1;
890       elseif( isset( $arg2 ) && ctype_digit( (string) $arg2 ))
891         $index = (int) $arg2;
892     }
893     elseif(( strlen( $arg1 ) > strlen( 'vfreebusy' )) && ( FALSE !== strpos( $arg1, '@' ))) { // UID as 1st argument
894       if( !$arg2 )
895         $index = $this->compix[$arg1] = ( isset( $this->compix[$arg1] )) ? $this->compix[$arg1] + 1 : 1;
896       elseif( isset( $arg2 ) && ctype_digit( (string) $arg2 ))
897         $index = (int) $arg2;
898     }
899     if( isset( $index ))
900       $index  -= 1;
901     $ckeys = array_keys( $this->components );
902     if( !empty( $index) && ( $index > end(  $ckeys )))
903       return FALSE;
904     $cix1gC = 0;
905     foreach ( $this->components as $cix => $component) {
906       if( empty( $component )) continue;
907       if(( 'INDEX' == $argType ) && ( $index == $cix ))
908         return $component->copy();
909       elseif( $argType == $component->objName ) {
910         if( $index == $cix1gC )
911           return $component->copy();
912         $cix1gC++;
913       }
914       elseif( is_array( $arg1 )) { // array( *[propertyName => propertyValue] )
915         $hit = FALSE;
916         foreach( $arg1 as $pName => $pValue ) {
917           $pName = strtoupper( $pName );
918           if( !in_array( $pName, $dateProps ) && !in_array( $pName, $otherProps ))
919             continue;
920           if(( 'ATTENDEE' == $pName ) || ( 'CATEGORIES' == $pName ) || ( 'RESOURCES' == $pName )) { // multiple ocurrence may occur
921             $propValues = array();
922             $component->_getProperties( $pName, $propValues );
923             $propValues = array_keys( $propValues );
924             $hit = ( in_array( $pValue, $propValues )) ? TRUE : FALSE;
925             continue;
926           } // end   if(( 'CATEGORIES' == $propName ) || ( 'RESOURCES' == $propName )) { // multiple ocurrence may occur
927           if( FALSE === ( $value = $component->getProperty( $pName ))) { // single ocurrency
928             $hit = FALSE; // missing property
929             continue;
930           }
931           if( 'SUMMARY' == $pName ) { // exists within (any case)
932             $hit = ( FALSE !== stripos( $d, $pValue )) ? TRUE : FALSE;
933             continue;
934           }
935           if( in_array( strtoupper( $pName ), $dateProps )) {
936             $valuedate = sprintf( '%04d%02d%02d', $value['year'], $value['month'], $value['day'] );
937             if( 8 < strlen( $pValue )) {
938               if( isset( $value['hour'] )) {
939                 if( 'T' == substr( $pValue, 8, 1 ))
940                   $pValue = str_replace( 'T', '', $pValue );
941                 $valuedate .= sprintf( '%02d%02d%02d', $value['hour'], $value['min'], $value['sec'] );
942               }
943               else
944                 $pValue = substr( $pValue, 0, 8 );
945             }
946             $hit = ( $pValue == $valuedate ) ? TRUE : FALSE;
947             continue;
948           }
949           elseif( !is_array( $value ))
950             $value = array( $value );
951           foreach( $value as $part ) {
952             $part = ( FALSE !== strpos( $part, ',' )) ? explode( ',', $part ) : array( $part );
953             foreach( $part as $subPart ) {
954               if( $pValue == $subPart ) {
955                 $hit = TRUE;
956                 continue 2;
957               }
958             }
959           }
960           $hit = FALSE; // no hit in property
961         } // end  foreach( $arg1 as $pName => $pValue )
962         if( $hit ) {
963           if( $index == $cix1gC )
964             return $component->copy();
965           $cix1gC++;
966         }
967       } // end elseif( is_array( $arg1 )) { // array( *[propertyName => propertyValue] )
968       elseif( !$argType && ($arg1 == $component->getProperty( 'uid' ))) { // UID
969         if( $index == $cix1gC )
970           return $component->copy();
971         $cix1gC++;
972       }
973     } // end foreach ( $this->components.. .
974             /* not found.. . */
975     unset( $this->compix );
976     return FALSE;
977   }
978 /**
979  * create new calendar component, already included within calendar
980  *
981  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
982  * @since 2.6.33 - 2011-01-03
983  * @param string $compType component type
984  * @return object (reference)
985  */
986   function & newComponent( $compType ) {
987     $config = $this->getConfig();
988     $keys   = array_keys( $this->components );
989     $ix     = end( $keys) + 1;
990     switch( strtoupper( $compType )) {
991       case 'EVENT':
992       case 'VEVENT':
993         $this->components[$ix] = new vevent( $config );
994         break;
995       case 'TODO':
996       case 'VTODO':
997         $this->components[$ix] = new vtodo( $config );
998         break;
999       case 'JOURNAL':
1000       case 'VJOURNAL':
1001         $this->components[$ix] = new vjournal( $config );
1002         break;
1003       case 'FREEBUSY':
1004       case 'VFREEBUSY':
1005         $this->components[$ix] = new vfreebusy( $config );
1006         break;
1007       case 'TIMEZONE':
1008       case 'VTIMEZONE':
1009         array_unshift( $this->components, new vtimezone( $config ));
1010         $ix = 0;
1011         break;
1012       default:
1013         return FALSE;
1014     }
1015     return $this->components[$ix];
1016   }
1017 /**
1018  * select components from calendar on date or selectOption basis
1019  *
1020  * Ensure DTSTART is set for every component.
1021  * No date controls occurs.
1022  *
1023  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1024  * @since 2.10.13 - 2011-09-23
1025  * @param mixed $startY optional, start Year, default current Year ALT. array selecOptions
1026  * @param int $startM optional,   start Month, default current Month
1027  * @param int $startD optional,   start Day, default current Day
1028  * @param int $endY optional,     end Year, default $startY
1029  * @param int $endY optional,     end Month, default $startM
1030  * @param int $endY optional,     end Day, default $startD
1031  * @param mixed $cType optional,  calendar component type(-s), default FALSE=all else string/array type(-s)
1032  * @param bool $flat optional,    FALSE (default) => output : array[Year][Month][Day][]
1033  *                                TRUE => output : array[] (ignores split)
1034  * @param bool $any optional,     TRUE (default) - select component that take place within period
1035  *                                FALSE - only components that starts within period
1036  * @param bool $split optional,   TRUE (default) - one component copy every day it take place during the
1037  *                                       period (implies flat=FALSE)
1038  *                                FALSE - one occurance of component only in output array
1039  * @return array or FALSE
1040  */
1041   function selectComponents(
1042             $startY=FALSE,
1043             $startM=FALSE,
1044             $startD=FALSE,
1045             $endY=FALSE,
1046             $endM=FALSE,
1047             $endD=FALSE,
1048             $cType=FALSE,
1049             $flat=FALSE,
1050             $any=TRUE,
1051             $split=TRUE ) {
1052             /* check  if empty calendar */
1053     if( 0 >= count( $this->components )) {
1054         return FALSE;
1055     }
1056     
1057     if( is_array( $startY )) {
1058          return $this->selectComponents2( $startY );
1059     }
1060
1061     /* check default dates */
1062     if( !$startY ) $startY = date( 'Y' );
1063     if( !$startM ) $startM = date( 'm' );
1064     if( !$startD ) $startD = date( 'd' );
1065     $startDate = mktime( 0, 0, 0, $startM, $startD, $startY );
1066     if( !$endY )   $endY   = $startY;
1067     if( !$endM )   $endM   = $startM;
1068     if( !$endD )   $endD   = $startD;
1069     $endDate   = mktime( 23, 59, 59, $endM, $endD, $endY );
1070     
1071     
1072     
1073     //echo 'selectComp arg='.date( 'Y-m-d H:i:s', $startDate).' -- '.date( 'Y-m-d H:i:s', $endDate)."<br />\n"; $tcnt = 0;// test ###
1074     /* check component types */
1075     $validTypes = array('vevent', 'vtodo', 'vjournal', 'vfreebusy' );
1076     if( is_array( $cType )) {
1077         foreach( $cType as $cix => $theType ) {
1078             $cType[$cix] = $theType = strtolower( $theType );
1079             if( !in_array( $theType, $validTypes )) {
1080                 $cType[$cix] = 'vevent';
1081             }
1082         }
1083         $cType = array_unique( $cType );
1084         
1085     } else if( !empty( $cType )) {
1086         $cType = strtolower( $cType );
1087         
1088         if( !in_array( $cType, $validTypes )) {
1089             $cType = array( 'vevent' );
1090         } else {
1091             $cType = array( $cType );
1092         }
1093         
1094     } else {
1095         $cType = $validTypes;
1096     }
1097     if( 0 >= count( $cType )) {
1098       $cType = $validTypes;
1099     }
1100     
1101     if(( TRUE === $flat ) && ( TRUE === $split )) {// invalid combination
1102          $split = FALSE;
1103     }
1104       
1105     // end validate and reset arguments..
1106       
1107       
1108             /* iterate components */
1109     $result = array();
1110     foreach ( $this->components as $cix => $component ) {
1111         //print_r($component);
1112         if( empty( $component )) {
1113             continue;
1114         }
1115         unset( $start );
1116         
1117         /* deselect unvalid type components */
1118         if( !in_array( $component->objName, $cType )) {
1119             continue;
1120         }
1121         
1122         $start = $component->getProperty( 'dtstart' );
1123               /* select due when dtstart is missing */
1124         if( empty( $start ) &&
1125             ( $component->objName == 'vtodo' ) &&
1126             ( FALSE === ( $start = $component->getProperty( 'due' )))) {
1127             continue;
1128         }
1129         
1130         $dtendExist = $dueExist = $durationExist = $endAllDayEvent = FALSE;
1131         unset( $end, $startWdate, $endWdate, $rdurWsecs, $rdur, $exdatelist, $workstart, $workend, $endDateFormat ); // clean up
1132         
1133         $startWdate = iCal_UtilityFunctions::_date2timestamp( $start );
1134         $startDateFormat = ( isset( $start['hour'] )) ? 'Y-m-d H:i:s' : 'Y-m-d';
1135         
1136         /* get end date from dtend/due/duration properties */
1137         $end = $component->getProperty( 'dtend' );
1138         if( !empty( $end )) {
1139             $dtendExist = TRUE;
1140             $endDateFormat = ( isset( $end['hour'] )) ? 'Y-m-d H:i:s' : 'Y-m-d';
1141         }
1142         if( empty( $end ) && ( $component->objName == 'vtodo' )) {
1143             $end = $component->getProperty( 'due' );
1144             if( !empty( $end )) {
1145                 $dueExist = TRUE;
1146                 $endDateFormat = ( isset( $end['hour'] )) ? 'Y-m-d H:i:s' : 'Y-m-d';
1147             }
1148         }
1149         if( !empty( $end ) && !isset( $end['hour'] )) {
1150             /* a DTEND without time part regards an event that ends the day before,
1151                for an all-day event DTSTART=20071201 DTEND=20071202 (taking place 20071201!!! */
1152             $endAllDayEvent = TRUE;
1153             $endWdate = mktime( 23, 59, 59, $end['month'], ($end['day'] - 1), $end['year'] );
1154             $end['year']  = date( 'Y', $endWdate );
1155             $end['month'] = date( 'm', $endWdate );
1156             $end['day']   = date( 'd', $endWdate );
1157             $end['hour']  = 23;
1158             $end['min']   = $end['sec'] = 59;
1159         }
1160         if( empty( $end )) {
1161             $end = $component->getProperty( 'duration', FALSE, FALSE, TRUE );// in dtend (array) format
1162             if( !empty( $end )) {
1163               $durationExist = TRUE;
1164             }
1165         
1166             $endDateFormat = ( isset( $start['hour'] )) ? 'Y-m-d H:i:s' : 'Y-m-d';
1167             // if( !empty($end))  echo 'selectComp 4 start='.implode('-',$start).' end='.implode('-',$end)."<br />\n"; // test ###
1168         }
1169         
1170         if( empty( $end )) { // assume one day duration if missing end date
1171               $end = array(
1172                 'year' => $start['year'],
1173                 'month' => $start['month'],
1174                 'day' => $start['day'],
1175                 'hour' => 23,
1176                 'min' => 59,
1177                 'sec' => 59
1178             );
1179         }
1180   // if( isset($end))  echo 'selectComp 5 start='.implode('-',$start).' end='.implode('-',$end)."<br />\n"; // test ###
1181         $endWdate = iCal_UtilityFunctions::_date2timestamp( $end );
1182         if( $endWdate < $startWdate ) { // MUST be after start date!!
1183             $end = array(
1184                 'year' => $start['year'],
1185                 'month' => $start['month'],
1186                 'day' => $start['day'],
1187                 'hour' => 23,
1188                 'min' => 59,
1189                 'sec' => 59
1190             );
1191             $endWdate = iCal_UtilityFunctions::_date2timestamp( $end );
1192         }
1193         
1194         
1195         
1196         $rdurWsecs  = $endWdate - $startWdate; // compute event (component) duration in seconds
1197               /* make a list of optional exclude dates for component occurence from exrule and exdate */
1198         $exdatelist = array();
1199         $workstart  = iCal_UtilityFunctions::_timestamp2date(( $startDate - $rdurWsecs ), 6);
1200         $workend    = iCal_UtilityFunctions::_timestamp2date(( $endDate + $rdurWsecs ), 6);
1201         
1202         while( FALSE !== ( $exrule = $component->getProperty( 'exrule' ))) {    // check exrule
1203             iCal_UtilityFunctions::_recur2date( $exdatelist, $exrule, $start, $workstart, $workend );
1204         }
1205         
1206         while( FALSE !== ( $exdate = $component->getProperty( 'exdate' ))) {  // check exdate
1207             foreach( $exdate as $theExdate ) {
1208                 $exWdate = iCal_UtilityFunctions::_date2timestamp( $theExdate );
1209                 $exWdate = mktime( 0, 0, 0, date( 'm', $exWdate ), date( 'd', $exWdate ), date( 'Y', $exWdate ) ); // on a day-basis !!!
1210                 if((( $startDate - $rdurWsecs ) <= $exWdate ) && ( $endDate >= $exWdate )) {
1211                       $exdatelist[$exWdate] = TRUE;
1212                 }
1213             } // end - foreach( $exdate as $theExdate )
1214         }  // end - check exdate
1215         
1216         //var_dump(array($startWdate ,$startDate , $startWdate , $endDate ));exit;
1217          /* select only components with startdate within period */
1218         if(( $startWdate >= $startDate ) && ( $startWdate <= $endDate )) {
1219               /* add the selected component (WITHIN valid dates) to output array */
1220             if( $flat ) {
1221                 $result[$component->getProperty( 'UID' )] = $component->copy(); // copy original to output;
1222                 
1223             } elseif( $split ) { // split the original component
1224                 if( $endWdate > $endDate ) {
1225                     $endWdate = $endDate;     // use period end date
1226                 }
1227                 
1228                 $rstart = $startWdate;
1229                 if( $rstart < $startDate ){
1230                     $rstart = $startDate; // use period start date
1231                 }
1232                 $startYMD = date( 'Ymd', $rstart );
1233                 $endYMD   = date( 'Ymd', $endWdate );
1234                 $checkDate = mktime( 0, 0, 0, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1235                 
1236                 while( date( 'Ymd', $rstart ) <= $endYMD ) { // iterate
1237                     $checkDate = mktime( 0, 0, 0, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1238                     if( isset( $exdatelist[$checkDate] )) { // exclude any recurrence date, found in exdatelist
1239                       $rstart = mktime( date( 'H', $rstart ), date( 'i', $rstart ), date( 's', $rstart ), date( 'm', $rstart ), date( 'd', $rstart ) + 1, date( 'Y', $rstart ) ); // step one day
1240                       continue;
1241                     }
1242                     if( date( 'Ymd', $rstart ) > $startYMD ) // date after dtstart
1243                       $datestring = date( $startDateFormat, mktime( 0, 0, 0, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart )));
1244                     else
1245                       $datestring = date( $startDateFormat, $rstart );
1246                     if( isset( $start['tz'] ))
1247                       $datestring .= ' '.$start['tz'];
1248                     // echo "X-CURRENT-DTSTART 3 = $datestring xRecurrence=$xRecurrence tcnt =".++$tcnt."<br />";$component->setProperty( 'X-CNT', $tcnt ); // test ###
1249                     $component->setProperty( 'X-CURRENT-DTSTART', $datestring );
1250                     if( $dtendExist || $dueExist || $durationExist ) {
1251                       if( date( 'Ymd', $rstart ) < $endYMD ) // not the last day
1252                         $tend = mktime( 23, 59, 59, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ));
1253                       else
1254                         $tend = mktime( date( 'H', $endWdate ), date( 'i', $endWdate ), date( 's', $endWdate ), date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1255                       $datestring = date( $endDateFormat, $tend );
1256                       if( isset( $end['tz'] ))
1257                         $datestring .= ' '.$end['tz'];
1258                       $propName = ( !$dueExist ) ? 'X-CURRENT-DTEND' : 'X-CURRENT-DUE';
1259                       $component->setProperty( $propName, $datestring );
1260                     } // end if( $dtendExist || $dueExist || $durationExist )
1261                     $wd = getdate( $rstart );
1262                     if (!isset($result[$wd['year']][$wd['mon']][$wd['mday']][$component->getProperty( 'UID' )] )) {
1263                         $result[$wd['year']][$wd['mon']][$wd['mday']][$component->getProperty( 'UID' )] = $component->copy(); // copy to output
1264                     }
1265                     $rstart = mktime( date( 'H', $rstart ), date( 'i', $rstart ), date( 's', $rstart ), date( 'm', $rstart ), date( 'd', $rstart ) + 1, date( 'Y', $rstart ) ); // step one day
1266                 } // end while( $rstart <= $endWdate )
1267                 
1268                 //print_R($result);exit;
1269                 
1270           } // end if( $split )   -  else use component date
1271         } // end if(( $startWdate >= $startDate ) && ( $startWdate <= $endDate ))
1272               /* if 'any' components, check components with reccurrence rules, removing all excluding dates */
1273         if( TRUE === $any ) {
1274               /* make a list of optional repeating dates for component occurence, rrule, rdate */
1275           $recurlist = array();
1276           while( FALSE !== ( $rrule = $component->getProperty( 'rrule' )))    // check rrule
1277             iCal_UtilityFunctions::_recur2date( $recurlist, $rrule, $start, $workstart, $workend );
1278           foreach( $recurlist as $recurkey => $recurvalue ) // key=match date as timestamp
1279             $recurlist[$recurkey] = $rdurWsecs; // add duration in seconds
1280           while( FALSE !== ( $rdate = $component->getProperty( 'rdate' ))) {  // check rdate
1281             foreach( $rdate as $theRdate ) {
1282               if( is_array( $theRdate ) && ( 2 == count( $theRdate )) &&  // all days within PERIOD
1283                      array_key_exists( '0', $theRdate ) &&  array_key_exists( '1', $theRdate )) {
1284                 $rstart = iCal_UtilityFunctions::_date2timestamp( $theRdate[0] );
1285                 if(( $rstart < ( $startDate - $rdurWsecs )) || ( $rstart > $endDate ))
1286                   continue;
1287                 if( isset( $theRdate[1]['year'] )) // date-date period
1288                   $rend = iCal_UtilityFunctions::_date2timestamp( $theRdate[1] );
1289                 else {                             // date-duration period
1290                   $rend = iCal_UtilityFunctions::_duration2date( $theRdate[0], $theRdate[1] );
1291                   $rend = iCal_UtilityFunctions::_date2timestamp( $rend );
1292                 }
1293                 while( $rstart < $rend ) {
1294                   $recurlist[$rstart] = $rdurWsecs; // set start date for recurrence instance + rdate duration in seconds
1295                   $rstart = mktime( date( 'H', $rstart ), date( 'i', $rstart ), date( 's', $rstart ), date( 'm', $rstart ), date( 'd', $rstart ) + 1, date( 'Y', $rstart ) ); // step one day
1296                 }
1297               } // PERIOD end
1298               else { // single date
1299                 $theRdate = iCal_UtilityFunctions::_date2timestamp( $theRdate );
1300                 if((( $startDate - $rdurWsecs ) <= $theRdate ) && ( $endDate >= $theRdate ))
1301                   $recurlist[$theRdate] = $rdurWsecs; // set start date for recurrence instance + event duration in seconds
1302               }
1303             }
1304           }  // end - check rdate
1305           if( 0 < count( $recurlist )) {
1306             ksort( $recurlist );
1307             $xRecurrence = 1;
1308             foreach( $recurlist as $recurkey => $durvalue ) {
1309   // echo "recurKey=".date( 'Y-m-d H:i:s', $recurkey ).' dur='.iCal_UtilityFunctions::offsetSec2His( $durvalue )."<br />\n"; // test ###;
1310               if((( $startDate - $rdurWsecs ) > $recurkey ) || ( $endDate < $recurkey )) // not within period
1311                 continue;
1312               $checkDate = mktime( 0, 0, 0, date( 'm', $recurkey ), date( 'd', $recurkey ), date( 'Y', $recurkey ) ); // on a day-basis !!!
1313               if( isset( $exdatelist[$checkDate] )) // check excluded dates
1314                 continue;
1315               if( $startWdate >= $recurkey ) // exclude component start date
1316                 continue;
1317               $component2   = $component->copy();
1318               $rstart = $recurkey;
1319               $rend   = $recurkey + $durvalue;
1320              /* add repeating components within valid dates to output array, only start date set */
1321               if( $flat ) {
1322                 $datestring = date( $startDateFormat, $recurkey );
1323                 if( isset( $start['tz'] ))
1324                   $datestring .= ' '.$start['tz'];
1325   // echo "X-CURRENT-DTSTART 0 =$datestring tcnt =".++$tcnt."<br />";$component2->setProperty( 'X-CNT', $tcnt ); // test ###
1326                 $component2->setProperty( 'X-CURRENT-DTSTART', $datestring );
1327                 if( $dtendExist || $dueExist || $durationExist ) {
1328                   $datestring = date( $endDateFormat, $recurkey + $durvalue );   // fixa korrekt sluttid
1329                   if( isset( $end['tz'] ))
1330                     $datestring .= ' '.$end['tz'];
1331                   $propName = ( !$dueExist ) ? 'X-CURRENT-DTEND' : 'X-CURRENT-DUE';
1332                   $component2->setProperty( $propName, $datestring );
1333                 } // end if( $dtendExist || $dueExist || $durationExist )
1334                 $component2->setProperty( 'X-RECURRENCE', ++$xRecurrence );
1335                 $result[$component2->getProperty( 'UID' )] = $component2->copy(); // copy to output
1336               }
1337              /* add repeating components within valid dates to output array, one each day */
1338               elseif( $split ) {
1339                 if( $rend > $endDate )
1340                   $rend = $endDate;
1341                 $startYMD = date( 'Ymd', $rstart );
1342                 $endYMD   = date( 'Ymd', $rend );
1343   // echo "splitStart=".date( 'Y-m-d H:i:s', $rstart ).' end='.date( 'Y-m-d H:i:s', $rend )."<br />\n"; // test ###;
1344                 while( $rstart <= $rend ) { // iterate.. .
1345                   $checkDate = mktime( 0, 0, 0, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1346                   if( isset( $exdatelist[$checkDate] ))  // exclude any recurrence START date, found in exdatelist
1347                     break;
1348   // echo "checking date after startdate=".date( 'Y-m-d H:i:s', $rstart ).' mot '.date( 'Y-m-d H:i:s', $startDate )."<br />"; // test ###;
1349                   if( $rstart >= $startDate ) {    // date after dtstart
1350                     if( date( 'Ymd', $rstart ) > $startYMD ) // date after dtstart
1351                       $datestring = date( $startDateFormat, $checkDate );
1352                     else
1353                       $datestring = date( $startDateFormat, $rstart );
1354                     if( isset( $start['tz'] ))
1355                       $datestring .= ' '.$start['tz'];
1356   //echo "X-CURRENT-DTSTART 1 = $datestring xRecurrence=$xRecurrence tcnt =".++$tcnt."<br />";$component2->setProperty( 'X-CNT', $tcnt ); // test ###
1357                     $component2->setProperty( 'X-CURRENT-DTSTART', $datestring );
1358                     if( $dtendExist || $dueExist || $durationExist ) {
1359                       if( date( 'Ymd', $rstart ) < $endYMD ) // not the last day
1360                         $tend = mktime( 23, 59, 59, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ));
1361                       else
1362                         $tend = mktime( date( 'H', $endWdate ), date( 'i', $endWdate ), date( 's', $endWdate ), date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1363                       $datestring = date( $endDateFormat, $tend );
1364                       if( isset( $end['tz'] ))
1365                         $datestring .= ' '.$end['tz'];
1366                       $propName = ( !$dueExist ) ? 'X-CURRENT-DTEND' : 'X-CURRENT-DUE';
1367                       $component2->setProperty( $propName, $datestring );
1368                     } // end if( $dtendExist || $dueExist || $durationExist )
1369                     $component2->setProperty( 'X-RECURRENCE', $xRecurrence );
1370                     $wd = getdate( $rstart );
1371                     if (!isset($result[$wd['year']][$wd['mon']][$wd['mday']][$component2->getProperty( 'UID' )] )) {
1372                         $result[$wd['year']][$wd['mon']][$wd['mday']][$component2->getProperty( 'UID' )] = $component2->copy(); // copy to output
1373                     }
1374                     
1375                   } // end if( $checkDate > $startYMD ) {    // date after dtstart
1376                   $rstart = mktime( date( 'H', $rstart ), date( 'i', $rstart ), date( 's', $rstart ), date( 'm', $rstart ), date( 'd', $rstart ) + 1, date( 'Y', $rstart ) ); // step one day
1377                 } // end while( $rstart <= $rend )
1378                 $xRecurrence += 1;
1379               } // end elseif( $split )
1380               elseif( $rstart >= $startDate ) {     // date within period   //* flat=FALSE && split=FALSE *//
1381                 $checkDate = mktime( 0, 0, 0, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1382                 if( !isset( $exdatelist[$checkDate] )) { // exclude any recurrence START date, found in exdatelist
1383                   $xRecurrence += 1;
1384                   $datestring = date( $startDateFormat, $rstart );
1385                   if( isset( $start['tz'] ))
1386                     $datestring .= ' '.$start['tz'];
1387   //echo "X-CURRENT-DTSTART 2 = $datestring xRecurrence=$xRecurrence tcnt =".++$tcnt."<br />";$component2->setProperty( 'X-CNT', $tcnt ); // test ###
1388                   $component2->setProperty( 'X-CURRENT-DTSTART', $datestring );
1389                   if( $dtendExist || $dueExist || $durationExist ) {
1390                     $rstart += $rdurWsecs;
1391                     if( date( 'Ymd', $rstart ) < date( 'Ymd', $endWdate ))
1392                       $tend = mktime( 23, 59, 59, date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ));
1393                     else
1394                       $tend = mktime( date( 'H', $endWdate ), date( 'i', $endWdate ), date( 's', $endWdate ), date( 'm', $rstart ), date( 'd', $rstart ), date( 'Y', $rstart ) ); // on a day-basis !!!
1395                     $datestring = date( $endDateFormat, $tend );
1396                     if( isset( $end['tz'] ))
1397                       $datestring .= ' '.$end['tz'];
1398                     $propName = ( !$dueExist ) ? 'X-CURRENT-DTEND' : 'X-CURRENT-DUE';
1399                     $component2->setProperty( $propName, $datestring );
1400                   } // end if( $dtendExist || $dueExist || $durationExist )
1401                   $component2->setProperty( 'X-RECURRENCE', $xRecurrence );
1402                   $wd = getdate( $rstart );
1403                   if (!isset($result[$wd['year']][$wd['mon']][$wd['mday']][$component2->getProperty( 'UID' )] )) {
1404                      $result[$wd['year']][$wd['mon']][$wd['mday']][$component2->getProperty( 'UID' )] = $component2->copy(); // copy to output
1405                   }
1406                 } // end if( !isset( $exdatelist[$checkDate] ))
1407               } // end elseif( $rstart >= $startDate )
1408             } // end foreach( $recurlist as $recurkey => $durvalue )
1409           } // end if( 0 < count( $recurlist ))
1410               /* deselect components with startdate/enddate not within period */
1411           if(( $endWdate < $startDate ) || ( $startWdate > $endDate ))
1412             continue;
1413         } // end if( TRUE === $any )
1414     } // end foreach ( $this->components as $cix => $component )
1415     if( 0 >= count( $result )) {
1416         return FALSE;
1417     }
1418     
1419     if( !$flat ) {
1420         foreach( $result as $y => $yeararr ) {
1421           foreach( $yeararr as $m => $montharr ) {
1422             foreach( $montharr as $d => $dayarr )
1423               $result[$y][$m][$d] = array_values( $dayarr ); // skip tricky UID-index, hoping they are in hour order.. .
1424             ksort( $result[$y][$m] );
1425           }
1426           ksort( $result[$y] );
1427         }
1428         ksort( $result );
1429     } // end elseif( !$flat )
1430     //print_R($result);
1431     return $result;
1432   }
1433 /**
1434  * select components from calendar on based on Categories, Location, Resources and/or Summary
1435  *
1436  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1437  * @since 2.8.8 - 2011-05-03
1438  * @param array $selectOptions, (string) key => (mixed) value, (key=propertyName)
1439  * @return array
1440  */
1441   function selectComponents2( $selectOptions ) {
1442     $output = array();
1443     $allowedProperties = array( 'ATTENDEE', 'CATEGORIES', 'LOCATION', 'ORGANIZER', 'RESOURCES', 'PRIORITY', 'STATUS', 'SUMMARY', 'UID' );
1444     foreach( $this->components as $cix => $component3 ) {
1445       if( !in_array( $component3->objName, array('vevent', 'vtodo', 'vjournal', 'vfreebusy' )))
1446         continue;
1447       $uid = $component3->getProperty( 'UID' );
1448       foreach( $selectOptions as $propName => $pvalue ) {
1449         $propName = strtoupper( $propName );
1450         if( !in_array( $propName, $allowedProperties ))
1451           continue;
1452         if( !is_array( $pvalue ))
1453           $pvalue = array( $pvalue );
1454         if(( 'UID' == $propName ) && in_array( $uid, $pvalue )) {
1455           $output[] = $component3->copy();
1456           continue;
1457         }
1458         elseif(( 'ATTENDEE' == $propName ) || ( 'CATEGORIES' == $propName ) || ( 'RESOURCES' == $propName )) {
1459           $propValues = array();
1460           $component3->_getProperties( $propName, $propValues );
1461           $propValues = array_keys( $propValues );
1462           foreach( $pvalue as $theValue ) {
1463             if( in_array( $theValue, $propValues ) && !isset( $output[$uid] )) {
1464               $output[$uid] = $component3->copy();
1465               break;
1466             }
1467           }
1468           continue;
1469         } // end   elseif(( 'ATTENDEE' == $propName ) || ( 'CATEGORIES' == $propName ) || ( 'RESOURCES' == $propName ))
1470         elseif( FALSE === ( $d = $component3->getProperty( $propName ))) // single ocurrence
1471           continue;
1472         if( is_array( $d )) {
1473           foreach( $d as $part ) {
1474             if( in_array( $part, $pvalue ) && !isset( $output[$uid] ))
1475               $output[$uid] = $component3->copy();
1476           }
1477         }
1478         elseif(( 'SUMMARY' == $propName ) && !isset( $output[$uid] )) {
1479           foreach( $pvalue as $pval ) {
1480             if( FALSE !== stripos( $d, $pval )) {
1481               $output[$uid] = $component3->copy();
1482               break;
1483             }
1484           }
1485         }
1486         elseif( in_array( $d, $pvalue ) && !isset( $output[$uid] ))
1487           $output[$uid] = $component3->copy();
1488       } // end foreach( $selectOptions as $propName => $pvalue ) {
1489     } // end foreach( $this->components as $cix => $component3 ) {
1490     if( !empty( $output )) {
1491       ksort( $output );
1492       $output = array_values( $output );
1493     }
1494     return $output;
1495   }
1496 /**
1497  * add calendar component to container
1498  *
1499  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1500  * @since 2.8.8 - 2011-03-15
1501  * @param object $component calendar component
1502  * @param mixed $arg1 optional, ordno/component type/ component uid
1503  * @param mixed $arg2 optional, ordno if arg1 = component type
1504  * @return void
1505  */
1506   function setComponent( $component, $arg1=FALSE, $arg2=FALSE  ) {
1507     $component->setConfig( $this->getConfig(), FALSE, TRUE );
1508     if( !in_array( $component->objName, array( 'valarm', 'vtimezone' ))) {
1509             /* make sure dtstamp and uid is set */
1510       $dummy1 = $component->getProperty( 'dtstamp' );
1511       $dummy2 = $component->getProperty( 'uid' );
1512     }
1513     if( !$arg1 ) { // plain insert, last in chain
1514       $this->components[] = $component->copy();
1515       return TRUE;
1516     }
1517     $argType = $index = null;
1518     if ( ctype_digit( (string) $arg1 )) { // index insert/replace
1519       $argType = 'INDEX';
1520       $index   = (int) $arg1 - 1;
1521     }
1522     elseif( in_array( strtolower( $arg1 ), array( 'vevent', 'vtodo', 'vjournal', 'vfreebusy', 'valarm', 'vtimezone' ))) {
1523       $argType = strtolower( $arg1 );
1524       $index = ( ctype_digit( (string) $arg2 )) ? ((int) $arg2) - 1 : 0;
1525     }
1526     // else if arg1 is set, arg1 must be an UID
1527     $cix1sC = 0;
1528     foreach ( $this->components as $cix => $component2) {
1529       if( empty( $component2 )) continue;
1530       if(( 'INDEX' == $argType ) && ( $index == $cix )) { // index insert/replace
1531         $this->components[$cix] = $component->copy();
1532         return TRUE;
1533       }
1534       elseif( $argType == $component2->objName ) { // component Type index insert/replace
1535         if( $index == $cix1sC ) {
1536           $this->components[$cix] = $component->copy();
1537           return TRUE;
1538         }
1539         $cix1sC++;
1540       }
1541       elseif( !$argType && ( $arg1 == $component2->getProperty( 'uid' ))) { // UID insert/replace
1542         $this->components[$cix] = $component->copy();
1543         return TRUE;
1544       }
1545     }
1546             /* arg1=index and not found.. . insert at index .. .*/
1547     if( 'INDEX' == $argType ) {
1548       $this->components[$index] = $component->copy();
1549       ksort( $this->components, SORT_NUMERIC );
1550     }
1551     else    /* not found.. . insert last in chain anyway .. .*/
1552       $this->components[] = $component->copy();
1553     return TRUE;
1554   }
1555 /**
1556  * sort iCal compoments
1557  *
1558  * ascending sort on properties (if exist) x-current-dtstart, dtstart,
1559  * x-current-dtend, dtend, x-current-due, due, duration, created, dtstamp, uid
1560  * if no arguments, otherwise sorting on argument CATEGORIES, LOCATION, SUMMARY or RESOURCES
1561  *
1562  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1563  * @since 2.8.4 - 2011-06-02
1564  * @param string $sortArg, optional
1565  * @return void
1566  *
1567  */
1568   function sort( $sortArg=FALSE ) {
1569     if( is_array( $this->components )) {
1570       if( $sortArg ) {
1571         $sortArg = strtoupper( $sortArg );
1572         if( !in_array( $sortArg, array( 'ATTENDEE', 'CATEGORIES', 'DTSTAMP', 'LOCATION', 'ORGANIZER', 'RESOURCES', 'PRIORITY', 'STATUS', 'SUMMARY' )))
1573           $sortArg = FALSE;
1574       }
1575             /* set sort parameters for each component */
1576       foreach( $this->components as $cix => & $c ) {
1577         $c->srtk = array( '0', '0', '0', '0' );
1578         if( 'vtimezone' == $c->objName ) {
1579           if( FALSE === ( $c->srtk[0] = $c->getProperty( 'tzid' )))
1580             $c->srtk[0] = 0;
1581           continue;
1582         }
1583         elseif( $sortArg ) {
1584           if(( 'ATTENDEE' == $sortArg ) || ( 'CATEGORIES' == $sortArg ) || ( 'RESOURCES'  == $sortArg )) {
1585             $propValues = array();
1586             $c->_getProperties( $sortArg, $propValues );
1587             $c->srtk[0] = reset( array_keys( $propValues ));
1588           }
1589           elseif( FALSE !== ( $d = $c->getProperty( $sortArg )))
1590             $c->srtk[0] = $d;
1591           continue;
1592         }
1593         if( FALSE !== ( $d = $c->getProperty( 'X-CURRENT-DTSTART' )))
1594           $c->srtk[0] = iCal_UtilityFunctions::_date_time_string( $d[1] );
1595         elseif( FALSE === ( $c->srtk[0] = $c->getProperty( 'dtstart' )))
1596           $c->srtk[1] = 0;                                                  // sortkey 0 : dtstart
1597         if( FALSE !== ( $d = $c->getProperty( 'X-CURRENT-DTEND' )))
1598           $c->srtk[1] = iCal_UtilityFunctions::_date_time_string( $d[1] );   // sortkey 1 : dtend/due(/dtstart+duration)
1599         elseif( FALSE === ( $c->srtk[1] = $c->getProperty( 'dtend' ))) {
1600           if( FALSE !== ( $d = $c->getProperty( 'X-CURRENT-DUE' )))
1601             $c->srtk[1] = iCal_UtilityFunctions::_date_time_string( $d[1] );
1602           elseif( FALSE === ( $c->srtk[1] = $c->getProperty( 'due' )))
1603             if( FALSE === ( $c->srtk[1] = $c->getProperty( 'duration', FALSE, FALSE, TRUE )))
1604               $c->srtk[1] = 0;
1605         }
1606         if( FALSE === ( $c->srtk[2] = $c->getProperty( 'created' )))      // sortkey 2 : created/dtstamp
1607           if( FALSE === ( $c->srtk[2] = $c->getProperty( 'dtstamp' )))
1608             $c->srtk[2] = 0;
1609         if( FALSE === ( $c->srtk[3] = $c->getProperty( 'uid' )))          // sortkey 3 : uid
1610           $c->srtk[3] = 0;
1611       } // end foreach( $this->components as & $c
1612             /* sort */
1613       usort( $this->components, array( $this, '_cmpfcn' ));
1614     }
1615   }
1616   function _cmpfcn( $a, $b ) {
1617     if(        empty( $a ))                       return -1;
1618     if(        empty( $b ))                       return  1;
1619     if( 'vtimezone' == $a->objName ) {
1620       if( 'vtimezone' != $b->objName )            return -1;
1621       elseif( $a->srtk[0] <= $b->srtk[0] )        return -1;
1622       else                                        return  1;
1623     }
1624     elseif( 'vtimezone' == $b->objName )          return  1;
1625     $sortkeys = array( 'year', 'month', 'day', 'hour', 'min', 'sec' );
1626     for( $k = 0; $k < 4 ; $k++ ) {
1627       if(        empty( $a->srtk[$k] ))           return -1;
1628       elseif(    empty( $b->srtk[$k] ))           return  1;
1629       if( is_array( $a->srtk[$k] )) {
1630         if( is_array( $b->srtk[$k] )) {
1631           foreach( $sortkeys as $key ) {
1632             if    (  empty( $a->srtk[$k][$key] )) return -1;
1633             elseif(  empty( $b->srtk[$k][$key] )) return  1;
1634             if    (         $a->srtk[$k][$key] == $b->srtk[$k][$key])
1635                                                   continue;
1636             if    ((  (int) $a->srtk[$k][$key] ) < ((int) $b->srtk[$k][$key] ))
1637                                                   return -1;
1638             elseif((  (int) $a->srtk[$k][$key] ) > ((int) $b->srtk[$k][$key] ))
1639                                                   return  1;
1640           }
1641         }
1642         else                                      return -1;
1643       }
1644       elseif( is_array( $b->srtk[$k] ))           return  1;
1645       elseif( $a->srtk[$k] < $b->srtk[$k] )       return -1;
1646       elseif( $a->srtk[$k] > $b->srtk[$k] )       return  1;
1647     }
1648     return 0;
1649   }
1650 /**
1651  * parse iCal text/file into iCal_Vcalendar, components, properties and parameters
1652  *
1653  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1654  * @since 2.8.2 - 2011-05-21
1655  * @param mixed $unparsedtext, optional, strict rfc2445 formatted, single property string or array of property strings
1656  * @return bool FALSE if error occurs during parsing
1657  *
1658  */
1659   function parse( $unparsedtext=FALSE ) {
1660     $nl = $this->getConfig( 'nl' );
1661     if(( FALSE === $unparsedtext ) || empty( $unparsedtext )) {
1662             /* directory+filename is set previously via setConfig directory+filename or url */
1663       if( FALSE === ( $filename = $this->getConfig( 'url' )))
1664         $filename = $this->getConfig( 'dirfile' );
1665             /* READ FILE */
1666       if( FALSE === ( $rows = file_get_contents( $filename )))
1667         return FALSE;                 /* err 1 */
1668     }
1669     elseif( is_array( $unparsedtext ))
1670       $rows =  implode( '\n'.$nl, $unparsedtext );
1671     else
1672       $rows = & $unparsedtext;
1673             /* identify BEGIN:VCALENDAR, MUST be first row */
1674     if( 'BEGIN:VCALENDAR' != strtoupper( substr( $rows, 0, 15 )))
1675       return FALSE;                   /* err 8 */
1676             /* fix line folding */
1677     $eolchars = array( "\r\n", "\n\r", "\n", "\r" ); // check all line endings
1678     $EOLmark = FALSE;
1679     foreach( $eolchars as $eolchar ) {
1680       if( !$EOLmark  && ( FALSE !== strpos( $rows, $eolchar ))) {
1681         $rows = str_replace( $eolchar." ",  '',  $rows );
1682         $rows = str_replace( $eolchar."\t", '',  $rows );
1683         if( $eolchar != $nl )
1684           $rows = str_replace( $eolchar,    $nl, $rows );
1685         $EOLmark = TRUE;
1686       }
1687     }
1688     $tmp = explode( $nl, $rows );
1689     $rows = array();
1690     foreach( $tmp as $tmpr )
1691       if( !empty( $tmpr ))
1692         $rows[] = $tmpr;
1693             /* skip trailing empty lines */
1694     $lix = count( $rows ) - 1;
1695     while( empty( $rows[$lix] ) && ( 0 < $lix ))
1696       $lix -= 1;
1697             /* identify ending END:VCALENDAR row, MUST  be last row */
1698     if( 'END:VCALENDAR'   != strtoupper( substr( $rows[$lix], 0, 13 )))
1699       return FALSE;                   /* err 9 */
1700     if( 3 > count( $rows ))
1701       return FALSE;                   /* err 10 */
1702     $comp    = & $this;
1703     $calsync = 0;
1704             /* identify components and update unparsed data within component */
1705     $config = $this->getConfig();
1706     foreach( $rows as $line ) {
1707       if( '' == trim( $line ))
1708         continue;
1709       if(     'BEGIN:VCALENDAR' == strtoupper( substr( $line, 0, 15 ))) {
1710         $calsync++;
1711         continue;
1712       }
1713       elseif( 'END:VCALENDAR'   == strtoupper( substr( $line, 0, 13 ))) {
1714         $calsync--;
1715         break;
1716       }
1717       elseif( 1 != $calsync )
1718         return FALSE;                 /* err 20 */
1719       elseif( in_array( strtoupper( substr( $line, 0, 6 )), array( 'END:VE', 'END:VF', 'END:VJ', 'END:VT' ))) {
1720         $this->components[] = $comp->copy();
1721         continue;
1722       }
1723
1724       if(     'BEGIN:VEVENT'    == strtoupper( substr( $line, 0, 12 )))
1725         $comp = new vevent( $config );
1726       elseif( 'BEGIN:VFREEBUSY' == strtoupper( substr( $line, 0, 15 )))
1727         $comp = new vfreebusy( $config );
1728       elseif( 'BEGIN:VJOURNAL'  == strtoupper( substr( $line, 0, 14 )))
1729         $comp = new vjournal( $config );
1730       elseif( 'BEGIN:VTODO'     == strtoupper( substr( $line, 0, 11 )))
1731         $comp = new vtodo( $config );
1732       elseif( 'BEGIN:VTIMEZONE' == strtoupper( substr( $line, 0, 15 )))
1733         $comp = new vtimezone( $config );
1734       else  /* update component with unparsed data */
1735         $comp->unparsed[] = $line;
1736     } // end - foreach( rows.. .
1737     unset( $config );
1738             /* parse data for calendar (this) object */
1739     if( isset( $this->unparsed ) && is_array( $this->unparsed ) && ( 0 < count( $this->unparsed ))) {
1740             /* concatenate property values spread over several lines */
1741       $lastix    = -1;
1742       $propnames = array( 'calscale','method','prodid','version','x-' );
1743       $proprows  = array();
1744       foreach( $this->unparsed as $line ) {
1745         if( '' == trim( $line ))
1746           continue;
1747         $newProp = FALSE;
1748         foreach ( $propnames as $propname ) {
1749           if( $propname == strtolower( substr( $line, 0, strlen( $propname )))) {
1750             $newProp = TRUE;
1751             break;
1752           }
1753         }
1754         if( $newProp ) {
1755           $newProp = FALSE;
1756           $lastix++;
1757           $proprows[$lastix]  = $line;
1758         }
1759         else
1760           $proprows[$lastix] .= '!"#¤%&/()=?'.$line;
1761       }
1762       foreach( $proprows as $line ) {
1763         $line = str_replace( '!"#¤%&/()=? ', '', $line );
1764         $line = str_replace( '!"#¤%&/()=?', '', $line );
1765         if( '\n' == substr( $line, -2 ))
1766           $line = substr( $line, 0, strlen( $line ) - 2 );
1767             /* get property name */
1768         $cix = $propname = null;
1769         for( $cix=0, $clen = strlen( $line ); $cix < $clen; $cix++ ) {
1770           if( in_array( $line[$cix], array( ':', ';' )))
1771             break;
1772           else
1773             $propname .= $line[$cix];
1774         }
1775             /* ignore version/prodid properties */
1776         if( in_array( strtoupper( $propname ), array( 'VERSION', 'PRODID' )))
1777           continue;
1778         $line = substr( $line, $cix);
1779             /* separate attributes from value */
1780         $attr   = array();
1781         $attrix = -1;
1782         $strlen = strlen( $line );
1783         for( $cix=0; $cix < $strlen; $cix++ ) {
1784           if((       ':'   == $line[$cix] )             &&
1785                    ( '://' != substr( $line, $cix, 3 )) &&
1786              ( !in_array( strtolower( substr( $line, $cix - 3, 4 )), array( 'fax:', 'cid:', 'sms:', 'tel:', 'urn:' ))) &&
1787              ( !in_array( strtolower( substr( $line, $cix - 4, 5 )), array( 'crid:', 'news:', 'pres:' ))) &&
1788              ( 'mailto:'   != strtolower( substr( $line, $cix - 6, 7 )))) {
1789             $attrEnd = TRUE;
1790             if(( $cix < ( $strlen - 4 )) &&
1791                  ctype_digit( substr( $line, $cix+1, 4 ))) { // an URI with a (4pos) portnr??
1792               for( $c2ix = $cix; 3 < $c2ix; $c2ix-- ) {
1793                 if( '://' == substr( $line, $c2ix - 2, 3 )) {
1794                   $attrEnd = FALSE;
1795                   break; // an URI with a portnr!!
1796                 }
1797               }
1798             }
1799             if( $attrEnd) {
1800               $line = substr( $line, $cix + 1 );
1801               break;
1802             }
1803           }
1804           if( ';' == $line[$cix] )
1805             $attr[++$attrix] = null;
1806           else
1807             $attr[$attrix] .= $line[$cix];
1808         }
1809
1810             /* make attributes in array format */
1811         $propattr = array();
1812         foreach( $attr as $attribute ) {
1813           $attrsplit = explode( '=', $attribute, 2 );
1814           if( 1 < count( $attrsplit ))
1815             $propattr[$attrsplit[0]] = $attrsplit[1];
1816           else
1817             $propattr[] = $attribute;
1818         }
1819             /* update Property */
1820         if( FALSE !== strpos( $line, ',' )) {
1821           $content  = explode( ',', $line );
1822           $clen     = count( $content );
1823           for( $cix = 0; $cix < $clen; $cix++ ) {
1824             if( "\\" == substr( $content[$cix], -1 )) {
1825               $content[$cix] .= ','.$content[$cix + 1];
1826               unset( $content[$cix + 1] );
1827               $cix++;
1828             }
1829           }
1830           if( 1 < count( $content )) {
1831             foreach( $content as $cix => $contentPart )
1832               $content[$cix] = calendarComponent::_strunrep( $contentPart );
1833             $this->setProperty( $propname, $content, $propattr );
1834             continue;
1835           }
1836           else
1837             $line = reset( $content );
1838           $line = calendarComponent::_strunrep( $line );
1839         }
1840         $this->setProperty( $propname, trim( $line ), $propattr );
1841       } // end - foreach( $this->unparsed.. .
1842     } // end - if( is_array( $this->unparsed.. .
1843     unset( $unparsedtext, $rows, $this->unparsed, $proprows );
1844             /* parse Components */
1845     if( is_array( $this->components ) && ( 0 < count( $this->components ))) {
1846       $ckeys = array_keys( $this->components );
1847       foreach( $ckeys as $ckey ) {
1848         if( !empty( $this->components[$ckey] ) && !empty( $this->components[$ckey]->unparsed )) {
1849           $this->components[$ckey]->parse();
1850         }
1851       }
1852     }
1853     else
1854       return FALSE;                   /* err 91 or something.. . */
1855     return TRUE;
1856   }
1857 /*********************************************************************************/
1858 /**
1859  * creates formatted output for calendar object instance
1860  *
1861  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1862  * @since 2.8.1 - 2011-03-12
1863  * @return string
1864  */
1865   function createCalendar() {
1866     $calendarInit1 = $calendarInit2 = $calendarxCaldecl = $calendarStart = $calendar = null;
1867     switch( $this->format ) {
1868       case 'xcal':
1869         $calendarInit1 = '<?xml version="1.0" encoding="UTF-8"?>'.$this->nl.
1870                          '<!DOCTYPE iCalendar PUBLIC "-//IETF//DTD XCAL/iCalendar XML//EN"'.$this->nl.
1871                          '"http://www.ietf.org/internet-drafts/draft-ietf-calsch-many-xcal-01.txt"';
1872         $calendarInit2 = '>'.$this->nl;
1873         $calendarStart = '<vcalendar';
1874         break;
1875       default:
1876         $calendarStart = 'BEGIN:VCALENDAR'.$this->nl;
1877         break;
1878     }
1879     $calendarStart .= $this->createVersion();
1880     $calendarStart .= $this->createProdid();
1881     $calendarStart .= $this->createCalscale();
1882     $calendarStart .= $this->createMethod();
1883     switch( $this->format ) {
1884       case 'xcal':
1885         $nlstrlen = strlen( $this->nl );
1886         if( $this->nl == substr( $calendarStart, ( 0 - $nlstrlen )))
1887           $calendarStart = substr( $calendarStart, 0, ( strlen( $calendarStart ) - $nlstrlen ));
1888         $calendarStart .= '>'.$this->nl;
1889         break;
1890       default:
1891         break;
1892     }
1893     $calendar .= $this->createXprop();
1894     foreach( $this->components as $component ) {
1895       if( empty( $component )) continue;
1896       $component->setConfig( $this->getConfig(), FALSE, TRUE );
1897       $calendar .= $component->createComponent( $this->xcaldecl );
1898     }
1899     if(( 0 < count( $this->xcaldecl )) && ( 'xcal' == $this->format )) { // xCal only
1900       $calendarInit1 .= $this->nl.'['.$this->nl;
1901       $old_xcaldecl = array();
1902       foreach( $this->xcaldecl as $declix => $declPart ) {
1903         if(( 0 < count( $old_xcaldecl)) &&
1904            ( in_array( $declPart['uri'],      $old_xcaldecl['uri'] )) &&
1905            ( in_array( $declPart['external'], $old_xcaldecl['external'] )))
1906           continue; // no duplicate uri and ext. references
1907         $calendarxCaldecl .= '<!';
1908         foreach( $declPart as $declKey => $declValue ) {
1909           switch( $declKey ) {                    // index
1910             case 'xmldecl':                       // no 1
1911               $calendarxCaldecl .= $declValue.' ';
1912               break;
1913             case 'uri':                           // no 2
1914               $calendarxCaldecl .= $declValue.' ';
1915               $old_xcaldecl['uri'][] = $declValue;
1916               break;
1917             case 'ref':                           // no 3
1918               $calendarxCaldecl .= $declValue.' ';
1919               break;
1920             case 'external':                      // no 4
1921               $calendarxCaldecl .= '"'.$declValue.'" ';
1922               $old_xcaldecl['external'][] = $declValue;
1923               break;
1924             case 'type':                          // no 5
1925               $calendarxCaldecl .= $declValue.' ';
1926               break;
1927             case 'type2':                         // no 6
1928               $calendarxCaldecl .= $declValue;
1929               break;
1930           }
1931         }
1932         $calendarxCaldecl .= '>'.$this->nl;
1933       }
1934       $calendarInit2 = ']'.$calendarInit2;
1935     }
1936     switch( $this->format ) {
1937       case 'xcal':
1938         $calendar .= '</vcalendar>'.$this->nl;
1939         break;
1940       default:
1941         $calendar .= 'END:VCALENDAR'.$this->nl;
1942         break;
1943     }
1944     return $calendarInit1.$calendarxCaldecl.$calendarInit2.$calendarStart.$calendar;
1945   }
1946 /**
1947  * a HTTP redirect header is sent with created, updated and/or parsed calendar
1948  *
1949  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1950  * @since 2.9.12 - 2011-07-13
1951  * @param bool $utf8Encode
1952  * @param bool $gzip
1953  * @return redirect
1954  */
1955   function returnCalendar( $utf8Encode=FALSE, $gzip=FALSE ) {
1956     $filename = $this->getConfig( 'filename' );
1957     $output   = $this->createCalendar();
1958     if( $utf8Encode )
1959       $output = utf8_encode( $output );
1960     if( $gzip ) {
1961       $output = gzencode( $output, 9 );
1962       header( 'Content-Encoding: gzip');
1963       header( 'Vary: *');
1964     }
1965     $filesize = strlen( $output );
1966     if( 'xcal' == $this->format )
1967       header( 'Content-Type: application/calendar+xml; charset=utf-8' );
1968     else
1969       header( 'Content-Type: text/calendar; charset=utf-8' );
1970     header( 'Content-Length: '.$filesize );
1971     header( 'Content-Disposition: attachment; filename="'.$filename.'"' );
1972     header( 'Cache-Control: max-age=10' );
1973     echo $output;
1974     die();
1975   }
1976 /**
1977  * save content in a file
1978  *
1979  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
1980  * @since 2.2.12 - 2007-12-30
1981  * @param string $directory optional
1982  * @param string $filename optional
1983  * @param string $delimiter optional
1984  * @return bool
1985  */
1986   function saveCalendar( $directory=FALSE, $filename=FALSE, $delimiter=FALSE ) {
1987     if( $directory )
1988       $this->setConfig( 'directory', $directory );
1989     if( $filename )
1990       $this->setConfig( 'filename',  $filename );
1991     if( $delimiter && ($delimiter != DIRECTORY_SEPARATOR ))
1992       $this->setConfig( 'delimiter', $delimiter );
1993     if( FALSE === ( $dirfile = $this->getConfig( 'url' )))
1994       $dirfile = $this->getConfig( 'dirfile' );
1995     $iCalFile = @fopen( $dirfile, 'w' );
1996     if( $iCalFile ) {
1997       if( FALSE === fwrite( $iCalFile, $this->createCalendar() ))
1998         return FALSE;
1999       fclose( $iCalFile );
2000       return TRUE;
2001     }
2002     else
2003       return FALSE;
2004   }
2005 /**
2006  * if recent version of calendar file exists (default one hour), an HTTP redirect header is sent
2007  * else FALSE is returned
2008  *
2009  * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
2010  * @since 2.2.12 - 2007-10-28
2011  * @param string $directory optional alt. int timeout
2012  * @param string $filename optional
2013  * @param string $delimiter optional
2014  * @param int timeout optional, default 3600 sec
2015  * @return redirect/FALSE
2016  */
2017   function useCachedCalendar( $directory=FALSE, $filename=FALSE, $delimiter=FALSE, $timeout=3600) {
2018     if ( $directory && ctype_digit( (string) $directory ) && !$filename ) {
2019       $timeout   = (int) $directory;
2020       $directory = FALSE;
2021     }
2022     if( $directory )
2023       $this->setConfig( 'directory', $directory );
2024     if( $filename )
2025       $this->setConfig( 'filename',  $filename );
2026     if( $delimiter && ( $delimiter != DIRECTORY_SEPARATOR ))
2027       $this->setConfig( 'delimiter', $delimiter );
2028     $filesize    = $this->getConfig( 'filesize' );
2029     if( 0 >= $filesize )
2030       return FALSE;
2031     $dirfile     = $this->getConfig( 'dirfile' );
2032     if( time() - filemtime( $dirfile ) < $timeout) {
2033       clearstatcache();
2034       $dirfile   = $this->getConfig( 'dirfile' );
2035       $filename  = $this->getConfig( 'filename' );
2036 //    if( headers_sent( $filename, $linenum ))
2037 //      die( "Headers already sent in $filename on line $linenum\n" );
2038       if( 'xcal' == $this->format )
2039         header( 'Content-Type: application/calendar+xml; charset=utf-8' );
2040       else
2041         header( 'Content-Type: text/calendar; charset=utf-8' );
2042       header( 'Content-Length: '.$filesize );
2043       header( 'Content-Disposition: attachment; filename="'.$filename.'"' );
2044       header( 'Cache-Control: max-age=10' );
2045       $fp = @fopen( $dirfile, 'r' );
2046       if( $fp ) {
2047         fpassthru( $fp );
2048         fclose( $fp );
2049       }
2050       die();
2051     }
2052     else
2053       return FALSE;
2054   }
2055 }