check collation for caseinsensitive search
[awl.git] / inc / vProperty.php
blob7a634ce17817337c6650fc8d19d3dfd2355ff59d
1 <?php
3 require_once('XMLElement.php');
4 /**
5 * A Class for representing properties within a myComponent (VCALENDAR or VCARD)
7 * @package awl
8 */
9 class vProperty extends vObject {
10 /**#@+
11 * @access private
14 /**
15 * The name of this property
17 * @var string
19 protected $name;
21 /**
22 * An array of parameters to this property, represented as key/value pairs.
24 * @var array
26 protected $parameters;
28 /**
29 * The value of this property.
31 * @var string
33 protected $content;
35 /**
36 * The original value that this was parsed from, if that's the way it happened.
38 * @var ArrayIterator
40 protected $iterator;
42 /**
43 * The original seek of iterator
44 * @var int
46 protected $seek;
48 protected $line;
50 //protected $rendered;
53 /**#@-*/
55 /**
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){
68 $this->name = $name;
69 } else {
70 unset($this->name);
73 unset($this->content);
74 unset($this->parameters);
78 if ( isset($refData)){
81 if(gettype($refData) == 'object') {
82 $this->iterator = &$refData;
83 $this->seek = &$seek;
84 unset($this->line);
85 } else {
86 $this->line = $refData;
88 /* test */
89 //$this->ParseFrom($refData);
90 //unset($this->line);
93 unset($this->iterator);
94 unset($this->seek);
96 //$this->ParseFrom($refData);
97 //unset($this->iterator);
98 //unset($this->seek);
99 } else {
100 unset($this->iterator);
101 unset($this->seek);
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 // TODO: add "\r" to preg_replace for contnent
122 $unescaped = preg_replace( '{\\\\[nN]}', "\n", $unescaped);
124 // Split into two parts on : which is not preceded by a \, or within quotes like "str:ing".
125 $offset = 0;
126 do {
127 $splitpos = strpos($unescaped,':',$offset);
128 $start = substr($unescaped,0,$splitpos);
129 if ( substr($start,-1) == '\\' ) {
130 $offset = $splitpos + 1;
131 continue;
133 $quotecount = strlen(preg_replace('{[^"]}', '', $start ));
134 if ( ($quotecount % 2) != 0 ) {
135 $offset = $splitpos + 1;
136 continue;
138 break;
140 while( true );
141 $values = substr($unescaped, $splitpos+1);
143 $possiblecontent = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $values);
144 // in case if the name was set manualy content by function Valued
145 // -> don't reset it by $rendered data
146 if(!isset($this->content)){
147 // TODO: add "\r" to preg_replace at begin
148 $len = strlen($possiblecontent);
149 if($len > 0 && "\r" == $possiblecontent[$len-1]){
151 $possiblecontent = substr($possiblecontent, 0, $len-1);
153 $this->content = $possiblecontent;
157 // Split on ; which is not preceded by a \
158 $parameters = preg_split( '{(?<!\\\\);}', $start);
161 $possiblename = strtoupper(array_shift( $parameters ));
162 // in case if the name was set manualy by function Name
163 // -> don't reset it by $rendered data
164 if(!isset($this->name)){
165 $this->name = $possiblename;
168 // in case if the parameter was set manualy by function Parameters
169 // -> don't reset it by $rendered data
170 if(!isset($this->parameters)){
171 $this->parameters = array();
172 foreach( $parameters AS $k => $v ) {
173 $pos = strpos($v,'=');
174 $name = strtoupper(substr( $v, 0, $pos));
175 $value = substr( $v, $pos + 1);
176 if ( preg_match( '{^"(.*)"$}', $value, $matches) ) {
177 $value = $matches[1];
179 if ( isset($this->parameters[$name]) && is_array($this->parameters[$name]) ) {
180 $this->parameters[$name][] = $value;
182 elseif ( isset($this->parameters[$name]) ) {
183 $this->parameters[$name] = array( $this->parameters[$name], $value);
185 else
186 $this->parameters[$name] = $value;
189 // dbg_error_log('myComponent', " vProperty::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
193 function ParseFromIterator(){
194 $unescaped;
196 if(isset($this->iterator)){
197 $this->iterator->seek($this->seek);
198 $unescaped = $this->iterator->current();
201 } else if(isset($this->line)){
202 $unescaped = $this->line;
203 } else {
204 $unescaped = '';
207 $this->ParseFrom($unescaped);
208 unset($unescaped);
213 * Get/Set name property
215 * @param string $newname [optional] A new name for the property
217 * @return string The name for the property.
219 function Name( $newname = null ) {
220 if ( $newname != null ) {
221 $this->name = strtoupper($newname);
222 if ( $this->isValid() ) $this->invalidate();
223 // dbg_error_log('myComponent', " vProperty::Name(%s)", $this->name );
224 } else if(!isset($this->name)){
225 $this->ParseFromIterator();
227 return $this->name;
232 * Get/Set the content of the property
234 * @param string $newvalue [optional] A new value for the property
236 * @return string The value of the property.
238 function Value( $newvalue = null ) {
239 if ( $newvalue != null ) {
240 $this->content = $newvalue;
241 if ( $this->isValid() ) $this->invalidate();
242 } else if(!isset($this->content)){
243 $this->ParseFromIterator();
245 return $this->content;
250 * Get/Set parameters in their entirety
252 * @param array $newparams An array of new parameter key/value pairs. The 'value' may be an array of values.
254 * @return array The current array of parameters for the property.
256 function Parameters( $newparams = null ) {
257 if ( $newparams != null ) {
258 $this->parameters = array();
259 foreach( $newparams AS $k => $v ) {
260 $this->parameters[strtoupper($k)] = $v;
262 if ( $this->isValid() ) $this->invalidate();
263 } else if(!isset($this->parameters)){
264 $this->ParseFromIterator();
266 return $this->parameters;
271 * Test if our value contains a string
273 * @param string $search The needle which we shall search the haystack for.
275 * @return string The name for the property.
277 function TextMatch( $search, $case_sensitive = true) {
278 if ( isset($this->content) ) {
279 if ($case_sensitive) {
280 return strstr( $this->content, $search );
281 } else {
282 return stristr( $this->content, $search );
285 return false;
289 * Get the value of a parameter
291 * @param string $name The name of the parameter to retrieve the value for
293 * @return string The value of the parameter
295 function GetParameterValue( $name ) {
296 $name = strtoupper($name);
298 if(!isset($this->parameters)){
299 $this->ParseFromIterator();
302 if ( isset($this->parameters[$name]) ){
303 return $this->parameters[$name];
305 return null;
309 * Set the value of a parameter
311 * @param string $name The name of the parameter to set the value for
313 * @param string $value The value of the parameter
315 function SetParameterValue( $name, $value ) {
316 if(!isset($this->parameters)){
317 $this->ParseFromIterator();
320 if ( $this->isValid() ) {
321 $this->invalidate();
323 //tests/regression-suite/0831-Spec-RRULE-1.result
324 //./dav_test --dsn 'davical_milan;port=5432' --webhost 127.0.0.1 --althost altcaldav --suite 'regression-suite' --case 'tests/regression-suite/0831-Spec-RRULE-1'
325 $this->parameters[strtoupper($name)] = $value;
326 // dbg_error_log('PUT', $this->name.$this->RenderParameters().':'.$this->content );
330 private static function escapeParameter($p) {
331 if ( strpos($p, ';') === false && strpos($p, ':') === false ) return $p;
332 return '"'.str_replace('"','\\"',$p).'"';
336 * Render the set of parameters as key1=value1[;key2=value2[; ...]] with
337 * any colons or semicolons escaped.
339 function RenderParameters() {
340 $rendered = "";
341 if(isset($this->parameters)){
342 foreach( $this->parameters AS $k => $v ) {
343 if ( is_array($v) ) {
344 foreach( $v AS $vv ) {
345 $rendered .= sprintf( ';%s=%s', $k, vProperty::escapeParameter($vv) );
348 else {
349 $rendered .= sprintf( ';%s=%s', $k, vProperty::escapeParameter($v) );
354 return $rendered;
359 * Render a suitably escaped RFC2445 content string.
361 function Render( $force = false ) {
362 // If we still have the string it was parsed in from, it hasn't been screwed with
363 // and we can just return that without modification.
364 // if ( $force === false && $this->isValid() && isset($this->rendered) && strlen($this->rendered) < 73 ) {
365 // return $this->rendered;
366 // }
368 // in case one of the memberts doesn't set -> try parse from rendered
369 if(!isset($this->name) || !isset($this->content) || !isset($this->parameters)) {
370 $this->ParseFromIterator();
373 $property = preg_replace( '/[;].*$/', '', $this->name );
374 $escaped = $this->content;
375 switch( $property ) {
376 /** Content escaping does not apply to these properties culled from RFC2445 */
377 case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
378 case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
379 case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
380 case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
381 case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'RDATE':
382 case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART':
383 case 'DTSTAMP': case 'LAST-MODIFIED': case 'CREATED': case 'EXDATE':
384 break;
386 /** Content escaping does not apply to these properties culled from RFC6350 / RFC2426 */
387 case 'ADR': case 'N':
388 // escaping for ';' for these fields also needs to happen to the components they are built from.
389 $escaped = str_replace( '\\', '\\\\', $escaped);
390 $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
391 $escaped = str_replace( ',', '\\,', $escaped);
392 break;
394 /** Content escaping applies by default to other properties */
395 default:
396 $escaped = str_replace( '\\', '\\\\', $escaped);
397 $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
398 $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
401 $rendered = '';
403 $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
404 if ( (strlen($property) + strlen($escaped)) <= 72 ) {
405 $rendered = $property . $escaped;
407 else if ( (strlen($property) <= 72) && (strlen($escaped) <= 72) ) {
408 $rendered = $property . "\r\n " . $escaped;
410 else {
411 $rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property.$escaped );
413 // trace_bug( 'Re-rendered "%s" property.', $this->name );
414 return $rendered;
418 public function __toString() {
419 return $this->Render();
424 * Test a PROP-FILTER or PARAM-FILTER and return a true/false
425 * PROP-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*))
426 * PARAM-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*))
428 * @param array $filter An array of XMLElement defining the filter
430 * @return boolean Whether or not this vProperty passes the test
432 function TestFilter( $filters ) {
433 foreach( $filters AS $k => $v ) {
434 $tag = $v->GetNSTag();
435 // dbg_error_log( 'vCalendar', "vProperty:TestFilter: '%s'='%s' => '%s'", $this->name, $tag, $this->content );
436 switch( $tag ) {
437 case 'urn:ietf:params:xml:ns:caldav:is-defined':
438 case 'urn:ietf:params:xml:ns:carddav:is-defined':
439 if ( empty($this->content) ) return false;
440 break;
442 case 'urn:ietf:params:xml:ns:caldav:is-not-defined':
443 case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
444 if ( ! empty($this->content) ) return false;
445 break;
447 case 'urn:ietf:params:xml:ns:caldav:time-range':
448 /** @todo: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */
449 break;
451 case 'urn:ietf:params:xml:ns:carddav:text-match':
452 case 'urn:ietf:params:xml:ns:caldav:text-match':
453 $search = $v->GetContent();
454 $case_sensitive = true;
455 $collation = $v->GetAttribute("collation");
456 switch( strtolower($collation) ) {
457 case 'i;ascii-casemap':
458 case 'i;unicode-casemap':
459 $case_sensitive = false;
460 break;
461 case 'i;octet':
462 default:
463 $case_sensitive = true;
464 break;
467 $match = $this->TextMatch($search, $case_sensitive);
468 $negate = $v->GetAttribute("negate-condition");
469 if ( isset($negate) && strtolower($negate) == "yes" ) {
470 $match = !$match;
472 if ( ! $match ) return false;
473 break;
475 case 'urn:ietf:params:xml:ns:carddav:param-filter':
476 case 'urn:ietf:params:xml:ns:caldav:param-filter':
477 $subfilter = $v->GetContent();
478 $parameter = $this->GetParameterValue($v->GetAttribute("name"));
479 if ( ! $this->TestParamFilter($subfilter,$parameter) ) return false;
480 break;
482 default:
483 dbg_error_log( 'myComponent', ' vProperty::TestFilter: unhandled tag "%s"', $tag );
484 break;
487 return true;
490 function fill($sp, $en, $pe){
494 function TestParamFilter( $filters, $parameter_value ) {
495 foreach( $filters AS $k => $v ) {
496 $subtag = $v->GetNSTag();
497 // dbg_error_log( 'vCalendar', "vProperty:TestParamFilter: '%s'='%s' => '%s'", $this->name, $subtag, $parameter_value );
498 switch( $subtag ) {
499 case 'urn:ietf:params:xml:ns:caldav:is-defined':
500 case 'urn:ietf:params:xml:ns:carddav:is-defined':
501 if ( empty($parameter_value) ) return false;
502 break;
504 case 'urn:ietf:params:xml:ns:caldav:is-not-defined':
505 case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
506 if ( ! empty($parameter_value) ) return false;
507 break;
509 case 'urn:ietf:params:xml:ns:caldav:time-range':
510 /** @todo: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */
511 break;
513 case 'urn:ietf:params:xml:ns:carddav:text-match':
514 case 'urn:ietf:params:xml:ns:caldav:text-match':
515 $search = $v->GetContent();
516 $match = false;
517 if ( !empty($parameter_value) ) $match = strstr( $this->content, $search );
518 $negate = $v->GetAttribute("negate-condition");
519 if ( isset($negate) && strtolower($negate) == "yes" ) {
520 $match = !$match;
522 if ( ! $match ) return false;
523 break;
525 default:
526 dbg_error_log( 'myComponent', ' vProperty::TestParamFilter: unhandled tag "%s"', $tag );
527 break;
530 return true;