Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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  }