* (bug 505) Time zones can now be specified by location in user preferences,
authorBrion Vibber <brion@users.mediawiki.org>
Mon, 22 Dec 2008 21:55:09 +0000 (21:55 +0000)
committerBrion Vibber <brion@users.mediawiki.org>
Mon, 22 Dec 2008 21:55:09 +0000 (21:55 +0000)
  avoiding the need to manually update for DST. Patch by Brad Jorsch.

RELEASE-NOTES
includes/DefaultSettings.php
includes/specials/SpecialPreferences.php
languages/Language.php
languages/messages/MessagesEn.php
skins/common/prefs.js

index da61207..e6cc2ae 100644 (file)
@@ -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 ===
 
index 57e304f..2b12f1c 100644 (file)
@@ -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:
index 3768055..ab50486 100644 (file)
@@ -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)' ) ) ) .
                        "<tr>
                                <td></td>
                                <td class='mw-submit'>" .
index b238c5f..505aa82 100644 (file)
@@ -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
index 4b966aa..f7db217 100644 (file)
@@ -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',
index d9a612f..c2554c0 100644 (file)
@@ -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);