diff options
Diffstat (limited to 'comm/suite/chatzilla/xul/content/static.js')
-rw-r--r-- | comm/suite/chatzilla/xul/content/static.js | 5639 |
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 + } +} |