]>
Commit | Line | Data |
---|---|---|
a5eae6b7 MR |
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 | ||
51 | /** | |
52 | * A Class for representing properties within an iCalendar | |
53 | * | |
54 | * @package awl | |
55 | */ | |
56 | class iCalProp { | |
57 | /**#@+ | |
58 | * @access private | |
59 | */ | |
60 | ||
61 | /** | |
62 | * The name of this property | |
63 | * | |
64 | * @var string | |
65 | */ | |
66 | var $name; | |
67 | ||
68 | /** | |
69 | * An array of parameters to this property, represented as key/value pairs. | |
70 | * | |
71 | * @var array | |
72 | */ | |
73 | var $parameters; | |
74 | ||
75 | /** | |
76 | * The value of this property. | |
77 | * | |
78 | * @var string | |
79 | */ | |
80 | var $content; | |
81 | ||
82 | /** | |
83 | * The original value that this was parsed from, if that's the way it happened. | |
84 | * | |
85 | * @var string | |
86 | */ | |
87 | var $rendered; | |
88 | ||
89 | /**#@-*/ | |
90 | ||
91 | /** | |
92 | * The constructor parses the incoming string, which is formatted as per RFC2445 as a | |
93 | * propname[;param1=pval1[; ... ]]:propvalue | |
94 | * however we allow ourselves to assume that the RFC2445 content unescaping has already | |
95 | * happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent(). | |
96 | * | |
97 | * @param string $propstring The string from the iCalendar which contains this property. | |
98 | */ | |
99 | function iCalProp( $propstring = null ) { | |
100 | $this->name = ""; | |
101 | $this->content = ""; | |
102 | $this->parameters = array(); | |
103 | unset($this->rendered); | |
104 | if ( $propstring != null && gettype($propstring) == 'string' ) { | |
105 | $this->ParseFrom($propstring); | |
106 | } | |
107 | } | |
108 | ||
109 | ||
110 | /** | |
111 | * The constructor parses the incoming string, which is formatted as per RFC2445 as a | |
112 | * propname[;param1=pval1[; ... ]]:propvalue | |
113 | * however we allow ourselves to assume that the RFC2445 content unescaping has already | |
114 | * happened when iCalComponent::ParseFrom() called iCalComponent::UnwrapComponent(). | |
115 | * | |
116 | * @param string $propstring The string from the iCalendar which contains this property. | |
117 | */ | |
118 | function ParseFrom( $propstring ) { | |
119 | $this->rendered = (strlen($propstring) < 72 ? $propstring : null); // Only pre-rendered if we didn't unescape it | |
120 | $pos = strpos( $propstring, ':'); | |
121 | $start = substr( $propstring, 0, $pos); | |
122 | ||
123 | $unescaped = str_replace( '\\n', "\n", substr( $propstring, $pos + 1)); | |
124 | $unescaped = str_replace( '\\N', "\n", $unescaped); | |
125 | $this->content = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $unescaped); | |
126 | ||
127 | $parameters = explode(';',$start); | |
128 | $this->name = array_shift( $parameters ); | |
129 | $this->parameters = array(); | |
130 | foreach( $parameters AS $k => $v ) { | |
131 | $pos = strpos($v,'='); | |
132 | $name = substr( $v, 0, $pos); | |
133 | $value = substr( $v, $pos + 1); | |
134 | $this->parameters[$name] = $value; | |
135 | } | |
136 | dbg_error_log("iCalendar", " iCalProp::ParseFrom found '%s' = '%s' with %d parameters", $this->name, $this->content, count($this->parameters) ); | |
137 | } | |
138 | ||
139 | ||
140 | /** | |
141 | * Get/Set name property | |
142 | * | |
143 | * @param string $newname [optional] A new name for the property | |
144 | * | |
145 | * @return string The name for the property. | |
146 | */ | |
147 | function Name( $newname = null ) { | |
148 | if ( $newname != null ) { | |
149 | $this->name = $newname; | |
150 | if ( isset($this->rendered) ) unset($this->rendered); | |
151 | dbg_error_log("iCalendar", " iCalProp::Name(%s)", $this->name ); | |
152 | } | |
153 | return $this->name; | |
154 | } | |
155 | ||
156 | ||
157 | /** | |
158 | * Get/Set the content of the property | |
159 | * | |
160 | * @param string $newvalue [optional] A new value for the property | |
161 | * | |
162 | * @return string The value of the property. | |
163 | */ | |
164 | function Value( $newvalue = null ) { | |
165 | if ( $newvalue != null ) { | |
166 | $this->content = $newvalue; | |
167 | if ( isset($this->rendered) ) unset($this->rendered); | |
168 | } | |
169 | return $this->content; | |
170 | } | |
171 | ||
172 | ||
173 | /** | |
174 | * Get/Set parameters in their entirety | |
175 | * | |
176 | * @param array $newparams An array of new parameter key/value pairs | |
177 | * | |
178 | * @return array The current array of parameters for the property. | |
179 | */ | |
180 | function Parameters( $newparams = null ) { | |
181 | if ( $newparams != null ) { | |
182 | $this->parameters = $newparams; | |
183 | if ( isset($this->rendered) ) unset($this->rendered); | |
184 | } | |
185 | return $this->parameters; | |
186 | } | |
187 | ||
188 | ||
189 | /** | |
190 | * Test if our value contains a string | |
191 | * | |
192 | * @param string $search The needle which we shall search the haystack for. | |
193 | * | |
194 | * @return string The name for the property. | |
195 | */ | |
196 | function TextMatch( $search ) { | |
197 | if ( isset($this->content) ) return strstr( $this->content, $search ); | |
198 | return false; | |
199 | } | |
200 | ||
201 | ||
202 | /** | |
203 | * Get the value of a parameter | |
204 | * | |
205 | * @param string $name The name of the parameter to retrieve the value for | |
206 | * | |
207 | * @return string The value of the parameter | |
208 | */ | |
209 | function GetParameterValue( $name ) { | |
210 | if ( isset($this->parameters[$name]) ) return $this->parameters[$name]; | |
211 | } | |
212 | ||
213 | /** | |
214 | * Set the value of a parameter | |
215 | * | |
216 | * @param string $name The name of the parameter to set the value for | |
217 | * | |
218 | * @param string $value The value of the parameter | |
219 | */ | |
220 | function SetParameterValue( $name, $value ) { | |
221 | if ( isset($this->rendered) ) unset($this->rendered); | |
222 | $this->parameters[$name] = $value; | |
223 | } | |
224 | ||
225 | /** | |
226 | * Render the set of parameters as key1=value1[;key2=value2[; ...]] with | |
227 | * any colons or semicolons escaped. | |
228 | */ | |
229 | function RenderParameters() { | |
230 | $rendered = ""; | |
231 | foreach( $this->parameters AS $k => $v ) { | |
232 | $escaped = preg_replace( "/([;:\"])/", '\\\\$1', $v); | |
233 | $rendered .= sprintf( ";%s=%s", $k, $escaped ); | |
234 | } | |
235 | return $rendered; | |
236 | } | |
237 | ||
238 | ||
239 | /** | |
240 | * Render a suitably escaped RFC2445 content string. | |
241 | */ | |
242 | function Render() { | |
243 | // If we still have the string it was parsed in from, it hasn't been screwed with | |
244 | // and we can just return that without modification. | |
245 | if ( isset($this->rendered) ) return $this->rendered; | |
246 | ||
247 | $property = preg_replace( '/[;].*$/', '', $this->name ); | |
248 | $escaped = $this->content; | |
249 | switch( $property ) { | |
250 | /** Content escaping does not apply to these properties culled from RFC2445 */ | |
251 | case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY': | |
252 | case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO': | |
253 | case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID': | |
254 | case 'URL': case 'EXRULE': case 'SEQUENCE': case 'CREATED': | |
255 | case 'RRULE': case 'REPEAT': case 'TRIGGER': | |
256 | break; | |
257 | ||
258 | case 'COMPLETED': case 'DTEND': | |
259 | case 'DUE': case 'DTSTART': | |
260 | case 'DTSTAMP': case 'LAST-MODIFIED': | |
261 | case 'CREATED': case 'EXDATE': | |
262 | case 'RDATE': | |
263 | if ( isset($this->parameters['VALUE']) && $this->parameters['VALUE'] == 'DATE' ) { | |
264 | $escaped = substr( $escaped, 0, 8); | |
265 | } | |
266 | break; | |
267 | ||
268 | /** Content escaping applies by default to other properties */ | |
269 | default: | |
270 | $escaped = str_replace( '\\', '\\\\', $escaped); | |
271 | $escaped = preg_replace( '/\r?\n/', '\\n', $escaped); | |
272 | $escaped = preg_replace( "/([,;\"])/", '\\\\$1', $escaped); | |
273 | } | |
274 | $property = sprintf( "%s%s:", $this->name, $this->RenderParameters() ); | |
275 | if ( (strlen($property) + strlen($escaped)) <= 72 ) { | |
276 | $this->rendered = $property . $escaped; | |
277 | } | |
278 | else if ( (strlen($property) + strlen($escaped)) > 72 && (strlen($property) < 72) && (strlen($escaped) < 72) ) { | |
279 | $this->rendered = $property . " \r\n " . $escaped; | |
280 | } | |
281 | else { | |
282 | $this->rendered = wordwrap( $property . $escaped, 72, " \r\n ", true ); | |
283 | } | |
284 | return $this->rendered; | |
285 | } | |
286 | ||
287 | } | |
288 | ||
289 | ||
290 | /** | |
291 | * A Class for representing components within an iCalendar | |
292 | * | |
293 | * @package awl | |
294 | */ | |
295 | class iCalComponent { | |
296 | /**#@+ | |
297 | * @access private | |
298 | */ | |
299 | ||
300 | /** | |
301 | * The type of this component, such as 'VEVENT', 'VTODO', 'VTIMEZONE', etc. | |
302 | * | |
303 | * @var string | |
304 | */ | |
305 | var $type; | |
306 | ||
307 | /** | |
308 | * An array of properties, which are iCalProp objects | |
309 | * | |
310 | * @var array | |
311 | */ | |
312 | var $properties; | |
313 | ||
314 | /** | |
315 | * An array of (sub-)components, which are iCalComponent objects | |
316 | * | |
317 | * @var array | |
318 | */ | |
319 | var $components; | |
320 | ||
321 | /** | |
322 | * The rendered result (or what was originally parsed, if there have been no changes) | |
323 | * | |
324 | * @var array | |
325 | */ | |
326 | var $rendered; | |
327 | ||
328 | /**#@-*/ | |
329 | ||
330 | /** | |
331 | * A basic constructor | |
332 | */ | |
333 | function iCalComponent( $content = null ) { | |
334 | $this->type = ""; | |
335 | $this->properties = array(); | |
336 | $this->components = array(); | |
337 | $this->rendered = ""; | |
338 | if ( $content != null && gettype($content) == 'string' ) { | |
339 | $this->ParseFrom($content); | |
340 | } | |
341 | } | |
342 | ||
343 | ||
344 | /** | |
345 | * Apply standard properties for a VCalendar | |
346 | * @param array $extra_properties Key/value pairs of additional properties | |
347 | */ | |
348 | function VCalendar( $extra_properties = null ) { | |
349 | $this->SetType('VCALENDAR'); | |
350 | $this->AddProperty('PRODID', '-//davical.org//NONSGML AWL Calendar//EN'); | |
351 | $this->AddProperty('VERSION', '2.0'); | |
352 | $this->AddProperty('CALSCALE', 'GREGORIAN'); | |
353 | if ( is_array($extra_properties) ) { | |
354 | foreach( $extra_properties AS $k => $v ) { | |
355 | $this->AddProperty($k,$v); | |
356 | } | |
357 | } | |
358 | } | |
359 | ||
360 | /** | |
361 | * Collect an array of all parameters of our properties which are the specified type | |
362 | * Mainly used for collecting the full variety of references TZIDs | |
363 | */ | |
364 | function CollectParameterValues( $parameter_name ) { | |
365 | $values = array(); | |
366 | foreach( $this->components AS $k => $v ) { | |
367 | $also = $v->CollectParameterValues($parameter_name); | |
368 | $values = array_merge( $values, $also ); | |
369 | } | |
370 | foreach( $this->properties AS $k => $v ) { | |
371 | $also = $v->GetParameterValue($parameter_name); | |
372 | if ( isset($also) && $also != "" ) { | |
373 | dbg_error_log( "iCalendar", "::CollectParameterValues(%s) : Found '%s'", $parameter_name, $also); | |
374 | $values[$also] = 1; | |
375 | } | |
376 | } | |
377 | return $values; | |
378 | } | |
379 | ||
380 | ||
381 | /** | |
382 | * Parse the text $content into sets of iCalProp & iCalComponent within this iCalComponent | |
383 | * @param string $content The raw RFC2445-compliant iCalendar component, including BEGIN:TYPE & END:TYPE | |
384 | */ | |
385 | function ParseFrom( $content ) { | |
386 | $this->rendered = $content; | |
387 | $content = $this->UnwrapComponent($content); | |
388 | ||
389 | $lines = preg_split('/\r?\n/', $content ); | |
390 | ||
391 | $type = false; | |
392 | $subtype = false; | |
393 | $finish = null; | |
394 | $subfinish = null; | |
395 | foreach( $lines AS $k => $v ) { | |
396 | if ( preg_match('/^\s*$/', $v ) ) continue; | |
397 | dbg_error_log( "iCalendar", "::ParseFrom: Parsing line: $v"); | |
398 | if ( $type === false ) { | |
399 | if ( preg_match( '/^BEGIN:(.+)$/', $v, $matches ) ) { | |
400 | // We have found the start of the main component | |
401 | $type = $matches[1]; | |
402 | $finish = "END:$type"; | |
403 | $this->type = $type; | |
404 | dbg_error_log( "iCalendar", "::ParseFrom: Start component of type '%s'", $type); | |
405 | } | |
406 | else { | |
407 | dbg_error_log( "iCalendar", "::ParseFrom: Ignoring crap before start of component"); | |
408 | unset($lines[$k]); // The content has crap before the start | |
409 | if ( $v != "" ) $this->rendered = null; | |
410 | } | |
411 | } | |
412 | else if ( $type == null ) { | |
413 | dbg_error_log( "iCalendar", "::ParseFrom: Ignoring crap after end of component"); | |
414 | unset($lines[$k]); // The content has crap after the end | |
415 | if ( $v != "" ) $this->rendered = null; | |
416 | } | |
417 | else if ( $v == $finish ) { | |
418 | dbg_error_log( "iCalendar", "::ParseFrom: End of component"); | |
419 | $type = null; // We have reached the end of our component | |
420 | } | |
421 | else { | |
422 | if ( $subtype === false && preg_match( '/^BEGIN:(.+)$/', $v, $matches ) ) { | |
423 | // We have found the start of a sub-component | |
424 | $subtype = $matches[1]; | |
425 | $subfinish = "END:$subtype"; | |
426 | $subcomponent = "$v\r\n"; | |
427 | dbg_error_log( "iCalendar", "::ParseFrom: Found a subcomponent '%s'", $subtype); | |
428 | } | |
429 | else if ( $subtype ) { | |
430 | // We are inside a sub-component | |
431 | $subcomponent .= $this->WrapComponent($v); | |
432 | if ( $v == $subfinish ) { | |
433 | dbg_error_log( "iCalendar", "::ParseFrom: End of subcomponent '%s'", $subtype); | |
434 | // We have found the end of a sub-component | |
435 | $this->components[] = new iCalComponent($subcomponent); | |
436 | $subtype = false; | |
437 | } | |
438 | else | |
439 | dbg_error_log( "iCalendar", "::ParseFrom: Inside a subcomponent '%s'", $subtype ); | |
440 | } | |
441 | else { | |
442 | dbg_error_log( "iCalendar", "::ParseFrom: Parse property of component"); | |
443 | // It must be a normal property line within a component. | |
444 | $this->properties[] = new iCalProp($v); | |
445 | } | |
446 | } | |
447 | } | |
448 | } | |
449 | ||
450 | ||
451 | /** | |
452 | * This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According | |
453 | * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising | |
454 | * XML parsers often muck with it and may remove the CR. We accept either case. | |
455 | */ | |
456 | function UnwrapComponent( $content ) { | |
457 | return preg_replace('/\r?\n[ \t]/', '', $content ); | |
458 | } | |
459 | ||
460 | /** | |
461 | * This imposes the (CRLF + linear space) wrapping specified in RFC2445. According | |
462 | * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising | |
463 | * XML parsers often muck with it and may remove the CR. We output RFC2445 compliance. | |
464 | * | |
465 | * In order to preserve pre-existing wrapping in the component, we split the incoming | |
466 | * string on line breaks before running wordwrap over each component of that. | |
467 | */ | |
468 | function WrapComponent( $content ) { | |
469 | $strs = preg_split( "/\r?\n/", $content ); | |
470 | $wrapped = ""; | |
471 | foreach ($strs as $str) { | |
472 | $wrapped .= wordwrap($str, 73, " \r\n ") . "\r\n"; | |
473 | } | |
474 | return $wrapped; | |
475 | } | |
476 | ||
477 | /** | |
478 | * Return the type of component which this is | |
479 | */ | |
480 | function GetType() { | |
481 | return $this->type; | |
482 | } | |
483 | ||
484 | ||
485 | /** | |
486 | * Set the type of component which this is | |
487 | */ | |
488 | function SetType( $type ) { | |
489 | if ( isset($this->rendered) ) unset($this->rendered); | |
490 | $this->type = $type; | |
491 | return $this->type; | |
492 | } | |
493 | ||
494 | ||
495 | /** | |
496 | * Get all properties, or the properties matching a particular type | |
497 | */ | |
498 | function GetProperties( $type = null ) { | |
499 | $properties = array(); | |
500 | foreach( $this->properties AS $k => $v ) { | |
501 | if ( $type == null || $v->Name() == $type ) { | |
502 | $properties[$k] = $v; | |
503 | } | |
504 | } | |
505 | return $properties; | |
506 | } | |
507 | ||
508 | ||
509 | /** | |
510 | * Get the value of the first property matching the name. Obviously this isn't | |
511 | * so useful for properties which may occur multiply, but most don't. | |
512 | * | |
513 | * @param string $type The type of property we are after. | |
514 | * @return string The value of the property, or null if there was no such property. | |
515 | */ | |
516 | function GetPValue( $type ) { | |
517 | foreach( $this->properties AS $k => $v ) { | |
518 | if ( $v->Name() == $type ) return $v->Value(); | |
519 | } | |
520 | return null; | |
521 | } | |
522 | ||
523 | ||
524 | /** | |
525 | * Get the value of the specified parameter for the first property matching the | |
526 | * name. Obviously this isn't so useful for properties which may occur multiply, but most don't. | |
527 | * | |
528 | * @param string $type The type of property we are after. | |
529 | * @param string $type The name of the parameter we are after. | |
530 | * @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. | |
531 | */ | |
532 | function GetPParamValue( $type, $parameter_name ) { | |
533 | foreach( $this->properties AS $k => $v ) { | |
534 | if ( $v->Name() == $type ) return $v->GetParameterValue($parameter_name); | |
535 | } | |
536 | return null; | |
537 | } | |
538 | ||
539 | ||
540 | /** | |
541 | * Clear all properties, or the properties matching a particular type | |
542 | * @param string $type The type of property - omit for all properties | |
543 | */ | |
544 | function ClearProperties( $type = null ) { | |
545 | if ( $type != null ) { | |
546 | // First remove all the existing ones of that type | |
547 | foreach( $this->properties AS $k => $v ) { | |
548 | if ( $v->Name() == $type ) { | |
549 | unset($this->properties[$k]); | |
550 | if ( isset($this->rendered) ) unset($this->rendered); | |
551 | } | |
552 | } | |
553 | $this->properties = array_values($this->properties); | |
554 | } | |
555 | else { | |
556 | if ( isset($this->rendered) ) unset($this->rendered); | |
557 | $this->properties = array(); | |
558 | } | |
559 | } | |
560 | ||
561 | ||
562 | /** | |
563 | * Set all properties, or the ones matching a particular type | |
564 | */ | |
565 | function SetProperties( $new_properties, $type = null ) { | |
566 | if ( isset($this->rendered) && count($new_properties) > 0 ) unset($this->rendered); | |
567 | $this->ClearProperties($type); | |
568 | foreach( $new_properties AS $k => $v ) { | |
569 | $this->AddProperty($v); | |
570 | } | |
571 | } | |
572 | ||
573 | ||
574 | /** | |
575 | * Adds a new property | |
576 | * | |
577 | * @param iCalProp $new_property The new property to append to the set, or a string with the name | |
578 | * @param string $value The value of the new property (default: param 1 is an iCalProp with everything | |
579 | * @param array $parameters The key/value parameter pairs (default: none, or param 1 is an iCalProp with everything) | |
580 | */ | |
581 | function AddProperty( $new_property, $value = null, $parameters = null ) { | |
582 | if ( isset($this->rendered) ) unset($this->rendered); | |
583 | if ( isset($value) && gettype($new_property) == 'string' ) { | |
584 | $new_prop = new iCalProp(); | |
585 | $new_prop->Name($new_property); | |
586 | $new_prop->Value($value); | |
587 | if ( $parameters != null ) $new_prop->Parameters($parameters); | |
588 | dbg_error_log("iCalendar"," Adding new property '%s'", $new_prop->Render() ); | |
589 | $this->properties[] = $new_prop; | |
590 | } | |
591 | else if ( gettype($new_property) ) { | |
592 | $this->properties[] = $new_property; | |
593 | } | |
594 | } | |
595 | ||
596 | ||
597 | /** | |
598 | * Get all sub-components, or at least get those matching a type | |
599 | * @return array an array of the sub-components | |
600 | */ | |
601 | function &FirstNonTimezone( $type = null ) { | |
602 | foreach( $this->components AS $k => $v ) { | |
603 | if ( $v->GetType() != 'VTIMEZONE' ) return $this->components[$k]; | |
604 | } | |
605 | $result = false; | |
606 | return $result; | |
607 | } | |
608 | ||
609 | ||
610 | /** | |
611 | * Return true if the person identified by the email address is down as an | |
612 | * organizer for this meeting. | |
613 | * @param string $email The e-mail address of the person we're seeking. | |
614 | * @return boolean true if we found 'em, false if we didn't. | |
615 | */ | |
616 | function IsOrganizer( $email ) { | |
617 | if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:$email'; | |
618 | $props = $this->GetPropertiesByPath('!VTIMEZONE/ORGANIZER'); | |
619 | foreach( $props AS $k => $prop ) { | |
620 | if ( $prop->Value() == $email ) return true; | |
621 | } | |
622 | return false; | |
623 | } | |
624 | ||
625 | ||
626 | /** | |
627 | * Return true if the person identified by the email address is down as an | |
628 | * attendee or organizer for this meeting. | |
629 | * @param string $email The e-mail address of the person we're seeking. | |
630 | * @return boolean true if we found 'em, false if we didn't. | |
631 | */ | |
632 | function IsAttendee( $email ) { | |
633 | if ( !preg_match( '#^mailto:#', $email ) ) $email = 'mailto:$email'; | |
634 | if ( $this->IsOrganizer($email) ) return true; /** an organizer is an attendee, as far as we're concerned */ | |
635 | $props = $this->GetPropertiesByPath('!VTIMEZONE/ATTENDEE'); | |
636 | foreach( $props AS $k => $prop ) { | |
637 | if ( $prop->Value() == $email ) return true; | |
638 | } | |
639 | return false; | |
640 | } | |
641 | ||
642 | ||
643 | /** | |
644 | * Get all sub-components, or at least get those matching a type, or failling to match, | |
645 | * should the second parameter be set to false. | |
646 | * | |
647 | * @param string $type The type to match (default: All) | |
648 | * @param boolean $normal_match Set to false to invert the match (default: true) | |
649 | * @return array an array of the sub-components | |
650 | */ | |
651 | function GetComponents( $type = null, $normal_match = true ) { | |
652 | $components = $this->components; | |
653 | if ( $type != null ) { | |
654 | foreach( $components AS $k => $v ) { | |
655 | if ( ($v->GetType() != $type) === $normal_match ) { | |
656 | unset($components[$k]); | |
657 | } | |
658 | } | |
659 | $components = array_values($components); | |
660 | } | |
661 | return $components; | |
662 | } | |
663 | ||
664 | ||
665 | /** | |
666 | * Clear all components, or the components matching a particular type | |
667 | * @param string $type The type of component - omit for all components | |
668 | */ | |
669 | function ClearComponents( $type = null ) { | |
670 | if ( $type != null ) { | |
671 | // First remove all the existing ones of that type | |
672 | foreach( $this->components AS $k => $v ) { | |
673 | if ( $v->GetType() == $type ) { | |
674 | unset($this->components[$k]); | |
675 | if ( isset($this->rendered) ) unset($this->rendered); | |
676 | } | |
677 | else { | |
678 | if ( ! $this->components[$k]->ClearComponents($type) ) { | |
679 | if ( isset($this->rendered) ) unset($this->rendered); | |
680 | } | |
681 | } | |
682 | } | |
683 | return isset($this->rendered); | |
684 | } | |
685 | else { | |
686 | if ( isset($this->rendered) ) unset($this->rendered); | |
687 | $this->components = array(); | |
688 | } | |
689 | } | |
690 | ||
691 | ||
692 | /** | |
693 | * Sets some or all sub-components of the component to the supplied new components | |
694 | * | |
695 | * @param array of iCalComponent $new_components The new components to replace the existing ones | |
696 | * @param string $type The type of components to be replaced. Defaults to null, which means all components will be replaced. | |
697 | */ | |
698 | function SetComponents( $new_component, $type = null ) { | |
699 | if ( isset($this->rendered) ) unset($this->rendered); | |
700 | if ( count($new_component) > 0 ) $this->ClearComponents($type); | |
701 | foreach( $new_component AS $k => $v ) { | |
702 | $this->components[] = $v; | |
703 | } | |
704 | } | |
705 | ||
706 | ||
707 | /** | |
708 | * Adds a new subcomponent | |
709 | * | |
710 | * @param iCalComponent $new_component The new component to append to the set | |
711 | */ | |
712 | function AddComponent( $new_component ) { | |
713 | if ( is_array($new_component) && count($new_component) == 0 ) return; | |
714 | if ( isset($this->rendered) ) unset($this->rendered); | |
715 | if ( is_array($new_component) ) { | |
716 | foreach( $new_component AS $k => $v ) { | |
717 | $this->components[] = $v; | |
718 | } | |
719 | } | |
720 | else { | |
721 | $this->components[] = $new_component; | |
722 | } | |
723 | } | |
724 | ||
725 | ||
726 | /** | |
727 | * Mask components, removing any that are not of the types in the list | |
728 | * @param array $keep An array of component types to be kept | |
729 | */ | |
730 | function MaskComponents( $keep ) { | |
731 | foreach( $this->components AS $k => $v ) { | |
732 | if ( ! in_array( $v->GetType(), $keep ) ) { | |
733 | unset($this->components[$k]); | |
734 | if ( isset($this->rendered) ) unset($this->rendered); | |
735 | } | |
736 | else { | |
737 | $v->MaskComponents($keep); | |
738 | } | |
739 | } | |
740 | } | |
741 | ||
742 | ||
743 | /** | |
744 | * Mask properties, removing any that are not in the list | |
745 | * @param array $keep An array of property names to be kept | |
746 | * @param array $component_list An array of component types to check within | |
747 | */ | |
748 | function MaskProperties( $keep, $component_list=null ) { | |
749 | foreach( $this->components AS $k => $v ) { | |
750 | $v->MaskProperties($keep, $component_list); | |
751 | } | |
752 | ||
753 | if ( !isset($component_list) || in_array($this->GetType(),$component_list) ) { | |
754 | foreach( $this->components AS $k => $v ) { | |
755 | if ( ! in_array( $v->GetType(), $keep ) ) { | |
756 | unset($this->components[$k]); | |
757 | if ( isset($this->rendered) ) unset($this->rendered); | |
758 | } | |
759 | } | |
760 | } | |
761 | } | |
762 | ||
763 | ||
764 | /** | |
765 | * Clone this component (and subcomponents) into a confidential version of it. A confidential | |
766 | * event will be scrubbed of any identifying characteristics other than time/date, repeat, uid | |
767 | * and a summary which is just a translated 'Busy'. | |
768 | */ | |
769 | function CloneConfidential() { | |
770 | $confidential = clone($this); | |
771 | $keep_properties = array( 'DTSTAMP', 'DTSTART', 'RRULE', 'DURATION', 'DTEND', 'UID', 'CLASS', 'TRANSP' ); | |
772 | $resource_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' ); | |
773 | $confidential->MaskComponents(array( 'VTIMEZONE', 'VEVENT', 'VTODO', 'VJOURNAL' )); | |
774 | $confidential->MaskProperties($keep_properties, $resource_components ); | |
775 | if ( in_array( $confidential->GetType(), $resource_components ) ) { | |
776 | $confidential->AddProperty( 'SUMMARY', translate('Busy') ); | |
777 | } | |
778 | foreach( $confidential->components AS $k => $v ) { | |
779 | if ( in_array( $v->GetType(), $resource_components ) ) { | |
780 | $v->AddProperty( 'SUMMARY', translate('Busy') ); | |
781 | } | |
782 | } | |
783 | ||
784 | return $confidential; | |
785 | } | |
786 | ||
787 | ||
788 | /** | |
789 | * Renders the component, possibly restricted to only the listed properties | |
790 | */ | |
791 | function Render( $restricted_properties = null) { | |
792 | ||
793 | $unrestricted = (!isset($restricted_properties) || count($restricted_properties) == 0); | |
794 | ||
795 | if ( isset($this->rendered) && $unrestricted ) | |
796 | return $this->rendered; | |
797 | ||
798 | $rendered = "BEGIN:$this->type\r\n"; | |
799 | foreach( $this->properties AS $k => $v ) { | |
800 | if ( $unrestricted || isset($restricted_properties[$v]) ) $rendered .= $v->Render() . "\r\n"; | |
801 | } | |
802 | foreach( $this->components AS $v ) { $rendered .= $v->Render(); } | |
803 | $rendered .= "END:$this->type\r\n"; | |
804 | // $rendered = $this->WrapComponent($rendered); | |
805 | ||
806 | if ( $unrestricted ) $this->rendered = $rendered; | |
807 | ||
808 | return $rendered; | |
809 | } | |
810 | ||
811 | /** | |
812 | * Return an array of properties matching the specified path | |
813 | * | |
814 | * @return array An array of iCalProp within the tree which match the path given, in the form | |
815 | * [/]COMPONENT[/...]/PROPERTY in a syntax kind of similar to our poor man's XML queries. We | |
816 | * also allow COMPONENT and PROPERTY to be !COMPONENT and !PROPERTY for ++fun. | |
817 | * | |
818 | * @note At some point post PHP4 this could be re-done with an iterator, which should be more efficient for common use cases. | |
819 | */ | |
820 | function GetPropertiesByPath( $path ) { | |
821 | $properties = array(); | |
822 | dbg_error_log( "iCalendar", "GetPropertiesByPath: Querying within '%s' for path '%s'", $this->type, $path ); | |
823 | if ( !preg_match( '#(/?)(!?)([^/]+)(/?.*)$#', $path, $matches ) ) return $properties; | |
824 | ||
825 | $adrift = ($matches[1] == ''); | |
826 | $normal = ($matches[2] == ''); | |
827 | $ourtest = $matches[3]; | |
828 | $therest = $matches[4]; | |
829 | dbg_error_log( "iCalendar", "GetPropertiesByPath: Matches: %s -- %s -- %s -- %s\n", $matches[1], $matches[2], $matches[3], $matches[4] ); | |
830 | if ( $ourtest == '*' || (($ourtest == $this->type) === $normal) && $therest != '' ) { | |
831 | if ( preg_match( '#^/(!?)([^/]+)$#', $therest, $matches ) ) { | |
832 | $normmatch = ($matches[1] ==''); | |
833 | $proptest = $matches[2]; | |
834 | foreach( $this->properties AS $k => $v ) { | |
835 | if ( $proptest = '*' || (($v->Name() == $proptest) === $normmatch ) ) { | |
836 | $properties[] = $v; | |
837 | } | |
838 | } | |
839 | } | |
840 | else { | |
841 | /** | |
842 | * There is more to the path, so we recurse into that sub-part | |
843 | */ | |
844 | foreach( $this->components AS $k => $v ) { | |
845 | $properties = array_merge( $properties, $v->GetPropertiesByPath($therest) ); | |
846 | } | |
847 | } | |
848 | } | |
849 | ||
850 | if ( $adrift ) { | |
851 | /** | |
852 | * Our input $path was not rooted, so we recurse further | |
853 | */ | |
854 | foreach( $this->components AS $k => $v ) { | |
855 | $properties = array_merge( $properties, $v->GetPropertiesByPath($path) ); | |
856 | } | |
857 | } | |
858 | dbg_error_log("iCalendar", "GetPropertiesByPath: Found %d within '%s' for path '%s'\n", count($properties), $this->type, $path ); | |
859 | return $properties; | |
860 | } | |
861 | ||
862 | } | |
863 | ||
864 | /** | |
865 | ************************************************************************************ | |
866 | * Pretty much everything below here is deprecated and should be avoided in favour | |
867 | * of using, improving and enhancing the more sensible structures above. | |
868 | ************************************************************************************ | |
869 | */ | |
870 | ||
871 | /** | |
872 | * A Class for handling Events on a calendar | |
873 | * | |
874 | * @package awl | |
875 | */ | |
876 | class iCalendar { | |
877 | /**#@+ | |
878 | * @access private | |
879 | */ | |
880 | ||
881 | /** | |
882 | * The component-ised version of the iCalendar | |
883 | * @var component iCalComponent | |
884 | */ | |
885 | var $component; | |
886 | ||
887 | /** | |
888 | * An array of arbitrary properties, containing arbitrary arrays of arbitrary properties | |
889 | * @var properties array | |
890 | */ | |
891 | var $properties; | |
892 | ||
893 | /** | |
894 | * An array of the lines of this iCalendar resource | |
895 | * @var lines array | |
896 | */ | |
897 | var $lines; | |
898 | ||
899 | /** | |
900 | * The typical location name for the standard timezone such as "Pacific/Auckland" | |
901 | * @var tz_locn string | |
902 | */ | |
903 | var $tz_locn; | |
904 | ||
905 | /** | |
906 | * The type of iCalendar data VEVENT/VTODO/VJOURNAL | |
907 | * @var type string | |
908 | */ | |
909 | var $type; | |
910 | ||
911 | /**#@-*/ | |
912 | ||
913 | /** | |
914 | * The constructor takes an array of args. If there is an element called 'icalendar' | |
915 | * then that will be parsed into the iCalendar object. Otherwise the array elements | |
916 | * are converted into properties of the iCalendar object directly. | |
917 | */ | |
918 | function iCalendar( $args ) { | |
919 | global $c; | |
920 | ||
921 | $this->tz_locn = ""; | |
922 | if ( !isset($args) || !(is_array($args) || is_object($args)) ) return; | |
923 | if ( is_object($args) ) { | |
924 | settype($args,'array'); | |
925 | } | |
926 | ||
927 | $this->component = new iCalComponent(); | |
928 | if ( isset($args['icalendar']) ) { | |
929 | $this->component->ParseFrom($args['icalendar']); | |
930 | $this->lines = preg_split('/\r?\n/', $args['icalendar'] ); | |
931 | $this->SaveTimeZones(); | |
932 | $first =& $this->component->FirstNonTimezone(); | |
933 | if ( $first ) { | |
934 | $this->type = $first->GetType(); | |
935 | $this->properties = $first->GetProperties(); | |
936 | } | |
937 | else { | |
938 | $this->properties = array(); | |
939 | } | |
940 | $this->properties['VCALENDAR'] = array('***ERROR*** This class is being referenced in an unsupported way!'); | |
941 | return; | |
942 | } | |
943 | ||
944 | if ( isset($args['type'] ) ) { | |
945 | $this->type = $args['type']; | |
946 | unset( $args['type'] ); | |
947 | } | |
948 | else { | |
949 | $this->type = 'VEVENT'; // Default to event | |
950 | } | |
951 | $this->component->SetType('VCALENDAR'); | |
952 | $this->component->SetProperties( | |
953 | array( | |
954 | new iCalProp('PRODID:-//davical.org//NONSGML AWL Calendar//EN'), | |
955 | new iCalProp('VERSION:2.0'), | |
956 | new iCalProp('CALSCALE:GREGORIAN') | |
957 | ) | |
958 | ); | |
959 | $first = new iCalComponent(); | |
960 | $first->SetType($this->type); | |
961 | $this->properties = array(); | |
962 | ||
963 | foreach( $args AS $k => $v ) { | |
964 | dbg_error_log( "iCalendar", ":Initialise: %s to >>>%s<<<", $k, $v ); | |
965 | $property = new iCalProp(); | |
966 | $property->Name($k); | |
967 | $property->Value($v); | |
968 | $this->properties[] = $property; | |
969 | } | |
970 | $first->SetProperties($this->properties); | |
971 | $this->component->SetComponents( array($first) ); | |
972 | ||
973 | $this->properties['VCALENDAR'] = array('***ERROR*** This class is being referenced in an unsupported way!'); | |
974 | ||
975 | /** | |
976 | * @todo Need to handle timezones!!! | |
977 | */ | |
978 | if ( $this->tz_locn == "" ) { | |
979 | $this->tz_locn = $this->Get("tzid"); | |
980 | if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) { | |
981 | $this->tz_locn = $c->local_tzid; | |
982 | } | |
983 | } | |
984 | } | |
985 | ||
986 | ||
987 | /** | |
988 | * Save any timezones by TZID in the PostgreSQL database for future re-use. | |
989 | */ | |
990 | function SaveTimeZones() { | |
991 | global $c; | |
992 | ||
993 | $this->tzid_list = array_keys($this->component->CollectParameterValues('TZID')); | |
994 | if ( ! isset($this->tzid) && count($this->tzid_list) > 0 ) { | |
995 | dbg_error_log( "icalendar", "::TZID_List[0] = '%s', count=%d", $this->tzid_list[0], count($this->tzid_list) ); | |
996 | $this->tzid = $this->tzid_list[0]; | |
997 | } | |
998 | ||
999 | $timezones = $this->component->GetComponents('VTIMEZONE'); | |
1000 | if ( $timezones === false || count($timezones) == 0 ) return; | |
1001 | $this->vtimezone = $timezones[0]->Render(); // Backward compatibility | |
1002 | ||
1003 | $tzid = $this->Get('TZID'); | |
1004 | if ( isset($c->save_time_zone_defs) && $c->save_time_zone_defs ) { | |
1005 | foreach( $timezones AS $k => $tz ) { | |
1006 | $tzid = $tz->GetPValue('TZID'); | |
1007 | ||
1008 | $qry = new PgQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $tzid ); | |
1009 | if ( $qry->Exec('iCalendar') && $qry->rows == 1 ) { | |
1010 | $row = $qry->Fetch(); | |
1011 | if ( !isset($first_tzid) ) $first_tzid = $row->tz_locn; | |
1012 | continue; | |
1013 | } | |
1014 | ||
1015 | if ( $tzid != "" && $qry->rows == 0 ) { | |
1016 | ||
1017 | $tzname = $tz->GetPValue('X-LIC-LOCATION'); | |
1018 | if ( !isset($tzname) ) { | |
1019 | /** | |
1020 | * Try and convert the TZID to a string like "Pacific/Auckland" if possible. | |
1021 | */ | |
1022 | $tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$2',$tzid ); | |
1023 | } | |
1024 | ||
1025 | $qry2 = new PgQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );", | |
1026 | $tzid, $tzname, $tz->Render() ); | |
1027 | $qry2->Exec("iCalendar"); | |
1028 | } | |
1029 | } | |
1030 | } | |
1031 | if ( ! isset($this->tzid) && isset($first_tzid) ) $this->tzid = $first_tzid; | |
1032 | ||
1033 | if ( (!isset($this->tz_locn) || $this->tz_locn == '') && isset($first_tzid) && $first_tzid != '' ) { | |
1034 | $tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$2', $first_tzid ); | |
1035 | if ( preg_match( '#\S+/\S+#', $tzname) ) { | |
1036 | $this->tz_locn = $tzname; | |
1037 | } | |
1038 | dbg_error_log( "icalendar", " TZCrap1: TZID '%s', Location '%s', Perhaps: %s", $tzid, $this->tz_locn, $tzname ); | |
1039 | } | |
1040 | ||
1041 | if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) { | |
1042 | $this->tz_locn = $c->local_tzid; | |
1043 | } | |
1044 | if ( ! isset($this->tzid) && isset($this->tz_locn) ) $this->tzid = $this->tz_locn; | |
1045 | } | |
1046 | ||
1047 | ||
1048 | /** | |
1049 | * An array of property names that we should always want when rendering an iCalendar | |
1050 | * | |
1051 | * @deprecated This function is deprecated and will be removed eventually. | |
1052 | * @todo Remove this function. | |
1053 | */ | |
1054 | function DefaultPropertyList() { | |
1055 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'DefaultPropertyList' ); | |
1056 | return array( "UID" => 1, "DTSTAMP" => 1, "DTSTART" => 1, "DURATION" => 1, | |
1057 | "LAST-MODIFIED" => 1,"CLASS" => 1, "TRANSP" => 1, "SEQUENCE" => 1, | |
1058 | "DUE" => 1, "SUMMARY" => 1, "RRULE" => 1 ); | |
1059 | } | |
1060 | ||
1061 | /** | |
1062 | * A function to extract the contents of a BEGIN:SOMETHING to END:SOMETHING (perhaps multiply) | |
1063 | * and return just that bit (or, of course, those bits :-) | |
1064 | * | |
1065 | * @var string The type of thing(s) we want returned. | |
1066 | * @var integer The number of SOMETHINGS we want to get. | |
1067 | * | |
1068 | * @return string A string from BEGIN:SOMETHING to END:SOMETHING, possibly multiple of these | |
1069 | * | |
1070 | * @deprecated This function is deprecated and will be removed eventually. | |
1071 | * @todo Remove this function. | |
1072 | */ | |
1073 | function JustThisBitPlease( $type, $count=1 ) { | |
1074 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'JustThisBitPlease' ); | |
1075 | $answer = ""; | |
1076 | $intags = false; | |
1077 | $start = "BEGIN:$type"; | |
1078 | $finish = "END:$type"; | |
1079 | dbg_error_log( "iCalendar", ":JTBP: Looking for %d subsets of type %s", $count, $type ); | |
1080 | reset($this->lines); | |
1081 | foreach( $this->lines AS $k => $v ) { | |
1082 | if ( !$intags && $v == $start ) { | |
1083 | $answer .= $v . "\n"; | |
1084 | $intags = true; | |
1085 | } | |
1086 | else if ( $intags && $v == $finish ) { | |
1087 | $answer .= $v . "\n"; | |
1088 | $intags = false; | |
1089 | } | |
1090 | else if ( $intags ) { | |
1091 | $answer .= $v . "\n"; | |
1092 | } | |
1093 | } | |
1094 | return $answer; | |
1095 | } | |
1096 | ||
1097 | ||
1098 | /** | |
1099 | * Function to parse lines from BEGIN:SOMETHING to END:SOMETHING into a nested array structure | |
1100 | * | |
1101 | * @var string The "SOMETHING" from the BEGIN:SOMETHING line we just met | |
1102 | * @return arrayref An array of the things we found between (excluding) the BEGIN & END, some of which might be sub-arrays | |
1103 | * | |
1104 | * @deprecated This function is deprecated and will be removed eventually. | |
1105 | * @todo Remove this function. | |
1106 | */ | |
1107 | function &ParseSomeLines( $type ) { | |
1108 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'ParseSomeLines' ); | |
1109 | $props = array(); | |
1110 | $properties =& $props; | |
1111 | while( isset($this->lines[$this->_current_parse_line]) ) { | |
1112 | $i = $this->_current_parse_line++; | |
1113 | $line =& $this->lines[$i]; | |
1114 | dbg_error_log( "iCalendar", ":Parse:%s LINE %03d: >>>%s<<<", $type, $i, $line ); | |
1115 | if ( $this->parsing_vtimezone ) { | |
1116 | $this->vtimezone .= $line."\n"; | |
1117 | } | |
1118 | if ( preg_match( '/^(BEGIN|END):([^:]+)$/', $line, $matches ) ) { | |
1119 | if ( $matches[1] == 'END' && $matches[2] == $type ) { | |
1120 | if ( $type == 'VTIMEZONE' ) { | |
1121 | $this->parsing_vtimezone = false; | |
1122 | } | |
1123 | return $properties; | |
1124 | } | |
1125 | else if( $matches[1] == 'END' ) { | |
1126 | dbg_error_log("ERROR"," iCalendar: parse error: Unexpected END:%s when we were looking for END:%s", $matches[2], $type ); | |
1127 | return $properties; | |
1128 | } | |
1129 | else if( $matches[1] == 'BEGIN' ) { | |
1130 | $subtype = $matches[2]; | |
1131 | if ( $subtype == 'VTIMEZONE' ) { | |
1132 | $this->parsing_vtimezone = true; | |
1133 | $this->vtimezone = $line."\n"; | |
1134 | } | |
1135 | if ( !isset($properties['INSIDE']) ) $properties['INSIDE'] = array(); | |
1136 | $properties['INSIDE'][] = $subtype; | |
1137 | if ( !isset($properties[$subtype]) ) $properties[$subtype] = array(); | |
1138 | $properties[$subtype][] = $this->ParseSomeLines($subtype); | |
1139 | } | |
1140 | } | |
1141 | else { | |
1142 | // Parse the property | |
1143 | @list( $property, $value ) = preg_split('/:/', $line, 2 ); | |
1144 | if ( strpos( $property, ';' ) > 0 ) { | |
1145 | $parameterlist = preg_split('/;/', $property ); | |
1146 | $property = array_shift($parameterlist); | |
1147 | foreach( $parameterlist AS $pk => $pv ) { | |
1148 | if ( $pv == "VALUE=DATE" ) { | |
1149 | $value .= 'T000000'; | |
1150 | } | |
1151 | elseif ( preg_match('/^([^;:=]+)=([^;:=]+)$/', $pv, $matches) ) { | |
1152 | switch( $matches[1] ) { | |
1153 | case 'TZID': $properties['TZID'] = $matches[2]; break; | |
1154 | default: | |
1155 | dbg_error_log( "icalendar", " FYI: Ignoring Resource '%s', Property '%s', Parameter '%s', Value '%s'", $type, $property, $matches[1], $matches[2] ); | |
1156 | } | |
1157 | } | |
1158 | } | |
1159 | } | |
1160 | if ( $this->parsing_vtimezone && (!isset($this->tz_locn) || $this->tz_locn == "") && $property == 'X-LIC-LOCATION' ) { | |
1161 | $this->tz_locn = $value; | |
1162 | } | |
1163 | $properties[strtoupper($property)] = $this->RFC2445ContentUnescape($value); | |
1164 | } | |
1165 | } | |
1166 | return $properties; | |
1167 | } | |
1168 | ||
1169 | ||
1170 | /** | |
1171 | * Build the iCalendar object from a text string which is a single iCalendar resource | |
1172 | * | |
1173 | * @var string The RFC2445 iCalendar resource to be parsed | |
1174 | * | |
1175 | * @deprecated This function is deprecated and will be removed eventually. | |
1176 | * @todo Remove this function. | |
1177 | */ | |
1178 | function BuildFromText( $icalendar ) { | |
1179 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'BuildFromText' ); | |
1180 | /** | |
1181 | * This unescapes the (CRLF + linear space) wrapping specified in RFC2445. According | |
1182 | * to RFC2445 we should always end with CRLF but the CalDAV spec says that normalising | |
1183 | * XML parsers often muck with it and may remove the CR. | |
1184 | */ | |
1185 | $icalendar = preg_replace('/\r?\n[ \t]/', '', $icalendar ); | |
1186 | ||
1187 | $this->lines = preg_split('/\r?\n/', $icalendar ); | |
1188 | ||
1189 | $this->_current_parse_line = 0; | |
1190 | $this->properties = $this->ParseSomeLines(''); | |
1191 | ||
1192 | /** | |
1193 | * Our 'type' is the type of non-timezone inside a VCALENDAR | |
1194 | */ | |
1195 | if ( isset($this->properties['VCALENDAR'][0]['INSIDE']) ) { | |
1196 | foreach ( $this->properties['VCALENDAR'][0]['INSIDE'] AS $k => $v ) { | |
1197 | if ( $v == 'VTIMEZONE' ) continue; | |
1198 | $this->type = $v; | |
1199 | break; | |
1200 | } | |
1201 | } | |
1202 | ||
1203 | } | |
1204 | ||
1205 | ||
1206 | /** | |
1207 | * Returns a content string with the RFC2445 escaping removed | |
1208 | * | |
1209 | * @param string $escaped The incoming string to be escaped. | |
1210 | * @return string The string with RFC2445 content escaping removed. | |
1211 | * | |
1212 | * @deprecated This function is deprecated and will be removed eventually. | |
1213 | * @todo Remove this function. | |
1214 | */ | |
1215 | function RFC2445ContentUnescape( $escaped ) { | |
1216 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'RFC2445ContentUnescape' ); | |
1217 | $unescaped = str_replace( '\\n', "\n", $escaped); | |
1218 | $unescaped = str_replace( '\\N', "\n", $unescaped); | |
1219 | $unescaped = preg_replace( "/\\\\([,;:\"\\\\])/", '$1', $unescaped); | |
1220 | return $unescaped; | |
1221 | } | |
1222 | ||
1223 | ||
1224 | ||
1225 | /** | |
1226 | * Do what must be done with time zones from on file. Attempt to turn | |
1227 | * them into something that PostgreSQL can understand... | |
1228 | * | |
1229 | * @deprecated This function is deprecated and will be removed eventually. | |
1230 | * @todo Remove this function. | |
1231 | */ | |
1232 | function DealWithTimeZones() { | |
1233 | global $c; | |
1234 | ||
1235 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'DealWithTimeZones' ); | |
1236 | $tzid = $this->Get('TZID'); | |
1237 | if ( isset($c->save_time_zone_defs) && $c->save_time_zone_defs ) { | |
1238 | $qry = new PgQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $tzid ); | |
1239 | if ( $qry->Exec('iCalendar') && $qry->rows == 1 ) { | |
1240 | $row = $qry->Fetch(); | |
1241 | $this->tz_locn = $row->tz_locn; | |
1242 | } | |
1243 | dbg_error_log( "icalendar", " TZCrap2: TZID '%s', DB Rows=%d, Location '%s'", $tzid, $qry->rows, $this->tz_locn ); | |
1244 | } | |
1245 | ||
1246 | if ( (!isset($this->tz_locn) || $this->tz_locn == '') && $tzid != '' ) { | |
1247 | /** | |
1248 | * In case there was no X-LIC-LOCATION defined, let's hope there is something in the TZID | |
1249 | * that we can use. We are looking for a string like "Pacific/Auckland" if possible. | |
1250 | */ | |
1251 | $tzname = preg_replace('#^(.*[^a-z])?([a-z]+/[a-z]+)$#i','$1',$tzid ); | |
1252 | /** | |
1253 | * Unfortunately this kind of thing will never work well :-( | |
1254 | * | |
1255 | if ( strstr( $tzname, ' ' ) ) { | |
1256 | $words = preg_split('/\s/', $tzname ); | |
1257 | $tzabbr = ''; | |
1258 | foreach( $words AS $i => $word ) { | |
1259 | $tzabbr .= substr( $word, 0, 1); | |
1260 | } | |
1261 | $this->tz_locn = $tzabbr; | |
1262 | } | |
1263 | */ | |
1264 | if ( preg_match( '#\S+/\S+#', $tzname) ) { | |
1265 | $this->tz_locn = $tzname; | |
1266 | } | |
1267 | dbg_error_log( "icalendar", " TZCrap3: TZID '%s', Location '%s', Perhaps: %s", $tzid, $this->tz_locn, $tzname ); | |
1268 | } | |
1269 | ||
1270 | if ( $tzid != '' && isset($c->save_time_zone_defs) && $c->save_time_zone_defs && $qry->rows != 1 && isset($this->vtimezone) && $this->vtimezone != "" ) { | |
1271 | $qry2 = new PgQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );", | |
1272 | $tzid, $this->tz_locn, $this->vtimezone ); | |
1273 | $qry2->Exec("iCalendar"); | |
1274 | } | |
1275 | ||
1276 | if ( (!isset($this->tz_locn) || $this->tz_locn == "") && isset($c->local_tzid) ) { | |
1277 | $this->tz_locn = $c->local_tzid; | |
1278 | } | |
1279 | } | |
1280 | ||
1281 | ||
1282 | /** | |
1283 | * Get the value of a property in the first non-VTIMEZONE | |
1284 | */ | |
1285 | function Get( $key ) { | |
1286 | if ( strtoupper($key) == 'TZID' ) { | |
1287 | // backward compatibility hack | |
1288 | dbg_error_log( "icalendar", " Get(TZID): TZID '%s', Location '%s'", (isset($this->tzid)?$this->tzid:"[not set]"), $this->tz_locn ); | |
1289 | if ( isset($this->tzid) ) return $this->tzid; | |
1290 | return $this->tz_locn; | |
1291 | } | |
1292 | /** | |
1293 | * The property we work on is the first non-VTIMEZONE we find. | |
1294 | */ | |
1295 | $component =& $this->component->FirstNonTimezone(); | |
1296 | if ( $component === false ) return null; | |
1297 | return $component->GetPValue(strtoupper($key)); | |
1298 | } | |
1299 | ||
1300 | ||
1301 | /** | |
1302 | * Set the value of a property | |
1303 | */ | |
1304 | function Set( $key, $value ) { | |
1305 | if ( $value == "" ) return; | |
1306 | $key = strtoupper($key); | |
1307 | $property = new iCalProp(); | |
1308 | $property->Name($key); | |
1309 | $property->Value($value); | |
1310 | if (isset($this->component->rendered) ) unset( $this->component->rendered ); | |
1311 | $component =& $this->component->FirstNonTimezone(); | |
1312 | $component->SetProperties( array($property), $key); | |
1313 | return $this->Get($key); | |
1314 | } | |
1315 | ||
1316 | ||
1317 | /** | |
1318 | * Add a new property/value, regardless of whether it exists already | |
1319 | * | |
1320 | * @param string $key The property key | |
1321 | * @param string $value The property value | |
1322 | * @param string $parameters Any parameters to set for the property, as an array of key/value pairs | |
1323 | */ | |
1324 | function Add( $key, $value, $parameters = null ) { | |
1325 | if ( $value == "" ) return; | |
1326 | $key = strtoupper($key); | |
1327 | $property = new iCalProp(); | |
1328 | $property->Name($key); | |
1329 | $property->Value($value); | |
1330 | if ( isset($parameters) && is_array($parameters) ) { | |
1331 | $property->parameters = $parameters; | |
1332 | } | |
1333 | $component =& $this->component->FirstNonTimezone(); | |
1334 | $component->AddProperty($property); | |
1335 | if (isset($this->component->rendered) ) unset( $this->component->rendered ); | |
1336 | } | |
1337 | ||
1338 | ||
1339 | /** | |
1340 | * Because I screwed up with the name originally... | |
1341 | * @DEPRECATED | |
1342 | * todo:: Remove this function after June 2008. | |
1343 | */ | |
1344 | function Put( $key, $value ) { | |
1345 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'Put' ); | |
1346 | $this->Set($key,$value); | |
1347 | } | |
1348 | ||
1349 | /** | |
1350 | * Returns a PostgreSQL Date Format string suitable for returning HTTP (RFC2068) dates | |
1351 | * Preferred is "Sun, 06 Nov 1994 08:49:37 GMT" so we do that. | |
1352 | */ | |
1353 | function HttpDateFormat() { | |
1354 | return "'Dy, DD Mon IYYY HH24:MI:SS \"GMT\"'"; | |
1355 | } | |
1356 | ||
1357 | ||
1358 | /** | |
1359 | * Returns a PostgreSQL Date Format string suitable for returning iCal dates | |
1360 | */ | |
1361 | function SqlDateFormat() { | |
1362 | return "'YYYYMMDD\"T\"HH24MISS'"; | |
1363 | } | |
1364 | ||
1365 | ||
1366 | /** | |
1367 | * Returns a PostgreSQL Date Format string suitable for returning dates which | |
1368 | * have been cast to UTC | |
1369 | */ | |
1370 | function SqlUTCFormat() { | |
1371 | return "'YYYYMMDD\"T\"HH24MISS\"Z\"'"; | |
1372 | } | |
1373 | ||
1374 | ||
1375 | /** | |
1376 | * Returns a PostgreSQL Date Format string suitable for returning iCal durations | |
1377 | * - this doesn't work for negative intervals, but events should not have such! | |
1378 | */ | |
1379 | function SqlDurationFormat() { | |
1380 | return "'\"PT\"HH24\"H\"MI\"M\"'"; | |
1381 | } | |
1382 | ||
1383 | /** | |
1384 | * Returns a suitably escaped RFC2445 content string. | |
1385 | * | |
1386 | * @param string $name The incoming name[;param] prefixing the string. | |
1387 | * @param string $value The incoming string to be escaped. | |
1388 | * | |
1389 | * @deprecated This function is deprecated and will be removed eventually. | |
1390 | * @todo Remove this function. | |
1391 | */ | |
1392 | function RFC2445ContentEscape( $name, $value ) { | |
1393 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'RFC2445ContentEscape' ); | |
1394 | $property = preg_replace( '/[;].*$/', '', $name ); | |
1395 | switch( $property ) { | |
1396 | /** Content escaping does not apply to these properties culled from RFC2445 */ | |
1397 | case 'ATTACH': case 'GEO': case 'PERCENT-COMPLETE': case 'PRIORITY': | |
1398 | case 'COMPLETED': case 'DTEND': case 'DUE': case 'DTSTART': | |
1399 | case 'DURATION': case 'FREEBUSY': case 'TZOFFSETFROM': case 'TZOFFSETTO': | |
1400 | case 'TZURL': case 'ATTENDEE': case 'ORGANIZER': case 'RECURRENCE-ID': | |
1401 | case 'URL': case 'EXDATE': case 'EXRULE': case 'RDATE': | |
1402 | case 'RRULE': case 'REPEAT': case 'TRIGGER': case 'CREATED': | |
1403 | case 'DTSTAMP': case 'LAST-MODIFIED': case 'SEQUENCE': | |
1404 | break; | |
1405 | ||
1406 | /** Content escaping applies by default to other properties */ | |
1407 | default: | |
1408 | $value = str_replace( '\\', '\\\\', $value); | |
1409 | $value = preg_replace( '/\r?\n/', '\\n', $value); | |
1410 | $value = preg_replace( "/([,;:\"])/", '\\\\$1', $value); | |
1411 | } | |
1412 | $result = wordwrap("$name:$value", 73, " \r\n ", true ) . "\r\n"; | |
1413 | return $result; | |
1414 | } | |
1415 | ||
1416 | /** | |
1417 | * Return all sub-components of the given type, which are part of the | |
1418 | * component we pass in as an array of lines. | |
1419 | * | |
1420 | * @param array $component The component to be parsed | |
1421 | * @param string $type The type of sub-components to be extracted | |
1422 | * @param int $count The number of sub-components to extract (default: 9999) | |
1423 | * | |
1424 | * @return array The sub-component lines | |
1425 | */ | |
1426 | function ExtractSubComponent( $component, $type, $count=9999 ) { | |
1427 | $answer = array(); | |
1428 | $intags = false; | |
1429 | $start = "BEGIN:$type"; | |
1430 | $finish = "END:$type"; | |
1431 | dbg_error_log( "iCalendar", ":ExtractSubComponent: Looking for %d subsets of type %s", $count, $type ); | |
1432 | reset($component); | |
1433 | foreach( $component AS $k => $v ) { | |
1434 | if ( !$intags && $v == $start ) { | |
1435 | $answer[] = $v; | |
1436 | $intags = true; | |
1437 | } | |
1438 | else if ( $intags && $v == $finish ) { | |
1439 | $answer[] = $v; | |
1440 | $intags = false; | |
1441 | } | |
1442 | else if ( $intags ) { | |
1443 | $answer[] = $v; | |
1444 | } | |
1445 | } | |
1446 | return $answer; | |
1447 | } | |
1448 | ||
1449 | ||
1450 | /** | |
1451 | * Extract a particular property from the provided component. In doing so we | |
1452 | * assume that the content was unescaped when iCalComponent::ParseFrom() | |
1453 | * called iCalComponent::UnwrapComponent(). | |
1454 | * | |
1455 | * @param array $component An array of lines of this component | |
1456 | * @param string $type The type of parameter | |
1457 | * | |
1458 | * @return array An array of iCalProperty objects | |
1459 | */ | |
1460 | function ExtractProperty( $component, $type, $count=9999 ) { | |
1461 | $answer = array(); | |
1462 | dbg_error_log( "iCalendar", ":ExtractProperty: Looking for %d properties of type %s", $count, $type ); | |
1463 | reset($component); | |
1464 | foreach( $component AS $k => $v ) { | |
1465 | if ( preg_match( "/$type"."[;:]/i", $v ) ) { | |
1466 | $answer[] = new iCalProp($v); | |
1467 | dbg_error_log( "iCalendar", ":ExtractProperty: Found property %s", $type ); | |
1468 | if ( --$count < 1 ) return $answer; | |
1469 | } | |
1470 | } | |
1471 | return $answer; | |
1472 | } | |
1473 | ||
1474 | ||
1475 | /** | |
1476 | * Applies the filter conditions, possibly recursively, to the value which will be either | |
1477 | * a single property, or an array of lines of the component under test. | |
1478 | * | |
1479 | * @todo Eventually we need to handle all of these possibilities, which will mean writing | |
1480 | * several routines: | |
1481 | * - Get Property from Component | |
1482 | * - Get Parameter from Property | |
1483 | * - Test TimeRange | |
1484 | * For the moment we will leave these, until there is a perceived need. | |
1485 | * | |
1486 | * @param array $filter An array of XMLElement defining the filter(s) | |
1487 | * @param mixed $value Either a string which is the single property, or an array of lines, for the component. | |
1488 | * @return boolean Whether the filter passed / failed. | |
1489 | */ | |
1490 | function ApplyFilter( $filter, $value ) { | |
1491 | foreach( $filter AS $k => $v ) { | |
1492 | $tag = $v->GetTag(); | |
1493 | $value_type = gettype($value); | |
1494 | $value_defined = (isset($value) && $value_type == 'string') || ($value_type == 'array' && count($value) > 0 ); | |
1495 | if ( $tag == 'urn:ietf:params:xml:ns:caldav:is-not-defined' && $value_defined ) { | |
1496 | dbg_error_log( "iCalendar", ":ApplyFilter: Value is set ('%s'), want unset, for filter %s", count($value), $tag ); | |
1497 | return false; | |
1498 | } | |
1499 | elseif ( $tag == 'urn:ietf:params:xml:ns:caldav:is-defined' && !$value_defined ) { | |
1500 | dbg_error_log( "iCalendar", ":ApplyFilter: Want value, but it is not set for filter %s", $tag ); | |
1501 | return false; | |
1502 | } | |
1503 | else { | |
1504 | switch( $tag ) { | |
1505 | case 'urn:ietf:params:xml:ns:caldav:time-range': | |
1506 | /** todo:: While this is unimplemented here at present, most time-range tests should occur at the SQL level. */ | |
1507 | break; | |
1508 | case 'urn:ietf:params:xml:ns:caldav:text-match': | |
1509 | $search = $v->GetContent(); | |
1510 | // In this case $value will either be a string, or an array of iCalProp objects | |
1511 | // since TEXT-MATCH does not apply to COMPONENT level - only property/parameter | |
1512 | if ( gettype($value) != 'string' ) { | |
1513 | if ( gettype($value) == 'array' ) { | |
1514 | $match = false; | |
1515 | foreach( $value AS $k1 => $v1 ) { | |
1516 | // $v1 could be an iCalProp object | |
1517 | if ( $match = $v1->TextMatch($search)) break; | |
1518 | } | |
1519 | } | |
1520 | else { | |
1521 | dbg_error_log( "iCalendar", ":ApplyFilter: TEXT-MATCH will only work on strings or arrays of iCalProp. %s unsupported", gettype($value) ); | |
1522 | return true; // We return _true_ in this case, so the client sees the item | |
1523 | } | |
1524 | } | |
1525 | else { | |
1526 | $match = strstr( $value, $search[0] ); | |
1527 | } | |
1528 | $negate = $v->GetAttribute("negate-condition"); | |
1529 | if ( isset($negate) && strtolower($negate) == "yes" && $match ) { | |
1530 | dbg_error_log( "iCalendar", ":ApplyFilter: TEXT-MATCH of %s'%s' against '%s'", (isset($negate) && strtolower($negate) == "yes"?'!':''), $search, $value ); | |
1531 | return false; | |
1532 | } | |
1533 | break; | |
1534 | case 'urn:ietf:params:xml:ns:caldav:comp-filter': | |
1535 | $subfilter = $v->GetContent(); | |
1536 | $component = $this->ExtractSubComponent($value,$v->GetAttribute("name")); | |
1537 | if ( ! $this->ApplyFilter($subfilter,$component) ) return false; | |
1538 | break; | |
1539 | case 'urn:ietf:params:xml:ns:caldav:prop-filter': | |
1540 | $subfilter = $v->GetContent(); | |
1541 | $properties = $this->ExtractProperty($value,$v->GetAttribute("name")); | |
1542 | if ( ! $this->ApplyFilter($subfilter,$properties) ) return false; | |
1543 | break; | |
1544 | case 'urn:ietf:params:xml:ns:caldav:param-filter': | |
1545 | $subfilter = $v->GetContent(); | |
1546 | $parameter = $this->ExtractParameter($value,$v->GetAttribute("NAME")); | |
1547 | if ( ! $this->ApplyFilter($subfilter,$parameter) ) return false; | |
1548 | break; | |
1549 | } | |
1550 | } | |
1551 | } | |
1552 | return true; | |
1553 | } | |
1554 | ||
1555 | /** | |
1556 | * Test a PROP-FILTER or COMP-FILTER and return a true/false | |
1557 | * COMP-FILTER (is-defined | is-not-defined | (time-range?, prop-filter*, comp-filter*)) | |
1558 | * PROP-FILTER (is-defined | is-not-defined | ((time-range | text-match)?, param-filter*)) | |
1559 | * | |
1560 | * @param array $filter An array of XMLElement defining the filter | |
1561 | * | |
1562 | * @return boolean Whether or not this iCalendar passes the test | |
1563 | */ | |
1564 | function TestFilter( $filters ) { | |
1565 | ||
1566 | foreach( $filters AS $k => $v ) { | |
1567 | $tag = $v->GetTag(); | |
1568 | $name = $v->GetAttribute("name"); | |
1569 | $filter = $v->GetContent(); | |
1570 | if ( $tag == "urn:ietf:params:xml:ns:caldav:prop-filter" ) { | |
1571 | $value = $this->ExtractProperty($this->lines,$name); | |
1572 | } | |
1573 | else { | |
1574 | $value = $this->ExtractSubComponent($this->lines,$v->GetAttribute("name")); | |
1575 | } | |
1576 | if ( count($value) == 0 ) unset($value); | |
1577 | if ( ! $this->ApplyFilter($filter,$value) ) return false; | |
1578 | } | |
1579 | return true; | |
1580 | } | |
1581 | ||
1582 | /** | |
1583 | * Returns the header we always use at the start of our iCalendar resources | |
1584 | * | |
1585 | * @deprecated This function is deprecated and will be removed eventually. | |
1586 | * @todo Remove this function. | |
1587 | */ | |
1588 | function iCalHeader() { | |
1589 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'iCalHeader' ); | |
1590 | return <<<EOTXT | |
1591 | BEGIN:VCALENDAR\r | |
1592 | PRODID:-//davical.org//NONSGML AWL Calendar//EN\r | |
1593 | VERSION:2.0\r | |
1594 | ||
1595 | EOTXT; | |
1596 | } | |
1597 | ||
1598 | ||
1599 | ||
1600 | /** | |
1601 | * Returns the footer we always use at the finish of our iCalendar resources | |
1602 | * | |
1603 | * @deprecated This function is deprecated and will be removed eventually. | |
1604 | * @todo Remove this function. | |
1605 | */ | |
1606 | function iCalFooter() { | |
1607 | dbg_error_log( "LOG", " iCalendar: Call to deprecated method '%s'", 'iCalFooter' ); | |
1608 | return "END:VCALENDAR\r\n"; | |
1609 | } | |
1610 | ||
1611 | ||
1612 | /** | |
1613 | * Render the iCalendar object as a text string which is a single VEVENT (or other) | |
1614 | * | |
1615 | * @param boolean $as_calendar Whether or not to wrap the event in a VCALENDAR | |
1616 | * @param string $type The type of iCalendar object (VEVENT, VTODO, VFREEBUSY etc.) | |
1617 | * @param array $restrict_properties The names of the properties we want in our rendered result. | |
1618 | */ | |
1619 | function Render( $as_calendar = true, $type = null, $restrict_properties = null ) { | |
1620 | if ( $as_calendar ) { | |
1621 | return $this->component->Render(); | |
1622 | } | |
1623 | else { | |
1624 | $components = $this->component->GetComponents($type); | |
1625 | $rendered = ""; | |
1626 | foreach( $components AS $k => $v ) { | |
1627 | $rendered .= $v->Render($restrict_properties); | |
1628 | } | |
1629 | return $rendered; | |
1630 | } | |
1631 | } | |
1632 | ||
1633 | } |