MDL-22308 Update lib/bennu to latest upstream.
[moodle.git] / lib / bennu / iCalendar_rfc2445.php
1 <?php
3 /**
4  *  BENNU - PHP iCalendar library
5  *  (c) 2005-2006 Ioannis Papaioannou (pj@moodle.org). All rights reserved.
6  *
7  *  Released under the LGPL.
8  *
9  *  See http://bennu.sourceforge.net/ for more information and downloads.
10  *
11  * @author Ioannis Papaioannou 
12  * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
13  */
15 /*
17    All names of properties, property parameters, enumerated property
18    values and property parameter values are case-insensitive. However,
19    all other property values are case-sensitive, unless otherwise
20    stated.
22 */
24 define('RFC2445_CRLF',               "\r\n");
25 define('RFC2445_WSP',                "\t ");
26 define('RFC2445_WEEKDAYS',           'MO,TU,WE,TH,FR,SA,SU');
27 define('RFC2445_FOLDED_LINE_LENGTH', 75);
29 define('RFC2445_PARAMETER_SEPARATOR',   ';');
30 define('RFC2445_VALUE_SEPARATOR',       ':');
32 define('RFC2445_REQUIRED', 0x01);
33 define('RFC2445_OPTIONAL', 0x02);
34 define('RFC2445_ONCE',     0x04);
36 define('RFC2445_PROP_FLAGS',       0);
37 define('RFC2445_PROP_TYPE',        1);
38 define('RFC2445_PROP_DEFAULT',     2);
40 define('RFC2445_XNAME', 'X-');
42 define('RFC2445_TYPE_BINARY',       0);
43 define('RFC2445_TYPE_BOOLEAN',      1);
44 define('RFC2445_TYPE_CAL_ADDRESS',  2);
45 define('RFC2445_TYPE_DATE',         3);
46 define('RFC2445_TYPE_DATE_TIME',    4);
47 define('RFC2445_TYPE_DURATION',     5);
48 define('RFC2445_TYPE_FLOAT',        6);
49 define('RFC2445_TYPE_INTEGER',      7);
50 define('RFC2445_TYPE_PERIOD',       8);
51 define('RFC2445_TYPE_RECUR',        9);
52 define('RFC2445_TYPE_TEXT',        10);
53 define('RFC2445_TYPE_TIME',        11);
54 define('RFC2445_TYPE_URI',         12); // CAL_ADDRESS === URI
55 define('RFC2445_TYPE_UTC_OFFSET',  13);
58 function rfc2445_fold($string) {
59     if(mb_strlen($string, 'utf-8') <= RFC2445_FOLDED_LINE_LENGTH) {
60         return $string;
61     }
63     $retval = '';
64   
65     $i=0;
66     $len_count=0;
68     //multi-byte string, get the correct length
69     $section_len = mb_strlen($string, 'utf-8');
71     while($len_count<$section_len) {
72         
73         //get the current portion of the line
74         $section = mb_substr($string, ($i * RFC2445_FOLDED_LINE_LENGTH), (RFC2445_FOLDED_LINE_LENGTH), 'utf-8');
76         //increment the length we've processed by the length of the new portion
77         $len_count += mb_strlen($section, 'utf-8');
78         
79         /* Add the portion to the return value, terminating with CRLF.HTAB
80            As per RFC 2445, CRLF.HTAB will be replaced by the processor of the 
81            data */
82         $retval .= $section.RFC2445_CRLF.RFC2445_WSP;
83         
84         $i++;
85     }
87     return $retval;
89 }
91 function rfc2445_unfold($string) {
92     for($i = 0; $i < strlen(RFC2445_WSP); ++$i) {
93         $string = str_replace(RFC2445_CRLF.substr(RFC2445_WSP, $i, 1), '', $string);
94     }
96     return $string;
97 }
99 function rfc2445_is_xname($name) {
101     // If it's less than 3 chars, it cannot be legal
102     if(strlen($name) < 3) {
103         return false;
104     }
106     // If it contains an illegal char anywhere, reject it
107     if(strspn($name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') != strlen($name)) {
108         return false;
109     }
111     // To be legal, it must still start with "X-"
112     return substr($name, 0, 2) === 'X-';
115 function rfc2445_is_valid_value($value, $type) {
117     // This branch should only be taken with xname values
118     if($type === NULL) {
119         return true;
120     }
122     switch($type) {
123         case RFC2445_TYPE_CAL_ADDRESS:
124         case RFC2445_TYPE_URI:
125             if(!is_string($value)) {
126                 return false;
127             }
129             $valid_schemes = array('ftp', 'http', 'ldap', 'gopher', 'mailto', 'news', 'nntp', 'telnet', 'wais', 'file', 'prospero');
131             $pos = strpos($value, ':');
132             if(!$pos) {
133                 return false;
134             }
135         
136             $scheme = strtolower(substr($value, 0, $pos));
137             $remain = substr($value, $pos + 1);
138             
139             if(!in_array($scheme, $valid_schemes)) {
140                 return false;
141             }
142         
143             if($scheme === 'mailto') {
144                 $regexp = '#^[a-zA-Z0-9]+[_a-zA-Z0-9\-]*(\.[_a-z0-9\-]+)*@(([0-9a-zA-Z\-]+\.)+[a-zA-Z][0-9a-zA-Z\-]+|([0-9]{1,3}\.){3}[0-9]{1,3})$#';
145             }
146             else {
147                 $regexp = '#^//(.+(:.*)?@)?(([0-9a-zA-Z\-]+\.)+[a-zA-Z][0-9a-zA-Z\-]+|([0-9]{1,3}\.){3}[0-9]{1,3})(:[0-9]{1,5})?(/.*)?$#';
148             }
149         
150             return preg_match($regexp, $remain);
151         break;
153         case RFC2445_TYPE_BINARY:
154             if(!is_string($value)) {
155                 return false;
156             }
158             $len = strlen($value);
159             
160             if($len % 4 != 0) {
161                 return false;
162             }
164             for($i = 0; $i < $len; ++$i) {
165                 $ch = $value{$i};
166                 if(!($ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || $ch >= '0' && $ch <= '9' || $ch == '-' || $ch == '+')) {
167                     if($ch == '=' && $len - $i <= 2) {
168                         continue;
169                     }
170                     return false;
171                 }
172             }
173             return true;
174         break;
176         case RFC2445_TYPE_BOOLEAN:
177             if(is_bool($value)) {
178                 return true;
179             }
180             if(is_string($value)) {
181                 $value = strtoupper($value);
182                 return ($value == 'TRUE' || $value == 'FALSE');
183             }
184             return false;
185         break;
187         case RFC2445_TYPE_DATE:
188             if(is_int($value)) {
189                 if($value < 0) {
190                     return false;
191                 }
192                 $value = "$value";
193             }
194             else if(!is_string($value)) {
195                 return false;
196             }
198             if(strlen($value) != 8) {
199                 return false;
200             }
202             $y = intval(substr($value, 0, 4));
203             $m = intval(substr($value, 4, 2));
204             $d = intval(substr($value, 6, 2));
206             return checkdate($m, $d, $y);
207         break;
209         case RFC2445_TYPE_DATE_TIME:
210             if(!is_string($value) || strlen($value) < 15) {
211                 return false;
212             }
214             return($value{8} == 'T' && 
215                    rfc2445_is_valid_value(substr($value, 0, 8), RFC2445_TYPE_DATE) &&
216                    rfc2445_is_valid_value(substr($value, 9), RFC2445_TYPE_TIME));
217         break;
219         case RFC2445_TYPE_DURATION:
220             if(!is_string($value)) {
221                 return false;
222             }
224             $len = strlen($value);
226             if($len < 3) {
227                 // Minimum conformant length: "P1W"
228                 return false;
229             }
231             if($value{0} == '+' || $value{0} == '-') {
232                 $value = substr($value, 1);
233                 --$len; // Don't forget to update this!
234             }
236             if($value{0} != 'P') {
237                 return false;
238             }
240             // OK, now break it up
241             $num = '';
242             $allowed = 'WDT';
244             for($i = 1; $i < $len; ++$i) {
245                 $ch = $value{$i};
246                 if($ch >= '0' && $ch <= '9') {
247                     $num .= $ch;
248                     continue;
249                 }
250                 if(strpos($allowed, $ch) === false) {
251                     // Non-numeric character which shouldn't be here
252                     return false;
253                 }
254                 if($num === '' && $ch != 'T') {
255                     // Allowed non-numeric character, but no digits came before it
256                     return false;
257                 }
259                 // OK, $ch now holds a character which tells us what $num is
260                 switch($ch) {
261                     case 'W':
262                         // If duration in weeks is specified, this must end the string
263                         return ($i == $len - 1);
264                     break;
266                     case 'D':
267                         // Days specified, now if anything comes after it must be a 'T'
268                         $allowed = 'T';
269                     break;
271                     case 'T':
272                         // Starting to specify time, H M S are now valid delimiters
273                         $allowed = 'HMS';
274                     break;
276                     case 'H':
277                         $allowed = 'M';
278                     break;
280                     case 'M':
281                         $allowed = 'S';
282                     break;
284                     case 'S':
285                         return ($i == $len - 1);
286                     break;
287                 }
289                 // If we 're going to continue, reset $num
290                 $num = '';
292             }
294             // $num is kept for this reason: if we 're here, we ran out of chars
295             // therefore $num must be empty for the period to be legal
296             return ($num === '' && $ch != 'T');
298         break;
299         
300         case RFC2445_TYPE_FLOAT:
301             if(is_float($value)) {
302                 return true;
303             }
304             if(!is_string($value) || $value === '') {
305                 return false;
306             }
308             $dot = false;
309             $int = false;
310             $len = strlen($value);
311             for($i = 0; $i < $len; ++$i) {
312                 switch($value{$i}) {
313                     case '-': case '+':
314                         // A sign can only be seen at position 0 and cannot be the only char
315                         if($i != 0 || $len == 1) {
316                             return false;
317                         }
318                     break;
319                     case '.':
320                         // A second dot is an error
321                         // Make sure we had at least one int before the dot
322                         if($dot || !$int) {
323                             return false;
324                         }
325                         $dot = true;
326                         // Make also sure that the float doesn't end with a dot
327                         if($i == $len - 1) {
328                             return false;
329                         }
330                     break;
331                     case '0': case '1': case '2': case '3': case '4':
332                     case '5': case '6': case '7': case '8': case '9':
333                         $int = true;
334                     break;
335                     default:
336                         // Any other char is a no-no
337                         return false;
338                     break;
339                 }
340             }
341             return true;
342         break;
344         case RFC2445_TYPE_INTEGER:
345             if(is_int($value)) {
346                 return true;
347             }
348             if(!is_string($value) || $value === '') {
349                 return false;
350             }
352             if($value{0} == '+' || $value{0} == '-') {
353                 if(strlen($value) == 1) {
354                     return false;
355                 }
356                 $value = substr($value, 1);
357             }
359             if(strspn($value, '0123456789') != strlen($value)) {
360                 return false;
361             }
363             return ($value >= -2147483648 && $value <= 2147483647);
364         break;
366         case RFC2445_TYPE_PERIOD:
367             if(!is_string($value) || empty($value)) {
368                 return false;
369             }
371             $parts = explode('/', $value);
372             if(count($parts) != 2) {
373                 return false;
374             }
376             if(!rfc2445_is_valid_value($parts[0], RFC2445_TYPE_DATE_TIME)) {
377                 return false;
378             }
380             // Two legal cases for the second part:
381             if(rfc2445_is_valid_value($parts[1], RFC2445_TYPE_DATE_TIME)) {
382                 // It has to be after the start time, so
383                 return ($parts[1] > $parts[0]);
384             }
385             else if(rfc2445_is_valid_value($parts[1], RFC2445_TYPE_DURATION)) {
386                 // The period MUST NOT be negative
387                 return ($parts[1]{0} != '-');
388             }
390             // It seems to be illegal
391             return false;
392         break;
394         case RFC2445_TYPE_RECUR:
395             if(!is_string($value)) {
396                 return false;
397             }
399             $parts = explode(';', strtoupper($value));
401             // First of all, we need at least a FREQ and a UNTIL or COUNT part, so...
402             if(count($parts) < 2) {
403                 return false;
404             }
406             // Let's get that into a more easily comprehensible format
407             $vars = array();
408             foreach($parts as $part) {
410                 $pieces = explode('=', $part);
411                 // There must be exactly 2 pieces, e.g. FREQ=WEEKLY
412                 if(count($pieces) != 2) {
413                     return false;
414                 }
416                 // It's illegal for a variable to appear twice
417                 if(isset($vars[$pieces[0]])) {
418                     return false;
419                 }
421                 // Sounds good
422                 $vars[$pieces[0]] = $pieces[1];
423             }
425             // OK... now to test everything else
427             // FREQ must be the first thing appearing
428             reset($vars);
429             if(key($vars) != 'FREQ') {
430                 return false;
431             }
433             // It's illegal to have both UNTIL and COUNT appear
434             if(isset($vars['UNTIL']) && isset($vars['COUNT'])) {
435                 return false;
436             }
438             // Special case: BYWEEKNO is only valid for FREQ=YEARLY
439             if(isset($vars['BYWEEKNO']) && $vars['FREQ'] != 'YEARLY') {
440                 return false;
441             }
443             // Special case: BYSETPOS is only valid if another BY option is specified
444             if(isset($vars['BYSETPOS'])) {
445                 $options = array('BYSECOND', 'BYMINUTE', 'BYHOUR', 'BYDAY', 'BYMONTHDAY', 'BYYEARDAY', 'BYWEEKNO', 'BYMONTH');
446                 $defined = array_keys($vars);
447                 $common  = array_intersect($options, $defined);
448                 if(empty($common)) {
449                     return false;
450                 }
451             }
453             // OK, now simply check if each element has a valid value,
454             // unsetting them on the way. If at the end the array still
455             // has some elements, they are illegal.
457             if($vars['FREQ'] != 'SECONDLY' && $vars['FREQ'] != 'MINUTELY' && $vars['FREQ'] != 'HOURLY' && 
458                $vars['FREQ'] != 'DAILY'    && $vars['FREQ'] != 'WEEKLY' &&
459                $vars['FREQ'] != 'MONTHLY'  && $vars['FREQ'] != 'YEARLY') {
460                 return false;
461             }
462             unset($vars['FREQ']);
464             // Set this, we may need it later
465             $weekdays = explode(',', RFC2445_WEEKDAYS);
467             if(isset($vars['UNTIL'])) {
468                 if(rfc2445_is_valid_value($vars['UNTIL'], RFC2445_TYPE_DATE_TIME)) {
469                     // The time MUST be in UTC format
470                     if(!(substr($vars['UNTIL'], -1) == 'Z')) {
471                         return false;
472                     }
473                 }
474                 else if(!rfc2445_is_valid_value($vars['UNTIL'], RFC2445_TYPE_DATE_TIME)) {
475                     return false;
476                 }
477             }
478             unset($vars['UNTIL']);
481             if(isset($vars['COUNT'])) {
482                 if(empty($vars['COUNT'])) {
483                     // This also catches the string '0', which makes no sense
484                     return false;
485                 }
486                 if(strspn($vars['COUNT'], '0123456789') != strlen($vars['COUNT'])) {
487                     return false;
488                 }
489             }
490             unset($vars['COUNT']);
492             
493             if(isset($vars['INTERVAL'])) {
494                 if(empty($vars['INTERVAL'])) {
495                     // This also catches the string '0', which makes no sense
496                     return false;
497                 }
498                 if(strspn($vars['INTERVAL'], '0123456789') != strlen($vars['INTERVAL'])) {
499                     return false;
500                 }
501             }
502             unset($vars['INTERVAL']);
504             
505             if(isset($vars['BYSECOND'])) {
506                 if($vars['BYSECOND'] == '') {
507                     return false;
508                 }
509                 // Comma also allowed
510                 if(strspn($vars['BYSECOND'], '0123456789,') != strlen($vars['BYSECOND'])) {
511                     return false;
512                 }
513                 $secs = explode(',', $vars['BYSECOND']);
514                 foreach($secs as $sec) {
515                     if($sec == '' || $sec < 0 || $sec > 59) {
516                         return false;
517                     }
518                 }
519             }
520             unset($vars['BYSECOND']);
522             
523             if(isset($vars['BYMINUTE'])) {
524                 if($vars['BYMINUTE'] == '') {
525                     return false;
526                 }
527                 // Comma also allowed
528                 if(strspn($vars['BYMINUTE'], '0123456789,') != strlen($vars['BYMINUTE'])) {
529                     return false;
530                 }
531                 $mins = explode(',', $vars['BYMINUTE']);
532                 foreach($mins as $min) {
533                     if($min == '' || $min < 0 || $min > 59) {
534                         return false;
535                     }
536                 }
537             }
538             unset($vars['BYMINUTE']);
540             
541             if(isset($vars['BYHOUR'])) {
542                 if($vars['BYHOUR'] == '') {
543                     return false;
544                 }
545                 // Comma also allowed
546                 if(strspn($vars['BYHOUR'], '0123456789,') != strlen($vars['BYHOUR'])) {
547                     return false;
548                 }
549                 $hours = explode(',', $vars['BYHOUR']);
550                 foreach($hours as $hour) {
551                     if($hour == '' || $hour < 0 || $hour > 23) {
552                         return false;
553                     }
554                 }
555             }
556             unset($vars['BYHOUR']);
557             
559             if(isset($vars['BYDAY'])) {
560                 if(empty($vars['BYDAY'])) {
561                     return false;
562                 }
564                 // First off, split up all values we may have
565                 $days = explode(',', $vars['BYDAY']);
566                 
567                 foreach($days as $day) {
568                     $daypart = substr($day, -2);
569                     if(!in_array($daypart, $weekdays)) {
570                         return false;
571                     }
573                     if(strlen($day) > 2) {
574                         $intpart = substr($day, 0, strlen($day) - 2);
575                         if(!rfc2445_is_valid_value($intpart, RFC2445_TYPE_INTEGER)) {
576                             return false;
577                         }
578                         if(intval($intpart) == 0) {
579                             return false;
580                         }
581                     }
582                 }
583             }
584             unset($vars['BYDAY']);
587             if(isset($vars['BYMONTHDAY'])) {
588                 if(empty($vars['BYMONTHDAY'])) {
589                     return false;
590                 }
591                 $mdays = explode(',', $vars['BYMONTHDAY']);
592                 foreach($mdays as $mday) {
593                     if(!rfc2445_is_valid_value($mday, RFC2445_TYPE_INTEGER)) {
594                         return false;
595                     }
596                     $mday = abs(intval($mday));
597                     if($mday == 0 || $mday > 31) {
598                         return false;
599                     }
600                 }
601             }
602             unset($vars['BYMONTHDAY']);
605             if(isset($vars['BYYEARDAY'])) {
606                 if(empty($vars['BYYEARDAY'])) {
607                     return false;
608                 }
609                 $ydays = explode(',', $vars['BYYEARDAY']);
610                 foreach($ydays as $yday) {
611                     if(!rfc2445_is_valid_value($yday, RFC2445_TYPE_INTEGER)) {
612                         return false;
613                     }
614                     $yday = abs(intval($yday));
615                     if($yday == 0 || $yday > 366) {
616                         return false;
617                     }
618                 }
619             }
620             unset($vars['BYYEARDAY']);
623             if(isset($vars['BYWEEKNO'])) {
624                 if(empty($vars['BYWEEKNO'])) {
625                     return false;
626                 }
627                 $weeknos = explode(',', $vars['BYWEEKNO']);
628                 foreach($weeknos as $weekno) {
629                     if(!rfc2445_is_valid_value($weekno, RFC2445_TYPE_INTEGER)) {
630                         return false;
631                     }
632                     $weekno = abs(intval($weekno));
633                     if($weekno == 0 || $weekno > 53) {
634                         return false;
635                     }
636                 }
637             }
638             unset($vars['BYWEEKNO']);
641             if(isset($vars['BYMONTH'])) {
642                 if(empty($vars['BYMONTH'])) {
643                     return false;
644                 }
645                 // Comma also allowed
646                 if(strspn($vars['BYMONTH'], '0123456789,') != strlen($vars['BYMONTH'])) {
647                     return false;
648                 }
649                 $months = explode(',', $vars['BYMONTH']);
650                 foreach($months as $month) {
651                     if($month == '' || $month < 1 || $month > 12) {
652                         return false;
653                     }
654                 }
655             }
656             unset($vars['BYMONTH']);
659             if(isset($vars['BYSETPOS'])) {
660                 if(empty($vars['BYSETPOS'])) {
661                     return false;
662                 }
663                 $sets = explode(',', $vars['BYSETPOS']);
664                 foreach($sets as $set) {
665                     if(!rfc2445_is_valid_value($set, RFC2445_TYPE_INTEGER)) {
666                         return false;
667                     }
668                     $set = abs(intval($set));
669                     if($set == 0 || $set > 366) {
670                         return false;
671                     }
672                 }
673             }
674             unset($vars['BYSETPOS']);
677             if(isset($vars['WKST'])) {
678                 if(!in_array($vars['WKST'], $weekdays)) {
679                     return false;
680                 }
681             }
682             unset($vars['WKST']);
685             // Any remaining vars must be x-names
686             if(empty($vars)) {
687                 return true;
688             }
690             foreach($vars as $name => $var) {
691                 if(!rfc2445_is_xname($name)) {
692                     return false;
693                 }
694             }
696             // At last, all is OK!
697             return true;
699         break;
701         case RFC2445_TYPE_TEXT:
702             return true;
703         break;
705         case RFC2445_TYPE_TIME:
706             if(is_int($value)) {
707                 if($value < 0) {
708                     return false;
709                 }
710                 $value = "$value";
711             }
712             else if(!is_string($value)) {
713                 return false;
714             }
716             if(strlen($value) == 7) {
717                 if(strtoupper(substr($value, -1)) != 'Z') {
718                     return false;
719                 }
720                 $value = substr($value, 0, 6);
721             }
722             if(strlen($value) != 6) {
723                 return false;
724             }
726             $h = intval(substr($value, 0, 2));
727             $m = intval(substr($value, 2, 2));
728             $s = intval(substr($value, 4, 2));
730             return ($h <= 23 && $m <= 59 && $s <= 60);
731         break;
733         case RFC2445_TYPE_UTC_OFFSET:
734             if(is_int($value)) {
735                 if($value >= 0) {
736                     $value = "+$value";
737                 }
738                 else {
739                     $value = "$value";
740                 }
741             }
742             else if(!is_string($value)) {
743                 return false;
744             }
746             $s = 0;
747             if(strlen($value) == 7) {
748                 $s = intval(substr($value, 5, 2));
749                 $value = substr($value, 0, 5);
750             }
751             if(strlen($value) != 5 || $value == "-0000") {
752                 return false;
753             }
755             if($value{0} != '+' && $value{0} != '-') {
756                 return false;
757             }
759             $h = intval(substr($value, 1, 2));
760             $m = intval(substr($value, 3, 2));
762             return ($h <= 23 && $m <= 59 && $s <= 59);
763         break;
764     }
766     // TODO: remove this assertion
767     trigger_error('bad code path', E_USER_WARNING);
768     var_dump($type);
769     return false;
772 function rfc2445_do_value_formatting($value, $type) {
773     // Note: this does not only do formatting; it also does conversion to string!
774     switch($type) {
775         case RFC2445_TYPE_CAL_ADDRESS:
776         case RFC2445_TYPE_URI:
777             // Enclose in double quotes
778             $value = '"'.$value.'"';
779         break;
780         case RFC2445_TYPE_TEXT:
781             // Escape entities
782             $value = strtr($value, array("\r\n" => '\\n', "\n" => '\\n', '\\' => '\\\\', ',' => '\\,', ';' => '\\;'));
783         break;
784     }
785     return $value;
788 function rfc2445_undo_value_formatting($value, $type) {
789     switch($type) {
790         case RFC2445_TYPE_CAL_ADDRESS:
791         case RFC2445_TYPE_URI:
792             // Trim beginning and end double quote
793             $value = substr($value, 1, strlen($value) - 2);
794         break;
795         case RFC2445_TYPE_TEXT:
796             // Unescape entities
797             $value = strtr($value, array('\\n' => "\n", '\\N' => "\n", '\\\\' => '\\', '\\,' => ',', '\\;' => ';'));
798         break;
799     }
800     return $value;