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