Add .gitignore and .ratignore files for various directories
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
gyurix
2025-04-29 21:43:12 +02:00
parent 983ecbfc53
commit be9f66dee9
2167 changed files with 254128 additions and 0 deletions

6
guacamole/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
src/main/webapp/META-INF/
src/main/webapp/generated/
nb-configuration.xml
customs.json
target/
*~

1
guacamole/.ratignore Normal file
View File

@@ -0,0 +1 @@
src/main/frontend/dist/**/*

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<user-mapping>
<!-- Per-user authentication and config information -->
<authorize username="USERNAME" password="PASSWORD">
<protocol>vnc</protocol>
<param name="hostname">localhost</param>
<param name="port">5900</param>
<param name="password">VNCPASS</param>
</authorize>
<!-- Another user, but using md5 to hash the password
(example below uses the md5 hash of "PASSWORD") -->
<authorize
username="USERNAME2"
password="319f4d26e3c536b5dd871bb2c52e3178"
encoding="md5">
<!-- First authorized connection -->
<connection name="localhost">
<protocol>vnc</protocol>
<param name="hostname">localhost</param>
<param name="port">5901</param>
<param name="password">VNCPASS</param>
</connection>
<!-- Second authorized connection -->
<connection name="otherhost">
<protocol>vnc</protocol>
<param name="hostname">otherhost</param>
<param name="port">5900</param>
<param name="password">VNCPASS</param>
</connection>
</authorize>
<!-- Another user, but using SHA-256 to hash the password -->
<authorize
username="USERNAME3"
password="5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
encoding="sha256">
<connection name="localhost">
<protocol>vnc</protocol>
<param name="hostname">localhost</param>
<param name="port">5900</param>
<param name="password">VNCPASS</param>
</connection>
</authorize>
</user-mapping>

379
guacamole/pom.xml Normal file
View File

@@ -0,0 +1,379 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole</artifactId>
<packaging>war</packaging>
<version>1.6.0</version>
<name>guacamole</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-client</artifactId>
<version>1.6.0</version>
<relativePath>../</relativePath>
</parent>
<description>
The Guacamole web application, providing authentication and an HTML5
remote desktop client.
</description>
<!-- All applicable licenses -->
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<!-- Git repository -->
<scm>
<url>https://github.com/apache/guacamole-client</url>
<connection>scm:git:https://git.wip-us.apache.org/repos/asf/guacamole-client.git</connection>
</scm>
<build>
<plugins>
<!-- Build AngularJS portion of application using NPM -->
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.11.3</version>
<configuration>
<workingDirectory>src/main/frontend</workingDirectory>
<installDirectory>${project.build.directory}</installDirectory>
<!-- Newer Node.js requires the following to avoid an "ERR_OSSL_EVP_UNSUPPORTED"
error when WebPack attempts to use its default hash (MD4) for content hashing -->
<environmentVariables>
<NODE_OPTIONS>--openssl-legacy-provider</NODE_OPTIONS>
</environmentVariables>
</configuration>
<executions>
<execution>
<id>install-node-and-npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v18.18.0</nodeVersion>
<npmVersion>9.8.1</npmVersion>
</configuration>
</execution>
<execution>
<id>npm-ci</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>ci</arguments>
</configuration>
</execution>
<execution>
<id>npm-build</id>
<phase>generate-resources</phase>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
<!-- Copy automatically-generated set of NPM module dependencies for
later use by LICENSE generator -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-npm-dependency-list</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${dependency.list.directory}</outputDirectory>
<resources>
<resource>
<directory>src/main/frontend/dist</directory>
<includes>
<include>npm-dependencies.txt</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Automatically generate LICENSE and NOTICE -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-license-files</id>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<webResources>
<!-- Add frontend folder to war package ... -->
<resource>
<directory>src/main/frontend/dist</directory>
<excludes>
<exclude>translations/*.json</exclude>
<exclude>index.html</exclude>
<exclude>verifyCachedVersion.js</exclude>
</excludes>
</resource>
<!-- ... but filter index.html and translation strings -->
<resource>
<directory>src/main/frontend/dist</directory>
<filtering>true</filtering>
<includes>
<include>translations/*.json</include>
<include>index.html</include>
<include>verifyCachedVersion.js</include>
</includes>
</resource>
<!-- Include all licenses within META-INF -->
<resource>
<directory>${project.build.directory}/licenses</directory>
<targetPath>META-INF</targetPath>
</resource>
</webResources>
<!-- Add files from guacamole-common-js -->
<overlays>
<overlay>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-common-js</artifactId>
<type>zip</type>
</overlay>
</overlays>
</configuration>
<executions>
<execution>
<id>default-cli</id>
<phase>process-resources</phase>
<goals>
<goal>exploded</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- JSR 356 WebSocket API -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<exclusions>
<!-- Exclude optional dependency on JavaMail -->
<exclusion>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.6.0</version>
</dependency>
<!-- Guacamole JavaScript API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-common-js</artifactId>
<version>1.6.0</version>
<type>zip</type>
<scope>runtime</scope>
</dependency>
<!-- Jetty 8 servlet API (websocket) -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-websocket</artifactId>
<version>8.1.1.v20120215</version>
<scope>provided</scope>
</dependency>
<!-- Jetty 9.0 servlet API (websocket) -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-parent</artifactId>
<version>20</version>
<scope>provided</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<version>9.0.7.v20131107</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-servlet</artifactId>
<version>9.0.7.v20131107</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat servlet API (websocket) -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>7.0.37</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-coyote</artifactId>
<version>7.0.37</version>
<scope>provided</scope>
</dependency>
<!-- Guice - Dependency Injection -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-servlet</artifactId>
</dependency>
<!-- Jersey - JAX-RS Implementation -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>guice-bridge</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<exclusions>
<!-- Resolve version conflict (see below - transitive
dependencies of jersey-media-json-jackson disagree on
1.2.1 vs. 1.2.2) -->
<exclusion>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- JSR-250 annotations -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
<!-- Guava Base Libraries -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- Force use of version 1.2.2 (transitive dependencies of
jersey-media-json-jackson disagree on 1.2.1 vs. 1.2.2) -->
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Jed Richards
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.

View File

@@ -0,0 +1,8 @@
angular-module-shim (https://github.com/jedrichards/angular-module-shim)
------------------------------------------------------------------------
Version: 0.0.4
From: 'Jed Richards' (https://github.com/jedrichards)
License(s):
MIT (bundled/angular-module-shim-0.0.4/LICENSE)

View File

@@ -0,0 +1,95 @@
Copyright (c) 2010-2013 by tyPoland Lukasz Dziedzic with Reserved Font Name "Carlito".
This Font Software is licensed under the SIL Open Font License,
Version 1.1 as shown below.
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
worldwide development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to provide
a free and open framework in which fonts may be shared and improved in
partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves.
The fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such.
This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components
as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting ? in part or in whole ?
any of the components of the Original Version, by changing formats or
by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer
or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this license, and must not be distributed
under any other license. The requirement for fonts to remain under
this license does not apply to any document created using the Font
Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,8 @@
Carlito (http://code.google.com/p/chromium/issues/detail?id=280557)
-------------------------------------------------------------------
Version: N/A
From: 'tyPoland Lukasz Dziedzic' (http://www.lukaszdziedzic.eu/)
License(s):
SIL Open Font (bundled/carlito/LICENSE)

View File

@@ -0,0 +1,19 @@
Copyright (C) 2019 Glyptodon, Inc.
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.

View File

@@ -0,0 +1,9 @@
Session Recording Player for Glyptodon Enterprise
(https://github.com/glyptodon/glyptodon-enterprise-player)
----------------------------------------------------------
Version: 1.1.0-1
From: 'Glyptodon, Inc.' (https://glyptodon.com/)
License(s):
MIT (bundled/glyptodon-enterprise-player/LICENSE)

View File

@@ -0,0 +1,3 @@
*~
node_modules
dist

10612
guacamole/src/main/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
{
"private": true,
"scripts": {
"build": "webpack --progress"
},
"dependencies": {
"@simonwep/pickr": "^1.8.2",
"angular": "^1.8.3",
"angular-route": "^1.8.3",
"angular-templatecache-webpack-plugin": "^1.0.1",
"angular-translate": "^2.19.1",
"angular-translate-interpolation-messageformat": "^2.19.1",
"angular-translate-loader-static-files": "^2.19.1",
"blob-polyfill": "^9.0.20240710",
"csv": "^6.3.9",
"d3-path": "^3.1.0",
"d3-shape": "^3.2.0",
"datalist-polyfill": "^1.25.1",
"file-saver": "^2.0.5",
"fuzzysort": "^3.0.2",
"jquery": "^3.7.1",
"jstz": "^2.1.1",
"lodash": "^4.17.21",
"yaml": "^2.5.0"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"babel-loader": "^8.3.0",
"clean-webpack-plugin": "^4.0.0",
"closure-webpack-plugin": "^2.6.1",
"copy-webpack-plugin": "^5.1.2",
"css-loader": "^5.2.7",
"css-minimizer-webpack-plugin": "^1.3.0",
"exports-loader": "^1.1.1",
"find-package-json": "^1.2.0",
"google-closure-compiler": "20240317.0.0",
"html-webpack-plugin": "^4.5.2",
"mini-css-extract-plugin": "^1.6.2",
"webpack": "^4.47.0",
"webpack-cli": "^4.10.0"
}
}

View File

@@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const finder = require('find-package-json');
const fs = require('fs');
const path = require('path');
const validateOptions = require('schema-utils');
/**
* The name of this plugin.
*
* @type {string}
*/
const PLUGIN_NAME = 'dependency-list-plugin';
/**
* The schema of the configuration options object accepted by the constructor
* of DependencyListPlugin.
*
* @see https://github.com/webpack/schema-utils/blob/v1.0.0/README.md#usage
*/
const PLUGIN_OPTIONS_SCHEMA = {
type: 'object',
properties: {
/**
* The name of the file that should contain the dependency list. By
* default, this will be "npm-dependencies.txt".
*/
filename: { type: 'string' },
/**
* The path in which the dependency list file should be saved. By
* default, this will be the output path of the Webpack compiler.
*/
path: { type: 'string' }
},
additionalProperties: false
};
/**
* Webpack plugin that automatically lists each of the NPM dependencies
* included within any bundles produced by the compile process.
*/
class DependencyListPlugin {
/**
* Creates a new DependencyListPlugin configured with the given options.
* The options given must conform to the options schema.
*
* @see PLUGIN_OPTIONS_SCHEMA
*
* @param {*} options
* The configuration options to apply to the plugin.
*/
constructor(options = {}) {
validateOptions(PLUGIN_OPTIONS_SCHEMA, options, 'DependencyListPlugin');
this.options = options;
}
/**
* Entrypoint for all Webpack plugins. This function will be invoked when
* the plugin is being associated with the compile process.
*
* @param {Compiler} compiler
* A reference to the Webpack compiler.
*/
apply(compiler) {
/**
* Logger for this plugin.
*
* @type {Logger}
*/
const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
/**
* The directory receiving the dependency list file.
*
* @type {string}
*/
const outputPath = this.options.path || compiler.options.output.path;
/**
* The full path to the output file that should contain the list of
* discovered NPM module dependencies.
*
* @type {string}
*/
const outputFile = path.join(
outputPath,
this.options.filename || 'npm-dependencies.txt'
);
// Wait for compilation to fully complete
compiler.hooks.done.tap(PLUGIN_NAME, (stats) => {
const moduleCoords = {};
// Map each file used within any bundle built by the compiler to
// its corresponding NPM package, ignoring files that have no such
// package
stats.compilation.fileDependencies.forEach(file => {
// Locate NPM package corresponding to file dependency (there
// may not be one)
const moduleFinder = finder(file);
const npmPackage = moduleFinder.next().value;
// Translate absolute path into more readable path relative to
// root of compilation process
const relativePath = path.relative(compiler.options.context, file);
if (npmPackage.name) {
moduleCoords[npmPackage.name + ':' + npmPackage.version] = true;
logger.info('File dependency "%s" mapped to NPM package "%s" (v%s)',
relativePath, npmPackage.name, npmPackage.version);
}
else
logger.info('Skipping file dependency "%s" (no NPM package)',
relativePath);
});
// Create output path if it doesn't yet exist
if (!fs.existsSync(outputPath))
fs.mkdirSync(outputPath, { recursive: true, mode: 0o755 });
// Write all discovered NPM packages to configured output file
const sortedCoords = Object.keys(moduleCoords).sort();
fs.writeFileSync(outputFile, sortedCoords.join('\n') + '\n');
});
}
}
module.exports = DependencyListPlugin;

View File

@@ -0,0 +1,56 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Jed Richards
*
* 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.
*/
(function(angular) {
'use strict';
if ( !angular ) {
throw new Error('angular-module-shim: Missing Angular');
}
var origFn = angular.module;
var hash = {};
angular.module = function(name,requires,configFn) {
var requires = requires || [];
var registered = hash[name];
var module;
if ( registered ) {
module = origFn(name);
module.requires.push.apply(module.requires,requires);
// Register the config function if it exists.
if (configFn) {
module.config(configFn);
}
} else {
hash[name] = true;
module = origFn(name,requires,configFn);
}
return module;
};
})(window.angular);

View File

@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for authentication and management of tokens.
*/
angular.module('auth', [
'rest',
'storage'
]);

View File

@@ -0,0 +1,550 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for authenticating a user against the REST API. Invoking the
* authenticate() or login() functions of this service will automatically
* affect the login dialog, if visible.
*
* This service broadcasts events on $rootScope depending on the status and
* result of authentication operations:
*
* "guacLoginPending"
* An authentication request is being submitted and we are awaiting the
* result. The request may not yet have been submitted if the parameters
* for that request are not ready. This event receives a promise that
* resolves with the HTTP parameters that were ultimately submitted as its
* sole parameter.
*
* "guacLogin"
* Authentication was successful and a new token was created. This event
* receives the authentication token as its sole parameter.
*
* "guacLogout"
* An existing token is being destroyed. This event receives the
* authentication token as its sole parameter. If the existing token for
* the current session is being replaced without destroying that session,
* this event is not fired.
*
* "guacLoginFailed"
* An authentication request has failed for any reason. This event is
* broadcast before any other events that are specific to the nature of
* the failure, and may be used to detect login failures in lieu of those
* events. This event receives two parameters: the HTTP parameters
* submitted and the Error object received from the REST endpoint.
*
* "guacInsufficientCredentials"
* An authentication request failed because additional credentials are
* needed before the request can be processed. This event receives two
* parameters: the HTTP parameters submitted and the Error object received
* from the REST endpoint.
*
* "guacInvalidCredentials"
* An authentication request failed because the credentials provided are
* invalid. This event receives two parameters: the HTTP parameters
* submitted and the Error object received from the REST endpoint.
*/
angular.module('auth').factory('authenticationService', ['$injector',
function authenticationService($injector) {
// Required types
var AuthenticationResult = $injector.get('AuthenticationResult');
var Error = $injector.get('Error');
// Required services
var $q = $injector.get('$q');
var $rootScope = $injector.get('$rootScope');
var localStorageService = $injector.get('localStorageService');
var requestService = $injector.get('requestService');
var service = {};
/**
* The most recent authentication result, or null if no authentication
* result is cached.
*
* @type AuthenticationResult
*/
var cachedResult = null;
/**
* The unique identifier of the local storage key which stores the latest
* authentication token.
*
* @type String
*/
var AUTH_TOKEN_STORAGE_KEY = 'GUAC_AUTH_TOKEN';
/**
* Retrieves the authentication result cached in memory. If the user has not
* yet authenticated, the user has logged out, or the last authentication
* attempt failed, null is returned.
*
* NOTE: setAuthenticationResult() will be called upon page load, so the
* cache should always be populated after the page has successfully loaded.
*
* @returns {AuthenticationResult}
* The last successful authentication result, or null if the user is not
* currently authenticated.
*/
var getAuthenticationResult = function getAuthenticationResult() {
// Use cached result, if any
if (cachedResult)
return cachedResult;
// Return explicit null if no auth data is currently stored
return null;
};
/**
* Stores the given authentication result for future retrieval. The given
* result MUST be the result of the most recent authentication attempt.
*
* @param {AuthenticationResult} data
* The last successful authentication result, or null if the last
* authentication attempt failed.
*/
var setAuthenticationResult = function setAuthenticationResult(data) {
// Clear the currently-stored result and auth token if the last
// attempt failed
if (!data) {
cachedResult = null;
localStorageService.removeItem(AUTH_TOKEN_STORAGE_KEY);
}
// Otherwise, store the authentication attempt directly.
// Note that only the auth token is stored in persistent local storage.
// To re-obtain an autentication result upon a fresh page load,
// reauthenticate with the persistent token, which can be obtained by
// calling getCurrentToken().
else {
// Always store in cache
cachedResult = data;
// Persist only the auth token past tab/window closure, and only
// if not anonymous
if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME)
localStorageService.setItem(
AUTH_TOKEN_STORAGE_KEY, data.authToken);
}
};
/**
* Clears the stored authentication result, if any. If no authentication
* result is currently stored, this function has no effect.
*/
var clearAuthenticationResult = function clearAuthenticationResult() {
setAuthenticationResult(null);
};
/**
* Makes a request to authenticate a user using the token REST API endpoint
* and given arbitrary parameters, returning a promise that succeeds only
* if the authentication operation was successful. The resulting
* authentication data can be retrieved later via getCurrentToken() or
* getCurrentUsername(). Invoking this function will affect the UI,
* including the login screen if visible.
*
* The provided parameters can be virtually any object, as each property
* will be sent as an HTTP parameter in the authentication request.
* Standard parameters include "username" for the user's username,
* "password" for the user's associated password, and "token" for the
* auth token to check/update.
*
* If a token is provided, it will be reused if possible.
*
* @param {Object|Promise} parameters
* Arbitrary parameters to authenticate with. If a Promise is provided,
* that Promise must resolve with the parameters to be submitted when
* those parameters are available, and any error will be handled as if
* from the authentication endpoint of the REST API itself.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.authenticate = function authenticate(parameters) {
// Coerce received parameters object into a Promise, if it isn't
// already a Promise
parameters = $q.resolve(parameters);
// Notify that a fresh authentication request is underway
$rootScope.$broadcast('guacLoginPending', parameters);
// Attempt authentication after auth parameters are available ...
return parameters.then(function requestParametersReady(requestParams) {
// Strip any properties that are from AngularJS core, such as the
// '$$state' property added by $q. Properties added by AngularJS
// core will have a '$' prefix. The '$$state' property is
// particularly problematic, as it is self-referential and explodes
// the stack when fed to $.param().
requestParams = _.omitBy(requestParams, (value, key) => key.startsWith('$'));
return requestService({
method: 'POST',
url: 'api/tokens',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: $.param(requestParams)
})
// ... if authentication succeeds, handle received auth data ...
.then(function authenticationSuccessful(data) {
var currentToken = service.getCurrentToken();
// If a new token was received, ensure the old token is invalidated,
// if any, and notify listeners of the new token
if (data.authToken !== currentToken) {
// If an old token existed, request that the token be revoked
if (currentToken) {
service.revokeToken(currentToken).catch(angular.noop);
}
// Notify of login and new token
setAuthenticationResult(new AuthenticationResult(data));
$rootScope.$broadcast('guacLogin', data.authToken);
}
// Update cached authentication result, even if the token remains
// the same
else
setAuthenticationResult(new AuthenticationResult(data));
// Authentication was successful
return data;
});
})
// ... if authentication fails, propogate failure to returned promise
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
// Notify of generic login failure, for any event consumers that
// wish to handle all types of failures at once
$rootScope.$broadcast('guacLoginFailed', parameters, error);
// Request credentials if provided credentials were invalid
if (error.type === Error.Type.INVALID_CREDENTIALS) {
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
clearAuthenticationResult();
}
// Request more credentials if provided credentials were not enough
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) {
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
clearAuthenticationResult();
}
// Abort rendering of page if an internal error occurs
else if (error.type === Error.Type.INTERNAL_ERROR)
$rootScope.$broadcast('guacFatalPageError', error);
// Authentication failed
throw error;
}));
};
/**
* Makes a request to update the current auth token, if any, using the
* token REST API endpoint. If the optional parameters object is provided,
* its properties will be included as parameters in the update request.
* This function returns a promise that succeeds only if the authentication
* operation was successful. The resulting authentication data can be
* retrieved later via getCurrentToken() or getCurrentUsername().
*
* If there is no current auth token, this function behaves identically to
* authenticate(), and makes a general authentication request.
*
* @param {Object} [parameters]
* Arbitrary parameters to authenticate with, if any.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.updateCurrentToken = function updateCurrentToken(parameters) {
// HTTP parameters for the authentication request
var httpParameters = {};
// Add token parameter if current token is known
var token = service.getCurrentToken();
if (token)
httpParameters.token = service.getCurrentToken();
// Add any additional parameters
if (parameters)
angular.extend(httpParameters, parameters);
// Make the request
return service.authenticate(httpParameters);
};
/**
* Determines whether the session associated with a particular token is
* still valid, without performing an operation that would result in that
* session being marked as active. If no token is provided, the session of
* the current user is checked.
*
* @param {string} [token]
* The authentication token to pass with the "Guacamole-Token" header.
* If omitted, and the user is logged in, the user's current
* authentication token will be used.
*
* @returns {Promise.<!boolean>}
* A promise that resolves with the boolean value "true" if the session
* is valid, and resolves with the boolean value "false" otherwise,
* including if an error prevents session validity from being
* determined. The promise is never rejected.
*/
service.getValidity = function getValidity(token) {
// NOTE: Because this is a HEAD request, we will not receive a JSON
// response body. We will only have a simple yes/no regarding whether
// the auth token can be expected to be usable.
return service.request({
method: 'HEAD',
url: 'api/session'
}, token)
.then(function sessionIsValid() {
return true;
})
['catch'](function sessionIsNotValid() {
return false;
});
};
/**
* Makes a request to revoke an authentication token using the token REST
* API endpoint, returning a promise that succeeds only if the token was
* successfully revoked.
*
* @param {string} token
* The authentication token to revoke.
*
* @returns {Promise}
* A promise which succeeds only if the token was successfully revoked.
*/
service.revokeToken = function revokeToken(token) {
return service.request({
method: 'DELETE',
url: 'api/session'
}, token);
};
/**
* Makes a request to authenticate a user using the token REST API endpoint
* with a username and password, ignoring any currently-stored token,
* returning a promise that succeeds only if the login operation was
* successful. The resulting authentication data can be retrieved later
* via getCurrentToken() or getCurrentUsername(). Invoking this function
* will affect the UI, including the login screen if visible.
*
* @param {String} username
* The username to log in with.
*
* @param {String} password
* The password to log in with.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.login = function login(username, password) {
return service.authenticate({
username: username,
password: password
});
};
/**
* Makes a request to logout a user using the token REST API endpoint,
* returning a promise that succeeds only if the logout operation was
* successful. Invoking this function will affect the UI, causing the
* visible components of the application to be replaced with a status
* message noting that the user has been logged out.
*
* @returns {Promise}
* A promise which succeeds only if the logout operation was
* successful.
*/
service.logout = function logout() {
// Clear authentication data
var token = service.getCurrentToken();
clearAuthenticationResult();
// Notify listeners that a token is being destroyed
$rootScope.$broadcast('guacLogout', token);
// Delete old token
return service.revokeToken(token);
};
/**
* Returns whether the current user has authenticated anonymously. An
* anonymous user is denoted by the identifier reserved by the Guacamole
* extension API for anonymous users (the empty string).
*
* @returns {Boolean}
* true if the current user has authenticated anonymously, false
* otherwise.
*/
service.isAnonymous = function isAnonymous() {
return service.getCurrentUsername() === '';
};
/**
* Returns the username of the current user. If the current user is not
* logged in, this value may not be valid.
*
* @returns {String}
* The username of the current user, or null if no authentication data
* is present.
*/
service.getCurrentUsername = function getCurrentUsername() {
// Return username, if available
var authData = getAuthenticationResult();
if (authData)
return authData.username;
// No auth data present
return null;
};
/**
* Returns the auth token associated with the current user. If the current
* user is not logged in, this token may not be valid.
*
* @returns {String}
* The auth token associated with the current user, or null if no
* authentication data is present.
*/
service.getCurrentToken = function getCurrentToken() {
// Return cached auth token, if available
var authData = getAuthenticationResult();
if (authData)
return authData.authToken;
// Fall back to the value from local storage if not found in cache
return localStorageService.getItem(AUTH_TOKEN_STORAGE_KEY);
};
/**
* Returns the identifier of the data source that authenticated the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String}
* The identifier of the data source that authenticated the current
* user, or null if no authentication data is present.
*/
service.getDataSource = function getDataSource() {
// Return data source, if available
var authData = getAuthenticationResult();
if (authData)
return authData.dataSource;
// No auth data present
return null;
};
/**
* Returns the identifiers of all data sources available to the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String[]}
* The identifiers of all data sources availble to the current user,
* or an empty array if no authentication data is present.
*/
service.getAvailableDataSources = function getAvailableDataSources() {
// Return data sources, if available
var authData = getAuthenticationResult();
if (authData)
return authData.availableDataSources;
// No auth data present
return [];
};
/**
* Makes an HTTP request leveraging the requestService(), automatically
* including the given authentication token using the "Guacamole-Token"
* header. If no token is provided, the user's current authentication token
* is used instead. If the user is not logged in, the "Guacamole-Token"
* header is simply omitted. The provided configuration object is not
* modified by this function.
*
* @param {Object} object
* A configuration object describing the HTTP request to be made by
* requestService(). As described by requestService(), this object must
* be a configuration object accepted by AngularJS' $http service.
*
* @param {string} [token]
* The authentication token to pass with the "Guacamole-Token" header.
* If omitted, and the user is logged in, the user's current
* authentication token will be used.
*
* @returns {Promise.<Object>}
* A promise that will resolve with the data from the HTTP response for
* the underlying requestService() call if successful, or reject with
* an @link{Error} describing the failure.
*/
service.request = function request(object, token) {
// Attempt to use current token if none is provided
token = token || service.getCurrentToken();
// Add "Guacamole-Token" header if an authentication token is available
if (token) {
object = _.merge({
headers : { 'Guacamole-Token' : token }
}, object);
}
return requestService(object);
};
return service;
}]);

View File

@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Service which defines the AuthenticationResult class.
*/
angular.module('auth').factory('AuthenticationResult', [function defineAuthenticationResult() {
/**
* The object returned by REST API calls when representing the successful
* result of an authentication attempt.
*
* @constructor
* @param {AuthenticationResult|Object} [template={}]
* The object whose properties should be copied within the new
* AuthenticationResult.
*/
var AuthenticationResult = function AuthenticationResult(template) {
// Use empty object by default
template = template || {};
/**
* The unique token generated for the user that authenticated.
*
* @type String
*/
this.authToken = template.authToken;
/**
* The name which uniquely identifies the user that authenticated.
*
* @type String
*/
this.username = template.username;
/**
* The unique identifier of the data source which authenticated the
* user.
*
* @type String
*/
this.dataSource = template.dataSource;
/**
* The identifiers of all data sources available to the user that
* authenticated.
*
* @type String[]
*/
this.availableDataSources = template.availableDataSources;
};
/**
* The username reserved by the Guacamole extension API for users which have
* authenticated anonymously.
*
* @type String
*/
AuthenticationResult.ANONYMOUS_USERNAME = '';
return AuthenticationResult;
}]);

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for code used to connect to a connection or balancing group.
*/
angular.module('client', [
'auth',
'clipboard',
'element',
'history',
'navigation',
'notification',
'osk',
'rest',
'textInput',
'touch'
]);

View File

@@ -0,0 +1,879 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The controller for the page used to connect to a connection or balancing group.
*/
angular.module('client').controller('clientController', ['$scope', '$routeParams', '$injector',
function clientController($scope, $routeParams, $injector) {
// Required types
const ConnectionGroup = $injector.get('ConnectionGroup');
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedClientState = $injector.get('ManagedClientState');
const ManagedFilesystem = $injector.get('ManagedFilesystem');
const Protocol = $injector.get('Protocol');
const ScrollState = $injector.get('ScrollState');
// Required services
const $location = $injector.get('$location');
const authenticationService = $injector.get('authenticationService');
const connectionGroupService = $injector.get('connectionGroupService');
const clipboardService = $injector.get('clipboardService');
const dataSourceService = $injector.get('dataSourceService');
const guacClientManager = $injector.get('guacClientManager');
const guacFullscreen = $injector.get('guacFullscreen');
const iconService = $injector.get('iconService');
const preferenceService = $injector.get('preferenceService');
const requestService = $injector.get('requestService');
const tunnelService = $injector.get('tunnelService');
const userPageService = $injector.get('userPageService');
/**
* The minimum number of pixels a drag gesture must move to result in the
* menu being shown or hidden.
*
* @type Number
*/
var MENU_DRAG_DELTA = 64;
/**
* The maximum X location of the start of a drag gesture for that gesture
* to potentially show the menu.
*
* @type Number
*/
var MENU_DRAG_MARGIN = 64;
/**
* When showing or hiding the menu via a drag gesture, the maximum number
* of pixels the touch can move vertically and still affect the menu.
*
* @type Number
*/
var MENU_DRAG_VERTICAL_TOLERANCE = 10;
/**
* 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,
0xFFE7 : true, 0xFFE8 : true},
CTRL_KEYS = {0xFFE3 : true, 0xFFE4 : true},
MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS);
/**
* Keysym for detecting any END key presses, for the purpose of passing through
* the Ctrl-Alt-Del sequence to a remote system.
*/
var END_KEYS = {0xFF57 : true, 0xFFB1 : true};
/**
* Keysym for sending the DELETE key when the Ctrl-Alt-End hotkey
* combo is pressed.
*
* @type Number
*/
var DEL_KEY = 0xFFFF;
/**
* Menu-specific properties.
*/
$scope.menu = {
/**
* Whether the menu is currently shown.
*
* @type Boolean
*/
shown : false,
/**
* The currently selected input method. This may be any of the values
* defined within preferenceService.inputMethods.
*
* @type String
*/
inputMethod : preferenceService.preferences.inputMethod,
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type Boolean
*/
emulateAbsoluteMouse : preferenceService.preferences.emulateAbsoluteMouse,
/**
* The current scroll state of the menu.
*
* @type ScrollState
*/
scrollState : new ScrollState(),
/**
* The current desired values of all editable connection parameters as
* a set of name/value pairs, including any changes made by the user.
*
* @type {Object.<String, String>}
*/
connectionParameters : {}
};
// Convenience method for closing the menu
$scope.closeMenu = function closeMenu() {
$scope.menu.shown = false;
};
/**
* Applies any changes to connection parameters made by the user within the
* Guacamole menu to the given ManagedClient. If no client is supplied,
* this function has no effect.
*
* @param {ManagedClient} client
* The client to apply parameter changes to.
*/
$scope.applyParameterChanges = function applyParameterChanges(client) {
angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
if (client)
ManagedClient.setArgument(client, name, value);
});
};
/**
* The currently-focused client within the current ManagedClientGroup. If
* there is no current group, no client is focused, or multiple clients are
* focused, this will be null.
*
* @type ManagedClient
*/
$scope.focusedClient = null;
/**
* The set of clients that should be attached to the client UI. This will
* be immediately initialized by a call to updateAttachedClients() below.
*
* @type ManagedClientGroup
*/
$scope.clientGroup = null;
/**
* @borrows ManagedClientGroup.getName
*/
$scope.getName = ManagedClientGroup.getName;
/**
* @borrows ManagedClientGroup.getTitle
*/
$scope.getTitle = ManagedClientGroup.getTitle;
/**
* Arbitrary context that should be exposed to the guacGroupList directive
* displaying the dropdown list of available connections within the
* Guacamole menu.
*/
$scope.connectionListContext = {
/**
* The set of clients desired within the current view. For each client
* that should be present within the current view, that client's ID
* will map to "true" here.
*
* @type {Object.<string, boolean>}
*/
attachedClients : {},
/**
* Notifies that the client with the given ID has been added or
* removed from the set of clients desired within the current view,
* and the current view should be updated accordingly.
*
* @param {string} id
* The ID of the client that was added or removed from the current
* view.
*/
updateAttachedClients : function updateAttachedClients(id) {
$scope.addRemoveClient(id, !$scope.connectionListContext.attachedClients[id]);
}
};
/**
* Adds or removes the client with the given ID from the set of clients
* within the current view, updating the current URL accordingly.
*
* @param {string} id
* The ID of the client to add or remove from the current view.
*
* @param {boolean} [remove=false]
* Whether the specified client should be added (false) or removed
* (true).
*/
$scope.addRemoveClient = function addRemoveClient(id, remove) {
// Deconstruct current path into corresponding client IDs
const ids = ManagedClientGroup.getClientIdentifiers($routeParams.id);
// Add/remove ID as requested
if (remove)
_.pull(ids, id);
else
ids.push(id);
// Reconstruct path, updating attached clients via change in route
$location.path('/client/' + ManagedClientGroup.getIdentifier(ids));
};
/**
* Reloads the contents of $scope.clientGroup to reflect the client IDs
* currently listed in the URL.
*/
const reparseRoute = function reparseRoute() {
const previousClients = $scope.clientGroup ? $scope.clientGroup.clients.slice() : [];
// Replace existing group with new group
setAttachedGroup(guacClientManager.getManagedClientGroup($routeParams.id));
// Store current set of attached clients for later use within the
// Guacamole menu
$scope.connectionListContext.attachedClients = {};
$scope.clientGroup.clients.forEach((client) => {
$scope.connectionListContext.attachedClients[client.id] = true;
});
// Ensure menu is closed if updated view is not a modification of the
// current view (has no clients in common). The menu should remain open
// only while the current view is being modified, not when navigating
// to an entirely different view.
if (_.isEmpty(_.intersection(previousClients, $scope.clientGroup.clients)))
$scope.menu.shown = false;
// Update newly-attached clients with current contents of clipboard
clipboardService.resyncClipboard();
};
/**
* Replaces the ManagedClientGroup currently attached to the client
* interface via $scope.clientGroup with the given ManagedClientGroup,
* safely cleaning up after the previous group. If no ManagedClientGroup is
* provided, the existing group is simply removed.
*
* @param {ManagedClientGroup} [managedClientGroup]
* The ManagedClientGroup to attach to the interface, if any.
*/
const setAttachedGroup = function setAttachedGroup(managedClientGroup) {
// Do nothing if group is not actually changing
if ($scope.clientGroup === managedClientGroup)
return;
if ($scope.clientGroup) {
// Remove all disconnected clients from management (the user has
// seen their status)
_.filter($scope.clientGroup.clients, client => {
const connectionState = client.clientState.connectionState;
return connectionState === ManagedClientState.ConnectionState.DISCONNECTED
|| connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR;
}).forEach(client => {
guacClientManager.removeManagedClient(client.id);
});
// Flag group as detached
$scope.clientGroup.attached = false;
}
if (managedClientGroup) {
$scope.clientGroup = managedClientGroup;
$scope.clientGroup.attached = true;
$scope.clientGroup.lastUsed = new Date().getTime();
}
};
// Init sets of clients based on current URL ...
reparseRoute();
// ... and re-initialize those sets if the URL has changed without
// reloading the route
$scope.$on('$routeUpdate', reparseRoute);
/**
* The root connection groups of the connection hierarchy that should be
* presented to the user for selecting a different connection, as a map of
* data source identifier to the root connection group of that data
* source. This will be null if the connection group hierarchy has not yet
* been loaded or if the hierarchy is inapplicable due to only one
* connection or balancing group being available.
*
* @type Object.<String, ConnectionGroup>
*/
$scope.rootConnectionGroups = null;
/**
* Array of all connection properties that are filterable.
*
* @type String[]
*/
$scope.filteredConnectionProperties = [
'name'
];
/**
* Array of all connection group properties that are filterable.
*
* @type String[]
*/
$scope.filteredConnectionGroupProperties = [
'name'
];
// Retrieve root groups and all descendants
dataSourceService.apply(
connectionGroupService.getConnectionGroupTree,
authenticationService.getAvailableDataSources(),
ConnectionGroup.ROOT_IDENTIFIER
)
.then(function rootGroupsRetrieved(rootConnectionGroups) {
// Store retrieved groups only if there are multiple connections or
// balancing groups available
var clientPages = userPageService.getClientPages(rootConnectionGroups);
if (clientPages.length > 1)
$scope.rootConnectionGroups = rootConnectionGroups;
}, requestService.WARN);
/**
* Map of all available sharing profiles for the current connection by
* their identifiers. If this information is not yet available, or no such
* sharing profiles exist, this will be an empty object.
*
* @type Object.<String, SharingProfile>
*/
$scope.sharingProfiles = {};
/**
* Map of all substituted key presses. If one key is pressed in place of another
* the value of the substituted key is stored in an object with the keysym of
* the original key.
*
* @type Object.<Number, Number>
*/
var substituteKeysPressed = {};
/**
* Returns whether the shortcut for showing/hiding the Guacamole menu
* (Ctrl+Alt+Shift) has been pressed.
*
* @param {Guacamole.Keyboard} keyboard
* The Guacamole.Keyboard object tracking the local keyboard state.
*
* @returns {boolean}
* true if Ctrl+Alt+Shift has been pressed, false otherwise.
*/
const isMenuShortcutPressed = function isMenuShortcutPressed(keyboard) {
// Ctrl+Alt+Shift has NOT been pressed if any key is currently held
// down that isn't Ctrl, Alt, or Shift
if (_.findKey(keyboard.pressed, (val, keysym) => !MENU_KEYS[keysym]))
return false;
// Verify that one of each required key is held, regardless of
// left/right location on the keyboard
return !!(
_.findKey(SHIFT_KEYS, (val, keysym) => keyboard.pressed[keysym])
&& _.findKey(ALT_KEYS, (val, keysym) => keyboard.pressed[keysym])
&& _.findKey(CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym])
);
};
// Show menu if the user swipes from the left, hide menu when the user
// swipes from the right, scroll menu while visible
$scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
if ($scope.menu.shown) {
// Hide menu if swipe-from-right gesture is detected
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
&& startX - currentX >= MENU_DRAG_DELTA)
$scope.menu.shown = false;
// Scroll menu by default
else {
$scope.menu.scrollState.left -= deltaX;
$scope.menu.scrollState.top -= deltaY;
}
}
// Show menu if swipe-from-left gesture is detected
else if (startX <= MENU_DRAG_MARGIN) {
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
&& currentX - startX >= MENU_DRAG_DELTA)
$scope.menu.shown = true;
}
return false;
};
// Show/hide UI elements depending on input method
$scope.$watch('menu.inputMethod', function setInputMethod(inputMethod) {
// Show input methods only if selected
$scope.showOSK = (inputMethod === 'osk');
$scope.showTextInput = (inputMethod === 'text');
});
// Update client state/behavior as visibility of the Guacamole menu changes
$scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
// Re-update available connection parameters, if there is a focused
// client (parameter information may not have been available at the
// time focus changed)
if (menuShown)
$scope.menu.connectionParameters = $scope.focusedClient ?
ManagedClient.getArgumentModel($scope.focusedClient) : {};
// Send any argument value data once menu is hidden
else if (menuShownPreviousState)
$scope.applyParameterChanges($scope.focusedClient);
/* Broadcast changes to the menu display state */
$scope.$broadcast('guacMenuShown', menuShown);
});
// Toggle the menu when the guacClientToggleMenu event is received
$scope.$on('guacToggleMenu',
() => $scope.menu.shown = !$scope.menu.shown);
// Show the menu when the guacClientShowMenu event is received
$scope.$on('guacShowMenu', () => $scope.menu.shown = true);
// Hide the menu when the guacClientHideMenu event is received
$scope.$on('guacHideMenu', () => $scope.menu.shown = false);
// Automatically track and cache the currently-focused client
$scope.$on('guacClientFocused', function focusedClientChanged(event, newFocusedClient) {
const oldFocusedClient = $scope.focusedClient;
$scope.focusedClient = newFocusedClient;
// Apply any parameter changes when focus is changing
if (oldFocusedClient)
$scope.applyParameterChanges(oldFocusedClient);
// Update available connection parameters, if there is a focused
// client
$scope.menu.connectionParameters = newFocusedClient ?
ManagedClient.getArgumentModel(newFocusedClient) : {};
});
// Automatically update connection parameters that have been modified
// for the current focused client
$scope.$on('guacClientArgumentsUpdated', function argumentsChanged(event, focusedClient) {
// Ignore any updated arguments not for the current focused client
if ($scope.focusedClient && $scope.focusedClient === focusedClient)
$scope.menu.connectionParameters = ManagedClient.getArgumentModel(focusedClient);
});
// Update page icon when thumbnail changes
$scope.$watch('focusedClient.thumbnail.canvas', function thumbnailChanged(canvas) {
iconService.setIcons(canvas);
});
// Pull sharing profiles once the tunnel UUID is known
$scope.$watch('focusedClient.tunnel.uuid', function retrieveSharingProfiles(uuid) {
// Only pull sharing profiles if tunnel UUID is actually available
if (!uuid) {
$scope.sharingProfiles = {};
return;
}
// Pull sharing profiles for the current connection
tunnelService.getSharingProfiles(uuid)
.then(function sharingProfilesRetrieved(sharingProfiles) {
$scope.sharingProfiles = sharingProfiles;
}, requestService.WARN);
});
/**
* Produces a sharing link for the current connection using the given
* sharing profile. The resulting sharing link, and any required login
* information, will be displayed to the user within the Guacamole menu.
*
* @param {SharingProfile} sharingProfile
* The sharing profile to use to generate the sharing link.
*/
$scope.share = function share(sharingProfile) {
if ($scope.focusedClient)
ManagedClient.createShareLink($scope.focusedClient, sharingProfile);
};
/**
* Returns whether the current connection has any associated share links.
*
* @returns {Boolean}
* true if the current connection has at least one associated share
* link, false otherwise.
*/
$scope.isShared = function isShared() {
return !!$scope.focusedClient && ManagedClient.isShared($scope.focusedClient);
};
/**
* Returns the total number of share links associated with the current
* connection.
*
* @returns {Number}
* The total number of share links associated with the current
* connection.
*/
$scope.getShareLinkCount = function getShareLinkCount() {
if (!$scope.focusedClient)
return 0;
// Count total number of links within the ManagedClient's share link map
var linkCount = 0;
for (const dummy in $scope.focusedClient.shareLinks)
linkCount++;
return linkCount;
};
// Opening the Guacamole menu after Ctrl+Alt+Shift, preventing those
// keypresses from reaching any Guacamole client
$scope.$on('guacBeforeKeydown', function incomingKeydown(event, keysym, keyboard) {
// Toggle menu if menu shortcut (Ctrl+Alt+Shift) is pressed
if (isMenuShortcutPressed(keyboard)) {
// Don't send this key event through to the client, and release
// all other keys involved in performing this shortcut
event.preventDefault();
keyboard.reset();
// Toggle the menu
$scope.$apply(function() {
$scope.menu.shown = !$scope.menu.shown;
});
}
// Prevent all keydown events while menu is open
else if ($scope.menu.shown)
event.preventDefault();
});
// Prevent all keyup events while menu is open
$scope.$on('guacBeforeKeyup', function incomingKeyup(event, keysym, keyboard) {
if ($scope.menu.shown)
event.preventDefault();
});
// Send Ctrl-Alt-Delete when Ctrl-Alt-End is pressed.
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
// If one of the End keys is pressed, and we have a one keysym from each
// of Ctrl and Alt groups, send Ctrl-Alt-Delete.
if (END_KEYS[keysym]
&& _.findKey(ALT_KEYS, (val, keysym) => keyboard.pressed[keysym])
&& _.findKey(CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym])
) {
// Don't send this event through to the client.
event.preventDefault();
// Record the substituted key press so that it can be
// properly dealt with later.
substituteKeysPressed[keysym] = DEL_KEY;
// Send through the delete key.
$scope.$broadcast('guacSyntheticKeydown', DEL_KEY);
}
});
// Update pressed keys as they are released
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
// Deal with substitute key presses
if (substituteKeysPressed[keysym]) {
event.preventDefault();
$scope.$broadcast('guacSyntheticKeyup', substituteKeysPressed[keysym]);
delete substituteKeysPressed[keysym];
}
});
// Update page title when client title changes
$scope.$watch('getTitle(clientGroup)', function clientTitleChanged(title) {
$scope.page.title = title;
});
/**
* Returns whether the current connection has been flagged as unstable due
* to an apparent network disruption.
*
* @returns {Boolean}
* true if the current connection has been flagged as unstable, false
* otherwise.
*/
$scope.isConnectionUnstable = function isConnectionUnstable() {
return _.findIndex($scope.clientGroup.clients, client => client.clientState.tunnelUnstable) !== -1;
};
/**
* Immediately disconnects all currently-focused clients, if any.
*/
$scope.disconnect = function disconnect() {
// Disconnect if client is available
if ($scope.clientGroup) {
$scope.clientGroup.clients.forEach(client => {
if (client.clientProperties.focused)
client.client.disconnect();
});
}
// Hide menu
$scope.menu.shown = false;
};
/**
* Disconnects the given ManagedClient, removing it from the current
* view.
*
* @param {ManagedClient} client
* The client to disconnect.
*/
$scope.closeClientTile = function closeClientTile(client) {
$scope.addRemoveClient(client.id, true);
guacClientManager.removeManagedClient(client.id);
// Ensure at least one client has focus (the only client with
// focus may just have been removed)
ManagedClientGroup.verifyFocus($scope.clientGroup);
};
/**
* Action which immediately disconnects the currently-connected client, if
* any.
*/
var DISCONNECT_MENU_ACTION = {
name : 'CLIENT.ACTION_DISCONNECT',
className : 'danger disconnect',
callback : $scope.disconnect
};
/**
* Action that toggles fullscreen mode within the
* currently-connected client and then closes the menu.
*/
var FULLSCREEN_MENU_ACTION = {
name : 'CLIENT.ACTION_FULLSCREEN',
classname : 'fullscreen action',
callback : function fullscreen() {
guacFullscreen.toggleFullscreenMode();
$scope.menu.shown = false;
}
};
// Set client-specific menu actions
$scope.clientMenuActions = [ DISCONNECT_MENU_ACTION,FULLSCREEN_MENU_ACTION ];
/**
* @borrows Protocol.getNamespace
*/
$scope.getProtocolNamespace = Protocol.getNamespace;
/**
* The currently-visible filesystem within the filesystem menu, if the
* filesystem menu is open. If no filesystem is currently visible, this
* will be null.
*
* @type ManagedFilesystem
*/
$scope.filesystemMenuContents = null;
/**
* Hides the filesystem menu.
*/
$scope.hideFilesystemMenu = function hideFilesystemMenu() {
$scope.filesystemMenuContents = null;
};
/**
* Shows the filesystem menu, displaying the contents of the given
* filesystem within it.
*
* @param {ManagedFilesystem} filesystem
* The filesystem to show within the filesystem menu.
*/
$scope.showFilesystemMenu = function showFilesystemMenu(filesystem) {
$scope.filesystemMenuContents = filesystem;
};
/**
* Returns whether the filesystem menu should be visible.
*
* @returns {Boolean}
* true if the filesystem menu is shown, false otherwise.
*/
$scope.isFilesystemMenuShown = function isFilesystemMenuShown() {
return !!$scope.filesystemMenuContents && $scope.menu.shown;
};
// Automatically refresh display when filesystem menu is shown
$scope.$watch('isFilesystemMenuShown()', function refreshFilesystem() {
// Refresh filesystem, if defined
var filesystem = $scope.filesystemMenuContents;
if (filesystem)
ManagedFilesystem.refresh(filesystem, filesystem.currentDirectory);
});
/**
* Returns the full path to the given file as an ordered array of parent
* directories.
*
* @param {ManagedFilesystem.File} file
* The file whose full path should be retrieved.
*
* @returns {ManagedFilesystem.File[]}
* An array of directories which make up the hierarchy containing the
* given file, in order of increasing depth.
*/
$scope.getPath = function getPath(file) {
var path = [];
// Add all files to path in ascending order of depth
while (file && file.parent) {
path.unshift(file);
file = file.parent;
}
return path;
};
/**
* Changes the current directory of the given filesystem to the given
* directory.
*
* @param {ManagedFilesystem} filesystem
* The filesystem whose current directory should be changed.
*
* @param {ManagedFilesystem.File} file
* The directory to change to.
*/
$scope.changeDirectory = function changeDirectory(filesystem, file) {
ManagedFilesystem.changeDirectory(filesystem, file);
};
/**
* Begins a file upload through the attached Guacamole client for
* each file in the given FileList.
*
* @param {FileList} files
* The files to upload.
*/
$scope.uploadFiles = function uploadFiles(files) {
// Upload each file
for (var i = 0; i < files.length; i++)
ManagedClient.uploadFile($scope.filesystemMenuContents.client, files[i], $scope.filesystemMenuContents);
};
/**
* Determines whether the attached client group has any associated file
* transfers, regardless of those file transfers' state.
*
* @returns {Boolean}
* true if there are any file transfers associated with the
* attached client group, false otherise.
*/
$scope.hasTransfers = function hasTransfers() {
// There are no file transfers if there is no client group
if (!$scope.clientGroup)
return false;
return _.findIndex($scope.clientGroup.clients, ManagedClient.hasTransfers) !== -1;
};
/**
* Returns whether the current user can share the current connection with
* other users. A connection can be shared if and only if there is at least
* one associated sharing profile.
*
* @returns {Boolean}
* true if the current user can share the current connection with other
* users, false otherwise.
*/
$scope.canShareConnection = function canShareConnection() {
// If there is at least one sharing profile, the connection can be shared
for (var dummy in $scope.sharingProfiles)
return true;
// Otherwise, sharing is not possible
return false;
};
// Clean up when view destroyed
$scope.$on('$destroy', function clientViewDestroyed() {
setAttachedGroup(null);
// always unset fullscreen mode to not confuse user
guacFullscreen.setFullscreenMode(false);
});
}]);

View File

@@ -0,0 +1,675 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for the guacamole client.
*/
angular.module('client').directive('guacClient', [function guacClient() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClient.html'
};
directive.scope = {
/**
* The client to display within this guacClient directive.
*
* @type ManagedClient
*/
client : '=',
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type boolean
*/
emulateAbsoluteMouse : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientController($scope, $injector, $element) {
// Required types
const ManagedClient = $injector.get('ManagedClient');
// Required services
const $rootScope = $injector.get('$rootScope');
const $window = $injector.get('$window');
/**
* Whether the local, hardware mouse cursor is in use.
*
* @type Boolean
*/
let localCursor = false;
/**
* The current Guacamole client instance.
*
* @type Guacamole.Client
*/
let client = null;
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
let display = null;
/**
* The element associated with the display of the current
* Guacamole client instance.
*
* @type Element
*/
let displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
const displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
const main = $element[0];
/**
* Guacamole mouse event object, wrapped around the main client
* display.
*
* @type Guacamole.Mouse
*/
const mouse = new Guacamole.Mouse(displayContainer);
/**
* Guacamole absolute mouse emulation object, wrapped around the
* main client display.
*
* @type Guacamole.Mouse.Touchscreen
*/
const touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer);
/**
* Guacamole relative mouse emulation object, wrapped around the
* main client display.
*
* @type Guacamole.Mouse.Touchpad
*/
const touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
/**
* Guacamole touch event handling object, wrapped around the main
* client dislay.
*
* @type Guacamole.Touch
*/
const touch = new Guacamole.Touch(displayContainer);
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
const updateDisplayScale = function updateDisplayScale() {
if (!display) return;
// Calculate scale to fit screen
$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.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
// Clamp zoom level, maintain auto-fit
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
else if (display.getScale() > $scope.client.clientProperties.maxScale)
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
};
/**
* Scrolls the client view such that the mouse cursor is visible.
*
* @param {Guacamole.Mouse.State} mouseState The current mouse
* state.
*/
const scrollToMouse = function scrollToMouse(mouseState) {
// Determine mouse position within view
const mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft;
const mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop;
// Determine viewport dimensions
const view_width = main.offsetWidth;
const view_height = main.offsetHeight;
// Determine scroll amounts based on mouse position relative to document
let 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;
let 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.
main.scrollLeft += scroll_amount_x;
main.scrollTop += scroll_amount_y;
};
/**
* Return the name of the angular event associated with the provided
* mouse event.
*
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to determine an angular event name for.
*
* @returns
* The name of the angular event associated with the provided
* mouse event.
*/
const getMouseEventName = event => {
switch (event.type) {
case 'mousedown':
return 'guacClientMouseDown';
case 'mouseup':
return 'guacClientMouseUp';
default:
return 'guacClientMouseMove';
}
};
/**
* Handles a mouse event originating from the user's actual mouse.
* This differs from handleEmulatedMouseEvent() in that the
* software mouse cursor must be shown only if the user's browser
* does not support explicitly setting the hardware mouse cursor.
*
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to handle.
*/
const handleMouseEvent = function handleMouseEvent(event) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.stopPropagation();
event.preventDefault();
// Send mouse state, show cursor if necessary
display.showCursor(!localCursor);
client.sendMouseState(event.state, true);
// Broadcast the mouse event
$rootScope.$broadcast(getMouseEventName(event), event, client);
};
/**
* Handles a mouse event originating from one of Guacamole's mouse
* emulation objects. This differs from handleMouseState() in that
* the software mouse cursor must always be shown (as the emulated
* mouse device will not have its own cursor).
*
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to handle.
*/
const handleEmulatedMouseEvent = function handleEmulatedMouseEvent(event) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.stopPropagation();
event.preventDefault();
// Ensure software cursor is shown
display.showCursor(true);
// Send mouse state, ensure cursor is visible
scrollToMouse(event.state);
client.sendMouseState(event.state, true);
// Broadcast the mouse event
$rootScope.$broadcast(getMouseEventName(event), event, client);
};
/**
* Return the name of the angular event associated with the provided
* touch event.
*
* @param {Guacamole.Touch.TouchEvent} event
* The touch event to determine an angular event name for.
*
* @returns
* The name of the angular event associated with the provided
* touch event.
*/
const getTouchEventName = event => {
switch (event.type) {
case 'touchstart':
return 'guacClientTouchStart';
case 'touchend':
return 'guacClientTouchEnd';
default:
return 'guacClientTouchMove';
}
};
/**
* Handles a touch event originating from the user's device.
*
* @param {Guacamole.Touch.Event} touchEvent
* The touch event.
*/
const handleTouchEvent = function handleTouchEvent(event) {
// Do not attempt to handle touch state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.preventDefault();
// Send touch state, hiding local cursor
display.showCursor(false);
client.sendTouchState(event.state, true);
// Broadcast the touch event
$rootScope.$broadcast(getTouchEventName(event), event, client);
};
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Remove any existing display
displayContainer.innerHTML = "";
// Only proceed if a client is given
if (!managedClient)
return;
// Get Guacamole client instance
client = managedClient.client;
// Attach possibly new display
display = client.getDisplay();
display.scale($scope.client.clientProperties.scale);
// Add display element
displayElement = display.getElement();
displayContainer.appendChild(displayElement);
// Do nothing when the display element is clicked on
display.getElement().onclick = function(e) {
e.preventDefault();
return false;
};
// Connect and update interface to match required size, deferring
// connecting until a future element resize if the main element
// size (desired display size) is not known and thus can't be sent
// during the handshake
$scope.mainElementResized();
});
// Update actual view scrollLeft when scroll properties change
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
});
// Update actual view scrollTop when scroll properties change
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
main.scrollTop = scrollTop;
$scope.client.clientProperties.scrollTop = main.scrollTop;
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize() {
$scope.$evalAsync(updateDisplayScale);
});
// Keep local cursor up-to-date
$scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) {
if (cursor)
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
});
// Update touch event handling depending on remote multi-touch
// support and mouse emulation mode
$scope.$watchGroup([
'client.multiTouchSupport',
'emulateAbsoluteMouse'
], function touchBehaviorChanged() {
// Clear existing event handling
touch.offEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
touchScreen.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
touchPad.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
// Directly forward local touch events
if ($scope.client.multiTouchSupport)
touch.onEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
// Switch to touchscreen if mouse emulation is required and
// absolute mouse emulation is preferred
else if ($scope.emulateAbsoluteMouse)
touchScreen.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
// Use touchpad for mouse emulation if absolute mouse emulation
// is not preferred
else
touchPad.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
});
// Adjust scale if modified externally
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
// Fix scale within limits
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.client.clientProperties.minScale)
main.style.overflow = "hidden";
// If not at minimum zoom level, show scroll bars
else
main.style.overflow = "auto";
// Apply scale if client attached
if (display)
display.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('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
if(autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
});
/**
* Sends the current size of the main element (the display container)
* to the Guacamole server, requesting that the remote display be
* resized. If the Guacamole client is not yet connected, it will be
* connected and the current size will sent through the initial
* handshake. If the size of the main element is not yet known, this
* function may need to be invoked multiple times until the size is
* known and the client may be connected.
*/
$scope.mainElementResized = function mainElementResized() {
// Send new display size, if changed
if (client && display && main.offsetWidth && main.offsetHeight) {
// Connect, if not already connected
ManagedClient.connect($scope.client, main.offsetWidth, main.offsetHeight);
const pixelDensity = $window.devicePixelRatio || 1;
const width = main.offsetWidth * pixelDensity;
const height = main.offsetHeight * pixelDensity;
if (display.getWidth() !== width || display.getHeight() !== height)
client.sendSize(width, height);
}
$scope.$evalAsync(updateDisplayScale);
};
// Scroll client display if absolute mouse is in use (the same drag
// gesture is needed for moving the mouse pointer with relative mouse)
$scope.clientDrag = function clientDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
if ($scope.emulateAbsoluteMouse) {
$scope.client.clientProperties.scrollLeft -= deltaX;
$scope.client.clientProperties.scrollTop -= deltaY;
}
return false;
};
/**
* If a pinch gesture is in progress, the scale of the client display when
* the pinch gesture began.
*
* @type Number
*/
let initialScale = null;
/**
* If a pinch gesture is in progress, the X coordinate of the point on the
* client display that was centered within the pinch at the time the
* gesture began.
*
* @type Number
*/
let initialCenterX = 0;
/**
* If a pinch gesture is in progress, the Y coordinate of the point on the
* client display that was centered within the pinch at the time the
* gesture began.
*
* @type Number
*/
let initialCenterY = 0;
// Zoom and pan client via pinch gestures
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
// Do not handle pinch gestures if they would conflict with remote
// handling of similar gestures
if ($scope.client.multiTouchSupport > 1)
return false;
// Do not handle pinch gestures while relative mouse is in use (2+
// contact point gestures are used by relative mouse emulation to
// support right click, middle click, and scrolling)
if (!$scope.emulateAbsoluteMouse)
return false;
// Stop gesture if not in progress
if (!inProgress) {
initialScale = null;
return false;
}
// Set initial scale if gesture has just started
if (!initialScale) {
initialScale = $scope.client.clientProperties.scale;
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
}
// Determine new scale absolutely
let currentScale = initialScale * currentLength / startLength;
// Fix scale within limits - scroll will be miscalculated otherwise
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
// Update scale based on pinch distance
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale = currentScale;
// Scroll display to keep original pinch location centered within current pinch
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
return false;
};
// Ensure focus is regained via mousedown before forwarding event
mouse.on('mousedown', document.body.focus.bind(document.body));
// Forward all mouse events
mouse.onEach(['mousedown', 'mousemove', 'mouseup'], handleMouseEvent);
// Hide software cursor when mouse leaves display
mouse.on('mouseout', function() {
if (!display) return;
display.showCursor(false);
});
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function onClipboard(event, data) {
ManagedClient.setClipboard($scope.client, data);
});
// Translate local keydown events to remote keydown events if keyboard is enabled
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.focused) {
client.sendKeyEvent(1, keysym);
event.preventDefault();
}
});
// Translate local keyup events to remote keyup events if keyboard is enabled
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.focused) {
client.sendKeyEvent(0, keysym);
event.preventDefault();
}
});
// Universally handle all synthetic keydown events
$scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) {
if ($scope.client.clientProperties.focused)
client.sendKeyEvent(1, keysym);
});
// Universally handle all synthetic keyup events
$scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) {
if ($scope.client.clientProperties.focused)
client.sendKeyEvent(0, keysym);
});
/**
* Whether a drag/drop operation is currently in progress (the user has
* dragged a file over the Guacamole connection but has not yet
* dropped it).
*
* @type boolean
*/
$scope.dropPending = false;
/**
* Displays a visual indication that dropping the file currently
* being dragged is possible. Further propagation and default behavior
* of the given event is automatically prevented.
*
* @param {Event} e
* The event related to the in-progress drag/drop operation.
*/
const notifyDragStart = function notifyDragStart(e) {
e.preventDefault();
e.stopPropagation();
$scope.$apply(() => {
$scope.dropPending = true;
});
};
/**
* Removes the visual indication that dropping the file currently
* being dragged is possible. Further propagation and default behavior
* of the given event is automatically prevented.
*
* @param {Event} e
* The event related to the end of the former drag/drop operation.
*/
const notifyDragEnd = function notifyDragEnd(e) {
e.preventDefault();
e.stopPropagation();
$scope.$apply(() => {
$scope.dropPending = false;
});
};
main.addEventListener('dragenter', notifyDragStart, false);
main.addEventListener('dragover', notifyDragStart, false);
main.addEventListener('dragleave', notifyDragEnd, false);
// File drop event handler
main.addEventListener('drop', function(e) {
notifyDragEnd(e);
// Ignore file drops if no attached client
if (!$scope.client)
return;
// Upload each file
const files = e.dataTransfer.files;
for (let i = 0; i < files.length; i++)
ManagedClient.uploadFile($scope.client, files[i]);
}, false);
}];
return directive;
}]);

View File

@@ -0,0 +1,409 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying a non-global notification describing the status
* of a specific Guacamole client, including prompts for any information
* necessary to continue the connection.
*/
angular.module('client').directive('guacClientNotification', [function guacClientNotification() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClientNotification.html'
};
directive.scope = {
/**
* The client whose status should be displayed.
*
* @type ManagedClient
*/
client : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientNotificationController($scope, $injector, $element) {
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientState = $injector.get('ManagedClientState');
const Protocol = $injector.get('Protocol');
// Required services
const $location = $injector.get('$location');
const authenticationService = $injector.get('authenticationService');
const guacClientManager = $injector.get('guacClientManager');
const guacTranslate = $injector.get('guacTranslate');
const requestService = $injector.get('requestService');
const userPageService = $injector.get('userPageService');
/**
* A Notification object describing the client status to display as a
* dialog or prompt, as would be accepted by guacNotification.showStatus(),
* or false if no status should be shown.
*
* @type {Notification|Object|Boolean}
*/
$scope.status = false;
/**
* All error codes for which automatic reconnection is appropriate when a
* client error occurs.
*/
const CLIENT_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0308: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* tunnel error occurs.
*/
const TUNNEL_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0308: true
};
/**
* Action which logs out from Guacamole entirely.
*/
const LOGOUT_ACTION = {
name : "CLIENT.ACTION_LOGOUT",
className : "logout button",
callback : function logoutCallback() {
authenticationService.logout()
['catch'](requestService.IGNORE);
}
};
/**
* Action which returns the user to the home screen. If the home page has
* not yet been determined, this will be null.
*/
let NAVIGATE_HOME_ACTION = null;
// Assign home page action once user's home page has been determined
userPageService.getHomePage()
.then(function homePageRetrieved(homePage) {
// Define home action only if different from current location
if ($location.path() !== homePage.url) {
NAVIGATE_HOME_ACTION = {
name : "CLIENT.ACTION_NAVIGATE_HOME",
className : "home button",
callback : function navigateHomeCallback() {
$location.url(homePage.url);
}
};
}
}, requestService.WARN);
/**
* Action which replaces the current client with a newly-connected client.
*/
const RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT",
className : "reconnect button",
callback : function reconnectCallback() {
$scope.client = guacClientManager.replaceManagedClient($scope.client.id);
$scope.status = false;
}
};
/**
* The reconnect countdown to display if an error or status warrants an
* automatic, timed reconnect.
*/
const RECONNECT_COUNTDOWN = {
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
callback: RECONNECT_ACTION.callback,
remaining: 15
};
/**
* Displays a notification at the end of a Guacamole connection, whether
* that connection is ending normally or due to an error. As the end of
* a Guacamole connection may be due to changes in authentication status,
* this will also implicitly peform a re-authentication attempt to check
* for such changes, possibly resulting in auth-related events like
* guacInvalidCredentials.
*
* @param {Notification|Boolean|Object} status
* The status notification to show, as would be accepted by
* guacNotification.showStatus().
*/
const notifyConnectionClosed = function notifyConnectionClosed(status) {
// Re-authenticate to verify auth status at end of connection
authenticationService.updateCurrentToken($location.search())
['catch'](requestService.IGNORE)
// Show the requested status once the authentication check has finished
['finally'](function authenticationCheckComplete() {
$scope.status = status;
});
};
/**
* Notifies the user that the connection state has changed.
*
* @param {String} connectionState
* The current connection state, as defined by
* ManagedClientState.ConnectionState.
*/
const notifyConnectionState = function notifyConnectionState(connectionState) {
// Hide any existing status
$scope.status = false;
// Do not display status if status not known
if (!connectionState)
return;
// Build array of available actions
let actions;
if (NAVIGATE_HOME_ACTION)
actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
else
actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
// Get any associated status code
const status = $scope.client.clientState.statusCode;
// Connecting
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
$scope.status = {
className : "connecting",
title: "CLIENT.DIALOG_HEADER_CONNECTING",
text: {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
}
};
}
// Client error
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
// Translation IDs for this error code
const errorPrefix = "CLIENT.ERROR_CLIENT_";
const errorId = errorPrefix + status.toString(16).toUpperCase();
const defaultErrorId = errorPrefix + "DEFAULT";
// Determine whether the reconnect countdown applies
const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Use the guacTranslate service to determine if there is a translation for
// this error code; if not, use the default
guacTranslate(errorId, defaultErrorId).then(
// Show error status
translationResult => notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : translationResult.id
},
countdown : countdown,
actions : actions
})
);
}
// Tunnel error
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
// Translation IDs for this error code
const errorPrefix = "CLIENT.ERROR_TUNNEL_";
const errorId = errorPrefix + status.toString(16).toUpperCase();
const defaultErrorId = errorPrefix + "DEFAULT";
// Determine whether the reconnect countdown applies
const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Use the guacTranslate service to determine if there is a translation for
// this error code; if not, use the default
guacTranslate(errorId, defaultErrorId).then(
// Show error status
translationResult => notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : translationResult.id
},
countdown : countdown,
actions : actions
})
);
}
// Disconnected
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
notifyConnectionClosed({
title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
text : {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
},
actions : actions
});
}
// Hide status for all other states
else
$scope.status = false;
};
/**
* Prompts the user to enter additional connection parameters. If the
* protocol and associated parameters of the underlying connection are not
* yet known, this function has no effect and should be re-invoked once
* the parameters are known.
*
* @param {Object.<String, String>} requiredParameters
* The set of all parameters requested by the server via "required"
* instructions, where each object key is the name of a requested
* parameter and each value is the current value entered by the user.
*/
const notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
/**
* Action which submits the current set of parameter values, requesting
* that the connection continue.
*/
const SUBMIT_PARAMETERS = {
name : "CLIENT.ACTION_CONTINUE",
className : "button",
callback : function submitParameters() {
if ($scope.client) {
const params = $scope.client.requiredParameters;
$scope.client.requiredParameters = null;
ManagedClient.sendArguments($scope.client, params);
}
}
};
/**
* Action which cancels submission of additional parameters and
* disconnects from the current connection.
*/
const CANCEL_PARAMETER_SUBMISSION = {
name : "CLIENT.ACTION_CANCEL",
className : "button",
callback : function cancelSubmission() {
$scope.client.requiredParameters = null;
$scope.client.client.disconnect();
}
};
// Attempt to prompt for parameters only if the parameters that apply
// to the underlying connection are known
if (!$scope.client.protocol || !$scope.client.forms)
return;
// Prompt for parameters
$scope.status = {
className : "parameters-required",
formNamespace : Protocol.getNamespace($scope.client.protocol),
forms : $scope.client.forms,
formModel : requiredParameters,
formSubmitCallback : SUBMIT_PARAMETERS.callback,
actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
};
};
/**
* Returns whether the given connection state allows for submission of
* connection parameters via "argv" instructions.
*
* @param {String} connectionState
* The connection state to test, as defined by
* ManagedClientState.ConnectionState.
*
* @returns {boolean}
* true if the given connection state allows submission of connection
* parameters via "argv" instructions, false otherwise.
*/
const canSubmitParameters = function canSubmitParameters(connectionState) {
return (connectionState === ManagedClientState.ConnectionState.WAITING ||
connectionState === ManagedClientState.ConnectionState.CONNECTED);
};
// Show status dialog when connection status changes
$scope.$watchGroup([
'client.clientState.connectionState',
'client.requiredParameters',
'client.protocol',
'client.forms'
], function clientStateChanged(newValues) {
const connectionState = newValues[0];
const requiredParameters = newValues[1];
// Prompt for parameters only if parameters can actually be submitted
if (requiredParameters && canSubmitParameters(connectionState))
notifyParametersRequired(requiredParameters);
// Otherwise, just show general connection state
else
notifyConnectionState(connectionState);
});
/**
* Prevents the default behavior of the given AngularJS event if a
* notification is currently shown and the client is focused.
*
* @param {event} e
* The AngularJS event to selectively prevent.
*/
const preventDefaultDuringNotification = function preventDefaultDuringNotification(e) {
if ($scope.status && $scope.client.clientProperties.focused)
e.preventDefault();
};
// Block internal handling of key events (by the client) if a
// notification is visible
$scope.$on('guacBeforeKeydown', preventDefaultDuringNotification);
$scope.$on('guacBeforeKeyup', preventDefaultDuringNotification);
}];
return directive;
}]);

View File

@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A toolbar/panel which displays a list of active Guacamole connections. The
* panel is fixed to the bottom-right corner of its container and can be
* manually hidden/exposed by the user.
*/
angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
// Required services
const guacClientManager = $injector.get('guacClientManager');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedClientState = $injector.get('ManagedClientState');
/**
* Getter/setter for the boolean flag controlling whether the client panel
* is currently hidden. This flag is maintained in session-local storage to
* allow the state of the panel to persist despite navigation within the
* same tab. When hidden, the panel will be collapsed against the right
* side of the container. By default, the panel is visible.
*
* @type Function
*/
var panelHidden = sessionStorageFactory.create(false);
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The ManagedClientGroup instances associated with the active
* connections to be displayed within this panel.
*
* @type ManagedClientGroup[]
*/
clientGroups : '='
},
templateUrl: 'app/client/templates/guacClientPanel.html',
controller: ['$scope', '$element', function guacClientPanelController($scope, $element) {
/**
* The DOM element containing the scrollable portion of the client
* panel.
*
* @type Element
*/
var scrollableArea = $element.find('.client-panel-connection-list')[0];
/**
* On-scope reference to session-local storage of the flag
* controlling whether then panel is hidden.
*/
$scope.panelHidden = panelHidden;
/**
* Returns whether this panel currently has any client groups
* associated with it.
*
* @return {Boolean}
* true if at least one client group is associated with this
* panel, false otherwise.
*/
$scope.hasClientGroups = function hasClientGroups() {
return $scope.clientGroups && $scope.clientGroups.some(function(group) {
// Check if there is any group that is not attached
return !group.attached;
});
};
/**
* @borrows ManagedClientGroup.getIdentifier
*/
$scope.getIdentifier = ManagedClientGroup.getIdentifier;
/**
* @borrows ManagedClientGroup.getTitle
*/
$scope.getTitle = ManagedClientGroup.getTitle;
/**
* Returns whether the status of any client within the given client
* group has changed in a way that requires the user's attention.
* This may be due to an error, or due to a server-initiated
* disconnect.
*
* @param {ManagedClientGroup} clientGroup
* The client group to test.
*
* @returns {Boolean}
* true if the given client requires the user's attention,
* false otherwise.
*/
$scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) {
return _.findIndex(clientGroup.clients, (client) => {
// Test whether the client has encountered an error
switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED:
return true;
}
return false;
}) !== -1;
};
/**
* Initiates an orderly disconnect of all clients within the given
* group. The clients are removed from management such that
* attempting to connect to any of the same connections will result
* in new connections being established, rather than displaying a
* notification that the connection has ended.
*
* @param {ManagedClientGroup} clientGroup
* The group of clients to disconnect.
*/
$scope.disconnect = function disconnect(clientGroup) {
guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
};
/**
* Toggles whether the client panel is currently hidden.
*/
$scope.togglePanel = function togglePanel() {
panelHidden(!panelHidden());
};
// Override vertical scrolling, scrolling horizontally instead
scrollableArea.addEventListener('wheel', function reorientVerticalScroll(e) {
var deltaMultiplier = {
/* DOM_DELTA_PIXEL */ 0x00: 1,
/* DOM_DELTA_LINE */ 0x01: 15,
/* DOM_DELTA_PAGE */ 0x02: scrollableArea.offsetWidth
};
if (e.deltaY) {
this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01));
e.preventDefault();
}
});
}]
};
}]);

View File

@@ -0,0 +1,237 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive that displays a status indicator showing the number of users
* joined to a connection. The specific usernames of those users are visible in
* a tooltip on mouseover, and small notifications are displayed as users
* join/leave the connection.
*/
angular.module('client').directive('guacClientUserCount', [function guacClientUserCount() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClientUserCount.html'
};
directive.scope = {
/**
* The client whose current users should be displayed.
*
* @type ManagedClient
*/
client : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientUserCountController($scope, $injector, $element) {
// Required types
var AuthenticationResult = $injector.get('AuthenticationResult');
// Required services
var $translate = $injector.get('$translate');
/**
* The maximum number of messages displayed by this directive at any
* given time. Old messages will be discarded as necessary to ensure
* the number of messages displayed never exceeds this value.
*
* @constant
* @type number
*/
var MAX_MESSAGES = 3;
/**
* The list that should contain any notifications regarding users
* joining or leaving the connection.
*
* @type HTMLUListElement
*/
var messages = $element.find('.client-user-count-messages')[0];
/**
* Map of the usernames of all users of the current connection to the
* number of concurrent connections those users have to the current
* connection.
*
* @type Object.<string, number>
*/
$scope.userCounts = {};
/**
* Displays a message noting that a change related to a particular user
* of this connection has occurred.
*
* @param {!string} str
* The key of the translation string containing the message to
* display. This translation key must accept "USERNAME" as the
* name of the translation parameter containing the username of
* the user in question.
*
* @param {!string} username
* The username of the user in question.
*/
var notify = function notify(str, username) {
$translate(str, { 'USERNAME' : username }).then(function translationReady(text) {
if (messages.childNodes.length === 3)
messages.removeChild(messages.lastChild);
var message = document.createElement('li');
message.className = 'client-user-count-message';
message.textContent = text;
messages.insertBefore(message, messages.firstChild);
// Automatically remove the notification after its "fadeout"
// animation ends. NOTE: This will not fire if the element is
// not visible at all.
message.addEventListener('animationend', function animationEnded() {
messages.removeChild(message);
});
});
};
/**
* Displays a message noting that a particular user has joined the
* current connection.
*
* @param {!string} username
* The username of the user that joined.
*/
var notifyUserJoined = function notifyUserJoined(username) {
if ($scope.isAnonymous(username))
notify('CLIENT.TEXT_ANONYMOUS_USER_JOINED', username);
else
notify('CLIENT.TEXT_USER_JOINED', username);
};
/**
* Displays a message noting that a particular user has left the
* current connection.
*
* @param {!string} username
* The username of the user that left.
*/
var notifyUserLeft = function notifyUserLeft(username) {
if ($scope.isAnonymous(username))
notify('CLIENT.TEXT_ANONYMOUS_USER_LEFT', username);
else
notify('CLIENT.TEXT_USER_LEFT', username);
};
/**
* The ManagedClient attached to this directive at the time the
* notification update scope watch was last invoked. This is necessary
* as $scope.$watchGroup() does not allow for the callback to know
* whether the scope was previously uninitialized (it's "oldValues"
* parameter receives a copy of the new values if there are no old
* values).
*
* @type ManagedClient
*/
var oldClient = null;
/**
* Returns whether the given username represents an anonymous user.
*
* @param {!string} username
* The username of the user to check.
*
* @returns {!boolean}
* true if the given username represents an anonymous user, false
* otherwise.
*/
$scope.isAnonymous = function isAnonymous(username) {
return username === AuthenticationResult.ANONYMOUS_USERNAME;
};
/**
* Returns the translation key of the translation string that should be
* used to render the number of connections a user with the given
* username has to the current connection. The appropriate string will
* vary by whether the user is anonymous.
*
* @param {!string} username
* The username of the user to check.
*
* @returns {!string}
* The translation key of the translation string that should be
* used to render the number of connections the user with the given
* username has to the current connection.
*/
$scope.getUserCountTranslationKey = function getUserCountTranslationKey(username) {
return $scope.isAnonymous(username) ? 'CLIENT.INFO_ANONYMOUS_USER_COUNT' : 'CLIENT.INFO_USER_COUNT';
};
// Update visible notifications as users join/leave
$scope.$watchGroup([ 'client', 'client.userCount' ], function usersChanged() {
// Resynchronize directive with state of any attached client when
// the client changes, to ensure notifications are only shown for
// future changes in users present
if (oldClient !== $scope.client) {
$scope.userCounts = {};
oldClient = $scope.client;
angular.forEach($scope.client.users, function initUsers(connections, username) {
var count = Object.keys(connections).length;
$scope.userCounts[username] = count;
});
return;
}
// Display join/leave notifications for users who are currently
// connected but whose connection counts have changed
angular.forEach($scope.client.users, function addNewUsers(connections, username) {
var count = Object.keys(connections).length;
var known = $scope.userCounts[username] || 0;
if (count > known)
notifyUserJoined(username);
else if (count < known)
notifyUserLeft(username);
$scope.userCounts[username] = count;
});
// Display leave notifications for users who are no longer connected
angular.forEach($scope.userCounts, function removeOldUsers(count, username) {
if (!$scope.client.users[username]) {
notifyUserLeft(username);
delete $scope.userCounts[username];
}
});
});
}];
return directive;
}]);

View File

@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for controlling the zoom level and scale-to-fit behavior of a
* a single Guacamole client.
*/
angular.module('client').directive('guacClientZoom', [function guacClientZoom() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClientZoom.html'
};
directive.scope = {
/**
* The client to control the zoom/autofit of.
*
* @type ManagedClient
*/
client : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientZoomController($scope, $injector, $element) {
/**
* Zooms in by 10%, automatically disabling autofit.
*/
$scope.zoomIn = function zoomIn() {
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale += 0.1;
};
/**
* Zooms out by 10%, automatically disabling autofit.
*/
$scope.zoomOut = function zoomOut() {
$scope.client.clientProperties.autoFit = false;
$scope.client.clientProperties.scale -= 0.1;
};
/**
* Resets the client autofit setting to false.
*/
$scope.clearAutoFit = function clearAutoFit() {
$scope.client.clientProperties.autoFit = false;
};
/**
* Notifies that the autofit setting has been manually changed by the
* user.
*/
$scope.autoFitChanged = function autoFitChanged() {
// Reset to 100% scale when autofit is first disabled
if (!$scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = 1;
};
}];
return directive;
}]);

View File

@@ -0,0 +1,281 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which displays the contents of a filesystem received through the
* Guacamole client.
*/
angular.module('client').directive('guacFileBrowser', [function guacFileBrowser() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* @type ManagedFilesystem
*/
filesystem : '='
},
templateUrl: 'app/client/templates/guacFileBrowser.html',
controller: ['$scope', '$element', '$injector', function guacFileBrowserController($scope, $element, $injector) {
// Required types
var ManagedFilesystem = $injector.get('ManagedFilesystem');
// Required services
var $interpolate = $injector.get('$interpolate');
var $templateRequest = $injector.get('$templateRequest');
/**
* The jQuery-wrapped element representing the contents of the
* current directory within the file browser.
*
* @type Element[]
*/
var currentDirectoryContents = $element.find('.current-directory-contents');
/**
* Statically-cached template HTML used to render each file within
* a directory. Once available, this will be used through
* createFileElement() to generate the DOM elements which make up
* a directory listing.
*
* @type String
*/
var fileTemplate = null;
/**
* Returns whether the given file is a normal file.
*
* @param {ManagedFilesystem.File} file
* The file to test.
*
* @returns {Boolean}
* true if the given file is a normal file, false otherwise.
*/
$scope.isNormalFile = function isNormalFile(file) {
return file.type === ManagedFilesystem.File.Type.NORMAL;
};
/**
* Returns whether the given file is a directory.
*
* @param {ManagedFilesystem.File} file
* The file to test.
*
* @returns {Boolean}
* true if the given file is a directory, false otherwise.
*/
$scope.isDirectory = function isDirectory(file) {
return file.type === ManagedFilesystem.File.Type.DIRECTORY;
};
/**
* Changes the currently-displayed directory to the given
* directory.
*
* @param {ManagedFilesystem.File} file
* The directory to change to.
*/
$scope.changeDirectory = function changeDirectory(file) {
ManagedFilesystem.changeDirectory($scope.filesystem, file);
};
/**
* Initiates a download of the given file. The progress of the
* download can be observed through guacFileTransferManager.
*
* @param {ManagedFilesystem.File} file
* The file to download.
*/
$scope.downloadFile = function downloadFile(file) {
ManagedFilesystem.downloadFile($scope.filesystem, file.streamName);
};
/**
* Recursively interpolates all text nodes within the DOM tree of
* the given element. All other node types, attributes, etc. will
* be left uninterpolated.
*
* @param {Element} element
* The element at the root of the DOM tree to be interpolated.
*
* @param {Object} context
* The evaluation context to use when evaluating expressions
* embedded in text nodes within the provided element.
*/
var interpolateElement = function interpolateElement(element, context) {
// Interpolate the contents of text nodes directly
if (element.nodeType === Node.TEXT_NODE)
element.nodeValue = $interpolate(element.nodeValue)(context);
// Recursively interpolate the contents of all descendant text
// nodes
if (element.hasChildNodes()) {
var children = element.childNodes;
for (var i = 0; i < children.length; i++)
interpolateElement(children[i], context);
}
};
/**
* Creates a new element representing the given file and properly
* handling user events, bypassing the overhead incurred through
* use of ngRepeat and related techniques.
*
* Note that this function depends on the availability of the
* statically-cached fileTemplate.
*
* @param {ManagedFilesystem.File} file
* The file to generate an element for.
*
* @returns {Element[]}
* A jQuery-wrapped array containing a single DOM element
* representing the given file.
*/
var createFileElement = function createFileElement(file) {
// Create from internal template
var element = angular.element(fileTemplate);
interpolateElement(element[0], file);
// Double-clicking on unknown file types will do nothing
var fileAction = function doNothing() {};
// Change current directory when directories are clicked
if ($scope.isDirectory(file)) {
element.addClass('directory');
fileAction = function changeDirectory() {
$scope.changeDirectory(file);
};
}
// Initiate downloads when normal files are clicked
else if ($scope.isNormalFile(file)) {
element.addClass('normal-file');
fileAction = function downloadFile() {
$scope.downloadFile(file);
};
}
// Mark file as focused upon click
element.on('click', function handleFileClick() {
// Fire file-specific action if already focused
if (element.hasClass('focused')) {
fileAction();
element.removeClass('focused');
}
// Otherwise mark as focused
else {
element.parent().children().removeClass('focused');
element.addClass('focused');
}
});
// Prevent text selection during navigation
element.on('selectstart', function avoidSelect(e) {
e.preventDefault();
e.stopPropagation();
});
return element;
};
/**
* Sorts the given map of files, returning an array of those files
* grouped by file type (directories first, followed by non-
* directories) and sorted lexicographically.
*
* @param {Object.<String, ManagedFilesystem.File>} files
* The map of files to sort.
*
* @returns {ManagedFilesystem.File[]}
* An array of all files in the given map, sorted
* lexicographically with directories first, followed by non-
* directories.
*/
var sortFiles = function sortFiles(files) {
// Get all given files as an array
var unsortedFiles = [];
for (var name in files)
unsortedFiles.push(files[name]);
// Sort files - directories first, followed by all other files
// sorted by name
return unsortedFiles.sort(function fileComparator(a, b) {
// Directories come before non-directories
if ($scope.isDirectory(a) && !$scope.isDirectory(b))
return -1;
// Non-directories come after directories
if (!$scope.isDirectory(a) && $scope.isDirectory(b))
return 1;
// All other combinations are sorted by name
return a.name.localeCompare(b.name);
});
};
// Watch directory contents once file template is available
$templateRequest('app/client/templates/file.html').then(function fileTemplateRetrieved(html) {
// Store file template statically
fileTemplate = html;
// Update the contents of the file browser whenever the current directory (or its contents) changes
$scope.$watch('filesystem.currentDirectory.files', function currentDirectoryChanged(files) {
// Clear current content
currentDirectoryContents.html('');
// Display all files within current directory, sorted
angular.forEach(sortFiles(files), function displayFile(file) {
currentDirectoryContents.append(createFileElement(file));
});
});
}, angular.noop); // end retrieve file template
// Refresh file browser when any upload completes
$scope.$on('guacUploadComplete', function uploadComplete(event, filename) {
// Refresh filesystem, if it exists
if ($scope.filesystem)
ManagedFilesystem.refresh($scope.filesystem, $scope.filesystem.currentDirectory);
});
}]
};
}]);

View File

@@ -0,0 +1,216 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Directive which displays an active file transfer, providing links for
* downloads, if applicable.
*/
angular.module('client').directive('guacFileTransfer', [function guacFileTransfer() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The file transfer to display.
*
* @type ManagedFileUpload|ManagedFileDownload
*/
transfer : '='
},
templateUrl: 'app/client/templates/guacFileTransfer.html',
controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) {
// Required services
const guacTranslate = $injector.get('guacTranslate');
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Returns the unit string that is most appropriate for the
* number of bytes transferred thus far - either 'gb', 'mb', 'kb',
* or 'b'.
*
* @returns {String}
* The unit string that is most appropriate for the number of
* bytes transferred thus far.
*/
$scope.getProgressUnit = function getProgressUnit() {
var bytes = $scope.transfer.progress;
// Gigabytes
if (bytes > 1000000000)
return 'gb';
// Megabytes
if (bytes > 1000000)
return 'mb';
// Kilobytes
if (bytes > 1000)
return 'kb';
// Bytes
return 'b';
};
/**
* Returns the amount of data transferred thus far, in the units
* returned by getProgressUnit().
*
* @returns {Number}
* The amount of data transferred thus far, in the units
* returned by getProgressUnit().
*/
$scope.getProgressValue = function getProgressValue() {
var bytes = $scope.transfer.progress;
if (!bytes)
return bytes;
// Convert bytes to necessary units
switch ($scope.getProgressUnit()) {
// Gigabytes
case 'gb':
return (bytes / 1000000000).toFixed(1);
// Megabytes
case 'mb':
return (bytes / 1000000).toFixed(1);
// Kilobytes
case 'kb':
return (bytes / 1000).toFixed(1);
// Bytes
case 'b':
default:
return bytes;
}
};
/**
* Returns the percentage of bytes transferred thus far, if the
* overall length of the file is known.
*
* @returns {Number}
* The percentage of bytes transferred thus far, if the
* overall length of the file is known.
*/
$scope.getPercentDone = function getPercentDone() {
return $scope.transfer.progress / $scope.transfer.length * 100;
};
/**
* Determines whether the associated file transfer is in progress.
*
* @returns {Boolean}
* true if the file transfer is in progress, false othherwise.
*/
$scope.isInProgress = function isInProgress() {
// Not in progress if there is no transfer
if (!$scope.transfer)
return false;
// Determine in-progress status based on stream state
switch ($scope.transfer.transferState.streamState) {
// IDLE or OPEN file transfers are active
case ManagedFileTransferState.StreamState.IDLE:
case ManagedFileTransferState.StreamState.OPEN:
return true;
// All others are not active
default:
return false;
}
};
/**
* Returns whether the file associated with this file transfer can
* be saved locally via a call to save().
*
* @returns {Boolean}
* true if a call to save() will result in the file being
* saved, false otherwise.
*/
$scope.isSavable = function isSavable() {
return !!$scope.transfer.blob;
};
/**
* Saves the downloaded file, if any. If this transfer is an upload
* or the download is not yet complete, this function has no
* effect.
*/
$scope.save = function save() {
// Ignore if no blob exists
if (!$scope.transfer.blob)
return;
// Save file
saveAs($scope.transfer.blob, $scope.transfer.filename);
};
/**
* Returns whether an error has occurred. If an error has occurred,
* the transfer is no longer active, and the text of the error can
* be read from getErrorText().
*
* @returns {Boolean}
* true if an error has occurred during transfer, false
* otherwise.
*/
$scope.hasError = function hasError() {
return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR;
};
// The translated error message for the current status code
$scope.translatedErrorMessage = '';
$scope.$watch('transfer.transferState.statusCode', function statusCodeChanged(statusCode) {
// Determine translation name of error
const errorName = 'CLIENT.ERROR_UPLOAD_' + statusCode.toString(16).toUpperCase();
// Use translation string, or the default if no translation is found for this error code
guacTranslate(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').then(
translationResult => $scope.translatedErrorMessage = translationResult.message
);
});
}] // end file transfer controller
};
}]);

View File

@@ -0,0 +1,105 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Directive which displays all active file transfers.
*/
angular.module('client').directive('guacFileTransferManager', [function guacFileTransferManager() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The client group whose file transfers should be managed by this
* directive.
*
* @type ManagedClientGroup
*/
clientGroup : '='
},
templateUrl: 'app/client/templates/guacFileTransferManager.html',
controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
const ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Determines whether the given file transfer state indicates an
* in-progress transfer.
*
* @param {ManagedFileTransferState} transferState
* The file transfer state to check.
*
* @returns {Boolean}
* true if the given file transfer state indicates an in-
* progress transfer, false otherwise.
*/
var isInProgress = function isInProgress(transferState) {
switch (transferState.streamState) {
// IDLE or OPEN file transfers are active
case ManagedFileTransferState.StreamState.IDLE:
case ManagedFileTransferState.StreamState.OPEN:
return true;
// All others are not active
default:
return false;
}
};
/**
* Removes all file transfers which are not currently in-progress.
*/
$scope.clearCompletedTransfers = function clearCompletedTransfers() {
// Nothing to clear if no client group attached
if (!$scope.clientGroup)
return;
// Remove completed uploads
$scope.clientGroup.clients.forEach(client => {
client.uploads = client.uploads.filter(function isUploadInProgress(upload) {
return isInProgress(upload.transferState);
});
});
};
/**
* @borrows ManagedClientGroup.hasMultipleClients
*/
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
/**
* @borrows ManagedClient.hasTransfers
*/
$scope.hasTransfers = ManagedClient.hasTransfers;
}]
};
}]);

View File

@@ -0,0 +1,120 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying a Guacamole client as a non-interactive
* thumbnail.
*/
angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The client to display within this guacThumbnail directive.
*
* @type ManagedClient
*/
client : '='
},
templateUrl: 'app/client/templates/guacThumbnail.html',
controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) {
// Required services
var $window = $injector.get('$window');
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
var display = null;
/**
* The element associated with the display of the current
* Guacamole client instance.
*
* @type Element
*/
var displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
var displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
var main = $element[0];
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
$scope.updateDisplayScale = function updateDisplayScale() {
if (!display) return;
// Fit within available area
display.scale(Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
));
};
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Remove any existing display
displayContainer.innerHTML = "";
// Only proceed if a client is given
if (!managedClient)
return;
// Get Guacamole client instance
var client = managedClient.client;
// Attach possibly new display
display = client.getDisplay();
// Add display element
displayElement = display.getElement();
displayContainer.appendChild(displayElement);
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) {
$scope.$evalAsync($scope.updateDisplayScale);
});
}]
};
}]);

View File

@@ -0,0 +1,184 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which displays one or more Guacamole clients in an evenly-tiled
* view. The number of rows and columns used for the arrangement of tiles is
* automatically determined by the number of clients present.
*/
angular.module('client').directive('guacTiledClients', [function guacTiledClients() {
const directive = {
restrict: 'E',
templateUrl: 'app/client/templates/guacTiledClients.html',
};
directive.scope = {
/**
* The function to invoke when the "close" button in the header of a
* client tile is clicked. The ManagedClient that is closed will be
* made available to the Angular expression defining the callback as
* "$client".
*
* @type function
*/
onClose : '&',
/**
* The group of Guacamole clients that should be displayed in an
* evenly-tiled grid arrangement.
*
* @type ManagedClientGroup
*/
clientGroup : '=',
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type boolean
*/
emulateAbsoluteMouse : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacTiledClientsController($scope, $injector, $element) {
// Required services
const $rootScope = $injector.get('$rootScope');
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
/**
* Returns the currently-focused ManagedClient. If there is no such
* client, or multiple clients are focused, null is returned.
*
* @returns {ManagedClient}
* The currently-focused client, or null if there are no focused
* clients or if multiple clients are focused.
*/
$scope.getFocusedClient = function getFocusedClient() {
const managedClientGroup = $scope.clientGroup;
if (managedClientGroup) {
const focusedClients = _.filter(managedClientGroup.clients, client => client.clientProperties.focused);
if (focusedClients.length === 1)
return focusedClients[0];
}
return null;
};
// Notify whenever identify of currently-focused client changes
$scope.$watch('getFocusedClient()', function focusedClientChanged(focusedClient) {
$rootScope.$broadcast('guacClientFocused', focusedClient);
});
// Notify whenever arguments of currently-focused client changes
$scope.$watch('getFocusedClient().arguments', function focusedClientParametersChanged() {
$rootScope.$broadcast('guacClientArgumentsUpdated', $scope.getFocusedClient());
}, true);
// Notify whenever protocol of currently-focused client changes
$scope.$watch('getFocusedClient().protocol', function focusedClientParametersChanged() {
$rootScope.$broadcast('guacClientProtocolUpdated', $scope.getFocusedClient());
}, true);
/**
* Returns a callback for guacClick that assigns or updates keyboard
* focus to the given client, allowing that client to receive and
* handle keyboard events. Multiple clients may have keyboard focus
* simultaneously.
*
* @param {ManagedClient} client
* The client that should receive keyboard focus.
*
* @return {guacClick~callback}
* The callback that guacClient should invoke when the given client
* has been clicked.
*/
$scope.getFocusAssignmentCallback = function getFocusAssignmentCallback(client) {
return (shift, ctrl) => {
// Clear focus of all other clients if not selecting multiple
if (!shift && !ctrl) {
$scope.clientGroup.clients.forEach(client => {
client.clientProperties.focused = false;
});
}
client.clientProperties.focused = true;
// Fill in any gaps if performing rectangular multi-selection
// via shift-click
if (shift) {
let minRow = $scope.clientGroup.rows - 1;
let minColumn = $scope.clientGroup.columns - 1;
let maxRow = 0;
let maxColumn = 0;
// Determine extents of selected area
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
if (client.clientProperties.focused) {
minRow = Math.min(minRow, row);
minColumn = Math.min(minColumn, column);
maxRow = Math.max(maxRow, row);
maxColumn = Math.max(maxColumn, column);
}
});
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
client.clientProperties.focused =
row >= minRow
&& row <= maxRow
&& column >= minColumn
&& column <= maxColumn;
});
}
};
};
/**
* @borrows ManagedClientGroup.hasMultipleClients
*/
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
/**
* @borrows ManagedClientGroup.getClientGrid
*/
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
/**
* @borrows ManagedClient.isShared
*/
$scope.isShared = ManagedClient.isShared;
}];
return directive;
}]);

View File

@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying a group of Guacamole clients as a non-interactive
* thumbnail of tiled client displays.
*/
angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() {
const directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacTiledThumbnails.html'
};
directive.scope = {
/**
* The group of clients to display as a thumbnail of tiled client
* displays.
*
* @type ManagedClientGroup
*/
clientGroup : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacTiledThumbnailsController($scope, $injector, $element) {
// Required types
const ManagedClientGroup = $injector.get('ManagedClientGroup');
/**
* The overall height of the thumbnail view of the tiled grid of
* clients within the client group, in pixels. This value is
* intentionally based off a snapshot of the current browser size at
* the time the directive comes into existence to ensure the contents
* of the thumbnail are familiar in appearance and aspect ratio.
*/
$scope.height = Math.min(window.innerHeight, 128);
/**
* The overall width of the thumbnail view of the tiled grid of
* clients within the client group, in pixels. This value is
* intentionally based off a snapshot of the current browser size at
* the time the directive comes into existence to ensure the contents
* of the thumbnail are familiar in appearance and aspect ratio.
*/
$scope.width = window.innerWidth / window.innerHeight * $scope.height;
/**
* @borrows ManagedClientGroup.getClientGrid
*/
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
}];
return directive;
}]);

View File

@@ -0,0 +1,112 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which provides a fullscreen environment for its content.
*/
angular.module('client').directive('guacViewport', [function guacViewport() {
return {
// Element only
restrict: 'E',
scope: {},
transclude: true,
templateUrl: 'app/client/templates/guacViewport.html',
controller: ['$scope', '$injector', '$element',
function guacViewportController($scope, $injector, $element) {
// Required services
var $window = $injector.get('$window');
/**
* The fullscreen container element.
*
* @type Element
*/
var element = $element.find('.viewport')[0];
/**
* The width of the browser viewport when fitVisibleArea() was last
* invoked, in pixels, or null if fitVisibleArea() has not yet been
* called.
*
* @type Number
*/
var lastViewportWidth = null;
/**
* The height of the browser viewport when fitVisibleArea() was
* last invoked, in pixels, or null if fitVisibleArea() has not yet
* been called.
*
* @type Number
*/
var lastViewportHeight = null;
/**
* Resizes the container element inside the guacViewport such that
* it exactly fits within the visible area, even if the browser has
* been scrolled.
*/
var fitVisibleArea = function fitVisibleArea() {
// Calculate viewport dimensions (this is NOT necessarily the
// same as 100vw and 100vh, 100%, etc., particularly when the
// on-screen keyboard of a mobile device pops open)
var viewportWidth = $window.innerWidth;
var viewportHeight = $window.innerHeight;
// Adjust element width to fit exactly within visible area
if (viewportWidth !== lastViewportWidth) {
element.style.width = viewportWidth + 'px';
lastViewportWidth = viewportWidth;
}
// Adjust element height to fit exactly within visible area
if (viewportHeight !== lastViewportHeight) {
element.style.height = viewportHeight + 'px';
lastViewportHeight = viewportHeight;
}
// Scroll element such that its upper-left corner is exactly
// within the viewport upper-left corner, if not already there
if (element.scrollLeft || element.scrollTop) {
$window.scrollTo(
$window.pageXOffset + element.scrollLeft,
$window.pageYOffset + element.scrollTop
);
}
};
// Fit container within visible region when window scrolls
$window.addEventListener('scroll', fitVisibleArea);
// Poll every 10ms, in case scroll event does not fire
var pollArea = $window.setInterval(fitVisibleArea, 10);
// Clean up on destruction
$scope.$on('$destroy', function destroyViewport() {
$window.removeEventListener('scroll', fitVisibleArea);
$window.clearInterval(pollArea);
});
}]
};
}]);

View File

@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which converts between human-readable zoom
* percentage and display scale.
*/
angular.module('client').directive('guacZoomCtrl', function guacZoomCtrl() {
return {
restrict: 'A',
require: 'ngModel',
priority: 101,
link: function(scope, element, attrs, ngModel) {
// Evaluate the ngChange attribute when the model
// changes.
ngModel.$viewChangeListeners.push(function() {
scope.$eval(attrs.ngChange);
});
// When pushing to the menu, mutiply by 100.
ngModel.$formatters.push(function(value) {
return Math.round(value * 100);
});
// When parsing value from menu, divide by 100.
ngModel.$parsers.push(function(value) {
return Math.round(value) / 100;
});
}
}
});

View File

@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* 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() {
/**
* Array of all supported audio mimetypes.
*
* @type String[]
*/
this.supported = Guacamole.AudioPlayer.getSupportedTypes();
})();
}]);

View File

@@ -0,0 +1,328 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for managing several active Guacamole clients.
*/
angular.module('client').factory('guacClientManager', ['$injector',
function guacClientManager($injector) {
// Required types
const ManagedClient = $injector.get('ManagedClient');
const ManagedClientGroup = $injector.get('ManagedClientGroup');
// Required services
const $window = $injector.get('$window');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {};
/**
* Getter/setter which retrieves or sets the map of all active managed
* clients. Each key is the ID of the connection used by that client.
*
* @type Function
*/
var storedManagedClients = sessionStorageFactory.create({}, function destroyClientStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns a map of all active managed clients. Each key is the ID of the
* connection used by that client.
*
* @returns {Object.<String, ManagedClient>}
* A map of all active managed clients.
*/
service.getManagedClients = function getManagedClients() {
return storedManagedClients();
};
/**
* Getter/setter which retrieves or sets the array of all active managed
* client groups.
*
* @type Function
*/
const storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns an array of all managed client groups.
*
* @returns {ManagedClientGroup[]>}
* An array of all active managed client groups.
*/
service.getManagedClientGroups = function getManagedClientGroups() {
return storedManagedClientGroups();
};
/**
* Removes the ManagedClient with the given ID from all
* ManagedClientGroups, automatically adjusting the tile size of the
* clients that remain in each group. All client groups that are empty
* after the client is removed will also be removed.
*
* @param {string} id
* The ID of the ManagedClient to remove.
*/
const ungroupManagedClient = function ungroupManagedClient(id) {
const managedClientGroups = storedManagedClientGroups();
// Remove client from all groups
managedClientGroups.forEach(group => {
const removed = _.remove(group.clients, client => (client.id === id));
if (removed.length) {
// Reset focus state if client is being removed from a group
// that isn't currently attached (focus may otherwise be
// retained and result in a newly added connection unexpectedly
// sharing focus)
if (!group.attached)
removed.forEach(client => { client.clientProperties.focused = false; });
// Recalculate group grid if number of clients is changing
ManagedClientGroup.recalculateTiles(group);
}
});
// Remove any groups that are now empty
_.remove(managedClientGroups, group => !group.clients.length);
};
/**
* Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this
* function has no effect.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be removed.
*
* @returns {Boolean}
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function removeManagedClient(id) {
var managedClients = storedManagedClients();
// Remove client if it exists
if (id in managedClients) {
// Pull client out of any containing groups
ungroupManagedClient(id);
// Disconnect and remove
managedClients[id].client.disconnect();
delete managedClients[id];
// A client was removed
return true;
}
// No client was removed
return false;
};
/**
* 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.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.replaceManagedClient = function replaceManagedClient(id) {
const managedClients = storedManagedClients();
const managedClientGroups = storedManagedClientGroups();
// Remove client if it exists
if (id in managedClients) {
const hadFocus = managedClients[id].clientProperties.focused;
managedClients[id].client.disconnect();
delete managedClients[id];
// Remove client from all groups
managedClientGroups.forEach(group => {
const index = _.findIndex(group.clients, client => (client.id === id));
if (index === -1)
return;
group.clients[index] = managedClients[id] = ManagedClient.getInstance(id);
managedClients[id].clientProperties.focused = hadFocus;
});
}
return managedClients[id];
};
/**
* 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.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.getManagedClient = function getManagedClient(id) {
var managedClients = storedManagedClients();
// Ensure any existing client is removed from its containing group
// prior to being returned
ungroupManagedClient(id);
// Create new managed client if it doesn't already exist
if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id);
// Return existing client
return managedClients[id];
};
/**
* Returns the ManagedClientGroup having the given ID. If no such
* ManagedClientGroup exists, a new ManagedClientGroup is created by
* extracting the relevant connections from the ID.
*
* @param {String} id
* The ID of the ManagedClientGroup to retrieve or create.
*
* @returns {ManagedClientGroup}
* The ManagedClientGroup having the given ID.
*/
service.getManagedClientGroup = function getManagedClientGroup(id) {
const managedClientGroups = storedManagedClientGroups();
const existingGroup = _.find(managedClientGroups, (group) => {
return id === ManagedClientGroup.getIdentifier(group);
});
// Prefer to return the existing group if it exactly matches
if (existingGroup)
return existingGroup;
const clients = [];
const clientIds = ManagedClientGroup.getClientIdentifiers(id);
// Separate active clients by whether they should be displayed within
// the current view
clientIds.forEach(function groupClients(id) {
clients.push(service.getManagedClient(id));
});
const group = new ManagedClientGroup({
clients : clients
});
// Focus the first client if there are no clients focused
ManagedClientGroup.verifyFocus(group);
managedClientGroups.push(group);
return group;
};
/**
* Removes the existing ManagedClientGroup having the given ID, if any,
* disconnecting and removing all ManagedClients associated with that
* group. If no such a ManagedClientGroup currently exists, this function
* has no effect.
*
* @param {String} id
* The ID of the ManagedClientGroup to remove.
*
* @returns {Boolean}
* true if a ManagedClientGroup was removed, false otherwise.
*/
service.removeManagedClientGroup = function removeManagedClientGroup(id) {
const managedClients = storedManagedClients();
const managedClientGroups = storedManagedClientGroups();
// Remove all matching groups (there SHOULD only be one)
const removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id);
// Disconnect all clients associated with the removed group(s)
removed.forEach((group) => {
group.clients.forEach((client) => {
const id = client.id;
if (managedClients[id]) {
managedClients[id].client.disconnect();
delete managedClients[id];
}
});
});
return !!removed.length;
};
/**
* Disconnects and removes all currently-connected clients and client
* groups.
*/
service.clear = function clear() {
var managedClients = storedManagedClients();
// Disconnect each managed client
for (var id in managedClients)
managedClients[id].client.disconnect();
// Clear managed clients and client groups
storedManagedClients({});
storedManagedClientGroups([]);
};
// Disconnect all clients when window is unloaded
$window.addEventListener('unload', service.clear);
return service;
}]);

View File

@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for providing true fullscreen and keyboard lock support.
* Keyboard lock is currently only supported by Chromium based browsers
* (Edge >= V79, Chrome >= V68 and Opera >= V55)
*/
angular.module('client').factory('guacFullscreen', ['$injector',
function guacFullscreen($injector) {
var service = {};
// check is browser in true fullscreen mode
service.isInFullscreenMode = function isInFullscreenMode() {
return document.fullscreenElement;
}
// set fullscreen mode
service.setFullscreenMode = function setFullscreenMode(state) {
if (document.fullscreenEnabled) {
if (state && !service.isInFullscreenMode())
document.documentElement.requestFullscreen().then(navigator.keyboard.lock());
else if (!state && service.isInFullscreenMode())
document.exitFullscreen().then(navigator.keyboard.unlock());
}
}
// toggles current fullscreen mode (off if on, on if off)
service.toggleFullscreenMode = function toggleFullscreenMode() {
if (!service.isInFullscreenMode())
service.setFullscreenMode(true);
else
service.setFullscreenMode(false);
}
return service;
}]);

View File

@@ -0,0 +1,135 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for checking browser image support.
*/
angular.module('client').factory('guacImage', ['$injector', function guacImage($injector) {
// Required services
var $q = $injector.get('$q');
var service = {};
/**
* Map of possibly-supported image mimetypes to corresponding test images
* encoded with base64. If the image is correctly decoded, it will be a
* single pixel (1x1) image.
*
* @type Object.<String, String>
*/
var testImages = {
/**
* Test JPEG image, encoded as base64.
*/
'image/jpeg' :
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH'
+ 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME'
+ 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU'
+ 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA'
+ 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA'
+ 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==',
/**
* Test PNG image, encoded as base64.
*/
'image/png' :
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI'
+ 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==',
/**
* Test WebP image, encoded as base64.
*/
'image/webp' : 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='
};
/**
* Deferred which tracks the progress and ultimate result of all pending
* image format tests.
*
* @type Deferred
*/
var deferredSupportedMimetypes = $q.defer();
/**
* Array of all promises associated with pending image tests. Each image
* test promise MUST be guaranteed to resolve and MUST NOT be rejected.
*
* @type Promise[]
*/
var pendingTests = [];
/**
* The array of supported image formats. This will be gradually populated
* by the various image tests that occur in the background, and will not be
* fully populated until all promises within pendingTests are resolved.
*
* @type String[]
*/
var supported = [];
/**
* Return a promise which resolves with to an array of image mimetypes
* supported by the browser, once those mimetypes are known. The returned
* promise is guaranteed to resolve successfully.
*
* @returns {Promise.<String[]>}
* A promise which resolves with an array of image mimetypes supported
* by the browser.
*/
service.getSupportedMimetypes = function getSupportedMimetypes() {
return deferredSupportedMimetypes.promise;
};
// Test each possibly-supported image
angular.forEach(testImages, function testImageSupport(data, mimetype) {
// Add promise for current image test
var imageTest = $q.defer();
pendingTests.push(imageTest.promise);
// Attempt to load image
var image = new Image();
image.src = 'data:' + mimetype + ';base64,' + data;
// Store as supported depending on whether load was successful
image.onload = image.onerror = function imageTestComplete() {
// Image format is supported if successfully decoded
if (image.width === 1 && image.height === 1)
supported.push(mimetype);
// Test is complete
imageTest.resolve();
};
});
// When all image tests are complete, resolve promise with list of
// supported formats
$q.all(pendingTests).then(function imageTestsCompleted() {
deferredSupportedMimetypes.resolve(supported);
});
return service;
}]);

View File

@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A wrapper around the angular-translate $translate service that offers a
* convenient way to fall back to a default translation if the requested
* translation is not available.
*/
angular.module('client').factory('guacTranslate', ['$injector', function guacTranslate($injector) {
// Required services
const $q = $injector.get('$q');
const $translate = $injector.get('$translate');
// Required types
const TranslationResult = $injector.get('TranslationResult');
/**
* Returns a promise that will be resolved with a TranslationResult containg either the
* requested ID and message (if translated), or the default ID and message if translated,
* or the literal value of `defaultTranslationId` for both the ID and message if neither
* is translated.
*
* @param {String} translationId
* The requested translation ID, which may or may not be translated.
*
* @param {Sting} defaultTranslationId
* The translation ID that will be used if no translation is found for `translationId`.
*
* @returns {Promise.<TranslationResult>}
* A promise which resolves with a TranslationResult containing the results from
* the translation attempt.
*/
var translateWithFallback = function translateWithFallback(translationId, defaultTranslationId) {
const deferredTranslation = $q.defer();
// Attempt to translate the requested translation ID
$translate(translationId).then(
// If the requested translation is available, use that
translation => deferredTranslation.resolve(new TranslationResult({
id: translationId, message: translation
})),
// Otherwise, try the default translation ID
() => $translate(defaultTranslationId).then(
// Default translation worked, so use that
defaultTranslation =>
deferredTranslation.resolve(new TranslationResult({
id: defaultTranslationId, message: defaultTranslation
})),
// Neither translation is available; as a fallback, return default ID for both
() => deferredTranslation.resolve(new TranslationResult({
id: defaultTranslationId, message: defaultTranslationId
})),
)
);
return deferredTranslation.promise;
};
return translateWithFallback;
}]);

View File

@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* 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() {
/**
* Array of all supported video mimetypes.
*/
this.supported = Guacamole.VideoPlayer.getSupportedTypes();
})();
}]);

View File

@@ -0,0 +1,137 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
body.client {
background: black;
padding: 0;
margin: 0;
overflow: hidden;
}
#preload {
visibility: hidden;
position: absolute;
left: 0;
right: 0;
width: 0;
height: 0;
overflow: hidden;
}
.client-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-size: 0px;
}
.client-view-content {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
-ms-flex-pack: end;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
-moz-box-pack: end;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
-webkit-box-pack: end;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
-webkit-flex-pack: end;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
flex-pack: end;
width: 100%;
height: 100%;
font-size: 12pt;
}
.client-view .client-body {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
position: relative;
}
.client-view .client-bottom {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.client-view .client-body guac-tiled-clients {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: auto;
height: auto;
}
.client .menu .header h2 {
text-transform: none;
}
.client .user-menu .menu-contents li a.disconnect {
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
background-image: url('images/x.svg');
}
.client .drop-pending .display {
background: #3161a9;
}
.client .drop-pending .display > *{
opacity: 0.5;
}

View File

@@ -0,0 +1,62 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#guac-menu .header h2.connection-select-menu {
overflow: visible;
}
.connection-select-menu {
padding: 0;
min-width: 0;
}
.connection-select-menu .menu-dropdown {
border: none;
}
.connection-select-menu .menu-dropdown .menu-contents {
font-weight: normal;
font-size: 0.8em;
right: auto;
left: 0;
max-width: 100vw;
width: 400px;
}
.connection-select-menu .menu-dropdown .menu-contents .filter input {
border-bottom: 1px solid rgba(0,0,0,0.125);
border-left: none;
}
.connection-select-menu .menu-dropdown .menu-contents .filter {
margin-bottom: 0.5em;
padding: 0;
}
.connection-select-menu .menu-dropdown .menu-contents .group-list .caption {
display: inline-block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.connection-select-menu .menu-dropdown .menu-contents .caption .connection,
.connection-select-menu .menu-dropdown .menu-contents .caption .connection-group {
display: inline-block;
}

View File

@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#connection-warning {
position: absolute;
right: 0.25em;
bottom: 0.25em;
z-index: 20;
width: 3in;
max-width: 100%;
min-height: 1em;
border-left: 2em solid #FA0;
box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
background: #FFE;
padding: 0.5em 0.75em;
font-size: .8em;
}
#connection-warning::before {
content: ' ';
display: block;
position: absolute;
left: -2em;
top: 0;
width: 1.25em;
height: 100%;
margin: 0 0.375em;
background: url('images/warning.svg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}

View File

@@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.software-cursor {
cursor: url('images/mouse/blank.gif'),url('images/mouse/blank.cur'),default;
overflow: hidden;
cursor: none;
}
.guac-error .software-cursor {
cursor: default;
}
div.main {
overflow: auto;
width: 100%;
height: 100%;
position: relative;
font-size: 0px;
}
div.displayOuter {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
display: table;
}
div.displayMiddle {
width: 100%;
height: 100%;
display: table-cell;
vertical-align: middle;
text-align: center;
}
div.display {
display: inline-block;
}
div.display * {
position: relative;
}
div.display > * {
margin-left: auto;
margin-right: auto;
}

View File

@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* Hide directory contents by default */
.file-browser .directory > .children {
padding-left: 1em;
display: none;
}
.file-browser .list-item .caption {
white-space: nowrap;
border: 1px solid transparent;
}
.file-browser .list-item.focused .caption {
border: 1px dotted rgba(0, 0, 0, 0.5);
background: rgba(204, 221, 170, 0.5);
}
/* Directory / file icons */
.file-browser .normal-file > .caption .icon {
background-image: url('images/file.svg');
}
.file-browser .directory > .caption .icon {
background-image: url('images/folder-closed.svg');
}
.file-browser .directory.previous > .caption .icon {
background-image: url('images/folder-up.svg');
}

View File

@@ -0,0 +1,118 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#file-transfer-dialog {
position: absolute;
right: 0;
bottom: 0;
z-index: 20;
font-size: 0.8em;
width: 4in;
max-width: 100%;
max-height: 3in;
}
#file-transfer-dialog .transfer-manager {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
max-width: inherit;
max-height: inherit;
border: 1px solid rgba(0, 0, 0, 0.5);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
}
#file-transfer-dialog .transfer-manager .header {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
#file-transfer-dialog .transfer-manager .transfer-manager-body {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
overflow: auto;
}
/*
* Shrink maximum height if viewport is too small for default 3in dialog.
*/
@media all and (max-height: 3in) {
#file-transfer-dialog {
max-height: 1.5in;
}
}
/*
* If viewport is too small for even the 1.5in dialog, fit all available space.
*/
@media all and (max-height: 1.5in) {
#file-transfer-dialog {
height: 100%;
}
#file-transfer-dialog .transfer-manager {
position: absolute;
left: 0.5em;
top: 0.5em;
right: 0.5em;
bottom: 0.5em;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#filesystem-menu .header h2 {
font-size: 1em;
font-weight: normal;
padding-top: 0;
padding-bottom: 0;
}
#filesystem-menu .header {
-ms-flex-align: center;
-moz-box-align: center;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
}
#filesystem-menu .menu-body {
padding: 0.25em;
}
#filesystem-menu .header.breadcrumbs {
display: block;
background: rgba(0,0,0,0.0125);
border-bottom: 1px solid rgba(0,0,0,0.05);
box-shadow: none;
margin-top: 0;
border-top: none;
}
#filesystem-menu .header.breadcrumbs .breadcrumb {
display: inline-block;
padding: 0.5em;
font-size: 0.8em;
font-weight: bold;
}
#filesystem-menu .header.breadcrumbs .breadcrumb:hover {
background-color: #CDA;
cursor: pointer;
}
#filesystem-menu .header.breadcrumbs .breadcrumb.root {
background-size: 1.5em 1.5em;
-moz-background-size: 1.5em 1.5em;
-webkit-background-size: 1.5em 1.5em;
-khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat;
background-position: center center;
background-image: url('images/drive.svg');
width: 2em;
height: 2em;
padding: 0;
vertical-align: middle;
}

View File

@@ -0,0 +1,170 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#guac-menu .content {
padding: 0;
margin: 0;
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
}
#guac-menu .content > * {
margin: 0;
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
#guac-menu .content > * + * {
margin-top: 1em;
}
#guac-menu .header h2 {
white-space: nowrap;
overflow: hidden;
width: 100%;
text-overflow: ellipsis;
}
#guac-menu #mouse-settings .choice {
text-align: center;
}
#guac-menu #mouse-settings .choice .figure {
display: inline-block;
vertical-align: middle;
width: 75%;
max-width: 320px;
}
#guac-menu #keyboard-settings .caption {
font-size: 0.9em;
margin-left: 2em;
margin-right: 2em;
}
#guac-menu #mouse-settings .figure .caption {
text-align: center;
font-size: 0.9em;
}
#guac-menu #mouse-settings .figure img {
display: block;
width: 100%;
max-width: 320px;
margin: 1em auto;
}
#guac-menu #keyboard-settings .figure {
float: right;
max-width: 30%;
margin: 1em;
}
#guac-menu #keyboard-settings .figure img {
width: 100%;
}
#guac-menu #zoom-settings {
text-align: center;
}
#guac-menu #devices .device {
padding: 1em;
border: 1px solid rgba(0, 0, 0, 0.125);
background: rgba(0, 0, 0, 0.04);
padding-left: 3.5em;
background-size: 1.5em 1.5em;
-moz-background-size: 1.5em 1.5em;
-webkit-background-size: 1.5em 1.5em;
-khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat;
background-position: 1em center;
}
#guac-menu #devices .device:hover {
cursor: pointer;
border-color: black;
}
#guac-menu #devices .device.filesystem {
background-image: url('images/drive.svg');
}
#guac-menu #share-links {
padding: 1em;
border: 1px solid rgba(0, 0, 0, 0.125);
background: rgba(0, 0, 0, 0.04);
font-size: 0.8em;
}
#guac-menu #share-links h3 {
padding-bottom: 0;
}
#guac-menu #share-links th {
white-space: nowrap;
}
#guac-menu #share-links a[href] {
display: block;
padding: 0 1em;
font-family: monospace;
font-weight: bold;
}

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.keyboard-container {
text-align: center;
width: 100%;
margin: 0;
padding: 0;
border-top: 1px solid black;
background: #222;
opacity: 0.85;
z-index: 1;
}

View File

@@ -0,0 +1,146 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.menu {
overflow: hidden;
position: absolute;
top: 0;
height: 100%;
max-width: 100%;
width: 480px;
background: #EEE;
box-shadow: inset -1px 0 2px white, 1px 0 2px black;
z-index: 100;
-webkit-transition: left 0.125s, opacity 0.125s;
-moz-transition: left 0.125s, opacity 0.125s;
-ms-transition: left 0.125s, opacity 0.125s;
-o-transition: left 0.125s, opacity 0.125s;
transition: left 0.125s, opacity 0.125s;
}
.menu-content {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
width: 100%;
height: 100%;
}
.menu-content .header {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
margin-bottom: 0;
}
.menu-body {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding: 1em;
overflow: auto;
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
}
.menu-body > * {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.menu-section h3 {
margin: 0;
padding: 0;
padding-bottom: 1em;
}
.menu-section ~ .menu-section h3 {
padding-top: 1em;
}
.menu,
.menu.closed {
left: -480px;
opacity: 0;
}
.menu.open {
left: 0px;
opacity: 1;
}

View File

@@ -0,0 +1,95 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.client-status-modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: rgba(0, 0, 0, 0.5);
}
.client-status-modal.shown {
display: block;
}
.client-status-modal guac-modal {
position: absolute;
}
.client-status-modal .notification {
background: rgba(40, 40, 40, 0.75);
color: white;
width: 100%;
max-width: 100%;
padding: 1em;
text-align: center;
border: none;
}
.client-status-modal .notification.error {
background: rgba(112, 9, 8, 0.75)
}
.client-status-modal .notification .title-bar {
display: none
}
.client-status-modal .notification .button {
background: transparent;
border: 2px solid white;
box-shadow: none;
text-shadow: none;
font-weight: normal;
}
.client-status-modal .notification .button:hover {
text-decoration: underline;
background: rgba(255, 255, 255, 0.25);
}
.client-status-modal .notification .button:active {
background: rgba(255, 255, 255, 0.5);
}
.client-status-modal .notification .parameters {
width: 100%;
max-width: 5in;
margin: 0 auto;
}
.client-status-modal .notification .parameters h3,
.client-status-modal .notification .parameters .password-field .toggle-password {
display: none;
}
.client-status-modal .notification .parameters input[type=email],
.client-status-modal .notification .parameters input[type=number],
.client-status-modal .notification .parameters input[type=password],
.client-status-modal .notification .parameters input[type=text],
.client-status-modal .notification .parameters textarea {
background: transparent;
border: 2px solid white;
color: white;
}

View File

@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.share-menu {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: row;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: horizontal;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: horizontal;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: row;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: row;
}
.share-menu .menu-dropdown .menu-title {
padding-left: 2em;
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/share.svg');
}

View File

@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
div.thumbnail-main {
overflow: hidden;
width: 100%;
height: 100%;
position: relative;
font-size: 0px;
}
.thumbnail-main .display {
pointer-events: none;
}

View File

@@ -0,0 +1,277 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Overall tiled grid layout.
*/
.tiled-client-grid {
width: 100%;
height: 100%;
}
.tiled-client-grid,
.tiled-client-grid .tiled-client-row,
.tiled-client-grid .tiled-client-cell,
.tiled-client-grid .client-tile {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.tiled-client-grid {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.tiled-client-grid .tiled-client-row {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
/*
* Rendering of individual clients within tiles.
*/
.tiled-client-grid .client-tile {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
line-height: 1.5;
}
.tiled-client-grid .client-tile .client-tile-header {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
margin: 0;
background: #444;
padding: 0 0.25em;
font-size: 0.8em;
color: white;
z-index: 30;
min-height: 1.5em;
}
.tiled-client-grid .client-tile.focused .client-tile-header {
background-color: #3161a9;
}
.tiled-client-grid .client-tile .client-tile-header > * {
-webkit-box-flex: 0;
-webkit-flex: 0;
-ms-flex: 0;
flex: 0;
}
.tiled-client-grid .client-tile .client-tile-header .client-tile-name {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
padding: 0 0.5em;
margin-bottom: -0.125em;
}
.tiled-client-grid .client-tile .main {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.tiled-client-grid .client-tile-disconnect,
.tiled-client-grid .client-tile-shared-indicator {
max-height: 1em;
height: 100%;
}
.tiled-client-grid .client-tile-shared-indicator {
display: none;
}
.tiled-client-grid .shared .client-tile-shared-indicator {
display: inline;
}
.tiled-client-grid .client-user-count {
visibility: hidden;
display: block;
position: absolute;
right: 0;
top: 0;
z-index: 1;
border-radius: 0.25em;
padding: 0.125em 0.75em;
margin: 0.5em;
background: #055;
color: white;
font-weight: bold;
font-size: 0.8em;
}
.tiled-client-grid .client-user-count::before {
content: ' ';
display: inline-block;
margin-bottom: -0.2em;
padding-right: 0.25em;
width: 1em;
height: 1em;
background: center / contain no-repeat url('images/user-icons/guac-user-white.svg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
.tiled-client-grid .client-user-count .client-user-count-users,
.tiled-client-grid .client-user-count .client-user-count-messages {
position: absolute;
right: 0;
margin: 0;
padding: 0;
margin-top: 0.5em;
list-style: none;
}
.tiled-client-grid .client-user-count .client-user-count-users,
.tiled-client-grid .client-user-count .client-user-count-message {
border-radius: 0.25em;
background: black;
color: white;
padding: 0.5em;
}
.tiled-client-grid .client-user-count .client-user-count-message {
white-space: nowrap;
animation: 1s linear 3s fadeout;
}
.tiled-client-grid .client-tile-header .client-user-count {
display: inline-block;
position: relative;
white-space: nowrap;
background: black;
padding-left: 0.5em;
padding-right: 0.75em;
}
.tiled-client-grid .client-tile-header .client-user-count::before {
padding-right: 0.75em;
}
.tiled-client-grid .joined .client-user-count {
visibility: visible;
}
.tiled-client-grid .client-user-count .client-user-count-users {
display: none;
}
.tiled-client-grid .client-user-count:hover .client-user-count-users {
display: block;
}
.tiled-client-grid .client-user-count .client-user-count-user::after {
content: ', ';
margin-right: 0.25em;
}
.tiled-client-grid .client-user-count .client-user-count-user:last-child::after {
content: none;
}
.tiled-client-grid .client-user-count .client-user-count-user {
display: inline-block;
}
.tiled-client-grid .client-user-count .client-user-count-user.anonymous {
font-style: italic;
opacity: 0.5;
}
.tiled-client-grid .client-user-count .client-user-count-users {
width: 256px;
max-width: 75vw;
white-space: normal;
border: 1px solid #333;
}
.tiled-client-grid .client-user-count .client-user-count-users::before {
content: ' ';
display: block;
position: absolute;
right: 0.5em;
top: -0.5em;
width: 1em;
height: 1em;
background: black;
border: 1px solid #333;
border-right: none;
border-bottom: none;
transform: rotate(45deg);
}

View File

@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.transfer-manager {
background: white;
}
.transfer-manager .header h2 {
font-size: 1em;
padding-top: 0;
padding-bottom: 0;
}
.transfer-manager .header {
margin: 0;
-ms-flex-align: center;
-moz-box-align: center;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
}
.transfer-manager h3 {
margin: 0.25em;
font-size: 1em;
margin-bottom: 0;
opacity: 0.5;
text-align: center;
}
.transfer-manager .transfers {
display: table;
padding: 0.25em;
width: 100%;
}

View File

@@ -0,0 +1,132 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.transfer {
display: table-row;
}
.transfer .transfer-status {
display: table-cell;
padding: 0.25em;
position: relative;
}
.transfer .text {
display: table-cell;
text-align: right;
padding: 0.25em
}
.transfer .filename {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
font-family: monospace;
font-weight: bold;
padding: 0.125em;
}
@keyframes transfer-progress {
from {background-position: 0px 0px;}
to {background-position: 64px 0px;}
}
@-webkit-keyframes transfer-progress {
from {background-position: 0px 0px;}
to {background-position: 64px 0px;}
}
.transfer .progress {
width: 100%;
padding: 0.25em;
position: absolute;
top: 0;
left: 0;
bottom: 0;
opacity: 0.25;
}
.transfer.in-progress .progress {
background-color: #EEE;
background-image: url('images/progress.svg');
background-size: 16px 16px;
-moz-background-size: 16px 16px;
-webkit-background-size: 16px 16px;
-khtml-background-size: 16px 16px;
animation-name: transfer-progress;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
-webkit-animation-name: transfer-progress;
-webkit-animation-duration: 2s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
}
.transfer .progress .bar {
display: none;
background: #A3D655;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0;
}
.transfer.in-progress .progress .bar {
display: initial;
}
.transfer.savable {
cursor: pointer;
}
.transfer.savable .filename {
color: blue;
text-decoration: underline;
}
.transfer.error {
background: #FDD;
}
.transfer.error .text,
.transfer.error .progress .bar {
display: none;
}
.transfer .error-text {
display: none;
}
.transfer.error .error-text {
display: block;
margin: 0;
margin-top: 0.5em;
width: 100%;
}

View File

@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.viewport {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.client-zoom .client-zoom-out,
.client-zoom .client-zoom-in,
.client-zoom .client-zoom-state {
display: inline-block;
vertical-align: middle;
}
.client-zoom .client-zoom-out,
.client-zoom .client-zoom-in {
max-width: 3em;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.1);
border-radius: 2em;
margin: 0.5em;
cursor: pointer;
}
.client-zoom .client-zoom-out img,
.client-zoom .client-zoom-in img {
width: 100%;
opacity: 0.5;
}
.client-zoom .client-zoom-out:hover,
.client-zoom .client-zoom-in:hover {
border: 1px solid rgba(0, 0, 0, 1);
background: #CDA;
}
.client-zoom .client-zoom-out:hover img,
.client-zoom .client-zoom-in:hover img {
opacity: 1;
}
.client-zoom .client-zoom-state {
font-size: 1.5em;
}
.client-zoom .client-zoom-autofit {
text-align: left;
margin-top: 1em;
}
.client-zoom .client-zoom-state input {
width: 2em;
font-size: 1em;
padding: 0;
background: transparent;
border-color: rgba(0, 0, 0, 0.125);
}
.client-zoom .client-zoom-state input::-webkit-inner-spin-button,
.client-zoom .client-zoom-state input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@@ -0,0 +1,228 @@
<guac-viewport>
<!-- Client view -->
<div class="client-view">
<div class="client-view-content">
<!-- Central portion of view -->
<div class="client-body" guac-touch-drag="menuDrag">
<!-- All connections in current display -->
<guac-tiled-clients
on-close="closeClientTile($client)"
client-group="clientGroup"
emulate-absolute-mouse="menu.emulateAbsoluteMouse">
</guac-tiled-clients>
</div>
<!-- Bottom portion of view -->
<div class="client-bottom">
<!-- Text input -->
<div class="text-input-container" ng-if="showTextInput">
<guac-text-input></guac-text-input>
</div>
<!-- On-screen keyboard -->
<div class="keyboard-container" ng-if="showOSK">
<guac-osk layout="'CLIENT.URL_OSK_LAYOUT' | translate"></guac-osk>
</div>
</div>
</div>
</div>
<!-- File transfers -->
<div id="file-transfer-dialog" ng-show="hasTransfers()">
<guac-file-transfer-manager client-group="clientGroup"></guac-file-transfer-manager>
</div>
<!-- Connection stability warning -->
<div id="connection-warning" ng-show="isConnectionUnstable()">
{{'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | translate}}
</div>
<!-- Menu -->
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
<div class="menu-content" ng-if="menu.shown" guac-touch-drag="menuDrag">
<!-- Stationary header -->
<div class="header">
<h2 ng-hide="rootConnectionGroups">{{ getName(clientGroup) }}</h2>
<h2 class="connection-select-menu" ng-show="rootConnectionGroups">
<guac-menu menu-title="getName(clientGroup)" interactive="true">
<div class="all-connections">
<guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups"
placeholder="'CLIENT.FIELD_PLACEHOLDER_FILTER' | translate"
connection-properties="filteredConnectionProperties"
connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
<guac-group-list
connection-groups="filteredRootConnectionGroups"
context="connectionListContext"
templates="{
'connection' : 'app/client/templates/connection.html',
'connection-group' : 'app/client/templates/connectionGroup.html'
}"
page-size="10"></guac-group-list>
</div>
</guac-menu>
</h2>
<div class="share-menu" ng-show="canShareConnection()">
<guac-menu menu-title="'CLIENT.ACTION_SHARE' | translate">
<ul ng-repeat="sharingProfile in sharingProfiles">
<li><a ng-click="share(sharingProfile)">{{sharingProfile.name}}</a></li>
</ul>
</guac-menu>
</div>
<guac-user-menu local-actions="clientMenuActions"></guac-user-menu>
</div>
<!-- Scrollable body -->
<div class="menu-body" guac-touch-drag="visibleMenuDrag" guac-scroll="menu.scrollState">
<!-- Connection sharing -->
<div class="menu-section" id="share-links" ng-show="isShared()">
<div class="content">
<h3>{{'CLIENT.INFO_CONNECTION_SHARED' | translate}}</h3>
<p class="description"
translate="CLIENT.HELP_SHARE_LINK"
translate-values="{LINKS : getShareLinkCount()}"></p>
<table>
<tr ng-repeat="link in focusedClient.shareLinks | toArray | orderBy: value.name">
<th>{{link.value.name}}</th>
<td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td>
</tr>
</table>
</div>
</div>
<!-- Clipboard -->
<div class="menu-section" id="clipboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<guac-clipboard></guac-clipboard>
</div>
</div>
<!-- Devices -->
<div class="menu-section" id="devices" ng-if="focusedClient.filesystems.length">
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content">
<div class="device filesystem" ng-repeat="filesystem in focusedClient.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
</div>
</div>
</div>
<!-- Connection parameters which may be modified while the connection is open -->
<div class="menu-section connection-parameters" id="connection-settings" ng-if="focusedClient.protocol">
<guac-form namespace="getProtocolNamespace(focusedClient.protocol)"
content="focusedClient.forms"
model="menu.connectionParameters"
client="focusedClient"
model-only="true"></guac-form>
</div>
<!-- Input method -->
<div class="menu-section" id="keyboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
<div class="content">
<!-- No IME -->
<div class="choice">
<label><input id="ime-none" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="none"> {{'CLIENT.NAME_INPUT_METHOD_NONE' | translate}}</label>
<p class="caption"><label for="ime-none">{{'CLIENT.HELP_INPUT_METHOD_NONE' | translate}}</label></p>
</div>
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.svg" alt=""></label></div>
<label><input id="ime-text" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="text"> {{'CLIENT.NAME_INPUT_METHOD_TEXT' | translate}}</label>
<p class="caption"><label for="ime-text">{{'CLIENT.HELP_INPUT_METHOD_TEXT' | translate}} </label></p>
</div>
<!-- Guac OSK -->
<div class="choice">
<label><input id="ime-osk" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="osk"> {{'CLIENT.NAME_INPUT_METHOD_OSK' | translate}}</label>
<p class="caption"><label for="ime-osk">{{'CLIENT.HELP_INPUT_METHOD_OSK' | translate}}</label></p>
</div>
</div>
</div>
<!-- Mouse mode -->
<div class="menu-section" id="mouse-settings">
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div>
</div>
</div>
</div>
<!-- Display options -->
<div class="menu-section" id="display-settings" ng-if="focusedClient">
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content">
<div id="zoom-settings">
<guac-client-zoom client="focusedClient"></guac-client-zoom>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filesystem menu -->
<div id="filesystem-menu" class="menu" ng-class="{open: isFilesystemMenuShown()}">
<div class="menu-content">
<!-- Stationary header -->
<div class="header">
<h2>{{filesystemMenuContents.name}}</h2>
<button class="upload button" guac-upload="uploadFiles">{{'CLIENT.ACTION_UPLOAD_FILES' | translate}}</button>
<button class="back" ng-click="hideFilesystemMenu()">{{'CLIENT.ACTION_NAVIGATE_BACK' | translate}}</button>
</div>
<!-- Breadcrumbs -->
<div class="header breadcrumbs"><div
class="breadcrumb root"
ng-click="changeDirectory(filesystemMenuContents, filesystemMenuContents.root)"></div><div
class="breadcrumb"
ng-repeat="file in getPath(filesystemMenuContents.currentDirectory)"
ng-click="changeDirectory(filesystemMenuContents, file)">{{file.name}}</div>
</div>
<!-- Scrollable body -->
<div class="menu-body">
<guac-file-browser client="client" filesystem="filesystemMenuContents"></guac-file-browser>
</div>
</div>
</div>
</guac-viewport>

View File

@@ -0,0 +1,9 @@
<div class="connection-select-menu-connection connection">
<input type="checkbox"
ng-model="context.attachedClients[item.getClientIdentifier()]"
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
<a ng-href="{{ item.getClientURL() }}">
<div class="icon type" ng-class="item.protocol"></div>
<span class="name">{{item.name}}</span>
</a>
</div>

View File

@@ -0,0 +1,10 @@
<div class="connection-select-menu-connection-group connection-group">
<input type="checkbox"
ng-show="item.balancing"
ng-model="context.attachedClients[item.getClientIdentifier()]"
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
<a ng-href="{{ item.getClientURL() }}">
<div ng-show="item.balancing" class="icon type balancer"></div>
<span class="name">{{item.name}}</span>
</a>
</div>

View File

@@ -0,0 +1,9 @@
<div class="file-browser-file list-item">
<!-- Filename and icon -->
<div class="caption">
<div class="icon"></div>
{{::name}}
</div>
</div>

View File

@@ -0,0 +1,17 @@
<div class="client-main main"
ng-class="{ 'drop-pending': dropPending }"
guac-resize="mainElementResized"
guac-touch-drag="clientDrag"
guac-touch-pinch="clientPinch">
<!-- Display -->
<div class="displayOuter">
<div class="displayMiddle">
<div class="display software-cursor">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<div class="client-status-modal" ng-class="{ shown: status }">
<guac-modal>
<guac-notification notification="status"></guac-notification>
</guac-modal>
</div>

View File

@@ -0,0 +1,30 @@
<div class="client-panel"
ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
<!-- Toggle panel visibility -->
<div class="client-panel-handle" ng-click="togglePanel()"></div>
<!-- List of connection thumbnails -->
<ul class="client-panel-connection-list">
<li ng-repeat="clientGroup in clientGroups | orderBy: '-lastUsed'"
ng-if="!clientGroup.attached"
ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
class="client-panel-connection">
<!-- Close connection -->
<button class="close-other-connection" ng-click="disconnect(clientGroup)">
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.svg">
</button>
<!-- Thumbnail -->
<a href="#/client/{{ getIdentifier(clientGroup) }}">
<guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
<div class="name">{{ getTitle(clientGroup) }}</div>
</a>
</li>
</ul>
</div>

View File

@@ -0,0 +1,11 @@
<div class="client-user-count" title="{{ instance }}">
<span class="client-user-count-value">{{ client.userCount }}</span>
<ul class="client-user-count-messages"></ul>
<ul class="client-user-count-users">
<li class="client-user-count-user"
ng-repeat="user in userCounts | toArray | orderBy: key"
ng-class="{ anonymous : isAnonymous(user.key) }"
translate="{{ getUserCountTranslationKey(user.key) }}"
translate-values="{ USERNAME : user.key, COUNT : user.value }"></li>
</ul>
</div>

View File

@@ -0,0 +1,18 @@
<div class="client-zoom">
<div class="client-zoom-editor">
<div ng-click="zoomOut()" class="client-zoom-out"><img src="images/settings/zoom-out.svg" alt="-"></div>
<div class="client-zoom-state">
<input type="number" guac-zoom-ctrl
ng-model="client.clientProperties.scale"
ng-model-options="{ updateOn: 'blur submit' }"
ng-change="zoomSet()">%
</div>
<div ng-click="zoomIn()" class="client-zoom-in"><img src="images/settings/zoom-in.svg" alt="+"></div>
</div>
<div class="client-zoom-autofit">
<label><input ng-model="client.clientProperties.autoFit"
ng-change="changeAutoFit()"
ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit">
{{'CLIENT.TEXT_ZOOM_AUTO_FIT' | translate}}</label>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<div class="file-browser">
<!-- Current directory contents -->
<div class="current-directory-contents"></div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="transfer" ng-class="{'in-progress': isInProgress(), 'savable': isSavable(), 'error': hasError()}" ng-click="save()">
<!-- Overall status of transfer -->
<div class="transfer-status">
<!-- Filename and progress bar -->
<div class="filename">
<div class="progress"><div ng-style="{'width': getPercentDone() + '%'}" class="bar"></div></div>
{{transfer.filename}}
</div>
<!-- Error text -->
<p class="error-text">{{translatedErrorMessage}}</p>
</div>
<!-- Progress/status text -->
<div class="text"
translate="CLIENT.TEXT_FILE_TRANSFER_PROGRESS"
translate-values="{PROGRESS: getProgressValue(), UNIT: getProgressUnit()}"></div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="transfer-manager">
<!-- File transfer manager header -->
<div class="header">
<h2>{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}</h2>
<button ng-click="clearCompletedTransfers()">{{'CLIENT.ACTION_CLEAR_COMPLETED_TRANSFERS' | translate}}</button>
</div>
<!-- Sent/received files -->
<div class="transfer-manager-body">
<div class="transfer-manager-body-section" ng-repeat="client in clientGroup.clients" ng-show="hasTransfers(client)">
<h3 ng-show="hasMultipleClients(clientGroup)">{{ client.name }}</h3>
<div class="transfers">
<guac-file-transfer
transfer="upload"
ng-repeat="upload in client.uploads">
</guac-file-transfer>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<div class="thumbnail-main" guac-resize="updateDisplayScale">
<!-- Display -->
<div class="displayOuter">
<div class="displayMiddle">
<div class="display">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,33 @@
<div class="tiled-client-grid">
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
<div class="tiled-client-cell" ng-repeat="client in clientRow">
<div class="client-tile" ng-if="client"
ng-class="{
'focused' : client.clientProperties.focused,
'shared' : isShared(client),
'joined' : client.userCount
}"
guac-click="getFocusAssignmentCallback(client)">
<h3 class="client-tile-header" ng-if="hasMultipleClients(clientGroup)">
<img class="client-tile-shared-indicator" src="images/share-white.svg">
<span class="client-tile-name">{{ client.title }}</span>
<guac-client-user-count client="client"></guac-client-user-count>
<img ng-click="onClose({ '$client' : client })"
class="client-tile-disconnect"
ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.svg">
</h3>
<guac-client client="client" emulate-absolute-mouse="emulateAbsoluteMouse"></guac-client>
<!-- Client-specific status/error dialog -->
<guac-client-notification client="client"></guac-client-notification>
<guac-client-user-count client="client" ng-if="!hasMultipleClients(clientGroup)"></guac-client-user-count>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
<div class="tiled-client-grid" ng-style="{
'width' : width + 'px',
'height' : height + 'px',
}">
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
<div class="tiled-client-cell" ng-repeat="client in clientRow">
<div class="client-tile" ng-if="client">
<guac-thumbnail client="client"></guac-thumbnail>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<div class="viewport" ng-transclude>
</div>

View File

@@ -0,0 +1,95 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for generating new guacClient properties objects.
*/
angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
/**
* Object used for interacting with a guacClient directive.
*
* @constructor
* @param {ClientProperties|Object} [template={}]
* The object whose properties should be copied within the new
* ClientProperties.
*/
var ClientProperties = function ClientProperties(template) {
// Use empty object by default
template = template || {};
/**
* Whether the display should be scaled automatically to fit within the
* available space.
*
* @type Boolean
*/
this.autoFit = template.autoFit || true;
/**
* The current scale. If autoFit is true, the effect of setting this
* value is undefined.
*
* @type Number
*/
this.scale = template.scale || 1;
/**
* The minimum scale value.
*
* @type Number
*/
this.minScale = template.minScale || 1;
/**
* The maximum scale value.
*
* @type Number
*/
this.maxScale = template.maxScale || 3;
/**
* Whether this client should receive keyboard events.
*
* @type Boolean
*/
this.focused = template.focused || false;
/**
* The relative Y coordinate of the scroll offset of the display within
* the client element.
*
* @type Number
*/
this.scrollTop = template.scrollTop || 0;
/**
* The relative X coordinate of the scroll offset of the display within
* the client element.
*
* @type Number
*/
this.scrollLeft = template.scrollLeft || 0;
};
return ClientProperties;
}]);

View File

@@ -0,0 +1,153 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedArgument class used by ManagedClient.
*/
angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
/**
* Object which represents an argument (connection parameter) which may be
* changed by the user while the connection is open.
*
* @constructor
* @param {ManagedArgument|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedArgument.
*/
var ManagedArgument = function ManagedArgument(template) {
// Use empty object by default
template = template || {};
/**
* The name of the connection parameter.
*
* @type {String}
*/
this.name = template.name;
/**
* The current value of the connection parameter.
*
* @type {String}
*/
this.value = template.value;
/**
* A valid, open output stream which may be used to apply a new value
* to the connection parameter.
*
* @type {Guacamole.OutputStream}
*/
this.stream = template.stream;
/**
* True if this argument has been modified in the webapp, but yet to
* be confirmed by guacd, or false in any other case. A pending
* argument cannot be modified again, and must be recreated before
* editing is enabled again.
*
* @type {boolean}
*/
this.pending = false;
};
/**
* Requests editable access to a given connection parameter, returning a
* promise which is resolved with a ManagedArgument instance that provides
* such access if the parameter is indeed editable.
*
* @param {ManagedClient} managedClient
* The ManagedClient instance associated with the connection for which
* an editable version of the connection parameter is being retrieved.
*
* @param {String} name
* The name of the connection parameter.
*
* @param {String} value
* The current value of the connection parameter, as received from a
* prior, inbound "argv" stream.
*
* @returns {Promise.<ManagedArgument>}
* A promise which is resolved with the new ManagedArgument instance
* once the requested parameter has been verified as editable.
*/
ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
var deferred = $q.defer();
// Create internal, fully-populated instance of ManagedArgument, to be
// returned only once mutability of the associated connection parameter
// has been verified
var managedArgument = new ManagedArgument({
name : name,
value : value,
stream : managedClient.client.createArgumentValueStream('text/plain', name)
});
// The connection parameter is editable only if a successful "ack" is
// received
managedArgument.stream.onack = function ackReceived(status) {
if (status.isError())
deferred.reject(status);
else
deferred.resolve(managedArgument);
};
return deferred.promise;
};
/**
* Sets the given editable argument (connection parameter) to the given
* value, updating the behavior of the associated connection in real-time.
* If successful, the ManagedArgument provided cannot be used for future
* calls to setValue() and will be read-only until replaced with a new
* instance. This function only has an effect if the new parameter value
* is different from the current value.
*
* @param {ManagedArgument} managedArgument
* The ManagedArgument instance associated with the connection
* parameter being modified.
*
* @param {String} value
* The new value to assign to the connection parameter.
*/
ManagedArgument.setValue = function setValue(managedArgument, value) {
// Stream new value only if value has changed and a change is not
// already pending
if (!managedArgument.pending && value !== managedArgument.value) {
var writer = new Guacamole.StringWriter(managedArgument.stream);
writer.sendText(value);
writer.sendEnd();
// ManagedArgument instance is no longer usable
managedArgument.pending = true;
}
};
return ManagedArgument;
}]);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,359 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedClientGroup class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClientGroup', ['$injector', function defineManagedClientGroup($injector) {
/**
* Object which serves as a grouping of ManagedClients. Each
* ManagedClientGroup may be attached, detached, and reattached dynamically
* from different client views, with its contents automatically displayed
* in a tiled arrangment if needed.
*
* @constructor
* @param {ManagedClientGroup|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientGroup.
*/
const ManagedClientGroup = function ManagedClientGroup(template) {
// Use empty object by default
template = template || {};
/**
* The time that this group was last brought to the foreground of
* the current tab, as the number of milliseconds elapsed since
* midnight of January 1, 1970 UTC. If the group has not yet been
* viewed, this will be 0.
*
* @type Number
*/
this.lastUsed = template.lastUsed || 0;
/**
* Whether this ManagedClientGroup is currently attached to the client
* interface (true) or is running in the background (false).
*
* @type {boolean}
* @default false
*/
this.attached = template.attached || false;
/**
* The clients that should be displayed within the client interface
* when this group is attached.
*
* @type {ManagedClient[]}
* @default []
*/
this.clients = template.clients || [];
/**
* The number of rows that should be used when arranging the clients
* within this group in a grid. By default, this value is automatically
* calculated from the number of clients.
*
* @type {number}
*/
this.rows = template.rows || ManagedClientGroup.getRows(this);
/**
* The number of columns that should be used when arranging the clients
* within this group in a grid. By default, this value is automatically
* calculated from the number of clients.
*
* @type {number}
*/
this.columns = template.columns || ManagedClientGroup.getColumns(this);
};
/**
* Updates the number of rows and columns stored within the given
* ManagedClientGroup such that the clients within the group are evenly
* distributed. This function should be called whenever the size of a
* group changes.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup that should be updated.
*/
ManagedClientGroup.recalculateTiles = function recalculateTiles(group) {
const recalculated = new ManagedClientGroup({
clients : group.clients
});
group.rows = recalculated.rows;
group.columns = recalculated.columns;
};
/**
* Returns the unique ID representing the given ManagedClientGroup or set
* of client IDs. The ID of a ManagedClientGroup consists simply of the
* IDs of all its ManagedClients, separated by periods.
*
* @param {ManagedClientGroup|string[]} group
* The ManagedClientGroup or array of client IDs to determine the
* ManagedClientGroup ID of.
*
* @returns {string}
* The unique ID representing the given ManagedClientGroup, or the
* unique ID that would represent a ManagedClientGroup containing the
* clients with the given IDs.
*/
ManagedClientGroup.getIdentifier = function getIdentifier(group) {
if (!_.isArray(group))
group = _.map(group.clients, client => client.id);
return group.join('.');
};
/**
* Returns an array of client identifiers for all clients contained within
* the given ManagedClientGroup. Order of the identifiers is preserved
* with respect to the order of the clients within the group.
*
* @param {ManagedClientGroup|string} group
* The ManagedClientGroup to retrieve the client identifiers from,
* or its ID.
*
* @returns {string[]}
* The client identifiers of all clients contained within the given
* ManagedClientGroup.
*/
ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) {
if (_.isString(group))
return group.split(/\./);
return group.clients.map(client => client.id);
};
/**
* Returns the number of columns that should be used to evenly arrange
* all provided clients in a tiled grid.
*
* @returns {Number}
* The number of columns that should be used for the grid of
* clients.
*/
ManagedClientGroup.getColumns = function getColumns(group) {
if (!group.clients.length)
return 0;
return Math.ceil(Math.sqrt(group.clients.length));
};
/**
* Returns the number of rows that should be used to evenly arrange all
* provided clients in a tiled grid.
*
* @returns {Number}
* The number of rows that should be used for the grid of clients.
*/
ManagedClientGroup.getRows = function getRows(group) {
if (!group.clients.length)
return 0;
return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group));
};
/**
* Returns the title which should be displayed as the page title if the
* given client group is attached to the interface.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the title of.
*
* @returns {string}
* The title of the given ManagedClientGroup.
*/
ManagedClientGroup.getTitle = function getTitle(group) {
// Use client-specific title if only one client
if (group.clients.length === 1)
return group.clients[0].title;
// With multiple clients, somehow combining multiple page titles would
// be confusing. Instead, use the combined names.
return ManagedClientGroup.getName(group);
};
/**
* Returns the combined names of all clients within the given
* ManagedClientGroup, as determined by the names of the associated
* connections or connection groups.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the name of.
*
* @returns {string}
* The combined names of all clients within the given
* ManagedClientGroup.
*/
ManagedClientGroup.getName = function getName(group) {
// Generate a name from ONLY the focused clients, unless there are no
// focused clients
let relevantClients = _.filter(group.clients, client => client.clientProperties.focused);
if (!relevantClients.length)
relevantClients = group.clients;
return _.filter(relevantClients, (client => !!client.name)).map(client => client.name).join(', ') || '...';
};
/**
* A callback that is invoked for a ManagedClient within a ManagedClientGroup.
*
* @callback ManagedClientGroup~clientCallback
* @param {ManagedClient} client
* The relevant ManagedClient.
*
* @param {number} row
* The row number of the client within the tiled grid, where 0 is the
* first row.
*
* @param {number} column
* The column number of the client within the tiled grid, where 0 is
* the first column.
*
* @param {number} index
* The index of the client within the relevant
* {@link ManagedClientGroup#clients} array.
*/
/**
* Loops through each of the clients associated with the given
* ManagedClientGroup, invoking the given callback for each client.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to loop through.
*
* @param {ManagedClientGroup~clientCallback} callback
* The callback to invoke for each of the clients within the given
* ManagedClientGroup.
*/
ManagedClientGroup.forEach = function forEach(group, callback) {
let current = 0;
for (let row = 0; row < group.rows; row++) {
for (let column = 0; column < group.columns; column++) {
callback(group.clients[current], row, column, current);
current++;
if (current >= group.clients.length)
return;
}
}
};
/**
* Returns whether the given ManagedClientGroup contains more than one
* client.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to test.
*
* @returns {boolean}
* true if two or more clients are currently present in the given
* group, false otherwise.
*/
ManagedClientGroup.hasMultipleClients = function hasMultipleClients(group) {
return group && group.clients.length > 1;
};
/**
* Returns a two-dimensional array of all ManagedClients within the given
* group, arranged in the grid defined by {@link ManagedClientGroup#rows}
* and {@link ManagedClientGroup#columns}. If any grid cell lacks a
* corresponding client (because the number of clients does not divide
* evenly into a grid), that cell will be null.
*
* For the sake of AngularJS scope watches, the results of calling this
* function are cached and will always favor modifying an existing array
* over creating a new array, even for nested arrays.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup defining the tiled grid arrangement of
* ManagedClients.
*
* @returns {ManagedClient[][]}
* A two-dimensional array of all ManagedClients within the given
* group.
*/
ManagedClientGroup.getClientGrid = function getClientGrid(group) {
let index = 0;
// Operate on cached copy of grid
const clientGrid = group._grid || (group._grid = []);
// Delete any rows in excess of the required size
clientGrid.splice(group.rows);
for (let row = 0; row < group.rows; row++) {
// Prefer to use existing column arrays, deleting any columns in
// excess of the required size
const currentRow = clientGrid[row] || (clientGrid[row] = []);
currentRow.splice(group.columns);
for (let column = 0; column < group.columns; column++) {
currentRow[column] = group.clients[index++] || null;
}
}
return clientGrid;
};
/**
* Verifies that focus is assigned to at least one client in the given
* group. If no client has focus, focus is assigned to the first client in
* the group.
*
* @param {ManagedClientGroup} group
* The group to verify.
*/
ManagedClientGroup.verifyFocus = function verifyFocus(group) {
// Focus the first client if there are no clients focused
if (group.clients.length >= 1 && _.findIndex(group.clients, client => client.clientProperties.focused) === -1) {
group.clients[0].clientProperties.focused = true;
}
};
return ManagedClientGroup;
}]);

View File

@@ -0,0 +1,185 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the 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;
/**
* Whether the network connection used by the tunnel seems unstable. If
* the network connection is unstable, the remote desktop connection
* may perform poorly or disconnect.
*
* @type Boolean
* @default false
*/
this.tunnelUnstable = template.tunnelUnstable || false;
/**
* 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. If the
* client state was previously marked as unstable, that flag is implicitly
* cleared.
*
* @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;
clientState.tunnelUnstable = false;
// Set status code, if given
if (statusCode)
clientState.statusCode = statusCode;
};
/**
* Updates the given client state, setting whether the underlying tunnel
* is currently unstable. An unstable tunnel is not necessarily
* disconnected, but appears to be misbehaving and may be disconnected.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
*
* @param {Boolean} unstable
* Whether the underlying tunnel of the connection currently appears
* unstable.
*/
ManagedClientState.setTunnelUnstable = function setTunnelUnstable(clientState, unstable) {
clientState.tunnelUnstable = unstable;
};
return ManagedClientState;
}]);

View File

@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedClientThumbnail class used by ManagedClient.
*/
angular.module('client').factory('ManagedClientThumbnail', [function defineManagedClientThumbnail() {
/**
* Object which represents a thumbnail of the Guacamole client display,
* along with the time that the thumbnail was generated.
*
* @constructor
* @param {ManagedClientThumbnail|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientThumbnail.
*/
var ManagedClientThumbnail = function ManagedClientThumbnail(template) {
// Use empty object by default
template = template || {};
/**
* The time that this thumbnail was generated, as the number of
* milliseconds elapsed since midnight of January 1, 1970 UTC.
*
* @type Number
*/
this.timestamp = template.timestamp;
/**
* The thumbnail of the Guacamole client display.
*
* @type HTMLCanvasElement
*/
this.canvas = template.canvas;
};
return ManagedClientThumbnail;
}]);

View File

@@ -0,0 +1,174 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedDisplay class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedDisplay', ['$rootScope',
function defineManagedDisplay($rootScope) {
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* display while it is active, allowing it to be detached and reattached
* from different client views.
*
* @constructor
* @param {ManagedDisplay|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedDisplay.
*/
var ManagedDisplay = function ManagedDisplay(template) {
// Use empty object by default
template = template || {};
/**
* The underlying Guacamole display.
*
* @type Guacamole.Display
*/
this.display = template.display;
/**
* The current size of the Guacamole display.
*
* @type ManagedDisplay.Dimensions
*/
this.size = new ManagedDisplay.Dimensions(template.size);
/**
* The current mouse cursor, if any.
*
* @type ManagedDisplay.Cursor
*/
this.cursor = template.cursor;
};
/**
* Object which represents the size of the Guacamole display.
*
* @constructor
* @param {ManagedDisplay.Dimensions|Object} template
* The object whose properties should be copied within the new
* ManagedDisplay.Dimensions.
*/
ManagedDisplay.Dimensions = function Dimensions(template) {
// Use empty object by default
template = template || {};
/**
* The current width of the Guacamole display, in pixels.
*
* @type Number
*/
this.width = template.width || 0;
/**
* The current width of the Guacamole display, in pixels.
*
* @type Number
*/
this.height = template.height || 0;
};
/**
* Object which represents a mouse cursor used by the Guacamole display.
*
* @constructor
* @param {ManagedDisplay.Cursor|Object} template
* The object whose properties should be copied within the new
* ManagedDisplay.Cursor.
*/
ManagedDisplay.Cursor = function Cursor(template) {
// Use empty object by default
template = template || {};
/**
* The actual mouse cursor image.
*
* @type HTMLCanvasElement
*/
this.canvas = template.canvas;
/**
* The X coordinate of the cursor hotspot.
*
* @type Number
*/
this.x = template.x;
/**
* The Y coordinate of the cursor hotspot.
*
* @type Number
*/
this.y = template.y;
};
/**
* Creates a new ManagedDisplay which represents the current state of the
* given Guacamole display.
*
* @param {Guacamole.Display} display
* The Guacamole display to represent. Changes to this display will
* affect this ManagedDisplay.
*
* @returns {ManagedDisplay}
* A new ManagedDisplay which represents the current state of the
* given Guacamole display.
*/
ManagedDisplay.getInstance = function getInstance(display) {
var managedDisplay = new ManagedDisplay({
display : display
});
// Store changes to display size
display.onresize = function setClientSize() {
$rootScope.$apply(function updateClientSize() {
managedDisplay.size = new ManagedDisplay.Dimensions({
width : display.getWidth(),
height : display.getHeight()
});
});
};
// Store changes to display cursor
display.oncursor = function setClientCursor(canvas, x, y) {
$rootScope.$apply(function updateClientCursor() {
managedDisplay.cursor = new ManagedDisplay.Cursor({
canvas : canvas,
x : x,
y : y
});
});
};
return managedDisplay;
};
return ManagedDisplay;
}]);

View File

@@ -0,0 +1,133 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedFileTransferState class used by the guacClientManager
* service.
*/
angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() {
/**
* Object which represents the state of a Guacamole stream, including any
* error conditions.
*
* @constructor
* @param {ManagedFileTransferState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileTransferState.
*/
var ManagedFileTransferState = function ManagedFileTransferState(template) {
// Use empty object by default
template = template || {};
/**
* The current stream state. Valid values are described by
* ManagedFileTransferState.StreamState.
*
* @type String
* @default ManagedFileTransferState.StreamState.IDLE
*/
this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE;
/**
* The status code of the current error condition, if streamState
* is ERROR. For all other streamState 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 stream state strings. Each state string is associated with a
* specific state of a Guacamole stream.
*/
ManagedFileTransferState.StreamState = {
/**
* The stream has not yet been opened.
*
* @type String
*/
IDLE : "IDLE",
/**
* The stream has been successfully established. Data can be sent or
* received.
*
* @type String
*/
OPEN : "OPEN",
/**
* The stream has terminated successfully. No errors are indicated.
*
* @type String
*/
CLOSED : "CLOSED",
/**
* The stream has terminated due to an error. The associated error code
* is stored in statusCode.
*
* @type String
*/
ERROR : "ERROR"
};
/**
* Sets the current transfer state and, if given, the associated status
* code. If an error is already represented, this function has no effect.
*
* @param {ManagedFileTransferState} transferState
* The ManagedFileTransferState to update.
*
* @param {String} streamState
* The stream state to assign to the given ManagedFileTransferState, as
* listed within ManagedFileTransferState.StreamState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedFileTransferState, if
* any, as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedFileTransferState is not
* touched.
*/
ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) {
// Do not set state after an error is registered
if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR)
return;
// Update stream state
transferState.streamState = streamState;
// Set status code, if given
if (statusCode)
transferState.statusCode = statusCode;
};
return ManagedFileTransferState;
}]);

View File

@@ -0,0 +1,208 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedFileUpload class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector',
function defineManagedFileUpload($rootScope, $injector) {
// Required types
var Error = $injector.get('Error');
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
// Required services
var requestService = $injector.get('requestService');
var tunnelService = $injector.get('tunnelService');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* file upload while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFileUpload|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileUpload.
*/
var ManagedFileUpload = function ManagedFileUpload(template) {
// Use empty object by default
template = template || {};
/**
* The current state of the file transfer stream.
*
* @type ManagedFileTransferState
*/
this.transferState = template.transferState || new ManagedFileTransferState();
/**
* The mimetype of the file being transferred.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The filename of the file being transferred.
*
* @type String
*/
this.filename = template.filename;
/**
* The number of bytes transferred so far.
*
* @type Number
*/
this.progress = template.progress;
/**
* The total number of bytes in the file.
*
* @type Number
*/
this.length = template.length;
};
/**
* Creates a new ManagedFileUpload which uploads the given file to the
* server through the given Guacamole client.
*
* @param {ManagedClient} managedClient
* The ManagedClient through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*
* @param {Object} [object]
* The object to upload the file to, if any, such as a filesystem
* object.
*
* @param {String} [streamName]
* The name of the stream to upload the file to. If an object is given,
* this must be specified.
*
* @return {ManagedFileUpload}
* A new ManagedFileUpload object which can be used to track the
* progress of the upload.
*/
ManagedFileUpload.getInstance = function getInstance(managedClient, file, object, streamName) {
var managedFileUpload = new ManagedFileUpload();
// Pull Guacamole.Tunnel and Guacamole.Client from given ManagedClient
var client = managedClient.client;
var tunnel = managedClient.tunnel;
// Open file for writing
var stream;
if (!object)
stream = client.createFileStream(file.type, file.name);
// If object/streamName specified, upload to that instead of a file
// stream
else
stream = object.createOutputStream(file.type, streamName);
// Notify that the file transfer is pending
$rootScope.$evalAsync(function uploadStreamOpen() {
// Init managed upload
managedFileUpload.filename = file.name;
managedFileUpload.mimetype = file.type;
managedFileUpload.progress = 0;
managedFileUpload.length = file.size;
// Notify that stream is open
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.OPEN);
});
// Upload file once stream is acknowledged
stream.onack = function beginUpload(status) {
// Notify of any errors from the Guacamole server
if (status.isError()) {
$rootScope.$apply(function uploadStreamError() {
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
status.code);
});
return;
}
// Begin upload
tunnelService.uploadToStream(tunnel.uuid, stream, file, function uploadContinuing(length) {
$rootScope.$apply(function uploadStreamProgress() {
managedFileUpload.progress = length;
});
})
// Notify if upload succeeds
.then(function uploadSuccessful() {
// Upload complete
managedFileUpload.progress = file.size;
// Close the stream
stream.sendEnd();
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.CLOSED);
// Notify of upload completion
$rootScope.$broadcast('guacUploadComplete', file.name);
},
// Notify if upload fails
requestService.createErrorCallback(function uploadFailed(error) {
// Use provide status code if the error is coming from the stream
if (error.type === Error.Type.STREAM_ERROR)
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
error.statusCode);
// Fail with internal error for all other causes
else
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
Guacamole.Status.Code.INTERNAL_ERROR);
// Close the stream
stream.sendEnd();
}));
// Ignore all further acks
stream.onack = null;
};
return managedFileUpload;
};
return ManagedFileUpload;
}]);

View File

@@ -0,0 +1,340 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedFilesystem class used by ManagedClient to represent
* available remote filesystems.
*/
angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector',
function defineManagedFilesystem($rootScope, $injector) {
// Required types
var tunnelService = $injector.get('tunnelService');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* filesystem object while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFilesystem|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFilesystem.
*/
var ManagedFilesystem = function ManagedFilesystem(template) {
// Use empty object by default
template = template || {};
/**
* The client that originally received the "filesystem" instruction
* that resulted in the creation of this ManagedFilesystem.
*
* @type ManagedClient
*/
this.client = template.client;
/**
* The Guacamole filesystem object, as received via a "filesystem"
* instruction.
*
* @type Guacamole.Object
*/
this.object = template.object;
/**
* The declared, human-readable name of the filesystem
*
* @type String
*/
this.name = template.name;
/**
* The root directory of the filesystem.
*
* @type ManagedFilesystem.File
*/
this.root = template.root;
/**
* The current directory being viewed or manipulated within the
* filesystem.
*
* @type ManagedFilesystem.File
*/
this.currentDirectory = template.currentDirectory || template.root;
};
/**
* Refreshes the contents of the given file, if that file is a directory.
* Only the immediate children of the file are refreshed. Files further
* down the directory tree are not refreshed.
*
* @param {ManagedFilesystem} filesystem
* The filesystem associated with the file being refreshed.
*
* @param {ManagedFilesystem.File} file
* The file being refreshed.
*/
ManagedFilesystem.refresh = function updateDirectory(filesystem, file) {
// Do not attempt to refresh the contents of directories
if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE)
return;
// Request contents of given file
filesystem.object.requestInputStream(file.streamName, function handleStream(stream, mimetype) {
// Ignore stream if mimetype is wrong
if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) {
stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED);
return;
}
// Signal server that data is ready to be received
stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);
// Read stream as JSON
var reader = new Guacamole.JSONReader(stream);
// Acknowledge received JSON blobs
reader.onprogress = function onprogress() {
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Reset contents of directory
reader.onend = function jsonReady() {
$rootScope.$evalAsync(function updateFileContents() {
// Empty contents
file.files = {};
// Determine the expected filename prefix of each stream
var expectedPrefix = file.streamName;
if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/')
expectedPrefix += '/';
// For each received stream name
var mimetypes = reader.getJSON();
for (var name in mimetypes) {
// Assert prefix is correct
if (name.substring(0, expectedPrefix.length) !== expectedPrefix)
continue;
// Extract filename from stream name
var filename = name.substring(expectedPrefix.length);
// Deduce type from mimetype
var type = ManagedFilesystem.File.Type.NORMAL;
if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE)
type = ManagedFilesystem.File.Type.DIRECTORY;
// Add file entry
file.files[filename] = new ManagedFilesystem.File({
mimetype : mimetypes[name],
streamName : name,
type : type,
parent : file,
name : filename
});
}
});
};
});
};
/**
* Creates a new ManagedFilesystem instance from the given Guacamole.Object
* and human-readable name. Upon creation, a request to populate the
* contents of the root directory will be automatically dispatched.
*
* @param {ManagedClient} client
* The client that originally received the "filesystem" instruction
* that resulted in the creation of this ManagedFilesystem.
*
* @param {Guacamole.Object} object
* The Guacamole.Object defining the filesystem.
*
* @param {String} name
* A human-readable name for the filesystem.
*
* @returns {ManagedFilesystem}
* The newly-created ManagedFilesystem.
*/
ManagedFilesystem.getInstance = function getInstance(client, object, name) {
// Init new filesystem object
var managedFilesystem = new ManagedFilesystem({
client : client,
object : object,
name : name,
root : new ManagedFilesystem.File({
mimetype : Guacamole.Object.STREAM_INDEX_MIMETYPE,
streamName : Guacamole.Object.ROOT_STREAM,
type : ManagedFilesystem.File.Type.DIRECTORY
})
});
// Retrieve contents of root
ManagedFilesystem.refresh(managedFilesystem, managedFilesystem.root);
return managedFilesystem;
};
/**
* Downloads the given file from the server using the given Guacamole
* client and filesystem. The browser will automatically start the
* download upon completion of this function.
*
* @param {ManagedFilesystem} managedFilesystem
* The ManagedFilesystem from which the file is to be downloaded. Any
* path information provided must be relative to this filesystem.
*
* @param {String} path
* The full, absolute path of the file to download.
*/
ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) {
// Request download
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
// Parse filename from string
var filename = path.match(/(.*[\\/])?(.*)/)[2];
// Start download
tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid, stream, mimetype, filename);
});
};
/**
* Changes the current directory of the given filesystem, automatically
* refreshing the contents of that directory.
*
* @param {ManagedFilesystem} filesystem
* The filesystem whose current directory should be changed.
*
* @param {ManagedFilesystem.File} file
* The directory to change to.
*/
ManagedFilesystem.changeDirectory = function changeDirectory(filesystem, file) {
// Refresh contents
ManagedFilesystem.refresh(filesystem, file);
// Set current directory
filesystem.currentDirectory = file;
};
/**
* A file within a ManagedFilesystem. Each ManagedFilesystem.File provides
* sufficient information for retrieval or replacement of the file's
* contents, as well as the file's name and type.
*
* @param {ManagedFilesystem|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFilesystem.File.
*/
ManagedFilesystem.File = function File(template) {
/**
* The mimetype of the data contained within this file.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The name of the stream representing this files contents within its
* associated filesystem object.
*
* @type String
*/
this.streamName = template.streamName;
/**
* The type of this file. All legal file type strings are defined
* within ManagedFilesystem.File.Type.
*
* @type String
*/
this.type = template.type;
/**
* The name of this file.
*
* @type String
*/
this.name = template.name;
/**
* The parent directory of this file. In the case of the root
* directory, this will be null.
*
* @type ManagedFilesystem.File
*/
this.parent = template.parent;
/**
* Map of all known files containined within this file by name. This is
* only applicable to directories.
*
* @type Object.<String, ManagedFilesystem.File>
*/
this.files = template.files || {};
};
/**
* All legal type strings for a ManagedFilesystem.File.
*
* @type Object.<String, String>
*/
ManagedFilesystem.File.Type = {
/**
* A normal file. As ManagedFilesystem does not currently represent any
* other non-directory types of files, like symbolic links, this type
* string may be used for any non-directory file.
*
* @type String
*/
NORMAL : 'NORMAL',
/**
* A directory.
*
* @type String
*/
DIRECTORY : 'DIRECTORY'
};
return ManagedFilesystem;
}]);

View File

@@ -0,0 +1,105 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedShareLink class used by ManagedClient to represent
* generated connection sharing links.
*/
angular.module('client').factory('ManagedShareLink', ['$injector',
function defineManagedShareLink($injector) {
// Required types
var UserCredentials = $injector.get('UserCredentials');
/**
* Object which represents a link which can be used to gain access to an
* active Guacamole connection.
*
* @constructor
* @param {ManagedShareLink|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedShareLink.
*/
var ManagedShareLink = function ManagedShareLink(template) {
// Use empty object by default
template = template || {};
/**
* The human-readable display name of this share link.
*
* @type String
*/
this.name = template.name;
/**
* The actual URL of the link which can be used to access the shared
* connection.
*
* @type String
*/
this.href = template.href;
/**
* The sharing profile which was used to generate the share link.
*
* @type SharingProfile
*/
this.sharingProfile = template.sharingProfile;
/**
* The credentials from which the share link was derived.
*
* @type UserCredentials
*/
this.sharingCredentials = template.sharingCredentials;
};
/**
* Creates a new ManagedShareLink from a set of UserCredentials and the
* SharingProfile which was used to generate those UserCredentials.
*
* @param {SharingProfile} sharingProfile
* The SharingProfile which was used, via the REST API, to generate the
* given UserCredentials.
*
* @param {UserCredentials} sharingCredentials
* The UserCredentials object returned by the REST API in response to a
* request to share a connection using the given SharingProfile.
*
* @return {ManagedShareLink}
* A new ManagedShareLink object can be used to access the connection
* shared via the given SharingProfile and resulting UserCredentials.
*/
ManagedShareLink.getInstance = function getInstance(sharingProfile, sharingCredentials) {
// Generate new share link using the given profile and credentials
return new ManagedShareLink({
'name' : sharingProfile.name,
'href' : UserCredentials.getLink(sharingCredentials),
'sharingProfile' : sharingProfile,
'sharingCredentials' : sharingCredentials
});
};
return ManagedShareLink;
}]);

View File

@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the TranslationResult class used by the guacTranslate service. This class contains
* both the translated message and the translation ID that generated the message, in the case
* where it's unknown whether a translation is defined or not.
*/
angular.module('client').factory('TranslationResult', [function defineTranslationResult() {
/**
* Object which represents the result of a translation as returned from
* the guacTranslate service.
*
* @constructor
* @param {TranslationResult|Object} [template={}]
* The object whose properties should be copied within the new
* TranslationResult.
*/
const TranslationResult = function TranslationResult(template) {
// Use empty object by default
template = template || {};
/**
* The translation ID.
*
* @type {String}
*/
this.id = template.id;
/**
* The translated message.
*
* @type {String}
*/
this.message = template.message;
};
return TranslationResult;
}]);

View File

@@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for code used to manipulate/observe the clipboard.
*/
angular.module('clipboard', []);

View File

@@ -0,0 +1,133 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive provides an editor for the clipboard content maintained by
* clipboardService. Changes to the clipboard by clipboardService will
* automatically be reflected in the editor, and changes in the editor will
* automatically be reflected in the clipboard by clipboardService.
*/
angular.module('clipboard').directive('guacClipboard', ['$injector',
function guacClipboard($injector) {
// Required types
const ClipboardData = $injector.get('ClipboardData');
// Required services
const $window = $injector.get('$window');
const clipboardService = $injector.get('clipboardService');
/**
* Configuration object for the guacClipboard directive.
*
* @type Object.<String, Object>
*/
var config = {
restrict : 'E',
replace : true,
templateUrl : 'app/clipboard/templates/guacClipboard.html'
};
// guacClipboard directive controller
config.controller = ['$scope', '$injector', '$element',
function guacClipboardController($scope, $injector, $element) {
/**
* The DOM element which will contain the clipboard contents within the
* user interface provided by this directive. We populate the clipboard
* editor via this DOM element rather than updating a model so that we
* are prepared for future support of rich text contents.
*
* @type {!Element}
*/
var element = $element[0].querySelectorAll('.clipboard')[0];
/**
* Whether clipboard contents should be displayed in the clipboard
* editor. If false, clipboard contents will not be displayed until
* the user manually reveals them.
*
* @type {!boolean}
*/
$scope.contentsShown = false;
/**
* Reveals the contents of the clipboard editor, automatically
* assigning input focus to the editor if possible.
*/
$scope.showContents = function showContents() {
$scope.contentsShown = true;
$window.setTimeout(function setFocus() {
element.focus();
}, 0);
};
/**
* Rereads the contents of the clipboard field, updating the
* ClipboardData object on the scope as necessary. The type of data
* stored within the ClipboardData object will be heuristically
* determined from the HTML contents of the clipboard field.
*/
var updateClipboardData = function updateClipboardData() {
// Read contents of clipboard textarea
clipboardService.setClipboard(new ClipboardData({
type : 'text/plain',
data : element.value
}));
};
/**
* Updates the contents of the clipboard editor to the given data.
*
* @param {ClipboardData} data
* The ClipboardData to display within the clipboard editor for
* editing.
*/
const updateClipboardEditor = function updateClipboardEditor(data) {
// If the clipboard data is a string, render it as text
if (typeof data.data === 'string')
element.value = data.data;
// Ignore other data types for now
};
// Update the internally-stored clipboard data when events are fired
// that indicate the clipboard field may have been changed
element.addEventListener('input', updateClipboardData);
element.addEventListener('change', updateClipboardData);
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function clipboardChanged(event, data) {
updateClipboardEditor(data);
});
// Init clipboard editor with current clipboard contents
clipboardService.getClipboard().then((data) => {
updateClipboardEditor(data);
}, angular.noop);
}];
return config;
}]);

View File

@@ -0,0 +1,625 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for maintaining and accessing clipboard data. If possible, this
* service will leverage the local clipboard. If the local clipboard is not
* available, an internal in-memory clipboard will be used instead.
*/
angular.module('clipboard').factory('clipboardService', ['$injector',
function clipboardService($injector) {
// Get required services
const $q = $injector.get('$q');
const $window = $injector.get('$window');
const $rootScope = $injector.get('$rootScope');
const sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
const ClipboardData = $injector.get('ClipboardData');
/**
* Getter/setter which retrieves or sets the current stored clipboard
* contents. The stored clipboard contents are strictly internal to
* Guacamole, and may not reflect the local clipboard if local clipboard
* access is unavailable.
*
* @type Function
*/
const storedClipboardData = sessionStorageFactory.create(new ClipboardData());
var service = {};
/**
* The amount of time to wait before actually serving a request to read
* clipboard data, in milliseconds. Providing a reasonable delay between
* request and read attempt allows the cut/copy operation to settle, in
* case the data we are anticipating to be present is not actually present
* in the clipboard yet.
*
* @constant
* @type Number
*/
var CLIPBOARD_READ_DELAY = 100;
/**
* The promise associated with the current pending clipboard read attempt.
* If no clipboard read is active, this will be null.
*
* @type Promise.<ClipboardData>
*/
var pendingRead = null;
/**
* Reference to the window.document object.
*
* @private
* @type HTMLDocument
*/
var document = $window.document;
/**
* The textarea that will be used to hold the local clipboard contents.
*
* @type Element
*/
var clipboardContent = document.createElement('textarea');
// Ensure clipboard target is selectable but not visible
clipboardContent.className = 'clipboard-service-target';
// Add clipboard target to DOM
document.body.appendChild(clipboardContent);
/**
* Stops the propagation of the given event through the DOM tree. This is
* identical to invoking stopPropagation() on the event directly, except
* that this function is usable as an event handler itself.
*
* @param {Event} e
* The event whose propagation through the DOM tree should be stopped.
*/
var stopEventPropagation = function stopEventPropagation(e) {
e.stopPropagation();
};
// Prevent events generated due to execCommand() from disturbing external things
clipboardContent.addEventListener('cut', stopEventPropagation);
clipboardContent.addEventListener('copy', stopEventPropagation);
clipboardContent.addEventListener('paste', stopEventPropagation);
clipboardContent.addEventListener('input', stopEventPropagation);
/**
* A stack of past node selection ranges. A range convering the nodes
* currently selected within the document can be pushed onto this stack
* with pushSelection(), and the most recently pushed selection can be
* popped off the stack (and thus re-selected) with popSelection().
*
* @type Range[]
*/
var selectionStack = [];
/**
* Pushes the current selection range to the selection stack such that it
* can later be restored with popSelection().
*/
var pushSelection = function pushSelection() {
// Add a range representing the current selection to the stack
var selection = $window.getSelection();
if (selection.getRangeAt && selection.rangeCount)
selectionStack.push(selection.getRangeAt(0));
};
/**
* Pops a selection range off the selection stack restoring the document's
* previous selection state. The selection range will be the most recent
* selection range pushed by pushSelection(). If there are no selection
* ranges currently on the stack, this function has no effect.
*/
var popSelection = function popSelection() {
// Pull one selection range from the stack
var range = selectionStack.pop();
if (!range)
return;
// Replace any current selection with the retrieved selection
var selection = $window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
/**
* Selects all nodes within the given element. This will replace the
* current selection with a new selection range that covers the element's
* contents. If the original selection should be preserved, use
* pushSelection() and popSelection().
*
* @param {Element} element
* The element whose contents should be selected.
*/
var selectAll = function selectAll(element) {
// Use the select() function defined for input elements, if available
if (element.select)
element.select();
// Fallback to manual manipulation of the selection
else {
// Generate a range which selects all nodes within the given element
var range = document.createRange();
range.selectNodeContents(element);
// Replace any current selection with the generated range
var selection = $window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Sets the local clipboard, if possible, to the given text.
*
* @param {ClipboardData} data
* The data to assign to the local clipboard should be set.
*
* @return {Promise}
* A promise that will resolve if setting the clipboard was successful,
* and will reject if it failed.
*/
const setLocalClipboard = function setLocalClipboard(data) {
var deferred = $q.defer();
try {
// Attempt to read the clipboard using the Asynchronous Clipboard
// API, if it's available
if (navigator.clipboard && navigator.clipboard.writeText) {
if (data.type === 'text/plain') {
navigator.clipboard.writeText(data.data).then(deferred.resolve, deferred.reject);
return deferred.promise;
}
}
}
// Ignore any hard failures to use Asynchronous Clipboard API, falling
// back to traditional document.execCommand()
catch (ignore) {}
// Track the originally-focused element prior to changing focus
var originalElement = document.activeElement;
pushSelection();
// Copy the given value into the clipboard DOM element
if (typeof data.data === 'string')
clipboardContent.value = data.data;
else {
clipboardContent.innerHTML = '';
var img = document.createElement('img');
img.src = URL.createObjectURL(data.data);
clipboardContent.appendChild(img);
}
// Select all data within the clipboard target
clipboardContent.focus();
selectAll(clipboardContent);
// Attempt to copy data from clipboard element into local clipboard
if (document.execCommand('copy'))
deferred.resolve();
else
deferred.reject();
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
// restoring whichever element was originally focused
clipboardContent.blur();
originalElement.focus();
popSelection();
return deferred.promise;
};
/**
* Parses the given data URL, returning its decoded contents as a new Blob.
* If the URL is not a valid data URL, null will be returned instead.
*
* @param {String} url
* The data URL to parse.
*
* @returns {Blob}
* A new Blob containing the decoded contents of the data URL, or null
* if the URL is not a valid data URL.
*/
service.parseDataURL = function parseDataURL(url) {
// Parse given string as a data URL
var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url);
if (!result)
return null;
// Pull the mimetype and base64 contents of the data URL
var type = result[1];
var data = $window.atob(result[2]);
// Convert the decoded binary string into a typed array
var buffer = new Uint8Array(data.length);
for (var i = 0; i < data.length; i++)
buffer[i] = data.charCodeAt(i);
// Produce a proper blob containing the data and type provided in
// the data URL
return new Blob([buffer], { type : type });
};
/**
* Returns the content of the given element as plain, unformatted text,
* preserving only individual characters and newlines. Formatting, images,
* etc. are not taken into account.
*
* @param {Element} element
* The element whose text content should be returned.
*
* @returns {String}
* The plain text contents of the given element, including newlines and
* spacing but otherwise without any formatting.
*/
service.getTextContent = function getTextContent(element) {
var blocks = [];
var currentBlock = '';
// For each child of the given element
var current = element.firstChild;
while (current) {
// Simply append the content of any text nodes
if (current.nodeType === Node.TEXT_NODE)
currentBlock += current.nodeValue;
// Render <br> as a newline character
else if (current.nodeName === 'BR')
currentBlock += '\n';
// Render <img> as alt text, if available
else if (current.nodeName === 'IMG')
currentBlock += current.getAttribute('alt') || '';
// For all other nodes, handling depends on whether they are
// block-level elements
else {
// If we are entering a new block context, start a new block if
// the current block is non-empty
if (currentBlock.length && $window.getComputedStyle(current).display === 'block') {
// Trim trailing newline (would otherwise inflate the line count by 1)
if (currentBlock.substring(currentBlock.length - 1) === '\n')
currentBlock = currentBlock.substring(0, currentBlock.length - 1);
// Finish current block and start a new block
blocks.push(currentBlock);
currentBlock = '';
}
// Append the content of the current element to the current block
currentBlock += service.getTextContent(current);
}
current = current.nextSibling;
}
// Add any in-progress block
if (currentBlock.length)
blocks.push(currentBlock);
// Combine all non-empty blocks, separated by newlines
return blocks.join('\n');
};
/**
* Replaces the current text content of the given element with the given
* text. To avoid affecting the position of the cursor within an editable
* element, or firing unnecessary DOM modification events, the underlying
* <code>textContent</code> property of the element is only touched if
* doing so would actually change the text.
*
* @param {Element} element
* The element whose text content should be changed.
*
* @param {String} text
* The text content to assign to the given element.
*/
service.setTextContent = function setTextContent(element, text) {
// Strip out any images
$(element).find('img').remove();
// Reset text content only if doing so will actually change the content
if (service.getTextContent(element) !== text)
element.textContent = text;
};
/**
* Returns the URL of the single image within the given element, if the
* element truly contains only one child and that child is an image. If the
* content of the element is mixed or not an image, null is returned.
*
* @param {Element} element
* The element whose image content should be retrieved.
*
* @returns {String}
* The URL of the image contained within the given element, if that
* element contains only a single child element which happens to be an
* image, or null if the content of the element is not purely an image.
*/
service.getImageContent = function getImageContent(element) {
// Return the source of the single child element, if it is an image
var firstChild = element.firstChild;
if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling)
return firstChild.getAttribute('src');
// Otherwise, the content of this element is not simply an image
return null;
};
/**
* Replaces the current contents of the given element with a single image
* having the given URL. To avoid affecting the position of the cursor
* within an editable element, or firing unnecessary DOM modification
* events, the content of the element is only touched if doing so would
* actually change content.
*
* @param {Element} element
* The element whose image content should be changed.
*
* @param {String} url
* The URL of the image which should be assigned as the contents of the
* given element.
*/
service.setImageContent = function setImageContent(element, url) {
// Retrieve the URL of the current image contents, if any
var currentImage = service.getImageContent(element);
// If the current contents are not the given image (or not an image
// at all), reassign the contents
if (currentImage !== url) {
// Clear current contents
element.innerHTML = '';
// Add a new image as the sole contents of the element
var img = document.createElement('img');
img.src = url;
element.appendChild(img);
}
};
/**
* Get the current value of the local clipboard.
*
* @return {Promise.<ClipboardData>}
* A promise that will resolve with the contents of the local clipboard
* if getting the clipboard was successful, and will reject if it
* failed.
*/
const getLocalClipboard = function getLocalClipboard() {
// If the clipboard is already being read, do not overlap the read
// attempts; instead share the result across all requests
if (pendingRead)
return pendingRead;
var deferred = $q.defer();
try {
// Attempt to read the clipboard using the Asynchronous Clipboard
// API, if it's available
if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText().then(function textRead(text) {
deferred.resolve(new ClipboardData({
type : 'text/plain',
data : text
}));
}, deferred.reject);
return deferred.promise;
}
}
// Ignore any hard failures to use Asynchronous Clipboard API, falling
// back to traditional document.execCommand()
catch (ignore) {}
// Track the originally-focused element prior to changing focus
var originalElement = document.activeElement;
/**
* Attempts to paste the clipboard contents into the
* currently-focused element. The promise related to the current
* attempt to read the clipboard will be resolved or rejected
* depending on whether the attempt to paste succeeds.
*/
var performPaste = function performPaste() {
// Attempt paste local clipboard into clipboard DOM element
if (document.execCommand('paste')) {
// If the pasted data is a single image, resolve with a blob
// containing that image
var currentImage = service.getImageContent(clipboardContent);
if (currentImage) {
// Convert the image's data URL into a blob
var blob = service.parseDataURL(currentImage);
if (blob) {
deferred.resolve(new ClipboardData({
type : blob.type,
data : blob
}));
}
// Reject if conversion fails
else
deferred.reject();
} // end if clipboard is an image
// Otherwise, assume the clipboard contains plain text
else
deferred.resolve(new ClipboardData({
type : 'text/plain',
data : clipboardContent.value
}));
}
// Otherwise, reading from the clipboard has failed
else
deferred.reject();
};
// Mark read attempt as in progress, cleaning up event listener and
// selection once the paste attempt has completed
pendingRead = deferred.promise['finally'](function cleanupReadAttempt() {
// Do not use future changes in focus
clipboardContent.removeEventListener('focus', performPaste);
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
// restoring whichever element was originally focused
clipboardContent.blur();
originalElement.focus();
popSelection();
// No read is pending any longer
pendingRead = null;
});
// Wait for the next event queue run before attempting to read
// clipboard data (in case the copy/cut has not yet completed)
$window.setTimeout(function deferredClipboardRead() {
pushSelection();
// Ensure clipboard element is blurred (and that the "focus" event
// will fire)
clipboardContent.blur();
clipboardContent.addEventListener('focus', performPaste);
// Clear and select the clipboard DOM element
clipboardContent.value = '';
clipboardContent.focus();
selectAll(clipboardContent);
// If focus failed to be set, we cannot read the clipboard
if (document.activeElement !== clipboardContent)
deferred.reject();
}, CLIPBOARD_READ_DELAY);
return pendingRead;
};
/**
* Returns the current value of the internal clipboard shared across all
* active Guacamole connections running within the current browser tab. If
* access to the local clipboard is available, the internal clipboard is
* first synchronized with the current local clipboard contents. If access
* to the local clipboard is unavailable, only the internal clipboard will
* be used.
*
* @return {Promise.<ClipboardData>}
* A promise that will resolve with the contents of the internal
* clipboard, first retrieving those contents from the local clipboard
* if permission to do so has been granted. This promise is always
* resolved.
*/
service.getClipboard = function getClipboard() {
return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData());
};
/**
* Sets the content of the internal clipboard shared across all active
* Guacamole connections running within the current browser tab. If
* access to the local clipboard is available, the local clipboard is
* first set to the provided clipboard content. If access to the local
* clipboard is unavailable, only the internal clipboard will be used. A
* "guacClipboard" event will be broadcast with the assigned data once the
* operation has completed.
*
* @param {ClipboardData} data
* The data to assign to the clipboard.
*
* @return {Promise}
* A promise that will resolve after the clipboard content has been
* set. This promise is always resolved.
*/
service.setClipboard = function setClipboard(data) {
return setLocalClipboard(data)['catch'](angular.noop).finally(() => {
// Update internal clipboard and broadcast event notifying of
// updated contents
storedClipboardData(data);
$rootScope.$broadcast('guacClipboard', data);
});
};
/**
* Resynchronizes the local and internal clipboards, setting the contents
* of the internal clipboard to that of the local clipboard (if local
* clipboard access is granted) and broadcasting a "guacClipboard" event
* with the current internal clipboard contents for consumption by external
* components like the "guacClient" directive.
*/
service.resyncClipboard = function resyncClipboard() {
getLocalClipboard().then(function clipboardRead(data) {
return service.setClipboard(data);
}, angular.noop);
};
return service;
}]);

View File

@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.clipboard, .clipboard-service-target {
background: white;
}
.clipboard {
position: relative;
border: 1px solid #AAA;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
-khtml-border-radius: 0.25em;
border-radius: 0.25em;
width: 100%;
height: 2in;
white-space: pre;
padding: 0.25em;
}
.clipboard p,
.clipboard div {
margin: 0;
}
.clipboard img {
max-width: 100%;
max-height: 100%;
display: block;
margin: 0 auto;
border: 1px solid black;
background: url('images/checker.svg');
}
.clipboard-service-target {
position: fixed;
left: -1em;
right: -1em;
width: 1em;
height: 1em;
white-space: pre;
overflow: hidden;
}
.clipboard-editor {
position: relative;
}
.clipboard-editor .clipboard {
overflow: auto;
font-size: 1em;
}
.clipboard-editor .clipboard.clipboard-contents-hidden {
color: transparent;
overflow: hidden;
}
.clipboard-editor .clipboard-contents-hidden-hint {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
cursor: pointer;
display: flex;
align-items: center;
}
.clipboard-editor .clipboard-contents-hidden-hint .clipboard-contents-hidden-hint-text {
flex: 1;
background: rgba(0, 0, 0, 0.125);
color: #888;
padding: 0.5em;
overflow: hidden;
font-size: 0.9em;
text-align: center;
font-style: italic;
}
.clipboard-editor .clipboard-contents-hidden-hint:hover .clipboard-contents-hidden-hint-text {
text-decoration: underline;
}

View File

@@ -0,0 +1,12 @@
<div class="clipboard-editor">
<textarea class="clipboard"
ng-class="{
'clipboard-contents-hidden' : !contentsShown
}"
ng-disabled="!contentsShown"></textarea>
<div class="clipboard-contents-hidden-hint"
ng-click="showContents()"
ng-show="!contentsShown">
<p class="clipboard-contents-hidden-hint-text">{{ 'CLIENT.ACTION_SHOW_CLIPBOARD' | translate }}</p>
</div>
</div>

View File

@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ClipboardData class used for interchange between the
* guacClipboard directive, clipboardService service, etc.
*/
angular.module('clipboard').factory('ClipboardData', [function defineClipboardData() {
/**
* Arbitrary data which can be contained by the clipboard.
*
* @constructor
* @param {ClipboardData|Object} [template={}]
* The object whose properties should be copied within the new
* ClipboardData.
*/
var ClipboardData = function ClipboardData(template) {
// Use empty object by default
template = template || {};
/**
* The ID of the ManagedClient handling the remote desktop connection
* that originated this clipboard data, or null if the data originated
* from the clipboard editor or local clipboard.
*
* @type {string}
*/
this.source = template.source;
/**
* The mimetype of the data currently stored within the clipboard.
*
* @type String
*/
this.type = template.type || 'text/plain';
/**
* The data currently stored within the clipboard. Depending on the
* nature of the stored data, this may be either a String, a Blob, or a
* File.
*
* @type String|Blob|File
*/
this.data = template.data || '';
};
return ClipboardData;
}]);

View File

@@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which provides handling of click and click-like touch events.
* The state of Shift and Ctrl modifiers is tracked through these click events
* to allow for specific handling of Shift+Click and Ctrl+Click.
*/
angular.module('element').directive('guacClick', [function guacClick() {
return {
restrict: 'A',
link: function linkGuacClick($scope, $element, $attrs) {
/**
* A callback that is invoked by the guacClick directive when a
* click or click-like event is received.
*
* @callback guacClick~callback
* @param {boolean} shift
* Whether Shift was held down at the time the click occurred.
*
* @param {boolean} ctrl
* Whether Ctrl or Meta (the Mac "Command" key) was held down
* at the time the click occurred.
*/
/**
* The callback to invoke when a click or click-like event is
* received on the associated element.
*
* @type guacClick~callback
*/
const guacClick = $scope.$eval($attrs.guacClick);
/**
* The element which will register the click.
*
* @type Element
*/
const element = $element[0];
/**
* Whether either Shift key is currently pressed.
*
* @type boolean
*/
let shift = false;
/**
* Whether either Ctrl key is currently pressed. To allow the
* Command key to be used on Mac platforms, this flag also
* considers the state of either Meta key.
*
* @type boolean
*/
let ctrl = false;
/**
* Updates the state of the {@link shift} and {@link ctrl} flags
* based on which keys are currently marked as held down by the
* given Guacamole.Keyboard.
*
* @param {Guacamole.Keyboard} keyboard
* The Guacamole.Keyboard instance to read key states from.
*/
const updateModifiers = function updateModifiers(keyboard) {
shift = !!(
keyboard.pressed[0xFFE1] // Left shift
|| keyboard.pressed[0xFFE2] // Right shift
);
ctrl = !!(
keyboard.pressed[0xFFE3] // Left ctrl
|| keyboard.pressed[0xFFE4] // Right ctrl
|| keyboard.pressed[0xFFE7] // Left meta (command)
|| keyboard.pressed[0xFFE8] // Right meta (command)
);
};
// Update tracking of modifier states for each key press
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
updateModifiers(keyboard);
});
// Update tracking of modifier states for each key release
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
updateModifiers(keyboard);
});
// Fire provided callback for each mouse-initiated "click" event ...
element.addEventListener('click', function elementClicked(e) {
if (element.contains(e.target))
$scope.$apply(() => guacClick(shift, ctrl));
});
// ... and for touch-initiated click-like events
element.addEventListener('touchstart', function elementClicked(e) {
if (element.contains(e.target))
$scope.$apply(() => guacClick(shift, ctrl));
});
} // end guacClick link function
};
}]);

View File

@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows multiple files to be uploaded. Dragging files onto
* the associated element will call the provided callback function with any
* dragged files.
*/
angular.module('element').directive('guacDrop', ['$injector', function guacDrop($injector) {
// Required services
const guacNotification = $injector.get('guacNotification');
return {
restrict: 'A',
link: function linkGuacDrop($scope, $element, $attrs) {
/**
* The function to call whenever files are dragged. The callback is
* provided a single parameter: the FileList containing all dragged
* files.
*
* @type Function
*/
const guacDrop = $scope.$eval($attrs.guacDrop);
/**
* Any number of space-seperated classes to be applied to the
* element a drop is pending: when the user has dragged something
* over the element, but not yet dropped. These classes will be
* removed when a drop is not pending.
*
* @type String
*/
const guacDraggedClass = $scope.$eval($attrs.guacDraggedClass);
/**
* Whether upload of multiple files should be allowed. If false, an
* error will be displayed explaining the restriction, otherwise
* any number of files may be dragged. Defaults to true if not set.
*
* @type Boolean
*/
const guacMultiple = 'guacMultiple' in $attrs
? $scope.$eval($attrs.guacMultiple) : true;
/**
* The element which will register drag event.
*
* @type Element
*/
const element = $element[0];
/**
* Applies any classes provided in the guacDraggedClass attribute.
* Further propagation and default behavior of the given event is
* automatically prevented.
*
* @param {Event} e
* The event related to the in-progress drag/drop operation.
*/
const notifyDragStart = function notifyDragStart(e) {
e.preventDefault();
e.stopPropagation();
// Skip further processing if no classes were provided
if (!guacDraggedClass)
return;
// Add each provided class
guacDraggedClass.split(' ').forEach(classToApply =>
element.classList.add(classToApply));
};
/**
* Removes any classes provided in the guacDraggedClass attribute.
* Further propagation and default behavior of the given event is
* automatically prevented.
*
* @param {Event} e
* The event related to the end of the drag/drop operation.
*/
const notifyDragEnd = function notifyDragEnd(e) {
e.preventDefault();
e.stopPropagation();
// Skip further processing if no classes were provided
if (!guacDraggedClass)
return;
// Remove each provided class
guacDraggedClass.split(' ').forEach(classToRemove =>
element.classList.remove(classToRemove));
};
// Add listeners to the drop target to ensure that the visual state
// stays up to date
element.addEventListener('dragenter', notifyDragStart);
element.addEventListener('dragover', notifyDragStart);
element.addEventListener('dragleave', notifyDragEnd);
/**
* Event listener that will be invoked if the user drops anything
* onto the event. If a valid file is provided, the onFile callback
* provided to this directive will be called; otherwise an error
* will be displayed, if appropriate.
*
* @param {Event} e
* The drop event that triggered this handler.
*/
element.addEventListener('drop', e => {
notifyDragEnd(e);
const files = e.dataTransfer.files;
// Ignore any non-files that are dragged into the drop area
if (files.length < 1)
return;
// If multi-file upload is disabled, If more than one file was
// provided, print an error explaining the problem
if (!guacMultiple && files.length >= 2) {
guacNotification.showStatus({
className : 'error',
title : 'APP.DIALOG_HEADER_ERROR',
text: { key : 'APP.ERROR_SINGLE_FILE_ONLY'},
// Add a button to hide the error
actions : [{
name : 'APP.ACTION_ACKNOWLEDGE',
callback : () => guacNotification.showStatus(false)
}]
});
return;
}
// Invoke the callback with the files. Note that if guacMultiple
// is set to false, this will always be a single file.
guacDrop(files);
});
} // end guacDrop link function
};
}]);

View File

@@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows elements to be manually focused / blurred.
*/
angular.module('element').directive('guacFocus', ['$injector', function guacFocus($injector) {
// Required services
var $parse = $injector.get('$parse');
var $timeout = $injector.get('$timeout');
return {
restrict: 'A',
link: function linkGuacFocus($scope, $element, $attrs) {
/**
* Whether the element associated with this directive should be
* focussed.
*
* @type Boolean
*/
var guacFocus = $parse($attrs.guacFocus);
/**
* The element which will be focused / blurred.
*
* @type Element
*/
var element = $element[0];
// Set/unset focus depending on value of guacFocus
$scope.$watch(guacFocus, function updateFocus(value) {
$timeout(function updateFocusAfterRender() {
if (value)
element.focus();
else
element.blur();
});
});
} // end guacFocus link function
};
}]);

View File

@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which stores a marker which refers to a specific element,
* allowing that element to be scrolled into view when desired.
*/
angular.module('element').directive('guacMarker', ['$injector', function guacMarker($injector) {
// Required types
var Marker = $injector.get('Marker');
// Required services
var $parse = $injector.get('$parse');
return {
restrict: 'A',
link: function linkGuacMarker($scope, $element, $attrs) {
/**
* The property in which a new Marker should be stored. The new
* Marker will refer to the element associated with this directive.
*
* @type Marker
*/
var guacMarker = $parse($attrs.guacMarker);
/**
* The element to associate with the new Marker.
*
* @type Element
*/
var element = $element[0];
// Assign new marker
guacMarker.assign($scope, new Marker(element));
}
};
}]);

View File

@@ -0,0 +1,114 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which calls a given callback when its associated element is
* resized. This will modify the internal DOM tree of the associated element,
* and the associated element MUST have position (for example,
* "position: relative").
*/
angular.module('element').directive('guacResize', ['$document', function guacResize($document) {
return {
restrict: 'A',
link: function linkGuacResize($scope, $element, $attrs) {
/**
* The function to call whenever the associated element is
* resized. The function will be passed the width and height of
* the element, in pixels.
*
* @type Function
*/
var guacResize = $scope.$eval($attrs.guacResize);
/**
* The element which will monitored for size changes.
*
* @type Element
*/
var element = $element[0];
/**
* The resize sensor - an HTML object element.
*
* @type HTMLObjectElement
*/
var resizeSensor = $document[0].createElement('object');
/**
* The width of the associated element, in pixels.
*
* @type Number
*/
var lastWidth = element.offsetWidth;
/**
* The height of the associated element, in pixels.
*
* @type Number
*/
var lastHeight = element.offsetHeight;
/**
* Checks whether the size of the associated element has changed
* and, if so, calls the resize callback with the new width and
* height as parameters.
*/
var checkSize = function checkSize() {
// Call callback only if size actually changed
if (element.offsetWidth !== lastWidth
|| element.offsetHeight !== lastHeight) {
// Call resize callback, if defined
if (guacResize) {
$scope.$evalAsync(function elementSizeChanged() {
guacResize(element.offsetWidth, element.offsetHeight);
});
}
// Update stored size
lastWidth = element.offsetWidth;
lastHeight = element.offsetHeight;
}
};
// Register event listener once window object exists
resizeSensor.onload = function resizeSensorReady() {
resizeSensor.contentDocument.defaultView.addEventListener('resize', checkSize);
checkSize();
};
// Load blank contents
resizeSensor.className = 'resize-sensor';
resizeSensor.type = 'text/html';
resizeSensor.data = 'app/element/templates/blank.html';
// Add resize sensor to associated element
element.insertBefore(resizeSensor, element.firstChild);
} // end guacResize link function
};
}]);

View File

@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows elements to be manually scrolled, and for their
* scroll state to be observed.
*/
angular.module('element').directive('guacScroll', [function guacScroll() {
return {
restrict: 'A',
link: function linkGuacScroll($scope, $element, $attrs) {
/**
* The current scroll state of the element.
*
* @type ScrollState
*/
var guacScroll = $scope.$eval($attrs.guacScroll);
/**
* The element which is being scrolled, or monitored for changes
* in scroll.
*
* @type Element
*/
var element = $element[0];
/**
* Returns the current left edge of the scrolling rectangle.
*
* @returns {Number}
* The current left edge of the scrolling rectangle.
*/
var getScrollLeft = function getScrollLeft() {
return guacScroll.left;
};
/**
* Returns the current top edge of the scrolling rectangle.
*
* @returns {Number}
* The current top edge of the scrolling rectangle.
*/
var getScrollTop = function getScrollTop() {
return guacScroll.top;
};
// Update underlying scrollLeft property when left changes
$scope.$watch(getScrollLeft, function scrollLeftChanged(left) {
element.scrollLeft = left;
guacScroll.left = element.scrollLeft;
});
// Update underlying scrollTop property when top changes
$scope.$watch(getScrollTop, function scrollTopChanged(top) {
element.scrollTop = top;
guacScroll.top = element.scrollTop;
});
} // end guacScroll link function
};
}]);

View File

@@ -0,0 +1,103 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows files to be uploaded. Clicking on the associated
* element will result in a file selector dialog, which then calls the provided
* callback function with any chosen files.
*/
angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) {
return {
restrict: 'A',
link: function linkGuacUpload($scope, $element, $attrs) {
/**
* The function to call whenever files are chosen. The callback is
* provided a single parameter: the FileList containing all chosen
* files.
*
* @type Function
*/
const guacUpload = $scope.$eval($attrs.guacUpload);
/**
* Whether upload of multiple files should be allowed. If false, the
* file dialog will only allow a single file to be chosen at once,
* otherwise any number of files may be chosen. Defaults to true if
* not set.
*
* @type Boolean
*/
const guacMultiple = 'guacMultiple' in $attrs
? $scope.$eval($attrs.guacMultiple) : true;
/**
* The element which will register the click.
*
* @type Element
*/
const element = $element[0];
/**
* Internal form, containing a single file input element.
*
* @type HTMLFormElement
*/
const form = $document[0].createElement('form');
/**
* Internal file input element.
*
* @type HTMLInputElement
*/
const input = $document[0].createElement('input');
// Init input element
input.type = 'file';
input.multiple = guacMultiple;
// Add input element to internal form
form.appendChild(input);
// Notify of any chosen files
input.addEventListener('change', function filesSelected() {
$scope.$apply(function setSelectedFiles() {
// Only set chosen files selection is not canceled
if (guacUpload && input.files.length > 0)
guacUpload(input.files);
// Reset selection
form.reset();
});
});
// Open file chooser when element is clicked
element.addEventListener('click', function elementClicked() {
input.click();
});
} // end guacUpload link function
};
}]);

View File

@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Module for manipulating element state, such as focus or scroll position, as
* well as handling browser events.
*/
angular.module('element', []);

View File

@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.resize-sensor {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
overflow: hidden;
border: none;
opacity: 0;
z-index: -1;
}

Some files were not shown because too many files have changed in this diff Show More