summaryrefslogtreecommitdiffstats
path: root/comm/suite/chatzilla/xul/content/static.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/suite/chatzilla/xul/content/static.js')
-rw-r--r--comm/suite/chatzilla/xul/content/static.js5639
1 files changed, 5639 insertions, 0 deletions
diff --git a/comm/suite/chatzilla/xul/content/static.js b/comm/suite/chatzilla/xul/content/static.js
new file mode 100644
index 0000000000..8ee7753210
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/static.js
@@ -0,0 +1,5639 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+#expand const __cz_version = "__CHATZILLA_VERSION__";
+const __cz_condition = "green";
+
+var warn;
+var ASSERT;
+var TEST;
+
+if (DEBUG)
+{
+ _dd_pfx = "cz: ";
+ warn = function (msg) { dumpln ("** WARNING " + msg + " **"); }
+ TEST = ASSERT = function _assert(expr, msg) {
+ if (!expr) {
+ dd("** ASSERTION FAILED: " + msg + " **\n" +
+ getStackTrace() + "\n");
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+else
+ dd = warn = TEST = ASSERT = function (){};
+
+var client = new Object();
+
+client.TYPE = "IRCClient";
+client.COMMAND_CHAR = "/";
+client.STEP_TIMEOUT = 500;
+client.MAX_MESSAGES = 200;
+client.MAX_HISTORY = 50;
+/* longest nick to show in display before forcing the message to a block level
+ * element */
+client.MAX_NICK_DISPLAY = 14;
+/* longest word to show in display before abbreviating */
+client.MAX_WORD_DISPLAY = 20;
+
+client.NOTIFY_TIMEOUT = 5 * 60 * 1000; /* update notify list every 5 minutes */
+
+// Check every minute which networks have away statuses that need an update.
+client.AWAY_TIMEOUT = 60 * 1000;
+
+client.SLOPPY_NETWORKS = true; /* true if msgs from a network can be displayed
+ * on the current object if it is related to
+ * the network (ie, /whois results will appear
+ * on the channel you're viewing, if that channel
+ * is on the network that the results came from)
+ */
+client.DOUBLETAB_TIME = 500;
+client.HIDE_CODES = true; /* true if you'd prefer to show numeric response
+ * codes as some default value (ie, "===") */
+client.DEFAULT_RESPONSE_CODE = "===";
+
+/* Maximum number of channels we'll try to list without complaining */
+client.SAFE_LIST_COUNT = 500;
+
+/* Minimum number of users above or below the conference limit the user count
+ * must go, before it is changed. This allows the user count to fluctuate
+ * around the limit without continously going on and off.
+ */
+client.CONFERENCE_LOW_PASS = 10;
+
+client.viewsArray = new Array();
+client.activityList = new Object();
+client.hostCompat = new Object();
+client.inputHistory = new Array();
+client.lastHistoryReferenced = -1;
+client.incompleteLine = "";
+client.lastTabUp = new Date();
+client.awayMsgs = new Array();
+client.awayMsgCount = 5;
+client.statusMessages = new Array();
+
+CIRCNetwork.prototype.INITIAL_CHANNEL = "";
+CIRCNetwork.prototype.STS_MODULE = new CIRCSTS();
+CIRCNetwork.prototype.MAX_MESSAGES = 100;
+CIRCNetwork.prototype.IGNORE_MOTD = false;
+CIRCNetwork.prototype.RECLAIM_WAIT = 15000;
+CIRCNetwork.prototype.RECLAIM_TIMEOUT = 400000;
+CIRCNetwork.prototype.MIN_RECONNECT_MS = 15 * 1000; // 15s
+CIRCNetwork.prototype.MAX_RECONNECT_MS = 2 * 60 * 60 * 1000; // 2h
+
+CIRCServer.prototype.READ_TIMEOUT = 0;
+CIRCServer.prototype.PRUNE_OLD_USERS = 0; // prune on user quit.
+
+CIRCUser.prototype.MAX_MESSAGES = 200;
+
+CIRCChannel.prototype.MAX_MESSAGES = 300;
+
+function init()
+{
+ if (("initialized" in client) && client.initialized)
+ return;
+
+ client.initialized = false;
+
+ client.networks = new Object();
+ client.entities = new Object();
+ client.eventPump = new CEventPump (200);
+
+ if (DEBUG)
+ {
+ /* hook all events EXCEPT server.poll and *.event-end types
+ * (the 4th param inverts the match) */
+ client.debugHook =
+ client.eventPump.addHook([{type: "poll", set:/^(server|dcc-chat)$/},
+ {type: "event-end"}], event_tracer,
+ "event-tracer", true /* negate */,
+ false /* disable */);
+ }
+
+ initApplicationCompatibility();
+ initMessages();
+
+ initCommands();
+ initPrefs();
+ initMunger();
+ initNetworks();
+ initMenus();
+ initStatic();
+ initHandlers();
+
+ // Create DCC handler.
+ client.dcc = new CIRCDCC(client);
+
+ client.ident = new IdentServer(client);
+
+ // Initialize the STS module.
+ var stsFile = new nsLocalFile(client.prefs["profilePath"]);
+ stsFile.append("sts.json");
+
+ client.sts = CIRCNetwork.prototype.STS_MODULE;
+ client.sts.init(stsFile);
+ client.sts.ENABLED = client.prefs["sts.enabled"];
+
+ // Start log rotation checking first. This will schedule the next check.
+ checkLogFiles();
+ // Start logging. Nothing should call display() before this point.
+ if (client.prefs["log"])
+ client.openLogFile(client);
+
+ // Make sure the userlist is on the correct side.
+ updateUserlistSide(client.prefs["userlistLeft"]);
+
+ client.display(MSG_WELCOME, "HELLO");
+ client.dispatch("set-current-view", { view: client });
+
+ /*
+ * Due to Firefox 44 changes regarding ES6 lexical scope, these 'const'
+ * items are no longer accessible from the global object ('window') but
+ * are required by the output window. The compromise is to copy them on
+ * to the global object so they can be used.
+ */
+ window.__cz_version = __cz_version;
+ window.__cz_condition = __cz_condition;
+ window.NET_CONNECTING = NET_CONNECTING;
+
+ importFromFrame("updateHeader");
+ importFromFrame("setHeaderState");
+ importFromFrame("changeCSS");
+ importFromFrame("scrollToElement");
+ importFromFrame("updateMotifSettings");
+ importFromFrame("addUsers");
+ importFromFrame("updateUsers");
+ importFromFrame("removeUsers");
+
+ processStartupScripts();
+
+ client.commandManager.installKeys(document);
+ createMenus();
+
+ client.busy = false;
+ updateProgress();
+ initOfflineIcon();
+ updateAlertIcon(false);
+ client.isIdleAway = false;
+ initIdleAutoAway(client.prefs["awayIdleTime"]);
+
+ client.initialized = true;
+
+ dispatch("help", { hello: true });
+ dispatch("networks");
+
+ setTimeout(function() {
+ dispatch("focus-input");
+ }, 0);
+ setTimeout(processStartupAutoperform, 0);
+ setTimeout(processStartupURLs, 0);
+}
+
+function initStatic()
+{
+ client.mainWindow = window;
+
+ try
+ {
+ const nsISound = Components.interfaces.nsISound;
+ client.sound =
+ Components.classes["@mozilla.org/sound;1"].createInstance(nsISound);
+
+ client.soundList = new Object();
+ }
+ catch (ex)
+ {
+ dd("Sound failed to initialize: " + ex);
+ }
+
+ try
+ {
+ const nsIAlertsService = Components.interfaces.nsIAlertsService;
+ client.alert = new Object();
+ client.alert.service =
+ Components.classes["@mozilla.org/alerts-service;1"].getService(nsIAlertsService);
+ client.alert.alertList = new Object();
+ client.alert.floodProtector = new FloodProtector(
+ client.prefs['alert.floodDensity'],
+ client.prefs['alert.floodDispersion']);
+ }
+ catch (ex)
+ {
+ dd("Alert service failed to initialize: " + ex);
+ client.alert = null;
+ }
+
+ try
+ {
+ // Mmmm, fun. This ONLY affects the ChatZilla window, don't worry!
+ Date.prototype.toStringInt = Date.prototype.toString;
+ Date.prototype.toString = function() {
+ let dtf = new Services.intl.DateTimeFormat(undefined,
+ { dateStyle: "full",
+ timeStyle: "long" });
+ return dtf.format(this);
+ }
+ }
+ catch (ex)
+ {
+ dd("Locale-correct date formatting failed to initialize: " + ex);
+ }
+
+ // XXX Bug 335998: See cmdHideView for usage of this.
+ client.hiddenDocument = document.implementation.createDocument(null, null, null);
+
+ multilineInputMode(client.prefs["multiline"]);
+ updateSpellcheck(client.prefs["inputSpellcheck"]);
+
+ // Initialize userlist stuff
+ if (client.prefs["showModeSymbols"])
+ setListMode("symbol");
+ else
+ setListMode("graphic");
+
+ var tree = document.getElementById('user-list');
+ tree.setAttribute("ondragstart", "userlistDNDObserver.onDragStart(event);");
+
+ setDebugMode(client.prefs["debugMode"]);
+
+ var version = getVersionInfo();
+ client.userAgent = getMsg(MSG_VERSION_REPLY, [version.cz, version.ua]);
+ CIRCServer.prototype.VERSION_RPLY = client.userAgent;
+ CIRCServer.prototype.HOST_RPLY = version.host;
+ CIRCServer.prototype.SOURCE_RPLY = MSG_SOURCE_REPLY;
+
+ client.statusBar = new Object();
+
+ client.statusBar["server-nick"] = document.getElementById("server-nick");
+
+ client.tabs = document.getElementById("views-tbar-inner");
+ client.tabDragBar = document.getElementById("tabs-drop-indicator-bar");
+ client.tabDragMarker = document.getElementById("tabs-drop-indicator");
+
+ client.statusElement = document.getElementById("status-text");
+ client.currentStatus = "";
+ client.defaultStatus = MSG_DEFAULT_STATUS;
+
+ client.progressPanel = document.getElementById("status-progress-panel");
+ client.progressBar = document.getElementById("status-progress-bar");
+
+ client.logFile = null;
+ setInterval(onNotifyTimeout, client.NOTIFY_TIMEOUT);
+ // Call every minute, will check only the networks necessary.
+ setInterval(onWhoTimeout, client.AWAY_TIMEOUT);
+
+ client.awayMsgs = [{ message: MSG_AWAY_DEFAULT }];
+ var awayFile = new nsLocalFile(client.prefs["profilePath"]);
+ awayFile.append("awayMsgs.txt");
+ if (awayFile.exists())
+ {
+ var awayLoader = new TextSerializer(awayFile);
+ if (awayLoader.open("<"))
+ {
+ // Load the first item from the file.
+ var item = awayLoader.deserialize();
+ if (isinstance(item, Array))
+ {
+ // If the first item is an array, it is the entire thing.
+ client.awayMsgs = item;
+ }
+ else if (item != null)
+ {
+ /* Not an array, so we have the old format of a single object
+ * per entry.
+ */
+ client.awayMsgs = [item];
+ while ((item = awayLoader.deserialize()))
+ client.awayMsgs.push(item);
+ }
+ awayLoader.close();
+
+ /* we have to close the file before we can move it,
+ * hence the second if statement */
+ if (item == null)
+ {
+ var invalidFile = new nsLocalFile(client.prefs["profilePath"]);
+ invalidFile.append("awayMsgs.invalid");
+ invalidFile.createUnique(FTYPE_FILE, 0o600);
+ var msg = getMsg(MSG_ERR_INVALID_FILE,
+ [awayFile.leafName, invalidFile.leafName]);
+ setTimeout(function() {
+ client.display(msg, MT_WARN);
+ }, 0);
+ awayFile.moveTo(null, invalidFile.leafName);
+ }
+ }
+ }
+
+ // Get back input history from previous session:
+ var inputHistoryFile = new nsLocalFile(client.prefs["profilePath"]);
+ inputHistoryFile.append("inputHistory.txt");
+ try
+ {
+ client.inputHistoryLogger = new TextLogger(inputHistoryFile.path,
+ client.MAX_HISTORY);
+ }
+ catch (ex)
+ {
+ msg = getMsg(MSG_ERR_INPUTHISTORY_NOT_WRITABLE, inputHistoryFile.path);
+ setTimeout(function() {
+ client.display(msg, MT_ERROR);
+ }, 0);
+ dd(formatException(ex));
+ client.inputHistoryLogger = null;
+ }
+ if (client.inputHistoryLogger)
+ client.inputHistory = client.inputHistoryLogger.read().reverse();
+
+ // Set up URL collector.
+ var urlsFile = new nsLocalFile(client.prefs["profilePath"]);
+ urlsFile.append("urls.txt");
+ try
+ {
+ client.urlLogger = new TextLogger(urlsFile.path,
+ client.prefs["urls.store.max"]);
+ }
+ catch (ex)
+ {
+ msg = getMsg(MSG_ERR_URLS_NOT_WRITABLE, urlsFile.path);
+ setTimeout(function() {
+ client.display(msg, MT_ERROR);
+ }, 0);
+ dd(formatException(ex));
+ client.urlLogger = null;
+ }
+
+ // Migrate old list preference to file.
+ try
+ {
+ // Throws if the preference doesn't exist.
+ if (client.urlLogger)
+ var urls = client.prefManager.prefBranch.getCharPref("urls.list");
+ }
+ catch (ex)
+ {
+ }
+ if (urls)
+ {
+ // Add the old URLs to the new file.
+ urls = client.prefManager.stringToArray(urls);
+ for (var i = 0; i < urls.length; i++)
+ client.urlLogger.append(urls[i]);
+ // Completely purge the old preference.
+ client.prefManager.prefBranch.clearUserPref("urls.list");
+ }
+
+ client.defaultCompletion = client.COMMAND_CHAR + "help ";
+
+ client.deck = document.getElementById('output-deck');
+}
+
+function getVersionInfo()
+{
+ var version = new Object();
+ version.cz = __cz_version;
+
+ var app = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+ version.hostName = app.vendor + " " + app.name;
+ version.hostVersion = app.version;
+ version.host = version.hostName + " " + version.hostVersion + ", " +
+ client.platform;
+ version.hostBuildID = app.platformBuildID;
+ version.ua = app.name + " " + app.version + "/" + version.hostBuildID;
+
+ return version;
+}
+
+function initApplicationCompatibility()
+{
+ // This function does nothing more than tweak the UI based on the platform.
+
+ client.lineEnd = "\n";
+
+ // Set up simple platform information.
+ switch (AppConstants.platform) {
+ case "linux":
+ client.platform = "Linux";
+ break;
+ case "macosx":
+ client.platform = "Mac";
+ break;
+ case "win":
+ client.platform = "Windows";
+ // Windows likes \r\n line endings, as notepad can't cope with just
+ // \n logs.
+ client.lineEnd = "\r\n";
+ break;
+ default:
+ client.platform = "Unknown";
+ }
+
+ CIRCServer.prototype.OS_RPLY = navigator.oscpu + " (" +
+ navigator.platform + ")";
+}
+
+function getFindData(e)
+{
+ // findNext() wrapper to add our findStart/findEnd events.
+ function _cz_findNext() {
+ // Send start notification.
+ var ev = new CEvent("find", "findStart", e.sourceObject, "onFindStart");
+ client.eventPump.routeEvent(ev);
+
+ // Call the original findNext() and keep the result for later.
+ var rv = this.__proto__.findNext();
+
+ // Send end notification with result code.
+ var ev = new CEvent("find", "findEnd", e.sourceObject, "onFindEnd");
+ ev.findResult = rv;
+ client.eventPump.routeEvent(ev);
+
+ // Return the original findNext()'s result to keep up appearances.
+ return rv;
+ };
+
+ // Getter for webBrowserFind property.
+ function _cz_webBrowserFind() {
+ return this._cz_wbf;
+ };
+
+ var findData = new nsFindInstData();
+ findData.browser = e.sourceObject.frame;
+ findData.rootSearchWindow = getContentWindow(e.sourceObject.frame);
+ findData.currentSearchWindow = getContentWindow(e.sourceObject.frame);
+
+ /* Wrap up the webBrowserFind object so we get called for findNext(). Use
+ * __proto__ so that everything else is exactly like the original object.
+ */
+ findData._cz_wbf = { findNext: _cz_findNext };
+ findData._cz_wbf.__proto__ = findData.webBrowserFind;
+
+ /* Replace the nsFindInstData getter for webBrowserFind to call our
+ * function which in turn returns our object (_cz_wbf).
+ */
+ findData.__defineGetter__("webBrowserFind", _cz_webBrowserFind);
+
+ /* Yay, evil hacks! findData.init doesn't care about the findService, it
+ * gets option settings from webBrowserFind. As we want the wrap option *on*
+ * when we use /find foo, we set it on the findService there. However,
+ * restoring the original value afterwards doesn't help, because init() here
+ * overrides that value. Unless we make .init do something else, of course:
+ */
+ findData._init = findData.init;
+ findData.init =
+ function init()
+ {
+ this._init();
+ const FINDSVC_ID = "@mozilla.org/find/find_service;1";
+ var findService = getService(FINDSVC_ID, "nsIFindService");
+ this.webBrowserFind.wrapFind = findService.wrapFind;
+ };
+
+ return findData;
+}
+
+function importFromFrame(method)
+{
+ client.__defineGetter__(method, import_wrapper);
+ CIRCNetwork.prototype.__defineGetter__(method, import_wrapper);
+ CIRCChannel.prototype.__defineGetter__(method, import_wrapper);
+ CIRCUser.prototype.__defineGetter__(method, import_wrapper);
+ CIRCDCCChat.prototype.__defineGetter__(method, import_wrapper);
+ CIRCDCCFileTransfer.prototype.__defineGetter__(method, import_wrapper);
+
+ function import_wrapper()
+ {
+ var dummy = function(){};
+
+ if (!("frame" in this))
+ return dummy;
+
+ try
+ {
+ var window = getContentWindow(this.frame);
+ if (window && "initialized" in window && window.initialized &&
+ method in window)
+ {
+ return function import_wrapper_apply()
+ {
+ window[method].apply(this, arguments);
+ };
+ }
+ }
+ catch (ex)
+ {
+ ASSERT(0, "Caught exception calling: " + method + "\n" + ex);
+ }
+
+ return dummy;
+ };
+}
+
+function processStartupScripts()
+{
+ client.plugins = new Object();
+ var scripts = client.prefs["initialScripts"];
+ var basePath = getURLSpecFromFile(client.prefs["profilePath"]);
+ var baseURL = Services.io.newURI(basePath);
+ for (var i = 0; i < scripts.length; ++i)
+ {
+ try
+ {
+ var url = Services.io.newURI(scripts[i], null, baseURL);
+ var path = getFileFromURLSpec(url.spec);
+ }
+ catch(ex)
+ {
+ var params = ["initialScripts", scripts[i]];
+ display(getMsg(MSG_ERR_INVALID_PREF, params), MT_ERROR);
+ dd(formatException(ex));
+ continue;
+ }
+
+ if (url.scheme != "file" && url.scheme != "chrome")
+ {
+ display(getMsg(MSG_ERR_INVALID_SCHEME, scripts[i]), MT_ERROR);
+ continue;
+ }
+
+ if (!path.exists())
+ {
+ display(getMsg(MSG_ERR_ITEM_NOT_FOUND, url.spec), MT_WARN);
+ continue;
+ }
+
+ if (path.isDirectory())
+ loadPluginDirectory(path);
+ else
+ loadLocalFile(path);
+ }
+}
+
+function loadPluginDirectory(localPath, recurse)
+{
+ if (typeof recurse == "undefined")
+ recurse = 1;
+
+ var initPath = localPath.clone();
+ initPath.append("init.js");
+ if (initPath.exists())
+ loadLocalFile(initPath);
+
+ if (recurse < 1)
+ return;
+
+ var enumer = localPath.directoryEntries;
+ while (enumer.hasMoreElements())
+ {
+ var entry = enumer.getNext();
+ entry = entry.QueryInterface(Components.interfaces.nsIFile);
+ if (entry.isDirectory())
+ loadPluginDirectory(entry, recurse - 1);
+ }
+}
+
+function loadLocalFile(localFile)
+{
+ var url = getURLSpecFromFile(localFile);
+ var glob = new Object();
+ dispatch("load", {url: url, scope: glob});
+}
+
+function getPluginById(id)
+{
+ return client.plugins[id] || null;
+}
+
+function getPluginByURL(url)
+{
+ for (var k in client.plugins)
+ {
+ if (client.plugins[k].url == url)
+ return client.plugins[k];
+
+ }
+
+ return null;
+}
+
+function disablePlugin(plugin, destroy)
+{
+ if (!plugin.enabled)
+ {
+ display(getMsg(MSG_IS_DISABLED, plugin.id));
+ return true;
+ }
+
+ if (plugin.API > 0)
+ {
+ if (!plugin.disable())
+ {
+ display(getMsg(MSG_CANT_DISABLE, plugin.id));
+ return false;
+ }
+
+ if (destroy)
+ {
+ client.prefManager.removeObserver(plugin.prefManager);
+ plugin.prefManager.destroy();
+ }
+ else
+ {
+ plugin.prefs["enabled"] = false;
+ }
+ }
+ else if ("disablePlugin" in plugin.scope)
+ {
+ plugin.scope.disablePlugin();
+ }
+ else
+ {
+ display(getMsg(MSG_CANT_DISABLE, plugin.id));
+ return false;
+ }
+
+ display(getMsg(MSG_PLUGIN_DISABLED, plugin.id));
+ if (!destroy)
+ {
+ plugin.enabled = false;
+ }
+ return true;
+}
+
+function processStartupAutoperform()
+{
+ var cmdary = client.prefs["autoperform.client"];
+ for (var i = 0; i < cmdary.length; ++i)
+ {
+ if (cmdary[i][0] == "/")
+ client.dispatch(cmdary[i].substr(1));
+ else
+ client.dispatch(cmdary[i]);
+ }
+}
+
+function processStartupURLs()
+{
+ var wentSomewhere = false;
+
+ if ("arguments" in window &&
+ 0 in window.arguments && typeof window.arguments[0] == "object" &&
+ "url" in window.arguments[0])
+ {
+ var url = window.arguments[0].url;
+ if (url.search(/^ircs?:\/?\/?\/?$/i) == -1)
+ {
+ /* if the url is not irc: irc:/, irc://, or ircs equiv then go to it. */
+ gotoIRCURL(url);
+ wentSomewhere = true;
+ }
+ }
+ /* check to see whether the URL has been passed via the command line
+ instead. */
+ else if ("arguments" in window &&
+ 0 in window.arguments && typeof window.arguments[0] == "string")
+ {
+ var url = window.arguments[0]
+ var urlMatches = url.match(/^ircs?:\/\/\/?(.*)$/)
+ if (urlMatches)
+ {
+ if (urlMatches[1])
+ {
+ /* if the url is not "irc://", "irc:///" or an ircs equiv then
+ go to it. */
+ gotoIRCURL(url);
+ wentSomewhere = true;
+ }
+ }
+ else if (url)
+ {
+ /* URL parameter is not blank, but does not not conform to the
+ irc[s] scheme. */
+ display(getMsg(MSG_ERR_INVALID_SCHEME, url), MT_ERROR);
+ }
+ }
+
+ /* if we had nowhere else to go, connect to any default urls */
+ if (!wentSomewhere)
+ openStartupURLs();
+
+ if (client.viewsArray.length > 1 && !isStartupURL("irc://"))
+ dispatch("delete-view", { view: client });
+
+ /* XXX: If we have the "stop XBL breaking" hidden tab, remove it, to
+ * stop XBL breaking later. Oh, the irony.
+ */
+ if (client.tabs.firstChild.hidden)
+ {
+ client.tabs.removeChild(client.tabs.firstChild);
+ updateTabAttributes();
+ }
+}
+
+function openStartupURLs()
+{
+ var ary = client.prefs["initialURLs"];
+ for (var i = 0; i < ary.length; ++i)
+ {
+ if (ary[i] && ary[i] == "irc:///")
+ {
+ // Clean out "default network" entries, which we don't
+ // support any more; replace with the harmless irc:// URL.
+ ary[i] = "irc://";
+ client.prefs["initialURLs"].update();
+ }
+ if (ary[i] && ary[i] != "irc://")
+ gotoIRCURL(ary[i]);
+ }
+}
+
+function destroy()
+{
+ destroyPrefs();
+}
+
+function addURLToHistory(url) {
+ url = Services.io.newURI(url, "UTF-8");
+ PlacesUtils.history.insert({
+ url,
+ visits: [{
+ date: new Date(),
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ }],
+ });
+}
+
+function addStatusMessage(message)
+{
+ const DELAY_SCALE = 100;
+ const DELAY_MINIMUM = 5000;
+
+ var delay = message.length * DELAY_SCALE;
+ if (delay < DELAY_MINIMUM)
+ delay = DELAY_MINIMUM;
+
+ client.statusMessages.push({ message: message, delay: delay });
+ updateStatusMessages();
+}
+
+function updateStatusMessages()
+{
+ if (client.statusMessages.length == 0)
+ {
+ var status = client.currentStatus || client.defaultStatus;
+ client.statusElement.setAttribute("label", status);
+ client.statusElement.removeAttribute("notice");
+ return;
+ }
+
+ var now = Number(new Date());
+ var currentMsg = client.statusMessages[0];
+ if ("expires" in currentMsg)
+ {
+ if (now >= currentMsg.expires)
+ {
+ client.statusMessages.shift();
+ setTimeout(updateStatusMessages, 0);
+ }
+ else
+ {
+ setTimeout(updateStatusMessages, 1000);
+ }
+ }
+ else
+ {
+ currentMsg.expires = now + currentMsg.delay;
+ client.statusElement.setAttribute("label", currentMsg.message);
+ client.statusElement.setAttribute("notice", "true");
+ setTimeout(updateStatusMessages, currentMsg.delay);
+ }
+}
+
+
+function setStatus(str)
+{
+ client.currentStatus = str;
+ updateStatusMessages();
+ return str;
+}
+
+client.__defineSetter__("status", setStatus);
+
+function getStatus()
+{
+ return client.currentStatus;
+}
+
+client.__defineGetter__("status", getStatus);
+
+function isVisible (id)
+{
+ var e = document.getElementById(id);
+
+ if (!ASSERT(e,"Bogus id ``" + id + "'' passed to isVisible() **"))
+ return false;
+
+ return (e.getAttribute ("collapsed") != "true");
+}
+
+client.getConnectedNetworks =
+function getConnectedNetworks()
+{
+ var rv = [];
+ for (var n in client.networks)
+ {
+ if (client.networks[n].isConnected())
+ rv.push(client.networks[n]);
+ }
+ return rv;
+}
+
+function combineNicks(nickList, max)
+{
+ if (!max)
+ max = 4;
+
+ var combinedList = [];
+
+ for (var i = 0; i < nickList.length; i += max)
+ {
+ count = Math.min(max, nickList.length - i);
+ var nicks = nickList.slice(i, i + count);
+ var str = new String(nicks.join(" "));
+ str.count = count;
+ combinedList.push(str);
+ }
+
+ return combinedList;
+}
+
+function updateAllStalkExpressions()
+{
+ var list = client.prefs["stalkWords"];
+
+ for (var name in client.networks)
+ {
+ if ("stalkExpression" in client.networks[name])
+ updateStalkExpression(client.networks[name], list);
+ }
+}
+
+function updateStalkExpression(network)
+{
+ function escapeChar(ch)
+ {
+ return "\\" + ch;
+ };
+
+ var list = client.prefs["stalkWords"];
+
+ var ary = new Array();
+
+ ary.push(network.primServ.me.unicodeName.replace(/[^\w\d]/g, escapeChar));
+
+ for (var i = 0; i < list.length; ++i)
+ ary.push(list[i].replace(/[^\w\d]/g, escapeChar));
+
+ var re;
+ if (client.prefs["stalkWholeWords"])
+ re = "(^|[\\W\\s])((" + ary.join(")|(") + "))([\\W\\s]|$)";
+ else
+ re = "(" + ary.join(")|(") + ")";
+
+ network.stalkExpression = new RegExp(re, "i");
+}
+
+function getDefaultFontSize()
+{
+ const PREF_CTRID = "@mozilla.org/preferences-service;1";
+ const nsIPrefService = Components.interfaces.nsIPrefService;
+ const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+
+ var prefSvc = Components.classes[PREF_CTRID].getService(nsIPrefService);
+ var prefBranch = prefSvc.getBranch(null);
+
+ // PX size pref: font.size.variable.x-western
+ var pxSize = 16;
+ try
+ {
+ pxSize = prefBranch.getIntPref("font.size.variable.x-western");
+ }
+ catch(ex) { }
+
+ var dpi = 96;
+ try
+ {
+ // Get the DPI the fun way (make Mozilla do the work).
+ var b = document.createElement("box");
+ b.style.width = "1in";
+ dpi = window.getComputedStyle(b, null).width.match(/^\d+/);
+ }
+ catch(ex)
+ {
+ try
+ {
+ // Get the DPI the fun way (make Mozilla do the work).
+ b = document.createElementNS("box", XHTML_NS);
+ b.style.width = "1in";
+ dpi = window.getComputedStyle(b, null).width.match(/^\d+/);
+ }
+ catch(ex) { }
+ }
+
+ return Math.round((pxSize / dpi) * 72);
+}
+
+function getDefaultContext(cx)
+{
+ if (!cx)
+ cx = new Object();
+ /* Use __proto__ here and in all other get*Context so that the command can
+ * tell the difference between getObjectDetails and actual parameters. See
+ * cmdJoin for more details.
+ */
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ return cx;
+}
+
+function getMessagesContext(cx, element)
+{
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ if (!element)
+ element = document.popupNode;
+
+ while (element)
+ {
+ switch (element.localName)
+ {
+ case "a":
+ var href = element.getAttribute("href");
+ cx.url = href;
+ break;
+
+ case "tr":
+ // NOTE: msg-user is the canonicalName.
+ cx.canonNick = element.getAttribute("msg-user");
+ if (!cx.canonNick)
+ break;
+
+ // Strip out a potential ME! suffix.
+ var ary = cx.canonNick.match(/([^ ]+)/);
+ cx.canonNick = ary[1];
+
+ if (!cx.network)
+ break;
+
+ if (cx.channel)
+ cx.user = cx.channel.getUser(cx.canonNick);
+ else
+ cx.user = cx.network.getUser(cx.canonNick);
+
+ if (cx.user)
+ cx.nickname = cx.user.unicodeName;
+ else
+ cx.nickname = toUnicode(cx.canonNick, cx.network);
+ break;
+ }
+
+ element = element.parentNode;
+ }
+
+ return cx;
+}
+
+function getTabContext(cx, element)
+{
+ if (!cx)
+ cx = new Object();
+ if (!element)
+ element = document.popupNode;
+
+ while (element)
+ {
+ if (element.localName == "tab")
+ {
+ cx.__proto__ = getObjectDetails(element.view);
+ return cx;
+ }
+ element = element.parentNode;
+ }
+
+ return cx;
+}
+
+function getUserlistContext(cx)
+{
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ if (!cx.channel)
+ return cx;
+
+ var user, tree = document.getElementById("user-list");
+ cx.userList = new Array();
+ cx.canonNickList = new Array();
+ cx.nicknameList = getSelectedNicknames(tree);
+
+ for (var i = 0; i < cx.nicknameList.length; ++i)
+ {
+ user = cx.channel.getUser(cx.nicknameList[i])
+ cx.userList.push(user);
+ cx.canonNickList.push(user.canonicalName);
+ if (i == 0)
+ {
+ cx.user = user;
+ cx.nickname = user.unicodeName;
+ cx.canonNick = user.canonicalName;
+ }
+ }
+ cx.userCount = cx.userList.length;
+
+ return cx;
+}
+
+function getViewsContext(cx)
+{
+ function addView(view)
+ {
+ // We only need the view to have messages, so we accept hidden views.
+ if (!("messages" in view))
+ return;
+
+ var url = view.getURL();
+ if (url in urls)
+ return;
+
+ var label = view.viewName;
+ if (!getTabForObject(view))
+ label = getMsg(MSG_VIEW_HIDDEN, [label]);
+
+ var types = ["IRCClient", "IRCNetwork", "IRCDCCChat",
+ "IRCDCCFileTransfer"];
+ var typesNetwork = ["IRCNetwork", "IRCChannel", "IRCUser"];
+ var group = String(arrayIndexOf(types, view.TYPE));
+ if (arrayIndexOf(typesNetwork, view.TYPE) != -1)
+ group = "1-" + getObjectDetails(view).network.viewName;
+
+ var sort = group + "-" + view.viewName;
+ if (view.TYPE == "IRCNetwork")
+ sort = group;
+
+ cx.views.push({url: url, label: label, group: group, sort: sort});
+ urls[url] = true
+ };
+
+ function sortViews(a, b)
+ {
+ if (a.sort < b.sort)
+ return -1;
+ if (a.sort > b.sort)
+ return 1;
+ return 0;
+ };
+
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+
+ cx.views = new Array();
+ var urls = new Object();
+
+ /* XXX The code here works its way through all the open views *and* any
+ * possibly visible objects in the object model. This is necessary because
+ * occasionally objects get removed from the object model while still
+ * having a view open. See bug 459318 for one such case. Note that we
+ * won't be able to correctly switch to the "lost" view but showing it is
+ * less confusing than not.
+ */
+
+ for (var i in client.viewsArray)
+ addView(client.viewsArray[i].source);
+
+ addView(client);
+ for (var n in client.networks)
+ {
+ addView(client.networks[n]);
+ for (var s in client.networks[n].servers) {
+ var server = client.networks[n].servers[s];
+ for (var c in server.channels)
+ addView(server.channels[c]);
+ for (var u in server.users)
+ addView(server.users[u]);
+ }
+ }
+
+ for (var u in client.dcc.users)
+ addView(client.dcc.users[u]);
+ for (var i = 0; i < client.dcc.chats.length; i++)
+ addView(client.dcc.chats[i]);
+ for (var i = 0; i < client.dcc.files.length; i++)
+ addView(client.dcc.files[i]);
+
+ cx.views.sort(sortViews);
+
+ return cx;
+}
+
+function getSelectedNicknames(tree)
+{
+ var rv = [];
+ if (!tree || !tree.view || !tree.view.selection)
+ return rv;
+ var rangeCount = tree.view.selection.getRangeCount();
+
+ // Loop through the selection ranges.
+ for (var i = 0; i < rangeCount; ++i)
+ {
+ var start = {}, end = {};
+ tree.view.selection.getRangeAt(i, start, end);
+
+ // If they == -1, we've got no selection, so bail.
+ if ((start.value == -1) && (end.value == -1))
+ continue;
+ /* Workaround: Because we use select(-1) instead of clearSelection()
+ * (see bug 197667) the tree will then give us selection ranges
+ * starting from -1 instead of 0! (See bug 319066.)
+ */
+ if (start.value == -1)
+ start.value = 0;
+
+ // Loop through the contents of the current selection range.
+ for (var k = start.value; k <= end.value; ++k)
+ rv.push(getNicknameForUserlistRow(k));
+ }
+ return rv;
+}
+
+function getFontContext(cx)
+{
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ cx.fontSizeDefault = getDefaultFontSize();
+ var view = client;
+
+ if ("prefs" in cx.sourceObject)
+ {
+ cx.fontFamily = view.prefs["font.family"];
+ if (cx.fontFamily.match(/^(default|(sans-)?serif|monospace)$/))
+ delete cx.fontFamily;
+
+ cx.fontSize = view.prefs["font.size"];
+ if (cx.fontSize == 0)
+ delete cx.fontSize;
+ }
+
+ return cx;
+}
+
+function msgIsImportant(msg, sourceNick, network)
+{
+ var plainMsg = removeColorCodes(msg);
+
+ var re = network.stalkExpression;
+ if (plainMsg.search(re) != -1 || sourceNick && sourceNick.search(re) == 0)
+ return true;
+
+ return false;
+}
+
+function ensureCachedCanonicalURLs(array)
+{
+ if ("canonicalURLs" in array)
+ return;
+
+ /* Caching this on the array is safe because the PrefManager constructs
+ * a new array if the preference changes, but otherwise keeps the same
+ * one around.
+ */
+ array.canonicalURLs = new Array();
+ for (var i = 0; i < array.length; i++)
+ array.canonicalURLs.push(makeCanonicalIRCURL(array[i]));
+}
+
+function isStartupURL(url)
+{
+ // We canonicalize all URLs before we do the (string) comparison.
+ url = makeCanonicalIRCURL(url);
+ var list = client.prefs["initialURLs"];
+ ensureCachedCanonicalURLs(list);
+ return arrayContains(list.canonicalURLs, url);
+}
+
+function cycleView(amount)
+{
+ var len = client.viewsArray.length;
+ if (len <= 1)
+ return;
+
+ var tb = getTabForObject (client.currentObject);
+ if (!tb)
+ return;
+
+ var vk = Number(tb.getAttribute("viewKey"));
+ var destKey = (vk + amount) % len; /* wrap around */
+ if (destKey < 0)
+ destKey += len;
+
+ dispatch("set-current-view", { view: client.viewsArray[destKey].source });
+}
+
+// Plays the sound for a particular event on a type of object.
+function playEventSounds(type, event, source)
+{
+ if (!client.sound || !client.prefs["sound.enabled"])
+ return;
+
+ // Converts .TYPE values into the event object names.
+ // IRCChannel => channel, IRCUser => user, etc.
+ if (type.match(/^IRC/))
+ type = type.substr(3, type.length).toLowerCase();
+
+ // DCC Chat sessions should act just like user views.
+ if (type == "dccchat")
+ type = "user";
+
+ var ev = type + "." + event;
+
+ if (ev in client.soundList)
+ return;
+
+ var src = source ? source : client;
+
+ if (!(("sound." + ev) in src.prefs))
+ return;
+
+ var s = src.prefs["sound." + ev];
+
+ if (!s)
+ return;
+
+ if (client.prefs["sound.overlapDelay"] > 0)
+ {
+ client.soundList[ev] = true;
+ setTimeout(function() {
+ delete client.soundList[ev];
+ }, client.prefs["sound.overlapDelay"]);
+ }
+
+ if (event == "start")
+ {
+ blockEventSounds(type, "event");
+ blockEventSounds(type, "chat");
+ blockEventSounds(type, "stalk");
+ }
+
+ playSounds(s);
+}
+
+// Blocks a particular type of event sound occuring.
+function blockEventSounds(type, event)
+{
+ if (!client.sound || !client.prefs["sound.enabled"])
+ return;
+
+ // Converts .TYPE values into the event object names.
+ // IRCChannel => channel, IRCUser => user, etc.
+ if (type.match(/^IRC/))
+ type = type.substr(3, type.length).toLowerCase();
+
+ var ev = type + "." + event;
+
+ if (client.prefs["sound.overlapDelay"] > 0)
+ {
+ client.soundList[ev] = true;
+ setTimeout(function() {
+ delete client.soundList[ev];
+ }, client.prefs["sound.overlapDelay"]);
+ }
+}
+
+function playSounds(list)
+{
+ var ary = list.split (" ");
+ if (ary.length == 0)
+ return;
+
+ playSound(ary[0]);
+ for (var i = 1; i < ary.length; ++i)
+ setTimeout(playSound, 250 * i, ary[i]);
+}
+
+function playSound(file)
+{
+ if (!client.sound || !client.prefs["sound.enabled"] || !file)
+ return;
+
+ if (file == "beep")
+ {
+ client.sound.beep();
+ }
+ else
+ {
+ try
+ {
+ client.sound.play(Services.io.newURI(file));
+ }
+ catch (ex)
+ {
+ // ignore exceptions from this pile of code.
+ }
+ }
+}
+
+/* timer-based mainloop */
+function mainStep()
+{
+ try
+ {
+ var count = client.eventPump.stepEvents();
+ if (count > 0)
+ setTimeout(mainStep, client.STEP_TIMEOUT);
+ else
+ setTimeout(mainStep, client.STEP_TIMEOUT / 5);
+ }
+ catch(ex)
+ {
+ dd("Exception in mainStep!");
+ dd(formatException(ex));
+ setTimeout(mainStep, client.STEP_TIMEOUT);
+ }
+}
+
+function openQueryTab(server, nick)
+{
+ var user = server.addUser(nick);
+ addURLToHistory(user.getURL());
+ if (!("messages" in user))
+ {
+ var value = "";
+ var same = true;
+ for (var c in server.channels)
+ {
+ var chan = server.channels[c];
+ if (!(user.collectionKey in chan.users))
+ continue;
+ /* This takes a boolean value for each channel (true - channel has
+ * same value as first), and &&-s them all together. Thus, |same|
+ * will tell us, at the end, if all the channels found have the
+ * same value for charset.
+ */
+ if (value)
+ same = same && (value == chan.prefs["charset"]);
+ else
+ value = chan.prefs["charset"];
+ }
+ /* If we've got a value, and it's the same accross all channels,
+ * we use it as the *default* for the charset pref. If not, it'll
+ * just keep the "defer" default which pulls it off the network.
+ */
+ if (value && same)
+ {
+ user.prefManager.prefRecords["charset"].defaultValue = value;
+ }
+
+ dispatch("create-tab-for-view", { view: user });
+
+ user.doAutoPerform();
+ }
+ return user;
+}
+
+function arraySpeak (ary, single, plural)
+{
+ var rv = "";
+ var and = MSG_AND;
+
+ switch (ary.length)
+ {
+ case 0:
+ break;
+
+ case 1:
+ rv = ary[0];
+ if (single)
+ rv += " " + single;
+ break;
+
+ case 2:
+ rv = ary[0] + " " + and + " " + ary[1];
+ if (plural)
+ rv += " " + plural;
+ break;
+
+ default:
+ for (var i = 0; i < ary.length - 1; ++i)
+ rv += ary[i] + ", ";
+ rv += and + " " + ary[ary.length - 1];
+ if (plural)
+ rv += " " + plural;
+ break;
+ }
+
+ return rv;
+
+}
+
+function getObjectDetails (obj, rv)
+{
+ if (!rv)
+ rv = new Object();
+
+ if (!ASSERT(obj && typeof obj == "object",
+ "INVALID OBJECT passed to getObjectDetails (" + obj + "). **"))
+ {
+ return rv;
+ }
+
+ rv.sourceObject = obj;
+ rv.TYPE = obj.TYPE;
+ rv.parent = ("parent" in obj) ? obj.parent : null;
+ rv.user = null;
+ rv.channel = null;
+ rv.server = null;
+ rv.network = null;
+ if (window && window.content && window.content.getSelection() != "")
+ rv.selectedText = window.content.getSelection();
+
+ switch (obj.TYPE)
+ {
+ case "IRCChannel":
+ rv.viewType = MSG_CHANNEL;
+ rv.channel = obj;
+ rv.channelName = obj.unicodeName;
+ rv.server = rv.channel.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "IRCUser":
+ rv.viewType = MSG_USER;
+ rv.user = obj;
+ rv.userName = rv.nickname = obj.unicodeName;
+ rv.server = rv.user.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "IRCChanUser":
+ rv.viewType = MSG_USER;
+ rv.user = obj;
+ rv.userName = rv.nickname = obj.unicodeName;
+ rv.channel = rv.user.parent;
+ rv.server = rv.channel.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "IRCNetwork":
+ rv.network = obj;
+ rv.viewType = MSG_NETWORK;
+ if ("primServ" in rv.network)
+ rv.server = rv.network.primServ;
+ else
+ rv.server = null;
+ break;
+
+ case "IRCClient":
+ rv.viewType = MSG_TAB;
+ break;
+
+ case "IRCDCCUser":
+ //rv.viewType = MSG_USER;
+ rv.user = obj;
+ rv.userName = obj.unicodeName;
+ break;
+
+ case "IRCDCCChat":
+ //rv.viewType = MSG_USER;
+ rv.chat = obj;
+ rv.user = obj.user;
+ rv.userName = obj.unicodeName;
+ break;
+
+ case "IRCDCCFileTransfer":
+ //rv.viewType = MSG_USER;
+ rv.file = obj;
+ rv.user = obj.user;
+ rv.userName = obj.unicodeName;
+ rv.fileName = obj.filename;
+ break;
+
+ default:
+ /* no setup for unknown object */
+ break;
+ }
+
+ if (rv.network)
+ rv.networkName = rv.network.unicodeName;
+
+ return rv;
+
+}
+
+function findDynamicRule (selector)
+{
+ var rules = frames[0].document.styleSheets[1].cssRules;
+
+ if (isinstance(selector, RegExp))
+ fun = "search";
+ else
+ fun = "indexOf";
+
+ for (var i = 0; i < rules.length; ++i)
+ {
+ var rule = rules.item(i);
+ if (rule.selectorText && rule.selectorText[fun](selector) == 0)
+ return {sheet: frames[0].document.styleSheets[1], rule: rule,
+ index: i};
+ }
+
+ return null;
+}
+
+function addDynamicRule (rule)
+{
+ var rules = frames[0].document.styleSheets[1];
+
+ var pos = rules.cssRules.length;
+ rules.insertRule (rule, pos);
+}
+
+function getCommandEnabled(command)
+{
+ try {
+ var dispatcher = document.commandDispatcher;
+ var controller = dispatcher.getControllerForCommand(command);
+
+ return controller.isCommandEnabled(command);
+ }
+ catch (e)
+ {
+ return false;
+ }
+}
+
+function doCommand(command)
+{
+ try {
+ var dispatcher = document.commandDispatcher;
+ var controller = dispatcher.getControllerForCommand(command);
+ if (controller && controller.isCommandEnabled(command))
+ controller.doCommand(command);
+ }
+ catch (e)
+ {
+ }
+}
+
+function doCommandWithParams(command, params)
+{
+ try {
+ var dispatcher = document.commandDispatcher;
+ var controller = dispatcher.getControllerForCommand(command);
+ controller.QueryInterface(Components.interfaces.nsICommandController);
+
+ if (!controller || !controller.isCommandEnabled(command))
+ return;
+
+ var cmdparams = newObject("@mozilla.org/embedcomp/command-params;1",
+ "nsICommandParams");
+ for (var i in params)
+ cmdparams.setISupportsValue(i, params[i]);
+
+ controller.doCommandWithParams(command, cmdparams);
+ }
+ catch (e)
+ {
+ }
+}
+
+var testURLs = [
+ "irc:",
+ "irc://",
+ "irc://foo",
+ "irc://foo/",
+ "irc://foo/,isserver",
+ "irc://foo/chatzilla",
+ "irc://foo/chatzilla/",
+ "irc://foo:6666",
+ "irc://foo:6666/",
+ "irc://irc.foo.org",
+ "irc://irc.foo.org/",
+ "irc://irc.foo.org/,needpass",
+ "irc://irc.foo.org/?msg=hello%20there",
+ "irc://irc.foo.org/?msg=hello%20there&ignorethis",
+ "irc://irc.foo.org/%23mozilla,needkey?msg=hello%20there&ignorethis",
+ "irc://libera.chat/",
+ "irc://libera.chat/,isserver",
+ "irc://[fe80::5d49:767b:4b68:1b17]",
+ "irc://[fe80::5d49:767b:4b68:1b17]/",
+ "irc://[fe80::5d49:767b:4b68:1b17]:6666",
+ "irc://[fe80::5d49:767b:4b68:1b17]:6666/"
+];
+
+var testFailURLs = [
+ "irc:///",
+ "irc:///help",
+ "irc:///help,needkey",
+ "irc://irc.foo.org/,isnick",
+ "invalids"
+];
+
+function doURLTest()
+{
+ var passed = 0, total = testURLs.length + testFailURLs.length;
+ for (var i = 0; i < testURLs.length; i++)
+ {
+ var o = parseIRCURL(testURLs[i]);
+ if (!o)
+ display("Parse of '" + testURLs[i] + "' failed.", MT_ERROR);
+ else
+ passed++;
+ }
+ for (var i = 0; i < testFailURLs.length; i++)
+ {
+ var o = parseIRCURL(testFailURLs[i]);
+ if (o)
+ display("Parse of '" + testFailURLs[i] + "' unexpectedly succeeded.", MT_ERROR);
+ else
+ passed++;
+ }
+ display("Passed " + passed + " out of " + total + " tests (" +
+ passed / total * 100 + "%).", MT_INFO);
+}
+
+var testIRCURLObjects = [
+ [{}, "irc://"],
+ [{host: "undernet"}, "irc://undernet/"],
+ [{host: "irc.undernet.org"}, "irc://irc.undernet.org/"],
+ [{host: "irc.undernet.org", isserver: true}, "irc://irc.undernet.org/"],
+ [{host: "undernet", isserver: true}, "irc://undernet/,isserver"],
+ [{host: "irc.undernet.org", port: 6667}, "irc://irc.undernet.org/"],
+ [{host: "irc.undernet.org", port: 1}, "irc://irc.undernet.org:1/"],
+ [{host: "irc.undernet.org", port: 1, scheme: "ircs"},
+ "ircs://irc.undernet.org:1/"],
+ [{host: "irc.undernet.org", port: 6697, scheme: "ircs"},
+ "ircs://irc.undernet.org/"],
+ [{host: "undernet", needpass: true}, "irc://undernet/,needpass"],
+ [{host: "undernet", pass: "cz"}, "irc://undernet/?pass=cz"],
+ [{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"],
+ [{host: "undernet", target: "#foo"}, "irc://undernet/%23foo"],
+ [{host: "undernet", target: "#foo", needkey: true},
+ "irc://undernet/%23foo,needkey"],
+ [{host: "undernet", target: "John", isnick: true},
+ "irc://undernet/John,isnick"],
+ [{host: "undernet", target: "#foo", key: "cz"},
+ "irc://undernet/%23foo?key=cz"],
+ [{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"],
+ [{host: "undernet", target: "John", msg: "spam!"},
+ "irc://undernet/John?msg=spam%21"],
+ [{host: "undernet", target: "foo", isnick: true, msg: "spam!", pass: "cz"},
+ "irc://undernet/foo,isnick?msg=spam%21&pass=cz"]
+];
+
+function doObjectURLtest()
+{
+ var passed = 0, total = testIRCURLObjects.length;
+ for (var i = 0; i < total; i++)
+ {
+ var obj = testIRCURLObjects[i][0];
+ var url = testIRCURLObjects[i][1];
+ var parsedURL = constructIRCURL(obj)
+ if (url != parsedURL)
+ {
+ display("Parsed IRC Object incorrectly! Expected '" + url +
+ "', got '" + parsedURL, MT_ERROR);
+ }
+ else
+ {
+ passed++;
+ }
+ }
+ display("Passed " + passed + " out of " + total + " tests (" +
+ passed / total * 100 + "%).", MT_INFO);
+}
+
+
+function gotoIRCURL(url, e)
+{
+ var urlspec = url;
+ if (typeof url == "string")
+ url = parseIRCURL(url);
+
+ if (!url)
+ {
+ window.alert(getMsg(MSG_ERR_BAD_IRCURL, urlspec));
+ return;
+ }
+
+ if (!url.host)
+ {
+ /* focus the *client* view for irc:, irc:/, and irc:// (the only irc
+ * urls that don't have a host. (irc:/// implies a connect to the
+ * default network.)
+ */
+ client.pendingViewContext = e;
+ dispatch("client");
+ delete client.pendingViewContext;
+ return;
+ }
+
+ let isSecure = url.scheme == "ircs";
+ let network;
+ // Make sure host is in lower case.
+ url.host = url.host.toLowerCase();
+
+ // Convert a request for a server to a network if we know it.
+ if (url.isserver)
+ {
+ for (var n in client.networks)
+ {
+ network = client.networks[n];
+ for (var s in network.servers)
+ {
+ let server = network.servers[s];
+ if ((server.hostname == url.host) &&
+ (server.isSecure == isSecure) &&
+ (!url.port || (server.port == url.port)))
+ {
+ url.isserver = false;
+ url.host = network.canonicalName;
+ if (!url.port)
+ url.port = server.port;
+ break;
+ }
+ }
+ if (!url.isserver)
+ break;
+ }
+ }
+
+ let name = url.host;
+ network = client.getNetwork(name);
+
+ if (url.isserver)
+ {
+ let found = false;
+ if (network) {
+ for (let s in network.servers) {
+ let server = network.servers[s];
+ if ((server.isSecure == isSecure) &&
+ (!url.port || (server.port == url.port))) {
+ found = true;
+ if (!url.port)
+ url.port = server.port;
+ break;
+ }
+ }
+ }
+
+ // If still no port set, use the default.
+ if (!url.port)
+ url.port = isSecure ? 6697 : 6667;
+
+ if (!found) {
+ name += ":" + url.port;
+
+ // If there is no temporary network for this server:port, create one.
+ if (!client.getNetwork(name)) {
+ let server = {name: url.host, port: url.port, isSecure: isSecure};
+ client.addNetwork(name, [server], true);
+ }
+ network = client.getNetwork(name);
+ }
+ }
+ else
+ {
+ // There is no network called this, sorry.
+ if (!network)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name));
+ return;
+ }
+ }
+
+ // We should only prompt for a password if we're not connected.
+ if (network.state == NET_OFFLINE)
+ {
+ // Check for a network password.
+ url.pass = client.tryToGetLogin(network.getURL(), "serv", "*",
+ url.pass, url.needpass,
+ getMsg(MSG_HOST_PASSWORD,
+ network.getURL()));
+ }
+
+ // Adjust secure setting for temporary networks (so user can override).
+ if (network.temporary)
+ network.serverList[0].isSecure = url.scheme == "ircs";
+
+ // Adjust password for all servers (so user can override).
+ if (url.pass)
+ {
+ for (var s in network.servers)
+ network.servers[s].password = url.pass;
+ }
+
+ // Start the connection and pend anything else if we're not ready.
+ if (network.state != NET_ONLINE)
+ {
+ client.pendingViewContext = e;
+ if (!network.isConnected())
+ {
+ client.connectToNetwork(network, url.scheme == "ircs");
+ }
+ else
+ {
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+ }
+ delete client.pendingViewContext;
+
+ if (!url.target)
+ return;
+
+ // We're not completely online, so everything else is pending.
+ if (!("pendingURLs" in network))
+ network.pendingURLs = new Array();
+ network.pendingURLs.unshift({ url: url, e: e });
+ return;
+ }
+
+ // We're connected now, process the target.
+ if (url.target)
+ {
+ var targetObject;
+ var ev;
+ if (url.isnick)
+ {
+ /* url points to a person. */
+ var nick = url.target;
+ var ary = url.target.split("!");
+ if (ary)
+ nick = ary[0];
+
+ client.pendingViewContext = e;
+ targetObject = network.dispatch("query", {nickname: nick});
+ delete client.pendingViewContext;
+ }
+ else
+ {
+ /* url points to a channel */
+ var key;
+ var serv = network.primServ;
+ var target = url.target;
+ if (url.charset)
+ {
+ var chan = new CIRCChannel(serv, target, fromUnicode(target, url.charset));
+ chan.prefs["charset"] = url.charset;
+ }
+ else
+ {
+ // Must do this the hard way... we have the server's format
+ // for the channel name here, and all our commands only work
+ // with the Unicode forms.
+
+ /* If we don't have a valid prefix, stick a "#" on it.
+ * NOTE: This is always a "#" so that URLs may be compared
+ * properly without involving the server (e.g. off-line).
+ */
+ if ((arrayIndexOf(["#", "&", "+", "!"], target[0]) == -1) &&
+ (arrayIndexOf(serv.channelTypes, target[0]) == -1))
+ {
+ target = "#" + target;
+ }
+
+ var chan = new CIRCChannel(serv, null, target);
+ }
+
+ if (url.needkey && !chan.joined)
+ {
+ if (url.key)
+ key = url.key;
+ else
+ key = window.promptPassword(getMsg(MSG_URL_KEY, url.spec));
+ }
+ client.pendingViewContext = e;
+ d = {channelToJoin: chan, key: key};
+ targetObject = network.dispatch("join", d);
+ delete client.pendingViewContext;
+
+ if (!targetObject)
+ return;
+ }
+
+ if (url.msg)
+ {
+ client.pendingViewContext = e;
+ var msg;
+ if (url.msg.indexOf("\01ACTION") == 0)
+ {
+ msg = filterOutput(url.msg, "ACTION", targetObject);
+ targetObject.display(msg, "ACTION", "ME!",
+ client.currentObject);
+ }
+ else
+ {
+ msg = filterOutput(url.msg, "PRIVMSG", targetObject);
+ targetObject.display(msg, "PRIVMSG", "ME!",
+ client.currentObject);
+ }
+ targetObject.say(msg);
+ dispatch("set-current-view", { view: targetObject });
+ delete client.pendingViewContext;
+ }
+ }
+ else
+ {
+ client.pendingViewContext = e;
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+ delete client.pendingViewContext;
+ }
+}
+
+function updateProgress()
+{
+ var busy;
+ var progress = -1;
+
+ if ("busy" in client.currentObject)
+ busy = client.currentObject.busy;
+
+ if ("progress" in client.currentObject)
+ progress = client.currentObject.progress;
+
+ if (!busy)
+ progress = 0;
+
+ client.progressPanel.collapsed = !busy;
+ client.progressBar.mode = (progress < 0 ? "undetermined" : "determined");
+ if (progress >= 0)
+ client.progressBar.value = progress;
+}
+
+function updateSecurityIcon()
+{
+ var o = getObjectDetails(client.currentObject);
+ var securityButton = window.document.getElementById("security-button");
+ securityButton.label = "";
+ securityButton.removeAttribute("level");
+ securityButton.removeAttribute("tooltiptext");
+ if (!o.server || !o.server.isConnected) // No server or connection?
+ {
+ securityButton.setAttribute("tooltiptext", MSG_SECURITY_INFO);
+ return;
+ }
+
+ let tooltiptext = MSG_SECURITY_INFO;
+ switch (o.server.connection.getSecurityState()) {
+ case STATE_IS_SECURE:
+ securityButton.setAttribute("level", "high");
+
+ // Update the tooltip.
+ var issuer = o.server.connection.getCertificate().issuerOrganization;
+ tooltiptext = getMsg(MSG_SECURE_CONNECTION, issuer);
+ break;
+ case STATE_IS_BROKEN:
+ securityButton.setAttribute("level", "broken");
+ break;
+ case STATE_IS_INSECURE:
+ default:
+ securityButton.setAttribute("level", "none");
+ }
+ securityButton.label = o.server.hostname;
+ securityButton.setAttribute("tooltiptext", tooltiptext);
+}
+
+function updateLoggingIcon()
+{
+ var state = client.currentObject.prefs["log"] ? "on" : "off";
+ var icon = window.document.getElementById("logging-status");
+ icon.setAttribute("loggingstate", state);
+ icon.setAttribute("tooltiptext", getMsg("msg.logging.icon." + state));
+}
+
+function updateAlertIcon(aToggle) {
+ let alertState = client.prefs["alert.globalEnabled"];
+ if (aToggle) {
+ alertState = !alertState;
+ client.prefs["alert.globalEnabled"] = alertState;
+ }
+ let state = alertState ? "on" : "off";
+ let icon = window.document.getElementById("alert-status");
+ icon.setAttribute("alertstate", state);
+ icon.setAttribute("tooltiptext", getMsg("msg.alert.icon." + state));
+}
+
+function initOfflineIcon()
+{
+ const PRBool_CID = "@mozilla.org/supports-PRBool;1";
+ const OS_CID = "@mozilla.org/observer-service;1";
+ const nsISupportsPRBool = Components.interfaces.nsISupportsPRBool;
+
+ client.offlineObserver = {
+ _element: document.getElementById("offline-status"),
+ state: function offline_state()
+ {
+ return (Services.io.offline ? "offline" : "online");
+ },
+ observe: function offline_observe(subject, topic, state)
+ {
+ if ((topic == "offline-requested") &&
+ (client.getConnectionCount() > 0))
+ {
+ var buttonAry = [MSG_REALLY_GO_OFFLINE, MSG_DONT_GO_OFFLINE];
+ var rv = confirmEx(MSG_GOING_OFFLINE, buttonAry);
+ if (rv == 1) // Don't go offline, please!
+ {
+ subject.QueryInterface(nsISupportsPRBool);
+ subject.data = true;
+ }
+ }
+ else if (topic == "network:offline-status-changed")
+ {
+ this.updateOfflineUI();
+ }
+ },
+ updateOfflineUI: function offline_uiUpdate()
+ {
+ this._element.setAttribute("offline", Services.io.offline);
+ var tooltipMsgId = "MSG_OFFLINESTATE_" + this.state().toUpperCase();
+ this._element.setAttribute("tooltiptext", window[tooltipMsgId]);
+ },
+ toggleOffline: function offline_toggle()
+ {
+ // Check whether people are OK with us going offline:
+ if (!Services.io.offline && !this.canGoOffline())
+ return;
+
+ // Stop automatic management of the offline status, if existing.
+ Services.io.manageOfflineStatus = false;
+
+ // Actually change the offline state.
+ Services.io.offline = !Services.io.offline;
+ },
+ canGoOffline: function offline_check()
+ {
+ try
+ {
+ var canGoOffline = newObject(PRBool_CID, "nsISupportsPRBool");
+ Services.obs.notifyObservers(canGoOffline, "offline-requested");
+ // Someone called for a halt
+ if (canGoOffline.data)
+ return false;
+ }
+ catch (ex)
+ {
+ dd("Exception when trying to ask if we could go offline:" + ex);
+ }
+ return true;
+ }
+ };
+
+ Services.obs.addObserver(client.offlineObserver, "offline-requested");
+ Services.obs.addObserver(client.offlineObserver,
+ "network:offline-status-changed");
+ client.offlineObserver.updateOfflineUI();
+}
+
+function uninitOfflineIcon()
+{
+ Services.obs.removeObserver(client.offlineObserver, "offline-requested");
+ Services.obs.removeObserver(client.offlineObserver,
+ "network:offline-status-changed");
+}
+
+client.idleObserver = {
+ QueryInterface: function io_qi(iid)
+ {
+ if (!iid || (!iid.equals(Components.interfaces.nsIObserver) &&
+ !iid.equals(Components.interfaces.nsISupports)))
+ {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+ observe: function io_observe(subject, topic, data)
+ {
+ if ((topic == "idle") && !client.prefs["away"])
+ {
+ if (!client.prefs["awayIdleMsg"])
+ client.prefs["awayIdleMsg"] = MSG_AWAY_IDLE_DEFAULT;
+ client.dispatch("idle-away", {reason: client.prefs["awayIdleMsg"]});
+ client.isIdleAway = true;
+ }
+ else if ((topic == "back" || topic == "active") && client.isIdleAway)
+ {
+ client.dispatch("idle-back");
+ client.isIdleAway = false;
+ }
+ }
+};
+
+function initIdleAutoAway(timeout)
+{
+ // Don't try to do anything if we are disabled
+ if (!timeout)
+ return;
+
+ var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService");
+ if (!is)
+ {
+ display(MSG_ERR_NO_IDLESERVICE, MT_WARN);
+ client.prefs["autoIdleTime"] = 0;
+ return;
+ }
+
+ try
+ {
+ is.addIdleObserver(client.idleObserver, timeout * 60);
+ }
+ catch (ex)
+ {
+ display(formatException(ex), MT_ERROR);
+ }
+}
+
+function uninitIdleAutoAway(timeout)
+{
+ // Don't try to do anything if we were disabled before
+ if (!timeout)
+ return;
+
+ var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService");
+ if (!is)
+ return;
+
+ try
+ {
+ is.removeIdleObserver(client.idleObserver, timeout * 60);
+ }
+ catch (ex)
+ {
+ display(formatException(ex), MT_ERROR);
+ }
+}
+
+function updateAppMotif(motifURL)
+{
+ var node = document.firstChild;
+ while (node && ((node.nodeType != node.PROCESSING_INSTRUCTION_NODE) ||
+ !(/name="dyn-motif"/).test(node.data)))
+ {
+ node = node.nextSibling;
+ }
+
+ motifURL = motifURL.replace(/"/g, "%22");
+ var dataStr = "href=\"" + motifURL + "\" name=\"dyn-motif\"";
+ try
+ {
+ // No dynamic style node yet.
+ if (!node)
+ {
+ node = document.createProcessingInstruction("xml-stylesheet", dataStr);
+ document.insertBefore(node, document.firstChild);
+ }
+ else if (node.data != dataStr)
+ {
+ node.data = dataStr;
+ document.insertBefore(node, node.nextSibling);
+ }
+ }
+ catch (ex)
+ {
+ dd(formatException(ex));
+ var err = ex.name;
+ // Mozilla 1.0 doesn't like document.insertBefore(...,
+ // document.firstChild); though it has a prototype for it -
+ // check for the right error:
+ if (err == "NS_ERROR_NOT_IMPLEMENTED")
+ {
+ display(MSG_NO_DYNAMIC_STYLE, MT_INFO);
+ updateAppMotif = function() {};
+ }
+ }
+}
+
+function updateSpellcheck(value)
+{
+ value = value.toString();
+ document.getElementById("input").setAttribute("spellcheck", value);
+ document.getElementById("multiline-input").setAttribute("spellcheck",
+ value);
+}
+
+function updateNetwork()
+{
+ var o = getObjectDetails (client.currentObject);
+
+ var lag = MSG_UNKNOWN;
+ var nick = "";
+ if (o.server)
+ {
+ if (o.server.me)
+ nick = o.server.me.unicodeName;
+ lag = (o.server.lag != -1) ? o.server.lag.toFixed(2) : MSG_UNKNOWN;
+ }
+ client.statusBar["header-url"].setAttribute("value",
+ client.currentObject.getURL());
+ client.statusBar["header-url"].setAttribute("href",
+ client.currentObject.getURL());
+ client.statusBar["header-url"].setAttribute("name",
+ client.currentObject.unicodeName);
+}
+
+function updateTitle (obj)
+{
+ if (!(("currentObject" in client) && client.currentObject) ||
+ (obj && obj != client.currentObject))
+ return;
+
+ var tstring = MSG_TITLE_UNKNOWN;
+ var o = getObjectDetails(client.currentObject);
+ var net = o.network ? o.network.unicodeName : "";
+ var nick = "";
+ client.statusBar["server-nick"].disabled = false;
+
+ switch (client.currentObject.TYPE)
+ {
+ case "IRCNetwork":
+ var serv = "", port = "";
+ if (client.currentObject.isConnected())
+ {
+ serv = o.server.hostname;
+ port = o.server.port;
+ if (o.server.me)
+ nick = o.server.me.unicodeName;
+ tstring = getMsg(MSG_TITLE_NET_ON, [nick, net, serv, port]);
+ }
+ else
+ {
+ nick = client.currentObject.INITIAL_NICK;
+ tstring = getMsg(MSG_TITLE_NET_OFF, [nick, net]);
+ }
+ break;
+
+ case "IRCChannel":
+ var chan = "", mode = "", topic = "";
+ if ("me" in o.parent)
+ {
+ nick = o.parent.me.unicodeName;
+ if (o.parent.me.collectionKey in client.currentObject.users)
+ {
+ let cuser = client.currentObject.users[o.parent.me.collectionKey];
+ if (cuser.isFounder)
+ nick = "~" + nick;
+ else if (cuser.isAdmin)
+ nick = "&" + nick;
+ else if (cuser.isOp)
+ nick = "@" + nick;
+ else if (cuser.isHalfOp)
+ nick = "%" + nick;
+ else if (cuser.isVoice)
+ nick = "+" + nick;
+ }
+ }
+ else
+ {
+ nick = MSG_TITLE_NONICK;
+ }
+ chan = o.channel.unicodeName;
+ mode = o.channel.mode.getModeStr();
+ if (!mode)
+ mode = MSG_TITLE_NO_MODE;
+ topic = o.channel.topic ? o.channel.topic : MSG_TITLE_NO_TOPIC;
+ var re = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
+ topic = topic.replace(re, "");
+
+ tstring = getMsg(MSG_TITLE_CHANNEL, [nick, chan, mode, topic]);
+ break;
+
+ case "IRCUser":
+ nick = client.currentObject.unicodeName;
+ var source = "";
+ if (client.currentObject.name)
+ {
+ source = "<" + client.currentObject.name + "@" +
+ client.currentObject.host +">";
+ }
+ tstring = getMsg(MSG_TITLE_USER, [nick, source]);
+ nick = "me" in o.parent ? o.parent.me.unicodeName : MSG_TITLE_NONICK;
+ break;
+
+ case "IRCClient":
+ nick = client.prefs["nickname"];
+ break;
+
+ case "IRCDCCChat":
+ client.statusBar["server-nick"].disabled = true;
+ nick = o.chat.me.unicodeName;
+ tstring = getMsg(MSG_TITLE_DCCCHAT, o.userName);
+ break;
+
+ case "IRCDCCFileTransfer":
+ client.statusBar["server-nick"].disabled = true;
+ nick = o.file.me.unicodeName;
+ var data = [o.file.progress, o.file.filename, o.userName];
+ if (o.file.state.dir == 1)
+ tstring = getMsg(MSG_TITLE_DCCFILE_SEND, data);
+ else
+ tstring = getMsg(MSG_TITLE_DCCFILE_GET, data);
+ break;
+ }
+
+ if (0 && !client.uiState["tabstrip"])
+ {
+ var actl = new Array();
+ for (var i in client.activityList)
+ actl.push ((client.activityList[i] == "!") ?
+ (Number(i) + 1) + "!" : (Number(i) + 1));
+ if (actl.length > 0)
+ tstring = getMsg(MSG_TITLE_ACTIVITY,
+ [tstring, actl.join (", ")]);
+ }
+
+ document.title = tstring;
+ client.statusBar["server-nick"].setAttribute("label", nick);
+}
+
+// Where 'right' is orientation, not wrong/right:
+function updateUserlistSide(shouldBeLeft)
+{
+ var listParent = document.getElementById("tabpanels-contents-box");
+ var isLeft = (listParent.childNodes[0].id == "user-list-box");
+ if (isLeft == shouldBeLeft)
+ return;
+ if (shouldBeLeft) // Move from right to left.
+ {
+ listParent.insertBefore(listParent.childNodes[1], listParent.childNodes[0]);
+ listParent.insertBefore(listParent.childNodes[2], listParent.childNodes[0]);
+ listParent.childNodes[1].setAttribute("collapse", "before");
+ }
+ else // Move from left to right.
+ {
+ listParent.appendChild(listParent.childNodes[1]);
+ listParent.appendChild(listParent.childNodes[0]);
+ listParent.childNodes[1].setAttribute("collapse", "after");
+ }
+ var userlist = document.getElementById("user-list")
+ if (client.currentObject && (client.currentObject.TYPE == "IRCChannel"))
+ userlist.view = client.currentObject.userList;
+}
+
+function multilineInputMode (state)
+{
+ var multiInput = document.getElementById("multiline-input");
+ var multiInputBox = document.getElementById("multiline-box");
+ var singleInput = document.getElementById("input");
+ var singleInputBox = document.getElementById("singleline-box");
+ var splitter = document.getElementById("input-splitter");
+ var iw = document.getElementById("input-widgets");
+ var h;
+
+ client._mlMode = state;
+
+ if (state) /* turn on multiline input mode */
+ {
+
+ h = iw.getAttribute ("lastHeight");
+ if (h)
+ iw.setAttribute ("height", h); /* restore the slider position */
+
+ singleInputBox.setAttribute ("collapsed", "true");
+ splitter.setAttribute ("collapsed", "false");
+ multiInputBox.setAttribute ("collapsed", "false");
+ // multiInput should have the same direction as singleInput
+ multiInput.setAttribute("dir", singleInput.getAttribute("dir"));
+ multiInput.value = (client.input ? client.input.value : "");
+ client.input = multiInput;
+ }
+ else /* turn off multiline input mode */
+ {
+ h = iw.getAttribute ("height");
+ iw.setAttribute ("lastHeight", h); /* save the slider position */
+ iw.removeAttribute ("height"); /* let the slider drop */
+
+ splitter.setAttribute ("collapsed", "true");
+ multiInputBox.setAttribute ("collapsed", "true");
+ singleInputBox.setAttribute ("collapsed", "false");
+ // singleInput should have the same direction as multiInput
+ singleInput.setAttribute("dir", multiInput.getAttribute("dir"));
+ singleInput.value = (client.input ? client.input.value : "");
+ client.input = singleInput;
+ }
+
+ client.input.focus();
+}
+
+function displayCertificateInfo()
+{
+ var o = getObjectDetails(client.currentObject);
+ if (!o.server)
+ return;
+
+ if (!o.server.isSecure)
+ {
+ alert(getMsg(MSG_INSECURE_SERVER, o.server.hostname));
+ return;
+ }
+
+ viewCert(o.server.connection.getCertificate());
+}
+
+function onLoggingIcon() {
+ client.currentObject.dispatch("log", { state: "toggle" });
+}
+
+function newInlineText (data, className, tagName)
+{
+ if (typeof tagName == "undefined")
+ tagName = "html:span";
+
+ var a = document.createElementNS(XHTML_NS, tagName);
+ if (className)
+ a.setAttribute ("class", className);
+
+ switch (typeof data)
+ {
+ case "string":
+ a.appendChild (document.createTextNode (data));
+ break;
+
+ case "object":
+ for (var p in data)
+ if (p != "data")
+ a.setAttribute (p, data[p]);
+ else
+ a.appendChild (document.createTextNode (data[p]));
+ break;
+
+ case "undefined":
+ break;
+
+ default:
+ ASSERT(0, "INVALID TYPE ('" + typeof data + "') passed to " +
+ "newInlineText.");
+ break;
+
+ }
+
+ return a;
+
+}
+
+function stringToMsg (message, obj)
+{
+ var ary = message.split ("\n");
+ var span = document.createElementNS(XHTML_NS, "html:span");
+ var data = getObjectDetails(obj);
+
+ if (ary.length == 1)
+ client.munger.munge(ary[0], span, data);
+ else
+ {
+ for (var l = 0; l < ary.length - 1; ++l)
+ {
+ client.munger.munge(ary[l], span, data);
+ span.appendChild(document.createElementNS(XHTML_NS, "html:br"));
+ }
+ client.munger.munge(ary[l], span, data);
+ }
+
+ return span;
+}
+
+function getFrame()
+{
+ if (client.deck.childNodes.length == 0)
+ return undefined;
+ var panel = client.deck.selectedPanel;
+ return getContentWindow(panel);
+}
+
+client.__defineGetter__ ("currentFrame", getFrame);
+
+function setCurrentObject (obj)
+{
+ if (!ASSERT(obj.messages, "INVALID OBJECT passed to setCurrentObject **"))
+ return;
+
+ if ("currentObject" in client && client.currentObject == obj)
+ {
+ if (typeof client.pendingViewContext == "object")
+ dispatch("create-tab-for-view", { view: obj });
+ return;
+ }
+
+ // Set window.content to make screenreader apps find the chat content.
+ if (obj.frame && getContentWindow(obj.frame))
+ window.content = getContentWindow(obj.frame);
+
+ var tb, userList;
+ userList = document.getElementById("user-list");
+
+ if ("currentObject" in client && client.currentObject)
+ tb = getTabForObject(client.currentObject);
+ if (tb)
+ tb.setAttribute("state", "normal");
+
+ // If we're tracking last read lines, set a mark on the current view
+ // before switching to the new one.
+ if (tb && client.currentObject.prefs["autoMarker"])
+ client.currentObject.dispatch("marker-set");
+
+ client.currentObject = obj;
+
+ // Update userlist:
+ userList.view = null;
+ if (obj.TYPE == "IRCChannel")
+ {
+ userList.view = obj.userList;
+ updateUserList();
+ }
+
+ tb = dispatch("create-tab-for-view", { view: obj });
+ if (tb)
+ {
+ tb.parentNode.selectedItem = tb;
+ tb.setAttribute("state", "current");
+ }
+
+ var vk = Number(tb.getAttribute("viewKey"));
+ delete client.activityList[vk];
+ client.deck.selectedPanel = obj.frame;
+
+ // Style userlist and the like:
+ updateAppMotif(obj.prefs["motif.current"]);
+
+ updateTitle();
+ updateProgress();
+ updateSecurityIcon();
+ updateLoggingIcon();
+
+ scrollDown(obj.frame, false);
+
+ // Input area should have the same direction as the output area
+ if (("frame" in client.currentObject) &&
+ client.currentObject.frame &&
+ getContentDocument(client.currentObject.frame) &&
+ ("body" in getContentDocument(client.currentObject.frame)) &&
+ getContentDocument(client.currentObject.frame).body)
+ {
+ var contentArea = getContentDocument(client.currentObject.frame).body;
+ client.input.setAttribute("dir", contentArea.getAttribute("dir"));
+ }
+ client.input.focus();
+}
+
+function checkScroll(frame)
+{
+ var window = getContentWindow(frame);
+ if (!window || !window.document || !window.document.body)
+ return false;
+
+ return (window.document.body.clientHeight - window.innerHeight -
+ window.pageYOffset) < 160;
+}
+
+function scrollDown(frame, force)
+{
+ var window = getContentWindow(frame);
+ if (!window || !window.document || !window.document.body)
+ return;
+
+ if (force || checkScroll(frame))
+ window.scrollTo(0, window.document.body.clientHeight);
+}
+
+function advanceKeyboardFocus(amount)
+{
+ var contentWin = getContentWindow(client.currentObject.frame);
+ var contentDoc = getContentDocument(client.currentObject.frame);
+ var userList = document.getElementById("user-list");
+
+ // Focus userlist, inputbox and outputwindow in turn:
+ var focusableElems = [userList, client.input.inputField, contentWin];
+
+ var elem = document.commandDispatcher.focusedElement;
+ // Finding focus in the content window is "hard". It's going to be null
+ // if the window itself is focused, and "some element" inside of it if the
+ // user starts tabbing through.
+ if (!elem || (elem.ownerDocument == contentDoc))
+ elem = contentWin;
+
+ var newIndex = (arrayIndexOf(focusableElems, elem) * 1 + 3 + amount) % 3;
+ focusableElems[newIndex].focus();
+
+ // Make it obvious this element now has focus.
+ var outlinedElem;
+ if (focusableElems[newIndex] == client.input.inputField)
+ outlinedElem = client.input.parentNode.id;
+ else if (focusableElems[newIndex] == userList)
+ outlinedElem = "user-list-box"
+ else
+ outlinedElem = "browser-box";
+
+ // Do magic, and make sure it gets undone at the right time:
+ if (("focusedElemTimeout" in client) && client.focusedElemTimeout)
+ clearTimeout(client.focusedElemTimeout);
+ outlineFocusedElem(outlinedElem);
+ client.focusedElemTimeout = setTimeout(outlineFocusedElem, 1000, "");
+}
+
+function outlineFocusedElem(id)
+{
+ var outlinedElements = ["user-list-box", "browser-box", "multiline-hug-box",
+ "singleline-hug-box"];
+ for (var i = 0; i < outlinedElements.length; i++)
+ {
+ var elem = document.getElementById(outlinedElements[i]);
+ if (outlinedElements[i] == id)
+ elem.setAttribute("focusobvious", "true");
+ else
+ elem.removeAttribute("focusobvious");
+ }
+ client.focusedElemTimeout = 0;
+}
+
+/* valid values for |what| are "superfluous", "activity", and "attention".
+ * final value for state is dependant on priority of the current state, and the
+ * new state. the priority is: normal < superfluous < activity < attention.
+ */
+function setTabState(source, what, callback)
+{
+ if (typeof source != "object")
+ {
+ if (!ASSERT(source in client.viewsArray,
+ "INVALID SOURCE passed to setTabState"))
+ return;
+ source = client.viewsArray[source].source;
+ }
+
+ callback = callback || false;
+
+ var tb = source.dispatch("create-tab-for-view", { view: source });
+ var vk = Number(tb.getAttribute("viewKey"));
+
+ var current = ("currentObject" in client && client.currentObject == source);
+
+ /* We want to play sounds if they're from a non-current view, or we don't
+ * have focus at all. Also make sure stalk matches always play sounds.
+ * Also make sure we don't play on the 2nd half of the flash (Callback).
+ */
+ if (!callback && (!window.isFocused || !current || (what == "attention")))
+ {
+ if (what == "attention")
+ playEventSounds(source.TYPE, "stalk", source);
+ else if (what == "activity")
+ playEventSounds(source.TYPE, "chat", source);
+ else if (what == "superfluous")
+ playEventSounds(source.TYPE, "event", source);
+ }
+
+ // Only change the tab's colour if it's not the active view.
+ if (!current)
+ {
+ var state = tb.getAttribute("state");
+ if (state == what)
+ {
+ /* if the tab state has an equal priority to what we are setting
+ * then blink it */
+ if (client.prefs["activityFlashDelay"] > 0)
+ {
+ tb.setAttribute("state", "normal");
+ setTimeout(setTabState, client.prefs["activityFlashDelay"],
+ vk, what, true);
+ }
+ }
+ else
+ {
+ if (state == "normal" || state == "superfluous" ||
+ (state == "activity" && what == "attention"))
+ {
+ /* if the tab state has a lower priority than what we are
+ * setting, change it to the new state */
+ tb.setAttribute("state", what);
+ /* we only change the activity list if priority has increased */
+ if (what == "attention")
+ client.activityList[vk] = "!";
+ else if (what == "activity")
+ client.activityList[vk] = "+";
+ else
+ {
+ /* this is functionally equivalent to "+" for now */
+ client.activityList[vk] = "-";
+ }
+ updateTitle();
+ }
+ else
+ {
+ /* the current state of the tab has a higher priority than the
+ * new state.
+ * blink the new lower state quickly, then back to the old */
+ if (client.prefs["activityFlashDelay"] > 0)
+ {
+ tb.setAttribute("state", what);
+ setTimeout(setTabState,
+ client.prefs["activityFlashDelay"], vk,
+ state, true);
+ }
+ }
+ }
+ }
+}
+
+function notifyAttention (source)
+{
+ if (typeof source != "object")
+ source = client.viewsArray[source].source;
+
+ if (client.currentObject != source)
+ {
+ var tb = dispatch("create-tab-for-view", { view: source });
+ var vk = Number(tb.getAttribute("viewKey"));
+
+ tb.setAttribute ("state", "attention");
+ client.activityList[vk] = "!";
+ updateTitle();
+ }
+
+ if (client.prefs["notify.aggressive"])
+ window.getAttention();
+
+}
+
+function setDebugMode(mode)
+{
+ if (mode.indexOf("t") != -1)
+ client.debugHook.enabled = true;
+ else
+ client.debugHook.enabled = false;
+
+ if (mode.indexOf("c") != -1)
+ client.dbgContexts = true;
+ else
+ delete client.dbgContexts;
+
+ if (mode.indexOf("d") != -1)
+ client.dbgDispatch = true;
+ else
+ delete client.dbgDispatch;
+}
+
+function setListMode(mode)
+{
+ var elem = document.getElementById("user-list");
+ if (mode)
+ elem.setAttribute("mode", mode);
+ else
+ elem.removeAttribute("mode");
+ if (elem && elem.view && elem.treeBoxObject)
+ {
+ elem.treeBoxObject.clearStyleAndImageCaches();
+ elem.treeBoxObject.invalidate();
+ }
+}
+
+function updateUserList()
+{
+ var node, chan;
+
+ node = document.getElementById("user-list");
+ if (!node.view)
+ return;
+
+ if (("currentObject" in client) && client.currentObject &&
+ client.currentObject.TYPE == "IRCChannel")
+ {
+ reSortUserlist(client.currentObject);
+ }
+}
+
+function reSortUserlist(channel)
+{
+ if (!channel || !channel.userList)
+ return;
+ channel.userList.childData.reSort();
+}
+
+function getNicknameForUserlistRow(index)
+{
+ // This wouldn't be so hard if APIs didn't change so much... see bug 221619
+ var userlist = document.getElementById("user-list");
+ if (userlist.columns)
+ var col = userlist.columns.getNamedColumn("usercol");
+ else
+ col = "usercol";
+ return userlist.view.getCellText(index, col);
+}
+
+function getFrameForDOMWindow(window)
+{
+ var frame;
+ for (var i = 0; i < client.deck.childNodes.length; i++)
+ {
+ frame = client.deck.childNodes[i];
+ if (frame.contentWindow == window)
+ return frame;
+ }
+ return undefined;
+}
+
+function replaceColorCodes(msg)
+{
+ // Find things that look like URLs and escape the color code inside of those
+ // to prevent munging the URLs resulting in broken links. Leave codes at
+ // the start of the URL alone.
+ msg = msg.replace(new RegExp(client.linkRE.source, "g"), function(url, _foo, scheme) {
+ if (scheme && !client.checkURLScheme(scheme))
+ return url;
+ return url.replace(/%[BC][0-9A-Fa-f]/g, function(hex, index) {
+ // as JS does not support lookbehind and we don't want to consume the
+ // preceding character, we test for an existing %% manually
+ var needPercent = ("%" == url.substr(index - 1, 1)) || (index == 0);
+ return (needPercent ? "" : "%") + hex;
+ });
+ });
+
+ // mIRC codes: underline, bold, Original (reset), colors, reverse colors.
+ msg = msg.replace(/(^|[^%])%U/g, "$1\x1f");
+ msg = msg.replace(/(^|[^%])%B/g, "$1\x02");
+ msg = msg.replace(/(^|[^%])%O/g, "$1\x0f");
+ msg = msg.replace(/(^|[^%])%C/g, "$1\x03");
+ msg = msg.replace(/(^|[^%])%R/g, "$1\x16");
+ // %%[UBOCR] --> %[UBOCR].
+ msg = msg.replace(/%(%[UBOCR])/g, "$1");
+
+ return msg;
+}
+
+function decodeColorCodes(msg)
+{
+ // %[UBOCR] --> %%[UBOCR].
+ msg = msg.replace(/(%[UBOCR])/g, "%$1");
+ // Put %-codes back in place of special character codes.
+ msg = msg.replace(/\x1f/g, "%U");
+ msg = msg.replace(/\x02/g, "%B");
+ msg = msg.replace(/\x0f/g, "%O");
+ msg = msg.replace(/\x03/g, "%C");
+ msg = msg.replace(/\x16/g, "%R");
+
+ return msg;
+}
+
+function removeColorCodes(msg)
+{
+ msg = msg.replace(/[\x1f\x02\x0f\x16]/g, "");
+ // We need this to be global:
+ msg = msg.replace(new RegExp(client.colorRE.source, "g"), "");
+ return msg;
+}
+
+client.progressListener = new Object();
+
+client.progressListener.QueryInterface =
+function qi(iid)
+{
+ return this;
+}
+
+client.progressListener.onStateChange =
+function client_statechange (webProgress, request, stateFlags, status)
+{
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ const START = nsIWebProgressListener.STATE_START;
+ const STOP = nsIWebProgressListener.STATE_STOP;
+ const IS_NETWORK = nsIWebProgressListener.STATE_IS_NETWORK;
+ const IS_DOCUMENT = nsIWebProgressListener.STATE_IS_DOCUMENT;
+ const IS_REQUEST = nsIWebProgressListener.STATE_IS_REQUEST;
+
+ var frame;
+ //dd("progressListener.onStateChange(" + stateFlags.toString(16) + ")");
+
+ // We only care about the initial start of loading, not the loading of
+ // and page sub-components (IS_DOCUMENT, etc.).
+ if ((stateFlags & START) && (stateFlags & IS_NETWORK) &&
+ (stateFlags & IS_DOCUMENT))
+ {
+ frame = getFrameForDOMWindow(webProgress.DOMWindow);
+ if (!frame)
+ {
+ dd("can't find frame for window (start)");
+ try
+ {
+ webProgress.removeProgressListener(this);
+ }
+ catch(ex)
+ {
+ dd("Exception removing progress listener (start): " + ex);
+ }
+ }
+ }
+ // We only want to know when the *network* stops, not the page's
+ // individual components (STATE_IS_REQUEST/STATE_IS_DOCUMENT/somesuch).
+ else if ((stateFlags & STOP) && (stateFlags & IS_NETWORK))
+ {
+ frame = getFrameForDOMWindow(webProgress.DOMWindow);
+ if (!frame)
+ {
+ dd("can't find frame for window (stop)");
+ try
+ {
+ webProgress.removeProgressListener(this);
+ }
+ catch(ex)
+ {
+ dd("Exception removing progress listener (stop): " + ex);
+ }
+ }
+ else
+ {
+ var cwin = getContentWindow(frame);
+ if (cwin && "initOutputWindow" in cwin)
+ {
+ if (!("_called_initOutputWindow" in cwin))
+ {
+ cwin._called_initOutputWindow = true;
+ cwin.initOutputWindow(client, frame.source, onMessageViewClick);
+ cwin.changeCSS(frame.source.getFontCSS("data"), "cz-fonts");
+ scrollDown(frame, true);
+ //dd("initOutputWindow(" + frame.source.getURL() + ")");
+ }
+ }
+ // XXX: For about:blank it won't find initOutputWindow. Cope.
+ else if (!cwin || !cwin.location ||
+ (cwin.location.href != "about:blank"))
+ {
+ // This should totally never ever happen. It will if we get in a
+ // fight with xpcnativewrappers, though. Oops:
+ dd("Couldn't find a content window or its initOutputWindow " +
+ "function. This is BAD!");
+ }
+ }
+ }
+ // Requests stopping are either the page, or its components loading. We're
+ // interested in its components.
+ else if ((stateFlags & STOP) && (stateFlags & IS_REQUEST))
+ {
+ frame = getFrameForDOMWindow(webProgress.DOMWindow);
+ if (frame)
+ {
+ var cwin = getContentWindow(frame);
+ if (cwin && ("_called_initOutputWindow" in cwin))
+ {
+ scrollDown(frame, false);
+ //dd("scrollDown(" + frame.source.getURL() + ")");
+ }
+ }
+
+ }
+}
+
+client.progressListener.onProgressChange =
+function client_progresschange (webProgress, request, currentSelf, totalSelf,
+ currentMax, selfMax)
+{
+}
+
+client.progressListener.onLocationChange =
+function client_locationchange (webProgress, request, uri)
+{
+}
+
+client.progressListener.onStatusChange =
+function client_statuschange (webProgress, request, status, message)
+{
+}
+
+client.progressListener.onSecurityChange =
+function client_securitychange (webProgress, request, state)
+{
+}
+
+client.installPlugin =
+function cli_installPlugin(name, source)
+{
+ function checkPluginInstalled(name, path)
+ {
+ var installed = path.exists();
+ installed |= (name in client.plugins);
+
+ if (installed)
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_ALREADY_INST, MT_ERROR);
+ throw CZ_PI_ABORT;
+ }
+ };
+ function getZipEntry(reader, entryEnum)
+ {
+ // nsIZipReader was rewritten...
+ var itemName = entryEnum.getNext();
+ if (typeof itemName != "string")
+ name = itemName.QueryInterface(nsIZipEntry).name;
+ return itemName;
+ };
+ function checkZipMore(items)
+ {
+ return (("hasMoreElements" in items) && items.hasMoreElements()) ||
+ (("hasMore" in items) && items.hasMore());
+ };
+
+ const DIRECTORY_TYPE = Components.interfaces.nsIFile.DIRECTORY_TYPE;
+ const CZ_PI_ABORT = "CZ_PI_ABORT";
+ const nsIZipEntry = Components.interfaces.nsIZipEntry;
+
+ var dest;
+ // Find a suitable location if there was none specified.
+ var destList = client.prefs["initialScripts"];
+ if ((destList.length == 0) ||
+ ((destList.length == 1) && /^\s*$/.test(destList[0])))
+ {
+ // Reset to default because it is empty.
+ try
+ {
+ client.prefManager.clearPref("initialScripts");
+ }
+ catch(ex) {/* If this really fails, we're mostly screwed anyway */}
+ destList = client.prefs["initialScripts"];
+ }
+
+ // URLs for initialScripts can be relative (the default is):
+ var profilePath = getURLSpecFromFile(client.prefs["profilePath"]);
+ profilePath = Services.io.newURI(profilePath);
+ for (var i = 0; i < destList.length; i++)
+ {
+ var destURL = Services.io.newURI(destList[i], null, profilePath);
+ var file = new nsLocalFile(getFileFromURLSpec(destURL.spec).path);
+ if (file.exists() && file.isDirectory()) {
+ // A directory that exists! We'll take it!
+ dest = file.clone();
+ break;
+ }
+ }
+ if (!dest) {
+ display(MSG_INSTALL_PLUGIN_ERR_INSTALL_TO, MT_ERROR);
+ return;
+ }
+
+ try {
+ if (typeof source == "string")
+ source = getFileFromURLSpec(source);
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_CHECK_SD, ex), MT_ERROR);
+ return;
+ }
+
+ display(getMsg(MSG_INSTALL_PLUGIN_INSTALLING, [source.path, dest.path]),
+ MT_INFO);
+
+ var ary;
+ if (source.path.match(/\.(jar|zip)$/i))
+ {
+ try
+ {
+ var zipReader = newObject("@mozilla.org/libjar/zip-reader;1",
+ "nsIZipReader");
+ zipReader.open(source);
+
+ // This is set to the base path found on ALL items in the zip file.
+ // when we extract, this WILL BE REMOVED from all paths.
+ var zipPathBase = "";
+ // This always points to init.js, even if we're messing with paths.
+ var initPath = "init.js";
+
+ // Look for init.js within a directory...
+ var items = zipReader.findEntries("*/init.js");
+ while (checkZipMore(items))
+ {
+ var itemName = getZipEntry(zipReader, items);
+ // Do we already have one?
+ if (zipPathBase)
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_MANY_INITJS, MT_WARN);
+ throw CZ_PI_ABORT;
+ }
+ zipPathBase = itemName.match(/^(.*\/)init.js$/)[1];
+ initPath = itemName;
+ }
+
+ if (zipPathBase)
+ {
+ // We have a base for init.js, assert that all files are inside
+ // it. If not, we drop the path and install exactly as-is
+ // instead (which will probably cause it to not work because the
+ // init.js isn't in the right place).
+ items = zipReader.findEntries("*");
+ while (checkZipMore(items))
+ {
+ itemName = getZipEntry(zipReader, items);
+ if (itemName.substr(0, zipPathBase.length) != zipPathBase)
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_MIXED_BASE, MT_WARN);
+ zipPathBase = "";
+ break;
+ }
+ }
+ }
+
+ // Test init.js for a plugin ID.
+ var initJSFile = getTempFile(client.prefs["profilePath"],
+ "install-plugin.temp");
+ zipReader.extract(initPath, initJSFile);
+ initJSFile.permissions = 438; // 0666
+ var initJSFileH = fopen(initJSFile, "<");
+ var initJSData = initJSFileH.read();
+ initJSFileH.close();
+ initJSFile.remove(false);
+
+ //XXXgijs: this is fragile. Anyone got a better idea?
+ ary = initJSData.match(/plugin\.id\s*=\s*(['"])(.*?)(\1)/);
+ if (ary && (name != ary[2]))
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_WARN_NAME, [name, ary[2]]),
+ MT_WARN);
+ name = ary[2];
+ }
+
+ dest.append(name);
+ checkPluginInstalled(name, dest);
+
+ dest.create(DIRECTORY_TYPE, 0o700);
+
+ // Actually extract files...
+ var destInit;
+ items = zipReader.findEntries("*");
+ while (checkZipMore(items))
+ {
+ itemName = getZipEntry(zipReader, items);
+ if (!itemName.match(/\/$/))
+ {
+ var dirs = itemName;
+ // Remove common path if there is one.
+ if (zipPathBase)
+ dirs = dirs.substring(zipPathBase.length);
+ dirs = dirs.split("/");
+
+ // Construct the full path for the extracted file...
+ var zipFile = dest.clone();
+ for (var i = 0; i < dirs.length - 1; i++)
+ {
+ zipFile.append(dirs[i]);
+ if (!zipFile.exists())
+ zipFile.create(DIRECTORY_TYPE, 0o700);
+ }
+ zipFile.append(dirs[dirs.length - 1]);
+
+ if (zipFile.leafName == "init.js")
+ destInit = zipFile;
+
+ zipReader.extract(itemName, zipFile);
+ zipFile.permissions = 438; // 0666
+ }
+ }
+
+ var rv = dispatch("load ", {url: getURLSpecFromFile(destInit)});
+ if (rv)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_DONE, name));
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_REMOVING, MT_ERROR);
+ dest.remove(true);
+ }
+ }
+ catch (ex)
+ {
+ if (ex != CZ_PI_ABORT)
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_EXTRACT, ex), MT_ERROR);
+ zipReader.close();
+ return;
+ }
+ try
+ {
+ zipReader.close();
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_EXTRACT, ex), MT_ERROR);
+ }
+ }
+ else if (source.path.match(/\.(js)$/i))
+ {
+ try
+ {
+ // Test init.js for a plugin ID.
+ var initJSFileH = fopen(source, "<");
+ var initJSData = initJSFileH.read();
+ initJSFileH.close();
+
+ ary = initJSData.match(/plugin\.id\s*=\s*(['"])(.*?)(\1)/);
+ if (ary && (name != ary[2]))
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_WARN_NAME, [name, ary[2]]),
+ MT_WARN);
+ name = ary[2];
+ }
+
+ dest.append(name);
+ checkPluginInstalled(name, dest);
+
+ dest.create(DIRECTORY_TYPE, 0o700);
+
+ dest.append("init.js");
+
+ var destFile = fopen(dest, ">");
+ destFile.write(initJSData);
+ destFile.close();
+
+ var rv = dispatch("load", {url: getURLSpecFromFile(dest)});
+ if (rv)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_DONE, name));
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_REMOVING, MT_ERROR);
+ // We've appended "init.js" before, so go back up one level:
+ dest.parent.remove(true);
+ }
+ }
+ catch(ex)
+ {
+ if (ex != CZ_PI_ABORT)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_INSTALLING, ex),
+ MT_ERROR);
+ }
+ }
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_FORMAT, MT_ERROR);
+ }
+}
+
+client.uninstallPlugin =
+function cli_uninstallPlugin(plugin)
+{
+ if (!disablePlugin(plugin, true))
+ return;
+ delete client.plugins[plugin.id];
+ let file = getFileFromURLSpec(plugin.cwd);
+ if (file.exists() && file.isDirectory())
+ {
+ // Delete the directory and contents.
+ file.remove(true);
+ }
+ display(getMsg(MSG_PLUGIN_UNINSTALLED, plugin.id));
+}
+
+function syncOutputFrame(obj, nesting)
+{
+ const nsIWebProgress = Components.interfaces.nsIWebProgress;
+ const WINDOW = nsIWebProgress.NOTIFY_STATE_WINDOW;
+ const NETWORK = nsIWebProgress.NOTIFY_STATE_NETWORK;
+ const ALL = nsIWebProgress.NOTIFY_ALL;
+
+ var iframe = obj.frame;
+
+ function tryAgain(nLevel)
+ {
+ syncOutputFrame(obj, nLevel);
+ };
+
+ if (typeof nesting != "number")
+ nesting = 0;
+
+ if (nesting > 10)
+ {
+ dd("Aborting syncOutputFrame, taken too many tries.");
+ return;
+ }
+
+ /* We leave the progress listener attached so try to remove it first,
+ * should we be called on an already-set-up view.
+ */
+ try
+ {
+ iframe.removeProgressListener(client.progressListener, ALL);
+ }
+ catch (ex)
+ {
+ }
+
+ try
+ {
+ if (getContentDocument(iframe) && ("webProgress" in iframe))
+ {
+ var url = obj.prefs["outputWindowURL"];
+ iframe.addProgressListener(client.progressListener, ALL);
+ iframe.loadURI(url);
+ }
+ else
+ {
+ setTimeout(tryAgain, 500, nesting + 1);
+ }
+ }
+ catch (ex)
+ {
+ dd("caught exception showing session view, will try again later.");
+ dd(dumpObjectTree(ex) + "\n");
+ setTimeout(tryAgain, 500, nesting + 1);
+ }
+}
+
+function createMessages(source)
+{
+ playEventSounds(source.TYPE, "start", source);
+
+ source.messages = document.createElementNS(XHTML_NS, "html:table");
+ source.messages.setAttribute("class", "msg-table");
+ source.messages.setAttribute("view-type", source.TYPE);
+ source.messages.setAttribute("role", "log");
+ source.messages.setAttribute("aria-live", "polite");
+
+ var tbody = document.createElementNS(XHTML_NS, "html:tbody");
+ source.messages.appendChild (tbody);
+ source.messageCount = 0;
+}
+
+/* Gets the <tab> element associated with a view object.
+ * If |create| is present, and true, tab is created if not found.
+ */
+function getTabForObject(source, create)
+{
+ var name;
+
+ if (!ASSERT(source, "UNDEFINED passed to getTabForObject"))
+ return null;
+
+ if (!ASSERT("viewName" in source, "INVALID OBJECT in getTabForObject"))
+ return null;
+
+ name = source.viewName;
+
+ var tb, id = "tb[" + name + "]";
+ var matches = 1;
+
+ for (var i in client.viewsArray)
+ {
+ if (client.viewsArray[i].source == source)
+ {
+ tb = client.viewsArray[i].tb;
+ break;
+ }
+ else
+ if (client.viewsArray[i].tb.getAttribute("id") == id)
+ id = "tb[" + name + "<" + (++matches) + ">]";
+ }
+
+ /* If we found a <tab>, are allowed to create it, and have a pending view
+ * context, then we're expected to move the existing tab according to said
+ * context. We do that by removing the tab here, and below the creation
+ * code (which is not run) we readd it in the correct location.
+ */
+ if (tb && create && (typeof client.pendingViewContext == "object"))
+ {
+ /* If we're supposed to insert before ourselves, or the next <tab>,
+ * then bail out (since it's a no-op).
+ */
+ var tabBefore = client.pendingViewContext.tabInsertBefore;
+ if (tabBefore)
+ {
+ var tbAfter = tb.nextSibling;
+ while (tbAfter && tbAfter.collapsed && tbAfter.hidden)
+ tbAfter = tbAfter.nextSibling;
+ if ((tabBefore == tb) || (tabBefore == tbAfter))
+ return tb;
+ }
+
+ var viewKey = Number(tb.getAttribute("viewKey"));
+ arrayRemoveAt(client.viewsArray, viewKey);
+ for (i = viewKey; i < client.viewsArray.length; i++)
+ client.viewsArray[i].tb.setAttribute("viewKey", i);
+ client.tabs.removeChild(tb);
+ }
+ else if (tb || (!tb && !create))
+ {
+ /* Either: we have a tab and don't want it moved, or didn't find one
+ * and don't wish to create one.
+ */
+ return tb;
+ }
+
+ // Didn't found a <tab>, but we're allowed to create one.
+ if (!tb && create)
+ {
+ if (!("messages" in source) || source.messages == null)
+ createMessages(source);
+
+ tb = document.createElement("tab");
+ tb.setAttribute("ondragstart", "tabsDNDObserver.onDragStart(event);");
+ tb.setAttribute("href", source.getURL());
+ tb.setAttribute("name", source.unicodeName);
+ tb.setAttribute("onclick", "onTabClick(event, this.id);");
+ // This wouldn't be here if there was a supported CSS property for it.
+ tb.setAttribute("crop", "center");
+ tb.setAttribute("context", "context:tab");
+ tb.setAttribute("class", "tab-bottom view-button");
+ tb.setAttribute("id", id);
+ tb.setAttribute("state", "normal");
+ name = source.prefs["tabLabel"] || name;
+ tb.setAttribute("label", name + (matches > 1 ? "<" + matches + ">" : ""));
+ tb.setAttribute("tooltiptext", source.viewName);
+ tb.view = source;
+
+ var browser = document.createElement("browser");
+ browser.setAttribute("class", "output-container");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("tooltip", "html-tooltip-node");
+ browser.setAttribute("onclick",
+ "return onMessageViewClick(event)");
+ browser.setAttribute("onmousedown",
+ "return onMessageViewMouseDown(event)");
+ browser.setAttribute("oncontextmenu",
+ "return onMessageViewContextMenu(event)");
+ browser.setAttribute("ondragover",
+ "contentDNDObserver.onDragOver(event);");
+ browser.setAttribute("ondrop", "contentDNDObserver.onDrop(event);");
+ browser.source = source;
+ source.frame = browser;
+ ASSERT(client.deck, "no deck?");
+ client.deck.appendChild(browser);
+ syncOutputFrame(source);
+
+ if (!("userList" in source) && (source.TYPE == "IRCChannel"))
+ {
+ source.userListShare = new Object();
+ source.userList = new XULTreeView(source.userListShare);
+ source.userList.getRowProperties = ul_getrowprops;
+ source.userList.getCellProperties = ul_getcellprops;
+ source.userList.childData.setSortDirection(1);
+ }
+ }
+
+ var beforeTab = null;
+ if (typeof client.pendingViewContext == "object")
+ {
+ var c = client.pendingViewContext;
+ /* If we have a <tab> to insert before, and it is still in the tabs,
+ * move the newly-created <tab> into the right place.
+ */
+ if (c.tabInsertBefore && (c.tabInsertBefore.parentNode == client.tabs))
+ beforeTab = c.tabInsertBefore;
+ }
+
+ if (beforeTab)
+ {
+ var viewKey = beforeTab.getAttribute("viewKey");
+ arrayInsertAt(client.viewsArray, viewKey, {source: source, tb: tb});
+ for (i = viewKey; i < client.viewsArray.length; i++)
+ client.viewsArray[i].tb.setAttribute("viewKey", i);
+ client.tabs.insertBefore(tb, beforeTab);
+ }
+ else
+ {
+ client.viewsArray.push({source: source, tb: tb});
+ tb.setAttribute("viewKey", client.viewsArray.length - 1);
+ client.tabs.appendChild(tb);
+ }
+
+ updateTabAttributes();
+
+ return tb;
+}
+
+function updateTabAttributes()
+{
+ /* XXX: Workaround for Gecko bugs 272646 and 261826. Note that this breaks
+ * the location of the spacers before and after the tabs but, due to our
+ * own <spacer>, their flex was not being utilised anyway.
+ */
+ var tabOrdinal = 0;
+ for (var tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
+ tab.ordinal = tabOrdinal++;
+
+ /* XXX: Workaround for tabbox.xml not coping with updating attributes when
+ * tabs are moved. We correct the "first-tab", "last-tab", "beforeselected"
+ * and "afterselected" attributes.
+ *
+ * "last-tab" and "beforeselected" are updated on each valid (non-collapsed
+ * and non-hidden) tab found, to avoid having to work backwards as well as
+ * forwards. "first-tab" and "afterselected" are just set the once each.
+ * |foundSelected| tracks where we are in relation to the selected tab.
+ */
+ var tabAttrs = {
+ "first-tab": null,
+ "last-tab": null,
+ "beforeselected": null,
+ "afterselected": null
+ };
+ var foundSelected = "before";
+ for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
+ {
+ if (tab.collapsed || tab.hidden)
+ continue;
+
+ if (!tabAttrs["first-tab"])
+ tabAttrs["first-tab"] = tab;
+ tabAttrs["last-tab"] = tab;
+
+ if ((foundSelected == "before") && tab.selected)
+ foundSelected = "on";
+ else if (foundSelected == "on")
+ foundSelected = "after";
+
+ if (foundSelected == "before")
+ tabAttrs["beforeselected"] = tab;
+ if ((foundSelected == "after") && !tabAttrs["afterselected"])
+ tabAttrs["afterselected"] = tab;
+ }
+
+ // After picking a tab for each attribute, apply them to the tabs.
+ for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
+ {
+ for (var attr in tabAttrs)
+ {
+ if (tabAttrs[attr] == tab)
+ tab.setAttribute(attr, "true");
+ else
+ tab.removeAttribute(attr);
+ }
+ }
+}
+
+// Properties getter for user list tree view
+function ul_getrowprops(index)
+{
+ if ((index < 0) || (index >= this.childData.childData.length))
+ {
+ return "";
+ }
+
+ // See bug 432482 - work around Gecko deficiency.
+ if (!this.selection.isSelected(index))
+ {
+ return "unselected";
+ }
+
+ return "";
+}
+
+// Properties getter for user list tree view
+function ul_getcellprops(index, column)
+{
+ if ((index < 0) || (index >= this.childData.childData.length))
+ {
+ return "";
+ }
+
+ var resultProps = [];
+
+ // See bug 432482 - work around Gecko deficiency.
+ if (!this.selection.isSelected(index))
+ resultProps.push("unselected");
+
+ var userObj = this.childData.childData[index]._userObj;
+
+ resultProps.push("voice-" + userObj.isVoice);
+ resultProps.push("op-" + userObj.isOp);
+ resultProps.push("halfop-" + userObj.isHalfOp);
+ resultProps.push("admin-" + userObj.isAdmin);
+ resultProps.push("founder-" + userObj.isFounder);
+ resultProps.push("away-" + userObj.isAway);
+
+ return resultProps.join(" ");
+}
+
+var contentDNDObserver = {
+ onDragOver(aEvent) {
+ if (aEvent.target == aEvent.dataTransfer.mozSourceNode)
+ return;
+
+ if (Services.droppedLinkHandler.canDropLink(aEvent, true))
+ aEvent.preventDefault();
+ },
+
+ onDrop(aEvent) {
+ aEvent.stopPropagation();
+
+ var url = Services.droppedLinkHandler.dropLink(aEvent, {});
+ if (!url || url.search(client.linkRE) == -1)
+ return;
+
+ if (url.search(/\.css$/i) != -1 && confirm(getMsg(MSG_TABDND_DROP, url)))
+ dispatch("motif", {"motif": url});
+ else if (url.search(/^ircs?:\/\//i) != -1)
+ dispatch("goto-url", {"url": url});
+ },
+}
+
+var tabsDNDObserver = {
+ onDragOver(aEvent) {
+ if (aEvent.target == aEvent.dataTransfer.mozSourceNode)
+ return;
+
+ // If we're not accepting the drag, don't show the marker either.
+ if (!Services.droppedLinkHandler.canDropLink(aEvent, true)) {
+ client.tabDragBar.collapsed = true;
+ return;
+ }
+
+ aEvent.preventDefault();
+
+ /* Locate the tab we're about to drop onto. We split tabs in half, dropping
+ * on the side closest to the mouse, or after the last tab if the mouse is
+ * somewhere beyond all the tabs.
+ */
+ var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr");
+ var newPosition = client.tabs.firstChild.boxObject.x;
+ for (var dropTab = client.tabs.firstChild; dropTab;
+ dropTab = dropTab.nextSibling)
+ {
+ if (dropTab.collapsed || dropTab.hidden)
+ continue;
+ var bo = dropTab.boxObject;
+ if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) ||
+ (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2)))
+ {
+ break;
+ }
+ newPosition = bo.x + bo.width;
+ }
+
+ // Reposition the drop marker and show it. In that order.
+ client.tabDragMarker.style.MozMarginStart = newPosition + "px";
+ client.tabDragBar.collapsed = false;
+ },
+
+ onDragExit(aEvent) {
+ aEvent.stopPropagation();
+
+ /* We've either stopped being part of a drag operation, or the dragging is
+ * somewhere away from us.
+ */
+ client.tabDragBar.collapsed = true;
+ },
+
+ onDrop(aEvent) {
+ aEvent.stopPropagation();
+
+ // Dragging has finished.
+ client.tabDragBar.collapsed = true;
+
+ // See comment above |var tabsDropObserver|.
+ var url = Services.droppedLinkHandler.dropLink(aEvent, {});
+ if (!url || !(url.match(/^ircs?:/) || url.match(/^x-irc-dcc-(chat|file):/)))
+ return;
+
+ // Find the tab to insertBefore() the new one.
+ var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr");
+ for (var dropTab = client.tabs.firstChild; dropTab;
+ dropTab = dropTab.nextSibling)
+ {
+ if (dropTab.collapsed || dropTab.hidden)
+ continue;
+ var bo = dropTab.boxObject;
+ if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) ||
+ (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2)))
+ {
+ break;
+ }
+ }
+
+ // Check if the URL is already in the views.
+ for (var i = 0; i < client.viewsArray.length; i++)
+ {
+ var view = client.viewsArray[i].source;
+ if (view.getURL() == url)
+ {
+ client.pendingViewContext = { tabInsertBefore: dropTab };
+ dispatch("create-tab-for-view", { view: view });
+ delete client.pendingViewContext;
+ return;
+ }
+ }
+
+ // URL not found in tabs, so force it into life - this may connect/rejoin.
+ if (url.substring(0, 3) == "irc")
+ gotoIRCURL(url, { tabInsertBefore: dropTab });
+ },
+
+ onDragStart(aEvent) {
+ var tb = aEvent.currentTarget;
+ var href = tb.getAttribute("href");
+ var name = tb.getAttribute("name");
+
+ /* x-moz-url has the format "<url>\n<name>", goodie */
+ aEvent.dataTransfer.setData("text/x-moz-url", href + "\n" + name);
+ aEvent.dataTransfer.setData("text/unicode", href);
+ aEvent.dataTransfer.setData("text/plain", href);
+ aEvent.dataTransfer.setData("text/html", "<a href='" + href + "'>" +
+ name + "</a>");
+ },
+}
+
+var userlistDNDObserver = {
+ onDragStart(aEvent) {
+ var col = {};
+ var row = {};
+ var cell = {};
+ var tree = document.getElementById('user-list');
+ tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY,
+ row, col, cell);
+ // Check whether we're actually on a normal row and cell
+ if (!cell.value || (row.value == -1))
+ return;
+
+ var nickname = getNicknameForUserlistRow(row.value);
+ aEvent.dataTransfer.setData("text/unicode", nickname);
+ aEvent.dataTransfer.setData("text/plain", nickname);
+ },
+}
+
+function deleteTab(tb)
+{
+ if (!ASSERT(tb.hasAttribute("viewKey"),
+ "INVALID OBJECT passed to deleteTab (" + tb + ")"))
+ {
+ return null;
+ }
+
+ var key = Number(tb.getAttribute("viewKey"));
+
+ // Re-index higher tabs.
+ for (var i = key + 1; i < client.viewsArray.length; i++)
+ client.viewsArray[i].tb.setAttribute("viewKey", i - 1);
+ arrayRemoveAt(client.viewsArray, key);
+ client.tabs.removeChild(tb);
+ setTimeout(updateTabAttributes, 0);
+
+ return key;
+}
+
+function deleteFrame(view)
+{
+ const nsIWebProgress = Components.interfaces.nsIWebProgress;
+ const ALL = nsIWebProgress.NOTIFY_ALL;
+
+ // We leave the progress listener attached so try to remove it.
+ try
+ {
+ view.frame.removeProgressListener(client.progressListener, ALL);
+ }
+ catch (ex)
+ {
+ dd(formatException(ex));
+ }
+
+ client.deck.removeChild(view.frame);
+ delete view.frame;
+}
+
+function filterOutput(msg, msgtype, dest)
+{
+ if ("outputFilters" in client)
+ {
+ for (var f in client.outputFilters)
+ {
+ if (client.outputFilters[f].enabled)
+ msg = client.outputFilters[f].func(msg, msgtype, dest);
+ }
+ }
+
+ return msg;
+}
+
+function updateTimestamps(view)
+{
+ if (!("messages" in view))
+ return;
+
+ view._timestampLast = "";
+ var node = view.messages.firstChild.firstChild;
+ var nested;
+ while (node)
+ {
+ if(node.className == "msg-nested-tr")
+ {
+ nested = node.firstChild.firstChild.firstChild.firstChild;
+ while (nested)
+ {
+ updateTimestampFor(view, nested);
+ nested = nested.nextSibling;
+ }
+ }
+ else
+ {
+ updateTimestampFor(view, node);
+ }
+ node = node.nextSibling;
+ }
+}
+
+function updateTimestampFor(view, displayRow, forceOldStamp)
+{
+ var time = new Date(1 * displayRow.getAttribute("timestamp"));
+ var tsCell = displayRow.firstChild;
+ if (!tsCell)
+ return;
+
+ var fmt;
+ if (view.prefs["timestamps"])
+ fmt = strftime(view.prefs["timestamps.display"], time);
+
+ while (tsCell.lastChild)
+ tsCell.removeChild(tsCell.lastChild);
+
+ var needStamp = fmt && (forceOldStamp || !view.prefs["collapseMsgs"] ||
+ (fmt != view._timestampLast));
+ if (needStamp)
+ tsCell.appendChild(document.createTextNode(fmt));
+ if (!forceOldStamp)
+ view._timestampLast = fmt;
+}
+
+client.updateMenus =
+function c_updatemenus(menus)
+{
+ // Don't bother if the menus aren't even created yet.
+ if (!client.initialized)
+ return null;
+
+ return this.menuManager.updateMenus(document, menus);
+}
+
+client.checkURLScheme =
+function c_checkURLScheme(url)
+{
+ if (!("schemes" in client))
+ {
+ var pfx = "@mozilla.org/network/protocol;1?name=";
+ var len = pfx.length;
+
+ client.schemes = new Object();
+ for (var c in Components.classes)
+ {
+ if (c.indexOf(pfx) == 0)
+ client.schemes[c.substr(len)] = true;
+ }
+ }
+ return (url.toLowerCase() in client.schemes);
+}
+
+client.adoptNode =
+function cli_adoptnode(node, doc)
+{
+ try
+ {
+ doc.adoptNode(node);
+ }
+ catch(ex)
+ {
+ dd(formatException(ex));
+ var err = ex.name;
+ // TypeError from before adoptNode was added; NOT_IMPL after.
+ if ((err == "TypeError") || (err == "NS_ERROR_NOT_IMPLEMENTED"))
+ client.adoptNode = cli_adoptnode_noop;
+ }
+ return node;
+}
+
+function cli_adoptnode_noop(node, doc)
+{
+ return node;
+}
+
+client.addNetwork =
+function cli_addnet(name, serverList, temporary)
+{
+ let net = new CIRCNetwork(name, serverList, client.eventPump, temporary);
+ client.networks[net.collectionKey] = net;
+}
+
+client.getNetwork =
+function cli_getnet(name)
+{
+ return client.networks[":" + name] || null;
+}
+
+client.removeNetwork =
+function cli_removenet(name)
+{
+ let net = client.getNetwork(name);
+
+ // Allow network a chance to clean up any mess.
+ if (typeof net.destroy == "function")
+ net.destroy();
+
+ delete client.networks[net.collectionKey];
+}
+
+client.connectToNetwork =
+function cli_connect(networkOrName, requireSecurity)
+{
+ var network;
+ var name;
+
+
+ if (isinstance(networkOrName, CIRCNetwork))
+ {
+ network = networkOrName;
+ }
+ else
+ {
+ name = networkOrName;
+ network = client.getNetwork(name);
+
+ if (!network)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name), MT_ERROR);
+ return null;
+ }
+ }
+ name = network.unicodeName;
+
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+
+ if (network.isConnected())
+ {
+ network.display(getMsg(MSG_ALREADY_CONNECTED, name));
+ return network;
+ }
+
+ if (network.state != NET_OFFLINE)
+ return network;
+
+ if (network.prefs["nickname"] == DEFAULT_NICK)
+ network.prefs["nickname"] = prompt(MSG_ENTER_NICK, DEFAULT_NICK);
+
+ if (!("connecting" in network))
+ network.display(getMsg(MSG_NETWORK_CONNECTING, name));
+
+ network.connect(requireSecurity);
+
+ network.updateHeader();
+ client.updateHeader();
+ updateTitle();
+
+ return network;
+}
+
+
+client.getURL =
+function cli_geturl ()
+{
+ return "irc://";
+}
+
+client.load =
+function cli_load(url, scope)
+{
+ if (!("_loader" in client))
+ {
+ const LOADER_CTRID = "@mozilla.org/moz/jssubscript-loader;1";
+ const mozIJSSubScriptLoader =
+ Components.interfaces.mozIJSSubScriptLoader;
+
+ var cls;
+ if ((cls = Components.classes[LOADER_CTRID]))
+ client._loader = cls.getService(mozIJSSubScriptLoader);
+ }
+
+ if (client._loader.loadSubScriptWithOptions)
+ {
+ var opts = {target: scope, ignoreCache: true};
+ return client._loader.loadSubScriptWithOptions(url, opts);
+ }
+
+ return client._loader.loadSubScript(url, scope);
+}
+
+client.sayToCurrentTarget =
+function cli_say(msg, isInteractive)
+{
+ if ("say" in client.currentObject)
+ {
+ client.currentObject.dispatch("say", {message: msg}, isInteractive);
+ return;
+ }
+
+ switch (client.currentObject.TYPE)
+ {
+ case "IRCClient":
+ dispatch("eval", {expression: msg}, isInteractive);
+ break;
+
+ default:
+ if (msg != "")
+ display(MSG_ERR_NO_DEFAULT, MT_ERROR);
+ break;
+ }
+}
+
+CIRCNetwork.prototype.__defineGetter__("prefs", net_getprefs);
+function net_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getNetworkPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCNetwork.prototype.__defineGetter__("prefManager", net_getprefmgr);
+function net_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getNetworkPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCServer.prototype.__defineGetter__("prefs", srv_getprefs);
+function srv_getprefs()
+{
+ return this.parent.prefs;
+}
+
+CIRCServer.prototype.__defineGetter__("prefManager", srv_getprefmgr);
+function srv_getprefmgr()
+{
+ return this.parent.prefManager;
+}
+
+CIRCChannel.prototype.__defineGetter__("prefs", chan_getprefs);
+function chan_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getChannelPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCChannel.prototype.__defineGetter__("prefManager", chan_getprefmgr);
+function chan_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getChannelPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCUser.prototype.__defineGetter__("prefs", usr_getprefs);
+function usr_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCUser.prototype.__defineGetter__("prefManager", usr_getprefmgr);
+function usr_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCDCCUser.prototype.__defineGetter__("prefs", dccusr_getprefs);
+function dccusr_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getDCCUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCDCCUser.prototype.__defineGetter__("prefManager", dccusr_getprefmgr);
+function dccusr_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getDCCUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCDCCChat.prototype.__defineGetter__("prefs", dccchat_getprefs);
+function dccchat_getprefs()
+{
+ return this.user.prefs;
+}
+
+CIRCDCCChat.prototype.__defineGetter__("prefManager", dccchat_getprefmgr);
+function dccchat_getprefmgr()
+{
+ return this.user.prefManager;
+}
+
+CIRCDCCFileTransfer.prototype.__defineGetter__("prefs", dccfile_getprefs);
+function dccfile_getprefs()
+{
+ return this.user.prefs;
+}
+
+CIRCDCCFileTransfer.prototype.__defineGetter__("prefManager", dccfile_getprefmgr);
+function dccfile_getprefmgr()
+{
+ return this.user.prefManager;
+}
+
+/* Displays a network-centric message on the most appropriate view.
+ *
+ * When |client.SLOPPY_NETWORKS| is |true|, messages will be displayed on the
+ * *current* view instead of the network view, if the current view is part of
+ * the same network.
+ */
+CIRCNetwork.prototype.display =
+function net_display(message, msgtype, sourceObj, destObj, tags)
+{
+ var o = getObjectDetails(client.currentObject);
+ if (client.SLOPPY_NETWORKS && client.currentObject != this &&
+ o.network == this && o.server && o.server.isConnected)
+ {
+ client.currentObject.display(message, msgtype, sourceObj, destObj,
+ tags);
+ }
+ else
+ {
+ this.displayHere(message, msgtype, sourceObj, destObj, tags);
+ }
+}
+
+/* Displays a channel-centric message on the most appropriate view.
+ *
+ * If the channel view already exists (visible or hidden), messages are added
+ * to it; otherwise, messages go to the *network* view.
+ */
+CIRCChannel.prototype.display =
+function chan_display(message, msgtype, sourceObj, destObj, tags)
+{
+ if ("messages" in this)
+ this.displayHere(message, msgtype, sourceObj, destObj, tags);
+ else
+ this.parent.parent.displayHere(message, msgtype, sourceObj, destObj,
+ tags);
+}
+
+/* Displays a user-centric message on the most appropriate view.
+ *
+ * If the user view already exists (visible or hidden), messages are added to
+ * it; otherwise, it goes to the *current* view if the current view is part of
+ * the same network, or the *network* view if not.
+ */
+CIRCUser.prototype.display =
+function usr_display(message, msgtype, sourceObj, destObj, tags)
+{
+ if ("messages" in this)
+ {
+ this.displayHere(message, msgtype, sourceObj, destObj, tags);
+ }
+ else
+ {
+ var o = getObjectDetails(client.currentObject);
+ if (o.server && o.server.isConnected &&
+ o.network == this.parent.parent &&
+ client.currentObject.TYPE != "IRCUser")
+ client.currentObject.display(message, msgtype, sourceObj, destObj,
+ tags);
+ else
+ this.parent.parent.displayHere(message, msgtype, sourceObj,
+ destObj, tags);
+ }
+}
+
+/* Displays a DCC user/file transfer-centric message on the most appropriate view.
+ *
+ * If the DCC user/file transfer view already exists (visible or hidden),
+ * messages are added to it; otherwise, messages go to the *current* view.
+ */
+CIRCDCCChat.prototype.display =
+CIRCDCCFileTransfer.prototype.display =
+function dcc_display(message, msgtype, sourceObj, destObj)
+{
+ if ("messages" in this)
+ this.displayHere(message, msgtype, sourceObj, destObj);
+ else
+ client.currentObject.display(message, msgtype, sourceObj, destObj);
+}
+
+function feedback(e, message, msgtype, sourceObj, destObj)
+{
+ if ("isInteractive" in e && e.isInteractive)
+ display(message, msgtype, sourceObj, destObj);
+}
+
+CIRCChannel.prototype.feedback =
+CIRCNetwork.prototype.feedback =
+CIRCUser.prototype.feedback =
+CIRCDCCChat.prototype.feedback =
+CIRCDCCFileTransfer.prototype.feedback =
+client.feedback =
+function this_feedback(e, message, msgtype, sourceObj, destObj)
+{
+ if ("isInteractive" in e && e.isInteractive)
+ this.displayHere(message, msgtype, sourceObj, destObj);
+}
+
+function display (message, msgtype, sourceObj, destObj, tags)
+{
+ client.currentObject.display (message, msgtype, sourceObj, destObj, tags);
+}
+
+client.getFontCSS =
+CIRCNetwork.prototype.getFontCSS =
+CIRCChannel.prototype.getFontCSS =
+CIRCUser.prototype.getFontCSS =
+CIRCDCCChat.prototype.getFontCSS =
+CIRCDCCFileTransfer.prototype.getFontCSS =
+function this_getFontCSS(format)
+{
+ /* Wow, this is cool. We just put together a CSS-rule string based on the
+ * font preferences. *This* is what CSS is all about. :)
+ * We also provide a "data: URL" format, to simplify other code.
+ */
+ var css;
+ var fs;
+ var fn;
+
+ if (this.prefs["font.family"] != "default")
+ fn = "font-family: " + this.prefs["font.family"] + ";";
+ else
+ fn = "font-family: inherit;";
+ if (this.prefs["font.size"] != 0)
+ fs = "font-size: " + this.prefs["font.size"] + "pt;";
+ else
+ fs = "font-size: medium;";
+
+ css = ".chatzilla-body { " + fs + fn + " }";
+
+ if (format == "data")
+ return "data:text/css," + encodeURIComponent(css);
+ return css;
+}
+
+client.startMsgGroup =
+CIRCNetwork.prototype.startMsgGroup =
+CIRCChannel.prototype.startMsgGroup =
+CIRCUser.prototype.startMsgGroup =
+CIRCDCCChat.prototype.startMsgGroup =
+CIRCDCCFileTransfer.prototype.startMsgGroup =
+function __startMsgGroup(id, groupMsg, msgtype)
+{
+ // The given ID may not be unique, so append a timestamp to ensure it is.
+ var groupId = id + "-" + Date.now();
+
+ // Add the button to the end of the message.
+ var headerMsg = groupMsg + " " + getMsg(MSG_COLLAPSE_BUTTON,
+ [MSG_COLLAPSE_HIDE,
+ MSG_COLLAPSE_HIDETITLE,
+ groupId]);
+
+ // Show the group header message.
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.displayHere(headerMsg, msgtype);
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ // Add the group to a list of active message groups.
+ if (!this.msgGroups)
+ this.msgGroups = [];
+ this.msgGroups.push(groupId);
+
+ // Return the actual ID in case the caller wants to use it later.
+ return groupId;
+}
+
+function startMsgGroup(groupId, headerMsg, msgtype)
+{
+ client.currentObject.startMsgGroup(groupId, headerMsg, msgtype);
+}
+
+client.endMsgGroup =
+CIRCNetwork.prototype.endMsgGroup =
+CIRCChannel.prototype.endMsgGroup =
+CIRCUser.prototype.endMsgGroup =
+CIRCDCCChat.prototype.endMsgGroup =
+CIRCDCCFileTransfer.prototype.endMsgGroup =
+function __endMsgGroup(groupId, message)
+{
+ if (!this.msgGroups)
+ return;
+
+ // Remove the group from the list of active message groups.
+ this.msgGroups.pop();
+ if (this.msgGroups.length == 0)
+ delete this.msgGroups;
+}
+
+function endMsgGroup()
+{
+ client.currentObject.endMsgGroup();
+}
+
+client.display =
+client.displayHere =
+CIRCNetwork.prototype.displayHere =
+CIRCChannel.prototype.displayHere =
+CIRCUser.prototype.displayHere =
+CIRCDCCChat.prototype.displayHere =
+CIRCDCCFileTransfer.prototype.displayHere =
+function __display(message, msgtype, sourceObj, destObj, tags)
+{
+ // We need a message type, assume "INFO".
+ if (!msgtype)
+ msgtype = MT_INFO;
+
+ var msgprefix = "";
+ if (msgtype.indexOf("/") != -1)
+ {
+ var ary = msgtype.match(/^(.*)\/(.*)$/);
+ msgtype = ary[1];
+ msgprefix = ary[2];
+ }
+
+ var blockLevel = false; /* true if this row should be rendered at block
+ * level, (like, if it has a really long nickname
+ * that might disturb the rest of the layout) */
+ var o = getObjectDetails(this); /* get the skinny on |this| */
+
+ // Get the 'me' object, so we can be sure to get the attributes right.
+ var me;
+ if ("me" in this)
+ me = this.me;
+ else if (o.server && "me" in o.server)
+ me = o.server.me;
+
+ /* Allow for matching (but not identical) user objects here. This tends to
+ * happen with bouncers and proxies, when they send channel messages
+ * pretending to be from the user; the sourceObj is a CIRCChanUser
+ * instead of a CIRCUser so doesn't == 'me'.
+ */
+ if (me)
+ {
+ if (sourceObj && (sourceObj.canonicalName == me.canonicalName))
+ sourceObj = me;
+ if (destObj && (destObj.canonicalName == me.canonicalName))
+ destObj = me;
+ }
+
+ // Let callers get away with "ME!" and we have to substitute here.
+ if (sourceObj == "ME!")
+ sourceObj = me;
+ if (destObj == "ME!")
+ destObj = me;
+
+ // Get the TYPE of the source object.
+ var fromType = (sourceObj && sourceObj.TYPE) ? sourceObj.TYPE : "unk";
+ // Is the source a user?
+ var fromUser = (fromType.search(/IRC.*User/) != -1);
+ // Get some sort of "name" for the source.
+ var fromAttr = "";
+ if (sourceObj)
+ {
+ if ("canonicalName" in sourceObj)
+ fromAttr = sourceObj.canonicalName;
+ else if ("name" in sourceObj)
+ fromAttr = sourceObj.name;
+ else
+ fromAttr = sourceObj.viewName;
+ }
+
+ // Get the dest TYPE too...
+ var toType = (destObj) ? destObj.TYPE : "unk";
+ // Is the dest a user?
+ var toUser = (toType.search(/IRC.*User/) != -1);
+ // Get a dest name too...
+ var toAttr = "";
+ if (destObj)
+ {
+ if ("canonicalName" in destObj)
+ toAttr = destObj.canonicalName;
+ else if ("name" in destObj)
+ toAttr = destObj.name;
+ else
+ toAttr = destObj.viewName;
+ }
+
+ // Is the message 'to' or 'from' somewhere other than this view
+ var toOther = ((sourceObj == me) && destObj && (destObj != this));
+ var fromOther = (toUser && (destObj == me) && (sourceObj != this) &&
+ // Need extra check for DCC users:
+ !((this.TYPE == "IRCDCCChat") && (this.user == sourceObj)));
+
+ // Attach "ME!" if appropriate, so motifs can style differently.
+ if ((sourceObj == me) && !toOther)
+ fromAttr = fromAttr + " ME!";
+ if (destObj && destObj == me)
+ toAttr = me.canonicalName + " ME!";
+
+ /* isImportant means to style the messages as important, and flash the
+ * window, getAttention means just flash the window. */
+ var isImportant = false, getAttention = false, isSuperfluous = false;
+ var viewType = this.TYPE;
+ var code;
+ var time;
+ if (tags && ("time" in tags))
+ time = new Date(tags.time);
+ else
+ time = new Date();
+
+ var timeStamp = strftime(this.prefs["timestamps.log"], time);
+
+ // Statusbar text, and the line that gets saved to the log.
+ var statusString;
+ var logStringPfx = timeStamp + " ";
+ var logStrings = new Array();
+
+ if (fromUser)
+ {
+ statusString = getMsg(MSG_FMT_STATUS,
+ [timeStamp,
+ sourceObj.unicodeName + "!" +
+ sourceObj.name + "@" + sourceObj.host]);
+ }
+ else
+ {
+ var name;
+ if (sourceObj)
+ name = sourceObj.viewName;
+ else
+ name = this.viewName;
+
+ statusString = getMsg(MSG_FMT_STATUS,
+ [timeStamp, name]);
+ }
+
+ // The table row, and it's attributes.
+ var msgRow = document.createElementNS(XHTML_NS, "html:tr");
+ msgRow.setAttribute("class", "msg");
+ if (this.msgGroups)
+ msgRow.setAttribute("msg-groups", this.msgGroups.join(', '));
+ msgRow.setAttribute("msg-type", msgtype);
+ msgRow.setAttribute("msg-prefix", msgprefix);
+ msgRow.setAttribute("msg-dest", toAttr);
+ msgRow.setAttribute("dest-type", toType);
+ msgRow.setAttribute("view-type", viewType);
+ msgRow.setAttribute("status-text", statusString);
+ msgRow.setAttribute("timestamp", Number(time));
+ if (fromAttr)
+ {
+ if (fromUser)
+ {
+ msgRow.setAttribute("msg-user", fromAttr);
+ // Set some mode information for channel users
+ if (fromType == 'IRCChanUser')
+ msgRow.setAttribute("msg-user-mode", sourceObj.modes.join(" "));
+ }
+ else
+ {
+ msgRow.setAttribute("msg-source", fromAttr);
+ }
+ }
+ if (toOther)
+ msgRow.setAttribute("to-other", toOther);
+ if (fromOther)
+ msgRow.setAttribute("from-other", fromOther);
+
+ // Timestamp cell.
+ var msgRowTimestamp = document.createElementNS(XHTML_NS, "html:td");
+ msgRowTimestamp.setAttribute("class", "msg-timestamp");
+
+ var canMergeData;
+ var msgRowSource, msgRowType, msgRowData;
+ if (fromUser && msgtype.match(/^(PRIVMSG|ACTION|NOTICE|WALLOPS)$/))
+ {
+ var nick = sourceObj.unicodeName;
+ var decorSt = "";
+ var decorEn = "";
+
+ // Set default decorations.
+ if (msgtype == "ACTION")
+ {
+ decorSt = "* ";
+ }
+ else
+ {
+ decorSt = "<";
+ decorEn = ">";
+ }
+
+ var nickURL;
+ if ((sourceObj != me) && ("getURL" in sourceObj))
+ nickURL = sourceObj.getURL();
+ if (toOther && ("getURL" in destObj))
+ nickURL = destObj.getURL();
+
+ if (sourceObj != me)
+ {
+ // Not from us...
+ if (destObj == me)
+ {
+ // ...but to us. Messages from someone else to us.
+
+ getAttention = true;
+ this.defaultCompletion = "/msg " + nick + " ";
+
+ // If this is a private message, and it's not in a query view,
+ // use *nick* instead of <nick>.
+ if ((msgtype != "ACTION") && (this.TYPE != "IRCUser"))
+ {
+ decorSt = "*";
+ decorEn = "*";
+ }
+ }
+ else
+ {
+ // ...or to us. Messages from someone else to channel or similar.
+
+ if ((typeof message == "string") && me)
+ isImportant = msgIsImportant(message, nick, o.network);
+ else if (message.hasAttribute("isImportant") && me)
+ isImportant = true;
+
+ if (isImportant)
+ {
+ this.defaultCompletion = nick +
+ client.prefs["nickCompleteStr"] + " ";
+ }
+ }
+ }
+ else
+ {
+ // Messages from us, to somewhere other than this view
+ if (toOther)
+ {
+ nick = destObj.unicodeName;
+ decorSt = ">";
+ decorEn = "<";
+ }
+ }
+
+ // Log the nickname in the same format as we'll let the user copy.
+ // If the message has a prefix, show it after a "/".
+ if (msgprefix)
+ logStringPfx += decorSt + nick + "/" + msgprefix + decorEn + " ";
+ else
+ logStringPfx += decorSt + nick + decorEn + " ";
+
+ if (!("lastNickDisplayed" in this) || this.lastNickDisplayed != nick)
+ {
+ this.lastNickDisplayed = nick;
+ this.mark = (("mark" in this) && this.mark == "even") ? "odd" : "even";
+ }
+
+ msgRowSource = document.createElementNS(XHTML_NS, "html:td");
+ msgRowSource.setAttribute("class", "msg-user");
+
+ // Make excessive nicks get shunted.
+ if (nick && (nick.length > client.MAX_NICK_DISPLAY))
+ blockLevel = true;
+
+ if (decorSt)
+ msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor"));
+ if (nickURL)
+ {
+ var nick_anchor = document.createElementNS(XHTML_NS, "html:a");
+ nick_anchor.setAttribute("class", "chatzilla-link");
+ nick_anchor.setAttribute("href", nickURL);
+ nick_anchor.appendChild(newInlineText(nick));
+ msgRowSource.appendChild(nick_anchor);
+ }
+ else
+ {
+ msgRowSource.appendChild(newInlineText(nick));
+ }
+ if (msgprefix)
+ {
+ /* We don't style the "/" with chatzilla-decor because the one
+ * thing we don't want is it disappearing!
+ */
+ msgRowSource.appendChild(newInlineText("/", ""));
+ msgRowSource.appendChild(newInlineText(msgprefix,
+ "chatzilla-prefix"));
+ }
+ if (decorEn)
+ msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor"));
+ canMergeData = this.prefs["collapseMsgs"];
+ }
+ else if (msgprefix)
+ {
+ decorSt = "<";
+ decorEn = ">";
+
+ logStringPfx += decorSt + "/" + msgprefix + decorEn + " ";
+
+ msgRowSource = document.createElementNS(XHTML_NS, "html:td");
+ msgRowSource.setAttribute("class", "msg-user");
+
+ msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor"));
+ msgRowSource.appendChild(newInlineText("/", ""));
+ msgRowSource.appendChild(newInlineText(msgprefix, "chatzilla-prefix"));
+ msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor"));
+ canMergeData = this.prefs["collapseMsgs"];
+ }
+ else
+ {
+ isSuperfluous = true;
+ if (!client.debugHook.enabled && msgtype in client.responseCodeMap)
+ {
+ code = client.responseCodeMap[msgtype];
+ }
+ else
+ {
+ if (!client.debugHook.enabled && client.HIDE_CODES)
+ code = client.DEFAULT_RESPONSE_CODE;
+ else
+ code = "[" + msgtype + "]";
+ }
+
+ /* Display the message code */
+ msgRowType = document.createElementNS(XHTML_NS, "html:td");
+ msgRowType.setAttribute("class", "msg-type");
+
+ msgRowType.appendChild(newInlineText(code));
+ logStringPfx += code + " ";
+ }
+
+ if (message)
+ {
+ msgRowData = document.createElementNS(XHTML_NS, "html:td");
+ msgRowData.setAttribute("class", "msg-data");
+
+ var tmpMsgs = message;
+ if (typeof message == "string")
+ {
+ msgRowData.appendChild(stringToMsg(message, this));
+ }
+ else
+ {
+ msgRowData.appendChild(message);
+ tmpMsgs = tmpMsgs.innerHTML.replace(/<[^<]*>/g, "");
+ }
+ tmpMsgs = tmpMsgs.split(/\r?\n/);
+ for (var l = 0; l < tmpMsgs.length; l++)
+ logStrings[l] = logStringPfx + tmpMsgs[l];
+ }
+
+ if ("mark" in this)
+ msgRow.setAttribute("mark", this.mark);
+
+ if (isImportant)
+ {
+ if ("importantMessages" in this)
+ {
+ var importantId = "important" + (this.importantMessages++);
+ msgRow.setAttribute("id", importantId);
+ }
+ msgRow.setAttribute("important", "true");
+ msgRow.setAttribute("aria-live", "assertive");
+ }
+
+ // Timestamps first...
+ msgRow.appendChild(msgRowTimestamp);
+ // Now do the rest of the row, after block-level stuff.
+ if (msgRowSource)
+ msgRow.appendChild(msgRowSource);
+ else
+ msgRow.appendChild(msgRowType);
+ if (msgRowData)
+ msgRow.appendChild(msgRowData);
+ updateTimestampFor(this, msgRow);
+
+ if (blockLevel)
+ {
+ /* putting a div here crashes mozilla, so fake it with nested tables
+ * for now */
+ var tr = document.createElementNS(XHTML_NS, "html:tr");
+ tr.setAttribute ("class", "msg-nested-tr");
+ var td = document.createElementNS(XHTML_NS, "html:td");
+ td.setAttribute ("class", "msg-nested-td");
+ td.setAttribute ("colspan", "3");
+
+ tr.appendChild(td);
+ var table = document.createElementNS(XHTML_NS, "html:table");
+ table.setAttribute ("class", "msg-nested-table");
+ table.setAttribute("role", "presentation");
+
+ td.appendChild (table);
+ var tbody = document.createElementNS(XHTML_NS, "html:tbody");
+
+ tbody.appendChild(msgRow);
+ table.appendChild(tbody);
+ msgRow = tr;
+ }
+
+ // Actually add the item.
+ addHistory (this, msgRow, canMergeData);
+
+ // Update attention states...
+ if (isImportant || getAttention)
+ {
+ setTabState(this, "attention");
+ if (client.prefs["notify.aggressive"])
+ window.getAttention();
+ }
+ else
+ {
+ if (isSuperfluous)
+ {
+ setTabState(this, "superfluous");
+ }
+ else
+ {
+ setTabState(this, "activity");
+ }
+ }
+
+ // Copy Important Messages [to network view].
+ if (isImportant && client.prefs["copyMessages"] && (o.network != this))
+ {
+ if (importantId)
+ {
+ // Create the linked inline button
+ var msgspan = document.createElementNS(XHTML_NS, "html:span");
+ msgspan.setAttribute("isImportant", "true");
+
+ var cmd = "jump-to-anchor " + importantId + " " + this.unicodeName;
+ var prefix = getMsg(MSG_JUMPTO_BUTTON, [this.unicodeName, cmd]);
+
+ // Munge prefix as a button
+ client.munger.getRule(".inline-buttons").enabled = true;
+ client.munger.munge(prefix + " ", msgspan, o);
+
+ // Munge rest of message normally
+ client.munger.getRule(".inline-buttons").enabled = false;
+ client.munger.munge(message, msgspan, o);
+
+ o.network.displayHere(msgspan, msgtype, sourceObj, destObj);
+ }
+ else
+ {
+ o.network.displayHere(message, msgtype, sourceObj, destObj);
+ }
+ }
+
+ // Log file time!
+ if (this.prefs["log"])
+ {
+ if (!this.logFile)
+ client.openLogFile(this);
+
+ try
+ {
+ var LE = client.lineEnd;
+ for (var l = 0; l < logStrings.length; l++)
+ this.logFile.write(fromUnicode(logStrings[l] + LE, "utf-8"));
+ }
+ catch (ex)
+ {
+ // Stop logging before showing any messages!
+ this.prefs["log"] = false;
+ dd("Log file write error: " + formatException(ex));
+ this.displayHere(getMsg(MSG_LOGFILE_WRITE_ERROR, getLogPath(this)),
+ "ERROR");
+ }
+ }
+
+ /* We want to show alerts if they're from a non-current view (optional),
+ * or we don't have focus at all.
+ */
+ if (client.prefs["alert.globalEnabled"]
+ && this.prefs["alert.enabled"] && client.alert &&
+ (!window.isFocused
+ || (!client.prefs['alert.nonFocusedOnly'] &&
+ !("currentObject" in client && client.currentObject == this)
+ )
+ )
+ )
+ {
+ if (isImportant)
+ {
+ showEventAlerts(this.TYPE, "stalk", message, nick, o, this, msgtype);
+ }
+ else if (isSuperfluous)
+ {
+ showEventAlerts(this.TYPE, "event", message, nick, o, this, msgtype);
+ }
+ else
+ {
+ showEventAlerts(this.TYPE, "chat" , message, nick, o, this, msgtype);
+ }
+ }
+
+}
+
+function addHistory (source, obj, mergeData)
+{
+ if (!("messages" in source) || (source.messages == null))
+ createMessages(source);
+
+ var tbody = source.messages.firstChild;
+ var appendTo = tbody;
+
+ var needScroll = false;
+
+ if (mergeData)
+ {
+ var inobj = obj;
+ // This gives us the non-nested row when there is nesting.
+ if (inobj.className == "msg-nested-tr")
+ inobj = inobj.firstChild.firstChild.firstChild.firstChild;
+
+ var thisUserCol = inobj.firstChild;
+ while (thisUserCol && !thisUserCol.className.match(/^(msg-user|msg-type)$/))
+ thisUserCol = thisUserCol.nextSibling;
+
+ var thisMessageCol = inobj.firstChild;
+ while (thisMessageCol && !(thisMessageCol.className == "msg-data"))
+ thisMessageCol = thisMessageCol.nextSibling;
+
+ let columnInfo = findPreviousColumnInfo(source.messages);
+ let nickColumns = columnInfo.nickColumns;
+ let rowExtents = columnInfo.extents;
+ let nickColumnCount = nickColumns.length;
+
+ let lastRowSpan = 0;
+ let sameNick = false;
+ let samePrefix = false;
+ let sameDest = false;
+ let haveSameType = false;
+ let isAction = false;
+ let collapseActions;
+ let needSameType = false;
+ // 1 or messages, check for doubles.
+ if (nickColumnCount > 0)
+ {
+ var lastRow = nickColumns[nickColumnCount - 1].parentNode;
+ // What was the span last time?
+ lastRowSpan = Number(nickColumns[0].getAttribute("rowspan"));
+ // Are we the same user as last time?
+ sameNick = (lastRow.getAttribute("msg-user") ==
+ inobj.getAttribute("msg-user"));
+ // Do we have the same prefix as last time?
+ samePrefix = (lastRow.getAttribute("msg-prefix") ==
+ inobj.getAttribute("msg-prefix"));
+ // Do we have the same destination as last time?
+ sameDest = (lastRow.getAttribute("msg-dest") ==
+ inobj.getAttribute("msg-dest"));
+ // Is this message the same type as the last one?
+ haveSameType = (lastRow.getAttribute("msg-type") ==
+ inobj.getAttribute("msg-type"));
+ // Is either of the messages an action? We may not want to collapse
+ // depending on the collapseActions pref
+ isAction = ((inobj.getAttribute("msg-type") == "ACTION") ||
+ (lastRow.getAttribute("msg-type") == "ACTION"));
+ // Do we collapse actions?
+ collapseActions = source.prefs["collapseActions"];
+
+ // Does the motif collapse everything, regardless of type?
+ // NOTE: the collapseActions pref can override this for actions
+ needSameType = !(("motifSettings" in source) &&
+ source.motifSettings &&
+ ("collapsemore" in source.motifSettings));
+ }
+
+ if (sameNick && samePrefix && sameDest &&
+ (haveSameType || !needSameType) &&
+ (!isAction || collapseActions))
+ {
+ obj = inobj;
+ if (columnInfo.nested)
+ appendTo = source.messages.firstChild.lastChild.firstChild.firstChild.firstChild;
+
+ if (obj.getAttribute("important"))
+ {
+ nickColumns[nickColumnCount - 1].setAttribute("important",
+ true);
+ }
+
+ // Remove nickname column from new row.
+ obj.removeChild(thisUserCol);
+
+ // Expand previous grouping's nickname cell(s) to fill-in the gap.
+ for (var i = 0; i < nickColumns.length; ++i)
+ nickColumns[i].setAttribute("rowspan", rowExtents.length + 1);
+ }
+ }
+
+ if ("frame" in source)
+ needScroll = checkScroll(source.frame);
+ if (obj)
+ appendTo.appendChild(client.adoptNode(obj, appendTo.ownerDocument));
+
+ if (source.MAX_MESSAGES)
+ {
+ if (typeof source.messageCount != "number")
+ source.messageCount = 1;
+ else
+ source.messageCount++;
+
+ if (source.messageCount > source.MAX_MESSAGES)
+ removeExcessMessages(source);
+ }
+
+ if (needScroll)
+ scrollDown(source.frame, true);
+}
+
+function removeExcessMessages(source)
+{
+ var window = getContentWindow(source.frame);
+ var rows = source.messages.rows;
+ var lastItemOffset = rows[rows.length - 1].offsetTop;
+ var tbody = source.messages.firstChild;
+ while (source.messageCount > source.MAX_MESSAGES)
+ {
+ if (tbody.firstChild.className == "msg-nested-tr")
+ {
+ var table = tbody.firstChild.firstChild.firstChild;
+ var toBeRemoved = source.messageCount - source.MAX_MESSAGES;
+ // If we can remove the entire table, do that...
+ if (table.rows.length <= toBeRemoved)
+ {
+ tbody.removeChild(tbody.firstChild);
+ source.messageCount -= table.rows.length;
+ table = null; // Don't hang onto this.
+ continue;
+ }
+ // Otherwise, remove rows from this table instead:
+ tbody = table.firstChild;
+ }
+ var nextLastNode = tbody.firstChild.nextSibling;
+ // If the next node has only 2 childNodes,
+ // assume we're dealing with collapsed msgs,
+ // and move the nickname element:
+ if (nextLastNode.childNodes.length == 2)
+ {
+ var nickElem = tbody.firstChild.childNodes[1];
+ var rowspan = nickElem.getAttribute("rowspan") - 1;
+ tbody.firstChild.removeChild(nickElem);
+ nickElem.setAttribute("rowspan", rowspan);
+ nextLastNode.insertBefore(nickElem, nextLastNode.lastChild);
+ }
+ tbody.removeChild(tbody.firstChild);
+ --source.messageCount;
+ }
+ var oldestItem = rows[0];
+ if (oldestItem.className == "msg-nested-tr")
+ oldestItem = rows[0].firstChild.firstChild.firstChild.firstChild;
+ updateTimestampFor(source, oldestItem, true);
+
+ // Scroll by as much as the lowest item has moved up:
+ lastItemOffset -= rows[rows.length - 1].offsetTop;
+ var y = window.pageYOffset;
+ if (!checkScroll(source.frame) && (y > lastItemOffset))
+ window.scrollBy(0, -lastItemOffset);
+}
+
+function findPreviousColumnInfo(table)
+{
+ // All the rows in the grouping (for merged rows).
+ var extents = new Array();
+ // Get the last row in the table.
+ var tr = table.firstChild.lastChild;
+ // Bail if there's no rows.
+ if (!tr)
+ return {extents: [], nickColumns: [], nested: false};
+ // Get message type.
+ if (tr.className == "msg-nested-tr")
+ {
+ var rv = findPreviousColumnInfo(tr.firstChild.firstChild);
+ rv.nested = true;
+ return rv;
+ }
+ // Now get the read one...
+ var className = (tr && tr.childNodes[1]) ? tr.childNodes[1].getAttribute("class") : "";
+ // Keep going up rows until you find the first in a group.
+ // This will go up until it hits the top of a multiline/merged block.
+ while (tr && tr.childNodes[1] && className.search(/msg-user|msg-type/) == -1)
+ {
+ extents.push(tr);
+ tr = tr.previousSibling;
+ if (tr && tr.childNodes[1])
+ className = tr.childNodes[1].getAttribute("class");
+ }
+
+ // If we ran out of rows, or it's not a talking line, we're outta here.
+ if (!tr || className != "msg-user")
+ return {extents: [], nickColumns: [], nested: false};
+
+ extents.push(tr);
+
+ // Time to collect the nick data...
+ var nickCol = tr.firstChild;
+ // All the cells that contain nickname info.
+ var nickCols = new Array();
+ while (nickCol)
+ {
+ // Just collect nickname column cells.
+ if (nickCol.getAttribute("class") == "msg-user")
+ nickCols.push(nickCol);
+ nickCol = nickCol.nextSibling;
+ }
+
+ // And we're done.
+ return {extents: extents, nickColumns: nickCols, nested: false};
+}
+
+function getLogPath(obj)
+{
+ // If we're logging, return the currently-used URL.
+ if (obj.logFile)
+ return getURLSpecFromFile(obj.logFile.path);
+ // If not, return the ideal URL.
+ return getURLSpecFromFile(obj.prefs["logFileName"]);
+}
+
+client.getConnectionCount =
+function cli_gccount ()
+{
+ var count = 0;
+
+ for (var n in client.networks)
+ {
+ if (client.networks[n].isConnected())
+ ++count;
+ }
+
+ return count;
+}
+
+client.quit =
+function cli_quit (reason)
+{
+ var net, netReason;
+ for (var n in client.networks)
+ {
+ net = client.networks[n];
+ if (net.isConnected())
+ {
+ netReason = (reason ? reason : net.prefs["defaultQuitMsg"]);
+ netReason = (netReason ? netReason : client.userAgent);
+ net.quit(netReason);
+ }
+ }
+}
+
+client.wantToQuit =
+function cli_wantToQuit(reason, deliberate)
+{
+
+ var close = true;
+ if (client.prefs["warnOnClose"] && !deliberate)
+ {
+ const buttons = [MSG_QUIT_ANYWAY, MSG_DONT_QUIT];
+ var checkState = { value: true };
+ var rv = confirmEx(MSG_CONFIRM_QUIT, buttons, 0, MSG_WARN_ON_EXIT,
+ checkState);
+ close = (rv == 0);
+ client.prefs["warnOnClose"] = checkState.value;
+ }
+
+ if (close)
+ {
+ client.userClose = true;
+ display(MSG_CLOSING);
+ client.quit(reason);
+ }
+}
+
+client.promptToSaveLogin =
+function cli_promptToSaveLogin(url, type, username, password)
+{
+ var name = "";
+ switch (type)
+ {
+ case "nick":
+ case "oper":
+ case "sasl":
+ name = username;
+ break;
+ case "serv":
+ case "chan":
+ name = url;
+ username = "*";
+ break;
+ default:
+ display(getMsg(MSG_LOGIN_ERR_UNKNOWN_TYPE, type), MT_ERROR);
+ return;
+ }
+
+ const buttons = [MSG_LOGIN_SAVE, MSG_LOGIN_DONT];
+ var checkState = { value: true };
+ var rv = confirmEx(getMsg(MSG_LOGIN_CONFIRM, name), buttons, 0,
+ MSG_LOGIN_PROMPT, checkState);
+ if (rv == 0)
+ {
+ client.prefs["login.promptToSave"] = checkState.value;
+
+ var updated = addOrUpdateLogin(url, type, username, password);
+ if (updated) {
+ display(getMsg(MSG_LOGIN_UPDATED, name), MT_INFO);
+ } else {
+ display(getMsg(MSG_LOGIN_ADDED, name), MT_INFO);
+ }
+ }
+}
+
+client.tryToGetLogin =
+function cli_tryToGetLogin(url, type, username, existing, needpass,
+ promptstring)
+{
+ // Password is optional. If it is not given, we look for a saved password
+ // first. If there isn't one, we potentially use a safe prompt.
+ var info = getLogin(url, type, username);
+ var stored = (info && info.password) ? info.password : "";
+ var promptToSave = false;
+ if (!existing && stored) {
+ existing = stored;
+ } else if (!existing && needpass) {
+ existing = promptPassword(promptstring, "");
+ if (existing)
+ promptToSave = true;
+ } else if (existing && stored != existing) {
+ promptToSave = true;
+ }
+
+ if (promptToSave && client.prefs["login.promptToSave"])
+ client.promptToSaveLogin(url, type, username, existing);
+
+ return existing;
+}
+
+/* gets a tab-complete match for the line of text specified by |line|.
+ * wordStart is the position within |line| that starts the word being matched,
+ * wordEnd marks the end position. |cursorPos| marks the position of the caret
+ * in the textbox.
+ */
+client.performTabMatch =
+function gettabmatch_usr (line, wordStart, wordEnd, word, cursorPos)
+{
+ if (wordStart != 0 || line[0] != client.COMMAND_CHAR)
+ return null;
+
+ var matches = client.commandManager.listNames(word.substr(1), CMD_CONSOLE);
+ if (matches.length == 1 && wordEnd == line.length)
+ {
+ matches[0] = client.COMMAND_CHAR + matches[0] + " ";
+ }
+ else
+ {
+ for (var i in matches)
+ matches[i] = client.COMMAND_CHAR + matches[i];
+ }
+
+ return matches;
+}
+
+client.openLogFile =
+function cli_startlog(view, showMessage)
+{
+ function getNextLogFileDate()
+ {
+ var d = new Date();
+ d.setMilliseconds(0);
+ d.setSeconds(0);
+ d.setMinutes(0);
+ switch (view.smallestLogInterval)
+ {
+ case "h":
+ return d.setHours(d.getHours() + 1);
+ case "d":
+ d.setHours(0);
+ return d.setDate(d.getDate() + 1);
+ case "m":
+ d.setHours(0);
+ d.setDate(1);
+ return d.setMonth(d.getMonth() + 1);
+ case "y":
+ d.setHours(0);
+ d.setDate(1);
+ d.setMonth(0);
+ return d.setFullYear(d.getFullYear() + 1);
+ }
+ //XXXhack: This should work...
+ return Infinity;
+ };
+
+ const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE;
+
+ try
+ {
+ var file = new LocalFile(view.prefs["logFileName"]);
+ if (!file.localFile.exists())
+ {
+ // futils.umask may be 0022. Result is 0644.
+ file.localFile.create(NORMAL_FILE_TYPE, 0o666 & ~futils.umask);
+ }
+ view.logFile = fopen(file.localFile, ">>");
+ // If we're here, it's safe to say when we should re-open:
+ view.nextLogFileDate = getNextLogFileDate();
+ }
+ catch (ex)
+ {
+ view.prefs["log"] = false;
+ dd("Log file open error: " + formatException(ex));
+ view.displayHere(getMsg(MSG_LOGFILE_ERROR, getLogPath(view)), MT_ERROR);
+ return;
+ }
+
+ if (showMessage)
+ view.displayHere(getMsg(MSG_LOGFILE_OPENED, getLogPath(view)));
+}
+
+client.closeLogFile =
+function cli_stoplog(view, showMessage)
+{
+ if (showMessage)
+ view.displayHere(getMsg(MSG_LOGFILE_CLOSING, getLogPath(view)));
+
+ if (view.logFile)
+ {
+ view.logFile.close();
+ view.logFile = null;
+ }
+}
+
+function checkLogFiles()
+{
+ // For every view that has a logfile, check if we need a different file
+ // based on the current date and the logfile preference. We close the
+ // current logfile, and display will open the new one based on the pref
+ // when it's needed.
+
+ var d = new Date();
+ for (var n in client.networks)
+ {
+ var net = client.networks[n];
+ if (net.logFile && (d > net.nextLogFileDate))
+ client.closeLogFile(net);
+ if (("primServ" in net) && net.primServ && ("channels" in net.primServ))
+ {
+ for (var c in net.primServ.channels)
+ {
+ var chan = net.primServ.channels[c];
+ if (chan.logFile && (d > chan.nextLogFileDate))
+ client.closeLogFile(chan);
+ }
+ }
+ if ("users" in net)
+ {
+ for (var u in net.users)
+ {
+ var user = net.users[u];
+ if (user.logFile && (d > user.nextLogFileDate))
+ client.closeLogFile(user);
+ }
+ }
+ }
+
+ for (var dc in client.dcc.chats)
+ {
+ var dccChat = client.dcc.chats[dc];
+ if (dccChat.logFile && (d > dccChat.nextLogFileDate))
+ client.closeLogFile(dccChat);
+ }
+ for (var df in client.dcc.files)
+ {
+ var dccFile = client.dcc.files[df];
+ if (dccFile.logFile && (d > dccFile.nextLogFileDate))
+ client.closeLogFile(dccFile);
+ }
+
+ // Don't forget about the client tab:
+ if (client.logFile && (d > client.nextLogFileDate))
+ client.closeLogFile(client);
+
+ /* We need to calculate the correct time for the next check. This is
+ * attempting to hit 2 seconds past the hour. We need the timezone offset
+ * here for when it is not a whole number of hours from UTC.
+ */
+ var shiftedDate = d.getTime() + d.getTimezoneOffset() * 60000;
+ setTimeout(checkLogFiles, 3602000 - (shiftedDate % 3600000));
+}
+
+CIRCChannel.prototype.getLCFunction =
+CIRCNetwork.prototype.getLCFunction =
+CIRCUser.prototype.getLCFunction =
+CIRCDCCChat.prototype.getLCFunction =
+CIRCDCCFileTransfer.prototype.getLCFunction =
+function getlcfn()
+{
+ var details = getObjectDetails(this);
+ var lcFn;
+
+ if (details.server)
+ {
+ lcFn = function(text)
+ {
+ return details.server.toLowerCase(text);
+ }
+ }
+
+ return lcFn;
+}
+
+CIRCChannel.prototype.performTabMatch =
+CIRCNetwork.prototype.performTabMatch =
+CIRCUser.prototype.performTabMatch =
+CIRCDCCChat.prototype.performTabMatch =
+CIRCDCCFileTransfer.prototype.performTabMatch =
+function gettabmatch_other (line, wordStart, wordEnd, word, cursorpos, lcFn)
+{
+ if (wordStart == 0 && line[0] == client.COMMAND_CHAR)
+ {
+ return client.performTabMatch(line, wordStart, wordEnd, word,
+ cursorpos);
+ }
+
+ var matchList = new Array();
+ var users;
+ var channels;
+ var userIndex = -1;
+
+ var details = getObjectDetails(this);
+
+ if (details.channel && word == details.channel.unicodeName[0])
+ {
+ /* When we have #<tab>, we just want the current channel,
+ if possible. */
+ matchList.push(details.channel.unicodeName);
+ }
+ else
+ {
+ /* Ok, not #<tab> or no current channel, so get the full list. */
+
+ if (details.channel)
+ users = details.channel.users;
+
+ if (details.server)
+ {
+ channels = details.server.channels;
+ for (var c in channels)
+ matchList.push(channels[c].unicodeName);
+ if (!users)
+ users = details.server.users;
+ }
+
+ if (users)
+ {
+ userIndex = matchList.length;
+ for (var n in users)
+ matchList.push(users[n].unicodeName);
+ }
+ }
+
+ var matches = matchEntry(word, matchList, lcFn);
+
+ var list = new Array();
+ for (var i = 0; i < matches.length; i++)
+ list.push(matchList[matches[i]]);
+
+ if (list.length == 1)
+ {
+ if (users && (userIndex >= 0) && (matches[0] >= userIndex))
+ {
+ if (wordStart == 0)
+ list[0] += client.prefs["nickCompleteStr"];
+ }
+
+ if (wordEnd == line.length)
+ {
+ /* add a space if the word is at the end of the line. */
+ list[0] += " ";
+ }
+ }
+
+ return list;
+}
+
+/*
+ * 290miliseconds for 1st derive is allowing about 3-4 events per
+ * second. 200ms for 2nd derivative allows max 200ms difference of
+ * frequency. This means when the flood is massive, this value is
+ * very closed to zero. But runtime issues should cause some delay
+ * in the core js, so zero value is not too good. We need increase
+ * this with a small, to make more strict. And when flood is done,
+ * we need detect it - based on arithmetic medium. When doesn't happen
+ * anything for a long time, perhaps for 2seconds the
+ * value - based on last 10 events - the 2nd value goes
+ * over 200ms average, so event should start again.
+ */
+
+function FloodProtector (density, dispersion)
+{
+ this.lastHit = Number(new Date());
+
+ if (density)
+ this.floodDensity = density;
+
+ if (dispersion)
+ this.floodDispersion = dispersion;
+}
+
+FloodProtector.prototype.requestedTotal = 0;
+FloodProtector.prototype.acceptedTotal = 0;
+FloodProtector.prototype.firedTotal = 0;
+FloodProtector.prototype.lastHit = 0;
+FloodProtector.prototype.derivative1 = 100;
+FloodProtector.prototype.derivative1Count = 100;
+FloodProtector.prototype.derivative2 = 0;
+FloodProtector.prototype.derivative2Count = 0;
+FloodProtector.prototype.floodDensity = 290;
+FloodProtector.prototype.floodDispersion = 200;
+
+FloodProtector.prototype.request = function ()
+{
+ this.requestedTotal++;
+ var current = Number(new Date());
+ var oldDerivative1 = this.derivative1;
+ this.derivative1 = current - this.lastHit;
+ this.derivative1Count = ((this.derivative1Count * 9) + this.derivative1) / 10;
+ this.derivative2 = Math.abs(this.derivative1 - oldDerivative1);
+ this.derivative2Count = ((this.derivative2Count * 9) + this.derivative2) / 10;
+ this.lastHit = current;
+}
+
+FloodProtector.prototype.accept = function ()
+{
+ this.acceptedTotal++;
+}
+
+FloodProtector.prototype.fire = function ()
+{
+ // There is no activity for 10 seconds - flood is possibly done.
+ // No need more recall. In other way the first normal activity
+ // overwrites it automatically earlier, if nessesary.
+ if ((Number(new Date()) - this.lastHit) > 10000)
+ return false;
+
+ // The activity is not too frequent or not massive so should not be fire.
+ if ((this.derivative1Count > this.floodDensity)
+ || (this.derivative2Count > this.floodDispersion))
+ {
+ return false;
+ }
+
+ this.firedTotal++;
+ return true;
+
+}
+
+
+function toasterPopupOverlapDelayReset (eventType)
+{
+ // it smells like a flood attack so rather wait more...
+ if (client.alert.floodProtector.fire())
+ {
+ setTimeout(
+ toasterPopupOverlapDelayReset,
+ client.prefs['alert.overlapDelay'], eventType);
+ }
+ else
+ {
+ delete client.alert.alertList[eventType];
+ }
+}
+
+var alertClickerObserver = {
+ observe: function(subject, topic, data)
+ {
+ if (topic == "alertclickcallback")
+ {
+ var tb = document.getElementById(data);
+ if (tb && tb.view)
+ {
+ tb.view.dispatch("set-current-view", {view: tb.view});
+ window.focus();
+ }
+ }
+ },
+
+ // Gecko 1.7.* rulez
+ onAlertClickCallback: function(data)
+ {
+ var tb = document.getElementById(data);
+ if (tb && tb.view)
+ {
+ tb.view.dispatch("set-current-view", {view: tb.view});
+ window.focus();
+ }
+ },
+
+ onAlertFinished: function(data)
+ {
+ }
+};
+
+
+// Show the alert for a particular event on a type of object.
+function showEventAlerts (type, event, message, nick, o, thisp, msgtype)
+{
+
+ // Converts .TYPE values into the event object names.
+ // IRCChannel => channel, IRCUser => user, etc.
+ type = type.replace(/^IRC/i,'').toLowerCase();
+
+ var source = type;
+ // DCC Chat sessions should act just like user views.
+ if (type == "dccchat") type = "user";
+
+ var ev = type + "." + event;
+ if (!(("alert."+ev) in thisp.prefs))
+ return;
+ if (!thisp.prefs["alert."+ev])
+ return;
+
+ client.alert.floodProtector.request();
+ if (ev in client.alert.alertList)
+ return;
+
+ client.alert.floodProtector.accept();
+ if(client.prefs['alert.overlapDelay'] > 0)
+ {
+ client.alert.alertList[ev] = true;
+ setTimeout(toasterPopupOverlapDelayReset,
+ client.prefs['alert.overlapDelay'], ev);
+ }
+
+ var clickable = client.prefs['alert.clickable'];
+ var tabId = clickable ? getTabForObject(thisp,false).id : "";
+ var listener = clickable ? alertClickerObserver : null;
+
+ message = removeColorCodes(message);
+ if (nick)
+ {
+ if (msgtype == "ACTION")
+ {
+ message = "* " + nick + " " + message;
+ }
+ else
+ {
+ message = "<" + nick + "> " + message;
+ }
+ }
+
+ if ((source == "channel") && o.channel)
+ {
+ source = o.channel.viewName;
+ }
+ else if ((source == "user") && o.network)
+ {
+ source = o.network.viewName;
+ }
+
+ // We can't be sure if it is a macOS and Growl is now turned off or not
+ try
+ {
+ client.alert.service.showAlertNotification(
+ "chrome://chatzilla/skin/images/logo.png",
+ "ChatZilla - " + source + " - " + event,
+ message, clickable, tabId, listener);
+ }
+ catch(ex)
+ {
+ // yup. it is probably a MAC or NsIAlertsService is not initialized
+ }
+}