1 <?php 2 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 */ 14 15 /* 16 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. 21 22 */ 23 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); 28 29 define('RFC2445_PARAMETER_SEPARATOR', ';'); 30 define('RFC2445_VALUE_SEPARATOR', ':'); 31 32 define('RFC2445_REQUIRED', 0x01); 33 define('RFC2445_OPTIONAL', 0x02); 34 define('RFC2445_ONCE', 0x04); 35 36 define('RFC2445_PROP_FLAGS', 0); 37 define('RFC2445_PROP_TYPE', 1); 38 define('RFC2445_PROP_DEFAULT', 2); 39 40 define('RFC2445_XNAME', 'X-'); 41 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); 56 57 58 function rfc2445_fold($string) { 59 if(core_text::strlen($string, 'utf-8') <= RFC2445_FOLDED_LINE_LENGTH) { 60 return $string; 61 } 62 63 $retval = ''; 64 65 $i=0; 66 $len_count=0; 67 68 //multi-byte string, get the correct length 69 $section_len = core_text::strlen($string, 'utf-8'); 70 71 while($len_count<$section_len) { 72 73 //get the current portion of the line 74 $section = core_text::substr($string, ($i * RFC2445_FOLDED_LINE_LENGTH), (RFC2445_FOLDED_LINE_LENGTH), 'utf-8'); 75 76 //increment the length we've processed by the length of the new portion 77 $len_count += core_text::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 . substr(RFC2445_WSP, 0, 1); 83 84 $i++; 85 } 86 87 return $retval; 88 89 } 90 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 } 95 96 return $string; 97 } 98 99 function rfc2445_is_xname($name) { 100 101 // If it's less than 3 chars, it cannot be legal 102 if(strlen($name) < 3) { 103 return false; 104 } 105 106 // If it contains an illegal char anywhere, reject it 107 if(strspn($name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') != strlen($name)) { 108 return false; 109 } 110 111 // To be legal, it must still start with "X-" 112 return substr($name, 0, 2) === 'X-'; 113 } 114 115 function rfc2445_is_valid_value($value, $type) { 116 117 // This branch should only be taken with xname values 118 if($type === NULL) { 119 return true; 120 } 121 122 switch($type) { 123 case RFC2445_TYPE_CAL_ADDRESS: 124 case RFC2445_TYPE_URI: 125 if(!is_string($value)) { 126 return false; 127 } 128 129 $valid_schemes = array('ftp', 'http', 'ldap', 'gopher', 'mailto', 'news', 'nntp', 'telnet', 'wais', 'file', 'prospero'); 130 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; 152 153 case RFC2445_TYPE_BINARY: 154 if(!is_string($value)) { 155 return false; 156 } 157 158 $len = strlen($value); 159 160 if($len % 4 != 0) { 161 return false; 162 } 163 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; 175 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; 186 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 } 197 198 if(strlen($value) != 8) { 199 return false; 200 } 201 202 $y = intval(substr($value, 0, 4)); 203 $m = intval(substr($value, 4, 2)); 204 $d = intval(substr($value, 6, 2)); 205 206 return checkdate($m, $d, $y); 207 break; 208 209 case RFC2445_TYPE_DATE_TIME: 210 if(!is_string($value) || strlen($value) < 15) { 211 return false; 212 } 213 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; 218 219 case RFC2445_TYPE_DURATION: 220 if(!is_string($value)) { 221 return false; 222 } 223 224 $len = strlen($value); 225 226 if($len < 3) { 227 // Minimum conformant length: "P1W" 228 return false; 229 } 230 231 if($value[0] == '+' || $value[0] == '-') { 232 $value = substr($value, 1); 233 --$len; // Don't forget to update this! 234 } 235 236 if($value[0] != 'P') { 237 return false; 238 } 239 240 // OK, now break it up 241 $num = ''; 242 $allowed = 'WDT'; 243 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 } 258 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; 265 266 case 'D': 267 // Days specified, now if anything comes after it must be a 'T' 268 $allowed = 'T'; 269 break; 270 271 case 'T': 272 // Starting to specify time, H M S are now valid delimiters 273 $allowed = 'HMS'; 274 break; 275 276 case 'H': 277 $allowed = 'M'; 278 break; 279 280 case 'M': 281 $allowed = 'S'; 282 break; 283 284 case 'S': 285 return ($i == $len - 1); 286 break; 287 } 288 289 // If we 're going to continue, reset $num 290 $num = ''; 291 292 } 293 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'); 297 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 } 307 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; 343 344 case RFC2445_TYPE_INTEGER: 345 if(is_int($value)) { 346 return true; 347 } 348 if(!is_string($value) || $value === '') { 349 return false; 350 } 351 352 if($value[0] == '+' || $value[0] == '-') { 353 if(strlen($value) == 1) { 354 return false; 355 } 356 $value = substr($value, 1); 357 } 358 359 if(strspn($value, '0123456789') != strlen($value)) { 360 return false; 361 } 362 363 return ($value >= -2147483648 && $value <= 2147483647); 364 break; 365 366 case RFC2445_TYPE_PERIOD: 367 if(!is_string($value) || empty($value)) { 368 return false; 369 } 370 371 $parts = explode('/', $value); 372 if(count($parts) != 2) { 373 return false; 374 } 375 376 if(!rfc2445_is_valid_value($parts[0], RFC2445_TYPE_DATE_TIME)) { 377 return false; 378 } 379 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 } 389 390 // It seems to be illegal 391 return false; 392 break; 393 394 case RFC2445_TYPE_RECUR: 395 if(!is_string($value)) { 396 return false; 397 } 398 399 $parts = explode(';', strtoupper($value)); 400 401 // We need at least one part for a valid rule, for example: "FREQ=DAILY". 402 if(empty($parts)) { 403 return false; 404 } 405 406 // Let's get that into a more easily comprehensible format 407 $vars = array(); 408 foreach($parts as $part) { 409 410 $pieces = explode('=', $part); 411 // There must be exactly 2 pieces, e.g. FREQ=WEEKLY 412 if(count($pieces) != 2) { 413 return false; 414 } 415 416 // It's illegal for a variable to appear twice 417 if(isset($vars[$pieces[0]])) { 418 return false; 419 } 420 421 // Sounds good 422 $vars[$pieces[0]] = $pieces[1]; 423 } 424 425 // OK... now to test everything else 426 427 // FREQ must be the first thing appearing 428 reset($vars); 429 if(key($vars) != 'FREQ') { 430 return false; 431 } 432 433 // It's illegal to have both UNTIL and COUNT appear 434 if(isset($vars['UNTIL']) && isset($vars['COUNT'])) { 435 return false; 436 } 437 438 // Special case: BYWEEKNO is only valid for FREQ=YEARLY 439 if(isset($vars['BYWEEKNO']) && $vars['FREQ'] != 'YEARLY') { 440 return false; 441 } 442 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 } 452 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. 456 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']); 463 464 // Set this, we may need it later 465 $weekdays = explode(',', RFC2445_WEEKDAYS); 466 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']); 479 480 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']); 491 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']); 503 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']); 521 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']); 539 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 558 559 if(isset($vars['BYDAY'])) { 560 if(empty($vars['BYDAY'])) { 561 return false; 562 } 563 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 } 572 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']); 585 586 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']); 603 604 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']); 621 622 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']); 639 640 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']); 657 658 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']); 675 676 677 if(isset($vars['WKST'])) { 678 if(!in_array($vars['WKST'], $weekdays)) { 679 return false; 680 } 681 } 682 unset($vars['WKST']); 683 684 685 // Any remaining vars must be x-names 686 if(empty($vars)) { 687 return true; 688 } 689 690 foreach($vars as $name => $var) { 691 if(!rfc2445_is_xname($name)) { 692 return false; 693 } 694 } 695 696 // At last, all is OK! 697 return true; 698 699 break; 700 701 case RFC2445_TYPE_TEXT: 702 return true; 703 break; 704 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 } 715 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 } 725 726 $h = intval(substr($value, 0, 2)); 727 $m = intval(substr($value, 2, 2)); 728 $s = intval(substr($value, 4, 2)); 729 730 return ($h <= 23 && $m <= 59 && $s <= 60); 731 break; 732 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 } 745 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 } 754 755 if($value[0] != '+' && $value[0] != '-') { 756 return false; 757 } 758 759 $h = intval(substr($value, 1, 2)); 760 $m = intval(substr($value, 3, 2)); 761 762 return ($h <= 23 && $m <= 59 && $s <= 59); 763 break; 764 } 765 766 // TODO: remove this assertion 767 trigger_error('bad code path', E_USER_WARNING); 768 var_dump($type); 769 return false; 770 } 771 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; 786 } 787 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; 801 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body