- Click or tap on a user below to manage that user. Depending
- on your access level, users can be added and deleted, and their
- passwords can be changed.
-
-
-
-
-
-
-
-
-
-
-
-
-
Connections
-
-
-
- Click or tap on a connection below to manage that connection.
- Depending on your access level, connections can be added and
- deleted, and their properties (protocol, hostname, port, etc.)
- can be changed.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/guacamole/src/main/webapp/app/client/clientModule.js b/guacamole/src/main/webapp/app/client/clientModule.js
new file mode 100644
index 000000000..dd27a21c1
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/clientModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for code used to connect to a connection or balancing group.
+ */
+angular.module('client', []);
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
new file mode 100644
index 000000000..d90c56c5c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/*
+ * In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are
+ * several possible keysysms for each key.
+ */
+
+var SHIFT_KEYS = {0xFFE1 : true, 0xFFE2: true},
+ ALT_KEYS = {0xFFE9 : true, 0xFFEA : true, 0xFE03: true},
+ CTRL_KEYS = {0xFFE3 : true, 0xFFE4: true},
+ MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS);
+
+/**
+ * The controller for the page used to connect to a connection or balancing group.
+ */
+angular.module('home').controller('clientController', ['$scope', '$routeParams', 'localStorageUtility', '$injector',
+ function clientController($scope, $routeParams, localStorageUtility, $injector) {
+
+ // Get DAO for reading connections and groups
+ var connectionGroupDAO = $injector.get('connectionGroupDAO');
+ var connectionDAO = $injector.get('connectionDAO');
+
+ // Client settings and state
+ $scope.clientParameters = {scale: 1};
+
+ // Hide menu by default
+ $scope.menuShown = false;
+ $scope.menuHasBeenShown = false;
+
+ /*
+ * Parse the type, name, and id out of the url paramteres,
+ * as well as any extra parameters if set.
+ */
+ $scope.type = $routeParams.type;
+ $scope.id = $routeParams.id;
+ $scope.connectionParameters = $routeParams.params || '';
+
+ // Keep title in sync with connection state
+ $scope.$watch('connectionName', function updateTitle() {
+ $scope.page.title = $scope.connectionName;
+ });
+
+ // Pull connection name from server
+ switch ($scope.type) {
+
+ // Connection
+ case 'c':
+ connectionDAO.getConnection($scope.id).success(function (connection) {
+ $scope.connectionName = connection.name;
+ });
+ break;
+
+ // Connection group
+ case 'g':
+ connectionGroupDAO.getConnectionGroup($scope.id).success(function (group) {
+ $scope.connectionName = group.name;
+ });
+ break;
+
+ }
+
+ var keysCurrentlyPressed = {};
+
+ /*
+ * Check to see if all currently pressed keys are in the set of menu keys.
+ */
+ function checkMenuModeActive() {
+ for(var keysym in keysCurrentlyPressed) {
+ if(!MENU_KEYS[keysym]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
+ keysCurrentlyPressed[keysym] = true;
+ });
+
+ // Listen for broadcasted keyup events and fire the appropriate listeners
+ $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
+ /*
+ * If only menu keys are pressed, and we have one keysym from each group,
+ * and one of the keys is being released, show the menu.
+ */
+ if(checkMenuModeActive()) {
+ var currentKeysPressedKeys = Object.keys(keysCurrentlyPressed);
+
+ // Check that there is a key pressed for each of the required key classes
+ if(!_.isEmpty(_.pick(SHIFT_KEYS, currentKeysPressedKeys)) &&
+ !_.isEmpty(_.pick(ALT_KEYS, currentKeysPressedKeys)) &&
+ !_.isEmpty(_.pick(CTRL_KEYS, currentKeysPressedKeys))
+ ) {
+
+ // Toggle the menu
+ $scope.safeApply(function() {
+ $scope.menuShown = !$scope.menuShown;
+
+ // The menu has been shown at least once before
+ $scope.menuHasBeenShown = true;
+ });
+
+ // Reset the keys pressed
+ keysCurrentlyPressed = {};
+ }
+ }
+
+ delete keysCurrentlyPressed[keysym];
+ });
+
+}]);
diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js
new file mode 100644
index 000000000..1aaba56b7
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A directive for the guacamole client.
+ */
+angular.module('client').directive('guacClient', [function guacClient() {
+
+ return {
+ // Element only
+ restrict: 'E',
+ replace: true,
+ scope: {
+ // Parameters for controlling client state
+ clientParameters : '=',
+
+ // Parameters for initially connecting
+ id : '=',
+ type : '=',
+ connectionName : '=',
+ connectionParameters : '='
+ },
+ templateUrl: 'app/client/templates/guacClient.html',
+ controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
+
+ var $window = $injector.get('$window'),
+ guacAudio = $injector.get('guacAudio'),
+ guacVideo = $injector.get('guacVideo'),
+ localStorageUtility = $injector.get('localStorageUtility');
+
+ var authToken = localStorageUtility.get('authToken'),
+ uniqueId = encodeURIComponent($scope.type + '/' + $scope.id);
+
+ // Get elements for DOM manipulation
+ $scope.main = $element[0];
+
+ // Settings and constants
+ $.extend(true, $scope, {
+
+ /**
+ * All error codes for which automatic reconnection is appropriate when a
+ * tunnel error occurs.
+ */
+ "tunnel_auto_reconnect": {
+ 0x0200: true,
+ 0x0202: true,
+ 0x0203: true,
+ 0x0308: true
+ },
+
+ /**
+ * All error codes for which automatic reconnection is appropriate when a
+ * client error occurs.
+ */
+ "client_auto_reconnect": {
+ 0x0200: true,
+ 0x0202: true,
+ 0x0203: true,
+ 0x0301: true,
+ 0x0308: true
+ },
+
+ /* Constants */
+
+ "KEYBOARD_AUTO_RESIZE_INTERVAL" : 30, /* milliseconds */
+ "RECONNECT_PERIOD" : 15, /* seconds */
+ "TEXT_INPUT_PADDING" : 128, /* characters */
+ "TEXT_INPUT_PADDING_CODEPOINT" : 0x200B,
+
+ /* Settings for zoom */
+ "min_zoom" : 1,
+ "max_zoom" : 3,
+
+ /* Current connection parameters */
+
+ /* The user defined named for this connection */
+ "connectionName" : "Guacamole",
+
+ /* The attached client instance */
+ "attachedClient" : null,
+
+ /* Mouse emulation */
+
+ "emulate_absolute" : true,
+ "touch" : null,
+ "touch_screen" : null,
+ "touch_pad" : null,
+
+ /* Clipboard */
+
+ "remote_clipboard" : "",
+ "clipboard_integration_enabled" : undefined
+ });
+
+ /**
+ * Updates the scale of the attached Guacamole.Client based on current window
+ * size and "auto-fit" setting.
+ */
+ $scope.updateDisplayScale = function() {
+
+ var guac = $scope.attachedClient;
+ if (!guac)
+ return;
+
+ // Determine whether display is currently fit to the screen
+ var auto_fit = (guac.getDisplay().getScale() === $scope.min_zoom);
+
+ // Calculate scale to fit screen
+ $scope.min_zoom = Math.min(
+ $scope.main.offsetWidth / Math.max(guac.getDisplay().getWidth(), 1),
+ $scope.main.offsetHeight / Math.max(guac.getDisplay().getHeight(), 1)
+ );
+
+ // Calculate appropriate maximum zoom level
+ $scope.max_zoom = Math.max($scope.min_zoom, 3);
+
+ // Clamp zoom level, maintain auto-fit
+ if (guac.getDisplay().getScale() < $scope.min_zoom || auto_fit)
+ $scope.setScale($scope.min_zoom);
+
+ else if (guac.getDisplay().getScale() > $scope.max_zoom)
+ $scope.setScale($scope.max_zoom);
+
+ };
+
+ /**
+ * Attaches a Guacamole.Client to the client UI, such that Guacamole events
+ * affect the UI, and local events affect the Guacamole.Client. If a client
+ * is already attached, it is replaced.
+ *
+ * @param {Guacamole.Client} guac The Guacamole.Client to attach to the UI.
+ */
+ $scope.attach = function(guac) {
+
+ // If a client is already attached, ensure it is disconnected
+ if ($scope.attachedClient)
+ $scope.attachedClient.disconnect();
+
+ // Store attached client
+ $scope.attachedClient = guac;
+
+ // Get display element
+ var guac_display = guac.getDisplay().getElement();
+
+ /*
+ * Update the scale of the display when the client display size changes.
+ */
+
+ guac.getDisplay().onresize = function() {
+ $scope.updateDisplayScale();
+ };
+
+ /*
+ * Update UI when the state of the Guacamole.Client changes.
+ */
+
+ guac.onstatechange = function(clientState) {
+
+ switch (clientState) {
+
+ // Idle
+ case 0:
+
+ $scope.$emit('guacClientStatusChange', guac, "idle");
+ break;
+
+ // Connecting
+ case 1:
+
+ $scope.$emit('guacClientStatusChange', guac, "connecting");
+ break;
+
+ // Connected + waiting
+ case 2:
+
+ $scope.$emit('guacClientStatusChange', guac, "waiting");
+ break;
+
+ // Connected
+ case 3:
+
+ $scope.$emit('guacClientStatusChange', guac, null);
+
+ // Update server clipboard with current data
+ var clipboard = localStorageUtility.get("clipboard");
+ if (clipboard)
+ guac.setClipboard(clipboard);
+
+ break;
+
+ // Disconnecting / disconnected are handled by tunnel instead
+ case 4:
+ case 5:
+ break;
+
+ // Unknown status code
+ default:
+ $scope.$emit('guacClientError', guac, "unknown");
+ }
+ };
+
+ // Listen for clipboard events not sent by the client
+ $scope.$on('guacClipboard', function onClipboardChange(event, data) {
+ // Update server clipboard with current data
+ $scope.guac.setClipboard(data);
+ });
+
+ /*
+ * Emit a name change event
+ */
+ guac.onname = function(name) {
+ $scope.connectionDisplayName = name;
+ $scope.$emit('name', guac, name);
+ };
+
+ /*
+ * Disconnect and emits an error when the client receives an error
+ */
+ guac.onerror = function(status) {
+
+ // Disconnect, if connected
+ guac.disconnect();
+
+ $scope.$emit('guacClientError', guac, status.code, {operations: {reconnect: function reconnect () {
+ $scope.connect();
+ }}});
+
+ };
+
+ // Server copy handler
+ guac.onclipboard = function(stream, mimetype) {
+
+ // Only text/plain is supported for now
+ if (mimetype !== "text/plain") {
+ stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED);
+ return;
+ }
+
+ var reader = new Guacamole.StringReader(stream);
+ var data = "";
+
+ // Append any received data to buffer
+ reader.ontext = function clipboard_text_received(text) {
+ data += text;
+ stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
+ };
+
+ // Emit event when done
+ reader.onend = function clipboard_text_end() {
+ $scope.$emit('guacClientClipboard', guac, data);
+ };
+
+ };
+
+ /*
+ * Prompt to download file when file received.
+ */
+
+ guac.onfile = function onfile(stream, mimetype, filename) {
+
+ // Begin file download
+ var guacFileStartEvent = $scope.$emit('guacFileStart', guac, stream.index, mimetype, filename);
+ if (!guacFileStartEvent.defaultPrevented) {
+
+ var blob_reader = new Guacamole.BlobReader(stream, mimetype);
+
+ // Update progress as data is received
+ blob_reader.onprogress = function onprogress() {
+ $scope.$emit('guacFileProgress', guac, stream.index, mimetype, filename);
+ stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
+ };
+
+ // When complete, prompt for download
+ blob_reader.onend = function onend() {
+ $scope.$emit('guacFileEnd', guac, stream.index, mimetype, filename);
+ };
+
+ stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS);
+
+ }
+
+ // Respond with UNSUPPORTED if download (default action) canceled within event handler
+ else
+ stream.sendAck("Download canceled", Guacamole.Status.Code.UNSUPPORTED);
+
+ };
+
+ /*
+ * Do nothing when the display element is clicked on.
+ */
+
+ guac_display.onclick = function(e) {
+ e.preventDefault();
+ return false;
+ };
+
+ /*
+ * Handle mouse and touch events relative to the display element.
+ */
+
+ // Touchscreen
+ var touch_screen = new Guacamole.Mouse.Touchscreen(guac_display);
+ $scope.touch_screen = touch_screen;
+
+ // Touchpad
+ var touch_pad = new Guacamole.Mouse.Touchpad(guac_display);
+ $scope.touch_pad = touch_pad;
+
+ // Mouse
+ var mouse = new Guacamole.Mouse(guac_display);
+ mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = function(mouseState) {
+
+ // Scale event by current scale
+ var scaledState = new Guacamole.Mouse.State(
+ mouseState.x / guac.getDisplay().getScale(),
+ mouseState.y / guac.getDisplay().getScale(),
+ mouseState.left,
+ mouseState.middle,
+ mouseState.right,
+ mouseState.up,
+ mouseState.down);
+
+ // Send mouse event
+ guac.sendMouseState(scaledState);
+
+ };
+
+ // Hide any existing status notifications
+ $scope.$emit('guacClientStatusChange', guac, null);
+
+ var $display = $element.find('.display');
+
+ // Remove old client from UI, if any
+ $display.html("");
+
+ // Add client to UI
+ guac.getDisplay().getElement().className = "software-cursor";
+ $display.append(guac.getDisplay().getElement());
+
+ };
+
+ // Watch for changes to mouse emulation mode
+ $scope.$watch('parameters.emulateAbsolute', function(emulateAbsolute) {
+ $scope.setMouseEmulationAbsolute(emulateAbsolute);
+ });
+
+ /**
+ * Sets the mouse emulation mode to absolute or relative.
+ *
+ * @param {Boolean} absolute Whether mouse emulation should use absolute
+ * (touchscreen) mode.
+ */
+ $scope.setMouseEmulationAbsolute = function(absolute) {
+
+ function __handle_mouse_state(mouseState) {
+
+ // Get client - do nothing if not attached
+ var guac = $scope.attachedClient;
+ if (!guac) return;
+
+ // Determine mouse position within view
+ var guac_display = guac.getDisplay().getElement();
+ var mouse_view_x = mouseState.x + guac_display.offsetLeft - $scope.main.scrollLeft;
+ var mouse_view_y = mouseState.y + guac_display.offsetTop - $scope.main.scrollTop;
+
+ // Determine viewport dimensioins
+ var view_width = $scope.main.offsetWidth;
+ var view_height = $scope.main.offsetHeight;
+
+ // Determine scroll amounts based on mouse position relative to document
+
+ var scroll_amount_x;
+ if (mouse_view_x > view_width)
+ scroll_amount_x = mouse_view_x - view_width;
+ else if (mouse_view_x < 0)
+ scroll_amount_x = mouse_view_x;
+ else
+ scroll_amount_x = 0;
+
+ var scroll_amount_y;
+ if (mouse_view_y > view_height)
+ scroll_amount_y = mouse_view_y - view_height;
+ else if (mouse_view_y < 0)
+ scroll_amount_y = mouse_view_y;
+ else
+ scroll_amount_y = 0;
+
+ // Scroll (if necessary) to keep mouse on screen.
+ $scope.main.scrollLeft += scroll_amount_x;
+ $scope.main.scrollTop += scroll_amount_y;
+
+ // Scale event by current scale
+ var scaledState = new Guacamole.Mouse.State(
+ mouseState.x / guac.getDisplay().getScale(),
+ mouseState.y / guac.getDisplay().getScale(),
+ mouseState.left,
+ mouseState.middle,
+ mouseState.right,
+ mouseState.up,
+ mouseState.down);
+
+ // Send mouse event
+ guac.sendMouseState(scaledState);
+
+ };
+
+ var new_mode, old_mode;
+ $scope.emulate_absolute = absolute;
+
+ // Switch to touchscreen if absolute
+ if (absolute) {
+ new_mode = $scope.touch_screen;
+ old_mode = $scope.touch;
+ }
+
+ // Switch to touchpad if not absolute (relative)
+ else {
+ new_mode = $scope.touch_pad;
+ old_mode = $scope.touch;
+ }
+
+ // Perform switch
+ if (new_mode) {
+
+ if (old_mode) {
+ old_mode.onmousedown = old_mode.onmouseup = old_mode.onmousemove = null;
+ new_mode.currentState.x = old_mode.currentState.x;
+ new_mode.currentState.y = old_mode.currentState.y;
+ }
+
+ new_mode.onmousedown = new_mode.onmouseup = new_mode.onmousemove = __handle_mouse_state;
+ $scope.touch = new_mode;
+ }
+
+ };
+
+ /**
+ * Connects to the current Guacamole connection, attaching a new Guacamole
+ * client to the user interface. If a Guacamole client is already attached,
+ * it is replaced.
+ */
+ $scope.connect = function connect() {
+
+ // If WebSocket available, try to use it.
+ if ($window.WebSocket)
+ $scope.tunnel = new Guacamole.ChainedTunnel(
+ new Guacamole.WebSocketTunnel("websocket-tunnel"),
+ new Guacamole.HTTPTunnel("tunnel")
+ );
+
+ // If no WebSocket, then use HTTP.
+ else
+ $scope.tunnel = new Guacamole.HTTPTunnel("tunnel");
+
+ // Instantiate client
+ $scope.guac = new Guacamole.Client($scope.tunnel);
+
+ // Tie UI to client
+ $scope.attach($scope.guac);
+
+ // Calculate optimal width/height for display
+ var pixel_density = $window.devicePixelRatio || 1;
+ var optimal_dpi = pixel_density * 96;
+ var optimal_width = $window.innerWidth * pixel_density;
+ var optimal_height = $window.innerHeight * pixel_density;
+
+ // Scale width/height to be at least 600x600
+ if (optimal_width < 600 || optimal_height < 600) {
+ var scale = Math.max(600 / optimal_width, 600 / optimal_height);
+ optimal_width = optimal_width * scale;
+ optimal_height = optimal_height * scale;
+ }
+
+ // Get entire query string, and pass to connect().
+ // Normally, only the "id" parameter is required, but
+ // all parameters should be preserved and passed on for
+ // the sake of authentication.
+
+ var connectString =
+ "id=" + uniqueId + ($scope.connectionParameters ? '&' + $scope.connectionParameters : '')
+ + "&authToken="+ authToken
+ + "&width=" + Math.floor(optimal_width)
+ + "&height=" + Math.floor(optimal_height)
+ + "&dpi=" + Math.floor(optimal_dpi);
+
+ // Add audio mimetypes to connect_string
+ guacAudio.supported.forEach(function(mimetype) {
+ connectString += "&audio=" + encodeURIComponent(mimetype);
+ });
+
+ // Add video mimetypes to connect_string
+ guacVideo.supported.forEach(function(mimetype) {
+ connectString += "&video=" + encodeURIComponent(mimetype);
+ });
+
+
+ // Show connection errors from tunnel
+ $scope.tunnel.onerror = function onerror(status) {
+
+ //FIXME: Needs to auto reconnect - should that be here, or in the error handler further up?
+ $scope.$emit('guacTunnelError', $scope.guac, status.code);
+ };
+
+
+ // Notify of disconnections (if not already notified of something else)
+ $scope.tunnel.onstatechange = function onstatechange(state) {
+ if (state === Guacamole.Tunnel.State.CLOSED) {
+ $scope.$emit('guacTunnelError', $scope.guac, "disconnected", state);
+ }
+ };
+
+ // Connect
+ $scope.guac.connect(connectString);
+ };
+
+ /**
+ * Sets the current display scale to the given value, where 1 is 100% (1:1
+ * pixel ratio). Out-of-range values will be clamped in-range.
+ *
+ * @param {Number} scale The new scale to apply
+ */
+ $scope.setScale = function setScale(scale) {
+
+ scale = Math.max(scale, $scope.min_zoom);
+ scale = Math.min(scale, $scope.max_zoom);
+
+ if ($scope.attachedClient)
+ $scope.attachedClient.getDisplay().scale(scale);
+
+ return scale;
+ };
+
+ // Adjust scale if modified externally
+ $scope.$watch('clientParameters.scale', function changeScale(scale) {
+ $scope.setScale(scale);
+ checkScale();
+ });
+
+ // Verify that the scale is within acceptable bounds, and adjust if needed
+ function checkScale() {
+
+ // If at minimum zoom level, auto fit is ON
+ if ($scope.scale === $scope.min_zoom) {
+ $scope.main.style.overflow = "hidden";
+ $scope.autoFitEnabled = true;
+ }
+
+ // If at minimum zoom level, auto fit is OFF
+ else {
+ $scope.main.style.overflow = "auto";
+ $scope.autoFitEnabled = false;
+ }
+ }
+
+ var show_keyboard_gesture_possible = true;
+
+ // Handle Keyboard events
+ function __send_key(pressed, keysym) {
+ $scope.attachedClient.sendKeyEvent(pressed, keysym);
+ return false;
+ }
+
+ $scope.keydown = function keydown (keysym, keyboard) {
+
+ // Only handle key events if client is attached
+ var guac = $scope.attachedClient;
+ if (!guac) return true;
+
+ // Handle Ctrl-shortcuts specifically
+ if (keyboard.modifiers.ctrl && !keyboard.modifiers.alt && !keyboard.modifiers.shift) {
+
+ // Allow event through if Ctrl+C or Ctrl+X
+ if (keyboard.pressed[0x63] || keyboard.pressed[0x78]) {
+ __send_key(1, keysym);
+ return true;
+ }
+
+ // If Ctrl+V, wait until after paste event (next event loop)
+ if (keyboard.pressed[0x76]) {
+ window.setTimeout(function after_paste() {
+ __send_key(1, keysym);
+ }, 10);
+ return true;
+ }
+
+ }
+
+ // If key is NOT one of the expected keys, gesture not possible
+ if (keysym !== 0xFFE3 && keysym !== 0xFFE9 && keysym !== 0xFFE1)
+ show_keyboard_gesture_possible = false;
+
+ // Send key event
+ return __send_key(1, keysym);
+
+ };
+
+ $scope.keyup = function keyup(keysym, keyboard) {
+
+ // Only handle key events if client is attached
+ var guac = $scope.attachedClient;
+ if (!guac) return true;
+
+ // If lifting up on shift, toggle menu visibility if rest of gesture
+ // conditions satisfied
+ if (show_keyboard_gesture_possible && keysym === 0xFFE1
+ && keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) {
+ __send_key(0, 0xFFE1);
+ __send_key(0, 0xFFE9);
+ __send_key(0, 0xFFE3);
+
+ // Emit an event to show the menu
+ $scope.$emit('guacClientMenu', true);
+ }
+
+ // Detect if no keys are pressed
+ var reset_gesture = true;
+ for (var pressed in keyboard.pressed) {
+ reset_gesture = false;
+ break;
+ }
+
+ // Reset gesture state if possible
+ if (reset_gesture)
+ show_keyboard_gesture_possible = true;
+
+ // Send key event
+ return __send_key(0, keysym);
+
+ };
+
+ // Listen for broadcasted keydown events and fire the appropriate listeners
+ $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
+ var preventDefault = $scope.keydown(keysym, keyboard);
+ if(preventDefault) {
+ event.preventDefault();
+ }
+ });
+
+ // Listen for broadcasted keyup events and fire the appropriate listeners
+ $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
+ var preventDefault = $scope.keyup(keysym, keyboard);
+ if(preventDefault) {
+ event.preventDefault();
+ }
+ });
+
+ // Connect!
+ $scope.connect();
+ }]
+ };
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/services/guacAudio.js b/guacamole/src/main/webapp/app/client/services/guacAudio.js
new file mode 100644
index 000000000..f648ecbb4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/services/guacAudio.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for checking browser audio support.
+ */
+angular.module('client').factory('guacAudio', [function guacAudio() {
+
+ /**
+ * Object describing the UI's level of audio support.
+ */
+ return new (function() {
+
+ var codecs = [
+ 'audio/ogg; codecs="vorbis"',
+ 'audio/mp4; codecs="mp4a.40.5"',
+ 'audio/mpeg; codecs="mp3"',
+ 'audio/webm; codecs="vorbis"',
+ 'audio/wav; codecs=1'
+ ];
+
+ var probably_supported = [];
+ var maybe_supported = [];
+
+ /**
+ * Array of all supported audio mimetypes, ordered by liklihood of
+ * working.
+ */
+ this.supported = [];
+
+ // Build array of supported audio formats
+ codecs.forEach(function(mimetype) {
+
+ var audio = new Audio();
+ var support_level = audio.canPlayType(mimetype);
+
+ // Trim semicolon and trailer
+ var semicolon = mimetype.indexOf(";");
+ if (semicolon != -1)
+ mimetype = mimetype.substring(0, semicolon);
+
+ // Partition by probably/maybe
+ if (support_level == "probably")
+ probably_supported.push(mimetype);
+ else if (support_level == "maybe")
+ maybe_supported.push(mimetype);
+
+ });
+
+ // Add probably supported types first
+ Array.prototype.push.apply(
+ this.supported, probably_supported);
+
+ // Prioritize "maybe" supported types second
+ Array.prototype.push.apply(
+ this.supported, maybe_supported);
+
+ })();
+
+}]);
diff --git a/guacamole/src/main/webapp/app/client/services/guacVideo.js b/guacamole/src/main/webapp/app/client/services/guacVideo.js
new file mode 100644
index 000000000..4ed95d14f
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/services/guacVideo.js
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for checking browser video support.
+ */
+angular.module('client').factory('guacVideo', [function guacVideo() {
+
+ /**
+ * Object describing the UI's level of video support.
+ */
+ return new (function() {
+
+ var codecs = [
+ 'video/ogg; codecs="theora, vorbis"',
+ 'video/mp4; codecs="avc1.4D401E, mp4a.40.5"',
+ 'video/webm; codecs="vp8.0, vorbis"'
+ ];
+
+ var probably_supported = [];
+ var maybe_supported = [];
+
+ /**
+ * Array of all supported video mimetypes, ordered by liklihood of
+ * working.
+ */
+ this.supported = [];
+
+ // Build array of supported audio formats
+ codecs.forEach(function(mimetype) {
+
+ var video = document.createElement("video");
+ var support_level = video.canPlayType(mimetype);
+
+ // Trim semicolon and trailer
+ var semicolon = mimetype.indexOf(";");
+ if (semicolon != -1)
+ mimetype = mimetype.substring(0, semicolon);
+
+ // Partition by probably/maybe
+ if (support_level == "probably")
+ probably_supported.push(mimetype);
+ else if (support_level == "maybe")
+ maybe_supported.push(mimetype);
+
+ });
+
+ // Add probably supported types first
+ Array.prototype.push.apply(
+ this.supported, probably_supported);
+
+ // Prioritize "maybe" supported types second
+ Array.prototype.push.apply(
+ this.supported, maybe_supported);
+
+ })();
+
+}]);
diff --git a/guacamole/src/main/webapp/app/client/styles/client.css b/guacamole/src/main/webapp/app/client/styles/client.css
new file mode 100644
index 000000000..2ae327d24
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/styles/client.css
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+body {
+ background: black;
+ font-family: FreeSans, Helvetica, Arial, sans-serif;
+ padding: 0;
+ margin: 0;
+}
+
+img {
+ border: none;
+}
+
+.software-cursor {
+ cursor: url('images/mouse/blank.gif'),url('images/mouse/blank.cur'),default;
+ overflow: hidden;
+ cursor: none;
+}
+
+.guac-error .software-cursor {
+ cursor: default;
+}
+
+* {
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+.event-target {
+ position: fixed;
+ opacity: 0;
+}
+
+/* Dialogs */
+
+div.dialogOuter {
+ display: table;
+ height: 100%;
+ width: 100%;
+ position: fixed;
+ left: 0;
+ top: 0;
+ background: rgba(0, 0, 0, 0.75);
+}
+
+div.dialogMiddle {
+ width: 100%;
+ text-align: center;
+ display: table-cell;
+ vertical-align: middle;
+}
+
+button {
+
+ border-style: solid;
+ border-width: 1px;
+
+ padding: 0.25em;
+ padding-right: 1em;
+ padding-left: 1em;
+
+}
+
+button:active {
+
+ padding-top: 0.35em;
+ padding-left: 1.1em;
+
+ padding-bottom: 0.15em;
+ padding-right: 0.9em;
+
+}
+
+button#reconnect {
+ display: none;
+}
+
+.guac-error button#reconnect {
+ display: inline;
+
+ background: #200;
+ border-color: #822;
+ color: #944;
+}
+
+.guac-error button#reconnect:hover {
+ background: #822;
+ border-color: #B33;
+ color: black;
+}
+
+
+div.dialog p {
+ margin: 0;
+}
+
+div.displayOuter {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ display: table;
+}
+
+div.displayMiddle {
+ width: 100%;
+ display: table-cell;
+ vertical-align: middle;
+ text-align: center;
+}
+
+div.display * {
+ position: relative;
+}
+
+div.display > * {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+div.magnifier-background {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ overflow: hidden;
+}
+
+div.magnifier {
+
+ position: absolute;
+ left: 0;
+ top: 0;
+
+ box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.75);
+ width: 50%;
+ height: 50%;
+ overflow: hidden;
+
+}
+
+.pan-overlay,
+.type-overlay {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+.pan-overlay .indicator {
+ position: fixed;
+ background-size: 32px 32px;
+ -moz-background-size: 32px 32px;
+ -webkit-background-size: 32px 32px;
+ -khtml-background-size: 32px 32px;
+ background-position: center;
+ background-repeat: no-repeat;
+ opacity: 0.8;
+}
+
+.pan-overlay .indicator.up {
+
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 32px;
+
+ background-image: url('images/arrows/arrows-u.png');
+
+}
+
+.pan-overlay .indicator.down {
+
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 32px;
+
+ background-image: url('images/arrows/arrows-d.png');
+
+}
+
+.pan-overlay .indicator.left {
+
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 32px;
+
+ background-image: url('images/arrows/arrows-l.png');
+
+}
+
+.pan-overlay .indicator.right {
+
+ top: 0;
+ bottom: 0;
+ right: 0;
+ width: 32px;
+
+ background-image: url('images/arrows/arrows-r.png');
+
+}
+
+/* Viewport Clone */
+
+div#viewportClone {
+ display: table;
+ height: 100%;
+ width: 100%;
+ position: fixed;
+ left: 0;
+ top: 0;
+
+ visibility: hidden;
+}
+
+@keyframes show-dialog {
+ 0% {transform: scale(0.75); }
+ 100% {transform: scale(1); }
+}
+
+@-webkit-keyframes show-dialog {
+ 0% {-webkit-transform: scale(0.75); }
+ 100% {-webkit-transform: scale(1); }
+}
+
+.dialog {
+
+ animation-name: show-dialog;
+ animation-timing-function: linear;
+ animation-duration: 0.125s;
+ -webkit-animation-name: show-dialog;
+ -webkit-animation-timing-function: linear;
+ -webkit-animation-duration: 0.125s;
+
+ max-width: 75%;
+ max-height: none;
+ width: 4in;
+ -moz-border-radius: 0.2em;
+ -webkit-border-radius: 0.2em;
+ -khtml-border-radius: 0.2em;
+ border-radius: 0.2em;
+
+ padding: 0.5em;
+ text-align: left;
+
+}
+
+.guac-error .dialog {
+ background: #FDD;
+ border: 1px solid #964040;
+}
+
+.dialog .title {
+ font-size: 1.1em;
+ font-weight: bold;
+ border-bottom: 1px solid black;
+ margin-bottom: 0.5em;
+}
+
+.dialog .status {
+ padding: 0.5em;
+ font-size: 0.8em;
+}
+
+p.hint {
+
+ border: 0.25em solid rgba(255, 255, 255, 0.25);
+ background: black;
+ opacity: 0.75;
+
+ color: white;
+
+ max-width: 10em;
+ padding: 1em;
+ margin: 1em;
+
+ position: absolute;
+ left: 0;
+ top: 0;
+
+ box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.75);
+
+}
+
+#notificationArea {
+ position: fixed;
+ right: 0.5em;
+ bottom: 0.5em;
+ max-width: 25%;
+ min-width: 10em;
+}
+
+.notification {
+
+ font-size: 0.7em;
+ text-align: center;
+
+ border: 1px solid rgba(0, 0, 0, 0.75);
+ -moz-border-radius: 0.2em;
+ -webkit-border-radius: 0.2em;
+ -khtml-border-radius: 0.2em;
+ border-radius: 0.2em;
+ background: white;
+
+ color: black;
+
+ padding: 0.5em;
+ margin: 1em;
+ overflow: hidden;
+
+ box-shadow: 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.25);
+
+}
+
+.notification div {
+ display: inline-block;
+ text-align: left;
+}
+
+.notification .title-bar {
+ display: block;
+ white-space: nowrap;
+ font-weight: bold;
+
+ border-bottom: 1px solid black;
+ padding-bottom: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.notification .title-bar * {
+ vertical-align: middle;
+}
+
+.notification .close {
+
+ background: url('images/action-icons/guac-close.png');
+ background-size: 10px 10px;
+ -moz-background-size: 10px 10px;
+ -webkit-background-size: 10px 10px;
+ -khtml-background-size: 10px 10px;
+
+ width: 10px;
+ height: 10px;
+
+ float: right;
+ cursor: pointer;
+
+}
+
+@keyframes progress {
+ from {background-position: 0px 0px;}
+ to {background-position: 64px 0px;}
+}
+
+@-webkit-keyframes progress {
+ from {background-position: 0px 0px;}
+ to {background-position: 64px 0px;}
+}
+
+.notification .caption,
+.download.notification .caption {
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.upload.notification .status,
+.download.notification .status {
+ color: red;
+ font-size: 1em;
+ padding: 1em;
+}
+
+.download.notification .progress,
+.upload.notification .progress,
+.download.notification .download {
+
+ margin-top: 1em;
+ margin-left: 0.75em;
+ padding: 0.25em;
+ min-width: 5em;
+
+ border: 1px solid gray;
+ -moz-border-radius: 0.2em;
+ -webkit-border-radius: 0.2em;
+ -khtml-border-radius: 0.2em;
+ border-radius: 0.2em;
+
+ text-align: center;
+ float: right;
+
+ position: relative;
+
+}
+
+.upload.notification .progress {
+ float: none;
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.download.notification .progress div,
+.upload.notification .progress div {
+ position: relative;
+}
+
+.download.notification .progress .bar,
+.upload.notification .progress .bar {
+ background: #A3D655;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 0;
+ box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5),
+ inset -1px -1px 0 rgba( 0, 0, 0, 0.1),
+ 1px 1px 0 gray;
+}
+
+.upload.notification .progress,
+.download.notification .progress {
+
+ background: #C2C2C2 url('images/progress.png');
+ background-size: 16px 16px;
+ -moz-background-size: 16px 16px;
+ -webkit-background-size: 16px 16px;
+ -khtml-background-size: 16px 16px;
+
+ animation-name: progress;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+
+ -webkit-animation-name: progress;
+ -webkit-animation-duration: 2s;
+ -webkit-animation-timing-function: linear;
+ -webkit-animation-iteration-count: infinite;
+
+}
+
+.download.notification .download {
+ background: rgb(16, 87, 153);
+ cursor: pointer;
+}
+
+#preload {
+ visibility: hidden;
+ position: absolute;
+ left: 0;
+ right: 0;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
new file mode 100644
index 000000000..f6f2256fd
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Guacamole ${project.version}
+
+
{{'client.clipboard' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{'client.mouseMode' | translate}}
+
+
{{'client.mouseModeDesc' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{'client.display' | translate}}
+
+
+
+
{{formattedScale()}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/templates/clientError.html b/guacamole/src/main/webapp/app/client/templates/clientError.html
new file mode 100644
index 000000000..735d7ca0a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/templates/clientError.html
@@ -0,0 +1,35 @@
+
+
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/templates/guacClient.html b/guacamole/src/main/webapp/app/client/templates/guacClient.html
new file mode 100644
index 000000000..490f3cd34
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/templates/guacClient.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/connection/connectionModule.js b/guacamole/src/main/webapp/app/connection/connectionModule.js
new file mode 100644
index 000000000..4b195de4e
--- /dev/null
+++ b/guacamole/src/main/webapp/app/connection/connectionModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for code relating to connections.
+ */
+angular.module('connection', ['util']);
diff --git a/guacamole/src/main/webapp/app/connection/service/connectionDAO.js b/guacamole/src/main/webapp/app/connection/service/connectionDAO.js
new file mode 100644
index 000000000..774ad88c6
--- /dev/null
+++ b/guacamole/src/main/webapp/app/connection/service/connectionDAO.js
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The DAO for connection operations agains the REST API.
+ */
+angular.module('connection').factory('connectionDAO', ['$http', 'localStorageUtility',
+ function connectionDAO($http, localStorageUtility) {
+
+ var service = {};
+
+ /**
+ * Makes a request to the REST API to get a single connection, returning a
+ * promise that can be used for processing the results of the call.
+ *
+ * @param {string} id The ID of the connection.
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getConnection = function getConnection(id) {
+ return $http.get("api/connection/" + id + "?token=" + localStorageUtility.get('authToken'));
+ };
+
+ /**
+ * Makes a request to the REST API to get the list of connections,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} parentID The parent ID for the connection.
+ * If not passed in, it will query a list of the
+ * connections in the root group.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getConnections = function getConnections(parentID) {
+
+ var parentIDParam = "";
+ if(parentID !== undefined)
+ parentIDParam = "&parentID=" + parentID;
+
+ return $http.get("api/connection?token=" + localStorageUtility.get('authToken') + parentIDParam);
+ };
+
+ /**
+ * Makes a request to the REST API to save a connection,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} connection The connection to update
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.saveConnection = function saveConnection(connection) {
+
+ // Do not try to save the connection history records
+ var connectionToSave = angular.copy(connection);
+ delete connectionToSave.history;
+
+ // This is a new connection
+ if(!connectionToSave.identifier) {
+ return $http.post("api/connection/?token=" + localStorageUtility.get('authToken'), connectionToSave).success(
+ function setConnectionID(connectionID){
+ // Set the identifier on the new connection
+ connection.identifier = connectionID;
+ return connectionID;
+ });
+ } else {
+ return $http.post(
+ "api/connection/" + connectionToSave.identifier +
+ "?token=" + localStorageUtility.get('authToken'),
+ connectionToSave);
+ }
+ };
+
+ /**
+ * Makes a request to the REST API to move a connection to a different group,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} connection The connection to move.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.moveConnection = function moveConnection(connection) {
+
+ return $http.put(
+ "api/connection/" + connection.identifier +
+ "?token=" + localStorageUtility.get('authToken') +
+ "&parentID=" + connection.parentIdentifier,
+ connection);
+
+ };
+
+ /**
+ * Makes a request to the REST API to delete a connection,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} connection The connection to delete
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.deleteConnection = function deleteConnection(connection) {
+ return $http['delete'](
+ "api/connection/" + connection.identifier +
+ "?token=" + localStorageUtility.get('authToken'));
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/connectionGroup/connectionGroupModule.js b/guacamole/src/main/webapp/app/connectionGroup/connectionGroupModule.js
new file mode 100644
index 000000000..eff95131d
--- /dev/null
+++ b/guacamole/src/main/webapp/app/connectionGroup/connectionGroupModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for code relating to connection groups.
+ */
+angular.module('connectionGroup', ['util', 'connection']);
diff --git a/guacamole/src/main/webapp/app/connectionGroup/service/connectionGroupDAO.js b/guacamole/src/main/webapp/app/connectionGroup/service/connectionGroupDAO.js
new file mode 100644
index 000000000..4ce767ee5
--- /dev/null
+++ b/guacamole/src/main/webapp/app/connectionGroup/service/connectionGroupDAO.js
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The DAO for connection group operations agains the REST API.
+ */
+angular.module('connectionGroup').factory('connectionGroupDAO', ['$http', 'localStorageUtility',
+ function connectionGrouDAO($http, localStorageUtility) {
+
+ /**
+ * The ID of the root connection group.
+ */
+ var ROOT_CONNECTION_GROUP_ID = "ROOT";
+
+ var service = {};
+
+ /**
+ * Makes a request to the REST API to get the list of connection groups,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} parentID The parent ID for the connection group.
+ * If not passed in, it will query a list of the
+ * connection groups in the root group.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getConnectionGroups = function getConnectionGroups(parentID) {
+
+ var parentIDParam = "";
+ if(parentID !== undefined)
+ parentIDParam = "&parentID=" + parentID;
+
+ return $http.get("api/connectionGroup?token=" + localStorageUtility.get('authToken') + parentIDParam);
+ };
+
+ /**
+ * Makes a request to the REST API to get an individual connection group,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} connectionGroupID The ID for the connection group.
+ * If not passed in, it will query the
+ * root connection group.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getConnectionGroup = function getConnectionGroup(connectionGroupID) {
+
+ // Use the root connection group ID if no ID is passed in
+ connectionGroupID = connectionGroupID || ROOT_CONNECTION_GROUP_ID;
+
+ return $http.get("api/connectionGroup/" + connectionGroupID + "?token=" + localStorageUtility.get('authToken'));
+ };
+
+ /**
+ * Makes a request to the REST API to save a connection group,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} connectionGroup The connection group to update
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.saveConnectionGroup = function saveConnectionGroup(connectionGroup) {
+ // This is a new connection group
+ if(!connectionGroup.identifier) {
+ return $http.post("api/connectionGroup/?token=" + localStorageUtility.get('authToken'), connectionGroup).success(
+ function setConnectionGroupID(connectionGroupID){
+ // Set the identifier on the new connection
+ connectionGroup.identifier = connectionGroupID;
+ return connectionGroupID;
+ });
+ } else {
+ return $http.post(
+ "api/connectionGroup/" + connectionGroup.identifier +
+ "?token=" + localStorageUtility.get('authToken'),
+ connectionGroup);
+ }
+ };
+
+ /**
+ * Makes a request to the REST API to move a connection group to a different group,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} connectionGroup The connection group to move.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.moveConnectionGroup = function moveConnectionGroup(connectionGroup) {
+
+ return $http.put(
+ "api/connectionGroup/" + connectionGroup.identifier +
+ "?token=" + localStorageUtility.get('authToken') +
+ "&parentID=" + connectionGroup.parentIdentifier,
+ connectionGroup);
+ };
+
+ /**
+ * Makes a request to the REST API to delete a connection group,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} connectionGroup The connection group to delete
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.deleteConnectionGroup = function deleteConnectionGroup(connectionGroup) {
+ return $http['delete'](
+ "api/connectionGroup/" + connectionGroup.identifier +
+ "?token=" + localStorageUtility.get('authToken'));
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/connectionGroup/service/connectionGroupService.js b/guacamole/src/main/webapp/app/connectionGroup/service/connectionGroupService.js
new file mode 100644
index 000000000..92bb8c2a7
--- /dev/null
+++ b/guacamole/src/main/webapp/app/connectionGroup/service/connectionGroupService.js
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for performing useful connection group related functionaltiy.
+ */
+angular.module('connectionGroup').factory('connectionGroupService', ['$injector', function connectionGroupService($injector) {
+
+ var connectionGroupDAO = $injector.get('connectionGroupDAO');
+ var connectionDAO = $injector.get('connectionDAO');
+ var permissionCheckService = $injector.get('permissionCheckService');
+ var $q = $injector.get('$q');
+ var displayObjectPreparationService = $injector.get('displayObjectPreparationService');
+
+ var service = {};
+
+ // Add all groups from this group to the parent group child list
+ function addToParent(connectionGroup, parentGroup, context, includeConnections) {
+
+ // Include connections by default
+ if(typeof includeConnections === 'undefined')
+ includeConnections = true;
+
+ parentGroup.children.push(connectionGroup);
+
+ // Prepare this group for display
+ displayObjectPreparationService.prepareConnectionGroup(connectionGroup);
+
+ if(includeConnections) {
+ // Get all connections in the group and add them under this connection group
+ context.openRequest();
+ connectionDAO.getConnections(connectionGroup.identifier).success(function fetchConnections(connections) {
+ for(var i = 0; i < connections.length; i++) {
+ connections[i].isConnection = true;
+ connectionGroup.children.push(connections[i]);
+ }
+ context.closeRequest();
+ });
+ }
+
+ // Get all connection groups in the group and repeat
+ context.openRequest();
+ connectionGroupDAO.getConnectionGroups(connectionGroup.identifier).success(function fetchConnectionGroups(connectionGroups) {
+ for(var i = 0; i < connectionGroups.length; i++) {
+ addToParent(connectionGroups[i], connectionGroup, context, includeConnections);
+ }
+ context.closeRequest();
+ });
+ }
+
+ /**
+ * Queries all connections and connection groups under the connection group
+ * with the provided parent ID, and returns them in a heirarchical structure
+ * with convinient display properties set on the objects.
+ *
+ * @param {array} items The root list of connections and groups. Should be an
+ * initally empty array that will get filled in as the
+ * connections and groups are loaded.
+ *
+ * @param {string} parentID The parent ID for the connection group.
+ * If not passed in, it will begin with
+ * the root connection group.
+ *
+ * @param {boolean} includeConnections Whether or not to include connections
+ * in the structure. Defaults to true.
+ *
+ * @param {boolean} includeRoot Whether or not to include the root connection group
+ * in the structure. Defaults to false.
+ *
+ * @return {promise} A promise that will be fulfilled when all connections
+ * and groups have been loaded.
+ */
+ service.getAllGroupsAndConnections = function getAllGroupsAndConnections(items, parentID, includeConnections, includeRoot) {
+
+ // Include connections by default
+ if(typeof includeConnections === 'undefined')
+ includeConnections = true;
+
+ var context = {
+ // The number of requets to the server currently open
+ openRequests : 0,
+
+ // Create the promise
+ finishedFetching : $q.defer(),
+
+ // Notify the caller that the promise has been completed
+ complete : function complete() {
+ this.finishedFetching.resolve(items);
+ },
+
+ /**
+ * Indicate that a request has been started.
+ */
+ openRequest : function openRequest() {
+ this.openRequests++;
+ },
+
+ /**
+ * Indicate that a request has been completed. If this was the last
+ * open request, fulfill the promise.
+ */
+ closeRequest : function closeRequest() {
+ if(--this.openRequests === 0)
+ this.complete();
+ }
+ };
+
+ // Include the root only if it was asked for
+ if(includeRoot) {
+ context.openRequest();
+ connectionGroupDAO.getConnectionGroup(parentID).success(function setRootGroup (rootGroup) {
+ items.push(rootGroup);
+ rootGroup.children = [];
+ getChildrenOfRootGroup(rootGroup.children);
+ context.closeRequest();
+ });
+ } else {
+ getChildrenOfRootGroup(items);
+ }
+
+ // Get the children of the root group
+ function getChildrenOfRootGroup(children) {
+ context.openRequest();
+ connectionGroupDAO.getConnectionGroups(parentID).success(function fetchRootConnectionGroups(connectionGroups) {
+ for(var i = 0; i < connectionGroups.length; i++) {
+ addToParent(connectionGroups[i], {children: children}, context, includeConnections);
+ }
+
+ if(includeConnections) {
+ // Get all connections in the root group and add them under this connection group
+ context.openRequest();
+ connectionDAO.getConnections().success(function fetchRootConnections(connections) {
+ for(var i = 0; i < connections.length; i++) {
+
+ // Prepare this connection for display
+ displayObjectPreparationService.prepareConnection(connections[i]);
+
+ children.push(connections[i]);
+ }
+ context.closeRequest();
+ });
+ }
+
+ context.closeRequest();
+ });
+ }
+
+ // Return the promise
+ return context.finishedFetching.promise;
+ };
+
+
+ /**
+ * Filters the list of connections and groups using the provided permissions.
+ *
+ * @param {array} items The heirarchical list of groups and connections.
+ *
+ * @param {object} permissionList The list of permissions to use
+ * when filtering.
+ *
+ * @param {object} permissionCriteria A map of object type to permission type(s)
+ * required for that object type.
+ *
+ * @return {array} The filtered list.
+ */
+ service.filterConnectionsAndGroupByPermission = function filterConnectionsAndGroupByPermission(items, permissionList, permissionCriteria) {
+ var requiredConnectionPermission = permissionCriteria.CONNECTION;
+ var requiredConnectionGroupPermission = permissionCriteria.CONNECTION_GROUP;
+
+ for(var i = 0; i < items.length; i++) {
+ var item = items[i];
+
+ if(item.isConnection && requiredConnectionPermission) {
+
+ /*
+ * If item is a connection and a permission is required for this
+ * item, check now to see if the permission exists. If not,
+ * remove the item.
+ */
+ if(!permissionCheckService.checkPermission(permissionList,
+ "CONNECTION", item.identifier, requiredConnectionPermission)) {
+ items.splice(i, 1);
+ continue;
+ }
+ }
+ else {
+
+ /*
+ * If item is a group and a permission is required for this
+ * item, check now to see if the permission exists. If not,
+ * remove the item.
+ */
+ if(requiredConnectionGroupPermission) {
+ if(!permissionCheckService.checkPermission(permissionList,
+ "CONNECTION_GROUP", item.identifier, requiredConnectionGroupPermission)) {
+ items.splice(i, 1);
+ continue;
+ }
+ }
+
+ // Filter the children of this connection group as well
+ if(item.children && item.children.length)
+ service.filterConnectionsAndGroupByPermission(items.children);
+ }
+ }
+
+ return items;
+
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js
new file mode 100644
index 000000000..770f97b34
--- /dev/null
+++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The controller for the home page.
+ */
+angular.module('home').controller('homeController', ['$scope', '$injector',
+ function homeController($scope, $injector) {
+
+ // The parameter name for getting the history from local storage
+ var GUAC_HISTORY_STORAGE_KEY = "GUAC_HISTORY";
+
+ // Get the dependencies commonJS style
+ var connectionGroupService = $injector.get("connectionGroupService");
+ var localStorageUtility = $injector.get("localStorageUtility");
+
+ // All the connections and connection groups in root
+ $scope.connectionsAndGroups = [];
+
+ // All valid recent connections
+ $scope.recentConnections = [];
+
+ /* Fetch all connections and groups, then find which recent connections
+ * still refer to valid connections and groups.
+ */
+ connectionGroupService.getAllGroupsAndConnections($scope.connectionsAndGroups)
+ .then(function findRecentConnections() {
+
+ // Try to parse out the recent connections from local storage
+ var recentConnections;
+ try {
+ recentConnections = JSON.parse(localStorageUtility.get(GUAC_HISTORY_STORAGE_KEY));
+ } catch(e) {
+
+ // The recent history is corrupted - clear it
+ localStorageUtility.clear(GUAC_HISTORY_STORAGE_KEY);
+ }
+
+ // Figure out which recent connection entries are valid
+ $scope.connectionsAndGroups.forEach(function findValidEntries (connectionOrGroup) {
+
+ var type = connectionOrGroup.isConnection ? "c" : "cg";
+
+ // Find the unique ID to index into the recent connections
+ var uniqueId = encodeURIComponent(
+ type + "/" + connectionOrGroup.identifier
+ );
+
+ /*
+ * If it's a valid recent connection, add it to the list,
+ * along with enough information to make a connection url.
+ */
+ var recentConnection = recentConnections[uniqueId];
+ if(recentConnection) {
+ recentConnection.type = type;
+ recentConnection.id = connectionOrGroup.identifier;
+ $scope.recentConnections.push(recentConnection);
+ }
+ });
+ });
+
+ /**
+ * Toggle the open/closed status of the connectionGroup.
+ *
+ * @param {object} connectionGroup The connection group to toggle.
+ */
+ $scope.toggleExpanded = function toggleExpanded(connectionGroup) {
+ connectionGroup.expanded = !connectionGroup.expanded;
+ };
+
+}]);
diff --git a/guacamole/src/main/webapp/app/home/homeModule.js b/guacamole/src/main/webapp/app/home/homeModule.js
new file mode 100644
index 000000000..cbe17b62e
--- /dev/null
+++ b/guacamole/src/main/webapp/app/home/homeModule.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+angular.module('home', ['connection', 'connectionGroup', 'user', 'permission']);
diff --git a/guacamole/src/main/webapp/app/home/styles/home.css b/guacamole/src/main/webapp/app/home/styles/home.css
new file mode 100644
index 000000000..359589b5e
--- /dev/null
+++ b/guacamole/src/main/webapp/app/home/styles/home.css
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+.connection a, .group a {
+ text-decoration:none;
+ color: black;
+}
+
+.connection a:hover, .group a:hover {
+ text-decoration:none;
+ color: black;
+}
+
+.connection a:visited, .group a:visited {
+ text-decoration:none;
+ color: black;
+}
+
diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html
new file mode 100644
index 000000000..aa85e7283
--- /dev/null
+++ b/guacamole/src/main/webapp/app/home/templates/home.html
@@ -0,0 +1,78 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js b/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
new file mode 100644
index 000000000..dc5514952
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The config block for setting up the HTTP PATCH method.
+ */
+angular.module('index').config(['$httpProvider',
+ function indexHttpPatchConfig($httpProvider) {
+
+ $httpProvider.defaults.headers.patch = {
+ 'Content-Type': 'application/json'
+ }
+}]);
+
+
diff --git a/guacamole/src/main/webapp/app/index/config/indexInterceptorConfig.js b/guacamole/src/main/webapp/app/index/config/indexInterceptorConfig.js
new file mode 100644
index 000000000..8ceb6c09f
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/config/indexInterceptorConfig.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The config block for setting up the authentication interceptor.
+ */
+angular.module('index').config(['$httpProvider',
+ function indexInterceptorConfig($httpProvider) {
+ $httpProvider.interceptors.push('authenticationInterceptor');
+}]);
+
+
diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js
new file mode 100644
index 000000000..c1bebccaa
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The config block for setting up all the url routing.
+ */
+angular.module('index').config(['$routeProvider', '$locationProvider',
+ function indexRouteConfig($routeProvider, $locationProvider) {
+
+ // Disable HTML5 mode (use # for routing)
+ $locationProvider.html5Mode(false);
+
+ $routeProvider.
+ when('/', {
+ title: 'index.title',
+ templateUrl: 'app/home/templates/home.html',
+ controller: 'homeController'
+ }).
+ when('/manage/', {
+ title: 'index.title',
+ templateUrl: 'app/manage/templates/manage.html',
+ controller: 'manageController'
+ }).
+ when('/login/', {
+ title: 'index.title',
+ templateUrl: 'app/login/templates/login.html',
+ controller: 'loginController'
+ }).
+ when('/client/:type/:id/:params?', {
+ templateUrl: 'app/client/templates/client.html',
+ controller: 'clientController'
+ }).
+ otherwise({
+ redirectTo: '/'
+ });
+}]);
+
+
diff --git a/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js b/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js
new file mode 100644
index 000000000..237112493
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The configuration block for setting up everything having to do with i18n.
+ */
+angular.module('index').config(['$translateProvider', function($translateProvider) {
+ $translateProvider.preferredLanguage('en_US');
+
+ $translateProvider.useStaticFilesLoader({
+ prefix: 'translations/',
+ suffix: '.json'
+ });
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js
new file mode 100644
index 000000000..10380deb1
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The controller for the root of the application.
+ */
+angular.module('index').controller('indexController', ['$scope', '$injector',
+ function indexController($scope, $injector) {
+
+ // Get the dependencies commonJS style
+ var permissionDAO = $injector.get("permissionDAO"),
+ permissionCheckService = $injector.get("permissionCheckService"),
+ localStorageUtility = $injector.get("localStorageUtility"),
+ $q = $injector.get("$q"),
+ $document = $injector.get("$document"),
+ $window = $injector.get("$window"),
+ $location = $injector.get("$location");
+
+ /*
+ * Safe $apply implementation from Alex Vanston:
+ * https://coderwall.com/p/ngisma
+ */
+ $scope.safeApply = function(fn) {
+ var phase = this.$root.$$phase;
+ if(phase === '$apply' || phase === '$digest') {
+ if(fn && (typeof(fn) === 'function')) {
+ fn();
+ }
+ } else {
+ this.$apply(fn);
+ }
+ };
+
+ // Put some useful variables in the top level scope
+ $scope.page = { title: '' };
+ $scope.currentUserID = null;
+ $scope.currentUserIsAdmin = false;
+ $scope.currentUserHasUpdate = false;
+ $scope.currentUserPermissions = null;
+
+ // A promise to be fulfilled when all basic user permissions are loaded.
+ var permissionsLoaded= $q.defer();
+ $scope.basicPermissionsLoaded = permissionsLoaded.promise;
+
+ $scope.currentUserID = localStorageUtility.get('userID');
+
+ // If the user is unknown, force a login
+ if(!$scope.currentUserID)
+ $location.path('/login');
+
+ // Allow the permissions to be reloaded elsewhere if needed
+ $scope.loadBasicPermissions = function loadBasicPermissions() {
+
+ permissionDAO.getPermissions($scope.currentUserID).success(function fetchCurrentUserPermissions(permissions) {
+ $scope.currentUserPermissions = permissions;
+
+ // Will be true if the user is an admin
+ $scope.currentUserIsAdmin = permissionCheckService.checkPermission($scope.currentUserPermissions, "SYSTEM", undefined, "ADMINISTER");
+
+ // Will be true if the user is an admin or has update access to any object
+ $scope.currentUserHasUpdate = $scope.currentUserIsAdmin ||
+ permissionCheckService.checkPermission($scope.currentUserPermissions, undefined, undefined, "UPDATE");
+
+ permissionsLoaded.resolve();
+ });
+ };
+
+ // Try to load them now
+ $scope.loadBasicPermissions();
+
+ // Create event listeners at the global level
+ var keyboard = new Guacamole.Keyboard($document[0]);
+
+ // Broadcast keydown events down the scope heirarchy
+ keyboard.onkeydown = function onkeydown(keysym) {
+ var guacKeydownEvent = $scope.$broadcast('guacKeydown', keysym, keyboard);
+ return !guacKeydownEvent.defaultPrevented;
+ };
+
+ // Broadcast keyup events down the scope heirarchy
+ keyboard.onkeyup = function onkeyup(keysym) {
+ $scope.$broadcast('guacKeyup', keysym, keyboard);
+ };
+
+ // Release all keys when window loses focus
+ $window.onblur = function () {
+ keyboard.reset();
+ };
+
+ // Update title upon navigation
+ $scope.$on('$routeChangeSuccess', function(event, current, previous) {
+ var title = current.$$route.title;
+ if (title)
+ $scope.page.title = title;
+ });
+
+}]);
diff --git a/guacamole/src/main/webapp/app/index/indexModule.js b/guacamole/src/main/webapp/app/index/indexModule.js
new file mode 100644
index 000000000..ce6bd571d
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/indexModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for the root of the application.
+ */
+angular.module('index', ['ngRoute', 'pascalprecht.translate', 'home', 'manage', 'login', 'client']);
diff --git a/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js b/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js
new file mode 100644
index 000000000..f2ad9526a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/services/authenticationInterceptor.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+angular.module('index').factory('authenticationInterceptor', ['$location', '$q',
+ function authenticationInterceptor($location, $q) {
+
+ return {
+ 'response': function(response) {
+ return response || $q.when(response);
+ },
+
+ 'responseError': function(rejection) {
+ // Do not redirect failed login requests to the login page.
+ if ((rejection.status === 401 || rejection.status === 403)
+ && rejection.config.url.search('api/login') === -1) {
+ $location.path('/login');
+ }
+ return $q.reject(rejection);
+ }
+ };
+}]);
diff --git a/guacamole/src/main/webapp/app/login/controllers/loginController.js b/guacamole/src/main/webapp/app/login/controllers/loginController.js
new file mode 100644
index 000000000..38a621c76
--- /dev/null
+++ b/guacamole/src/main/webapp/app/login/controllers/loginController.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+angular.module('login').controller('loginController', ['$scope', '$injector',
+ function loginController($scope, $injector) {
+
+ // Get the dependencies commonJS style
+ var authenticationService = $injector.get("authenticationService");
+ var localStorageUtility = $injector.get("localStorageUtility");
+ var $location = $injector.get("$location");
+
+ // Clear the auth token and userID to log out the user
+ localStorageUtility.clear("authToken");
+ localStorageUtility.clear("userID");
+
+ $scope.loginError = false;
+
+ $scope.login = function login() {
+ authenticationService.login($scope.username, $scope.password)
+ .success(function success(data, status, headers, config) {
+ localStorageUtility.set('authToken', data.authToken);
+ localStorageUtility.set('userID', data.userID);
+
+ // Set up the basic permissions for the user
+ $scope.loadBasicPermissions();
+ $location.path('/');
+ }).error(function error(data, status, headers, config) {
+ $scope.loginError = true;
+ });
+ };
+}]);
diff --git a/guacamole/src/main/webapp/app/login/loginModule.js b/guacamole/src/main/webapp/app/login/loginModule.js
new file mode 100644
index 000000000..c6ad0d637
--- /dev/null
+++ b/guacamole/src/main/webapp/app/login/loginModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for the login functionality.
+ */
+angular.module('login', []);
diff --git a/guacamole/src/main/webapp/app/login/services/authenticationService.js b/guacamole/src/main/webapp/app/login/services/authenticationService.js
new file mode 100644
index 000000000..2d5498755
--- /dev/null
+++ b/guacamole/src/main/webapp/app/login/services/authenticationService.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for authenticating a user against the REST API.
+ */
+angular.module('index').factory('authenticationService', ['$http',
+ function authenticationService($http) {
+ var service = {};
+
+ /**
+ * Makes a request to authenticate a user using the login REST API endpoint,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} username The username to log in with.
+ * @param {string} password The password to log in with.
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.login = function login(username, password) {
+ return $http.post("api/login?username=" + username +"&password=" + password);
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/styles/login.css b/guacamole/src/main/webapp/app/login/styles/login.css
similarity index 79%
rename from guacamole/src/main/webapp/styles/login.css
rename to guacamole/src/main/webapp/app/login/styles/login.css
index c12f4ef28..5ebf5cc47 100644
--- a/guacamole/src/main/webapp/styles/login.css
+++ b/guacamole/src/main/webapp/app/login/styles/login.css
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Glyptodon LLC
+ * Copyright (C) 2014 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
@@ -39,26 +39,7 @@ body {
margin: 0;
}
-#manage {
- display: none;
-}
-
-.admin #manage {
- display: inline-block;
-}
-
-button#manage {
-
- background-image: url('../images/action-icons/guac-config.png');
- background-repeat: no-repeat;
- background-size: 1em;
- background-position: 0.5em 0.45em;
-
- padding-left: 1.8em;
-
-}
-
-div#login-ui {
+div.login-ui {
height: 100%;
width: 100%;
position: fixed;
@@ -83,11 +64,11 @@ div#login-ui {
100% { margin-left: 0.00em; margin-right: 0.00em; }
}
-p#login-error {
+p.login-error {
display: none;
}
-.error p#login-error {
+.error p.login-error {
display: block;
position: fixed;
@@ -107,7 +88,7 @@ p#login-error {
color: #964040;
}
-.error #login-form {
+.error .login-form {
animation-name: shake-head;
animation-duration: 0.25s;
animation-timing-function: linear;
@@ -116,21 +97,21 @@ p#login-error {
-webkit-animation-timing-function: linear;
}
-div#login-logo {
+div.login-logo {
position: relative;
bottom: 0;
display: inline-block;
vertical-align: middle;
}
-div#login-dialog-middle {
+div.login-dialog-middle {
width: 100%;
display: table-cell;
vertical-align: middle;
text-align: center;
}
-div#login-dialog {
+div.login-dialog {
max-width: 75%;
text-align: left;
@@ -138,20 +119,20 @@ div#login-dialog {
display: inline-block;
}
-div#login-dialog h1 {
+div.login-dialog h1 {
margin-top: 0;
margin-bottom: 0em;
text-align: center;
}
-div#login-dialog #buttons {
+div.login-dialog .buttons {
padding-top: 0.5em;
text-align: right;
}
-input[type="submit"]#login, button#login {
+input[type="submit"].login, button.login {
- background-image: url('../images/guacamole-logo-64.png');
+ background-image: url('images/guacamole-logo-64.png');
background-repeat: no-repeat;
background-size: 1.5em;
background-position: 0.5em 0.25em;
@@ -160,7 +141,7 @@ input[type="submit"]#login, button#login {
}
-div#login-dialog #login-fields {
+div.login-dialog .login-fields {
vertical-align: middle;
@@ -170,11 +151,11 @@ div#login-dialog #login-fields {
}
-div#login-dialog th {
+div.login-dialog th {
text-shadow: 1px 1px white;
}
-div#login-dialog #login-fields input {
+div.login-dialog .login-fields input {
border: 1px solid #777;
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
@@ -183,7 +164,7 @@ div#login-dialog #login-fields input {
width: 100%;
}
-div#login-dialog #login-fields img.logo {
+div.login-dialog .login-fields img.logo {
position: fixed;
margin: 10px;
left: 0;
@@ -192,7 +173,7 @@ div#login-dialog #login-fields img.logo {
z-index: -1;
}
-div#version {
+div.version {
text-align: center;
font-style: italic;
font-size: 0.75em;
@@ -206,12 +187,12 @@ img {
border: none;
}
-img#license {
+img.license {
float: right;
margin: 2px;
}
-div#connection-list-ui h1 {
+div.connection-list-ui h1 {
margin: 0;
padding: 0.5em;
@@ -222,7 +203,7 @@ div#connection-list-ui h1 {
}
-div#connection-list-ui h2 {
+div.connection-list-ui h2 {
padding: 0.5em;
margin: 0;
@@ -237,38 +218,38 @@ div#connection-list-ui h2 {
}
-div#connection-list-ui img {
+div.connection-list-ui img {
vertical-align: middle;
}
-div#logout-panel {
+div.logout-panel {
padding: 0.45em;
text-align: right;
float: right;
}
-.history-unavailable div#recent-connections {
+.history-unavailable div.recent-connections {
display: none;
}
-div#recent-connections,
-div#clipboardDiv,
-div#settings,
-div#all-connections {
+div.recent-connections,
+div.clipboardDiv,
+div.settings,
+div.all-connections {
margin: 1em;
padding: 0;
}
-#all-connections .list-buttons {
+.all-connections .list-buttons {
text-align: center;
padding: 0;
}
-div#recent-connections {
+div.recent-connections {
text-align: center;
}
-#no-recent {
+.no-recent {
color: black;
text-shadow: 1px 1px white;
@@ -278,7 +259,7 @@ div#recent-connections {
font-weight: bolder;
}
-div#recent-connections div.connection {
+div.recent-connections div.connection {
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
-khtml-border-radius: 0.5em;
@@ -296,15 +277,12 @@ div#recent-connections div.connection {
cursor: pointer;
}
-.connection:hover {
- background: #CDA;
+.group .connection .bears {
+ display: none;
}
-.group,
-.connection .name {
- color: black;
- font-weight: normal;
- padding: 0.1em;
+.connection:hover {
+ background: #CDA;
}
.connection .thumbnail {
@@ -317,20 +295,11 @@ div#recent-connections div.connection {
max-width: 75%;
}
-div#all-connections .connection {
- display: block;
- text-align: left;
-}
-
-div#recent-connections .connection .thumbnail {
+div.recent-connections .connection .thumbnail {
display: block;
}
-div#all-connections .connection {
- padding: 0.1em;
-}
-
-div#recent-connections .protocol {
+div.recent-connections .protocol {
display: none;
}
@@ -342,7 +311,7 @@ div#recent-connections .protocol {
margin-left: 0.25em;
}
-#clipboardDiv textarea {
+.clipboardDiv textarea {
width: 100%;
border: 1px solid #AAA;
-moz-border-radius: 0.25em;
@@ -352,12 +321,12 @@ div#recent-connections .protocol {
white-space: pre;
}
-#settings dt {
+.settings dt {
border-bottom: 1px dotted #AAA;
padding-bottom: 0.25em;
}
-#settings dd {
+.settings dd {
margin: 1.5em;
margin-left: 2.5em;
font-size: 0.75em;
diff --git a/guacamole/src/main/webapp/app/login/templates/login.html b/guacamole/src/main/webapp/app/login/templates/login.html
new file mode 100644
index 000000000..9dbd0519c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/login/templates/login.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
{{'login.loginError' | translate}}
+
+
+
+
+
+
+
diff --git a/guacamole/src/main/webapp/app/manage/controllers/connectionEditModalController.js b/guacamole/src/main/webapp/app/manage/controllers/connectionEditModalController.js
new file mode 100644
index 000000000..87f8598b8
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/controllers/connectionEditModalController.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The controller for the connection edit modal.
+ */
+angular.module('manage').controller('connectionEditModalController', ['$scope', '$injector',
+ function connectionEditModalController($scope, $injector) {
+
+ var connectionEditModal = $injector.get('connectionEditModal');
+ var connectionDAO = $injector.get('connectionDAO');
+ var displayObjectPreparationService = $injector.get('displayObjectPreparationService');
+
+ // Make a copy of the old connection so that we can copy over the changes when done
+ var oldConnection = $scope.connection;
+
+ // Copy data into a new conection object in case the user doesn't want to save
+ $scope.connection = angular.copy($scope.connection);
+
+ var newConnection = !$scope.connection.identifier;
+ if(newConnection)
+ // Prepare this connection for display
+ displayObjectPreparationService.prepareConnection($scope.connection);
+
+ // Set it to VNC by default
+ if(!$scope.connection.protocol)
+ $scope.connection.protocol = "vnc";
+
+ /**
+ * Close the modal.
+ */
+ $scope.close = function close() {
+ connectionEditModal.deactivate();
+ };
+
+ /**
+ * Save the connection and close the modal.
+ */
+ $scope.save = function save() {
+ connectionDAO.saveConnection($scope.connection).success(function successfullyUpdatedConnection() {
+
+ var oldParentID = oldConnection.parentIdentifier;
+ var newParentID = $scope.connection.parentIdentifier;
+
+ // Copy the data back to the original model
+ angular.extend(oldConnection, $scope.connection);
+
+ // We have to move this connection
+ if(oldParentID !== newParentID)
+
+ // New connections are created by default in root - don't try to move it if it's already there.
+ if(newConnection && newParentID === $scope.rootGroup.identifier) {
+ $scope.moveItem($scope.connection, oldParentID, newParentID);
+ } else {
+ connectionDAO.moveConnection($scope.connection).then(function moveConnection() {
+ $scope.moveItem($scope.connection, oldParentID, newParentID);
+ });
+ }
+
+ // Close the modal
+ connectionEditModal.deactivate();
+ });
+ };
+
+ /**
+ * Delete the connection and close the modal.
+ */
+ $scope['delete'] = function deleteConnection() {
+
+ // Nothing to delete if the connection is new
+ var newConnection = !$scope.connection.identifier;
+ if(newConnection) {
+ // Close the modal
+ connectionEditModal.deactivate();
+ return;
+ }
+
+ connectionDAO.deleteConnection($scope.connection).success(function successfullyDeletedConnection() {
+ var oldParentID = oldConnection.parentIdentifier;
+
+ // We have to remove this connection from the heirarchy
+ $scope.moveItem($scope.connection, oldParentID);
+
+ // Close the modal
+ connectionEditModal.deactivate();
+ });
+ }
+}]);
+
+
+
diff --git a/guacamole/src/main/webapp/app/manage/controllers/connectionGroupEditModalController.js b/guacamole/src/main/webapp/app/manage/controllers/connectionGroupEditModalController.js
new file mode 100644
index 000000000..7ab53779b
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/controllers/connectionGroupEditModalController.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The controller for the connection group edit modal.
+ */
+angular.module('manage').controller('connectionGroupEditModalController', ['$scope', '$injector',
+ function connectionEditModalController($scope, $injector) {
+
+ var connectionGroupEditModal = $injector.get('connectionGroupEditModal');
+ var connectionGroupDAO = $injector.get('connectionGroupDAO');
+ var displayObjectPreparationService = $injector.get('displayObjectPreparationService');
+
+ // Make a copy of the old connection group so that we can copy over the changes when done
+ var oldConnectionGroup = $scope.connectionGroup;
+
+ // Copy data into a new conection group object in case the user doesn't want to save
+ $scope.connectionGroup = angular.copy($scope.connectionGroup);
+
+ var newConnectionGroup = !$scope.connectionGroup.identifier;
+
+ $scope.types = [
+ {
+ label: "organizational",
+ value: "ORGANIZATIONAL"
+ },
+ {
+ label: "balancing",
+ value: "BALANCING"
+ }
+ ];
+
+ // Set it to organizational by default
+ if(!$scope.connectionGroup.type)
+ $scope.connectionGroup.type = $scope.types[0].value;
+
+ /**
+ * Close the modal.
+ */
+ $scope.close = function close() {
+ connectionGroupEditModal.deactivate();
+ };
+
+ /**
+ * Save the connection and close the modal.
+ */
+ $scope.save = function save() {
+ connectionGroupDAO.saveConnectionGroup($scope.connectionGroup).success(function successfullyUpdatedConnectionGroup() {
+
+ // Prepare this connection group for display
+ displayObjectPreparationService.prepareConnectionGroup($scope.connectionGroup);
+
+ var oldParentID = oldConnectionGroup.parentIdentifier;
+ var newParentID = $scope.connectionGroup.parentIdentifier;
+
+ // Copy the data back to the original model
+ angular.extend(oldConnectionGroup, $scope.connectionGroup);
+
+ // New groups are created by default in root - don't try to move it if it's already there.
+ if(newConnectionGroup && newParentID === $scope.rootGroup.identifier) {
+ $scope.moveItem($scope.connectionGroup, oldParentID, newParentID);
+ } else {
+ connectionGroupDAO.moveConnectionGroup($scope.connectionGroup).then(function moveConnectionGroup() {
+ $scope.moveItem($scope.connectionGroup, oldParentID, newParentID);
+ });
+ }
+
+ // Close the modal
+ connectionGroupEditModal.deactivate();
+ });
+ };
+
+ /**
+ * Delete the connection and close the modal.
+ */
+ $scope['delete'] = function deleteConnectionGroup() {
+
+ // Nothing to delete if the connection is new
+ if(newConnectionGroup)
+ // Close the modal
+ connectionGroupEditModal.deactivate();
+
+ connectionGroupDAO.deleteConnectionGroup($scope.connectionGroup).success(function successfullyDeletedConnectionGroup() {
+ var oldParentID = oldConnectionGroup.parentIdentifier;
+
+ // We have to remove this connection group from the heirarchy
+ $scope.moveItem($scope.connectionGroup, oldParentID);
+
+ // Close the modal
+ connectionGroupEditModal.deactivate();
+ });
+ }
+}]);
+
+
+
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageController.js b/guacamole/src/main/webapp/app/manage/controllers/manageController.js
new file mode 100644
index 000000000..1a097d60c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageController.js
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The controller for the administration page.
+ */
+angular.module('manage').controller('manageController', ['$scope', '$injector',
+ function manageController($scope, $injector) {
+
+ // Get the dependencies commonJS style
+ var connectionGroupService = $injector.get('connectionGroupService');
+ var connectionEditModal = $injector.get('connectionEditModal');
+ var connectionGroupEditModal = $injector.get('connectionGroupEditModal');
+ var userEditModal = $injector.get('userEditModal');
+ var protocolDAO = $injector.get('protocolDAO');
+ var userDAO = $injector.get('userDAO');
+ var userService = $injector.get('userService');
+
+ // All the connections and connection groups in root
+ $scope.connectionsAndGroups = [];
+
+ // All users that the current user has permission to edit
+ $scope.users = [];
+
+ $scope.basicPermissionsLoaded.then(function basicPermissionsHaveBeenLoaded() {
+ connectionGroupService.getAllGroupsAndConnections([], undefined, true, true).then(function filterConnectionsAndGroups(rootGroupList) {
+ $scope.rootGroup = rootGroupList[0];
+ $scope.connectionsAndGroups = $scope.rootGroup.children;
+
+ // Filter the items to only include ones that we have UPDATE for
+ if(!$scope.currentUserIsAdmin) {
+ connectionGroupService.filterConnectionsAndGroupByPermission(
+ $scope.connectionsAndGroups,
+ $scope.currentUserPermissions,
+ {
+ 'CONNECTION': 'UPDATE',
+ 'CONNECTION_GROUP': 'UPDATE'
+ }
+ );
+ }
+ });
+
+ userDAO.getUsers().success(function filterEditableUsers(users) {
+ $scope.users = users;
+
+ // Filter the users to only include ones that we have UPDATE for
+ if(!$scope.currentUserIsAdmin) {
+ userService.filterUsersByPermission(
+ $scope.users,
+ $scope.currentUserPermissions,
+ 'UPDATE'
+ );
+ }
+ });
+ });
+
+ /**
+ * Move the connection or connection group within the group heirarchy,
+ * initially place a new item, or remove an item from the heirarchy.
+ * @param {object} item The connection or connection group to move.
+ * @param {string} fromID The ID of the group to move the item from, if relevant.
+ * @param {string} toID The ID of the group to move the item to, if relevant.
+ */
+ $scope.moveItem = function moveItem(item, fromID, toID) {
+
+ // Remove the item from the old group, if there was one
+ if(fromID) {
+ var oldParent = findGroup($scope.rootGroup, fromID),
+ oldChildren = oldParent.children;
+
+ // Find and remove the item from the old group
+ for(var i = 0; i < oldChildren.length; i++) {
+ var child = oldChildren[i];
+ if(child.isConnection === item.isConnection &&
+ child.identifier === item.identifier) {
+ oldChildren.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ // Add the item to the new group, if there is one
+ if(toID) {
+ var newParent = findGroup($scope.rootGroup, toID);
+ newParent.children.push(item);
+ }
+ };
+
+ function findGroup(group, parentID) {
+ // Only searching in groups
+ if(group.isConnection)
+ return;
+
+ if(group.identifier === parentID)
+ return group;
+
+ for(var i = 0; i < group.children.length; i++) {
+ var child = group.children[i];
+ var foundGroup = findGroup(child, parentID);
+ if(foundGroup) return foundGroup;
+ }
+ }
+
+
+ $scope.protocols = {};
+
+ // Get the protocol information from the server and copy it into the scope
+ protocolDAO.getProtocols().success(function fetchProtocols(protocols) {
+ angular.extend($scope.protocols, protocols);
+ });
+
+ /**
+ * Toggle the open/closed status of the connectionGroup.
+ *
+ * @param {object} connectionGroup The connection group to toggle.
+ */
+ $scope.toggleExpanded = function toggleExpanded(connectionGroup) {
+ connectionGroup.expanded = !connectionGroup.expanded;
+ };
+
+ /**
+ * Open a modal to edit the connection.
+ *
+ * @param {object} connection The connection to edit.
+ */
+ $scope.editConnection = function editConnection(connection) {
+ connectionEditModal.activate(
+ {
+ connection : connection,
+ protocols : $scope.protocols,
+ moveItem : $scope.moveItem,
+ rootGroup : $scope.rootGroup
+ });
+ };
+
+ /**
+ * Open a modal to edit a new connection.
+ */
+ $scope.newConnection = function newConnection() {
+ connectionEditModal.activate(
+ {
+ connection : {},
+ protocols : $scope.protocols,
+ moveItem : $scope.moveItem,
+ rootGroup : $scope.rootGroup
+ });
+ };
+
+ /**
+ * Open a modal to edit a new connection group.
+ */
+ $scope.newConnectionGroup = function newConnectionGroup() {
+ connectionGroupEditModal.activate(
+ {
+ connectionGroup : {},
+ moveItem : $scope.moveItem,
+ rootGroup : $scope.rootGroup
+ });
+ };
+
+ /**
+ * Open a modal to edit the connection group.
+ *
+ * @param {object} connection The connection group to edit.
+ */
+ $scope.editConnectionGroup = function editConnectionGroup(connectionGroup) {
+ connectionGroupEditModal.activate(
+ {
+ connectionGroup : connectionGroup,
+ moveItem : $scope.moveItem,
+ rootGroup : $scope.rootGroup
+ });
+ };
+
+ // Remove the user from the current list of users
+ function removeUser(user) {
+ for(var i = 0; i < $scope.users.length; i++) {
+ if($scope.users[i].username === user.username) {
+ $scope.users.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Open a modal to edit the user.
+ *
+ * @param {object} user The user to edit.
+ */
+ $scope.editUser = function editUser(user) {
+ userEditModal.activate(
+ {
+ user : user,
+ rootGroup : $scope.rootGroup,
+ removeUser : removeUser
+ });
+ };
+
+ $scope.newUsername = "";
+
+ /**
+ * Open a modal to edit the user.
+ *
+ * @param {object} user The user to edit.
+ */
+ $scope.newUser = function newUser() {
+ if($scope.newUsername) {
+ var newUser = {
+ username: $scope.newUsername
+ };
+
+ userDAO.createUser(newUser).success(function addUserToList() {
+ $scope.users.push(newUser);
+ });
+
+ $scope.newUsername = "";
+ }
+ };
+
+}]);
+
+
+
diff --git a/guacamole/src/main/webapp/app/manage/controllers/userEditModalController.js b/guacamole/src/main/webapp/app/manage/controllers/userEditModalController.js
new file mode 100644
index 000000000..e53598089
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/controllers/userEditModalController.js
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The controller for the connection edit modal.
+ */
+angular.module('manage').controller('userEditModalController', ['$scope', '$injector',
+ function userEditModalController($scope, $injector) {
+
+ var userEditModal = $injector.get('userEditModal');
+ var userDAO = $injector.get('userDAO');
+ var permissionDAO = $injector.get('permissionDAO');
+
+ // Make a copy of the old user so that we can copy over the changes when done
+ var oldUser = $scope.user;
+
+ // Copy data into a new conection object in case the user doesn't want to save
+ $scope.user = angular.copy($scope.user);
+
+ /**
+ * Close the modal.
+ */
+ $scope.close = function close() {
+ userEditModal.deactivate();
+ };
+
+ /*
+ * All the permissions that have been modified since this modal was opened.
+ * Maps of type or id to value.
+ */
+ $scope.modifiedSystemPermissions = {};
+ $scope.modifiedConnectionPermissions = {};
+ $scope.modifiedConnectionGroupPermissions = {};
+
+ $scope.markSystemPermissionModified = function markSystemPermissionModified(type) {
+ $scope.modifiedSystemPermissions[type] = $scope.systemPermissions[type];
+ };
+
+ $scope.markConnectionPermissionModified = function markConnectionPermissionModified(id) {
+ $scope.modifiedConnectionPermissions[id] = $scope.connectionPermissions[id];
+ };
+
+ $scope.markConnectionGroupPermissionModified = function markConnectionGroupPermissionModified(id) {
+ $scope.modifiedConnectionGroupPermissions[id] = $scope.connectionGroupPermissions[id];
+ };
+
+ /**
+ * Save the user and close the modal.
+ */
+ $scope.save = function save() {
+
+ if($scope.passwordMatch !== $scope.user.password) {
+ //TODO: Display an error
+ return;
+ }
+
+ userDAO.saveUser($scope.user).success(function successfullyUpdatedUser() {
+
+ //Figure out what permissions have changed
+ var connectionPermissionsToCreate = [],
+ connectionPermissionsToDelete = [],
+ connectionGroupPermissionsToCreate = [],
+ connectionGroupPermissionsToDelete = [],
+ systemPermissionsToCreate = [],
+ systemPermissionsToDelete = [];
+
+ for(var type in $scope.modifiedSystemPermissions) {
+ // It was added
+ if($scope.modifiedSystemPermissions[type] && !originalSystemPermissions[type]) {
+ systemPermissionsToCreate.push(type);
+ }
+ // It was removed
+ else if(!$scope.modifiedSystemPermissions[type] && originalSystemPermissions[type]) {
+ systemPermissionsToDelete.push(type);
+ }
+ }
+
+ for(var id in $scope.modifiedConnectionPermissions) {
+ // It was added
+ if($scope.modifiedConnectionPermissions[id] && !originalConnectionPermissions[id]) {
+ connectionPermissionsToCreate.push(id);
+ }
+ // It was removed
+ else if(!$scope.modifiedConnectionPermissions[id] && originalConnectionPermissions[id]) {
+ connectionPermissionsToDelete.push(id);
+ }
+ }
+
+ for(var id in $scope.modifiedConnectionGroupPermissions) {
+ // It was added
+ if($scope.modifiedConnectionGroupPermissions[id] && !originalConnectionGroupPermissions[id]) {
+ connectionGroupPermissionsToCreate.push(id);
+ }
+ // It was removed
+ else if(!$scope.modifiedConnectionGroupPermissions[id] && originalConnectionGroupPermissions[id]) {
+ connectionGroupPermissionsToDelete.push(id);
+ }
+ }
+
+ var permissionsToAdd = [];
+ var permissionsToRemove = [];
+
+ // Create new connection permissions
+ for(var i = 0; i < connectionPermissionsToCreate.length; i++) {
+ permissionsToAdd.push({
+ objectType : "CONNECTION",
+ objectIdentifier : connectionPermissionsToCreate[i],
+ permissionType : "READ"
+ });
+ }
+
+ // Delete old connection permissions
+ for(var i = 0; i < connectionPermissionsToDelete.length; i++) {
+ permissionsToRemove.push({
+ objectType : "CONNECTION",
+ objectIdentifier : connectionPermissionsToDelete[i],
+ permissionType : "READ"
+ });
+ }
+
+ // Create new connection group permissions
+ for(var i = 0; i < connectionGroupPermissionsToCreate.length; i++) {
+ permissionsToAdd.push({
+ objectType : "CONNECTION_GROUP",
+ objectIdentifier : connectionGroupPermissionsToCreate[i],
+ permissionType : "READ"
+ });
+ }
+
+ // Delete old connection group permissions
+ for(var i = 0; i < connectionGroupPermissionsToDelete.length; i++) {
+ permissionsToRemove.push({
+ objectType : "CONNECTION_GROUP",
+ objectIdentifier : connectionGroupPermissionsToDelete[i],
+ permissionType : "READ"
+ });
+ }
+
+ // Create new system permissions
+ for(var i = 0; i < systemPermissionsToCreate.length; i++) {
+ permissionsToAdd.push({
+ objectType : "SYSTEM",
+ permissionType : systemPermissionsToCreate[i]
+ });
+ }
+
+ // Delete old system permissions
+ for(var i = 0; i < systemPermissionsToDelete.length; i++) {
+ permissionsToRemove.push({
+ objectType : "SYSTEM",
+ permissionType : systemPermissionsToDelete[i]
+ });
+ }
+
+ function completeSaveProcess() {
+ // Close the modal
+ userEditModal.deactivate();
+ }
+
+ function handleFailure() {
+ //TODO: Handle the permission API call failure
+ }
+
+ if(permissionsToAdd.length || permissionsToRemove.length) {
+ // Make the call to update the permissions
+ permissionDAO.patchPermissions(
+ $scope.user.username, permissionsToAdd, permissionsToRemove)
+ .success(completeSaveProcess).error(handleFailure);
+ } else {
+ completeSaveProcess();
+ }
+
+ });
+ };
+
+ $scope.permissions = [];
+
+ // Maps of connection and connection group IDs to access permission booleans
+ $scope.connectionPermissions = {};
+ $scope.connectionGroupPermissions = {};
+ $scope.systemPermissions = {};
+
+ // The original permissions to compare against
+ var originalConnectionPermissions,
+ originalConnectionGroupPermissions,
+ originalSystemPermissions;
+
+ // Get the permissions for the user we are editing
+ permissionDAO.getPermissions($scope.user.username).success(function gotPermissions(permissions) {
+ $scope.permissions = permissions;
+
+ // Figure out if the user has any system level permissions
+ for(var i = 0; i < $scope.permissions.length; i++) {
+ var permission = $scope.permissions[i];
+ if(permission.objectType === "SYSTEM") {
+
+ $scope.systemPermissions[permission.permissionType] = true;
+
+ // Only READ permission is editable via this UI
+ } else if (permission.permissionType === "READ") {
+ switch(permission.objectType) {
+ case "CONNECTION":
+ $scope.connectionPermissions[permission.objectIdentifier] = true;
+ break;
+ case "CONNECTION_GROUP":
+ $scope.connectionGroupPermissions[permission.objectIdentifier] = true;
+ break;
+ }
+ }
+ }
+
+ // Copy the original permissions so we can compare later
+ originalConnectionPermissions = angular.copy($scope.connectionPermissions);
+ originalConnectionGroupPermissions = angular.copy($scope.connectionGroupPermissions);
+ originalSystemPermissions = angular.copy($scope.systemPermissions);
+
+ });
+
+ /**
+ * Delete the user and close the modal.
+ */
+ $scope['delete'] = function deleteUser() {
+ userDAO.deleteUser($scope.user).success(function successfullyDeletedUser() {
+
+ // Remove the user from the list
+ $scope.removeUser($scope.user);
+
+ // Close the modal
+ userEditModal.deactivate();
+ });
+ }
+
+ /**
+ * Toggle the open/closed status of the connectionGroup.
+ *
+ * @param {object} connectionGroup The connection group to toggle.
+ */
+ $scope.toggleExpanded = function toggleExpanded(connectionGroup) {
+ connectionGroup.expanded = !connectionGroup.expanded;
+ };
+}]);
+
+
+
diff --git a/guacamole/src/main/webapp/app/manage/directives/locationChooser.js b/guacamole/src/main/webapp/app/manage/directives/locationChooser.js
new file mode 100644
index 000000000..d476a0ffb
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/directives/locationChooser.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+
+/**
+ * A directive for choosing the location of a connection or connection group.
+ */
+angular.module('manage').directive('locationChooser', [function locationChooser() {
+
+ return {
+ // Element only
+ restrict: 'E',
+ replace: true,
+ scope: {
+ item: '=item',
+ root: '=root',
+ },
+ templateUrl: 'app/manage/templates/locationChooser.html',
+ controller: ['$scope', '$injector', function locationChooserController($scope, $injector) {
+ // The dropdown should start closed
+ $scope.showDropDown = false;
+
+ // Map of ID to name for all connection groups
+ $scope.connectionGroupNameMap = {};
+
+ // Set up the group for display and search
+ mapConnectionGroupNames($scope.root);
+ $scope.connectionGroups = [$scope.root];
+
+ // Should be in the root group by default
+ if(!$scope.item.parentIdentifier)
+ $scope.item.parentIdentifier = $scope.root.parentIdentifier;
+
+ setCurrentParentName();
+
+ // Add the name of all connection groups under group to the group name map
+ function mapConnectionGroupNames(group) {
+ $scope.connectionGroupNameMap[group.identifier] = group.name;
+ for(var i = 0; i < group.children.length; i++) {
+ var child = group.children[i];
+ if(!child.isConnection)
+ mapConnectionGroupNames(child);
+ }
+ }
+
+ //Set the current connection group name to the name of the connection group with the currently chosen ID
+ function setCurrentParentName() {
+ $scope.currentConnectionGroupName = $scope.connectionGroupNameMap[$scope.item.parentIdentifier];
+ }
+
+ // Watch for changes to the parentID, and update the current name as needed
+ $scope.currentConnectionGroupName = "";
+ $scope.$watch('item.parentIdentifier', function watchParentID() {
+ setCurrentParentName();
+ });
+
+ /**
+ * Toggle the drop down - open or closed.
+ */
+ $scope.toggleDropDown = function toggleDropDown() {
+ $scope.showDropDown = !$scope.showDropDown;
+ }
+
+ /**
+ * Choose a new parent ID for the item.
+ * @param {type} parentID The new parentID.
+ */
+ $scope.chooseParentID = function chooseParentID(parentID) {
+ $scope.item.parentIdentifier = parentID;
+ }
+ }]
+ };
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/manage/manageModule.js b/guacamole/src/main/webapp/app/manage/manageModule.js
new file mode 100644
index 000000000..1f469b40c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/manageModule.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for the administration functionality.
+ */
+angular.module('manage', ['btford.modal', 'protocol', 'connectionGroup', 'util']);
+
diff --git a/guacamole/src/main/webapp/app/manage/services/connectionEditModal.js b/guacamole/src/main/webapp/app/manage/services/connectionEditModal.js
new file mode 100644
index 000000000..cc251798a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/services/connectionEditModal.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A modal for editing a connection.
+ */
+angular.module('manage').factory('connectionEditModal', ['btfModal',
+ function connectionEditModal(btfModal) {
+
+ // Create the modal object to be used later to actually create the modal
+ return btfModal({
+ controller: 'connectionEditModalController',
+ controllerAs: 'modal',
+ templateUrl: 'app/manage/templates/editableConnection.html',
+ });
+}]);
diff --git a/guacamole/src/main/webapp/app/manage/services/connectionGroupEditModal.js b/guacamole/src/main/webapp/app/manage/services/connectionGroupEditModal.js
new file mode 100644
index 000000000..70f57903a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/services/connectionGroupEditModal.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A modal for editing a connection group.
+ */
+angular.module('manage').factory('connectionGroupEditModal', ['btfModal',
+ function connectionGroupEditModal(btfModal) {
+
+ // Create the modal object to be used later to actually create the modal
+ return btfModal({
+ controller: 'connectionGroupEditModalController',
+ controllerAs: 'modal',
+ templateUrl: 'app/manage/templates/editableConnectionGroup.html',
+ });
+}]);
diff --git a/guacamole/src/main/webapp/app/manage/services/userEditModal.js b/guacamole/src/main/webapp/app/manage/services/userEditModal.js
new file mode 100644
index 000000000..5dd04d8ad
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/services/userEditModal.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A modal for editing a connection.
+ */
+angular.module('manage').factory('userEditModal', ['btfModal',
+ function userEditModal(btfModal) {
+
+ // Create the modal object to be used later to actually create the modal
+ return btfModal({
+ controller: 'userEditModalController',
+ controllerAs: 'modal',
+ templateUrl: 'app/manage/templates/editableUser.html',
+ });
+}]);
diff --git a/guacamole/src/main/webapp/styles/admin.css b/guacamole/src/main/webapp/app/manage/styles/manage.css
similarity index 73%
rename from guacamole/src/main/webapp/styles/admin.css
rename to guacamole/src/main/webapp/app/manage/styles/manage.css
index 420917f8f..20f2ce3f0 100644
--- a/guacamole/src/main/webapp/styles/admin.css
+++ b/guacamole/src/main/webapp/app/manage/styles/manage.css
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Glyptodon LLC
+ * Copyright (C) 2014 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
@@ -20,9 +20,9 @@
* THE SOFTWARE.
*/
-button#back {
+button.add-user {
- background-image: url('../images/action-icons/guac-back.png');
+ background-image: url('images/action-icons/guac-user-add.png');
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em 0.45em;
@@ -31,9 +31,9 @@ button#back {
}
-button#add-user {
+button.add-connection {
- background-image: url('../images/action-icons/guac-user-add.png');
+ background-image: url('images/action-icons/guac-monitor-add.png');
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em 0.45em;
@@ -42,20 +42,9 @@ button#add-user {
}
-button#add-connection {
+button.add-connection-group {
- background-image: url('../images/action-icons/guac-monitor-add.png');
- background-repeat: no-repeat;
- background-size: 1em;
- background-position: 0.5em 0.45em;
-
- padding-left: 1.8em;
-
-}
-
-button#add-connection-group {
-
- background-image: url('../images/action-icons/guac-group-add.png');
+ background-image: url('images/action-icons/guac-group-add.png');
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em 0.45em;
diff --git a/guacamole/src/main/webapp/app/manage/templates/editableConnection.html b/guacamole/src/main/webapp/app/manage/templates/editableConnection.html
new file mode 100644
index 000000000..b89e437a4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/templates/editableConnection.html
@@ -0,0 +1,131 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/manage/templates/editableConnectionGroup.html b/guacamole/src/main/webapp/app/manage/templates/editableConnectionGroup.html
new file mode 100644
index 000000000..13a04e2e0
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/templates/editableConnectionGroup.html
@@ -0,0 +1,79 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/manage/templates/locationChooser.html b/guacamole/src/main/webapp/app/manage/templates/locationChooser.html
new file mode 100644
index 000000000..2bf72826e
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/templates/locationChooser.html
@@ -0,0 +1,46 @@
+
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/permission/permissionModule.js b/guacamole/src/main/webapp/app/permission/permissionModule.js
new file mode 100644
index 000000000..e6afa2826
--- /dev/null
+++ b/guacamole/src/main/webapp/app/permission/permissionModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A module for code relating to permissions.
+ */
+angular.module('permission', []);
diff --git a/guacamole/src/main/webapp/app/permission/services/permissionCheckService.js b/guacamole/src/main/webapp/app/permission/services/permissionCheckService.js
new file mode 100644
index 000000000..4e3cc6c72
--- /dev/null
+++ b/guacamole/src/main/webapp/app/permission/services/permissionCheckService.js
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for checking if a specific permission exists
+ * in a given list of permissions.
+ */
+angular.module('permission').factory('permissionCheckService', [
+ function permissionCheckService() {
+
+ var service = {};
+
+ /**
+ * A service for checking if the given permission list contains the given
+ * permission, defined by the objectType, objectID, and permissionType.
+ * If the objectType or objectID are not passed, they will not be checked.
+ *
+ * For example, checkPermission(list, "CONNECTION", undefined, "READ") would
+ * check if the permission list contains permission to read any connection.
+ *
+ * @param {array} permissions The array of permissions to check.
+ * @param {string} objectType The object type for the permission.
+ * If not passed, this will not be checked.
+ * @param {string} objectID The ID of the object the permission is for.
+ * If not passed, this will not be checked.
+ * @param {string} permissionType The actual permission type to check for.
+ * @returns {boolean} True if the given permissions contain the requested permission, false otherwise.
+ */
+ service.checkPermission = function checkPermission(permissions, objectType, objectID, permissionType) {
+
+ // Loop through all the permissions and check if any of them match the given parameters
+ for(var i = 0; i < permissions.length; i++) {
+ var permission = permissions[i];
+
+ if(objectType === "SYSTEM") {
+ // System permissions have no object ID, we only need to check the type.
+ if(permission.permissionType === permissionType)
+ return true;
+ }
+ else {
+ // Object permissions need to match the object ID and type if given.
+ if(permission.permissionType === permissionType &&
+ (!objectType || permission.objectType === objectType) &&
+ (!objectID || permission.objectID === objectID))
+ return true;
+ }
+ }
+
+ // Didn't find any that matched
+ return false;
+ }
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/permission/services/permissionDAO.js b/guacamole/src/main/webapp/app/permission/services/permissionDAO.js
new file mode 100644
index 000000000..5a51c61bf
--- /dev/null
+++ b/guacamole/src/main/webapp/app/permission/services/permissionDAO.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The DAO for permission operations agains the REST API.
+ */
+angular.module('permission').factory('permissionDAO', ['$http', 'localStorageUtility',
+ function permissionDAO($http, localStorageUtility) {
+
+ var service = {};
+
+ /**
+ * Makes a request to the REST API to get the list of permissions for a given user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} userID The ID of the user to retrieve the permissions for.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getPermissions = function getPermissions(userID) {
+ return $http.get("api/permission/" + userID + "/?token=" + localStorageUtility.get('authToken'));
+ };
+
+ /**
+ * Makes a request to the REST API to add a permission for a given user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} userID The ID of the user to add the permission for.
+ * @param {object} permission The permission to add.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.addPermission = function addPermission(userID, permission) {
+ return $http.post("api/permission/" + userID + "/?token=" + localStorageUtility.get('authToken'), permission);
+ };
+
+
+
+ /**
+ * Makes a request to the REST API to remove a permission for a given user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} userID The ID of the user to remove the permission for.
+ * @param {object} permission The permission to remove.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.removePermission = function removePermission(userID, permission) {
+ return $http.post("api/permission/remove/" + userID + "/?token=" + localStorageUtility.get('authToken'), permission);
+ };
+
+
+ /**
+ * Makes a request to the REST API to modify the permissions for a given user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} userID The ID of the user to remove the permission for.
+ * @param {array} permissionsToAdd The permissions to add.
+ * @param {array} permissionsToRemove The permissions to remove.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.patchPermissions = function patchPermissions(userID, permissionsToAdd, permissionsToRemove) {
+ var permissionPatch = [];
+
+ // Add all the add operations to the patch
+ for(var i = 0; i < permissionsToAdd.length; i++ ) {
+ permissionPatch.push({
+ op : "add",
+ path : userID,
+ value : permissionsToAdd[i]
+ });
+ }
+
+ // Add all the remove operations to the patch
+ for(var i = 0; i < permissionsToRemove.length; i++ ) {
+ permissionPatch.push({
+ op : "remove",
+ path : userID,
+ value : permissionsToRemove[i]
+ });
+ }
+
+ // Make the HTTP call
+ return $http({
+ method : 'PATCH',
+ url : "api/permission/?token=" + localStorageUtility.get('authToken'),
+ data : permissionPatch
+ });
+ }
+
+
+
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/protocol/protocolModule.js b/guacamole/src/main/webapp/app/protocol/protocolModule.js
new file mode 100644
index 000000000..d6908b49c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/protocol/protocolModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The module for the protocol functionality.
+ */
+angular.module('protocol', []);
diff --git a/guacamole/src/main/webapp/app/protocol/services/protocolDAO.js b/guacamole/src/main/webapp/app/protocol/services/protocolDAO.js
new file mode 100644
index 000000000..b45fed2f5
--- /dev/null
+++ b/guacamole/src/main/webapp/app/protocol/services/protocolDAO.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The DAO for protocol operations agains the REST API.
+ */
+angular.module('protocol').factory('protocolDAO', ['$http', function protocolDAO($http) {
+
+ var service = {};
+
+ /**
+ * Makes a request to the REST API to get the list of protocols,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getProtocols = function getProtocols() {
+ return $http.get("api/protocol");
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/user/services/userDAO.js b/guacamole/src/main/webapp/app/user/services/userDAO.js
new file mode 100644
index 000000000..99ffd12b8
--- /dev/null
+++ b/guacamole/src/main/webapp/app/user/services/userDAO.js
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * The DAO for connection operations agains the REST API.
+ */
+angular.module('user').factory('userDAO', ['$http', 'localStorageUtility',
+ function userDAO($http, localStorageUtility) {
+
+ var service = {};
+
+ /**
+ * Makes a request to the REST API to get the list of users,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getUsers = function getUsers() {
+ return $http.get("api/user?token=" + localStorageUtility.get('authToken'));
+ };
+
+ /**
+ * Makes a request to the REST API to get the list of users,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {string} userID The ID of the user to retrieve.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.getUser = function getUser(userID) {
+ return $http.get("api/user/" + userID + "/?token=" + localStorageUtility.get('authToken'));
+ };
+
+ /**
+ * Makes a request to the REST API to delete a user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} user The user to delete.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.deleteUser = function deleteUser(user) {
+ return $http['delete'](
+ "api/user/" + user.username +
+ "?token=" + localStorageUtility.get('authToken'));
+ };
+
+
+ /**
+ * Makes a request to the REST API to create a user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} user The user to create.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.createUser = function createUser(user) {
+ return $http.post(
+ "api/user/"
+ + "?token=" + localStorageUtility.get('authToken'),
+ user
+ );
+ }
+
+ /**
+ * Makes a request to the REST API to save a user,
+ * returning a promise that can be used for processing the results of the call.
+ *
+ * @param {object} user The user to update.
+ *
+ * @returns {promise} A promise for the HTTP call.
+ */
+ service.saveUser = function saveUser(user) {
+ return $http.post(
+ "api/user/" + user.username +
+ "?token=" + localStorageUtility.get('authToken'),
+ user);
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/user/services/userService.js b/guacamole/src/main/webapp/app/user/services/userService.js
new file mode 100644
index 000000000..ea1cb633e
--- /dev/null
+++ b/guacamole/src/main/webapp/app/user/services/userService.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for performing useful user related functionaltiy.
+ */
+angular.module('user').factory('userService', ['$injector', function userService($injector) {
+
+ var permissionCheckService = $injector.get('permissionCheckService');
+
+ var service = {};
+
+ /**
+ * Filters the list of users using the provided permissions.
+ *
+ * @param {array} users The user list.
+ *
+ * @param {object} permissionList The list of permissions to use
+ * when filtering.
+ *
+ * @param {object} permissionCriteria The required permission for each user.
+ *
+ * @return {array} The filtered list.
+ */
+ service.filterUsersByPermission = function filterUsersByPermission(users, permissionList, permissionCriteria) {
+ for(var i = 0; i < users.length; i++) {
+ if(!permissionCheckService.checkPermission(permissionList,
+ "USER", user.username, permissionCriteria)) {
+ items.splice(i, 1);
+ continue;
+ }
+ }
+
+ return users;
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/user/userModule.js b/guacamole/src/main/webapp/app/user/userModule.js
new file mode 100644
index 000000000..4e3a5f602
--- /dev/null
+++ b/guacamole/src/main/webapp/app/user/userModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A module for code relating to users.
+ */
+angular.module('user', []);
diff --git a/guacamole/src/main/webapp/app/util/services/displayObjectPreparationService.js b/guacamole/src/main/webapp/app/util/services/displayObjectPreparationService.js
new file mode 100644
index 000000000..053deb88a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/util/services/displayObjectPreparationService.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service to help prepare objects from the REST API for display.
+ */
+angular.module('util').factory('displayObjectPreparationService', [function displayObjectPreparationService() {
+ var service = {};
+
+ /**
+ * Adds properties to the connection that will be useful for display.
+ *
+ * @param {object} connection The connection to add display properties to.
+ */
+ service.prepareConnection = function prepareConnection(connection) {
+
+ // This is a connection
+ connection.isConnection = true;
+ };
+
+ /**
+ * Adds properties to the connection that will be useful for display.
+ *
+ * @param {object} connectionGroup The connection group to add display properties to.
+ */
+ service.prepareConnectionGroup = function prepareConnectionGroup(connectionGroup) {
+
+ // This is not a connection
+ connectionGroup.isConnection = false;
+
+ connectionGroup.balancer = connectionGroup.type !== "ORGANIZATIONAL";
+ connectionGroup.expanded = false;
+ connectionGroup.children = [];
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/util/services/localStorageUtility.js b/guacamole/src/main/webapp/app/util/services/localStorageUtility.js
new file mode 100644
index 000000000..5841d14fc
--- /dev/null
+++ b/guacamole/src/main/webapp/app/util/services/localStorageUtility.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A service for handling storage and retrieval of values on localStorage.
+ * If local storage is not available, cookies will be used as a fallback.
+ */
+angular.module('util').factory('localStorageUtility', ['$cookieStore',
+ function localStorageUtility($cookieStore) {
+
+ var service = {};
+
+ // The prefix to use when storing cookies
+ var COOKIE_PREFIX = "guacamole.ui.localstorage.";
+
+ // Check if we can actually use localStorage
+ var localStorageEnabled;
+ try {
+ window.localStorage.setItem("test", "test");
+ window.localStorage.removeItem("test");
+ localStorageEnabled = true;
+ } catch(e) {
+ localStorageEnabled = false;
+ }
+
+ var getFunc, setFunc;
+
+ if(localStorageEnabled) {
+
+ // Just a passthrough to localStorage
+ getFunc = function getFromLocalStorage(key) {
+ return window.localStorage.getItem(key);
+ };
+
+ setFunc = function setOnLocalStorage(key, value) {
+ return window.localStorage.setItem(key, value);
+ };
+ }
+ else {
+
+ // Store the values as cookies
+ getFunc = function getValueFromCookie(key) {
+ return $cookieStore.get(COOKIE_PREFIX + key);
+ };
+
+ setFunc = function setValueOnCookie(key, value) {
+ return $cookieStore.put(COOKIE_PREFIX + key, value);
+ }
+ }
+
+ /**
+ * Gets a value from the persistent local store.
+ *
+ * @param {string} key The key to use as an index into the map.
+ *
+ * @returns {string} The value, if found.
+ */
+ service.get = getFunc;
+
+ /**
+ * Sets a value on the persistent local store.
+ *
+ * @param {string} key The key to use as an index into the map.
+ * @param {string} value The value to store in the map.
+ */
+ service.set = setFunc;
+
+ /**
+ * Clear a value from the persistent local store.
+ *
+ * @param {string} key The key to clear from the map.
+ */
+ service.clear = function clear(key) {
+ return service.set(key, undefined);
+ };
+
+ return service;
+}]);
diff --git a/guacamole/src/main/webapp/app/util/utilModule.js b/guacamole/src/main/webapp/app/util/utilModule.js
new file mode 100644
index 000000000..b86ffb432
--- /dev/null
+++ b/guacamole/src/main/webapp/app/util/utilModule.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+/**
+ * A module for miscellaneous services and utilities that don't belong elsewhere.
+ */
+angular.module('util', ['ngCookies']);
diff --git a/guacamole/src/main/webapp/client.xhtml b/guacamole/src/main/webapp/client.xhtml
deleted file mode 100644
index 5209ff330..000000000
--- a/guacamole/src/main/webapp/client.xhtml
+++ /dev/null
@@ -1,173 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Guacamole ${project.version}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Guacamole ${project.version}
-
-
Clipboard
-
-
Text copied/cut within Guacamole will appear here. Changes to the text below will affect the remote clipboard.
-
-
-
-
Input method
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Mouse emulation mode
-
-
Determines how the remote mouse behaves with respect to touches.