GUACAMOLE-630: Allow parameters received via "argv" streams to be edited within the Guacamole menu.
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index 41c6ba6..b90f263 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -27,6 +27,7 @@
var ManagedClient = $injector.get('ManagedClient');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedFilesystem = $injector.get('ManagedFilesystem');
+ var Protocol = $injector.get('Protocol');
var ScrollState = $injector.get('ScrollState');
// Required services
@@ -248,7 +249,15 @@
*
* @type ScrollState
*/
- scrollState : new ScrollState()
+ scrollState : new ScrollState(),
+
+ /**
+ * The current desired values of all editable connection parameters as
+ * a set of name/value pairs, including any changes made by the user.
+ *
+ * @type {Object.<String, String>}
+ */
+ connectionParameters : {}
};
@@ -258,6 +267,16 @@
};
/**
+ * Applies any changes to connection parameters made by the user within the
+ * Guacamole menu.
+ */
+ $scope.applyParameterChanges = function applyParameterChanges() {
+ angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
+ ManagedClient.setArgument($scope.client, name, value);
+ });
+ };
+
+ /**
* The client which should be attached to the client UI.
*
* @type ManagedClient
@@ -429,12 +448,20 @@
});
+ // Update client state/behavior as visibility of the Guacamole menu changes
$scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
- // Send clipboard data if menu is hidden
- if (!menuShown && menuShownPreviousState)
+ // Send clipboard and argument value data once menu is hidden
+ if (!menuShown && menuShownPreviousState) {
$scope.$broadcast('guacClipboard', $scope.client.clipboardData);
-
+ $scope.applyParameterChanges();
+ }
+
+ // Obtain snapshot of current editable connection parameters when menu
+ // is opened
+ else if (menuShown)
+ $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
+
// Disable client keyboard if the menu is shown
$scope.client.clientProperties.keyboardEnabled = !menuShown;
@@ -806,6 +833,11 @@
$scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ];
/**
+ * @borrows Protocol.getNamespace
+ */
+ $scope.getProtocolNamespace = Protocol.getNamespace;
+
+ /**
* The currently-visible filesystem within the filesystem menu, if the
* filesystem menu is open. If no filesystem is currently visible, this
* will be null.
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index ad85f23..7690e75 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -96,6 +96,14 @@
</div>
</div>
+ <!-- Connection parameters which may be modified while the connection is open -->
+ <div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
+ <guac-form namespace="getProtocolNamespace(client.protocol)"
+ content="client.forms"
+ model="menu.connectionParameters"
+ model-only="true"></guac-form>
+ </div>
+
<!-- Input method -->
<div class="menu-section" id="keyboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedArgument.js b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
new file mode 100644
index 0000000..247d9f6
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides the ManagedArgument class used by ManagedClient.
+ */
+angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
+
+ /**
+ * Object which represents an argument (connection parameter) which may be
+ * changed by the user while the connection is open.
+ *
+ * @constructor
+ * @param {ManagedArgument|Object} [template={}]
+ * The object whose properties should be copied within the new
+ * ManagedArgument.
+ */
+ var ManagedArgument = function ManagedArgument(template) {
+
+ // Use empty object by default
+ template = template || {};
+
+ /**
+ * The name of the connection parameter.
+ *
+ * @type {String}
+ */
+ this.name = template.name;
+
+ /**
+ * The current value of the connection parameter.
+ *
+ * @type {String}
+ */
+ this.value = template.value;
+
+ /**
+ * A valid, open output stream which may be used to apply a new value
+ * to the connection parameter.
+ *
+ * @type {Guacamole.OutputStream}
+ */
+ this.stream = template.stream;
+
+ };
+
+ /**
+ * Requests editable access to a given connection parameter, returning a
+ * promise which is resolved with a ManagedArgument instance that provides
+ * such access if the parameter is indeed editable.
+ *
+ * @param {ManagedClient} managedClient
+ * The ManagedClient instance associated with the connection for which
+ * an editable version of the connection parameter is being retrieved.
+ *
+ * @param {String} name
+ * The name of the connection parameter.
+ *
+ * @param {String} value
+ * The current value of the connection parameter, as received from a
+ * prior, inbound "argv" stream.
+ *
+ * @returns {Promise.<ManagedArgument>}
+ * A promise which is resolved with the new ManagedArgument instance
+ * once the requested parameter has been verified as editable.
+ */
+ ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
+
+ var deferred = $q.defer();
+
+ // Create internal, fully-populated instance of ManagedArgument, to be
+ // returned only once mutability of the associated connection parameter
+ // has been verified
+ var managedArgument = new ManagedArgument({
+ name : name,
+ value : value,
+ stream : managedClient.client.createArgumentValueStream('text/plain', name)
+ });
+
+ // The connection parameter is editable only if a successful "ack" is
+ // received
+ managedArgument.stream.onack = function ackReceived(status) {
+ if (status.isError())
+ deferred.reject(status);
+ else
+ deferred.resolve(managedArgument);
+ };
+
+ return deferred.promise;
+
+ };
+
+ /**
+ * Sets the given editable argument (connection parameter) to the given
+ * value, updating the behavior of the associated connection in real-time.
+ * If successful, the ManagedArgument provided cannot be used for future
+ * calls to setValue() and must be replaced with a new instance. This
+ * function only has an effect if the new parameter value is different from
+ * the current value.
+ *
+ * @param {ManagedArgument} managedArgument
+ * The ManagedArgument instance associated with the connection
+ * parameter being modified.
+ *
+ * @param {String} value
+ * The new value to assign to the connection parameter.
+ *
+ * @returns {Boolean}
+ * true if the connection parameter was sent and the provided
+ * ManagedArgument instance may no longer be used for future setValue()
+ * calls, false if the connection parameter was NOT sent as it has not
+ * changed.
+ */
+ ManagedArgument.setValue = function setValue(managedArgument, value) {
+
+ // Stream new value only if value has changed
+ if (value !== managedArgument.value) {
+
+ var writer = new Guacamole.StringWriter(managedArgument.stream);
+ writer.sendText(value);
+ writer.sendEnd();
+
+ // ManagedArgument instance is no longer usable
+ return true;
+
+ }
+
+ // No parameter value change was attempted and the ManagedArgument
+ // instance may be reused
+ return false;
+
+ };
+
+ return ManagedArgument;
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index b4637e9..4551208 100644
--- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js
+++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
@@ -27,6 +27,7 @@
var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData');
+ var ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay');
@@ -44,6 +45,7 @@
var connectionService = $injector.get('connectionService');
var preferenceService = $injector.get('preferenceService');
var requestService = $injector.get('requestService');
+ var schemaService = $injector.get('schemaService');
var tunnelService = $injector.get('tunnelService');
var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory');
@@ -118,6 +120,23 @@
this.title = template.title;
/**
+ * The name which uniquely identifies the protocol of the connection in
+ * use. If the protocol cannot be determined, such as when a connection
+ * group is in use, this will be null.
+ *
+ * @type {String}
+ */
+ this.protocol = template.protocol || null;
+
+ /**
+ * An array of forms describing all known parameters for the connection
+ * in use, including those which may not be editable.
+ *
+ * @type {Form[]}
+ */
+ this.forms = template.forms || [];
+
+ /**
* The most recently-generated thumbnail for this connection, as
* stored within the local connection history. If no thumbnail is
* stored, this will be null.
@@ -179,6 +198,17 @@
*/
this.clientProperties = template.clientProperties || new ClientProperties();
+ /**
+ * All editable arguments (connection parameters), stored by their
+ * names. Arguments will only be present within this set if their
+ * current values have been exposed by the server via an inbound "argv"
+ * stream and the server has confirmed that the value may be changed
+ * through a successful "ack" to an outbound "argv" stream.
+ *
+ * @type {Object.<String, ManagedArgument>}
+ */
+ this.arguments = template.arguments || {};
+
};
/**
@@ -448,6 +478,33 @@
};
+ // Test for argument mutability whenever an argument value is
+ // received
+ client.onargv = function clientArgumentValueReceived(stream, mimetype, name) {
+
+ // Ignore arguments which do not use a mimetype currently supported
+ // by the web application
+ if (mimetype !== 'text/plain')
+ return;
+
+ var reader = new Guacamole.StringReader(stream);
+
+ // Assemble received data into a single string
+ var value = '';
+ reader.ontext = function textReceived(text) {
+ value += text;
+ };
+
+ // Test mutability once stream is finished, storing the current
+ // value for the argument only if it is mutable
+ reader.onend = function textComplete() {
+ ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) {
+ managedClient.arguments[name] = argument;
+ }, function ignoreImmutableArguments() {});
+ };
+
+ };
+
// Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(stream, mimetype) {
@@ -522,11 +579,16 @@
client.connect(connectString);
});
- // If using a connection, pull connection name
+ // If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
- connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
- .then(function connectionRetrieved(connection) {
- managedClient.name = managedClient.title = connection.name;
+ $q.all({
+ connection : connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id),
+ protocols : schemaService.getProtocols(clientIdentifier.dataSource)
+ })
+ .then(function dataRetrieved(values) {
+ managedClient.name = managedClient.title = values.connection.name;
+ managedClient.protocol = values.connection.protocol;
+ managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
}, requestService.WARN);
}
@@ -620,6 +682,52 @@
};
/**
+ * Assigns the given value to the connection parameter having the given
+ * name, updating the behavior of the connection in real-time. If the
+ * connection parameter is not editable, this function has no effect.
+ *
+ * @param {ManagedClient} managedClient
+ * The ManagedClient instance associated with the active connection
+ * being modified.
+ *
+ * @param {String} name
+ * The name of the connection parameter to modify.
+ *
+ * @param {String} value
+ * The value to attempt to assign to the given connection parameter.
+ */
+ ManagedClient.setArgument = function setArgument(managedClient, name, value) {
+ var managedArgument = managedClient.arguments[name];
+ if (managedArgument && ManagedArgument.setValue(managedArgument, value))
+ delete managedClient.arguments[name];
+ };
+
+ /**
+ * Retrieves the current values of all editable connection parameters as a
+ * set of name/value pairs suitable for use as the model of a form which
+ * edits those parameters.
+ *
+ * @param {ManagedClient} client
+ * The ManagedClient instance associated with the active connection
+ * whose parameter values are being retrieved.
+ *
+ * @returns {Object.<String, String>}
+ * A new set of name/value pairs containing the current values of all
+ * editable parameters.
+ */
+ ManagedClient.getArgumentModel = function getArgumentModel(client) {
+
+ var model = {};
+
+ angular.forEach(client.arguments, function addModelEntry(managedArgument) {
+ model[managedArgument.name] = managedArgument.value;
+ });
+
+ return model;
+
+ };
+
+ /**
* Produces a sharing link for the given ManagedClient using the given
* sharing profile. The resulting sharing link, and any required login
* information, can be retrieved from the <code>shareLinks</code> property