diff --git a/include/class.format.php b/include/class.format.php index 864bc6456c6e36863339f612e7e7fa9f2c91c581..6058142d8c0adb2b51e996943f2ac1562d60b647 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -512,74 +512,128 @@ class Format { global $cfg; static $cache; - if (!$timestamp) - return ''; - - if ($fromDb) + if ($timestamp && $fromDb) $timestamp = Misc::db2gmtime($timestamp); - if (class_exists('IntlDateFormatter')) { - $locale = Internationalization::getCurrentLocale($user); - $key = "{$locale}:{$dayType}:{$timeType}:{$timezone}:{$format}"; - if (!isset($cache[$key])) { - // Setting up the IntlDateFormatter is pretty expensive, so - // cache it since there aren't many variations of the - // arguments passed to the constructor - $cache[$key] = $formatter = new IntlDateFormatter( - $locale, - $dayType, - $timeType, - $timezone, - IntlDateFormatter::GREGORIAN, - $format ?: null + // Make sure timestamp is valid for realz. + if (!$timestamp || !($datetime = DateTime::createFromFormat('U', $timestamp))) + return ''; + + // Set the desired timezone (caching since it will be mostly same + // for most date formatting. + if (isset($cache[$timezone])) + $tz = $cache[$timezone]; + else + $cache[$timezone] = $tz = new DateTimeZone($timezone ?: $cfg->getTimezone()); + $datetime->setTimezone($tz); + + // Formmating options + $options = array( + 'timezone' => $tz->getName(), + 'locale' => Internationalization::getCurrentLocale($user), + 'daytype' => $dayType, + 'timetype' => $timeType, + 'strftime' => $strftimeFallback, ); - if ($cfg->isForce24HourTime()) { - $format = str_replace(array('a', 'h'), array('', 'H'), - $formatter->getPattern()); - $formatter->setPattern($format); - } - } - else { - $formatter = $cache[$key]; - } - return $formatter->format($timestamp); - } - // Fallback using strftime - static $user_timezone; - if (!isset($user_timezone)) - $user_timezone = new DateTimeZone($cfg->getTimezone() ?: date_default_timezone_get()); - $format = self::getStrftimeFormat($format); - // Properly convert to user local time - if (!($time = DateTime::createFromFormat('U', $timestamp, new DateTimeZone('UTC')))) - return ''; + return self::IntDateFormat($datetime, $format, $options); - $offset = $user_timezone->getOffset($time); - $timestamp = $time->getTimestamp() + $offset; - return strftime($format ?: $strftimeFallback, $timestamp); } - function parseDate($date, $format=false) { + // IntDateFormat + // Format datetime to desired format in accorrding to desired locale + function IntDateFormat(DateTime $datetime, $format, $options=array()) { global $cfg; + if (!$datetime instanceof DateTime) + return ''; + + $format = $format ?: $cfg->getDateFormat(); + $timezone = $datetime->getTimeZone(); + // Use IntlDateFormatter if available if (class_exists('IntlDateFormatter')) { - $formatter = new IntlDateFormatter( - Internationalization::getCurrentLocale(), - null, - null, - null, - IntlDateFormatter::GREGORIAN, - $format ?: null - ); - if ($cfg->isForce24HourTime()) { + + if ($cfg && $cfg->isForce24HourTime()) $format = str_replace(array('a', 'h'), array('', 'H'), - $formatter->getPattern()); - $formatter->setPattern($format); - } - return $formatter->parse($date); + $format); + + $options += array( + 'pattern' => $format, + 'timezone' => $timezone->getName()); + + if ($fmt=Internationalization::getIntDateFormatter($options)) + return $fmt->format($datetime); + } + + // Fallback to using strftime which is not timezone aware + // Figure out timezone offset for given timestamp + $timestamp = $datetime->format('U'); + $time = DateTime::createFromFormat('U', $timestamp, new DateTimeZone('UTC')); + $timestamp += $timezone->getOffset($time); + // Change format to strftime format otherwise us a fallback format + $format = self::getStrftimeFormat($format) ?: $options['strftime'] + ?: '%x %X'; + return strftime($format, $timestamp); + } + + // Normalize ambiguous timezones + function timezone($tz, $default=false) { + + // Translate ambiguous 'GMT' timezone + if ($tz == 'GMT') + return 'Europe/London'; + + if (!$tz || !strcmp($tz, '+00:00')) + $tz = 'UTC'; + + if (is_numeric($tz)) + $tz = timezone_name_from_abbr('', $tz, false); + // Forbid timezone abbreviations like 'CDT' + elseif ($tz !== 'UTC' && strpos($tz, '/') === false) { + // Attempt to lookup based on the abbreviation + if (!($tz = timezone_name_from_abbr($tz))) + // Abbreviation doesn't point to anything valid + return $default; } - // Fallback using strtotime - return strtotime($date); + + // SYSTEM does not describe a time zone, ensure we have a valid zone + // by attempting to create an instance of DateTimeZone() + try { + $timezone = new DateTimeZone($tz); + return $timezone->getName(); + } catch(Exception $ex) { + return $default; + } + + return $tz; + } + + function parseDatetime($date, $locale=null, $format=false) { + global $cfg; + + if (!$date) + return null; + + // Timestamp format? + if (is_numeric($date)) + return DateTime::createFromFormat('U', $date); + + $datetime = null; + try { + $datetime = new DateTime($date); + $tz = $datetime->getTimezone()->getName(); + if ($tz && $tz[0] == '+' || $tz[0] == '-') + $tz = (int) $datetime->format('Z'); + $timezone = new DateTimeZone(Format::timezone($tz) ?: 'UTC'); + $datetime->setTimezone($timezone); + } catch (Exception $ex) { + // Fallback using strtotime + if (($time=strtotime($date))) + $datetime = DateTime::createFromFormat('U', $time); + + } + + return $datetime; } function time($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { @@ -886,23 +940,45 @@ else { class FormattedLocalDate implements TemplateVariable { + var $date; var $timezone; + var $datetime; var $fromdb; + var $format; + + function __construct($date, $options=array()) { + + // Date to be formatted + $this->datetime = Format::parseDateTime($date); + $this->date = $this->datetime->getTimestamp(); + // Desired timezone + if (isset($options['timezone'])) + $this->timezone = $options['timezone']; + else + $this->timezone = false; + // User + if (isset($options['user'])) + $this->user = $options['user']; + else + $this->user = false; - function __construct($date, $timezone=false, $user=false, $fromdb=true) { - $this->date = $date; - $this->timezone = $timezone; - $this->user = $user; - $this->fromdb = $fromdb; + // DB date or nah? + if (isset($options['fromdb'])) + $this->fromdb = $options['fromdb']; + else + $this->fromdb = true; + // Desired format + if (isset($options['format']) && $options['format']) + $this->format = $options['format']; } - function asVar() { - return $this->getVar('long'); + function getDateTime() { + return $this->datetime; } - function __toString() { - return $this->asVar(); + function asVar() { + return $this->getVar($this->format ?: 'long'); } function getVar($what) { @@ -920,6 +996,10 @@ implements TemplateVariable { } } + function __toString() { + return $this->asVar() ?: ''; + } + static function getVarScope() { return array( 'full' => 'Expanded date, e.g. day, month dd, yyyy', @@ -938,7 +1018,28 @@ extends FormattedLocalDate { function __toString() { global $cfg; - return (string) new FormattedLocalDate($this->date, $cfg->getTimezone(), false, $this->fromdb); + + $timezone = new DatetimeZone($this->timezone ?: + $cfg->getTimezone()); + $options = array( + 'timezone' => $timezone->getName(), + 'fromdb' => $this->fromdb, + 'format' => $this->format + ); + + $val = (string) new FormattedLocalDate($this->date, $options); + if ($this->timezone && $this->format == 'long') { + try { + $this->datetime->setTimezone($timezone); + $val = sprintf('%s %s', + $val, $this->datetime->format('T')); + + } catch(Exception $ex) { + // ignore + } + } + + return $val; } function getVar($what, $context=null) { @@ -951,13 +1052,19 @@ extends FormattedLocalDate { case 'user': // Fetch $recipient from the context and find that user's time zone if ($context && ($recipient = $context->getObj('recipient'))) { - $tz = $recipient->getTimezone() ?: $cfg->getDefaultTimezone(); - return new FormattedLocalDate($this->date, $tz, $recipient); + $options = array( + 'timezone' => $recipient->getTimezone() ?: $cfg->getDefaultTimezone(), + 'user' => $recipient + ); + return new FormattedLocalDate($this->date, $options); } // Don't resolve the variable until correspondance is sent out return false; case 'system': - return new FormattedLocalDate($this->date, $cfg->getDefaultTimezone()); + return new FormattedLocalDate($this->date, array( + 'timezone' => $cfg->getDefaultTimezone() + ) + ); } } diff --git a/include/class.forms.php b/include/class.forms.php index fa5f2d882eb7d666f2a184234dceb3e567786c31..bc3a21b142704c70558a0f101e523f621853754b 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1765,37 +1765,116 @@ class ChoiceField extends FormField { class DatetimeField extends FormField { static $widget = 'DatetimePickerWidget'; + var $min = null; + var $max = null; + + // Get php DatateTime object of the field - null if value is empty + function getDateTime($value=null) { + return Format::parseDateTime($value ?: $this->value); + } + + function getMinDateTime() { + + if (!isset($this->min)) { + $config = $this->getConfiguration(); + $this->min = $config['min'] + ? Format::parseDateTime($config['min']) : false; + } + + return $this->min; + } + + function getMaxDateTime() { + + if (!isset($this->max)) { + $config = $this->getConfiguration(); + $this->max = $config['max'] + ? Format::parseDateTime($config['max']) : false; + } + + return $this->max; + } + function to_database($value) { - // Store time in gmt time, unix epoch format - return $value ? date('Y-m-d H:i:s', $value) : $value; + // Store time in format given by Date Picker (DateTime::W3C) + return $value; } function to_php($value) { - if (!$value) - return $value; - else - return (int) strtotime($value); + + if (strtotime($value) <= 0) + return 0; + + return $value; } - function asVar($value, $id=false) { - if (!$value) return null; - return new FormattedDate((int) $value, 'UTC', false, false); + function display($value) { + global $cfg; + + if (!$value || !($datetime = Format::parseDatetime($value))) + return ''; + + $config = $this->getConfiguration(); + if ($config['gmt']) + return $this->format((int) $datetime->format('U')); + + $value = $this->format($datetime->format('U'), + $datetime->getTimezone()->getName()); + + // No need to show timezone + if (!$config['time']) + return $value; + + // Display is NOT timezone aware show entry's timezone. + return sprintf('%s (%s)', + $value, $datetime->format('T')); } - function asVarType() { - return 'FormattedDate'; + + function format($timestamp, $timezone=false) { + + if (!$timestamp || $timestamp <= 0) + return ''; + + $config = $this->getConfiguration(); + if ($config['time']) + $formatted = Format::datetime($timestamp, false, $timezone); + else + $formatted = Format::date($timestamp, false, false, $timezone); + + return $formatted; } function toString($value) { + + $timestamp = is_int($value) ? $value : (int) strtotime($value); + if ($timestamp <= 0) + return ''; + + return $this->format($timestamp); + } + + function asVar($value, $id=false) { global $cfg; - $config = $this->getConfiguration(); - // If GMT is set, convert to local time zone. Otherwise, leave - // unchanged (default TZ is UTC) + if (!$value) - return ''; - if ($config['time']) - return Format::datetime($value, false, !$config['gmt'] ? 'UTC' : false); + return null; + + $datetime = $this->getDateTime($value); + $config = $this->getConfiguration(); + if (!$config['gmt'] || !$config['time']) + $timezone = $datetime->getTimezone()->getName(); else - return Format::date($value, false, false, !$config['gmt'] ? 'UTC' : false); + $timezone = false; + + return new FormattedDate($value, array( + 'timezone' => $timezone, + 'format' => $config['time'] ? 'long' : 'short' + ) + ); + } + + function asVarType() { + return 'FormattedDate'; } function getConfigurationOptions() { @@ -1823,16 +1902,34 @@ class DatetimeField extends FormField { } function validateEntry($value) { + global $cfg; + $config = $this->getConfiguration(); parent::validateEntry($value); - if (!$value) return; - if ($config['min'] and $value < $config['min']) - $this->_errors[] = __('Selected date is earlier than permitted'); - elseif ($config['max'] and $value > $config['max']) - $this->_errors[] = __('Selected date is later than permitted'); - // strtotime returns -1 on error for PHP < 5.1.0 and false thereafter - elseif ($value === -1 or $value === false) + if (!$value || !($datetime = Format::parseDatetime($value))) + return; + + // Parse value to DateTime object + $val = Format::parseDatetime($value); + // Get configured min/max (if any) + $min = $this->getMinDatetime(); + $max = $this->getMaxDatetime(); + + if (!$val) { $this->_errors[] = __('Enter a valid date'); + } elseif ($min and $val < $min) { + $this->_errors[] = sprintf('%s (%s)', + __('Selected date is earlier than permitted'), + Format::date($min->getTimestamp(), false, false, + $min->getTimezone()->getName() ?: 'UTC') + ); + } elseif ($max and $val > $max) { + $this->_errors[] = sprintf('%s (%s)', + __('Selected date is later than permitted'), + Format::date($max->getTimestamp(), false, false, + $max->getTimezone()->getName() ?: 'UTC') + ); + } } // SearchableField interface ------------------------------ @@ -3591,33 +3688,43 @@ class DatetimePickerWidget extends Widget { $config = $this->field->getConfiguration(); if ($this->value) { - $this->value = is_int($this->value) ? $this->value : - strtotime($this->value); - - if ($config['gmt']) { - // Convert to GMT time - $tz = new DateTimeZone($cfg->getTimezone()); - $D = DateTime::createFromFormat('U', $this->value); - $this->value += $tz->getOffset($D); + + $timezone = null; + if (is_int($this->value)) + // Assuming UTC timezone. + $datetime = DateTime::createFromFormat('U', $this->value); + else { + $datetime = Format::parseDateTime($this->value); } - list($hr, $min) = explode(':', date('H:i', $this->value)); - $this->value = Format::date($this->value, false, false, 'UTC'); + + if ($config['time']) { + // Convert to user's timezone for update. + $timezone = new DateTimeZone($cfg->getTimezone()); + $datetime->setTimezone($timezone); + } + + $this->value = Format::date($datetime->getTimestamp(), false, + false, $timezone ? $timezone->getName() : 'UTC'); + } else { + $timezone = new DateTimeZone($cfg->getTimezone()); + $datetime = new DateTime('now'); + $datetime->setTimezone($timezone); } ?> <input type="text" name="<?php echo $this->name; ?>" id="<?php echo $this->id; ?>" style="display:inline-block;width:auto" - value="<?php echo Format::htmlchars($this->value); ?>" size="12" + value="<?php echo Format::htmlchars($this->value ?: ''); ?>" size="12" autocomplete="off" class="dp" /> <script type="text/javascript"> $(function() { $('input[name="<?php echo $this->name; ?>"]').datepicker({ <?php - if ($config['min']) - echo "minDate: new Date({$config['min']}000),"; - if ($config['max']) - echo "maxDate: new Date({$config['max']}000),"; + if ($dt=$this->field->getMinDateTime()) + echo sprintf("minDate: new Date(%s),\n", $dt->format('U')*1000); + if ($dt=$this->field->getMaxDateTime()) + echo sprintf("maxDate: new Date(%s),\n", $dt->format('U')*1000); elseif (!$config['future']) - echo "maxDate: new Date().getTime(),"; + echo "maxDate: new Date().getTime(),\n"; ?> numberOfMonths: 2, showButtonPanel: true, @@ -3628,37 +3735,38 @@ class DatetimePickerWidget extends Widget { }); </script> <?php - if ($config['time']) + if ($config['time']) { + list($hr, $min) = explode(':', $datetime ? + $datetime->format('H:i') : ''); // TODO: Add time picker -- requires time picker or selection with // Misc::timeDropdown echo ' ' . Misc::timeDropdown($hr, $min, $this->name . ':time'); + echo sprintf(' <span class="faded">(%s)</span>', + $datetime->format('T')); + } } /** * Function: getValue * Combines the datepicker date value and the time dropdown selected - * time value into a single date and time string value. + * time value into a single date and time string value in DateTime::W3C */ function getValue() { global $cfg; - $data = $this->field->getSource(); - $config = $this->field->getConfiguration(); - if ($datetime = parent::getValue()) { - $datetime = is_int($datetime) ? $datetime : - strtotime($datetime); - if ($datetime && isset($data[$this->name . ':time'])) { - list($hr, $min) = explode(':', $data[$this->name . ':time']); - $datetime += $hr * 3600 + $min * 60; - } - if ($datetime && $config['gmt']) { - // Convert to GMT time - $tz = new DateTimeZone($cfg->getTimezone()); - $D = DateTime::createFromFormat('U', $datetime); - $datetime -= $tz->getOffset($D); - } + if ($value = parent::getValue()) { + // Effective timezone for the selection + $tz = new DateTimeZone($cfg->getTimezone()); + // See if we have time + $data = $this->field->getSource(); + if ($value && isset($data[$this->name . ':time'])) + $value .=' '.$data[$this->name . ':time']; + + $dt = new DateTime($value, $tz); + $value = $dt->format('Y-m-d H:i:s T'); } - return $datetime; + + return $value; } } diff --git a/include/class.i18n.php b/include/class.i18n.php index ac6db0a332a0e97da6b216b5ac56f3c262087222..49c4d8e5acb7f0d6d2ba2b64ac5c3b9f4f0255ae 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -401,6 +401,46 @@ class Internationalization { return $locale; } + + // getIntDateFormatter($options) + // + // Setting up the IntlDateFormatter is pretty expensive, so cache it since + // there aren't many variations of the arguments passed to the constructor + static function getIntDateFormatter($options) { + static $cache = false; + global $cfg; + + // Set some defaults + $options['locale'] = $options['locale'] ?: self::getCurrentLocale(); + + // Generate signature key for options given + $k = md5(implode(':', array_filter( + array_intersect_key($options, + array_flip(array( + 'locale', + 'daytype', + 'timetype', + 'timezone', + 'pattern') + ))))); + + // We if we have it cached + if (isset($cache[$k]) && $cache[$k]) + return $cache[$k]; + + // Create formatter && cache + $cache[$k] = $formatter = new IntlDateFormatter( + $options['locale'], + $options['daytype'] ?: null, + $options['timetype'] ?: null, + $options['timezone'] ?: null, + $options['calendar'] ?: IntlDateFormatter::GREGORIAN, + $options['pattern'] ?: null + ); + + return $formatter; + } + static function rfc1766($what) { if (is_array($what)) return array_map(array(get_called_class(), 'rfc1766'), $what); diff --git a/include/class.misc.php b/include/class.misc.php index 9b706168db939d6cf30b9b721e3ec1c9fa98c95b..b5ce887053eba3be836dbc976aa23447c2b8ca80 100644 --- a/include/class.misc.php +++ b/include/class.misc.php @@ -167,31 +167,32 @@ class Misc { function timeDropdown($hr=null, $min =null,$name='time') { global $cfg; - $hr =is_null($hr)?0:$hr; - $min =is_null($min)?0:$min; - //normalize; - if($hr>=24) - $hr=$hr%24; - elseif($hr<0) - $hr=0; - - if($min>=45) - $min=45; - elseif($min>=30) - $min=30; - elseif($min>=15) - $min=15; + if ($hr >= 24) + $hr = $hr%24; + elseif ($hr < 0) + $hr = 0; + elseif ($hr) + $hr = (int) $hr; + else // Default to 5pm + $hr = 17; + + if ($min >= 45) + $min = 45; + elseif ($min >= 30) + $min = 30; + elseif ($min >= 15) + $min = 15; else - $min=0; + $min = 0; $time = Misc::user2gmtime(mktime(0,0,0)); ob_start(); echo sprintf('<select name="%s" id="%s" style="display:inline-block;width:auto">',$name,$name); - echo '<option value="" selected>'.__('Time').'</option>'; + echo '<option value="" selected="selected">—'.__('Time').'—</option>'; for($i=23; $i>=0; $i--) { for ($minute=45; $minute>=0; $minute-=15) { - $sel=($hr==$i && $min==$minute)?'selected="selected"':''; + $sel=($hr===$i && $min===$minute) ? 'selected="selected"' : ''; $_minute=str_pad($minute, 2, '0',STR_PAD_LEFT); $_hour=str_pad($i, 2, '0',STR_PAD_LEFT); $disp = Format::time($time + ($i*3600 + $minute*60 + 1), false); diff --git a/include/class.timezone.php b/include/class.timezone.php index eb2afca6771cc7e545e0805b9a0dde325fe4888f..1f24531f2151e4b4a8b1bb3fae7746c994570633 100644 --- a/include/class.timezone.php +++ b/include/class.timezone.php @@ -173,23 +173,8 @@ class DbTimezone { // timezone in PHP which honors BST (British Summer Time) return 'Europe/London'; } - // Forbid timezone abbreviations like 'CDT' - elseif ($TZ !== 'UTC' && strpos($TZ, '/') === false) { - // Attempt to lookup based on the abbreviation - if (!($TZ = timezone_name_from_abbr($TZ))) - // Abbreviation doesn't point to anything valid - return false; - } - // SYSTEM does not describe a time zone, ensure we have a valid zone - // by attempting to create an instance of DateTimeZone() - try { - new DateTimeZone($TZ); - return $TZ; - } - catch (Exception $ex) { - return false; - } + return Format::timezone($TZ); } function dst_dates($year) {