]> jfr.im git - irc/quakenet/qwebirc.git/blame - js/ui/baseui.js
Fix XSS issue where external pages could alter DOM.
[irc/quakenet/qwebirc.git] / js / ui / baseui.js
CommitLineData
3e66d49c
CP
1qwebirc.ui.WINDOW_STATUS = 0x01;
2qwebirc.ui.WINDOW_QUERY = 0x02;
3qwebirc.ui.WINDOW_CHANNEL = 0x04;
4qwebirc.ui.WINDOW_CUSTOM = 0x08;
5qwebirc.ui.WINDOW_CONNECT = 0x10;
8c3e644b 6qwebirc.ui.WINDOW_MESSAGES = 0x20;
3e66d49c 7
e20e5a6b 8qwebirc.ui.CUSTOM_CLIENT = "custom";
ffb5ea9a 9qwebirc.ui.DEFAULT_HUE = 210; /* nice blue */
9e769c12 10
e20e5a6b 11qwebirc.ui.BaseUI = new Class({
2cad083e 12 Implements: [Events],
a59dc700 13 initialize: function(parentElement, windowClass, uiName, options) {
2cad083e 14 this.options = options;
a59dc700 15
ea29e3d7
CP
16 this.windows = new QHash();
17 this.clients = new QHash();
18 this.windows.put(qwebirc.ui.CUSTOM_CLIENT, new QHash());
9e769c12
CP
19 this.windowArray = [];
20 this.windowClass = windowClass;
21 this.parentElement = parentElement;
22 this.parentElement.addClass("qwebirc");
23 this.parentElement.addClass("qwebirc-" + uiName);
e8db8558 24 this.firstClient = false;
e20e5a6b 25 this.commandhistory = new qwebirc.irc.CommandHistory();
ffbb638d 26 this.clientId = 0;
eb271d86
CP
27
28 this.windowFocused = true;
85449eee
CP
29
30 if(Browser.Engine.trident) {
31 var checkFocus = function() {
32 var hasFocus = document.hasFocus();
33 if(hasFocus != this.windowFocused) {
34 this.windowFocused = hasFocus;
35 this.focusChange(hasFocus);
36 }
37 }
38
39 checkFocus.periodical(100, this);
40 } else {
41 var blur = function() { if(this.windowFocused) { this.windowFocused = false; this.focusChange(false); } }.bind(this);
42 var focus = function() { if(!this.windowFocused) { this.windowFocused = true; this.focusChange(true); } }.bind(this);
43
44 /* firefox requires both */
45
46 document.addEvent("blur", blur);
47 window.addEvent("blur", blur);
48 document.addEvent("focus", focus);
49 window.addEvent("focus", focus);
50 }
e87f9924
CP
51
52 qwebirc.util.__log = function(x) {
53 if(QWEBIRC_DEBUG) {
54 if(typeof console != "undefined")
55 console.log(x);
56 this.getActiveWindow().addLine(null, x);
57 }
58 }.bind(this);
9e769c12
CP
59 },
60 newClient: function(client) {
ea29e3d7 61 client.id = String(this.clientId++);
96f28062 62 client.hilightController = new qwebirc.ui.HilightController(client);
fc38a626 63 client.addEvent("signedOn", function() {
f2c48132 64 this.poller = new qwebirc.xdomain.Poller(this.oobMessage.bind(this));
fc38a626
CP
65 this.fireEvent("signedOn", client);
66 }.bind(this));
ea29e3d7
CP
67 this.windows.put(client.id, new QHash());
68 this.clients.put(client.id, client);
e20e5a6b 69 var w = this.newWindow(client, qwebirc.ui.WINDOW_STATUS, "Status");
9e769c12 70 this.selectWindow(w);
e8db8558
CP
71 if(!this.firstClient) {
72 this.firstClient = true;
e20e5a6b 73 w.addLine("", "qwebirc v" + qwebirc.VERSION);
9d3205c1 74 w.addLine("", "Copyright (C) 2008-2014 Chris Porter and the qwebirc project.");
2dfab0e1
CP
75 w.addLine("", "http://www.qwebirc.org");
76 w.addLine("", "Licensed under the GNU General Public License, Version 2.");
e8db8558 77 }
9e769c12
CP
78 return w;
79 },
ffbb638d
CP
80 getClientId: function(client) {
81 if(client == qwebirc.ui.CUSTOM_CLIENT) {
82 return qwebirc.ui.CUSTOM_CLIENT;
83 } else {
84 return client.id;
85 }
86 },
fd3734d4 87 getWindowIdentifier: function(client, type, name) {
f74802c5
CP
88 if(type == qwebirc.ui.WINDOW_MESSAGES)
89 return "-M";
e20e5a6b 90 if(type == qwebirc.ui.WINDOW_STATUS)
f74802c5 91 return "";
fd3734d4
CP
92
93 if(client == qwebirc.ui.CUSTOM_CLIENT) /* HACK */
94 return "_" + name;
95
96 return "_" + client.toIRCLower(name);
f74802c5
CP
97 },
98 newWindow: function(client, type, name) {
99 var w = this.getWindow(client, type, name);
100 if($defined(w))
101 return w;
9e769c12 102
fd3734d4 103 var wId = this.getWindowIdentifier(client, type, name);
ea29e3d7
CP
104 var w = new this.windowClass(this, client, type, name, wId);
105 this.windows.get(this.getClientId(client)).put(wId, w);
9e769c12
CP
106 this.windowArray.push(w);
107
108 return w;
109 },
f74802c5 110 getWindow: function(client, type, name) {
ea29e3d7 111 var c = this.windows.get(this.getClientId(client));
f74802c5
CP
112 if(!$defined(c))
113 return null;
114
69fbfa6d 115 return c.get(this.getWindowIdentifier(client, type, name));
f74802c5 116 },
9e769c12
CP
117 getActiveWindow: function() {
118 return this.active;
119 },
1d42a76f
CP
120 getActiveIRCWindow: function(client) {
121 if(!this.active || this.active.type == qwebirc.ui.WINDOW_CUSTOM) {
ea29e3d7 122 return this.windows.get(this.getClientId(client)).get(this.getWindowIdentifier(client, qwebirc.ui.WINDOW_STATUS));
1d42a76f
CP
123 } else {
124 return this.active;
125 }
126 },
9e769c12
CP
127 __setActiveWindow: function(window) {
128 this.active = window;
129 },
5aa173fb
CP
130 renameWindow: function(window, name) {
131 if(this.getWindow(window.client, window.type, name))
132 return null;
133
134 var clientId = this.getClientId(window.client);
135 var index = this.windowArray.indexOf(window);
136 if(index == -1)
137 return null;
138
ea29e3d7 139 this.windows.get(clientId).remove(window.identifier);
5aa173fb
CP
140
141 var window = this.windowArray[index];
142 window.name = name;
143 window.identifier = this.getWindowIdentifier(window.client, window.type, window.name);
144
ea29e3d7 145 this.windows.get(clientId).put(window.identifier, this.windowArray[index]);
5aa173fb
CP
146
147 if(window.active)
148 this.updateTitle(window.name + " - " + this.options.appTitle);
149
150 window.rename(window.name);
151 return window;
152 },
9e769c12
CP
153 selectWindow: function(window) {
154 if(this.active)
155 this.active.deselect();
156 window.select(); /* calls setActiveWindow */
326478c2
CP
157 this.updateTitle(window.name + " - " + this.options.appTitle);
158 },
159 updateTitle: function(text) {
160 document.title = text;
9e769c12 161 },
ff4befd8
CP
162 nextWindow: function(direction) {
163 if(this.windowArray.length == 0 || !this.active)
164 return;
165
166 if(!direction)
167 direction = 1;
168
169 var index = this.windowArray.indexOf(this.active);
170 if(index == -1)
171 return;
172
173 index = index + direction;
174 if(index < 0) {
175 index = this.windowArray.length - 1;
176 } else if(index >= this.windowArray.length) {
177 index = 0;
178 }
179
180 this.selectWindow(this.windowArray[index]);
181 },
182 prevWindow: function() {
183 this.nextWindow(-1);
184 },
9e769c12
CP
185 __closed: function(window) {
186 if(window.active) {
187 this.active = undefined;
188 if(this.windowArray.length == 1) {
189 this.windowArray = [];
190 } else {
191 var index = this.windowArray.indexOf(window);
6f2e4a37
CP
192 if(index == -1) {
193 return;
194 } else if(index == 0) {
9e769c12
CP
195 this.selectWindow(this.windowArray[1]);
196 } else {
197 this.selectWindow(this.windowArray[index - 1]);
198 }
9e769c12
CP
199 }
200 }
201
404cfb58 202 this.windowArray = this.windowArray.erase(window);
ea29e3d7 203 this.windows.get(this.getClientId(window.client)).remove(window.identifier);
eb9b087b 204 },
eb9b087b
CP
205 /*
206 this shouldn't be called by overriding classes!
66de775f 207 they should implement their own!
eb9b087b
CP
208 some form of user input MUST be received before an
209 IRC connection is made, else users are going to get
210 tricked into getting themselves glined
211 */
66de775f 212 loginBox: function(callback, initialNickname, initialChannels, autoConnect, autoNick) {
e89c812f
CP
213 this.postInitialize();
214
e3f97d79 215 this.addCustomWindow("Connect", qwebirc.ui.ConnectPane, "connectpane", {
13afa38f
CP
216 initialNickname: initialNickname, initialChannels: initialChannels, autoConnect: autoConnect, callback: callback, autoNick: autoNick,
217 uiOptions: this.options
e89c812f 218 }, qwebirc.ui.WINDOW_CONNECT);
eb271d86
CP
219 },
220 focusChange: function(newValue) {
221 var window_ = this.getActiveWindow();
222 if($defined(window_))
223 window_.focusChange(newValue);
c22afc5d 224 },
c22afc5d
CP
225 oobMessage: function(message) {
226 var c = message.splitMax(" ", 2);
227 if(c.length != 2)
228 return;
229
230 var command = c[0];
231 if(command != "CMD")
232 return;
233
234 var d = c[1].splitMax(" ", 2);
235 if(d.length != 2)
236 return;
237
238 var command = d[0];
239 var args = d[1];
240 if(command == "SAY") {
241 var w = this.getActiveIRCWindow();
44c29df2 242 if($defined(w) && (w.type == qwebirc.ui.WINDOW_CHANNEL || w.type == qwebirc.ui.WINDOW_QUERY)) {
c22afc5d
CP
243 w.client.exec("/SAY " + args);
244 return;
245 }
246 }
9e769c12
CP
247 }
248});
381fddfd 249
e20e5a6b
CP
250qwebirc.ui.StandardUI = new Class({
251 Extends: qwebirc.ui.BaseUI,
f3d0c9f5 252 UICommands: qwebirc.ui.UI_COMMANDS,
381fddfd
CP
253 initialize: function(parentElement, windowClass, uiName, options) {
254 this.parent(parentElement, windowClass, uiName, options);
3184781b 255
ffb5ea9a
CP
256 this.__styleValues = {hue: qwebirc.ui.DEFAULT_HUE, saturation: 0, lightness: 0, textHue: null, textSaturation: null, textLightness: null};
257 if($defined(this.options.hue)) this.__styleValues.hue = this.options.hue;
3184781b 258 this.tabCompleter = new qwebirc.ui.TabCompleterFactory(this);
c0f2f367 259 this.uiOptions = new qwebirc.ui.DefaultOptionsClass(this, options.uiOptionsArg);
ea29e3d7 260 this.customWindows = new QHash();
ffb5ea9a 261
6f8a20df
CP
262 if($defined(this.options.saturation)) this.__styleValues.saturation = this.options.saturation;
263 if($defined(this.options.lightness)) this.__styleValues.lightness = this.options.lightness;
69d01d1d
CP
264 if($defined(this.options.tsaturation)) this.__styleValues.textSaturation = this.options.tsaturation;
265 if($defined(this.options.tlightness)) this.__styleValues.textLightness = this.options.tlightness;
ffb5ea9a
CP
266
267 if($defined(this.options.hue)) { /* overridden in url */
268 /* ugh... this will go away when we add proper options for hue/sat/light for text and background */
269 this.uiOptions.setValueByPrefix("STYLE_HUE", this.__styleValues.hue);
270 } else {
271 this.__styleValues.hue = this.uiOptions.STYLE_HUE; /* otherwise copy from serialised store */
272 }
273 this.__styleValues.textHue = $defined(this.options.thue) ? this.options.thue : this.__styleValues.hue;
274
83d21243 275 document.addEvent("keydown", this.__handleHotkey.bind(this));
20157c51
CP
276 },
277 __handleHotkey: function(x) {
20157c51 278 var success = false;
710fd882 279 if(!x.alt && !x.control && !x.shift && !x.meta) {
83d21243
CP
280 if((x.key == "backspace" || x.key == "/") && !this.getInputFocused(x)) {
281 success = true;
282 }
710fd882
CP
283 } else if(!x.alt || x.control || x.meta) {
284 /* do nothing */
83d21243 285 } else if(x.key == "a" || x.key == "A") {
20157c51
CP
286 var highestNum = 0;
287 var highestIndex = -1;
288 success = true;
83d21243 289
20157c51
CP
290 for(var i=0;i<this.windowArray.length;i++) {
291 var h = this.windowArray[i].hilighted;
292 if(h > highestNum) {
293 highestIndex = i;
294 highestNum = h;
381fddfd 295 }
20157c51
CP
296 }
297 if(highestIndex > -1)
298 this.selectWindow(this.windowArray[highestIndex]);
710fd882 299 } else if((x.key >= '0' && x.key <= '9') && !x.shift) {
20157c51
CP
300 success = true;
301
302 number = x.key - '0';
303 if(number == 0)
304 number = 10
424608ac 305
20157c51
CP
306 number = number - 1;
307
308 if(number >= this.windowArray.length)
309 return;
381fddfd 310
20157c51 311 this.selectWindow(this.windowArray[number]);
710fd882 312 } else if((x.key == "left" || x.key == "up") && !x.shift) {
20157c51
CP
313 this.prevWindow();
314 success = true;
710fd882 315 } else if((x.key == "right" || x.key == "down") && !x.shift) {
20157c51
CP
316 this.nextWindow();
317 success = true;
318 }
710fd882 319
83d21243 320 if(success) {
20157c51 321 new Event(x).stop();
83d21243
CP
322 x.preventDefault();
323 }
20157c51
CP
324 },
325 getInputFocused: function(x) {
deebe19a
CP
326 if($$("input").indexOf(x.target) == -1 && $$("textarea").indexOf(x.target) == -1)
327 return false;
328 return true;
841a451d 329 },
8af49135
CP
330 newCustomWindow: function(name, select, type) {
331 if(!type)
e20e5a6b 332 type = qwebirc.ui.WINDOW_CUSTOM;
e89c812f 333
e20e5a6b 334 var w = this.newWindow(qwebirc.ui.CUSTOM_CLIENT, type, name);
8af49135 335 w.addEvent("close", function(w) {
ea29e3d7 336 this.windows.get(qwebirc.ui.CUSTOM_CLIENT).remove(w.identifier);
8af49135
CP
337 }.bind(this));
338
339 if(select)
340 this.selectWindow(w);
6c19eb8f 341
8af49135
CP
342 return w;
343 },
e89c812f 344 addCustomWindow: function(windowName, class_, cssClass, options, type) {
ebb21d2e
CP
345 if(!$defined(options))
346 options = {};
347
ea29e3d7
CP
348 if(this.customWindows.contains(windowName)) {
349 this.selectWindow(this.customWindows.get(windowName));
8af49135 350 return;
841a451d 351 }
8af49135 352
e89c812f 353 var d = this.newCustomWindow(windowName, true, type);
ea29e3d7 354 this.customWindows.put(windowName, d);
ebb21d2e
CP
355
356 d.addEvent("close", function() {
ea29e3d7 357 this.customWindows.remove(windowName);
8af49135
CP
358 }.bind(this));
359
ebb21d2e 360 if(cssClass)
e1a91a8a 361 d.lines.addClass("qwebirc-" + cssClass);
ebb21d2e
CP
362
363 var ew = new class_(d.lines, options);
8af49135 364 ew.addEvent("close", function() {
ebb21d2e 365 d.close();
8af49135 366 }.bind(this));
17f40fd9
CP
367
368 d.setSubWindow(ew);
841a451d 369 },
ebb21d2e 370 embeddedWindow: function() {
bcd2d24f 371 this.addCustomWindow("Add webchat to your site", qwebirc.ui.EmbedWizard, "embeddedwizard", {baseURL: this.options.baseURL, uiOptions: this.uiOptions, optionsCallback: function() {
c0f2f367
CP
372 this.optionsWindow();
373 }.bind(this)});
ebb21d2e
CP
374 },
375 optionsWindow: function() {
376 this.addCustomWindow("Options", qwebirc.ui.OptionsPane, "optionspane", this.uiOptions);
377 },
e1a91a8a
CP
378 aboutWindow: function() {
379 this.addCustomWindow("About", qwebirc.ui.AboutPane, "aboutpane", this.uiOptions);
380 },
b35116e2
CP
381 privacyWindow: function() {
382 this.addCustomWindow("Privacy policy", qwebirc.ui.PrivacyPolicyPane, "privacypolicypane", this.uiOptions);
383 },
391f51ff
CP
384 feedbackWindow: function() {
385 this.addCustomWindow("Feedback", qwebirc.ui.FeedbackPane, "feedbackpane", this.uiOptions);
386 },
355dbcb7
CP
387 helpWindow: function() {
388 this.addCustomWindow("Help!", qwebirc.ui.HelpPane, "helppane", this.uiOptions);
f3d0c9f5 389 },
144ee52f 390 urlDispatcher: function(name, window) {
8af49135 391 if(name == "embedded")
925fc357 392 return ["a", this.embeddedWindow.bind(this)];
ebb21d2e
CP
393
394 if(name == "options")
395 return ["a", this.optionsWindow.bind(this)];
8af49135 396
5f2808af
CP
397 /* doesn't really belong here */
398 if(name == "whois") {
399 return ["span", function(nick) {
cbef082a
CP
400 if(this.uiOptions.QUERY_ON_NICK_CLICK) {
401 window.client.exec("/QUERY " + nick);
402 } else {
403 window.client.exec("/WHOIS " + nick);
404 }
405 }.bind(this)];
5f2808af
CP
406 }
407
8af49135 408 return null;
3184781b 409 },
710fd882
CP
410 tabComplete: function(element, backwards) {
411 this.tabCompleter.tabComplete(element, backwards);
3184781b
CP
412 },
413 resetTabComplete: function() {
414 this.tabCompleter.reset();
4dd199c3
CP
415 },
416 setModifiableStylesheet: function(name) {
34d18f7b 417 this.__styleSheet = new qwebirc.ui.style.ModifiableStylesheet(qwebirc.global.staticBaseURL + "css/" + (QWEBIRC_DEBUG ? "debug/" : "") + name + qwebirc.FILE_SUFFIX + ".mcss");
6f8a20df 418 this.setModifiableStylesheetValues({});
4dd199c3 419 },
6f8a20df 420 setModifiableStylesheetValues: function(values) {
ffb5ea9a 421 for (var k in values)
6f8a20df 422 this.__styleValues[k] = values[k];
ffb5ea9a
CP
423
424 if (!$defined(this.__styleSheet))
4dd199c3 425 return;
656385a2 426
ffb5ea9a
CP
427 var back = {hue: this.__styleValues.hue, lightness: this.__styleValues.lightness, saturation: this.__styleValues.saturation};
428 var front;
429 if (!$defined(this.__styleValues.textHue) && !$defined(this.__styleValues.textLightness) && !$defined(this.__styleValues.textSaturation)) {
656385a2 430 front = back;
ffb5ea9a
CP
431 } else {
432 front = {hue: Number(this.__styleValues.textHue), lightness: Number(this.__styleValues.textLightness), saturation: Number(this.__styleValues.textSaturation)}
433 }
656385a2
CP
434 var colours = {
435 back: back,
436 front: front
437 };
438
6f8a20df
CP
439 this.__styleSheet.set(function() {
440 var mode = arguments[0];
441 if(mode == "c") {
656385a2 442 var t = colours[arguments[2]];
6f8a20df 443 var x = new Color(arguments[1]);
656385a2 444 var c = x.setHue(t.hue).setSaturation(x.hsb[1] + t.saturation).setBrightness(x.hsb[2] + t.lightness);
6f8a20df
CP
445 if(c == "255,255,255") /* IE confuses white with transparent... */
446 c = "255,255,254";
447
448 return "rgb(" + c + ")";
449 } else if(mode == "o") {
450 return this.uiOptions[arguments[1]] ? arguments[2] : arguments[3];
451 }
452 }.bind(this));
8af49135 453 }
381fddfd 454});
6f2e4a37 455
326478c2 456qwebirc.ui.NotificationUI = new Class({
e20e5a6b 457 Extends: qwebirc.ui.StandardUI,
fb71087a
CP
458 initialize: function(parentElement, windowClass, uiName, options) {
459 this.parent(parentElement, windowClass, uiName, options);
460
326478c2
CP
461 this.__beeper = new qwebirc.ui.Beeper(this.uiOptions);
462 this.__flasher = new qwebirc.ui.Flasher(this.uiOptions);
f63006ab
CP
463 this.__notifier = new qwebirc.ui.Notifier(this.uiOptions);
464
326478c2 465 this.cancelFlash = this.__flasher.cancelFlash.bind(this.__flasher);
fb71087a 466 },
f63006ab
CP
467 beep: function() {
468 this.__beeper.beep();
469 },
470 notify: function(title, message, callback) {
471 this.__beeper.beep();
472 this.__flasher.flash();
473 this.__notifier.notify(title, message, callback);
474 },
127631e0
CP
475 setBeepOnMention: function(value) {
476 if(value)
326478c2
CP
477 this.__beeper.soundInit();
478 },
f63006ab
CP
479 setNotifications: function(value) {
480 this.__notifier.setEnabled(value);
481 },
326478c2
CP
482 updateTitle: function(text) {
483 if(this.__flasher.updateTitle(text))
484 this.parent(text);
eb271d86
CP
485 },
486 focusChange: function(value) {
487 this.parent(value);
488 this.__flasher.focusChange(value);
f63006ab 489 this.__notifier.focusChange(value);
1211ddcd 490 }
fb71087a
CP
491});
492
5f2808af 493qwebirc.ui.QuakeNetUI = new Class({
e89c812f 494 Extends: qwebirc.ui.NotificationUI,
2cd9e32d
CP
495 urlDispatcher: function(name, window) {
496 if(name == "qwhois") {
7cb09779 497 return ["span", function(auth) {
13afa38f
CP
498 if($defined(this.parentObject.options.accountWhoisCommand))
499 this.client.exec(this.parentObject.options.accountWhoisCommand + auth);
925fc357
CP
500 }.bind(window)];
501 }
144ee52f 502 return this.parent(name, window);
ffbb638d
CP
503 },
504 logout: function() {
505 if(!qwebirc.auth.loggedin())
506 return;
507 if(confirm("Log out?")) {
ea29e3d7
CP
508 this.clients.each(function(k, v) {
509 v.quit("Logged out");
510 }, this);
4b9f894d
CP
511
512 /* HACK */
fbe5af77 513 var foo = function() { document.location = qwebirc.global.dynamicBaseURL + "auth?logout=1"; };
4b9f894d 514 foo.delay(500);
ffbb638d 515 }
2cd9e32d
CP
516 }
517});
144ee52f
CP
518
519qwebirc.ui.RootUI = qwebirc.ui.QuakeNetUI;
fbe5af77
CP
520
521qwebirc.ui.RequestTransformHTML = function(options) {
522 var HREF_ELEMENTS = {
523 "IMG": 1
524 };
525
526 var update = options.update;
527 var onSuccess = options.onSuccess;
528
529 var fixUp = function(node) {
530 if(node.nodeType != 1)
531 return;
532
533 var tagName = node.nodeName.toUpperCase();
534 if(HREF_ELEMENTS[tagName]) {
535 var attr = node.getAttribute("transform_attr");
536 var value = node.getAttribute("transform_value");
537 if($defined(attr) && $defined(value)) {
538 node.removeAttribute("transform_attr");
539 node.removeAttribute("transform_value");
540 node.setAttribute(attr, qwebirc.global.staticBaseURL + value);
541 }
542 }
543
544 for(var i=0;i<node.childNodes.length;i++)
545 fixUp(node.childNodes[i]);
546 };
547
548 delete options["update"];
549 options.onSuccess = function(tree, elements, html, js) {
550 var container = new Element("div");
551 container.set("html", html);
552 fixUp(container);
553 update.empty();
554
555 while(container.childNodes.length > 0) {
556 var x = container.firstChild;
557 container.removeChild(x);
558 update.appendChild(x);
559 }
560 onSuccess();
561 };
562
563 return new Request.HTML(options);
564};
565