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 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
---|
18 | */ |
---|
19 | |
---|
20 | /** |
---|
21 | * General set of UI elements and UI-related functions regarding user login and |
---|
22 | * connection management. |
---|
23 | */ |
---|
24 | var GuacamoleRootUI = { |
---|
25 | |
---|
26 | "sections": { |
---|
27 | "login_form" : document.getElementById("login-form"), |
---|
28 | "recent_connections" : document.getElementById("recent-connections"), |
---|
29 | "all_connections" : document.getElementById("all-connections") |
---|
30 | }, |
---|
31 | |
---|
32 | "messages": { |
---|
33 | "login_error" : document.getElementById("login-error"), |
---|
34 | "no_recent_connections" : document.getElementById("no-recent") |
---|
35 | }, |
---|
36 | |
---|
37 | "fields": { |
---|
38 | "username" : document.getElementById("username"), |
---|
39 | "password" : document.getElementById("password"), |
---|
40 | "clipboard" : document.getElementById("clipboard") |
---|
41 | }, |
---|
42 | |
---|
43 | "buttons": { |
---|
44 | "login" : document.getElementById("login"), |
---|
45 | "logout" : document.getElementById("logout") |
---|
46 | }, |
---|
47 | |
---|
48 | "settings": { |
---|
49 | "auto_fit" : document.getElementById("auto-fit"), |
---|
50 | "disable_sound" : document.getElementById("disable-sound") |
---|
51 | }, |
---|
52 | |
---|
53 | "views": { |
---|
54 | "login" : document.getElementById("login-ui"), |
---|
55 | "connections" : document.getElementById("connection-list-ui") |
---|
56 | }, |
---|
57 | |
---|
58 | "session_state" : new GuacamoleSessionState() |
---|
59 | |
---|
60 | }; |
---|
61 | |
---|
62 | /** |
---|
63 | * Attempts to login the given user using the given password, throwing an |
---|
64 | * error if the process fails. |
---|
65 | * |
---|
66 | * @param {String} username The name of the user to login as. |
---|
67 | * @param {String} password The password to use to authenticate the user. |
---|
68 | */ |
---|
69 | GuacamoleRootUI.login = function(username, password) { |
---|
70 | |
---|
71 | // Get parameters from query string |
---|
72 | var parameters = window.location.search.substring(1); |
---|
73 | |
---|
74 | // Get username and password from form |
---|
75 | var data = |
---|
76 | "username=" + encodeURIComponent(username) |
---|
77 | + "&password=" + encodeURIComponent(password) |
---|
78 | |
---|
79 | // Include query parameters in submission data |
---|
80 | if (parameters) data += "&" + parameters; |
---|
81 | |
---|
82 | // Log in |
---|
83 | var xhr = new XMLHttpRequest(); |
---|
84 | xhr.open("POST", "login", false); |
---|
85 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); |
---|
86 | xhr.send(data); |
---|
87 | |
---|
88 | // Handle failures |
---|
89 | if (xhr.status != 200) |
---|
90 | throw new Error("Invalid login"); |
---|
91 | |
---|
92 | }; |
---|
93 | |
---|
94 | /** |
---|
95 | * An arbitrary Guacamole configuration, consisting of an ID/protocol pair. |
---|
96 | * |
---|
97 | * @constructor |
---|
98 | * @param {String} protocol The protocol used by this configuration. |
---|
99 | * @param {String} id The ID associated with this configuration. |
---|
100 | */ |
---|
101 | GuacamoleRootUI.Configuration = function(protocol, id) { |
---|
102 | |
---|
103 | /** |
---|
104 | * The protocol associated with this configuration. |
---|
105 | */ |
---|
106 | this.protocol = protocol; |
---|
107 | |
---|
108 | /** |
---|
109 | * The ID associated with this configuration. |
---|
110 | */ |
---|
111 | this.id = id; |
---|
112 | |
---|
113 | }; |
---|
114 | |
---|
115 | GuacamoleRootUI.getConfigurations = function(parameters) { |
---|
116 | |
---|
117 | // Construct request URL |
---|
118 | var configs_url = "configs"; |
---|
119 | if (parameters) configs_url += "?" + parameters; |
---|
120 | |
---|
121 | // Get config list |
---|
122 | var xhr = new XMLHttpRequest(); |
---|
123 | xhr.open("GET", configs_url, false); |
---|
124 | xhr.send(null); |
---|
125 | |
---|
126 | // If fail, throw error |
---|
127 | if (xhr.status != 200) |
---|
128 | throw new Error(xhr.statusText); |
---|
129 | |
---|
130 | // Otherwise, get list |
---|
131 | var configs = new Array(); |
---|
132 | |
---|
133 | var configElements = xhr.responseXML.getElementsByTagName("config"); |
---|
134 | for (var i=0; i<configElements.length; i++) { |
---|
135 | configs.push(new Config( |
---|
136 | configElements[i].getAttribute("protocol"), |
---|
137 | configElements[i].getAttribute("id") |
---|
138 | )); |
---|
139 | } |
---|
140 | |
---|
141 | return configs; |
---|
142 | |
---|
143 | }; |
---|
144 | |
---|
145 | /** |
---|
146 | * A connection UI object which can be easily added to a list of connections |
---|
147 | * for sake of display. |
---|
148 | */ |
---|
149 | GuacamoleRootUI.Connection = function(config) { |
---|
150 | |
---|
151 | /** |
---|
152 | * The configuration associated with this connection. |
---|
153 | */ |
---|
154 | this.configuration = config; |
---|
155 | |
---|
156 | function element(tagname, classname) { |
---|
157 | var new_element = document.createElement(tagname); |
---|
158 | new_element.className = classname; |
---|
159 | return new_element; |
---|
160 | } |
---|
161 | |
---|
162 | // Create connection display elements |
---|
163 | var connection = element("div", "connection"); |
---|
164 | var caption = element("div", "caption"); |
---|
165 | var protocol = element("div", "protocol"); |
---|
166 | var name = element("span", "name"); |
---|
167 | var protocol_icon = element("div", "icon " + config.protocol); |
---|
168 | var thumbnail = element("div", "thumbnail"); |
---|
169 | var thumb_img; |
---|
170 | |
---|
171 | // Get URL |
---|
172 | var url = "client.xhtml?id=" + encodeURIComponent(config.id); |
---|
173 | |
---|
174 | // Create link to client |
---|
175 | connection.onclick = function() { |
---|
176 | |
---|
177 | // Attempt to focus existing window |
---|
178 | var current = window.open(null, config.id); |
---|
179 | |
---|
180 | // If window did not already exist, set up as |
---|
181 | // Guacamole client |
---|
182 | if (!current.GuacUI) |
---|
183 | window.open(url, config.id); |
---|
184 | |
---|
185 | }; |
---|
186 | |
---|
187 | // Add icon |
---|
188 | protocol.appendChild(protocol_icon); |
---|
189 | |
---|
190 | // Set name |
---|
191 | name.textContent = config.id; |
---|
192 | |
---|
193 | // Assemble caption |
---|
194 | caption.appendChild(protocol); |
---|
195 | caption.appendChild(name); |
---|
196 | |
---|
197 | // Assemble connection icon |
---|
198 | connection.appendChild(thumbnail); |
---|
199 | connection.appendChild(caption); |
---|
200 | |
---|
201 | // Add screenshot if available |
---|
202 | var thumbnail_url = GuacamoleHistory.get(config.id).thumbnail; |
---|
203 | if (thumbnail_url) { |
---|
204 | |
---|
205 | // Create thumbnail element |
---|
206 | thumb_img = document.createElement("img"); |
---|
207 | thumb_img.src = thumbnail_url; |
---|
208 | thumbnail.appendChild(thumb_img); |
---|
209 | |
---|
210 | } |
---|
211 | |
---|
212 | /** |
---|
213 | * Returns the DOM element representing this connection. |
---|
214 | */ |
---|
215 | this.getElement = function() { |
---|
216 | return connection; |
---|
217 | }; |
---|
218 | |
---|
219 | /** |
---|
220 | * Returns whether this connection has an associated thumbnail. |
---|
221 | */ |
---|
222 | this.hasThumbnail = function() { |
---|
223 | return thumb_img && true; |
---|
224 | }; |
---|
225 | |
---|
226 | /** |
---|
227 | * Sets the thumbnail URL of this existing connection. Note that this will |
---|
228 | * only work if the connection already had a thumbnail associated with it. |
---|
229 | */ |
---|
230 | this.setThumbnail = function(url) { |
---|
231 | |
---|
232 | // If no image element, create it |
---|
233 | if (!thumb_img) { |
---|
234 | thumb_img = document.createElement("img"); |
---|
235 | thumb_img.src = url; |
---|
236 | thumbnail.appendChild(thumb_img); |
---|
237 | } |
---|
238 | |
---|
239 | // Otherwise, set source of existing |
---|
240 | else |
---|
241 | thumb_img.src = url; |
---|
242 | |
---|
243 | }; |
---|
244 | |
---|
245 | }; |
---|
246 | |
---|
247 | /** |
---|
248 | * Set of all thumbnailed connections, indexed by ID. |
---|
249 | */ |
---|
250 | GuacamoleRootUI.thumbnailConnections = {}; |
---|
251 | |
---|
252 | /** |
---|
253 | * Set of all configurations, indexed by ID. |
---|
254 | */ |
---|
255 | GuacamoleRootUI.configurations = {}; |
---|
256 | |
---|
257 | /** |
---|
258 | * Adds the given connection to the recent connections list. |
---|
259 | */ |
---|
260 | GuacamoleRootUI.addRecentConnection = function(connection) { |
---|
261 | |
---|
262 | // Add connection object to list of thumbnailed connections |
---|
263 | GuacamoleRootUI.thumbnailConnections[connection.configuration.id] = |
---|
264 | connection; |
---|
265 | |
---|
266 | // Add connection to recent list |
---|
267 | GuacamoleRootUI.sections.recent_connections.appendChild( |
---|
268 | connection.getElement()); |
---|
269 | |
---|
270 | // Hide "No recent connections" message |
---|
271 | GuacamoleRootUI.messages.no_recent_connections.style.display = "none"; |
---|
272 | |
---|
273 | }; |
---|
274 | |
---|
275 | |
---|
276 | /** |
---|
277 | * Resets the interface such that the login UI is displayed if |
---|
278 | * the user is not authenticated (or authentication fails) and |
---|
279 | * the connection list UI (or the client for the only available |
---|
280 | * connection, if there is only one) is displayed if the user is |
---|
281 | * authenticated. |
---|
282 | */ |
---|
283 | GuacamoleRootUI.reset = function() { |
---|
284 | |
---|
285 | // Get parameters from query string |
---|
286 | var parameters = window.location.search.substring(1); |
---|
287 | |
---|
288 | // Read configs |
---|
289 | var configs; |
---|
290 | try { |
---|
291 | configs = GuacamoleRootUI.getConfigurations(parameters); |
---|
292 | } |
---|
293 | catch (e) { |
---|
294 | |
---|
295 | // Show login UI if unable to get configs |
---|
296 | GuacamoleRootUI.views.login.style.display = ""; |
---|
297 | GuacamoleRootUI.views.connections.style.display = "none"; |
---|
298 | |
---|
299 | return; |
---|
300 | |
---|
301 | } |
---|
302 | |
---|
303 | // Add connection icons |
---|
304 | for (var i=0; i<configs.length; i++) { |
---|
305 | |
---|
306 | // Add configuration to set |
---|
307 | GuacamoleRootUI.configurations[configs[i].id] = configs[i]; |
---|
308 | |
---|
309 | // Get connection element |
---|
310 | var connection = new GuacamoleRootUI.Connection(configs[i]); |
---|
311 | |
---|
312 | // If screenshot present, add to recent connections |
---|
313 | if (connection.hasThumbnail()) |
---|
314 | GuacamoleRootUI.addRecentConnection(connection); |
---|
315 | |
---|
316 | // Add connection to connection list |
---|
317 | GuacamoleRootUI.sections.all_connections.appendChild( |
---|
318 | new GuacamoleRootUI.Connection(configs[i]).getElement()); |
---|
319 | |
---|
320 | } |
---|
321 | |
---|
322 | // If configs could be retrieved, display list |
---|
323 | GuacamoleRootUI.views.login.style.display = "none"; |
---|
324 | GuacamoleRootUI.views.connections.style.display = ""; |
---|
325 | |
---|
326 | }; |
---|
327 | |
---|
328 | GuacamoleHistory.onchange = function(id, old_entry, new_entry) { |
---|
329 | |
---|
330 | // Get existing connection, if any |
---|
331 | var connection = GuacamoleRootUI.thumbnailConnections[id]; |
---|
332 | |
---|
333 | // If we are adding or updating a connection |
---|
334 | if (new_entry) { |
---|
335 | |
---|
336 | // Ensure connection is added |
---|
337 | if (!connection) { |
---|
338 | |
---|
339 | // Create new connection |
---|
340 | connection = new GuacamoleRootUI.Connection( |
---|
341 | GuacamoleRootUI.configurations[id] |
---|
342 | ); |
---|
343 | |
---|
344 | GuacamoleRootUI.addRecentConnection(connection); |
---|
345 | |
---|
346 | } |
---|
347 | |
---|
348 | // Set new thumbnail |
---|
349 | connection.setThumbnail(new_entry.thumbnail); |
---|
350 | |
---|
351 | } |
---|
352 | |
---|
353 | // Otherwise, delete existing connection |
---|
354 | else { |
---|
355 | |
---|
356 | GuacamoleRootUI.sections.recent_connections.removeChild( |
---|
357 | connection.getElement()); |
---|
358 | |
---|
359 | delete GuacamoleRootUI.thumbnailConnections[id]; |
---|
360 | |
---|
361 | // Display "No recent connections" message if none left |
---|
362 | if (GuacamoleRootUI.thumbnailConnections.length == 0) |
---|
363 | GuacamoleRootUI.messages.no_recent_connections.style.display = ""; |
---|
364 | |
---|
365 | } |
---|
366 | |
---|
367 | }; |
---|
368 | |
---|
369 | /* |
---|
370 | * This window has no name. We need it to have no name. If someone navigates |
---|
371 | * to the root UI within the same window as a previous connection, we need to |
---|
372 | * remove the name from that window such that new attempts to use that previous |
---|
373 | * connection do not replace the contents of this very window. |
---|
374 | */ |
---|
375 | window.name = ""; |
---|
376 | |
---|
377 | /* |
---|
378 | * Update session state when auto-fit checkbox is changed |
---|
379 | */ |
---|
380 | |
---|
381 | GuacamoleRootUI.settings.auto_fit.onchange = |
---|
382 | GuacamoleRootUI.settings.auto_fit.onclick = function() { |
---|
383 | |
---|
384 | GuacamoleRootUI.session_state.setProperty( |
---|
385 | "auto-fit", GuacamoleRootUI.settings.auto_fit.checked); |
---|
386 | |
---|
387 | }; |
---|
388 | |
---|
389 | /* |
---|
390 | * Update session state when disable-sound checkbox is changed |
---|
391 | */ |
---|
392 | |
---|
393 | GuacamoleRootUI.settings.disable_sound.onchange = |
---|
394 | GuacamoleRootUI.settings.disable_sound.onclick = function() { |
---|
395 | |
---|
396 | GuacamoleRootUI.session_state.setProperty( |
---|
397 | "disable-sound", GuacamoleRootUI.settings.disable_sound.checked); |
---|
398 | |
---|
399 | }; |
---|
400 | |
---|
401 | /* |
---|
402 | * Update clipboard contents when changed |
---|
403 | */ |
---|
404 | |
---|
405 | window.onblur = |
---|
406 | GuacamoleRootUI.fields.clipboard.onchange = function() { |
---|
407 | |
---|
408 | // Set value if changed |
---|
409 | var new_value = GuacamoleRootUI.fields.clipboard.value; |
---|
410 | if (GuacamoleRootUI.session_state.getProperty("clipboard") != new_value) |
---|
411 | GuacamoleRootUI.session_state.setProperty("clipboard", new_value); |
---|
412 | |
---|
413 | }; |
---|
414 | |
---|
415 | /* |
---|
416 | * Update element states when session state changes |
---|
417 | */ |
---|
418 | |
---|
419 | GuacamoleRootUI.session_state.onchange = |
---|
420 | function(old_state, new_state, name) { |
---|
421 | |
---|
422 | // Clipboard |
---|
423 | if (name == "clipboard") |
---|
424 | GuacamoleRootUI.fields.clipboard.value = new_state[name]; |
---|
425 | |
---|
426 | // Auto-fit display |
---|
427 | else if (name == "auto-fit") |
---|
428 | GuacamoleRootUI.fields.auto_fit.checked = new_state[name]; |
---|
429 | |
---|
430 | // Disable Sound |
---|
431 | else if (name == "disable-sound") |
---|
432 | GuacamoleRootUI.fields.disable_sound.checked = new_state[name]; |
---|
433 | |
---|
434 | }; |
---|
435 | |
---|
436 | /* |
---|
437 | * Initialize clipboard with current data |
---|
438 | */ |
---|
439 | |
---|
440 | if (GuacamoleRootUI.session_state.getProperty("clipboard")) |
---|
441 | GuacamoleRootUI.fields.clipboard.value = |
---|
442 | GuacamoleRootUI.session_state.getProperty("clipboard"); |
---|
443 | |
---|
444 | /* |
---|
445 | * Default to true if auto-fit not specified |
---|
446 | */ |
---|
447 | |
---|
448 | if (GuacamoleRootUI.session_state.getProperty("auto-fit") === undefined) |
---|
449 | GuacamoleRootUI.session_state.setProperty("auto-fit", true); |
---|
450 | |
---|
451 | /* |
---|
452 | * Initialize auto-fit setting in UI |
---|
453 | */ |
---|
454 | |
---|
455 | GuacamoleRootUI.settings.auto_fit.checked = |
---|
456 | GuacamoleRootUI.session_state.getProperty("auto-fit"); |
---|
457 | |
---|
458 | /* |
---|
459 | * Initialize disable-sound setting in UI |
---|
460 | */ |
---|
461 | GuacamoleRootUI.settings.disable_sound.checked = |
---|
462 | GuacamoleRootUI.session_state.getProperty("disable-sound"); |
---|
463 | |
---|
464 | /* |
---|
465 | * Set handler for logout |
---|
466 | */ |
---|
467 | |
---|
468 | GuacamoleRootUI.buttons.logout.onclick = function() { |
---|
469 | window.location.href = "logout"; |
---|
470 | }; |
---|
471 | |
---|
472 | /* |
---|
473 | * Set handler for login |
---|
474 | */ |
---|
475 | |
---|
476 | GuacamoleRootUI.sections.login_form.onsubmit = function() { |
---|
477 | |
---|
478 | try { |
---|
479 | |
---|
480 | // Attempt login |
---|
481 | GuacamoleRootUI.login( |
---|
482 | GuacamoleRootUI.fields.username.value, |
---|
483 | GuacamoleRootUI.fields.password.value |
---|
484 | ); |
---|
485 | |
---|
486 | // Ensure username/password fields are blurred after login attempt |
---|
487 | GuacamoleRootUI.fields.username.blur(); |
---|
488 | GuacamoleRootUI.fields.password.blur(); |
---|
489 | |
---|
490 | // Reset UI |
---|
491 | GuacamoleRootUI.reset(); |
---|
492 | |
---|
493 | } |
---|
494 | catch (e) { |
---|
495 | |
---|
496 | // Display error, reset and refocus password field |
---|
497 | GuacamoleRootUI.messages.login_error.textContent = e.message; |
---|
498 | |
---|
499 | // Reset and recofus password field |
---|
500 | GuacamoleRootUI.fields.password.value = ""; |
---|
501 | GuacamoleRootUI.fields.password.focus(); |
---|
502 | |
---|
503 | } |
---|
504 | |
---|
505 | // Always cancel submit |
---|
506 | return false; |
---|
507 | |
---|
508 | }; |
---|
509 | |
---|
510 | /* |
---|
511 | * Turn off autocorrect and autocapitalization on usename |
---|
512 | */ |
---|
513 | |
---|
514 | GuacamoleRootUI.fields.username.setAttribute("autocorrect", "off"); |
---|
515 | GuacamoleRootUI.fields.username.setAttribute("autocapitalize", "off"); |
---|
516 | |
---|
517 | /* |
---|
518 | * Initialize UI |
---|
519 | */ |
---|
520 | |
---|
521 | GuacamoleRootUI.reset(); |
---|
522 | |
---|
523 | /* |
---|
524 | * Make sure body has an associated touch event handler such that CSS styles |
---|
525 | * will work in browsers that require this. |
---|
526 | */ |
---|
527 | document.body.ontouchstart = function() {}; |
---|