Commit 0ca7cf48 authored by Solly's avatar Solly

Merge pull request #368 from DirectXMan12/refactor/cleanup

Cleanup and test all the things (plus ditching Crockford)!
parents 91127741 e6af0f60
language: node_js
node_js:
- '0.11'
env:
matrix:
- TEST_BROWSER_NAME=PhantomJS
- TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Windows 7,Linux'
- TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Windows 7,Linux' TEST_BROWSER_VERSION='30,26'
- TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 7' TEST_BROWSER_VERSION=10
- TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 8.1' TEST_BROWSER_VERSION=11
- TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.8' TEST_BROWSER_VERSION=6
- TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.9' TEST_BROWSER_VERSION=7
before_script: npm install -g karma-cli
## noVNC: HTML5 VNC Client ## noVNC: HTML5 VNC Client
[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=refactor%2Fcleanup)](https://travis-ci.org/kanaka/noVNC)
### Description ### Description
......
...@@ -4,38 +4,36 @@ ...@@ -4,38 +4,36 @@
// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js // From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
/*jslint white: false, bitwise: false, plusplus: false */ /*jslint white: false */
/*global console */ /*global console */
var Base64 = { var Base64 = {
/* Convert data (an array of integers) to a Base64 string. */
toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
base64Pad : '=',
/* Convert data (an array of integers) to a Base64 string. */ encode: function (data) {
toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
base64Pad : '=',
encode: function (data) {
"use strict"; "use strict";
var result = ''; var result = '';
var toBase64Table = Base64.toBase64Table; var toBase64Table = Base64.toBase64Table;
var length = data.length var length = data.length;
var lengthpad = (length%3); var lengthpad = (length % 3);
var i = 0, j = 0;
// Convert every three bytes to 4 ascii characters. // Convert every three bytes to 4 ascii characters.
/* BEGIN LOOP */
for (i = 0; i < (length - 2); i += 3) { for (var i = 0; i < (length - 2); i += 3) {
result += toBase64Table[data[i] >> 2]; result += toBase64Table[data[i] >> 2];
result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];
result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
result += toBase64Table[data[i+2] & 0x3f]; result += toBase64Table[data[i + 2] & 0x3f];
} }
/* END LOOP */
// Convert the remaining 1 or 2 bytes, pad out to 4 characters. // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
var j = 0;
if (lengthpad === 2) { if (lengthpad === 2) {
j = length - lengthpad; j = length - lengthpad;
result += toBase64Table[data[j] >> 2]; result += toBase64Table[data[j] >> 2];
result += toBase64Table[((data[j] & 0x03) << 4) + (data[j+1] >> 4)]; result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
result += toBase64Table[(data[j+1] & 0x0f) << 2]; result += toBase64Table[(data[j + 1] & 0x0f) << 2];
result += toBase64Table[64]; result += toBase64Table[64];
} else if (lengthpad === 1) { } else if (lengthpad === 1) {
j = length - lengthpad; j = length - lengthpad;
...@@ -46,10 +44,11 @@ encode: function (data) { ...@@ -46,10 +44,11 @@ encode: function (data) {
} }
return result; return result;
}, },
/* Convert Base64 data to a string */ /* Convert Base64 data to a string */
toBinaryTable : [ /* jshint -W013 */
toBinaryTable : [
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
...@@ -58,14 +57,15 @@ toBinaryTable : [ ...@@ -58,14 +57,15 @@ toBinaryTable : [
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
], ],
/* jshint +W013 */
decode: function (data, offset) { decode: function (data, offset) {
"use strict"; "use strict";
offset = typeof(offset) !== 'undefined' ? offset : 0; offset = typeof(offset) !== 'undefined' ? offset : 0;
var toBinaryTable = Base64.toBinaryTable; var toBinaryTable = Base64.toBinaryTable;
var base64Pad = Base64.base64Pad; var base64Pad = Base64.base64Pad;
var result, result_length, idx, i, c, padding; var result, result_length;
var leftbits = 0; // number of bits decoded, but yet to be appended var leftbits = 0; // number of bits decoded, but yet to be appended
var leftdata = 0; // bits decoded, but yet to be appended var leftdata = 0; // bits decoded, but yet to be appended
var data_length = data.indexOf('=') - offset; var data_length = data.indexOf('=') - offset;
...@@ -73,14 +73,13 @@ decode: function (data, offset) { ...@@ -73,14 +73,13 @@ decode: function (data, offset) {
if (data_length < 0) { data_length = data.length - offset; } if (data_length < 0) { data_length = data.length - offset; }
/* Every four characters is 3 resulting numbers */ /* Every four characters is 3 resulting numbers */
result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5); result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5);
result = new Array(result_length); result = new Array(result_length);
// Convert one by one. // Convert one by one.
/* BEGIN LOOP */ for (var idx = 0, i = offset; i < data.length; i++) {
for (idx = 0, i = offset; i < data.length; i++) { var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
c = toBinaryTable[data.charCodeAt(i) & 0x7f]; var padding = (data.charAt(i) === base64Pad);
padding = (data.charAt(i) === base64Pad);
// Skip illegal characters and whitespace // Skip illegal characters and whitespace
if (c === -1) { if (c === -1) {
console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
...@@ -101,15 +100,14 @@ decode: function (data, offset) { ...@@ -101,15 +100,14 @@ decode: function (data, offset) {
leftdata &= (1 << leftbits) - 1; leftdata &= (1 << leftbits) - 1;
} }
} }
/* END LOOP */
// If there are any bits left, the base64 string was corrupted // If there are any bits left, the base64 string was corrupted
if (leftbits) { if (leftbits) {
throw {name: 'Base64-Error', err = new Error('Corrupted base64 string');
message: 'Corrupted base64 string'}; err.name = 'Base64-Error';
throw err;
} }
return result; return result;
} }
}; /* End of Base64 namespace */ }; /* End of Base64 namespace */
...@@ -75,67 +75,70 @@ ...@@ -75,67 +75,70 @@
* fine Java utilities: http://www.acme.com/java/ * fine Java utilities: http://www.acme.com/java/
*/ */
"use strict"; /* jslint white: false */
/*jslint white: false, bitwise: false, plusplus: false */
function DES(passwd) { function DES(passwd) {
"use strict";
// Tables, permutations, S-boxes, etc. // Tables, permutations, S-boxes, etc.
var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, // jshint -W013
var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28],
z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8,
keys = []; keys = [];
a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; // jshint -W015
SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
// jshint +W013,+W015
// Set the key. // Set the key.
function setKeys(keyBlock) { function setKeys(keyBlock) {
var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [],
raw0, raw1, rawi, KnLi; raw0, raw1, rawi, KnLi;
for (j = 0, l = 56; j < 56; ++j, l-=8) { for (j = 0, l = 56; j < 56; ++j, l -= 8) {
l += l<-5 ? 65 : l<-3 ? 31 : l<-1 ? 63 : l===27 ? 35 : 0; // PC1 l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
m = l & 0x7; m = l & 0x7;
pc1m[j] = ((keyBlock[l >>> 3] & (1<<m)) !== 0) ? 1: 0; pc1m[j] = ((keyBlock[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
} }
...@@ -144,8 +147,8 @@ function setKeys(keyBlock) { ...@@ -144,8 +147,8 @@ function setKeys(keyBlock) {
m = i << 1; m = i << 1;
n = m + 1; n = m + 1;
kn[m] = kn[n] = 0; kn[m] = kn[n] = 0;
for (o=28; o<59; o+=28) { for (o = 28; o < 59; o += 28) {
for (j = o-28; j < o; ++j) { for (j = o - 28; j < o; ++j) {
l = j + totrot[i]; l = j + totrot[i];
if (l < o) { if (l < o) {
pcr[j] = pc1m[l]; pcr[j] = pc1m[l];
...@@ -156,10 +159,10 @@ function setKeys(keyBlock) { ...@@ -156,10 +159,10 @@ function setKeys(keyBlock) {
} }
for (j = 0; j < 24; ++j) { for (j = 0; j < 24; ++j) {
if (pcr[PC2[j]] !== 0) { if (pcr[PC2[j]] !== 0) {
kn[m] |= 1<<(23-j); kn[m] |= 1 << (23 - j);
} }
if (pcr[PC2[j + 24]] !== 0) { if (pcr[PC2[j + 24]] !== 0) {
kn[n] |= 1<<(23-j); kn[n] |= 1 << (23 - j);
} }
} }
} }
...@@ -179,10 +182,10 @@ function setKeys(keyBlock) { ...@@ -179,10 +182,10 @@ function setKeys(keyBlock) {
keys[KnLi] |= (raw1 & 0x0000003f); keys[KnLi] |= (raw1 & 0x0000003f);
++KnLi; ++KnLi;
} }
} }
// Encrypt 8 bytes of text // Encrypt 8 bytes of text
function enc8(text) { function enc8(text) {
var i = 0, b = text.slice(), fval, keysi = 0, var i = 0, b = text.slice(), fval, keysi = 0,
l, r, x; // left, right, accumulator l, r, x; // left, right, accumulator
...@@ -256,18 +259,18 @@ function enc8(text) { ...@@ -256,18 +259,18 @@ function enc8(text) {
// Spread ints to bytes // Spread ints to bytes
x = [r, l]; x = [r, l];
for (i = 0; i < 8; i++) { for (i = 0; i < 8; i++) {
b[i] = (x[i>>>2] >>> (8*(3 - (i%4)))) % 256; b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
if (b[i] < 0) { b[i] += 256; } // unsigned if (b[i] < 0) { b[i] += 256; } // unsigned
} }
return b; return b;
} }
// Encrypt 16 bytes of text using passwd as key // Encrypt 16 bytes of text using passwd as key
function encrypt(t) { function encrypt(t) {
return enc8(t.slice(0,8)).concat(enc8(t.slice(8,16))); return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16)));
} }
setKeys(passwd); // Setup keys setKeys(passwd); // Setup keys
return {'encrypt': encrypt}; // Public interface return {'encrypt': encrypt}; // Public interface
} // function DES } // function DES
...@@ -6,617 +6,594 @@ ...@@ -6,617 +6,594 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
/*jslint browser: true, white: false, bitwise: false */ /*jslint browser: true, white: false */
/*global Util, Base64, changeCursor */ /*global Util, Base64, changeCursor */
function Display(defaults) { var Display;
"use strict";
var that = {}, // Public API methods
conf = {}, // Configuration attributes
// Private Display namespace variables
c_ctx = null,
c_forceCanvas = false,
// Queued drawing actions for in-order rendering
renderQ = [],
// Predefine function variables (jslint)
imageDataGet, rgbImageData, bgrxImageData, cmapImageData,
setFillColor, rescale, scan_renderQ,
// The full frame buffer (logical canvas) size
fb_width = 0,
fb_height = 0,
// The visible "physical canvas" viewport
viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
c_prevStyle = "",
tile = null,
tile16x16 = null,
tile_x = 0,
tile_y = 0;
// Configuration attributes
Util.conf_defaults(conf, that, defaults, [
['target', 'wo', 'dom', null, 'Canvas element for rendering'],
['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'],
['width', 'rw', 'int', null, 'Display area width'],
['height', 'rw', 'int', null, 'Display area height'],
['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
]);
// Override some specific getters/setters (function () {
that.get_context = function () { return c_ctx; }; "use strict";
that.set_scale = function(scale) { rescale(scale); }; Display = function (defaults) {
this._drawCtx = null;
this._c_forceCanvas = false;
that.set_width = function (val) { that.resize(val, fb_height); }; this._renderQ = []; // queue drawing actions for in-oder rendering
that.get_width = function() { return fb_width; };
that.set_height = function (val) { that.resize(fb_width, val); }; // the full frame buffer (logical canvas) size
that.get_height = function() { return fb_height; }; this._fb_width = 0;
this._fb_height = 0;
// the visible "physical canvas" viewport
this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 };
this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 };
this._prevDrawStyle = "";
this._tile = null;
this._tile16x16 = null;
this._tile_x = 0;
this._tile_y = 0;
// Util.set_defaults(this, defaults, {
// Private functions 'true_color': true,
// 'colourMap': [],
'scale': 1.0,
'viewport': false,
'render_mode': ''
});
// Create the public API interface
function constructor() {
Util.Debug(">> Display.constructor"); Util.Debug(">> Display.constructor");
var c, func, i, curDat, curSave, if (!this._target) {
has_imageData = false, UE = Util.Engine; throw new Error("Target must be set");
if (! conf.target) { throw("target must be set"); }
if (typeof conf.target === 'string') {
throw("target must be a DOM element");
} }
c = conf.target; if (typeof this._target === 'string') {
throw new Error('target must be a DOM element');
}
if (! c.getContext) { throw("no getContext method"); } if (!this._target.getContext) {
throw new Error("no getContext method");
}
if (! c_ctx) { c_ctx = c.getContext('2d'); } if (!this._drawCtx) {
this._drawCtx = this._target.getContext('2d');
}
Util.Debug("User Agent: " + navigator.userAgent); Util.Debug("User Agent: " + navigator.userAgent);
if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); } if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); }
if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); } if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); }
if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); } if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); }
if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); } if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); }
that.clear(); this.clear();
// Check canvas features // Check canvas features
if ('createImageData' in c_ctx) { if ('createImageData' in this._drawCtx) {
conf.render_mode = "canvas rendering"; this._render_mode = 'canvas rendering';
} else { } else {
throw("Canvas does not support createImageData"); throw new Error("Canvas does not support createImageData");
} }
if (conf.prefer_js === null) {
if (this._prefer_js === null) {
Util.Info("Prefering javascript operations"); Util.Info("Prefering javascript operations");
conf.prefer_js = true; this._prefer_js = true;
} }
// Initialize cached tile imageData // Determine browser support for setting the cursor via data URI scheme
tile16x16 = c_ctx.createImageData(16, 16); var curDat = [];
for (var i = 0; i < 8 * 8 * 4; i++) {
/*
* Determine browser support for setting the cursor via data URI
* scheme
*/
curDat = [];
for (i=0; i < 8 * 8 * 4; i += 1) {
curDat.push(255); curDat.push(255);
} }
try { try {
curSave = c.style.cursor; var curSave = this._target.style.cursor;
changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8); Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8);
if (c.style.cursor) { if (this._target.style.cursor) {
if (conf.cursor_uri === null) { if (this._cursor_uri === null) {
conf.cursor_uri = true; this._cursor_uri = true;
} }
Util.Info("Data URI scheme cursor supported"); Util.Info("Data URI scheme cursor supported");
} else { } else {
if (conf.cursor_uri === null) { if (this._cursor_uri === null) {
conf.cursor_uri = false; this._cursor_uri = false;
} }
Util.Warn("Data URI scheme cursor not supported"); Util.Warn("Data URI scheme cursor not supported");
} }
c.style.cursor = curSave; this._target.style.cursor = curSave;
} catch (exc2) { } catch (exc) {
Util.Error("Data URI scheme cursor test exception: " + exc2); Util.Error("Data URI scheme cursor test exception: " + exc);
conf.cursor_uri = false; this._cursor_uri = false;
} }
Util.Debug("<< Display.constructor"); Util.Debug("<< Display.constructor");
return that ; };
}
rescale = function(factor) {
var c, tp, x, y,
properties = ['transform', 'WebkitTransform', 'MozTransform', null];
c = conf.target;
tp = properties.shift();
while (tp) {
if (typeof c.style[tp] !== 'undefined') {
break;
}
tp = properties.shift();
}
if (tp === null) {
Util.Debug("No scaling support");
return;
}
if (typeof(factor) === "undefined") {
factor = conf.scale;
} else if (factor > 1.0) {
factor = 1.0;
} else if (factor < 0.1) {
factor = 0.1;
}
if (conf.scale === factor) {
//Util.Debug("Display already scaled to '" + factor + "'");
return;
}
conf.scale = factor;
x = c.width - c.width * factor;
y = c.height - c.height * factor;
c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
};
setFillColor = function(color) {
var bgr, newStyle;
if (conf.true_color) {
bgr = color;
} else {
bgr = conf.colourMap[color[0]];
}
newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")";
if (newStyle !== c_prevStyle) {
c_ctx.fillStyle = newStyle;
c_prevStyle = newStyle;
}
};
//
// Public API interface functions
//
// Shift and/or resize the visible viewport Display.prototype = {
that.viewportChange = function(deltaX, deltaY, width, height) { // Public methods
var c = conf.target, v = viewport, cr = cleanRect, viewportChange: function (deltaX, deltaY, width, height) {
saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h; var vp = this._viewportLoc;
var cr = this._cleanRect;
var canvas = this._target;
if (!conf.viewport) { if (!this._viewport) {
Util.Debug("Setting viewport to full display region"); Util.Debug("Setting viewport to full display region");
deltaX = -v.w; // Clamped later if out of bounds deltaX = -vp.w; // clamped later of out of bounds
deltaY = -v.h; // Clamped later if out of bounds deltaY = -vp.h;
width = fb_width; width = this._fb_width;
height = fb_height; height = this._fb_height;
} }
if (typeof(deltaX) === "undefined") { deltaX = 0; } if (typeof(deltaX) === "undefined") { deltaX = 0; }
if (typeof(deltaY) === "undefined") { deltaY = 0; } if (typeof(deltaY) === "undefined") { deltaY = 0; }
if (typeof(width) === "undefined") { width = v.w; } if (typeof(width) === "undefined") { width = vp.w; }
if (typeof(height) === "undefined") { height = v.h; } if (typeof(height) === "undefined") { height = vp.h; }
// Size change // Size change
if (width > this._fb_width) { width = this._fb_width; }
if (height > this._fb_height) { height = this._fb_height; }
if (width > fb_width) { width = fb_width; } if (vp.w !== width || vp.h !== height) {
if (height > fb_height) { height = fb_height; }
if ((v.w !== width) || (v.h !== height)) {
// Change width // Change width
if ((width < v.w) && (cr.x2 > v.x + width -1)) { if (width < vp.w && cr.x2 > vp.x + width - 1) {
cr.x2 = v.x + width - 1; cr.x2 = vp.x + width - 1;
} }
v.w = width; vp.w = width;
// Change height // Change height
if ((height < v.h) && (cr.y2 > v.y + height -1)) { if (height < vp.h && cr.y2 > vp.y + height - 1) {
cr.y2 = v.y + height - 1; cr.y2 = vp.y + height - 1;
} }
v.h = height; vp.h = height;
var saveImg = null;
if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) { if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) {
saveImg = c_ctx.getImageData(0, 0, var img_width = canvas.width < vp.w ? canvas.width : vp.w;
(c.width < v.w) ? c.width : v.w, var img_height = canvas.height < vp.h ? canvas.height : vp.h;
(c.height < v.h) ? c.height : v.h); saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height);
} }
c.width = v.w; canvas.width = vp.w;
c.height = v.h; canvas.height = vp.h;
if (saveImg) { if (saveImg) {
c_ctx.putImageData(saveImg, 0, 0); this._drawCtx.putImageData(saveImg, 0, 0);
} }
} }
vx2 = v.x + v.w - 1; var vx2 = vp.x + vp.w - 1;
vy2 = v.y + v.h - 1; var vy2 = vp.y + vp.h - 1;
// Position change // Position change
if ((deltaX < 0) && ((v.x + deltaX) < 0)) { if (deltaX < 0 && vp.x + deltaX < 0) {
deltaX = - v.x; deltaX = -vp.x;
} }
if ((vx2 + deltaX) >= fb_width) { if (vx2 + deltaX >= this._fb_width) {
deltaX -= ((vx2 + deltaX) - fb_width + 1); deltaX -= vx2 + deltaX - this._fb_width + 1;
} }
if ((v.y + deltaY) < 0) { if (vp.y + deltaY < 0) {
deltaY = - v.y; deltaY = -vp.y;
} }
if ((vy2 + deltaY) >= fb_height) { if (vy2 + deltaY >= this._fb_height) {
deltaY -= ((vy2 + deltaY) - fb_height + 1); deltaY -= (vy2 + deltaY - this._fb_height + 1);
} }
if ((deltaX === 0) && (deltaY === 0)) { if (deltaX === 0 && deltaY === 0) {
//Util.Debug("skipping viewport change");
return; return;
} }
Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
v.x += deltaX; vp.x += deltaX;
vx2 += deltaX; vx2 += deltaX;
v.y += deltaY; vp.y += deltaY;
vy2 += deltaY; vy2 += deltaY;
// Update the clean rectangle // Update the clean rectangle
if (v.x > cr.x1) { if (vp.x > cr.x1) {
cr.x1 = v.x; cr.x1 = vp.x;
} }
if (vx2 < cr.x2) { if (vx2 < cr.x2) {
cr.x2 = vx2; cr.x2 = vx2;
} }
if (v.y > cr.y1) { if (vp.y > cr.y1) {
cr.y1 = v.y; cr.y1 = vp.y;
} }
if (vy2 < cr.y2) { if (vy2 < cr.y2) {
cr.y2 = vy2; cr.y2 = vy2;
} }
var x1, w;
if (deltaX < 0) { if (deltaX < 0) {
// Shift viewport left, redraw left section // Shift viewport left, redraw left section
x1 = 0; x1 = 0;
w = - deltaX; w = -deltaX;
} else { } else {
// Shift viewport right, redraw right section // Shift viewport right, redraw right section
x1 = v.w - deltaX; x1 = vp.w - deltaX;
w = deltaX; w = deltaX;
} }
var y1, h;
if (deltaY < 0) { if (deltaY < 0) {
// Shift viewport up, redraw top section // Shift viewport up, redraw top section
y1 = 0; y1 = 0;
h = - deltaY; h = -deltaY;
} else { } else {
// Shift viewport down, redraw bottom section // Shift viewport down, redraw bottom section
y1 = v.h - deltaY; y1 = vp.h - deltaY;
h = deltaY; h = deltaY;
} }
// Copy the valid part of the viewport to the shifted location // Copy the valid part of the viewport to the shifted location
saveStyle = c_ctx.fillStyle; var saveStyle = this._drawCtx.fillStyle;
c_ctx.fillStyle = "rgb(255,255,255)"; this._drawCtx.fillStyle = "rgb(255,255,255)";
if (deltaX !== 0) { if (deltaX !== 0) {
//that.copyImage(0, 0, -deltaX, 0, v.w, v.h); this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h);
//that.fillRect(x1, 0, w, v.h, [255,255,255]); this._drawCtx.fillRect(x1, 0, w, vp.h);
c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
c_ctx.fillRect(x1, 0, w, v.h);
} }
if (deltaY !== 0) { if (deltaY !== 0) {
//that.copyImage(0, 0, 0, -deltaY, v.w, v.h); this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h);
//that.fillRect(0, y1, v.w, h, [255,255,255]); this._drawCtx.fillRect(0, y1, vp.w, h);
c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h); }
c_ctx.fillRect(0, y1, v.w, h); this._drawCtx.fillStyle = saveStyle;
} },
c_ctx.fillStyle = saveStyle;
}; // Return a map of clean and dirty areas of the viewport and reset the
// tracking of clean and dirty areas
//
// Return a map of clean and dirty areas of the viewport and reset the // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h},
// tracking of clean and dirty areas. // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] }
// getCleanDirtyReset: function () {
// Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h}, var vp = this._viewportLoc;
// 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]} var cr = this._cleanRect;
that.getCleanDirtyReset = function() {
var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [], var cleanBox = { 'x': cr.x1, 'y': cr.y1,
vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1; 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 };
var dirtyBoxes = [];
// Copy the cleanRect if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) {
cleanBox = {'x': c.x1, 'y': c.y1,
'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
// Whole viewport is dirty // Whole viewport is dirty
dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h}); dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h });
} else { } else {
// Redraw dirty regions // Redraw dirty regions
if (v.x < c.x1) { var vx2 = vp.x + vp.w - 1;
var vy2 = vp.y + vp.h - 1;
if (vp.x < cr.x1) {
// left side dirty region // left side dirty region
dirtyBoxes.push({'x': v.x, 'y': v.y, dirtyBoxes.push({'x': vp.x, 'y': vp.y,
'w': c.x1 - v.x + 1, 'h': v.h}); 'w': cr.x1 - vp.x + 1, 'h': vp.h});
} }
if (vx2 > c.x2) { if (vx2 > cr.x2) {
// right side dirty region // right side dirty region
dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y, dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y,
'w': vx2 - c.x2, 'h': v.h}); 'w': vx2 - cr.x2, 'h': vp.h});
} }
if (v.y < c.y1) { if(vp.y < cr.y1) {
// top/middle dirty region // top/middle dirty region
dirtyBoxes.push({'x': c.x1, 'y': v.y, dirtyBoxes.push({'x': cr.x1, 'y': vp.y,
'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y}); 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y});
} }
if (vy2 > c.y2) { if (vy2 > cr.y2) {
// bottom/middle dirty region // bottom/middle dirty region
dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1, dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1,
'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2}); 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2});
} }
} }
// Reset the cleanRect to the whole viewport this._cleanRect = {'x1': vp.x, 'y1': vp.y,
cleanRect = {'x1': v.x, 'y1': v.y, 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1};
'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
}; },
// Translate viewport coordinates to absolute coordinates absX: function (x) {
that.absX = function(x) { return x + this._viewportLoc.x;
return x + viewport.x; },
};
that.absY = function(y) {
return y + viewport.y;
};
absY: function (y) {
return y + this._viewportLoc.y;
},
that.resize = function(width, height) { resize: function (width, height) {
c_prevStyle = ""; this._prevDrawStyle = "";
fb_width = width; this._fb_width = width;
fb_height = height; this._fb_height = height;
rescale(conf.scale); this._rescale(this._scale);
that.viewportChange();
};
that.clear = function() { this.viewportChange();
},
if (conf.logo) { clear: function () {
that.resize(conf.logo.width, conf.logo.height); if (this._logo) {
that.blitStringImage(conf.logo.data, 0, 0); this.resize(this._logo.width, this._logo.height);
this.blitStringImage(this._logo.data, 0, 0);
} else { } else {
that.resize(640, 20); this.resize(640, 20);
c_ctx.clearRect(0, 0, viewport.w, viewport.h); this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h);
} }
renderQ = []; this._renderQ = [];
},
// No benefit over default ("source-over") in Chrome and firefox fillRect: function (x, y, width, height, color) {
//c_ctx.globalCompositeOperation = "copy"; this._setFillColor(color);
}; this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height);
},
that.fillRect = function(x, y, width, height, color) { copyImage: function (old_x, old_y, new_x, new_y, w, h) {
setFillColor(color); var x1 = old_x - this._viewportLoc.x;
c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height); var y1 = old_y - this._viewportLoc.y;
}; var x2 = new_x - this._viewportLoc.x;
var y2 = new_y - this._viewportLoc.y;
that.copyImage = function(old_x, old_y, new_x, new_y, w, h) { this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h);
var x1 = old_x - viewport.x, y1 = old_y - viewport.y, },
x2 = new_x - viewport.x, y2 = new_y - viewport.y;
c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
};
// start updating a tile
// Start updating a tile startTile: function (x, y, width, height, color) {
that.startTile = function(x, y, width, height, color) { this._tile_x = x;
var data, bgr, red, green, blue, i; this._tile_y = y;
tile_x = x; if (width === 16 && height === 16) {
tile_y = y; this._tile = this._tile16x16;
if ((width === 16) && (height === 16)) {
tile = tile16x16;
} else { } else {
tile = c_ctx.createImageData(width, height); this._tile = this._drawCtx.createImageData(width, height);
} }
data = tile.data;
if (conf.prefer_js) { if (this._prefer_js) {
if (conf.true_color) { var bgr;
if (this._true_color) {
bgr = color; bgr = color;
} else { } else {
bgr = conf.colourMap[color[0]]; bgr = this._colourMap[color[0]];
} }
red = bgr[2]; var red = bgr[2];
green = bgr[1]; var green = bgr[1];
blue = bgr[0]; var blue = bgr[0];
for (i = 0; i < (width * height * 4); i+=4) {
data[i ] = red; var data = this._tile.data;
for (var i = 0; i < width * height * 4; i += 4) {
data[i] = red;
data[i + 1] = green; data[i + 1] = green;
data[i + 2] = blue; data[i + 2] = blue;
data[i + 3] = 255; data[i + 3] = 255;
} }
} else { } else {
that.fillRect(x, y, width, height, color); this.fillRect(x, y, width, height, color);
} }
}; },
// Update sub-rectangle of the current tile // update sub-rectangle of the current tile
that.subTile = function(x, y, w, h, color) { subTile: function (x, y, w, h, color) {
var data, p, bgr, red, green, blue, width, j, i, xend, yend; if (this._prefer_js) {
if (conf.prefer_js) { var bgr;
data = tile.data; if (this._true_color) {
width = tile.width;
if (conf.true_color) {
bgr = color; bgr = color;
} else { } else {
bgr = conf.colourMap[color[0]]; bgr = this._colourMap[color[0]];
} }
red = bgr[2]; var red = bgr[2];
green = bgr[1]; var green = bgr[1];
blue = bgr[0]; var blue = bgr[0];
xend = x + w; var xend = x + w;
yend = y + h; var yend = y + h;
for (j = y; j < yend; j += 1) {
for (i = x; i < xend; i += 1) { var data = this._tile.data;
p = (i + (j * width) ) * 4; var width = this._tile.width;
data[p ] = red; for (var j = y; j < yend; j++) {
for (var i = x; i < xend; i++) {
var p = (i + (j * width)) * 4;
data[p] = red;
data[p + 1] = green; data[p + 1] = green;
data[p + 2] = blue; data[p + 2] = blue;
data[p + 3] = 255; data[p + 3] = 255;
} }
} }
} else { } else {
that.fillRect(tile_x + x, tile_y + y, w, h, color); this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color);
}
},
// draw the current tile to the screen
finishTile: function () {
if (this._prefer_js) {
this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x,
this._tile_y - this._viewportLoc.y);
}
// else: No-op -- already done by setSubTile
},
blitImage: function (x, y, width, height, arr, offset) {
if (this._true_color) {
this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
} else {
this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
} }
}; },
// Draw the current tile to the screen blitRgbImage: function (x, y , width, height, arr, offset) {
that.finishTile = function() { if (this._true_color) {
if (conf.prefer_js) { this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y); } else {
// probably wrong?
this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
} }
// else: No-op, if not prefer_js then already done by setSubTile },
};
blitStringImage: function (str, x, y) {
var img = new Image();
img.onload = function () {
this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y);
}.bind(this);
img.src = str;
return img; // for debugging purposes
},
// wrap ctx.drawImage but relative to viewport
drawImage: function (img, x, y) {
this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y);
},
rgbImageData = function(x, y, vx, vy, width, height, arr, offset) { renderQ_push: function (action) {
var img, i, j, data; this._renderQ.push(action);
/* if (this._renderQ.length === 1) {
if ((x - v.x >= v.w) || (y - v.y >= v.h) || // If this can be rendered immediately it will be, otherwise
(x - v.x + width < 0) || (y - v.y + height < 0)) { // the scanner will start polling the queue (every
// Skipping because outside of viewport // requestAnimationFrame interval)
this._scan_renderQ();
}
},
changeCursor: function (pixels, mask, hotx, hoty, w, h) {
if (this._cursor_uri === false) {
Util.Warn("changeCursor called but no cursor data URI support");
return; return;
} }
*/
img = c_ctx.createImageData(width, height); if (this._true_color) {
data = img.data; Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) { } else {
data[i ] = arr[j ]; Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
data[i + 1] = arr[j + 1]; }
data[i + 2] = arr[j + 2]; },
data[i + 3] = 255; // Set Alpha
defaultCursor: function () {
this._target.style.cursor = "default";
},
// Overridden getters/setters
get_context: function () {
return this._drawCtx;
},
set_scale: function (scale) {
this._rescale(scale);
},
set_width: function (w) {
this.resize(w, this._fb_height);
},
get_width: function () {
return this._fb_width;
},
set_height: function (h) {
this.resize(this._fb_width, h);
},
get_height: function () {
return this._fb_height;
},
// Private Methods
_rescale: function (factor) {
var canvas = this._target;
var properties = ['transform', 'WebkitTransform', 'MozTransform'];
var transform_prop;
while ((transform_prop = properties.shift())) {
if (typeof canvas.style[transform_prop] !== 'undefined') {
break;
}
} }
c_ctx.putImageData(img, x - vx, y - vy);
};
bgrxImageData = function(x, y, vx, vy, width, height, arr, offset) { if (transform_prop === null) {
var img, i, j, data; Util.Debug("No scaling support");
/*
if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
(x - v.x + width < 0) || (y - v.y + height < 0)) {
// Skipping because outside of viewport
return; return;
} }
*/
img = c_ctx.createImageData(width, height); if (typeof(factor) === "undefined") {
data = img.data; factor = this._scale;
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { } else if (factor > 1.0) {
data[i ] = arr[j + 2]; factor = 1.0;
data[i + 1] = arr[j + 1]; } else if (factor < 0.1) {
data[i + 2] = arr[j ]; factor = 0.1;
data[i + 3] = 255; // Set Alpha
}
c_ctx.putImageData(img, x - vx, y - vy);
};
cmapImageData = function(x, y, vx, vy, width, height, arr, offset) {
var img, i, j, data, bgr, cmap;
img = c_ctx.createImageData(width, height);
data = img.data;
cmap = conf.colourMap;
for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
bgr = cmap[arr[j]];
data[i ] = bgr[2];
data[i + 1] = bgr[1];
data[i + 2] = bgr[0];
data[i + 3] = 255; // Set Alpha
} }
c_ctx.putImageData(img, x - vx, y - vy);
};
that.blitImage = function(x, y, width, height, arr, offset) { if (this._scale === factor) {
if (conf.true_color) { return;
bgrxImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
} else {
cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
} }
};
that.blitRgbImage = function(x, y, width, height, arr, offset) { this._scale = factor;
if (conf.true_color) { var x = canvas.width - (canvas.width * factor);
rgbImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); var y = canvas.height - (canvas.height * factor);
canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)';
},
_setFillColor: function (color) {
var bgr;
if (this._true_color) {
bgr = color;
} else { } else {
// prolly wrong... bgr = this._colourMap[color[0]];
cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
} }
};
that.blitStringImage = function(str, x, y) { var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
var img = new Image(); if (newStyle !== this._prevDrawStyle) {
img.onload = function () { this._drawCtx.fillStyle = newStyle;
c_ctx.drawImage(img, x - viewport.x, y - viewport.y); this._prevDrawStyle = newStyle;
}; }
img.src = str; },
};
// Wrap ctx.drawImage but relative to viewport _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) {
that.drawImage = function(img, x, y) { var img = this._drawCtx.createImageData(width, height);
c_ctx.drawImage(img, x - viewport.x, y - viewport.y); var data = img.data;
}; for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
data[i] = arr[j];
data[i + 1] = arr[j + 1];
data[i + 2] = arr[j + 2];
data[i + 3] = 255; // Alpha
}
this._drawCtx.putImageData(img, x - vx, y - vy);
},
that.renderQ_push = function(action) { _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) {
renderQ.push(action); var img = this._drawCtx.createImageData(width, height);
if (renderQ.length === 1) { var data = img.data;
// If this can be rendered immediately it will be, otherwise for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
// the scanner will start polling the queue (every data[i] = arr[j + 2];
// requestAnimationFrame interval) data[i + 1] = arr[j + 1];
scan_renderQ(); data[i + 2] = arr[j];
data[i + 3] = 255; // Alpha
}
this._drawCtx.putImageData(img, x - vx, y - vy);
},
_cmapImageData: function (x, y, vx, vy, width, height, arr, offset) {
var img = this._drawCtx.createImageData(width, height);
var data = img.data;
var cmap = this._colourMap;
for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) {
var bgr = cmap[arr[j]];
data[i] = bgr[2];
data[i + 1] = bgr[1];
data[i + 2] = bgr[0];
data[i + 3] = 255; // Alpha
} }
}; this._drawCtx.putImageData(img, x - vx, y - vy);
},
scan_renderQ = function() { _scan_renderQ: function () {
var a, ready = true; var ready = true;
while (ready && renderQ.length > 0) { while (ready && this._renderQ.length > 0) {
a = renderQ[0]; var a = this._renderQ[0];
switch (a.type) { switch (a.type) {
case 'copy': case 'copy':
that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
break; break;
case 'fill': case 'fill':
that.fillRect(a.x, a.y, a.width, a.height, a.color); this.fillRect(a.x, a.y, a.width, a.height, a.color);
break; break;
case 'blit': case 'blit':
that.blitImage(a.x, a.y, a.width, a.height, a.data, 0); this.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
break; break;
case 'blitRgb': case 'blitRgb':
that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
break; break;
case 'img': case 'img':
if (a.img.complete) { if (a.img.complete) {
that.drawImage(a.img, a.x, a.y); this.drawImage(a.img, a.x, a.y);
} else { } else {
// We need to wait for this image to 'load' // We need to wait for this image to 'load'
// to keep things in-order // to keep things in-order
...@@ -624,69 +601,63 @@ scan_renderQ = function() { ...@@ -624,69 +601,63 @@ scan_renderQ = function() {
} }
break; break;
} }
if (ready) { if (ready) {
a = renderQ.shift(); this._renderQ.shift();
}
} }
if (renderQ.length > 0) {
requestAnimFrame(scan_renderQ);
}
};
that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
if (conf.cursor_uri === false) {
Util.Warn("changeCursor called but no cursor data URI support");
return;
} }
if (conf.true_color) { if (this._renderQ.length > 0) {
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h); requestAnimFrame(this._scan_renderQ.bind(this));
} else {
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
} }
}; },
};
that.defaultCursor = function() {
conf.target.style.cursor = "default";
};
return constructor(); // Return the public API interface
} // End of Display()
/* Set CSS cursor property using data URI encoded cursor file */ Util.make_properties(Display, [
function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) { ['target', 'wo', 'dom'], // Canvas element for rendering
"use strict"; ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y; ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data}
//Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0); ['true_color', 'rw', 'bool'], // Use true-color pixel data
['colourMap', 'rw', 'arr'], // Colour map array (when not true-color)
['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
['viewport', 'rw', 'bool'], // Use a viewport set with viewportChange()
['width', 'rw', 'int'], // Display area width
['height', 'rw', 'int'], // Display area height
['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI
]);
// Class Methods
Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) {
var w = w0; var w = w0;
var h = h0; var h = h0;
if (h < w) if (h < w) {
h = w; // increase h to make it square h = w; // increase h to make it square
else } else {
w = h; // increace w to make it square w = h; // increase w to make it square
}
var cur = [];
// Push multi-byte little-endian values // Push multi-byte little-endian values
cur.push16le = function (num) { cur.push16le = function (num) {
this.push((num ) & 0xFF, this.push(num & 0xFF, (num >> 8) & 0xFF);
(num >> 8) & 0xFF );
}; };
cur.push32le = function (num) { cur.push32le = function (num) {
this.push((num ) & 0xFF, this.push(num & 0xFF,
(num >> 8) & 0xFF, (num >> 8) & 0xFF,
(num >> 16) & 0xFF, (num >> 16) & 0xFF,
(num >> 24) & 0xFF ); (num >> 24) & 0xFF);
}; };
IHDRsz = 40; var IHDRsz = 40;
RGBsz = w * h * 4; var RGBsz = w * h * 4;
XORsz = Math.ceil( (w * h) / 8.0 ); var XORsz = Math.ceil((w * h) / 8.0);
ANDsz = Math.ceil( (w * h) / 8.0 ); var ANDsz = Math.ceil((w * h) / 8.0);
// Main header
cur.push16le(0); // 0: Reserved cur.push16le(0); // 0: Reserved
cur.push16le(2); // 2: .CUR type cur.push16le(2); // 2: .CUR type
cur.push16le(1); // 4: Number of images, 1 for non-animated ico cur.push16le(1); // 4: Number of images, 1 for non-animated ico
...@@ -702,69 +673,62 @@ function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) { ...@@ -702,69 +673,62 @@ function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) {
// 14: cursor data byte size // 14: cursor data byte size
cur.push32le(22); // 18: offset of cursor data in the file cur.push32le(22); // 18: offset of cursor data in the file
// Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
cur.push32le(IHDRsz); // 22: Infoheader size cur.push32le(IHDRsz); // 22: InfoHeader size
cur.push32le(w); // 26: Cursor width cur.push32le(w); // 26: Cursor width
cur.push32le(h*2); // 30: XOR+AND height cur.push32le(h * 2); // 30: XOR+AND height
cur.push16le(1); // 34: number of planes cur.push16le(1); // 34: number of planes
cur.push16le(32); // 36: bits per pixel cur.push16le(32); // 36: bits per pixel
cur.push32le(0); // 38: Type of compression cur.push32le(0); // 38: Type of compression
cur.push32le(XORsz + ANDsz); // 43: Size of Image cur.push32le(XORsz + ANDsz);
// Gimp leaves this as 0 // 42: Size of Image
cur.push32le(0); // 46: reserved cur.push32le(0); // 46: reserved
cur.push32le(0); // 50: reserved cur.push32le(0); // 50: reserved
cur.push32le(0); // 54: reserved cur.push32le(0); // 54: reserved
cur.push32le(0); // 58: reserved cur.push32le(0); // 58: reserved
// 62: color data (RGBQUAD icColors[]) // 62: color data (RGBQUAD icColors[])
for (y = h-1; y >= 0; y -= 1) { var y, x;
for (x = 0; x < w; x += 1) { for (y = h - 1; y >= 0; y--) {
for (x = 0; x < w; x++) {
if (x >= w0 || y >= h0) { if (x >= w0 || y >= h0) {
cur.push(0); // blue cur.push(0); // blue
cur.push(0); // green cur.push(0); // green
cur.push(0); // red cur.push(0); // red
cur.push(0); // alpha cur.push(0); // alpha
} else { } else {
idx = y * Math.ceil(w0 / 8) + Math.floor(x/8); var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
if (cmap) { if (cmap) {
idx = (w0 * y) + x; idx = (w0 * y) + x;
rgb = cmap[pixels[idx]]; var rgb = cmap[pixels[idx]];
cur.push(rgb[2]); // blue cur.push(rgb[2]); // blue
cur.push(rgb[1]); // green cur.push(rgb[1]); // green
cur.push(rgb[0]); // red cur.push(rgb[0]); // red
cur.push(alpha); // alpha cur.push(alpha); // alpha
} else {
idx = ((w0 * y) + x) * 4;
cur.push(pixels[idx + 2]); // blue
cur.push(pixels[idx + 1]); // green
cur.push(pixels[idx ]); // red
cur.push(alpha); // alpha
} }
} }
} }
} }
// XOR/bitmask data (BYTE icXOR[]) // XOR/bitmask data (BYTE icXOR[])
// (ignored, just needs to be right size) // (ignored, just needs to be the right size)
for (y = 0; y < h; y += 1) { for (y = 0; y < h; y++) {
for (x = 0; x < Math.ceil(w / 8); x += 1) { for (x = 0; x < Math.ceil(w / 8); x++) {
cur.push(0x00); cur.push(0);
} }
} }
// AND/bitmask data (BYTE icAND[]) // AND/bitmask data (BYTE icAND[])
// (ignored, just needs to be right size) // (ignored, just needs to be the right size)
for (y = 0; y < h; y += 1) { for (y = 0; y < h; y++) {
for (x = 0; x < Math.ceil(w / 8); x += 1) { for (x = 0; x < Math.ceil(w / 8); x++) {
cur.push(0x00); cur.push(0);
} }
} }
url = "data:image/x-icon;base64," + Base64.encode(cur); var url = 'data:image/x-icon;base64,' + Base64.encode(cur);
target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
//Util.Debug("<< changeCursor, cur.length: " + cur.length); };
} })();
...@@ -5,60 +5,60 @@ ...@@ -5,60 +5,60 @@
* Licensed under MPL 2.0 or any later version (see LICENSE.txt) * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/ */
/*jslint browser: true, white: false, bitwise: false */ /*jslint browser: true, white: false */
/*global window, Util */ /*global window, Util */
var Keyboard, Mouse;
// (function () {
// Keyboard event handler "use strict";
//
function Keyboard(defaults) { //
"use strict"; // Keyboard event handler
//
var that = {}, // Public API methods Keyboard = function (defaults) {
conf = {}, // Configuration attributes this._keyDownList = []; // List of depressed keys
keyDownList = []; // List of depressed keys
// (even if they are happy) // (even if they are happy)
// Configuration attributes Util.set_defaults(this, defaults, {
Util.conf_defaults(conf, that, defaults, [ 'target': document,
['target', 'wo', 'dom', document, 'DOM element that captures keyboard input'], 'focused': true
['focused', 'rw', 'bool', true, 'Capture and send key events'], });
['onKeyPress', 'rw', 'func', null, 'Handler for key press/release']
]);
// // create the keyboard handler
// Private functions this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(),
// VerifyCharModifier( /* jshint newcap: false */
/////// setup
function onRfbEvent(evt) {
if (conf.onKeyPress) {
Util.Debug("onKeyPress " + (evt.type == 'keydown' ? "down" : "up")
+ ", keysym: " + evt.keysym.keysym + "(" + evt.keysym.keyname + ")");
conf.onKeyPress(evt.keysym.keysym, evt.type == 'keydown');
}
}
// create the keyboard handler
var k = KeyEventDecoder(kbdUtil.ModifierSync(),
VerifyCharModifier(
TrackKeyState( TrackKeyState(
EscapeModifiers(onRfbEvent) EscapeModifiers(this._handleRfbEvent.bind(this))
) )
) )
); ); /* jshint newcap: true */
function onKeyDown(e) { // keep these here so we can refer to them later
if (! conf.focused) { this._eventHandlers = {
return true; 'keyup': this._handleKeyUp.bind(this),
'keydown': this._handleKeyDown.bind(this),
'keypress': this._handleKeyPress.bind(this),
'blur': this._allKeysUp.bind(this)
};
};
Keyboard.prototype = {
// private methods
_handleRfbEvent: function (e) {
if (this._onKeyPress) {
Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") +
", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")");
this._onKeyPress(e.keysym.keysym, e.type == 'keydown');
} }
if (k.keydown(e)) { },
_handleKeyDown: function (e) {
if (!this._focused) { return true; }
if (this._handler.keydown(e)) {
// Suppress bubbling/default actions // Suppress bubbling/default actions
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
...@@ -67,12 +67,12 @@ function onKeyDown(e) { ...@@ -67,12 +67,12 @@ function onKeyDown(e) {
// will have the character code translated // will have the character code translated
return true; return true;
} }
} },
function onKeyPress(e) {
if (! conf.focused) { _handleKeyPress: function (e) {
return true; if (!this._focused) { return true; }
}
if (k.keypress(e)) { if (this._handler.keypress(e)) {
// Suppress bubbling/default actions // Suppress bubbling/default actions
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
...@@ -81,13 +81,12 @@ function onKeyPress(e) { ...@@ -81,13 +81,12 @@ function onKeyPress(e) {
// will have the character code translated // will have the character code translated
return true; return true;
} }
} },
function onKeyUp(e) { _handleKeyUp: function (e) {
if (! conf.focused) { if (!this._focused) { return true; }
return true;
} if (this._handler.keyup(e)) {
if (k.keyup(e)) {
// Suppress bubbling/default actions // Suppress bubbling/default actions
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
...@@ -96,153 +95,147 @@ function onKeyUp(e) { ...@@ -96,153 +95,147 @@ function onKeyUp(e) {
// will have the character code translated // will have the character code translated
return true; return true;
} }
} },
function onOther(e) { _allKeysUp: function () {
k.syncModifiers(e);
}
function allKeysUp() {
Util.Debug(">> Keyboard.allKeysUp"); Util.Debug(">> Keyboard.allKeysUp");
this._handler.releaseAll();
k.releaseAll();
Util.Debug("<< Keyboard.allKeysUp"); Util.Debug("<< Keyboard.allKeysUp");
} },
// // Public methods
// Public API interface functions
//
that.grab = function() { grab: function () {
//Util.Debug(">> Keyboard.grab"); //Util.Debug(">> Keyboard.grab");
var c = conf.target; var c = this._target;
Util.addEvent(c, 'keydown', onKeyDown); Util.addEvent(c, 'keydown', this._eventHandlers.keydown);
Util.addEvent(c, 'keyup', onKeyUp); Util.addEvent(c, 'keyup', this._eventHandlers.keyup);
Util.addEvent(c, 'keypress', onKeyPress); Util.addEvent(c, 'keypress', this._eventHandlers.keypress);
// Release (key up) if window loses focus // Release (key up) if window loses focus
Util.addEvent(window, 'blur', allKeysUp); Util.addEvent(window, 'blur', this._eventHandlers.blur);
//Util.Debug("<< Keyboard.grab"); //Util.Debug("<< Keyboard.grab");
}; },
that.ungrab = function() { ungrab: function () {
//Util.Debug(">> Keyboard.ungrab"); //Util.Debug(">> Keyboard.ungrab");
var c = conf.target; var c = this._target;
Util.removeEvent(c, 'keydown', onKeyDown); Util.removeEvent(c, 'keydown', this._eventHandlers.keydown);
Util.removeEvent(c, 'keyup', onKeyUp); Util.removeEvent(c, 'keyup', this._eventHandlers.keyup);
Util.removeEvent(c, 'keypress', onKeyPress); Util.removeEvent(c, 'keypress', this._eventHandlers.keypress);
Util.removeEvent(window, 'blur', allKeysUp); Util.removeEvent(window, 'blur', this._eventHandlers.blur);
// Release (key up) all keys that are in a down state // Release (key up) all keys that are in a down state
allKeysUp(); this._allKeysUp();
//Util.Debug(">> Keyboard.ungrab"); //Util.Debug(">> Keyboard.ungrab");
}; },
that.sync = function(e) {
k.syncModifiers(e);
}
return that; // Return the public API interface
} // End of Keyboard()
// sync: function (e) {
// Mouse event handler this._handler.syncModifiers(e);
// }
};
function Mouse(defaults) {
"use strict";
var that = {}, // Public API methods
conf = {}, // Configuration attributes
mouseCaptured = false;
var doubleClickTimer = null,
lastTouchPos = null;
// Configuration attributes Util.make_properties(Keyboard, [
Util.conf_defaults(conf, that, defaults, [ ['target', 'wo', 'dom'], // DOM element that captures keyboard input
['target', 'ro', 'dom', document, 'DOM element that captures mouse input'], ['focused', 'rw', 'bool'], // Capture and send key events
['notify', 'ro', 'func', null, 'Function to call to notify whenever a mouse event is received'],
['focused', 'rw', 'bool', true, 'Capture and send mouse clicks/movement'],
['scale', 'rw', 'float', 1.0, 'Viewport scale factor 0.0 - 1.0'],
['onMouseButton', 'rw', 'func', null, 'Handler for mouse button click/release'], ['onKeyPress', 'rw', 'func'] // Handler for key press/release
['onMouseMove', 'rw', 'func', null, 'Handler for mouse movement'],
['touchButton', 'rw', 'int', 1, 'Button mask (1, 2, 4) for touch devices (0 means ignore clicks)']
]); ]);
function captureMouse() { //
// Mouse event handler
//
Mouse = function (defaults) {
this._mouseCaptured = false;
this._doubleClickTimer = null;
this._lastTouchPos = null;
// Configuration attributes
Util.set_defaults(this, defaults, {
'target': document,
'focused': true,
'scale': 1.0,
'touchButton': 1
});
this._eventHandlers = {
'mousedown': this._handleMouseDown.bind(this),
'mouseup': this._handleMouseUp.bind(this),
'mousemove': this._handleMouseMove.bind(this),
'mousewheel': this._handleMouseWheel.bind(this),
'mousedisable': this._handleMouseDisable.bind(this)
};
};
Mouse.prototype = {
// private methods
_captureMouse: function () {
// capturing the mouse ensures we get the mouseup event // capturing the mouse ensures we get the mouseup event
if (conf.target.setCapture) { if (this._target.setCapture) {
conf.target.setCapture(); this._target.setCapture();
} }
// some browsers give us mouseup events regardless, // some browsers give us mouseup events regardless,
// so if we never captured the mouse, we can disregard the event // so if we never captured the mouse, we can disregard the event
mouseCaptured = true; this._mouseCaptured = true;
} },
function releaseMouse() { _releaseMouse: function () {
if (conf.target.releaseCapture) { if (this._target.releaseCapture) {
conf.target.releaseCapture(); this._target.releaseCapture();
}
mouseCaptured = false;
}
//
// Private functions
//
function resetDoubleClickTimer() {
doubleClickTimer = null;
}
function onMouseButton(e, down) {
var evt, pos, bmask;
if (! conf.focused) {
return true;
} }
this._mouseCaptured = false;
},
_resetDoubleClickTimer: function () {
this._doubleClickTimer = null;
},
if (conf.notify) { _handleMouseButton: function (e, down) {
conf.notify(e); if (!this._focused) { return true; }
if (this._notify) {
this._notify(e);
} }
evt = (e ? e : window.event); var evt = (e ? e : window.event);
pos = Util.getEventPosition(e, conf.target, conf.scale); var pos = Util.getEventPosition(e, this._target, this._scale);
var bmask;
if (e.touches || e.changedTouches) { if (e.touches || e.changedTouches) {
// Touch device // Touch device
// When two touches occur within 500 ms of each other and are // When two touches occur within 500 ms of each other and are
// closer than 20 pixels together a double click is triggered. // closer than 20 pixels together a double click is triggered.
if (down == 1) { if (down == 1) {
if (doubleClickTimer == null) { if (this._doubleClickTimer === null) {
lastTouchPos = pos; this._lastTouchPos = pos;
} else { } else {
clearTimeout(doubleClickTimer); clearTimeout(this._doubleClickTimer);
// When the distance between the two touches is small enough // When the distance between the two touches is small enough
// force the position of the latter touch to the position of // force the position of the latter touch to the position of
// the first. // the first.
var xs = lastTouchPos.x - pos.x; var xs = this._lastTouchPos.x - pos.x;
var ys = lastTouchPos.y - pos.y; var ys = this._lastTouchPos.y - pos.y;
var d = Math.sqrt((xs * xs) + (ys * ys)); var d = Math.sqrt((xs * xs) + (ys * ys));
// The goal is to trigger on a certain physical width, the // The goal is to trigger on a certain physical width, the
// devicePixelRatio brings us a bit closer but is not optimal. // devicePixelRatio brings us a bit closer but is not optimal.
if (d < 20 * window.devicePixelRatio) { if (d < 20 * window.devicePixelRatio) {
pos = lastTouchPos; pos = this._lastTouchPos;
} }
} }
doubleClickTimer = setTimeout(resetDoubleClickTimer, 500); this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
} }
bmask = conf.touchButton; bmask = this._touchButton;
// If bmask is set // If bmask is set
} else if (evt.which) { } else if (evt.which) {
/* everything except IE */ /* everything except IE */
...@@ -253,149 +246,143 @@ function onMouseButton(e, down) { ...@@ -253,149 +246,143 @@ function onMouseButton(e, down) {
(evt.button & 0x2) * 2 + // Right (evt.button & 0x2) * 2 + // Right
(evt.button & 0x4) / 2; // Middle (evt.button & 0x4) / 2; // Middle
} }
//Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down +
// " bmask: " + bmask + "(evt.button: " + evt.button + ")"); if (this._onMouseButton) {
if (conf.onMouseButton) {
Util.Debug("onMouseButton " + (down ? "down" : "up") + Util.Debug("onMouseButton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
conf.onMouseButton(pos.x, pos.y, down, bmask); this._onMouseButton(pos.x, pos.y, down, bmask);
} }
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
} },
function onMouseDown(e) { _handleMouseDown: function (e) {
captureMouse(); this._captureMouse();
onMouseButton(e, 1); this._handleMouseButton(e, 1);
} },
function onMouseUp(e) { _handleMouseUp: function (e) {
if (!mouseCaptured) { if (!this._mouseCaptured) { return; }
return;
}
onMouseButton(e, 0); this._handleMouseButton(e, 0);
releaseMouse(); this._releaseMouse();
} },
function onMouseWheel(e) { _handleMouseWheel: function (e) {
var evt, pos, bmask, wheelData; if (!this._focused) { return true; }
if (! conf.focused) {
return true; if (this._notify) {
} this._notify(e);
if (conf.notify) {
conf.notify(e);
} }
evt = (e ? e : window.event); var evt = (e ? e : window.event);
pos = Util.getEventPosition(e, conf.target, conf.scale); var pos = Util.getEventPosition(e, this._target, this._scale);
wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
var bmask;
if (wheelData > 0) { if (wheelData > 0) {
bmask = 1 << 3; bmask = 1 << 3;
} else { } else {
bmask = 1 << 4; bmask = 1 << 4;
} }
//Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y);
if (conf.onMouseButton) { if (this._onMouseButton) {
conf.onMouseButton(pos.x, pos.y, 1, bmask); this._onMouseButton(pos.x, pos.y, 1, bmask);
conf.onMouseButton(pos.x, pos.y, 0, bmask); this._onMouseButton(pos.x, pos.y, 0, bmask);
} }
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
} },
function onMouseMove(e) { _handleMouseMove: function (e) {
var evt, pos; if (! this._focused) { return true; }
if (! conf.focused) {
return true; if (this._notify) {
} this._notify(e);
if (conf.notify) {
conf.notify(e);
} }
evt = (e ? e : window.event); var evt = (e ? e : window.event);
pos = Util.getEventPosition(e, conf.target, conf.scale); var pos = Util.getEventPosition(e, this._target, this._scale);
//Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); if (this._onMouseMove) {
if (conf.onMouseMove) { this._onMouseMove(pos.x, pos.y);
conf.onMouseMove(pos.x, pos.y);
} }
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
} },
_handleMouseDisable: function (e) {
if (!this._focused) { return true; }
var evt = (e ? e : window.event);
var pos = Util.getEventPosition(e, this._target, this._scale);
function onMouseDisable(e) {
var evt, pos;
if (! conf.focused) {
return true;
}
evt = (e ? e : window.event);
pos = Util.getEventPosition(e, conf.target, conf.scale);
/* Stop propagation if inside canvas area */ /* Stop propagation if inside canvas area */
if ((pos.realx >= 0) && (pos.realy >= 0) && if ((pos.realx >= 0) && (pos.realy >= 0) &&
(pos.realx < conf.target.offsetWidth) && (pos.realx < this._target.offsetWidth) &&
(pos.realy < conf.target.offsetHeight)) { (pos.realy < this._target.offsetHeight)) {
//Util.Debug("mouse event disabled"); //Util.Debug("mouse event disabled");
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
} }
//Util.Debug("mouse event not disabled");
return true; return true;
} },
//
// Public API interface functions
//
that.grab = function() { // Public methods
//Util.Debug(">> Mouse.grab"); grab: function () {
var c = conf.target; var c = this._target;
if ('ontouchstart' in document.documentElement) { if ('ontouchstart' in document.documentElement) {
Util.addEvent(c, 'touchstart', onMouseDown); Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown);
Util.addEvent(window, 'touchend', onMouseUp); Util.addEvent(window, 'touchend', this._eventHandlers.mouseup);
Util.addEvent(c, 'touchend', onMouseUp); Util.addEvent(c, 'touchend', this._eventHandlers.mouseup);
Util.addEvent(c, 'touchmove', onMouseMove); Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove);
} else { } else {
Util.addEvent(c, 'mousedown', onMouseDown); Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown);
Util.addEvent(window, 'mouseup', onMouseUp); Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup);
Util.addEvent(c, 'mouseup', onMouseUp); Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup);
Util.addEvent(c, 'mousemove', onMouseMove); Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove);
Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
onMouseWheel); this._eventHandlers.mousewheel);
} }
/* Work around right and middle click browser behaviors */ /* Work around right and middle click browser behaviors */
Util.addEvent(document, 'click', onMouseDisable); Util.addEvent(document, 'click', this._eventHandlers.mousedisable);
Util.addEvent(document.body, 'contextmenu', onMouseDisable); Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
},
//Util.Debug("<< Mouse.grab"); ungrab: function () {
}; var c = this._target;
that.ungrab = function() {
//Util.Debug(">> Mouse.ungrab");
var c = conf.target;
if ('ontouchstart' in document.documentElement) { if ('ontouchstart' in document.documentElement) {
Util.removeEvent(c, 'touchstart', onMouseDown); Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown);
Util.removeEvent(window, 'touchend', onMouseUp); Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup);
Util.removeEvent(c, 'touchend', onMouseUp); Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup);
Util.removeEvent(c, 'touchmove', onMouseMove); Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove);
} else { } else {
Util.removeEvent(c, 'mousedown', onMouseDown); Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown);
Util.removeEvent(window, 'mouseup', onMouseUp); Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup);
Util.removeEvent(c, 'mouseup', onMouseUp); Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup);
Util.removeEvent(c, 'mousemove', onMouseMove); Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove);
Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
onMouseWheel); this._eventHandlers.mousewheel);
} }
/* Work around right and middle click browser behaviors */ /* Work around right and middle click browser behaviors */
Util.removeEvent(document, 'click', onMouseDisable); Util.removeEvent(document, 'click', this._eventHandlers.mousedisable);
Util.removeEvent(document.body, 'contextmenu', onMouseDisable); Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
//Util.Debug(">> Mouse.ungrab"); }
}; };
return that; // Return the public API interface Util.make_properties(Mouse, [
['target', 'ro', 'dom'], // DOM element that captures mouse input
['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received
['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement
['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0
} // End of Mouse() ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release
['onMouseMove', 'rw', 'func'], // Handler for mouse movement
['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
]);
})();
...@@ -15,7 +15,7 @@ var kbdUtil = (function() { ...@@ -15,7 +15,7 @@ var kbdUtil = (function() {
var sub = substitutions[cp]; var sub = substitutions[cp];
return sub ? sub : cp; return sub ? sub : cp;
}; }
function isMac() { function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform); return navigator && !!(/mac/i).exec(navigator.platform);
...@@ -387,17 +387,22 @@ function VerifyCharModifier(next) { ...@@ -387,17 +387,22 @@ function VerifyCharModifier(next) {
if (timer) { if (timer) {
return; return;
} }
var delayProcess = function () {
clearTimeout(timer);
timer = null;
process();
};
while (queue.length !== 0) { while (queue.length !== 0) {
var cur = queue[0]; var cur = queue[0];
queue = queue.splice(1); queue = queue.splice(1);
switch (cur.type) { switch (cur.type) {
case 'stall': case 'stall':
// insert a delay before processing available events. // insert a delay before processing available events.
timer = setTimeout(function() { /* jshint loopfunc: true */
clearTimeout(timer); timer = setTimeout(delayProcess, 5);
timer = null; /* jshint loopfunc: false */
process();
}, 5);
return; return;
case 'keydown': case 'keydown':
// is the next element a keypress? Then we should merge the two // is the next element a keypress? Then we should merge the two
...@@ -489,23 +494,25 @@ function TrackKeyState(next) { ...@@ -489,23 +494,25 @@ function TrackKeyState(next) {
var item = state.splice(idx, 1)[0]; var item = state.splice(idx, 1)[0];
// for each keysym tracked by this key entry, clone the current event and override the keysym // for each keysym tracked by this key entry, clone the current event and override the keysym
for (var key in item.keysyms) {
var clone = (function(){ var clone = (function(){
function Clone(){} function Clone(){}
return function (obj) { Clone.prototype=obj; return new Clone(); }; return function (obj) { Clone.prototype=obj; return new Clone(); };
}()); }());
for (var key in item.keysyms) {
var out = clone(evt); var out = clone(evt);
out.keysym = item.keysyms[key]; out.keysym = item.keysyms[key];
next(out); next(out);
} }
break; break;
case 'releaseall': case 'releaseall':
/* jshint shadow: true */
for (var i = 0; i < state.length; ++i) { for (var i = 0; i < state.length; ++i) {
for (var key in state[i].keysyms) { for (var key in state[i].keysyms) {
var keysym = state[i].keysyms[key]; var keysym = state[i].keysyms[key];
next({keyId: 0, keysym: keysym, type: 'keyup'}); next({keyId: 0, keysym: keysym, type: 'keyup'});
} }
} }
/* jshint shadow: false */
state = []; state = [];
} }
}; };
...@@ -527,8 +534,10 @@ function EscapeModifiers(next) { ...@@ -527,8 +534,10 @@ function EscapeModifiers(next) {
// send the character event // send the character event
next(evt); next(evt);
// redo modifiers // redo modifiers
/* jshint shadow: true */
for (var i = 0; i < evt.escape.length; ++i) { for (var i = 0; i < evt.escape.length; ++i) {
next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
} }
/* jshint shadow: false */
}; };
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -7,54 +7,57 @@ ...@@ -7,54 +7,57 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
"use strict"; /* jslint white: false, browser: true */
/*jslint white: false, browser: true */ /* global window, $D, Util, WebUtil, RFB, Display */
/*global window, $D, Util, WebUtil, RFB, Display */
var UI;
// Load supporting scripts
window.onscriptsload = function () { UI.load(); }; (function () {
window.onload = function () { UI.keyboardinputReset(); }; "use strict";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
// Load supporting scripts
window.onscriptsload = function () { UI.load(); };
window.onload = function () { UI.keyboardinputReset(); };
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "keyboard.js", "input.js", "display.js",
"jsunzip.js", "rfb.js", "keysym.js"]); "jsunzip.js", "rfb.js", "keysym.js"]);
var UI = { var UI = {
rfb_state : 'loaded', rfb_state : 'loaded',
settingsOpen : false, settingsOpen : false,
connSettingsOpen : false, connSettingsOpen : false,
popupStatusOpen : false, popupStatusOpen : false,
clipboardOpen: false, clipboardOpen: false,
keyboardVisible: false, keyboardVisible: false,
hideKeyboardTimeout: null, hideKeyboardTimeout: null,
lastKeyboardinput: null, lastKeyboardinput: null,
defaultKeyboardinputLen: 100, defaultKeyboardinputLen: 100,
extraKeysVisible: false, extraKeysVisible: false,
ctrlOn: false, ctrlOn: false,
altOn: false, altOn: false,
isTouchDevice: false, isTouchDevice: false,
// Setup rfb object, load settings from browser storage, then call // Setup rfb object, load settings from browser storage, then call
// UI.init to setup the UI/menus // UI.init to setup the UI/menus
load: function (callback) { load: function (callback) {
WebUtil.initSettings(UI.start, callback); WebUtil.initSettings(UI.start, callback);
}, },
// Render default UI and initialize settings menu
start: function(callback) {
var html = '', i, sheet, sheets, llevels, port, autoconnect;
// Render default UI and initialize settings menu
start: function(callback) {
UI.isTouchDevice = 'ontouchstart' in document.documentElement; UI.isTouchDevice = 'ontouchstart' in document.documentElement;
// Stylesheet selection dropdown // Stylesheet selection dropdown
sheet = WebUtil.selectStylesheet(); var sheet = WebUtil.selectStylesheet();
sheets = WebUtil.getStylesheets(); var sheets = WebUtil.getStylesheets();
var i;
for (i = 0; i < sheets.length; i += 1) { for (i = 0; i < sheets.length; i += 1) {
UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title);
} }
// Logging selection dropdown // Logging selection dropdown
llevels = ['error', 'warn', 'info', 'debug']; var llevels = ['error', 'warn', 'info', 'debug'];
for (i = 0; i < llevels.length; i += 1) { for (i = 0; i < llevels.length; i += 1) {
UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]);
} }
...@@ -70,7 +73,7 @@ start: function(callback) { ...@@ -70,7 +73,7 @@ start: function(callback) {
// if port == 80 (or 443) then it won't be present and should be // if port == 80 (or 443) then it won't be present and should be
// set manually // set manually
port = window.location.port; var port = window.location.port;
if (!port) { if (!port) {
if (window.location.protocol.substring(0,5) == 'https') { if (window.location.protocol.substring(0,5) == 'https') {
port = 443; port = 443;
...@@ -92,13 +95,13 @@ start: function(callback) { ...@@ -92,13 +95,13 @@ start: function(callback) {
UI.initSetting('path', 'websockify'); UI.initSetting('path', 'websockify');
UI.initSetting('repeaterID', ''); UI.initSetting('repeaterID', '');
UI.rfb = RFB({'target': $D('noVNC_canvas'), UI.rfb = new RFB({'target': $D('noVNC_canvas'),
'onUpdateState': UI.updateState, 'onUpdateState': UI.updateState,
'onXvpInit': UI.updateXvpVisualState, 'onXvpInit': UI.updateXvpVisualState,
'onClipboard': UI.clipReceive, 'onClipboard': UI.clipReceive,
'onDesktopName': UI.updateDocumentTitle}); 'onDesktopName': UI.updateDocumentTitle});
autoconnect = WebUtil.getQueryVar('autoconnect', false); var autoconnect = WebUtil.getQueryVar('autoconnect', false);
if (autoconnect === 'true' || autoconnect == '1') { if (autoconnect === 'true' || autoconnect == '1') {
autoconnect = true; autoconnect = true;
UI.connect(); UI.connect();
...@@ -108,14 +111,6 @@ start: function(callback) { ...@@ -108,14 +111,6 @@ start: function(callback) {
UI.updateVisualState(); UI.updateVisualState();
// Unfocus clipboard when over the VNC area
//$D('VNC_screen').onmousemove = function () {
// var keyboard = UI.rfb.get_keyboard();
// if ((! keyboard) || (! keyboard.get_focused())) {
// $D('VNC_clipboard_text').blur();
// }
// };
// Show mouse selector buttons on touch screen devices // Show mouse selector buttons on touch screen devices
if (UI.isTouchDevice) { if (UI.isTouchDevice) {
// Show mobile buttons // Show mobile buttons
...@@ -167,9 +162,9 @@ start: function(callback) { ...@@ -167,9 +162,9 @@ start: function(callback) {
if (typeof callback === "function") { if (typeof callback === "function") {
callback(UI.rfb); callback(UI.rfb);
} }
}, },
addMouseHandlers: function() { addMouseHandlers: function() {
// Setup interface handlers that can't be inline // Setup interface handlers that can't be inline
$D("noVNC_view_drag_button").onclick = UI.setViewDrag; $D("noVNC_view_drag_button").onclick = UI.setViewDrag;
$D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
...@@ -210,12 +205,12 @@ addMouseHandlers: function() { ...@@ -210,12 +205,12 @@ addMouseHandlers: function() {
$D("noVNC_apply").onclick = UI.settingsApply; $D("noVNC_apply").onclick = UI.settingsApply;
$D("noVNC_connect_button").onclick = UI.connect; $D("noVNC_connect_button").onclick = UI.connect;
}, },
// Read form control compatible setting from cookie // Read form control compatible setting from cookie
getSetting: function(name) { getSetting: function(name) {
var val, ctrl = $D('noVNC_' + name); var ctrl = $D('noVNC_' + name);
val = WebUtil.readSetting(name); var val = WebUtil.readSetting(name);
if (val !== null && ctrl.type === 'checkbox') { if (val !== null && ctrl.type === 'checkbox') {
if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
val = false; val = false;
...@@ -224,13 +219,12 @@ getSetting: function(name) { ...@@ -224,13 +219,12 @@ getSetting: function(name) {
} }
} }
return val; return val;
}, },
// Update cookie and form control setting. If value is not set, then // Update cookie and form control setting. If value is not set, then
// updates from control to current cookie setting. // updates from control to current cookie setting.
updateSetting: function(name, value) { updateSetting: function(name, value) {
var i, ctrl = $D('noVNC_' + name);
// Save the cookie for this session // Save the cookie for this session
if (typeof value !== 'undefined') { if (typeof value !== 'undefined') {
WebUtil.writeSetting(name, value); WebUtil.writeSetting(name, value);
...@@ -239,11 +233,12 @@ updateSetting: function(name, value) { ...@@ -239,11 +233,12 @@ updateSetting: function(name, value) {
// Update the settings control // Update the settings control
value = UI.getSetting(name); value = UI.getSetting(name);
var ctrl = $D('noVNC_' + name);
if (ctrl.type === 'checkbox') { if (ctrl.type === 'checkbox') {
ctrl.checked = value; ctrl.checked = value;
} else if (typeof ctrl.options !== 'undefined') { } else if (typeof ctrl.options !== 'undefined') {
for (i = 0; i < ctrl.options.length; i += 1) { for (var i = 0; i < ctrl.options.length; i += 1) {
if (ctrl.options[i].value === value) { if (ctrl.options[i].value === value) {
ctrl.selectedIndex = i; ctrl.selectedIndex = i;
break; break;
...@@ -257,10 +252,10 @@ updateSetting: function(name, value) { ...@@ -257,10 +252,10 @@ updateSetting: function(name, value) {
} }
ctrl.value = value; ctrl.value = value;
} }
}, },
// Save control setting to cookie // Save control setting to cookie
saveSetting: function(name) { saveSetting: function(name) {
var val, ctrl = $D('noVNC_' + name); var val, ctrl = $D('noVNC_' + name);
if (ctrl.type === 'checkbox') { if (ctrl.type === 'checkbox') {
val = ctrl.checked; val = ctrl.checked;
...@@ -272,31 +267,28 @@ saveSetting: function(name) { ...@@ -272,31 +267,28 @@ saveSetting: function(name) {
WebUtil.writeSetting(name, val); WebUtil.writeSetting(name, val);
//Util.Debug("Setting saved '" + name + "=" + val + "'"); //Util.Debug("Setting saved '" + name + "=" + val + "'");
return val; return val;
}, },
// Initial page load read/initialization of settings
initSetting: function(name, defVal) {
var val;
// Initial page load read/initialization of settings
initSetting: function(name, defVal) {
// Check Query string followed by cookie // Check Query string followed by cookie
val = WebUtil.getQueryVar(name); var val = WebUtil.getQueryVar(name);
if (val === null) { if (val === null) {
val = WebUtil.readSetting(name, defVal); val = WebUtil.readSetting(name, defVal);
} }
UI.updateSetting(name, val); UI.updateSetting(name, val);
//Util.Debug("Setting '" + name + "' initialized to '" + val + "'");
return val; return val;
}, },
// Force a setting to be a certain value // Force a setting to be a certain value
forceSetting: function(name, val) { forceSetting: function(name, val) {
UI.updateSetting(name, val); UI.updateSetting(name, val);
return val; return val;
}, },
// Show the popup status panel // Show the popup status panel
togglePopupStatusPanel: function() { togglePopupStatusPanel: function() {
var psp = $D('noVNC_popup_status_panel'); var psp = $D('noVNC_popup_status_panel');
if (UI.popupStatusOpen === true) { if (UI.popupStatusOpen === true) {
psp.style.display = "none"; psp.style.display = "none";
...@@ -308,10 +300,10 @@ togglePopupStatusPanel: function() { ...@@ -308,10 +300,10 @@ togglePopupStatusPanel: function() {
parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px";
UI.popupStatusOpen = true; UI.popupStatusOpen = true;
} }
}, },
// Show the XVP panel // Show the XVP panel
toggleXvpPanel: function() { toggleXvpPanel: function() {
// Close the description panel // Close the description panel
$D('noVNC_description').style.display = "none"; $D('noVNC_description').style.display = "none";
// Close settings if open // Close settings if open
...@@ -341,10 +333,10 @@ toggleXvpPanel: function() { ...@@ -341,10 +333,10 @@ toggleXvpPanel: function() {
$D('xvpButton').className = "noVNC_status_button_selected"; $D('xvpButton').className = "noVNC_status_button_selected";
UI.xvpOpen = true; UI.xvpOpen = true;
} }
}, },
// Show the clipboard panel // Show the clipboard panel
toggleClipboardPanel: function() { toggleClipboardPanel: function() {
// Close the description panel // Close the description panel
$D('noVNC_description').style.display = "none"; $D('noVNC_description').style.display = "none";
// Close settings if open // Close settings if open
...@@ -374,10 +366,10 @@ toggleClipboardPanel: function() { ...@@ -374,10 +366,10 @@ toggleClipboardPanel: function() {
$D('clipboardButton').className = "noVNC_status_button_selected"; $D('clipboardButton').className = "noVNC_status_button_selected";
UI.clipboardOpen = true; UI.clipboardOpen = true;
} }
}, },
// Show the connection settings panel/menu // Show the connection settings panel/menu
toggleConnectPanel: function() { toggleConnectPanel: function() {
// Close the description panel // Close the description panel
$D('noVNC_description').style.display = "none"; $D('noVNC_description').style.display = "none";
// Close connection settings if open // Close connection settings if open
...@@ -413,12 +405,12 @@ toggleConnectPanel: function() { ...@@ -413,12 +405,12 @@ toggleConnectPanel: function() {
UI.connSettingsOpen = true; UI.connSettingsOpen = true;
$D('noVNC_host').focus(); $D('noVNC_host').focus();
} }
}, },
// Toggle the settings menu: // Toggle the settings menu:
// On open, settings are refreshed from saved cookies. // On open, settings are refreshed from saved cookies.
// On close, settings are applied // On close, settings are applied
toggleSettingsPanel: function() { toggleSettingsPanel: function() {
// Close the description panel // Close the description panel
$D('noVNC_description').style.display = "none"; $D('noVNC_description').style.display = "none";
if (UI.settingsOpen) { if (UI.settingsOpen) {
...@@ -443,10 +435,10 @@ toggleSettingsPanel: function() { ...@@ -443,10 +435,10 @@ toggleSettingsPanel: function() {
UI.openSettingsMenu(); UI.openSettingsMenu();
} }
}, },
// Open menu // Open menu
openSettingsMenu: function() { openSettingsMenu: function() {
// Close the description panel // Close the description panel
$D('noVNC_description').style.display = "none"; $D('noVNC_description').style.display = "none";
// Close clipboard panel if open // Close clipboard panel if open
...@@ -468,17 +460,17 @@ openSettingsMenu: function() { ...@@ -468,17 +460,17 @@ openSettingsMenu: function() {
$D('noVNC_settings').style.display = "block"; $D('noVNC_settings').style.display = "block";
$D('settingsButton').className = "noVNC_status_button_selected"; $D('settingsButton').className = "noVNC_status_button_selected";
UI.settingsOpen = true; UI.settingsOpen = true;
}, },
// Close menu (without applying settings) // Close menu (without applying settings)
closeSettingsMenu: function() { closeSettingsMenu: function() {
$D('noVNC_settings').style.display = "none"; $D('noVNC_settings').style.display = "none";
$D('settingsButton').className = "noVNC_status_button"; $D('settingsButton').className = "noVNC_status_button";
UI.settingsOpen = false; UI.settingsOpen = false;
}, },
// Save/apply settings when 'Apply' button is pressed // Save/apply settings when 'Apply' button is pressed
settingsApply: function() { settingsApply: function() {
//Util.Debug(">> settingsApply"); //Util.Debug(">> settingsApply");
UI.saveSetting('encrypt'); UI.saveSetting('encrypt');
UI.saveSetting('true_color'); UI.saveSetting('true_color');
...@@ -499,11 +491,11 @@ settingsApply: function() { ...@@ -499,11 +491,11 @@ settingsApply: function() {
UI.setViewClip(); UI.setViewClip();
UI.setViewDrag(UI.rfb.get_viewportDrag()); UI.setViewDrag(UI.rfb.get_viewportDrag());
//Util.Debug("<< settingsApply"); //Util.Debug("<< settingsApply");
}, },
setPassword: function() { setPassword: function() {
UI.rfb.sendPassword($D('noVNC_password').value); UI.rfb.sendPassword($D('noVNC_password').value);
//Reset connect button. //Reset connect button.
$D('noVNC_connect_button').value = "Connect"; $D('noVNC_connect_button').value = "Connect";
...@@ -511,27 +503,25 @@ setPassword: function() { ...@@ -511,27 +503,25 @@ setPassword: function() {
//Hide connection panel. //Hide connection panel.
UI.toggleConnectPanel(); UI.toggleConnectPanel();
return false; return false;
}, },
sendCtrlAltDel: function() { sendCtrlAltDel: function() {
UI.rfb.sendCtrlAltDel(); UI.rfb.sendCtrlAltDel();
}, },
xvpShutdown: function() { xvpShutdown: function() {
UI.rfb.xvpShutdown(); UI.rfb.xvpShutdown();
}, },
xvpReboot: function() { xvpReboot: function() {
UI.rfb.xvpReboot(); UI.rfb.xvpReboot();
}, },
xvpReset: function() { xvpReset: function() {
UI.rfb.xvpReset(); UI.rfb.xvpReset();
}, },
setMouseButton: function(num) {
var b, blist = [0, 1,2,4], button;
setMouseButton: function(num) {
if (typeof num === 'undefined') { if (typeof num === 'undefined') {
// Disable mouse buttons // Disable mouse buttons
num = -1; num = -1;
...@@ -540,25 +530,20 @@ setMouseButton: function(num) { ...@@ -540,25 +530,20 @@ setMouseButton: function(num) {
UI.rfb.get_mouse().set_touchButton(num); UI.rfb.get_mouse().set_touchButton(num);
} }
for (b = 0; b < blist.length; b++) { var blist = [0, 1,2,4];
button = $D('noVNC_mouse_button' + blist[b]); for (var b = 0; b < blist.length; b++) {
var button = $D('noVNC_mouse_button' + blist[b]);
if (blist[b] === num) { if (blist[b] === num) {
button.style.display = ""; button.style.display = "";
} else { } else {
button.style.display = "none"; button.style.display = "none";
/*
button.style.backgroundColor = "black";
button.style.color = "lightgray";
button.style.backgroundColor = "";
button.style.color = "";
*/
} }
} }
}, },
updateState: function(rfb, state, oldstate, msg) { updateState: function(rfb, state, oldstate, msg) {
var s, sb, c, d, cad, vd, klass;
UI.rfb_state = state; UI.rfb_state = state;
var klass;
switch (state) { switch (state) {
case 'failed': case 'failed':
case 'fatal': case 'fatal':
...@@ -569,7 +554,7 @@ updateState: function(rfb, state, oldstate, msg) { ...@@ -569,7 +554,7 @@ updateState: function(rfb, state, oldstate, msg) {
break; break;
case 'disconnected': case 'disconnected':
$D('noVNC_logo').style.display = "block"; $D('noVNC_logo').style.display = "block";
// Fall through /* falls through */
case 'loaded': case 'loaded':
klass = "noVNC_status_normal"; klass = "noVNC_status_normal";
break; break;
...@@ -593,10 +578,10 @@ updateState: function(rfb, state, oldstate, msg) { ...@@ -593,10 +578,10 @@ updateState: function(rfb, state, oldstate, msg) {
} }
UI.updateVisualState(); UI.updateVisualState();
}, },
// Disable/enable controls depending on connection state // Disable/enable controls depending on connection state
updateVisualState: function() { updateVisualState: function() {
var connected = UI.rfb_state === 'normal' ? true : false; var connected = UI.rfb_state === 'normal' ? true : false;
//Util.Debug(">> updateVisualState"); //Util.Debug(">> updateVisualState");
...@@ -649,10 +634,10 @@ updateVisualState: function() { ...@@ -649,10 +634,10 @@ updateVisualState: function() {
} }
//Util.Debug("<< updateVisualState"); //Util.Debug("<< updateVisualState");
}, },
// Disable/enable XVP button // Disable/enable XVP button
updateXvpVisualState: function(ver) { updateXvpVisualState: function(ver) {
if (ver >= 1) { if (ver >= 1) {
$D('xvpButton').style.display = 'inline'; $D('xvpButton').style.display = 'inline';
} else { } else {
...@@ -662,34 +647,29 @@ updateXvpVisualState: function(ver) { ...@@ -662,34 +647,29 @@ updateXvpVisualState: function(ver) {
UI.toggleXvpPanel(); UI.toggleXvpPanel();
} }
} }
}, },
// Display the desktop name in the document title
// Display the desktop name in the document title updateDocumentTitle: function(rfb, name) {
updateDocumentTitle: function(rfb, name) {
document.title = name + " - noVNC"; document.title = name + " - noVNC";
}, },
clipReceive: function(rfb, text) { clipReceive: function(rfb, text) {
Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
$D('noVNC_clipboard_text').value = text; $D('noVNC_clipboard_text').value = text;
Util.Debug("<< UI.clipReceive"); Util.Debug("<< UI.clipReceive");
}, },
connect: function() {
var host, port, password, path;
connect: function() {
UI.closeSettingsMenu(); UI.closeSettingsMenu();
UI.toggleConnectPanel(); UI.toggleConnectPanel();
host = $D('noVNC_host').value; var host = $D('noVNC_host').value;
port = $D('noVNC_port').value; var port = $D('noVNC_port').value;
password = $D('noVNC_password').value; var password = $D('noVNC_password').value;
path = $D('noVNC_path').value; var path = $D('noVNC_path').value;
if ((!host) || (!port)) { if ((!host) || (!port)) {
throw("Must set host and port"); throw new Error("Must set host and port");
} }
UI.rfb.set_encrypt(UI.getSetting('encrypt')); UI.rfb.set_encrypt(UI.getSetting('encrypt'));
...@@ -704,51 +684,49 @@ connect: function() { ...@@ -704,51 +684,49 @@ connect: function() {
//Close dialog. //Close dialog.
setTimeout(UI.setBarPosition, 100); setTimeout(UI.setBarPosition, 100);
$D('noVNC_logo').style.display = "none"; $D('noVNC_logo').style.display = "none";
}, },
disconnect: function() { disconnect: function() {
UI.closeSettingsMenu(); UI.closeSettingsMenu();
UI.rfb.disconnect(); UI.rfb.disconnect();
$D('noVNC_logo').style.display = "block"; $D('noVNC_logo').style.display = "block";
UI.connSettingsOpen = false; UI.connSettingsOpen = false;
UI.toggleConnectPanel(); UI.toggleConnectPanel();
}, },
displayBlur: function() { displayBlur: function() {
UI.rfb.get_keyboard().set_focused(false); UI.rfb.get_keyboard().set_focused(false);
UI.rfb.get_mouse().set_focused(false); UI.rfb.get_mouse().set_focused(false);
}, },
displayFocus: function() { displayFocus: function() {
UI.rfb.get_keyboard().set_focused(true); UI.rfb.get_keyboard().set_focused(true);
UI.rfb.get_mouse().set_focused(true); UI.rfb.get_mouse().set_focused(true);
}, },
clipClear: function() { clipClear: function() {
$D('noVNC_clipboard_text').value = ""; $D('noVNC_clipboard_text').value = "";
UI.rfb.clipboardPasteFrom(""); UI.rfb.clipboardPasteFrom("");
}, },
clipSend: function() { clipSend: function() {
var text = $D('noVNC_clipboard_text').value; var text = $D('noVNC_clipboard_text').value;
Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "...");
UI.rfb.clipboardPasteFrom(text); UI.rfb.clipboardPasteFrom(text);
Util.Debug("<< UI.clipSend"); Util.Debug("<< UI.clipSend");
}, },
// Enable/disable and configure viewport clipping
setViewClip: function(clip) {
var display, cur_clip, pos, new_w, new_h;
// Enable/disable and configure viewport clipping
setViewClip: function(clip) {
var display;
if (UI.rfb) { if (UI.rfb) {
display = UI.rfb.get_display(); display = UI.rfb.get_display();
} else { } else {
return; return;
} }
cur_clip = display.get_viewport(); var cur_clip = display.get_viewport();
if (typeof(clip) !== 'boolean') { if (typeof(clip) !== 'boolean') {
// Use current setting // Use current setting
...@@ -768,16 +746,16 @@ setViewClip: function(clip) { ...@@ -768,16 +746,16 @@ setViewClip: function(clip) {
if (UI.getSetting('clip')) { if (UI.getSetting('clip')) {
// If clipping, update clipping settings // If clipping, update clipping settings
$D('noVNC_canvas').style.position = 'absolute'; $D('noVNC_canvas').style.position = 'absolute';
pos = Util.getPosition($D('noVNC_canvas')); var pos = Util.getPosition($D('noVNC_canvas'));
new_w = window.innerWidth - pos.x; var new_w = window.innerWidth - pos.x;
new_h = window.innerHeight - pos.y; var new_h = window.innerHeight - pos.y;
display.set_viewport(true); display.set_viewport(true);
display.viewportChange(0, 0, new_w, new_h); display.viewportChange(0, 0, new_w, new_h);
} }
}, },
// Toggle/set/unset the viewport drag/move button // Toggle/set/unset the viewport drag/move button
setViewDrag: function(drag) { setViewDrag: function(drag) {
var vmb = $D('noVNC_view_drag_button'); var vmb = $D('noVNC_view_drag_button');
if (!UI.rfb) { return; } if (!UI.rfb) { return; }
...@@ -800,14 +778,13 @@ setViewDrag: function(drag) { ...@@ -800,14 +778,13 @@ setViewDrag: function(drag) {
vmb.className = "noVNC_status_button"; vmb.className = "noVNC_status_button";
UI.rfb.set_viewportDrag(false); UI.rfb.set_viewportDrag(false);
} }
}, },
// On touch devices, show the OS keyboard // On touch devices, show the OS keyboard
showKeyboard: function() { showKeyboard: function() {
var kbi, skb, l; var kbi = $D('keyboardinput');
kbi = $D('keyboardinput'); var skb = $D('showKeyboard');
skb = $D('showKeyboard'); var l = kbi.value.length;
l = kbi.value.length;
if(UI.keyboardVisible === false) { if(UI.keyboardVisible === false) {
kbi.focus(); kbi.focus();
try { kbi.setSelectionRange(l, l); } // Move the caret to the end try { kbi.setSelectionRange(l, l); } // Move the caret to the end
...@@ -819,9 +796,9 @@ showKeyboard: function() { ...@@ -819,9 +796,9 @@ showKeyboard: function() {
skb.className = "noVNC_status_button"; skb.className = "noVNC_status_button";
UI.keyboardVisible = false; UI.keyboardVisible = false;
} }
}, },
keepKeyboard: function() { keepKeyboard: function() {
clearTimeout(UI.hideKeyboardTimeout); clearTimeout(UI.hideKeyboardTimeout);
if(UI.keyboardVisible === true) { if(UI.keyboardVisible === true) {
$D('keyboardinput').focus(); $D('keyboardinput').focus();
...@@ -830,23 +807,23 @@ keepKeyboard: function() { ...@@ -830,23 +807,23 @@ keepKeyboard: function() {
$D('keyboardinput').blur(); $D('keyboardinput').blur();
$D('showKeyboard').className = "noVNC_status_button"; $D('showKeyboard').className = "noVNC_status_button";
} }
}, },
keyboardinputReset: function() { keyboardinputReset: function() {
var kbi = $D('keyboardinput'); var kbi = $D('keyboardinput');
kbi.value = Array(UI.defaultKeyboardinputLen).join("_"); kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
UI.lastKeyboardinput = kbi.value; UI.lastKeyboardinput = kbi.value;
}, },
// When normal keyboard events are left uncought, use the input events from // When normal keyboard events are left uncought, use the input events from
// the keyboardinput element instead and generate the corresponding key events. // the keyboardinput element instead and generate the corresponding key events.
// This code is required since some browsers on Android are inconsistent in // This code is required since some browsers on Android are inconsistent in
// sending keyCodes in the normal keyboard events when using on screen keyboards. // sending keyCodes in the normal keyboard events when using on screen keyboards.
keyInput: function(event) { keyInput: function(event) {
var newValue, oldValue, newLen, oldLen; var newValue = event.target.value;
newValue = event.target.value; var oldValue = UI.lastKeyboardinput;
oldValue = UI.lastKeyboardinput;
var newLen;
try { try {
// Try to check caret position since whitespace at the end // Try to check caret position since whitespace at the end
// will not be considered by value.length in some browsers // will not be considered by value.length in some browsers
...@@ -855,18 +832,20 @@ keyInput: function(event) { ...@@ -855,18 +832,20 @@ keyInput: function(event) {
// selectionStart is undefined in Google Chrome // selectionStart is undefined in Google Chrome
newLen = newValue.length; newLen = newValue.length;
} }
oldLen = oldValue.length; var oldLen = oldValue.length;
var backspaces; var backspaces;
var inputs = newLen - oldLen; var inputs = newLen - oldLen;
if (inputs < 0) if (inputs < 0) {
backspaces = -inputs; backspaces = -inputs;
else } else {
backspaces = 0; backspaces = 0;
}
// Compare the old string with the new to account for // Compare the old string with the new to account for
// text-corrections or other input that modify existing text // text-corrections or other input that modify existing text
for (var i = 0; i < Math.min(oldLen, newLen); i++) { var i;
for (i = 0; i < Math.min(oldLen, newLen); i++) {
if (newValue.charAt(i) != oldValue.charAt(i)) { if (newValue.charAt(i) != oldValue.charAt(i)) {
inputs = newLen - i; inputs = newLen - i;
backspaces = oldLen - i; backspaces = oldLen - i;
...@@ -875,10 +854,12 @@ keyInput: function(event) { ...@@ -875,10 +854,12 @@ keyInput: function(event) {
} }
// Send the key events // Send the key events
for (var i = 0; i < backspaces; i++) for (i = 0; i < backspaces; i++) {
UI.rfb.sendKey(XK_BackSpace); UI.rfb.sendKey(XK_BackSpace);
for (var i = newLen - inputs; i < newLen; i++) }
for (i = newLen - inputs; i < newLen; i++) {
UI.rfb.sendKey(newValue.charCodeAt(i)); UI.rfb.sendKey(newValue.charCodeAt(i));
}
// Control the text content length in the keyboardinput element // Control the text content length in the keyboardinput element
if (newLen > 2 * UI.defaultKeyboardinputLen) { if (newLen > 2 * UI.defaultKeyboardinputLen) {
...@@ -893,21 +874,20 @@ keyInput: function(event) { ...@@ -893,21 +874,20 @@ keyInput: function(event) {
event.target.blur(); event.target.blur();
// This has to be ran outside of the input handler in order to work // This has to be ran outside of the input handler in order to work
setTimeout(function() { UI.keepKeyboard(); }, 0); setTimeout(function() { UI.keepKeyboard(); }, 0);
} else { } else {
UI.lastKeyboardinput = newValue; UI.lastKeyboardinput = newValue;
} }
}, },
keyInputBlur: function() { keyInputBlur: function() {
$D('showKeyboard').className = "noVNC_status_button"; $D('showKeyboard').className = "noVNC_status_button";
//Weird bug in iOS if you change keyboardVisible //Weird bug in iOS if you change keyboardVisible
//here it does not actually occur so next time //here it does not actually occur so next time
//you click keyboard icon it doesnt work. //you click keyboard icon it doesnt work.
UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100);
}, },
showExtraKeys: function() { showExtraKeys: function() {
UI.keepKeyboard(); UI.keepKeyboard();
if(UI.extraKeysVisible === false) { if(UI.extraKeysVisible === false) {
$D('toggleCtrlButton').style.display = "inline"; $D('toggleCtrlButton').style.display = "inline";
...@@ -924,9 +904,9 @@ showExtraKeys: function() { ...@@ -924,9 +904,9 @@ showExtraKeys: function() {
$D('showExtraKeysButton').className = "noVNC_status_button"; $D('showExtraKeysButton').className = "noVNC_status_button";
UI.extraKeysVisible = false; UI.extraKeysVisible = false;
} }
}, },
toggleCtrl: function() { toggleCtrl: function() {
UI.keepKeyboard(); UI.keepKeyboard();
if(UI.ctrlOn === false) { if(UI.ctrlOn === false) {
UI.rfb.sendKey(XK_Control_L, true); UI.rfb.sendKey(XK_Control_L, true);
...@@ -937,9 +917,9 @@ toggleCtrl: function() { ...@@ -937,9 +917,9 @@ toggleCtrl: function() {
$D('toggleCtrlButton').className = "noVNC_status_button"; $D('toggleCtrlButton').className = "noVNC_status_button";
UI.ctrlOn = false; UI.ctrlOn = false;
} }
}, },
toggleAlt: function() { toggleAlt: function() {
UI.keepKeyboard(); UI.keepKeyboard();
if(UI.altOn === false) { if(UI.altOn === false) {
UI.rfb.sendKey(XK_Alt_L, true); UI.rfb.sendKey(XK_Alt_L, true);
...@@ -950,54 +930,50 @@ toggleAlt: function() { ...@@ -950,54 +930,50 @@ toggleAlt: function() {
$D('toggleAltButton').className = "noVNC_status_button"; $D('toggleAltButton').className = "noVNC_status_button";
UI.altOn = false; UI.altOn = false;
} }
}, },
sendTab: function() { sendTab: function() {
UI.keepKeyboard(); UI.keepKeyboard();
UI.rfb.sendKey(XK_Tab); UI.rfb.sendKey(XK_Tab);
}, },
sendEsc: function() { sendEsc: function() {
UI.keepKeyboard(); UI.keepKeyboard();
UI.rfb.sendKey(XK_Escape); UI.rfb.sendKey(XK_Escape);
}, },
setKeyboard: function() { setKeyboard: function() {
UI.keyboardVisible = false; UI.keyboardVisible = false;
}, },
// iOS < Version 5 does not support position fixed. Javascript workaround: // iOS < Version 5 does not support position fixed. Javascript workaround:
setOnscroll: function() { setOnscroll: function() {
window.onscroll = function() { window.onscroll = function() {
UI.setBarPosition(); UI.setBarPosition();
}; };
}, },
setResize: function () { setResize: function () {
window.onResize = function() { window.onResize = function() {
UI.setBarPosition(); UI.setBarPosition();
}; };
}, },
//Helper to add options to dropdown. //Helper to add options to dropdown.
addOption: function(selectbox,text,value ) addOption: function(selectbox, text, value) {
{
var optn = document.createElement("OPTION"); var optn = document.createElement("OPTION");
optn.text = text; optn.text = text;
optn.value = value; optn.value = value;
selectbox.options.add(optn); selectbox.options.add(optn);
}, },
setBarPosition: function() { setBarPosition: function() {
$D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px';
$D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px';
var vncwidth = $D('noVNC_screen').style.offsetWidth; var vncwidth = $D('noVNC_screen').style.offsetWidth;
$D('noVNC-control-bar').style.width = vncwidth + 'px'; $D('noVNC-control-bar').style.width = vncwidth + 'px';
} }
};
};
})();
...@@ -6,9 +6,8 @@ ...@@ -6,9 +6,8 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
"use strict"; /* jshint white: false, nonstandard: true */
/*jslint bitwise: false, white: false */ /*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */
/*global window, console, document, navigator, ActiveXObject */
// Globals defined here // Globals defined here
var Util = {}; var Util = {};
...@@ -19,39 +18,42 @@ var Util = {}; ...@@ -19,39 +18,42 @@ var Util = {};
*/ */
Array.prototype.push8 = function (num) { Array.prototype.push8 = function (num) {
"use strict";
this.push(num & 0xFF); this.push(num & 0xFF);
}; };
Array.prototype.push16 = function (num) { Array.prototype.push16 = function (num) {
"use strict";
this.push((num >> 8) & 0xFF, this.push((num >> 8) & 0xFF,
(num ) & 0xFF ); num & 0xFF);
}; };
Array.prototype.push32 = function (num) { Array.prototype.push32 = function (num) {
"use strict";
this.push((num >> 24) & 0xFF, this.push((num >> 24) & 0xFF,
(num >> 16) & 0xFF, (num >> 16) & 0xFF,
(num >> 8) & 0xFF, (num >> 8) & 0xFF,
(num ) & 0xFF ); num & 0xFF);
}; };
// IE does not support map (even in IE9) // IE does not support map (even in IE9)
//This prototype is provided by the Mozilla foundation and //This prototype is provided by the Mozilla foundation and
//is distributed under the MIT license. //is distributed under the MIT license.
//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
if (!Array.prototype.map) if (!Array.prototype.map) {
{ Array.prototype.map = function (fun /*, thisp*/) {
Array.prototype.map = function(fun /*, thisp*/) "use strict";
{
var len = this.length; var len = this.length;
if (typeof fun != "function") if (typeof fun != "function") {
throw new TypeError(); throw new TypeError();
}
var res = new Array(len); var res = new Array(len);
var thisp = arguments[1]; var thisp = arguments[1];
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++) {
{ if (i in this) {
if (i in this)
res[i] = fun.call(thisp, this[i], i, this); res[i] = fun.call(thisp, this[i], i, this);
} }
}
return res; return res;
}; };
...@@ -61,41 +63,111 @@ if (!Array.prototype.map) ...@@ -61,41 +63,111 @@ if (!Array.prototype.map)
//This prototype is provided by the Mozilla foundation and //This prototype is provided by the Mozilla foundation and
//is distributed under the MIT license. //is distributed under the MIT license.
//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
if (!Array.prototype.indexOf) if (!Array.prototype.indexOf) {
{ Array.prototype.indexOf = function (elt /*, from*/) {
Array.prototype.indexOf = function(elt /*, from*/) "use strict";
{
var len = this.length >>> 0; var len = this.length >>> 0;
var from = Number(arguments[1]) || 0; var from = Number(arguments[1]) || 0;
from = (from < 0) from = (from < 0) ? Math.ceil(from) : Math.floor(from);
? Math.ceil(from) if (from < 0) {
: Math.floor(from);
if (from < 0)
from += len; from += len;
}
for (; from < len; from++) for (; from < len; from++) {
{
if (from in this && if (from in this &&
this[from] === elt) this[from] === elt) {
return from; return from;
} }
}
return -1; return -1;
}; };
} }
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
})();
}
// PhantomJS 1.x doesn't support bind,
// so leave this in until PhantomJS 2.0 is released
//This prototype is provided by the Mozilla foundation and
//is distributed under the MIT license.
//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - " +
"what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
// //
// requestAnimationFrame shim with setTimeout fallback // requestAnimationFrame shim with setTimeout fallback
// //
window.requestAnimFrame = (function(){ window.requestAnimFrame = (function () {
"use strict";
return window.requestAnimationFrame || return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame || window.oRequestAnimationFrame ||
window.msRequestAnimationFrame || window.msRequestAnimationFrame ||
function(callback){ function (callback) {
window.setTimeout(callback, 1000 / 60); window.setTimeout(callback, 1000 / 60);
}; };
})(); })();
...@@ -112,6 +184,7 @@ window.requestAnimFrame = (function(){ ...@@ -112,6 +184,7 @@ window.requestAnimFrame = (function(){
Util._log_level = 'warn'; Util._log_level = 'warn';
Util.init_logging = function (level) { Util.init_logging = function (level) {
"use strict";
if (typeof level === 'undefined') { if (typeof level === 'undefined') {
level = Util._log_level; level = Util._log_level;
} else { } else {
...@@ -122,26 +195,34 @@ Util.init_logging = function (level) { ...@@ -122,26 +195,34 @@ Util.init_logging = function (level) {
window.console = { window.console = {
'log' : window.opera.postError, 'log' : window.opera.postError,
'warn' : window.opera.postError, 'warn' : window.opera.postError,
'error': window.opera.postError }; 'error': window.opera.postError
};
} else { } else {
window.console = { window.console = {
'log' : function(m) {}, 'log' : function (m) {},
'warn' : function(m) {}, 'warn' : function (m) {},
'error': function(m) {}}; 'error': function (m) {}
};
} }
} }
Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {};
/* jshint -W086 */
switch (level) { switch (level) {
case 'debug': Util.Debug = function (msg) { console.log(msg); }; case 'debug':
case 'info': Util.Info = function (msg) { console.log(msg); }; Util.Debug = function (msg) { console.log(msg); };
case 'warn': Util.Warn = function (msg) { console.warn(msg); }; case 'info':
case 'error': Util.Error = function (msg) { console.error(msg); }; Util.Info = function (msg) { console.log(msg); };
case 'warn':
Util.Warn = function (msg) { console.warn(msg); };
case 'error':
Util.Error = function (msg) { console.error(msg); };
case 'none': case 'none':
break; break;
default: default:
throw("invalid logging type '" + level + "'"); throw new Error("invalid logging type '" + level + "'");
} }
/* jshint +W086 */
}; };
Util.get_logging = function () { Util.get_logging = function () {
return Util._log_level; return Util._log_level;
...@@ -149,93 +230,133 @@ Util.get_logging = function () { ...@@ -149,93 +230,133 @@ Util.get_logging = function () {
// Initialize logging level // Initialize logging level
Util.init_logging(); Util.init_logging();
Util.make_property = function (proto, name, mode, type) {
"use strict";
// Set configuration default for Crockford style function namespaces var getter;
Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) { if (type === 'arr') {
var getter, setter;
// Default getter function
getter = function (idx) { getter = function (idx) {
if ((type in {'arr':1, 'array':1}) && if (typeof idx !== 'undefined') {
(typeof idx !== 'undefined')) { return this['_' + name][idx];
return cfg[v][idx];
} else { } else {
return cfg[v]; return this['_' + name];
} }
}; };
} else {
getter = function () {
return this['_' + name];
};
}
// Default setter function var make_setter = function (process_val) {
setter = function (val, idx) { if (process_val) {
if (type in {'boolean':1, 'bool':1}) { return function (val, idx) {
if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { if (typeof idx !== 'undefined') {
val = false; this['_' + name][idx] = process_val(val);
} else { } else {
val = true; this['_' + name] = process_val(val);
}
};
} else {
return function (val, idx) {
if (typeof idx !== 'undefined') {
this['_' + name][idx] = val;
} else {
this['_' + name] = val;
}
};
} }
} else if (type in {'integer':1, 'int':1}) { };
val = parseInt(val, 10);
var setter;
if (type === 'bool') {
setter = make_setter(function (val) {
if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) {
return false;
} else {
return true;
}
});
} else if (type === 'int') {
setter = make_setter(function (val) { return parseInt(val, 10); });
} else if (type === 'float') {
setter = make_setter(parseFloat);
} else if (type === 'str') { } else if (type === 'str') {
val = String(val); setter = make_setter(String);
} else if (type === 'func') { } else if (type === 'func') {
setter = make_setter(function (val) {
if (!val) { if (!val) {
val = function () {}; return function () {};
} } else {
return val;
} }
if (typeof idx !== 'undefined') { });
cfg[v][idx] = val; } else if (type === 'arr' || type === 'dom' || type == 'raw') {
setter = make_setter();
} else { } else {
cfg[v] = val; throw new Error('Unknown property type ' + type); // some sanity checking
} }
};
// Set the description
api[v + '_description'] = desc;
// Set the getter function // set the getter
if (typeof api['get_' + v] === 'undefined') { if (typeof proto['get_' + name] === 'undefined') {
api['get_' + v] = getter; proto['get_' + name] = getter;
} }
// Set the setter function with extra sanity checks // set the setter if needed
if (typeof api['set_' + v] === 'undefined') { if (typeof proto['set_' + name] === 'undefined') {
api['set_' + v] = function (val, idx) { if (mode === 'rw') {
if (mode in {'RO':1, 'ro':1}) { proto['set_' + name] = setter;
throw(v + " is read-only"); } else if (mode === 'wo') {
} else if ((mode in {'WO':1, 'wo':1}) && proto['set_' + name] = function (val, idx) {
(typeof cfg[v] !== 'undefined')) { if (typeof this['_' + name] !== 'undefined') {
throw(v + " can only be set once"); throw new Error(name + " can only be set once");
} }
setter(val, idx); setter.call(this, val, idx);
}; };
} }
}
// Set the default value // make a special setter that we can use in set defaults
if (typeof defaults[v] !== 'undefined') { proto['_raw_set_' + name] = function (val, idx) {
defval = defaults[v]; setter.call(this, val, idx);
} else if ((type in {'arr':1, 'array':1}) && //delete this['_init_set_' + name]; // remove it after use
(! (defval instanceof Array))) { };
defval = []; };
Util.make_properties = function (constructor, arr) {
"use strict";
for (var i = 0; i < arr.length; i++) {
Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]);
} }
// Coerce existing setting to the right type
//Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]);
setter(defval);
}; };
// Set group of configuration defaults Util.set_defaults = function (obj, conf, defaults) {
Util.conf_defaults = function(cfg, api, defaults, arr) { var defaults_keys = Object.keys(defaults);
var conf_keys = Object.keys(conf);
var keys_obj = {};
var i; var i;
for (i = 0; i < arr.length; i++) { for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; }
Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1], for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; }
arr[i][2], arr[i][3], arr[i][4]); var keys = Object.keys(keys_obj);
for (i = 0; i < keys.length; i++) {
var setter = obj['_raw_set_' + keys[i]];
if (conf[keys[i]]) {
setter.call(obj, conf[keys[i]]);
} else {
setter.call(obj, defaults[keys[i]]);
}
} }
}; };
/* /*
* Decode from UTF-8 * Decode from UTF-8
*/ */
Util.decodeUTF8 = function(utf8string) { Util.decodeUTF8 = function (utf8string) {
"use strict";
return decodeURIComponent(escape(utf8string)); return decodeURIComponent(escape(utf8string));
} };
...@@ -250,20 +371,17 @@ Util.decodeUTF8 = function(utf8string) { ...@@ -250,20 +371,17 @@ Util.decodeUTF8 = function(utf8string) {
// Handles the case where load_scripts is invoked from a script that // Handles the case where load_scripts is invoked from a script that
// itself is loaded via load_scripts. Once all scripts are loaded the // itself is loaded via load_scripts. Once all scripts are loaded the
// window.onscriptsloaded handler is called (if set). // window.onscriptsloaded handler is called (if set).
Util.get_include_uri = function() { Util.get_include_uri = function () {
return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/";
} };
Util._loading_scripts = []; Util._loading_scripts = [];
Util._pending_scripts = []; Util._pending_scripts = [];
Util.load_scripts = function(files) { Util.load_scripts = function (files) {
"use strict";
var head = document.getElementsByTagName('head')[0], script, var head = document.getElementsByTagName('head')[0], script,
ls = Util._loading_scripts, ps = Util._pending_scripts; ls = Util._loading_scripts, ps = Util._pending_scripts;
for (var f=0; f<files.length; f++) {
script = document.createElement('script'); var loadFunc = function (e) {
script.type = 'text/javascript';
script.src = Util.get_include_uri() + files[f];
//console.log("loading script: " + script.src);
script.onload = script.onreadystatechange = function (e) {
while (ls.length > 0 && (ls[0].readyState === 'loaded' || while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
ls[0].readyState === 'complete')) { ls[0].readyState === 'complete')) {
// For IE, append the script to trigger execution // For IE, append the script to trigger execution
...@@ -286,6 +404,13 @@ Util.load_scripts = function(files) { ...@@ -286,6 +404,13 @@ Util.load_scripts = function(files) {
} }
} }
}; };
for (var f = 0; f < files.length; f++) {
script = document.createElement('script');
script.type = 'text/javascript';
script.src = Util.get_include_uri() + files[f];
//console.log("loading script: " + script.src);
script.onload = script.onreadystatechange = loadFunc;
// In-order script execution tricks // In-order script execution tricks
if (Util.Engine.trident) { if (Util.Engine.trident) {
// For IE wait until readyState is 'loaded' before // For IE wait until readyState is 'loaded' before
...@@ -300,20 +425,22 @@ Util.load_scripts = function(files) { ...@@ -300,20 +425,22 @@ Util.load_scripts = function(files) {
} }
ps.push(script); ps.push(script);
} }
} };
// Get DOM element position on page // Get DOM element position on page
// This solution is based based on http://www.greywyvern.com/?post=331 // This solution is based based on http://www.greywyvern.com/?post=331
// Thanks to Brian Huisman AKA GreyWyvern! // Thanks to Brian Huisman AKA GreyWyvern!
Util.getPosition = (function() { Util.getPosition = (function () {
"use strict";
function getStyle(obj, styleProp) { function getStyle(obj, styleProp) {
var y;
if (obj.currentStyle) { if (obj.currentStyle) {
var y = obj.currentStyle[styleProp]; y = obj.currentStyle[styleProp];
} else if (window.getComputedStyle) } else if (window.getComputedStyle)
var y = window.getComputedStyle(obj, null)[styleProp]; y = window.getComputedStyle(obj, null)[styleProp];
return y; return y;
}; }
function scrollDist() { function scrollDist() {
var myScrollTop = 0, myScrollLeft = 0; var myScrollTop = 0, myScrollLeft = 0;
...@@ -342,7 +469,7 @@ Util.getPosition = (function() { ...@@ -342,7 +469,7 @@ Util.getPosition = (function() {
} }
return [myScrollLeft, myScrollTop]; return [myScrollLeft, myScrollTop];
}; }
return function (obj) { return function (obj) {
var curleft = 0, curtop = 0, scr = obj, fixed = false; var curleft = 0, curtop = 0, scr = obj, fixed = false;
...@@ -362,7 +489,7 @@ Util.getPosition = (function() { ...@@ -362,7 +489,7 @@ Util.getPosition = (function() {
do { do {
curleft += obj.offsetLeft; curleft += obj.offsetLeft;
curtop += obj.offsetTop; curtop += obj.offsetTop;
} while (obj = obj.offsetParent); } while ((obj = obj.offsetParent));
return {'x': curleft, 'y': curtop}; return {'x': curleft, 'y': curtop};
}; };
...@@ -371,6 +498,7 @@ Util.getPosition = (function() { ...@@ -371,6 +498,7 @@ Util.getPosition = (function() {
// Get mouse event position in DOM element // Get mouse event position in DOM element
Util.getEventPosition = function (e, obj, scale) { Util.getEventPosition = function (e, obj, scale) {
"use strict";
var evt, docX, docY, pos; var evt, docX, docY, pos;
//if (!e) evt = window.event; //if (!e) evt = window.event;
evt = (e ? e : window.event); evt = (e ? e : window.event);
...@@ -390,38 +518,41 @@ Util.getEventPosition = function (e, obj, scale) { ...@@ -390,38 +518,41 @@ Util.getEventPosition = function (e, obj, scale) {
} }
var realx = docX - pos.x; var realx = docX - pos.x;
var realy = docY - pos.y; var realy = docY - pos.y;
var x = Math.max(Math.min(realx, obj.width-1), 0); var x = Math.max(Math.min(realx, obj.width - 1), 0);
var y = Math.max(Math.min(realy, obj.height-1), 0); var y = Math.max(Math.min(realy, obj.height - 1), 0);
return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale};
}; };
// Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events
Util.addEvent = function (obj, evType, fn){ Util.addEvent = function (obj, evType, fn) {
if (obj.attachEvent){ "use strict";
var r = obj.attachEvent("on"+evType, fn); if (obj.attachEvent) {
var r = obj.attachEvent("on" + evType, fn);
return r; return r;
} else if (obj.addEventListener){ } else if (obj.addEventListener) {
obj.addEventListener(evType, fn, false); obj.addEventListener(evType, fn, false);
return true; return true;
} else { } else {
throw("Handler could not be attached"); throw new Error("Handler could not be attached");
} }
}; };
Util.removeEvent = function(obj, evType, fn){ Util.removeEvent = function (obj, evType, fn) {
if (obj.detachEvent){ "use strict";
var r = obj.detachEvent("on"+evType, fn); if (obj.detachEvent) {
var r = obj.detachEvent("on" + evType, fn);
return r; return r;
} else if (obj.removeEventListener){ } else if (obj.removeEventListener) {
obj.removeEventListener(evType, fn, false); obj.removeEventListener(evType, fn, false);
return true; return true;
} else { } else {
throw("Handler could not be removed"); throw new Error("Handler could not be removed");
} }
}; };
Util.stopEvent = function(e) { Util.stopEvent = function (e) {
"use strict";
if (e.stopPropagation) { e.stopPropagation(); } if (e.stopPropagation) { e.stopPropagation(); }
else { e.cancelBubble = true; } else { e.cancelBubble = true; }
...@@ -433,38 +564,85 @@ Util.stopEvent = function(e) { ...@@ -433,38 +564,85 @@ Util.stopEvent = function(e) {
// Set browser engine versions. Based on mootools. // Set browser engine versions. Based on mootools.
Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)};
Util.Engine = { (function () {
"use strict";
// 'presto': (function () { return (!window.opera) ? false : true; }()),
var detectPresto = function () {
return !!window.opera;
};
// 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
var detectTrident = function () {
if (!window.ActiveXObject) {
return false;
} else {
if (window.XMLHttpRequest) {
return (document.querySelectorAll) ? 6 : 5;
} else {
return 4;
}
}
};
// 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
var detectInitialWebkit = function () {
try {
if (navigator.taintEnabled) {
return false;
} else {
if (Util.Features.xpath) {
return (Util.Features.query) ? 525 : 420;
} else {
return 419;
}
}
} catch (e) {
return false;
}
};
var detectActualWebkit = function (initial_ver) {
var re = /WebKit\/([0-9\.]*) /;
var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1];
return parseFloat(str_ver, 10);
};
// 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }())
var detectGecko = function () {
/* jshint -W041 */
if (!document.getBoxObjectFor && window.mozInnerScreenX == null) {
return false;
} else {
return (document.getElementsByClassName) ? 19 : 18;
}
/* jshint +W041 */
};
Util.Engine = {
// Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference)
//'presto': (function() { //'presto': (function() {
// return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
'presto': (function() { return (!window.opera) ? false : true; }()), 'presto': detectPresto(),
'trident': detectTrident(),
'trident': (function() { 'webkit': detectInitialWebkit(),
return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), 'gecko': detectGecko(),
'webkit': (function() { };
try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
//'webkit': (function() { if (Util.Engine.webkit) {
// return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()),
'gecko': (function() {
return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }())
};
if (Util.Engine.webkit) {
// Extract actual webkit version if available // Extract actual webkit version if available
Util.Engine.webkit = (function(v) { Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit);
var re = new RegExp('WebKit/([0-9\.]*) '); }
v = (navigator.userAgent.match(re) || ['', v])[1]; })();
return parseFloat(v, 10);
})(Util.Engine.webkit);
}
Util.Flash = (function(){ Util.Flash = (function () {
"use strict";
var v, version; var v, version;
try { try {
v = navigator.plugins['Shockwave Flash'].description; v = navigator.plugins['Shockwave Flash'].description;
} catch(err1) { } catch (err1) {
try { try {
v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
} catch(err2) { } catch (err2) {
v = '0 r0'; v = '0 r0';
} }
} }
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* read binary data off of the receive queue. * read binary data off of the receive queue.
*/ */
/*jslint browser: true, bitwise: false, plusplus: false */ /*jslint browser: true, bitwise: true */
/*global Util, Base64 */ /*global Util, Base64 */
...@@ -43,244 +43,185 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { ...@@ -43,244 +43,185 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
} }
Util.load_scripts(["web-socket-js/swfobject.js", Util.load_scripts(["web-socket-js/swfobject.js",
"web-socket-js/web_socket.js"]); "web-socket-js/web_socket.js"]);
}()); })();
} }
function Websock() { function Websock() {
"use strict"; "use strict";
var api = {}, // Public API this._websocket = null; // WebSocket object
websocket = null, // WebSocket object this._rQ = []; // Receive queue
mode = 'base64', // Current WebSocket mode: 'binary', 'base64' this._rQi = 0; // Receive queue index
rQ = [], // Receive queue this._rQmax = 10000; // Max receive queue size before compacting
rQi = 0, // Receive queue index this._sQ = []; // Send queue
rQmax = 10000, // Max receive queue size before compacting
sQ = [], // Send queue this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64'
this.maxBufferedAmount = 200;
eventHandlers = {
'message' : function() {}, this._eventHandlers = {
'open' : function() {}, 'message': function () {},
'close' : function() {}, 'open': function () {},
'error' : function() {} 'close': function () {},
'error': function () {}
};
}
(function () {
"use strict";
Websock.prototype = {
// Getters and Setters
get_sQ: function () {
return this._sQ;
}, },
test_mode = false; get_rQ: function () {
return this._rQ;
},
get_rQi: function () {
return this._rQi;
},
// set_rQi: function (val) {
// Queue public functions this._rQi = val;
// },
function get_sQ() { // Receive Queue
return sQ; rQlen: function () {
} return this._rQ.length - this._rQi;
},
function get_rQ() { rQpeek8: function () {
return rQ; return this._rQ[this._rQi];
} },
function get_rQi() {
return rQi;
}
function set_rQi(val) {
rQi = val;
}
function rQlen() { rQshift8: function () {
return rQ.length - rQi; return this._rQ[this._rQi++];
} },
function rQpeek8() { rQskip8: function () {
return (rQ[rQi] ); this._rQi++;
} },
function rQshift8() {
return (rQ[rQi++] ); rQskipBytes: function (num) {
} this._rQi += num;
function rQunshift8(num) { },
if (rQi === 0) {
rQ.unshift(num); rQunshift8: function (num) {
if (this._rQi === 0) {
this._rQ.unshift(num);
} else { } else {
rQi -= 1; this._rQi--;
rQ[rQi] = num; this._rQ[this._rQi] = num;
} }
},
} rQshift16: function () {
function rQshift16() { return (this._rQ[this._rQi++] << 8) +
return (rQ[rQi++] << 8) + this._rQ[this._rQi++];
(rQ[rQi++] ); },
}
function rQshift32() { rQshift32: function () {
return (rQ[rQi++] << 24) + return (this._rQ[this._rQi++] << 24) +
(rQ[rQi++] << 16) + (this._rQ[this._rQi++] << 16) +
(rQ[rQi++] << 8) + (this._rQ[this._rQi++] << 8) +
(rQ[rQi++] ); this._rQ[this._rQi++];
} },
function rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = rQlen(); } rQshiftStr: function (len) {
var arr = rQ.slice(rQi, rQi + len); if (typeof(len) === 'undefined') { len = this.rQlen(); }
rQi += len; var arr = this._rQ.slice(this._rQi, this._rQi + len);
this._rQi += len;
return String.fromCharCode.apply(null, arr); return String.fromCharCode.apply(null, arr);
} },
function rQshiftBytes(len) {
if (typeof(len) === 'undefined') { len = rQlen(); } rQshiftBytes: function (len) {
rQi += len; if (typeof(len) === 'undefined') { len = this.rQlen(); }
return rQ.slice(rQi-len, rQi); this._rQi += len;
} return this._rQ.slice(this._rQi - len, this._rQi);
},
function rQslice(start, end) { rQslice: function (start, end) {
if (end) { if (end) {
return rQ.slice(rQi + start, rQi + end); return this._rQ.slice(this._rQi + start, this._rQi + end);
} else { } else {
return rQ.slice(rQi + start); return this._rQ.slice(this._rQi + start);
} }
} },
// Check to see if we must wait for 'num' bytes (default to FBU.bytes) // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
// to be available in the receive queue. Return true if we need to // to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false. // wait (and possibly print a debug message), otherwise false.
function rQwait(msg, num, goback) { rQwait: function (msg, num, goback) {
var rQlen = rQ.length - rQi; // Skip rQlen() function call var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call
if (rQlen < num) { if (rQlen < num) {
if (goback) { if (goback) {
if (rQi < goback) { if (this._rQi < goback) {
throw("rQwait cannot backup " + goback + " bytes"); throw new Error("rQwait cannot backup " + goback + " bytes");
} }
rQi -= goback; this._rQi -= goback;
} }
//Util.Debug(" waiting for " + (num-rQlen) +
// " " + msg + " byte(s)");
return true; // true means need more data return true; // true means need more data
} }
return false; return false;
} },
// // Send Queue
// Private utility routines
//
function encode_message() { flush: function () {
if (mode === 'binary') { if (this._websocket.bufferedAmount !== 0) {
// Put in a binary arraybuffer Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount);
return (new Uint8Array(sQ)).buffer;
} else {
// base64 encode
return Base64.encode(sQ);
} }
}
function decode_message(data) { if (this._websocket.bufferedAmount < this.maxBufferedAmount) {
//Util.Debug(">> decode_message: " + data); if (this._sQ.length > 0) {
if (mode === 'binary') { this._websocket.send(this._encode_message());
// push arraybuffer values onto the end this._sQ = [];
var u8 = new Uint8Array(data);
for (var i = 0; i < u8.length; i++) {
rQ.push(u8[i]);
}
} else {
// base64 decode and concat to the end
rQ = rQ.concat(Base64.decode(data, 0));
} }
//Util.Debug(">> decode_message, rQ: " + rQ);
}
//
// Public Send functions
//
function flush() {
if (websocket.bufferedAmount !== 0) {
Util.Debug("bufferedAmount: " + websocket.bufferedAmount);
}
if (websocket.bufferedAmount < api.maxBufferedAmount) {
//Util.Debug("arr: " + arr);
//Util.Debug("sQ: " + sQ);
if (sQ.length > 0) {
websocket.send(encode_message(sQ));
sQ = [];
}
return true; return true;
} else { } else {
Util.Info("Delaying send, bufferedAmount: " + Util.Info("Delaying send, bufferedAmount: " +
websocket.bufferedAmount); this._websocket.bufferedAmount);
return false; return false;
} }
} },
// overridable for testing
function send(arr) {
//Util.Debug(">> send_array: " + arr);
sQ = sQ.concat(arr);
return flush();
}
function send_string(str) {
//Util.Debug(">> send_string: " + str);
api.send(str.split('').map(
function (chr) { return chr.charCodeAt(0); } ) );
}
//
// Other public functions
function recv_message(e) {
//Util.Debug(">> recv_message: " + e.data.length);
try {
decode_message(e.data);
if (rQlen() > 0) {
eventHandlers.message();
// Compact the receive queue
if (rQ.length > rQmax) {
//Util.Debug("Compacting receive queue");
rQ = rQ.slice(rQi);
rQi = 0;
}
} else {
Util.Debug("Ignoring empty message");
}
} catch (exc) {
if (typeof exc.stack !== 'undefined') {
Util.Warn("recv_message, caught exception: " + exc.stack);
} else if (typeof exc.description !== 'undefined') {
Util.Warn("recv_message, caught exception: " + exc.description);
} else {
Util.Warn("recv_message, caught exception:" + exc);
}
if (typeof exc.name !== 'undefined') {
eventHandlers.error(exc.name + ": " + exc.message);
} else {
eventHandlers.error(exc);
}
}
//Util.Debug("<< recv_message");
}
send: function (arr) {
this._sQ = this._sQ.concat(arr);
return this.flush();
},
// Set event handlers send_string: function (str) {
function on(evt, handler) { this.send(str.split('').map(function (chr) {
eventHandlers[evt] = handler; return chr.charCodeAt(0);
} }));
},
function init(protocols, ws_schema) { // Event Handlers
rQ = []; on: function (evt, handler) {
rQi = 0; this._eventHandlers[evt] = handler;
sQ = []; },
websocket = null;
var bt = false, init: function (protocols, ws_schema) {
wsbt = false, this._rQ = [];
try_binary = false; this._rQi = 0;
this._sQ = [];
this._websocket = null;
// Check for full typed array support // Check for full typed array support
var bt = false;
if (('Uint8Array' in window) && if (('Uint8Array' in window) &&
('set' in Uint8Array.prototype)) { ('set' in Uint8Array.prototype)) {
bt = true; bt = true;
} }
// Check for full binary type support in WebSocket
// Check for full binary type support in WebSockets
// Inspired by: // Inspired by:
// https://github.com/Modernizr/Modernizr/issues/370 // https://github.com/Modernizr/Modernizr/issues/370
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
var wsbt = false;
try { try {
if (bt && ('binaryType' in WebSocket.prototype || if (bt && ('binaryType' in WebSocket.prototype ||
!!(new WebSocket(ws_schema + '://.').binaryType))) { !!(new WebSocket(ws_schema + '://.').binaryType))) {
...@@ -288,7 +229,7 @@ function init(protocols, ws_schema) { ...@@ -288,7 +229,7 @@ function init(protocols, ws_schema) {
wsbt = true; wsbt = true;
} }
} catch (exc) { } catch (exc) {
// Just ignore failed test localhost connections // Just ignore failed test localhost connection
} }
// Default protocols if not specified // Default protocols if not specified
...@@ -300,125 +241,144 @@ function init(protocols, ws_schema) { ...@@ -300,125 +241,144 @@ function init(protocols, ws_schema) {
} }
} }
// If no binary support, make sure it was not requested
if (!wsbt) { if (!wsbt) {
if (protocols === 'binary') { if (protocols === 'binary') {
throw("WebSocket binary sub-protocol requested but not supported"); throw new Error('WebSocket binary sub-protocol requested but not supported');
} }
if (typeof(protocols) === "object") {
if (typeof(protocols) === 'object') {
var new_protocols = []; var new_protocols = [];
for (var i = 0; i < protocols.length; i++) { for (var i = 0; i < protocols.length; i++) {
if (protocols[i] === 'binary') { if (protocols[i] === 'binary') {
Util.Error("Skipping unsupported WebSocket binary sub-protocol"); Util.Error('Skipping unsupported WebSocket binary sub-protocol');
} else { } else {
new_protocols.push(protocols[i]); new_protocols.push(protocols[i]);
} }
} }
if (new_protocols.length > 0) { if (new_protocols.length > 0) {
protocols = new_protocols; protocols = new_protocols;
} else { } else {
throw("Only WebSocket binary sub-protocol was requested and not supported."); throw new Error("Only WebSocket binary sub-protocol was requested and is not supported.");
} }
} }
} }
return protocols; return protocols;
} },
function open(uri, protocols) { open: function (uri, protocols) {
var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
protocols = init(protocols, ws_schema); protocols = this.init(protocols, ws_schema);
this._websocket = new WebSocket(uri, protocols);
if (test_mode) {
websocket = {};
} else {
websocket = new WebSocket(uri, protocols);
if (protocols.indexOf('binary') >= 0) { if (protocols.indexOf('binary') >= 0) {
websocket.binaryType = 'arraybuffer'; this._websocket.binaryType = 'arraybuffer';
}
} }
websocket.onmessage = recv_message; this._websocket.onmessage = this._recv_message.bind(this);
websocket.onopen = function() { this._websocket.onopen = (function () {
Util.Debug(">> WebSock.onopen"); Util.Debug('>> WebSock.onopen');
if (websocket.protocol) { if (this._websocket.protocol) {
mode = websocket.protocol; this._mode = this._websocket.protocol;
Util.Info("Server chose sub-protocol: " + websocket.protocol); Util.Info("Server choose sub-protocol: " + this._websocket.protocol);
} else { } else {
mode = 'base64'; this._mode = 'base64';
Util.Error("Server select no sub-protocol!: " + websocket.protocol); Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol);
} }
eventHandlers.open(); this._eventHandlers.open();
Util.Debug("<< WebSock.onopen"); Util.Debug("<< WebSock.onopen");
}; }).bind(this);
websocket.onclose = function(e) { this._websocket.onclose = (function (e) {
Util.Debug(">> WebSock.onclose"); Util.Debug(">> WebSock.onclose");
eventHandlers.close(e); this._eventHandlers.close(e);
Util.Debug("<< WebSock.onclose"); Util.Debug("<< WebSock.onclose");
}; }).bind(this);
websocket.onerror = function(e) { this._websocket.onerror = (function (e) {
Util.Debug(">> WebSock.onerror: " + e); Util.Debug(">> WebSock.onerror: " + e);
eventHandlers.error(e); this._eventHandlers.error(e);
Util.Debug("<< WebSock.onerror"); Util.Debug("<< WebSock.onerror: " + e);
}; }).bind(this);
} },
function close() { close: function () {
if (websocket) { if (this._websocket) {
if ((websocket.readyState === WebSocket.OPEN) || if ((this._websocket.readyState === WebSocket.OPEN) ||
(websocket.readyState === WebSocket.CONNECTING)) { (this._websocket.readyState === WebSocket.CONNECTING)) {
Util.Info("Closing WebSocket connection"); Util.Info("Closing WebSocket connection");
websocket.close(); this._websocket.close();
} }
websocket.onmessage = function (e) { return; };
this._websocket.onmessage = function (e) { return; };
} }
} },
// Override internal functions for testing // private methods
// Takes a send function, returns reference to recv function _encode_message: function () {
function testMode(override_send, data_mode) { if (this._mode === 'binary') {
test_mode = true; // Put in a binary arraybuffer
mode = data_mode; return (new Uint8Array(this._sQ)).buffer;
api.send = override_send; } else {
api.close = function () {}; // base64 encode
return recv_message; return Base64.encode(this._sQ);
} }
},
function constructor() { _decode_message: function (data) {
// Configuration settings if (this._mode === 'binary') {
api.maxBufferedAmount = 200; // push arraybuffer values onto the end
var u8 = new Uint8Array(data);
// Direct access to send and receive queues for (var i = 0; i < u8.length; i++) {
api.get_sQ = get_sQ; this._rQ.push(u8[i]);
api.get_rQ = get_rQ; }
api.get_rQi = get_rQi; } else {
api.set_rQi = set_rQi; // base64 decode and concat to end
this._rQ = this._rQ.concat(Base64.decode(data, 0));
// Routines to read from the receive queue }
api.rQlen = rQlen; },
api.rQpeek8 = rQpeek8;
api.rQshift8 = rQshift8;
api.rQunshift8 = rQunshift8;
api.rQshift16 = rQshift16;
api.rQshift32 = rQshift32;
api.rQshiftStr = rQshiftStr;
api.rQshiftBytes = rQshiftBytes;
api.rQslice = rQslice;
api.rQwait = rQwait;
api.flush = flush;
api.send = send;
api.send_string = send_string;
api.on = on;
api.init = init;
api.open = open;
api.close = close;
api.testMode = testMode;
return api;
}
return constructor(); _recv_message: function (e) {
try {
this._decode_message(e.data);
if (this.rQlen() > 0) {
this._eventHandlers.message();
// Compact the receive queue
if (this._rQ.length > this._rQmax) {
this._rQ = this._rQ.slice(this._rQi);
this._rQi = 0;
}
} else {
Util.Debug("Ignoring empty message");
}
} catch (exc) {
var exception_str = "";
if (exc.name) {
exception_str += "\n name: " + exc.name + "\n";
exception_str += " message: " + exc.message + "\n";
}
} if (typeof exc.description !== 'undefined') {
exception_str += " description: " + exc.description + "\n";
}
if (typeof exc.stack !== 'undefined') {
exception_str += exc.stack;
}
if (exception_str.length > 0) {
Util.Error("recv_message, caught exception: " + exception_str);
} else {
Util.Error("recv_message, caught exception: " + exc);
}
if (typeof exc.name !== 'undefined') {
this._eventHandlers.error(exc.name + ": " + exc.message);
} else {
this._eventHandlers.error(exc);
}
}
}
};
})();
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
"use strict"; /*jslint bitwise: false, white: false, browser: true, devel: true */
/*jslint bitwise: false, white: false */
/*global Util, window, document */ /*global Util, window, document */
// Globals defined here // Globals defined here
...@@ -38,37 +37,39 @@ if (!window.$D) { ...@@ -38,37 +37,39 @@ if (!window.$D) {
*/ */
// init log level reading the logging HTTP param // init log level reading the logging HTTP param
WebUtil.init_logging = function(level) { WebUtil.init_logging = function (level) {
"use strict";
if (typeof level !== "undefined") { if (typeof level !== "undefined") {
Util._log_level = level; Util._log_level = level;
} else { } else {
Util._log_level = (document.location.href.match( var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/);
/logging=([A-Za-z0-9\._\-]*)/) || Util._log_level = (param || ['', Util._log_level])[1];
['', Util._log_level])[1];
} }
Util.init_logging(); Util.init_logging();
}; };
WebUtil.dirObj = function (obj, depth, parent) { WebUtil.dirObj = function (obj, depth, parent) {
var i, msg = "", val = ""; "use strict";
if (! depth) { depth=2; } if (! depth) { depth = 2; }
if (! parent) { parent= ""; } if (! parent) { parent = ""; }
// Print the properties of the passed-in object // Print the properties of the passed-in object
for (i in obj) { var msg = "";
for (var i in obj) {
if ((depth > 1) && (typeof obj[i] === "object")) { if ((depth > 1) && (typeof obj[i] === "object")) {
// Recurse attributes that are objects // Recurse attributes that are objects
msg += WebUtil.dirObj(obj[i], depth-1, parent + "." + i); msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i);
} else { } else {
//val = new String(obj[i]).replace("\n", " "); //val = new String(obj[i]).replace("\n", " ");
var val = "";
if (typeof(obj[i]) === "undefined") { if (typeof(obj[i]) === "undefined") {
val = "undefined"; val = "undefined";
} else { } else {
val = obj[i].toString().replace("\n", " "); val = obj[i].toString().replace("\n", " ");
} }
if (val.length > 30) { if (val.length > 30) {
val = val.substr(0,30) + "..."; val = val.substr(0, 30) + "...";
} }
msg += parent + "." + i + ": " + val + "\n"; msg += parent + "." + i + ": " + val + "\n";
} }
...@@ -77,7 +78,8 @@ WebUtil.dirObj = function (obj, depth, parent) { ...@@ -77,7 +78,8 @@ WebUtil.dirObj = function (obj, depth, parent) {
}; };
// Read a query string variable // Read a query string variable
WebUtil.getQueryVar = function(name, defVal) { WebUtil.getQueryVar = function (name, defVal) {
"use strict";
var re = new RegExp('.*[?&]' + name + '=([^&#]*)'), var re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re); match = document.location.href.match(re);
if (typeof defVal === 'undefined') { defVal = null; } if (typeof defVal === 'undefined') { defVal = null; }
...@@ -94,42 +96,50 @@ WebUtil.getQueryVar = function(name, defVal) { ...@@ -94,42 +96,50 @@ WebUtil.getQueryVar = function(name, defVal) {
*/ */
// No days means only for this browser session // No days means only for this browser session
WebUtil.createCookie = function(name,value,days) { WebUtil.createCookie = function (name, value, days) {
var date, expires, secure; "use strict";
var date, expires;
if (days) { if (days) {
date = new Date(); date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000)); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires="+date.toGMTString(); expires = "; expires=" + date.toGMTString();
} else { } else {
expires = ""; expires = "";
} }
var secure;
if (document.location.protocol === "https:") { if (document.location.protocol === "https:") {
secure = "; secure"; secure = "; secure";
} else { } else {
secure = ""; secure = "";
} }
document.cookie = name+"="+value+expires+"; path=/"+secure; document.cookie = name + "=" + value + expires + "; path=/" + secure;
}; };
WebUtil.readCookie = function(name, defaultValue) { WebUtil.readCookie = function (name, defaultValue) {
var i, c, nameEQ = name + "=", ca = document.cookie.split(';'); "use strict";
for(i=0; i < ca.length; i += 1) { var nameEQ = name + "=",
c = ca[i]; ca = document.cookie.split(';');
while (c.charAt(0) === ' ') { c = c.substring(1,c.length); }
if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); } for (var i = 0; i < ca.length; i += 1) {
var c = ca[i];
while (c.charAt(0) === ' ') { c = c.substring(1, c.length); }
if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); }
} }
return (typeof defaultValue !== 'undefined') ? defaultValue : null; return (typeof defaultValue !== 'undefined') ? defaultValue : null;
}; };
WebUtil.eraseCookie = function(name) { WebUtil.eraseCookie = function (name) {
WebUtil.createCookie(name,"",-1); "use strict";
WebUtil.createCookie(name, "", -1);
}; };
/* /*
* Setting handling. * Setting handling.
*/ */
WebUtil.initSettings = function(callback) { WebUtil.initSettings = function (callback /*, ...callbackArgs */) {
"use strict";
var callbackArgs = Array.prototype.slice.call(arguments, 1); var callbackArgs = Array.prototype.slice.call(arguments, 1);
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.get(function (cfg) { window.chrome.storage.sync.get(function (cfg) {
...@@ -148,7 +158,8 @@ WebUtil.initSettings = function(callback) { ...@@ -148,7 +158,8 @@ WebUtil.initSettings = function(callback) {
}; };
// No days means only for this browser session // No days means only for this browser session
WebUtil.writeSetting = function(name, value) { WebUtil.writeSetting = function (name, value) {
"use strict";
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
//console.log("writeSetting:", name, value); //console.log("writeSetting:", name, value);
if (WebUtil.settings[name] !== value) { if (WebUtil.settings[name] !== value) {
...@@ -160,7 +171,8 @@ WebUtil.writeSetting = function(name, value) { ...@@ -160,7 +171,8 @@ WebUtil.writeSetting = function(name, value) {
} }
}; };
WebUtil.readSetting = function(name, defaultValue) { WebUtil.readSetting = function (name, defaultValue) {
"use strict";
var value; var value;
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
value = WebUtil.settings[name]; value = WebUtil.settings[name];
...@@ -177,7 +189,8 @@ WebUtil.readSetting = function(name, defaultValue) { ...@@ -177,7 +189,8 @@ WebUtil.readSetting = function(name, defaultValue) {
} }
}; };
WebUtil.eraseSetting = function(name) { WebUtil.eraseSetting = function (name) {
"use strict";
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name); window.chrome.storage.sync.remove(name);
delete WebUtil.settings[name]; delete WebUtil.settings[name];
...@@ -189,9 +202,12 @@ WebUtil.eraseSetting = function(name) { ...@@ -189,9 +202,12 @@ WebUtil.eraseSetting = function(name) {
/* /*
* Alternate stylesheet selection * Alternate stylesheet selection
*/ */
WebUtil.getStylesheets = function() { var i, links, sheets = []; WebUtil.getStylesheets = function () {
links = document.getElementsByTagName("link"); "use strict";
for (i = 0; i < links.length; i += 1) { var links = document.getElementsByTagName("link");
var sheets = [];
for (var i = 0; i < links.length; i += 1) {
if (links[i].title && if (links[i].title &&
links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) {
sheets.push(links[i]); sheets.push(links[i]);
...@@ -202,13 +218,15 @@ WebUtil.getStylesheets = function() { var i, links, sheets = []; ...@@ -202,13 +218,15 @@ WebUtil.getStylesheets = function() { var i, links, sheets = [];
// No sheet means try and use value from cookie, null sheet used to // No sheet means try and use value from cookie, null sheet used to
// clear all alternates. // clear all alternates.
WebUtil.selectStylesheet = function(sheet) { WebUtil.selectStylesheet = function (sheet) {
var i, link, sheets = WebUtil.getStylesheets(); "use strict";
if (typeof sheet === 'undefined') { if (typeof sheet === 'undefined') {
sheet = 'default'; sheet = 'default';
} }
for (i=0; i < sheets.length; i += 1) {
link = sheets[i]; var sheets = WebUtil.getStylesheets();
for (var i = 0; i < sheets.length; i += 1) {
var link = sheets[i];
if (link.title === sheet) { if (link.title === sheet) {
Util.Debug("Using stylesheet " + sheet); Util.Debug("Using stylesheet " + sheet);
link.disabled = false; link.disabled = false;
......
// Karma configuration
module.exports = function(config) {
/*var customLaunchers = {
sl_chrome_win7: {
base: 'SauceLabs',
browserName: 'chrome',
platform: 'Windows 7'
},
sl_firefox30_linux: {
base: 'SauceLabs',
browserName: 'firefox',
version: '30',
platform: 'Linux'
},
sl_firefox26_linux: {
base: 'SauceLabs',
browserName: 'firefox',
version: 26,
platform: 'Linux'
},
sl_windows7_ie10: {
base: 'SauceLabs',
browserName: 'internet explorer',
platform: 'Windows 7',
version: '10'
},
sl_windows81_ie11: {
base: 'SauceLabs',
browserName: 'internet explorer',
platform: 'Windows 8.1',
version: '11'
},
sl_osxmavericks_safari7: {
base: 'SauceLabs',
browserName: 'safari',
platform: 'OS X 10.9',
version: '7'
},
sl_osxmtnlion_safari6: {
base: 'SauceLabs',
browserName: 'safari',
platform: 'OS X 10.8',
version: '6'
}
};*/
var customLaunchers = {};
var browsers = [];
var useSauce = false;
if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) {
useSauce = true;
}
if (useSauce && process.env.TEST_BROWSER_NAME && process.env.TEST_BROWSER_NAME != 'PhantomJS') {
var names = process.env.TEST_BROWSER_NAME.split(',');
var platforms = process.env.TEST_BROWSER_OS.split(',');
var versions = [];
if (process.env.TEST_BROWSER_VERSION) {
versions = process.env.TEST_BROWSER_VERSION.split(',');
} else {
versions = [null];
}
for (var i = 0; i < names.length; i++) {
for (var j = 0; j < platforms.length; j++) {
for (var k = 0; k < versions.length; k++) {
var launcher_name = 'sl_' + platforms[j].replace(/[^a-zA-Z0-9]/g, '') + '_' + names[i];
if (versions[k]) {
launcher_name += '_' + versions[k];
}
customLaunchers[launcher_name] = {
base: 'SauceLabs',
browserName: names[i],
platform: platforms[j],
};
if (versions[i]) {
customLaunchers[launcher_name].version = versions[k];
}
}
}
}
browsers = Object.keys(customLaunchers);
} else {
useSauce = false;
browsers = ['PhantomJS'];
}
var my_conf = {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'sinon', 'chai', 'sinon-chai'],
// list of files / patterns to load in the browser (loaded in order)
files: [
'tests/fake.*.js',
'include/util.js', // load first to avoid issues, since methods are called immediately
//'../include/*.js',
'include/base64.js',
'include/keysym.js',
'include/keysymdef.js',
'include/keyboard.js',
'include/input.js',
'include/websock.js',
'include/rfb.js',
'include/jsunzip.js',
'include/des.js',
'include/display.js',
'tests/test.*.js'
],
client: {
mocha: {
'ui': 'bdd'
}
},
// list of files to exclude
exclude: [
'../include/playback.js',
'../include/ui.js'
],
customLaunchers: customLaunchers,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: browsers,
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['mocha', 'saucelabs'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Increase timeout in case connection is slow/we run more browsers than possible
// (we currently get 3 for free, and we try to run 7, so it can take a while)
captureTimeout: 240000
};
if (useSauce) {
my_conf.sauceLabs = {
testName: 'noVNC Tests (all)',
startConnect: true,
};
}
config.set(my_conf);
};
{
"name": "noVNC",
"version": "0.5.0",
"description": "An HTML5 VNC client",
"main": "karma.conf.js",
"directories": {
"doc": "docs",
"test": "tests"
},
"scripts": {
"test": "karma start karma.conf.js"
},
"repository": {
"type": "git",
"url": "https://github.com/kanaka/noVNC.git"
},
"author": "Joel Martin <github@martintribe.org> (https://github.com/kanaka)",
"contributors": [
"Solly Ross <sross@redhat.com> (https://github.com/directxman12)",
"Peter Åstrand <astrand@cendio.se> (https://github.com/astrand)",
"Samuel Mannehed <samuel@cendio.se> (https://github.com/samhed)"
],
"license": "MPL 2.0",
"bugs": {
"url": "https://github.com/kanaka/noVNC/issues"
},
"homepage": "https://github.com/kanaka/noVNC",
"devDependencies": {
"ansi": "^0.3.0",
"casperjs": "^1.1.0-beta3",
"chai": "^1.9.1",
"commander": "^2.2.0",
"karma": "^0.12.16",
"karma-chai": "^0.1.0",
"karma-mocha": "^0.1.4",
"karma-mocha-reporter": "^0.2.5",
"karma-phantomjs-launcher": "^0.1.4",
"karma-sauce-launcher": "^0.2.8",
"karma-sinon": "^1.0.3",
"karma-sinon-chai": "^0.1.6",
"mocha": "^1.20.1",
"open": "0.0.5",
"phantom": "^0.6.3",
"phantomjs": "^1.9.7-9",
"sinon": "^1.10.2",
"sinon-chai": "^2.5.0",
"spooky": "^0.2.4",
"temp": "^0.8.0"
}
}
var FakeWebSocket;
(function () {
// PhantomJS can't create Event objects directly, so we need to use this
function make_event(name, props) {
var evt = document.createEvent('Event');
evt.initEvent(name, true, true);
if (props) {
for (var prop in props) {
evt[prop] = props[prop];
}
}
return evt;
}
FakeWebSocket = function (uri, protocols) {
this.url = uri;
this.binaryType = "arraybuffer";
this.extensions = "";
if (!protocols || typeof protocols === 'string') {
this.protocol = protocols;
} else {
this.protocol = protocols[0];
}
this._send_queue = new Uint8Array(20000);
this.readyState = FakeWebSocket.CONNECTING;
this.bufferedAmount = 0;
this.__is_fake = true;
};
FakeWebSocket.prototype = {
close: function (code, reason) {
this.readyState = FakeWebSocket.CLOSED;
if (this.onclose) {
this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true }));
}
},
send: function (data) {
if (this.protocol == 'base64') {
data = Base64.decode(data);
} else {
data = new Uint8Array(data);
}
this._send_queue.set(data, this.bufferedAmount);
this.bufferedAmount += data.length;
},
_get_sent_data: function () {
var arr = [];
for (var i = 0; i < this.bufferedAmount; i++) {
arr[i] = this._send_queue[i];
}
this.bufferedAmount = 0;
return arr;
},
_open: function (data) {
this.readyState = FakeWebSocket.OPEN;
if (this.onopen) {
this.onopen(make_event('open'));
}
},
_receive_data: function (data) {
this.onmessage(make_event("message", { 'data': data }));
}
};
FakeWebSocket.OPEN = WebSocket.OPEN;
FakeWebSocket.CONNECTING = WebSocket.CONNECTING;
FakeWebSocket.CLOSING = WebSocket.CLOSING;
FakeWebSocket.CLOSED = WebSocket.CLOSED;
FakeWebSocket.__is_fake = true;
FakeWebSocket.replace = function () {
if (!WebSocket.__is_fake) {
var real_version = WebSocket;
WebSocket = FakeWebSocket;
FakeWebSocket.__real_version = real_version;
}
};
FakeWebSocket.restore = function () {
if (WebSocket.__is_fake) {
WebSocket = WebSocket.__real_version;
}
};
})();
...@@ -2,7 +2,7 @@ var Spooky = require('spooky'); ...@@ -2,7 +2,7 @@ var Spooky = require('spooky');
var path = require('path'); var path = require('path');
var phantom_path = require('phantomjs').path; var phantom_path = require('phantomjs').path;
var casper_path = path.resolve(__dirname, 'node_modules/casperjs/bin/casperjs'); var casper_path = path.resolve(__dirname, '../node_modules/casperjs/bin/casperjs');
process.env.PHANTOMJS_EXECUTABLE = phantom_path; process.env.PHANTOMJS_EXECUTABLE = phantom_path;
var casper_opts = { var casper_opts = {
child: { child: {
......
...@@ -67,16 +67,16 @@ if (program.autoInject) { ...@@ -67,16 +67,16 @@ if (program.autoInject) {
temp.track(); temp.track();
var template = { var template = {
header: "<html>\n<head>\n<meta charset='utf-8' />\n<link rel='stylesheet' href='" + path.resolve(__dirname, 'node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>", header: "<html>\n<head>\n<meta charset='utf-8' />\n<link rel='stylesheet' href='" + path.resolve(__dirname, '../node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>",
script_tag: function(p) { return "<script src='" + p + "'></script>"; }, script_tag: function(p) { return "<script src='" + p + "'></script>"; },
footer: "<script>\nmocha.checkLeaks();\nmocha.globals(['navigator', 'create', 'ClientUtils', '__utils__']);\nmocha.run(function () { window.__mocha_done = true; });\n</script>\n</body>\n</html>" footer: "<script>\nmocha.checkLeaks();\nmocha.globals(['navigator', 'create', 'ClientUtils', '__utils__']);\nmocha.run(function () { window.__mocha_done = true; });\n</script>\n</body>\n</html>"
}; };
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/chai/chai.js')); template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/chai/chai.js'));
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/mocha/mocha.js')); template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/mocha/mocha.js'));
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon/pkg/sinon.js')); template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon/pkg/sinon.js'));
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon-chai/lib/sinon-chai.js')); template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js'));
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon-chai/lib/sinon-chai.js')); template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js'));
template.header += "\n<script>mocha.setup('bdd');</script>"; template.header += "\n<script>mocha.setup('bdd');</script>";
......
// requires local modules: base64
var assert = chai.assert;
var expect = chai.expect;
describe('Base64 Tools', function() {
"use strict";
var BIN_ARR = new Array(256);
for (var i = 0; i < 256; i++) {
BIN_ARR[i] = i;
}
var B64_STR = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==";
describe('encode', function() {
it('should encode a binary string into Base64', function() {
var encoded = Base64.encode(BIN_ARR);
expect(encoded).to.equal(B64_STR);
});
});
describe('decode', function() {
it('should decode a Base64 string into a normal string', function() {
var decoded = Base64.decode(B64_STR);
expect(decoded).to.deep.equal(BIN_ARR);
});
it('should throw an error if we have extra characters at the end of the string', function() {
expect(function () { Base64.decode(B64_STR+'abcdef'); }).to.throw(Error);
});
});
});
// requires local modules: util, base64, display
/* jshint expr: true */
var expect = chai.expect;
chai.use(function (_chai, utils) {
_chai.Assertion.addMethod('displayed', function (target_data) {
var obj = this._obj;
var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data;
// NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
var data = new Uint8Array(data_cl);
this.assert(utils.eql(data, target_data),
"expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
"expected #{this} not to have displayed the image #{act}",
target_data,
data);
});
});
describe('Display/Canvas Helper', function () {
var checked_data = [
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
];
checked_data = new Uint8Array(checked_data);
var basic_data = [0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255];
basic_data = new Uint8Array(basic_data);
function make_image_canvas (input_data) {
var canvas = document.createElement('canvas');
canvas.width = 4;
canvas.height = 4;
var ctx = canvas.getContext('2d');
var data = ctx.createImageData(4, 4);
for (var i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; }
ctx.putImageData(data, 0, 0);
return canvas;
}
describe('viewport handling', function () {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
display.resize(5, 5);
display.viewportChange(1, 1, 3, 3);
display.getCleanDirtyReset();
});
it('should take viewport location into consideration when drawing images', function () {
display.resize(4, 4);
display.viewportChange(0, 0, 2, 2);
display.drawImage(make_image_canvas(basic_data), 1, 1);
var expected = new Uint8Array(16);
var i;
for (i = 0; i < 8; i++) { expected[i] = basic_data[i]; }
for (i = 8; i < 16; i++) { expected[i] = 0; }
expect(display).to.have.displayed(expected);
});
it('should redraw the left side when shifted left', function () {
display.viewportChange(-1, 0, 3, 3);
var cdr = display.getCleanDirtyReset();
expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 2, h: 3 });
expect(cdr.dirtyBoxes).to.have.length(1);
expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 1, w: 2, h: 3 });
});
it('should redraw the right side when shifted right', function () {
display.viewportChange(1, 0, 3, 3);
var cdr = display.getCleanDirtyReset();
expect(cdr.cleanBox).to.deep.equal({ x: 2, y: 1, w: 2, h: 3 });
expect(cdr.dirtyBoxes).to.have.length(1);
expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 4, y: 1, w: 1, h: 3 });
});
it('should redraw the top part when shifted up', function () {
display.viewportChange(0, -1, 3, 3);
var cdr = display.getCleanDirtyReset();
expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 3, h: 2 });
expect(cdr.dirtyBoxes).to.have.length(1);
expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 0, w: 3, h: 1 });
});
it('should redraw the bottom part when shifted down', function () {
display.viewportChange(0, 1, 3, 3);
var cdr = display.getCleanDirtyReset();
expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 2 });
expect(cdr.dirtyBoxes).to.have.length(1);
expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 4, w: 3, h: 1 });
});
it('should reset the entire viewport to being clean after calculating the clean/dirty boxes', function () {
display.viewportChange(0, 1, 3, 3);
var cdr1 = display.getCleanDirtyReset();
var cdr2 = display.getCleanDirtyReset();
expect(cdr1).to.not.deep.equal(cdr2);
expect(cdr2.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 3 });
expect(cdr2.dirtyBoxes).to.be.empty;
});
it('should simply mark the whole display area as dirty if not using viewports', function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: false });
display.resize(5, 5);
var cdr = display.getCleanDirtyReset();
expect(cdr.cleanBox).to.deep.equal({ x: 0, y: 0, w: 0, h: 0 });
expect(cdr.dirtyBoxes).to.have.length(1);
expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 0, w: 5, h: 5 });
});
});
describe('resizing', function () {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
display.resize(4, 3);
});
it('should change the size of the logical canvas', function () {
display.resize(5, 7);
expect(display._fb_width).to.equal(5);
expect(display._fb_height).to.equal(7);
});
it('should update the viewport dimensions', function () {
sinon.spy(display, 'viewportChange');
display.resize(2, 2);
expect(display.viewportChange).to.have.been.calledOnce;
});
});
describe('drawing', function () {
// TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the
// basic cases
function drawing_tests (pref_js) {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: pref_js });
display.resize(4, 4);
});
it('should clear the screen on #clear without a logo set', function () {
display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]);
display._logo = null;
display.clear();
display.resize(4, 4);
var empty = [];
for (var i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; }
expect(display).to.have.displayed(new Uint8Array(empty));
});
it('should draw the logo on #clear with a logo set', function (done) {
display._logo = { width: 4, height: 4, data: make_image_canvas(checked_data).toDataURL() };
display._drawCtx._act_drawImg = display._drawCtx.drawImage;
display._drawCtx.drawImage = function (img, x, y) {
this._act_drawImg(img, x, y);
expect(display).to.have.displayed(checked_data);
done();
};
display.clear();
expect(display._fb_width).to.equal(4);
expect(display._fb_height).to.equal(4);
});
it('should support filling a rectangle with particular color via #fillRect', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0]);
display.fillRect(2, 2, 2, 2, [0xff, 0, 0]);
expect(display).to.have.displayed(checked_data);
});
it('should support copying an portion of the canvas via #copyImage', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]);
display.copyImage(0, 0, 2, 2, 2, 2);
expect(display).to.have.displayed(checked_data);
});
it('should support drawing tile data with a background color and sub tiles', function () {
display.startTile(0, 0, 4, 4, [0, 0xff, 0]);
display.subTile(0, 0, 2, 2, [0xff, 0, 0]);
display.subTile(2, 2, 2, 2, [0xff, 0, 0]);
display.finishTile();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing BGRX blit images with true color via #blitImage', function () {
var data = [];
for (var i = 0; i < 16; i++) {
data[i * 4] = checked_data[i * 4 + 2];
data[i * 4 + 1] = checked_data[i * 4 + 1];
data[i * 4 + 2] = checked_data[i * 4];
data[i * 4 + 3] = checked_data[i * 4 + 3];
}
display.blitImage(0, 0, 4, 4, data, 0);
expect(display).to.have.displayed(checked_data);
});
it('should support drawing RGB blit images with true color via #blitRgbImage', function () {
var data = [];
for (var i = 0; i < 16; i++) {
data[i * 3] = checked_data[i * 4];
data[i * 3 + 1] = checked_data[i * 4 + 1];
data[i * 3 + 2] = checked_data[i * 4 + 2];
}
display.blitRgbImage(0, 0, 4, 4, data, 0);
expect(display).to.have.displayed(checked_data);
});
it('should support drawing blit images from a data URL via #blitStringImage', function (done) {
var img_url = make_image_canvas(checked_data).toDataURL();
display._drawCtx._act_drawImg = display._drawCtx.drawImage;
display._drawCtx.drawImage = function (img, x, y) {
this._act_drawImg(img, x, y);
expect(display).to.have.displayed(checked_data);
done();
};
display.blitStringImage(img_url, 0, 0);
});
it('should support drawing solid colors with color maps', function () {
display._true_color = false;
display.set_colourMap({ 0: [0xff, 0, 0], 1: [0, 0xff, 0] });
display.fillRect(0, 0, 4, 4, [1]);
display.fillRect(0, 0, 2, 2, [0]);
display.fillRect(2, 2, 2, 2, [0]);
expect(display).to.have.displayed(checked_data);
});
it('should support drawing blit images with color maps', function () {
display._true_color = false;
display.set_colourMap({ 1: [0xff, 0, 0], 0: [0, 0xff, 0] });
var data = [1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1].map(function (elem) { return [elem]; });
display.blitImage(0, 0, 4, 4, data, 0);
expect(display).to.have.displayed(checked_data);
});
it('should support drawing an image object via #drawImage', function () {
var img = make_image_canvas(checked_data);
display.drawImage(img, 0, 0);
expect(display).to.have.displayed(checked_data);
});
}
describe('(prefering native methods)', function () { drawing_tests.call(this, false); });
describe('(prefering JavaScript)', function () { drawing_tests.call(this, true); });
});
describe('the render queue processor', function () {
var display;
beforeEach(function () {
display = new Display({ target: document.createElement('canvas'), prefer_js: false });
display.resize(4, 4);
sinon.spy(display, '_scan_renderQ');
this.old_requestAnimFrame = window.requestAnimFrame;
window.requestAnimFrame = function (cb) {
this.next_frame_cb = cb;
}.bind(this);
this.next_frame = function () { this.next_frame_cb(); };
});
afterEach(function () {
window.requestAnimFrame = this.old_requestAnimFrame;
});
it('should try to process an item when it is pushed on, if nothing else is on the queue', function () {
display.renderQ_push({ type: 'noop' }); // does nothing
expect(display._scan_renderQ).to.have.been.calledOnce;
});
it('should not try to process an item when it is pushed on if we are waiting for other items', function () {
display._renderQ.length = 2;
display.renderQ_push({ type: 'noop' });
expect(display._scan_renderQ).to.not.have.been.called;
});
it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () {
var img = { complete: false };
display._renderQ = [{ type: 'img', x: 3, y: 4, img: img },
{ type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }];
display.drawImage = sinon.spy();
display.fillRect = sinon.spy();
display._scan_renderQ();
expect(display.drawImage).to.not.have.been.called;
expect(display.fillRect).to.not.have.been.called;
display._renderQ[0].img.complete = true;
this.next_frame();
expect(display.drawImage).to.have.been.calledOnce;
expect(display.fillRect).to.have.been.calledOnce;
});
it('should draw a blit image on type "blit"', function () {
display.blitImage = sinon.spy();
display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
expect(display.blitImage).to.have.been.calledOnce;
expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);
});
it('should draw a blit RGB image on type "blitRgb"', function () {
display.blitRgbImage = sinon.spy();
display.renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
expect(display.blitRgbImage).to.have.been.calledOnce;
expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);
});
it('should copy a region on type "copy"', function () {
display.copyImage = sinon.spy();
display.renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 });
expect(display.copyImage).to.have.been.calledOnce;
expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6);
});
it('should fill a rect with a given color on type "fill"', function () {
display.fillRect = sinon.spy();
display.renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]});
expect(display.fillRect).to.have.been.calledOnce;
expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]);
});
it('should draw an image from an image object on type "img" (if complete)', function () {
display.drawImage = sinon.spy();
display.renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } });
expect(display.drawImage).to.have.been.calledOnce;
expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4);
});
});
});
var assert = chai.assert; // requires local modules: keysym, keysymdef, keyboard
var assert = chai.assert;
var expect = chai.expect; var expect = chai.expect;
describe('Helpers', function() { describe('Helpers', function() {
......
// requires local modules: input, keyboard, keysymdef
var assert = chai.assert; var assert = chai.assert;
var expect = chai.expect; var expect = chai.expect;
/* jshint newcap: false, expr: true */
describe('Key Event Pipeline Stages', function() { describe('Key Event Pipeline Stages', function() {
"use strict"; "use strict";
describe('Decode Keyboard Events', function() { describe('Decode Keyboard Events', function() {
...@@ -50,7 +51,7 @@ describe('Key Event Pipeline Stages', function() { ...@@ -50,7 +51,7 @@ describe('Key Event Pipeline Stages', function() {
KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'}); expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
done(); done();
}).keydown({keyCode: 0x41}) }).keydown({keyCode: 0x41});
}); });
it('should forward keyup events with the right type', function(done) { it('should forward keyup events with the right type', function(done) {
KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
......
// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, jsunzip, des, display
// requires test modules: fake.websocket
/* jshint expr: true */
var assert = chai.assert;
var expect = chai.expect;
function make_rfb (extra_opts) {
if (!extra_opts) {
extra_opts = {};
}
extra_opts.target = extra_opts.target || document.createElement('canvas');
return new RFB(extra_opts);
}
// some useful assertions for noVNC
chai.use(function (_chai, utils) {
_chai.Assertion.addMethod('displayed', function (target_data) {
var obj = this._obj;
var data_cl = obj._drawCtx.getImageData(0, 0, obj._fb_width, obj._fb_height).data;
// NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
var data = new Uint8Array(data_cl);
this.assert(utils.eql(data, target_data),
"expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
"expected #{this} not to have displayed the image #{act}",
target_data,
data);
});
_chai.Assertion.addMethod('sent', function (target_data) {
var obj = this._obj;
var data = obj._websocket._get_sent_data();
this.assert(utils.eql(data, target_data),
"expected #{this} to have sent the data #{exp}, but it actually sent #{act}",
"expected #{this} not to have sent the data #{act}",
target_data,
data);
});
});
describe('Remote Frame Buffer Protocol Client', function() {
"use strict";
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
describe('Public API Basic Behavior', function () {
var client;
beforeEach(function () {
client = make_rfb();
});
describe('#connect', function () {
beforeEach(function () { client._updateState = sinon.spy(); });
it('should set the current state to "connect"', function () {
client.connect('host', 8675);
expect(client._updateState).to.have.been.calledOnce;
expect(client._updateState).to.have.been.calledWith('connect');
});
it('should fail if we are missing a host', function () {
sinon.spy(client, '_fail');
client.connect(undefined, 8675);
expect(client._fail).to.have.been.calledOnce;
});
it('should fail if we are missing a port', function () {
sinon.spy(client, '_fail');
client.connect('abc');
expect(client._fail).to.have.been.calledOnce;
});
it('should not update the state if we are missing a host or port', function () {
sinon.spy(client, '_fail');
client.connect('abc');
expect(client._fail).to.have.been.calledOnce;
expect(client._updateState).to.have.been.calledOnce;
expect(client._updateState).to.have.been.calledWith('failed');
});
});
describe('#disconnect', function () {
beforeEach(function () { client._updateState = sinon.spy(); });
it('should set the current state to "disconnect"', function () {
client.disconnect();
expect(client._updateState).to.have.been.calledOnce;
expect(client._updateState).to.have.been.calledWith('disconnect');
});
});
describe('#sendPassword', function () {
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
afterEach(function () { this.clock.restore(); });
it('should set the state to "Authentication"', function () {
client._rfb_state = "blah";
client.sendPassword('pass');
expect(client._rfb_state).to.equal('Authentication');
});
it('should call init_msg "soon"', function () {
client._init_msg = sinon.spy();
client.sendPassword('pass');
this.clock.tick(5);
expect(client._init_msg).to.have.been.calledOnce;
});
});
describe('#sendCtrlAlDel', function () {
beforeEach(function () {
client._sock = new Websock();
client._sock.open('ws://', 'binary');
client._sock._websocket._open();
sinon.spy(client._sock, 'send');
client._rfb_state = "normal";
client._view_only = false;
});
it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () {
var expected = [];
expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 1));
expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 1));
expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 1));
expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 0));
expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 0));
expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 0));
client.sendCtrlAltDel();
expect(client._sock).to.have.sent(expected);
});
it('should not send the keys if we are not in a normal state', function () {
client._rfb_state = "broken";
client.sendCtrlAltDel();
expect(client._sock.send).to.not.have.been.called;
});
it('should not send the keys if we are set as view_only', function () {
client._view_only = true;
client.sendCtrlAltDel();
expect(client._sock.send).to.not.have.been.called;
});
});
describe('#sendKey', function () {
beforeEach(function () {
client._sock = new Websock();
client._sock.open('ws://', 'binary');
client._sock._websocket._open();
sinon.spy(client._sock, 'send');
client._rfb_state = "normal";
client._view_only = false;
});
it('should send a single key with the given code and state (down = true)', function () {
var expected = RFB.messages.keyEvent(123, 1);
client.sendKey(123, true);
expect(client._sock).to.have.sent(expected);
});
it('should send both a down and up event if the state is not specified', function () {
var expected = RFB.messages.keyEvent(123, 1);
expected = expected.concat(RFB.messages.keyEvent(123, 0));
client.sendKey(123);
expect(client._sock).to.have.sent(expected);
});
it('should not send the key if we are not in a normal state', function () {
client._rfb_state = "broken";
client.sendKey(123);
expect(client._sock.send).to.not.have.been.called;
});
it('should not send the key if we are set as view_only', function () {
client._view_only = true;
client.sendKey(123);
expect(client._sock.send).to.not.have.been.called;
});
});
describe('#clipboardPasteFrom', function () {
beforeEach(function () {
client._sock = new Websock();
client._sock.open('ws://', 'binary');
client._sock._websocket._open();
sinon.spy(client._sock, 'send');
client._rfb_state = "normal";
client._view_only = false;
});
it('should send the given text in a paste event', function () {
var expected = RFB.messages.clientCutText('abc');
client.clipboardPasteFrom('abc');
expect(client._sock).to.have.sent(expected);
});
it('should not send the text if we are not in a normal state', function () {
client._rfb_state = "broken";
client.clipboardPasteFrom('abc');
expect(client._sock.send).to.not.have.been.called;
});
});
describe("XVP operations", function () {
beforeEach(function () {
client._sock = new Websock();
client._sock.open('ws://', 'binary');
client._sock._websocket._open();
sinon.spy(client._sock, 'send');
client._rfb_state = "normal";
client._view_only = false;
client._rfb_xvp_ver = 1;
});
it('should send the shutdown signal on #xvpShutdown', function () {
client.xvpShutdown();
expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x02]);
});
it('should send the reboot signal on #xvpReboot', function () {
client.xvpReboot();
expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x03]);
});
it('should send the reset signal on #xvpReset', function () {
client.xvpReset();
expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x04]);
});
it('should support sending arbitrary XVP operations via #xvpOp', function () {
client.xvpOp(1, 7);
expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x07]);
});
it('should not send XVP operations with higher versions than we support', function () {
expect(client.xvpOp(2, 7)).to.be.false;
expect(client._sock.send).to.not.have.been.called;
});
});
});
describe('Misc Internals', function () {
describe('#_updateState', function () {
var client;
beforeEach(function () {
this.clock = sinon.useFakeTimers();
client = make_rfb();
});
afterEach(function () {
this.clock.restore();
});
it('should clear the disconnect timer if the state is not disconnect', function () {
var spy = sinon.spy();
client._disconnTimer = setTimeout(spy, 50);
client._updateState('normal');
this.clock.tick(51);
expect(spy).to.not.have.been.called;
expect(client._disconnTimer).to.be.null;
});
});
});
describe('Page States', function () {
describe('loaded', function () {
var client;
beforeEach(function () { client = make_rfb(); });
it('should close any open WebSocket connection', function () {
sinon.spy(client._sock, 'close');
client._updateState('loaded');
expect(client._sock.close).to.have.been.calledOnce;
});
});
describe('disconnected', function () {
var client;
beforeEach(function () { client = make_rfb(); });
it('should close any open WebSocket connection', function () {
sinon.spy(client._sock, 'close');
client._updateState('disconnected');
expect(client._sock.close).to.have.been.calledOnce;
});
});
describe('connect', function () {
var client;
beforeEach(function () { client = make_rfb(); });
it('should reset the variable states', function () {
sinon.spy(client, '_init_vars');
client._updateState('connect');
expect(client._init_vars).to.have.been.calledOnce;
});
it('should actually connect to the websocket', function () {
sinon.spy(client._sock, 'open');
client._updateState('connect');
expect(client._sock.open).to.have.been.calledOnce;
});
it('should use wss:// to connect if encryption is enabled', function () {
sinon.spy(client._sock, 'open');
client.set_encrypt(true);
client._updateState('connect');
expect(client._sock.open.args[0][0]).to.contain('wss://');
});
it('should use ws:// to connect if encryption is not enabled', function () {
sinon.spy(client._sock, 'open');
client.set_encrypt(true);
client._updateState('connect');
expect(client._sock.open.args[0][0]).to.contain('wss://');
});
it('should use a uri with the host, port, and path specified to connect', function () {
sinon.spy(client._sock, 'open');
client.set_encrypt(false);
client._rfb_host = 'HOST';
client._rfb_port = 8675;
client._rfb_path = 'PATH';
client._updateState('connect');
expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH');
});
it('should attempt to close the websocket before we open an new one', function () {
sinon.spy(client._sock, 'close');
client._updateState('connect');
expect(client._sock.close).to.have.been.calledOnce;
});
});
describe('disconnect', function () {
var client;
beforeEach(function () {
this.clock = sinon.useFakeTimers();
client = make_rfb();
client.connect('host', 8675);
});
afterEach(function () {
this.clock.restore();
});
it('should fail if we do not call Websock.onclose within the disconnection timeout', function () {
client._sock._websocket.close = function () {}; // explicitly don't call onclose
client._updateState('disconnect');
this.clock.tick(client.get_disconnectTimeout() * 1000);
expect(client._rfb_state).to.equal('failed');
});
it('should not fail if Websock.onclose gets called within the disconnection timeout', function () {
client._updateState('disconnect');
this.clock.tick(client.get_disconnectTimeout() * 500);
client._sock._websocket.close();
this.clock.tick(client.get_disconnectTimeout() * 500 + 1);
expect(client._rfb_state).to.equal('disconnected');
});
it('should close the WebSocket connection', function () {
sinon.spy(client._sock, 'close');
client._updateState('disconnect');
expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect
});
});
describe('failed', function () {
var client;
beforeEach(function () {
this.clock = sinon.useFakeTimers();
client = make_rfb();
client.connect('host', 8675);
});
afterEach(function () {
this.clock.restore();
});
it('should close the WebSocket connection', function () {
sinon.spy(client._sock, 'close');
client._updateState('failed');
expect(client._sock.close).to.have.been.called;
});
it('should transition to disconnected but stay in failed state', function () {
client.set_onUpdateState(sinon.spy());
client._updateState('failed');
this.clock.tick(50);
expect(client._rfb_state).to.equal('failed');
var onUpdateState = client.get_onUpdateState();
expect(onUpdateState).to.have.been.called;
// it should be specifically the last call
expect(onUpdateState.args[onUpdateState.args.length - 1][1]).to.equal('disconnected');
expect(onUpdateState.args[onUpdateState.args.length - 1][2]).to.equal('failed');
});
});
describe('fatal', function () {
var client;
beforeEach(function () { client = make_rfb(); });
it('should close any open WebSocket connection', function () {
sinon.spy(client._sock, 'close');
client._updateState('fatal');
expect(client._sock.close).to.have.been.calledOnce;
});
});
// NB(directxman12): Normal does *nothing* in updateState
});
describe('Protocol Initialization States', function () {
describe('ProtocolVersion', function () {
beforeEach(function () {
this.clock = sinon.useFakeTimers();
});
afterEach(function () {
this.clock.restore();
});
function send_ver (ver, client) {
var arr = new Uint8Array(12);
for (var i = 0; i < ver.length; i++) {
arr[i+4] = ver.charCodeAt(i);
}
arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
arr[11] = '\n';
client._sock._websocket._receive_data(arr);
}
describe('version parsing', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
});
it('should interpret version 000.000 as a repeater', function () {
client._repeaterID = '\x01\x02\x03\x04\x05';
send_ver('000.000', client);
expect(client._rfb_version).to.equal(0);
var sent_data = client._sock._websocket._get_sent_data();
expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]);
});
it('should interpret version 003.003 as version 3.3', function () {
send_ver('003.003', client);
expect(client._rfb_version).to.equal(3.3);
});
it('should interpret version 003.006 as version 3.3', function () {
send_ver('003.006', client);
expect(client._rfb_version).to.equal(3.3);
});
it('should interpret version 003.889 as version 3.3', function () {
send_ver('003.889', client);
expect(client._rfb_version).to.equal(3.3);
});
it('should interpret version 003.007 as version 3.7', function () {
send_ver('003.007', client);
expect(client._rfb_version).to.equal(3.7);
});
it('should interpret version 003.008 as version 3.8', function () {
send_ver('003.008', client);
expect(client._rfb_version).to.equal(3.8);
});
it('should interpret version 004.000 as version 3.8', function () {
send_ver('004.000', client);
expect(client._rfb_version).to.equal(3.8);
});
it('should interpret version 004.001 as version 3.8', function () {
send_ver('004.001', client);
expect(client._rfb_version).to.equal(3.8);
});
it('should fail on an invalid version', function () {
send_ver('002.000', client);
expect(client._rfb_state).to.equal('failed');
});
});
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
});
it('should handle two step repeater negotiation', function () {
client._repeaterID = '\x01\x02\x03\x04\x05';
send_ver('000.000', client);
expect(client._rfb_version).to.equal(0);
var sent_data = client._sock._websocket._get_sent_data();
expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]);
expect(sent_data).to.have.length(250);
send_ver('003.008', client);
expect(client._rfb_version).to.equal(3.8);
});
it('should initialize the flush interval', function () {
client._sock.flush = sinon.spy();
send_ver('003.008', client);
this.clock.tick(100);
expect(client._sock.flush).to.have.been.calledThrice;
});
it('should send back the interpreted version', function () {
send_ver('004.000', client);
var expected_str = 'RFB 003.008\n';
var expected = [];
for (var i = 0; i < expected_str.length; i++) {
expected[i] = expected_str.charCodeAt(i);
}
expect(client._sock).to.have.sent(expected);
});
it('should transition to the Security state on successful negotiation', function () {
send_ver('003.008', client);
expect(client._rfb_state).to.equal('Security');
});
});
describe('Security', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'Security';
});
it('should simply receive the auth scheme when for versions < 3.7', function () {
client._rfb_version = 3.6;
var auth_scheme_raw = [1, 2, 3, 4];
var auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) +
(auth_scheme_raw[2] << 8) + auth_scheme_raw[3];
client._sock._websocket._receive_data(auth_scheme_raw);
expect(client._rfb_auth_scheme).to.equal(auth_scheme);
});
it('should choose for the most prefered scheme possible for versions >= 3.7', function () {
client._rfb_version = 3.7;
var auth_schemes = [2, 1, 2];
client._sock._websocket._receive_data(auth_schemes);
expect(client._rfb_auth_scheme).to.equal(2);
expect(client._sock).to.have.sent([2]);
});
it('should fail if there are no supported schemes for versions >= 3.7', function () {
client._rfb_version = 3.7;
var auth_schemes = [1, 32];
client._sock._websocket._receive_data(auth_schemes);
expect(client._rfb_state).to.equal('failed');
});
it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () {
client._rfb_version = 3.7;
var failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
sinon.spy(client, '_fail');
client._sock._websocket._receive_data(failure_data);
expect(client._fail).to.have.been.calledTwice;
expect(client._fail).to.have.been.calledWith('Security failure: whoops');
});
it('should transition to the Authentication state and continue on successful negotiation', function () {
client._rfb_version = 3.7;
var auth_schemes = [1, 1];
client._negotiate_authentication = sinon.spy();
client._sock._websocket._receive_data(auth_schemes);
expect(client._rfb_state).to.equal('Authentication');
expect(client._negotiate_authentication).to.have.been.calledOnce;
});
});
describe('Authentication', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'Security';
});
function send_security(type, cl) {
cl._sock._websocket._receive_data(new Uint8Array([1, type]));
}
it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {
client._rfb_version = 3.6;
var err_msg = "Whoopsies";
var data = [0, 0, 0, 0];
var err_len = err_msg.length;
data.push32(err_len);
for (var i = 0; i < err_len; i++) {
data.push(err_msg.charCodeAt(i));
}
sinon.spy(client, '_fail');
client._sock._websocket._receive_data(new Uint8Array(data));
expect(client._rfb_state).to.equal('failed');
expect(client._fail).to.have.been.calledWith('Auth failure: Whoopsies');
});
it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
client._rfb_version = 3.8;
send_security(1, client);
expect(client._rfb_state).to.equal('SecurityResult');
});
it('should transition straight to ClientInitialisation on "no auth" for versions < 3.8', function () {
client._rfb_version = 3.7;
sinon.spy(client, '_updateState');
send_security(1, client);
expect(client._updateState).to.have.been.calledWith('ClientInitialisation');
expect(client._rfb_state).to.equal('ServerInitialisation');
});
it('should fail on an unknown auth scheme', function () {
client._rfb_version = 3.8;
send_security(57, client);
expect(client._rfb_state).to.equal('failed');
});
describe('VNC Authentication (type 2) Handler', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'Security';
client._rfb_version = 3.8;
});
it('should transition to the "password" state if missing a password', function () {
send_security(2, client);
expect(client._rfb_state).to.equal('password');
});
it('should encrypt the password with DES and then send it back', function () {
client._rfb_password = 'passwd';
send_security(2, client);
client._sock._websocket._get_sent_data(); // skip the choice of auth reply
var challenge = [];
for (var i = 0; i < 16; i++) { challenge[i] = i; }
client._sock._websocket._receive_data(new Uint8Array(challenge));
var des_pass = RFB.genDES('passwd', challenge);
expect(client._sock).to.have.sent(des_pass);
});
it('should transition to SecurityResult immediately after sending the password', function () {
client._rfb_password = 'passwd';
send_security(2, client);
var challenge = [];
for (var i = 0; i < 16; i++) { challenge[i] = i; }
client._sock._websocket._receive_data(new Uint8Array(challenge));
expect(client._rfb_state).to.equal('SecurityResult');
});
});
describe('XVP Authentication (type 22) Handler', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'Security';
client._rfb_version = 3.8;
});
it('should fall through to standard VNC authentication upon completion', function () {
client.set_xvp_password_sep('#');
client._rfb_password = 'user#target#password';
client._negotiate_std_vnc_auth = sinon.spy();
send_security(22, client);
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
});
it('should transition to the "password" state if the passwords is missing', function() {
send_security(22, client);
expect(client._rfb_state).to.equal('password');
});
it('should transition to the "password" state if the passwords is improperly formatted', function() {
client._rfb_password = 'user@target';
send_security(22, client);
expect(client._rfb_state).to.equal('password');
});
it('should split the password, send the first two parts, and pass on the last part', function () {
client.set_xvp_password_sep('#');
client._rfb_password = 'user#target#password';
client._negotiate_std_vnc_auth = sinon.spy();
send_security(22, client);
expect(client._rfb_password).to.equal('password');
var expected = [22, 4, 6]; // auth selection, len user, len target
for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); }
expect(client._sock).to.have.sent(expected);
});
});
describe('TightVNC Authentication (type 16) Handler', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'Security';
client._rfb_version = 3.8;
send_security(16, client);
client._sock._websocket._get_sent_data(); // skip the security reply
});
function send_num_str_pairs(pairs, client) {
var pairs_len = pairs.length;
var data = [];
data.push32(pairs_len);
for (var i = 0; i < pairs_len; i++) {
data.push32(pairs[i][0]);
var j;
for (j = 0; j < 4; j++) {
data.push(pairs[i][1].charCodeAt(j));
}
for (j = 0; j < 8; j++) {
data.push(pairs[i][2].charCodeAt(j));
}
}
client._sock._websocket._receive_data(new Uint8Array(data));
}
it('should skip tunnel negotiation if no tunnels are requested', function () {
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
expect(client._rfb_tightvnc).to.be.true;
});
it('should fail if no supported tunnels are listed', function () {
send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client);
expect(client._rfb_state).to.equal('failed');
});
it('should choose the notunnel tunnel type', function () {
send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client);
expect(client._sock).to.have.sent([0, 0, 0, 0]);
});
it('should continue to sub-auth negotiation after tunnel negotiation', function () {
send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client);
client._sock._websocket._get_sent_data(); // skip the tunnel choice here
send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client);
expect(client._sock).to.have.sent([0, 0, 0, 1]);
expect(client._rfb_state).to.equal('SecurityResult');
});
/*it('should attempt to use VNC auth over no auth when possible', function () {
client._rfb_tightvnc = true;
client._negotiate_std_vnc_auth = sinon.spy();
send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client);
expect(client._sock).to.have.sent([0, 0, 0, 1]);
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
expect(client._rfb_auth_scheme).to.equal(2);
});*/ // while this would make sense, the original code doesn't actually do this
it('should accept the "no auth" auth type and transition to SecurityResult', function () {
client._rfb_tightvnc = true;
send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client);
expect(client._sock).to.have.sent([0, 0, 0, 1]);
expect(client._rfb_state).to.equal('SecurityResult');
});
it('should accept VNC authentication and transition to that', function () {
client._rfb_tightvnc = true;
client._negotiate_std_vnc_auth = sinon.spy();
send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client);
expect(client._sock).to.have.sent([0, 0, 0, 2]);
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
expect(client._rfb_auth_scheme).to.equal(2);
});
it('should fail if there are no supported auth types', function () {
client._rfb_tightvnc = true;
send_num_str_pairs([[23, 'stdv', 'badval__']], client);
expect(client._rfb_state).to.equal('failed');
});
});
});
describe('SecurityResult', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'SecurityResult';
});
it('should fall through to ClientInitialisation on a response code of 0', function () {
client._updateState = sinon.spy();
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
expect(client._updateState).to.have.been.calledOnce;
expect(client._updateState).to.have.been.calledWith('ClientInitialisation');
});
it('should fail on an error code of 1 with the given message for versions >= 3.8', function () {
client._rfb_version = 3.8;
sinon.spy(client, '_fail');
var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
client._sock._websocket._receive_data(new Uint8Array(failure_data));
expect(client._rfb_state).to.equal('failed');
expect(client._fail).to.have.been.calledWith('whoops');
});
it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
client._rfb_version = 3.7;
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1]));
expect(client._rfb_state).to.equal('failed');
});
});
describe('ClientInitialisation', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'SecurityResult';
});
it('should transition to the ServerInitialisation state', function () {
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
expect(client._rfb_state).to.equal('ServerInitialisation');
});
it('should send 1 if we are in shared mode', function () {
client.set_shared(true);
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
expect(client._sock).to.have.sent([1]);
});
it('should send 0 if we are not in shared mode', function () {
client.set_shared(false);
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
expect(client._sock).to.have.sent([0]);
});
});
describe('ServerInitialisation', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'ServerInitialisation';
});
function send_server_init(opts, client) {
var full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0,
true_color: 1, red_max: 255, green_max: 255, blue_max: 255,
red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' };
for (var opt in opts) {
full_opts[opt] = opts[opt];
}
var data = [];
data.push16(full_opts.width);
data.push16(full_opts.height);
data.push(full_opts.bpp);
data.push(full_opts.depth);
data.push(full_opts.big_endian);
data.push(full_opts.true_color);
data.push16(full_opts.red_max);
data.push16(full_opts.green_max);
data.push16(full_opts.blue_max);
data.push8(full_opts.red_shift);
data.push8(full_opts.green_shift);
data.push8(full_opts.blue_shift);
// padding
data.push8(0);
data.push8(0);
data.push8(0);
client._sock._websocket._receive_data(new Uint8Array(data));
var name_data = [];
name_data.push32(full_opts.name.length);
for (var i = 0; i < full_opts.name.length; i++) {
name_data.push(full_opts.name.charCodeAt(i));
}
client._sock._websocket._receive_data(new Uint8Array(name_data));
}
it('should set the framebuffer width and height', function () {
send_server_init({ width: 32, height: 84 }, client);
expect(client._fb_width).to.equal(32);
expect(client._fb_height).to.equal(84);
});
// NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them
it('should set the framebuffer name and call the callback', function () {
client.set_onDesktopName(sinon.spy());
send_server_init({ name: 'some name' }, client);
var spy = client.get_onDesktopName();
expect(client._fb_name).to.equal('some name');
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][1]).to.equal('some name');
});
it('should handle the extended init message of the tight encoding', function () {
// NB(sross): we don't actually do anything with it, so just test that we can
// read it w/o throwing an error
client._rfb_tightvnc = true;
send_server_init({}, client);
var tight_data = [];
tight_data.push16(1);
tight_data.push16(2);
tight_data.push16(3);
tight_data.push16(0);
for (var i = 0; i < 16 + 32 + 48; i++) {
tight_data.push(i);
}
client._sock._websocket._receive_data(tight_data);
expect(client._rfb_state).to.equal('normal');
});
it('should set the true color mode on the display to the configuration variable', function () {
client.set_true_color(false);
sinon.spy(client._display, 'set_true_color');
send_server_init({ true_color: 1 }, client);
expect(client._display.set_true_color).to.have.been.calledOnce;
expect(client._display.set_true_color).to.have.been.calledWith(false);
});
it('should call the resize callback and resize the display', function () {
client.set_onFBResize(sinon.spy());
sinon.spy(client._display, 'resize');
send_server_init({ width: 27, height: 32 }, client);
var spy = client.get_onFBResize();
expect(client._display.resize).to.have.been.calledOnce;
expect(client._display.resize).to.have.been.calledWith(27, 32);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][1]).to.equal(27);
expect(spy.args[0][2]).to.equal(32);
});
it('should grab the mouse and keyboard', function () {
sinon.spy(client._keyboard, 'grab');
sinon.spy(client._mouse, 'grab');
send_server_init({}, client);
expect(client._keyboard.grab).to.have.been.calledOnce;
expect(client._mouse.grab).to.have.been.calledOnce;
});
it('should set the BPP and depth to 4 and 3 respectively if in true color mode', function () {
client.set_true_color(true);
send_server_init({}, client);
expect(client._fb_Bpp).to.equal(4);
expect(client._fb_depth).to.equal(3);
});
it('should set the BPP and depth to 1 and 1 respectively if not in true color mode', function () {
client.set_true_color(false);
send_server_init({}, client);
expect(client._fb_Bpp).to.equal(1);
expect(client._fb_depth).to.equal(1);
});
// TODO(directxman12): test the various options in this configuration matrix
it('should reply with the pixel format, client encodings, and initial update request', function () {
client.set_true_color(true);
client.set_local_cursor(false);
var expected = RFB.messages.pixelFormat(4, 3, true);
expected = expected.concat(RFB.messages.clientEncodings(client._encodings, false, true));
var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 },
dirtyBoxes: [ { x: 0, y: 0, w: 27, h: 32 } ] };
expected = expected.concat(RFB.messages.fbUpdateRequests(expected_cdr, 27, 32));
send_server_init({ width: 27, height: 32 }, client);
expect(client._sock).to.have.sent(expected);
});
it('should check for sending mouse events', function () {
// be lazy with our checking so we don't have to check through the whole sent buffer
sinon.spy(client, '_checkEvents');
send_server_init({}, client);
expect(client._checkEvents).to.have.been.calledOnce;
});
it('should transition to the "normal" state', function () {
send_server_init({}, client);
expect(client._rfb_state).to.equal('normal');
});
});
});
describe('Protocol Message Processing After Completing Initialization', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'normal';
client._fb_name = 'some device';
client._fb_width = 640;
client._fb_height = 20;
});
describe('Framebuffer Update Handling', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'normal';
client._fb_name = 'some device';
client._fb_width = 640;
client._fb_height = 20;
});
var target_data_arr = [
0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255,
0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255
];
var target_data;
var target_data_check_arr = [
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
];
var target_data_check;
before(function () {
// NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray
target_data = new Uint8Array(target_data_arr);
target_data_check = new Uint8Array(target_data_check_arr);
});
function send_fbu_msg (rect_info, rect_data, client, rect_cnt) {
var data = [];
if (!rect_cnt || rect_cnt > -1) {
// header
data.push(0); // msg type
data.push(0); // padding
data.push16(rect_cnt || rect_data.length);
}
for (var i = 0; i < rect_data.length; i++) {
if (rect_info[i]) {
data.push16(rect_info[i].x);
data.push16(rect_info[i].y);
data.push16(rect_info[i].width);
data.push16(rect_info[i].height);
data.push32(rect_info[i].encoding);
}
data = data.concat(rect_data[i]);
}
client._sock._websocket._receive_data(new Uint8Array(data));
}
it('should send an update request if there is sufficient data', function () {
var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 },
dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] };
var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20);
client._framebufferUpdate = function () { return true; };
client._sock._websocket._receive_data(new Uint8Array([0]));
expect(client._sock).to.have.sent(expected_msg);
});
it('should not send an update request if we need more data', function () {
client._sock._websocket._receive_data(new Uint8Array([0]));
expect(client._sock._websocket._get_sent_data()).to.have.length(0);
});
it('should resume receiving an update if we previously did not have enough data', function () {
var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 },
dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] };
var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20);
// just enough to set FBU.rects
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3]));
expect(client._sock._websocket._get_sent_data()).to.have.length(0);
client._framebufferUpdate = function () { return true; }; // we magically have enough data
// 247 should *not* be used as the message type here
client._sock._websocket._receive_data(new Uint8Array([247]));
expect(client._sock).to.have.sent(expected_msg);
});
it('should parse out information from a header before any actual data comes in', function () {
client.set_onFBUReceive(sinon.spy());
var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02, encodingName: 'RRE' };
send_fbu_msg([rect_info], [[]], client);
var spy = client.get_onFBUReceive();
expect(spy).to.have.been.calledOnce;
expect(spy).to.have.been.calledWith(sinon.match.any, rect_info);
});
it('should fire onFBUComplete when the update is complete', function () {
client.set_onFBUComplete(sinon.spy());
var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: -224, encodingName: 'last_rect' };
send_fbu_msg([rect_info], [[]], client); // last_rect
var spy = client.get_onFBUComplete();
expect(spy).to.have.been.calledOnce;
expect(spy).to.have.been.calledWith(sinon.match.any, rect_info);
});
it('should not fire onFBUComplete if we have not finished processing the update', function () {
client.set_onFBUComplete(sinon.spy());
var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x00, encodingName: 'RAW' };
send_fbu_msg([rect_info], [[]], client);
expect(client.get_onFBUComplete()).to.not.have.been.called;
});
it('should call the appropriate encoding handler', function () {
client._encHandlers[0x02] = sinon.spy();
var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02 };
send_fbu_msg([rect_info], [[]], client);
expect(client._encHandlers[0x02]).to.have.been.calledOnce;
});
it('should fail on an unsupported encoding', function () {
client.set_onFBUReceive(sinon.spy());
var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 };
send_fbu_msg([rect_info], [[]], client);
expect(client._rfb_state).to.equal('failed');
});
it('should be able to pause and resume receiving rects if not enought data', function () {
// seed some initial data to copy
client._fb_width = 4;
client._fb_height = 4;
client._display.resize(4, 4);
var initial_data = client._display._drawCtx.createImageData(4, 2);
var initial_data_arr = target_data_check_arr.slice(0, 32);
for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; }
client._display._drawCtx.putImageData(initial_data, 0, 0);
var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01},
{ x: 2, y: 2, width: 2, height: 2, encoding: 0x01}];
// data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }]
var rects = [[0, 2, 0, 0], [0, 0, 0, 0]];
send_fbu_msg([info[0]], [rects[0]], client, 2);
send_fbu_msg([info[1]], [rects[1]], client, -1);
expect(client._display).to.have.displayed(target_data_check);
});
describe('Message Encoding Handlers', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'normal';
client._fb_name = 'some device';
// a really small frame
client._fb_width = 4;
client._fb_height = 4;
client._display._fb_width = 4;
client._display._fb_height = 4;
client._fb_Bpp = 4;
});
it('should handle the RAW encoding', function () {
var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 },
{ x: 2, y: 0, width: 2, height: 2, encoding: 0x00 },
{ x: 0, y: 2, width: 4, height: 1, encoding: 0x00 },
{ x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }];
// data is in bgrx
var rects = [
[0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0],
[0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0],
[0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0],
[0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]];
send_fbu_msg(info, rects, client);
expect(client._display).to.have.displayed(target_data);
});
it('should handle the COPYRECT encoding', function () {
// seed some initial data to copy
var initial_data = client._display._drawCtx.createImageData(4, 2);
var initial_data_arr = target_data_check_arr.slice(0, 32);
for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; }
client._display._drawCtx.putImageData(initial_data, 0, 0);
var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01},
{ x: 2, y: 2, width: 2, height: 2, encoding: 0x01}];
// data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }]
var rects = [[0, 2, 0, 0], [0, 0, 0, 0]];
send_fbu_msg(info, rects, client);
expect(client._display).to.have.displayed(target_data_check);
});
// TODO(directxman12): for encodings with subrects, test resuming on partial send?
// TODO(directxman12): test rre_chunk_sz (related to above about subrects)?
it('should handle the RRE encoding', function () {
var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }];
var rect = [];
rect.push32(2); // 2 subrects
rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
rect.push(0xff); // becomes ff0000ff --> #0000FF color
rect.push(0x00);
rect.push(0x00);
rect.push(0xff);
rect.push16(0); // x: 0
rect.push16(0); // y: 0
rect.push16(2); // width: 2
rect.push16(2); // height: 2
rect.push(0xff); // becomes ff0000ff --> #0000FF color
rect.push(0x00);
rect.push(0x00);
rect.push(0xff);
rect.push16(2); // x: 2
rect.push16(2); // y: 2
rect.push16(2); // width: 2
rect.push16(2); // height: 2
send_fbu_msg(info, [rect], client);
expect(client._display).to.have.displayed(target_data_check);
});
describe('the HEXTILE encoding handler', function () {
var client;
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'normal';
client._fb_name = 'some device';
// a really small frame
client._fb_width = 4;
client._fb_height = 4;
client._display._fb_width = 4;
client._display._fb_height = 4;
client._fb_Bpp = 4;
});
it('should handle a tile with fg, bg specified, normal subrects', function () {
var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
var rect = [];
rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
rect.push(0x00);
rect.push(0x00);
rect.push(0xff);
rect.push(2); // 2 subrects
rect.push(0); // x: 0, y: 0
rect.push(1 | (1 << 4)); // width: 2, height: 2
rect.push(2 | (2 << 4)); // x: 2, y: 2
rect.push(1 | (1 << 4)); // width: 2, height: 2
send_fbu_msg(info, [rect], client);
expect(client._display).to.have.displayed(target_data_check);
});
it('should handle a raw tile', function () {
var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
var rect = [];
rect.push(0x01); // raw
for (var i = 0; i < target_data.length; i += 4) {
rect.push(target_data[i + 2]);
rect.push(target_data[i + 1]);
rect.push(target_data[i]);
rect.push(target_data[i + 3]);
}
send_fbu_msg(info, [rect], client);
expect(client._display).to.have.displayed(target_data);
});
it('should handle a tile with only bg specified (solid bg)', function () {
var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
var rect = [];
rect.push(0x02);
rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
send_fbu_msg(info, [rect], client);
var expected = [];
for (var i = 0; i < 16; i++) { expected.push32(0xff00ff); }
expect(client._display).to.have.displayed(new Uint8Array(expected));
});
it('should handle a tile with bg and coloured subrects', function () {
var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
var rect = [];
rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects
rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
rect.push(2); // 2 subrects
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
rect.push(0x00);
rect.push(0x00);
rect.push(0xff);
rect.push(0); // x: 0, y: 0
rect.push(1 | (1 << 4)); // width: 2, height: 2
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
rect.push(0x00);
rect.push(0x00);
rect.push(0xff);
rect.push(2 | (2 << 4)); // x: 2, y: 2
rect.push(1 | (1 << 4)); // width: 2, height: 2
send_fbu_msg(info, [rect], client);
expect(client._display).to.have.displayed(target_data_check);
});
it('should carry over fg and bg colors from the previous tile if not specified', function () {
client._fb_width = 4;
client._fb_height = 17;
client._display.resize(4, 17);
var info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}];
var rect = [];
rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
rect.push(0x00);
rect.push(0x00);
rect.push(0xff);
rect.push(8); // 8 subrects
var i;
for (i = 0; i < 4; i++) {
rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4
rect.push(1 | (1 << 4)); // width: 2, height: 2
rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2
rect.push(1 | (1 << 4)); // width: 2, height: 2
}
rect.push(0x08); // anysubrects
rect.push(1); // 1 subrect
rect.push(0); // x: 0, y: 0
rect.push(1 | (1 << 4)); // width: 2, height: 2
send_fbu_msg(info, [rect], client);
var expected = [];
for (i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); }
expected = expected.concat(target_data_check_arr.slice(0, 16));
expect(client._display).to.have.displayed(new Uint8Array(expected));
});
it('should fail on an invalid subencoding', function () {
var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
var rects = [[45]]; // an invalid subencoding
send_fbu_msg(info, rects, client);
expect(client._rfb_state).to.equal('failed');
});
});
it.skip('should handle the TIGHT encoding', function () {
// TODO(directxman12): test this
});
it.skip('should handle the TIGHT_PNG encoding', function () {
// TODO(directxman12): test this
});
it('should handle the DesktopSize pseduo-encoding', function () {
client.set_onFBResize(sinon.spy());
sinon.spy(client._display, 'resize');
send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client);
var spy = client.get_onFBResize();
expect(spy).to.have.been.calledOnce;
expect(spy).to.have.been.calledWith(sinon.match.any, 20, 50);
expect(client._fb_width).to.equal(20);
expect(client._fb_height).to.equal(50);
expect(client._display.resize).to.have.been.calledOnce;
expect(client._display.resize).to.have.been.calledWith(20, 50);
});
it.skip('should handle the Cursor pseudo-encoding', function () {
// TODO(directxman12): test
});
it('should handle the last_rect pseudo-encoding', function () {
client.set_onFBUReceive(sinon.spy());
send_fbu_msg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100);
expect(client._FBU.rects).to.equal(0);
expect(client.get_onFBUReceive()).to.have.been.calledOnce;
});
});
});
it('should set the colour map on the display on SetColourMapEntries', function () {
var expected_cm = [];
var data = [1, 0, 0, 1, 0, 4];
var i;
for (i = 0; i < 4; i++) {
expected_cm[i + 1] = [i * 10, i * 10 + 1, i * 10 + 2];
data.push16(expected_cm[i + 1][2] << 8);
data.push16(expected_cm[i + 1][1] << 8);
data.push16(expected_cm[i + 1][0] << 8);
}
client._sock._websocket._receive_data(new Uint8Array(data));
expect(client._display.get_colourMap()).to.deep.equal(expected_cm);
});
describe('XVP Message Handling', function () {
beforeEach(function () {
client = make_rfb();
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'normal';
client._fb_name = 'some device';
client._fb_width = 27;
client._fb_height = 32;
});
it('should call updateState with a message on XVP_FAIL, but keep the same state', function () {
client._updateState = sinon.spy();
client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 0]));
expect(client._updateState).to.have.been.calledOnce;
expect(client._updateState).to.have.been.calledWith('normal', 'Operation Failed');
});
it('should set the XVP version and fire the callback with the version on XVP_INIT', function () {
client.set_onXvpInit(sinon.spy());
client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 1]));
expect(client._rfb_xvp_ver).to.equal(10);
expect(client.get_onXvpInit()).to.have.been.calledOnce;
expect(client.get_onXvpInit()).to.have.been.calledWith(10);
});
it('should fail on unknown XVP message types', function () {
client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237]));
expect(client._rfb_state).to.equal('failed');
});
});
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
var expected_str = 'cheese!';
var data = [3, 0, 0, 0];
data.push32(expected_str.length);
for (var i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); }
client.set_onClipboard(sinon.spy());
client._sock._websocket._receive_data(new Uint8Array(data));
var spy = client.get_onClipboard();
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][1]).to.equal(expected_str);
});
it('should fire the bell callback on Bell', function () {
client.set_onBell(sinon.spy());
client._sock._websocket._receive_data(new Uint8Array([2]));
expect(client.get_onBell()).to.have.been.calledOnce;
});
it('should fail on an unknown message type', function () {
client._sock._websocket._receive_data(new Uint8Array([87]));
expect(client._rfb_state).to.equal('failed');
});
});
describe('Asynchronous Events', function () {
describe('Mouse event handlers', function () {
var client;
beforeEach(function () {
client = make_rfb();
client._sock.send = sinon.spy();
client._rfb_state = 'normal';
});
it('should not send button messages in view-only mode', function () {
client._view_only = true;
client._mouse._onMouseButton(0, 0, 1, 0x001);
expect(client._sock.send).to.not.have.been.called;
});
it('should not send movement messages in view-only mode', function () {
client._view_only = true;
client._mouse._onMouseMove(0, 0);
expect(client._sock.send).to.not.have.been.called;
});
it('should send a pointer event on mouse button presses', function () {
client._mouse._onMouseButton(10, 12, 1, 0x001);
expect(client._sock.send).to.have.been.calledOnce;
var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x001);
expect(client._sock.send).to.have.been.calledWith(pointer_msg);
});
it('should send a pointer event on mouse movement', function () {
client._mouse._onMouseMove(10, 12);
expect(client._sock.send).to.have.been.calledOnce;
var pointer_msg = RFB.messages.pointerEvent(10, 12, 0);
expect(client._sock.send).to.have.been.calledWith(pointer_msg);
});
it('should set the button mask so that future mouse movements use it', function () {
client._mouse._onMouseButton(10, 12, 1, 0x010);
client._sock.send = sinon.spy();
client._mouse._onMouseMove(13, 9);
expect(client._sock.send).to.have.been.calledOnce;
var pointer_msg = RFB.messages.pointerEvent(13, 9, 0x010);
expect(client._sock.send).to.have.been.calledWith(pointer_msg);
});
// NB(directxman12): we don't need to test not sending messages in
// non-normal modes, since we haven't grabbed input
// yet (grabbing input should be checked in the lifecycle tests).
it('should not send movement messages when viewport dragging', function () {
client._viewportDragging = true;
client._display.viewportChange = sinon.spy();
client._mouse._onMouseMove(13, 9);
expect(client._sock.send).to.not.have.been.called;
});
it('should not send button messages when initiating viewport dragging', function () {
client._viewportDrag = true;
client._mouse._onMouseButton(13, 9, 0x001);
expect(client._sock.send).to.not.have.been.called;
});
it('should be initiate viewport dragging on a button down event, if enabled', function () {
client._viewportDrag = true;
client._mouse._onMouseButton(13, 9, 0x001);
expect(client._viewportDragging).to.be.true;
expect(client._viewportDragPos).to.deep.equal({ x: 13, y: 9 });
});
it('should terminate viewport dragging on a button up event, if enabled', function () {
client._viewportDrag = true;
client._viewportDragging = true;
client._mouse._onMouseButton(13, 9, 0x000);
expect(client._viewportDragging).to.be.false;
});
it('if enabled, viewportDragging should occur on mouse movement while a button is down', function () {
client._viewportDrag = true;
client._viewportDragging = true;
client._viewportDragPos = { x: 13, y: 9 };
client._display.viewportChange = sinon.spy();
client._mouse._onMouseMove(10, 4);
expect(client._viewportDragging).to.be.true;
expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 });
expect(client._display.viewportChange).to.have.been.calledOnce;
expect(client._display.viewportChange).to.have.been.calledWith(3, 5);
});
});
describe('Keyboard Event Handlers', function () {
var client;
beforeEach(function () {
client = make_rfb();
client._sock.send = sinon.spy();
});
it('should send a key message on a key press', function () {
client._keyboard._onKeyPress(1234, 1);
expect(client._sock.send).to.have.been.calledOnce;
var key_msg = RFB.messages.keyEvent(1234, 1);
expect(client._sock.send).to.have.been.calledWith(key_msg);
});
it('should not send messages in view-only mode', function () {
client._view_only = true;
client._keyboard._onKeyPress(1234, 1);
expect(client._sock.send).to.not.have.been.called;
});
});
describe('WebSocket event handlers', function () {
var client;
beforeEach(function () {
client = make_rfb();
this.clock = sinon.useFakeTimers();
});
afterEach(function () { this.clock.restore(); });
// message events
it ('should do nothing if we receive an empty message and have nothing in the queue', function () {
client.connect('host', 8675);
client._rfb_state = 'normal';
client._normal_msg = sinon.spy();
client._sock._websocket._receive_data(Base64.encode([]));
expect(client._normal_msg).to.not.have.been.called;
});
it('should handle a message in the normal state as a normal message', function () {
client.connect('host', 8675);
client._rfb_state = 'normal';
client._normal_msg = sinon.spy();
client._sock._websocket._receive_data(Base64.encode([1, 2, 3]));
expect(client._normal_msg).to.have.been.calledOnce;
});
it('should handle a message in any non-disconnected/failed state like an init message', function () {
client.connect('host', 8675);
client._rfb_state = 'ProtocolVersion';
client._init_msg = sinon.spy();
client._sock._websocket._receive_data(Base64.encode([1, 2, 3]));
expect(client._init_msg).to.have.been.calledOnce;
});
it('should split up the handling of muplitle normal messages across 10ms intervals', function () {
client.connect('host', 8675);
client._sock._websocket._open();
client._rfb_state = 'normal';
client.set_onBell(sinon.spy());
client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02]));
expect(client.get_onBell()).to.have.been.calledOnce;
this.clock.tick(20);
expect(client.get_onBell()).to.have.been.calledTwice;
});
// open events
it('should update the state to ProtocolVersion on open (if the state is "connect")', function () {
client.connect('host', 8675);
client._sock._websocket._open();
expect(client._rfb_state).to.equal('ProtocolVersion');
});
it('should fail if we are not currently ready to connect and we get an "open" event', function () {
client.connect('host', 8675);
client._rfb_state = 'some_other_state';
client._sock._websocket._open();
expect(client._rfb_state).to.equal('failed');
});
// close events
it('should transition to "disconnected" from "disconnect" on a close event', function () {
client.connect('host', 8675);
client._rfb_state = 'disconnect';
client._sock._websocket.close();
expect(client._rfb_state).to.equal('disconnected');
});
it('should transition to failed if we get a close event from any non-"disconnection" state', function () {
client.connect('host', 8675);
client._rfb_state = 'normal';
client._sock._websocket.close();
expect(client._rfb_state).to.equal('failed');
});
// error events do nothing
});
});
});
// requires local modules: util
/* jshint expr: true */
var assert = chai.assert;
var expect = chai.expect;
describe('Utils', function() {
"use strict";
describe('Array instance methods', function () {
describe('push8', function () {
it('should push a byte on to the array', function () {
var arr = [1];
arr.push8(128);
expect(arr).to.deep.equal([1, 128]);
});
it('should only use the least significant byte of any number passed in', function () {
var arr = [1];
arr.push8(0xABCD);
expect(arr).to.deep.equal([1, 0xCD]);
});
});
describe('push16', function () {
it('should push two bytes on to the array', function () {
var arr = [1];
arr.push16(0xABCD);
expect(arr).to.deep.equal([1, 0xAB, 0xCD]);
});
it('should only use the two least significant bytes of any number passed in', function () {
var arr = [1];
arr.push16(0xABCDEF);
expect(arr).to.deep.equal([1, 0xCD, 0xEF]);
});
});
describe('push32', function () {
it('should push four bytes on to the array', function () {
var arr = [1];
arr.push32(0xABCDEF12);
expect(arr).to.deep.equal([1, 0xAB, 0xCD, 0xEF, 0x12]);
});
it('should only use the four least significant bytes of any number passed in', function () {
var arr = [1];
arr.push32(0xABCDEF1234);
expect(arr).to.deep.equal([1, 0xCD, 0xEF, 0x12, 0x34]);
});
});
});
describe('logging functions', function () {
beforeEach(function () {
sinon.spy(console, 'log');
sinon.spy(console, 'warn');
sinon.spy(console, 'error');
});
afterEach(function () {
console.log.restore();
console.warn.restore();
console.error.restore();
});
it('should use noop for levels lower than the min level', function () {
Util.init_logging('warn');
Util.Debug('hi');
Util.Info('hello');
expect(console.log).to.not.have.been.called;
});
it('should use console.log for Debug and Info', function () {
Util.init_logging('debug');
Util.Debug('dbg');
Util.Info('inf');
expect(console.log).to.have.been.calledWith('dbg');
expect(console.log).to.have.been.calledWith('inf');
});
it('should use console.warn for Warn', function () {
Util.init_logging('warn');
Util.Warn('wrn');
expect(console.warn).to.have.been.called;
expect(console.warn).to.have.been.calledWith('wrn');
});
it('should use console.error for Error', function () {
Util.init_logging('error');
Util.Error('err');
expect(console.error).to.have.been.called;
expect(console.error).to.have.been.calledWith('err');
});
});
// TODO(directxman12): test the conf_default and conf_defaults methods
// TODO(directxman12): test decodeUTF8
// TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent)
// TODO(directxman12): figure out a good way to test getPosition and getEventPosition
// TODO(directxman12): figure out how to test the browser detection functions properly
// (we can't really test them against the browsers, except for Gecko
// via PhantomJS, the default test driver)
// TODO(directxman12): figure out how to test Util.Flash
});
// requires local modules: websock, base64, util
// requires test modules: fake.websocket
/* jshint expr: true */
var assert = chai.assert;
var expect = chai.expect;
describe('Websock', function() {
"use strict";
describe('Queue methods', function () {
var sock;
var RQ_TEMPLATE = [0, 1, 2, 3, 4, 5, 6, 7];
beforeEach(function () {
sock = new Websock();
for (var i = RQ_TEMPLATE.length - 1; i >= 0; i--) {
sock.rQunshift8(RQ_TEMPLATE[i]);
}
});
describe('rQlen', function () {
it('should return the length of the receive queue', function () {
sock.set_rQi(0);
expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length);
});
it("should return the proper length if we read some from the receive queue", function () {
sock.set_rQi(1);
expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length - 1);
});
});
describe('rQpeek8', function () {
it('should peek at the next byte without poping it off the queue', function () {
var bef_len = sock.rQlen();
var peek = sock.rQpeek8();
expect(sock.rQpeek8()).to.equal(peek);
expect(sock.rQlen()).to.equal(bef_len);
});
});
describe('rQshift8', function () {
it('should pop a single byte from the receive queue', function () {
var peek = sock.rQpeek8();
var bef_len = sock.rQlen();
expect(sock.rQshift8()).to.equal(peek);
expect(sock.rQlen()).to.equal(bef_len - 1);
});
});
describe('rQunshift8', function () {
it('should place a byte at the front of the queue', function () {
sock.rQunshift8(255);
expect(sock.rQpeek8()).to.equal(255);
expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length + 1);
});
});
describe('rQshift16', function () {
it('should pop two bytes from the receive queue and return a single number', function () {
var bef_len = sock.rQlen();
var expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1];
expect(sock.rQshift16()).to.equal(expected);
expect(sock.rQlen()).to.equal(bef_len - 2);
});
});
describe('rQshift32', function () {
it('should pop four bytes from the receive queue and return a single number', function () {
var bef_len = sock.rQlen();
var expected = (RQ_TEMPLATE[0] << 24) +
(RQ_TEMPLATE[1] << 16) +
(RQ_TEMPLATE[2] << 8) +
RQ_TEMPLATE[3];
expect(sock.rQshift32()).to.equal(expected);
expect(sock.rQlen()).to.equal(bef_len - 4);
});
});
describe('rQshiftStr', function () {
it('should shift the given number of bytes off of the receive queue and return a string', function () {
var bef_len = sock.rQlen();
var bef_rQi = sock.get_rQi();
var shifted = sock.rQshiftStr(3);
expect(shifted).to.be.a('string');
expect(shifted).to.equal(String.fromCharCode.apply(null, RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3)));
expect(sock.rQlen()).to.equal(bef_len - 3);
});
it('should shift the entire rest of the queue off if no length is given', function () {
sock.rQshiftStr();
expect(sock.rQlen()).to.equal(0);
});
});
describe('rQshiftBytes', function () {
it('should shift the given number of bytes of the receive queue and return an array', function () {
var bef_len = sock.rQlen();
var bef_rQi = sock.get_rQi();
var shifted = sock.rQshiftBytes(3);
expect(shifted).to.be.an.instanceof(Array);
expect(shifted).to.deep.equal(RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3));
expect(sock.rQlen()).to.equal(bef_len - 3);
});
it('should shift the entire rest of the queue off if no length is given', function () {
sock.rQshiftBytes();
expect(sock.rQlen()).to.equal(0);
});
});
describe('rQslice', function () {
beforeEach(function () {
sock.set_rQi(0);
});
it('should not modify the receive queue', function () {
var bef_len = sock.rQlen();
sock.rQslice(0, 2);
expect(sock.rQlen()).to.equal(bef_len);
});
it('should return an array containing the given slice of the receive queue', function () {
var sl = sock.rQslice(0, 2);
expect(sl).to.be.an.instanceof(Array);
expect(sl).to.deep.equal(RQ_TEMPLATE.slice(0, 2));
});
it('should use the rest of the receive queue if no end is given', function () {
var sl = sock.rQslice(1);
expect(sl).to.have.length(RQ_TEMPLATE.length - 1);
expect(sl).to.deep.equal(RQ_TEMPLATE.slice(1));
});
it('should take the current rQi in to account', function () {
sock.set_rQi(1);
expect(sock.rQslice(0, 2)).to.deep.equal(RQ_TEMPLATE.slice(1, 3));
});
});
describe('rQwait', function () {
beforeEach(function () {
sock.set_rQi(0);
});
it('should return true if there are not enough bytes in the receive queue', function () {
expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true;
});
it('should return false if there are enough bytes in the receive queue', function () {
expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false;
});
it('should return true and reduce rQi by "goback" if there are not enough bytes', function () {
sock.set_rQi(5);
expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true;
expect(sock.get_rQi()).to.equal(1);
});
it('should raise an error if we try to go back more than possible', function () {
sock.set_rQi(5);
expect(function () { sock.rQwait('hi', RQ_TEMPLATE.length, 6); }).to.throw(Error);
});
it('should not reduce rQi if there are enough bytes', function () {
sock.set_rQi(5);
sock.rQwait('hi', 1, 6);
expect(sock.get_rQi()).to.equal(5);
});
});
describe('flush', function () {
beforeEach(function () {
sock._websocket = {
send: sinon.spy()
};
});
it('should actually send on the websocket if the websocket does not have too much buffered', function () {
sock.maxBufferedAmount = 10;
sock._websocket.bufferedAmount = 8;
sock._sQ = [1, 2, 3];
var encoded = sock._encode_message();
sock.flush();
expect(sock._websocket.send).to.have.been.calledOnce;
expect(sock._websocket.send).to.have.been.calledWith(encoded);
});
it('should return true if the websocket did not have too much buffered', function () {
sock.maxBufferedAmount = 10;
sock._websocket.bufferedAmount = 8;
expect(sock.flush()).to.be.true;
});
it('should not call send if we do not have anything queued up', function () {
sock._sQ = [];
sock.maxBufferedAmount = 10;
sock._websocket.bufferedAmount = 8;
sock.flush();
expect(sock._websocket.send).not.to.have.been.called;
});
it('should not send and return false if the websocket has too much buffered', function () {
sock.maxBufferedAmount = 10;
sock._websocket.bufferedAmount = 12;
expect(sock.flush()).to.be.false;
expect(sock._websocket.send).to.not.have.been.called;
});
});
describe('send', function () {
beforeEach(function () {
sock.flush = sinon.spy();
});
it('should add to the send queue', function () {
sock.send([1, 2, 3]);
var sq = sock.get_sQ();
expect(sock.get_sQ().slice(sq.length - 3)).to.deep.equal([1, 2, 3]);
});
it('should call flush', function () {
sock.send([1, 2, 3]);
expect(sock.flush).to.have.been.calledOnce;
});
});
describe('send_string', function () {
beforeEach(function () {
sock.send = sinon.spy();
});
it('should call send after converting the string to an array', function () {
sock.send_string("\x01\x02\x03");
expect(sock.send).to.have.been.calledWith([1, 2, 3]);
});
});
});
describe('lifecycle methods', function () {
var old_WS;
before(function () {
old_WS = WebSocket;
});
var sock;
beforeEach(function () {
sock = new Websock();
WebSocket = sinon.spy();
WebSocket.OPEN = old_WS.OPEN;
WebSocket.CONNECTING = old_WS.CONNECTING;
WebSocket.CLOSING = old_WS.CLOSING;
WebSocket.CLOSED = old_WS.CLOSED;
});
describe('opening', function () {
it('should pick the correct protocols if none are given' , function () {
});
it('should open the actual websocket', function () {
sock.open('ws://localhost:8675', 'base64');
expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'base64');
});
it('should fail if we try to use binary but do not support it', function () {
expect(function () { sock.open('ws:///', 'binary'); }).to.throw(Error);
});
it('should fail if we specified an array with only binary and we do not support it', function () {
expect(function () { sock.open('ws:///', ['binary']); }).to.throw(Error);
});
it('should skip binary if we have multiple options for encoding and do not support binary', function () {
sock.open('ws:///', ['binary', 'base64']);
expect(WebSocket).to.have.been.calledWith('ws:///', ['base64']);
});
// it('should initialize the event handlers')?
});
describe('closing', function () {
beforeEach(function () {
sock.open('ws://');
sock._websocket.close = sinon.spy();
});
it('should close the actual websocket if it is open', function () {
sock._websocket.readyState = WebSocket.OPEN;
sock.close();
expect(sock._websocket.close).to.have.been.calledOnce;
});
it('should close the actual websocket if it is connecting', function () {
sock._websocket.readyState = WebSocket.CONNECTING;
sock.close();
expect(sock._websocket.close).to.have.been.calledOnce;
});
it('should not try to close the actual websocket if closing', function () {
sock._websocket.readyState = WebSocket.CLOSING;
sock.close();
expect(sock._websocket.close).not.to.have.been.called;
});
it('should not try to close the actual websocket if closed', function () {
sock._websocket.readyState = WebSocket.CLOSED;
sock.close();
expect(sock._websocket.close).not.to.have.been.called;
});
it('should reset onmessage to not call _recv_message', function () {
sinon.spy(sock, '_recv_message');
sock.close();
sock._websocket.onmessage(null);
try {
expect(sock._recv_message).not.to.have.been.called;
} finally {
sock._recv_message.restore();
}
});
});
describe('event handlers', function () {
beforeEach(function () {
sock._recv_message = sinon.spy();
sock.on('open', sinon.spy());
sock.on('close', sinon.spy());
sock.on('error', sinon.spy());
sock.open('ws://');
});
it('should call _recv_message on a message', function () {
sock._websocket.onmessage(null);
expect(sock._recv_message).to.have.been.calledOnce;
});
it('should copy the mode over upon opening', function () {
sock._websocket.protocol = 'cheese';
sock._websocket.onopen();
expect(sock._mode).to.equal('cheese');
});
it('should assume base64 if no protocol was available on opening', function () {
sock._websocket.protocol = null;
sock._websocket.onopen();
expect(sock._mode).to.equal('base64');
});
it('should call the open event handler on opening', function () {
sock._websocket.onopen();
expect(sock._eventHandlers.open).to.have.been.calledOnce;
});
it('should call the close event handler on closing', function () {
sock._websocket.onclose();
expect(sock._eventHandlers.close).to.have.been.calledOnce;
});
it('should call the error event handler on error', function () {
sock._websocket.onerror();
expect(sock._eventHandlers.error).to.have.been.calledOnce;
});
});
after(function () {
WebSocket = old_WS;
});
});
describe('WebSocket Receiving', function () {
var sock;
beforeEach(function () {
sock = new Websock();
});
it('should support decoding base64 string data to add it to the receive queue', function () {
var msg = { data: Base64.encode([1, 2, 3]) };
sock._mode = 'base64';
sock._recv_message(msg);
expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
});
it('should support adding binary Uint8Array data to the receive queue', function () {
var msg = { data: new Uint8Array([1, 2, 3]) };
sock._mode = 'binary';
sock._recv_message(msg);
expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
});
it('should call the message event handler if present', function () {
sock._eventHandlers.message = sinon.spy();
var msg = { data: Base64.encode([1, 2, 3]) };
sock._mode = 'base64';
sock._recv_message(msg);
expect(sock._eventHandlers.message).to.have.been.calledOnce;
});
it('should not call the message event handler if there is nothing in the receive queue', function () {
sock._eventHandlers.message = sinon.spy();
var msg = { data: Base64.encode([]) };
sock._mode = 'base64';
sock._recv_message(msg);
expect(sock._eventHandlers.message).not.to.have.been.called;
});
it('should compact the receive queue', function () {
// NB(sross): while this is an internal implementation detail, it's important to
// test, otherwise the receive queue could become very large very quickly
sock._rQ = [0, 1, 2, 3, 4, 5];
sock.set_rQi(6);
sock._rQmax = 3;
var msg = { data: Base64.encode([1, 2, 3]) };
sock._mode = 'base64';
sock._recv_message(msg);
expect(sock._rQ.length).to.equal(3);
expect(sock.get_rQi()).to.equal(0);
});
it('should call the error event handler on an exception', function () {
sock._eventHandlers.error = sinon.spy();
sock._eventHandlers.message = sinon.stub().throws();
var msg = { data: Base64.encode([1, 2, 3]) };
sock._mode = 'base64';
sock._recv_message(msg);
expect(sock._eventHandlers.error).to.have.been.calledOnce;
});
});
describe('Data encoding', function () {
before(function () { FakeWebSocket.replace(); });
after(function () { FakeWebSocket.restore(); });
describe('as binary data', function () {
var sock;
beforeEach(function () {
sock = new Websock();
sock.open('ws://', 'binary');
sock._websocket._open();
});
it('should convert the send queue into an ArrayBuffer', function () {
sock._sQ = [1, 2, 3];
var res = sock._encode_message(); // An ArrayBuffer
expect(new Uint8Array(res)).to.deep.equal(new Uint8Array(res));
});
it('should properly pass the encoded data off to the actual WebSocket', function () {
sock.send([1, 2, 3]);
expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]);
});
});
describe('as Base64 data', function () {
var sock;
beforeEach(function () {
sock = new Websock();
sock.open('ws://', 'base64');
sock._websocket._open();
});
it('should convert the send queue into a Base64-encoded string', function () {
sock._sQ = [1, 2, 3];
expect(sock._encode_message()).to.equal(Base64.encode([1, 2, 3]));
});
it('should properly pass the encoded data off to the actual WebSocket', function () {
sock.send([1, 2, 3]);
expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]);
});
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment