3 require_once('XMLElement.php');
5 * A Class for representing properties within a myComponent (VCALENDAR or VCARD)
9 class vProperty
extends vObject
{
15 * The name of this property
22 * An array of parameters to this property, represented as key/value pairs.
26 protected $parameters;
29 * The value of this property.
36 * The original value that this was parsed from, if that's the way it happened.
43 * The original seek of iterator
50 //protected $rendered;
56 * The constructor parses the incoming string, which is formatted as per RFC2445 as a
57 * propname[;param1=pval1[; ... ]]:propvalue
58 * however we allow ourselves to assume that the RFC2445 content unescaping has already
59 * happened when myComponent::ParseFrom() called myComponent::UnwrapComponent().
61 * @param HeapLines $heapLines The string from the myComponent which contains this property.
63 function __construct( $name = null, &$master = null, &$refData = null, $seek = null ) {
64 parent
::__construct($master);
67 if(isset($name) && strlen($name) > 0){
73 unset($this->content
);
74 unset($this->parameters
);
78 if ( isset($refData)){
81 if(gettype($refData) == 'object') {
82 $this->iterator
= &$refData;
86 $this->line
= $refData;
89 //$this->ParseFrom($refData);
93 unset($this->iterator
);
96 //$this->ParseFrom($refData);
97 //unset($this->iterator);
100 unset($this->iterator
);
104 unset($this->rendered
);
109 * The constructor parses the incoming string, which is formatted as per RFC2445 as a
110 * propname[;param1=pval1[; ... ]]:propvalue
111 * however we allow ourselves to assume that the RFC2445 content unescaping has already
112 * happened when myComponent::ParseFrom() called myComponent::UnwrapComponent().
114 * @param string $propstring The string from the myComponent which contains this property.
116 function ParseFrom( &$unescaped ) {
117 //$this->rendered = $heapLines;
118 // temporady like string
119 //$this->rendered = (strlen($propstring) < 73 ? $propstring : null); // Only pre-rendered if we didn't unescape it
121 $unescaped = preg_replace( '{\\\\[nN]}', "\n", $unescaped);
123 // Split into two parts on : which is not preceded by a \, or within quotes like "str:ing".
126 $splitpos = strpos($unescaped,':',$offset);
127 $start = substr($unescaped,0,$splitpos);
128 if ( substr($start,-1) == '\\' ) {
129 $offset = $splitpos +
1;
132 $quotecount = strlen(preg_replace('{[^"]}', '', $start ));
133 if ( ($quotecount %
2) != 0 ) {
134 $offset = $splitpos +
1;
140 $values = substr($unescaped, $splitpos+
1);
142 $possiblecontent = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $values);
143 // in case if the name was set manualy content by function Valued
144 // -> don't reset it by $rendered data
145 if(!isset($this->content
)){
146 $this->content
= $possiblecontent;
150 // Split on ; which is not preceded by a \
151 $parameters = preg_split( '{(?<!\\\\);}', $start);
154 $possiblename = strtoupper(array_shift( $parameters ));
155 // in case if the name was set manualy by function Name
156 // -> don't reset it by $rendered data
157 if(!isset($this->name
)){
158 $this->name
= $possiblename;
161 // in case if the parameter was set manualy by function Parameters
162 // -> don't reset it by $rendered data
163 if(!isset($this->parameters
)){
164 $this->parameters
= array();
165 foreach( $parameters AS $k => $v ) {
166 $pos = strpos($v,'=');
167 $name = strtoupper(substr( $v, 0, $pos));
168 $value = substr( $v, $pos +
1);
169 if ( preg_match( '{^"(.*)"$}', $value, $matches) ) {
170 $value = $matches[1];
172 if ( isset($this->parameters
[$name]) && is_array($this->parameters
[$name]) ) {
173 $this->parameters
[$name][] = $value;
175 elseif ( isset($this->parameters
[$name]) ) {
176 $this->parameters
[$name] = array( $this->parameters
[$name], $value);
179 $this->parameters
[$name] = $value;
182 // dbg_error_log('myComponent', " vProperty::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
186 function ParseFromIterator(){
189 if(isset($this->iterator
)){
190 $this->iterator
->seek($this->seek
);
191 $unescaped = $this->iterator
->current();
194 } else if(isset($this->line
)){
195 $unescaped = $this->line
;
200 $this->ParseFrom($unescaped);
206 * Get/Set name property
208 * @param string $newname [optional] A new name for the property
210 * @return string The name for the property.
212 function Name( $newname = null ) {
213 if ( $newname != null ) {
214 $this->name
= strtoupper($newname);
215 if ( $this->isValid() ) $this->invalidate();
216 // dbg_error_log('myComponent', " vProperty::Name(%s)", $this->name );
217 } else if(!isset($this->name
)){
218 $this->ParseFromIterator();
225 * Get/Set the content of the property
227 * @param string $newvalue [optional] A new value for the property
229 * @return string The value of the property.
231 function Value( $newvalue = null ) {
232 if ( $newvalue != null ) {
233 $this->content
= $newvalue;
234 if ( $this->isValid() ) $this->invalidate();
235 } else if(!isset($this->content
)){
236 $this->ParseFromIterator();
238 return $this->content
;
243 * Get/Set parameters in their entirety
245 * @param array $newparams An array of new parameter key/value pairs. The 'value' may be an array of values.
247 * @return array The current array of parameters for the property.
249 function Parameters( $newparams = null ) {
250 if ( $newparams != null ) {
251 $this->parameters
= array();
252 foreach( $newparams AS $k => $v ) {
253 $this->parameters
[strtoupper($k)] = $v;
255 if ( $this->isValidate() ) $this->invalidate();
256 } else if(!isset($this->parameters
)){
257 $this->ParseFromIterator();
259 return $this->parameters
;
264 * Test if our value contains a string
266 * @param string $search The needle which we shall search the haystack for.
268 * @return string The name for the property.
270 function TextMatch( $search ) {
271 if ( isset($this->content
) ) return strstr( $this->content
, $search );
277 * Get the value of a parameter
279 * @param string $name The name of the parameter to retrieve the value for
281 * @return string The value of the parameter
283 function GetParameterValue( $name ) {
284 $name = strtoupper($name);
286 if(!isset($this->parameters
)){
287 $this->ParseFromIterator();
290 if ( isset($this->parameters
[$name]) ){
291 return $this->parameters
[$name];
297 * Set the value of a parameter
299 * @param string $name The name of the parameter to set the value for
301 * @param string $value The value of the parameter
303 function SetParameterValue( $name, $value ) {
304 if(!isset($this->parameters
)){
305 $this->ParseFromIterator();
308 if ( $this->isValid() ) {
312 $this->parameters
[strtoupper($name)] = $value;
313 // dbg_error_log('PUT', $this->name.$this->RenderParameters().':'.$this->content );
317 private static function escapeParameter($p) {
318 if ( strpos($p, ';') === false && strpos($p, ':') === false ) return $p;
319 return '"'.str_replace('"','\\"',$p).'"';
323 * Render the set of parameters as key1=value1[;key2=value2[; ...]] with
324 * any colons or semicolons escaped.
326 function RenderParameters() {
328 if(isset($this->parameters
)){
329 foreach( $this->parameters
AS $k => $v ) {
330 if ( is_array($v) ) {
331 foreach( $v AS $vv ) {
332 $rendered .= sprintf( ';%s=%s', $k, vProperty
::escapeParameter($vv) );
336 $rendered .= sprintf( ';%s=%s', $k, vProperty
::escapeParameter($v) );
346 * Render a suitably escaped RFC2445 content string.
348 function Render( $force = false ) {
349 // If we still have the string it was parsed in from, it hasn't been screwed with
350 // and we can just return that without modification.
351 if ( $force === false && $this->isValid() && isset($this->rendered
) && strlen($this->rendered
) < 73 ) {
352 return $this->rendered
;
355 // in case one of the memberts doesn't set -> try parse from rendered
356 if(!isset($this->name
) ||
!isset($this->content
) ||
!isset($this->parameters
)) {
358 $this->ParseFromIterator();
362 $property = preg_replace( '/[;].*$/', '', $this->name
);
363 $escaped = $this->content
;
364 switch( $property ) {
365 /** Content escaping does not apply to these properties culled from RFC2445 */
366 case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
367 case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
368 case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
369 case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
370 case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'RDATE':
371 case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART':
372 case 'DTSTAMP': case 'LAST-MODIFIED': case 'CREATED': case 'EXDATE':
375 /** Content escaping does not apply to these properties culled from RFC6350 / RFC2426 */
376 case 'ADR': case 'N':
377 // escaping for ';' for these fields also needs to happen to the components they are built from.
378 $escaped = str_replace( '\\', '\\\\', $escaped);
379 $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
380 $escaped = str_replace( ',', '\\,', $escaped);
383 /** Content escaping applies by default to other properties */
385 $escaped = str_replace( '\\', '\\\\', $escaped);
386 $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
387 $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
390 $property = sprintf( "%s%s:", $this->name
, $this->RenderParameters() );
391 if ( (strlen($property) +
strlen($escaped)) <= 72 ) {
392 $this->rendered
= $property . $escaped;
394 else if ( (strlen($property) <= 72) && (strlen($escaped) <= 72) ) {
395 $this->rendered
= $property . "\r\n " . $escaped;
398 $this->rendered
= preg_replace( '/(.{72})/u', '$1'."\r\n ", $property.$escaped );
400 // trace_bug( 'Re-rendered "%s" property.', $this->name );
401 return $this->rendered
;
405 public function __toString() {
406 return $this->Render();
411 * Test a PROP-FILTER or PARAM-FILTER and return a true/false
412 * PROP-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*))
413 * PARAM-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*))
415 * @param array $filter An array of XMLElement defining the filter
417 * @return boolean Whether or not this vProperty passes the test
419 function TestFilter( $filters ) {
420 foreach( $filters AS $k => $v ) {
421 $tag = $v->GetNSTag();
422 // dbg_error_log( 'vCalendar', "vProperty:TestFilter: '%s'='%s' => '%s'", $this->name, $tag, $this->content );
424 case 'urn:ietf:params:xml:ns:caldav:is-defined':
425 case 'urn:ietf:params:xml:ns:carddav:is-defined':
426 if ( empty($this->content
) ) return false;
429 case 'urn:ietf:params:xml:ns:caldav:is-not-defined':
430 case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
431 if ( ! empty($this->content
) ) return false;
434 case 'urn:ietf:params:xml:ns:caldav:time-range':
435 /** @todo: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */
438 case 'urn:ietf:params:xml:ns:carddav:text-match':
439 case 'urn:ietf:params:xml:ns:caldav:text-match':
440 $search = $v->GetContent();
441 $match = $this->TextMatch($search);
442 $negate = $v->GetAttribute("negate-condition");
443 if ( isset($negate) && strtolower($negate) == "yes" ) {
446 if ( ! $match ) return false;
449 case 'urn:ietf:params:xml:ns:carddav:param-filter':
450 case 'urn:ietf:params:xml:ns:caldav:param-filter':
451 $subfilter = $v->GetContent();
452 $parameter = $this->GetParameterValue($v->GetAttribute("name"));
453 if ( ! $this->TestParamFilter($subfilter,$parameter) ) return false;
457 dbg_error_log( 'myComponent', ' vProperty::TestFilter: unhandled tag "%s"', $tag );
464 function fill($sp, $en, $pe){
468 function TestParamFilter( $filters, $parameter_value ) {
469 foreach( $filters AS $k => $v ) {
470 $subtag = $v->GetNSTag();
471 // dbg_error_log( 'vCalendar', "vProperty:TestParamFilter: '%s'='%s' => '%s'", $this->name, $subtag, $parameter_value );
473 case 'urn:ietf:params:xml:ns:caldav:is-defined':
474 case 'urn:ietf:params:xml:ns:carddav:is-defined':
475 if ( empty($parameter_value) ) return false;
478 case 'urn:ietf:params:xml:ns:caldav:is-not-defined':
479 case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
480 if ( ! empty($parameter_value) ) return false;
483 case 'urn:ietf:params:xml:ns:caldav:time-range':
484 /** @todo: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */
487 case 'urn:ietf:params:xml:ns:carddav:text-match':
488 case 'urn:ietf:params:xml:ns:caldav:text-match':
489 $search = $v->GetContent();
491 if ( !empty($parameter_value) ) $match = strstr( $this->content
, $search );
492 $negate = $v->GetAttribute("negate-condition");
493 if ( isset($negate) && strtolower($negate) == "yes" ) {
496 if ( ! $match ) return false;
500 dbg_error_log( 'myComponent', ' vProperty::TestParamFilter: unhandled tag "%s"', $tag );