diff --git a/guacamole/pom.xml b/guacamole/pom.xml index 24cc59ed9..32f434520 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -154,6 +154,7 @@ translations/*.json index.html + verifyCachedVersion.js @@ -164,6 +165,7 @@ translations/*.json index.html + verifyCachedVersion.js diff --git a/guacamole/src/main/frontend/src/index.html b/guacamole/src/main/frontend/src/index.html index 9dab9ba36..7f007eb55 100644 --- a/guacamole/src/main/frontend/src/index.html +++ b/guacamole/src/main/frontend/src/index.html @@ -24,6 +24,7 @@ + @@ -34,7 +35,7 @@ <% } %> - + @@ -105,7 +106,7 @@ - + diff --git a/guacamole/src/main/frontend/src/verifyCachedVersion.js b/guacamole/src/main/frontend/src/verifyCachedVersion.js new file mode 100644 index 000000000..db9e14409 --- /dev/null +++ b/guacamole/src/main/frontend/src/verifyCachedVersion.js @@ -0,0 +1,85 @@ +/* + * 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. + */ + +/** + * Automatically reloads the current page and clears relevant browser cache if + * the build that produced index.html is different/older than the build that + * produced the JavaScript loaded by index.html. + * + * @private + * @param {Location} location + * The Location object representing the URL of the current page. + * + * @param {Storate} [sessionStorage] + * The Storage object that should optionally be used to avoid reloading the + * current page in a loop if it proves impossible to clear cache. + */ +(function verifyCachedVersion(location, sessionStorage) { + + /** + * The meta element containing the build identifier of the Guacamole build + * that produced index.html. + * + * @private + * @type {HTMLMetaElement} + */ + var buildMeta = document.head.querySelector('meta[name=build]'); + + // Verify that index.html came from the same build as this JavaScript file, + // forcing a reload if out-of-date + if (!buildMeta || buildMeta.content !== '${guacamole.build.identifier}') { + + if (sessionStorage) { + + // Bail out if we have already tried to automatically refresh the + // cache but were unsuccessful + if (sessionStorage.getItem('reloadedFor') === '${guacamole.build.identifier}') { + console.warn('The version of Guacamole cached by your ' + + 'browser does not match the version of Guacamole on the ' + + 'server. To avoid unexpected errors, please clear your ' + + 'browser cache.'); + return; + } + + sessionStorage.setItem('reloadedFor', '${guacamole.build.identifier}'); + + } + + // Force refresh of cache by issuing an HTTP request with headers that + // request revalidation of cached content + var xhr = new XMLHttpRequest(); + xhr.open('GET', '', true); + xhr.setRequestHeader('Cache-Control', 'no-cache'); + xhr.setRequestHeader('Pragma', 'no-cache'); + + xhr.onreadystatechange = function readyStateChanged() { + + // Reload current page when ready (this call to reload MAY be + // sufficient in itself to clear cache, but this is not + // guaranteed by any standard) + if (xhr.readyState === XMLHttpRequest.DONE) + location.reload(true); + + }; + + xhr.send(); + + } + +})(window.location, window.sessionStorage); diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index aa337fbb5..29bb8ddd5 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -121,7 +121,8 @@ module.exports = { { from: 'fonts/**/*' }, { from: 'images/**/*' }, { from: 'layouts/**/*' }, - { from: 'translations/**/*' } + { from: 'translations/**/*' }, + { from: 'verifyCachedVersion.js' } ], { context: 'src/' }), diff --git a/guacamole/src/main/java/org/apache/guacamole/CacheRevalidationFilter.java b/guacamole/src/main/java/org/apache/guacamole/CacheRevalidationFilter.java new file mode 100644 index 000000000..38dd40303 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/CacheRevalidationFilter.java @@ -0,0 +1,61 @@ +/* + * 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; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Filter that sets the HTTP response headers necessary to request that the + * browser always revalidate its cached copy of the response before using that + * cached copy. + */ +public class CacheRevalidationFilter implements Filter { + + @Override + public void init(FilterConfig config) throws ServletException { + // No configuration + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.addHeader("Cache-Control", "no-cache"); + httpResponse.addHeader("Pragma", "no-cache"); + + chain.doFilter(request, response); + + } + + @Override + public void destroy() { + // Nothing to clean up + } + + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index d66d5f455..d5aedb005 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -41,6 +41,7 @@ import org.apache.guacamole.properties.StringSetProperty; import org.apache.guacamole.resource.Resource; import org.apache.guacamole.resource.ResourceServlet; import org.apache.guacamole.resource.SequenceResource; +import org.apache.guacamole.resource.WebApplicationResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -598,6 +599,9 @@ public class ExtensionModule extends ServletModule { Collection javaScriptResources = new ArrayList(); Collection cssResources = new ArrayList(); + // Veriffy that the possibly-cached index.html matches the current build + javaScriptResources.add(new WebApplicationResource(getServletContext(), "/verifyCachedVersion.js")); + // Load all extensions final Set toleratedAuthProviders = getToleratedAuthenticationProviders(); loadExtensions(javaScriptResources, cssResources, toleratedAuthProviders); diff --git a/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java b/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java index a6cf97357..c61687ea3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java +++ b/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java @@ -68,6 +68,10 @@ public class ResourceServlet extends HttpServlet { protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // Request that the browser revalidate cached data + response.addHeader("Cache-Control", "no-cache"); + response.addHeader("Pragma", "no-cache"); + // Set last modified and content type headers response.addDateHeader("Last-Modified", resource.getLastModified()); response.setContentType(resource.getMimeType()); diff --git a/guacamole/src/main/webapp/WEB-INF/web.xml b/guacamole/src/main/webapp/WEB-INF/web.xml index 52a52a624..ba1f6115e 100644 --- a/guacamole/src/main/webapp/WEB-INF/web.xml +++ b/guacamole/src/main/webapp/WEB-INF/web.xml @@ -28,6 +28,16 @@ index.html + + + cacheRevalidationFilter + org.apache.guacamole.CacheRevalidationFilter + + + cacheRevalidationFilter + /index.html + + guiceFilter diff --git a/pom.xml b/pom.xml index e384ad9cc..0480d5bf1 100644 --- a/pom.xml +++ b/pom.xml @@ -115,19 +115,37 @@ - org.codehaus.mojo build-helper-maven-plugin 3.2.0 + + define-project-root rootlocation + + + + define-build-timestamp + + guacamole.build.identifier + build + yyyyMMddHHmmss + + + timestamp-property + + +