Overview

Packages

  • awl
    • AuthPlugin
    • AwlDatabase
    • Browser
    • classEditor
    • DataEntry
    • DataUpdate
    • EMail
    • iCalendar
    • MenuSet
    • PgQuery
    • Session
    • Translation
    • User
    • Utilities
    • Validation
    • vCalendar
    • vComponent
    • XMLDocument
    • XMLElement
  • None
  • PHP

Classes

  • iCalComponent
  • iCalProp
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: /**
  3: * A Class for handling iCalendar data.
  4: *
  5: * When parsed the underlying structure is roughly as follows:
  6: *
  7: *   iCalendar( array(iCalComponent), array(iCalProp) )
  8: *
  9: * each iCalComponent is similarly structured:
 10: *
 11: *   iCalComponent( array(iCalComponent), array(iCalProp) )
 12: *
 13: * Once parsed, $ical->component will point to the wrapping VCALENDAR component of
 14: * the iCalendar.  This will be fine for simple iCalendar usage as sampled below,
 15: * but more complex iCalendar such as a VEVENT with RRULE which has repeat overrides
 16: * will need quite a bit more thought to process correctly.
 17: *
 18: * @example
 19: * To create a new iCalendar from several data values:
 20: *   $ical = new iCalendar( array('DTSTART' => $dtstart, 'SUMMARY' => $summary, 'DURATION' => $duration ) );
 21: *
 22: * @example
 23: * To render it as an iCalendar string:
 24: *   echo $ical->Render();
 25: *
 26: * @example
 27: * To render just the VEVENTs in the iCalendar with a restricted list of properties:
 28: *   echo $ical->Render( false, 'VEVENT', array( 'DTSTART', 'DURATION', 'DTEND', 'RRULE', 'SUMMARY') );
 29: *
 30: * @example
 31: * To parse an existing iCalendar string for manipulation:
 32: *   $ical = new iCalendar( array('icalendar' => $icalendar_text ) );
 33: *
 34: * @example
 35: * To clear any 'VALARM' components in an iCalendar object
 36: *   $ical->component->ClearComponents('VALARM');
 37: *
 38: * @example
 39: * To replace any 'RRULE' property in an iCalendar object
 40: *   $ical->component->SetProperties( 'RRULE', $rrule_definition );
 41: *
 42: * @package awl
 43: * @subpackage iCalendar
 44: * @author Andrew McMillan <andrew@mcmillan.net.nz>
 45: * @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
 46: * @license   http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
 47: *
 48: */
 49: require_once('XMLElement.php');
 50: require_once('AwlQuery.php');
 51: 
 52: /**
 53: * A Class for representing properties within an iCalendar
 54: *
 55: * @package awl
 56: */
 57: class iCalProp {
 58:   /**#@+
 59:    * @access private
 60:    */
 61: 
 62:   /**
 63:    * The name of this property
 64:    *
 65:    * @var string
 66:    */
 67:   var $name;
 68: 
 69:   /**
 70:    * An array of parameters to this property, represented as key/value pairs.
 71:    *
 72:    * @var array
 73:    */
 74:   var $parameters;
 75: 
 76:   /**
 77:    * The value of this property.
 78:    *
 79:    * @var string
 80:    */
 81:   var $content;
 82: 
 83:   /**
 84:    * The original value that this was parsed from, if that's the way it happened.
 85:    *
 86:    * @var string
 87:    */
 88:   var $rendered;
 89: 
 90:   /**#@-*/
 91: 
 92:   /**
 93:    * The constructor parses the incoming string, which is formatted as per RFC2445 as a
 94:    *   propname[;param1=pval1[; ... ]]:propvalue
 95:    * however we allow ourselves to assume that the RFC2445 content unescaping has already
 96:    * happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent().
 97:    *
 98:    * @param string $propstring The string from the iCalendar which contains this property.
 99:    */
100:   function iCalProp( $propstring = null ) {
101:     $this->name = "";
102:     $this->content = "";
103:     $this->parameters = array();
104:     unset($this->rendered);
105:     if ( $propstring != null && gettype($propstring) == 'string' ) {
106:       $this->ParseFrom($propstring);
107:     }
108:   }
109: 
110: 
111:   /**
112:    * The constructor parses the incoming string, which is formatted as per RFC2445 as a
113:    *   propname[;param1=pval1[; ... ]]:propvalue
114:    * however we allow ourselves to assume that the RFC2445 content unescaping has already
115:    * happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent().
116:    *
117:    * @param string $propstring The string from the iCalendar which contains this property.
118:    */
119:   function ParseFrom( $propstring ) {
120:     $this->rendered = (strlen($propstring) < 72 ? $propstring : null);  // Only pre-rendered if we didn't unescape it
121: 
122:     // Unescape newlines
123:     $unescaped = preg_replace('{\\\\[nN]}', "\n", $propstring);
124: 
125:     /*
126:      * Split propname with params from propvalue. Searches for the first unquoted COLON.
127:      *
128:      * RFC5545 3.2
129:      *
130:      * Property parameter values that contain the COLON, SEMICOLON, or COMMA
131:      * character separators MUST be specified as quoted-string text values.
132:      * Property parameter values MUST NOT contain the DQUOTE character.
133:      */
134:     $split = $this->SplitQuoted($unescaped, ':', 2);
135:     if (count($split) != 2) {
136:       // Bad things happended...
137:       throw new \RuntimeException(sprintf("Couldn't parse property from string: `%s`", $propstring));
138:     }
139:     list($prop, $value) = $split;
140: 
141:     // Unescape ESCAPED-CHAR
142:     $this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $value);
143: 
144:     // Split property name and parameters
145:     $parameters = $this->SplitQuoted($prop, ';');
146:     $this->name = array_shift($parameters);
147:     $this->parameters = array();
148:     foreach ($parameters AS $k => $v) {
149:       $pos = strpos($v, '=');
150:       $name = substr($v, 0, $pos);
151:       $value = substr($v, $pos + 1);
152:       $this->parameters[$name] = preg_replace('/^"(.+)"$/', '$1', $value); // Removes DQUOTE on demand
153:     }
154: //    dbg_error_log('iCalendar', " iCalProp::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
155:   }
156: 
157:   /**
158:    * Splits quoted strings
159:    *
160:    * @param string $str The string
161:    * @param string $sep The delimeter character
162:    * @param integer $limit Limit number of results, rest of string in last element
163:    * @return array
164:    */
165:   function SplitQuoted($str, $sep = ',', $limit = 0) {
166:     $result = array();
167:     $cursor = 0;
168:     $inquote = false;
169:     $num = 0;
170:     for($i = 0, $len = strlen($str); $i < $len; ++$i) {
171:       $ch = $str[$i];
172:       if ($ch == '"') {
173:         $inquote = !$inquote;
174:       }
175:       if (!$inquote && $ch == $sep) {
176:         //var_dump("Found sep `$sep` - Splitting from $cursor to $i from $len.");
177:         // If we reached the maximal number of splits, we cut till the end and stop here.
178:         ++$num;
179:         if ($limit > 0 && $num == $limit) {
180:           $result[] = substr($str, $cursor);
181:           break;
182:         }
183:         $result[] = substr($str, $cursor, $i - $cursor);
184:         $cursor = $i + 1;
185:       }
186:       // Add rest of string on end reached
187:       if ($i + 1 == $len) {
188:         //var_dump("Reached end - Splitting from $cursor to $len.");
189:         $result[] = substr($str, $cursor);
190:       }
191:     }
192: 
193:     return $result;
194:   }
195: 
196:   /**
197:    * Get/Set name property
198:    *
199:    * @param string $newname [optional] A new name for the property
200:    *
201:    * @return string The name for the property.
202:    */
203:   function Name( $newname = null ) {
204:     if ( $newname != null ) {
205:       $this->name = $newname;
206:       if ( isset($this->rendered) ) unset($this->rendered);
207: //      dbg_error_log('iCalendar', " iCalProp::Name(%s)", $this->name );
208:     }
209:     return $this->name;
210:   }
211: 
212: 
213:   /**
214:    * Get/Set the content of the property
215:    *
216:    * @param string $newvalue [optional] A new value for the property
217:    *
218:    * @return string The value of the property.
219:    */
220:   function Value( $newvalue = null ) {
221:     if ( $newvalue != null ) {
222:       $this->content = $newvalue;
223:       if ( isset($this->rendered) ) unset($this->rendered);
224:     }
225:     return $this->content;
226:   }
227: 
228: 
229:   /**
230:    * Get/Set parameters in their entirety
231:    *
232:    * @param array $newparams An array of new parameter key/value pairs
233:    *
234:    * @return array The current array of parameters for the property.
235:    */
236:   function Parameters( $newparams = null ) {
237:     if ( $newparams != null ) {
238:       $this->parameters = $newparams;
239:       if ( isset($this->rendered) ) unset($this->rendered);
240:     }
241:     return $this->parameters;
242:   }
243: 
244: 
245:   /**
246:    * Test if our value contains a string
247:    *
248:    * @param string $search The needle which we shall search the haystack for.
249:    *
250:    * @return string The name for the property.
251:    */
252:   function TextMatch( $search ) {
253:     if ( isset($this->content) ) {
254:       return (stristr( $this->content, $search ) !== false);
255:     }
256:     return false;
257:   }
258: 
259: 
260:   /**
261:    * Get the value of a parameter
262:    *
263:    * @param string $name The name of the parameter to retrieve the value for
264:    *
265:    * @return string The value of the parameter
266:    */
267:   function GetParameterValue( $name ) {
268:     if ( isset($this->parameters[$name]) ) return $this->parameters[$name];
269:   }
270: 
271:   /**
272:    * Set the value of a parameter
273:    *
274:    * @param string $name The name of the parameter to set the value for
275:    *
276:    * @param string $value The value of the parameter
277:    */
278:   function SetParameterValue( $name, $value ) {
279:     if ( isset($this->rendered) ) unset($this->rendered);
280:     $this->parameters[$name] = $value;
281:   }
282: 
283:   /**
284:   * Render the set of parameters as key1=value1[;key2=value2[; ...]] with
285:   * any colons or semicolons escaped.
286:   */
287:   function RenderParameters() {
288:     $rendered = "";
289:     foreach( $this->parameters AS $k => $v ) {
290:       $escaped = preg_replace( "/([;:])/", '\\\\$1', $v);
291:       $rendered .= sprintf( ";%s=%s", $k, $escaped );
292:     }
293:     return $rendered;
294:   }
295: 
296: 
297:   /**
298:   * Render a suitably escaped RFC2445 content string.
299:   */
300:   function Render() {
301:     // If we still have the string it was parsed in from, it hasn't been screwed with
302:     // and we can just return that without modification.
303:     if ( isset($this->rendered) ) return $this->rendered;
304: 
305:     $property = preg_replace( '/[;].*$/', '', $this->name );
306:     $escaped = $this->content;
307:     switch( $property ) {
308:         /** Content escaping does not apply to these properties culled from RFC2445 */
309:       case 'ATTACH':                case 'GEO':                       case 'PERCENT-COMPLETE':      case 'PRIORITY':
310:       case 'DURATION':              case 'FREEBUSY':                  case 'TZOFFSETFROM':          case 'TZOFFSETTO':
311:       case 'TZURL':                 case 'ATTENDEE':                  case 'ORGANIZER':             case 'RECURRENCE-ID':
312:       case 'URL':                   case 'EXRULE':                    case 'SEQUENCE':              case 'CREATED':
313:       case 'RRULE':                 case 'REPEAT':                    case 'TRIGGER':
314:         break;
315: 
316:       case 'COMPLETED':             case 'DTEND':
317:       case 'DUE':                   case 'DTSTART':
318:       case 'DTSTAMP':               case 'LAST-MODIFIED':
319:       case 'CREATED':               case 'EXDATE':
320:       case 'RDATE':
321:         if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) {
322:           $escaped = substr( $escaped, 0, 8);
323:         }
324:         break;
325: 
326:         /** Content escaping applies by default to other properties */
327:       default:
328:         $escaped = str_replace( '\\', '\\\\', $escaped);
329:         $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
330:         $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
331:     }
332:     $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
333:     if ( (strlen($property) + strlen($escaped)) <= 72 ) {
334:       $this->rendered = $property . $escaped;
335:     }
336:     else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) {
337:       $this->rendered = $property . "\r\n " . $escaped;
338:     }
339:     else {
340:       $this->rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property . $escaped );
341:     }
342:     return $this->rendered;
343:   }
344: 
345: }
346: 
347: 
348: /**
349: * A Class for representing components within an iCalendar
350: *
351: * @package awl
352: */
353: class iCalComponent {
354:   /**#@+
355:    * @access private
356:    */
357: 
358:   /**
359:    * The type of this component, such as 'VEVENT', 'VTODO', 'VTIMEZONE', etc.
360:    *
361:    * @var string
362:    */
363:   var $type;
364: 
365:   /**
366:    * An array of properties, which are iCalProp objects
367:    *
368:    * @var array
369:    */
370:   var $properties;
371: 
372:   /**
373:    * An array of (sub-)components, which are iCalComponent objects
374:    *
375:    * @var array
376:    */
377:   var $components;
378: 
379:   /**
380:    * The rendered result (or what was originally parsed, if there have been no changes)
381:    *
382:    * @var array
383:    */
384:   var $rendered;
385: 
386:   /**#@-*/
387: 
388:   /**
389:   * A basic constructor
390:   */
391:   function iCalComponent( $content = null ) {
392:     $this->type = "";
393:     $this->properties = array();
394:     $this->components = array();
395:     $this->rendered = "";
396:     if ( $content != null && (gettype($content) == 'string' || gettype($content) == 'array') ) {
397:       $this->ParseFrom($content);
398:     }
399:   }
400: 
401: 
402:   /**
403:   * Apply standard properties for a VCalendar
404:   * @param array $extra_properties Key/value pairs of additional properties
405:   */
406:   function VCalendar( $extra_properties = null ) {
407:     $this->SetType('VCALENDAR');
408:     $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN');
409:     $this->AddProperty('VERSION', '2.0');
410:     $this->AddProperty('CALSCALE', 'GREGORIAN');
411:     if ( is_array($extra_properties) ) {
412:       foreach( $extra_properties AS $k => $v ) {
413:         $this->AddProperty($k,$v);
414:       }
415:     }
416:   }
417: 
418:   /**
419:   * Collect an array of all parameters of our properties which are the specified type
420:   * Mainly used for collecting the full variety of references TZIDs
421:   */
422:   function CollectParameterValues( $parameter_name ) {
423:     $values = array();
424:     foreach( $this->components AS $k => $v ) {
425:       $also = $v->CollectParameterValues($parameter_name);
426:       $values = array_merge( $values, $also );
427:     }
428:     foreach( $this->properties AS $k => $v ) {
429:       $also = $v->GetParameterValue($parameter_name);
430:       if ( isset($also) && $also != "" ) {
431: //        dbg_error_log( 'iCalendar', "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also);
432:         $values[$also] = 1;
433:       }
434:     }
435:     return $values;
436:   }
437: 
438: 
439:   /**
440:   * Parse the text $content into sets of iCalProp & iCalComponent within this iCalComponent
441:   * @param string $content The raw RFC2445-compliant iCalendar component, including BEGIN:TYPE & END:TYPE
442:   */
443:   function ParseFrom( $content ) {
444:     $this->rendered = $content;
445:     $content = $this->UnwrapComponent($content);
446: 
447:     $type = false;
448:     $subtype = false;
449:     $finish = null;
450:     $subfinish = null;
451: 
452:     $length = strlen($content);
453:     $linefrom = 0;
454:     while( $linefrom < $length ) {
455:       $lineto = strpos( $content, "\n", $linefrom );
456:       if ( $lineto === false ) {
457:         $lineto = strpos( $content, "\r", $linefrom );
458:       }
459:       if ( $lineto > 0 ) {
460:         $line = substr( $content, $linefrom, $lineto - $linefrom);
461:         $linefrom = $lineto + 1;
462:       }
463:       else {
464:         $line = substr( $content, $linefrom );
465:         $linefrom = $length;
466:       }
467:       if ( preg_match('/^\s*$/', $line ) ) continue;
468:       $line = rtrim( $line, "\r\n" );
469: //      dbg_error_log( 'iCalendar',  "::ParseFrom: Parsing line: $line");
470: 
471:       if ( $type === false ) {
472:         if ( preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
473:           // We have found the start of the main component
474:           $type = $matches[1];
475:           $finish = "END:$type";
476:           $this->type = $type;
477:           dbg_error_log( 'iCalendar', "::ParseFrom: Start component of type '%s'", $type);
478:         }
479:         else {
480:           dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap before start of component: $line");
481:           // unset($lines[$k]);  // The content has crap before the start
482:           if ( $line != "" ) $this->rendered = null;
483:         }
484:       }
485:       else if ( $type == null ) {
486:         dbg_error_log( 'iCalendar', "::ParseFrom: Ignoring crap after end of component");
487:         if ( $line != "" ) $this->rendered = null;
488:       }
489:       else if ( $line == $finish ) {
490:         dbg_error_log( 'iCalendar', "::ParseFrom: End of component");
491:         $type = null;  // We have reached the end of our component
492:       }
493:       else {
494:         if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $line, $matches ) ) {
495:           // We have found the start of a sub-component
496:           $subtype = $matches[1];
497:           $subfinish = "END:$subtype";
498:           $subcomponent = $line . "\r\n";
499:           dbg_error_log( 'iCalendar', "::ParseFrom: Found a subcomponent '%s'", $subtype);
500:         }
501:         else if ( $subtype ) {
502:           // We are inside a sub-component
503:           $subcomponent .= $this->WrapComponent($line);
504:           if ( $line == $subfinish ) {
505:             dbg_error_log( 'iCalendar', "::ParseFrom: End of subcomponent '%s'", $subtype);
506:             // We have found the end of a sub-component
507:             $this->components[] = new iCalComponent($subcomponent);
508:             $subtype = false;
509:           }
510: //          else
511: //            dbg_error_log( 'iCalendar', "::ParseFrom: Inside a subcomponent '%s'", $subtype );
512:         }
513:         else {
514: //          dbg_error_log( 'iCalendar', "::ParseFrom: Parse property of component");
515:           // It must be a normal property line within a component.
516:           $this->properties[] = new iCalProp($line);
517:         }
518:       }
519:     }
520:   }
521: 
522: 
523:   /**
524:     * This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According
525:     * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising
526:     * XML parsers often muck with it and may remove the CR.  We accept either case.
527:     */
528:   function UnwrapComponent( $content ) {
529:     return preg_replace('/\r?\n[ \t]/', '', $content );
530:   }
531: 
532:   /**
533:     * This imposes the (CRLF + linear space) wrapping specified in RFC2445. According
534:     * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising
535:     * XML parsers often muck with it and may remove the CR.  We output RFC2445 compliance.
536:     *
537:     * In order to preserve pre-existing wrapping in the component, we split the incoming
538:     * string on line breaks before running wordwrap over each component of that.
539:     */
540:   function WrapComponent( $content ) {
541:     $strs = preg_split( "/\r?\n/", $content );
542:     $wrapped = "";
543:     foreach ($strs as $str) {
544:       $wrapped .= preg_replace( '/(.{72})/u', '$1'."\r\n ", $str ) ."\r\n";
545:     }
546:     return $wrapped;
547:   }
548: 
549:   /**
550:   * Return the type of component which this is
551:   */
552:   function GetType() {
553:     return $this->type;
554:   }
555: 
556: 
557:   /**
558:   * Set the type of component which this is
559:   */
560:   function SetType( $type ) {
561:     if ( isset($this->rendered) ) unset($this->rendered);
562:     $this->type = $type;
563:     return $this->type;
564:   }
565: 
566: 
567:   /**
568:   * Get all properties, or the properties matching a particular type
569:   */
570:   function GetProperties( $type = null ) {
571:     $properties = array();
572:     foreach( $this->properties AS $k => $v ) {
573:       if ( $type == null || $v->Name() == $type ) {
574:         $properties[$k] = $v;
575:       }
576:     }
577:     return $properties;
578:   }
579: 
580: 
581:   /**
582:   * Get the value of the first property matching the name. Obviously this isn't
583:   * so useful for properties which may occur multiply, but most don't.
584:   *
585:   * @param string $type The type of property we are after.
586:   * @return string The value of the property, or null if there was no such property.
587:   */
588:   function GetPValue( $type ) {
589:     foreach( $this->properties AS $k => $v ) {
590:       if ( $v->Name() == $type ) return $v->Value();
591:     }
592:     return null;
593:   }
594: 
595: 
596:   /**
597:   * Get the value of the specified parameter for the first property matching the
598:   * name. Obviously this isn't so useful for properties which may occur multiply, but most don't.
599:   *
600:   * @param string $type The type of property we are after.
601:   * @param string $type The name of the parameter we are after.
602:   * @return string The value of the parameter for the property, or null in the case that there was no such property, or no such parameter.
603:   */
604:   function GetPParamValue( $type, $parameter_name ) {
605:     foreach( $this->properties AS $k => $v ) {
606:       if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name);
607:     }
608:     return null;
609:   }
610: 
611: 
612:   /**
613:   * Clear all properties, or the properties matching a particular type
614:   * @param string $type The type of property - omit for all properties
615:   */
616:   function ClearProperties( $type = null ) {
617:     if ( $type != null ) {
618:       // First remove all the existing ones of that type
619:       foreach( $this->properties AS $k => $v ) {
620:         if ( $v->Name() == $type ) {
621:           unset($this->properties[$k]);
622:           if ( isset($this->rendered) ) unset($this->rendered);
623:         }
624:       }
625:       $this->properties = array_values($this->properties);
626:     }
627:     else {
628:       if ( isset($this->rendered) ) unset($this->rendered);
629:       $this->properties = array();
630:     }
631:   }
632: 
633: 
634:   /**
635:   * Set all properties, or the ones matching a particular type
636:   */
637:   function SetProperties( $new_properties, $type = null ) {
638:     if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered);
639:     $this->ClearProperties($type);
640:     foreach( $new_properties AS $k => $v ) {
641:       $this->AddProperty($v);
642:     }
643:   }
644: 
645: 
646:   /**
647:   * Adds a new property
648:   *
649:   * @param iCalProp $new_property The new property to append to the set, or a string with the name
650:   * @param string $value The value of the new property (default: param 1 is an iCalProp with everything
651:   * @param array $parameters The key/value parameter pairs (default: none, or param 1 is an iCalProp with everything)
652:   */
653:   function AddProperty( $new_property, $value = null, $parameters = null ) {
654:     if ( isset($this->rendered) ) unset($this->rendered);
655:     if ( isset($value) && gettype($new_property) == 'string' ) {
656:       $new_prop = new iCalProp();
657:       $new_prop->Name($new_property);
658:       $new_prop->Value($value);
659:       if ( $parameters != null ) $new_prop->Parameters($parameters);
660:       dbg_error_log('iCalendar'," Adding new property '%s'", $new_prop->Render() );
661:       $this->properties[] = $new_prop;
662:     }
663:     else if ( gettype($new_property) ) {
664:       $this->properties[] = $new_property;
665:     }
666:   }
667: 
668: 
669:   /**
670:   * Get all sub-components, or at least get those matching a type
671:   * @return array an array of the sub-components
672:   */
673:   function &FirstNonTimezone( $type = null ) {
674:     foreach( $this->components AS $k => $v ) {
675:       if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k];
676:     }
677:     $result = false;
678:     return $result;
679:   }
680: 
681: 
682:   /**
683:   * Return true if the person identified by the email address is down as an
684:   * organizer for this meeting.
685:   * @param string $email The e-mail address of the person we're seeking.
686:   * @return boolean true if we found 'em, false if we didn't.
687:   */
688:   function IsOrganizer( $email ) {
689:     if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
690:     $props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER');
691:     foreach( $props AS $k => $prop ) {
692:       if ( $prop->Value() == $email ) return true;
693:     }
694:     return false;
695:   }
696: 
697: 
698:   /**
699:   * Return true if the person identified by the email address is down as an
700:   * attendee or organizer for this meeting.
701:   * @param string $email The e-mail address of the person we're seeking.
702:   * @return boolean true if we found 'em, false if we didn't.
703:   */
704:   function IsAttendee( $email ) {
705:     if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:'.$email;
706:     if ( $this->IsOrganizer($email) ) return true; /** an organizer is an attendee, as far as we're concerned */
707:     $props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE');
708:     foreach( $props AS $k => $prop ) {
709:       if ( $prop->Value() == $email ) return true;
710:     }
711:     return false;
712:   }
713: 
714: 
715:   /**
716:   * Get all sub-components, or at least get those matching a type, or failling to match,
717:   * should the second parameter be set to false.
718:   *
719:   * @param string $type The type to match (default: All)
720:   * @param boolean $normal_match Set to false to invert the match (default: true)
721:   * @return array an array of the sub-components
722:   */
723:   function GetComponents( $type = null, $normal_match = true ) {
724:     $components = $this->components;
725:     if ( $type != null ) {
726:       foreach( $components AS $k => $v ) {
727:         if ( ($v->GetType() != $type) === $normal_match ) {
728:           unset($components[$k]);
729:         }
730:       }
731:       $components = array_values($components);
732:     }
733:     return $components;
734:   }
735: 
736: 
737:   /**
738:   * Clear all components, or the components matching a particular type
739:   * @param string $type The type of component - omit for all components
740:   */
741:   function ClearComponents( $type = null ) {
742:     if ( $type != null ) {
743:       // First remove all the existing ones of that type
744:       foreach( $this->components AS $k => $v ) {
745:         if ( $v->GetType() == $type ) {
746:           unset($this->components[$k]);
747:           if ( isset($this->rendered) ) unset($this->rendered);
748:         }
749:         else {
750:           if ( ! $this->components[$k]->ClearComponents($type) ) {
751:             if ( isset($this->rendered) ) unset($this->rendered);
752:           }
753:         }
754:       }
755:       return isset($this->rendered);
756:     }
757:     else {
758:       if ( isset($this->rendered) ) unset($this->rendered);
759:       $this->components = array();
760:     }
761:   }
762: 
763: 
764:   /**
765:   * Sets some or all sub-components of the component to the supplied new components
766:   *
767:   * @param array of iCalComponent $new_components The new components to replace the existing ones
768:   * @param string $type The type of components to be replaced.  Defaults to null, which means all components will be replaced.
769:   */
770:   function SetComponents( $new_component, $type = null ) {
771:     if ( isset($this->rendered) ) unset($this->rendered);
772:     if ( count($new_component) > 0 ) $this->ClearComponents($type);
773:     foreach( $new_component AS $k => $v ) {
774:       $this->components[] = $v;
775:     }
776:   }
777: 
778: 
779:   /**
780:   * Adds a new subcomponent
781:   *
782:   * @param iCalComponent $new_component The new component to append to the set
783:   */
784:   function AddComponent( $new_component ) {
785:     if ( is_array($new_component) && count($new_component) == 0 ) return;
786:     if ( isset($this->rendered) ) unset($this->rendered);
787:     if ( is_array($new_component) ) {
788:       foreach( $new_component AS $k => $v ) {
789:         $this->components[] = $v;
790:       }
791:     }
792:     else {
793:       $this->components[] = $new_component;
794:     }
795:   }
796: 
797: 
798:   /**
799:   * Mask components, removing any that are not of the types in the list
800:   * @param array $keep An array of component types to be kept
801:   */
802:   function MaskComponents( $keep ) {
803:     foreach( $this->components AS $k => $v ) {
804:       if ( ! in_array( $v->GetType(), $keep ) ) {
805:         unset($this->components[$k]);
806:         if ( isset($this->rendered) ) unset($this->rendered);
807:       }
808:       else {
809:         $v->MaskComponents($keep);
810:       }
811:     }
812:   }
813: 
814: 
815:   /**
816:   * Mask properties, removing any that are not in the list
817:   * @param array $keep An array of property names to be kept
818:   * @param array $component_list An array of component types to check within
819:   */
820:   function MaskProperties( $keep, $component_list=null ) {
821:     foreach( $this->components AS $k => $v ) {
822:       $v->MaskProperties($keep, $component_list);
823:     }
824: 
825:     if ( !isset($component_list) || in_array($this->GetType(), $component_list) ) {
826:       foreach( $this->properties AS $k => $v ) {
827:         if ( ! in_array( $v->name, $keep ) ) {
828:           unset($this->properties[$k]);
829:           if ( isset($this->rendered) ) unset($this->rendered);
830:         }
831:       }
832:     }
833:   }
834: 
835: 
836:   /**
837:   * Clone this component (and subcomponents) into a confidential version of it.  A confidential
838:   * event will be scrubbed of any identifying characteristics other than time/date, repeat, uid
839:   * and a summary which is just a translated 'Busy'.
840:   */
841:   function CloneConfidential() {
842:     $confidential = clone($this);
843:     $keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'DUE', 'UID', 'CLASS', 'TRANSP', 'CREATED', 'LAST-MODIFIED' );
844:     $resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' );
845:     $confidential->MaskComponents(array( 'VTIMEZONE', 'STANDARD', 'DAYLIGHT', 'VEVENT', 'VTODO', 'VJOURNAL' ));
846:     $confidential->MaskProperties($keep_properties, $resource_components );
847: 
848:     if ( isset($confidential->rendered) )
849:         unset($confidential->rendered); // we need to re-render the whole object
850: 
851:     if ( in_array( $confidential->GetType(), $resource_components ) ) {
852:       $confidential->AddProperty( 'SUMMARY', translate('Busy') );
853:     }
854:     foreach( $confidential->components AS $k => $v ) {
855:       if ( in_array( $v->GetType(), $resource_components ) ) {
856:         $v->AddProperty( 'SUMMARY', translate('Busy') );
857:       }
858:     }
859: 
860:     return $confidential;
861:   }
862: 
863:     /**
864:      * this function supstitute function from vCalendar::RenderWithoutWrap
865:      * NOTE: vCalendar::RenderWithoutWrap - return string without \r\n on end
866:      *          thats here removed the tail of iCalendar::Render
867:      *          which return \r\n on end
868:      * @param null $restricted_properties
869:      * @return string - rendered string
870:      */
871:     function RenderWithoutWrap($restricted_properties = null){
872:       // substr - remove new line of end, because new line
873:       // are handled in vComponent::RenderWithoutWrap
874:       return substr($this->Render($restricted_properties), 0 , -2);
875:   }
876: 
877: 
878:   /**
879:   *  Renders the component, possibly restricted to only the listed properties
880:   */
881:   function Render( $restricted_properties = null) {
882: 
883:     $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0);
884: 
885:     if ( isset($this->rendered) && $unrestricted )
886:       return $this->rendered;
887: 
888:     $rendered = "BEGIN:$this->type\r\n";
889:     foreach( $this->properties AS $k => $v ) {
890:       if ( method_exists($v, 'Render') ) {
891:         if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n";
892:       }
893:     }
894:     foreach( $this->components AS $v ) {   $rendered .= $v->Render();  }
895:     $rendered .= "END:$this->type\r\n";
896: 
897:     $rendered = preg_replace('{(?<!\r)\n}', "\r\n", $rendered);
898:     if ( $unrestricted ) $this->rendered = $rendered;
899: 
900:     return $rendered;
901:   }
902: 
903: 
904:   /**
905:   * Return an array of properties matching the specified path
906:   *
907:   * @return array An array of iCalProp within the tree which match the path given, in the form
908:   *  [/]COMPONENT[/...]/PROPERTY in a syntax kind of similar to our poor man's XML queries. We
909:   *  also allow COMPONENT and PROPERTY to be !COMPONENT and !PROPERTY for ++fun.
910:   *
911:   * @note At some point post PHP4 this could be re-done with an iterator, which should be more efficient for common use cases.
912:   */
913:   function GetPropertiesByPath( $path ) {
914:     $properties = array();
915:     dbg_error_log( 'iCalendar', "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path );
916:     if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties;
917: 
918:     $adrift = ($matches[1] == '');
919:     $normal = ($matches[2] == '');
920:     $ourtest = $matches[3];
921:     $therest = $matches[4];
922:     dbg_error_log( 'iCalendar', "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] );
923:     if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) {
924:       if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) {
925:         $normmatch = ($matches[1] =='');
926:         $proptest  = $matches[2];
927:         foreach( $this->properties AS $k => $v ) {
928:           if ( $proptest == '*' || (($v->Name() == $proptest) === $normmatch ) ) {
929:             $properties[] = $v;
930:           }
931:         }
932:       }
933:       else {
934:         /**
935:         * There is more to the path, so we recurse into that sub-part
936:         */
937:         foreach( $this->components AS $k => $v ) {
938:           $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) );
939:         }
940:       }
941:     }
942: 
943:     if ( $adrift ) {
944:       /**
945:       * Our input $path was not rooted, so we recurse further
946:       */
947:       foreach( $this->components AS $k => $v ) {
948:         $properties = array_merge( $properties, $v->GetPropertiesByPath($path) );
949:       }
950:     }
951:     dbg_error_log('iCalendar', "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path );
952:     return $properties;
953:   }
954: 
955: }
956: 
AWL API documentation generated by ApiGen 2.8.0