diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java index 3e8c70af3..b9f070412 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java @@ -100,7 +100,7 @@ public class ExtensionModule extends ServletModule { /** * Service for adding and retrieving language resources. */ - private final LanguageResourceService languageResourceService = new LanguageResourceService(); + private final LanguageResourceService languageResourceService; /** * Returns the classloader that should be used as the parent classloader @@ -139,6 +139,7 @@ public class ExtensionModule extends ServletModule { */ public ExtensionModule(Environment environment) { this.environment = environment; + this.languageResourceService = new LanguageResourceService(environment); } /** diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java index 9955b5bf6..dece975e6 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java @@ -36,6 +36,9 @@ import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.JsonNodeFactory; import org.codehaus.jackson.node.ObjectNode; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.environment.Environment; +import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; import org.glyptodon.guacamole.net.basic.resource.ByteArrayResource; import org.glyptodon.guacamole.net.basic.resource.Resource; import org.glyptodon.guacamole.net.basic.resource.WebApplicationResource; @@ -76,6 +79,13 @@ public class LanguageResourceService { */ private static final Pattern LANGUAGE_KEY_PATTERN = Pattern.compile(".*/([a-z]+(_[A-Z]+)?)\\.json"); + /** + * The set of all language keys which are explicitly listed as allowed + * within guacamole.properties, or null if all defined languages should be + * allowed. + */ + private final Set allowedLanguages; + /** * Map of all language resources by language key. Language keys are * language and country code pairs, separated by an underscore, like @@ -86,6 +96,35 @@ public class LanguageResourceService { */ private final Map resources = new HashMap(); + /** + * Creates a new service for tracking and parsing available translations + * which reads its configuration from the given environment. + * + * @param environment + * The environment from which the configuration properties of this + * service should be read. + */ + public LanguageResourceService(Environment environment) { + + Set parsedAllowedLanguages; + + // Parse list of available languages from properties + try { + parsedAllowedLanguages = environment.getProperty(BasicGuacamoleProperties.ALLOWED_LANGUAGES); + logger.debug("Available languages will be restricted to: {}", parsedAllowedLanguages); + } + + // Warn of failure to parse + catch (GuacamoleException e) { + parsedAllowedLanguages = null; + logger.error("Unable to parse list of allowed languages: {}", e.getMessage()); + logger.debug("Error parsing list of allowed languages.", e); + } + + this.allowedLanguages = parsedAllowedLanguages; + + } + /** * Derives a language key from the filename within the given path, if * possible. If the filename is not a valid language key, null is returned. @@ -184,6 +223,31 @@ public class LanguageResourceService { } + /** + * Returns whether a language having the given key should be allowed to be + * loaded. If language availability restrictions are imposed through + * guacamole.properties, this may return false in some cases. By default, + * this function will always return true. Note that just because a language + * key is allowed to be loaded does not imply that the language key is + * valid. + * + * @param languageKey + * The language key of the language to test. + * + * @return + * true if the given language key should be allowed to be loaded, false + * otherwise. + */ + private boolean isLanguageAllowed(String languageKey) { + + // If no list is provided, all languages are implicitly available + if (allowedLanguages == null) + return true; + + return allowedLanguages.contains(languageKey); + + } + /** * Adds or overlays the given language resource, which need not exist in * the ServletContext. If a language resource is already defined for the @@ -202,6 +266,12 @@ public class LanguageResourceService { */ public void addLanguageResource(String key, Resource resource) { + // Skip loading of language if not allowed + if (!isLanguageAllowed(key)) { + logger.debug("OMITTING language: \"{}\"", key); + return; + } + // Merge language resources if already defined Resource existing = resources.get(key); if (existing != null) { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java index a1b2e1336..2bccfdb5e 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java @@ -24,6 +24,7 @@ package org.glyptodon.guacamole.net.basic.properties; import org.glyptodon.guacamole.properties.FileGuacamoleProperty; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; +import org.glyptodon.guacamole.properties.StringGuacamoleProperty; /** * Properties used by the default Guacamole web application. @@ -73,4 +74,17 @@ public class BasicGuacamoleProperties { }; + /** + * Comma-separated list of all allowed languages, where each language is + * represented by a language key, such as "en" or "en_US". If specified, + * only languages within this list will be listed as available by the REST + * service. + */ + public static final StringSetProperty ALLOWED_LANGUAGES = new StringSetProperty() { + + @Override + public String getName() { return "allowed-languages"; } + + }; + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java new file mode 100644 index 000000000..037427f3e --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.properties; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.properties.GuacamoleProperty; + +/** + * A GuacamoleProperty whose value is a Set of unique Strings. The string value + * parsed to produce this set is a comma-delimited list. Duplicate values are + * ignored, as is any whitespace following delimiters. To maintain + * compatibility with the behavior of Java properties in general, only + * whitespace at the beginning of each value is ignored; trailing whitespace + * becomes part of the value. + * + * @author Michael Jumper + */ +public abstract class StringSetProperty implements GuacamoleProperty> { + + /** + * A pattern which matches against the delimiters between values. This is + * currently simply a comma and any following whitespace. Parts of the + * input string which match this pattern will not be included in the parsed + * result. + */ + private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*"); + + @Override + public Set parseValue(String values) throws GuacamoleException { + + // If no property provided, return null. + if (values == null) + return null; + + // Split string into a set of individual values + List valueList = Arrays.asList(DELIMITER_PATTERN.split(values)); + return new HashSet(valueList); + + } + +} diff --git a/guacamole/src/main/webapp/app/locale/services/translationLoader.js b/guacamole/src/main/webapp/app/locale/services/translationLoader.js index ec05ef540..a95f85f1b 100644 --- a/guacamole/src/main/webapp/app/locale/services/translationLoader.js +++ b/guacamole/src/main/webapp/app/locale/services/translationLoader.js @@ -29,9 +29,10 @@ angular.module('locale').factory('translationLoader', ['$injector', function translationLoader($injector) { // Required services - var $http = $injector.get('$http'); - var $q = $injector.get('$q'); - var cacheService = $injector.get('cacheService'); + var $http = $injector.get('$http'); + var $q = $injector.get('$q'); + var cacheService = $injector.get('cacheService'); + var languageService = $injector.get('languageService'); /** * Satisfies a translation request for the given key by searching for the @@ -62,22 +63,48 @@ angular.module('locale').factory('translationLoader', ['$injector', function tra return; } - // Attempt to retrieve language - $http({ - cache : cacheService.languages, - method : 'GET', - url : 'translations/' + encodeURIComponent(currentKey) + '.json' - }) - - // Resolve promise if translation retrieved successfully - .success(function translationFileRetrieved(translation) { - deferred.resolve(translation); - }) - - // Retry with remaining languages if translation file could not be retrieved - .error(function translationFileUnretrievable() { + /** + * Continues trying possible translation files until no possibilities + * exist. + * + * @private + */ + var tryNextTranslation = function tryNextTranslation() { satisfyTranslation(deferred, requestedKey, remainingKeys); - }); + }; + + // Retrieve list of supported languages + languageService.getLanguages() + + // Attempt to retrieve translation if language is supported + .success(function retrievedLanguages(languages) { + + // Skip retrieval if language is not supported + if (!(currentKey in languages)) { + tryNextTranslation(); + return; + } + + // Attempt to retrieve language + $http({ + cache : cacheService.languages, + method : 'GET', + url : 'translations/' + encodeURIComponent(currentKey) + '.json' + }) + + // Resolve promise if translation retrieved successfully + .success(function translationFileRetrieved(translation) { + deferred.resolve(translation); + }) + + // Retry with remaining languages if translation file could not be + // retrieved + .error(tryNextTranslation); + + }) + + // Retry with remaining languages if translation does not exist + .error(tryNextTranslation); };