Commit 53f53b6d authored by webchick's avatar webchick
Browse files

#11077 by mfb, KarenS, macgirvin, and jjkd: Can you say Daylight Savings Time?...

#11077 by mfb, KarenS, macgirvin, and jjkd: Can you say Daylight Savings Time? I bet you didn't think Drupal ever would! :)
parent 07211d40
......@@ -37,6 +37,16 @@ Drupal 7.0, xxxx-xx-xx (development version)
* 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. Africa/Abidjan.
* In some cases the upgrade and install scripts do not choose the preferred
site default time zone. The automatically-selected time zone can be
corrected at admin/settings/date-time.
* If your site is being upgraded from Drupal 6 and you do not have the
contributed date or event modules installed, user time zone settings will
fallback to the system time zone and will have to be reconfigured by each user.
- Removed ping module:
* Contributed modules with similar functionality are available.
- Refactored the "access rules" component of user module:
......
......@@ -1340,7 +1340,7 @@ function format_interval($timestamp, $granularity = 2, $langcode = NULL) {
* 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.
......@@ -1348,17 +1348,21 @@ function format_interval($timestamp, $granularity = 2, $langcode = NULL) {
* A translated date string in the requested format.
*/
function format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) {
static $timezones = array();
if (!isset($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':
......@@ -1377,28 +1381,27 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL
$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);
}
elseif ($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));
}
elseif (strpos('BdgGhHiIjLmnsStTUwWYyz', $c) !== FALSE) {
$date .= gmdate($c, $timestamp);
elseif (strpos('BcdGgHhIijLmNnOoPSstUuWwYyZz', $c) !== FALSE) {
$date .= date_format($date_time, $c);
}
elseif ($c == 'r') {
$date .= format_date($timestamp - $timezone, 'custom', 'D, d M Y H:i:s O', $timezone, $langcode);
}
elseif ($c == 'O') {
$date .= sprintf('%s%02d%02d', ($timezone < 0 ? '-' : '+'), abs($timezone / 3600), abs($timezone % 3600) / 60);
}
elseif ($c == 'Z') {
$date .= $timezone;
$date .= format_date($timestamp, 'custom', 'D, d M Y H:i:s O', $timezone, $langcode);
}
elseif ($c == '\\') {
$date .= $format[++$i];
......
......@@ -726,6 +726,8 @@ function install_tasks($profile, $task) {
// Add JavaScript validation.
_user_password_dynamic_validation();
drupal_add_js(drupal_get_path('module', 'system') . '/system.js');
// Add JavaScript time zone detection.
drupal_add_js('misc/timezone.js');
// We add these strings as settings because JavaScript translation does not
// work on install time.
drupal_add_js(array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail')), 'cleanURL' => array('success' => st('Your server has been successfully tested to support this feature.'), 'failure' => st('Your system configuration does not currently support this feature. The <a href="http://drupal.org/node/15365">handbook page on Clean URLs</a> has additional troubleshooting information.'), 'testing' => st('Testing clean URLs...'))), 'setting');
......@@ -734,7 +736,6 @@ function install_tasks($profile, $task) {
if (Drupal.jsEnabled) {
$(document).ready(function() {
Drupal.cleanURLsInstallCheck();
Drupal.setDefaultTimezone();
});
}', 'inline');
// Build menu to allow clean URL check.
......@@ -1081,10 +1082,11 @@ function install_configure_form(&$form_state, $url) {
$form['server_settings']['date_default_timezone'] = array(
'#type' => 'select',
'#title' => st('Default time zone'),
'#default_value' => 0,
'#options' => _system_zonelist(),
'#default_value' => date_default_timezone_get(),
'#options' => system_time_zones(),
'#description' => st('By default, dates in this site will be displayed in the chosen time zone.'),
'#weight' => 5,
'#attributes' => array('class' => 'timezone-detect'),
);
$form['server_settings']['clean_url'] = array(
......
......@@ -1532,7 +1532,7 @@ function system_date_time_settings() {
drupal_add_js(array('dateTime' => array('lookup' => url('admin/settings/date-time/lookup'))), 'setting');
// Date settings:
$zones = _system_zonelist();
$zones = system_time_zones();
// Date settings: possible date formats
$date_short = array('Y-m-d H:i', 'm/d/Y - H:i', 'd/m/Y - H:i', 'Y/m/d - H:i',
......@@ -1567,19 +1567,11 @@ function system_date_time_settings() {
$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.')
);
$form['locale']['configurable_timezones'] = array(
'#type' => 'radios',
'#title' => t('User-configurable time zones'),
'#default_value' => variable_get('configurable_timezones', 1),
'#options' => array(t('Disabled'), t('Enabled')),
'#description' => t('When enabled, users can set their own time zone and dates will be displayed accordingly.')
);
$form['locale']['date_first_day'] = array(
'#type' => 'select',
'#title' => t('First day of week'),
......@@ -1588,6 +1580,42 @@ function system_date_time_settings() {
'#description' => t('The first day of the week for calendar views.')
);
$form['timezone'] = array(
'#type' => 'fieldset',
'#title' => t('User time zones'),
);
$form['timezone']['configurable_timezones'] = array(
'#type' => 'radios',
'#title' => t('User-configurable time zones'),
'#default_value' => variable_get('configurable_timezones', 1),
'#options' => array(0 => t('Disabled'), 1 => t('Enabled')),
'#description' => t('When enabled, users can set their own time zone and dates will be displayed accordingly.')
);
$form['timezone']['user_default_timezone'] = array(
'#type' => 'radios',
'#title' => t('User time zone defaults'),
'#default_value' => variable_get('user_default_timezone', DRUPAL_USER_TIMEZONE_DEFAULT),
'#options' => array(
DRUPAL_USER_TIMEZONE_DEFAULT => t('New users will be set to the default time zone at registration.'),
DRUPAL_USER_TIMEZONE_EMPTY => t('New users will get an empty time zone at registration.'),
DRUPAL_USER_TIMEZONE_SELECT => t('New users will select their own time zone at registration.'),
),
'#description' => t('Method for setting user time zones at registration when user-configurable time zones are enabled. This only affects the initial time zone setting for a new registration. Users will be able to change their time zone any time they edit their account.')
);
$form['timezone']['empty_timezone_message'] = array(
'#type' => 'radios',
'#title' => t('Empty user time zones'),
'#default_value' => variable_get('empty_timezone_message', 0),
'#options' => array(
0 => t('Ignore empty user time zones.'),
1 => t('Remind users at login if their time zone is not set.'),
),
'#description' => t('Handling for empty user time zones when user-configurable time zones are enabled. Use this option to help ensure that users set the correct time zone.')
);
$form['date_formats'] = array(
'#type' => 'fieldset',
'#title' => t('Formatting'),
......
......@@ -3096,8 +3096,48 @@ function system_update_7012() {
return $ret;
}
/**
* Convert default time zone offset to default time zone name.
*/
function system_update_7013() {
$ret = array();
$timezone = NULL;
$timezones = system_time_zones();
// If the contributed Date module set a default time zone name, use this
// setting as the default time zone.
if (($timezone_name = variable_get('date_default_timezone_name', NULL)) && isset($timezones[$timezone_name])) {
$timezone = $timezone_name;
}
// If the contributed Event module has set a default site time zone, look up
// the time zone name and use it as the default time zone.
if (!$timezone && ($timezone_id = variable_get('date_default_timezone_id', 0))) {
try {
$timezone_name = db_result(db_query('SELECT name FROM {event_timezones} WHERE timezone = :timezone_id', array(':timezone_id' => $timezone_id)));
if (($timezone_name = str_replace(' ', '_', $timezone_name)) && isset($timezones[$timezone_name])) {
$timezone = $timezone_name;
}
}
catch (PDOException $e) {
// Ignore error if event_timezones table does not exist or unexpected
// schema found.
}
}
// 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.
if (!$timezone && ($offset = variable_get('date_default_timezone', 0)) && ($timezone_name = timezone_name_from_abbr('', intval($offset), date('I'))) && isset($timezones[$timezone_name])) {
$timezone = $timezone_name;
}
// Otherwise, the default time zone offset was zero, which is UTC.
if (!$timezone) {
$timezone = 'UTC';
}
variable_set('date_default_timezone', $timezone);
drupal_set_message('The default time zone has been set to <em>' . check_plain($timezone) . '</em>. Please 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"
* The next series of updates should start at 8000.
*/
......@@ -48,7 +48,10 @@ Drupal.cleanURLsInstallCheck = function() {
var url = location.protocol +"//"+ location.host + Drupal.settings.basePath +"admin/settings/clean-urls/check";
$("#clean-url .description").append('<span><div id="testing">'+ Drupal.settings.cleanURL.testing +"</div></span>");
$("#clean-url.install").css("display", "block");
// Submit a synchronous request to avoid database errors associated with
// concurrent requests during install.
$.ajax({
async: false,
url: url,
dataType: 'json',
success: function () {
......
......@@ -41,6 +41,21 @@
*/
define('DRUPAL_MAXIMUM_TEMP_FILE_AGE', 21600);
/**
* New users will be set to the default time zone at registration.
*/
define('DRUPAL_USER_TIMEZONE_DEFAULT', 0);
/**
* New users will get an empty time zone at registration.
*/
define('DRUPAL_USER_TIMEZONE_EMPTY', 1);
/**
* New users will select their own timezone at registration.
*/
define('DRUPAL_USER_TIMEZONE_SELECT', 2);
/**
* Implementation of hook_help().
*/
......@@ -356,6 +371,12 @@ function system_menu() {
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['system/timezone'] = array(
'title' => 'Time zone',
'page callback' => 'system_timezone',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['admin'] = array(
'title' => 'Administer',
'access arguments' => array('access administration pages'),
......@@ -591,7 +612,7 @@ function system_menu() {
);
$items['admin/settings/date-time'] = array(
'title' => 'Date and time',
'description' => "Settings for how Drupal displays date and time, as well as the system's default timezone.",
'description' => "Settings for how Drupal displays date and time, as well as the system's default time zone.",
'page callback' => 'drupal_get_form',
'page arguments' => array('system_date_time_settings'),
'access arguments' => array('administer site configuration'),
......@@ -736,28 +757,67 @@ function system_preprocess_page(&$variables) {
function system_user_form(&$edit, &$user, $category = NULL) {
if ($category == 'account') {
$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)) {
$zones = _system_zonelist();
system_user_timezone($edit, $form);
}
return $form;
}
}
/**
* Implementation of hook_user_register().
*/
function system_user_register(&$edit, &$user, $category = NULL) {
if (variable_get('configurable_timezones', 1)) {
$form = array();
if (variable_get('user_default_timezone', DRUPAL_USER_TIMEZONE_DEFAULT) == DRUPAL_USER_TIMEZONE_SELECT) {
system_user_timezone($edit, $form);
}
else {
$form['timezone'] = array(
'#type' => 'fieldset',
'#title' => t('Locale settings'),
'#weight' => 6,
'#collapsible' => TRUE,
);
$form['timezone']['timezone'] = array(
'#type' => 'select',
'#title' => t('Time zone'),
'#default_value' => strlen($edit['timezone']) ? $edit['timezone'] : variable_get('date_default_timezone', 0),
'#options' => $zones,
'#description' => t('Select your current local time. Dates and times throughout this site will be displayed using this time zone.'),
'#type' => 'hidden',
'#value' => variable_get('user_default_timezone', DRUPAL_USER_TIMEZONE_DEFAULT) ? '' : variable_get('date_default_timezone', ''),
);
}
return $form;
}
}
/**
* Implementation of hook_user_login().
*/
function system_user_login(&$edit, &$user, $category = NULL) {
// If the user has a NULL time zone, notify them to set a time zone.
if (!$user->timezone && variable_get('configurable_timezones', 1) && variable_get('empty_timezone_message', 0)) {
drupal_set_message(t('Please configure your <a href="@user-edit">account time zone setting</a>.', array('@user-edit' => url("user/$user->uid/edit", array('query' => drupal_get_destination(), 'fragment' => 'edit-timezone')))));
}
}
/**
* Add the time zone field to the user edit and register forms.
*/
function system_user_timezone(&$edit, &$form) {
global $user;
$form['timezone'] = array(
'#type' => 'fieldset',
'#title' => t('Locale settings'),
'#weight' => 6,
'#collapsible' => TRUE,
);
$form['timezone']['timezone'] = array(
'#type' => 'select',
'#title' => t('Time zone'),
'#default_value' => $edit['timezone'] ? $edit['timezone'] : ($edit['uid'] == $user->uid ? variable_get('date_default_timezone', '') : ''),
'#options' => system_time_zones(($edit['uid'] != $user->uid)),
'#description' => t('Select the desired local time and time zone. Dates and times throughout this site will be displayed using this time zone.'),
);
if (!$edit['timezone'] && $edit['uid'] == $user->uid) {
$form['timezone']['#description'] = t('Your time zone setting will be automatically detected if possible. Please confirm the selection and click save.');
$form['timezone']['timezone']['#attributes'] = array('class' => 'timezone-detect');
drupal_add_js('misc/timezone.js');
}
}
/**
* Implementation of hook_block().
*
......@@ -2059,15 +2119,23 @@ function system_block_ip_action() {
/**
* Generate an array of time zones and their local time&date.
*/
function _system_zonelist() {
$timestamp = REQUEST_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);
$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);
*
* @param $blank
* If evaluates true, prepend an empty time zone option to the array.
*/
function system_time_zones($blank = NULL) {
$zonelist = timezone_identifiers_list();
$zones = $blank ? array('' => t('- None selected -')) : array();
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(REQUEST_TIME, 'custom', variable_get('date_format_long', 'l, F j, Y - H:i') . ' O', $zone)));
}
}
// Sort the translated time zones alphabetically.
asort($zones);
return $zones;
}
......@@ -2097,6 +2165,17 @@ function system_check_http_request() {
return $works;
}
/**
* Menu callback; Retrieve a JSON object containing a suggested time zone name.
*/
function system_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);
drupal_json($timezone);
}
/**
* Format the Powered by Drupal text.
*
......
......@@ -469,6 +469,50 @@ class PageNotFoundTestCase extends DrupalWebTestCase {
}
}
/**
* Tests generic date and time handling capabilities of Drupal.
*/
class DateTimeFunctionalTest extends DrupalWebTestCase {
function getInfo() {
return array(
'name' => t('Date and time'),
'description' => t('Configure date and time settings. Test date formatting and time zone handling, including daylight saving time.'),
'group' => t('System'),
);
}
/**
* Test time zones and DST handling.
*/
function testTimeZoneHandling() {
// 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', t('Date should be identical, with GMT offset of -10 hours.'));
$this->drupalGet("node/$node2->nid");
$this->assertText('2007-07-31 21:00:00 -1000', t('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', t('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', t('Date should be three hours ahead, with GMT offset of -7 hours.'));
}
}
class PageTitleFiltering extends DrupalWebTestCase {
protected $content_user;
protected $saved_title;
......@@ -532,4 +576,3 @@ class PageTitleFiltering extends DrupalWebTestCase {
$this->assertText(check_plain($edit['title']), 'Check to make sure tags in the node title are converted.');
}
}
......@@ -158,9 +158,9 @@ function user_schema() {
),
'timezone' => array(
'type' => 'varchar',
'length' => 8,
'length' => 32,
'not null' => FALSE,
'description' => "User's timezone.",
'description' => "User's time zone.",
),
'language' => array(
'type' => 'varchar',
......@@ -291,6 +291,74 @@ function user_update_7001() {
return $ret;
}
/**
* Convert user time zones from time zone offsets to time zone names.
*/
function user_update_7002(&$sandbox) {
$ret = array('#finished' => 0);
// Multi-part update.
if (!isset($sandbox['user_from'])) {
db_change_field($ret, 'users', 'timezone', 'timezone', array('type' => 'varchar', 'length' => 32, 'not null' => FALSE));
$sandbox['user_from'] = 0;
$sandbox['user_count'] = db_result(db_query("SELECT COUNT(uid) FROM {users}"));
$sandbox['user_not_migrated'] = 0;
}
else {
$timezones = system_time_zones();
// Update this many per page load.
$count = 10000;
$contributed_date_module = db_column_exists('users', 'timezone_name');
$contributed_event_module = db_column_exists('users', 'timezone_id');
$results = db_query_range("SELECT uid FROM {users} ORDER BY uid", array(), $sandbox['user_from'], $count);
foreach ($results as $account) {
$timezone = NULL;
// If the contributed Date module has created a users.timezone_name
// column, use this data to set each user's time zone.
if ($contributed_date_module) {
$date_timezone = db_query("SELECT timezone_name FROM {users} WHERE uid = :uid", array(':uid' => $account->uid))->fetchField();
if (isset($timezones[$date_timezone])) {
$timezone = $date_timezone;
}
}
// If the contributed Event module has stored user time zone information
// use that information to update the user accounts.
if (!$timezone && $contributed_event_module) {
try {
$event_timezone = db_query("SELECT t.name FROM {users} u LEFT JOIN {event_timezones} t ON u.timezone_id = t.timezone WHERE u.uid = :uid", array(':uid' => $account->uid))->fetchField();
$event_timezone = str_replace(' ', '_', $event_timezone);
if (isset($timezones[$event_timezone])) {
$timezone = $event_timezone;
}
}
catch (PDOException $e) {
// Ignore error if event_timezones table does not exist or unexpected
// schema found.
}
}
if ($timezone) {
db_query("UPDATE {users} SET timezone = :timezone WHERE uid = :uid", array(':timezone' => $timezone, ':uid' => $account->uid));
}
else {
$sandbox['user_not_migrated']++;
db_query("UPDATE {users} SET timezone = NULL WHERE uid = :uid", array(':uid' => $account->uid));
}
$sandbox['user_from']++;
}
$ret['#finished'] = $sandbox['user_from'] / $sandbox['user_count'];
if ($sandbox['user_from'] == $sandbox['user_count']) {
$ret[] = array('success' => TRUE, 'query' => "Migrate user time zones.");
if ($sandbox['user_not_migrated'] > 0) {
variable_set('empty_timezone_message', 1);
drupal_set_message('Some user time zones have been emptied and need to be set to the correct values. Use the new ' . l('time zone options', 'admin/settings/date-time') . ' to choose whether to remind users at login to set the correct time zone.', 'warning');
}
}
}
return $ret;
}
/**
* @} End of "defgroup user-updates-6.x-to-7.x"
* The next series of updates should start at 8000.
......
......@@ -159,14 +159,6 @@ Drupal.evaluatePasswordStrength = function (password) {
return { strength: strength, message: msg };
};
/**
* Set the client's system timezone 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);
};
/**
* On the admin/user/settings page, conditionally show all of the
* picture-related form elements depending on the current value of the
......
......@@ -2390,25 +2390,6 @@ function user_register() {
$form = array_merge($form, $extra);
}
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',
);