Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.266 diff -u -r1.266 CHANGELOG.txt --- CHANGELOG.txt 7 May 2008 07:05:55 -0000 1.266 +++ CHANGELOG.txt 9 May 2008 02:13:15 -0000 @@ -15,6 +15,13 @@ * Added support for language-aware searches. - Testing: * Added test framework and tests. +- Improved time zone support: + * Drupal now uses PHP's time zone database when rendering dates in local + time. Site-wide and user-configured time zone offsets have been converted + to time zone names, e.g. America/Buenos_Aires. + * In some cases the upgrade and install scripts do not choose the preferred + site or user time zone. The automatically-selected time zone can be + corrected at admin/settings/date-time or on the user edit page. - Removed ping module: * Contributed modules with similar functionality are available. - Refactored the "access rules" component of user module: Index: install.php =================================================================== RCS file: /cvs/drupal/drupal/install.php,v retrieving revision 1.118 diff -u -r1.118 install.php --- install.php 6 May 2008 12:18:44 -0000 1.118 +++ install.php 9 May 2008 02:13:15 -0000 @@ -1047,7 +1047,7 @@ $form['server_settings']['date_default_timezone'] = array( '#type' => 'select', '#title' => st('Default time zone'), - '#default_value' => 0, + '#default_value' => date_default_timezone_get(), '#options' => _system_zonelist(), '#description' => st('By default, dates in this site will be displayed in the chosen time zone.'), '#weight' => 5, Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.765 diff -u -r1.765 common.inc --- includes/common.inc 6 May 2008 12:18:45 -0000 1.765 +++ includes/common.inc 9 May 2008 02:13:15 -0000 @@ -1141,7 +1141,7 @@ * before a character to avoid interpreting the character as part of a date * format. * @param $timezone - * Time zone offset in seconds; if omitted, the user's time zone is used. + * Time zone identifier; if omitted, the user's time zone is used. * @param $langcode * Optional language code to translate to a language other than what is used * to display the page. @@ -1149,17 +1149,21 @@ * A translated date string in the requested format. */ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) { - if (!isset($timezone)) { + static $timezones = array(); + if (!$timezone) { global $user; - if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { + if (variable_get('configurable_timezones', 1) && $user->uid && $user->timezone) { $timezone = $user->timezone; } else { - $timezone = variable_get('date_default_timezone', 0); + $timezone = variable_get('date_default_timezone', 'UTC'); } } - - $timestamp += $timezone; + // Store DateTimeZone objects in an array rather than repeatedly + // contructing identical objects over the life of a request. + if (!isset($timezones[$timezone])) { + $timezones[$timezone] = timezone_open($timezone); + } switch ($type) { case 'small': @@ -1178,28 +1182,27 @@ $max = strlen($format); $date = ''; + // Create a DateTime object from the timestamp. + $date_time = date_create('@' . $timestamp); + // Set the time zone for the DateTime object. + date_timezone_set($date_time, $timezones[$timezone]); + for ($i = 0; $i < $max; $i++) { $c = $format[$i]; - if (strpos('AaDlM', $c) !== FALSE) { - $date .= t(gmdate($c, $timestamp), array(), $langcode); + if (strpos('AaeDlMT', $c) !== FALSE) { + $date .= t(date_format($date_time, $c), array(), $langcode); } else if ($c == 'F') { // Special treatment for long month names: May is both an abbreviation // and a full month name in English, but other languages have // different abbreviations. - $date .= trim(t('!long-month-name ' . gmdate($c, $timestamp), array('!long-month-name' => ''), $langcode)); + $date .= trim(t('!long-month-name ' . date_format($date_time, $c), array('!long-month-name' => ''), $langcode)); } - else if (strpos('BdgGhHiIjLmnsStTUwWYyz', $c) !== FALSE) { - $date .= gmdate($c, $timestamp); + else if (strpos('BcdGgHhIijLmNnOoPSstUuWwYyZz', $c) !== FALSE) { + $date .= date_format($date_time, $c); } else if ($c == 'r') { - $date .= format_date($timestamp - $timezone, 'custom', 'D, d M Y H:i:s O', $timezone, $langcode); - } - else if ($c == 'O') { - $date .= sprintf('%s%02d%02d', ($timezone < 0 ? '-' : '+'), abs($timezone / 3600), abs($timezone % 3600) / 60); - } - else if ($c == 'Z') { - $date .= $timezone; + $date .= format_date($timestamp, 'custom', 'D, d M Y H:i:s O', $timezone, $langcode); } else if ($c == '\\') { $date .= $format[++$i]; Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.75 diff -u -r1.75 system.admin.inc --- modules/system/system.admin.inc 7 May 2008 19:17:50 -0000 1.75 +++ modules/system/system.admin.inc 9 May 2008 02:13:15 -0000 @@ -1558,7 +1558,7 @@ $form['locale']['date_default_timezone'] = array( '#type' => 'select', '#title' => t('Default time zone'), - '#default_value' => variable_get('date_default_timezone', 0), + '#default_value' => variable_get('date_default_timezone', date_default_timezone_get()), '#options' => $zones, '#description' => t('Select the default site time zone.') ); Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.253 diff -u -r1.253 system.install --- modules/system/system.install 7 May 2008 19:34:24 -0000 1.253 +++ modules/system/system.install 9 May 2008 05:02:24 -0000 @@ -2995,6 +2995,36 @@ return $ret; } +/** + * Convert default time zone offset to default time zone name. + */ +function system_update_7008() { + $ret = array(); + // If the contributed Date module set a default time zone name, use + // this setting as the default time zone. + if ($timezone = variable_get('date_default_timezone_name', NULL)) { + variable_set('date_default_timezone', $timezone); + } + // If the contributed Event module has set a default site timezone + // use that information. + elseif ($timezone_id = variable_get('date_default_timezone_id', 0) && !empty($timezone_id)) { + $timezone = db_result(db_query("SELECT name FROM {event_timezone} t WHERE t.timezone=%d", $timezone_id)); + $timezone = str_replace(' ', '_', $timezone); + variable_set('date_default_timezone', $timezone); + } + // If the previous default time zone was a non-zero offset, guess the + // site's intended time zone based on that offset and the server's + // daylight saving time status. + elseif ($timezone = variable_get('date_default_timezone', 0)) { + variable_set('date_default_timezone', timezone_name_from_abbr('', intval($timezone), intval(date('I')))); + } + // Otherwise, set the default time zone to UTC. + else { + variable_set('date_default_timezone', 'UTC'); + } + drupal_set_message('The default time zone has been set to ' . check_plain(variable_get('date_default_timezone', 'UTC')) . '. Check the ' . l('date and time configuration page', 'admin/settings/date-time') . ' to configure it correctly.', 'warning'); + return $ret; +} /** * @} End of "defgroup updates-6.x-to-7.x" Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.602 diff -u -r1.602 system.module --- modules/system/system.module 7 May 2008 19:17:50 -0000 1.602 +++ modules/system/system.module 9 May 2008 02:59:11 -0000 @@ -694,6 +694,13 @@ $form['theme_select'] = system_theme_select_form(t('Selecting a different theme will change the look and feel of the site.'), isset($edit['theme']) ? $edit['theme'] : NULL, 2); if (variable_get('configurable_timezones', 1)) { + if (!$edit['timezone']) { + drupal_add_js('if (Drupal.jsEnabled) { + $(document).ready(function() { + Drupal.setDefaultTimezone(); + }); + }', 'inline'); + } $zones = _system_zonelist(); $form['timezone'] = array( '#type' => 'fieldset', @@ -704,7 +711,7 @@ $form['timezone']['timezone'] = array( '#type' => 'select', '#title' => t('Time zone'), - '#default_value' => strlen($edit['timezone']) ? $edit['timezone'] : variable_get('date_default_timezone', 0), + '#default_value' => $edit['timezone'] ? $edit['timezone'] : variable_get('date_default_timezone', 'UTC'), '#options' => $zones, '#description' => t('Select your current local time. Dates and times throughout this site will be displayed using this time zone.'), ); @@ -712,6 +719,15 @@ return $form; } + elseif ($type == 'login') { + return; + // If the user has a NULL time zone, notify the user to set a time zone. + if (!$user->timezone && variable_get('configurable_timezones', 1)) { + $destination = $_GET['destination']; + drupal_goto(base_path() .'user/'. $user->uid .'/edit'); + drupal_set_message(t('The user time zones on this site have been updated. !verify_your_time_zone_setting.', array('!verify_your_time_zone_setting' => l(t('Please verify the time zone setting in your user account.'), 'user/' . $user->uid. '/edit', array('query' => drupal_get_destination(), 'fragment' => 'edit-timezone'))))); + } + } } /** @@ -2009,11 +2025,15 @@ */ function _system_zonelist() { $timestamp = time(); - $zonelist = array(-11, -10, -9.5, -9, -8, -7, -6, -5, -4, -3.5, -3, -2, -1, 0, 1, 2, 3, 3.5, 4, 5, 5.5, 5.75, 6, 6.5, 7, 8, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.75, 13, 14); + $zonelist = timezone_identifiers_list(); $zones = array(); - foreach ($zonelist as $offset) { - $zone = $offset * 3600; - $zones[$zone] = format_date($timestamp, 'custom', variable_get('date_format_long', 'l, F j, Y - H:i') . ' O', $zone); + foreach ($zonelist as $zone) { + // Because many time zones exist in PHP only for backward + // compatibility reasons and should not be used, the list is + // filtered by a regular expression. + if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) { + $zones[$zone] = t('@zone: @date', array('@zone' => t($zone), '@date' => format_date($timestamp, 'custom', variable_get('date_format_long', 'l, F j, Y - H:i') . ' O', $zone))); + } } return $zones; } Index: modules/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.1 diff -u -r1.1 system.test --- modules/system/system.test 20 Apr 2008 18:23:31 -0000 1.1 +++ modules/system/system.test 9 May 2008 02:13:15 -0000 @@ -126,3 +126,44 @@ } } } + +class DateTimeTestCase extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('Date and time functionality'), + 'description' => t('Configure date and time settings. Test date formatting and time zone handling, including daylight saving time.'), + 'group' => t('System'), + ); + } + + function testDateTimeZone() { + // Setup date/time settings for Honolulu time. + variable_set('date_default_timezone', 'Pacific/Honolulu'); + variable_set('configurable_timezones', 0); + variable_set('date_format_medium', 'Y-m-d H:i:s O'); + + // Create some nodes with different authored-on dates. + $date1 = '2007-01-31 21:00:00 -1000'; + $date2 = '2007-07-31 21:00:00 -1000'; + $node1 = $this->drupalCreateNode(array('created' => strtotime($date1), 'type' => 'article')); + $node2 = $this->drupalCreateNode(array('created' => strtotime($date2), 'type' => 'article')); + + // Confirm date format and time zone. + $this->drupalGet("node/$node1->nid"); + $this->assertText('2007-01-31 21:00:00 -1000', 'Date should be identical, with GMT offset of -10 hours.'); + $this->drupalGet("node/$node2->nid"); + $this->assertText('2007-07-31 21:00:00 -1000', 'Date should be identical, with GMT offset of -10 hours.'); + + // Set time zone to Los Angeles time. + variable_set('date_default_timezone', 'America/Los_Angeles'); + + // Confirm date format and time zone. + $this->drupalGet("node/$node1->nid"); + $this->assertText('2007-01-31 23:00:00 -0800', 'Date should be two hours ahead, with GMT offset of -8 hours.'); + $this->drupalGet("node/$node2->nid"); + $this->assertText('2007-08-01 00:00:00 -0700', 'Date should be three hours ahead, with GMT offset of -7 hours.'); + } +} Index: modules/user/user.install =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.install,v retrieving revision 1.12 diff -u -r1.12 user.install --- modules/user/user.install 7 May 2008 19:34:24 -0000 1.12 +++ modules/user/user.install 9 May 2008 04:56:15 -0000 @@ -158,7 +158,7 @@ ), 'timezone' => array( 'type' => 'varchar', - 'length' => 8, + 'length' => 32, 'not null' => FALSE, 'description' => t("User's timezone."), ), @@ -292,6 +292,32 @@ } /** + * Convert user time zones from time zone offsets to time zone names. + */ +function user_update_7002() { + $ret = array(); + db_change_field($ret, 'users', 'timezone', 'timezone', array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'description' => t("User's timezone."))); + // If the contributed Date module has created a users.timezone_name + // column, use this data to set each user's time zone. + if (db_column_exists('users', 'timezone_name')) { + $ret[] = update_sql("UPDATE {users} SET timezone = timezone_name"); + } + // If the contributed Event module has stored user timezone information + // use that information to update the user accounts. + elseif (db_column_exists('users', 'timezone_id')) { + $results = db_query("SELECT DISTINCT timezone_id, name FROM {users} u LEFT JOIN {event_timezones} t on u.timezone_id = t.timezone"); + while ($row = db_fetch_object($results)) { + $name = str_replace(' ', '_', $row->name); + $ret[] = update_sql("UPDATE {users} SET timezone = '$name' WHERE timezone_id = ". $row->timezone_id); + } + } + else { + $ret[] = update_sql("UPDATE {users} SET timezone = NULL"); + } + return $ret; +} + +/** * @} End of "defgroup user-updates-6.x-to-7.x" * The next series of updates should start at 8000. */ Index: modules/user/user.js =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.js,v retrieving revision 1.6 diff -u -r1.6 user.js --- modules/user/user.js 12 Sep 2007 18:29:32 -0000 1.6 +++ modules/user/user.js 9 May 2008 02:13:15 -0000 @@ -168,11 +168,53 @@ }; /** - * Set the client's system timezone as default values of form fields. + * Set the client's system time zone as default values of form fields. */ Drupal.setDefaultTimezone = function() { - var offset = new Date().getTimezoneOffset() * -60; - $("#edit-date-default-timezone, #edit-user-register-timezone").val(offset); + var dateString = Date(); + // In some client environments, date strings include a time zone + // abbreviation, between 3 and 5 letters enclosed in parentheses, + // which can be interpreted by PHP. + var matches = dateString.match(/\(([A-Z]{3,5})\)/); + var abbreviation = matches ? matches[1] : 0; + + // For all other client environments, the abbreviation is set to "0" + // and the current offset from UTC and daylight saving time status are + // used to guess the time zone. + var dateNow = new Date(); + var offsetNow = dateNow.getTimezoneOffset() * -60; + + // Use January 1 and July 1 as test dates for determining daylight + // saving time status by comparing their offsets. + var dateJan = new Date(dateNow.getFullYear(), 0, 1, 12, 0, 0, 0); + var dateJul = new Date(dateNow.getFullYear(), 6, 1, 12, 0, 0, 0); + var offsetJan = dateJan.getTimezoneOffset() * -60; + var offsetJul = dateJul.getTimezoneOffset() * -60; + + // If the offset from UTC is identical on January 1 and July 1, + // assume daylight saving time is not used in this time zone. + if (offsetJan == offsetJul) { + var isDaylightSavingTime = ''; + } + // If the maximum annual offset is equivalent to the current offset, + // assume daylight saving time is in effect. + else if (Math.max(offsetJan, offsetJul) == offsetNow) { + var isDaylightSavingTime = 1; + } + // Otherwise, assume daylight saving time is not in effect. + else { + var isDaylightSavingTime = 0; + } + + // Submit request to the user/timezone callback and set the form field + // to the response time zone. + var path = 'user/timezone/' + abbreviation + '/' + offsetNow + '/' + isDaylightSavingTime; + // The client date is passed to the callback for debugging purposes. + $.getJSON(Drupal.settings.basePath, { q: path, date: dateString }, function (data) { + if (data) { + $("#edit-date-default-timezone, #edit-timezone").val(data); + } + }); }; /** Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.907 diff -u -r1.907 user.module --- modules/user/user.module 7 May 2008 19:34:24 -0000 1.907 +++ modules/user/user.module 9 May 2008 04:41:16 -0000 @@ -907,6 +907,14 @@ 'type' => MENU_CALLBACK, ); + $items['user/timezone'] = array( + 'title' => 'User timezone', + 'page callback' => 'user_timezone', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + 'file' => 'user.pages.inc', + ); + // Registration and login pages. $items['user'] = array( 'title' => 'User account', @@ -1332,11 +1340,32 @@ * * The user is redirected to the My Account page. Setting the destination in * the query string (as done by the user login block) overrides the redirect. + * + * The user's timezone is checked when they log in. If the value + * has not been set, they will be redirected to the edit form where + * they can select and set their timezone. */ function user_login_submit($form, &$form_state) { global $user; if ($user->uid) { - $form_state['redirect'] = 'user/' . $user->uid; + if (!$user->timezone && variable_get('configurable_timezones', 1)) { + // If the timezone is empty, override redirection provided by the + // login block to send the user first to the edit form, and then + // to the original destination. + if (isset($_REQUEST['destination'])) { + $destination = $_REQUEST['destination']; + unset($_REQUEST['destination']); + } + elseif (isset($_REQUEST['edit']['destination'])) { + $destination = $_REQUEST['edit']['destination']; + unset($_REQUEST['edit']['destination']); + } + drupal_set_message(t('Please verify and save your timezone settings.')); + $form_state['redirect'] = array('user/' . $user->uid .'/edit', array('destination' => $destination)); + } + else { + $form_state['redirect'] = 'user/' . $user->uid; + } return; } } @@ -2350,12 +2379,9 @@ } if (variable_get('configurable_timezones', 1)) { - // Override field ID, so we only change timezone on user registration, - // and never touch it on user edit pages. $form['timezone'] = array( '#type' => 'hidden', '#default_value' => variable_get('date_default_timezone', NULL), - '#id' => 'edit-user-register-timezone', ); // Add the JavaScript callback to automatically set the timezone. Index: modules/user/user.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.pages.inc,v retrieving revision 1.13 diff -u -r1.13 user.pages.inc --- modules/user/user.pages.inc 14 Apr 2008 17:48:43 -0000 1.13 +++ modules/user/user.pages.inc 9 May 2008 02:13:16 -0000 @@ -22,6 +22,22 @@ } /** + * Menu callback; Retrieve a JSON object containing a suggested time + * zone name. + */ +function user_timezone($abbreviation = '', $offset = -1, $is_daylight_saving_time = NULL) { + // An abbreviation of "0" passed in the callback arguments should be + // interpreted as the empty string. + $abbreviation = $abbreviation ? $abbreviation : ''; + $timezone = timezone_name_from_abbr($abbreviation, intval($offset), $is_daylight_saving_time); + // The client date is passed in for debugging purposes. + $date = isset($_GET['date']) ? $_GET['date'] : ''; + // Log a debug message. + watchdog('timezone', 'Detected time zone: %timezone; client date: %date; abbreviation: %abbreviation; offset: %offset; daylight saving time: %is_daylight_saving_time.', array('%timezone' => $timezone, '%date' => $date, '%abbreviation' => $abbreviation, '%offset' => $offset, '%is_daylight_saving_time' => $is_daylight_saving_time)); + drupal_json($timezone); +} + +/** * Form builder; Request a password reset. * * @ingroup forms Index: modules/user/user.test =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.test,v retrieving revision 1.3 diff -u -r1.3 user.test --- modules/user/user.test 7 May 2008 19:34:24 -0000 1.3 +++ modules/user/user.test 9 May 2008 02:13:16 -0000 @@ -23,9 +23,9 @@ // Set user registration to "Visitors can create accounts and no administrator approval is required." variable_set('user_register', 1); - // Enable user configurable timezone, and set the default timezone to +1 hour (or +3600 seconds). + // Enable user-configurable time zones, and set the default time zone to Brussels time. variable_set('configurable_timezones', 1); - variable_set('date_default_timezone', 3600); + variable_set('date_default_timezone', 'Europe/Brussels'); $edit = array(); $edit['name'] = $name = $this->randomName(); @@ -261,3 +261,59 @@ } } + + +class DateTimeZoneTestCase extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('User time zone'), + 'description' => t('Set a user time zone and verify that dates are displayed in local time.'), + 'group' => t('User'), + ); + } + + function testDateTimeZone() { + // Setup date/time settings for Los Angeles time. + variable_set('date_default_timezone', 'America/Los_Angeles'); + variable_set('configurable_timezones', 1); + variable_set('date_format_medium', 'Y-m-d H:i T'); + + // Create a user account and login. + $web_user = $this->drupalCreateUser(); + $this->drupalLogin($web_user); + + // Create some nodes with different authored-on dates. + $date1 = '2007-03-09 21:00:00 -0800'; + $date2 = '2007-03-11 01:00:00 -0800'; + $date3 = '2007-03-20 21:00:00 -0700'; + $node1 = $this->drupalCreateNode(array('created' => strtotime($date1), 'type' => 'article')); + $node2 = $this->drupalCreateNode(array('created' => strtotime($date2), 'type' => 'article')); + $node3 = $this->drupalCreateNode(array('created' => strtotime($date3), 'type' => 'article')); + + // Confirm date format and time zone. + $this->drupalGet("node/$node1->nid"); + $this->assertText('2007-03-09 21:00 PST', 'Date should be PST.'); + $this->drupalGet("node/$node2->nid"); + $this->assertText('2007-03-11 01:00 PST', 'Date should be PST.'); + $this->drupalGet("node/$node3->nid"); + $this->assertText('2007-03-20 21:00 PDT', 'Date should be PDT.'); + + // Change user time zone to Santiago time. + $edit = array(); + $edit['mail'] = $web_user->mail; + $edit['timezone'] = 'America/Santiago'; + $this->drupalPost("user/$web_user->uid/edit", $edit, t('Save')); + $this->assertText(t('The changes have been saved.'), t('Time zone changed to Santiago time.')); + + // Confirm date format and time zone. + $this->drupalGet("node/$node1->nid"); + $this->assertText('2007-03-10 02:00 CLST', 'Date should be Chile summer time; five hours ahead of PST.'); + $this->drupalGet("node/$node2->nid"); + $this->assertText('2007-03-11 05:00 CLT', 'Date should be Chile time; four hours ahead of PST'); + $this->drupalGet("node/$node3->nid"); + $this->assertText('2007-03-21 00:00 CLT', 'Date should be Chile time; three hours ahead of PDT.'); + } +}