Merge 1.3.0 changes back to master.
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java
index 92bc541..d1fc93d 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java
@@ -24,6 +24,7 @@
 import com.google.inject.name.Names;
 import java.io.File;
 import java.util.Properties;
+import java.util.TimeZone;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.mysql.conf.MySQLDriver;
 import org.apache.guacamole.auth.mysql.conf.MySQLEnvironment;
@@ -115,6 +116,11 @@
         // Get the MySQL-compatible driver to use.
         mysqlDriver = environment.getMySQLDriver();
 
+        // If timezone is present, set it.
+        TimeZone serverTz = environment.getServerTimeZone();
+        if (serverTz != null)
+            driverProperties.setProperty("serverTimezone", serverTz.getID());
+
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
index f452319..31f04e5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
@@ -23,6 +23,7 @@
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.SQLException;
+import java.util.TimeZone;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
 import org.slf4j.Logger;
@@ -393,4 +394,18 @@
                 false);
     }
 
+    /**
+     * Return the server timezone if configured in guacamole.properties, or
+     * null if the configuration option is not present.
+     * 
+     * @return
+     *     The server timezone as configured in guacamole.properties.
+     * 
+     * @throws GuacamoleException 
+     *     If an error occurs retrieving the configuration value.
+     */
+    public TimeZone getServerTimeZone() throws GuacamoleException {
+        return getProperty(MySQLGuacamoleProperties.SERVER_TIMEZONE);
+    }
+
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java
index 8318b89..925f82a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java
@@ -24,6 +24,7 @@
 import org.apache.guacamole.properties.FileGuacamoleProperty;
 import org.apache.guacamole.properties.IntegerGuacamoleProperty;
 import org.apache.guacamole.properties.StringGuacamoleProperty;
+import org.apache.guacamole.properties.TimeZoneGuacamoleProperty;
 
 /**
  * Properties used by the MySQL Authentication plugin.
@@ -251,6 +252,16 @@
     
         @Override
         public String getName() { return "mysql-auto-create-accounts"; }
+    };
+
+    /**
+     * The time zone of the MySQL database server.
+     */
+    public static final TimeZoneGuacamoleProperty SERVER_TIMEZONE =
+            new TimeZoneGuacamoleProperty() {
+                
+        @Override
+        public String getName() { return "mysql-server-timezone"; }
                 
     };
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/properties/TimeZoneGuacamoleProperty.java b/guacamole-ext/src/main/java/org/apache/guacamole/properties/TimeZoneGuacamoleProperty.java
new file mode 100644
index 0000000..a294bda
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/properties/TimeZoneGuacamoleProperty.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.properties;
+
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+
+/**
+ * A GuacamoleProperty whose value is a TimeZone.
+ */
+public abstract class TimeZoneGuacamoleProperty
+        implements GuacamoleProperty<TimeZone> {
+    
+    /**
+     * A regex that matches valid variants of GMT timezones.
+     */
+    public static final Pattern GMT_REGEX =
+            Pattern.compile("^GMT([+-](0|00)((:)?00)?)?$");
+    
+    @Override
+    public TimeZone parseValue(String value) throws GuacamoleException {
+        
+        // Nothing in, nothing out
+        if (value == null || value.isEmpty())
+            return null;
+        
+        // Attempt to return the TimeZone of the provided string value.
+        TimeZone tz = TimeZone.getTimeZone(value);
+        
+        // If the input is not GMT, but the output is GMT, the TimeZone is not
+        // valid and we throw an exception.
+        if (!GMT_REGEX.matcher(value).matches()
+                && GMT_REGEX.matcher(tz.getID()).matches())
+            throw new GuacamoleServerException("Property \"" + getName()
+                + "\" does not specify a valid time zone.");
+
+        return tz;
+        
+    }
+    
+}
diff --git a/guacamole-ext/src/test/java/org/apache/guacamole/properties/TimeZoneGuacamolePropertyTest.java b/guacamole-ext/src/test/java/org/apache/guacamole/properties/TimeZoneGuacamolePropertyTest.java
new file mode 100644
index 0000000..4c4ced1
--- /dev/null
+++ b/guacamole-ext/src/test/java/org/apache/guacamole/properties/TimeZoneGuacamolePropertyTest.java
@@ -0,0 +1,258 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.properties;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.TimeZone;
+import org.apache.guacamole.GuacamoleException;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+/**
+ * Tests that validate Time Zone property input.
+ */
+public class TimeZoneGuacamolePropertyTest {
+    
+    /**
+     * An array of valid TimeZones that should be correct parsed by the TimeZone
+     * property, returning either the same or synonymous zone.
+     */
+    private static final List<String> TZ_TEST_VALID = Arrays.asList(
+            "America/Los_Angeles",
+            "America/New_York",
+            "Australia/Sydney",
+            "Africa/Johannesburg",
+            "Asia/Shanghai"
+    );
+    
+    /**
+     * An array of invalid timezone names that should be parsed to GMT, which
+     * should cause an exception to be thrown by the TimeZone property.
+     */
+    private static final List<String> TZ_TEST_INVALID = Arrays.asList(
+            "Chips/Guacamole",
+            "Chips/Queso",
+            "Chips/Salsa",
+            "Mashed/Avacado",
+            "Pico/De_Guayo"
+    );
+    
+    /**
+     * An array of valid GMT specifications that should be correctly parsed
+     * by the TimeZone property as GMT.
+     */
+    private static final List<String> TZ_GMT_VALID = Arrays.asList(
+            "GMT",
+            "GMT-0000",
+            "GMT+000",
+            "GMT+00:00",
+            "GMT-0:00",
+            "GMT+0"
+    );
+    
+    /**
+     * An array of invalid GMT specifications that should cause an exception to
+     * be thrown for the TimeZone property.
+     */
+    private static final List<String> TZ_GMT_INVALID = Arrays.asList(
+            "GMTx0000",
+            "GMT=00:00",
+            "GMT0:00",
+            "GMT+000000",
+            "GMT-000:000",
+            "GMT100"
+    );
+    
+    /**
+     * An array of custom GMT offsets that should evaluate correctly for
+     * the TimeZone property.
+     */
+    private static final List<String> TZ_CUSTOM_VALID = Arrays.asList(
+            "GMT-23:59",
+            "GMT+01:30",
+            "GMT-00:30",
+            "GMT-11:25"
+    );
+    
+    /**
+     * An array of invalid custom GMT offsets that should cause an exception
+     * to be thrown by the TimeZone property.
+     */
+    private static final List<String> TZ_CUSTOM_INVALID = Arrays.asList(
+            "GMT-9999",
+            "GMT+2500",
+            "GMT+29:30",
+            "GMT-1:99",
+            "GMT+10:65"
+    );
+    
+    /**
+     * The list of all available timezones that are known to the TimeZone class.
+     */
+    private static final List<String> TZ_AVAIL_IDS =
+            Arrays.asList(TimeZone.getAvailableIDs());
+    
+    /**
+     * An example TimeZoneGuacamoleProperty for testing how various possible
+     * TimeZone values will be parsed.
+     */
+    private static final TimeZoneGuacamoleProperty WHERE_IN_WORLD =
+            new TimeZoneGuacamoleProperty() {
+            
+        @Override
+        public String getName() {
+            return "carmen-sandiego";
+        }
+        
+    };
+    
+    /**
+     * Tests to verify that each of the items in this list returns a valid,
+     * non-GMT timezone.
+     * 
+     * @throws GuacamoleException 
+     *     If a test value fails to parse correctly as a non-GMT timezone.
+     */
+    @Test
+    public void testValidTZs() throws GuacamoleException {
+        for (String tzStr : TZ_TEST_VALID) {
+            String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+            assertFalse(TimeZoneGuacamoleProperty.GMT_REGEX.matcher(tzId).matches());
+        }
+    }
+    
+    /**
+     * Tests invalid time zones to make sure that they produce the desired
+     * result, which is an exception thrown failing to parse the value.
+     */
+    @Test
+    public void testInvalidTZs() {
+        TZ_TEST_INVALID.forEach((tzStr) -> {
+            try {
+                String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+                fail("Invalid TimeZoneGuacamoleProperty should fail to parse with an exception.");
+            }
+            catch (GuacamoleException e) {
+                String msg = e.getMessage();
+                assertTrue(msg.contains("does not specify a valid time zone"));
+            }
+        });
+    }
+    
+    /**
+     * Tests a list of strings that should be valid representations of the GMT
+     * time zone, throwing an exception if an invalid String is found.
+     * 
+     * @throws GuacamoleException 
+     *     If the test value incorrectly fails to parse as a valid GMT string.
+     */
+    @Test
+    public void testValidGMT() throws GuacamoleException {
+        for (String tzStr : TZ_GMT_VALID) {
+            String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+            assertNotNull(tzId);
+        }
+    }
+    
+    /**
+     * Tests various invalid GMT representations to insure that parsing of these
+     * values fails and the expected GuacamoleException is thrown.
+     */
+    @Test
+    public void testInvalidGMT() {
+        TZ_GMT_INVALID.forEach((tzStr) -> {
+            try {
+                String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+                fail("Invalid GMT value \"" + tzStr + "\" for TimeZoneGuacamoleProperty should fail to parse with an exception.");
+            }
+            catch (GuacamoleException e) {
+                String msg = e.getMessage();
+                assertTrue(msg.contains("does not specify a valid time zone"));
+            }
+        });
+    }
+    
+    /**
+     * Tests several custom offsets from GMT to make sure that they are returned
+     * as valid TimeZone objects.
+     * 
+     * @throws GuacamoleException 
+     *     If the test unexpectedly fails because a custom offset throws an
+     *     exception as an invalid TimeZone.
+     */
+    @Test
+    public void testValidCustomTz() throws GuacamoleException {
+        for (String tzStr : TZ_CUSTOM_VALID) {
+            String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+            assertNotNull(tzId);
+        }
+    }
+    
+    /**
+     * Tests several invalid custom timezone offsets to make sure that they are
+     * not accepted as valid timezones.
+     */
+    @Test
+    public void testInvalidCustomTz() {
+        TZ_CUSTOM_INVALID.forEach((tzStr) -> {
+            try {
+                String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+                fail("Invalid custom time zone value \"" + tzStr + "\" for TimeZoneGuacamoleProperty should fail to parse with an exception.");
+            }
+            catch (GuacamoleException e) {
+                String msg = e.getMessage();
+                assertTrue(msg.contains("does not specify a valid time zone"));
+            }
+        });
+    }
+    
+    /**
+     * Tests the list of available identifiers provided by the TimeZone class
+     * to make sure that all identifiers provided pass through successfully and
+     * do not yield unexpected results.
+     * 
+     * @throws GuacamoleException 
+     *     If the test fails unexpectedly because the timezone is not recognized
+     *     and is converted to GMT.
+     */
+    public void availTzCheck() throws GuacamoleException {
+        for (String tzStr : TZ_AVAIL_IDS) {
+            String tzId = WHERE_IN_WORLD.parseValue(tzStr).getID();
+            assertNotNull(tzId);
+            assertTrue(tzId.equals(tzStr));
+        }
+    }
+    
+    /**
+     * Tests parse of null input values to make sure the resuling parsed value
+     * is also null.
+     * 
+     * @throws GuacamoleException
+     *     If the test unexpectedly fails parsing a null value instead
+     *     recognizing it as an invalid value.
+     */
+    @Test
+    public void nullTzCheck() throws GuacamoleException {
+        assertNull(WHERE_IN_WORLD.parseValue(null));
+    }
+    
+    
+}