From c672ce603272d5206f0967f70f2474b4c09ce43a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 22 Dec 2008 21:55:09 +0000 Subject: [PATCH] * (bug 505) Time zones can now be specified by location in user preferences, avoiding the need to manually update for DST. Patch by Brad Jorsch. --- RELEASE-NOTES | 3 + includes/DefaultSettings.php | 2 +- includes/specials/SpecialPreferences.php | 153 ++++++++++++++++++----- languages/Language.php | 53 ++++---- languages/messages/MessagesEn.php | 3 + skins/common/prefs.js | 45 +++++++ 6 files changed, 204 insertions(+), 55 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index da61207d18..e6cc2ae7f9 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -239,6 +239,9 @@ The following extensions are migrated into MediaWiki 1.14: * Added 'UserCryptPassword' and 'UserComparePasswords' hooks to allow extensions to implement their own password hashing methods. * (bug 16760) Add CSS-class to action links of Special:Log +* (bug 505) Time zones can now be specified by location in user preferences, + avoiding the need to manually update for DST. Patch by Brad Jorsch. + === Bug fixes in 1.14 === diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 57e304f158..2b12f1c1ea 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1444,7 +1444,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '191'; +$wgStyleVersion = '192'; # Server-side caching: diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 3768055aba..ab50486c95 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -24,7 +24,7 @@ class PreferencesForm { var $mQuickbar, $mStubs; var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick; var $mUserLanguage, $mUserVariant; - var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction; + var $mSearch, $mRecent, $mRecentDays, $mTimeZone, $mHourDiff, $mSearchLines, $mSearchChars, $mAction; var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize; var $mUnderline, $mWatchlistEdits; @@ -51,6 +51,7 @@ class PreferencesForm { $this->mSearch = $request->getVal( 'wpSearch' ); $this->mRecent = $request->getVal( 'wpRecent' ); $this->mRecentDays = $request->getVal( 'wpRecentDays' ); + $this->mTimeZone = $request->getVal( 'wpTimeZone' ); $this->mHourDiff = $request->getVal( 'wpHourDiff' ); $this->mSearchLines = $request->getVal( 'wpSearchLines' ); $this->mSearchChars = $request->getVal( 'wpSearchChars' ); @@ -170,34 +171,37 @@ class PreferencesForm { /** * Used to validate the user inputed timezone before saving it as - * 'timecorrection', will return '00:00' if fed bogus data. - * Note: It's not a 100% correct implementation timezone-wise, it will - * accept stuff like '14:30', + * 'timecorrection', will return 'System' if fed bogus data. * @access private - * @param string $s the user input + * @param string $tz the user input Zoneinfo timezone + * @param string $s the user input offset string * @return string */ - function validateTimeZone( $s ) { - if ( $s !== '' ) { - if ( strpos( $s, ':' ) ) { - # HH:MM - $array = explode( ':' , $s ); - $hour = intval( $array[0] ); - $minute = intval( $array[1] ); - } else { - $minute = intval( $s * 60 ); - $hour = intval( $minute / 60 ); - $minute = abs( $minute ) % 60; - } - # Max is +14:00 and min is -12:00, see: - # http://en.wikipedia.org/wiki/Timezone - $hour = min( $hour, 14 ); - $hour = max( $hour, -12 ); - $minute = min( $minute, 59 ); - $minute = max( $minute, 0 ); - $s = sprintf( "%02d:%02d", $hour, $minute ); + function validateTimeZone( $tz, $s ) { + $data = explode( '|', $tz, 3 ); + switch ( $data[0] ) { + case 'ZoneInfo': + case 'System': + return $tz; + case 'Offset': + default: + $data = explode( ':', $s, 2 ); + $minDiff = 0; + if( count( $data ) == 2 ) { + $data[0] = intval( $data[0] ); + $data[1] = intval( $data[1] ); + $minDiff = abs( $data[0] ) * 60 + $data[1]; + if ( $data[0] < 0 ) $minDiff = -$minDiff; + } else { + $minDiff = intval( $data[0] ) * 60; + } + + # Max is +14:00 and min is -12:00, see: + # http://en.wikipedia.org/wiki/Timezone + $minDiff = min( $minDiff, 840 ); # 14:00 + $minDiff = max( $minDiff, -720 ); # -12:00 + return 'Offset|'.$minDiff; } - return $s; } /** @@ -259,7 +263,7 @@ class PreferencesForm { $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) ); $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) ); $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) ); - $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mHourDiff, -12, 14 ) ); + $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mTimeZone, $this->mHourDiff ) ); $wgUser->setOption( 'imagesize', $this->mImageSize ); $wgUser->setOption( 'thumbsize', $this->mThumbSize ); $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) ); @@ -344,7 +348,7 @@ class PreferencesForm { * @access private */ function resetPrefs() { - global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName; + global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName, $wgLocalTZoffset; $this->mUserEmail = $wgUser->getEmail(); $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp(); @@ -364,7 +368,47 @@ class PreferencesForm { $this->mRows = $wgUser->getOption( 'rows' ); $this->mCols = $wgUser->getOption( 'cols' ); $this->mStubs = $wgUser->getOption( 'stubthreshold' ); - $this->mHourDiff = $wgUser->getOption( 'timecorrection' ); + + $tz = $wgUser->getOption( 'timecorrection' ); + $data = explode( '|', $tz, 3 ); + $minDiff = null; + switch ( $data[0] ) { + case 'ZoneInfo': + $this->mTimeZone = $tz; + # Check if the specified TZ exists, and change to 'Offset' if + # not. + if ( !function_exists('timezone_open') || @timezone_open( $data[2] ) === false ) { + $this->mTimeZone = 'Offset'; + $minDiff = intval( $data[1] ); + } + break; + case '': + case 'System': + $this->mTimeZone = 'System|'.$wgLocalTZoffset; + break; + case 'Offset': + $this->mTimeZone = 'Offset'; + $minDiff = intval( $data[1] ); + break; + default: + $this->mTimeZone = 'Offset'; + $data = explode( ':', $tz, 2 ); + if( count( $data ) == 2 ) { + $data[0] = intval( $data[0] ); + $data[1] = intval( $data[1] ); + $minDiff = abs( $data[0] ) * 60 + $data[1]; + if ( $data[0] < 0 ) $minDiff = -$minDiff; + } else { + $minDiff = intval( $data[0] ) * 60; + } + break; + } + if ( is_null( $minDiff ) ) { + $this->mHourDiff = ''; + } else { + $this->mHourDiff = sprintf( '%+03d:%02d', floor($minDiff/60), abs($minDiff)%60 ); + } + $this->mSearch = $wgUser->getOption( 'searchlimit' ); $this->mSearchLines = $wgUser->getOption( 'contextlines' ); $this->mSearchChars = $wgUser->getOption( 'contextchars' ); @@ -490,7 +534,7 @@ class PreferencesForm { global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress; global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication; global $wgContLanguageCode, $wgDefaultSkin, $wgCookieExpiration; - global $wgEmailConfirmToEdit, $wgEnableMWSuggest; + global $wgEmailConfirmToEdit, $wgEnableMWSuggest, $wgLocalTZoffset; $wgOut->setPageTitle( wfMsg( 'preferences' ) ); $wgOut->setArticleRelated( false ); @@ -908,18 +952,61 @@ class PreferencesForm { $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" ); } - $nowlocal = $wgLang->time( $now = wfTimestampNow(), true ); - $nowserver = $wgLang->time( $now, false ); + $nowlocal = Xml::openElement( 'span', array( 'id' => 'wpLocalTime' ) ) . + $wgLang->time( $now = wfTimestampNow(), true ) . + Xml::closeElement( 'span' ); + $nowserver = $wgLang->time( $now, false ) . + Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) ); $wgOut->addHTML( Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) . Xml::openElement( 'table' ) . $this->addRow( wfMsg( 'servertime' ), $nowserver ) . - $this->addRow( wfMsg( 'localtime' ), $nowlocal ) . + $this->addRow( wfMsg( 'localtime' ), $nowlocal ) + ); + $opt = Xml::openElement( 'select', array( + 'name' => 'wpTimeZone', + 'id' => 'wpTimeZone', + 'onchange' => 'javascript:updateTimezoneSelection(false)' ) ); + $opt .= Xml::option( wfMsg( 'timezoneuseserverdefault' ), "System|$wgLocalTZoffset", $this->mTimeZone === "System|$wgLocalTZoffset" ); + $opt .= Xml::option( wfMsg( 'timezoneuseoffset' ), 'Offset', $this->mTimeZone === 'Offset' ); + if ( function_exists( 'timezone_identifiers_list' ) ) { + $optgroup = ''; + $tzs = timezone_identifiers_list(); + sort( $tzs ); + $selZone = explode( '|', $this->mTimeZone, 3); + $selZone = ( $selZone[0] == 'ZoneInfo' ) ? $selZone[2] : null; + $now = date_create( 'now' ); + foreach ( $tzs as $tz ) { + $z = explode( '/', $tz, 2 ); + # timezone_identifiers_list() returns a number of + # backwards-compatibility entries. This filters them out of the + # list presented to the user. + if ( count( $z ) != 2 || !in_array( $z[0], array( 'Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific' ) ) ) continue; + if ( $optgroup != $z[0] ) { + if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' ); + $optgroup = $z[0]; + $opt .= Xml::openElement( 'optgroup', array( 'label' => $z[0] ) ); + } + $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 ); + $opt .= Xml::option( str_replace( '_', ' ', $tz ), "ZoneInfo|$minDiff|$tz", $selZone === $tz, array( 'label' => $z[1] ) ); + } + if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' ); + } + $opt .= Xml::closeElement( 'select' ); + $wgOut->addHTML( + $this->addRow( + Xml::label( wfMsg( 'timezoneselect' ), 'wpTimeZone' ), + $opt ) + ); + $wgOut->addHTML( $this->addRow( Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ), - Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) . + Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( + 'id' => 'wpHourDiff', + 'onfocus' => 'javascript:updateTimezoneSelection(true)', + 'onblur' => 'javascript:updateTimezoneSelection(false)' ) ) ) . " " . diff --git a/languages/Language.php b/languages/Language.php index b238c5fb9b..505aa82e5f 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -479,39 +479,50 @@ class Language { function userAdjust( $ts, $tz = false ) { global $wgUser, $wgLocalTZoffset; - if (!$tz) { + if ( $tz === false ) { $tz = $wgUser->getOption( 'timecorrection' ); } - # minutes and hours differences: - $minDiff = 0; - $hrDiff = 0; + $data = explode( '|', $tz, 3 ); - if ( $tz === '' ) { - # Global offset in minutes. - if( isset($wgLocalTZoffset) ) { - if( $wgLocalTZoffset >= 0 ) { - $hrDiff = floor($wgLocalTZoffset / 60); - } else { - $hrDiff = ceil($wgLocalTZoffset / 60); - } - $minDiff = $wgLocalTZoffset % 60; + if ( $data[0] == 'ZoneInfo' ) { + if ( function_exists( 'timezone_open' ) && @timezone_open( $data[2] ) !== false ) { + $date = date_create( $ts, timezone_open( 'UTC' ) ); + date_timezone_set( $date, timezone_open( $data[2] ) ); + $date = date_format( $date, 'YmdHis' ); + return $date; } - } elseif ( strpos( $tz, ':' ) !== false ) { - $tzArray = explode( ':', $tz ); - $hrDiff = intval($tzArray[0]); - $minDiff = intval($hrDiff < 0 ? -$tzArray[1] : $tzArray[1]); + # Unrecognized timezone, default to 'Offset' with the stored offset. + $data[0] = 'Offset'; + } + + $minDiff = 0; + if ( $data[0] == 'System' || $tz == '' ) { + # Global offset in minutes. + if( isset($wgLocalTZoffset) ) $minDiff = $wgLocalTZoffset; + } else if ( $data[0] == 'Offset' ) { + $minDiff = intval( $data[1] ); } else { - $hrDiff = intval( $tz ); + $data = explode( ':', $tz ); + if( count( $data ) == 2 ) { + $data[0] = intval( $data[0] ); + $data[1] = intval( $data[1] ); + $minDiff = abs( $data[0] ) * 60 + $data[1]; + if ( $data[0] < 0 ) $minDiff = -$minDiff; + } else { + $minDiff = intval( $data[0] ) * 60; + } } # No difference ? Return time unchanged - if ( 0 == $hrDiff && 0 == $minDiff ) { return $ts; } + if ( 0 == $minDiff ) return $ts; wfSuppressWarnings(); // E_STRICT system time bitching - # Generate an adjusted date + # Generate an adjusted date; take advantage of the fact that mktime + # will normalize out-of-range values so we don't have to split $minDiff + # into hours and minutes. $t = mktime( ( - (int)substr( $ts, 8, 2) ) + $hrDiff, # Hours + (int)substr( $ts, 8, 2) ), # Hours (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes (int)substr( $ts, 12, 2 ), # Seconds (int)substr( $ts, 4, 2 ), # Month diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 4b966aab9e..f7db217f43 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -1566,6 +1566,9 @@ please see math/README to configure.', 'timezonelegend' => 'Time zone', 'timezonetext' => '¹The number of hours your local time differs from server time (UTC).', 'localtime' => 'Local time', +'timezoneselect' => 'Timezone', +'timezoneuseserverdefault' => 'Use server default', +'timezoneuseoffset' => 'Other (specify offset)', 'timezoneoffset' => 'Offset¹', 'servertime' => 'Server time', 'guesstimezone' => 'Fill in from browser', diff --git a/skins/common/prefs.js b/skins/common/prefs.js index d9a612f90a..c2554c00d5 100644 --- a/skins/common/prefs.js +++ b/skins/common/prefs.js @@ -95,6 +95,7 @@ function unhidetzbutton() { if (tzb) { tzb.style.display = 'inline'; } + updateTimezoneSelection(false); } // in [-]HH:MM format... @@ -113,6 +114,50 @@ function fetchTimezone() { function guessTimezone(box) { document.getElementsByName("wpHourDiff")[0].value = fetchTimezone(); + updateTimezoneSelection(true); +} + +function updateTimezoneSelection(force_offset) { + var wpTimeZone = document.getElementsByName("wpTimeZone")[0]; + var wpHourDiff = document.getElementsByName("wpHourDiff")[0]; + var wpLocalTime = document.getElementById("wpLocalTime"); + var wpServerTime = document.getElementsByName("wpServerTime")[0]; + var minDiff = 0; + + if (force_offset) wpTimeZone.selectedIndex = 1; + if (wpTimeZone.selectedIndex == 1) { + wpHourDiff.disabled = false; + var diffArr = wpHourDiff.value.split(':'); + if (diffArr.length == 1) { + minDiff = parseInt(diffArr[0], 10) * 60; + } else { + minDiff = Math.abs(parseInt(diffArr[0], 10))*60 + parseInt(diffArr[1], 10); + if (parseInt(diffArr[0], 10) < 0) minDiff = -minDiff; + } + } else { + wpHourDiff.disabled = true; + var diffArr = wpTimeZone.options[wpTimeZone.selectedIndex].value.split('|'); + minDiff = parseInt(diffArr[1], 10); + } + if (isNaN(minDiff)) minDiff = 0; + var localTime = parseInt(wpServerTime.value, 10) + minDiff; + while (localTime < 0) localTime += 1440; + while (localTime >= 1440) localTime -= 1440; + + var hour = String(Math.floor(localTime/60)); + if (hour.length<2) hour = '0'+hour; + var min = String(localTime%60); + if (min.length<2) min = '0'+min; + changeText(wpLocalTime, hour+':'+min); + + if (wpTimeZone.selectedIndex != 1) { + hour = String(Math.abs(Math.floor(minDiff/60))); + if (hour.length<2) hour = '0'+hour; + if (minDiff < 0) hour = '-'+hour; + min = String(minDiff%60); + if (min.length<2) min = '0'+min; + wpHourDiff.value = hour+':'+min; + } } hookEvent("load", unhidetzbutton); -- 2.20.1