Andrew's Web Libraries (AWL)
vProperty.php
1 <?php
2 
3 require_once('XMLElement.php');
9 class vProperty extends vObject {
19  protected $name;
20 
26  protected $parameters;
27 
33  protected $content;
34 
40  protected $iterator;
41 
46  protected $seek;
47 
48  protected $line;
49 
50  //protected $rendered;
51 
52 
61  function __construct( $name = null, &$master = null, &$refData = null, $seek = null ) {
62  parent::__construct($master);
63 
64 
65  if(isset($name) && strlen($name) > 0){
66  $this->name = $name;
67  } else {
68  unset($this->name);
69  }
70 
71  unset($this->content);
72  unset($this->parameters);
73 
74  if ( isset($refData)){
75 
76  if(gettype($refData) == 'object') {
77  $this->iterator = &$refData;
78  $this->seek = &$seek;
79  unset($this->line);
80  } else {
81  $this->line = $refData;
82 
83  unset($this->iterator);
84  unset($this->seek);
85  }
86  } else {
87  unset($this->iterator);
88  unset($this->seek);
89 
90  }
91  }
92 
102  function ParseFromIterator()
103  {
104  $unescaped;
105 
106  if (isset($this->iterator)) {
107  $this->iterator->seek($this->seek);
108  $unescaped = $this->iterator->current();
109  } else if (isset($this->line)) {
110  $unescaped = $this->line;
111  } else {
112  $unescaped = '';
113  }
114 
115  $this->ParseFrom($unescaped);
116  unset($unescaped);
117  }
118 
119  function ParseFrom( &$unescaped ) {
120  // unescape \r and \n in the value
121  $unescaped = preg_replace( array('{\\\\[nN]}', '{\\\\[rR]}'), array("\n", "\r"), $unescaped);
122 
123  // Split into two parts on : which is not preceded by a \, or within quotes like "str:ing".
124  $offset = 0;
125  do {
126  $splitpos = strpos($unescaped,':',$offset);
127  $start = substr($unescaped,0,$splitpos);
128  if ( substr($start,-1) == '\\' ) {
129  $offset = $splitpos + 1;
130  continue;
131  }
132  $quotecount = strlen(preg_replace('{[^"]}', '', $start ));
133  if ( ($quotecount % 2) != 0 ) {
134  $offset = $splitpos + 1;
135  continue;
136  }
137  break;
138  }
139  while( true );
140  $values = substr($unescaped, $splitpos+1);
141 
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  // TODO: add "\r" to preg_replace at begin
147  $len = strlen($possiblecontent);
148  if($len > 0 && "\r" == $possiblecontent[$len-1]){
149 
150  $possiblecontent = substr($possiblecontent, 0, $len-1);
151  }
152  $this->content = $possiblecontent;
153  }
154 
155 
156  // Split on ; which is not preceded by a \
157  $parameters = preg_split( '{(?<!\\\\‍);}', $start);
158 
159 
160  $possiblename = strtoupper(array_shift( $parameters ));
161  // in case if the name was set manualy by function Name
162  // -> don't reset it by $rendered data
163  if(!isset($this->name)){
164  $this->name = $possiblename;
165  }
166 
167  // in case if the parameter was set manualy by function Parameters
168  // -> don't reset it by $rendered data
169  if(!isset($this->parameters)){
170  $this->parameters = array();
171  foreach( $parameters AS $k => $v ) {
172  $pos = strpos($v,'=');
173  if($pos !== FALSE) {
174  $name = strtoupper(substr( $v, 0, $pos));
175  $value = substr( $v, $pos + 1);
176  }
177  else {
178  $name = strtoupper($v);
179  $value = null;
180  }
181  if ( preg_match( '{^"(.*)"$}', $value, $matches) ) {
182  $value = $matches[1];
183  }
184  if ( isset($this->parameters[$name]) && is_array($this->parameters[$name]) ) {
185  $this->parameters[$name][] = $value;
186  }
187  elseif ( isset($this->parameters[$name]) ) {
188  $this->parameters[$name] = array( $this->parameters[$name], $value);
189  }
190  else
191  $this->parameters[$name] = $value;
192  }
193  }
194 // dbg_error_log('myComponent', " vProperty::ParseFrom found '%s' = '%s' with %d parameters", $this->name, substr($this->content,0,200), count($this->parameters) );
195  }
196 
197 
205  function Name( $newname = null ) {
206  if ( $newname != null ) {
207  $this->name = strtoupper($newname);
208  if ( $this->isValid() ) $this->invalidate();
209 // dbg_error_log('myComponent', " vProperty::Name(%s)", $this->name );
210  } else if(!isset($this->name)){
211  $this->ParseFromIterator();
212  }
213  return $this->name;
214  }
215 
216 
224  function Value( $newvalue = null ) {
225  if ( $newvalue != null ) {
226  $this->content = $newvalue;
227  if ( $this->isValid() ) $this->invalidate();
228  } else if(!isset($this->content)){
229  $this->ParseFromIterator();
230  }
231  return $this->content;
232  }
233 
234 
242  function Parameters( $newparams = null ) {
243  if ( $newparams != null ) {
244  $this->parameters = array();
245  foreach( $newparams AS $k => $v ) {
246  $this->parameters[strtoupper($k)] = $v;
247  }
248  if ( $this->isValid() ) $this->invalidate();
249  } else if(!isset($this->parameters)){
250  $this->ParseFromIterator();
251  }
252  return $this->parameters;
253  }
254 
255 
263  function TextMatch( $search ) {
264  if ( isset($this->content) ) return strstr( $this->content, $search );
265  return false;
266  }
267 
268 
276  function GetParameterValue( $name ) {
277  $name = strtoupper($name);
278 
279  if(!isset($this->parameters)){
280  $this->ParseFromIterator();
281  }
282 
283  if ( isset($this->parameters[$name]) ){
284  return $this->parameters[$name];
285  }
286  return null;
287  }
288 
296  function SetParameterValue( $name, $value ) {
297  if(!isset($this->parameters)){
298  $this->ParseFromIterator();
299  }
300 
301  if ( $this->isValid() ) {
302  $this->invalidate();
303  }
304  //tests/regression-suite/0831-Spec-RRULE-1.result
305  //./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'
306  $this->parameters[strtoupper($name)] = $value;
307 // dbg_error_log('PUT', $this->name.$this->RenderParameters().':'.$this->content );
308  }
309 
315  function ClearParameters( $type = null ) {
316  if(!isset($this->parameters)){
317  $this->ParseFromIterator();
318  }
319 
320  if ( $this->isValid() ) {
321  $this->invalidate();
322  }
323 
324  if ( $type != null ) {
325  $testtypes = (gettype($type) == 'string' ? array( $type => true ) : $type );
326  // First remove all the existing ones of that type
327  foreach( $this->parameters AS $k => $v ) {
328  if ( isset($testtypes[$k]) && $testtypes[$k] ) {
329  unset($this->parameters[$k]);
330  }
331  }
332  }
333  }
334 
335  private static function escapeParameter($p) {
336  if ( strpos($p, ';') === false && strpos($p, ':') === false ) return $p;
337  return '"'.str_replace('"','\\"',$p).'"';
338  }
339 
344  function RenderParameters() {
345  $rendered = "";
346  if(isset($this->parameters)){
347  foreach( $this->parameters AS $k => $v ) {
348  if ( is_array($v) ) {
349  foreach( $v AS $vv ) {
350  $rendered .= sprintf( ';%s=%s', $k, vProperty::escapeParameter($vv) );
351  }
352  }
353  else {
354  if($v !== null) {
355  $rendered .= sprintf( ';%s=%s', $k, vProperty::escapeParameter($v) );
356  }
357  else {
358  $rendered .= sprintf( ';%s', $k);
359  }
360  }
361  }
362  }
363 
364  return $rendered;
365  }
366 
367 
371  function Render( $force = false ) {
372  // If we still have the string it was parsed in from, it hasn't been screwed with
373  // and we can just return that without modification.
374 // if ( $force === false && $this->isValid() && isset($this->rendered) && strlen($this->rendered) < 73 ) {
375 // return $this->rendered;
376 // }
377 
378  // in case one of the memberts doesn't set -> try parse from rendered
379  if(!isset($this->name) || !isset($this->content) || !isset($this->parameters)) {
380  $this->ParseFromIterator();
381  }
382 
383  $property = preg_replace( '/[;].*$/', '', $this->name );
384  $escaped = $this->content;
385  $property = preg_replace( '/^.*[.]/', '', $property ); //temporarily remove grouping prefix from CARDDAV attributes ("item1.", "item2.", etc)
386  switch( $property ) {
388  case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY':
389  case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO':
390  case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID':
391  case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED':
392  case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'RDATE':
393  case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART':
394  case 'DTSTAMP': case 'LAST-MODIFIED': case 'CREATED': case 'EXDATE':
395  case 'CATEGORIES' :
396  break;
397 
399  case 'ADR': case 'N': case 'ORG':
400  // escaping for ';' for these fields also needs to happen to the components they are built from.
401  $escaped = str_replace( '\\', '\\\\', $escaped);
402  $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
403  $escaped = str_replace( ',', '\\,', $escaped);
404  break;
406  default:
407  $escaped = str_replace( '\\', '\\\\', $escaped);
408  $escaped = preg_replace( '/\r?\n/', '\\n', $escaped);
409  $escaped = preg_replace( "/([,;])/", '\\\\$1', $escaped);
410  }
411 
412  $rendered = '';
413  $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() );
414  if ( (strlen($property) + strlen($escaped)) <= 72 ) {
415  $rendered = $property . $escaped;
416  }
417  else if ( (strlen($property) <= 72) && (strlen($escaped) <= 72) ) {
418  $rendered = $property . "\r\n " . $escaped;
419  }
420  else {
421  $rendered = preg_replace( '/(.{72})/u', '$1'."\r\n ", $property.$escaped );
422  }
423 // trace_bug( 'Re-rendered "%s" property.', $this->name );
424  return $rendered;
425  }
426 
427 
428  public function __toString() {
429  return $this->Render();
430  }
431 
432 
443  function TestFilter( $filters, $matchAll = true ) {
444  foreach( $filters AS $k => $v ) {
445  $tag = $v->GetNSTag();
446 // dbg_error_log( 'vCalendar', "vProperty:TestFilter: '%s'='%s' => '%s'", $this->name, $tag, $this->content );
447  switch( $tag ) {
448  case 'urn:ietf:params:xml:ns:caldav:is-defined':
449  case 'urn:ietf:params:xml:ns:carddav:is-defined':
450  if ( empty($this->content) ) return false;
451  break;
452 
453  case 'urn:ietf:params:xml:ns:caldav:is-not-defined':
454  case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
455  if ( ! empty($this->content) ) return false;
456  break;
457 
458  case 'urn:ietf:params:xml:ns:caldav:time-range':
460  break;
461 
462  case 'urn:ietf:params:xml:ns:carddav:text-match':
463  case 'urn:ietf:params:xml:ns:caldav:text-match':
464  // Call the Value() getter method to get hold of the vProperty content - need to ensure parsing has occurred
465  $haystack = $this->Value();
466  $match = $this->TestTextMatch($v, $haystack);
467  // Early exit: OR match can stop if we have on success, AND match can stop if we have a fail
468  if ( $match != $matchAll ) return $match;
469  break;
470 
471  case 'urn:ietf:params:xml:ns:carddav:param-filter':
472  case 'urn:ietf:params:xml:ns:caldav:param-filter':
473  $subfilter = $v->GetContent();
474  $parameter = $this->GetParameterValue($v->GetAttribute("name"));
475 
476  if (empty($subfilter)) {
477  // param-filter without subfilter matches if the parameter is defined
478  $match = isset($parameter);
479  } else {
480  if (!isset($parameter)) {
481  $parameter = [ null ];
482  } elseif (!is_array($parameter)) {
483  $parameter = explode(',', $parameter);
484  }
485 
486  $match = false;
487  foreach ($parameter as $param) {
488  $match = $this->TestParamFilter($subfilter,$param);
489  if ($match) break;
490  }
491  }
492 
493  // Early exit: OR match can stop if we have on success, AND match can stop if we have a fail
494  if ( $match != $matchAll ) return $match;
495  break;
496 
497  default:
498  dbg_error_log( 'myComponent', ' vProperty::TestFilter: unhandled tag "%s"', $tag );
499  break;
500  }
501  }
502 
503  // AND-case: all matched; OR-case: all failed
504  return $matchAll;
505  }
506 
507  function fill($sp, $en, $pe){
508 
509  }
510 
511  function TestTextMatch( $filter, $haystack ) {
512  // if the property / parameter is not defined, the prop-filter/param-filter fails
513  if (!isset($haystack)) {
514  return false;
515  }
516 
517  $search = $filter->GetContent();
518  $collation = $filter->GetAttribute("collation");
519  switch( strtolower($collation) ) {
520  case 'i;octet':
521  // don't change search and haystack
522  break;
523  case 'i;ascii-casemap':
524  case 'i;unicode-casemap':
525  default:
526  // for ignore case search we transform
527  // search and haystack to lowercase
528  $search = strtolower( $search );
529  $haystack = strtolower( $haystack );
530  break;
531  }
532 
533  $matchType = $filter->GetAttribute("match-type");
534  switch( strtolower($matchType) ) {
535  case 'equals':
536  $match = ( $haystack === $search );
537  break;
538  case 'starts-with':
539  $length = strlen($search);
540  if ($length == 0) {
541  $match = true;
542  } else {
543  $match = !strncmp($haystack, $search, $length);
544  }
545  break;
546  case 'ends-with':
547  $length = strlen($search);
548  if ($length == 0) {
549  $match = true;
550  } else {
551  $match = ( substr($haystack, -$length) === $search );
552  }
553  break;
554  default: // contains
555  $length = strlen($search);
556  if ($length == 0) {
557  $match = true;
558  } else {
559  $match = strstr( $haystack, $search );
560  }
561  break;
562  }
563 
564  $negate = $filter->GetAttribute("negate-condition");
565  if ( isset($negate) && strtolower($negate) == "yes" ) {
566  $match = !$match;
567  }
568 
569  return $match;
570  }
571 
572  function TestParamFilter( $filters, $parameter_value ) {
573  foreach( $filters AS $k => $v ) {
574  $subtag = $v->GetNSTag();
575 // dbg_error_log( 'vCalendar', "vProperty:TestParamFilter: '%s'='%s' => '%s'", $this->name, $subtag, $parameter_value );
576  switch( $subtag ) {
577  case 'urn:ietf:params:xml:ns:caldav:is-not-defined':
578  case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
579  if ( ! empty($parameter_value) ) return false;
580  break;
581 
582  case 'urn:ietf:params:xml:ns:caldav:time-range':
584  break;
585 
586  case 'urn:ietf:params:xml:ns:carddav:text-match':
587  case 'urn:ietf:params:xml:ns:caldav:text-match':
588  $match = $this->TestTextMatch($v, $parameter_value);
589  if ( ! $match ) return false;
590  break;
591 
592  default:
593  dbg_error_log( 'myComponent', ' vProperty::TestParamFilter: unhandled tag "%s"', $tag );
594  break;
595  }
596  }
597  return true;
598  }
599 }
Render( $force=false)
Definition: vProperty.php:371
ClearParameters( $type=null)
Definition: vProperty.php:315
__construct( $name=null, &$master=null, &$refData=null, $seek=null)
Definition: vProperty.php:61
Name( $newname=null)
Definition: vProperty.php:205
TextMatch( $search)
Definition: vProperty.php:263
TestParamFilter( $filters, $parameter_value)
Definition: vProperty.php:572
Value( $newvalue=null)
Definition: vProperty.php:224
SetParameterValue( $name, $value)
Definition: vProperty.php:296
RenderParameters()
Definition: vProperty.php:344
ParseFromIterator()
Definition: vProperty.php:102
GetParameterValue( $name)
Definition: vProperty.php:276
TestFilter( $filters, $matchAll=true)
Definition: vProperty.php:443
Parameters( $newparams=null)
Definition: vProperty.php:242