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

4
guacamole-common-js/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node/
node_modules/
target/
*~

View File

View File

@@ -0,0 +1,14 @@
{
"source" : {
"include" : "src"
},
"opts" : {
"recurse" : true,
"destination" : "target/apidocs"
},
"templates" : {
"default" : {
"useLongnameInNav" : true
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 karma configuration intended for use in builds or CI. Runs all discovered
* unit tests under a headless firefox browser and immediately exits.
*/
module.exports = function(config) {
config.set({
// Discover and run jasmine tests
frameworks: ['jasmine'],
// Pattern matching all javascript source and tests
files: [
'src/**/*.js'
],
// Run the tests once and exit
singleRun: true,
// Disable automatic test running on changed files
autoWatch: false,
// Use a headless firefox browser to run the tests
browsers: ['FirefoxHeadless'],
customLaunchers: {
'FirefoxHeadless': {
base: 'Firefox',
flags: [
'--headless'
]
}
}
});
};

1881
guacamole-common-js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{
"description": "Dependencies to be installed by Maven for running unit tests and generating documentation",
"devDependencies": {
"jsdoc": "^4.0.3",
"karma": "^6.4.4",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "^5.1.0"
}
}

237
guacamole-common-js/pom.xml Normal file
View File

@@ -0,0 +1,237 @@
<?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-common-js</artifactId>
<packaging>pom</packaging>
<version>1.6.0</version>
<name>guacamole-common-js</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>
<properties>
<!--
The location where temporary files should be stored for communicating
between karma and firefox. The default location, /tmp, does not work
if firefox is installed via snap.
-->
<firefox.temp.dir>${project.build.directory}/tmp</firefox.temp.dir>
<!--
Skip tests unless requested otherwise with -DskipTests=false.
Skipped by default because these tests require firefox to be installed.
-->
<skipTests>true</skipTests>
</properties>
<description>
The base JavaScript API of the Guacamole project, providing JavaScript
support for the Guacamole stack, including a full client
implementation for the Guacamole protocol.
</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>
<!-- Assemble JS files into single .zip -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>static.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-zip</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- JS/CSS Minification Plugin -->
<plugin>
<groupId>com.github.buckelieg</groupId>
<artifactId>minify-maven-plugin</artifactId>
<executions>
<execution>
<id>default-minify</id>
<configuration>
<charset>UTF-8</charset>
<jsEngine>CLOSURE</jsEngine>
<jsSourceDir>/</jsSourceDir>
<jsTargetDir>/</jsTargetDir>
<jsFinalFile>all.js</jsFinalFile>
<jsSourceFiles>
<jsSourceFile>common/license.js</jsSourceFile>
</jsSourceFiles>
<jsSourceIncludes>
<jsSourceInclude>modules/**/*.js</jsSourceInclude>
</jsSourceIncludes>
</configuration>
<goals>
<goal>minify</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Skip tests if configured to do so -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
<!-- Ensure the firefox temp directory exists -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>createFirefoxTempdir</id>
<phase>test</phase>
<configuration>
<target>
<mkdir dir="${firefox.temp.dir}"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Unit test using Jasmin and Firefox -->
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.1</version>
<configuration>
<!-- The version of node to use for running tests -->
<nodeVersion>v16.19.1</nodeVersion>
<!-- Install dependencies with "npm ci" for repeatability -->
<arguments>ci</arguments>
<!-- The location of the karma config file -->
<karmaConfPath>karma-ci.conf.js</karmaConfPath>
<!-- Tell karma to use the custom temp directory -->
<environmentVariables>
<TMPDIR>${firefox.temp.dir}</TMPDIR>
</environmentVariables>
</configuration>
<executions>
<!-- Install node.js and NPM before running tests or generating documentation -->
<execution>
<id>install-node-and-npm</id>
<phase>process-sources</phase>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<!-- Generate documentation using JSDoc -->
<execution>
<id>generate-docs</id>
<goals>
<goal>npx</goal>
</goals>
<phase>package</phase>
<configuration>
<arguments>jsdoc -c jsdoc-conf.json</arguments>
</configuration>
</execution>
<!-- Install test and documentation dependencies -->
<execution>
<id>npm-install</id>
<phase>test</phase>
<goals>
<goal>npm</goal>
</goals>
</execution>
<!-- Run all tests non-interactively -->
<execution>
<id>run-tests</id>
<phase>test</phase>
<goals>
<goal>karma</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,18 @@
/*
* 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.
*/

View File

@@ -0,0 +1,91 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, returning
* strictly received packets as array buffers. Note that this object will
* overwrite any installed event handlers on the given Guacamole.InputStream.
*
* @constructor
* @param {!Guacamole.InputStream} stream
* The stream that data will be read from.
*/
Guacamole.ArrayBufferReader = function(stream) {
/**
* Reference to this Guacamole.InputStream.
* @private
*/
var guac_reader = this;
// Receive blobs as array buffers
stream.onblob = function(data) {
var arrayBuffer, bufferView;
// Use native methods for directly decoding base64 to an array buffer
// when possible
if (Uint8Array.fromBase64) {
bufferView = Uint8Array.fromBase64(data);
arrayBuffer = bufferView.buffer;
}
// Rely on binary strings and manual conversions where native methods
// like fromBase64() are not available
else {
var binary = window.atob(data);
arrayBuffer = new ArrayBuffer(binary.length);
bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
}
// Call handler, if present
if (guac_reader.ondata)
guac_reader.ondata(arrayBuffer);
};
// Simply call onend when end received
stream.onend = function() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Fired once for every blob of data received.
*
* @event
* @param {!ArrayBuffer} buffer
* The data packet received.
*/
this.ondata = null;
/**
* Fired once this stream is finished and no further data will be written.
* @event
*/
this.onend = null;
};

View File

@@ -0,0 +1,128 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A writer which automatically writes to the given output stream with arbitrary
* binary data, supplied as ArrayBuffers.
*
* @constructor
* @param {!Guacamole.OutputStream} stream
* The stream that data will be written to.
*/
Guacamole.ArrayBufferWriter = function(stream) {
/**
* Reference to this Guacamole.StringWriter.
*
* @private
* @type {!Guacamole.ArrayBufferWriter}
*/
var guac_writer = this;
// Simply call onack for acknowledgements
stream.onack = function(status) {
if (guac_writer.onack)
guac_writer.onack(status);
};
/**
* Encodes the given data as base64, sending it as a blob. The data must
* be small enough to fit into a single blob instruction.
*
* @private
* @param {!Uint8Array} bytes
* The data to send.
*/
function __send_blob(bytes) {
var binary = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
binary += String.fromCharCode(bytes[i]);
// Send as base64
stream.sendBlob(window.btoa(binary));
}
/**
* The maximum length of any blob sent by this Guacamole.ArrayBufferWriter,
* in bytes. Data sent via
* [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds
* this length will be split into multiple blobs. As the Guacamole protocol
* limits the maximum size of any instruction or instruction element to
* 8192 bytes, and the contents of blobs will be base64-encoded, this value
* should only be increased with extreme caution.
*
* @type {!number}
* @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH}
*/
this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH;
/**
* Sends the given data.
*
* @param {!(ArrayBuffer|TypedArray)} data
* The data to send.
*/
this.sendData = function(data) {
var bytes = new Uint8Array(data);
// If small enough to fit into single instruction, send as-is
if (bytes.length <= guac_writer.blobLength)
__send_blob(bytes);
// Otherwise, send as multiple instructions
else {
for (var offset=0; offset<bytes.length; offset += guac_writer.blobLength)
__send_blob(bytes.subarray(offset, offset + guac_writer.blobLength));
}
};
/**
* Signals that no further text will be sent, effectively closing the
* stream.
*/
this.sendEnd = function() {
stream.sendEnd();
};
/**
* Fired for received data, if acknowledged by the server.
* @event
* @param {!Guacamole.Status} status
* The status of the operation.
*/
this.onack = null;
};
/**
* The default maximum blob length for new Guacamole.ArrayBufferWriter
* instances.
*
* @constant
* @type {!number}
*/
Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH = 6048;

View File

@@ -0,0 +1,79 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Maintains a singleton instance of the Web Audio API AudioContext class,
* instantiating the AudioContext only in response to the first call to
* getAudioContext(), and only if no existing AudioContext instance has been
* provided via the singleton property. Subsequent calls to getAudioContext()
* will return the same instance.
*
* @namespace
*/
Guacamole.AudioContextFactory = {
/**
* A singleton instance of a Web Audio API AudioContext object, or null if
* no instance has yes been created. This property may be manually set if
* you wish to supply your own AudioContext instance, but care must be
* taken to do so as early as possible. Assignments to this property will
* not retroactively affect the value returned by previous calls to
* getAudioContext().
*
* @type {AudioContext}
*/
'singleton' : null,
/**
* Returns a singleton instance of a Web Audio API AudioContext object.
*
* @return {AudioContext}
* A singleton instance of a Web Audio API AudioContext object, or null
* if the Web Audio API is not supported.
*/
'getAudioContext' : function getAudioContext() {
// Fallback to Webkit-specific AudioContext implementation
var AudioContext = window.AudioContext || window.webkitAudioContext;
// Get new AudioContext instance if Web Audio API is supported
if (AudioContext) {
try {
// Create new instance if none yet exists
if (!Guacamole.AudioContextFactory.singleton)
Guacamole.AudioContextFactory.singleton = new AudioContext();
// Return singleton instance
return Guacamole.AudioContextFactory.singleton;
}
catch (e) {
// Do not use Web Audio API if not allowed by browser
}
}
// Web Audio API not supported
return null;
}
};

View File

@@ -0,0 +1,506 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Abstract audio player which accepts, queues and plays back arbitrary audio
* data. It is up to implementations of this class to provide some means of
* handling a provided Guacamole.InputStream. Data received along the provided
* stream is to be played back immediately.
*
* @constructor
*/
Guacamole.AudioPlayer = function AudioPlayer() {
/**
* Notifies this Guacamole.AudioPlayer that all audio up to the current
* point in time has been given via the underlying stream, and that any
* difference in time between queued audio data and the current time can be
* considered latency.
*/
this.sync = function sync() {
// Default implementation - do nothing
};
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.AudioPlayer, and thus will be properly handled
* by Guacamole.AudioPlayer.getInstance().
*
* @param {!string} mimetype
* The mimetype to check.
*
* @returns {!boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.AudioPlayer, false otherwise.
*/
Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) {
return Guacamole.RawAudioPlayer.isSupportedType(mimetype);
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.AudioPlayer, in rough order of priority. Beware that only the core
* mimetypes themselves will be listed. Any mimetype parameters, even required
* ones, will not be included in the list. For example, "audio/L8" is a
* supported raw audio mimetype that is supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {!string[]}
* A list of all mimetypes supported by any built-in Guacamole.AudioPlayer,
* excluding any parameters.
*/
Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() {
return Guacamole.RawAudioPlayer.getSupportedTypes();
};
/**
* Returns an instance of Guacamole.AudioPlayer providing support for the given
* audio format. If support for the given audio format is not available, null
* is returned.
*
* @param {!Guacamole.InputStream} stream
* The Guacamole.InputStream to read audio data from.
*
* @param {!string} mimetype
* The mimetype of the audio data in the provided stream.
*
* @return {Guacamole.AudioPlayer}
* A Guacamole.AudioPlayer instance supporting the given mimetype and
* reading from the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) {
// Use raw audio player if possible
if (Guacamole.RawAudioPlayer.isSupportedType(mimetype))
return new Guacamole.RawAudioPlayer(stream, mimetype);
// No support for given mimetype
return null;
};
/**
* Implementation of Guacamole.AudioPlayer providing support for raw PCM format
* audio. This player relies only on the Web Audio API and does not require any
* browser-level support for its audio formats.
*
* @constructor
* @augments Guacamole.AudioPlayer
* @param {!Guacamole.InputStream} stream
* The Guacamole.InputStream to read audio data from.
*
* @param {!string} mimetype
* The mimetype of the audio data in the provided stream, which must be a
* "audio/L8" or "audio/L16" mimetype with necessary parameters, such as:
* "audio/L16;rate=44100,channels=2".
*/
Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) {
/**
* The format of audio this player will decode.
*
* @private
* @type {Guacamole.RawAudioFormat}
*/
var format = Guacamole.RawAudioFormat.parse(mimetype);
/**
* An instance of a Web Audio API AudioContext object, or null if the
* Web Audio API is not supported.
*
* @private
* @type {AudioContext}
*/
var context = Guacamole.AudioContextFactory.getAudioContext();
/**
* The earliest possible time that the next packet could play without
* overlapping an already-playing packet, in seconds. Note that while this
* value is in seconds, it is not an integer value and has microsecond
* resolution.
*
* @private
* @type {!number}
*/
var nextPacketTime = context.currentTime;
/**
* Guacamole.ArrayBufferReader wrapped around the audio input stream
* provided with this Guacamole.RawAudioPlayer was created.
*
* @private
* @type {!Guacamole.ArrayBufferReader}
*/
var reader = new Guacamole.ArrayBufferReader(stream);
/**
* The minimum size of an audio packet split by splitAudioPacket(), in
* seconds. Audio packets smaller than this will not be split, nor will the
* split result of a larger packet ever be smaller in size than this
* minimum.
*
* @private
* @constant
* @type {!number}
*/
var MIN_SPLIT_SIZE = 0.02;
/**
* The maximum amount of latency to allow between the buffered data stream
* and the playback position, in seconds. Initially, this is set to
* roughly one third of a second.
*
* @private
* @type {!number}
*/
var maxLatency = 0.3;
/**
* The type of typed array that will be used to represent each audio packet
* internally. This will be either Int8Array or Int16Array, depending on
* whether the raw audio format is 8-bit or 16-bit.
*
* @private
* @constructor
*/
var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
/**
* The maximum absolute value of any sample within a raw audio packet
* received by this audio player. This depends only on the size of each
* sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio.
*
* @private
* @type {!number}
*/
var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
/**
* The queue of all pending audio packets, as an array of sample arrays.
* Audio packets which are pending playback will be added to this queue for
* further manipulation prior to scheduling via the Web Audio API. Once an
* audio packet leaves this queue and is scheduled via the Web Audio API,
* no further modifications can be made to that packet.
*
* @private
* @type {!SampleArray[]}
*/
var packetQueue = [];
/**
* Given an array of audio packets, returns a single audio packet
* containing the concatenation of those packets.
*
* @private
* @param {!SampleArray[]} packets
* The array of audio packets to concatenate.
*
* @returns {SampleArray}
* A single audio packet containing the concatenation of all given
* audio packets. If no packets are provided, this will be undefined.
*/
var joinAudioPackets = function joinAudioPackets(packets) {
// Do not bother joining if one or fewer packets are in the queue
if (packets.length <= 1)
return packets[0];
// Determine total sample length of the entire queue
var totalLength = 0;
packets.forEach(function addPacketLengths(packet) {
totalLength += packet.length;
});
// Append each packet within queue
var offset = 0;
var joined = new SampleArray(totalLength);
packets.forEach(function appendPacket(packet) {
joined.set(packet, offset);
offset += packet.length;
});
return joined;
};
/**
* Given a single packet of audio data, splits off an arbitrary length of
* audio data from the beginning of that packet, returning the split result
* as an array of two packets. The split location is determined through an
* algorithm intended to minimize the liklihood of audible clicking between
* packets. If no such split location is possible, an array containing only
* the originally-provided audio packet is returned.
*
* @private
* @param {!SampleArray} data
* The audio packet to split.
*
* @returns {!SampleArray[]}
* An array of audio packets containing the result of splitting the
* provided audio packet. If splitting is possible, this array will
* contain two packets. If splitting is not possible, this array will
* contain only the originally-provided packet.
*/
var splitAudioPacket = function splitAudioPacket(data) {
var minValue = Number.MAX_VALUE;
var optimalSplitLength = data.length;
// Calculate number of whole samples in the provided audio packet AND
// in the minimum possible split packet
var samples = Math.floor(data.length / format.channels);
var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE);
// Calculate the beginning of the "end" of the audio packet
var start = Math.max(
format.channels * minSplitSamples,
format.channels * (samples - minSplitSamples)
);
// For all samples at the end of the given packet, find a point where
// the perceptible volume across all channels is lowest (and thus is
// the optimal point to split)
for (var offset = start; offset < data.length; offset += format.channels) {
// Calculate the sum of all values across all channels (the result
// will be proportional to the average volume of a sample)
var totalValue = 0;
for (var channel = 0; channel < format.channels; channel++) {
totalValue += Math.abs(data[offset + channel]);
}
// If this is the smallest average value thus far, set the split
// length such that the first packet ends with the current sample
if (totalValue <= minValue) {
optimalSplitLength = offset + format.channels;
minValue = totalValue;
}
}
// If packet is not split, return the supplied packet untouched
if (optimalSplitLength === data.length)
return [data];
// Otherwise, split the packet into two new packets according to the
// calculated optimal split length
return [
new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)),
new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample))
];
};
/**
* Pushes the given packet of audio data onto the playback queue. Unlike
* other private functions within Guacamole.RawAudioPlayer, the type of the
* ArrayBuffer packet of audio data here need not be specific to the type
* of audio (as with SampleArray). The ArrayBuffer type provided by a
* Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary
* conversions will be performed automatically internally.
*
* @private
* @param {!ArrayBuffer} data
* A raw packet of audio data that should be pushed onto the audio
* playback queue.
*/
var pushAudioPacket = function pushAudioPacket(data) {
packetQueue.push(new SampleArray(data));
};
/**
* Shifts off and returns a packet of audio data from the beginning of the
* playback queue. The length of this audio packet is determined
* dynamically according to the click-reduction algorithm implemented by
* splitAudioPacket().
*
* @private
* @returns {SampleArray}
* A packet of audio data pulled from the beginning of the playback
* queue. If there is no audio currently in the playback queue, this
* will be null.
*/
var shiftAudioPacket = function shiftAudioPacket() {
// Flatten data in packet queue
var data = joinAudioPackets(packetQueue);
if (!data)
return null;
// Pull an appropriate amount of data from the front of the queue
packetQueue = splitAudioPacket(data);
data = packetQueue.shift();
return data;
};
/**
* Converts the given audio packet into an AudioBuffer, ready for playback
* by the Web Audio API. Unlike the raw audio packets received by this
* audio player, AudioBuffers require floating point samples and are split
* into isolated planes of channel-specific data.
*
* @private
* @param {!SampleArray} data
* The raw audio packet that should be converted into a Web Audio API
* AudioBuffer.
*
* @returns {!AudioBuffer}
* A new Web Audio API AudioBuffer containing the provided audio data,
* converted to the format used by the Web Audio API.
*/
var toAudioBuffer = function toAudioBuffer(data) {
// Calculate total number of samples
var samples = data.length / format.channels;
// Determine exactly when packet CAN play
var packetTime = context.currentTime;
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// Get audio buffer for specified format
var audioBuffer = context.createBuffer(format.channels, samples, format.rate);
// Convert each channel
for (var channel = 0; channel < format.channels; channel++) {
var audioData = audioBuffer.getChannelData(channel);
// Fill audio buffer with data for channel
var offset = channel;
for (var i = 0; i < samples; i++) {
audioData[i] = data[offset] / maxSampleValue;
offset += format.channels;
}
}
return audioBuffer;
};
// Defer playback of received audio packets slightly
reader.ondata = function playReceivedAudio(data) {
// Push received samples onto queue
pushAudioPacket(new SampleArray(data));
// Shift off an arbitrary packet of audio data from the queue (this may
// be different in size from the packet just pushed)
var packet = shiftAudioPacket();
if (!packet)
return;
// Determine exactly when packet CAN play
var packetTime = context.currentTime;
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// Set up buffer source
var source = context.createBufferSource();
source.connect(context.destination);
// Use noteOn() instead of start() if necessary
if (!source.start)
source.start = source.noteOn;
// Schedule packet
source.buffer = toAudioBuffer(packet);
source.start(nextPacketTime);
// Update timeline by duration of scheduled packet
nextPacketTime += packet.length / format.channels / format.rate;
};
/** @override */
this.sync = function sync() {
// Calculate elapsed time since last sync
var now = context.currentTime;
// Reschedule future playback time such that playback latency is
// bounded within a reasonable latency threshold
nextPacketTime = Math.min(nextPacketTime, now + maxLatency);
};
};
Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer();
/**
* Determines whether the given mimetype is supported by
* Guacamole.RawAudioPlayer.
*
* @param {!string} mimetype
* The mimetype to check.
*
* @returns {!boolean}
* true if the given mimetype is supported by Guacamole.RawAudioPlayer,
* false otherwise.
*/
Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return false;
return Guacamole.RawAudioFormat.parse(mimetype) !== null;
};
/**
* Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only
* the core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a raw audio mimetype that may be supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {!string[]}
* A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding
* any parameters. If the necessary JavaScript APIs for playing raw audio
* are absent, this list will be empty.
*/
Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return [];
// We support 8-bit and 16-bit raw PCM
return [
'audio/L8',
'audio/L16'
];
};

View File

@@ -0,0 +1,604 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Abstract audio recorder which streams arbitrary audio data to an underlying
* Guacamole.OutputStream. It is up to implementations of this class to provide
* some means of handling this Guacamole.OutputStream. Data produced by the
* recorder is to be sent along the provided stream immediately.
*
* @constructor
*/
Guacamole.AudioRecorder = function AudioRecorder() {
/**
* Callback which is invoked when the audio recording process has stopped
* and the underlying Guacamole stream has been closed normally. Audio will
* only resume recording if a new Guacamole.AudioRecorder is started. This
* Guacamole.AudioRecorder instance MAY NOT be reused.
*
* @event
*/
this.onclose = null;
/**
* Callback which is invoked when the audio recording process cannot
* continue due to an error, if it has started at all. The underlying
* Guacamole stream is automatically closed. Future attempts to record
* audio should not be made, and this Guacamole.AudioRecorder instance
* MAY NOT be reused.
*
* @event
*/
this.onerror = null;
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.AudioRecorder, and thus will be properly handled
* by Guacamole.AudioRecorder.getInstance().
*
* @param {!string} mimetype
* The mimetype to check.
*
* @returns {!boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.AudioRecorder, false otherwise.
*/
Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) {
return Guacamole.RawAudioRecorder.isSupportedType(mimetype);
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.AudioRecorder, in rough order of priority. Beware that only the
* core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a supported raw audio mimetype that is supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {!string[]}
* A list of all mimetypes supported by any built-in
* Guacamole.AudioRecorder, excluding any parameters.
*/
Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() {
return Guacamole.RawAudioRecorder.getSupportedTypes();
};
/**
* Returns an instance of Guacamole.AudioRecorder providing support for the
* given audio format. If support for the given audio format is not available,
* null is returned.
*
* @param {!Guacamole.OutputStream} stream
* The Guacamole.OutputStream to send audio data through.
*
* @param {!string} mimetype
* The mimetype of the audio data to be sent along the provided stream.
*
* @return {Guacamole.AudioRecorder}
* A Guacamole.AudioRecorder instance supporting the given mimetype and
* writing to the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) {
// Use raw audio recorder if possible
if (Guacamole.RawAudioRecorder.isSupportedType(mimetype))
return new Guacamole.RawAudioRecorder(stream, mimetype);
// No support for given mimetype
return null;
};
/**
* Implementation of Guacamole.AudioRecorder providing support for raw PCM
* format audio. This recorder relies only on the Web Audio API and does not
* require any browser-level support for its audio formats.
*
* @constructor
* @augments Guacamole.AudioRecorder
* @param {!Guacamole.OutputStream} stream
* The Guacamole.OutputStream to write audio data to.
*
* @param {!string} mimetype
* The mimetype of the audio data to send along the provided stream, which
* must be a "audio/L8" or "audio/L16" mimetype with necessary parameters,
* such as: "audio/L16;rate=44100,channels=2".
*/
Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) {
/**
* Reference to this RawAudioRecorder.
*
* @private
* @type {!Guacamole.RawAudioRecorder}
*/
var recorder = this;
/**
* The size of audio buffer to request from the Web Audio API when
* recording or processing audio, in sample-frames. This must be a power of
* two between 256 and 16384 inclusive, as required by
* AudioContext.createScriptProcessor().
*
* @private
* @constant
* @type {!number}
*/
var BUFFER_SIZE = 2048;
/**
* The window size to use when applying Lanczos interpolation, commonly
* denoted by the variable "a".
* See: https://en.wikipedia.org/wiki/Lanczos_resampling
*
* @private
* @contant
* @type {!number}
*/
var LANCZOS_WINDOW_SIZE = 3;
/**
* The format of audio this recorder will encode.
*
* @private
* @type {Guacamole.RawAudioFormat}
*/
var format = Guacamole.RawAudioFormat.parse(mimetype);
/**
* An instance of a Web Audio API AudioContext object, or null if the
* Web Audio API is not supported.
*
* @private
* @type {AudioContext}
*/
var context = Guacamole.AudioContextFactory.getAudioContext();
// Some browsers do not implement navigator.mediaDevices - this
// shims in this functionality to ensure code compatibility.
if (!navigator.mediaDevices)
navigator.mediaDevices = {};
// Browsers that either do not implement navigator.mediaDevices
// at all or do not implement it completely need the getUserMedia
// method defined. This shims in this function by detecting
// one of the supported legacy methods.
if (!navigator.mediaDevices.getUserMedia)
navigator.mediaDevices.getUserMedia = (navigator.getUserMedia
|| navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia
|| navigator.msGetUserMedia).bind(navigator);
/**
* Guacamole.ArrayBufferWriter wrapped around the audio output stream
* provided when this Guacamole.RawAudioRecorder was created.
*
* @private
* @type {!Guacamole.ArrayBufferWriter}
*/
var writer = new Guacamole.ArrayBufferWriter(stream);
/**
* The type of typed array that will be used to represent each audio packet
* internally. This will be either Int8Array or Int16Array, depending on
* whether the raw audio format is 8-bit or 16-bit.
*
* @private
* @constructor
*/
var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
/**
* The maximum absolute value of any sample within a raw audio packet sent
* by this audio recorder. This depends only on the size of each sample,
* and will be 128 for 8-bit audio and 32768 for 16-bit audio.
*
* @private
* @type {!number}
*/
var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
/**
* The total number of audio samples read from the local audio input device
* over the life of this audio recorder.
*
* @private
* @type {!number}
*/
var readSamples = 0;
/**
* The total number of audio samples written to the underlying Guacamole
* connection over the life of this audio recorder.
*
* @private
* @type {!number}
*/
var writtenSamples = 0;
/**
* The audio stream provided by the browser, if allowed. If no stream has
* yet been received, this will be null.
*
* @private
* @type {MediaStream}
*/
var mediaStream = null;
/**
* The source node providing access to the local audio input device.
*
* @private
* @type {MediaStreamAudioSourceNode}
*/
var source = null;
/**
* The script processing node which receives audio input from the media
* stream source node as individual audio buffers.
*
* @private
* @type {ScriptProcessorNode}
*/
var processor = null;
/**
* The normalized sinc function. The normalized sinc function is defined as
* 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x.
*
* See: https://en.wikipedia.org/wiki/Sinc_function
*
* @private
* @param {!number} x
* The point at which the normalized sinc function should be computed.
*
* @returns {!number}
* The value of the normalized sinc function at x.
*/
var sinc = function sinc(x) {
// The value of sinc(0) is defined as 1
if (x === 0)
return 1;
// Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x)
var piX = Math.PI * x;
return Math.sin(piX) / piX;
};
/**
* Calculates the value of the Lanczos kernal at point x for a given window
* size. See: https://en.wikipedia.org/wiki/Lanczos_resampling
*
* @private
* @param {!number} x
* The point at which the value of the Lanczos kernel should be
* computed.
*
* @param {!number} a
* The window size to use for the Lanczos kernel.
*
* @returns {!number}
* The value of the Lanczos kernel at the given point for the given
* window size.
*/
var lanczos = function lanczos(x, a) {
// Lanczos is sinc(x) * sinc(x / a) for -a < x < a ...
if (-a < x && x < a)
return sinc(x) * sinc(x / a);
// ... and 0 otherwise
return 0;
};
/**
* Determines the value of the waveform represented by the audio data at
* the given location. If the value cannot be determined exactly as it does
* not correspond to an exact sample within the audio data, the value will
* be derived through interpolating nearby samples.
*
* @private
* @param {!Float32Array} audioData
* An array of audio data, as returned by AudioBuffer.getChannelData().
*
* @param {!number} t
* The relative location within the waveform from which the value
* should be retrieved, represented as a floating point number between
* 0 and 1 inclusive, where 0 represents the earliest point in time and
* 1 represents the latest.
*
* @returns {!number}
* The value of the waveform at the given location.
*/
var interpolateSample = function getValueAt(audioData, t) {
// Convert [0, 1] range to [0, audioData.length - 1]
var index = (audioData.length - 1) * t;
// Determine the start and end points for the summation used by the
// Lanczos interpolation algorithm (see: https://en.wikipedia.org/wiki/Lanczos_resampling)
var start = Math.floor(index) - LANCZOS_WINDOW_SIZE + 1;
var end = Math.floor(index) + LANCZOS_WINDOW_SIZE;
// Calculate the value of the Lanczos interpolation function for the
// required range
var sum = 0;
for (var i = start; i <= end; i++) {
sum += (audioData[i] || 0) * lanczos(index - i, LANCZOS_WINDOW_SIZE);
}
return sum;
};
/**
* Converts the given AudioBuffer into an audio packet, ready for streaming
* along the underlying output stream. Unlike the raw audio packets used by
* this audio recorder, AudioBuffers require floating point samples and are
* split into isolated planes of channel-specific data.
*
* @private
* @param {!AudioBuffer} audioBuffer
* The Web Audio API AudioBuffer that should be converted to a raw
* audio packet.
*
* @returns {!SampleArray}
* A new raw audio packet containing the audio data from the provided
* AudioBuffer.
*/
var toSampleArray = function toSampleArray(audioBuffer) {
// Track overall amount of data read
var inSamples = audioBuffer.length;
readSamples += inSamples;
// Calculate the total number of samples that should be written as of
// the audio data just received and adjust the size of the output
// packet accordingly
var expectedWrittenSamples = Math.round(readSamples * format.rate / audioBuffer.sampleRate);
var outSamples = expectedWrittenSamples - writtenSamples;
// Update number of samples written
writtenSamples += outSamples;
// Get array for raw PCM storage
var data = new SampleArray(outSamples * format.channels);
// Convert each channel
for (var channel = 0; channel < format.channels; channel++) {
var audioData = audioBuffer.getChannelData(channel);
// Fill array with data from audio buffer channel
var offset = channel;
for (var i = 0; i < outSamples; i++) {
data[offset] = interpolateSample(audioData, i / (outSamples - 1)) * maxSampleValue;
offset += format.channels;
}
}
return data;
};
/**
* getUserMedia() callback which handles successful retrieval of an
* audio stream (successful start of recording).
*
* @private
* @param {!MediaStream} stream
* A MediaStream which provides access to audio data read from the
* user's local audio input device.
*/
var streamReceived = function streamReceived(stream) {
// Create processing node which receives appropriately-sized audio buffers
processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels);
processor.connect(context.destination);
// Send blobs when audio buffers are received
processor.onaudioprocess = function processAudio(e) {
writer.sendData(toSampleArray(e.inputBuffer).buffer);
};
// Connect processing node to user's audio input source
source = context.createMediaStreamSource(stream);
source.connect(processor);
// Attempt to explicitly resume AudioContext, as it may be paused
// by default
if (context.state === 'suspended')
context.resume();
// Save stream for later cleanup
mediaStream = stream;
};
/**
* getUserMedia() callback which handles audio recording denial. The
* underlying Guacamole output stream is closed, and the failure to
* record is noted using onerror.
*
* @private
*/
var streamDenied = function streamDenied() {
// Simply end stream if audio access is not allowed
writer.sendEnd();
// Notify of closure
if (recorder.onerror)
recorder.onerror();
};
/**
* Requests access to the user's microphone and begins capturing audio. All
* received audio data is resampled as necessary and forwarded to the
* Guacamole stream underlying this Guacamole.RawAudioRecorder. This
* function must be invoked ONLY ONCE per instance of
* Guacamole.RawAudioRecorder.
*
* @private
*/
var beginAudioCapture = function beginAudioCapture() {
// Attempt to retrieve an audio input stream from the browser
var promise = navigator.mediaDevices.getUserMedia({
'audio' : true
}, streamReceived, streamDenied);
// Handle stream creation/rejection via Promise for newer versions of
// getUserMedia()
if (promise && promise.then)
promise.then(streamReceived, streamDenied);
};
/**
* Stops capturing audio, if the capture has started, freeing all associated
* resources. If the capture has not started, this function simply ends the
* underlying Guacamole stream.
*
* @private
*/
var stopAudioCapture = function stopAudioCapture() {
// Disconnect media source node from script processor
if (source)
source.disconnect();
// Disconnect associated script processor node
if (processor)
processor.disconnect();
// Stop capture
if (mediaStream) {
var tracks = mediaStream.getTracks();
for (var i = 0; i < tracks.length; i++)
tracks[i].stop();
}
// Remove references to now-unneeded components
processor = null;
source = null;
mediaStream = null;
// End stream
writer.sendEnd();
};
// Once audio stream is successfully open, request and begin reading audio
writer.onack = function audioStreamAcknowledged(status) {
// Begin capture if successful response and not yet started
if (status.code === Guacamole.Status.Code.SUCCESS && !mediaStream)
beginAudioCapture();
// Otherwise stop capture and cease handling any further acks
else {
// Stop capturing audio
stopAudioCapture();
writer.onack = null;
// Notify if stream has closed normally
if (status.code === Guacamole.Status.Code.RESOURCE_CLOSED) {
if (recorder.onclose)
recorder.onclose();
}
// Otherwise notify of closure due to error
else {
if (recorder.onerror)
recorder.onerror();
}
}
};
};
Guacamole.RawAudioRecorder.prototype = new Guacamole.AudioRecorder();
/**
* Determines whether the given mimetype is supported by
* Guacamole.RawAudioRecorder.
*
* @param {!string} mimetype
* The mimetype to check.
*
* @returns {!boolean}
* true if the given mimetype is supported by Guacamole.RawAudioRecorder,
* false otherwise.
*/
Guacamole.RawAudioRecorder.isSupportedType = function isSupportedType(mimetype) {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return false;
return Guacamole.RawAudioFormat.parse(mimetype) !== null;
};
/**
* Returns a list of all mimetypes supported by Guacamole.RawAudioRecorder. Only
* the core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a raw audio mimetype that may be supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {!string[]}
* A list of all mimetypes supported by Guacamole.RawAudioRecorder,
* excluding any parameters. If the necessary JavaScript APIs for recording
* raw audio are absent, this list will be empty.
*/
Guacamole.RawAudioRecorder.getSupportedTypes = function getSupportedTypes() {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return [];
// We support 8-bit and 16-bit raw PCM
return [
'audio/L8',
'audio/L16'
];
};

View File

@@ -0,0 +1,139 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, assembling all
* received blobs into a single blob by appending them to each other in order.
* Note that this object will overwrite any installed event handlers on the
* given Guacamole.InputStream.
*
* @constructor
* @param {!Guacamole.InputStream} stream
* The stream that data will be read from.
*
* @param {!string} mimetype
* The mimetype of the blob being built.
*/
Guacamole.BlobReader = function(stream, mimetype) {
/**
* Reference to this Guacamole.InputStream.
*
* @private
* @type {!Guacamole.BlobReader}
*/
var guac_reader = this;
/**
* The length of this Guacamole.InputStream in bytes.
*
* @private
* @type {!number}
*/
var length = 0;
// Get blob builder
var blob_builder;
if (window.BlobBuilder) blob_builder = new BlobBuilder();
else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder();
else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder();
else
blob_builder = new (function() {
var blobs = [];
/** @ignore */
this.append = function(data) {
blobs.push(new Blob([data], {"type": mimetype}));
};
/** @ignore */
this.getBlob = function() {
return new Blob(blobs, {"type": mimetype});
};
})();
// Append received blobs
stream.onblob = function(data) {
// Convert to ArrayBuffer
var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
blob_builder.append(arrayBuffer);
length += arrayBuffer.byteLength;
// Call handler, if present
if (guac_reader.onprogress)
guac_reader.onprogress(arrayBuffer.byteLength);
// Send success response
stream.sendAck("OK", 0x0000);
};
// Simply call onend when end received
stream.onend = function() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Returns the current length of this Guacamole.InputStream, in bytes.
*
* @return {!number}
* The current length of this Guacamole.InputStream.
*/
this.getLength = function() {
return length;
};
/**
* Returns the contents of this Guacamole.BlobReader as a Blob.
*
* @return {!Blob}
* The contents of this Guacamole.BlobReader.
*/
this.getBlob = function() {
return blob_builder.getBlob();
};
/**
* Fired once for every blob of data received.
*
* @event
* @param {!number} length
* The number of bytes received.
*/
this.onprogress = null;
/**
* Fired once this stream is finished and no further data will be written.
* @event
*/
this.onend = null;
};

View File

@@ -0,0 +1,245 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A writer which automatically writes to the given output stream with the
* contents of provided Blob objects.
*
* @constructor
* @param {!Guacamole.OutputStream} stream
* The stream that data will be written to.
*/
Guacamole.BlobWriter = function BlobWriter(stream) {
/**
* Reference to this Guacamole.BlobWriter.
*
* @private
* @type {!Guacamole.BlobWriter}
*/
var guacWriter = this;
/**
* Wrapped Guacamole.ArrayBufferWriter which will be used to send any
* provided file data.
*
* @private
* @type {!Guacamole.ArrayBufferWriter}
*/
var arrayBufferWriter = new Guacamole.ArrayBufferWriter(stream);
// Initially, simply call onack for acknowledgements
arrayBufferWriter.onack = function(status) {
if (guacWriter.onack)
guacWriter.onack(status);
};
/**
* Browser-independent implementation of Blob.slice() which uses an end
* offset to determine the span of the resulting slice, rather than a
* length.
*
* @private
* @param {!Blob} blob
* The Blob to slice.
*
* @param {!number} start
* The starting offset of the slice, in bytes, inclusive.
*
* @param {!number} end
* The ending offset of the slice, in bytes, exclusive.
*
* @returns {!Blob}
* A Blob containing the data within the given Blob starting at
* <code>start</code> and ending at <code>end - 1</code>.
*/
var slice = function slice(blob, start, end) {
// Use prefixed implementations if necessary
var sliceImplementation = (
blob.slice
|| blob.webkitSlice
|| blob.mozSlice
).bind(blob);
var length = end - start;
// The old Blob.slice() was length-based (not end-based). Try the
// length version first, if the two calls are not equivalent.
if (length !== end) {
// If the result of the slice() call matches the expected length,
// trust that result. It must be correct.
var sliceResult = sliceImplementation(start, length);
if (sliceResult.size === length)
return sliceResult;
}
// Otherwise, use the most-recent standard: end-based slice()
return sliceImplementation(start, end);
};
/**
* Sends the contents of the given blob over the underlying stream.
*
* @param {!Blob} blob
* The blob to send.
*/
this.sendBlob = function sendBlob(blob) {
var offset = 0;
var reader = new FileReader();
/**
* Reads the next chunk of the blob provided to
* [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The chunk itself
* is read asynchronously, and will not be available until
* reader.onload fires.
*
* @private
*/
var readNextChunk = function readNextChunk() {
// If no further chunks remain, inform of completion and stop
if (offset >= blob.size) {
// Fire completion event for completed blob
if (guacWriter.oncomplete)
guacWriter.oncomplete(blob);
// No further chunks to read
return;
}
// Obtain reference to next chunk as a new blob
var chunk = slice(blob, offset, offset + arrayBufferWriter.blobLength);
offset += arrayBufferWriter.blobLength;
// Attempt to read the blob contents represented by the blob into
// a new array buffer
reader.readAsArrayBuffer(chunk);
};
// Send each chunk over the stream, continue reading the next chunk
reader.onload = function chunkLoadComplete() {
// Send the successfully-read chunk
arrayBufferWriter.sendData(reader.result);
// Continue sending more chunks after the latest chunk is
// acknowledged
arrayBufferWriter.onack = function sendMoreChunks(status) {
if (guacWriter.onack)
guacWriter.onack(status);
// Abort transfer if an error occurs
if (status.isError())
return;
// Inform of blob upload progress via progress events
if (guacWriter.onprogress)
guacWriter.onprogress(blob, offset - arrayBufferWriter.blobLength);
// Queue the next chunk for reading
readNextChunk();
};
};
// If an error prevents further reading, inform of error and stop
reader.onerror = function chunkLoadFailed() {
// Fire error event, including the context of the error
if (guacWriter.onerror)
guacWriter.onerror(blob, offset, reader.error);
};
// Begin reading the first chunk
readNextChunk();
};
/**
* Signals that no further text will be sent, effectively closing the
* stream.
*/
this.sendEnd = function sendEnd() {
arrayBufferWriter.sendEnd();
};
/**
* Fired for received data, if acknowledged by the server.
*
* @event
* @param {!Guacamole.Status} status
* The status of the operation.
*/
this.onack = null;
/**
* Fired when an error occurs reading a blob passed to
* [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The transfer for the
* the given blob will cease, but the stream will remain open.
*
* @event
* @param {!Blob} blob
* The blob that was being read when the error occurred.
*
* @param {!number} offset
* The offset of the failed read attempt within the blob, in bytes.
*
* @param {!DOMError} error
* The error that occurred.
*/
this.onerror = null;
/**
* Fired for each successfully-read chunk of data as a blob is being sent
* via [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}.
*
* @event
* @param {!Blob} blob
* The blob that is being read.
*
* @param {!number} offset
* The offset of the read that just succeeded.
*/
this.onprogress = null;
/**
* Fired when a blob passed to
* [sendBlob()]{@link Guacamole.BlobWriter#sendBlob} has finished being
* sent.
*
* @event
* @param {!Blob} blob
* The blob that was sent.
*/
this.oncomplete = null;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, returning
* received blobs as a single data URI built over the course of the stream.
* Note that this object will overwrite any installed event handlers on the
* given Guacamole.InputStream.
*
* @constructor
* @param {!Guacamole.InputStream} stream
* The stream that data will be read from.
*
* @param {!string} mimetype
* The mimetype of the data being received.
*/
Guacamole.DataURIReader = function(stream, mimetype) {
/**
* Reference to this Guacamole.DataURIReader.
*
* @private
* @type {!Guacamole.DataURIReader}
*/
var guac_reader = this;
/**
* Current data URI.
*
* @private
* @type {!string}
*/
var uri = 'data:' + mimetype + ';base64,';
// Receive blobs as array buffers
stream.onblob = function dataURIReaderBlob(data) {
// Currently assuming data will ALWAYS be safe to simply append. This
// will not be true if the received base64 data encodes a number of
// bytes that isn't a multiple of three (as base64 expands in a ratio
// of exactly 3:4).
uri += data;
};
// Simply call onend when end received
stream.onend = function dataURIReaderEnd() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Returns the data URI of all data received through the underlying stream
* thus far.
*
* @returns {!string}
* The data URI of all data received through the underlying stream thus
* far.
*/
this.getURI = function getURI() {
return uri;
};
/**
* Fired once this stream is finished and no further data will be written.
*
* @event
*/
this.onend = null;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* An arbitrary event, emitted by a {@link Guacamole.Event.Target}. This object
* should normally serve as the base class for a different object that is more
* specific to the event type.
*
* @constructor
* @param {!string} type
* The unique name of this event type.
*/
Guacamole.Event = function Event(type) {
/**
* The unique name of this event type.
*
* @type {!string}
*/
this.type = type;
/**
* An arbitrary timestamp in milliseconds, indicating this event's
* position in time relative to other events.
*
* @type {!number}
*/
this.timestamp = new Date().getTime();
/**
* Returns the number of milliseconds elapsed since this event was created.
*
* @return {!number}
* The number of milliseconds elapsed since this event was created.
*/
this.getAge = function getAge() {
return new Date().getTime() - this.timestamp;
};
/**
* Requests that the legacy event handler associated with this event be
* invoked on the given event target. This function will be invoked
* automatically by implementations of {@link Guacamole.Event.Target}
* whenever {@link Guacamole.Event.Target#emit emit()} is invoked.
* <p>
* Older versions of Guacamole relied on single event handlers with the
* prefix "on", such as "onmousedown" or "onkeyup". If a Guacamole.Event
* implementation is replacing the event previously represented by one of
* these handlers, this function gives the implementation the opportunity
* to provide backward compatibility with the old handler.
* <p>
* Unless overridden, this function does nothing.
*
* @param {!Guacamole.Event.Target} eventTarget
* The {@link Guacamole.Event.Target} that emitted this event.
*/
this.invokeLegacyHandler = function invokeLegacyHandler(eventTarget) {
// Do nothing
};
};
/**
* A {@link Guacamole.Event} that may relate to one or more DOM events.
* Continued propagation and default behavior of the related DOM events may be
* prevented with {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()}
* and {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()}
* respectively.
*
* @constructor
* @augments Guacamole.Event
*
* @param {!string} type
* The unique name of this event type.
*
* @param {Event|Event[]} [events=[]]
* The DOM events that are related to this event, if any. Future calls to
* {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} and
* {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} will
* affect these events.
*/
Guacamole.Event.DOMEvent = function DOMEvent(type, events) {
Guacamole.Event.call(this, type);
// Default to empty array
events = events || [];
// Automatically wrap non-array single Event in an array
if (!Array.isArray(events))
events = [ events ];
/**
* Requests that the default behavior of related DOM events be prevented.
* Whether this request will be honored by the browser depends on the
* nature of those events and the timing of the request.
*/
this.preventDefault = function preventDefault() {
events.forEach(function applyPreventDefault(event) {
if (event.preventDefault) event.preventDefault();
event.returnValue = false;
});
};
/**
* Stops further propagation of related events through the DOM. Only events
* that are directly related to this event will be stopped.
*/
this.stopPropagation = function stopPropagation() {
events.forEach(function applyStopPropagation(event) {
event.stopPropagation();
});
};
};
/**
* Convenience function for cancelling all further processing of a given DOM
* event. Invoking this function prevents the default behavior of the event and
* stops any further propagation.
*
* @param {!Event} event
* The DOM event to cancel.
*/
Guacamole.Event.DOMEvent.cancelEvent = function cancelEvent(event) {
event.stopPropagation();
if (event.preventDefault) event.preventDefault();
event.returnValue = false;
};
/**
* An object which can dispatch {@link Guacamole.Event} objects. Listeners
* registered with {@link Guacamole.Event.Target#on on()} will automatically
* be invoked based on the type of {@link Guacamole.Event} passed to
* {@link Guacamole.Event.Target#dispatch dispatch()}. It is normally
* subclasses of Guacamole.Event.Target that will dispatch events, and usages
* of those subclasses that will catch dispatched events with on().
*
* @constructor
*/
Guacamole.Event.Target = function Target() {
/**
* A callback function which handles an event dispatched by an event
* target.
*
* @callback Guacamole.Event.Target~listener
* @param {!Guacamole.Event} event
* The event that was dispatched.
*
* @param {!Guacamole.Event.Target} target
* The object that dispatched the event.
*/
/**
* All listeners (callback functions) registered for each event type passed
* to {@link Guacamole.Event.Targer#on on()}.
*
* @private
* @type {!Object.<string, Guacamole.Event.Target~listener[]>}
*/
var listeners = {};
/**
* Registers a listener for events having the given type, as dictated by
* the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
* provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
*
* @param {!string} type
* The unique name of this event type.
*
* @param {!Guacamole.Event.Target~listener} listener
* The function to invoke when an event having the given type is
* dispatched. The {@link Guacamole.Event} object provided to
* {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
* this function, along with the dispatching Guacamole.Event.Target.
*/
this.on = function on(type, listener) {
var relevantListeners = listeners[type];
if (!relevantListeners)
listeners[type] = relevantListeners = [];
relevantListeners.push(listener);
};
/**
* Registers a listener for events having the given types, as dictated by
* the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
* provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
* <p>
* Invoking this function is equivalent to manually invoking
* {@link Guacamole.Event.Target#on on()} for each of the provided types.
*
* @param {!string[]} types
* The unique names of the event types to associate with the given
* listener.
*
* @param {!Guacamole.Event.Target~listener} listener
* The function to invoke when an event having any of the given types
* is dispatched. The {@link Guacamole.Event} object provided to
* {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
* this function, along with the dispatching Guacamole.Event.Target.
*/
this.onEach = function onEach(types, listener) {
types.forEach(function addListener(type) {
this.on(type, listener);
}, this);
};
/**
* Dispatches the given event, invoking all event handlers registered with
* this Guacamole.Event.Target for that event's
* {@link Guacamole.Event#type type}.
*
* @param {!Guacamole.Event} event
* The event to dispatch.
*/
this.dispatch = function dispatch(event) {
// Invoke any relevant legacy handler for the event
event.invokeLegacyHandler(this);
// Invoke all registered listeners
var relevantListeners = listeners[event.type];
if (relevantListeners) {
for (var i = 0; i < relevantListeners.length; i++) {
relevantListeners[i](event, this);
}
}
};
/**
* Unregisters a listener that was previously registered with
* {@link Guacamole.Event.Target#on on()} or
* {@link Guacamole.Event.Target#onEach onEach()}. If no such listener was
* registered, this function has no effect. If multiple copies of the same
* listener were registered, the first listener still registered will be
* removed.
*
* @param {!string} type
* The unique name of the event type handled by the listener being
* removed.
*
* @param {!Guacamole.Event.Target~listener} listener
* The listener function previously provided to
* {@link Guacamole.Event.Target#on on()}or
* {@link Guacamole.Event.Target#onEach onEach()}.
*
* @returns {!boolean}
* true if the specified listener was removed, false otherwise.
*/
this.off = function off(type, listener) {
var relevantListeners = listeners[type];
if (!relevantListeners)
return false;
for (var i = 0; i < relevantListeners.length; i++) {
if (relevantListeners[i] === listener) {
relevantListeners.splice(i, 1);
return true;
}
}
return false;
};
/**
* Unregisters listeners that were previously registered with
* {@link Guacamole.Event.Target#on on()} or
* {@link Guacamole.Event.Target#onEach onEach()}. If no such listeners
* were registered, this function has no effect. If multiple copies of the
* same listener were registered for the same event type, the first
* listener still registered will be removed.
* <p>
* Invoking this function is equivalent to manually invoking
* {@link Guacamole.Event.Target#off off()} for each of the provided types.
*
* @param {!string[]} types
* The unique names of the event types handled by the listeners being
* removed.
*
* @param {!Guacamole.Event.Target~listener} listener
* The listener function previously provided to
* {@link Guacamole.Event.Target#on on()} or
* {@link Guacamole.Event.Target#onEach onEach()}.
*
* @returns {!boolean}
* true if any of the specified listeners were removed, false
* otherwise.
*/
this.offEach = function offEach(types, listener) {
var changed = false;
types.forEach(function removeListener(type) {
changed |= this.off(type, listener);
}, this);
return changed;
};
};

View File

@@ -0,0 +1,129 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A hidden input field which attempts to keep itself focused at all times,
* except when another input field has been intentionally focused, whether
* programatically or by the user. The actual underlying input field, returned
* by getElement(), may be used as a reliable source of keyboard-related events,
* particularly composition and input events which may require a focused input
* field to be dispatched at all.
*
* @constructor
*/
Guacamole.InputSink = function InputSink() {
/**
* Reference to this instance of Guacamole.InputSink.
*
* @private
* @type {!Guacamole.InputSink}
*/
var sink = this;
/**
* The underlying input field, styled to be invisible.
*
* @private
* @type {!Element}
*/
var field = document.createElement('textarea');
field.style.position = 'fixed';
field.style.outline = 'none';
field.style.border = 'none';
field.style.margin = '0';
field.style.padding = '0';
field.style.height = '0';
field.style.width = '0';
field.style.left = '0';
field.style.bottom = '0';
field.style.resize = 'none';
field.style.background = 'transparent';
field.style.color = 'transparent';
// Keep field clear when modified via normal keypresses
field.addEventListener("keypress", function clearKeypress(e) {
field.value = '';
}, false);
// Keep field clear when modofied via composition events
field.addEventListener("compositionend", function clearCompletedComposition(e) {
if (e.data)
field.value = '';
}, false);
// Keep field clear when modofied via input events
field.addEventListener("input", function clearCompletedInput(e) {
if (e.data && !e.isComposing)
field.value = '';
}, false);
// Whenever focus is gained, automatically click to ensure cursor is
// actually placed within the field (the field may simply be highlighted or
// outlined otherwise)
field.addEventListener("focus", function focusReceived() {
window.setTimeout(function deferRefocus() {
field.click();
field.select();
}, 0);
}, true);
/**
* Attempts to focus the underlying input field. The focus attempt occurs
* asynchronously, and may silently fail depending on browser restrictions.
*/
this.focus = function focus() {
window.setTimeout(function deferRefocus() {
field.focus(); // Focus must be deferred to work reliably across browsers
}, 0);
};
/**
* Returns the underlying input field. This input field MUST be manually
* added to the DOM for the Guacamole.InputSink to have any effect.
*
* @returns {!Element}
* The underlying input field.
*/
this.getElement = function getElement() {
return field;
};
// Automatically refocus input sink if part of DOM
document.addEventListener("keydown", function refocusSink(e) {
// Do not refocus if focus is on an input field
var focused = document.activeElement;
if (focused && focused !== document.body) {
// Only consider focused input fields which are actually visible
var rect = focused.getBoundingClientRect();
if (rect.left + rect.width > 0 && rect.top + rect.height > 0)
return;
}
// Refocus input sink instead of handling click
sink.focus();
}, true);
};

View File

@@ -0,0 +1,141 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* An input stream abstraction used by the Guacamole client to facilitate
* transfer of files or other binary data.
*
* @constructor
* @param {!Guacamole.Client} client
* The client owning this stream.
*
* @param {!number} index
* The index of this stream.
*/
Guacamole.InputStream = function(client, index) {
/**
* Reference to this stream.
*
* @private
* @type {!Guacamole.InputStream}
*/
var guac_stream = this;
/**
* The index of this stream.
*
* @type {!number}
*/
this.index = index;
/**
* Called when a blob of data is received.
*
* @event
* @param {!string} data
* The received base64 data.
*/
this.onblob = null;
/**
* Called when this stream is closed.
*
* @event
*/
this.onend = null;
/**
* Acknowledges the receipt of a blob.
*
* @param {!string} message
* A human-readable message describing the error or status.
*
* @param {!number} code
* The error code, if any, or 0 for success.
*/
this.sendAck = function(message, code) {
client.sendAck(guac_stream.index, message, code);
};
/**
* Creates a new ReadableStream that receives the data sent to this stream
* by the Guacamole server. This function may be invoked at most once per
* stream, and invoking this function will overwrite any installed event
* handlers on this stream.
*
* A ReadableStream is a JavaScript object defined by the "Streams"
* standard. It is supported by most browsers, but not necessarily all
* browsers. The caller should verify this support is present before
* invoking this function. The behavior of this function when the browser
* does not support ReadableStream is not defined.
*
* @see {@link https://streams.spec.whatwg.org/#rs-class}
*
* @returns {!ReadableStream}
* A new ReadableStream that receives the bytes sent along this stream
* by the Guacamole server.
*/
this.toReadableStream = function toReadableStream() {
return new ReadableStream({
type: 'bytes',
start: function startStream(controller) {
var reader = new Guacamole.ArrayBufferReader(guac_stream);
// Provide any received blocks of data to the ReadableStream
// controller, such that they will be read by whatever is
// consuming the ReadableStream
reader.ondata = function dataReceived(data) {
if (controller.byobRequest) {
var view = controller.byobRequest.view;
var length = Math.min(view.byteLength, data.byteLength);
var byobBlock = new Uint8Array(data, 0, length);
view.buffer.set(byobBlock);
controller.byobRequest.respond(length);
if (length < data.byteLength) {
controller.enqueue(data.slice(length));
}
}
else {
controller.enqueue(new Uint8Array(data));
}
};
// Notify the ReadableStream when the end of the stream is
// reached
reader.onend = function dataComplete() {
controller.close();
};
}
});
};
};

View File

@@ -0,0 +1,79 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Integer pool which returns consistently increasing integers while integers
* are in use, and previously-used integers when possible.
* @constructor
*/
Guacamole.IntegerPool = function() {
/**
* Reference to this integer pool.
*
* @private
*/
var guac_pool = this;
/**
* Array of available integers.
*
* @private
* @type {!number[]}
*/
var pool = [];
/**
* The next integer to return if no more integers remain.
*
* @type {!number}
*/
this.next_int = 0;
/**
* Returns the next available integer in the pool. If possible, a previously
* used integer will be returned.
*
* @return {!number}
* The next available integer.
*/
this.next = function() {
// If free'd integers exist, return one of those
if (pool.length > 0)
return pool.shift();
// Otherwise, return a new integer
return guac_pool.next_int++;
};
/**
* Frees the given integer, allowing it to be reused.
*
* @param {!number} integer
* The integer to free.
*/
this.free = function(integer) {
pool.push(integer);
};
};

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.
*/
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, assembling all
* received blobs into a JavaScript object by appending them to each other, in
* order, and decoding the result as JSON. Note that this object will overwrite
* any installed event handlers on the given Guacamole.InputStream.
*
* @constructor
* @param {Guacamole.InputStream} stream
* The stream that JSON will be read from.
*/
Guacamole.JSONReader = function guacamoleJSONReader(stream) {
/**
* Reference to this Guacamole.JSONReader.
*
* @private
* @type {!Guacamole.JSONReader}
*/
var guacReader = this;
/**
* Wrapped Guacamole.StringReader.
*
* @private
* @type {!Guacamole.StringReader}
*/
var stringReader = new Guacamole.StringReader(stream);
/**
* All JSON read thus far.
*
* @private
* @type {!string}
*/
var json = '';
/**
* Returns the current length of this Guacamole.JSONReader, in characters.
*
* @return {!number}
* The current length of this Guacamole.JSONReader.
*/
this.getLength = function getLength() {
return json.length;
};
/**
* Returns the contents of this Guacamole.JSONReader as a JavaScript
* object.
*
* @return {object}
* The contents of this Guacamole.JSONReader, as parsed from the JSON
* contents of the input stream.
*/
this.getJSON = function getJSON() {
return JSON.parse(json);
};
// Append all received text
stringReader.ontext = function ontext(text) {
// Append received text
json += text;
// Call handler, if present
if (guacReader.onprogress)
guacReader.onprogress(text.length);
};
// Simply call onend when end received
stringReader.onend = function onend() {
if (guacReader.onend)
guacReader.onend();
};
/**
* Fired once for every blob of data received.
*
* @event
* @param {!number} length
* The number of characters received.
*/
this.onprogress = null;
/**
* Fired once this stream is finished and no further data will be written.
*
* @event
*/
this.onend = null;
};

View File

@@ -0,0 +1,335 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* An object that will accept raw key events and produce a chronologically
* ordered array of key event objects. These events can be obtained by
* calling getEvents().
*
* @constructor
* @param {number} [startTimestamp=0]
* The starting timestamp for the recording being intepreted. If provided,
* the timestamp of each intepreted event will be relative to this timestamp.
* If not provided, the raw recording timestamp will be used.
*/
Guacamole.KeyEventInterpreter = function KeyEventInterpreter(startTimestamp) {
// Default to 0 seconds to keep the raw timestamps
if (startTimestamp === undefined || startTimestamp === null)
startTimestamp = 0;
/**
* A precursor array to the KNOWN_KEYS map. The objects contained within
* will be constructed into full KeyDefinition objects.
*
* @constant
* @private
* @type {Object[]}
*/
var _KNOWN_KEYS = [
{keysym: 0xFE03, name: 'AltGr' },
{keysym: 0xFF08, name: 'Backspace' },
{keysym: 0xFF09, name: 'Tab' },
{keysym: 0xFF0B, name: 'Clear' },
{keysym: 0xFF0D, name: 'Return', value: "\n" },
{keysym: 0xFF13, name: 'Pause' },
{keysym: 0xFF14, name: 'Scroll' },
{keysym: 0xFF15, name: 'SysReq' },
{keysym: 0xFF1B, name: 'Escape' },
{keysym: 0xFF50, name: 'Home' },
{keysym: 0xFF51, name: 'Left' },
{keysym: 0xFF52, name: 'Up' },
{keysym: 0xFF53, name: 'Right' },
{keysym: 0xFF54, name: 'Down' },
{keysym: 0xFF55, name: 'Page Up' },
{keysym: 0xFF56, name: 'Page Down' },
{keysym: 0xFF57, name: 'End' },
{keysym: 0xFF63, name: 'Insert' },
{keysym: 0xFF65, name: 'Undo' },
{keysym: 0xFF6A, name: 'Help' },
{keysym: 0xFF7F, name: 'Num' },
{keysym: 0xFF80, name: 'Space', value: " " },
{keysym: 0xFF8D, name: 'Enter', value: "\n" },
{keysym: 0xFF95, name: 'Home' },
{keysym: 0xFF96, name: 'Left' },
{keysym: 0xFF97, name: 'Up' },
{keysym: 0xFF98, name: 'Right' },
{keysym: 0xFF99, name: 'Down' },
{keysym: 0xFF9A, name: 'Page Up' },
{keysym: 0xFF9B, name: 'Page Down' },
{keysym: 0xFF9C, name: 'End' },
{keysym: 0xFF9E, name: 'Insert' },
{keysym: 0xFFAA, name: '*', value: "*" },
{keysym: 0xFFAB, name: '+', value: "+" },
{keysym: 0xFFAD, name: '-', value: "-" },
{keysym: 0xFFAE, name: '.', value: "." },
{keysym: 0xFFAF, name: '/', value: "/" },
{keysym: 0xFFB0, name: '0', value: "0" },
{keysym: 0xFFB1, name: '1', value: "1" },
{keysym: 0xFFB2, name: '2', value: "2" },
{keysym: 0xFFB3, name: '3', value: "3" },
{keysym: 0xFFB4, name: '4', value: "4" },
{keysym: 0xFFB5, name: '5', value: "5" },
{keysym: 0xFFB6, name: '6', value: "6" },
{keysym: 0xFFB7, name: '7', value: "7" },
{keysym: 0xFFB8, name: '8', value: "8" },
{keysym: 0xFFB9, name: '9', value: "9" },
{keysym: 0xFFBE, name: 'F1' },
{keysym: 0xFFBF, name: 'F2' },
{keysym: 0xFFC0, name: 'F3' },
{keysym: 0xFFC1, name: 'F4' },
{keysym: 0xFFC2, name: 'F5' },
{keysym: 0xFFC3, name: 'F6' },
{keysym: 0xFFC4, name: 'F7' },
{keysym: 0xFFC5, name: 'F8' },
{keysym: 0xFFC6, name: 'F9' },
{keysym: 0xFFC7, name: 'F10' },
{keysym: 0xFFC8, name: 'F11' },
{keysym: 0xFFC9, name: 'F12' },
{keysym: 0xFFCA, name: 'F13' },
{keysym: 0xFFCB, name: 'F14' },
{keysym: 0xFFCC, name: 'F15' },
{keysym: 0xFFCD, name: 'F16' },
{keysym: 0xFFCE, name: 'F17' },
{keysym: 0xFFCF, name: 'F18' },
{keysym: 0xFFD0, name: 'F19' },
{keysym: 0xFFD1, name: 'F20' },
{keysym: 0xFFD2, name: 'F21' },
{keysym: 0xFFD3, name: 'F22' },
{keysym: 0xFFD4, name: 'F23' },
{keysym: 0xFFD5, name: 'F24' },
{keysym: 0xFFE1, name: 'Shift' },
{keysym: 0xFFE2, name: 'Shift' },
{keysym: 0xFFE3, name: 'Ctrl' },
{keysym: 0xFFE4, name: 'Ctrl' },
{keysym: 0xFFE5, name: 'Caps' },
{keysym: 0xFFE7, name: 'Meta' },
{keysym: 0xFFE8, name: 'Meta' },
{keysym: 0xFFE9, name: 'Alt' },
{keysym: 0xFFEA, name: 'Alt' },
{keysym: 0xFFEB, name: 'Super' },
{keysym: 0xFFEC, name: 'Super' },
{keysym: 0xFFED, name: 'Hyper' },
{keysym: 0xFFEE, name: 'Hyper' },
{keysym: 0xFFFF, name: 'Delete' }
];
/**
* All known keys, as a map of X11 keysym to KeyDefinition.
*
* @constant
* @private
* @type {Object.<String, KeyDefinition>}
*/
var KNOWN_KEYS = {};
_KNOWN_KEYS.forEach(function createKeyDefinitionMap(keyDefinition) {
// Construct a map of keysym to KeyDefinition object
KNOWN_KEYS[keyDefinition.keysym] = (
new Guacamole.KeyEventInterpreter.KeyDefinition(keyDefinition));
});
/**
* All key events parsed as of the most recent handleKeyEvent() invocation.
*
* @private
* @type {!Guacamole.KeyEventInterpreter.KeyEvent[]}
*/
var parsedEvents = [];
/**
* If the provided keysym corresponds to a valid UTF-8 character, return
* a KeyDefinition for that keysym. Otherwise, return null.
*
* @private
* @param {Number} keysym
* The keysym to produce a UTF-8 KeyDefinition for, if valid.
*
* @returns {Guacamole.KeyEventInterpreter.KeyDefinition}
* A KeyDefinition for the provided keysym, if it's a valid UTF-8
* keysym, or null otherwise.
*/
function getUnicodeKeyDefinition(keysym) {
// Translate only if keysym maps to Unicode
if (keysym < 0x00 || (keysym > 0xFF && (keysym | 0xFFFF) != 0x0100FFFF))
return null;
// Convert to UTF8 string
var codepoint = keysym & 0xFFFF;
var name = String.fromCharCode(codepoint);
// Create and return the definition
return new Guacamole.KeyEventInterpreter.KeyDefinition({
keysym: keysym, name: name, value: name});
}
/**
* Return a KeyDefinition corresponding to the provided keysym.
*
* @private
* @param {Number} keysym
* The keysym to return a KeyDefinition for.
*
* @returns {KeyDefinition}
* A KeyDefinition corresponding to the provided keysym.
*/
function getKeyDefinitionByKeysym(keysym) {
// If it's a known type, return the existing definition
if (keysym in KNOWN_KEYS)
return KNOWN_KEYS[keysym];
// Return a UTF-8 KeyDefinition, if valid
var definition = getUnicodeKeyDefinition(keysym);
if (definition != null)
return definition;
// If it's not UTF-8, return an unknown definition, with the name
// just set to the hex value of the keysym
return new Guacamole.KeyEventInterpreter.KeyDefinition({
keysym: keysym,
name: '0x' + String(keysym.toString(16))
})
}
/**
* Handles a raw key event, appending a new key event object for every
* handled raw event.
*
* @param {!string[]} args
* The arguments of the key event.
*/
this.handleKeyEvent = function handleKeyEvent(args) {
// The X11 keysym
var keysym = parseInt(args[0]);
// Either 1 or 0 for pressed or released, respectively
var pressed = parseInt(args[1]);
// The timestamp when this key event occured
var timestamp = parseInt(args[2]);
// The timestamp relative to the provided initial timestamp
var relativeTimestap = timestamp - startTimestamp;
// Known information about the parsed key
var definition = getKeyDefinitionByKeysym(keysym);
// Push the latest parsed event into the list
parsedEvents.push(new Guacamole.KeyEventInterpreter.KeyEvent({
definition: definition,
pressed: pressed,
timestamp: relativeTimestap
}));
};
/**
* Return the current batch of typed text. Note that the batch may be
* incomplete, as more key events might be processed before the next
* batch starts.
*
* @returns {Guacamole.KeyEventInterpreter.KeyEvent[]}
* The current batch of text.
*/
this.getEvents = function getEvents() {
return parsedEvents;
};
};
/**
* A definition for a known key.
*
* @constructor
* @param {Guacamole.KeyEventInterpreter.KeyDefinition|object} [template={}]
* The object whose properties should be copied within the new
* KeyDefinition.
*/
Guacamole.KeyEventInterpreter.KeyDefinition = function KeyDefinition(template) {
// Use empty object by default
template = template || {};
/**
* The X11 keysym of the key.
* @type {!number}
*/
this.keysym = parseInt(template.keysym);
/**
* A human-readable name for the key.
* @type {!String}
*/
this.name = template.name;
/**
* The value which would be typed in a typical text editor, if any. If the
* key is not associated with any typeable value, this will be undefined.
* @type {String}
*/
this.value = template.value;
};
/**
* A granular description of an extracted key event, including a human-readable
* text representation of the event, whether the event is directly typed or not,
* and the timestamp when the event occured.
*
* @constructor
* @param {Guacamole.KeyEventInterpreter.KeyEvent|object} [template={}]
* The object whose properties should be copied within the new
* KeyEvent.
*/
Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(template) {
// Use empty object by default
template = template || {};
/**
* The key definition for the pressed key.
*
* @type {!Guacamole.KeyEventInterpreter.KeyDefinition}
*/
this.definition = template.definition;
/**
* True if the key was pressed to create this event, or false if it was
* released.
*
* @type {!boolean}
*/
this.pressed = !!template.pressed;
/**
* The timestamp from the recording when this event occured.
*
* @type {!Number}
*/
this.timestamp = template.timestamp;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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 namespace used by the Guacamole JavaScript API. Absolutely all classes
* defined by the Guacamole JavaScript API will be within this namespace.
*
* @namespace
*/
var Guacamole = Guacamole || {};

View File

@@ -0,0 +1,210 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* An object used by the Guacamole client to house arbitrarily-many named
* input and output streams.
*
* @constructor
* @param {!Guacamole.Client} client
* The client owning this object.
*
* @param {!number} index
* The index of this object.
*/
Guacamole.Object = function guacamoleObject(client, index) {
/**
* Reference to this Guacamole.Object.
*
* @private
* @type {!Guacamole.Object}
*/
var guacObject = this;
/**
* Map of stream name to corresponding queue of callbacks. The queue of
* callbacks is guaranteed to be in order of request.
*
* @private
* @type {!Object.<string, function[]>}
*/
var bodyCallbacks = {};
/**
* Removes and returns the callback at the head of the callback queue for
* the stream having the given name. If no such callbacks exist, null is
* returned.
*
* @private
* @param {!string} name
* The name of the stream to retrieve a callback for.
*
* @returns {function}
* The next callback associated with the stream having the given name,
* or null if no such callback exists.
*/
var dequeueBodyCallback = function dequeueBodyCallback(name) {
// If no callbacks defined, simply return null
var callbacks = bodyCallbacks[name];
if (!callbacks)
return null;
// Otherwise, pull off first callback, deleting the queue if empty
var callback = callbacks.shift();
if (callbacks.length === 0)
delete bodyCallbacks[name];
// Return found callback
return callback;
};
/**
* Adds the given callback to the tail of the callback queue for the stream
* having the given name.
*
* @private
* @param {!string} name
* The name of the stream to associate with the given callback.
*
* @param {!function} callback
* The callback to add to the queue of the stream with the given name.
*/
var enqueueBodyCallback = function enqueueBodyCallback(name, callback) {
// Get callback queue by name, creating first if necessary
var callbacks = bodyCallbacks[name];
if (!callbacks) {
callbacks = [];
bodyCallbacks[name] = callbacks;
}
// Add callback to end of queue
callbacks.push(callback);
};
/**
* The index of this object.
*
* @type {!number}
*/
this.index = index;
/**
* Called when this object receives the body of a requested input stream.
* By default, all objects will invoke the callbacks provided to their
* requestInputStream() functions based on the name of the stream
* requested. This behavior can be overridden by specifying a different
* handler here.
*
* @event
* @param {!Guacamole.InputStream} inputStream
* The input stream of the received body.
*
* @param {!string} mimetype
* The mimetype of the data being received.
*
* @param {!string} name
* The name of the stream whose body has been received.
*/
this.onbody = function defaultBodyHandler(inputStream, mimetype, name) {
// Call queued callback for the received body, if any
var callback = dequeueBodyCallback(name);
if (callback)
callback(inputStream, mimetype);
};
/**
* Called when this object is being undefined. Once undefined, no further
* communication involving this object may occur.
*
* @event
*/
this.onundefine = null;
/**
* Requests read access to the input stream having the given name. If
* successful, a new input stream will be created.
*
* @param {!string} name
* The name of the input stream to request.
*
* @param {function} [bodyCallback]
* The callback to invoke when the body of the requested input stream
* is received. This callback will be provided a Guacamole.InputStream
* and its mimetype as its two only arguments. If the onbody handler of
* this object is overridden, this callback will not be invoked.
*/
this.requestInputStream = function requestInputStream(name, bodyCallback) {
// Queue body callback if provided
if (bodyCallback)
enqueueBodyCallback(name, bodyCallback);
// Send request for input stream
client.requestObjectInputStream(guacObject.index, name);
};
/**
* Creates a new output stream associated with this object and having the
* given mimetype and name. The legality of a mimetype and name is dictated
* by the object itself.
*
* @param {!string} mimetype
* The mimetype of the data which will be sent to the output stream.
*
* @param {!string} name
* The defined name of an output stream within this object.
*
* @returns {!Guacamole.OutputStream}
* An output stream which will write blobs to the named output stream
* of this object.
*/
this.createOutputStream = function createOutputStream(mimetype, name) {
return client.createObjectOutputStream(guacObject.index, mimetype, name);
};
};
/**
* The reserved name denoting the root stream of any object. The contents of
* the root stream MUST be a JSON map of stream name to mimetype.
*
* @constant
* @type {!string}
*/
Guacamole.Object.ROOT_STREAM = '/';
/**
* The mimetype of a stream containing JSON which maps available stream names
* to their corresponding mimetype. The root stream of a Guacamole.Object MUST
* have this mimetype.
*
* @constant
* @type {!string}
*/
Guacamole.Object.STREAM_INDEX_MIMETYPE = 'application/vnd.glyptodon.guacamole.stream-index+json';

View File

@@ -0,0 +1,947 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Dynamic on-screen keyboard. Given the layout object for an on-screen
* keyboard, this object will construct a clickable on-screen keyboard with its
* own key events.
*
* @constructor
* @param {!Guacamole.OnScreenKeyboard.Layout} layout
* The layout of the on-screen keyboard to display.
*/
Guacamole.OnScreenKeyboard = function(layout) {
/**
* Reference to this Guacamole.OnScreenKeyboard.
*
* @private
* @type {!Guacamole.OnScreenKeyboard}
*/
var osk = this;
/**
* Map of currently-set modifiers to the keysym associated with their
* original press. When the modifier is cleared, this keysym must be
* released.
*
* @private
* @type {!Object.<String, Number>}
*/
var modifierKeysyms = {};
/**
* Map of all key names to their current pressed states. If a key is not
* pressed, it may not be in this map at all, but all pressed keys will
* have a corresponding mapping to true.
*
* @private
* @type {!Object.<String, Boolean>}
*/
var pressed = {};
/**
* All scalable elements which are part of the on-screen keyboard. Each
* scalable element is carefully controlled to ensure the interface layout
* and sizing remains constant, even on browsers that would otherwise
* experience rounding error due to unit conversions.
*
* @private
* @type {!ScaledElement[]}
*/
var scaledElements = [];
/**
* Adds a CSS class to an element.
*
* @private
* @function
* @param {!Element} element
* The element to add a class to.
*
* @param {!string} classname
* The name of the class to add.
*/
var addClass = function addClass(element, classname) {
// If classList supported, use that
if (element.classList)
element.classList.add(classname);
// Otherwise, simply append the class
else
element.className += " " + classname;
};
/**
* Removes a CSS class from an element.
*
* @private
* @function
* @param {!Element} element
* The element to remove a class from.
*
* @param {!string} classname
* The name of the class to remove.
*/
var removeClass = function removeClass(element, classname) {
// If classList supported, use that
if (element.classList)
element.classList.remove(classname);
// Otherwise, manually filter out classes with given name
else {
element.className = element.className.replace(/([^ ]+)[ ]*/g,
function removeMatchingClasses(match, testClassname) {
// If same class, remove
if (testClassname === classname)
return "";
// Otherwise, allow
return match;
}
);
}
};
/**
* Counter of mouse events to ignore. This decremented by mousemove, and
* while non-zero, mouse events will have no effect.
*
* @private
* @type {!number}
*/
var ignoreMouse = 0;
/**
* Ignores all pending mouse events when touch events are the apparent
* source. Mouse events are ignored until at least touchMouseThreshold
* mouse events occur without corresponding touch events.
*
* @private
*/
var ignorePendingMouseEvents = function ignorePendingMouseEvents() {
ignoreMouse = osk.touchMouseThreshold;
};
/**
* An element whose dimensions are maintained according to an arbitrary
* scale. The conversion factor for these arbitrary units to pixels is
* provided later via a call to scale().
*
* @private
* @constructor
* @param {!Element} element
* The element whose scale should be maintained.
*
* @param {!number} width
* The width of the element, in arbitrary units, relative to other
* ScaledElements.
*
* @param {!number} height
* The height of the element, in arbitrary units, relative to other
* ScaledElements.
*
* @param {boolean} [scaleFont=false]
* Whether the line height and font size should be scaled as well.
*/
var ScaledElement = function ScaledElement(element, width, height, scaleFont) {
/**
* The width of this ScaledElement, in arbitrary units, relative to
* other ScaledElements.
*
* @type {!number}
*/
this.width = width;
/**
* The height of this ScaledElement, in arbitrary units, relative to
* other ScaledElements.
*
* @type {!number}
*/
this.height = height;
/**
* Resizes the associated element, updating its dimensions according to
* the given pixels per unit.
*
* @param {!number} pixels
* The number of pixels to assign per arbitrary unit.
*/
this.scale = function(pixels) {
// Scale element width/height
element.style.width = (width * pixels) + "px";
element.style.height = (height * pixels) + "px";
// Scale font, if requested
if (scaleFont) {
element.style.lineHeight = (height * pixels) + "px";
element.style.fontSize = pixels + "px";
}
};
};
/**
* Returns whether all modifiers having the given names are currently
* active.
*
* @private
* @param {!string[]} names
* The names of all modifiers to test.
*
* @returns {!boolean}
* true if all specified modifiers are pressed, false otherwise.
*/
var modifiersPressed = function modifiersPressed(names) {
// If any required modifiers are not pressed, return false
for (var i=0; i < names.length; i++) {
// Test whether current modifier is pressed
var name = names[i];
if (!(name in modifierKeysyms))
return false;
}
// Otherwise, all required modifiers are pressed
return true;
};
/**
* Returns the single matching Key object associated with the key of the
* given name, where that Key object's requirements (such as pressed
* modifiers) are all currently satisfied.
*
* @private
* @param {!string} keyName
* The name of the key to retrieve.
*
* @returns {Guacamole.OnScreenKeyboard.Key}
* The Key object associated with the given name, where that object's
* requirements are all currently satisfied, or null if no such Key
* can be found.
*/
var getActiveKey = function getActiveKey(keyName) {
// Get key array for given name
var keys = osk.keys[keyName];
if (!keys)
return null;
// Find last matching key
for (var i = keys.length - 1; i >= 0; i--) {
// Get candidate key
var candidate = keys[i];
// If all required modifiers are pressed, use that key
if (modifiersPressed(candidate.requires))
return candidate;
}
// No valid key
return null;
};
/**
* Presses the key having the given name, updating the associated key
* element with the "guac-keyboard-pressed" CSS class. If the key is
* already pressed, this function has no effect.
*
* @private
* @param {!string} keyName
* The name of the key to press.
*
* @param {!string} keyElement
* The element associated with the given key.
*/
var press = function press(keyName, keyElement) {
// Press key if not yet pressed
if (!pressed[keyName]) {
addClass(keyElement, "guac-keyboard-pressed");
// Get current key based on modifier state
var key = getActiveKey(keyName);
// Update modifier state
if (key.modifier) {
// Construct classname for modifier
var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier);
// Retrieve originally-pressed keysym, if modifier was already pressed
var originalKeysym = modifierKeysyms[key.modifier];
// Activate modifier if not pressed
if (originalKeysym === undefined) {
addClass(keyboard, modifierClass);
modifierKeysyms[key.modifier] = key.keysym;
// Send key event only if keysym is meaningful
if (key.keysym && osk.onkeydown)
osk.onkeydown(key.keysym);
}
// Deactivate if not pressed
else {
removeClass(keyboard, modifierClass);
delete modifierKeysyms[key.modifier];
// Send key event only if original keysym is meaningful
if (originalKeysym && osk.onkeyup)
osk.onkeyup(originalKeysym);
}
}
// If not modifier, send key event now
else if (osk.onkeydown)
osk.onkeydown(key.keysym);
// Mark key as pressed
pressed[keyName] = true;
}
};
/**
* Releases the key having the given name, removing the
* "guac-keyboard-pressed" CSS class from the associated element. If the
* key is already released, this function has no effect.
*
* @private
* @param {!string} keyName
* The name of the key to release.
*
* @param {!string} keyElement
* The element associated with the given key.
*/
var release = function release(keyName, keyElement) {
// Release key if currently pressed
if (pressed[keyName]) {
removeClass(keyElement, "guac-keyboard-pressed");
// Get current key based on modifier state
var key = getActiveKey(keyName);
// Send key event if not a modifier key
if (!key.modifier && osk.onkeyup)
osk.onkeyup(key.keysym);
// Mark key as released
pressed[keyName] = false;
}
};
// Create keyboard
var keyboard = document.createElement("div");
keyboard.className = "guac-keyboard";
// Do not allow selection or mouse movement to propagate/register.
keyboard.onselectstart =
keyboard.onmousemove =
keyboard.onmouseup =
keyboard.onmousedown = function handleMouseEvents(e) {
// If ignoring events, decrement counter
if (ignoreMouse)
ignoreMouse--;
e.stopPropagation();
return false;
};
/**
* The number of mousemove events to require before re-enabling mouse
* event handling after receiving a touch event.
*
* @type {!number}
*/
this.touchMouseThreshold = 3;
/**
* Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard.
*
* @event
* @param {!number} keysym
* The keysym of the key being pressed.
*/
this.onkeydown = null;
/**
* Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard.
*
* @event
* @param {!number} keysym
* The keysym of the key being released.
*/
this.onkeyup = null;
/**
* The keyboard layout provided at time of construction.
*
* @type {!Guacamole.OnScreenKeyboard.Layout}
*/
this.layout = new Guacamole.OnScreenKeyboard.Layout(layout);
/**
* Returns the element containing the entire on-screen keyboard.
*
* @returns {!Element}
* The element containing the entire on-screen keyboard.
*/
this.getElement = function() {
return keyboard;
};
/**
* Resizes all elements within this Guacamole.OnScreenKeyboard such that
* the width is close to but does not exceed the specified width. The
* height of the keyboard is determined based on the width.
*
* @param {!number} width
* The width to resize this Guacamole.OnScreenKeyboard to, in pixels.
*/
this.resize = function(width) {
// Get pixel size of a unit
var unit = Math.floor(width * 10 / osk.layout.width) / 10;
// Resize all scaled elements
for (var i=0; i<scaledElements.length; i++) {
var scaledElement = scaledElements[i];
scaledElement.scale(unit);
}
};
/**
* Given the name of a key and its corresponding definition, which may be
* an array of keys objects, a number (keysym), a string (key title), or a
* single key object, returns an array of key objects, deriving any missing
* properties as needed, and ensuring the key name is defined.
*
* @private
* @param {!string} name
* The name of the key being coerced into an array of Key objects.
*
* @param {!(number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[])} object
* The object defining the behavior of the key having the given name,
* which may be the title of the key (a string), the keysym (a number),
* a single Key object, or an array of Key objects.
*
* @returns {!Guacamole.OnScreenKeyboard.Key[]}
* An array of all keys associated with the given name.
*/
var asKeyArray = function asKeyArray(name, object) {
// If already an array, just coerce into a true Key[]
if (object instanceof Array) {
var keys = [];
for (var i=0; i < object.length; i++) {
keys.push(new Guacamole.OnScreenKeyboard.Key(object[i], name));
}
return keys;
}
// Derive key object from keysym if that's all we have
if (typeof object === 'number') {
return [new Guacamole.OnScreenKeyboard.Key({
name : name,
keysym : object
})];
}
// Derive key object from title if that's all we have
if (typeof object === 'string') {
return [new Guacamole.OnScreenKeyboard.Key({
name : name,
title : object
})];
}
// Otherwise, assume it's already a key object, just not an array
return [new Guacamole.OnScreenKeyboard.Key(object, name)];
};
/**
* Converts the rather forgiving key mapping allowed by
* Guacamole.OnScreenKeyboard.Layout into a rigorous mapping of key name
* to key definition, where the key definition is always an array of Key
* objects.
*
* @private
* @param {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>} keys
* A mapping of key name to key definition, where the key definition is
* the title of the key (a string), the keysym (a number), a single
* Key object, or an array of Key objects.
*
* @returns {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>}
* A more-predictable mapping of key name to key definition, where the
* key definition is always simply an array of Key objects.
*/
var getKeys = function getKeys(keys) {
var keyArrays = {};
// Coerce all keys into individual key arrays
for (var name in layout.keys) {
keyArrays[name] = asKeyArray(name, keys[name]);
}
return keyArrays;
};
/**
* Map of all key names to their corresponding set of keys. Each key name
* may correspond to multiple keys due to the effect of modifiers.
*
* @type {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>}
*/
this.keys = getKeys(layout.keys);
/**
* Given an arbitrary string representing the name of some component of the
* on-screen keyboard, returns a string formatted for use as a CSS class
* name. The result will be lowercase. Word boundaries previously denoted
* by CamelCase will be replaced by individual hyphens, as will all
* contiguous non-alphanumeric characters.
*
* @private
* @param {!string} name
* An arbitrary string representing the name of some component of the
* on-screen keyboard.
*
* @returns {!string}
* A string formatted for use as a CSS class name.
*/
var getCSSName = function getCSSName(name) {
// Convert name from possibly-CamelCase to hyphenated lowercase
var cssName = name
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[^A-Za-z0-9]+/g, '-')
.toLowerCase();
return cssName;
};
/**
* Appends DOM elements to the given element as dictated by the layout
* structure object provided. If a name is provided, an additional CSS
* class, prepended with "guac-keyboard-", will be added to the top-level
* element.
*
* If the layout structure object is an array, all elements within that
* array will be recursively appended as children of a group, and the
* top-level element will be given the CSS class "guac-keyboard-group".
*
* If the layout structure object is an object, all properties within that
* object will be recursively appended as children of a group, and the
* top-level element will be given the CSS class "guac-keyboard-group". The
* name of each property will be applied as the name of each child object
* for the sake of CSS. Each property will be added in sorted order.
*
* If the layout structure object is a string, the key having that name
* will be appended. The key will be given the CSS class
* "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name
* of the key. If the name of the key is a single character, this will
* first be transformed into the C-style hexadecimal literal for the
* Unicode codepoint of that character. For example, the key "A" would
* become "guac-keyboard-key-0x41".
*
* If the layout structure object is a number, a gap of that size will be
* inserted. The gap will be given the CSS class "guac-keyboard-gap", and
* will be scaled according to the same size units as each key.
*
* @private
* @param {!Element} element
* The element to append elements to.
*
* @param {!(Array|object|string|number)} object
* The layout structure object to use when constructing the elements to
* append.
*
* @param {string} [name]
* The name of the top-level element being appended, if any.
*/
var appendElements = function appendElements(element, object, name) {
var i;
// Create div which will become the group or key
var div = document.createElement('div');
// Add class based on name, if name given
if (name)
addClass(div, 'guac-keyboard-' + getCSSName(name));
// If an array, append each element
if (object instanceof Array) {
// Add group class
addClass(div, 'guac-keyboard-group');
// Append all elements of array
for (i=0; i < object.length; i++)
appendElements(div, object[i]);
}
// If an object, append each property value
else if (object instanceof Object) {
// Add group class
addClass(div, 'guac-keyboard-group');
// Append all children, sorted by name
var names = Object.keys(object).sort();
for (i=0; i < names.length; i++) {
var name = names[i];
appendElements(div, object[name], name);
}
}
// If a number, create as a gap
else if (typeof object === 'number') {
// Add gap class
addClass(div, 'guac-keyboard-gap');
// Maintain scale
scaledElements.push(new ScaledElement(div, object, object));
}
// If a string, create as a key
else if (typeof object === 'string') {
// If key name is only one character, use codepoint for name
var keyName = object;
if (keyName.length === 1)
keyName = '0x' + keyName.charCodeAt(0).toString(16);
// Add key container class
addClass(div, 'guac-keyboard-key-container');
// Create key element which will contain all possible caps
var keyElement = document.createElement('div');
keyElement.className = 'guac-keyboard-key '
+ 'guac-keyboard-key-' + getCSSName(keyName);
// Add all associated keys as caps within DOM
var keys = osk.keys[object];
if (keys) {
for (i=0; i < keys.length; i++) {
// Get current key
var key = keys[i];
// Create cap element for key
var capElement = document.createElement('div');
capElement.className = 'guac-keyboard-cap';
capElement.textContent = key.title;
// Add classes for any requirements
for (var j=0; j < key.requires.length; j++) {
var requirement = key.requires[j];
addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement));
addClass(keyElement, 'guac-keyboard-uses-' + getCSSName(requirement));
}
// Add cap to key within DOM
keyElement.appendChild(capElement);
}
}
// Add key to DOM, maintain scale
div.appendChild(keyElement);
scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true));
/**
* Handles a touch event which results in the pressing of an OSK
* key. Touch events will result in mouse events being ignored for
* touchMouseThreshold events.
*
* @private
* @param {!TouchEvent} e
* The touch event being handled.
*/
var touchPress = function touchPress(e) {
e.preventDefault();
ignoreMouse = osk.touchMouseThreshold;
press(object, keyElement);
};
/**
* Handles a touch event which results in the release of an OSK
* key. Touch events will result in mouse events being ignored for
* touchMouseThreshold events.
*
* @private
* @param {!TouchEvent} e
* The touch event being handled.
*/
var touchRelease = function touchRelease(e) {
e.preventDefault();
ignoreMouse = osk.touchMouseThreshold;
release(object, keyElement);
};
/**
* Handles a mouse event which results in the pressing of an OSK
* key. If mouse events are currently being ignored, this handler
* does nothing.
*
* @private
* @param {!MouseEvent} e
* The touch event being handled.
*/
var mousePress = function mousePress(e) {
e.preventDefault();
if (ignoreMouse === 0)
press(object, keyElement);
};
/**
* Handles a mouse event which results in the release of an OSK
* key. If mouse events are currently being ignored, this handler
* does nothing.
*
* @private
* @param {!MouseEvent} e
* The touch event being handled.
*/
var mouseRelease = function mouseRelease(e) {
e.preventDefault();
if (ignoreMouse === 0)
release(object, keyElement);
};
// Handle touch events on key
keyElement.addEventListener("touchstart", touchPress, true);
keyElement.addEventListener("touchend", touchRelease, true);
// Handle mouse events on key
keyElement.addEventListener("mousedown", mousePress, true);
keyElement.addEventListener("mouseup", mouseRelease, true);
keyElement.addEventListener("mouseout", mouseRelease, true);
} // end if object is key name
// Add newly-created group/key
element.appendChild(div);
};
// Create keyboard layout in DOM
appendElements(keyboard, layout.layout);
};
/**
* Represents an entire on-screen keyboard layout, including all available
* keys, their behaviors, and their relative position and sizing.
*
* @constructor
* @param {!(Guacamole.OnScreenKeyboard.Layout|object)} template
* The object whose identically-named properties will be used to initialize
* the properties of this layout.
*/
Guacamole.OnScreenKeyboard.Layout = function(template) {
/**
* The language of keyboard layout, such as "en_US". This property is for
* informational purposes only, but it is recommend to conform to the
* [language code]_[country code] format.
*
* @type {!string}
*/
this.language = template.language;
/**
* The type of keyboard layout, such as "qwerty". This property is for
* informational purposes only, and does not conform to any standard.
*
* @type {!string}
*/
this.type = template.type;
/**
* Map of key name to corresponding keysym, title, or key object. If only
* the keysym or title is provided, the key object will be created
* implicitly. In all cases, the name property of the key object will be
* taken from the name given in the mapping.
*
* @type {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>}
*/
this.keys = template.keys;
/**
* Arbitrarily nested, arbitrarily grouped key names. The contents of the
* layout will be traversed to produce an identically-nested grouping of
* keys in the DOM tree. All strings will be transformed into their
* corresponding sets of keys, while all objects and arrays will be
* transformed into named groups and anonymous groups respectively. Any
* numbers present will be transformed into gaps of that size, scaled
* according to the same units as each key.
*
* @type {!object}
*/
this.layout = template.layout;
/**
* The width of the entire keyboard, in arbitrary units. The width of each
* key is relative to this width, as both width values are assumed to be in
* the same units. The conversion factor between these units and pixels is
* derived later via a call to resize() on the Guacamole.OnScreenKeyboard.
*
* @type {!number}
*/
this.width = template.width;
/**
* The width of each key, in arbitrary units, relative to other keys in
* this layout. The true pixel size of each key will be determined by the
* overall size of the keyboard. If not defined here, the width of each
* key will default to 1.
*
* @type {!Object.<string, number>}
*/
this.keyWidths = template.keyWidths || {};
};
/**
* Represents a single key, or a single possible behavior of a key. Each key
* on the on-screen keyboard must have at least one associated
* Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or
* implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior
* depends on modifier states.
*
* @constructor
* @param {!(Guacamole.OnScreenKeyboard.Key|object)} template
* The object whose identically-named properties will be used to initialize
* the properties of this key.
*
* @param {string} [name]
* The name to use instead of any name provided within the template, if
* any. If omitted, the name within the template will be used, assuming the
* template contains a name.
*/
Guacamole.OnScreenKeyboard.Key = function(template, name) {
/**
* The unique name identifying this key within the keyboard layout.
*
* @type {!string}
*/
this.name = name || template.name;
/**
* The human-readable title that will be displayed to the user within the
* key. If not provided, this will be derived from the key name.
*
* @type {!string}
*/
this.title = template.title || this.name;
/**
* The keysym to be pressed/released when this key is pressed/released. If
* not provided, this will be derived from the title if the title is a
* single character.
*
* @type {number}
*/
this.keysym = template.keysym || (function deriveKeysym(title) {
// Do not derive keysym if title is not exactly one character
if (!title || title.length !== 1)
return null;
// For characters between U+0000 and U+00FF, the keysym is the codepoint
var charCode = title.charCodeAt(0);
if (charCode >= 0x0000 && charCode <= 0x00FF)
return charCode;
// For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000
if (charCode >= 0x0100 && charCode <= 0x10FFFF)
return 0x01000000 | charCode;
// Unable to derive keysym
return null;
})(this.title);
/**
* The name of the modifier set when the key is pressed and cleared when
* this key is released, if any. The names of modifiers are distinct from
* the names of keys; both the "RightShift" and "LeftShift" keys may set
* the "shift" modifier, for example. By default, the key will affect no
* modifiers.
*
* @type {string}
*/
this.modifier = template.modifier;
/**
* An array containing the names of each modifier required for this key to
* have an effect. For example, a lowercase letter may require nothing,
* while an uppercase letter would require "shift", assuming the Shift key
* is named "shift" within the layout. By default, the key will require
* no modifiers.
*
* @type {!string[]}
*/
this.requires = template.requires || [];
};

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.
*/
var Guacamole = Guacamole || {};
/**
* Abstract stream which can receive data.
*
* @constructor
* @param {!Guacamole.Client} client
* The client owning this stream.
*
* @param {!number} index
* The index of this stream.
*/
Guacamole.OutputStream = function(client, index) {
/**
* Reference to this stream.
*
* @private
* @type {!Guacamole.OutputStream}
*/
var guac_stream = this;
/**
* The index of this stream.
* @type {!number}
*/
this.index = index;
/**
* Fired whenever an acknowledgement is received from the server, indicating
* that a stream operation has completed, or an error has occurred.
*
* @event
* @param {!Guacamole.Status} status
* The status of the operation.
*/
this.onack = null;
/**
* Writes the given base64-encoded data to this stream as a blob.
*
* @param {!string} data
* The base64-encoded data to send.
*/
this.sendBlob = function(data) {
client.sendBlob(guac_stream.index, data);
};
/**
* Closes this stream.
*/
this.sendEnd = function() {
client.endStream(guac_stream.index);
};
};

View File

@@ -0,0 +1,348 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Simple Guacamole protocol parser that invokes an oninstruction event when
* full instructions are available from data received via receive().
*
* @constructor
*/
Guacamole.Parser = function Parser() {
/**
* Reference to this parser.
*
* @private
* @type {!Guacamole.Parser}
*/
var parser = this;
/**
* Current buffer of received data. This buffer grows until a full
* element is available. After a full element is available, that element
* is flushed into the element buffer.
*
* @private
* @type {!string}
*/
var buffer = '';
/**
* Buffer of all received, complete elements. After an entire instruction
* is read, this buffer is flushed, and a new instruction begins.
*
* @private
* @type {!string[]}
*/
var elementBuffer = [];
/**
* The character offset within the buffer of the current or most recently
* parsed element's terminator. If sufficient characters have not yet been
* read via calls to receive(), this may point to an offset well beyond the
* end of the buffer. If no characters for an element have yet been read,
* this will be -1.
*
* @private
* @type {!number}
*/
var elementEnd = -1;
/**
* The character offset within the buffer of the location that the parser
* should start looking for the next element length search or next element
* value.
*
* @private
* @type {!number}
*/
var startIndex = 0;
/**
* The declared length of the current element being parsed, in Unicode
* codepoints.
*
* @private
* @type {!number}
*/
var elementCodepoints = 0;
/**
* The number of parsed characters that must accumulate in the begining of
* the parse buffer before processing time is expended to truncate that
* buffer and conserve memory.
*
* @private
* @constant
* @type {!number}
*/
var BUFFER_TRUNCATION_THRESHOLD = 4096;
/**
* The lowest Unicode codepoint to require a surrogate pair when encoded
* with UTF-16. In UTF-16, characters with codepoints at or above this
* value are represented with a surrogate pair, while characters with
* codepoints below this value are represented with a single character.
*
* @private
* @constant
* @type {!number}
*/
var MIN_CODEPOINT_REQUIRES_SURROGATE = 0x10000;
/**
* Appends the given instruction data packet to the internal buffer of
* this Guacamole.Parser, executing all completed instructions at
* the beginning of this buffer, if any.
*
* @param {!string} packet
* The instruction data to receive.
*
* @param {!boolean} [isBuffer=false]
* Whether the provided data should be treated as an instruction buffer
* that grows continuously. If true, the data provided to receive()
* MUST always start with the data provided to the previous call. If
* false (the default), only the new data should be provided to
* receive(), and previously-received data will automatically be
* buffered by the parser as needed.
*/
this.receive = function receive(packet, isBuffer) {
if (isBuffer)
buffer = packet;
else {
// Truncate buffer as necessary
if (startIndex > BUFFER_TRUNCATION_THRESHOLD && elementEnd >= startIndex) {
buffer = buffer.substring(startIndex);
// Reset parse relative to truncation
elementEnd -= startIndex;
startIndex = 0;
}
// Append data to buffer ONLY if there is outstanding data present. It
// is otherwise much faster to simply parse the received buffer as-is,
// and tunnel implementations can take advantage of this by preferring
// to send only complete instructions. Both the HTTP and WebSocket
// tunnel implementations included with Guacamole already do this.
if (buffer.length)
buffer += packet;
else
buffer = packet;
}
// While search is within currently received data
while (elementEnd < buffer.length) {
// If we are waiting for element data
if (elementEnd >= startIndex) {
// If we have enough data in the buffer to fill the element
// value, but the number of codepoints in the expected substring
// containing the element value value is less that its declared
// length, that can only be because the element contains
// characters split between high and low surrogates, and the
// actual end of the element value is further out. The minimum
// number of additional characters that must be read to satisfy
// the declared length is simply the difference between the
// number of codepoints actually present vs. the expected
// length.
var codepoints = Guacamole.Parser.codePointCount(buffer, startIndex, elementEnd);
if (codepoints < elementCodepoints) {
elementEnd += elementCodepoints - codepoints;
continue;
}
// If the current element ends with a character involving both
// a high and low surrogate, elementEnd points to the low
// surrogate and NOT the element terminator. We must shift the
// end and reevaluate.
else if (elementCodepoints && buffer.codePointAt(elementEnd - 1) >= MIN_CODEPOINT_REQUIRES_SURROGATE) {
elementEnd++;
continue;
}
// We now have enough data for the element. Parse.
var element = buffer.substring(startIndex, elementEnd);
var terminator = buffer.substring(elementEnd, elementEnd + 1);
// Add element to array
elementBuffer.push(element);
// If last element, handle instruction
if (terminator === ';') {
// Get opcode
var opcode = elementBuffer.shift();
// Call instruction handler.
if (parser.oninstruction !== null)
parser.oninstruction(opcode, elementBuffer);
// Clear elements
elementBuffer = [];
// Immediately truncate buffer if its contents have been
// completely parsed, so that the next call to receive()
// need not append to the buffer unnecessarily
if (!isBuffer && elementEnd + 1 === buffer.length) {
elementEnd = -1;
buffer = '';
}
}
else if (terminator !== ',')
throw new Error('Element terminator of instruction was not ";" nor ",".');
// Start searching for length at character after
// element terminator
startIndex = elementEnd + 1;
}
// Search for end of length
var lengthEnd = buffer.indexOf('.', startIndex);
if (lengthEnd !== -1) {
// Parse length
elementCodepoints = parseInt(buffer.substring(elementEnd + 1, lengthEnd));
if (isNaN(elementCodepoints))
throw new Error('Non-numeric character in element length.');
// Calculate start of element
startIndex = lengthEnd + 1;
// Calculate location of element terminator
elementEnd = startIndex + elementCodepoints;
}
// If no period yet, continue search when more data
// is received
else {
startIndex = buffer.length;
break;
}
} // end parse loop
};
/**
* Fired once for every complete Guacamole instruction received, in order.
*
* @event
* @param {!string} opcode
* The Guacamole instruction opcode.
*
* @param {!string[]} parameters
* The parameters provided for the instruction, if any.
*/
this.oninstruction = null;
};
/**
* Returns the number of Unicode codepoints (not code units) within the given
* string. If character offsets are provided, only codepoints between those
* offsets are counted. Unlike the length property of a string, this function
* counts proper surrogate pairs as a single codepoint. High and low surrogate
* characters that are not part of a proper surrogate pair are counted
* separately as individual codepoints.
*
* @param {!string} str
* The string whose contents should be inspected.
*
* @param {number} [start=0]
* The index of the location in the given string where codepoint counting
* should start. If omitted, counting will begin at the start of the
* string.
*
* @param {number} [end]
* The index of the first location in the given string after where counting
* should stop (the character after the last character being counted). If
* omitted, all characters after the start location will be counted.
*
* @returns {!number}
* The number of Unicode codepoints within the requested portion of the
* given string.
*/
Guacamole.Parser.codePointCount = function codePointCount(str, start, end) {
// Count only characters within the specified region
str = str.substring(start || 0, end);
// Locate each proper Unicode surrogate pair (one high surrogate followed
// by one low surrogate)
var surrogatePairs = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
// Each surrogate pair represents a single codepoint but is represented by
// two characters in a JavaScript string, and thus is counted twice toward
// string length. Subtracting the number of surrogate pairs adjusts that
// length value such that it gives us the number of codepoints.
return str.length - (surrogatePairs ? surrogatePairs.length : 0);
};
/**
* Converts each of the values within the given array to strings, formatting
* those strings as length-prefixed elements of a complete Guacamole
* instruction.
*
* @param {!Array.<*>} elements
* The values that should be encoded as the elements of a Guacamole
* instruction. Order of these elements is preserved. This array MUST have
* at least one element.
*
* @returns {!string}
* A complete Guacamole instruction consisting of each of the provided
* element values, in order.
*/
Guacamole.Parser.toInstruction = function toInstruction(elements) {
/**
* Converts the given value to a length/string pair for use as an
* element in a Guacamole instruction.
*
* @private
* @param {*} value
* The value to convert.
*
* @return {!string}
* The converted value.
*/
var toElement = function toElement(value) {
var str = '' + value;
return Guacamole.Parser.codePointCount(str) + "." + str;
};
var instr = toElement(elements[0]);
for (var i = 1; i < elements.length; i++)
instr += ',' + toElement(elements[i]);
return instr + ';';
};

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.
*/
var Guacamole = Guacamole || {};
/**
* A position in 2-D space.
*
* @constructor
* @param {Guacamole.Position|object} [template={}]
* The object whose properties should be copied within the new
* Guacamole.Position.
*/
Guacamole.Position = function Position(template) {
template = template || {};
/**
* The current X position, in pixels.
*
* @type {!number}
* @default 0
*/
this.x = template.x || 0;
/**
* The current Y position, in pixels.
*
* @type {!number}
* @default 0
*/
this.y = template.y || 0;
/**
* Assigns the position represented by the given element and
* clientX/clientY coordinates. The clientX and clientY coordinates are
* relative to the browser viewport and are commonly available within
* JavaScript event objects. The final position is translated to
* coordinates that are relative the given element.
*
* @param {!Element} element
* The element the coordinates should be relative to.
*
* @param {!number} clientX
* The viewport-relative X coordinate to translate.
*
* @param {!number} clientY
* The viewport-relative Y coordinate to translate.
*/
this.fromClientPosition = function fromClientPosition(element, clientX, clientY) {
this.x = clientX - element.offsetLeft;
this.y = clientY - element.offsetTop;
// This is all JUST so we can get the position within the element
var parent = element.offsetParent;
while (parent && !(parent === document.body)) {
this.x -= parent.offsetLeft - parent.scrollLeft;
this.y -= parent.offsetTop - parent.scrollTop;
parent = parent.offsetParent;
}
// Element ultimately depends on positioning within document body,
// take document scroll into account.
if (parent) {
var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
this.x -= parent.offsetLeft - documentScrollLeft;
this.y -= parent.offsetTop - documentScrollTop;
}
};
};
/**
* Returns a new {@link Guacamole.Position} representing the relative position
* of the given clientX/clientY coordinates within the given element. The
* clientX and clientY coordinates are relative to the browser viewport and are
* commonly available within JavaScript event objects. The final position is
* translated to coordinates that are relative the given element.
*
* @param {!Element} element
* The element the coordinates should be relative to.
*
* @param {!number} clientX
* The viewport-relative X coordinate to translate.
*
* @param {!number} clientY
* The viewport-relative Y coordinate to translate.
*
* @returns {!Guacamole.Position}
* A new Guacamole.Position representing the relative position of the given
* client coordinates.
*/
Guacamole.Position.fromClientPosition = function fromClientPosition(element, clientX, clientY) {
var position = new Guacamole.Position();
position.fromClientPosition(element, clientX, clientY);
return position;
};

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.
*/
var Guacamole = Guacamole || {};
/**
* A description of the format of raw PCM audio, such as that used by
* Guacamole.RawAudioPlayer and Guacamole.RawAudioRecorder. This object
* describes the number of bytes per sample, the number of channels, and the
* overall sample rate.
*
* @constructor
* @param {!(Guacamole.RawAudioFormat|object)} template
* The object whose properties should be copied into the corresponding
* properties of the new Guacamole.RawAudioFormat.
*/
Guacamole.RawAudioFormat = function RawAudioFormat(template) {
/**
* The number of bytes in each sample of audio data. This value is
* independent of the number of channels.
*
* @type {!number}
*/
this.bytesPerSample = template.bytesPerSample;
/**
* The number of audio channels (ie: 1 for mono, 2 for stereo).
*
* @type {!number}
*/
this.channels = template.channels;
/**
* The number of samples per second, per channel.
*
* @type {!number}
*/
this.rate = template.rate;
};
/**
* Parses the given mimetype, returning a new Guacamole.RawAudioFormat
* which describes the type of raw audio data represented by that mimetype. If
* the mimetype is not a supported raw audio data mimetype, null is returned.
*
* @param {!string} mimetype
* The audio mimetype to parse.
*
* @returns {Guacamole.RawAudioFormat}
* A new Guacamole.RawAudioFormat which describes the type of raw
* audio data represented by the given mimetype, or null if the given
* mimetype is not supported.
*/
Guacamole.RawAudioFormat.parse = function parseFormat(mimetype) {
var bytesPerSample;
// Rate is absolutely required - if null is still present later, the
// mimetype must not be supported
var rate = null;
// Default for both "audio/L8" and "audio/L16" is one channel
var channels = 1;
// "audio/L8" has one byte per sample
if (mimetype.substring(0, 9) === 'audio/L8;') {
mimetype = mimetype.substring(9);
bytesPerSample = 1;
}
// "audio/L16" has two bytes per sample
else if (mimetype.substring(0, 10) === 'audio/L16;') {
mimetype = mimetype.substring(10);
bytesPerSample = 2;
}
// All other types are unsupported
else
return null;
// Parse all parameters
var parameters = mimetype.split(',');
for (var i = 0; i < parameters.length; i++) {
var parameter = parameters[i];
// All parameters must have an equals sign separating name from value
var equals = parameter.indexOf('=');
if (equals === -1)
return null;
// Parse name and value from parameter string
var name = parameter.substring(0, equals);
var value = parameter.substring(equals+1);
// Handle each supported parameter
switch (name) {
// Number of audio channels
case 'channels':
channels = parseInt(value);
break;
// Sample rate
case 'rate':
rate = parseInt(value);
break;
// All other parameters are unsupported
default:
return null;
}
};
// The rate parameter is required
if (rate === null)
return null;
// Return parsed format details
return new Guacamole.RawAudioFormat({
bytesPerSample : bytesPerSample,
channels : channels,
rate : rate
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,322 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A Guacamole status. Each Guacamole status consists of a status code, defined
* by the protocol, and an optional human-readable message, usually only
* included for debugging convenience.
*
* @constructor
* @param {!number} code
* The Guacamole status code, as defined by Guacamole.Status.Code.
*
* @param {string} [message]
* An optional human-readable message.
*/
Guacamole.Status = function(code, message) {
/**
* Reference to this Guacamole.Status.
*
* @private
* @type {!Guacamole.Status}
*/
var guac_status = this;
/**
* The Guacamole status code.
*
* @see Guacamole.Status.Code
* @type {!number}
*/
this.code = code;
/**
* An arbitrary human-readable message associated with this status, if any.
* The human-readable message is not required, and is generally provided
* for debugging purposes only. For user feedback, it is better to translate
* the Guacamole status code into a message.
*
* @type {string}
*/
this.message = message;
/**
* Returns whether this status represents an error.
*
* @returns {!boolean}
* true if this status represents an error, false otherwise.
*/
this.isError = function() {
return guac_status.code < 0 || guac_status.code > 0x00FF;
};
};
/**
* Enumeration of all Guacamole status codes.
*/
Guacamole.Status.Code = {
/**
* The operation succeeded.
*
* @type {!number}
*/
"SUCCESS": 0x0000,
/**
* The requested operation is unsupported.
*
* @type {!number}
*/
"UNSUPPORTED": 0x0100,
/**
* The operation could not be performed due to an internal failure.
*
* @type {!number}
*/
"SERVER_ERROR": 0x0200,
/**
* The operation could not be performed as the server is busy.
*
* @type {!number}
*/
"SERVER_BUSY": 0x0201,
/**
* The operation could not be performed because the upstream server is not
* responding.
*
* @type {!number}
*/
"UPSTREAM_TIMEOUT": 0x0202,
/**
* The operation was unsuccessful due to an error or otherwise unexpected
* condition of the upstream server.
*
* @type {!number}
*/
"UPSTREAM_ERROR": 0x0203,
/**
* The operation could not be performed as the requested resource does not
* exist.
*
* @type {!number}
*/
"RESOURCE_NOT_FOUND": 0x0204,
/**
* The operation could not be performed as the requested resource is
* already in use.
*
* @type {!number}
*/
"RESOURCE_CONFLICT": 0x0205,
/**
* The operation could not be performed as the requested resource is now
* closed.
*
* @type {!number}
*/
"RESOURCE_CLOSED": 0x0206,
/**
* The operation could not be performed because the upstream server does
* not appear to exist.
*
* @type {!number}
*/
"UPSTREAM_NOT_FOUND": 0x0207,
/**
* The operation could not be performed because the upstream server is not
* available to service the request.
*
* @type {!number}
*/
"UPSTREAM_UNAVAILABLE": 0x0208,
/**
* The session within the upstream server has ended because it conflicted
* with another session.
*
* @type {!number}
*/
"SESSION_CONFLICT": 0x0209,
/**
* The session within the upstream server has ended because it appeared to
* be inactive.
*
* @type {!number}
*/
"SESSION_TIMEOUT": 0x020A,
/**
* The session within the upstream server has been forcibly terminated.
*
* @type {!number}
*/
"SESSION_CLOSED": 0x020B,
/**
* The operation could not be performed because bad parameters were given.
*
* @type {!number}
*/
"CLIENT_BAD_REQUEST": 0x0300,
/**
* Permission was denied to perform the operation, as the user is not yet
* authorized (not yet logged in, for example).
*
* @type {!number}
*/
"CLIENT_UNAUTHORIZED": 0x0301,
/**
* Permission was denied to perform the operation, and this permission will
* not be granted even if the user is authorized.
*
* @type {!number}
*/
"CLIENT_FORBIDDEN": 0x0303,
/**
* The client took too long to respond.
*
* @type {!number}
*/
"CLIENT_TIMEOUT": 0x0308,
/**
* The client sent too much data.
*
* @type {!number}
*/
"CLIENT_OVERRUN": 0x030D,
/**
* The client sent data of an unsupported or unexpected type.
*
* @type {!number}
*/
"CLIENT_BAD_TYPE": 0x030F,
/**
* The operation failed because the current client is already using too
* many resources.
*
* @type {!number}
*/
"CLIENT_TOO_MANY": 0x031D
};
/**
* Returns the Guacamole protocol status code which most closely
* represents the given HTTP status code.
*
* @param {!number} status
* The HTTP status code to translate into a Guacamole protocol status
* code.
*
* @returns {!number}
* The Guacamole protocol status code which most closely represents the
* given HTTP status code.
*/
Guacamole.Status.Code.fromHTTPCode = function fromHTTPCode(status) {
// Translate status codes with known equivalents
switch (status) {
// HTTP 400 - Bad request
case 400:
return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
// HTTP 403 - Forbidden
case 403:
return Guacamole.Status.Code.CLIENT_FORBIDDEN;
// HTTP 404 - Resource not found
case 404:
return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
// HTTP 429 - Too many requests
case 429:
return Guacamole.Status.Code.CLIENT_TOO_MANY;
// HTTP 503 - Server unavailable
case 503:
return Guacamole.Status.Code.SERVER_BUSY;
}
// Default all other codes to generic internal error
return Guacamole.Status.Code.SERVER_ERROR;
};
/**
* Returns the Guacamole protocol status code which most closely
* represents the given WebSocket status code.
*
* @param {!number} code
* The WebSocket status code to translate into a Guacamole protocol
* status code.
*
* @returns {!number}
* The Guacamole protocol status code which most closely represents the
* given WebSocket status code.
*/
Guacamole.Status.Code.fromWebSocketCode = function fromWebSocketCode(code) {
// Translate status codes with known equivalents
switch (code) {
// Successful disconnect (no error)
case 1000: // Normal Closure
return Guacamole.Status.Code.SUCCESS;
// Codes which indicate the server is not reachable
case 1006: // Abnormal Closure (also signalled by JavaScript when the connection cannot be opened in the first place)
case 1015: // TLS Handshake
return Guacamole.Status.Code.UPSTREAM_NOT_FOUND;
// Codes which indicate the server is reachable but busy/unavailable
case 1001: // Going Away
case 1012: // Service Restart
case 1013: // Try Again Later
case 1014: // Bad Gateway
return Guacamole.Status.Code.UPSTREAM_UNAVAILABLE;
}
// Default all other codes to generic internal error
return Guacamole.Status.Code.SERVER_ERROR;
};

View File

@@ -0,0 +1,89 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, returning
* strictly text data. Note that this object will overwrite any installed event
* handlers on the given Guacamole.InputStream.
*
* @constructor
* @param {!Guacamole.InputStream} stream
* The stream that data will be read from.
*/
Guacamole.StringReader = function(stream) {
/**
* Reference to this Guacamole.InputStream.
*
* @private
* @type {!Guacamole.StringReader}
*/
var guac_reader = this;
/**
* Parser for received UTF-8 data.
*
* @type {!Guacamole.UTF8Parser}
*/
var utf8Parser = new Guacamole.UTF8Parser();
/**
* Wrapped Guacamole.ArrayBufferReader.
*
* @private
* @type {!Guacamole.ArrayBufferReader}
*/
var array_reader = new Guacamole.ArrayBufferReader(stream);
// Receive blobs as strings
array_reader.ondata = function(buffer) {
// Decode UTF-8
var text = utf8Parser.decode(buffer);
// Call handler, if present
if (guac_reader.ontext)
guac_reader.ontext(text);
};
// Simply call onend when end received
array_reader.onend = function() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Fired once for every blob of text data received.
*
* @event
* @param {!string} text
* The data packet received.
*/
this.ontext = null;
/**
* Fired once this stream is finished and no further data will be written.
* @event
*/
this.onend = null;
};

View File

@@ -0,0 +1,205 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* A writer which automatically writes to the given output stream with text
* data.
*
* @constructor
* @param {!Guacamole.OutputStream} stream
* The stream that data will be written to.
*/
Guacamole.StringWriter = function(stream) {
/**
* Reference to this Guacamole.StringWriter.
*
* @private
* @type {!Guacamole.StringWriter}
*/
var guac_writer = this;
/**
* Wrapped Guacamole.ArrayBufferWriter.
*
* @private
* @type {!Guacamole.ArrayBufferWriter}
*/
var array_writer = new Guacamole.ArrayBufferWriter(stream);
/**
* Internal buffer for UTF-8 output.
*
* @private
* @type {!Uint8Array}
*/
var buffer = new Uint8Array(8192);
/**
* The number of bytes currently in the buffer.
*
* @private
* @type {!number}
*/
var length = 0;
// Simply call onack for acknowledgements
array_writer.onack = function(status) {
if (guac_writer.onack)
guac_writer.onack(status);
};
/**
* Expands the size of the underlying buffer by the given number of bytes,
* updating the length appropriately.
*
* @private
* @param {!number} bytes
* The number of bytes to add to the underlying buffer.
*/
function __expand(bytes) {
// Resize buffer if more space needed
if (length+bytes >= buffer.length) {
var new_buffer = new Uint8Array((length+bytes)*2);
new_buffer.set(buffer);
buffer = new_buffer;
}
length += bytes;
}
/**
* Appends a single Unicode character to the current buffer, resizing the
* buffer if necessary. The character will be encoded as UTF-8.
*
* @private
* @param {!number} codepoint
* The codepoint of the Unicode character to append.
*/
function __append_utf8(codepoint) {
var mask;
var bytes;
// 1 byte
if (codepoint <= 0x7F) {
mask = 0x00;
bytes = 1;
}
// 2 byte
else if (codepoint <= 0x7FF) {
mask = 0xC0;
bytes = 2;
}
// 3 byte
else if (codepoint <= 0xFFFF) {
mask = 0xE0;
bytes = 3;
}
// 4 byte
else if (codepoint <= 0x1FFFFF) {
mask = 0xF0;
bytes = 4;
}
// If invalid codepoint, append replacement character
else {
__append_utf8(0xFFFD);
return;
}
// Offset buffer by size
__expand(bytes);
var offset = length - 1;
// Add trailing bytes, if any
for (var i=1; i<bytes; i++) {
buffer[offset--] = 0x80 | (codepoint & 0x3F);
codepoint >>= 6;
}
// Set initial byte
buffer[offset] = mask | codepoint;
}
/**
* Encodes the given string as UTF-8, returning an ArrayBuffer containing
* the resulting bytes.
*
* @private
* @param {!string} text
* The string to encode as UTF-8.
*
* @return {!Uint8Array}
* The encoded UTF-8 data.
*/
function __encode_utf8(text) {
// Fill buffer with UTF-8
for (var i=0; i<text.length; i++) {
var codepoint = text.charCodeAt(i);
__append_utf8(codepoint);
}
// Flush buffer
if (length > 0) {
var out_buffer = buffer.subarray(0, length);
length = 0;
return out_buffer;
}
}
/**
* Sends the given text.
*
* @param {!string} text
* The text to send.
*/
this.sendText = function(text) {
if (text.length)
array_writer.sendData(__encode_utf8(text));
};
/**
* Signals that no further text will be sent, effectively closing the
* stream.
*/
this.sendEnd = function() {
array_writer.sendEnd();
};
/**
* Fired for received data, if acknowledged by the server.
*
* @event
* @param {!Guacamole.Status} status
* The status of the operation.
*/
this.onack = null;
};

View File

@@ -0,0 +1,280 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Provides cross-browser multi-touch events for a given element. The events of
* the given element are automatically populated with handlers that translate
* touch events into a non-browser-specific event provided by the
* Guacamole.Touch instance.
*
* @constructor
* @augments Guacamole.Event.Target
* @param {!Element} element
* The Element to use to provide touch events.
*/
Guacamole.Touch = function Touch(element) {
Guacamole.Event.Target.call(this);
/**
* Reference to this Guacamole.Touch.
*
* @private
* @type {!Guacamole.Touch}
*/
var guacTouch = this;
/**
* The default X/Y radius of each touch if the device or browser does not
* expose the size of the contact area.
*
* @private
* @constant
* @type {!number}
*/
var DEFAULT_CONTACT_RADIUS = Math.floor(16 * window.devicePixelRatio);
/**
* The set of all active touches, stored by their unique identifiers.
*
* @type {!Object.<Number, Guacamole.Touch.State>}
*/
this.touches = {};
/**
* The number of active touches currently stored within
* {@link Guacamole.Touch#touches touches}.
*/
this.activeTouches = 0;
/**
* Fired whenever a new touch contact is initiated on the element
* associated with this Guacamole.Touch.
*
* @event Guacamole.Touch#touchstart
* @param {!Guacamole.Touch.Event} event
* A {@link Guacamole.Touch.Event} object representing the "touchstart"
* event.
*/
/**
* Fired whenever an established touch contact moves within the element
* associated with this Guacamole.Touch.
*
* @event Guacamole.Touch#touchmove
* @param {!Guacamole.Touch.Event} event
* A {@link Guacamole.Touch.Event} object representing the "touchmove"
* event.
*/
/**
* Fired whenever an established touch contact is lifted from the element
* associated with this Guacamole.Touch.
*
* @event Guacamole.Touch#touchend
* @param {!Guacamole.Touch.Event} event
* A {@link Guacamole.Touch.Event} object representing the "touchend"
* event.
*/
element.addEventListener('touchstart', function touchstart(e) {
// Fire "ontouchstart" events for all new touches
for (var i = 0; i < e.changedTouches.length; i++) {
var changedTouch = e.changedTouches[i];
var identifier = changedTouch.identifier;
// Ignore duplicated touches
if (guacTouch.touches[identifier])
continue;
var touch = guacTouch.touches[identifier] = new Guacamole.Touch.State({
id : identifier,
radiusX : changedTouch.radiusX || DEFAULT_CONTACT_RADIUS,
radiusY : changedTouch.radiusY || DEFAULT_CONTACT_RADIUS,
angle : changedTouch.angle || 0.0,
force : changedTouch.force || 1.0 /* Within JavaScript changedTouch events, a force of 0.0 indicates the device does not support reporting changedTouch force */
});
guacTouch.activeTouches++;
touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch));
}
}, false);
element.addEventListener('touchmove', function touchstart(e) {
// Fire "ontouchmove" events for all updated touches
for (var i = 0; i < e.changedTouches.length; i++) {
var changedTouch = e.changedTouches[i];
var identifier = changedTouch.identifier;
// Ignore any unrecognized touches
var touch = guacTouch.touches[identifier];
if (!touch)
continue;
// Update force only if supported by browser (otherwise, assume
// force is unchanged)
if (changedTouch.force)
touch.force = changedTouch.force;
// Update touch area, if supported by browser and device
touch.angle = changedTouch.angle || 0.0;
touch.radiusX = changedTouch.radiusX || DEFAULT_CONTACT_RADIUS;
touch.radiusY = changedTouch.radiusY || DEFAULT_CONTACT_RADIUS;
// Update with any change in position
touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch));
}
}, false);
element.addEventListener('touchend', function touchstart(e) {
// Fire "ontouchend" events for all updated touches
for (var i = 0; i < e.changedTouches.length; i++) {
var changedTouch = e.changedTouches[i];
var identifier = changedTouch.identifier;
// Ignore any unrecognized touches
var touch = guacTouch.touches[identifier];
if (!touch)
continue;
// Stop tracking this particular touch
delete guacTouch.touches[identifier];
guacTouch.activeTouches--;
// Touch has ended
touch.force = 0.0;
// Update with final position
touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
guacTouch.dispatch(new Guacamole.Touch.Event('touchend', e, touch));
}
}, false);
};
/**
* The current state of a touch contact.
*
* @constructor
* @augments Guacamole.Position
* @param {Guacamole.Touch.State|object} [template={}]
* The object whose properties should be copied within the new
* Guacamole.Touch.State.
*/
Guacamole.Touch.State = function State(template) {
template = template || {};
Guacamole.Position.call(this, template);
/**
* An arbitrary integer ID which uniquely identifies this contact relative
* to other active contacts.
*
* @type {!number}
* @default 0
*/
this.id = template.id || 0;
/**
* The Y radius of the ellipse covering the general area of the touch
* contact, in pixels.
*
* @type {!number}
* @default 0
*/
this.radiusX = template.radiusX || 0;
/**
* The X radius of the ellipse covering the general area of the touch
* contact, in pixels.
*
* @type {!number}
* @default 0
*/
this.radiusY = template.radiusY || 0;
/**
* The rough angle of clockwise rotation of the general area of the touch
* contact, in degrees.
*
* @type {!number}
* @default 0.0
*/
this.angle = template.angle || 0.0;
/**
* The relative force exerted by the touch contact, where 0 is no force
* (the touch has been lifted) and 1 is maximum force (the maximum amount
* of force representable by the device).
*
* @type {!number}
* @default 1.0
*/
this.force = template.force || 1.0;
};
/**
* An event which represents a change in state of a single touch contact,
* including the creation or removal of that contact. If multiple contacts are
* involved in a touch interaction, each contact will be associated with its
* own event.
*
* @constructor
* @augments Guacamole.Event.DOMEvent
* @param {!string} type
* The name of the touch event type. Possible values are "touchstart",
* "touchmove", and "touchend".
*
* @param {!TouchEvent} event
* The DOM touch event that produced this Guacamole.Touch.Event.
*
* @param {!Guacamole.Touch.State} state
* The state of the touch contact associated with this event.
*/
Guacamole.Touch.Event = function TouchEvent(type, event, state) {
Guacamole.Event.DOMEvent.call(this, type, [ event ]);
/**
* The state of the touch contact associated with this event.
*
* @type {!Guacamole.Touch.State}
*/
this.state = state;
};

File diff suppressed because it is too large Load Diff

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.
*/
var Guacamole = Guacamole || {};
/**
* Parser that decodes UTF-8 text from a series of provided ArrayBuffers.
* Multi-byte characters that continue from one buffer to the next are handled
* correctly.
*
* @constructor
*/
Guacamole.UTF8Parser = function UTF8Parser() {
/**
* The number of bytes remaining for the current codepoint.
*
* @private
* @type {!number}
*/
var bytesRemaining = 0;
/**
* The current codepoint value, as calculated from bytes read so far.
*
* @private
* @type {!number}
*/
var codepoint = 0;
/**
* Decodes the given UTF-8 data into a Unicode string, returning a string
* containing all complete UTF-8 characters within the provided data. The
* data may end in the middle of a multi-byte character, in which case the
* complete character will be returned from a later call to decode() after
* enough bytes have been provided.
*
* @private
* @param {!ArrayBuffer} buffer
* Arbitrary UTF-8 data.
*
* @return {!string}
* The decoded Unicode string.
*/
this.decode = function decode(buffer) {
var text = '';
var bytes = new Uint8Array(buffer);
for (var i=0; i<bytes.length; i++) {
// Get current byte
var value = bytes[i];
// Start new codepoint if nothing yet read
if (bytesRemaining === 0) {
// 1 byte (0xxxxxxx)
if ((value | 0x7F) === 0x7F)
text += String.fromCharCode(value);
// 2 byte (110xxxxx)
else if ((value | 0x1F) === 0xDF) {
codepoint = value & 0x1F;
bytesRemaining = 1;
}
// 3 byte (1110xxxx)
else if ((value | 0x0F )=== 0xEF) {
codepoint = value & 0x0F;
bytesRemaining = 2;
}
// 4 byte (11110xxx)
else if ((value | 0x07) === 0xF7) {
codepoint = value & 0x07;
bytesRemaining = 3;
}
// Invalid byte
else
text += '\uFFFD';
}
// Continue existing codepoint (10xxxxxx)
else if ((value | 0x3F) === 0xBF) {
codepoint = (codepoint << 6) | (value & 0x3F);
bytesRemaining--;
// Write codepoint if finished
if (bytesRemaining === 0)
text += String.fromCharCode(codepoint);
}
// Invalid byte
else {
bytesRemaining = 0;
text += '\uFFFD';
}
}
return text;
};
};

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.
*/
var Guacamole = Guacamole || {};
/**
* The unique ID of this version of the Guacamole JavaScript API. This ID will
* be the version string of the guacamole-common-js Maven project, and can be
* used in downstream applications as a sanity check that the proper version
* of the APIs is being used (in case an older version is cached, for example).
*
* @type {!string}
*/
Guacamole.API_VERSION = "1.6.0";

View File

@@ -0,0 +1,108 @@
/*
* 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.
*/
var Guacamole = Guacamole || {};
/**
* Abstract video player which accepts, queues and plays back arbitrary video
* data. It is up to implementations of this class to provide some means of
* handling a provided Guacamole.InputStream and rendering the received data to
* the provided Guacamole.Display.VisibleLayer. Data received along the
* provided stream is to be played back immediately.
*
* @constructor
*/
Guacamole.VideoPlayer = function VideoPlayer() {
/**
* Notifies this Guacamole.VideoPlayer that all video up to the current
* point in time has been given via the underlying stream, and that any
* difference in time between queued video data and the current time can be
* considered latency.
*/
this.sync = function sync() {
// Default implementation - do nothing
};
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.VideoPlayer, and thus will be properly handled
* by Guacamole.VideoPlayer.getInstance().
*
* @param {!string} mimetype
* The mimetype to check.
*
* @returns {!boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.VideoPlayer, false otherwise.
*/
Guacamole.VideoPlayer.isSupportedType = function isSupportedType(mimetype) {
// There are currently no built-in video players (and therefore no
// supported types)
return false;
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.VideoPlayer, in rough order of priority. Beware that only the core
* mimetypes themselves will be listed. Any mimetype parameters, even required
* ones, will not be included in the list.
*
* @returns {!string[]}
* A list of all mimetypes supported by any built-in Guacamole.VideoPlayer,
* excluding any parameters.
*/
Guacamole.VideoPlayer.getSupportedTypes = function getSupportedTypes() {
// There are currently no built-in video players (and therefore no
// supported types)
return [];
};
/**
* Returns an instance of Guacamole.VideoPlayer providing support for the given
* video format. If support for the given video format is not available, null
* is returned.
*
* @param {!Guacamole.InputStream} stream
* The Guacamole.InputStream to read video data from.
*
* @param {!Guacamole.Display.VisibleLayer} layer
* The destination layer in which this Guacamole.VideoPlayer should play
* the received video data.
*
* @param {!string} mimetype
* The mimetype of the video data in the provided stream.
*
* @return {Guacamole.VideoPlayer}
* A Guacamole.VideoPlayer instance supporting the given mimetype and
* reading from the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.VideoPlayer.getInstance = function getInstance(stream, layer, mimetype) {
// There are currently no built-in video players
return null;
};

View File

@@ -0,0 +1,141 @@
/*
* 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.
*/
/* global Guacamole, jasmine, expect */
describe("Guacamole.Event", function EventSpec() {
/**
* Test subclass of {@link Guacamole.Event} which provides a single
* "value" property supports an "ontest" legacy event handler.
*
* @constructor
* @augments Guacamole.Event
* @param {object} value
* An arbitrary value to expose to the handler of the event.
*/
var TestEvent = function TestEvent(value) {
Guacamole.Event.apply(this, [ 'test' ]);
/**
* An arbitrary value to expose to the handler of this event.
*
* @type {object}
*/
this.value = value;
/**
* @inheritdoc
*/
this.invokeLegacyHandler = function invokeLegacyHandler(target) {
if (target.ontest)
target.ontest(value);
};
};
/**
* Event target instance which will receive each fired {@link TestEvent}.
*
* @type {Guacamole.Event.Target}
*/
var eventTarget;
beforeEach(function() {
eventTarget = new Guacamole.Event.Target();
});
describe("when an event is dispatched", function(){
it("should invoke the legacy handler for matching events", function() {
eventTarget.ontest = jasmine.createSpy('ontest');
eventTarget.dispatch(new TestEvent('event1'));
expect(eventTarget.ontest).toHaveBeenCalledWith('event1');
});
it("should invoke all listeners for matching events", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test', listener1);
eventTarget.on('test', listener2);
eventTarget.dispatch(new TestEvent('event2'));
expect(listener1).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event2' }), eventTarget);
expect(listener2).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event2' }), eventTarget);
});
it("should not invoke any listeners for non-matching events", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test2', listener1);
eventTarget.on('test2', listener2);
eventTarget.dispatch(new TestEvent('event3'));
expect(listener1).not.toHaveBeenCalled();
expect(listener2).not.toHaveBeenCalled();
});
it("should not invoke any listeners that have been removed", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test', listener1);
eventTarget.on('test', listener2);
eventTarget.off('test', listener1);
eventTarget.dispatch(new TestEvent('event4'));
expect(listener1).not.toHaveBeenCalled();
expect(listener2).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event4' }), eventTarget);
});
});
describe("when listeners are removed", function(){
it("should return whether a listener is successfully removed", function() {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
eventTarget.on('test', listener1);
eventTarget.on('test', listener2);
expect(eventTarget.off('test', listener1)).toBe(true);
expect(eventTarget.off('test', listener1)).toBe(false);
expect(eventTarget.off('test', listener2)).toBe(true);
expect(eventTarget.off('test', listener2)).toBe(false);
});
});
});

View File

@@ -0,0 +1,221 @@
/*
* 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.
*/
/* global Guacamole, jasmine, expect */
describe('Guacamole.Parser', function ParserSpec() {
/**
* A single Unicode high surrogate character (any character between U+D800
* and U+DB7F).
*
* @constant
* @type {!string}
*/
const HIGH_SURROGATE = '\uD802';
/**
* A single Unicode low surrogate character (any character between U+DC00
* and U+DFFF).
*
* @constant
* @type {!string}
*/
const LOW_SURROGATE = '\uDF00';
/**
* A Unicode surrogate pair, consisting of a high and low surrogate.
*
* @constant
* @type {!string}
*/
const SURROGATE_PAIR = HIGH_SURROGATE + LOW_SURROGATE;
/**
* A 4-character test string containing Unicode characters that require
* multiple bytes when encoded as UTF-8, including at least one character
* that is encoded as a surrogate pair in UTF-16.
*
* @constant
* @type {!string}
*/
const UTF8_MULTIBYTE = '\u72AC' + SURROGATE_PAIR + 'z\u00C1';
/**
* The Guacamole.Parser instance to test. This instance is (re)created prior
* to each test via beforeEach().
*
* @type {Guacamole.Parser}
*/
var parser;
// Provide each test with a fresh parser
beforeEach(function() {
parser = new Guacamole.Parser();
});
// Empty instruction
describe('when an empty instruction is received', function() {
it('should parse the single empty opcode and invoke oninstruction', function() {
parser.oninstruction = jasmine.createSpy('oninstruction');
parser.receive('0.;');
expect(parser.oninstruction).toHaveBeenCalledOnceWith('', [ ]);
});
});
// Instruction using basic Latin characters
describe('when an instruction is containing only basic Latin characters', function() {
it('should correctly parse each element and invoke oninstruction', function() {
parser.oninstruction = jasmine.createSpy('oninstruction');
parser.receive('5.test2,'
+ '10.hellohello,'
+ '15.worldworldworld;'
);
expect(parser.oninstruction).toHaveBeenCalledOnceWith('test2', [
'hellohello',
'worldworldworld'
]);
});
});
// Instruction using characters requiring multiple bytes in UTF-8 and
// surrogate pairs in UTF-16, including an element ending with a surrogate
// pair
describe('when an instruction is received containing elements that '
+ 'contain characters involving surrogate pairs', function() {
it('should correctly parse each element and invoke oninstruction', function() {
parser.oninstruction = jasmine.createSpy('oninstruction');
parser.receive('4.test,'
+ '6.a' + UTF8_MULTIBYTE + 'b,'
+ '5.1234' + SURROGATE_PAIR + ','
+ '10.a' + UTF8_MULTIBYTE + UTF8_MULTIBYTE + 'c;'
);
expect(parser.oninstruction).toHaveBeenCalledOnceWith('test', [
'a' + UTF8_MULTIBYTE + 'b',
'1234' + SURROGATE_PAIR,
'a' + UTF8_MULTIBYTE + UTF8_MULTIBYTE + 'c'
]);
});
});
// Instruction with an element values ending with an incomplete surrogate
// pair (high or low surrogate only)
describe('when an instruction is received containing elements that end '
+ 'with incomplete surrogate pairs', function() {
it('should correctly parse each element and invoke oninstruction', function() {
parser.oninstruction = jasmine.createSpy('oninstruction');
parser.receive('4.test,'
+ '5.1234' + HIGH_SURROGATE + ','
+ '5.4567' + LOW_SURROGATE + ';'
);
expect(parser.oninstruction).toHaveBeenCalledOnceWith('test', [
'1234' + HIGH_SURROGATE,
'4567' + LOW_SURROGATE
]);
});
});
// Instruction with element values containing incomplete surrogate pairs,
describe('when an instruction is received containing incomplete surrogate pairs', function() {
it('should correctly parse each element and invoke oninstruction', function() {
parser.oninstruction = jasmine.createSpy('oninstruction');
parser.receive('5.te' + LOW_SURROGATE + 'st,'
+ '5.12' + HIGH_SURROGATE + '3' + LOW_SURROGATE + ','
+ '6.5' + LOW_SURROGATE + LOW_SURROGATE + '4' + HIGH_SURROGATE + HIGH_SURROGATE + ','
+ '10.' + UTF8_MULTIBYTE + HIGH_SURROGATE + UTF8_MULTIBYTE + HIGH_SURROGATE + ';',
);
expect(parser.oninstruction).toHaveBeenCalledOnceWith('te' + LOW_SURROGATE + 'st', [
'12' + HIGH_SURROGATE + '3' + LOW_SURROGATE,
'5' + LOW_SURROGATE + LOW_SURROGATE + '4' + HIGH_SURROGATE + HIGH_SURROGATE,
UTF8_MULTIBYTE + HIGH_SURROGATE + UTF8_MULTIBYTE + HIGH_SURROGATE
]);
});
});
// Instruction fed via blocks of characters that accumulate via an external
// buffer
describe('when an instruction is received via an external buffer', function() {
it('should correctly parse each element and invoke oninstruction once ready', function() {
parser.oninstruction = jasmine.createSpy('oninstruction');
parser.receive('5.test2,10.hello', true);
expect(parser.oninstruction).not.toHaveBeenCalled();
parser.receive('5.test2,10.hellohello,15', true);
expect(parser.oninstruction).not.toHaveBeenCalled();
parser.receive('5.test2,10.hellohello,15.worldworldworld;', true);
expect(parser.oninstruction).toHaveBeenCalledOnceWith('test2', [ 'hellohello', 'worldworldworld' ]);
});
});
// Verify codePointCount() utility function correctly counts codepoints in
// full strings
describe('when a string is provided to codePointCount()', function() {
it('should return the number of codepoints in that string', function() {
expect(Guacamole.Parser.codePointCount('')).toBe(0);
expect(Guacamole.Parser.codePointCount('test string')).toBe(11);
expect(Guacamole.Parser.codePointCount('surrogate' + SURROGATE_PAIR + 'pair')).toBe(14);
expect(Guacamole.Parser.codePointCount('missing' + HIGH_SURROGATE + 'surrogates' + LOW_SURROGATE)).toBe(19);
expect(Guacamole.Parser.codePointCount(HIGH_SURROGATE + LOW_SURROGATE + HIGH_SURROGATE)).toBe(2);
expect(Guacamole.Parser.codePointCount(HIGH_SURROGATE + HIGH_SURROGATE + LOW_SURROGATE)).toBe(2);
});
});
// Verify codePointCount() utility function correctly counts codepoints in
// substrings
describe('when a substring is provided to codePointCount()', function() {
it('should return the number of codepoints in that substring', function() {
expect(Guacamole.Parser.codePointCount('test string', 0)).toBe(11);
expect(Guacamole.Parser.codePointCount('surrogate' + SURROGATE_PAIR + 'pair', 5)).toBe(9);
expect(Guacamole.Parser.codePointCount('missing' + HIGH_SURROGATE + 'surrogates' + LOW_SURROGATE, 2, 17)).toBe(15);
expect(Guacamole.Parser.codePointCount(HIGH_SURROGATE + LOW_SURROGATE + HIGH_SURROGATE, 0, 2)).toBe(1);
expect(Guacamole.Parser.codePointCount(HIGH_SURROGATE + HIGH_SURROGATE + LOW_SURROGATE, 1, 2)).toBe(1);
});
});
// Verify toInstruction() utility function correctly encodes instructions
describe('when an array of elements is provided to toInstruction()', function() {
it('should return a correctly-encoded Guacamole instruction', function() {
expect(Guacamole.Parser.toInstruction([ 'test', 'instruction' ])).toBe('4.test,11.instruction;');
expect(Guacamole.Parser.toInstruction([ 'test' + SURROGATE_PAIR, 'instruction' ]))
.toBe('5.test' + SURROGATE_PAIR + ',11.instruction;');
expect(Guacamole.Parser.toInstruction([ UTF8_MULTIBYTE, HIGH_SURROGATE + 'xyz' + LOW_SURROGATE ]))
.toBe('4.' + UTF8_MULTIBYTE + ',5.' + HIGH_SURROGATE + 'xyz' + LOW_SURROGATE + ';');
expect(Guacamole.Parser.toInstruction([ UTF8_MULTIBYTE, LOW_SURROGATE + 'xyz' + HIGH_SURROGATE ]))
.toBe('4.' + UTF8_MULTIBYTE + ',5.' + LOW_SURROGATE + 'xyz' + HIGH_SURROGATE + ';');
});
});
});

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
/* global Guacamole, expect */
describe('Guacamole.SessionRecording', function() {
it('should create new SessionRecording instance from Blob', function() {
const blob = new Blob(['4.size,1.1,1.0,1.0;']);
const recording = new Guacamole.SessionRecording(blob);
expect(recording).not.toBeNull();
});
});

View File

@@ -0,0 +1,42 @@
<?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.
-->
<assembly>
<baseDirectory>guacamole-common-js</baseDirectory>
<id>guacamole-common-js</id>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>src/main/webapp/modules/</directory>
<includes>
<include>*.js</include>
</includes>
<outputDirectory>modules/</outputDirectory>
</fileSet>
<fileSet>
<directory>target/${project.name}-${project.version}/</directory>
<includes>
<include>*.js</include>
</includes>
<outputDirectory></outputDirectory>
</fileSet>
</fileSets>
</assembly>