function add_property($name, $value = NULL, $parameters = NULL) { // Uppercase first of all $name = strtoupper($name); // Are we trying to add a valid property? $xname = false; if (!isset($this->valid_properties[$name])) { // If not, is it an x-name as per RFC 2445? if (!rfc2445_is_xname($name)) { return false; } // Since this is an xname, all components are supposed to allow this property $xname = true; } // Create a property object of the correct class if ($xname) { $property = new iCalendar_property_x(); $property->set_name($name); } else { $classname = 'iCalendar_property_' . strtolower(str_replace('-', '_', $name)); $property = new $classname(); } // If $value is NULL, then this property must define a default value. if ($value === NULL) { $value = $property->default_value(); if ($value === NULL) { return false; } } // Set this property's parent component to ourselves, because some // properties behave differently according to what component they apply to. $property->set_parent_component($this->name); // Set parameters before value; this helps with some properties which // accept a VALUE parameter, and thus change their default value type. // The parameters must be valid according to property specifications if (!empty($parameters)) { foreach ($parameters as $paramname => $paramvalue) { if (!$property->set_parameter($paramname, $paramvalue)) { return false; } } // Some parameters interact among themselves (e.g. ENCODING and VALUE) // so make sure that after the dust settles, these invariants hold true if (!$property->invariant_holds()) { return false; } } // $value MUST be valid according to the property data type if (!$property->set_value($value)) { return false; } // If this property is restricted to only once, blindly overwrite value if (!$xname && $this->valid_properties[$name] & RFC2445_ONCE) { $this->properties[$name] = array($property); } else { $this->properties[$name][] = $property; } // Finally: after all these, does the component invariant hold? if (!$this->invariant_holds()) { // If not, completely undo the property addition array_pop($this->properties[$name]); if (empty($this->properties[$name])) { unset($this->properties[$name]); } return false; } return true; }
function rfc2445_is_valid_value($value, $type) { // This branch should only be taken with xname values if ($type === NULL) { return true; } switch ($type) { case RFC2445_TYPE_CAL_ADDRESS: case RFC2445_TYPE_URI: if (!is_string($value)) { return false; } $valid_schemes = array('ftp', 'http', 'ldap', 'gopher', 'mailto', 'news', 'nntp', 'telnet', 'wais', 'file', 'prospero'); $pos = strpos($value, ':'); if (!$pos) { return false; } $scheme = strtolower(substr($value, 0, $pos)); $remain = substr($value, $pos + 1); if (!in_array($scheme, $valid_schemes)) { return false; } if ($scheme === 'mailto') { $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})$'; } else { $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})?(/.*)?$'; } return ereg($regexp, $remain); break; case RFC2445_TYPE_BINARY: if (!is_string($value)) { return false; } $len = strlen($value); if ($len % 4 != 0) { return false; } for ($i = 0; $i < $len; ++$i) { $ch = $value[$i]; if (!($ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || $ch >= '0' && $ch <= '9' || $ch == '-' || $ch == '+')) { if ($ch == '=' && $len - $i <= 2) { continue; } return false; } } return true; break; case RFC2445_TYPE_BOOLEAN: if (is_bool($value)) { return true; } if (is_string($value)) { $value = strtoupper($value); return $value == 'TRUE' || $value == 'FALSE'; } return false; break; case RFC2445_TYPE_DATE: if (is_int($value)) { if ($value < 0) { return false; } $value = "{$value}"; } else { if (!is_string($value)) { return false; } } if (strlen($value) != 8) { return false; } $y = intval(substr($value, 0, 4)); $m = intval(substr($value, 4, 2)); $d = intval(substr($value, 6, 2)); return checkdate($m, $d, $y); break; case RFC2445_TYPE_DATE_TIME: if (!is_string($value) || strlen($value) < 15) { return false; } return $value[8] == 'T' && rfc2445_is_valid_value(substr($value, 0, 8), RFC2445_TYPE_DATE) && rfc2445_is_valid_value(substr($value, 9), RFC2445_TYPE_TIME); break; case RFC2445_TYPE_DURATION: if (!is_string($value)) { return false; } $len = strlen($value); if ($len < 3) { // Minimum conformant length: "P1W" return false; } if ($value[0] == '+' || $value[0] == '-') { $value = substr($value, 1); --$len; // Don't forget to update this! } if ($value[0] != 'P') { return false; } // OK, now break it up $num = ''; $allowed = 'WDT'; for ($i = 1; $i < $len; ++$i) { $ch = $value[$i]; if ($ch >= '0' && $ch <= '9') { $num .= $ch; continue; } if (strpos($allowed, $ch) === false) { // Non-numeric character which shouldn't be here return false; } if ($num === '' && $ch != 'T') { // Allowed non-numeric character, but no digits came before it return false; } // OK, $ch now holds a character which tells us what $num is switch ($ch) { case 'W': // If duration in weeks is specified, this must end the string return $i == $len - 1; break; case 'D': // Days specified, now if anything comes after it must be a 'T' $allowed = 'T'; break; case 'T': // Starting to specify time, H M S are now valid delimiters $allowed = 'HMS'; break; case 'H': $allowed = 'M'; break; case 'M': $allowed = 'S'; break; case 'S': return $i == $len - 1; break; } // If we 're going to continue, reset $num $num = ''; } // $num is kept for this reason: if we 're here, we ran out of chars // therefore $num must be empty for the period to be legal return $num === '' && $ch != 'T'; break; case RFC2445_TYPE_FLOAT: if (is_float($value)) { return true; } if (!is_string($value) || $value === '') { return false; } $dot = false; $int = false; $len = strlen($value); for ($i = 0; $i < $len; ++$i) { switch ($value[$i]) { case '-': case '+': // A sign can only be seen at position 0 and cannot be the only char if ($i != 0 || $len == 1) { return false; } break; case '.': // A second dot is an error // Make sure we had at least one int before the dot if ($dot || !$int) { return false; } $dot = true; // Make also sure that the float doesn't end with a dot if ($i == $len - 1) { return false; } break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $int = true; break; default: // Any other char is a no-no return false; break; } } return true; break; case RFC2445_TYPE_INTEGER: if (is_int($value)) { return true; } if (!is_string($value) || $value === '') { return false; } if ($value[0] == '+' || $value[0] == '-') { if (strlen($value) == 1) { return false; } $value = substr($value, 1); } if (strspn($value, '0123456789') != strlen($value)) { return false; } return $value >= -2147483648 && $value <= 2147483647; break; case RFC2445_TYPE_PERIOD: if (!is_string($value) || empty($value)) { return false; } $parts = explode('/', $value); if (count($parts) != 2) { return false; } if (!rfc2445_is_valid_value($parts[0], RFC2445_TYPE_DATE_TIME)) { return false; } // Two legal cases for the second part: if (rfc2445_is_valid_value($parts[1], RFC2445_TYPE_DATE_TIME)) { // It has to be after the start time, so return $parts[1] > $parts[0]; } else { if (rfc2445_is_valid_value($parts[1], RFC2445_TYPE_DURATION)) { // The period MUST NOT be negative return $parts[1][0] != '-'; } } // It seems to be illegal return false; break; case RFC2445_TYPE_RECUR: if (!is_string($value)) { return false; } $parts = explode(';', strtoupper($value)); // First of all, we need at least a FREQ and a UNTIL or COUNT part, so... if (count($parts) < 2) { return false; } // Let's get that into a more easily comprehensible format $vars = array(); foreach ($parts as $part) { $pieces = explode('=', $part); // There must be exactly 2 pieces, e.g. FREQ=WEEKLY if (count($pieces) != 2) { return false; } // It's illegal for a variable to appear twice if (isset($vars[$pieces[0]])) { return false; } // Sounds good $vars[$pieces[0]] = $pieces[1]; } // OK... now to test everything else // FREQ must be the first thing appearing reset($vars); if (key($vars) != 'FREQ') { return false; } // It's illegal to have both UNTIL and COUNT appear if (isset($vars['UNTIL']) && isset($vars['COUNT'])) { return false; } // Special case: BYWEEKNO is only valid for FREQ=YEARLY if (isset($vars['BYWEEKNO']) && $vars['FREQ'] != 'YEARLY') { return false; } // Special case: BYSETPOS is only valid if another BY option is specified if (isset($vars['BYSETPOS'])) { $options = array('BYSECOND', 'BYMINUTE', 'BYHOUR', 'BYDAY', 'BYMONTHDAY', 'BYYEARDAY', 'BYWEEKNO', 'BYMONTH'); $defined = array_keys($vars); $common = array_intersect($options, $defined); if (empty($common)) { return false; } } // OK, now simply check if each element has a valid value, // unsetting them on the way. If at the end the array still // has some elements, they are illegal. if ($vars['FREQ'] != 'SECONDLY' && $vars['FREQ'] != 'MINUTELY' && $vars['FREQ'] != 'HOURLY' && $vars['FREQ'] != 'DAILY' && $vars['FREQ'] != 'WEEKLY' && $vars['FREQ'] != 'MONTHLY' && $vars['FREQ'] != 'YEARLY') { return false; } unset($vars['FREQ']); // Set this, we may need it later $weekdays = explode(',', RFC2445_WEEKDAYS); if (isset($vars['UNTIL'])) { if (rfc2445_is_valid_value($vars['UNTIL'], RFC2445_TYPE_DATE_TIME)) { // The time MUST be in UTC format if (!(substr($vars['UNTIL'], -1) == 'Z')) { return false; } } else { if (!rfc2445_is_valid_value($vars['UNTIL'], RFC2445_TYPE_DATE_TIME)) { return false; } } } unset($vars['UNTIL']); if (isset($vars['COUNT'])) { if (empty($vars['COUNT'])) { // This also catches the string '0', which makes no sense return false; } if (strspn($vars['COUNT'], '0123456789') != strlen($vars['COUNT'])) { return false; } } unset($vars['COUNT']); if (isset($vars['INTERVAL'])) { if (empty($vars['INTERVAL'])) { // This also catches the string '0', which makes no sense return false; } if (strspn($vars['INTERVAL'], '0123456789') != strlen($vars['INTERVAL'])) { return false; } } unset($vars['INTERVAL']); if (isset($vars['BYSECOND'])) { if ($vars['BYSECOND'] == '') { return false; } // Comma also allowed if (strspn($vars['BYSECOND'], '0123456789,') != strlen($vars['BYSECOND'])) { return false; } $secs = explode(',', $vars['BYSECOND']); foreach ($secs as $sec) { if ($sec == '' || $sec < 0 || $sec > 59) { return false; } } } unset($vars['BYSECOND']); if (isset($vars['BYMINUTE'])) { if ($vars['BYMINUTE'] == '') { return false; } // Comma also allowed if (strspn($vars['BYMINUTE'], '0123456789,') != strlen($vars['BYMINUTE'])) { return false; } $mins = explode(',', $vars['BYMINUTE']); foreach ($mins as $min) { if ($min == '' || $min < 0 || $min > 59) { return false; } } } unset($vars['BYMINUTE']); if (isset($vars['BYHOUR'])) { if ($vars['BYHOUR'] == '') { return false; } // Comma also allowed if (strspn($vars['BYHOUR'], '0123456789,') != strlen($vars['BYHOUR'])) { return false; } $hours = explode(',', $vars['BYHOUR']); foreach ($hours as $hour) { if ($hour == '' || $hour < 0 || $hour > 23) { return false; } } } unset($vars['BYHOUR']); if (isset($vars['BYDAY'])) { if (empty($vars['BYDAY'])) { return false; } // First off, split up all values we may have $days = explode(',', $vars['BYDAY']); foreach ($days as $day) { $daypart = substr($day, -2); if (!in_array($daypart, $weekdays)) { return false; } if (strlen($day) > 2) { $intpart = substr($day, 0, strlen($day) - 2); if (!rfc2445_is_valid_value($intpart, RFC2445_TYPE_INTEGER)) { return false; } if (intval($intpart) == 0) { return false; } } } } unset($vars['BYDAY']); if (isset($vars['BYMONTHDAY'])) { if (empty($vars['BYMONTHDAY'])) { return false; } $mdays = explode(',', $vars['BYMONTHDAY']); foreach ($mdays as $mday) { if (!rfc2445_is_valid_value($mday, RFC2445_TYPE_INTEGER)) { return false; } $mday = abs(intval($mday)); if ($mday == 0 || $mday > 31) { return false; } } } unset($vars['BYMONTHDAY']); if (isset($vars['BYYEARDAY'])) { if (empty($vars['BYYEARDAY'])) { return false; } $ydays = explode(',', $vars['BYYEARDAY']); foreach ($ydays as $yday) { if (!rfc2445_is_valid_value($yday, RFC2445_TYPE_INTEGER)) { return false; } $yday = abs(intval($yday)); if ($yday == 0 || $yday > 366) { return false; } } } unset($vars['BYYEARDAY']); if (isset($vars['BYWEEKNO'])) { if (empty($vars['BYWEEKNO'])) { return false; } $weeknos = explode(',', $vars['BYWEEKNO']); foreach ($weeknos as $weekno) { if (!rfc2445_is_valid_value($weekno, RFC2445_TYPE_INTEGER)) { return false; } $weekno = abs(intval($weekno)); if ($weekno == 0 || $weekno > 53) { return false; } } } unset($vars['BYWEEKNO']); if (isset($vars['BYMONTH'])) { if (empty($vars['BYMONTH'])) { return false; } // Comma also allowed if (strspn($vars['BYMONTH'], '0123456789,') != strlen($vars['BYMONTH'])) { return false; } $months = explode(',', $vars['BYMONTH']); foreach ($months as $month) { if ($month == '' || $month < 1 || $month > 12) { return false; } } } unset($vars['BYMONTH']); if (isset($vars['BYSETPOS'])) { if (empty($vars['BYSETPOS'])) { return false; } $sets = explode(',', $vars['BYSETPOS']); foreach ($sets as $set) { if (!rfc2445_is_valid_value($set, RFC2445_TYPE_INTEGER)) { return false; } $set = abs(intval($set)); if ($set == 0 || $set > 366) { return false; } } } unset($vars['BYSETPOS']); if (isset($vars['WKST'])) { if (!in_array($vars['WKST'], $weekdays)) { return false; } } unset($vars['WKST']); // Any remaining vars must be x-names if (empty($vars)) { return true; } foreach ($vars as $name => $var) { if (!rfc2445_is_xname($name)) { return false; } } // At last, all is OK! return true; break; case RFC2445_TYPE_TEXT: return true; break; case RFC2445_TYPE_TIME: if (is_int($value)) { if ($value < 0) { return false; } $value = "{$value}"; } else { if (!is_string($value)) { return false; } } if (strlen($value) == 7) { if (strtoupper(substr($value, -1)) != 'Z') { return false; } $value = substr($value, 0, 6); } if (strlen($value) != 6) { return false; } $h = intval(substr($value, 0, 2)); $m = intval(substr($value, 2, 2)); $s = intval(substr($value, 4, 2)); return $h <= 23 && $m <= 59 && $s <= 60; break; case RFC2445_TYPE_UTC_OFFSET: if (is_int($value)) { if ($value >= 0) { $value = "+{$value}"; } else { $value = "{$value}"; } } else { if (!is_string($value)) { return false; } } if (strlen($value) == 7) { $s = intval(substr($value, 5, 2)); $value = substr($value, 0, 5); } if (strlen($value) != 5 || $value == "-0000") { return false; } if ($value[0] != '+' && $value[0] != '-') { return false; } $h = intval(substr($value, 1, 2)); $m = intval(substr($value, 3, 2)); return $h <= 23 && $m <= 59 && $s <= 59; break; } // TODO: remove this assertion trigger_error('bad code path', E_USER_WARNING); var_dump($type); return false; }
function set_name($name) { $name = strtoupper($name); if (rfc2445_is_xname($name)) { $this->name = $name; return true; } return false; }
function is_valid_value(&$parent_property, $parameter, $value) { switch ($parameter) { // These must all be a URI case 'ALTREP': case 'DIR': return rfc2445_is_valid_value($value, RFC2445_TYPE_URI); break; // These must be CAL-ADDRESS, which is equivalent to URI // These must be CAL-ADDRESS, which is equivalent to URI case 'DELEGATED-FROM': case 'DELEGATED-TO': case 'MEMBER': case 'SENT-BY': return rfc2445_is_valid_value($value, RFC2445_TYPE_CAL_ADDRESS); break; // These are textual parameters, so the MUST NOT contain double quotes // These are textual parameters, so the MUST NOT contain double quotes case 'CN': return strpos($value, '"') === false; break; // These have enumerated legal values // These have enumerated legal values case 'CUTYPE': $value = strtoupper($value); return $value == 'INDIVIDUAL' || $value == 'GROUP' || $value == 'RESOURCE' || $value == 'ROOM' || $value == 'UNKNOWN' || rfc2445_is_xname($value); break; case 'ENCODING': $value = strtoupper($value); return $value == '8BIT' || $value == 'BASE64' || rfc2445_is_xname($value); break; case 'FBTYPE': $value = strtoupper($value); return $value == 'FREE' || $value == 'BUSY' || $value == 'BUSY-UNAVAILABLE' || $value == 'BUSY-TENTATIVE' || rfc2445_is_xname($value); break; case 'FMTTYPE': $fmttypes = array('TEXT' => array('PLAIN', 'RICHTEXT', 'ENRICHED', 'TAB-SEPARATED-VALUES', 'HTML', 'SGML', 'VND.LATEX-Z', 'VND.FMI.FLEXSTOR'), 'MULTIPART' => array('MIXED', 'ALTERNATIVE', 'DIGEST', 'PARALLEL', 'APPLEDOUBLE', 'HEADER-SET', 'FORM-DATA', 'RELATED', 'REPORT', 'VOICE-MESSAGE', 'SIGNED', 'ENCRYPTED', 'BYTERANGES'), 'MESSAGE' => array('RFC822', 'PARTIAL', 'EXTERNAL-BODY', 'NEWS', 'HTTP'), 'APPLICATION' => array('OCTET-STREAM', 'POSTSCRIPT', 'ODA', 'ATOMICMAIL', 'ANDREW-INSET', 'SLATE', 'WITA', 'DEC-DX', 'DCA-RFT', 'ACTIVEMESSAGE', 'RTF', 'APPLEFILE', 'MAC-BINHEX40', 'NEWS-MESSAGE-ID', 'NEWS-TRANSMISSION', 'WORDPERFECT5.1', 'PDF', 'ZIP', 'MACWRITEII', 'MSWORD', 'REMOTE-PRINTING', 'MATHEMATICA', 'CYBERCASH', 'COMMONGROUND', 'IGES', 'RISCOS', 'ESHOP', 'X400-BP', 'SGML', 'CALS-1840', 'PGP-ENCRYPTED', 'PGP-SIGNATURE', 'PGP-KEYS', 'VND.FRAMEMAKER', 'VND.MIF', 'VND.MS-EXCEL', 'VND.MS-POWERPOINT', 'VND.MS-PROJECT', 'VND.MS-WORKS', 'VND.MS-TNEF', 'VND.SVD', 'VND.MUSIC-NIFF', 'VND.MS-ARTGALRY', 'VND.TRUEDOC', 'VND.KOAN', 'VND.STREET-STREAM', 'VND.FDF', 'SET-PAYMENT-INITIATION', 'SET-PAYMENT', 'SET-REGISTRATION-INITIATION', 'SET-REGISTRATION', 'VND.SEEMAIL', 'VND.BUSINESSOBJECTS', 'VND.MERIDIAN-SLINGSHOT', 'VND.XARA', 'SGML-OPEN-CATALOG', 'VND.RAPID', 'VND.ENLIVEN', 'VND.JAPANNET-REGISTRATION-WAKEUP', 'VND.JAPANNET-VERIFICATION-WAKEUP', 'VND.JAPANNET-PAYMENT-WAKEUP', 'VND.JAPANNET-DIRECTORY-SERVICE', 'VND.INTERTRUST.DIGIBOX', 'VND.INTERTRUST.NNCP'), 'IMAGE' => array('JPEG', 'GIF', 'IEF', 'G3FAX', 'TIFF', 'CGM', 'NAPLPS', 'VND.DWG', 'VND.SVF', 'VND.DXF', 'PNG', 'VND.FPX', 'VND.NET-FPX'), 'AUDIO' => array('BASIC', '32KADPCM', 'VND.QCELP'), 'VIDEO' => array('MPEG', 'QUICKTIME', 'VND.VIVO', 'VND.MOTOROLA.VIDEO', 'VND.MOTOROLA.VIDEOP')); $value = strtoupper($value); if (rfc2445_is_xname($value)) { return true; } @(list($type, $subtype) = explode('/', $value)); if (empty($type) || empty($subtype)) { return false; } if (!isset($fmttypes[$type]) || !in_array($subtype, $fmttypes[$type])) { return false; } return true; break; case 'LANGUAGE': $value = strtoupper($value); $parts = explode('-', $value); foreach ($parts as $part) { if (empty($part)) { return false; } if (strspn($part, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') != strlen($part)) { return false; } } return true; break; case 'PARTSTAT': $value = strtoupper($value); switch ($parent_property->parent_component) { case 'VEVENT': return $value == 'NEEDS-ACTION' || $value == 'ACCEPTED' || $value == 'DECLINED' || $value == 'TENTATIVE' || $value == 'DELEGATED' || rfc2445_is_xname($value); break; case 'VTODO': return $value == 'NEEDS-ACTION' || $value == 'ACCEPTED' || $value == 'DECLINED' || $value == 'TENTATIVE' || $value == 'DELEGATED' || $value == 'COMPLETED' || $value == 'IN-PROCESS' || rfc2445_is_xname($value); break; case 'VJOURNAL': return $value == 'NEEDS-ACTION' || $value == 'ACCEPTED' || $value == 'DECLINED' || rfc2445_is_xname($value); break; } return false; break; case 'RANGE': $value = strtoupper($value); return $value == 'THISANDPRIOR' || $value == 'THISANDFUTURE'; break; case 'RELATED': $value = strtoupper($value); return $value == 'START' || $value == 'END'; break; case 'RELTYPE': $value = strtoupper($value); return $value == 'PARENT' || $value == 'CHILD' || $value == 'SIBLING' || rfc2445_is_xname($value); break; case 'ROLE': $value = strtoupper($value); return $value == 'CHAIR' || $value == 'REQ-PARTICIPANT' || $value == 'OPT-PARTICIPANT' || $value == 'NON-PARTICIPANT' || rfc2445_is_xname($value); break; case 'RSVP': $value = strtoupper($value); return $value == 'TRUE' || $value == 'FALSE'; break; case 'TZID': if (empty($value)) { return false; } return strcspn($value, '";:,') == strlen($value); break; case 'VALUE': $value = strtoupper($value); return $value == 'BINARY' || $value == 'BOOLEAN' || $value == 'CAL-ADDRESS' || $value == 'DATE' || $value == 'DATE-TIME' || $value == 'DURATION' || $value == 'FLOAT' || $value == 'INTEGER' || $value == 'PERIOD' || $value == 'RECUR' || $value == 'TEXT' || $value == 'TIME' || $value == 'URI' || $value == 'UTC-OFFSET' || rfc2445_is_xname($value); break; } }