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,112 +4,110 @@ ...@@ -4,112 +4,110 @@
// 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 : '=',
encode: function (data) {
"use strict";
var result = '';
var toBase64Table = Base64.toBase64Table;
var length = data.length;
var lengthpad = (length % 3);
// Convert every three bytes to 4 ascii characters.
for (var i = 0; i < (length - 2); i += 3) {
result += toBase64Table[data[i] >> 2];
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 + 2] & 0x3f];
}
/* Convert data (an array of integers) to a Base64 string. */ // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), var j = 0;
base64Pad : '=', if (lengthpad === 2) {
j = length - lengthpad;
encode: function (data) { result += toBase64Table[data[j] >> 2];
"use strict"; result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
var result = ''; result += toBase64Table[(data[j + 1] & 0x0f) << 2];
var toBase64Table = Base64.toBase64Table; result += toBase64Table[64];
var length = data.length } else if (lengthpad === 1) {
var lengthpad = (length%3); j = length - lengthpad;
var i = 0, j = 0; result += toBase64Table[data[j] >> 2];
// Convert every three bytes to 4 ascii characters. result += toBase64Table[(data[j] & 0x03) << 4];
/* BEGIN LOOP */ result += toBase64Table[64];
for (i = 0; i < (length - 2); i += 3) { result += toBase64Table[64];
result += toBase64Table[data[i] >> 2];
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+2] & 0x3f];
}
/* END LOOP */
// Convert the remaining 1 or 2 bytes, pad out to 4 characters.
if (lengthpad === 2) {
j = length - lengthpad;
result += toBase64Table[data[j] >> 2];
result += toBase64Table[((data[j] & 0x03) << 4) + (data[j+1] >> 4)];
result += toBase64Table[(data[j+1] & 0x0f) << 2];
result += toBase64Table[64];
} else if (lengthpad === 1) {
j = length - lengthpad;
result += toBase64Table[data[j] >> 2];
result += toBase64Table[(data[j] & 0x03) << 4];
result += toBase64Table[64];
result += toBase64Table[64];
}
return result;
},
/* Convert Base64 data to a string */
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,62, -1,-1,-1,63,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
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,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
],
decode: function (data, offset) {
"use strict";
offset = typeof(offset) !== 'undefined' ? offset : 0;
var toBinaryTable = Base64.toBinaryTable;
var base64Pad = Base64.base64Pad;
var result, result_length, idx, i, c, padding;
var leftbits = 0; // number of bits decoded, but yet to be appended
var leftdata = 0; // bits decoded, but yet to be appended
var data_length = data.indexOf('=') - offset;
if (data_length < 0) { data_length = data.length - offset; }
/* Every four characters is 3 resulting numbers */
result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5);
result = new Array(result_length);
// Convert one by one.
/* BEGIN LOOP */
for (idx = 0, i = offset; i < data.length; i++) {
c = toBinaryTable[data.charCodeAt(i) & 0x7f];
padding = (data.charAt(i) === base64Pad);
// Skip illegal characters and whitespace
if (c === -1) {
console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
continue;
} }
// Collect data into leftdata, update bitcount
leftdata = (leftdata << 6) | c;
leftbits += 6;
// If we have 8 or more bits, append 8 bits to the result return result;
if (leftbits >= 8) { },
leftbits -= 8;
// Append if not padding. /* Convert Base64 data to a string */
if (!padding) { /* jshint -W013 */
result[idx++] = (leftdata >> leftbits) & 0xff; 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,62, -1,-1,-1,63,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
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,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
],
/* jshint +W013 */
decode: function (data, offset) {
"use strict";
offset = typeof(offset) !== 'undefined' ? offset : 0;
var toBinaryTable = Base64.toBinaryTable;
var base64Pad = Base64.base64Pad;
var result, result_length;
var leftbits = 0; // number of bits decoded, but yet to be appended
var leftdata = 0; // bits decoded, but yet to be appended
var data_length = data.indexOf('=') - offset;
if (data_length < 0) { data_length = data.length - offset; }
/* Every four characters is 3 resulting numbers */
result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5);
result = new Array(result_length);
// Convert one by one.
for (var idx = 0, i = offset; i < data.length; i++) {
var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
var padding = (data.charAt(i) === base64Pad);
// Skip illegal characters and whitespace
if (c === -1) {
console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
continue;
}
// Collect data into leftdata, update bitcount
leftdata = (leftdata << 6) | c;
leftbits += 6;
// If we have 8 or more bits, append 8 bits to the result
if (leftbits >= 8) {
leftbits -= 8;
// Append if not padding.
if (!padding) {
result[idx++] = (leftdata >> leftbits) & 0xff;
}
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,199 +75,202 @@ ...@@ -75,199 +75,202 @@
* 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
25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28],
keys = []; z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8,
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;
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, 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|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, 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,
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|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=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; 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];
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=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
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, 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,
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, 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|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|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,
a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|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];
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, a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
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, 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,
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|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|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]; 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,
a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|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];
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, a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
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, 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,
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, 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,
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]; 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,
a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|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];
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=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
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, 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,
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, 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,
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]; 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,
a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|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];
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, a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
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, 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,
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, 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,
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]; 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=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; 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];
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, a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|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, 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|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|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,
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]; 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,
a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|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];
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, a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
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, 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,
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, 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,
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]; 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];
// 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;
} }
for (i = 0; i < 16; ++i) { for (i = 0; i < 16; ++i) {
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];
} else { } else {
pcr[j] = pc1m[l - 28]; pcr[j] = pc1m[l - 28];
}
} }
} }
} 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); }
} }
} }
}
// cookey // cookey
for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) { for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
raw0 = kn[rawi++]; raw0 = kn[rawi++];
raw1 = kn[rawi++]; raw1 = kn[rawi++];
keys[KnLi] = (raw0 & 0x00fc0000) << 6; keys[KnLi] = (raw0 & 0x00fc0000) << 6;
keys[KnLi] |= (raw0 & 0x00000fc0) << 10; keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10; keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
++KnLi; ++KnLi;
keys[KnLi] = (raw0 & 0x0003f000) << 12; keys[KnLi] = (raw0 & 0x0003f000) << 12;
keys[KnLi] |= (raw0 & 0x0000003f) << 16; keys[KnLi] |= (raw0 & 0x0000003f) << 16;
keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
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
// Squash 8 bytes to 2 ints // Squash 8 bytes to 2 ints
l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
x = ((l >>> 4) ^ r) & 0x0f0f0f0f; x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
r ^= x; r ^= x;
l ^= (x << 4); l ^= (x << 4);
x = ((l >>> 16) ^ r) & 0x0000ffff; x = ((l >>> 16) ^ r) & 0x0000ffff;
r ^= x; r ^= x;
l ^= (x << 16); l ^= (x << 16);
x = ((r >>> 2) ^ l) & 0x33333333; x = ((r >>> 2) ^ l) & 0x33333333;
l ^= x; l ^= x;
r ^= (x << 2); r ^= (x << 2);
x = ((r >>> 8) ^ l) & 0x00ff00ff; x = ((r >>> 8) ^ l) & 0x00ff00ff;
l ^= x; l ^= x;
r ^= (x << 8); r ^= (x << 8);
r = (r << 1) | ((r >>> 31) & 1); r = (r << 1) | ((r >>> 31) & 1);
x = (l ^ r) & 0xaaaaaaaa; x = (l ^ r) & 0xaaaaaaaa;
l ^= x; l ^= x;
r ^= x; r ^= x;
l = (l << 1) | ((l >>> 31) & 1); l = (l << 1) | ((l >>> 31) & 1);
for (i = 0; i < 8; ++i) { for (i = 0; i < 8; ++i) {
x = (r << 28) | (r >>> 4); x = (r << 28) | (r >>> 4);
x ^= keys[keysi++]; x ^= keys[keysi++];
fval = SP7[x & 0x3f]; fval = SP7[x & 0x3f];
fval |= SP5[(x >>> 8) & 0x3f]; fval |= SP5[(x >>> 8) & 0x3f];
fval |= SP3[(x >>> 16) & 0x3f]; fval |= SP3[(x >>> 16) & 0x3f];
fval |= SP1[(x >>> 24) & 0x3f]; fval |= SP1[(x >>> 24) & 0x3f];
x = r ^ keys[keysi++]; x = r ^ keys[keysi++];
fval |= SP8[x & 0x3f]; fval |= SP8[x & 0x3f];
fval |= SP6[(x >>> 8) & 0x3f]; fval |= SP6[(x >>> 8) & 0x3f];
fval |= SP4[(x >>> 16) & 0x3f]; fval |= SP4[(x >>> 16) & 0x3f];
fval |= SP2[(x >>> 24) & 0x3f]; fval |= SP2[(x >>> 24) & 0x3f];
l ^= fval; l ^= fval;
x = (l << 28) | (l >>> 4); x = (l << 28) | (l >>> 4);
x ^= keys[keysi++]; x ^= keys[keysi++];
fval = SP7[x & 0x3f]; fval = SP7[x & 0x3f];
fval |= SP5[(x >>> 8) & 0x3f]; fval |= SP5[(x >>> 8) & 0x3f];
fval |= SP3[(x >>> 16) & 0x3f]; fval |= SP3[(x >>> 16) & 0x3f];
fval |= SP1[(x >>> 24) & 0x3f]; fval |= SP1[(x >>> 24) & 0x3f];
x = l ^ keys[keysi++]; x = l ^ keys[keysi++];
fval |= SP8[x & 0x0000003f]; fval |= SP8[x & 0x0000003f];
fval |= SP6[(x >>> 8) & 0x3f]; fval |= SP6[(x >>> 8) & 0x3f];
fval |= SP4[(x >>> 16) & 0x3f]; fval |= SP4[(x >>> 16) & 0x3f];
fval |= SP2[(x >>> 24) & 0x3f]; fval |= SP2[(x >>> 24) & 0x3f];
r ^= fval; r ^= fval;
} }
r = (r << 31) | (r >>> 1); r = (r << 31) | (r >>> 1);
x = (l ^ r) & 0xaaaaaaaa; x = (l ^ r) & 0xaaaaaaaa;
l ^= x; l ^= x;
r ^= x; r ^= x;
l = (l << 31) | (l >>> 1); l = (l << 31) | (l >>> 1);
x = ((l >>> 8) ^ r) & 0x00ff00ff; x = ((l >>> 8) ^ r) & 0x00ff00ff;
r ^= x; r ^= x;
l ^= (x << 8); l ^= (x << 8);
x = ((l >>> 2) ^ r) & 0x33333333; x = ((l >>> 2) ^ r) & 0x33333333;
r ^= x; r ^= x;
l ^= (x << 2); l ^= (x << 2);
x = ((r >>> 16) ^ l) & 0x0000ffff; x = ((r >>> 16) ^ l) & 0x0000ffff;
l ^= x; l ^= x;
r ^= (x << 16); r ^= (x << 16);
x = ((r >>> 4) ^ l) & 0x0f0f0f0f; x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
l ^= x; l ^= x;
r ^= (x << 4); r ^= (x << 4);
// 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,765 +6,729 @@ ...@@ -6,765 +6,729 @@
* 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 Util.Debug(">> Display.constructor");
function 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 this._target === 'string') {
throw new Error('target must be a DOM element');
}
if (typeof conf.target === 'string') { if (!this._target.getContext) {
throw("target must be a DOM element"); throw new Error("no getContext method");
} }
c = conf.target; if (!this._drawCtx) {
this._drawCtx = this._target.getContext('2d');
}
if (! c.getContext) { throw("no getContext method"); } Util.Debug("User Agent: " + navigator.userAgent);
if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); }
if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); }
if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); }
if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); }
if (! c_ctx) { c_ctx = c.getContext('2d'); } this.clear();
Util.Debug("User Agent: " + navigator.userAgent); // Check canvas features
if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); } if ('createImageData' in this._drawCtx) {
if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); } this._render_mode = 'canvas rendering';
if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); } } else {
if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); } throw new Error("Canvas does not support createImageData");
}
that.clear(); if (this._prefer_js === null) {
Util.Info("Prefering javascript operations");
this._prefer_js = true;
}
// Check canvas features // Determine browser support for setting the cursor via data URI scheme
if ('createImageData' in c_ctx) { var curDat = [];
conf.render_mode = "canvas rendering"; for (var i = 0; i < 8 * 8 * 4; i++) {
} else { curDat.push(255);
throw("Canvas does not support createImageData"); }
} try {
if (conf.prefer_js === null) { var curSave = this._target.style.cursor;
Util.Info("Prefering javascript operations"); Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8);
conf.prefer_js = true; if (this._target.style.cursor) {
} if (this._cursor_uri === null) {
this._cursor_uri = true;
}
Util.Info("Data URI scheme cursor supported");
} else {
if (this._cursor_uri === null) {
this._cursor_uri = false;
}
Util.Warn("Data URI scheme cursor not supported");
}
this._target.style.cursor = curSave;
} catch (exc) {
Util.Error("Data URI scheme cursor test exception: " + exc);
this._cursor_uri = false;
}
// Initialize cached tile imageData Util.Debug("<< Display.constructor");
tile16x16 = c_ctx.createImageData(16, 16); };
/* Display.prototype = {
* Determine browser support for setting the cursor via data URI // Public methods
* scheme viewportChange: function (deltaX, deltaY, width, height) {
*/ var vp = this._viewportLoc;
curDat = []; var cr = this._cleanRect;
for (i=0; i < 8 * 8 * 4; i += 1) { var canvas = this._target;
curDat.push(255);
} if (!this._viewport) {
try { Util.Debug("Setting viewport to full display region");
curSave = c.style.cursor; deltaX = -vp.w; // clamped later of out of bounds
changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8); deltaY = -vp.h;
if (c.style.cursor) { width = this._fb_width;
if (conf.cursor_uri === null) { height = this._fb_height;
conf.cursor_uri = true;
} }
Util.Info("Data URI scheme cursor supported");
} else { if (typeof(deltaX) === "undefined") { deltaX = 0; }
if (conf.cursor_uri === null) { if (typeof(deltaY) === "undefined") { deltaY = 0; }
conf.cursor_uri = false; if (typeof(width) === "undefined") { width = vp.w; }
if (typeof(height) === "undefined") { height = vp.h; }
// Size change
if (width > this._fb_width) { width = this._fb_width; }
if (height > this._fb_height) { height = this._fb_height; }
if (vp.w !== width || vp.h !== height) {
// Change width
if (width < vp.w && cr.x2 > vp.x + width - 1) {
cr.x2 = vp.x + width - 1;
}
vp.w = width;
// Change height
if (height < vp.h && cr.y2 > vp.y + height - 1) {
cr.y2 = vp.y + height - 1;
}
vp.h = height;
var saveImg = null;
if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) {
var img_width = canvas.width < vp.w ? canvas.width : vp.w;
var img_height = canvas.height < vp.h ? canvas.height : vp.h;
saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height);
}
canvas.width = vp.w;
canvas.height = vp.h;
if (saveImg) {
this._drawCtx.putImageData(saveImg, 0, 0);
}
} }
Util.Warn("Data URI scheme cursor not supported");
}
c.style.cursor = curSave;
} catch (exc2) {
Util.Error("Data URI scheme cursor test exception: " + exc2);
conf.cursor_uri = false;
}
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
that.viewportChange = function(deltaX, deltaY, width, height) {
var c = conf.target, v = viewport, cr = cleanRect,
saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
if (!conf.viewport) {
Util.Debug("Setting viewport to full display region");
deltaX = -v.w; // Clamped later if out of bounds
deltaY = -v.h; // Clamped later if out of bounds
width = fb_width;
height = fb_height;
}
if (typeof(deltaX) === "undefined") { deltaX = 0; }
if (typeof(deltaY) === "undefined") { deltaY = 0; }
if (typeof(width) === "undefined") { width = v.w; }
if (typeof(height) === "undefined") { height = v.h; }
// Size change
if (width > fb_width) { width = fb_width; }
if (height > fb_height) { height = fb_height; }
if ((v.w !== width) || (v.h !== height)) {
// Change width
if ((width < v.w) && (cr.x2 > v.x + width -1)) {
cr.x2 = v.x + width - 1;
}
v.w = width;
// Change height var vx2 = vp.x + vp.w - 1;
if ((height < v.h) && (cr.y2 > v.y + height -1)) { var vy2 = vp.y + vp.h - 1;
cr.y2 = v.y + height - 1;
}
v.h = height;
// Position change
if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) { if (deltaX < 0 && vp.x + deltaX < 0) {
saveImg = c_ctx.getImageData(0, 0, deltaX = -vp.x;
(c.width < v.w) ? c.width : v.w, }
(c.height < v.h) ? c.height : v.h); if (vx2 + deltaX >= this._fb_width) {
} deltaX -= vx2 + deltaX - this._fb_width + 1;
}
c.width = v.w; if (vp.y + deltaY < 0) {
c.height = v.h; deltaY = -vp.y;
}
if (vy2 + deltaY >= this._fb_height) {
deltaY -= (vy2 + deltaY - this._fb_height + 1);
}
if (saveImg) { if (deltaX === 0 && deltaY === 0) {
c_ctx.putImageData(saveImg, 0, 0); return;
} }
} Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
vx2 = v.x + v.w - 1; vp.x += deltaX;
vy2 = v.y + v.h - 1; vx2 += deltaX;
vp.y += deltaY;
vy2 += deltaY;
// Position change
// Update the clean rectangle
if ((deltaX < 0) && ((v.x + deltaX) < 0)) { if (vp.x > cr.x1) {
deltaX = - v.x; cr.x1 = vp.x;
} }
if ((vx2 + deltaX) >= fb_width) { if (vx2 < cr.x2) {
deltaX -= ((vx2 + deltaX) - fb_width + 1); cr.x2 = vx2;
} }
if (vp.y > cr.y1) {
if ((v.y + deltaY) < 0) { cr.y1 = vp.y;
deltaY = - v.y; }
} if (vy2 < cr.y2) {
if ((vy2 + deltaY) >= fb_height) { cr.y2 = vy2;
deltaY -= ((vy2 + deltaY) - fb_height + 1); }
}
var x1, w;
if ((deltaX === 0) && (deltaY === 0)) { if (deltaX < 0) {
//Util.Debug("skipping viewport change"); // Shift viewport left, redraw left section
return; x1 = 0;
} w = -deltaX;
Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); } else {
// Shift viewport right, redraw right section
v.x += deltaX; x1 = vp.w - deltaX;
vx2 += deltaX; w = deltaX;
v.y += deltaY; }
vy2 += deltaY;
var y1, h;
// Update the clean rectangle if (deltaY < 0) {
if (v.x > cr.x1) { // Shift viewport up, redraw top section
cr.x1 = v.x; y1 = 0;
} h = -deltaY;
if (vx2 < cr.x2) { } else {
cr.x2 = vx2; // Shift viewport down, redraw bottom section
} y1 = vp.h - deltaY;
if (v.y > cr.y1) { h = deltaY;
cr.y1 = v.y; }
}
if (vy2 < cr.y2) { // Copy the valid part of the viewport to the shifted location
cr.y2 = vy2; var saveStyle = this._drawCtx.fillStyle;
} this._drawCtx.fillStyle = "rgb(255,255,255)";
if (deltaX !== 0) {
if (deltaX < 0) { this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h);
// Shift viewport left, redraw left section this._drawCtx.fillRect(x1, 0, w, vp.h);
x1 = 0; }
w = - deltaX; if (deltaY !== 0) {
} else { this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h);
// Shift viewport right, redraw right section this._drawCtx.fillRect(0, y1, vp.w, h);
x1 = v.w - deltaX; }
w = deltaX; this._drawCtx.fillStyle = saveStyle;
} },
if (deltaY < 0) {
// Shift viewport up, redraw top section // Return a map of clean and dirty areas of the viewport and reset the
y1 = 0; // tracking of clean and dirty areas
h = - deltaY; //
} else { // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h},
// Shift viewport down, redraw bottom section // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] }
y1 = v.h - deltaY; getCleanDirtyReset: function () {
h = deltaY; var vp = this._viewportLoc;
} var cr = this._cleanRect;
// Copy the valid part of the viewport to the shifted location var cleanBox = { 'x': cr.x1, 'y': cr.y1,
saveStyle = c_ctx.fillStyle; 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 };
c_ctx.fillStyle = "rgb(255,255,255)";
if (deltaX !== 0) { var dirtyBoxes = [];
//that.copyImage(0, 0, -deltaX, 0, v.w, v.h); if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) {
//that.fillRect(x1, 0, w, v.h, [255,255,255]); // Whole viewport is dirty
c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h); dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h });
c_ctx.fillRect(x1, 0, w, v.h); } else {
} // Redraw dirty regions
if (deltaY !== 0) { var vx2 = vp.x + vp.w - 1;
//that.copyImage(0, 0, 0, -deltaY, v.w, v.h); var vy2 = vp.y + vp.h - 1;
//that.fillRect(0, y1, v.w, h, [255,255,255]);
c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h); if (vp.x < cr.x1) {
c_ctx.fillRect(0, y1, v.w, h); // left side dirty region
} dirtyBoxes.push({'x': vp.x, 'y': vp.y,
c_ctx.fillStyle = saveStyle; 'w': cr.x1 - vp.x + 1, 'h': vp.h});
}; }
if (vx2 > cr.x2) {
// right side dirty region
// Return a map of clean and dirty areas of the viewport and reset the dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y,
// tracking of clean and dirty areas. 'w': vx2 - cr.x2, 'h': vp.h});
// }
// Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h}, if(vp.y < cr.y1) {
// 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]} // top/middle dirty region
that.getCleanDirtyReset = function() { dirtyBoxes.push({'x': cr.x1, 'y': vp.y,
var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [], 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y});
vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1; }
if (vy2 > cr.y2) {
// bottom/middle dirty region
// Copy the cleanRect dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1,
cleanBox = {'x': c.x1, 'y': c.y1, 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2});
'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 this._cleanRect = {'x1': vp.x, 'y1': vp.y,
dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h}); 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1};
} else {
// Redraw dirty regions return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
if (v.x < c.x1) { },
// left side dirty region
dirtyBoxes.push({'x': v.x, 'y': v.y, absX: function (x) {
'w': c.x1 - v.x + 1, 'h': v.h}); return x + this._viewportLoc.x;
} },
if (vx2 > c.x2) {
// right side dirty region absY: function (y) {
dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y, return y + this._viewportLoc.y;
'w': vx2 - c.x2, 'h': v.h}); },
}
if (v.y < c.y1) { resize: function (width, height) {
// top/middle dirty region this._prevDrawStyle = "";
dirtyBoxes.push({'x': c.x1, 'y': v.y,
'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y}); this._fb_width = width;
} this._fb_height = height;
if (vy2 > c.y2) {
// bottom/middle dirty region this._rescale(this._scale);
dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2}); this.viewportChange();
} },
}
clear: function () {
// Reset the cleanRect to the whole viewport if (this._logo) {
cleanRect = {'x1': v.x, 'y1': v.y, this.resize(this._logo.width, this._logo.height);
'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1}; this.blitStringImage(this._logo.data, 0, 0);
} else {
return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; this.resize(640, 20);
}; this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h);
}
// Translate viewport coordinates to absolute coordinates
that.absX = function(x) { this._renderQ = [];
return x + viewport.x; },
};
that.absY = function(y) { fillRect: function (x, y, width, height, color) {
return y + viewport.y; this._setFillColor(color);
}; this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height);
},
that.resize = function(width, height) { copyImage: function (old_x, old_y, new_x, new_y, w, h) {
c_prevStyle = ""; var x1 = old_x - this._viewportLoc.x;
var y1 = old_y - this._viewportLoc.y;
fb_width = width; var x2 = new_x - this._viewportLoc.x;
fb_height = height; var y2 = new_y - this._viewportLoc.y;
rescale(conf.scale); this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h);
that.viewportChange(); },
};
// start updating a tile
that.clear = function() { startTile: function (x, y, width, height, color) {
this._tile_x = x;
if (conf.logo) { this._tile_y = y;
that.resize(conf.logo.width, conf.logo.height); if (width === 16 && height === 16) {
that.blitStringImage(conf.logo.data, 0, 0); this._tile = this._tile16x16;
} else { } else {
that.resize(640, 20); this._tile = this._drawCtx.createImageData(width, height);
c_ctx.clearRect(0, 0, viewport.w, viewport.h); }
}
if (this._prefer_js) {
renderQ = []; var bgr;
if (this._true_color) {
// No benefit over default ("source-over") in Chrome and firefox bgr = color;
//c_ctx.globalCompositeOperation = "copy";
};
that.fillRect = function(x, y, width, height, color) {
setFillColor(color);
c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
};
that.copyImage = function(old_x, old_y, new_x, new_y, 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
that.startTile = function(x, y, width, height, color) {
var data, bgr, red, green, blue, i;
tile_x = x;
tile_y = y;
if ((width === 16) && (height === 16)) {
tile = tile16x16;
} else {
tile = c_ctx.createImageData(width, height);
}
data = tile.data;
if (conf.prefer_js) {
if (conf.true_color) {
bgr = color;
} else {
bgr = conf.colourMap[color[0]];
}
red = bgr[2];
green = bgr[1];
blue = bgr[0];
for (i = 0; i < (width * height * 4); i+=4) {
data[i ] = red;
data[i + 1] = green;
data[i + 2] = blue;
data[i + 3] = 255;
}
} else {
that.fillRect(x, y, width, height, color);
}
};
// Update sub-rectangle of the current tile
that.subTile = function(x, y, w, h, color) {
var data, p, bgr, red, green, blue, width, j, i, xend, yend;
if (conf.prefer_js) {
data = tile.data;
width = tile.width;
if (conf.true_color) {
bgr = color;
} else {
bgr = conf.colourMap[color[0]];
}
red = bgr[2];
green = bgr[1];
blue = bgr[0];
xend = x + w;
yend = y + h;
for (j = y; j < yend; j += 1) {
for (i = x; i < xend; i += 1) {
p = (i + (j * width) ) * 4;
data[p ] = red;
data[p + 1] = green;
data[p + 2] = blue;
data[p + 3] = 255;
}
}
} else {
that.fillRect(tile_x + x, tile_y + y, w, h, color);
}
};
// Draw the current tile to the screen
that.finishTile = function() {
if (conf.prefer_js) {
c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y);
}
// else: No-op, if not prefer_js then already done by setSubTile
};
rgbImageData = function(x, y, vx, vy, width, height, arr, offset) {
var img, i, j, data;
/*
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;
}
*/
img = c_ctx.createImageData(width, height);
data = img.data;
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) {
data[i ] = arr[j ];
data[i + 1] = arr[j + 1];
data[i + 2] = arr[j + 2];
data[i + 3] = 255; // Set Alpha
}
c_ctx.putImageData(img, x - vx, y - vy);
};
bgrxImageData = function(x, y, vx, vy, width, height, arr, offset) {
var img, i, j, data;
/*
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;
}
*/
img = c_ctx.createImageData(width, height);
data = img.data;
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
data[i ] = arr[j + 2];
data[i + 1] = arr[j + 1];
data[i + 2] = arr[j ];
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 (conf.true_color) {
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) {
if (conf.true_color) {
rgbImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
} else {
// prolly wrong...
cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
}
};
that.blitStringImage = function(str, x, y) {
var img = new Image();
img.onload = function () {
c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
};
img.src = str;
};
// Wrap ctx.drawImage but relative to viewport
that.drawImage = function(img, x, y) {
c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
};
that.renderQ_push = function(action) {
renderQ.push(action);
if (renderQ.length === 1) {
// If this can be rendered immediately it will be, otherwise
// the scanner will start polling the queue (every
// requestAnimationFrame interval)
scan_renderQ();
}
};
scan_renderQ = function() {
var a, ready = true;
while (ready && renderQ.length > 0) {
a = renderQ[0];
switch (a.type) {
case 'copy':
that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
break;
case 'fill':
that.fillRect(a.x, a.y, a.width, a.height, a.color);
break;
case 'blit':
that.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
break;
case 'blitRgb':
that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
break;
case 'img':
if (a.img.complete) {
that.drawImage(a.img, a.x, a.y);
} else { } else {
// We need to wait for this image to 'load' bgr = this._colourMap[color[0]];
// to keep things in-order
ready = false;
} }
break; var red = bgr[2];
} var green = bgr[1];
if (ready) { var blue = bgr[0];
a = renderQ.shift();
} var data = this._tile.data;
} for (var i = 0; i < width * height * 4; i += 4) {
if (renderQ.length > 0) { data[i] = red;
requestAnimFrame(scan_renderQ); data[i + 1] = green;
} data[i + 2] = blue;
}; data[i + 3] = 255;
}
} else {
this.fillRect(x, y, width, height, color);
}
},
// update sub-rectangle of the current tile
subTile: function (x, y, w, h, color) {
if (this._prefer_js) {
var bgr;
if (this._true_color) {
bgr = color;
} else {
bgr = this._colourMap[color[0]];
}
var red = bgr[2];
var green = bgr[1];
var blue = bgr[0];
var xend = x + w;
var yend = y + h;
var data = this._tile.data;
var width = this._tile.width;
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 + 2] = blue;
data[p + 3] = 255;
}
}
} else {
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
},
that.changeCursor = function(pixels, mask, hotx, hoty, w, h) { blitImage: function (x, y, width, height, arr, offset) {
if (conf.cursor_uri === false) { if (this._true_color) {
Util.Warn("changeCursor called but no cursor data URI support"); this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
return; } else {
} this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
}
},
if (conf.true_color) { blitRgbImage: function (x, y , width, height, arr, offset) {
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h); if (this._true_color) {
} else { this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap); } else {
} // probably wrong?
}; this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
}
},
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);
},
renderQ_push: function (action) {
this._renderQ.push(action);
if (this._renderQ.length === 1) {
// If this can be rendered immediately it will be, otherwise
// the scanner will start polling the queue (every
// requestAnimationFrame interval)
this._scan_renderQ();
}
},
that.defaultCursor = function() { changeCursor: function (pixels, mask, hotx, hoty, w, h) {
conf.target.style.cursor = "default"; if (this._cursor_uri === false) {
}; Util.Warn("changeCursor called but no cursor data URI support");
return;
}
return constructor(); // Return the public API interface if (this._true_color) {
Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
} else {
Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
}
},
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;
}
}
} // End of Display() if (transform_prop === null) {
Util.Debug("No scaling support");
return;
}
if (typeof(factor) === "undefined") {
factor = this._scale;
} else if (factor > 1.0) {
factor = 1.0;
} else if (factor < 0.1) {
factor = 0.1;
}
/* Set CSS cursor property using data URI encoded cursor file */ if (this._scale === factor) {
function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) { return;
"use strict"; }
var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
//Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0); this._scale = factor;
var x = canvas.width - (canvas.width * factor);
var w = w0; var y = canvas.height - (canvas.height * factor);
var h = h0; canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)';
if (h < w) },
h = w; // increase h to make it square
else
w = h; // increace w to make it square
// Push multi-byte little-endian values
cur.push16le = function (num) {
this.push((num ) & 0xFF,
(num >> 8) & 0xFF );
};
cur.push32le = function (num) {
this.push((num ) & 0xFF,
(num >> 8) & 0xFF,
(num >> 16) & 0xFF,
(num >> 24) & 0xFF );
};
IHDRsz = 40; _setFillColor: function (color) {
RGBsz = w * h * 4; var bgr;
XORsz = Math.ceil( (w * h) / 8.0 ); if (this._true_color) {
ANDsz = Math.ceil( (w * h) / 8.0 ); bgr = color;
// Main header
cur.push16le(0); // 0: Reserved
cur.push16le(2); // 2: .CUR type
cur.push16le(1); // 4: Number of images, 1 for non-animated ico
// Cursor #1 header (ICONDIRENTRY)
cur.push(w); // 6: width
cur.push(h); // 7: height
cur.push(0); // 8: colors, 0 -> true-color
cur.push(0); // 9: reserved
cur.push16le(hotx); // 10: hotspot x coordinate
cur.push16le(hoty); // 12: hotspot y coordinate
cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
// 14: cursor data byte size
cur.push32le(22); // 18: offset of cursor data in the file
// Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
cur.push32le(IHDRsz); // 22: Infoheader size
cur.push32le(w); // 26: Cursor width
cur.push32le(h*2); // 30: XOR+AND height
cur.push16le(1); // 34: number of planes
cur.push16le(32); // 36: bits per pixel
cur.push32le(0); // 38: Type of compression
cur.push32le(XORsz + ANDsz); // 43: Size of Image
// Gimp leaves this as 0
cur.push32le(0); // 46: reserved
cur.push32le(0); // 50: reserved
cur.push32le(0); // 54: reserved
cur.push32le(0); // 58: reserved
// 62: color data (RGBQUAD icColors[])
for (y = h-1; y >= 0; y -= 1) {
for (x = 0; x < w; x += 1) {
if (x >= w0 || y >= h0) {
cur.push(0); // blue
cur.push(0); // green
cur.push(0); // red
cur.push(0); // alpha
} else { } else {
idx = y * Math.ceil(w0 / 8) + Math.floor(x/8); bgr = this._colourMap[color[0]];
alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; }
if (cmap) {
idx = (w0 * y) + x; var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
rgb = cmap[pixels[idx]]; if (newStyle !== this._prevDrawStyle) {
cur.push(rgb[2]); // blue this._drawCtx.fillStyle = newStyle;
cur.push(rgb[1]); // green this._prevDrawStyle = newStyle;
cur.push(rgb[0]); // red }
cur.push(alpha); // alpha },
_rgbImageData: function (x, y, vx, vy, width, height, arr, offset) {
var img = this._drawCtx.createImageData(width, height);
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);
},
_bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) {
var img = this._drawCtx.createImageData(width, height);
var data = img.data;
for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
data[i] = arr[j + 2];
data[i + 1] = arr[j + 1];
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 () {
var ready = true;
while (ready && this._renderQ.length > 0) {
var a = this._renderQ[0];
switch (a.type) {
case 'copy':
this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
break;
case 'fill':
this.fillRect(a.x, a.y, a.width, a.height, a.color);
break;
case 'blit':
this.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
break;
case 'blitRgb':
this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
break;
case 'img':
if (a.img.complete) {
this.drawImage(a.img, a.x, a.y);
} else {
// We need to wait for this image to 'load'
// to keep things in-order
ready = false;
}
break;
}
if (ready) {
this._renderQ.shift();
}
}
if (this._renderQ.length > 0) {
requestAnimFrame(this._scan_renderQ.bind(this));
}
},
};
Util.make_properties(Display, [
['target', 'wo', 'dom'], // Canvas element for rendering
['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data}
['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 h = h0;
if (h < w) {
h = w; // increase h to make it square
} else {
w = h; // increase w to make it square
}
var cur = [];
// Push multi-byte little-endian values
cur.push16le = function (num) {
this.push(num & 0xFF, (num >> 8) & 0xFF);
};
cur.push32le = function (num) {
this.push(num & 0xFF,
(num >> 8) & 0xFF,
(num >> 16) & 0xFF,
(num >> 24) & 0xFF);
};
var IHDRsz = 40;
var RGBsz = w * h * 4;
var XORsz = Math.ceil((w * h) / 8.0);
var ANDsz = Math.ceil((w * h) / 8.0);
cur.push16le(0); // 0: Reserved
cur.push16le(2); // 2: .CUR type
cur.push16le(1); // 4: Number of images, 1 for non-animated ico
// Cursor #1 header (ICONDIRENTRY)
cur.push(w); // 6: width
cur.push(h); // 7: height
cur.push(0); // 8: colors, 0 -> true-color
cur.push(0); // 9: reserved
cur.push16le(hotx); // 10: hotspot x coordinate
cur.push16le(hoty); // 12: hotspot y coordinate
cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
// 14: cursor data byte size
cur.push32le(22); // 18: offset of cursor data in the file
// Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
cur.push32le(IHDRsz); // 22: InfoHeader size
cur.push32le(w); // 26: Cursor width
cur.push32le(h * 2); // 30: XOR+AND height
cur.push16le(1); // 34: number of planes
cur.push16le(32); // 36: bits per pixel
cur.push32le(0); // 38: Type of compression
cur.push32le(XORsz + ANDsz);
// 42: Size of Image
cur.push32le(0); // 46: reserved
cur.push32le(0); // 50: reserved
cur.push32le(0); // 54: reserved
cur.push32le(0); // 58: reserved
// 62: color data (RGBQUAD icColors[])
var y, x;
for (y = h - 1; y >= 0; y--) {
for (x = 0; x < w; x++) {
if (x >= w0 || y >= h0) {
cur.push(0); // blue
cur.push(0); // green
cur.push(0); // red
cur.push(0); // alpha
} else { } else {
idx = ((w0 * y) + x) * 4; var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
cur.push(pixels[idx + 2]); // blue var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
cur.push(pixels[idx + 1]); // green if (cmap) {
cur.push(pixels[idx ]); // red idx = (w0 * y) + x;
cur.push(alpha); // alpha var rgb = cmap[pixels[idx]];
cur.push(rgb[2]); // blue
cur.push(rgb[1]); // green
cur.push(rgb[0]); // 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,397 +5,384 @@ ...@@ -5,397 +5,384 @@
* 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 () {
"use strict";
//
// Keyboard event handler
//
Keyboard = function (defaults) {
this._keyDownList = []; // List of depressed keys
// (even if they are happy)
Util.set_defaults(this, defaults, {
'target': document,
'focused': true
});
// create the keyboard handler
this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(),
VerifyCharModifier( /* jshint newcap: false */
TrackKeyState(
EscapeModifiers(this._handleRfbEvent.bind(this))
)
)
); /* jshint newcap: true */
// keep these here so we can refer to them later
this._eventHandlers = {
'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');
}
},
// _handleKeyDown: function (e) {
// Keyboard event handler if (!this._focused) { return true; }
//
function Keyboard(defaults) { if (this._handler.keydown(e)) {
"use strict"; // Suppress bubbling/default actions
Util.stopEvent(e);
return false;
} else {
// Allow the event to bubble and become a keyPress event which
// will have the character code translated
return true;
}
},
var that = {}, // Public API methods _handleKeyPress: function (e) {
conf = {}, // Configuration attributes if (!this._focused) { return true; }
keyDownList = []; // List of depressed keys if (this._handler.keypress(e)) {
// (even if they are happy) // Suppress bubbling/default actions
Util.stopEvent(e);
return false;
} else {
// Allow the event to bubble and become a keyPress event which
// will have the character code translated
return true;
}
},
// Configuration attributes _handleKeyUp: function (e) {
Util.conf_defaults(conf, that, defaults, [ if (!this._focused) { return true; }
['target', 'wo', 'dom', document, 'DOM element that captures keyboard input'],
['focused', 'rw', 'bool', true, 'Capture and send key events'],
['onKeyPress', 'rw', 'func', null, 'Handler for key press/release'] if (this._handler.keyup(e)) {
]); // Suppress bubbling/default actions
Util.stopEvent(e);
return false;
} else {
// Allow the event to bubble and become a keyPress event which
// will have the character code translated
return true;
}
},
_allKeysUp: function () {
Util.Debug(">> Keyboard.allKeysUp");
this._handler.releaseAll();
Util.Debug("<< Keyboard.allKeysUp");
},
// Public methods
grab: function () {
//Util.Debug(">> Keyboard.grab");
var c = this._target;
Util.addEvent(c, 'keydown', this._eventHandlers.keydown);
Util.addEvent(c, 'keyup', this._eventHandlers.keyup);
Util.addEvent(c, 'keypress', this._eventHandlers.keypress);
// Release (key up) if window loses focus
Util.addEvent(window, 'blur', this._eventHandlers.blur);
//Util.Debug("<< Keyboard.grab");
},
ungrab: function () {
//Util.Debug(">> Keyboard.ungrab");
var c = this._target;
Util.removeEvent(c, 'keydown', this._eventHandlers.keydown);
Util.removeEvent(c, 'keyup', this._eventHandlers.keyup);
Util.removeEvent(c, 'keypress', this._eventHandlers.keypress);
Util.removeEvent(window, 'blur', this._eventHandlers.blur);
// Release (key up) all keys that are in a down state
this._allKeysUp();
// //Util.Debug(">> Keyboard.ungrab");
// Private functions },
//
sync: function (e) {
/////// setup this._handler.syncModifiers(e);
}
function onRfbEvent(evt) { };
if (conf.onKeyPress) {
Util.Debug("onKeyPress " + (evt.type == 'keydown' ? "down" : "up") Util.make_properties(Keyboard, [
+ ", keysym: " + evt.keysym.keysym + "(" + evt.keysym.keyname + ")"); ['target', 'wo', 'dom'], // DOM element that captures keyboard input
conf.onKeyPress(evt.keysym.keysym, evt.type == 'keydown'); ['focused', 'rw', 'bool'], // Capture and send key events
}
} ['onKeyPress', 'rw', 'func'] // Handler for key press/release
// create the keyboard handler
var k = KeyEventDecoder(kbdUtil.ModifierSync(),
VerifyCharModifier(
TrackKeyState(
EscapeModifiers(onRfbEvent)
)
)
);
function onKeyDown(e) {
if (! conf.focused) {
return true;
}
if (k.keydown(e)) {
// Suppress bubbling/default actions
Util.stopEvent(e);
return false;
} else {
// Allow the event to bubble and become a keyPress event which
// will have the character code translated
return true;
}
}
function onKeyPress(e) {
if (! conf.focused) {
return true;
}
if (k.keypress(e)) {
// Suppress bubbling/default actions
Util.stopEvent(e);
return false;
} else {
// Allow the event to bubble and become a keyPress event which
// will have the character code translated
return true;
}
}
function onKeyUp(e) {
if (! conf.focused) {
return true;
}
if (k.keyup(e)) {
// Suppress bubbling/default actions
Util.stopEvent(e);
return false;
} else {
// Allow the event to bubble and become a keyPress event which
// will have the character code translated
return true;
}
}
function onOther(e) {
k.syncModifiers(e);
}
function allKeysUp() {
Util.Debug(">> Keyboard.allKeysUp");
k.releaseAll();
Util.Debug("<< Keyboard.allKeysUp");
}
//
// Public API interface functions
//
that.grab = function() {
//Util.Debug(">> Keyboard.grab");
var c = conf.target;
Util.addEvent(c, 'keydown', onKeyDown);
Util.addEvent(c, 'keyup', onKeyUp);
Util.addEvent(c, 'keypress', onKeyPress);
// Release (key up) if window loses focus
Util.addEvent(window, 'blur', allKeysUp);
//Util.Debug("<< Keyboard.grab");
};
that.ungrab = function() {
//Util.Debug(">> Keyboard.ungrab");
var c = conf.target;
Util.removeEvent(c, 'keydown', onKeyDown);
Util.removeEvent(c, 'keyup', onKeyUp);
Util.removeEvent(c, 'keypress', onKeyPress);
Util.removeEvent(window, 'blur', allKeysUp);
// Release (key up) all keys that are in a down state
allKeysUp();
//Util.Debug(">> Keyboard.ungrab");
};
that.sync = function(e) {
k.syncModifiers(e);
}
return that; // Return the public API interface
} // End of Keyboard()
//
// Mouse event handler
//
function Mouse(defaults) {
"use strict";
var that = {}, // Public API methods
conf = {}, // Configuration attributes
mouseCaptured = false;
var doubleClickTimer = null,
lastTouchPos = null;
// Configuration attributes
Util.conf_defaults(conf, that, defaults, [
['target', 'ro', 'dom', document, 'DOM element that captures mouse input'],
['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'],
['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() { //
// capturing the mouse ensures we get the mouseup event // Mouse event handler
if (conf.target.setCapture) { //
conf.target.setCapture();
} Mouse = function (defaults) {
this._mouseCaptured = false;
// some browsers give us mouseup events regardless,
// so if we never captured the mouse, we can disregard the event this._doubleClickTimer = null;
mouseCaptured = true; this._lastTouchPos = null;
}
// Configuration attributes
function releaseMouse() { Util.set_defaults(this, defaults, {
if (conf.target.releaseCapture) { 'target': document,
conf.target.releaseCapture(); 'focused': true,
} 'scale': 1.0,
mouseCaptured = false; 'touchButton': 1
} });
//
// Private functions this._eventHandlers = {
// 'mousedown': this._handleMouseDown.bind(this),
'mouseup': this._handleMouseUp.bind(this),
function resetDoubleClickTimer() { 'mousemove': this._handleMouseMove.bind(this),
doubleClickTimer = null; 'mousewheel': this._handleMouseWheel.bind(this),
} 'mousedisable': this._handleMouseDisable.bind(this)
};
function onMouseButton(e, down) { };
var evt, pos, bmask;
if (! conf.focused) { Mouse.prototype = {
return true; // private methods
} _captureMouse: function () {
// capturing the mouse ensures we get the mouseup event
if (conf.notify) { if (this._target.setCapture) {
conf.notify(e); this._target.setCapture();
} }
evt = (e ? e : window.event); // some browsers give us mouseup events regardless,
pos = Util.getEventPosition(e, conf.target, conf.scale); // so if we never captured the mouse, we can disregard the event
this._mouseCaptured = true;
if (e.touches || e.changedTouches) { },
// Touch device
_releaseMouse: function () {
// When two touches occur within 500 ms of each other and are if (this._target.releaseCapture) {
// closer than 20 pixels together a double click is triggered. this._target.releaseCapture();
if (down == 1) { }
if (doubleClickTimer == null) { this._mouseCaptured = false;
lastTouchPos = pos; },
} else {
clearTimeout(doubleClickTimer); _resetDoubleClickTimer: function () {
this._doubleClickTimer = null;
},
// When the distance between the two touches is small enough _handleMouseButton: function (e, down) {
// force the position of the latter touch to the position of if (!this._focused) { return true; }
// the first.
var xs = lastTouchPos.x - pos.x; if (this._notify) {
var ys = lastTouchPos.y - pos.y; this._notify(e);
var d = Math.sqrt((xs * xs) + (ys * ys)); }
// The goal is to trigger on a certain physical width, the var evt = (e ? e : window.event);
// devicePixelRatio brings us a bit closer but is not optimal. var pos = Util.getEventPosition(e, this._target, this._scale);
if (d < 20 * window.devicePixelRatio) {
pos = lastTouchPos; var bmask;
if (e.touches || e.changedTouches) {
// Touch device
// When two touches occur within 500 ms of each other and are
// closer than 20 pixels together a double click is triggered.
if (down == 1) {
if (this._doubleClickTimer === null) {
this._lastTouchPos = pos;
} else {
clearTimeout(this._doubleClickTimer);
// When the distance between the two touches is small enough
// force the position of the latter touch to the position of
// the first.
var xs = this._lastTouchPos.x - pos.x;
var ys = this._lastTouchPos.y - pos.y;
var d = Math.sqrt((xs * xs) + (ys * ys));
// The goal is to trigger on a certain physical width, the
// devicePixelRatio brings us a bit closer but is not optimal.
if (d < 20 * window.devicePixelRatio) {
pos = this._lastTouchPos;
}
}
this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
} }
bmask = this._touchButton;
// If bmask is set
} else if (evt.which) {
/* everything except IE */
bmask = 1 << evt.button;
} else {
/* IE including 9 */
bmask = (evt.button & 0x1) + // Left
(evt.button & 0x2) * 2 + // Right
(evt.button & 0x4) / 2; // Middle
} }
doubleClickTimer = setTimeout(resetDoubleClickTimer, 500);
if (this._onMouseButton) {
Util.Debug("onMouseButton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
this._onMouseButton(pos.x, pos.y, down, bmask);
}
Util.stopEvent(e);
return false;
},
_handleMouseDown: function (e) {
this._captureMouse();
this._handleMouseButton(e, 1);
},
_handleMouseUp: function (e) {
if (!this._mouseCaptured) { return; }
this._handleMouseButton(e, 0);
this._releaseMouse();
},
_handleMouseWheel: function (e) {
if (!this._focused) { return true; }
if (this._notify) {
this._notify(e);
}
var evt = (e ? e : window.event);
var pos = Util.getEventPosition(e, this._target, this._scale);
var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
var bmask;
if (wheelData > 0) {
bmask = 1 << 3;
} else {
bmask = 1 << 4;
}
if (this._onMouseButton) {
this._onMouseButton(pos.x, pos.y, 1, bmask);
this._onMouseButton(pos.x, pos.y, 0, bmask);
}
Util.stopEvent(e);
return false;
},
_handleMouseMove: function (e) {
if (! this._focused) { return true; }
if (this._notify) {
this._notify(e);
}
var evt = (e ? e : window.event);
var pos = Util.getEventPosition(e, this._target, this._scale);
if (this._onMouseMove) {
this._onMouseMove(pos.x, pos.y);
}
Util.stopEvent(e);
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);
/* Stop propagation if inside canvas area */
if ((pos.realx >= 0) && (pos.realy >= 0) &&
(pos.realx < this._target.offsetWidth) &&
(pos.realy < this._target.offsetHeight)) {
//Util.Debug("mouse event disabled");
Util.stopEvent(e);
return false;
}
return true;
},
// Public methods
grab: function () {
var c = this._target;
if ('ontouchstart' in document.documentElement) {
Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown);
Util.addEvent(window, 'touchend', this._eventHandlers.mouseup);
Util.addEvent(c, 'touchend', this._eventHandlers.mouseup);
Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove);
} else {
Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown);
Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup);
Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup);
Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove);
Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
this._eventHandlers.mousewheel);
}
/* Work around right and middle click browser behaviors */
Util.addEvent(document, 'click', this._eventHandlers.mousedisable);
Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
},
ungrab: function () {
var c = this._target;
if ('ontouchstart' in document.documentElement) {
Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown);
Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup);
Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup);
Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove);
} else {
Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown);
Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup);
Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup);
Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove);
Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
this._eventHandlers.mousewheel);
}
/* Work around right and middle click browser behaviors */
Util.removeEvent(document, 'click', this._eventHandlers.mousedisable);
Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
} }
bmask = conf.touchButton; };
// If bmask is set
} else if (evt.which) { Util.make_properties(Mouse, [
/* everything except IE */ ['target', 'ro', 'dom'], // DOM element that captures mouse input
bmask = 1 << evt.button; ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received
} else { ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement
/* IE including 9 */ ['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0
bmask = (evt.button & 0x1) + // Left
(evt.button & 0x2) * 2 + // Right ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release
(evt.button & 0x4) / 2; // Middle ['onMouseMove', 'rw', 'func'], // Handler for mouse movement
} ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
//Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down + ]);
// " bmask: " + bmask + "(evt.button: " + evt.button + ")"); })();
if (conf.onMouseButton) {
Util.Debug("onMouseButton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
conf.onMouseButton(pos.x, pos.y, down, bmask);
}
Util.stopEvent(e);
return false;
}
function onMouseDown(e) {
captureMouse();
onMouseButton(e, 1);
}
function onMouseUp(e) {
if (!mouseCaptured) {
return;
}
onMouseButton(e, 0);
releaseMouse();
}
function onMouseWheel(e) {
var evt, pos, bmask, wheelData;
if (! conf.focused) {
return true;
}
if (conf.notify) {
conf.notify(e);
}
evt = (e ? e : window.event);
pos = Util.getEventPosition(e, conf.target, conf.scale);
wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
if (wheelData > 0) {
bmask = 1 << 3;
} else {
bmask = 1 << 4;
}
//Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y);
if (conf.onMouseButton) {
conf.onMouseButton(pos.x, pos.y, 1, bmask);
conf.onMouseButton(pos.x, pos.y, 0, bmask);
}
Util.stopEvent(e);
return false;
}
function onMouseMove(e) {
var evt, pos;
if (! conf.focused) {
return true;
}
if (conf.notify) {
conf.notify(e);
}
evt = (e ? e : window.event);
pos = Util.getEventPosition(e, conf.target, conf.scale);
//Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y);
if (conf.onMouseMove) {
conf.onMouseMove(pos.x, pos.y);
}
Util.stopEvent(e);
return false;
}
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 */
if ((pos.realx >= 0) && (pos.realy >= 0) &&
(pos.realx < conf.target.offsetWidth) &&
(pos.realy < conf.target.offsetHeight)) {
//Util.Debug("mouse event disabled");
Util.stopEvent(e);
return false;
}
//Util.Debug("mouse event not disabled");
return true;
}
//
// Public API interface functions
//
that.grab = function() {
//Util.Debug(">> Mouse.grab");
var c = conf.target;
if ('ontouchstart' in document.documentElement) {
Util.addEvent(c, 'touchstart', onMouseDown);
Util.addEvent(window, 'touchend', onMouseUp);
Util.addEvent(c, 'touchend', onMouseUp);
Util.addEvent(c, 'touchmove', onMouseMove);
} else {
Util.addEvent(c, 'mousedown', onMouseDown);
Util.addEvent(window, 'mouseup', onMouseUp);
Util.addEvent(c, 'mouseup', onMouseUp);
Util.addEvent(c, 'mousemove', onMouseMove);
Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
onMouseWheel);
}
/* Work around right and middle click browser behaviors */
Util.addEvent(document, 'click', onMouseDisable);
Util.addEvent(document.body, 'contextmenu', onMouseDisable);
//Util.Debug("<< Mouse.grab");
};
that.ungrab = function() {
//Util.Debug(">> Mouse.ungrab");
var c = conf.target;
if ('ontouchstart' in document.documentElement) {
Util.removeEvent(c, 'touchstart', onMouseDown);
Util.removeEvent(window, 'touchend', onMouseUp);
Util.removeEvent(c, 'touchend', onMouseUp);
Util.removeEvent(c, 'touchmove', onMouseMove);
} else {
Util.removeEvent(c, 'mousedown', onMouseDown);
Util.removeEvent(window, 'mouseup', onMouseUp);
Util.removeEvent(c, 'mouseup', onMouseUp);
Util.removeEvent(c, 'mousemove', onMouseMove);
Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
onMouseWheel);
}
/* Work around right and middle click browser behaviors */
Util.removeEvent(document, 'click', onMouseDisable);
Util.removeEvent(document.body, 'contextmenu', onMouseDisable);
//Util.Debug(">> Mouse.ungrab");
};
return that; // Return the public API interface
} // End of Mouse()
...@@ -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
var clone = (function(){
function Clone(){}
return function (obj) { Clone.prototype=obj; return new Clone(); };
}());
for (var key in item.keysyms) { for (var key in item.keysyms) {
var clone = (function(){
function Clone(){}
return function (obj) { Clone.prototype=obj; return new Clone(); };
}());
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,997 +7,973 @@ ...@@ -7,997 +7,973 @@
* 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",
"keysymdef.js", "keyboard.js", "input.js", "display.js", // Load supporting scripts
"jsunzip.js", "rfb.js", "keysym.js"]); window.onscriptsload = function () { UI.load(); };
window.onload = function () { UI.keyboardinputReset(); };
var UI = { Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js",
rfb_state : 'loaded', "jsunzip.js", "rfb.js", "keysym.js"]);
settingsOpen : false,
connSettingsOpen : false, var UI = {
popupStatusOpen : false,
clipboardOpen: false, rfb_state : 'loaded',
keyboardVisible: false, settingsOpen : false,
hideKeyboardTimeout: null, connSettingsOpen : false,
lastKeyboardinput: null, popupStatusOpen : false,
defaultKeyboardinputLen: 100, clipboardOpen: false,
extraKeysVisible: false, keyboardVisible: false,
ctrlOn: false, hideKeyboardTimeout: null,
altOn: false, lastKeyboardinput: null,
isTouchDevice: false, defaultKeyboardinputLen: 100,
extraKeysVisible: false,
// Setup rfb object, load settings from browser storage, then call ctrlOn: false,
// UI.init to setup the UI/menus altOn: false,
load: function (callback) { isTouchDevice: false,
WebUtil.initSettings(UI.start, callback);
}, // Setup rfb object, load settings from browser storage, then call
// UI.init to setup the UI/menus
// Render default UI and initialize settings menu load: function (callback) {
start: function(callback) { WebUtil.initSettings(UI.start, callback);
var html = '', i, sheet, sheets, llevels, port, autoconnect; },
UI.isTouchDevice = 'ontouchstart' in document.documentElement; // Render default UI and initialize settings menu
start: function(callback) {
// Stylesheet selection dropdown UI.isTouchDevice = 'ontouchstart' in document.documentElement;
sheet = WebUtil.selectStylesheet();
sheets = WebUtil.getStylesheets(); // Stylesheet selection dropdown
for (i = 0; i < sheets.length; i += 1) { var sheet = WebUtil.selectStylesheet();
UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); var sheets = WebUtil.getStylesheets();
} var i;
for (i = 0; i < sheets.length; i += 1) {
// Logging selection dropdown UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title);
llevels = ['error', 'warn', 'info', 'debug']; }
for (i = 0; i < llevels.length; i += 1) {
UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); // Logging selection dropdown
} var llevels = ['error', 'warn', 'info', 'debug'];
for (i = 0; i < llevels.length; i += 1) {
// Settings with immediate effects UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]);
UI.initSetting('logging', 'warn'); }
WebUtil.init_logging(UI.getSetting('logging'));
// Settings with immediate effects
UI.initSetting('stylesheet', 'default'); UI.initSetting('logging', 'warn');
WebUtil.selectStylesheet(null); WebUtil.init_logging(UI.getSetting('logging'));
// call twice to get around webkit bug
WebUtil.selectStylesheet(UI.getSetting('stylesheet')); UI.initSetting('stylesheet', 'default');
WebUtil.selectStylesheet(null);
// if port == 80 (or 443) then it won't be present and should be // call twice to get around webkit bug
// set manually WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
port = window.location.port;
if (!port) { // if port == 80 (or 443) then it won't be present and should be
if (window.location.protocol.substring(0,5) == 'https') { // set manually
port = 443; var port = window.location.port;
} if (!port) {
else if (window.location.protocol.substring(0,4) == 'http') { if (window.location.protocol.substring(0,5) == 'https') {
port = 80; port = 443;
} }
} else if (window.location.protocol.substring(0,4) == 'http') {
port = 80;
/* Populate the controls if defaults are provided in the URL */ }
UI.initSetting('host', window.location.hostname); }
UI.initSetting('port', port);
UI.initSetting('password', ''); /* Populate the controls if defaults are provided in the URL */
UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('host', window.location.hostname);
UI.initSetting('true_color', true); UI.initSetting('port', port);
UI.initSetting('cursor', !UI.isTouchDevice); UI.initSetting('password', '');
UI.initSetting('shared', true); UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('view_only', false); UI.initSetting('true_color', true);
UI.initSetting('path', 'websockify'); UI.initSetting('cursor', !UI.isTouchDevice);
UI.initSetting('repeaterID', ''); UI.initSetting('shared', true);
UI.initSetting('view_only', false);
UI.rfb = RFB({'target': $D('noVNC_canvas'), UI.initSetting('path', 'websockify');
'onUpdateState': UI.updateState, UI.initSetting('repeaterID', '');
'onXvpInit': UI.updateXvpVisualState,
'onClipboard': UI.clipReceive, UI.rfb = new RFB({'target': $D('noVNC_canvas'),
'onDesktopName': UI.updateDocumentTitle}); 'onUpdateState': UI.updateState,
'onXvpInit': UI.updateXvpVisualState,
autoconnect = WebUtil.getQueryVar('autoconnect', false); 'onClipboard': UI.clipReceive,
if (autoconnect === 'true' || autoconnect == '1') { 'onDesktopName': UI.updateDocumentTitle});
autoconnect = true;
UI.connect(); var autoconnect = WebUtil.getQueryVar('autoconnect', false);
} else { if (autoconnect === 'true' || autoconnect == '1') {
autoconnect = false; autoconnect = true;
} UI.connect();
} else {
UI.updateVisualState(); autoconnect = false;
}
// Unfocus clipboard when over the VNC area
//$D('VNC_screen').onmousemove = function () { UI.updateVisualState();
// var keyboard = UI.rfb.get_keyboard();
// if ((! keyboard) || (! keyboard.get_focused())) { // Show mouse selector buttons on touch screen devices
// $D('VNC_clipboard_text').blur(); if (UI.isTouchDevice) {
// } // Show mobile buttons
// }; $D('noVNC_mobile_buttons').style.display = "inline";
UI.setMouseButton();
// Show mouse selector buttons on touch screen devices // Remove the address bar
if (UI.isTouchDevice) { setTimeout(function() { window.scrollTo(0, 1); }, 100);
// Show mobile buttons UI.forceSetting('clip', true);
$D('noVNC_mobile_buttons').style.display = "inline"; $D('noVNC_clip').disabled = true;
UI.setMouseButton(); } else {
// Remove the address bar UI.initSetting('clip', false);
setTimeout(function() { window.scrollTo(0, 1); }, 100); }
UI.forceSetting('clip', true);
$D('noVNC_clip').disabled = true; //iOS Safari does not support CSS position:fixed.
} else { //This detects iOS devices and enables javascript workaround.
UI.initSetting('clip', false); if ((navigator.userAgent.match(/iPhone/i)) ||
} (navigator.userAgent.match(/iPod/i)) ||
(navigator.userAgent.match(/iPad/i))) {
//iOS Safari does not support CSS position:fixed. //UI.setOnscroll();
//This detects iOS devices and enables javascript workaround. //UI.setResize();
if ((navigator.userAgent.match(/iPhone/i)) || }
(navigator.userAgent.match(/iPod/i)) || UI.setBarPosition();
(navigator.userAgent.match(/iPad/i))) {
//UI.setOnscroll(); $D('noVNC_host').focus();
//UI.setResize();
} UI.setViewClip();
UI.setBarPosition(); Util.addEvent(window, 'resize', UI.setViewClip);
$D('noVNC_host').focus(); Util.addEvent(window, 'beforeunload', function () {
if (UI.rfb_state === 'normal') {
UI.setViewClip(); return "You are currently connected.";
Util.addEvent(window, 'resize', UI.setViewClip); }
} );
Util.addEvent(window, 'beforeunload', function () {
if (UI.rfb_state === 'normal') { // Show description by default when hosted at for kanaka.github.com
return "You are currently connected."; if (location.host === "kanaka.github.io") {
} // Open the description dialog
} ); $D('noVNC_description').style.display = "block";
} else {
// Show description by default when hosted at for kanaka.github.com // Show the connect panel on first load unless autoconnecting
if (location.host === "kanaka.github.io") { if (autoconnect === UI.connSettingsOpen) {
// Open the description dialog UI.toggleConnectPanel();
$D('noVNC_description').style.display = "block"; }
} else { }
// Show the connect panel on first load unless autoconnecting
if (autoconnect === UI.connSettingsOpen) { // Add mouse event click/focus/blur event handlers to the UI
UI.addMouseHandlers();
if (typeof callback === "function") {
callback(UI.rfb);
}
},
addMouseHandlers: function() {
// Setup interface handlers that can't be inline
$D("noVNC_view_drag_button").onclick = UI.setViewDrag;
$D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
$D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); };
$D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); };
$D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); };
$D("showKeyboard").onclick = UI.showKeyboard;
$D("keyboardinput").oninput = UI.keyInput;
$D("keyboardinput").onblur = UI.keyInputBlur;
$D("showExtraKeysButton").onclick = UI.showExtraKeys;
$D("toggleCtrlButton").onclick = UI.toggleCtrl;
$D("toggleAltButton").onclick = UI.toggleAlt;
$D("sendTabButton").onclick = UI.sendTab;
$D("sendEscButton").onclick = UI.sendEsc;
$D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel;
$D("xvpShutdownButton").onclick = UI.xvpShutdown;
$D("xvpRebootButton").onclick = UI.xvpReboot;
$D("xvpResetButton").onclick = UI.xvpReset;
$D("noVNC_status").onclick = UI.togglePopupStatusPanel;
$D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel;
$D("xvpButton").onclick = UI.toggleXvpPanel;
$D("clipboardButton").onclick = UI.toggleClipboardPanel;
$D("settingsButton").onclick = UI.toggleSettingsPanel;
$D("connectButton").onclick = UI.toggleConnectPanel;
$D("disconnectButton").onclick = UI.disconnect;
$D("descriptionButton").onclick = UI.toggleConnectPanel;
$D("noVNC_clipboard_text").onfocus = UI.displayBlur;
$D("noVNC_clipboard_text").onblur = UI.displayFocus;
$D("noVNC_clipboard_text").onchange = UI.clipSend;
$D("noVNC_clipboard_clear_button").onclick = UI.clipClear;
$D("noVNC_settings_menu").onmouseover = UI.displayBlur;
$D("noVNC_settings_menu").onmouseover = UI.displayFocus;
$D("noVNC_apply").onclick = UI.settingsApply;
$D("noVNC_connect_button").onclick = UI.connect;
},
// Read form control compatible setting from cookie
getSetting: function(name) {
var ctrl = $D('noVNC_' + name);
var val = WebUtil.readSetting(name);
if (val !== null && ctrl.type === 'checkbox') {
if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
val = false;
} else {
val = true;
}
}
return val;
},
// Update cookie and form control setting. If value is not set, then
// updates from control to current cookie setting.
updateSetting: function(name, value) {
// Save the cookie for this session
if (typeof value !== 'undefined') {
WebUtil.writeSetting(name, value);
}
// Update the settings control
value = UI.getSetting(name);
var ctrl = $D('noVNC_' + name);
if (ctrl.type === 'checkbox') {
ctrl.checked = value;
} else if (typeof ctrl.options !== 'undefined') {
for (var i = 0; i < ctrl.options.length; i += 1) {
if (ctrl.options[i].value === value) {
ctrl.selectedIndex = i;
break;
}
}
} else {
/*Weird IE9 error leads to 'null' appearring
in textboxes instead of ''.*/
if (value === null) {
value = "";
}
ctrl.value = value;
}
},
// Save control setting to cookie
saveSetting: function(name) {
var val, ctrl = $D('noVNC_' + name);
if (ctrl.type === 'checkbox') {
val = ctrl.checked;
} else if (typeof ctrl.options !== 'undefined') {
val = ctrl.options[ctrl.selectedIndex].value;
} else {
val = ctrl.value;
}
WebUtil.writeSetting(name, val);
//Util.Debug("Setting saved '" + name + "=" + val + "'");
return val;
},
// Initial page load read/initialization of settings
initSetting: function(name, defVal) {
// Check Query string followed by cookie
var val = WebUtil.getQueryVar(name);
if (val === null) {
val = WebUtil.readSetting(name, defVal);
}
UI.updateSetting(name, val);
return val;
},
// Force a setting to be a certain value
forceSetting: function(name, val) {
UI.updateSetting(name, val);
return val;
},
// Show the popup status panel
togglePopupStatusPanel: function() {
var psp = $D('noVNC_popup_status_panel');
if (UI.popupStatusOpen === true) {
psp.style.display = "none";
UI.popupStatusOpen = false;
} else {
psp.innerHTML = $D('noVNC_status').innerHTML;
psp.style.display = "block";
psp.style.left = window.innerWidth/2 -
parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px";
UI.popupStatusOpen = true;
}
},
// Show the XVP panel
toggleXvpPanel: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
// Close settings if open
if (UI.settingsOpen === true) {
UI.settingsApply();
UI.closeSettingsMenu();
}
// Close connection settings if open
if (UI.connSettingsOpen === true) {
UI.toggleConnectPanel();
}
// Close popup status panel if open
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel();
}
// Close clipboard panel if open
if (UI.clipboardOpen === true) {
UI.toggleClipboardPanel();
}
// Toggle XVP panel
if (UI.xvpOpen === true) {
$D('noVNC_xvp').style.display = "none";
$D('xvpButton').className = "noVNC_status_button";
UI.xvpOpen = false;
} else {
$D('noVNC_xvp').style.display = "block";
$D('xvpButton').className = "noVNC_status_button_selected";
UI.xvpOpen = true;
}
},
// Show the clipboard panel
toggleClipboardPanel: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
// Close settings if open
if (UI.settingsOpen === true) {
UI.settingsApply();
UI.closeSettingsMenu();
}
// Close connection settings if open
if (UI.connSettingsOpen === true) {
UI.toggleConnectPanel();
}
// Close popup status panel if open
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel();
}
// Close XVP panel if open
if (UI.xvpOpen === true) {
UI.toggleXvpPanel();
}
// Toggle Clipboard Panel
if (UI.clipboardOpen === true) {
$D('noVNC_clipboard').style.display = "none";
$D('clipboardButton').className = "noVNC_status_button";
UI.clipboardOpen = false;
} else {
$D('noVNC_clipboard').style.display = "block";
$D('clipboardButton').className = "noVNC_status_button_selected";
UI.clipboardOpen = true;
}
},
// Show the connection settings panel/menu
toggleConnectPanel: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
// Close connection settings if open
if (UI.settingsOpen === true) {
UI.settingsApply();
UI.closeSettingsMenu();
$D('connectButton').className = "noVNC_status_button";
}
// Close clipboard panel if open
if (UI.clipboardOpen === true) {
UI.toggleClipboardPanel();
}
// Close popup status panel if open
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel();
}
// Close XVP panel if open
if (UI.xvpOpen === true) {
UI.toggleXvpPanel();
}
// Toggle Connection Panel
if (UI.connSettingsOpen === true) {
$D('noVNC_controls').style.display = "none";
$D('connectButton').className = "noVNC_status_button";
UI.connSettingsOpen = false;
UI.saveSetting('host');
UI.saveSetting('port');
//UI.saveSetting('password');
} else {
$D('noVNC_controls').style.display = "block";
$D('connectButton').className = "noVNC_status_button_selected";
UI.connSettingsOpen = true;
$D('noVNC_host').focus();
}
},
// Toggle the settings menu:
// On open, settings are refreshed from saved cookies.
// On close, settings are applied
toggleSettingsPanel: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
if (UI.settingsOpen) {
UI.settingsApply();
UI.closeSettingsMenu();
} else {
UI.updateSetting('encrypt');
UI.updateSetting('true_color');
if (UI.rfb.get_display().get_cursor_uri()) {
UI.updateSetting('cursor');
} else {
UI.updateSetting('cursor', !UI.isTouchDevice);
$D('noVNC_cursor').disabled = true;
}
UI.updateSetting('clip');
UI.updateSetting('shared');
UI.updateSetting('view_only');
UI.updateSetting('path');
UI.updateSetting('repeaterID');
UI.updateSetting('stylesheet');
UI.updateSetting('logging');
UI.openSettingsMenu();
}
},
// Open menu
openSettingsMenu: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
// Close clipboard panel if open
if (UI.clipboardOpen === true) {
UI.toggleClipboardPanel();
}
// Close connection settings if open
if (UI.connSettingsOpen === true) {
UI.toggleConnectPanel();
}
// Close popup status panel if open
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel();
}
// Close XVP panel if open
if (UI.xvpOpen === true) {
UI.toggleXvpPanel();
}
$D('noVNC_settings').style.display = "block";
$D('settingsButton').className = "noVNC_status_button_selected";
UI.settingsOpen = true;
},
// Close menu (without applying settings)
closeSettingsMenu: function() {
$D('noVNC_settings').style.display = "none";
$D('settingsButton').className = "noVNC_status_button";
UI.settingsOpen = false;
},
// Save/apply settings when 'Apply' button is pressed
settingsApply: function() {
//Util.Debug(">> settingsApply");
UI.saveSetting('encrypt');
UI.saveSetting('true_color');
if (UI.rfb.get_display().get_cursor_uri()) {
UI.saveSetting('cursor');
}
UI.saveSetting('clip');
UI.saveSetting('shared');
UI.saveSetting('view_only');
UI.saveSetting('path');
UI.saveSetting('repeaterID');
UI.saveSetting('stylesheet');
UI.saveSetting('logging');
// Settings with immediate (non-connected related) effect
WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
WebUtil.init_logging(UI.getSetting('logging'));
UI.setViewClip();
UI.setViewDrag(UI.rfb.get_viewportDrag());
//Util.Debug("<< settingsApply");
},
setPassword: function() {
UI.rfb.sendPassword($D('noVNC_password').value);
//Reset connect button.
$D('noVNC_connect_button').value = "Connect";
$D('noVNC_connect_button').onclick = UI.Connect;
//Hide connection panel.
UI.toggleConnectPanel(); UI.toggleConnectPanel();
} return false;
} },
// Add mouse event click/focus/blur event handlers to the UI
UI.addMouseHandlers();
if (typeof callback === "function") {
callback(UI.rfb);
}
},
addMouseHandlers: function() {
// Setup interface handlers that can't be inline
$D("noVNC_view_drag_button").onclick = UI.setViewDrag;
$D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
$D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); };
$D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); };
$D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); };
$D("showKeyboard").onclick = UI.showKeyboard;
$D("keyboardinput").oninput = UI.keyInput;
$D("keyboardinput").onblur = UI.keyInputBlur;
$D("showExtraKeysButton").onclick = UI.showExtraKeys;
$D("toggleCtrlButton").onclick = UI.toggleCtrl;
$D("toggleAltButton").onclick = UI.toggleAlt;
$D("sendTabButton").onclick = UI.sendTab;
$D("sendEscButton").onclick = UI.sendEsc;
$D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel;
$D("xvpShutdownButton").onclick = UI.xvpShutdown;
$D("xvpRebootButton").onclick = UI.xvpReboot;
$D("xvpResetButton").onclick = UI.xvpReset;
$D("noVNC_status").onclick = UI.togglePopupStatusPanel;
$D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel;
$D("xvpButton").onclick = UI.toggleXvpPanel;
$D("clipboardButton").onclick = UI.toggleClipboardPanel;
$D("settingsButton").onclick = UI.toggleSettingsPanel;
$D("connectButton").onclick = UI.toggleConnectPanel;
$D("disconnectButton").onclick = UI.disconnect;
$D("descriptionButton").onclick = UI.toggleConnectPanel;
$D("noVNC_clipboard_text").onfocus = UI.displayBlur;
$D("noVNC_clipboard_text").onblur = UI.displayFocus;
$D("noVNC_clipboard_text").onchange = UI.clipSend;
$D("noVNC_clipboard_clear_button").onclick = UI.clipClear;
$D("noVNC_settings_menu").onmouseover = UI.displayBlur;
$D("noVNC_settings_menu").onmouseover = UI.displayFocus;
$D("noVNC_apply").onclick = UI.settingsApply;
$D("noVNC_connect_button").onclick = UI.connect;
},
// Read form control compatible setting from cookie
getSetting: function(name) {
var val, ctrl = $D('noVNC_' + name);
val = WebUtil.readSetting(name);
if (val !== null && ctrl.type === 'checkbox') {
if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
val = false;
} else {
val = true;
}
}
return val;
},
// Update cookie and form control setting. If value is not set, then sendCtrlAltDel: function() {
// updates from control to current cookie setting. UI.rfb.sendCtrlAltDel();
updateSetting: function(name, value) { },
var i, ctrl = $D('noVNC_' + name); xvpShutdown: function() {
// Save the cookie for this session UI.rfb.xvpShutdown();
if (typeof value !== 'undefined') { },
WebUtil.writeSetting(name, value);
}
// Update the settings control xvpReboot: function() {
value = UI.getSetting(name); UI.rfb.xvpReboot();
},
if (ctrl.type === 'checkbox') { xvpReset: function() {
ctrl.checked = value; UI.rfb.xvpReset();
},
} else if (typeof ctrl.options !== 'undefined') { setMouseButton: function(num) {
for (i = 0; i < ctrl.options.length; i += 1) { if (typeof num === 'undefined') {
if (ctrl.options[i].value === value) { // Disable mouse buttons
ctrl.selectedIndex = i; num = -1;
break;
} }
} if (UI.rfb) {
} else { UI.rfb.get_mouse().set_touchButton(num);
/*Weird IE9 error leads to 'null' appearring }
in textboxes instead of ''.*/
if (value === null) { var blist = [0, 1,2,4];
value = ""; for (var b = 0; b < blist.length; b++) {
} var button = $D('noVNC_mouse_button' + blist[b]);
ctrl.value = value; if (blist[b] === num) {
} button.style.display = "";
}, } else {
button.style.display = "none";
// Save control setting to cookie }
saveSetting: function(name) { }
var val, ctrl = $D('noVNC_' + name); },
if (ctrl.type === 'checkbox') {
val = ctrl.checked; updateState: function(rfb, state, oldstate, msg) {
} else if (typeof ctrl.options !== 'undefined') { UI.rfb_state = state;
val = ctrl.options[ctrl.selectedIndex].value; var klass;
} else { switch (state) {
val = ctrl.value; case 'failed':
} case 'fatal':
WebUtil.writeSetting(name, val); klass = "noVNC_status_error";
//Util.Debug("Setting saved '" + name + "=" + val + "'"); break;
return val; case 'normal':
}, klass = "noVNC_status_normal";
break;
// Initial page load read/initialization of settings case 'disconnected':
initSetting: function(name, defVal) { $D('noVNC_logo').style.display = "block";
var val; /* falls through */
case 'loaded':
// Check Query string followed by cookie klass = "noVNC_status_normal";
val = WebUtil.getQueryVar(name); break;
if (val === null) { case 'password':
val = WebUtil.readSetting(name, defVal); UI.toggleConnectPanel();
}
UI.updateSetting(name, val); $D('noVNC_connect_button').value = "Send Password";
//Util.Debug("Setting '" + name + "' initialized to '" + val + "'"); $D('noVNC_connect_button').onclick = UI.setPassword;
return val; $D('noVNC_password').focus();
},
klass = "noVNC_status_warn";
// Force a setting to be a certain value break;
forceSetting: function(name, val) { default:
UI.updateSetting(name, val); klass = "noVNC_status_warn";
return val; break;
}, }
if (typeof(msg) !== 'undefined') {
// Show the popup status panel $D('noVNC-control-bar').setAttribute("class", klass);
togglePopupStatusPanel: function() { $D('noVNC_status').innerHTML = msg;
var psp = $D('noVNC_popup_status_panel'); }
if (UI.popupStatusOpen === true) {
psp.style.display = "none"; UI.updateVisualState();
UI.popupStatusOpen = false; },
} else {
psp.innerHTML = $D('noVNC_status').innerHTML; // Disable/enable controls depending on connection state
psp.style.display = "block"; updateVisualState: function() {
psp.style.left = window.innerWidth/2 - var connected = UI.rfb_state === 'normal' ? true : false;
parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px";
UI.popupStatusOpen = true; //Util.Debug(">> updateVisualState");
} $D('noVNC_encrypt').disabled = connected;
}, $D('noVNC_true_color').disabled = connected;
if (UI.rfb && UI.rfb.get_display() &&
// Show the XVP panel UI.rfb.get_display().get_cursor_uri()) {
toggleXvpPanel: function() { $D('noVNC_cursor').disabled = connected;
// Close the description panel } else {
$D('noVNC_description').style.display = "none"; UI.updateSetting('cursor', !UI.isTouchDevice);
// Close settings if open $D('noVNC_cursor').disabled = true;
if (UI.settingsOpen === true) { }
UI.settingsApply(); $D('noVNC_shared').disabled = connected;
UI.closeSettingsMenu(); $D('noVNC_view_only').disabled = connected;
} $D('noVNC_path').disabled = connected;
// Close connection settings if open $D('noVNC_repeaterID').disabled = connected;
if (UI.connSettingsOpen === true) {
UI.toggleConnectPanel(); if (connected) {
} UI.setViewClip();
// Close popup status panel if open UI.setMouseButton(1);
if (UI.popupStatusOpen === true) { $D('clipboardButton').style.display = "inline";
UI.togglePopupStatusPanel(); $D('showKeyboard').style.display = "inline";
} $D('noVNC_extra_keys').style.display = "";
// Close clipboard panel if open $D('sendCtrlAltDelButton').style.display = "inline";
if (UI.clipboardOpen === true) { } else {
UI.toggleClipboardPanel(); UI.setMouseButton();
} $D('clipboardButton').style.display = "none";
// Toggle XVP panel $D('showKeyboard').style.display = "none";
if (UI.xvpOpen === true) { $D('noVNC_extra_keys').style.display = "none";
$D('noVNC_xvp').style.display = "none"; $D('sendCtrlAltDelButton').style.display = "none";
$D('xvpButton').className = "noVNC_status_button"; UI.updateXvpVisualState(0);
UI.xvpOpen = false; }
} else {
$D('noVNC_xvp').style.display = "block"; // State change disables viewport dragging.
$D('xvpButton').className = "noVNC_status_button_selected"; // It is enabled (toggled) by direct click on the button
UI.xvpOpen = true; UI.setViewDrag(false);
}
}, switch (UI.rfb_state) {
case 'fatal':
// Show the clipboard panel case 'failed':
toggleClipboardPanel: function() { case 'loaded':
// Close the description panel case 'disconnected':
$D('noVNC_description').style.display = "none"; $D('connectButton').style.display = "";
// Close settings if open $D('disconnectButton').style.display = "none";
if (UI.settingsOpen === true) { break;
UI.settingsApply(); default:
UI.closeSettingsMenu(); $D('connectButton').style.display = "none";
} $D('disconnectButton').style.display = "";
// Close connection settings if open break;
if (UI.connSettingsOpen === true) { }
UI.toggleConnectPanel();
} //Util.Debug("<< updateVisualState");
// Close popup status panel if open },
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel(); // Disable/enable XVP button
} updateXvpVisualState: function(ver) {
// Close XVP panel if open if (ver >= 1) {
if (UI.xvpOpen === true) { $D('xvpButton').style.display = 'inline';
UI.toggleXvpPanel(); } else {
} $D('xvpButton').style.display = 'none';
// Toggle Clipboard Panel // Close XVP panel if open
if (UI.clipboardOpen === true) { if (UI.xvpOpen === true) {
$D('noVNC_clipboard').style.display = "none"; UI.toggleXvpPanel();
$D('clipboardButton').className = "noVNC_status_button"; }
UI.clipboardOpen = false; }
} else { },
$D('noVNC_clipboard').style.display = "block";
$D('clipboardButton').className = "noVNC_status_button_selected"; // Display the desktop name in the document title
UI.clipboardOpen = true; updateDocumentTitle: function(rfb, name) {
} document.title = name + " - noVNC";
}, },
// Show the connection settings panel/menu clipReceive: function(rfb, text) {
toggleConnectPanel: function() { Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
// Close the description panel $D('noVNC_clipboard_text').value = text;
$D('noVNC_description').style.display = "none"; Util.Debug("<< UI.clipReceive");
// Close connection settings if open },
if (UI.settingsOpen === true) {
UI.settingsApply(); connect: function() {
UI.closeSettingsMenu(); UI.closeSettingsMenu();
$D('connectButton').className = "noVNC_status_button";
}
// Close clipboard panel if open
if (UI.clipboardOpen === true) {
UI.toggleClipboardPanel();
}
// Close popup status panel if open
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel();
}
// Close XVP panel if open
if (UI.xvpOpen === true) {
UI.toggleXvpPanel();
}
// Toggle Connection Panel
if (UI.connSettingsOpen === true) {
$D('noVNC_controls').style.display = "none";
$D('connectButton').className = "noVNC_status_button";
UI.connSettingsOpen = false;
UI.saveSetting('host');
UI.saveSetting('port');
//UI.saveSetting('password');
} else {
$D('noVNC_controls').style.display = "block";
$D('connectButton').className = "noVNC_status_button_selected";
UI.connSettingsOpen = true;
$D('noVNC_host').focus();
}
},
// Toggle the settings menu:
// On open, settings are refreshed from saved cookies.
// On close, settings are applied
toggleSettingsPanel: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
if (UI.settingsOpen) {
UI.settingsApply();
UI.closeSettingsMenu();
} else {
UI.updateSetting('encrypt');
UI.updateSetting('true_color');
if (UI.rfb.get_display().get_cursor_uri()) {
UI.updateSetting('cursor');
} else {
UI.updateSetting('cursor', !UI.isTouchDevice);
$D('noVNC_cursor').disabled = true;
}
UI.updateSetting('clip');
UI.updateSetting('shared');
UI.updateSetting('view_only');
UI.updateSetting('path');
UI.updateSetting('repeaterID');
UI.updateSetting('stylesheet');
UI.updateSetting('logging');
UI.openSettingsMenu();
}
},
// Open menu
openSettingsMenu: function() {
// Close the description panel
$D('noVNC_description').style.display = "none";
// Close clipboard panel if open
if (UI.clipboardOpen === true) {
UI.toggleClipboardPanel();
}
// Close connection settings if open
if (UI.connSettingsOpen === true) {
UI.toggleConnectPanel();
}
// Close popup status panel if open
if (UI.popupStatusOpen === true) {
UI.togglePopupStatusPanel();
}
// Close XVP panel if open
if (UI.xvpOpen === true) {
UI.toggleXvpPanel();
}
$D('noVNC_settings').style.display = "block";
$D('settingsButton').className = "noVNC_status_button_selected";
UI.settingsOpen = true;
},
// Close menu (without applying settings)
closeSettingsMenu: function() {
$D('noVNC_settings').style.display = "none";
$D('settingsButton').className = "noVNC_status_button";
UI.settingsOpen = false;
},
// Save/apply settings when 'Apply' button is pressed
settingsApply: function() {
//Util.Debug(">> settingsApply");
UI.saveSetting('encrypt');
UI.saveSetting('true_color');
if (UI.rfb.get_display().get_cursor_uri()) {
UI.saveSetting('cursor');
}
UI.saveSetting('clip');
UI.saveSetting('shared');
UI.saveSetting('view_only');
UI.saveSetting('path');
UI.saveSetting('repeaterID');
UI.saveSetting('stylesheet');
UI.saveSetting('logging');
// Settings with immediate (non-connected related) effect
WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
WebUtil.init_logging(UI.getSetting('logging'));
UI.setViewClip();
UI.setViewDrag(UI.rfb.get_viewportDrag());
//Util.Debug("<< settingsApply");
},
setPassword: function() {
UI.rfb.sendPassword($D('noVNC_password').value);
//Reset connect button.
$D('noVNC_connect_button').value = "Connect";
$D('noVNC_connect_button').onclick = UI.Connect;
//Hide connection panel.
UI.toggleConnectPanel();
return false;
},
sendCtrlAltDel: function() {
UI.rfb.sendCtrlAltDel();
},
xvpShutdown: function() {
UI.rfb.xvpShutdown();
},
xvpReboot: function() {
UI.rfb.xvpReboot();
},
xvpReset: function() {
UI.rfb.xvpReset();
},
setMouseButton: function(num) {
var b, blist = [0, 1,2,4], button;
if (typeof num === 'undefined') {
// Disable mouse buttons
num = -1;
}
if (UI.rfb) {
UI.rfb.get_mouse().set_touchButton(num);
}
for (b = 0; b < blist.length; b++) {
button = $D('noVNC_mouse_button' + blist[b]);
if (blist[b] === num) {
button.style.display = "";
} else {
button.style.display = "none";
/*
button.style.backgroundColor = "black";
button.style.color = "lightgray";
button.style.backgroundColor = "";
button.style.color = "";
*/
}
}
},
updateState: function(rfb, state, oldstate, msg) {
var s, sb, c, d, cad, vd, klass;
UI.rfb_state = state;
switch (state) {
case 'failed':
case 'fatal':
klass = "noVNC_status_error";
break;
case 'normal':
klass = "noVNC_status_normal";
break;
case 'disconnected':
$D('noVNC_logo').style.display = "block";
// Fall through
case 'loaded':
klass = "noVNC_status_normal";
break;
case 'password':
UI.toggleConnectPanel(); UI.toggleConnectPanel();
$D('noVNC_connect_button').value = "Send Password"; var host = $D('noVNC_host').value;
$D('noVNC_connect_button').onclick = UI.setPassword; var port = $D('noVNC_port').value;
$D('noVNC_password').focus(); var password = $D('noVNC_password').value;
var path = $D('noVNC_path').value;
klass = "noVNC_status_warn"; if ((!host) || (!port)) {
break; throw new Error("Must set host and port");
default: }
klass = "noVNC_status_warn";
break;
}
if (typeof(msg) !== 'undefined') {
$D('noVNC-control-bar').setAttribute("class", klass);
$D('noVNC_status').innerHTML = msg;
}
UI.updateVisualState();
},
// Disable/enable controls depending on connection state
updateVisualState: function() {
var connected = UI.rfb_state === 'normal' ? true : false;
//Util.Debug(">> updateVisualState");
$D('noVNC_encrypt').disabled = connected;
$D('noVNC_true_color').disabled = connected;
if (UI.rfb && UI.rfb.get_display() &&
UI.rfb.get_display().get_cursor_uri()) {
$D('noVNC_cursor').disabled = connected;
} else {
UI.updateSetting('cursor', !UI.isTouchDevice);
$D('noVNC_cursor').disabled = true;
}
$D('noVNC_shared').disabled = connected;
$D('noVNC_view_only').disabled = connected;
$D('noVNC_path').disabled = connected;
$D('noVNC_repeaterID').disabled = connected;
if (connected) {
UI.setViewClip();
UI.setMouseButton(1);
$D('clipboardButton').style.display = "inline";
$D('showKeyboard').style.display = "inline";
$D('noVNC_extra_keys').style.display = "";
$D('sendCtrlAltDelButton').style.display = "inline";
} else {
UI.setMouseButton();
$D('clipboardButton').style.display = "none";
$D('showKeyboard').style.display = "none";
$D('noVNC_extra_keys').style.display = "none";
$D('sendCtrlAltDelButton').style.display = "none";
UI.updateXvpVisualState(0);
}
// State change disables viewport dragging.
// It is enabled (toggled) by direct click on the button
UI.setViewDrag(false);
switch (UI.rfb_state) {
case 'fatal':
case 'failed':
case 'loaded':
case 'disconnected':
$D('connectButton').style.display = "";
$D('disconnectButton').style.display = "none";
break;
default:
$D('connectButton').style.display = "none";
$D('disconnectButton').style.display = "";
break;
}
//Util.Debug("<< updateVisualState");
},
// Disable/enable XVP button
updateXvpVisualState: function(ver) {
if (ver >= 1) {
$D('xvpButton').style.display = 'inline';
} else {
$D('xvpButton').style.display = 'none';
// Close XVP panel if open
if (UI.xvpOpen === true) {
UI.toggleXvpPanel();
}
}
},
// Display the desktop name in the document title
updateDocumentTitle: function(rfb, name) {
document.title = name + " - noVNC";
},
clipReceive: function(rfb, text) {
Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
$D('noVNC_clipboard_text').value = text;
Util.Debug("<< UI.clipReceive");
},
connect: function() {
var host, port, password, path;
UI.closeSettingsMenu();
UI.toggleConnectPanel();
host = $D('noVNC_host').value;
port = $D('noVNC_port').value;
password = $D('noVNC_password').value;
path = $D('noVNC_path').value;
if ((!host) || (!port)) {
throw("Must set host and port");
}
UI.rfb.set_encrypt(UI.getSetting('encrypt'));
UI.rfb.set_true_color(UI.getSetting('true_color'));
UI.rfb.set_local_cursor(UI.getSetting('cursor'));
UI.rfb.set_shared(UI.getSetting('shared'));
UI.rfb.set_view_only(UI.getSetting('view_only'));
UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
UI.rfb.connect(host, port, password, path);
//Close dialog.
setTimeout(UI.setBarPosition, 100);
$D('noVNC_logo').style.display = "none";
},
disconnect: function() {
UI.closeSettingsMenu();
UI.rfb.disconnect();
$D('noVNC_logo').style.display = "block";
UI.connSettingsOpen = false;
UI.toggleConnectPanel();
},
displayBlur: function() {
UI.rfb.get_keyboard().set_focused(false);
UI.rfb.get_mouse().set_focused(false);
},
displayFocus: function() {
UI.rfb.get_keyboard().set_focused(true);
UI.rfb.get_mouse().set_focused(true);
},
clipClear: function() {
$D('noVNC_clipboard_text').value = "";
UI.rfb.clipboardPasteFrom("");
},
clipSend: function() {
var text = $D('noVNC_clipboard_text').value;
Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "...");
UI.rfb.clipboardPasteFrom(text);
Util.Debug("<< UI.clipSend");
},
// Enable/disable and configure viewport clipping
setViewClip: function(clip) {
var display, cur_clip, pos, new_w, new_h;
if (UI.rfb) {
display = UI.rfb.get_display();
} else {
return;
}
cur_clip = display.get_viewport();
if (typeof(clip) !== 'boolean') {
// Use current setting
clip = UI.getSetting('clip');
}
if (clip && !cur_clip) {
// Turn clipping on
UI.updateSetting('clip', true);
} else if (!clip && cur_clip) {
// Turn clipping off
UI.updateSetting('clip', false);
display.set_viewport(false);
$D('noVNC_canvas').style.position = 'static';
display.viewportChange();
}
if (UI.getSetting('clip')) {
// If clipping, update clipping settings
$D('noVNC_canvas').style.position = 'absolute';
pos = Util.getPosition($D('noVNC_canvas'));
new_w = window.innerWidth - pos.x;
new_h = window.innerHeight - pos.y;
display.set_viewport(true);
display.viewportChange(0, 0, new_w, new_h);
}
},
// Toggle/set/unset the viewport drag/move button
setViewDrag: function(drag) {
var vmb = $D('noVNC_view_drag_button');
if (!UI.rfb) { return; }
if (UI.rfb_state === 'normal' &&
UI.rfb.get_display().get_viewport()) {
vmb.style.display = "inline";
} else {
vmb.style.display = "none";
}
if (typeof(drag) === "undefined" ||
typeof(drag) === "object") {
// If not specified, then toggle
drag = !UI.rfb.get_viewportDrag();
}
if (drag) {
vmb.className = "noVNC_status_button_selected";
UI.rfb.set_viewportDrag(true);
} else {
vmb.className = "noVNC_status_button";
UI.rfb.set_viewportDrag(false);
}
},
// On touch devices, show the OS keyboard
showKeyboard: function() {
var kbi, skb, l;
kbi = $D('keyboardinput');
skb = $D('showKeyboard');
l = kbi.value.length;
if(UI.keyboardVisible === false) {
kbi.focus();
try { kbi.setSelectionRange(l, l); } // Move the caret to the end
catch (err) {} // setSelectionRange is undefined in Google Chrome
UI.keyboardVisible = true;
skb.className = "noVNC_status_button_selected";
} else if(UI.keyboardVisible === true) {
kbi.blur();
skb.className = "noVNC_status_button";
UI.keyboardVisible = false;
}
},
keepKeyboard: function() {
clearTimeout(UI.hideKeyboardTimeout);
if(UI.keyboardVisible === true) {
$D('keyboardinput').focus();
$D('showKeyboard').className = "noVNC_status_button_selected";
} else if(UI.keyboardVisible === false) {
$D('keyboardinput').blur();
$D('showKeyboard').className = "noVNC_status_button";
}
},
keyboardinputReset: function() {
var kbi = $D('keyboardinput');
kbi.value = Array(UI.defaultKeyboardinputLen).join("_");
UI.lastKeyboardinput = kbi.value;
},
// When normal keyboard events are left uncought, use the input events from
// the keyboardinput element instead and generate the corresponding key events.
// This code is required since some browsers on Android are inconsistent in
// sending keyCodes in the normal keyboard events when using on screen keyboards.
keyInput: function(event) {
var newValue, oldValue, newLen, oldLen;
newValue = event.target.value;
oldValue = UI.lastKeyboardinput;
try {
// Try to check caret position since whitespace at the end
// will not be considered by value.length in some browsers
newLen = Math.max(event.target.selectionStart, newValue.length);
} catch (err) {
// selectionStart is undefined in Google Chrome
newLen = newValue.length;
}
oldLen = oldValue.length;
var backspaces;
var inputs = newLen - oldLen;
if (inputs < 0)
backspaces = -inputs;
else
backspaces = 0;
// Compare the old string with the new to account for
// text-corrections or other input that modify existing text
for (var i = 0; i < Math.min(oldLen, newLen); i++) {
if (newValue.charAt(i) != oldValue.charAt(i)) {
inputs = newLen - i;
backspaces = oldLen - i;
break;
}
}
// Send the key events
for (var i = 0; i < backspaces; i++)
UI.rfb.sendKey(XK_BackSpace);
for (var i = newLen - inputs; i < newLen; i++)
UI.rfb.sendKey(newValue.charCodeAt(i));
// Control the text content length in the keyboardinput element
if (newLen > 2 * UI.defaultKeyboardinputLen) {
UI.keyboardinputReset();
} else if (newLen < 1) {
// There always have to be some text in the keyboardinput
// element with which backspace can interact.
UI.keyboardinputReset();
// This sometimes causes the keyboard to disappear for a second
// but it is required for the android keyboard to recognize that
// text has been added to the field
event.target.blur();
// This has to be ran outside of the input handler in order to work
setTimeout(function() { UI.keepKeyboard(); }, 0);
} else {
UI.lastKeyboardinput = newValue;
}
},
keyInputBlur: function() {
$D('showKeyboard').className = "noVNC_status_button";
//Weird bug in iOS if you change keyboardVisible
//here it does not actually occur so next time
//you click keyboard icon it doesnt work.
UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100);
},
showExtraKeys: function() {
UI.keepKeyboard();
if(UI.extraKeysVisible === false) {
$D('toggleCtrlButton').style.display = "inline";
$D('toggleAltButton').style.display = "inline";
$D('sendTabButton').style.display = "inline";
$D('sendEscButton').style.display = "inline";
$D('showExtraKeysButton').className = "noVNC_status_button_selected";
UI.extraKeysVisible = true;
} else if(UI.extraKeysVisible === true) {
$D('toggleCtrlButton').style.display = "";
$D('toggleAltButton').style.display = "";
$D('sendTabButton').style.display = "";
$D('sendEscButton').style.display = "";
$D('showExtraKeysButton').className = "noVNC_status_button";
UI.extraKeysVisible = false;
}
},
toggleCtrl: function() {
UI.keepKeyboard();
if(UI.ctrlOn === false) {
UI.rfb.sendKey(XK_Control_L, true);
$D('toggleCtrlButton').className = "noVNC_status_button_selected";
UI.ctrlOn = true;
} else if(UI.ctrlOn === true) {
UI.rfb.sendKey(XK_Control_L, false);
$D('toggleCtrlButton').className = "noVNC_status_button";
UI.ctrlOn = false;
}
},
toggleAlt: function() {
UI.keepKeyboard();
if(UI.altOn === false) {
UI.rfb.sendKey(XK_Alt_L, true);
$D('toggleAltButton').className = "noVNC_status_button_selected";
UI.altOn = true;
} else if(UI.altOn === true) {
UI.rfb.sendKey(XK_Alt_L, false);
$D('toggleAltButton').className = "noVNC_status_button";
UI.altOn = false;
}
},
sendTab: function() {
UI.keepKeyboard();
UI.rfb.sendKey(XK_Tab);
},
sendEsc: function() {
UI.keepKeyboard();
UI.rfb.sendKey(XK_Escape);
},
setKeyboard: function() {
UI.keyboardVisible = false;
},
// iOS < Version 5 does not support position fixed. Javascript workaround:
setOnscroll: function() {
window.onscroll = function() {
UI.setBarPosition();
};
},
setResize: function () { UI.rfb.set_encrypt(UI.getSetting('encrypt'));
window.onResize = function() { UI.rfb.set_true_color(UI.getSetting('true_color'));
UI.setBarPosition(); UI.rfb.set_local_cursor(UI.getSetting('cursor'));
}; UI.rfb.set_shared(UI.getSetting('shared'));
}, UI.rfb.set_view_only(UI.getSetting('view_only'));
UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
//Helper to add options to dropdown. UI.rfb.connect(host, port, password, path);
addOption: function(selectbox,text,value )
{
var optn = document.createElement("OPTION");
optn.text = text;
optn.value = value;
selectbox.options.add(optn);
},
setBarPosition: function() { //Close dialog.
$D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; setTimeout(UI.setBarPosition, 100);
$D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; $D('noVNC_logo').style.display = "none";
},
var vncwidth = $D('noVNC_screen').style.offsetWidth; disconnect: function() {
$D('noVNC-control-bar').style.width = vncwidth + 'px'; UI.closeSettingsMenu();
} UI.rfb.disconnect();
}; $D('noVNC_logo').style.display = "block";
UI.connSettingsOpen = false;
UI.toggleConnectPanel();
},
displayBlur: function() {
UI.rfb.get_keyboard().set_focused(false);
UI.rfb.get_mouse().set_focused(false);
},
displayFocus: function() {
UI.rfb.get_keyboard().set_focused(true);
UI.rfb.get_mouse().set_focused(true);
},
clipClear: function() {
$D('noVNC_clipboard_text').value = "";
UI.rfb.clipboardPasteFrom("");
},
clipSend: function() {
var text = $D('noVNC_clipboard_text').value;
Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "...");
UI.rfb.clipboardPasteFrom(text);
Util.Debug("<< UI.clipSend");
},
// Enable/disable and configure viewport clipping
setViewClip: function(clip) {
var display;
if (UI.rfb) {
display = UI.rfb.get_display();
} else {
return;
}
var cur_clip = display.get_viewport();
if (typeof(clip) !== 'boolean') {
// Use current setting
clip = UI.getSetting('clip');
}
if (clip && !cur_clip) {
// Turn clipping on
UI.updateSetting('clip', true);
} else if (!clip && cur_clip) {
// Turn clipping off
UI.updateSetting('clip', false);
display.set_viewport(false);
$D('noVNC_canvas').style.position = 'static';
display.viewportChange();
}
if (UI.getSetting('clip')) {
// If clipping, update clipping settings
$D('noVNC_canvas').style.position = 'absolute';
var pos = Util.getPosition($D('noVNC_canvas'));
var new_w = window.innerWidth - pos.x;
var new_h = window.innerHeight - pos.y;
display.set_viewport(true);
display.viewportChange(0, 0, new_w, new_h);
}
},
// Toggle/set/unset the viewport drag/move button
setViewDrag: function(drag) {
var vmb = $D('noVNC_view_drag_button');
if (!UI.rfb) { return; }
if (UI.rfb_state === 'normal' &&
UI.rfb.get_display().get_viewport()) {
vmb.style.display = "inline";
} else {
vmb.style.display = "none";
}
if (typeof(drag) === "undefined" ||
typeof(drag) === "object") {
// If not specified, then toggle
drag = !UI.rfb.get_viewportDrag();
}
if (drag) {
vmb.className = "noVNC_status_button_selected";
UI.rfb.set_viewportDrag(true);
} else {
vmb.className = "noVNC_status_button";
UI.rfb.set_viewportDrag(false);
}
},
// On touch devices, show the OS keyboard
showKeyboard: function() {
var kbi = $D('keyboardinput');
var skb = $D('showKeyboard');
var l = kbi.value.length;
if(UI.keyboardVisible === false) {
kbi.focus();
try { kbi.setSelectionRange(l, l); } // Move the caret to the end
catch (err) {} // setSelectionRange is undefined in Google Chrome
UI.keyboardVisible = true;
skb.className = "noVNC_status_button_selected";
} else if(UI.keyboardVisible === true) {
kbi.blur();
skb.className = "noVNC_status_button";
UI.keyboardVisible = false;
}
},
keepKeyboard: function() {
clearTimeout(UI.hideKeyboardTimeout);
if(UI.keyboardVisible === true) {
$D('keyboardinput').focus();
$D('showKeyboard').className = "noVNC_status_button_selected";
} else if(UI.keyboardVisible === false) {
$D('keyboardinput').blur();
$D('showKeyboard').className = "noVNC_status_button";
}
},
keyboardinputReset: function() {
var kbi = $D('keyboardinput');
kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
UI.lastKeyboardinput = kbi.value;
},
// When normal keyboard events are left uncought, use the input events from
// the keyboardinput element instead and generate the corresponding key events.
// This code is required since some browsers on Android are inconsistent in
// sending keyCodes in the normal keyboard events when using on screen keyboards.
keyInput: function(event) {
var newValue = event.target.value;
var oldValue = UI.lastKeyboardinput;
var newLen;
try {
// Try to check caret position since whitespace at the end
// will not be considered by value.length in some browsers
newLen = Math.max(event.target.selectionStart, newValue.length);
} catch (err) {
// selectionStart is undefined in Google Chrome
newLen = newValue.length;
}
var oldLen = oldValue.length;
var backspaces;
var inputs = newLen - oldLen;
if (inputs < 0) {
backspaces = -inputs;
} else {
backspaces = 0;
}
// Compare the old string with the new to account for
// text-corrections or other input that modify existing text
var i;
for (i = 0; i < Math.min(oldLen, newLen); i++) {
if (newValue.charAt(i) != oldValue.charAt(i)) {
inputs = newLen - i;
backspaces = oldLen - i;
break;
}
}
// Send the key events
for (i = 0; i < backspaces; i++) {
UI.rfb.sendKey(XK_BackSpace);
}
for (i = newLen - inputs; i < newLen; i++) {
UI.rfb.sendKey(newValue.charCodeAt(i));
}
// Control the text content length in the keyboardinput element
if (newLen > 2 * UI.defaultKeyboardinputLen) {
UI.keyboardinputReset();
} else if (newLen < 1) {
// There always have to be some text in the keyboardinput
// element with which backspace can interact.
UI.keyboardinputReset();
// This sometimes causes the keyboard to disappear for a second
// but it is required for the android keyboard to recognize that
// text has been added to the field
event.target.blur();
// This has to be ran outside of the input handler in order to work
setTimeout(function() { UI.keepKeyboard(); }, 0);
} else {
UI.lastKeyboardinput = newValue;
}
},
keyInputBlur: function() {
$D('showKeyboard').className = "noVNC_status_button";
//Weird bug in iOS if you change keyboardVisible
//here it does not actually occur so next time
//you click keyboard icon it doesnt work.
UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100);
},
showExtraKeys: function() {
UI.keepKeyboard();
if(UI.extraKeysVisible === false) {
$D('toggleCtrlButton').style.display = "inline";
$D('toggleAltButton').style.display = "inline";
$D('sendTabButton').style.display = "inline";
$D('sendEscButton').style.display = "inline";
$D('showExtraKeysButton').className = "noVNC_status_button_selected";
UI.extraKeysVisible = true;
} else if(UI.extraKeysVisible === true) {
$D('toggleCtrlButton').style.display = "";
$D('toggleAltButton').style.display = "";
$D('sendTabButton').style.display = "";
$D('sendEscButton').style.display = "";
$D('showExtraKeysButton').className = "noVNC_status_button";
UI.extraKeysVisible = false;
}
},
toggleCtrl: function() {
UI.keepKeyboard();
if(UI.ctrlOn === false) {
UI.rfb.sendKey(XK_Control_L, true);
$D('toggleCtrlButton').className = "noVNC_status_button_selected";
UI.ctrlOn = true;
} else if(UI.ctrlOn === true) {
UI.rfb.sendKey(XK_Control_L, false);
$D('toggleCtrlButton').className = "noVNC_status_button";
UI.ctrlOn = false;
}
},
toggleAlt: function() {
UI.keepKeyboard();
if(UI.altOn === false) {
UI.rfb.sendKey(XK_Alt_L, true);
$D('toggleAltButton').className = "noVNC_status_button_selected";
UI.altOn = true;
} else if(UI.altOn === true) {
UI.rfb.sendKey(XK_Alt_L, false);
$D('toggleAltButton').className = "noVNC_status_button";
UI.altOn = false;
}
},
sendTab: function() {
UI.keepKeyboard();
UI.rfb.sendKey(XK_Tab);
},
sendEsc: function() {
UI.keepKeyboard();
UI.rfb.sendKey(XK_Escape);
},
setKeyboard: function() {
UI.keyboardVisible = false;
},
// iOS < Version 5 does not support position fixed. Javascript workaround:
setOnscroll: function() {
window.onscroll = function() {
UI.setBarPosition();
};
},
setResize: function () {
window.onResize = function() {
UI.setBarPosition();
};
},
//Helper to add options to dropdown.
addOption: function(selectbox, text, value) {
var optn = document.createElement("OPTION");
optn.text = text;
optn.value = value;
selectbox.options.add(optn);
},
setBarPosition: function() {
$D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px';
$D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px';
var vncwidth = $D('noVNC_screen').style.offsetWidth;
$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,88 +18,161 @@ var Util = {}; ...@@ -19,88 +18,161 @@ 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;
}; };
} }
// IE <9 does not support indexOf // IE <9 does not support indexOf
//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) ? Math.ceil(from) : Math.floor(from);
from = (from < 0) if (from < 0) {
? Math.ceil(from) from += len;
: Math.floor(from); }
if (from < 0)
from += len; for (; from < len; from++) {
if (from in this &&
for (; from < len; from++) this[from] === elt) {
{ return from;
if (from in this && }
this[from] === elt) }
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 () {
return window.requestAnimationFrame || "use strict";
window.webkitRequestAnimationFrame || return window.requestAnimationFrame ||
window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.oRequestAnimationFrame || window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame || window.oRequestAnimationFrame ||
function(callback){ window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60); window.setTimeout(callback, 1000 / 60);
}; };
})(); })();
/* /*
* ------------------------------------------------------ * ------------------------------------------------------
* Namespaced in Util * Namespaced in Util
* ------------------------------------------------------ * ------------------------------------------------------
...@@ -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; getter = function (idx) {
if (typeof idx !== 'undefined') {
return this['_' + name][idx];
} else {
return this['_' + name];
}
};
} else {
getter = function () {
return this['_' + name];
};
}
// Default getter function var make_setter = function (process_val) {
getter = function (idx) { if (process_val) {
if ((type in {'arr':1, 'array':1}) && return function (val, idx) {
(typeof idx !== 'undefined')) { if (typeof idx !== 'undefined') {
return cfg[v][idx]; this['_' + name][idx] = process_val(val);
} else {
this['_' + name] = process_val(val);
}
};
} else { } else {
return cfg[v]; return function (val, idx) {
if (typeof idx !== 'undefined') {
this['_' + name][idx] = val;
} else {
this['_' + name] = val;
}
};
} }
}; };
// Default setter function var setter;
setter = function (val, idx) { if (type === 'bool') {
if (type in {'boolean':1, 'bool':1}) { setter = make_setter(function (val) {
if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) {
val = false; return false;
} else { } else {
val = true; return true;
} }
} else if (type in {'integer':1, 'int':1}) { });
val = parseInt(val, 10); } else if (type === 'int') {
} else if (type === 'str') { setter = make_setter(function (val) { return parseInt(val, 10); });
val = String(val); } else if (type === 'float') {
} else if (type === 'func') { setter = make_setter(parseFloat);
} else if (type === 'str') {
setter = make_setter(String);
} else if (type === 'func') {
setter = make_setter(function (val) {
if (!val) { if (!val) {
val = function () {}; return function () {};
} else {
return val;
} }
} });
if (typeof idx !== 'undefined') { } else if (type === 'arr' || type === 'dom' || type == 'raw') {
cfg[v][idx] = val; 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,42 +371,46 @@ Util.decodeUTF8 = function(utf8string) { ...@@ -250,42 +371,46 @@ 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++) {
var loadFunc = function (e) {
while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
ls[0].readyState === 'complete')) {
// For IE, append the script to trigger execution
var s = ls.shift();
//console.log("loaded script: " + s.src);
head.appendChild(s);
}
if (!this.readyState ||
(Util.Engine.presto && this.readyState === 'loaded') ||
this.readyState === 'complete') {
if (ps.indexOf(this) >= 0) {
this.onload = this.onreadystatechange = null;
//console.log("completed script: " + this.src);
ps.splice(ps.indexOf(this), 1);
// Call window.onscriptsload after last script loads
if (ps.length === 0 && window.onscriptsload) {
window.onscriptsload();
}
}
}
};
for (var f = 0; f < files.length; f++) {
script = document.createElement('script'); script = document.createElement('script');
script.type = 'text/javascript'; script.type = 'text/javascript';
script.src = Util.get_include_uri() + files[f]; script.src = Util.get_include_uri() + files[f];
//console.log("loading script: " + script.src); //console.log("loading script: " + script.src);
script.onload = script.onreadystatechange = function (e) { script.onload = script.onreadystatechange = loadFunc;
while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
ls[0].readyState === 'complete')) {
// For IE, append the script to trigger execution
var s = ls.shift();
//console.log("loaded script: " + s.src);
head.appendChild(s);
}
if (!this.readyState ||
(Util.Engine.presto && this.readyState === 'loaded') ||
this.readyState === 'complete') {
if (ps.indexOf(this) >= 0) {
this.onload = this.onreadystatechange = null;
//console.log("completed script: " + this.src);
ps.splice(ps.indexOf(this), 1);
// Call window.onscriptsload after last script loads
if (ps.length === 0 && window.onscriptsload) {
window.onscriptsload();
}
}
}
};
// 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,41 +564,88 @@ Util.stopEvent = function(e) { ...@@ -433,41 +564,88 @@ 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 () {
// Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) "use strict";
//'presto': (function() { // 'presto': (function () { return (!window.opera) ? false : true; }()),
// return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), var detectPresto = function () {
'presto': (function() { return (!window.opera) ? false : true; }()), return !!window.opera;
};
'trident': (function() {
return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
'webkit': (function() { var detectTrident = function () {
try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), if (!window.ActiveXObject) {
//'webkit': (function() { return false;
// return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()), } else {
'gecko': (function() { if (window.XMLHttpRequest) {
return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }()) return (document.querySelectorAll) ? 6 : 5;
}; } else {
if (Util.Engine.webkit) { return 4;
// Extract actual webkit version if available }
Util.Engine.webkit = (function(v) { }
var re = new RegExp('WebKit/([0-9\.]*) '); };
v = (navigator.userAgent.match(re) || ['', v])[1];
return parseFloat(v, 10); // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
})(Util.Engine.webkit); 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)
//'presto': (function() {
// return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
'presto': detectPresto(),
'trident': detectTrident(),
'webkit': detectInitialWebkit(),
'gecko': detectGecko(),
};
if (Util.Engine.webkit) {
// Extract actual webkit version if available
Util.Engine.webkit = detectActualWebkit(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';
} }
} }
version = v.match(/\d+/g); version = v.match(/\d+/g);
return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0};
}()); }());
...@@ -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,382 +43,342 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { ...@@ -43,382 +43,342 @@ 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 () {}
};
test_mode = false;
//
// Queue public functions
//
function get_sQ() {
return sQ;
} }
function get_rQ() { (function () {
return rQ; "use strict";
} Websock.prototype = {
function get_rQi() { // Getters and Setters
return rQi; get_sQ: function () {
} return this._sQ;
function set_rQi(val) { },
rQi = val;
} get_rQ: function () {
return this._rQ;
},
get_rQi: function () {
return this._rQi;
},
set_rQi: function (val) {
this._rQi = val;
},
// Receive Queue
rQlen: function () {
return this._rQ.length - this._rQi;
},
rQpeek8: function () {
return this._rQ[this._rQi];
},
rQshift8: function () {
return this._rQ[this._rQi++];
},
rQskip8: function () {
this._rQi++;
},
rQskipBytes: function (num) {
this._rQi += num;
},
rQunshift8: function (num) {
if (this._rQi === 0) {
this._rQ.unshift(num);
} else {
this._rQi--;
this._rQ[this._rQi] = num;
}
},
rQshift16: function () {
return (this._rQ[this._rQi++] << 8) +
this._rQ[this._rQi++];
},
rQshift32: function () {
return (this._rQ[this._rQi++] << 24) +
(this._rQ[this._rQi++] << 16) +
(this._rQ[this._rQi++] << 8) +
this._rQ[this._rQi++];
},
rQshiftStr: function (len) {
if (typeof(len) === 'undefined') { len = this.rQlen(); }
var arr = this._rQ.slice(this._rQi, this._rQi + len);
this._rQi += len;
return String.fromCharCode.apply(null, arr);
},
rQshiftBytes: function (len) {
if (typeof(len) === 'undefined') { len = this.rQlen(); }
this._rQi += len;
return this._rQ.slice(this._rQi - len, this._rQi);
},
rQslice: function (start, end) {
if (end) {
return this._rQ.slice(this._rQi + start, this._rQi + end);
} else {
return this._rQ.slice(this._rQi + start);
}
},
// 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
// wait (and possibly print a debug message), otherwise false.
rQwait: function (msg, num, goback) {
var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call
if (rQlen < num) {
if (goback) {
if (this._rQi < goback) {
throw new Error("rQwait cannot backup " + goback + " bytes");
}
this._rQi -= goback;
}
return true; // true means need more data
}
return false;
},
function rQlen() { // Send Queue
return rQ.length - rQi;
}
function rQpeek8() { flush: function () {
return (rQ[rQi] ); if (this._websocket.bufferedAmount !== 0) {
} Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount);
function rQshift8() { }
return (rQ[rQi++] );
}
function rQunshift8(num) {
if (rQi === 0) {
rQ.unshift(num);
} else {
rQi -= 1;
rQ[rQi] = num;
}
} if (this._websocket.bufferedAmount < this.maxBufferedAmount) {
function rQshift16() { if (this._sQ.length > 0) {
return (rQ[rQi++] << 8) + this._websocket.send(this._encode_message());
(rQ[rQi++] ); this._sQ = [];
} }
function rQshift32() {
return (rQ[rQi++] << 24) +
(rQ[rQi++] << 16) +
(rQ[rQi++] << 8) +
(rQ[rQi++] );
}
function rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = rQlen(); }
var arr = rQ.slice(rQi, rQi + len);
rQi += len;
return String.fromCharCode.apply(null, arr);
}
function rQshiftBytes(len) {
if (typeof(len) === 'undefined') { len = rQlen(); }
rQi += len;
return rQ.slice(rQi-len, rQi);
}
function rQslice(start, end) { return true;
if (end) { } else {
return rQ.slice(rQi + start, rQi + end); Util.Info("Delaying send, bufferedAmount: " +
} else { this._websocket.bufferedAmount);
return rQ.slice(rQi + start); return false;
} }
} },
send: function (arr) {
this._sQ = this._sQ.concat(arr);
return this.flush();
},
send_string: function (str) {
this.send(str.split('').map(function (chr) {
return chr.charCodeAt(0);
}));
},
// Event Handlers
on: function (evt, handler) {
this._eventHandlers[evt] = handler;
},
init: function (protocols, ws_schema) {
this._rQ = [];
this._rQi = 0;
this._sQ = [];
this._websocket = null;
// Check for full typed array support
var bt = false;
if (('Uint8Array' in window) &&
('set' in Uint8Array.prototype)) {
bt = true;
}
// Check to see if we must wait for 'num' bytes (default to FBU.bytes) // Check for full binary type support in WebSockets
// to be available in the receive queue. Return true if we need to // Inspired by:
// wait (and possibly print a debug message), otherwise false. // https://github.com/Modernizr/Modernizr/issues/370
function rQwait(msg, num, goback) { // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
var rQlen = rQ.length - rQi; // Skip rQlen() function call var wsbt = false;
if (rQlen < num) { try {
if (goback) { if (bt && ('binaryType' in WebSocket.prototype ||
if (rQi < goback) { !!(new WebSocket(ws_schema + '://.').binaryType))) {
throw("rQwait cannot backup " + goback + " bytes"); Util.Info("Detected binaryType support in WebSockets");
wsbt = true;
}
} catch (exc) {
// Just ignore failed test localhost connection
} }
rQi -= goback;
}
//Util.Debug(" waiting for " + (num-rQlen) +
// " " + msg + " byte(s)");
return true; // true means need more data
}
return false;
}
// // Default protocols if not specified
// Private utility routines if (typeof(protocols) === "undefined") {
// if (wsbt) {
protocols = ['binary', 'base64'];
function encode_message() { } else {
if (mode === 'binary') { protocols = 'base64';
// Put in a binary arraybuffer }
return (new Uint8Array(sQ)).buffer; }
} else {
// base64 encode
return Base64.encode(sQ);
}
}
function decode_message(data) { if (!wsbt) {
//Util.Debug(">> decode_message: " + data); if (protocols === 'binary') {
if (mode === 'binary') { throw new Error('WebSocket binary sub-protocol requested but not supported');
// push arraybuffer values onto the end }
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);
}
if (typeof(protocols) === 'object') {
var new_protocols = [];
for (var i = 0; i < protocols.length; i++) {
if (protocols[i] === 'binary') {
Util.Error('Skipping unsupported WebSocket binary sub-protocol');
} else {
new_protocols.push(protocols[i]);
}
}
if (new_protocols.length > 0) {
protocols = new_protocols;
} else {
throw new Error("Only WebSocket binary sub-protocol was requested and is not supported.");
}
}
}
// return protocols;
// 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;
} else {
Util.Info("Delaying send, bufferedAmount: " +
websocket.bufferedAmount);
return false;
}
}
// overridable for testing open: function (uri, protocols) {
function send(arr) { var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
//Util.Debug(">> send_array: " + arr); protocols = this.init(protocols, ws_schema);
sQ = sQ.concat(arr);
return flush();
}
function send_string(str) { this._websocket = new WebSocket(uri, protocols);
//Util.Debug(">> send_string: " + str);
api.send(str.split('').map(
function (chr) { return chr.charCodeAt(0); } ) );
}
// if (protocols.indexOf('binary') >= 0) {
// Other public functions this._websocket.binaryType = 'arraybuffer';
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");
}
// Set event handlers this._websocket.onmessage = this._recv_message.bind(this);
function on(evt, handler) { this._websocket.onopen = (function () {
eventHandlers[evt] = handler; Util.Debug('>> WebSock.onopen');
} if (this._websocket.protocol) {
this._mode = this._websocket.protocol;
function init(protocols, ws_schema) { Util.Info("Server choose sub-protocol: " + this._websocket.protocol);
rQ = [];
rQi = 0;
sQ = [];
websocket = null;
var bt = false,
wsbt = false,
try_binary = false;
// Check for full typed array support
if (('Uint8Array' in window) &&
('set' in Uint8Array.prototype)) {
bt = true;
}
// Check for full binary type support in WebSocket
// Inspired by:
// https://github.com/Modernizr/Modernizr/issues/370
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
try {
if (bt && ('binaryType' in WebSocket.prototype ||
!!(new WebSocket(ws_schema + '://.').binaryType))) {
Util.Info("Detected binaryType support in WebSockets");
wsbt = true;
}
} catch (exc) {
// Just ignore failed test localhost connections
}
// Default protocols if not specified
if (typeof(protocols) === "undefined") {
if (wsbt) {
protocols = ['binary', 'base64'];
} else {
protocols = 'base64';
}
}
// If no binary support, make sure it was not requested
if (!wsbt) {
if (protocols === 'binary') {
throw("WebSocket binary sub-protocol requested but not supported");
}
if (typeof(protocols) === "object") {
var new_protocols = [];
for (var i = 0; i < protocols.length; i++) {
if (protocols[i] === 'binary') {
Util.Error("Skipping unsupported WebSocket binary sub-protocol");
} else { } else {
new_protocols.push(protocols[i]); this._mode = 'base64';
Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol);
} }
this._eventHandlers.open();
Util.Debug("<< WebSock.onopen");
}).bind(this);
this._websocket.onclose = (function (e) {
Util.Debug(">> WebSock.onclose");
this._eventHandlers.close(e);
Util.Debug("<< WebSock.onclose");
}).bind(this);
this._websocket.onerror = (function (e) {
Util.Debug(">> WebSock.onerror: " + e);
this._eventHandlers.error(e);
Util.Debug("<< WebSock.onerror: " + e);
}).bind(this);
},
close: function () {
if (this._websocket) {
if ((this._websocket.readyState === WebSocket.OPEN) ||
(this._websocket.readyState === WebSocket.CONNECTING)) {
Util.Info("Closing WebSocket connection");
this._websocket.close();
}
this._websocket.onmessage = function (e) { return; };
} }
if (new_protocols.length > 0) { },
protocols = new_protocols;
// private methods
_encode_message: function () {
if (this._mode === 'binary') {
// Put in a binary arraybuffer
return (new Uint8Array(this._sQ)).buffer;
} else { } else {
throw("Only WebSocket binary sub-protocol was requested and not supported."); // base64 encode
return Base64.encode(this._sQ);
} }
} },
}
_decode_message: function (data) {
if (this._mode === 'binary') {
// push arraybuffer values onto the end
var u8 = new Uint8Array(data);
for (var i = 0; i < u8.length; i++) {
this._rQ.push(u8[i]);
}
} else {
// base64 decode and concat to end
this._rQ = this._rQ.concat(Base64.decode(data, 0));
}
},
_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";
}
return protocols; if (typeof exc.description !== 'undefined') {
} exception_str += " description: " + exc.description + "\n";
}
function open(uri, protocols) { if (typeof exc.stack !== 'undefined') {
var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; exception_str += exc.stack;
protocols = init(protocols, ws_schema); }
if (test_mode) { if (exception_str.length > 0) {
websocket = {}; Util.Error("recv_message, caught exception: " + exception_str);
} else { } else {
websocket = new WebSocket(uri, protocols); Util.Error("recv_message, caught exception: " + exc);
if (protocols.indexOf('binary') >= 0) { }
websocket.binaryType = 'arraybuffer';
}
}
websocket.onmessage = recv_message;
websocket.onopen = function() {
Util.Debug(">> WebSock.onopen");
if (websocket.protocol) {
mode = websocket.protocol;
Util.Info("Server chose sub-protocol: " + websocket.protocol);
} else {
mode = 'base64';
Util.Error("Server select no sub-protocol!: " + websocket.protocol);
}
eventHandlers.open();
Util.Debug("<< WebSock.onopen");
};
websocket.onclose = function(e) {
Util.Debug(">> WebSock.onclose");
eventHandlers.close(e);
Util.Debug("<< WebSock.onclose");
};
websocket.onerror = function(e) {
Util.Debug(">> WebSock.onerror: " + e);
eventHandlers.error(e);
Util.Debug("<< WebSock.onerror");
};
}
function close() { if (typeof exc.name !== 'undefined') {
if (websocket) { this._eventHandlers.error(exc.name + ": " + exc.message);
if ((websocket.readyState === WebSocket.OPEN) || } else {
(websocket.readyState === WebSocket.CONNECTING)) { this._eventHandlers.error(exc);
Util.Info("Closing WebSocket connection"); }
websocket.close(); }
} }
websocket.onmessage = function (e) { return; }; };
} })();
}
// Override internal functions for testing
// Takes a send function, returns reference to recv function
function testMode(override_send, data_mode) {
test_mode = true;
mode = data_mode;
api.send = override_send;
api.close = function () {};
return recv_message;
}
function constructor() {
// Configuration settings
api.maxBufferedAmount = 200;
// Direct access to send and receive queues
api.get_sQ = get_sQ;
api.get_rQ = get_rQ;
api.get_rQi = get_rQi;
api.set_rQi = set_rQi;
// 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();
}
...@@ -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
...@@ -31,45 +30,47 @@ if (!window.$D) { ...@@ -31,45 +30,47 @@ if (!window.$D) {
} }
/* /*
* ------------------------------------------------------ * ------------------------------------------------------
* Namespaced in WebUtil * Namespaced in WebUtil
* ------------------------------------------------------ * ------------------------------------------------------
*/ */
// 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 = "";
if ((depth > 1) && (typeof obj[i] === "object")) { for (var i in obj) {
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,14 +218,16 @@ WebUtil.getStylesheets = function() { var i, links, sheets = []; ...@@ -202,14 +218,16 @@ 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();
if (link.title === sheet) { for (var i = 0; i < sheets.length; i += 1) {
var link = sheets[i];
if (link.title === sheet) {
Util.Debug("Using stylesheet " + sheet); Util.Debug("Using stylesheet " + sheet);
link.disabled = false; link.disabled = false;
} else { } else {
......
// 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