From 88abd08a597d20b7d4a365f99a68d6b1a5c318bd Mon Sep 17 00:00:00 2001 From: Jason Oster Date: Thu, 15 Apr 2010 17:53:18 +0000 Subject: Performance improvements (especially for remote calendars) and some style cleanups --- functions/date_functions.php | 115 +++++++++++++++++++++----- functions/ical_parser.php | 188 ++++++++++++++++++++++++++++++++----------- 2 files changed, 236 insertions(+), 67 deletions(-) (limited to 'functions') diff --git a/functions/date_functions.php b/functions/date_functions.php index 43a15a0..cd82fd5 100644 --- a/functions/date_functions.php +++ b/functions/date_functions.php @@ -4,28 +4,101 @@ require_once(BASE."functions/is_daylight.php"); // functions for returning or comparing dates -// get remote file last modification date (returns unix timestamp) -function remote_filemtime($url) { - $fp = fopen($url, 'r'); - if (!$fp) return 0; - $metadata = stream_get_meta_data($fp); - fclose($fp); - - $unixtime = 0; - foreach ($metadata['wrapper_data'] as $response) { - // case: redirection - // WARNING: does not handle relative redirection - if (substr(strtolower($response), 0, 10) == 'location: ') { - return GetRemoteLastModified(substr($response, 10)); - } - // case: last-modified - else if (substr(strtolower($response), 0, 15) == 'last-modified: ') { - $unixtime = strtotime(substr($response, 15)); - break; +/* + * Get remote file last modification date (returns unix timestamp) + * Supports HTTPS, HTTP basic authentication, and location: redirects + * FIXME: Basic auth should not be sent over unencrypted connections + * unless an HTTP 401 Unauthorized is returned + */ +function remote_filemtime($url, $recurse = 0) { + // We hate infinite loops! + if (++$recurse > 5) return 0; + + // Caching the remote mtime is a Really Good Idea. + static $remote_files = array(); + if (isset($remote_files[$url])) return $remote_files[$url]; + + $uri = parse_url($url); + $uri['proto'] = ( + (isset($uri['proto']) && ($uri['proto'] == 'https')) ? + 'ssl://' : + '' + ); + $uri['port'] = isset($uri['port']) ? $uri['port'] : 80; + $path = ( + (isset($uri['path']) || isset($uri['query'])) ? + (@$uri['path'] . @$uri['query']) : + '/' + ); + $auth = ( + (isset($uri['user']) || isset($uri['pass'])) ? + ('Authentication: Basic ' . base64_encode(@$uri['user'] . ':' . @$uri['pass']) . "\r\n") : + '' + ); + + $handle = @fsockopen($uri['proto'] . $uri['host'], $uri['port']); + if (!$handle) { + $remote_files[$url] = 0; + return 0; + } + + fputs($handle, "HEAD {$path} HTTP/1.1\r\nHost: {$uri['host']}\r\n{$auth}Connection: close\r\n\r\n"); + $headers = array(); + while (!feof($handle)) { + $line = trim(fgets($handle, 1024)); + if (empty($line)) break; + $headers[] = $line; + } + fclose($handle); + + $result = 0; + array_shift($headers); + foreach ($headers as $header) { + list($key, $value) = explode(':', $header, 2); + $value = trim($value); + + switch (strtolower(trim($key))) { + case 'location': // Redirect + $result = remote_filemtime(resolve_path($url, $value), $recurse); + break; + + case 'last-modified': // Got it! + $result = strtotime($value); + break; } } - return $unixtime; + $remote_files[$url] = $result; + return $result; +} + +/* + * Resolve relative paths + * Utility function for remote_filemtime() + */ +function resolve_path($url, $rel_path) { + $uri = parse_url($url); + + $uri['proto'] = (isset($uri['proto']) ? $uri['proto'] : 'http://'); + $uri['port'] = (isset($uri['port']) ? (':' . $uri['port']) : ''); + $auth = ( + (isset($uri['user']) || isset($uri['pass'])) ? + (urlencode(@$uri['user']) . ':' . urlencode(@$uri['pass']) . '@') : + '' + ); + + if (parse_url($rel_path) === false) { + // Path is relative to this domain + $rel_path = str_replace('\\', '/', $rel_path); + + if ($rel_path{0} == '/') + return $uri['proto'] . '://' . $auth . $uri['host'] . $uri['port'] . $rel_path; + + return $uri['proto'] . '://' . $auth . $uri['host'] . $uri['port'] . $uri['path'] . '/' . $rel_path; + } + + // Path is absolute + return $rel_path; } // takes iCalendar 2 day format and makes it into 3 characters @@ -235,10 +308,10 @@ function openevent($event_date, $time, $uid, $arr, $lines = 0, $length = 0, $lin # for iCal pseudo tag comptability if (ereg('<([[:alpha:]]+://)([^<>[:space:]]+)>',$event_text,$matches)) { $full_event_text = $matches[1] . $matches[2]; - $event_text = $matches[2]; + $event_text = $matches[2]; } else { $full_event_text = $event_text; - $event_text = strip_tags($event_text, ''); + $event_text = strip_tags($event_text, ''); } if (!empty($link_class)) $link_class = ' class="'.$link_class.'"'; diff --git a/functions/ical_parser.php b/functions/ical_parser.php index 74a281a..36b2b3e 100644 --- a/functions/ical_parser.php +++ b/functions/ical_parser.php @@ -8,37 +8,59 @@ include_once(BASE.'functions/timezones.php'); include_once(BASE.'functions/parse/recur_functions.php'); // reading the file if it's allowed +$realcal_mtime = time(); $parse_file = true; -if ($phpiCal_config->save_parsed_cals == 'yes') { +if ($phpiCal_config->save_parsed_cals == 'yes') { if (sizeof ($cal_filelist) > 1) { + // This is a special case for "all calendars combined" $parsedcal = $phpiCal_config->tmp_dir.'/parsedcal-'.urlencode($cpath.'::'.$phpiCal_config->ALL_CALENDARS_COMBINED).'-'.$this_year; if (file_exists($parsedcal)) { $fd = fopen($parsedcal, 'r'); $contents = fread($fd, filesize($parsedcal)); fclose($fd); $master_array = unserialize($contents); - $z=1; $y=0; + // Check the calendars' last-modified time to determine if any need to be re-parsed if (sizeof($master_array['-4']) == (sizeof($cal_filelist))) { foreach ($master_array['-4'] as $temp_array) { - $mtime = $master_array['-4'][$z]['mtime']; - $fname = $master_array['-4'][$z]['filename']; - $wcalc = $master_array['-4'][$z]['webcal']; - - if ($wcalc == 'no') $realcal_mtime = filemtime($fname); - else $realcal_mtime = remote_filemtime($fname); + $mtime = $temp_array['mtime']; + $fname = $temp_array['filename']; + $wcalc = $temp_array['webcal']; + + if ($wcalc == 'no') { + /* + * Getting local file mtime is "fairly cheap" + * (disk I/O is expensive, but *much* cheaper than going to the network for remote files) + */ + $realcal_mtime = filemtime($fname); + } + else if ((time() - $mtime) >= $phpiCal_config->webcal_hours * 60 * 60) { + /* + * We limit remote file mtime checks based on the magic webcal_hours config variable + * This allows highly volatile web calendars to be cached for a period of time before + * downloading them again + */ + $realcal_mtime = remote_filemtime($fname); + } + else { + // This is our fallback, for the case where webcal_hours is taking effect + $realcal_mtime = $mtime; + } - if ($mtime == $realcal_mtime) { + // If this calendar is up-to-date, the $y magic number will be incremented... + if ($mtime >= $realcal_mtime) { $y++; } - $z++; } + foreach ($master_array['-3'] as $temp_array) { if (isset($temp_array) && $temp_array !='') $caldisplaynames[] = $temp_array; } + // And the $y magic number is used here to determine if all calendars are up-to-date if ($y == sizeof($cal_filelist)) { if ($master_array['-1'] == 'valid cal file') { + // At this point, all calendars are up-to-date, so we can simply used the pre-parsed data $parse_file = false; $calendar_name = $master_array['calendar_name']; $calendar_tz = $master_array['calendar_tz']; @@ -46,20 +68,25 @@ if ($phpiCal_config->save_parsed_cals == 'yes') { } } } - if ($parse_file == true) unset($master_array); + if ($parse_file == true) { + // We need to re-parse at least one calendar, so unset master_array + unset($master_array); + } } else { foreach ($cal_filelist as $filename) { - if (substr($filename, 0, 7) == 'http://' || substr($filename, 0, 8) == 'https://' || substr($filename, 0, 9) == 'webcal://') { - $realcal_mtime = remote_filemtime($filename); - } - else { - $realcal_mtime = filemtime($filename); - } - $parsedcal = $phpiCal_config->tmp_dir.'/parsedcal-'.urlencode($cpath.'::'.$cal_filename).'-'.$this_year; if (file_exists($parsedcal)) { $parsedcal_mtime = filemtime($parsedcal); - if ($realcal_mtime == $parsedcal_mtime) { + + if (((time() - $parsedcal_mtime) >= $phpiCal_config->webcal_hours * 60 * 60) && + (substr($filename, 0, 7) == 'http://' || substr($filename, 0, 8) == 'https://' || substr($filename, 0, 9) == 'webcal://')) { + $realcal_mtime = remote_filemtime($filename); + } + else { + $realcal_mtime = $parsedcal_mtime; + } + + if ($parsedcal_mtime >= $realcal_mtime) { $fd = fopen($parsedcal, 'r'); $contents = fread($fd, filesize($parsedcal)); fclose($fd); @@ -95,15 +122,16 @@ foreach ($cal_filelist as $cal_key=>$filename) { $cal_httpsPrefix = str_replace(array('http://', 'webcal://'), 'https://', $filename); $filename = $cal_httpPrefix; $master_array['-4'][$calnumber]['webcal'] = 'yes'; - $actual_mtime = @remote_filemtime($filename); + $actual_mtime = remote_filemtime($filename); } else { - $actual_mtime = @filemtime($filename); + $actual_mtime = filemtime($filename); } - include(BASE.'functions/parse/parse_tzs.php'); + $is_std = false; + $is_daylight = false; - $ifile = @fopen($filename, "r"); + $ifile = @fopen($filename, 'r'); if ($ifile == FALSE) exit(error($lang['l_error_cantopen'], $filename)); $nextline = fgets($ifile, 1024); #if (trim($nextline) != 'BEGIN:VCALENDAR') exit(error($lang['l_error_invalidcal'], $filename)); @@ -120,17 +148,55 @@ foreach ($cal_filelist as $cal_key=>$filename) { while (!feof($ifile)) { $line = $nextline; $nextline = fgets($ifile, 1024); - $nextline = ereg_replace("[\r\n]", "", $nextline); + $nextline = ereg_replace("[\r\n]", '', $nextline); #handle continuation lines that start with either a space or a tab (MS Outlook) - while (isset($nextline{0}) && ($nextline{0} == " " || $nextline{0} == "\t")) { + while (isset($nextline{0}) && ($nextline{0} == ' ' || $nextline{0} == "\t")) { $line = $line . substr($nextline, 1); $nextline = fgets($ifile, 1024); - $nextline = ereg_replace("[\r\n]", "", $nextline); + $nextline = ereg_replace("[\r\n]", '', $nextline); } - $line = str_replace('\n',"\n",$line); - $line = str_replace('\t',"\t",$line); + $line = str_replace('\n', "\n", $line); + $line = str_replace('\t', "\t", $line); $line = trim(stripslashes($line)); switch ($line) { + // Begin VTIMEZONE Parsing + // + case 'BEGIN:VTIMEZONE': + unset($tz_name, $offset_from, $offset_to, $tz_id); + break; + case 'BEGIN:STANDARD': + unset ($offset_s); + $is_std = true; + $is_daylight = false; + break; + case 'END:STANDARD': + $offset_s = $offset_to; + $is_std = false; + break; + case 'BEGIN:DAYLIGHT': + unset ($offset_d); + $is_daylight = true; + $is_std = false; + break; + case 'END:DAYLIGHT': + $offset_d = $offset_to; + $is_daylight = false; + break; + case 'END:VTIMEZONE': + if (!isset($offset_d) && isset($offset_s)) $offset_d = $offset_s; + $tz_array[$tz_id] = array( + 0 => @$offset_s, + 1 => @$offset_d, + 'dt_start' => @$begin_daylight, + 'st_start' => @$begin_std, + 'st_name' => @$st_name, + 'dt_name' => @$dt_name + + ); #echo "
$tz_id"; print_r($tz_array[$tz_id]);echo"
"; + break; + + // Begin VFREEBUSY/VEVENT Parsing + // case 'BEGIN:VFREEBUSY': case 'BEGIN:VEVENT': // each of these vars were being set to an empty string @@ -175,8 +241,11 @@ foreach ($cal_filelist as $cal_key=>$filename) { break; case 'END:VFREEBUSY': case 'END:VEVENT': - include BASE."functions/parse/end_vevent.php"; + include BASE.'functions/parse/end_vevent.php'; break; + + // Begin VTODO Parsing + // case 'END:VTODO': if (($vtodo_priority == '') && ($status == 'COMPLETED')) { $vtodo_sort = 11; @@ -233,6 +302,9 @@ foreach ($cal_filelist as $cal_key=>$filename) { $class = ''; $description = ''; break; + + // Begin VALARM Parsing + // case 'BEGIN:VALARM': $valarm_set = TRUE; break; @@ -251,7 +323,24 @@ foreach ($cal_filelist as $cal_key=>$filename) { if ($prop_pos !== false) $property = substr($property,0,$prop_pos); switch ($property) { - + // Start TZ Parsing + // + case 'TZID': + $tz_id = $data; + break; + case 'TZOFFSETFROM': + $offset_from = $data; + break; + case 'TZOFFSETTO': + $offset_to = $data; + break; + case 'TZNAME': + if ($is_std) $st_name = $data; + if ($is_daylight) $dt_name = $data; + break; + // + // End TZ Parsing + // Start VTODO Parsing // case 'DUE': @@ -286,19 +375,26 @@ foreach ($cal_filelist as $cal_key=>$filename) { $vtodo_categories = "$data"; break; // - // End VTODO Parsing + // End VTODO Parsing case 'DTSTART': $datetime = extractDateTime($data, $property, $field); $start_unixtime = $datetime[0]; $start_date = $datetime[1]; - $start_time = $datetime[2]; - $allday_start = $datetime[3]; - $start_tz = $datetime[4]; - preg_match ('/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{0,2})([0-9]{0,2})/', $data, $regs); - $vevent_start_date = $regs[1] . $regs[2] . $regs[3]; - $day_offset = dayCompare($start_date, $vevent_start_date); - #echo date("Ymd Hi", $start_unixtime)." $start_date $start_time $vevent_start_date $day_offset
"; + if ($is_std || $is_daylight) { + $year = substr($start_date, 0, 4); + if ($is_std) $begin_std[$year] = $data; + if ($is_daylight) $begin_daylight[$year] = $data; + } + else { + $start_time = $datetime[2]; + $allday_start = $datetime[3]; + $start_tz = $datetime[4]; + preg_match ('/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{0,2})([0-9]{0,2})/', $data, $regs); + $vevent_start_date = $regs[1] . $regs[2] . $regs[3]; + $day_offset = dayCompare($start_date, $vevent_start_date); + #echo date("Ymd Hi", $start_unixtime)." $start_date $start_time $vevent_start_date $day_offset
"; + } break; case 'DTEND': @@ -310,7 +406,7 @@ foreach ($cal_filelist as $cal_key=>$filename) { break; case 'EXDATE': - $data = split(",", $data); + $data = split(',', $data); foreach ($data as $exdata) { $exdata = str_replace('T', '', $exdata); $exdata = str_replace('Z', '', $exdata); @@ -424,11 +520,11 @@ foreach ($cal_filelist as $cal_key=>$filename) { } break; case 'ATTENDEE': - $email = preg_match("/mailto:(.*)/i", $data, $matches1); - $name = preg_match("/CN=([^;]*)/i", $field, $matches2); - $rsvp = preg_match("/RSVP=([^;]*)/i", $field, $matches3); - $partstat = preg_match("/PARTSTAT=([^;]*)/i", $field, $matches4); - $role = preg_match("/ROLE=([^;]*)/i", $field, $matches5); + $email = preg_match('/mailto:(.*)/i', $data, $matches1); + $name = preg_match('/CN=([^;]*)/i', $field, $matches2); + $rsvp = preg_match('/RSVP=([^;]*)/i', $field, $matches3); + $partstat = preg_match('/PARTSTAT=([^;]*)/i', $field, $matches4); + $role = preg_match('/ROLE=([^;]*)/i', $field, $matches5); $email = ($email ? $matches1[1] : ''); $name = ($name ? $matches2[1] : $email); @@ -447,8 +543,8 @@ foreach ($cal_filelist as $cal_key=>$filename) { ); break; case 'ORGANIZER': - $email = preg_match("/mailto:(.*)/i", $data, $matches1); - $name = preg_match("/CN=([^;]*)/i", $field, $matches2); + $email = preg_match('/mailto:(.*)/i', $data, $matches1); + $name = preg_match('/CN=([^;]*)/i', $field, $matches2); $email = ($email ? $matches1[1] : ''); $name = ($name ? $matches2[1] : $email); -- cgit v1.2.3