GUAC-963: Proof-of-concept ManagedClient implementation. Remove guacClientFactory and guacTunnelFactory (functionality replaced by ManagedClient).

This commit is contained in:
Michael Jumper
2014-12-28 19:27:04 -08:00
parent 9f6b7014e1
commit 35ca205653
9 changed files with 742 additions and 509 deletions

View File

@@ -33,6 +33,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Required services
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var guacClientManager = $injector.get('guacClientManager');
/**
* The minimum number of pixels a drag gesture must move to result in the
@@ -149,7 +150,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
name : "CLIENT.ACTION_RECONNECT",
// Handle reconnect action
callback : function reconnectCallback() {
$scope.id = uniqueId;
$scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params);
$scope.showStatus(false);
}
};
@@ -164,12 +165,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
remaining: 15
};
// Client settings and state
$scope.clientProperties = new ClientProperties();
// Initialize clipboard data to an empty string
$scope.clipboardData = "";
// Hide menu by default
$scope.menuShown = false;
@@ -198,8 +193,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* as well as any extra parameters if set.
*/
var uniqueId = $routeParams.type + '/' + $routeParams.id;
$scope.id = uniqueId;
$scope.connectionParameters = $routeParams.params || '';
$scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
// Pull connection name from server
switch ($routeParams.type) {
@@ -266,9 +260,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
}
// Scroll display if absolute mouse is in use
else if ($scope.clientProperties.emulateAbsoluteMouse) {
$scope.clientProperties.scrollLeft -= deltaX;
$scope.clientProperties.scrollTop -= deltaY;
else if ($scope.client.clientProperties.emulateAbsoluteMouse) {
$scope.client.clientProperties.scrollLeft -= deltaX;
$scope.client.clientProperties.scrollTop -= deltaY;
}
return false;
@@ -305,7 +299,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
// Do not handle pinch gestures while relative mouse is in use
if (!$scope.clientProperties.emulateAbsoluteMouse)
if (!$scope.client.clientProperties.emulateAbsoluteMouse)
return false;
// Stop gesture if not in progress
@@ -316,26 +310,26 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Set initial scale if gesture has just started
if (!initialScale) {
initialScale = $scope.clientProperties.scale;
initialCenterX = (centerX + $scope.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.clientProperties.scrollTop) / initialScale;
initialScale = $scope.client.clientProperties.scale;
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
}
// Determine new scale absolutely
var currentScale = initialScale * currentLength / startLength;
// Fix scale within limits - scroll will be miscalculated otherwise
currentScale = Math.max(currentScale, $scope.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.clientProperties.maxScale);
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
// Update scale based on pinch distance
$scope.autoFit = false;
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale = currentScale;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale = currentScale;
// Scroll display to keep original pinch location centered within current pinch
$scope.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
return false;
@@ -357,7 +351,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.$broadcast('guacClipboard', 'text/plain', $scope.clipboardData);
// Disable client keyboard if the menu is shown
$scope.clientProperties.keyboardEnabled = !menuShown;
$scope.client.clientProperties.keyboardEnabled = !menuShown;
});
@@ -385,7 +379,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
keyboard.reset();
// Toggle the menu
$scope.safeApply(function() {
$scope.$apply(function() {
$scope.menuShown = !$scope.menuShown;
});
}
@@ -478,33 +472,33 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
});
$scope.formattedScale = function formattedScale() {
return Math.round($scope.clientProperties.scale * 100);
return Math.round($scope.client.clientProperties.scale * 100);
};
$scope.zoomIn = function zoomIn() {
$scope.autoFit = false;
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale += 0.1;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale += 0.1;
};
$scope.zoomOut = function zoomOut() {
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale -= 0.1;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale -= 0.1;
};
$scope.autoFit = true;
$scope.changeAutoFit = function changeAutoFit() {
if ($scope.autoFit && $scope.clientProperties.minScale) {
$scope.clientProperties.autoFit = true;
if ($scope.autoFit && $scope.client.clientProperties.minScale) {
$scope.client.clientProperties.autoFit = true;
} else {
$scope.clientProperties.autoFit = false;
$scope.clientProperties.scale = 1;
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale = 1;
}
};
$scope.autoFitDisabled = function() {
return $scope.clientProperties.minZoom >= 1;
return $scope.client.clientProperties.minZoom >= 1;
};
/**
@@ -568,7 +562,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
var downloadNotificationIDs = {};
$scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = {
className : 'download',
@@ -583,7 +577,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
});
$scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = downloadNotifications[streamIndex];
if (notification)
@@ -593,7 +587,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
});
$scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = downloadNotifications[streamIndex];
var notificationID = downloadNotificationIDs[streamIndex];
@@ -629,7 +623,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
var uploadNotificationIDs = {};
$scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = {
className : 'upload',
@@ -644,7 +638,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
});
$scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = uploadNotifications[streamIndex];
if (notification)
@@ -654,7 +648,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
});
$scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = uploadNotifications[streamIndex];
var notificationID = uploadNotificationIDs[streamIndex];
@@ -683,7 +677,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
});
$scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) {
$scope.safeApply(function() {
$scope.$apply(function() {
var notification = uploadNotifications[streamIndex];
var notificationID = uploadNotificationIDs[streamIndex];

View File

@@ -32,46 +32,19 @@ angular.module('client').directive('guacClient', [function guacClient() {
scope: {
/**
* Parameters for controlling client state.
* The client to display within this guacClient directive.
*
* @type ClientProperties|Object
* @type ManagedClient
*/
clientProperties : '=',
client : '='
/**
* The ID of the Guacamole connection to connect to.
*
* @type String
*/
id : '=',
/**
* Arbitrary URL-encoded parameters to append to the connection
* string when connecting.
*
* @type String
*/
connectionParameters : '='
},
templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
/*
* 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);
}
};
// Required services
var $window = $injector.get('$window');
/**
* Whether the local, hardware mouse cursor is in use.
*
@@ -146,14 +119,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
*/
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
var $window = $injector.get('$window'),
guacAudio = $injector.get('guacAudio'),
guacVideo = $injector.get('guacVideo'),
guacHistory = $injector.get('guacHistory'),
guacTunnelFactory = $injector.get('guacTunnelFactory'),
guacClientFactory = $injector.get('guacClientFactory'),
authenticationService = $injector.get('authenticationService');
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
@@ -163,60 +128,20 @@ angular.module('client').directive('guacClient', [function guacClient() {
if (!display) return;
// Calculate scale to fit screen
$scope.clientProperties.minScale = Math.min(
$scope.client.clientProperties.minScale = Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
);
// Calculate appropriate maximum zoom level
$scope.clientProperties.maxScale = Math.max($scope.clientProperties.minScale, 3);
$scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
// Clamp zoom level, maintain auto-fit
if (display.getScale() < $scope.clientProperties.minScale || $scope.clientProperties.autoFit)
$scope.clientProperties.scale = $scope.clientProperties.minScale;
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
else if (display.getScale() > $scope.clientProperties.maxScale)
$scope.clientProperties.scale = $scope.clientProperties.maxScale;
};
/**
* Returns the string of connection parameters to be passed to the
* Guacamole client during connection. This string generally
* contains the desired connection ID, display resolution, and
* supported audio/video codecs.
*
* @returns {String} The string of connection parameters to be
* passed to the Guacamole client.
*/
var getConnectString = function getConnectString() {
// 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;
// Build base connect string
var connectString =
"id=" + encodeURIComponent($scope.id)
+ "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&width=" + Math.floor(optimal_width)
+ "&height=" + Math.floor(optimal_height)
+ "&dpi=" + Math.floor(optimal_dpi)
+ ($scope.connectionParameters ? '&' + $scope.connectionParameters : '');
// 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);
});
return connectString;
else if (display.getScale() > $scope.client.clientProperties.maxScale)
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
};
@@ -322,82 +247,36 @@ angular.module('client').directive('guacClient', [function guacClient() {
* SCROLLING
*/
$scope.$watch('clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
$scope.clientProperties.scrollLeft = main.scrollLeft;
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
});
$scope.$watch('clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
main.scrollTop = scrollTop;
$scope.clientProperties.scrollTop = main.scrollTop;
$scope.client.clientProperties.scrollTop = main.scrollTop;
});
/*
* CONNECT / RECONNECT
*/
// Attach any given managed client
$scope.$watch('client', function(managedClient) {
/**
* Store the thumbnail of the currently connected client within
* the connection history under the given ID. If the client is not
* connected, or if no ID is given, this function has no effect.
*
* @param {String} id
* The ID of the history entry to update.
*/
var updateHistoryEntry = function updateHistoryEntry(id) {
// Remove any existing display
displayContainer.innerHTML = "";
// Update stored thumbnail of previous connection
if (id && display && display.getWidth() > 0 && display.getHeight() > 0) {
// Get screenshot
var canvas = display.flatten();
// Calculate scale of thumbnail (max 320x240, max zoom 100%)
var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1);
// Create thumbnail canvas
var thumbnail = document.createElement("canvas");
thumbnail.width = canvas.width*scale;
thumbnail.height = canvas.height*scale;
// Scale screenshot to thumbnail
var context = thumbnail.getContext("2d");
context.drawImage(canvas,
0, 0, canvas.width, canvas.height,
0, 0, thumbnail.width, thumbnail.height
);
guacHistory.updateThumbnail(id, thumbnail.toDataURL("image/png"));
}
};
// Connect to given ID whenever ID changes
$scope.$watch('id', function(id, previousID) {
// If a client is already attached, ensure it is disconnected
if (client)
client.disconnect();
// Update stored thumbnail of previous connection
updateHistoryEntry(previousID);
// Only proceed if a new client is attached
if (!id)
// Only proceed if a client is given
if (!managedClient)
return;
// Get new client instance
var tunnel = guacTunnelFactory.getInstance($scope);
client = guacClientFactory.getInstance($scope, tunnel);
// Get Guacamole client instance
client = managedClient.client;
// Init display
// Attach possibly new display
display = client.getDisplay();
display.scale($scope.clientProperties.scale);
display.scale($scope.client.clientProperties.scale);
// Update the scale of the display when the client display size changes.
display.onresize = function() {
$scope.safeApply(updateDisplayScale);
$scope.$apply(updateDisplayScale);
};
// Use local cursor if possible, update localCursor flag
@@ -407,7 +286,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Add display element
displayElement = display.getElement();
displayContainer.innerHTML = "";
displayContainer.appendChild(displayElement);
// Do nothing when the display element is clicked on.
@@ -416,17 +294,6 @@ angular.module('client').directive('guacClient', [function guacClient() {
return false;
};
// Connect
client.connect(getConnectString());
});
// Clean up when client directive is destroyed
$scope.$on('$destroy', function destroyClient() {
// Update stored thumbnail of current connection
updateHistoryEntry($scope.id);
});
/*
@@ -434,7 +301,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
*/
// Watch for changes to mouse emulation mode
$scope.$watch('clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
$scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) {
if (!client || !display) return;
@@ -483,14 +350,14 @@ angular.module('client').directive('guacClient', [function guacClient() {
*/
// Adjust scale if modified externally
$scope.$watch('clientProperties.scale', function changeScale(scale) {
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
// Fix scale within limits
scale = Math.max(scale, $scope.clientProperties.minScale);
scale = Math.min(scale, $scope.clientProperties.maxScale);
scale = Math.max(scale, $scope.client.clientProperties.minScale);
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
// If at minimum zoom level, hide scroll bars
if (scale === $scope.clientProperties.minScale)
if (scale === $scope.client.clientProperties.minScale)
main.style.overflow = "hidden";
// If not at minimum zoom level, show scroll bars
@@ -501,15 +368,15 @@ angular.module('client').directive('guacClient', [function guacClient() {
if (display)
display.scale(scale);
if (scale !== $scope.clientProperties.scale)
$scope.clientProperties.scale = scale;
if (scale !== $scope.client.clientProperties.scale)
$scope.client.clientProperties.scale = scale;
});
// If autofit is set, the scale should be set to the minimum scale, filling the screen
$scope.$watch('clientProperties.autoFit', function changeAutoFit(autoFit) {
$scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
if(autoFit)
$scope.clientProperties.scale = $scope.clientProperties.minScale;
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
});
// If the element is resized, attempt to resize client
@@ -527,7 +394,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
}
$scope.safeApply(updateDisplayScale);
$scope.$apply(updateDisplayScale);
});
@@ -537,7 +404,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Listen for broadcasted keydown events and fire the appropriate listeners
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(1, keysym);
event.preventDefault();
}
@@ -545,7 +412,7 @@ angular.module('client').directive('guacClient', [function guacClient() {
// Listen for broadcasted keyup events and fire the appropriate listeners
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(0, keysym);
event.preventDefault();
}

View File

@@ -1,176 +0,0 @@
/*
* 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 creating Guacamole clients.
*/
angular.module('client').factory('guacClientFactory', ['$rootScope',
function guacClientFactory($rootScope) {
var service = {};
/**
* Returns a new Guacamole client instance which connects using the
* provided tunnel.
*
* @param {Scope} $scope The current scope.
* @param {Guacamole.Tunnel} tunnel The tunnel to connect through.
* @returns {Guacamole.Client} A new Guacamole client instance.
*/
service.getInstance = function getClientInstance($scope, tunnel) {
// Instantiate client
var guacClient = new Guacamole.Client(tunnel);
/*
* Fire guacClientStateChange events when client state changes.
*/
guacClient.onstatechange = function onClientStateChange(clientState) {
$scope.safeApply(function() {
switch (clientState) {
// Idle
case 0:
$scope.$emit('guacClientStateChange', guacClient, "idle");
break;
// Connecting
case 1:
$scope.$emit('guacClientStateChange', guacClient, "connecting");
break;
// Connected + waiting
case 2:
$scope.$emit('guacClientStateChange', guacClient, "waiting");
break;
// Connected
case 3:
$scope.$emit('guacClientStateChange', guacClient, "connected");
break;
// Disconnecting / disconnected are handled by tunnel instead
case 4:
case 5:
break;
}
});
};
/*
* Fire guacClientName events when a new name is received.
*/
guacClient.onname = function onClientName(name) {
$scope.safeApply(function() {
$scope.$emit('guacClientName', guacClient, name);
});
};
/*
* Disconnect and fire guacClientError when the client receives an
* error.
*/
guacClient.onerror = function onClientError(status) {
$scope.safeApply(function() {
// Disconnect, if connected
guacClient.disconnect();
$scope.$emit('guacClientError', guacClient, status.code);
});
};
/*
* Fire guacClientClipboard events after new clipboard data is received.
*/
guacClient.onclipboard = function onClientClipboard(stream, mimetype) {
$scope.safeApply(function() {
// 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', guacClient, mimetype, data);
};
});
};
/*
* Fire guacFileStart, guacFileProgress, and guacFileEnd events during
* the receipt of files.
*/
guacClient.onfile = function onClientFile(stream, mimetype, filename) {
$scope.safeApply(function() {
// Begin file download
var guacFileStartEvent = $scope.$emit('guacClientFileDownloadStart', guacClient, 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('guacClientFileDownloadProgress', guacClient, stream.index, mimetype, filename, blob_reader.getLength());
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// When complete, prompt for download
blob_reader.onend = function onend() {
$scope.$emit('guacClientFileDownloadEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob());
};
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);
});
};
return guacClient;
};
return service;
}]);

View File

@@ -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 managing several active Guacamole clients.
*/
angular.module('client').factory('guacClientManager', ['ManagedClient',
function guacClientManager(ManagedClient) {
var service = {};
/**
* Map of all active managed clients. Each key is the ID of the connection
* used by that client.
*
* @type Object.<String, ManagedClient>
*/
service.managedClients = {};
/**
* Creates a new ManagedClient associated with the connection having the
* given ID. If such a ManagedClient already exists, it is disconnected and
* replaced.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) {
// Disconnect any existing client
if (id in service.managedClients)
service.managedClients[id].client.disconnect();
// Set new client
return service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
};
/**
* Returns the ManagedClient associated with the connection having the
* given ID. If no such ManagedClient exists, a new ManagedClient is
* created.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.getManagedClient = function getManagedClient(id, connectionParameters) {
// Create new managed client if it doesn't already exist
if (!(id in service.managedClients))
service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
// Return existing client
return service.managedClients[id];
};
return service;
}]);

View File

@@ -1,89 +0,0 @@
/*
* 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 creating Guacamole tunnels.
*/
angular.module('client').factory('guacTunnelFactory', ['$rootScope', '$window',
function guacTunnelFactory($rootScope, $window) {
var service = {};
/**
* Returns a new Guacamole tunnel instance, using an implementation that is
* supported by the web browser.
*
* @param {Scope} $scope The current scope.
* @returns {Guacamole.Tunnel} A new Guacamole tunnel instance.
*/
service.getInstance = function getTunnelInstance($scope) {
var tunnel;
// If WebSocket available, try to use it.
if ($window.WebSocket)
tunnel = new Guacamole.ChainedTunnel(
new Guacamole.WebSocketTunnel('websocket-tunnel'),
new Guacamole.HTTPTunnel('tunnel')
);
// If no WebSocket, then use HTTP.
else
tunnel = new Guacamole.HTTPTunnel('tunnel');
// Fire events for tunnel errors
tunnel.onerror = function onTunnelError(status) {
$scope.safeApply(function() {
$scope.$emit('guacTunnelError', tunnel, status.code);
});
};
// Fire events for tunnel state changes
tunnel.onstatechange = function onTunnelStateChange(state) {
$scope.safeApply(function() {
switch (state) {
case Guacamole.Tunnel.State.CONNECTING:
$scope.$emit('guacTunnelStateChange', tunnel, 'connecting');
break;
case Guacamole.Tunnel.State.OPEN:
$scope.$emit('guacTunnelStateChange', tunnel, 'open');
break;
case Guacamole.Tunnel.State.CLOSED:
$scope.$emit('guacTunnelStateChange', tunnel, 'closed');
break;
}
});
};
return tunnel;
};
return service;
}]);

View File

@@ -28,10 +28,7 @@
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<!-- Client -->
<guac-client
client-properties="clientProperties"
id="id"
connection-parameters="connectionParameters"/></guac-client>
<guac-client client="client"/></guac-client>
</div>
@@ -60,7 +57,7 @@
<h2>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h2>
<div class="content" id="clipboard-settings">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<textarea ng-model="clipboardData" rows="10" cols="40" id="clipboard"></textarea>
<textarea ng-model="client.clipboardData" rows="10" cols="40" id="clipboard"></textarea>
</div>
<h2>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h2>
@@ -93,7 +90,7 @@
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
@@ -102,7 +99,7 @@
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>

View File

@@ -0,0 +1,399 @@
/*
* 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.
*/
/**
* Provides the ManagedClient class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
function defineManagedClient($rootScope, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ManagedClientState = $injector.get('ManagedClientState');
// Required services
var $window = $injector.get('$window');
var $document = $injector.get('$document');
var authenticationService = $injector.get('authenticationService');
var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory');
var guacVideo = $injector.get('guacVideo');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* client while it is active, allowing it to be detached and reattached
* from different client views.
*
* @constructor
* @param {ManagedClient|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClient.
*/
var ManagedClient = function ManagedClient(template) {
// Use empty object by default
template = template || {};
/**
* The ID of the connection associated with this client.
*
* @type String
*/
this.id = template.id;
/**
* The actual underlying Guacamole client.
*
* @type Guacamole.Client
*/
this.client = template.client;
/**
* The tunnel being used by the underlying Guacamole client.
*
* @type Guacamole.Tunnel
*/
this.tunnel = template.tunnel;
/**
* The name returned via the Guacamole protocol for this connection, if
* any.
*
* @type String
*/
this.name = template.name;
/**
* The current clipboard contents.
*
* @type String
*/
this.clipboardData = template.clipboardData;
/**
* The current state of the Guacamole client (idle, connecting,
* connected, terminated with error, etc.).
*
* @type ManagedClientState
*/
this.clientState = template.clientState || new ManagedClientState();
/**
* Properties associated with the display and behavior of the Guacamole
* client.
*
* @type ClientProperties
*/
this.clientProperties = template.clientProperties || new ClientProperties();
};
/**
* Returns the string of connection parameters to be passed to the
* Guacamole client during connection. This string generally contains the
* desired connection ID, display resolution, and supported audio/video
* codecs.
*
* @param {String} id
* The ID of the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {String}
* The string of connection parameters to be passed to the Guacamole
* client.
*/
var getConnectString = function getConnectString(id, connectionParameters) {
// 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;
// Build base connect string
var connectString =
"id=" + encodeURIComponent(id)
+ "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&width=" + Math.floor(optimal_width)
+ "&height=" + Math.floor(optimal_height)
+ "&dpi=" + Math.floor(optimal_dpi)
+ (connectionParameters ? '&' + connectionParameters : '');
// 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);
});
return connectString;
};
/**
* Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this
* function has no effect.
*
* @param {String} managedClient
* The client whose history entry should be updated.
*/
var updateHistoryEntry = function updateHistoryEntry(managedClient) {
var display = managedClient.client.getDisplay();
// Update stored thumbnail of previous connection
if (display && display.getWidth() > 0 && display.getHeight() > 0) {
// Get screenshot
var canvas = display.flatten();
// Calculate scale of thumbnail (max 320x240, max zoom 100%)
var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1);
// Create thumbnail canvas
var thumbnail = $document[0].createElement("canvas");
thumbnail.width = canvas.width*scale;
thumbnail.height = canvas.height*scale;
// Scale screenshot to thumbnail
var context = thumbnail.getContext("2d");
context.drawImage(canvas,
0, 0, canvas.width, canvas.height,
0, 0, thumbnail.width, thumbnail.height
);
guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png"));
}
};
/**
* Creates a new ManagedClient, connecting it to the specified connection
* or group.
*
* @param {String} id
* The ID of the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {ManagedClient}
* A new ManagedClient instance which is connected to the connection or
* connection group having the given ID.
*/
ManagedClient.getInstance = function getInstance(id, connectionParameters) {
var tunnel;
// If WebSocket available, try to use it.
if ($window.WebSocket)
tunnel = new Guacamole.ChainedTunnel(
new Guacamole.WebSocketTunnel('websocket-tunnel'),
new Guacamole.HTTPTunnel('tunnel')
);
// If no WebSocket, then use HTTP.
else
tunnel = new Guacamole.HTTPTunnel('tunnel');
// Get new client instance
var client = new Guacamole.Client(tunnel);
// Associate new managed client with new client and tunnel
var managedClient = new ManagedClient({
id : id,
client : client,
tunnel : tunnel
});
// Fire events for tunnel errors
tunnel.onerror = function tunnelError(status) {
$rootScope.$apply(function handleTunnelError() {
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.TUNNEL_ERROR,
status.code);
});
};
// Update connection state as tunnel state changes
tunnel.onstatechange = function tunnelStateChanged(state) {
$rootScope.$apply(function updateTunnelState() {
switch (state) {
// Connection is being established
case Guacamole.Tunnel.State.CONNECTING:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connection has closed
case Guacamole.Tunnel.State.CLOSED:
updateHistoryEntry(managedClient);
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.DISCONNECTED);
break;
}
});
};
// Update connection state as client state changes
client.onstatechange = function clientStateChanged(clientState) {
$rootScope.$apply(function updateClientState() {
switch (clientState) {
// Idle
case 0:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.IDLE);
break;
// Connected + waiting
case 2:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.WAITING);
break;
// Connected
case 3:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.DISCONNECTED);
break;
// Connecting, disconnecting, and disconnected are all
// either ignored or handled by tunnel state
case 1: // Connecting
case 4: // Disconnecting
case 5: // Disconnected
break;
}
});
};
// Update stored name if name changes
client.onname = function clientNameChanged(name) {
$rootScope.$apply(function updateName() {
managedClient.name = name;
});
};
// Disconnect and update status when the client receives an error
client.onerror = function clientError(status) {
$rootScope.$apply(function handleClientError() {
// Disconnect, if connected
client.disconnect();
// Update state
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CLIENT_ERROR,
status.code);
});
};
// Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(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);
};
// Update state when done
reader.onend = function clipboard_text_end() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = data;
});
};
};
/* TODO: Restore file transfer again */
/*
// Handle any received files
client.onfile = function onClientFile(stream, mimetype, filename) {
// Begin file download
var guacFileStartEvent = $rootScope.$emit('guacClientFileDownloadStart', client, 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() {
$rootScope.$emit('guacClientFileDownloadProgress', client, stream.index, mimetype, filename, blob_reader.getLength());
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// When complete, prompt for download
blob_reader.onend = function onend() {
$rootScope.$emit('guacClientFileDownloadEnd', client, stream.index, mimetype, filename, blob_reader.getBlob());
};
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);
};
*/
// Connect the Guacamole client
client.connect(getConnectString(id, connectionParameters));
return managedClient;
};
return ManagedClient;
}]);

View File

@@ -0,0 +1,159 @@
/*
* 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.
*/
/**
* Provides the ManagedClient class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClientState', [function defineManagedClientState() {
/**
* Object which represents the state of a Guacamole client and its tunnel,
* including any error conditions.
*
* @constructor
* @param {ManagedClientState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientState.
*/
var ManagedClientState = function ManagedClientState(template) {
// Use empty object by default
template = template || {};
/**
* The current connection state. Valid values are described by
* ManagedClientState.ConnectionState.
*
* @type String
* @default ManagedClientState.ConnectionState.IDLE
*/
this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE;
/**
* The status code of the current error condition, if connectionState
* is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState
* values, this will be @link{Guacamole.Status.Code.SUCCESS}.
*
* @type Number
* @default Guacamole.Status.Code.SUCCESS
*/
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
};
/**
* Valid connection state strings. Each state string is associated with a
* specific state of a Guacamole connection.
*/
ManagedClientState.ConnectionState = {
/**
* The Guacamole connection has not yet been attempted.
*
* @type String
*/
IDLE : "IDLE",
/**
* The Guacamole connection is being established.
*
* @type String
*/
CONNECTING : "CONNECTING",
/**
* The Guacamole connection has been successfully established, and the
* client is now waiting for receipt of initial graphical data.
*
* @type String
*/
WAITING : "WAITING",
/**
* The Guacamole connection has been successfully established, and
* initial graphical data has been received.
*
* @type String
*/
CONNECTED : "CONNECTED",
/**
* The Guacamole connection has terminated successfully. No errors are
* indicated.
*
* @type String
*/
DISCONNECTED : "DISCONNECTED",
/**
* The Guacamole connection has terminated due to an error reported by
* the client. The associated error code is stored in statusCode.
*
* @type String
*/
CLIENT_ERROR : "CLIENT_ERROR",
/**
* The Guacamole connection has terminated due to an error reported by
* the tunnel. The associated error code is stored in statusCode.
*
* @type String
*/
TUNNEL_ERROR : "TUNNEL_ERROR"
};
/**
* Sets the current client state and, if given, the associated status code.
* If an error is already represented, this function has no effect.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
*
* @param {String} connectionState
* The connection state to assign to the given ManagedClientState, as
* listed within ManagedClientState.ConnectionState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedClientState, if any,
* as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedClientState is not touched.
*/
ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) {
// Do not set state after an error is registered
if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
return;
// Update connection state
clientState.connectionState = connectionState;
// Set status code, if given
if (statusCode)
clientState.statusCode = statusCode;
};
return ManagedClientState;
}]);

View File

@@ -26,32 +26,17 @@
angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) {
// Get class dependencies
// Required types
var PermissionSet = $injector.get("PermissionSet");
// Get services
var permissionService = $injector.get("permissionService"),
authenticationService = $injector.get("authenticationService"),
$q = $injector.get("$q"),
$document = $injector.get("$document"),
$window = $injector.get("$window"),
$location = $injector.get("$location");
// Required services
var $document = $injector.get("$document");
var $location = $injector.get("$location");
var $q = $injector.get("$q");
var $window = $injector.get("$window");
var authenticationService = $injector.get("authenticationService");
var permissionService = $injector.get("permissionService");
/*
* 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);
}
};
/**
* The current status notification, or false if no status is currently
* shown.