JSDoc + namespace, some cleanup.
[guacamole-common-js.git] / src / main / resources / guacamole.js
1
2 /*
3  *  Guacamole - Clientless Remote Desktop
4  *  Copyright (C) 2010  Michael Jumper
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU Affero General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Affero General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Affero General Public License
17  */
18
19 // Guacamole namespace
20 var Guacamole = Guacamole || {};
21
22 Guacamole.Client = function(display, tunnel) {
23
24     var guac_client = this;
25
26     var STATE_IDLE          = 0;
27     var STATE_CONNECTING    = 1;
28     var STATE_WAITING       = 2;
29     var STATE_CONNECTED     = 3;
30     var STATE_DISCONNECTING = 4;
31     var STATE_DISCONNECTED  = 5;
32
33     var currentState = STATE_IDLE;
34
35     tunnel.oninstruction = doInstruction;
36
37     // Display must be relatively positioned for mouse to be handled properly
38     display.style.position = "relative";
39
40     function setState(state) {
41         if (state != currentState) {
42             currentState = state;
43             if (guac_client.onstatechange)
44                 guac_client.onstatechange(currentState);
45         }
46     }
47
48     function isConnected() {
49         return currentState == STATE_CONNECTED
50             || currentState == STATE_WAITING;
51     }
52
53     var cursorImage = null;
54     var cursorHotspotX = 0;
55     var cursorHotspotY = 0;
56
57     var cursorRectX = 0;
58     var cursorRectY = 0;
59     var cursorRectW = 0;
60     var cursorRectH = 0;
61
62     var cursorHidden = 0;
63
64     function redrawCursor(x, y) {
65
66         // Hide hardware cursor
67         if (cursorHidden == 0) {
68             display.className += " guac-hide-cursor";
69             cursorHidden = 1;
70         }
71
72         // Erase old cursor
73         cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
74
75         // Update rect
76         cursorRectX = x - cursorHotspotX;
77         cursorRectY = y - cursorHotspotY;
78         cursorRectW = cursorImage.width;
79         cursorRectH = cursorImage.height;
80
81         // Draw new cursor
82         cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
83     }
84
85     guac_client.sendKeyEvent = function(pressed, keysym) {
86         // Do not send requests if not connected
87         if (!isConnected())
88             return;
89
90         tunnel.sendMessage("key:" +  keysym + "," + pressed + ";");
91     };
92
93     guac_client.sendMouseState = function(mouseState) {
94
95         // Do not send requests if not connected
96         if (!isConnected())
97             return;
98
99         // Draw client-side cursor
100         if (cursorImage != null) {
101             redrawCursor(
102                 mouseState.getX(),
103                 mouseState.getY()
104             );
105         }
106
107         // Build mask
108         var buttonMask = 0;
109         if (mouseState.getLeft())   buttonMask |= 1;
110         if (mouseState.getMiddle()) buttonMask |= 2;
111         if (mouseState.getRight())  buttonMask |= 4;
112         if (mouseState.getUp())     buttonMask |= 8;
113         if (mouseState.getDown())   buttonMask |= 16;
114
115         // Send message
116         tunnel.sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
117     };
118
119     guac_client.setClipboard = function(data) {
120
121         // Do not send requests if not connected
122         if (!isConnected())
123             return;
124
125         tunnel.sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
126     };
127
128     // Handlers
129     guac_client.onstatechange = null;
130     guac_client.onname = null;
131     guac_client.onerror = null;
132     guac_client.onclipboard = null;
133
134     // Layers
135     var displayWidth = 0;
136     var displayHeight = 0;
137
138     var layers = new Array();
139     var buffers = new Array();
140     var cursor = null;
141
142     guac_client.getLayers = function() {
143         return layers;
144     };
145
146     function getLayer(index) {
147
148         // If negative index, use buffer
149         if (index < 0) {
150
151             index = -1 - index;
152             var buffer = buffers[index];
153
154             // Create buffer if necessary
155             if (buffer == null) {
156                 buffer = new Guacamole.Layer(0, 0);
157                 buffer.setAutosize(1);
158                 buffers[index] = buffer;
159             }
160
161             return buffer;
162         }
163
164         // If non-negative, use visible layer
165         else {
166
167             var layer = layers[index];
168             if (layer == null) {
169
170                 // Add new layer
171                 layer = new Guacamole.Layer(displayWidth, displayHeight);
172                 layers[index] = layer;
173
174                 // (Re)-add existing layers in order
175                 for (var i=0; i<layers.length; i++) {
176                     if (layers[i]) {
177
178                         // If already present, remove
179                         if (layers[i].parentNode === display)
180                             display.removeChild(layers[i].getCanvas());
181
182                         // Add to end
183                         display.appendChild(layers[i].getCanvas());
184                     }
185                 }
186
187                 // Add cursor layer last
188                 if (cursor != null) {
189                     if (cursor.parentNode === display)
190                         display.removeChild(cursor.getCanvas());
191                     display.appendChild(cursor.getCanvas());
192                 }
193
194             }
195             else {
196                 // Reset size
197                 layer.resize(displayWidth, displayHeight);
198             }
199
200             return layer;
201         }
202
203     }
204
205     var instructionHandlers = {
206
207         "error": function(parameters) {
208             if (guac_client.onerror) guac_client.onerror(unescapeGuacamoleString(parameters[0]));
209             disconnect();
210         },
211
212         "name": function(parameters) {
213             if (guac_client.onname) guac_client.onname(unescapeGuacamoleString(parameters[0]));
214         },
215
216         "clipboard": function(parameters) {
217             if (guac_client.onclipboard) guac_client.onclipboard(unescapeGuacamoleString(parameters[0]));
218         },
219
220         "size": function(parameters) {
221
222             displayWidth = parseInt(parameters[0]);
223             displayHeight = parseInt(parameters[1]);
224
225             // Update (set) display size
226             display.style.width = displayWidth + "px";
227             display.style.height = displayHeight + "px";
228
229             // Set cursor layer width/height
230             if (cursor != null)
231                 cursor.resize(displayWidth, displayHeight);
232
233         },
234
235         "png": function(parameters) {
236
237             var channelMask = parseInt(parameters[0]);
238             var layer = getLayer(parseInt(parameters[1]));
239             var x = parseInt(parameters[2]);
240             var y = parseInt(parameters[3]);
241             var data = parameters[4];
242
243             layer.setChannelMask(channelMask);
244
245             layer.draw(
246                 x,
247                 y,
248                 "data:image/png;base64," + data
249             );
250
251             // If received first update, no longer waiting.
252             if (currentState == STATE_WAITING)
253                 setState(STATE_CONNECTED);
254
255         },
256
257         "copy": function(parameters) {
258
259             var srcL = getLayer(parseInt(parameters[0]));
260             var srcX = parseInt(parameters[1]);
261             var srcY = parseInt(parameters[2]);
262             var srcWidth = parseInt(parameters[3]);
263             var srcHeight = parseInt(parameters[4]);
264             var channelMask = parseInt(parameters[5]);
265             var dstL = getLayer(parseInt(parameters[6]));
266             var dstX = parseInt(parameters[7]);
267             var dstY = parseInt(parameters[8]);
268
269             dstL.setChannelMask(channelMask);
270
271             dstL.copyRect(
272                 srcL,
273                 srcX,
274                 srcY,
275                 srcWidth, 
276                 srcHeight, 
277                 dstX,
278                 dstY 
279             );
280
281         },
282
283         "cursor": function(parameters) {
284
285             var x = parseInt(parameters[0]);
286             var y = parseInt(parameters[1]);
287             var data = parameters[2];
288
289             if (cursor == null) {
290                 cursor = new Guacamole.Layer(displayWidth, displayHeight);
291                 display.appendChild(cursor.getCanvas());
292             }
293
294             // Start cursor image load
295             var image = new Image();
296             image.onload = function() {
297                 cursorImage = image;
298
299                 var cursorX = cursorRectX + cursorHotspotX;
300                 var cursorY = cursorRectY + cursorHotspotY;
301
302                 cursorHotspotX = x;
303                 cursorHotspotY = y;
304
305                 redrawCursor(cursorX, cursorY);
306             };
307             image.src = "data:image/png;base64," + data
308
309         },
310
311         "sync": function(parameters) {
312
313             var timestamp = parameters[0];
314
315             // When all layers have finished rendering all instructions
316             // UP TO THIS POINT IN TIME, send sync response.
317
318             var layersToSync = 0;
319             function syncLayer() {
320
321                 layersToSync--;
322
323                 // Send sync response when layers are finished
324                 if (layersToSync == 0)
325                     tunnel.sendMessage("sync:" + timestamp + ";");
326
327             }
328
329             // Count active, not-ready layers and install sync tracking hooks
330             for (var i=0; i<layers.length; i++) {
331
332                 var layer = layers[i];
333                 if (layer && !layer.isReady()) {
334                     layersToSync++;
335                     layer.sync(syncLayer);
336                 }
337
338             }
339
340             // If all layers are ready, then we didn't install any hooks.
341             // Send sync message now,
342             if (layersToSync == 0)
343                 tunnel.sendMessage("sync:" + timestamp + ";");
344
345         }
346       
347     };
348
349
350     function doInstruction(opcode, parameters) {
351
352         var handler = instructionHandlers[opcode];
353         if (handler)
354             handler(parameters);
355
356     }
357
358
359     function disconnect() {
360
361         // Only attempt disconnection not disconnected.
362         if (currentState != STATE_DISCONNECTED
363                 && currentState != STATE_DISCONNECTING) {
364
365             setState(STATE_DISCONNECTING);
366             tunnel.sendMessage("disconnect;");
367             tunnel.disconnect();
368             setState(STATE_DISCONNECTED);
369         }
370
371     }
372
373     function escapeGuacamoleString(str) {
374
375         var escapedString = "";
376
377         for (var i=0; i<str.length; i++) {
378
379             var c = str.charAt(i);
380             if (c == ",")
381                 escapedString += "\\c";
382             else if (c == ";")
383                 escapedString += "\\s";
384             else if (c == "\\")
385                 escapedString += "\\\\";
386             else
387                 escapedString += c;
388
389         }
390
391         return escapedString;
392
393     }
394
395     function unescapeGuacamoleString(str) {
396
397         var unescapedString = "";
398
399         for (var i=0; i<str.length; i++) {
400
401             var c = str.charAt(i);
402             if (c == "\\" && i<str.length-1) {
403
404                 var escapeChar = str.charAt(++i);
405                 if (escapeChar == "c")
406                     unescapedString += ",";
407                 else if (escapeChar == "s")
408                     unescapedString += ";";
409                 else if (escapeChar == "\\")
410                     unescapedString += "\\";
411                 else
412                     unescapedString += "\\" + escapeChar;
413
414             }
415             else
416                 unescapedString += c;
417
418         }
419
420         return unescapedString;
421
422     }
423
424     guac_client.disconnect = disconnect;
425     guac_client.connect = function(data) {
426
427         setState(STATE_CONNECTING);
428
429         try {
430             tunnel.connect(data);
431         }
432         catch (e) {
433             setState(STATE_IDLE);
434             throw e;
435         }
436
437         setState(STATE_WAITING);
438     };
439
440     guac_client.escapeGuacamoleString   = escapeGuacamoleString;
441     guac_client.unescapeGuacamoleString = unescapeGuacamoleString;
442
443 }