summaryrefslogtreecommitdiffstats
path: root/comm/suite/chatzilla/js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/chatzilla/js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--comm/suite/chatzilla/js/lib/chatzilla-protocol-script.js10
-rw-r--r--comm/suite/chatzilla/js/lib/chatzilla-service.js311
-rw-r--r--comm/suite/chatzilla/js/lib/chatzilla-service.manifest6
-rw-r--r--comm/suite/chatzilla/js/lib/command-manager.js952
-rw-r--r--comm/suite/chatzilla/js/lib/connection-xpcom.js703
-rw-r--r--comm/suite/chatzilla/js/lib/dcc.js1198
-rw-r--r--comm/suite/chatzilla/js/lib/events.js365
-rw-r--r--comm/suite/chatzilla/js/lib/file-utils.js430
-rw-r--r--comm/suite/chatzilla/js/lib/http.js177
-rw-r--r--comm/suite/chatzilla/js/lib/ident.js203
-rw-r--r--comm/suite/chatzilla/js/lib/irc-debug.js84
-rw-r--r--comm/suite/chatzilla/js/lib/irc.js4518
-rw-r--r--comm/suite/chatzilla/js/lib/json-serializer.js103
-rw-r--r--comm/suite/chatzilla/js/lib/menu-manager.js848
-rw-r--r--comm/suite/chatzilla/js/lib/message-manager.js356
-rw-r--r--comm/suite/chatzilla/js/lib/pref-manager.js443
-rw-r--r--comm/suite/chatzilla/js/lib/protocol-handlers.jsm250
-rw-r--r--comm/suite/chatzilla/js/lib/sts.js210
-rw-r--r--comm/suite/chatzilla/js/lib/text-logger.js134
-rw-r--r--comm/suite/chatzilla/js/lib/text-serializer.js348
-rw-r--r--comm/suite/chatzilla/js/lib/utils.js1490
-rw-r--r--comm/suite/chatzilla/js/tests/DP.js612
-rw-r--r--comm/suite/chatzilla/js/tests/ircbot.js407
-rw-r--r--comm/suite/chatzilla/js/tests/mingus.js362
-rw-r--r--comm/suite/chatzilla/js/tests/mybot.js21
-rw-r--r--comm/suite/chatzilla/js/tests/test_matchobject.js41
-rw-r--r--comm/suite/chatzilla/js/tests/toys.js35
27 files changed, 14617 insertions, 0 deletions
diff --git a/comm/suite/chatzilla/js/lib/chatzilla-protocol-script.js b/comm/suite/chatzilla/js/lib/chatzilla-protocol-script.js
new file mode 100644
index 0000000000..e8cb72d259
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/chatzilla-protocol-script.js
@@ -0,0 +1,10 @@
+/* -*- 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/. */
+
+let { ChatZillaProtocols } = ChromeUtils.import(
+ "chrome://chatzilla/content/lib/js/protocol-handlers.jsm"
+);
+
+ChatZillaProtocols.init();
diff --git a/comm/suite/chatzilla/js/lib/chatzilla-service.js b/comm/suite/chatzilla/js/lib/chatzilla-service.js
new file mode 100644
index 0000000000..8772f3a782
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/chatzilla-service.js
@@ -0,0 +1,311 @@
+/* -*- 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const MEDIATOR_CONTRACTID =
+ "@mozilla.org/appshell/window-mediator;1";
+const ASS_CONTRACTID =
+ "@mozilla.org/appshell/appShellService;1";
+const RDFS_CONTRACTID =
+ "@mozilla.org/rdf/rdf-service;1";
+const CATMAN_CONTRACTID =
+ "@mozilla.org/categorymanager;1";
+const PPMM_CONTRACTID =
+ "@mozilla.org/parentprocessmessagemanager;1";
+
+const CLINE_SERVICE_CONTRACTID =
+ "@mozilla.org/commandlinehandler/general-startup;1?type=chat";
+const CLINE_SERVICE_CID =
+ Components.ID("{38a95514-1dd2-11b2-97e7-9da958640f2c}");
+const STARTUP_CID =
+ Components.ID("{ae6ad015-433b-42ab-9afc-1636af5a7fc4}");
+
+
+var {
+ ChatZillaProtocols,
+ IRCProtocolHandlerFactory,
+ IRCSProtocolHandlerFactory,
+ IRCPROT_HANDLER_CID,
+ IRCSPROT_HANDLER_CID,
+} = ChromeUtils.import(
+ "chrome://chatzilla/content/lib/js/protocol-handlers.jsm"
+);
+
+function spawnChatZilla(uri, count)
+{
+ const wm = Cc[MEDIATOR_CONTRACTID].getService(Ci.nsIWindowMediator);
+ const ass = Cc[ASS_CONTRACTID].getService(Ci.nsIAppShellService);
+ const hiddenWin = ass.hiddenDOMWindow;
+
+ // Ok, not starting currently, so check if we've got existing windows.
+ const w = wm.getMostRecentWindow("irc:chatzilla");
+
+ // Claiming that a ChatZilla window is loading.
+ if ("ChatZillaStarting" in hiddenWin)
+ {
+ dump("cz-service: ChatZilla claiming to be starting.\n");
+ if (w && ("client" in w) && ("initialized" in w.client) &&
+ w.client.initialized)
+ {
+ dump("cz-service: It lied. It's finished starting.\n");
+ // It's actually loaded ok.
+ delete hiddenWin.ChatZillaStarting;
+ }
+ }
+
+ if ("ChatZillaStarting" in hiddenWin)
+ {
+ count = count || 0;
+
+ if ((new Date() - hiddenWin.ChatZillaStarting) > 10000)
+ {
+ dump("cz-service: Continuing to be unable to talk to existing window!\n");
+ }
+ else
+ {
+ // We have a ChatZilla window, but we're still loading.
+ hiddenWin.setTimeout(function wrapper(count) {
+ spawnChatZilla(uri, count + 1);
+ }, 250, count);
+ return true;
+ }
+ }
+
+ // We have a window.
+ if (w)
+ {
+ dump("cz-service: Existing, fully loaded window. Using.\n");
+ // Window is working and initialized ok. Use it.
+ w.focus();
+ if (uri)
+ w.gotoIRCURL(uri);
+ return true;
+ }
+
+ dump("cz-service: No windows, starting new one.\n");
+ // Ok, no available window, loading or otherwise, so start ChatZilla.
+ const args = new Object();
+ if (uri)
+ args.url = uri;
+
+ hiddenWin.ChatZillaStarting = new Date();
+ hiddenWin.openDialog("chrome://chatzilla/content/chatzilla.xul", "_blank",
+ "chrome,menubar,toolbar,status,resizable,dialog=no",
+ args);
+
+ return true;
+}
+
+
+function CommandLineService()
+{
+}
+
+CommandLineService.prototype =
+{
+ /* nsISupports */
+ QueryInterface(iid)
+ {
+ if (iid.equals(Ci.nsISupports))
+ return this;
+
+ if (Ci.nsICommandLineHandler && iid.equals(Ci.nsICommandLineHandler))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ /* nsICommandLineHandler */
+ handle(cmdLine)
+ {
+ var uri;
+ try
+ {
+ uri = cmdLine.handleFlagWithParam("chat", false);
+ }
+ catch (e)
+ {
+ }
+
+ if (uri || cmdLine.handleFlag("chat", false))
+ {
+ spawnChatZilla(uri || null)
+ cmdLine.preventDefault = true;
+ }
+ },
+
+ helpInfo: "-chat [<ircurl>] Start with an IRC chat client.\n",
+};
+
+
+/* factory for command line handler service (CommandLineService) */
+const CommandLineFactory =
+{
+ createInstance(outer, iid)
+ {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ return new CommandLineService().QueryInterface(iid);
+ },
+};
+
+
+function ProcessHandler()
+{
+}
+
+ProcessHandler.prototype =
+{
+ /* nsISupports */
+ QueryInterface(iid)
+ {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIObserver) ||
+ iid.equals(Ci.nsIMessageListener))
+ {
+ return this;
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ /* nsIObserver */
+ observe(subject, topic, data)
+ {
+ if (topic !== "profile-after-change")
+ return;
+
+ var ppmm;
+ // Ci.nsIMessageBroadcaster went in Gecko 61.
+ if (Ci.nsIMessageBroadcaster)
+ {
+ ppmm = Cc[PPMM_CONTRACTID].getService(Ci.nsIMessageBroadcaster);
+ }
+ else
+ {
+ ppmm = Cc[PPMM_CONTRACTID].getService();
+ }
+ ppmm.loadProcessScript("chrome://chatzilla/content/lib/js/chatzilla-protocol-script.js", true);
+ ppmm.addMessageListener("ChatZilla:SpawnChatZilla", this);
+ },
+
+ /* nsIMessageListener */
+ receiveMessage(msg)
+ {
+ if (msg.name !== "ChatZilla:SpawnChatZilla")
+ return;
+
+ spawnChatZilla(msg.data.uri);
+ },
+};
+
+
+const StartupFactory =
+{
+ createInstance(outer, iid)
+ {
+ if (outer)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ if (!iid.equals(Ci.nsISupports))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+
+ // startup:
+ return new ProcessHandler();
+ },
+};
+
+
+const ChatZillaModule =
+{
+ registerSelf(compMgr, fileSpec, location, type)
+ {
+ compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
+ const catman = Cc[CATMAN_CONTRACTID].getService(Ci.nsICategoryManager);
+
+ debug("*** Registering -chat handler.\n");
+ compMgr.registerFactoryLocation(CLINE_SERVICE_CID,
+ "ChatZilla CommandLine Service",
+ CLINE_SERVICE_CONTRACTID,
+ fileSpec, location, type);
+ catman.addCategoryEntry("command-line-argument-handlers",
+ "chatzilla command line handler",
+ CLINE_SERVICE_CONTRACTID, true, true);
+ catman.addCategoryEntry("command-line-handler",
+ "m-irc",
+ CLINE_SERVICE_CONTRACTID, true, true);
+
+ debug("*** Registering irc protocol handler.\n");
+ ChatZillaProtocols.initObsolete(compMgr, fileSpec, location, type);
+
+ debug("*** Registering done.\n");
+ },
+
+ unregisterSelf(compMgr, fileSpec, location)
+ {
+ compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
+
+ const catman = Cc[CATMAN_CONTRACTID].getService(Ci.nsICategoryManager);
+ catman.deleteCategoryEntry("command-line-argument-handlers",
+ "chatzilla command line handler", true);
+ catman.deleteCategoryEntry("command-line-handler",
+ "m-irc", true);
+ },
+
+ getClassObject(compMgr, cid, iid)
+ {
+ // Checking if we're disabled in the Chrome Registry.
+ var rv;
+ try
+ {
+ const rdfSvc = Cc[RDFS_CONTRACTID].getService(Ci.nsIRDFService);
+ const rdfDS = rdfSvc.GetDataSource("rdf:chrome");
+ const resSelf = rdfSvc.GetResource("urn:mozilla:package:chatzilla");
+ const resDisabled = rdfSvc.GetResource("http://www.mozilla.org/rdf/chrome#disabled");
+ rv = rdfDS.GetTarget(resSelf, resDisabled, true);
+ }
+ catch (e)
+ {
+ }
+ if (rv)
+ throw Cr.NS_ERROR_NO_INTERFACE;
+
+ if (cid.equals(CLINE_SERVICE_CID))
+ return CommandLineFactory;
+
+ if (cid.equals(IRCPROT_HANDLER_CID))
+ return IRCProtocolHandlerFactory;
+
+ if (cid.equals(IRCSPROT_HANDLER_CID))
+ return IRCSProtocolHandlerFactory;
+
+ if (cid.equals(STARTUP_CID))
+ return StartupFactory;
+
+ if (!iid.equals(Ci.nsIFactory))
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ canUnload(compMgr)
+ {
+ return true;
+ },
+};
+
+
+/* entrypoint */
+function NSGetModule(compMgr, fileSpec)
+{
+ return ChatZillaModule;
+}
+
+function NSGetFactory(cid)
+{
+ return ChatZillaModule.getClassObject(null, cid, null);
+}
diff --git a/comm/suite/chatzilla/js/lib/chatzilla-service.manifest b/comm/suite/chatzilla/js/lib/chatzilla-service.manifest
new file mode 100644
index 0000000000..28a0d84d8f
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/chatzilla-service.manifest
@@ -0,0 +1,6 @@
+component {38a95514-1dd2-11b2-97e7-9da958640f2c} chatzilla-service.js
+contract @mozilla.org/commandlinehandler/general-startup;1?type=chat {38a95514-1dd2-11b2-97e7-9da958640f2c}
+category command-line-handler m-irc @mozilla.org/commandlinehandler/general-startup;1?type=chat
+component {ae6ad015-433b-42ab-9afc-1636af5a7fc4} chatzilla-service.js
+contract @chatzilla.mozilla.org/startup;1 {ae6ad015-433b-42ab-9afc-1636af5a7fc4}
+category profile-after-change chatzilla-startup @chatzilla.mozilla.org/startup;1
diff --git a/comm/suite/chatzilla/js/lib/command-manager.js b/comm/suite/chatzilla/js/lib/command-manager.js
new file mode 100644
index 0000000000..76d2818c3c
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/command-manager.js
@@ -0,0 +1,952 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+// @internal
+function getAccessKey(str)
+{
+ var i = str.indexOf("&");
+ if (i == -1)
+ return "";
+ return str[i + 1];
+}
+
+// @internal
+function CommandRecord(name, func, usage, help, label, accesskey, flags,
+ keystr, tip, format, helpUsage)
+{
+ this.name = name;
+ this.func = func;
+ this._usage = usage;
+ this.scanUsage();
+ this.help = help;
+ this.label = label ? label : name;
+ this.accesskey = accesskey ? accesskey : "";
+ this.format = format;
+ this.helpUsage = helpUsage;
+ this.labelstr = label.replace ("&", "");
+ this.tip = tip;
+ this.flags = flags;
+ this._enabled = true;
+ this.keyNodes = new Array();
+ this.keystr = keystr;
+ this.uiElements = new Array();
+}
+
+CommandRecord.prototype.__defineGetter__ ("enabled", cr_getenable);
+function cr_getenable ()
+{
+ return this._enabled;
+}
+
+CommandRecord.prototype.__defineSetter__ ("enabled", cr_setenable);
+function cr_setenable (state)
+{
+ for (var i = 0; i < this.uiElements.length; ++i)
+ {
+ if (state)
+ this.uiElements[i].removeAttribute ("disabled");
+ else
+ this.uiElements[i].setAttribute ("disabled", "true");
+ }
+ return (this._enabled = state);
+}
+
+CommandRecord.prototype.__defineSetter__ ("usage", cr_setusage);
+function cr_setusage (usage)
+{
+ this._usage = usage;
+ this.scanUsage();
+}
+
+CommandRecord.prototype.__defineGetter__ ("usage", cr_getusage);
+function cr_getusage()
+{
+ return this._usage;
+}
+
+/**
+ * @internal
+ *
+ * Scans the argument spec, in the format "<a1> <a2> [<o1> <o2>]", into an
+ * array of strings.
+ */
+CommandRecord.prototype.scanUsage =
+function cr_scanusage()
+{
+ var spec = this._usage;
+ var currentName = "";
+ var inName = false;
+ var len = spec.length;
+ var capNext = false;
+
+ this._usage = spec;
+ this.argNames = new Array();
+
+ for (var i = 0; i < len; ++i)
+ {
+ switch (spec[i])
+ {
+ case '[':
+ this.argNames.push (":");
+ break;
+
+ case '<':
+ inName = true;
+ break;
+
+ case '-':
+ capNext = true;
+ break;
+
+ case '>':
+ inName = false;
+ this.argNames.push (currentName);
+ currentName = "";
+ capNext = false;
+ break;
+
+ default:
+ if (inName)
+ currentName += capNext ? spec[i].toUpperCase() : spec[i];
+ capNext = false;
+ break;
+ }
+ }
+}
+
+/**
+ * Manages commands, with accelerator keys, help text and argument processing.
+ *
+ * You should never need to create an instance of this prototype; access the
+ * command manager through |client.commandManager|.
+ *
+ * @param defaultBundle An |nsIStringBundle| object to load command parameters,
+ * labels a help text from.
+ */
+function CommandManager(defaultBundle)
+{
+ this.commands = new Object();
+ this.commandHistory = new Object();
+ this.defaultBundle = defaultBundle;
+ this.currentDispatchDepth = 0;
+ this.maxDispatchDepth = 10;
+ this.dispatchUnwinding = false;
+}
+
+// @undocumented
+CommandManager.prototype.defaultFlags = 0;
+
+/**
+ * Adds multiple commands in a single call.
+ *
+ * @param cmdary |Array| containing commands to define; each item in the |Array|
+ * is also an |Array|, with either 3 or 4 items - corresponding to
+ * the first three or four arguments of |defineCommand|. An extra
+ * property, |stringBundle|, may be set on the |cmdary| |Array|
+ * to override the |defaultBundle| for all the commands.
+ */
+CommandManager.prototype.defineCommands =
+function cmgr_defcmds(cmdary)
+{
+ var len = cmdary.length;
+ var commands = new Object();
+ var bundle = "stringBundle" in cmdary ? cmdary.stringBundle : null;
+
+ for (var i = 0; i < len; ++i)
+ {
+ let name = cmdary[i][0];
+ let func = cmdary[i][1];
+ let flags = cmdary[i][2];
+ let usage = (3 in cmdary[i]) ? cmdary[i][3] : "";
+ commands[name] = this.defineCommand(name, func, flags, usage, bundle);
+ }
+
+ return commands;
+}
+
+/**
+ * Adds a single command.
+ *
+ * @param name The |String| name of the command to define.
+ * @param func A |Function| to call to handle dispatch of the new command.
+ * @param flags Optional. A |Number| indicating any special requirements for the
+ * command.
+ * @param usage Optional. A |String| specifying the arguments to the command. If
+ * not specified, then it is assumed there are none.
+ * @param bundle Optional. An |nsIStringBundle| to fetch parameters, labels,
+ * accelerator keys and help from. If not specified, the
+ * |defaultBundle| is used.
+ */
+CommandManager.prototype.defineCommand =
+function cmdmgr_defcmd(name, func, flags, usage, bundle)
+{
+ if (!bundle)
+ bundle = this.defaultBundle;
+
+ var helpDefault = MSG_NO_HELP;
+ var labelDefault = name;
+ var aliasFor;
+
+ if (typeof flags != "number")
+ flags = this.defaultFlags;
+
+ if (typeof usage != "string")
+ usage = "";
+
+ if (typeof func == "string")
+ {
+ var ary = func.match(/(\S+)/);
+ if (ary)
+ aliasFor = ary[1];
+ else
+ aliasFor = null;
+ helpDefault = getMsg (MSG_DEFAULT_ALIAS_HELP, func);
+ if (aliasFor)
+ labelDefault = getMsgFrom (bundle, "cmd." + aliasFor + ".label",
+ null, name);
+ }
+
+ var label = getMsgFrom(bundle, "cmd." + name + ".label", null,
+ labelDefault);
+ var accesskey = getMsgFrom(bundle, "cmd." + name + ".accesskey", null,
+ getAccessKey(label));
+ var help = helpDefault;
+ var helpUsage = "";
+ // Help is only shown for commands that available from the console.
+ if (flags & CMD_CONSOLE)
+ {
+ help = getMsgFrom(bundle, "cmd." + name + ".help", null, helpDefault);
+ // Only need to lookup localized helpUsage for commands that have them.
+ if (usage)
+ {
+ helpUsage = getMsgFrom(bundle, "cmd." + name + ".helpUsage", null,
+ "");
+ }
+ }
+ var keystr = getMsgFrom (bundle, "cmd." + name + ".key", null, "");
+ var format = getMsgFrom (bundle, "cmd." + name + ".format", null, null);
+ var tip = getMsgFrom (bundle, "cmd." + name + ".tip", null, "");
+ var command = new CommandRecord(name, func, usage, help, label, accesskey,
+ flags, keystr, tip, format, helpUsage);
+ this.addCommand(command);
+ if (aliasFor)
+ command.aliasFor = aliasFor;
+
+ return command;
+}
+
+/**
+ * Installs accelerator keys for commands into an existing document.
+ *
+ * @internal
+ * @param document An |XULDocument| within which to install the accelerator
+ * keys. Each command's key is installed by |installKey|.
+ * @param commands Optional. An |Array| or |Object| continaing |CommandRecord|
+ * objects. If not specified, all commands in the
+ * |CommandManager| are installed.
+ */
+CommandManager.prototype.installKeys =
+function cmgr_instkeys(document, commands)
+{
+ var parentElem = document.getElementById("dynamic-keys");
+ if (!parentElem)
+ {
+ parentElem = document.createElement("keyset");
+ parentElem.setAttribute("id", "dynamic-keys");
+ document.documentElement.appendChild(parentElem);
+ }
+
+ if (!commands)
+ commands = this.commands;
+
+ for (var c in commands)
+ this.installKey (parentElem, commands[c]);
+}
+
+/**
+ * Installs the accelerator key for a single command.
+ *
+ * This creates a <key> XUL element inside |parentElem|. It should usually be
+ * called once per command, per document, so that accelerator keys work in all
+ * application windows.
+ *
+ * @internal
+ * @param parentElem An |XULElement| to add the <key> too.
+ * @param command The |CommandRecord| to install.
+ */
+CommandManager.prototype.installKey =
+function cmgr_instkey(parentElem, command)
+{
+ if (!command.keystr)
+ return;
+
+ var ary = command.keystr.match (/(.*\s)?([\S]+)$/);
+ if (!ASSERT(ary, "couldn't parse key string ``" + command.keystr +
+ "'' for command ``" + command.name + "''"))
+ {
+ return;
+ }
+
+ var key = document.createElement ("key");
+ key.setAttribute ("id", "key:" + command.name);
+ key.setAttribute ("oncommand", "dispatch('" + command.name +
+ "', {isInteractive: true, source: 'keyboard'});");
+
+ if (ary[1])
+ key.setAttribute ("modifiers", ary[1]);
+
+ if (ary[2].indexOf("VK_") == 0)
+ key.setAttribute ("keycode", ary[2]);
+ else
+ key.setAttribute ("key", ary[2]);
+
+ parentElem.appendChild(key);
+ command.keyNodes.push(key);
+}
+
+/**
+ * Uninstalls accelerator keys for commands from a document.
+ *
+ * @internal
+ * @param commands Optional. An |Array| or |Object| continaing |CommandRecord|
+ * objects. If not specified, all commands in the
+ * |CommandManager| are uninstalled.
+ */
+CommandManager.prototype.uninstallKeys =
+function cmgr_uninstkeys(commands)
+{
+ if (!commands)
+ commands = this.commands;
+
+ for (var c in commands)
+ this.uninstallKey (commands[c]);
+}
+
+/**
+ * Uninstalls the accelerator key for a single command.
+ *
+ * @internal
+ * @param command The |CommandRecord| to uninstall.
+ */
+CommandManager.prototype.uninstallKey =
+function cmgr_uninstkey(command)
+{
+ for (var i in command.keyNodes)
+ {
+ try
+ {
+ /* document may no longer exist in a useful state. */
+ command.keyNodes[i].parentNode.removeChild(command.keyNodes[i]);
+ }
+ catch (ex)
+ {
+ dd ("*** caught exception uninstalling key node: " + ex);
+ }
+ }
+}
+
+/**
+ * Use |defineCommand|.
+ *
+ * @internal
+ * @param command The |CommandRecord| to add to the |CommandManager|.
+ */
+CommandManager.prototype.addCommand =
+function cmgr_add(command)
+{
+ if (objectContains(this.commands, command.name))
+ {
+ /* We've already got a command with this name - invoke the history
+ * storage so that we can undo this back to its original state.
+ */
+ if (!objectContains(this.commandHistory, command.name))
+ this.commandHistory[command.name] = new Array();
+ this.commandHistory[command.name].push(this.commands[command.name]);
+ }
+ this.commands[command.name] = command;
+}
+
+/**
+ * Removes multiple commands in a single call.
+ *
+ * @param cmdary An |Array| or |Object| containing |CommandRecord| objects.
+ * Ideally use the value returned from |defineCommands|.
+ */
+CommandManager.prototype.removeCommands =
+function cmgr_removes(cmdary)
+{
+ for (var i in cmdary)
+ {
+ var command = isinstance(cmdary[i], Array) ?
+ {name: cmdary[i][0]} : cmdary[i];
+ this.removeCommand(command);
+ }
+}
+
+/**
+ * Removes a single command.
+ *
+ * @param command The |CommandRecord| to remove from the |CommandManager|.
+ * Ideally use the value returned from |defineCommand|.
+ */
+CommandManager.prototype.removeCommand =
+function cmgr_remove(command)
+{
+ delete this.commands[command.name];
+ if (objectContains(this.commandHistory, command.name))
+ {
+ /* There was a previous command with this name - restore the most
+ * recent from the history, returning the command to its former glory.
+ */
+ this.commands[command.name] = this.commandHistory[command.name].pop();
+ if (this.commandHistory[command.name].length == 0)
+ delete this.commandHistory[command.name];
+ }
+}
+
+/**
+ * Registers a hook for a particular command.
+ *
+ * A command hook is uniquely identified by the pair |id|, |before|; only a
+ * single hook may exist for a given pair of |id| and |before| values. It is
+ * wise to use a unique |id|; plugins should construct an |id| using
+ * |plugin.id|, e.g. |plugin.id + "-my-hook-1"|.
+ *
+ * @param commandName A |String| command name to hook. The command named must
+ * already exist in the |CommandManager|; if it does not, no
+ * hook is added.
+ * @param func A |Function| to handle the hook.
+ * @param id A |String| identifier for the hook.
+ * @param before A |Boolean| indicating whether the hook wishes to be
+ * called before or after the command executes.
+ */
+CommandManager.prototype.addHook =
+function cmgr_hook (commandName, func, id, before)
+{
+ if (!ASSERT(objectContains(this.commands, commandName),
+ "Unknown command '" + commandName + "'"))
+ {
+ return;
+ }
+
+ var command = this.commands[commandName];
+
+ if (before)
+ {
+ if (!("beforeHooks" in command))
+ command.beforeHooks = new Object();
+ command.beforeHooks[id] = func;
+ }
+ else
+ {
+ if (!("afterHooks" in command))
+ command.afterHooks = new Object();
+ command.afterHooks[id] = func;
+ }
+}
+
+/**
+ * Registers multiple hooks for commands.
+ *
+ * @param hooks An |Object| containing |Function| objects to call for each
+ * hook; the key of each item is the name of the command it
+ * wishes to hook. Optionally, the |_before| property can be
+ * added to a |function| to override the default |before| value
+ * of |false|.
+ * @param prefix Optional. A |String| prefix to apply to each hook's command
+ * name to compute an |id| for it.
+ */
+CommandManager.prototype.addHooks =
+function cmgr_hooks (hooks, prefix)
+{
+ if (!prefix)
+ prefix = "";
+
+ for (var h in hooks)
+ {
+ this.addHook(h, hooks[h], prefix + ":" + h,
+ ("_before" in hooks[h]) ? hooks[h]._before : false);
+ }
+}
+
+/**
+ * Unregisters multiple hooks for commands.
+ *
+ * @param hooks An |Object| identical to the one passed to |addHooks|.
+ * @param prefix Optional. A |String| identical to the one passed to |addHooks|.
+ */
+CommandManager.prototype.removeHooks =
+function cmgr_remhooks (hooks, prefix)
+{
+ if (!prefix)
+ prefix = "";
+
+ for (var h in hooks)
+ {
+ this.removeHook(h, prefix + ":" + h,
+ ("before" in hooks[h]) ? hooks[h].before : false);
+ }
+}
+
+/**
+ * Unregisters a hook for a particular command.
+ *
+ * The arguments to |removeHook| are the same as |addHook|, but without the
+ * hook function itself.
+ *
+ * @param commandName The |String| command name to unhook.
+ * @param id The |String| identifier for the hook.
+ * @param before A |Boolean| indicating whether the hook was to be
+ * called before or after the command executed.
+ */
+CommandManager.prototype.removeHook =
+function cmgr_unhook (commandName, id, before)
+{
+ var command = this.commands[commandName];
+
+ if (before)
+ delete command.beforeHooks[id];
+ else
+ delete command.afterHooks[id];
+}
+
+/**
+ * Gets a sorted |Array| of |CommandRecord| objects which match.
+ *
+ * After filtering by |flags| (if specified), if an exact match for
+ * |partialName| is found, only that is returned; otherwise, all commands
+ * starting with |partialName| are returned in alphabetical order by |label|.
+ *
+ * @param partialName Optional. A |String| prefix to search for.
+ * @param flags Optional. Flags to logically AND with commands.
+ */
+CommandManager.prototype.list =
+function cmgr_list(partialName, flags, exact)
+{
+ /* returns array of command objects which look like |partialName|, or
+ * all commands if |partialName| is not specified */
+ function compare (a, b)
+ {
+ a = a.labelstr.toLowerCase();
+ b = b.labelstr.toLowerCase();
+
+ if (a == b)
+ return 0;
+
+ if (a > b)
+ return 1;
+
+ return -1;
+ }
+
+ var ary = new Array();
+ var commandNames = keys(this.commands);
+
+ for (var name of commandNames)
+ {
+ let command = this.commands[name];
+ if ((!flags || (command.flags & flags)) &&
+ (!partialName || command.name.startsWith(partialName)))
+ {
+ if (exact && partialName &&
+ partialName.length == command.name.length)
+ {
+ /* exact match */
+ return [command];
+ }
+ ary.push(command);
+ }
+ }
+
+ ary.sort(compare);
+ return ary;
+}
+
+/**
+ * Gets a sorted |Array| of command names which match.
+ *
+ * |listNames| operates identically to |list|, except that only command names
+ * are returned, not |CommandRecord| objects.
+ */
+CommandManager.prototype.listNames =
+function cmgr_listnames (partialName, flags)
+{
+ var cmds = this.list(partialName, flags, false);
+ var cmdNames = new Array();
+
+ for (var c in cmds)
+ cmdNames.push (cmds[c].name);
+
+ cmdNames.sort();
+ return cmdNames;
+}
+
+/**
+ * Internal use only.
+ *
+ * Called to parse the arguments stored in |e.inputData|, as properties of |e|,
+ * for the CommandRecord stored on |e.command|.
+ *
+ * @params e Event object to be processed.
+ */
+// @undocumented
+CommandManager.prototype.parseArguments =
+function cmgr_parseargs (e)
+{
+ var rv = this.parseArgumentsRaw(e);
+ //dd("parseArguments '" + e.command.usage + "' " +
+ // (rv ? "passed" : "failed") + "\n" + dumpObjectTree(e));
+ delete e.currentArgIndex;
+ return rv;
+}
+
+/**
+ * Internal use only.
+ *
+ * Don't call parseArgumentsRaw directly, use parseArguments instead.
+ *
+ * Parses the arguments stored in the |inputData| property of the event object,
+ * according to the format specified by the |command| property.
+ *
+ * On success this method returns true, and propery names corresponding to the
+ * argument names used in the format spec will be created on the event object.
+ * All optional parameters will be initialized to |null| if not already present
+ * on the event.
+ *
+ * On failure this method returns false and a description of the problem
+ * will be stored in the |parseError| property of the event.
+ *
+ * For example...
+ * Given the argument spec "<int> <word> [ <word2> <word3> ]", and given the
+ * input string "411 foo", stored as |e.command.usage| and |e.inputData|
+ * respectively, this method would add the following propertys to the event
+ * object...
+ * -name---value--notes-
+ * e.int 411 Parsed as an integer
+ * e.word foo Parsed as a string
+ * e.word2 null Optional parameters not specified will be set to null.
+ * e.word3 null If word2 had been provided, word3 would be required too.
+ *
+ * Each parameter is parsed by calling the function with the same name, located
+ * in this.argTypes. The first parameter is parsed by calling the function
+ * this.argTypes["int"], for example. This function is expected to act on
+ * e.unparsedData, taking it's chunk, and leaving the rest of the string.
+ * The default parse functions are...
+ * <word> parses contiguous non-space characters.
+ * <int> parses as an int.
+ * <rest> parses to the end of input data.
+ * <state> parses yes, on, true, 1, 0, false, off, no as a boolean.
+ * <toggle> parses like a <state>, except allows "toggle" as well.
+ * <...> parses according to the parameter type before it, until the end
+ * of the input data. Results are stored in an array named
+ * paramnameList, where paramname is the name of the parameter
+ * before <...>. The value of the parameter before this will be
+ * paramnameList[0].
+ *
+ * If there is no parse function for an argument type, "word" will be used by
+ * default. You can alias argument types with code like...
+ * commandManager.argTypes["my-integer-name"] = commandManager.argTypes["int"];
+ */
+// @undocumented
+CommandManager.prototype.parseArgumentsRaw =
+function parse_parseargsraw (e)
+{
+ var argc = e.command.argNames.length;
+
+ function initOptionals()
+ {
+ for (var i = 0; i < argc; ++i)
+ {
+ if (e.command.argNames[i] != ":" &&
+ e.command.argNames[i] != "..." &&
+ !(e.command.argNames[i] in e))
+ {
+ e[e.command.argNames[i]] = null;
+ }
+
+ if (e.command.argNames[i] == "...")
+ {
+ var paramName = e.command.argNames[i - 1];
+ if (paramName == ":")
+ paramName = e.command.argNames[i - 2];
+ var listName = paramName + "List";
+ if (!(listName in e))
+ e[listName] = [ e[paramName] ];
+ }
+ }
+ }
+
+ if ("inputData" in e && e.inputData)
+ {
+ /* if data has been provided, parse it */
+ e.unparsedData = e.inputData;
+ var parseResult;
+ var currentArg;
+ e.currentArgIndex = 0;
+
+ if (argc)
+ {
+ currentArg = e.command.argNames[e.currentArgIndex];
+
+ while (e.unparsedData)
+ {
+ if (currentArg != ":")
+ {
+ if (!this.parseArgument (e, currentArg))
+ return false;
+ }
+ if (++e.currentArgIndex < argc)
+ currentArg = e.command.argNames[e.currentArgIndex];
+ else
+ break;
+ }
+
+ if (e.currentArgIndex < argc && currentArg != ":")
+ {
+ /* parse loop completed because it ran out of data. We haven't
+ * parsed all of the declared arguments, and we're not stopped
+ * at an optional marker, so we must be missing something
+ * required... */
+ e.parseError = getMsg(MSG_ERR_REQUIRED_PARAM,
+ e.command.argNames[e.currentArgIndex]);
+ return false;
+ }
+ }
+
+ if (e.unparsedData)
+ {
+ /* parse loop completed with unparsed data, which means we've
+ * successfully parsed all arguments declared. Whine about the
+ * extra data... */
+ display (getMsg(MSG_EXTRA_PARAMS, e.unparsedData), MT_WARN);
+ }
+ }
+
+ var rv = this.isCommandSatisfied(e);
+ if (rv)
+ initOptionals();
+ return rv;
+}
+
+/**
+ * Returns true if |e| has the properties required to call the command
+ * |command|.
+ *
+ * If |command| is not provided, |e.command| is used instead.
+ *
+ * @param e Event object to test against the command.
+ * @param command Command to test.
+ */
+// @undocumented
+CommandManager.prototype.isCommandSatisfied =
+function cmgr_isok (e, command)
+{
+ if (typeof command == "undefined")
+ command = e.command;
+ else if (typeof command == "string")
+ command = this.commands[command];
+
+ if (!command.enabled)
+ return false;
+
+ for (var i = 0; i < command.argNames.length; ++i)
+ {
+ if (command.argNames[i] == ":")
+ return true;
+
+ if (!(command.argNames[i] in e))
+ {
+ e.parseError = getMsg(MSG_ERR_REQUIRED_PARAM, command.argNames[i]);
+ //dd("command '" + command.name + "' unsatisfied: " + e.parseError);
+ return false;
+ }
+ }
+
+ //dd ("command '" + command.name + "' satisfied.");
+ return true;
+}
+
+/**
+ * Internal use only.
+ * See parseArguments above and the |argTypes| object below.
+ *
+ * Parses the next argument by calling an appropriate parser function, or the
+ * generic "word" parser if none other is found.
+ *
+ * @param e event object.
+ * @param name property name to use for the parse result.
+ */
+// @undocumented
+CommandManager.prototype.parseArgument =
+function cmgr_parsearg (e, name)
+{
+ var parseResult;
+
+ if (name in this.argTypes)
+ parseResult = this.argTypes[name](e, name, this);
+ else
+ parseResult = this.argTypes["word"](e, name, this);
+
+ if (!parseResult)
+ e.parseError = getMsg(MSG_ERR_INVALID_PARAM,
+ [name, e.unparsedData]);
+
+ return parseResult;
+}
+
+// @undocumented
+CommandManager.prototype.argTypes = new Object();
+
+/**
+ * Convenience function used to map a list of new types to an existing parse
+ * function.
+ */
+// @undocumented
+CommandManager.prototype.argTypes.__aliasTypes__ =
+function at_alias (list, type)
+{
+ for (var i in list)
+ {
+ this[list[i]] = this[type];
+ }
+}
+
+/**
+ * Internal use only.
+ *
+ * Parses an integer, stores result in |e[name]|.
+ */
+// @undocumented
+CommandManager.prototype.argTypes["int"] =
+function parse_int (e, name)
+{
+ var ary = e.unparsedData.match (/(\d+)(?:\s+(.*))?$/);
+ if (!ary)
+ return false;
+ e[name] = Number(ary[1]);
+ e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
+ return true;
+}
+
+/**
+ * Internal use only.
+ *
+ * Parses a word, which is defined as a list of nonspace characters.
+ *
+ * Stores result in |e[name]|.
+ */
+// @undocumented
+CommandManager.prototype.argTypes["word"] =
+function parse_word (e, name)
+{
+ var ary = e.unparsedData.match (/(\S+)(?:\s+(.*))?$/);
+ if (!ary)
+ return false;
+ e[name] = ary[1];
+ e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
+ return true;
+}
+
+/**
+ * Internal use only.
+ *
+ * Parses a "state" which can be "true", "on", "yes", or 1 to indicate |true|,
+ * or "false", "off", "no", or 0 to indicate |false|.
+ *
+ * Stores result in |e[name]|.
+ */
+// @undocumented
+CommandManager.prototype.argTypes["state"] =
+function parse_state (e, name)
+{
+ var ary =
+ e.unparsedData.match (/(true|on|yes|1|false|off|no|0)(?:\s+(.*))?$/i);
+ if (!ary)
+ return false;
+ if (ary[1].search(/true|on|yes|1/i) != -1)
+ e[name] = true;
+ else
+ e[name] = false;
+ e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
+ return true;
+}
+
+/**
+ * Internal use only.
+ *
+ * Parses a "toggle" which can be "true", "on", "yes", or 1 to indicate |true|,
+ * or "false", "off", "no", or 0 to indicate |false|. In addition, the string
+ * "toggle" is accepted, in which case |e[name]| will be the string "toggle".
+ *
+ * Stores result in |e[name]|.
+ */
+// @undocumented
+CommandManager.prototype.argTypes["toggle"] =
+function parse_toggle (e, name)
+{
+ var ary = e.unparsedData.match
+ (/(toggle|true|on|yes|1|false|off|no|0)(?:\s+(.*))?$/i);
+
+ if (!ary)
+ return false;
+ if (ary[1].search(/toggle/i) != -1)
+ e[name] = "toggle";
+ else if (ary[1].search(/true|on|yes|1/i) != -1)
+ e[name] = true;
+ else
+ e[name] = false;
+ e.unparsedData = arrayHasElementAt(ary, 2) ? ary[2] : "";
+ return true;
+}
+
+/**
+ * Internal use only.
+ *
+ * Returns all unparsed data to the end of the line.
+ *
+ * Stores result in |e[name]|.
+ */
+// @undocumented
+CommandManager.prototype.argTypes["rest"] =
+function parse_rest (e, name)
+{
+ e[name] = e.unparsedData;
+ e.unparsedData = "";
+ return true;
+}
+
+/**
+ * Internal use only.
+ *
+ * Parses the rest of the unparsed data the same way the previous argument was
+ * parsed. Can't be used as the first parameter. if |name| is "..." then the
+ * name of the previous argument, plus the suffix "List" will be used instead.
+ *
+ * Stores result in |e[name]| or |e[lastName + "List"]|.
+ */
+// @undocumented
+CommandManager.prototype.argTypes["..."] =
+function parse_repeat (e, name, cm)
+{
+ ASSERT (e.currentArgIndex > 0, "<...> can't be the first argument.");
+
+ var lastArg = e.command.argNames[e.currentArgIndex - 1];
+ if (lastArg == ":")
+ lastArg = e.command.argNames[e.currentArgIndex - 2];
+
+ var listName = lastArg + "List";
+ e[listName] = [ e[lastArg] ];
+
+ while (e.unparsedData)
+ {
+ if (!cm.parseArgument(e, lastArg))
+ return false;
+ e[listName].push(e[lastArg]);
+ }
+
+ e[lastArg] = e[listName][0];
+ return true;
+}
diff --git a/comm/suite/chatzilla/js/lib/connection-xpcom.js b/comm/suite/chatzilla/js/lib/connection-xpcom.js
new file mode 100644
index 0000000000..031689d74d
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/connection-xpcom.js
@@ -0,0 +1,703 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const NS_ERROR_MODULE_NETWORK = 2152398848;
+
+const NS_ERROR_UNKNOWN_HOST = NS_ERROR_MODULE_NETWORK + 30;
+const NS_ERROR_CONNECTION_REFUSED = NS_ERROR_MODULE_NETWORK + 13;
+const NS_ERROR_NET_TIMEOUT = NS_ERROR_MODULE_NETWORK + 14;
+const NS_ERROR_OFFLINE = NS_ERROR_MODULE_NETWORK + 16;
+const NS_ERROR_NET_RESET = NS_ERROR_MODULE_NETWORK + 20;
+const NS_ERROR_UNKNOWN_PROXY_HOST = NS_ERROR_MODULE_NETWORK + 42;
+const NS_ERROR_NET_INTERRUPT = NS_ERROR_MODULE_NETWORK + 71;
+const NS_ERROR_PROXY_CONNECTION_REFUSED = NS_ERROR_MODULE_NETWORK + 72;
+
+// Offline error constants:
+const NS_ERROR_BINDING_ABORTED = NS_ERROR_MODULE_NETWORK + 2;
+const NS_ERROR_ABORT = 0x80004004;
+
+const NS_NET_STATUS_RESOLVING_HOST = NS_ERROR_MODULE_NETWORK + 3;
+const NS_NET_STATUS_CONNECTED_TO = NS_ERROR_MODULE_NETWORK + 4;
+const NS_NET_STATUS_SENDING_TO = NS_ERROR_MODULE_NETWORK + 5;
+const NS_NET_STATUS_RECEIVING_FROM = NS_ERROR_MODULE_NETWORK + 6;
+const NS_NET_STATUS_CONNECTING_TO = NS_ERROR_MODULE_NETWORK + 7;
+
+// Security error class constants:
+const ERROR_CLASS_SSL_PROTOCOL = 1;
+const ERROR_CLASS_BAD_CERT = 2;
+
+// Security Constants.
+const STATE_IS_BROKEN = 1;
+const STATE_IS_SECURE = 2;
+const STATE_IS_INSECURE = 3;
+
+const nsIScriptableInputStream = Components.interfaces.nsIScriptableInputStream;
+
+const nsIBinaryInputStream = Components.interfaces.nsIBinaryInputStream;
+const nsIBinaryOutputStream = Components.interfaces.nsIBinaryOutputStream;
+
+function toSInputStream(stream, binary)
+{
+ var sstream;
+
+ if (binary)
+ {
+ sstream = Components.classes["@mozilla.org/binaryinputstream;1"];
+ sstream = sstream.createInstance(nsIBinaryInputStream);
+ sstream.setInputStream(stream);
+ }
+ else
+ {
+ sstream = Components.classes["@mozilla.org/scriptableinputstream;1"];
+ sstream = sstream.createInstance(nsIScriptableInputStream);
+ sstream.init(stream);
+ }
+
+ return sstream;
+}
+
+function toSOutputStream(stream, binary)
+{
+ var sstream;
+
+ if (binary)
+ {
+ sstream = Components.classes["@mozilla.org/binaryoutputstream;1"];
+ sstream = sstream.createInstance(Components.interfaces.nsIBinaryOutputStream);
+ sstream.setOutputStream(stream);
+ }
+ else
+ {
+ sstream = stream;
+ }
+
+ return sstream;
+}
+
+/* This object implements nsIBadCertListener2
+ * The idea is to suppress the default UI's alert box
+ * and allow the exception to propagate normally
+ */
+function BadCertHandler()
+{
+}
+
+BadCertHandler.prototype.getInterface =
+function badcert_getinterface(aIID)
+{
+ return this.QueryInterface(aIID);
+}
+
+BadCertHandler.prototype.QueryInterface =
+function badcert_queryinterface(aIID)
+{
+ if (aIID.equals(Components.interfaces.nsIBadCertListener2) ||
+ aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ {
+ return this;
+ }
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+/* Returning true in the following two callbacks
+ * means suppress default the error UI (modal alert).
+ */
+BadCertHandler.prototype.notifyCertProblem =
+function badcert_notifyCertProblem(socketInfo, sslStatus, targetHost)
+{
+ return true;
+}
+
+/**
+ * Wraps up various mechanics of sockets for easy consumption by other code.
+ *
+ * @param binary Provide |true| or |false| here to override the automatic
+ * selection of binary or text streams. This should only ever be
+ * specified as |true| or omitted, otherwise you will be shooting
+ * yourself in the foot on some versions - let the code handle
+ * the choice unless you know you need binary.
+ */
+function CBSConnection (binary)
+{
+ /* Since 2003-01-17 18:14, Mozilla has had this contract ID for the STS.
+ * Prior to that it didn't have one, so we also include the CID for the
+ * STS back then - DO NOT UPDATE THE ID if it changes in Mozilla.
+ */
+ const sockClassByName =
+ Components.classes["@mozilla.org/network/socket-transport-service;1"];
+ const sockClassByID =
+ Components.classesByID["{c07e81e0-ef12-11d2-92b6-00105a1b0d64}"];
+
+ var sockServiceClass = (sockClassByName || sockClassByID);
+
+ if (!sockServiceClass)
+ throw ("Couldn't get socket service class.");
+
+ var sockService = sockServiceClass.getService();
+ if (!sockService)
+ throw ("Couldn't get socket service.");
+
+ this._sockService = sockService.QueryInterface
+ (Components.interfaces.nsISocketTransportService);
+
+ /* Note: as part of the mess from bug 315288 and bug 316178, ChatZilla now
+ * uses the *binary* stream interfaces for all network
+ * communications.
+ *
+ * However, these interfaces do not exist prior to 1999-11-05. To
+ * make matters worse, an incompatible change to the "readBytes"
+ * method of this interface was made on 2003-03-13; luckly, this
+ * change also added a "readByteArray" method, which we will check
+ * for below, to determine if we can use the binary streams.
+ */
+
+ // We want to check for working binary streams only the first time.
+ if (CBSConnection.prototype.workingBinaryStreams == -1)
+ {
+ CBSConnection.prototype.workingBinaryStreams = false;
+
+ if (typeof nsIBinaryInputStream != "undefined")
+ {
+ var isCls = Components.classes["@mozilla.org/binaryinputstream;1"];
+ var inputStream = isCls.createInstance(nsIBinaryInputStream);
+ if ("readByteArray" in inputStream)
+ CBSConnection.prototype.workingBinaryStreams = true;
+ }
+ }
+
+ /*
+ * As part of the changes in Gecko 1.9, invalid SSL certificates now
+ * produce a horrible error message. We must look up the toolkit version
+ * to see if we need to catch these errors cleanly - see bug 454966.
+ */
+ if (!("strictSSL" in CBSConnection.prototype))
+ {
+ CBSConnection.prototype.strictSSL = false;
+ var app = getService("@mozilla.org/xre/app-info;1", "nsIXULAppInfo");
+ if (app && ("platformVersion" in app) &&
+ compareVersions("1.9", app.platformVersion) >= 0)
+ {
+ CBSConnection.prototype.strictSSL = true;
+ }
+ }
+
+ this.wrappedJSObject = this;
+ if (typeof binary != "undefined")
+ this.binaryMode = binary;
+ else
+ this.binaryMode = this.workingBinaryStreams;
+
+ if (!ASSERT(!this.binaryMode || this.workingBinaryStreams,
+ "Unable to use binary streams in this build."))
+ {
+ throw ("Unable to use binary streams in this build.");
+ }
+}
+
+CBSConnection.prototype.workingBinaryStreams = -1;
+
+CBSConnection.prototype.connect =
+function bc_connect(host, port, config, observer)
+{
+ this.host = host.toLowerCase();
+ this.port = port;
+
+ /* The APIs below want host:port. Later on, we also reformat the host to
+ * strip IPv6 literal brackets.
+ */
+ var hostPort = host + ":" + port;
+
+ if (!config)
+ config = {};
+
+ if (!("proxyInfo" in config))
+ {
+ // Lets get a transportInfo for this
+ var pps = getService("@mozilla.org/network/protocol-proxy-service;1",
+ "nsIProtocolProxyService");
+
+ /* Force Necko to supply the HTTP proxy info if desired. For none,
+ * force no proxy. Other values will get default treatment.
+ */
+ var uri = "irc://" + hostPort;
+ if ("proxy" in config)
+ {
+ if (config.proxy == "http")
+ uri = "http://" + hostPort;
+ else if (config.proxy == "none")
+ uri = "";
+ }
+
+ var self = this;
+ function continueWithProxy(proxyInfo)
+ {
+ config.proxyInfo = proxyInfo;
+ try
+ {
+ self.connect(host, port, config, observer);
+ }
+ catch (ex)
+ {
+ if ("onSocketConnection" in observer)
+ observer.onSocketConnection(host, port, config, ex);
+ return;
+ }
+ if ("onSocketConnection" in observer)
+ observer.onSocketConnection(host, port, config);
+ }
+
+ if (uri)
+ {
+ uri = Services.io.newURI(uri);
+ if ("asyncResolve" in pps)
+ {
+ pps.asyncResolve(uri, 0, {
+ onProxyAvailable: function(request, uri, proxyInfo, status) {
+ continueWithProxy(proxyInfo);
+ }
+ });
+ }
+ else if ("resolve" in pps)
+ {
+ continueWithProxy(pps.resolve(uri, 0));
+ }
+ else if ("examineForProxy" in pps)
+ {
+ continueWithProxy(pps.examineForProxy(uri));
+ }
+ else
+ {
+ throw "Unable to find method to resolve proxies";
+ }
+ }
+ else
+ {
+ continueWithProxy(null);
+ }
+ return true;
+ }
+
+ // Strip the IPv6 literal brackets; all the APIs below don't want them.
+ if (host[0] == '[' && host[host.length - 1] == ']')
+ host = host.substr(1, host.length - 2);
+
+ /* Since the proxy info is opaque, we need to check that we got
+ * something for our HTTP proxy - we can't just check proxyInfo.type.
+ */
+ var proxyInfo = config.proxyInfo || null;
+ var usingHTTPCONNECT = ("proxy" in config) && (config.proxy == "http")
+ && proxyInfo;
+
+ if (proxyInfo && ("type" in proxyInfo) && (proxyInfo.type == "unknown"))
+ throw JSIRC_ERR_PAC_LOADING;
+
+ /* use new necko interfaces */
+ if (("isSecure" in config) && config.isSecure)
+ {
+ this._transport = this._sockService.
+ createTransport(["ssl"], 1, host, port,
+ proxyInfo);
+
+ if (this.strictSSL)
+ this._transport.securityCallbacks = new BadCertHandler();
+ }
+ else
+ {
+ this._transport = this._sockService.
+ createTransport(["starttls"], 1, host, port, proxyInfo);
+ }
+ if (!this._transport)
+ throw ("Error creating transport.");
+
+ var openFlags = 0;
+
+ /* no limit on the output stream buffer */
+ this._outputStream =
+ this._transport.openOutputStream(openFlags, 4096, -1);
+ if (!this._outputStream)
+ throw "Error getting output stream.";
+ this._sOutputStream = toSOutputStream(this._outputStream,
+ this.binaryMode);
+
+ this._inputStream = this._transport.openInputStream(openFlags, 0, 0);
+ if (!this._inputStream)
+ throw "Error getting input stream.";
+ this._sInputStream = toSInputStream(this._inputStream,
+ this.binaryMode);
+
+ this.connectDate = new Date();
+ this.isConnected = true;
+
+ // Bootstrap the connection if we're proxying via an HTTP proxy.
+ if (usingHTTPCONNECT)
+ this.sendData("CONNECT " + hostPort + " HTTP/1.1\r\n\r\n");
+
+ return true;
+
+}
+
+CBSConnection.prototype.startTLS =
+function bc_starttls()
+{
+ if (!this.isConnected || !this._transport.securityInfo)
+ return;
+
+ var secInfo = this._transport.securityInfo;
+ var sockControl = secInfo.QueryInterface(Ci.nsITLSSocketControl);
+ sockControl.StartTLS();
+}
+
+CBSConnection.prototype.listen =
+function bc_listen(port, observer)
+{
+ var serverSockClass =
+ Components.classes["@mozilla.org/network/server-socket;1"];
+
+ if (!serverSockClass)
+ throw ("Couldn't get server socket class.");
+
+ var serverSock = serverSockClass.createInstance();
+ if (!serverSock)
+ throw ("Couldn't get server socket.");
+
+ this._serverSock = serverSock.QueryInterface
+ (Components.interfaces.nsIServerSocket);
+
+ this._serverSock.init(port, false, -1);
+
+ this._serverSockListener = new SocketListener(this, observer);
+
+ this._serverSock.asyncListen(this._serverSockListener);
+
+ this.port = this._serverSock.port;
+
+ return true;
+}
+
+CBSConnection.prototype.accept =
+function bc_accept(transport, observer)
+{
+ this._transport = transport;
+ this.host = this._transport.host.toLowerCase();
+ this.port = this._transport.port;
+
+ var openFlags = 0;
+
+ /* no limit on the output stream buffer */
+ this._outputStream =
+ this._transport.openOutputStream(openFlags, 4096, -1);
+ if (!this._outputStream)
+ throw "Error getting output stream.";
+ this._sOutputStream = toSOutputStream(this._outputStream,
+ this.binaryMode);
+
+ this._inputStream = this._transport.openInputStream(openFlags, 0, 0);
+ if (!this._inputStream)
+ throw "Error getting input stream.";
+ this._sInputStream = toSInputStream(this._inputStream,
+ this.binaryMode);
+
+ this.connectDate = new Date();
+ this.isConnected = true;
+
+ // Clean up listening socket.
+ this.close();
+
+ return this.isConnected;
+}
+
+CBSConnection.prototype.close =
+function bc_close()
+{
+ if ("_serverSock" in this && this._serverSock)
+ this._serverSock.close();
+}
+
+CBSConnection.prototype.disconnect =
+function bc_disconnect()
+{
+ if ("_inputStream" in this && this._inputStream)
+ this._inputStream.close();
+ if ("_outputStream" in this && this._outputStream)
+ this._outputStream.close();
+ this.isConnected = false;
+ /*
+ this._streamProvider.close();
+ if (this._streamProvider.isBlocked)
+ this._write_req.resume();
+ */
+}
+
+CBSConnection.prototype.sendData =
+function bc_senddata(str)
+{
+ if (!this.isConnected)
+ throw "Not Connected.";
+
+ this.sendDataNow(str);
+}
+
+CBSConnection.prototype.readData =
+function bc_readdata(timeout, count)
+{
+ if (!this.isConnected)
+ throw "Not Connected.";
+
+ var rv;
+
+ if (!("_sInputStream" in this)) {
+ this._sInputStream = toSInputStream(this._inputStream);
+ dump("OMG, setting up _sInputStream!\n");
+ }
+
+ try
+ {
+ // XPCshell h4x
+ if (typeof count == "undefined")
+ count = this._sInputStream.available();
+ if (this.binaryMode)
+ rv = this._sInputStream.readBytes(count);
+ else
+ rv = this._sInputStream.read(count);
+ }
+ catch (ex)
+ {
+ dd ("*** Caught " + ex + " while reading.");
+ this.disconnect();
+ throw (ex);
+ }
+
+ return rv;
+}
+
+CBSConnection.prototype.startAsyncRead =
+function bc_saread (observer)
+{
+ var cls = Components.classes["@mozilla.org/network/input-stream-pump;1"];
+ var pump = cls.createInstance(Components.interfaces.nsIInputStreamPump);
+ // Account for Bug 1402888 which removed the startOffset and readLimit
+ // parameters from init.
+ if (pump.init.length > 5)
+ {
+ pump.init(this._inputStream, -1, -1, 0, 0, false);
+ } else
+ {
+ pump.init(this._inputStream, 0, 0, false);
+ }
+ pump.asyncRead(new StreamListener(observer), this);
+}
+
+CBSConnection.prototype.asyncWrite =
+function bc_awrite (str)
+{
+ this._streamProvider.pendingData += str;
+ if (this._streamProvider.isBlocked)
+ {
+ this._write_req.resume();
+ this._streamProvider.isBlocked = false;
+ }
+}
+
+CBSConnection.prototype.hasPendingWrite =
+function bc_haspwrite ()
+{
+ return false; /* data already pushed to necko */
+}
+
+CBSConnection.prototype.sendDataNow =
+function bc_senddatanow(str)
+{
+ var rv = false;
+
+ try
+ {
+ if (this.binaryMode)
+ this._sOutputStream.writeBytes(str, str.length);
+ else
+ this._sOutputStream.write(str, str.length);
+ rv = true;
+ }
+ catch (ex)
+ {
+ dd ("*** Caught " + ex + " while sending.");
+ this.disconnect();
+ throw (ex);
+ }
+
+ return rv;
+}
+
+/**
+ * Gets information about the security of the connection.
+ *
+ * |STATE_IS_BROKEN| is returned if any errors occur and |STATE_IS_INSECURE| is
+ * returned for disconnected sockets.
+ *
+ * @returns A value from the |STATE_IS_*| enumeration at the top of this file.
+ */
+CBSConnection.prototype.getSecurityState =
+function bc_getsecuritystate()
+{
+ if (!this.isConnected || !this._transport.securityInfo)
+ return STATE_IS_INSECURE;
+
+ try
+ {
+ // Get the actual SSL Status
+ let sslSp = this._transport.securityInfo
+ .QueryInterface(Ci.nsISSLStatusProvider);
+ if (!sslSp.SSLStatus)
+ return STATE_IS_BROKEN;
+ let sslStatus = sslSp.SSLStatus.QueryInterface(Ci.nsISSLStatus);
+ // Store appropriate status
+ if (!("keyLength" in sslStatus) || !sslStatus.keyLength)
+ return STATE_IS_BROKEN;
+
+ return STATE_IS_SECURE;
+ }
+ catch (ex)
+ {
+ // Something goes wrong -> broken security icon
+ dd("Exception getting certificate for connection: " + ex.message);
+ return STATE_IS_BROKEN;
+ }
+}
+
+CBSConnection.prototype.getCertificate =
+function bc_getcertificate()
+{
+ if (!this.isConnected || !this._transport.securityInfo)
+ return null;
+
+ // Get the actual SSL Status
+ let sslSp = this._transport.securityInfo
+ .QueryInterface(Ci.nsISSLStatusProvider);
+ if (!sslSp.SSLStatus)
+ return null;
+ let sslStatus = sslSp.SSLStatus.QueryInterface(Ci.nsISSLStatus);
+
+ // return the certificate
+ return sslStatus.serverCert;
+}
+
+CBSConnection.prototype.asyncWrite =
+function bc_asyncwrite()
+{
+ throw "Not Implemented.";
+}
+
+function StreamProvider(observer)
+{
+ this._observer = observer;
+}
+
+StreamProvider.prototype.pendingData = "";
+StreamProvider.prototype.isBlocked = true;
+
+StreamProvider.prototype.close =
+function sp_close ()
+{
+ this.isClosed = true;
+}
+
+StreamProvider.prototype.onDataWritable =
+function sp_datawrite (request, ctxt, ostream, offset, count)
+{
+ //dd ("StreamProvider.prototype.onDataWritable");
+
+ if ("isClosed" in this && this.isClosed)
+ throw Components.results.NS_BASE_STREAM_CLOSED;
+
+ if (!this.pendingData)
+ {
+ this.isBlocked = true;
+
+ /* this is here to support pre-XPCDOM builds (0.9.0 era), which
+ * don't have this result code mapped. */
+ if (!Components.results.NS_BASE_STREAM_WOULD_BLOCK)
+ throw 2152136711;
+
+ throw Components.results.NS_BASE_STREAM_WOULD_BLOCK;
+ }
+
+ var len = ostream.write (this.pendingData, this.pendingData.length);
+ this.pendingData = this.pendingData.substr (len);
+}
+
+StreamProvider.prototype.onStartRequest =
+function sp_startreq (request, ctxt)
+{
+ //dd ("StreamProvider::onStartRequest: " + request + ", " + ctxt);
+}
+
+
+StreamProvider.prototype.onStopRequest =
+function sp_stopreq (request, ctxt, status)
+{
+ //dd ("StreamProvider::onStopRequest: " + request + ", " + ctxt + ", " +
+ // status);
+ if (this._observer)
+ this._observer.onStreamClose(status);
+}
+
+function StreamListener(observer)
+{
+ this._observer = observer;
+}
+
+StreamListener.prototype.onStartRequest =
+function sl_startreq (request, ctxt)
+{
+ //dd ("StreamListener::onStartRequest: " + request + ", " + ctxt);
+}
+
+StreamListener.prototype.onStopRequest =
+function sl_stopreq (request, ctxt, status)
+{
+ //dd ("StreamListener::onStopRequest: " + request + ", " + ctxt + ", " +
+ //status);
+ if (this._observer)
+ this._observer.onStreamClose(status);
+}
+
+StreamListener.prototype.onDataAvailable =
+function sl_dataavail (request, ctxt, inStr, sourceOffset, count)
+{
+ ctxt = ctxt.wrappedJSObject;
+ if (!ctxt)
+ {
+ dd ("*** Can't get wrappedJSObject from ctxt in " +
+ "StreamListener.onDataAvailable ***");
+ return;
+ }
+
+ if (!("_sInputStream" in ctxt))
+ ctxt._sInputStream = toSInputStream(inStr, false);
+
+ if (this._observer)
+ this._observer.onStreamDataAvailable(request, inStr, sourceOffset,
+ count);
+}
+
+function SocketListener(connection, observer)
+{
+ this._connection = connection;
+ this._observer = observer;
+}
+
+SocketListener.prototype.onSocketAccepted =
+function sl_onSocketAccepted(socket, transport)
+{
+ this._observer.onSocketAccepted(socket, transport);
+}
+SocketListener.prototype.onStopListening =
+function sl_onStopListening(socket, status)
+{
+ delete this._connection._serverSockListener;
+ delete this._connection._serverSock;
+}
diff --git a/comm/suite/chatzilla/js/lib/dcc.js b/comm/suite/chatzilla/js/lib/dcc.js
new file mode 100644
index 0000000000..d12b8f1a52
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/dcc.js
@@ -0,0 +1,1198 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+/* We pick a random start ID, from 0 to DCC_ID_MAX inclusive, then go through
+ * the IDs one at a time, in sequence. We wrap when we get to DCC_ID_MAX. No
+ * uniqueness checking is done, but it takes DCC_ID_MAX connections before we
+ * hit the start ID again. 65,536 IDs ought to be enough for now. :)
+ */
+const DCC_ID_MAX = 0xFFFF;
+
+function CIRCDCC(parent)
+{
+ this.parent = parent;
+
+ this.users = new Object();
+ this.chats = new Array();
+ this.files = new Array();
+ this.last = null;
+ this.lastTime = null;
+
+ this.sendChunk = 4096;
+ this.maxUnAcked = 32; // 4096 * 32 == 128KiB 'in transit'.
+
+ this.requestTimeout = 3 * 60 * 1000; // 3 minutes.
+
+ // Can't do anything 'til this is set!
+ this.localIPlist = new Array();
+ this.localIP = null;
+ this._lastPort = null;
+
+ try {
+ var dnsComp = Components.classes["@mozilla.org/network/dns-service;1"];
+ this._dnsSvc = dnsComp.getService(Components.interfaces.nsIDNSService);
+
+ // Get local hostname.
+ if ("myHostName" in this._dnsSvc) {
+ // Using newer (1.7a+) version with DNS re-write.
+ this.addHost(this._dnsSvc.myHostName);
+ }
+ if ("myIPAddress" in this._dnsSvc) {
+ // Older Mozilla, have to use this method.
+ this.addIP(this._dnsSvc.myIPAddress);
+ }
+ this.addHost("localhost");
+ } catch(ex) {
+ // what to do?
+ dd("Error getting local IPs: " + ex);
+ }
+
+ this._lastID = Math.round(Math.random() * DCC_ID_MAX);
+
+ return this;
+}
+
+CIRCDCC.prototype.TYPE = "IRCDCC";
+CIRCDCC.prototype.listenPorts = [];
+
+CIRCDCC.prototype.addUser =
+function dcc_adduser(user, remoteIP)
+{
+ // user == CIRCUser object.
+ // remoteIP == remoteIP as specified in CTCP DCC message.
+ return new CIRCDCCUser(this, user, remoteIP);
+}
+
+CIRCDCC.prototype.addChat =
+function dcc_addchat(user, port)
+{
+ // user == CIRCDCCUser object.
+ // port == port as specified in CTCP DCC message.
+ return new CIRCDCCChat(this, user, port);
+}
+
+CIRCDCC.prototype.addFileTransfer =
+function dcc_addfile(user, port, file, size)
+{
+ return new CIRCDCCFileTransfer(this, user, port, file, size);
+}
+
+CIRCDCC.prototype.addHost =
+function dcc_addhost(host, auth)
+{
+ var me = this;
+ var listener = {
+ onLookupComplete: function _onLookupComplete(request, record, status) {
+ // record == null if it failed. We can't do anything with a failure.
+ if (record)
+ {
+ while (record.hasMore())
+ me.addIP(record.getNextAddrAsString(), auth);
+ }
+ }
+ };
+
+ try {
+ var th = getService("@mozilla.org/thread-manager;1").currentThread;
+ var dnsRecord = this._dnsSvc.asyncResolve(host, false, listener, th);
+ } catch (ex) {
+ dd("Error resolving host to IP: " + ex);
+ }
+}
+
+CIRCDCC.prototype.addIP =
+function dcc_addip(ip, auth)
+{
+ if (auth)
+ this.localIPlist.unshift(ip);
+ else
+ this.localIPlist.push(ip);
+
+ if (this.localIPlist.length > 0)
+ this.localIP = this.localIPlist[0];
+}
+
+CIRCDCC.prototype.getMatches =
+function dcc_getmatches(nickname, filename, types, dirs, states)
+{
+ function matchNames(name, otherName)
+ {
+ return ((name.match(new RegExp(otherName, "i"))) ||
+ (name.toLowerCase().indexOf(otherName.toLowerCase()) != -1));
+ };
+
+ var k;
+ var list = new Array();
+ if (!types)
+ types = ["chat", "file"];
+
+ var n = nickname;
+ var f = filename;
+
+ if (arrayIndexOf(types, "chat") >= 0)
+ {
+ for (k = 0; k < this.chats.length; k++)
+ {
+ if ((!nickname || matchNames(this.chats[k].user.unicodeName, n)) &&
+ (!dirs || arrayIndexOf(dirs, this.chats[k].state.dir) >= 0) &&
+ (!states || arrayIndexOf(states, this.chats[k].state.state) >= 0))
+ {
+ list.push(this.chats[k]);
+ }
+ }
+ }
+ if (arrayIndexOf(types, "file") >= 0)
+ {
+ for (k = 0; k < this.files.length; k++)
+ {
+ if ((!nickname || matchNames(this.files[k].user.unicodeName, n)) &&
+ (!filename || matchNames(this.files[k].filename, f)) &&
+ (!dirs || arrayIndexOf(dirs, this.files[k].state.dir) >= 0) &&
+ (!states || arrayIndexOf(states, this.files[k].state.state) >= 0))
+ {
+ list.push(this.files[k]);
+ }
+ }
+ }
+
+ return list;
+}
+
+CIRCDCC.prototype.getNextPort =
+function dcc_getnextport()
+{
+ var portList = this.listenPorts;
+
+ var newPort = this._lastPort;
+
+ for (var i = 0; i < portList.length; i++)
+ {
+ var m = portList[i].match(/^(\d+)(?:-(\d+))?$/);
+ if (m)
+ {
+ if (!newPort)
+ {
+ // We dodn't have any previous port, so just take the first we
+ // find.
+ return this._lastPort = Number(m[1]);
+ }
+ else if (arrayHasElementAt(m, 2))
+ {
+ // Port range. Anything before range, or in [exl. last value]
+ // is ok. Make sure first value is lowest value returned.
+ if (newPort < m[2])
+ return this._lastPort = Math.max(newPort + 1, Number(m[1]));
+ }
+ else
+ {
+ // Single port.
+ if (newPort < m[1])
+ return this._lastPort = Number(m[1]);
+ }
+ }
+ }
+
+ // No ports found, and no last port --> use OS.
+ if (newPort == null)
+ return -1;
+
+ // Didn't find anything... d'oh. Need to start from the begining again.
+ this._lastPort = null;
+ return this.getNextPort();
+}
+
+CIRCDCC.prototype.getNextID =
+function dcc_getnextid()
+{
+ this._lastID++;
+ if (this._lastID > DCC_ID_MAX)
+ this._lastID = 0;
+
+ // Format to DCC_ID_MAX's number of digits.
+ var id = this._lastID.toString(16);
+ while (id.length < DCC_ID_MAX.toString(16).length)
+ id = "0" + id;
+ return id;
+}
+
+CIRCDCC.prototype.findByID =
+function dcc_findbyid(id)
+{
+ if (typeof id != "string")
+ return null;
+
+ var i;
+ for (i = 0; i < this.chats.length; i++)
+ {
+ if (this.chats[i].id == id)
+ return this.chats[i];
+ }
+ for (i = 0; i < this.files.length; i++)
+ {
+ if (this.files[i].id == id)
+ return this.files[i];
+ }
+ return null;
+}
+
+
+// JavaScript won't let you delete things declared with "var", workaround:
+window.val = -1;
+
+const DCC_STATE_FAILED = val++; // try connect (accept), but it failed.
+const DCC_STATE_INIT = val++; // not "doing" anything
+const DCC_STATE_REQUESTED = val++; // waiting
+const DCC_STATE_ACCEPTED = val++; // accepted.
+const DCC_STATE_DECLINED = val++; // declined.
+const DCC_STATE_CONNECTED = val++; // all going ok.
+const DCC_STATE_DONE = val++; // finished ok.
+const DCC_STATE_ABORTED = val++; // send wasn't accepted in time.
+
+delete window.val;
+
+const DCC_DIR_UNKNOWN = 0;
+const DCC_DIR_SENDING = 1;
+const DCC_DIR_GETTING = 2;
+
+
+function CIRCDCCUser(parent, user, remoteIP)
+{
+ // user == CIRCUser object.
+ // remoteIP == remoteIP as specified in CTCP DCC message.
+
+ if ("dccUser" in user)
+ {
+ if (remoteIP)
+ user.dccUser.remoteIP = remoteIP;
+
+ return user.dccUser;
+ }
+
+ this.parent = parent;
+ this.netUser = user;
+ this.id = parent.getNextID();
+ this.unicodeName = user.unicodeName;
+ this.viewName = user.unicodeName;
+ this.canonicalName = user.collectionKey.substr(1);
+ this.remoteIP = remoteIP;
+
+ this.key = escape(user.collectionKey) + ":" + remoteIP;
+ user.dccUser = this;
+ this.parent.users[this.key] = this;
+
+ if ("onInit" in this)
+ this.onInit();
+
+ return this;
+}
+
+CIRCDCCUser.prototype.TYPE = "IRCDCCUser";
+
+
+// Keeps track of the state of a DCC connection.
+function CIRCDCCState(parent, owner, eventType)
+{
+ // parent == central CIRCDCC object.
+ // owner == DCC Chat or File object.
+ // eventType == "dcc-chat" or "dcc-file".
+
+ this.parent = parent;
+ this.owner = owner;
+ this.eventType = eventType;
+
+ this.eventPump = owner.eventPump;
+
+ this.state = DCC_STATE_INIT;
+ this.dir = DCC_DIR_UNKNOWN;
+
+ return this;
+}
+
+CIRCDCCState.prototype.TYPE = "IRCDCCState";
+
+CIRCDCCState.prototype.sendRequest =
+function dccstate_sendRequest()
+{
+ if (!this.parent.localIP || (this.state != DCC_STATE_INIT))
+ throw "Must have a local IP and be in INIT state.";
+
+ this.state = DCC_STATE_REQUESTED;
+ this.dir = DCC_DIR_SENDING;
+ this.requested = new Date();
+
+ this.requestTimeout = setTimeout(function (o){ o.abort(); },
+ this.parent.requestTimeout, this.owner);
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "request",
+ this.owner, "onRequest"));
+}
+
+CIRCDCCState.prototype.getRequest =
+function dccstate_getRequest()
+{
+ if (this.state != DCC_STATE_INIT)
+ throw "Must be in INIT state.";
+
+ this.state = DCC_STATE_REQUESTED;
+ this.dir = DCC_DIR_GETTING;
+ this.requested = new Date();
+
+ this.parent.last = this.owner;
+ this.parent.lastTime = new Date();
+
+ this.requestTimeout = setTimeout(function (o){ o.abort(); },
+ this.parent.requestTimeout, this.owner);
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "request",
+ this.owner, "onRequest"));
+}
+
+CIRCDCCState.prototype.sendAccept =
+function dccstate_sendAccept()
+{
+ if ((this.state != DCC_STATE_REQUESTED) || (this.dir != DCC_DIR_GETTING))
+ throw "Must be in REQUESTED state and direction GET.";
+
+ // Clear out "last" incoming request if that's us.
+ if (this.parent.last == this.owner)
+ {
+ this.parent.last = null;
+ this.parent.lastTime = null;
+ }
+
+ clearTimeout(this.requestTimeout);
+ delete this.requestTimeout;
+
+ this.state = DCC_STATE_ACCEPTED;
+ this.accepted = new Date();
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "accept",
+ this.owner, "onAccept"));
+}
+
+CIRCDCCState.prototype.getAccept =
+function dccstate_getAccept()
+{
+ if ((this.state != DCC_STATE_REQUESTED) || (this.dir != DCC_DIR_SENDING))
+ throw "Must be in REQUESTED state and direction SEND.";
+
+ clearTimeout(this.requestTimeout);
+ delete this.requestTimeout;
+
+ this.state = DCC_STATE_ACCEPTED;
+ this.accepted = new Date();
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "accept",
+ this.owner, "onAccept"));
+}
+
+CIRCDCCState.prototype.sendDecline =
+function dccstate_sendDecline()
+{
+ if ((this.state != DCC_STATE_REQUESTED) || (this.dir != DCC_DIR_GETTING))
+ throw "Must be in REQUESTED state and direction GET.";
+
+ // Clear out "last" incoming request if that's us.
+ if (this.parent.last == this.owner)
+ {
+ this.parent.last = null;
+ this.parent.lastTime = null;
+ }
+
+ clearTimeout(this.requestTimeout);
+ delete this.requestTimeout;
+
+ this.state = DCC_STATE_DECLINED;
+ this.declined = new Date();
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "decline",
+ this.owner, "onDecline"));
+}
+
+CIRCDCCState.prototype.getDecline =
+function dccstate_getDecline()
+{
+ if ((this.state != DCC_STATE_REQUESTED) || (this.dir != DCC_DIR_SENDING))
+ throw "Must be in REQUESTED state and direction SEND.";
+
+ clearTimeout(this.requestTimeout);
+ delete this.requestTimeout;
+
+ this.state = DCC_STATE_DECLINED;
+ this.declined = new Date();
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "decline",
+ this.owner, "onDecline"));
+}
+
+// The sockets connected.
+CIRCDCCState.prototype.socketConnected =
+function dccstate_socketConnected()
+{
+ if (this.state != DCC_STATE_ACCEPTED)
+ throw "Not in ACCEPTED state.";
+
+ this.state = DCC_STATE_CONNECTED;
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "connect",
+ this.owner, "onConnect"));
+}
+
+// Someone disconnected something.
+CIRCDCCState.prototype.socketDisconnected =
+function dccstate_socketDisconnected()
+{
+ if (this.state != DCC_STATE_CONNECTED)
+ throw "Not CONNECTED!";
+
+ this.state = DCC_STATE_DONE;
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "disconnect",
+ this.owner, "onDisconnect"));
+}
+
+CIRCDCCState.prototype.sendAbort =
+function dccstate_sendAbort()
+{
+ if ((this.state != DCC_STATE_REQUESTED) &&
+ (this.state != DCC_STATE_ACCEPTED) &&
+ (this.state != DCC_STATE_CONNECTED))
+ {
+ throw "Can't abort at this point.";
+ }
+
+ this.state = DCC_STATE_ABORTED;
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "abort",
+ this.owner, "onAbort"));
+}
+
+CIRCDCCState.prototype.getAbort =
+function dccstate_getAbort()
+{
+ if ((this.state != DCC_STATE_REQUESTED) &&
+ (this.state != DCC_STATE_ACCEPTED) &&
+ (this.state != DCC_STATE_CONNECTED))
+ {
+ throw "Can't abort at this point.";
+ }
+
+ this.state = DCC_STATE_ABORTED;
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "abort",
+ this.owner, "onAbort"));
+}
+
+CIRCDCCState.prototype.failed =
+function dccstate_failed()
+{
+ if ((this.state != DCC_STATE_REQUESTED) &&
+ (this.state != DCC_STATE_ACCEPTED) &&
+ (this.state != DCC_STATE_CONNECTED))
+ {
+ throw "Can't fail at this point.";
+ }
+
+ this.state = DCC_STATE_FAILED;
+
+ this.eventPump.addEvent(new CEvent(this.eventType, "fail",
+ this.owner, "onFail"));
+}
+
+
+
+
+function CIRCDCCChat(parent, user, port)
+{
+ // user == CIRCDCCUser object.
+ // port == port as specified in CTCP DCC message.
+
+ this.READ_TIMEOUT = 50;
+
+ // Link up all our data.
+ this.parent = parent;
+ this.id = parent.getNextID();
+ this.eventPump = parent.parent.eventPump;
+ this.user = user;
+ this.localIP = this.parent.localIP;
+ this.remoteIP = user.remoteIP;
+ this.port = port;
+ this.unicodeName = user.unicodeName;
+ this.viewName = "DCC: " + user.unicodeName;
+
+ // Set up the initial state.
+ this.requested = null;
+ this.connection = null;
+ this.savedLine = "";
+ this.state = new CIRCDCCState(parent, this, "dcc-chat");
+ this.parent.chats.push(this);
+
+ // Give ourselves a "me" object for the purposes of displaying stuff.
+ this.me = this.parent.addUser(this.user.netUser.parent.me, "0.0.0.0");
+
+ if ("onInit" in this)
+ this.onInit();
+
+ return this;
+}
+
+CIRCDCCChat.prototype.TYPE = "IRCDCCChat";
+
+CIRCDCCChat.prototype.getURL =
+function dchat_geturl()
+{
+ return "x-irc-dcc-chat:" + this.id;
+}
+
+CIRCDCCChat.prototype.isActive =
+function dchat_isactive()
+{
+ return (this.state.state == DCC_STATE_REQUESTED) ||
+ (this.state.state == DCC_STATE_ACCEPTED) ||
+ (this.state.state == DCC_STATE_CONNECTED);
+}
+
+// Call to make this end request DCC Chat with targeted user.
+CIRCDCCChat.prototype.request =
+function dchat_request()
+{
+ this.state.sendRequest();
+
+ this.localIP = this.parent.localIP;
+
+ this.connection = new CBSConnection();
+ if (!this.connection.listen(this.port, this))
+ {
+ this.state.failed();
+ return false;
+ }
+
+ this.port = this.connection.port;
+
+ // Send the CTCP DCC request via the net user (CIRCUser object).
+ var ipNumber;
+ var ipParts = this.localIP.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
+ if (ipParts)
+ ipNumber = Number(ipParts[1]) * 256 * 256 * 256 +
+ Number(ipParts[2]) * 256 * 256 +
+ Number(ipParts[3]) * 256 +
+ Number(ipParts[4]);
+ else
+ return false;
+ // What should we do here? Panic?
+
+ this.user.netUser.ctcp("DCC", "CHAT chat " + ipNumber + " " + this.port);
+
+ return true;
+}
+
+// Call to make this end accept DCC Chat with target user.
+CIRCDCCChat.prototype.accept =
+function dchat_accept()
+{
+ this.state.sendAccept();
+
+ this.connection = new CBSConnection();
+ this.connection.connect(this.remoteIP, this.port, null, this);
+
+ return true;
+}
+
+// This may be called synchronously or asynchronously by CBSConnection.connect.
+CIRCDCCChat.prototype.onSocketConnection =
+function dchat_onsocketconnection(host, port, config, exception)
+{
+ if (!exception)
+ {
+ this.state.socketConnected();
+
+ this.connection.startAsyncRead(this);
+ }
+ else
+ {
+ this.state.failed();
+ }
+}
+
+// Call to make this end decline DCC Chat with target user.
+CIRCDCCChat.prototype.decline =
+function dchat_decline()
+{
+ this.state.sendDecline();
+
+ // Tell the other end, if they care, that we refused.
+ this.user.netUser.ctcp("DCC", "REJECT CHAT chat");
+
+ return true;
+}
+
+// Call to close the connection.
+CIRCDCCChat.prototype.disconnect =
+function dchat_disconnect()
+{
+ this.connection.disconnect();
+
+ return true;
+}
+
+// Aborts the connection.
+CIRCDCCChat.prototype.abort =
+function dchat_abort()
+{
+ if (this.state.state == DCC_STATE_CONNECTED)
+ {
+ this.disconnect();
+ return;
+ }
+
+ this.state.sendAbort();
+
+ if (this.connection)
+ this.connection.close();
+}
+
+// Event to handle a request from the target user.
+// CIRCUser points the event here.
+CIRCDCCChat.prototype.onGotRequest =
+function dchat_onGotRequest(e)
+{
+ this.state.getRequest();
+
+ // Pass over to the base user.
+ e.destObject = this.user.netUser;
+}
+
+// Event to handle a client connecting to the listening socket.
+// CBSConnection points the event here.
+CIRCDCCChat.prototype.onSocketAccepted =
+function dchat_onSocketAccepted(socket, transport)
+{
+ this.state.getAccept();
+
+ this.connection.accept(transport, null);
+
+ this.state.socketConnected();
+
+ this.remoteIP = transport.host;
+
+ // Start the reading!
+ this.connection.startAsyncRead(this);
+}
+
+CIRCDCCChat.prototype.onStreamDataAvailable =
+function dchat_sda(request, inStream, sourceOffset, count)
+{
+ var ev = new CEvent("dcc-chat", "data-available", this, "onDataAvailable");
+ ev.line = this.connection.readData(0, count);
+ this.eventPump.routeEvent(ev);
+}
+
+CIRCDCCChat.prototype.onStreamClose =
+function dchat_sockdiscon(status)
+{
+ this.state.socketDisconnected();
+
+ //var ev = new CEvent("dcc-chat", "disconnect", this, "onDisconnect");
+ //ev.server = this;
+ //ev.disconnectStatus = status;
+ //this.eventPump.addEvent(ev);
+}
+
+CIRCDCCChat.prototype.onDataAvailable =
+function dchat_dataavailable(e)
+{
+ var line = e.line;
+
+ var incomplete = (line[line.length] != '\n');
+ var lines = line.split("\n");
+
+ if (this.savedLine)
+ {
+ lines[0] = this.savedLine + lines[0];
+ this.savedLine = "";
+ }
+
+ if (incomplete)
+ this.savedLine = lines.pop();
+
+ for (var i in lines)
+ {
+ var ev = new CEvent("dcc-chat", "rawdata", this, "onRawData");
+ ev.data = lines[i];
+ ev.replyTo = this;
+ this.eventPump.addEvent (ev);
+ }
+
+ return true;
+}
+
+// Raw data from DCC Chat stream.
+CIRCDCCChat.prototype.onRawData =
+function dchat_rawdata(e)
+{
+ e.code = "PRIVMSG";
+ e.line = e.data;
+ e.user = this.user;
+ e.type = "parseddata";
+ e.destObject = this;
+ e.destMethod = "onParsedData";
+
+ return true;
+}
+
+CIRCDCCChat.prototype.onParsedData =
+function dchat_onParsedData(e)
+{
+ e.type = e.code.toLowerCase();
+ if (!e.code[0])
+ {
+ dd (dumpObjectTree (e));
+ return false;
+ }
+
+ if (e.line.search(/\x01.*\x01/i) != -1) {
+ e.type = "ctcp";
+ e.destMethod = "onCTCP";
+ e.set = "dcc-chat";
+ e.destObject = this;
+ }
+ else
+ {
+ e.type = "privmsg";
+ e.destMethod = "onPrivmsg";
+ e.set = "dcc-chat";
+ e.destObject = this;
+ }
+
+ // Allow DCC Chat to handle it before "falling back" to DCC User.
+ if (typeof this[e.destMethod] == "function")
+ e.destObject = this;
+ else if (typeof this.user[e.destMethod] == "function")
+ e.destObject = this.user;
+ else if (typeof this["onUnknown"] == "function")
+ e.destMethod = "onUnknown";
+ else if (typeof this.parent[e.destMethod] == "function")
+ {
+ e.set = "dcc";
+ e.destObject = this.parent;
+ }
+ else
+ {
+ e.set = "dcc";
+ e.destObject = this.parent;
+ e.destMethod = "onUnknown";
+ }
+
+ return true;
+}
+
+CIRCDCCChat.prototype.onCTCP =
+function serv_ctcp(e)
+{
+ // The \x0D? is a BIG HACK to make this work with X-Chat.
+ var ary = e.line.match(/^\x01([^ ]+) ?(.*)\x01\x0D?$/i);
+ if (ary == null)
+ return false;
+
+ e.CTCPData = ary[2] ? ary[2] : "";
+ e.CTCPCode = ary[1].toLowerCase();
+ if (e.CTCPCode.search(/^reply/i) == 0)
+ {
+ dd("dropping spoofed reply.");
+ return false;
+ }
+
+ e.CTCPCode = toUnicode(e.CTCPCode, e.replyTo);
+ e.CTCPData = toUnicode(e.CTCPData, e.replyTo);
+
+ e.type = "ctcp-" + e.CTCPCode;
+ e.destMethod = "onCTCP" + ary[1][0].toUpperCase() +
+ ary[1].substr(1, ary[1].length).toLowerCase();
+
+ if (typeof this[e.destMethod] != "function")
+ {
+ e.destObject = e.user;
+ e.set = "dcc-user";
+ if (typeof e.user[e.destMethod] != "function")
+ {
+ e.type = "unk-ctcp";
+ e.destMethod = "onUnknownCTCP";
+ }
+ }
+ else
+ {
+ e.destObject = this;
+ }
+ return true;
+}
+
+CIRCDCCChat.prototype.ctcp =
+function dchat_ctcp(code, msg)
+{
+ msg = msg || "";
+
+ this.connection.sendData("\x01" + fromUnicode(code, this) + " " +
+ fromUnicode(msg, this) + "\x01\n");
+}
+
+CIRCDCCChat.prototype.say =
+function dchat_say (msg)
+{
+ this.connection.sendData(fromUnicode(msg, this) + "\n");
+}
+
+CIRCDCCChat.prototype.act =
+function dchat_act(msg)
+{
+ this.ctcp("ACTION", msg);
+}
+
+
+function CIRCDCCFileTransfer(parent, user, port, file, size)
+{
+ // user == CIRCDCCUser object.
+ // port == port as specified in CTCP DCC message.
+ // file == name of file being sent/got.
+ // size == size of said file.
+
+ this.READ_TIMEOUT = 50;
+
+ // Link up all our data.
+ this.parent = parent;
+ this.id = parent.getNextID();
+ this.eventPump = parent.parent.eventPump;
+ this.user = user;
+ this.localIP = this.parent.localIP;
+ this.remoteIP = user.remoteIP;
+ this.port = port;
+ this.filename = file;
+ this.size = size;
+ this.unicodeName = user.unicodeName;
+ this.viewName = "File: " + this.filename;
+
+ // Set up the initial state.
+ this.requested = null;
+ this.connection = null;
+ this.state = new CIRCDCCState(parent, this, "dcc-file");
+ this.parent.files.push(this);
+
+ // Give ourselves a "me" object for the purposes of displaying stuff.
+ this.me = this.parent.addUser(this.user.netUser.parent.me, "0.0.0.0");
+
+ if ("onInit" in this)
+ this.onInit();
+
+ return this;
+}
+
+CIRCDCCFileTransfer.prototype.TYPE = "IRCDCCFileTransfer";
+
+CIRCDCCFileTransfer.prototype.getURL =
+function dfile_geturl()
+{
+ return "x-irc-dcc-file:" + this.id;
+}
+
+CIRCDCCFileTransfer.prototype.isActive =
+function dfile_isactive()
+{
+ return (this.state.state == DCC_STATE_REQUESTED) ||
+ (this.state.state == DCC_STATE_ACCEPTED) ||
+ (this.state.state == DCC_STATE_CONNECTED);
+}
+
+CIRCDCCFileTransfer.prototype.dispose =
+function dfile_dispose()
+{
+ if (this.connection)
+ {
+ // close is for the server socket, disconnect for the client socket.
+ this.connection.close();
+ this.connection.disconnect();
+ }
+
+ if (this.localFile)
+ this.localFile.close();
+
+ this.connection = null;
+ this.localFile = null;
+ this.filestream = null;
+}
+
+// Call to make this end offer DCC File to targeted user.
+CIRCDCCFileTransfer.prototype.request =
+function dfile_request(localFile)
+{
+ this.state.sendRequest();
+
+ this.localFile = new LocalFile(localFile, "<");
+ this.filename = localFile.leafName;
+ this.size = localFile.fileSize;
+
+ // Update view name.
+ this.viewName = "File: " + this.filename;
+
+ // Double-quote file names with spaces.
+ // FIXME: Do we need any better checking?
+ if (this.filename.match(/ /))
+ this.filename = '"' + this.filename + '"';
+
+ this.localIP = this.parent.localIP;
+
+ this.connection = new CBSConnection(true);
+ if (!this.connection.listen(this.port, this))
+ {
+ this.state.failed();
+ this.dispose();
+ return false;
+ }
+
+ this.port = this.connection.port;
+
+ // Send the CTCP DCC request via the net user (CIRCUser object).
+ var ipNumber;
+ var ipParts = this.localIP.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
+ if (ipParts)
+ ipNumber = Number(ipParts[1]) * 256 * 256 * 256 +
+ Number(ipParts[2]) * 256 * 256 +
+ Number(ipParts[3]) * 256 +
+ Number(ipParts[4]);
+ else
+ return false;
+ // What should we do here? Panic?
+
+ this.user.netUser.ctcp("DCC", "SEND " + this.filename + " " +
+ ipNumber + " " + this.port + " " + this.size);
+
+ return true;
+}
+
+// Call to make this end accept DCC File from target user.
+CIRCDCCFileTransfer.prototype.accept =
+function dfile_accept(localFile)
+{
+ const nsIBinaryOutputStream = Components.interfaces.nsIBinaryOutputStream;
+
+ this.state.sendAccept();
+
+ this.localFile = new LocalFile(localFile, ">");
+ this.localPath = localFile.path;
+
+ this.filestream = Components.classes["@mozilla.org/binaryoutputstream;1"];
+ this.filestream = this.filestream.createInstance(nsIBinaryOutputStream);
+ this.filestream.setOutputStream(this.localFile.outputStream);
+
+ this.position = 0;
+ this.connection = new CBSConnection(true);
+ this.connection.connect(this.remoteIP, this.port, null, this);
+
+ return true;
+}
+
+// This may be called synchronously or asynchronously by CBSConnection.connect.
+CIRCDCCFileTransfer.prototype.onSocketConnection =
+function dfile_onsocketconnection(host, port, config, exception)
+{
+ if (!exception)
+ {
+ this.state.socketConnected();
+
+ this.connection.startAsyncRead(this);
+ }
+ else
+ {
+ this.state.failed();
+ this.dispose();
+ }
+}
+
+// Call to make this end decline DCC File from target user.
+CIRCDCCFileTransfer.prototype.decline =
+function dfile_decline()
+{
+ this.state.sendDecline();
+
+ // Tell the other end, if they care, that we refused.
+ this.user.netUser.ctcp("DCC", "REJECT FILE " + this.filename);
+
+ return true;
+}
+
+// Call to close the connection.
+CIRCDCCFileTransfer.prototype.disconnect =
+function dfile_disconnect()
+{
+ this.dispose();
+
+ return true;
+}
+
+// Aborts the connection.
+CIRCDCCFileTransfer.prototype.abort =
+function dfile_abort()
+{
+ if (this.state.state == DCC_STATE_CONNECTED)
+ {
+ this.disconnect();
+ return;
+ }
+
+ this.state.sendAbort();
+ this.dispose();
+}
+
+// Event to handle a request from the target user.
+// CIRCUser points the event here.
+CIRCDCCFileTransfer.prototype.onGotRequest =
+function dfile_onGotRequest(e)
+{
+ this.state.getRequest();
+
+ // Pass over to the base user.
+ e.destObject = this.user.netUser;
+}
+
+// Event to handle a client connecting to the listening socket.
+// CBSConnection points the event here.
+CIRCDCCFileTransfer.prototype.onSocketAccepted =
+function dfile_onSocketAccepted(socket, transport)
+{
+ this.state.getAccept();
+
+ this.connection.accept(transport, null);
+
+ this.state.socketConnected();
+
+ this.position = 0;
+ this.ackPosition = 0;
+ this.remoteIP = transport.host;
+
+ this.eventPump.addEvent(new CEvent("dcc-file", "connect",
+ this, "onConnect"));
+
+ try {
+ this.filestream = Components.classes["@mozilla.org/binaryinputstream;1"];
+ this.filestream = this.filestream.createInstance(nsIBinaryInputStream);
+ this.filestream.setInputStream(this.localFile.baseInputStream);
+
+ // Start the reading!
+ var d;
+ if (this.parent.sendChunk > this.size)
+ d = this.filestream.readBytes(this.size);
+ else
+ d = this.filestream.readBytes(this.parent.sendChunk);
+ this.position += d.length;
+ this.connection.sendData(d);
+ }
+ catch(ex)
+ {
+ dd(ex);
+ }
+
+ // Start the reading!
+ this.connection.startAsyncRead(this);
+}
+
+CIRCDCCFileTransfer.prototype.onStreamDataAvailable =
+function dfile_sda(request, inStream, sourceOffset, count)
+{
+ var ev = new CEvent("dcc-file", "data-available", this, "onDataAvailable");
+ ev.data = this.connection.readData(0, count);
+ this.eventPump.routeEvent(ev);
+}
+
+CIRCDCCFileTransfer.prototype.onStreamClose =
+function dfile_sockdiscon(status)
+{
+ if (this.position != this.size)
+ this.state.failed();
+ else
+ this.state.socketDisconnected();
+ this.dispose();
+}
+
+CIRCDCCFileTransfer.prototype.onDataAvailable =
+function dfile_dataavailable(e)
+{
+ e.type = "rawdata";
+ e.destMethod = "onRawData";
+
+ try
+ {
+ if (this.state.dir == DCC_DIR_SENDING)
+ {
+ while (e.data.length >= 4)
+ {
+ var word = e.data.substr(0, 4);
+ e.data = e.data.substr(4, e.data.length - 4);
+ var pos = word.charCodeAt(0) * 0x01000000
+ + word.charCodeAt(1) * 0x00010000
+ + word.charCodeAt(2) * 0x00000100
+ + word.charCodeAt(3) * 0x00000001;
+ this.ackPosition = pos;
+ }
+
+ while (this.position <= this.ackPosition + this.parent.maxUnAcked)
+ {
+ var d;
+ if (this.position + this.parent.sendChunk > this.size)
+ d = this.filestream.readBytes(this.size - this.position);
+ else
+ d = this.filestream.readBytes(this.parent.sendChunk);
+
+ this.position += d.length;
+ this.connection.sendData(d);
+
+ if (this.position >= this.size)
+ {
+ this.dispose();
+ break;
+ }
+ }
+ }
+ else if (this.state.dir == DCC_DIR_GETTING)
+ {
+ this.filestream.writeBytes(e.data, e.data.length);
+
+ // Send back ack data.
+ this.position += e.data.length;
+ var bytes = new Array();
+ for (var i = 0; i < 4; i++)
+ bytes.push(Math.floor(this.position / Math.pow(256, i)) & 255);
+
+ this.connection.sendData(String.fromCharCode(bytes[3], bytes[2],
+ bytes[1], bytes[0]));
+
+ if (this.size && (this.position >= this.size))
+ this.disconnect();
+ }
+ this.eventPump.addEvent(new CEvent("dcc-file", "progress",
+ this, "onProgress"));
+ }
+ catch(ex)
+ {
+ this.disconnect();
+ }
+
+ return true;
+}
+
+CIRCDCCFileTransfer.prototype.sendData =
+function dfile_say(data)
+{
+ this.connection.sendData(data);
+}
+
+CIRCDCCFileTransfer.prototype.size = 0;
+CIRCDCCFileTransfer.prototype.position = 0;
+CIRCDCCFileTransfer.prototype.__defineGetter__("progress",
+ function dfile_get_progress()
+ {
+ if (this.size > 0)
+ return Math.floor(100 * this.position / this.size);
+ return 0;
+ });
+
diff --git a/comm/suite/chatzilla/js/lib/events.js b/comm/suite/chatzilla/js/lib/events.js
new file mode 100644
index 0000000000..b48de11ede
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/events.js
@@ -0,0 +1,365 @@
+/* -*- 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/. */
+
+/**
+ * Event class for |CEventPump|.
+ */
+function CEvent (set, type, destObject, destMethod)
+{
+ this.set = set;
+ this.type = type;
+ this.destObject = destObject;
+ this.destMethod = destMethod;
+ this.hooks = new Array();
+
+}
+
+/**
+ * The event pump keeps a queue of pending events, processing them on-demand.
+ *
+ * You should never need to create an instance of this prototype; access the
+ * event pump through |client.eventPump|. Most code should only need to use the
+ * |addHook|, |getHook| and |removeHookByName| methods.
+ */
+function CEventPump (eventsPerStep)
+{
+ /* event routing stops after this many levels, safety valve */
+ this.MAX_EVENT_DEPTH = 50;
+ /* When there are this many 'used' items in a queue, always clean up. At
+ * this point it is MUCH more effecient to remove a block than a single
+ * item (i.e. removing 1000 is much much faster than removing 1 item 1000
+ * times [1]).
+ */
+ this.FORCE_CLEANUP_PTR = 1000;
+ /* If there are less than this many items in a queue, clean up. This keeps
+ * the queue empty normally, and is not that ineffecient [1].
+ */
+ this.MAX_AUTO_CLEANUP_LEN = 100;
+ this.eventsPerStep = eventsPerStep;
+ this.queue = new Array();
+ this.queuePointer = 0;
+ this.bulkQueue = new Array();
+ this.bulkQueuePointer = 0;
+ this.hooks = new Array();
+
+ /* [1] The delay when removing items from an array (with unshift or splice,
+ * and probably most operations) is NOT perportional to the number of items
+ * being removed, instead it is proportional to the number of items LEFT.
+ * Because of this, it is better to only remove small numbers of items when
+ * the queue is small (MAX_AUTO_CLEANUP_LEN), and when it is large remove
+ * only large chunks at a time (FORCE_CLEANUP_PTR), reducing the number of
+ * resizes being done.
+ */
+}
+
+CEventPump.prototype.onHook =
+function ep_hook(e, hooks)
+{
+ var h;
+
+ if (typeof hooks == "undefined")
+ hooks = this.hooks;
+
+ hook_loop:
+ for (h = hooks.length - 1; h >= 0; h--)
+ {
+ if (!hooks[h].enabled ||
+ !matchObject (e, hooks[h].pattern, hooks[h].neg))
+ continue hook_loop;
+
+ e.hooks.push(hooks[h]);
+ try
+ {
+ var rv = hooks[h].f(e);
+ }
+ catch(ex)
+ {
+ dd("hook #" + h + " '" +
+ ((typeof hooks[h].name != "undefined") ? hooks[h].name :
+ "") + "' had an error!");
+ dd(formatException(ex));
+ }
+ if ((typeof rv == "boolean") &&
+ (rv == false))
+ {
+ dd("hook #" + h + " '" +
+ ((typeof hooks[h].name != "undefined") ? hooks[h].name :
+ "") + "' stopped hook processing.");
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Adds an event hook to be called when matching events are processed.
+ *
+ * All hooks should be given a meaningful name, to aid removal and debugging.
+ * For plugins, an ideal technique for the name is to use |plugin.id| as a
+ * prefix (e.g. <tt>plugin.id + "-my-super-hook"</tt>).
+ *
+ * @param f The function to call when an event matches |pattern|.
+ * @param name A unique name for the hook. Used for removing the hook and
+ * debugging.
+ * @param neg Optional. If specified with a |true| value, the hook will be
+ * called for events *not* matching |pattern|. Otherwise, the hook
+ * will be called for events matching |pattern|.
+ * @param enabled Optional. If specified, sets the initial enabled/disabled
+ * state of the hook. By default, hooks are enabled. See
+ * |getHook|.
+ * @param hooks Internal. Do not use.
+ */
+CEventPump.prototype.addHook =
+function ep_addhook(pattern, f, name, neg, enabled, hooks)
+{
+ if (typeof hooks == "undefined")
+ hooks = this.hooks;
+
+ if (typeof f != "function")
+ return false;
+
+ if (typeof enabled == "undefined")
+ enabled = true;
+ else
+ enabled = Boolean(enabled);
+
+ neg = Boolean(neg);
+
+ var hook = {
+ pattern: pattern,
+ f: f,
+ name: name,
+ neg: neg,
+ enabled: enabled
+ };
+
+ hooks.push(hook);
+
+ return hook;
+
+}
+
+/**
+ * Finds and returns data about a named event hook.
+ *
+ * You can use |getHook| to change the enabled state of an existing event hook:
+ * <tt>client.eventPump.getHook(myHookName).enabled = false;</tt>
+ * <tt>client.eventPump.getHook(myHookName).enabled = true;</tt>
+ *
+ * @param name The unique hook name to find and return data about.
+ * @param hooks Internal. Do not use.
+ * @returns If a match is found, an |Object| with properties matching the
+ * arguments to |addHook| is returned. Otherwise, |null| is returned.
+ */
+CEventPump.prototype.getHook =
+function ep_gethook(name, hooks)
+{
+ if (typeof hooks == "undefined")
+ hooks = this.hooks;
+
+ for (var h in hooks)
+ if (hooks[h].name.toLowerCase() == name.toLowerCase())
+ return hooks[h];
+
+ return null;
+
+}
+
+/**
+ * Removes an existing event hook by its name.
+ *
+ * @param name The unique hook name to find and remove.
+ * @param hooks Internal. Do not use.
+ * @returns |true| if the hook was found and removed, |false| otherwise.
+ */
+CEventPump.prototype.removeHookByName =
+function ep_remhookname(name, hooks)
+{
+ if (typeof hooks == "undefined")
+ hooks = this.hooks;
+
+ for (var h in hooks)
+ if (hooks[h].name.toLowerCase() == name.toLowerCase())
+ {
+ arrayRemoveAt (hooks, h);
+ return true;
+ }
+
+ return false;
+
+}
+
+CEventPump.prototype.removeHookByIndex =
+function ep_remhooki(idx, hooks)
+{
+ if (typeof hooks == "undefined")
+ hooks = this.hooks;
+
+ return arrayRemoveAt (hooks, idx);
+
+}
+
+CEventPump.prototype.addEvent =
+function ep_addevent (e)
+{
+ e.queuedAt = new Date();
+ this.queue.push(e);
+ return true;
+}
+
+CEventPump.prototype.addBulkEvent =
+function ep_addevent (e)
+{
+ e.queuedAt = new Date();
+ this.bulkQueue.push(e);
+ return true;
+}
+
+CEventPump.prototype.routeEvent =
+function ep_routeevent (e)
+{
+ var count = 0;
+
+ this.currentEvent = e;
+
+ e.level = 0;
+ while (e.destObject)
+ {
+ e.level++;
+ this.onHook (e);
+ var destObject = e.destObject;
+ e.currentObject = destObject;
+ e.destObject = (void 0);
+
+ switch (typeof destObject[e.destMethod])
+ {
+ case "function":
+ if (1)
+ try
+ {
+ destObject[e.destMethod] (e);
+ }
+ catch (ex)
+ {
+ if (typeof ex == "string")
+ {
+ dd ("Error routing event " + e.set + "." +
+ e.type + ": " + ex);
+ }
+ else
+ {
+ dd ("Error routing event " + e.set + "." +
+ e.type + ": " + dumpObjectTree(ex) +
+ " in " + e.destMethod + "\n" + ex);
+ if ("stack" in ex)
+ dd(ex.stack);
+ }
+ }
+ else
+ destObject[e.destMethod] (e);
+
+ if (count++ > this.MAX_EVENT_DEPTH)
+ throw "Too many events in chain";
+ break;
+
+ case "undefined":
+ //dd ("** " + e.destMethod + " does not exist.");
+ break;
+
+ default:
+ dd ("** " + e.destMethod + " is not a function.");
+ }
+
+ if ((e.type != "event-end") && (!e.destObject))
+ {
+ e.lastSet = e.set;
+ e.set = "eventpump";
+ e.lastType = e.type;
+ e.type = "event-end";
+ e.destMethod = "onEventEnd";
+ e.destObject = this;
+ }
+
+ }
+
+ delete this.currentEvent;
+
+ return true;
+
+}
+
+CEventPump.prototype.stepEvents =
+function ep_stepevents()
+{
+ var i = 0;
+ var st, en, e;
+
+ st = new Date();
+ while (i < this.eventsPerStep)
+ {
+ if (this.queuePointer >= this.queue.length)
+ break;
+
+ e = this.queue[this.queuePointer++];
+
+ if (e.type == "yield")
+ break;
+
+ this.routeEvent(e);
+ i++;
+ }
+ while (i < this.eventsPerStep)
+ {
+ if (this.bulkQueuePointer >= this.bulkQueue.length)
+ break;
+
+ e = this.bulkQueue[this.bulkQueuePointer++];
+
+ if (e.type == "yield")
+ break;
+
+ this.routeEvent(e);
+ i++;
+ }
+ en = new Date();
+
+ // i == number of items handled this time.
+ // We only want to do this if we handled at least 25% of our step-limit
+ // and if we have a sane interval between st and en (not zero).
+ if ((i * 4 >= this.eventsPerStep) && (en - st > 0))
+ {
+ // Calculate the number of events that can be processed in 400ms.
+ var newVal = (400 * i) / (en - st);
+
+ // If anything skews it majorly, limit it to a minimum value.
+ if (newVal < 10)
+ newVal = 10;
+
+ // Adjust the step-limit based on this "target" limit, but only do a
+ // 25% change (underflow filter).
+ this.eventsPerStep += Math.round((newVal - this.eventsPerStep) / 4);
+ }
+
+ // Clean up if we've handled a lot, or the queue is small.
+ if ((this.queuePointer >= this.FORCE_CLEANUP_PTR) ||
+ (this.queue.length <= this.MAX_AUTO_CLEANUP_LEN))
+ {
+ this.queue.splice(0, this.queuePointer);
+ this.queuePointer = 0;
+ }
+
+ // Clean up if we've handled a lot, or the queue is small.
+ if ((this.bulkQueuePointer >= this.FORCE_CLEANUP_PTR) ||
+ (this.bulkQueue.length <= this.MAX_AUTO_CLEANUP_LEN))
+ {
+ this.bulkQueue.splice(0, this.bulkQueuePointer);
+ this.bulkQueuePointer = 0;
+ }
+
+ return i;
+
+}
diff --git a/comm/suite/chatzilla/js/lib/file-utils.js b/comm/suite/chatzilla/js/lib/file-utils.js
new file mode 100644
index 0000000000..d85edac8b6
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/file-utils.js
@@ -0,0 +1,430 @@
+/* -*- 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/. */
+
+/* notice that these valuse are octal. */
+const PERM_IRWXU = 0o700; /* read, write, execute/search by owner */
+const PERM_IRUSR = 0o400; /* read permission, owner */
+const PERM_IWUSR = 0o200; /* write permission, owner */
+const PERM_IXUSR = 0o100; /* execute/search permission, owner */
+const PERM_IRWXG = 0o070; /* read, write, execute/search by group */
+const PERM_IRGRP = 0o040; /* read permission, group */
+const PERM_IWGRP = 0o020; /* write permission, group */
+const PERM_IXGRP = 0o010; /* execute/search permission, group */
+const PERM_IRWXO = 0o007; /* read, write, execute/search by others */
+const PERM_IROTH = 0o004; /* read permission, others */
+const PERM_IWOTH = 0o002; /* write permission, others */
+const PERM_IXOTH = 0o001; /* execute/search permission, others */
+
+const MODE_RDONLY = 0x01;
+const MODE_WRONLY = 0x02;
+const MODE_RDWR = 0x04;
+const MODE_CREATE = 0x08;
+const MODE_APPEND = 0x10;
+const MODE_TRUNCATE = 0x20;
+const MODE_SYNC = 0x40;
+const MODE_EXCL = 0x80;
+
+const PICK_OK = Components.interfaces.nsIFilePicker.returnOK;
+const PICK_CANCEL = Components.interfaces.nsIFilePicker.returnCancel;
+const PICK_REPLACE = Components.interfaces.nsIFilePicker.returnReplace;
+
+const FILTER_ALL = Components.interfaces.nsIFilePicker.filterAll;
+const FILTER_HTML = Components.interfaces.nsIFilePicker.filterHTML;
+const FILTER_TEXT = Components.interfaces.nsIFilePicker.filterText;
+const FILTER_IMAGES = Components.interfaces.nsIFilePicker.filterImages;
+const FILTER_XML = Components.interfaces.nsIFilePicker.filterXML;
+const FILTER_XUL = Components.interfaces.nsIFilePicker.filterXUL;
+
+const FTYPE_DIR = Components.interfaces.nsIFile.DIRECTORY_TYPE;
+const FTYPE_FILE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE;
+
+// evald f = fopen("/home/rginda/foo.txt", MODE_WRONLY | MODE_CREATE)
+// evald f = fopen("/home/rginda/vnk.txt", MODE_RDONLY)
+
+var futils = new Object();
+
+futils.umask = PERM_IWOTH | PERM_IWGRP;
+futils.MSG_SAVE_AS = "Save As";
+futils.MSG_OPEN = "Open";
+
+/**
+ * Internal function used by |pickSaveAs|, |pickOpen| and |pickGetFolder|.
+ *
+ * @param initialPath (*defaultDir* in |pick| functions) Sets the
+ * initial directory for the dialog. The user may browse
+ * to any other directory - it does not restrict anything.
+ * @param typeList Optional. An |Array| or space-separated string of allowed
+ * file types for the dialog. An item in the array may be a
+ * string (used as title and filter) or a two-element array
+ * (title and filter, respectively); when using a string,
+ * the following standard filters may be used: |$all|, |$html|,
+ * |$text|, |$images|, |$xml|, |$xul|, |$noAll| (prevents "All
+ * Files" filter being included).
+ * @param attribs Optional. Takes an object with either or both of the
+ * properties: |defaultString| (*defaultFile* in |pick|
+ * functions) sets the initial/default filename, and
+ * |defaultExtension| XXX FIXME (this seems wrong?) XXX.
+ * @returns An |Object| with |ok| (Boolean), |file| (|nsIFile|) and
+ * |picker| (|nsIFilePicker|) properties.
+ */
+futils.getPicker =
+function futils_nosepicker(initialPath, typeList, attribs)
+{
+ const classes = Components.classes;
+ const interfaces = Components.interfaces;
+
+ const PICKER_CTRID = "@mozilla.org/filepicker;1";
+ const LOCALFILE_CTRID = "@mozilla.org/file/local;1";
+
+ const nsIFilePicker = interfaces.nsIFilePicker;
+ const nsIFile = interfaces.nsIFile;
+
+ var picker = classes[PICKER_CTRID].createInstance(nsIFilePicker);
+ if (attribs)
+ {
+ if (typeof attribs == "object")
+ {
+ for (var a in attribs)
+ picker[a] = attribs[a];
+ }
+ else
+ {
+ throw "bad type for param |attribs|";
+ }
+ }
+
+ if (initialPath)
+ {
+ var localFile;
+
+ if (typeof initialPath == "string")
+ {
+ localFile =
+ classes[LOCALFILE_CTRID].createInstance(nsIFile);
+ localFile.initWithPath(initialPath);
+ }
+ else
+ {
+ if (!isinstance(initialPath, nsIFile))
+ throw "bad type for argument |initialPath|";
+
+ localFile = initialPath;
+ }
+
+ picker.displayDirectory = localFile
+ }
+
+ var allIncluded = false;
+
+ if (typeof typeList == "string")
+ typeList = typeList.split(" ");
+
+ if (isinstance(typeList, Array))
+ {
+ for (var i in typeList)
+ {
+ switch (typeList[i])
+ {
+ case "$all":
+ allIncluded = true;
+ picker.appendFilters(FILTER_ALL);
+ break;
+
+ case "$html":
+ picker.appendFilters(FILTER_HTML);
+ break;
+
+ case "$text":
+ picker.appendFilters(FILTER_TEXT);
+ break;
+
+ case "$images":
+ picker.appendFilters(FILTER_IMAGES);
+ break;
+
+ case "$xml":
+ picker.appendFilters(FILTER_XML);
+ break;
+
+ case "$xul":
+ picker.appendFilters(FILTER_XUL);
+ break;
+
+ case "$noAll":
+ // This prevents the automatic addition of "All Files"
+ // as a file type option by pretending it is already there.
+ allIncluded = true;
+ break;
+
+ default:
+ if ((typeof typeList[i] == "object") && isinstance(typeList[i], Array))
+ picker.appendFilter(typeList[i][0], typeList[i][1]);
+ else
+ picker.appendFilter(typeList[i], typeList[i]);
+ break;
+ }
+ }
+ }
+
+ if (!allIncluded)
+ picker.appendFilters(FILTER_ALL);
+
+ return picker;
+}
+
+function getPickerChoice(picker)
+{
+ var obj = new Object();
+ obj.picker = picker;
+ obj.ok = false;
+ obj.file = null;
+
+ try
+ {
+ obj.reason = picker.show();
+ }
+ catch (ex)
+ {
+ dd ("caught exception from file picker: " + ex);
+ return obj;
+ }
+
+ if (obj.reason != PICK_CANCEL)
+ {
+ obj.file = picker.file;
+ obj.ok = true;
+ }
+
+ return obj;
+}
+
+/**
+ * Displays a standard file save dialog.
+ *
+ * @param title Optional. The title for the dialog.
+ * @param typeList Optional. See |futils.getPicker| for details.
+ * @param defaultFile Optional. See |futils.getPicker| for details.
+ * @param defaultDir Optional. See |futils.getPicker| for details.
+ * @param defaultExt Optional. See |futils.getPicker| for details.
+ * @returns An |Object| with "ok" (Boolean), "file" (|nsIFile|) and
+ * "picker" (|nsIFilePicker|) properties.
+ */
+function pickSaveAs (title, typeList, defaultFile, defaultDir, defaultExt)
+{
+ if (!defaultDir && "lastSaveAsDir" in futils)
+ defaultDir = futils.lastSaveAsDir;
+
+ var picker = futils.getPicker (defaultDir, typeList,
+ {defaultString: defaultFile,
+ defaultExtension: defaultExt});
+ picker.init (window, title ? title : futils.MSG_SAVE_AS,
+ Components.interfaces.nsIFilePicker.modeSave);
+
+ var rv = getPickerChoice(picker);
+ if (rv.ok)
+ futils.lastSaveAsDir = picker.file.parent;
+
+ return rv;
+}
+
+/**
+ * Displays a standard file open dialog.
+ *
+ * @param title Optional. The title for the dialog.
+ * @param typeList Optional. See |futils.getPicker| for details.
+ * @param defaultFile Optional. See |futils.getPicker| for details.
+ * @param defaultDir Optional. See |futils.getPicker| for details.
+ * @returns An |Object| with "ok" (Boolean), "file" (|nsIFile|) and
+ * "picker" (|nsIFilePicker|) properties.
+ */
+function pickOpen (title, typeList, defaultFile, defaultDir)
+{
+ if (!defaultDir && "lastOpenDir" in futils)
+ defaultDir = futils.lastOpenDir;
+
+ var picker = futils.getPicker (defaultDir, typeList,
+ {defaultString: defaultFile});
+ picker.init (window, title ? title : futils.MSG_OPEN,
+ Components.interfaces.nsIFilePicker.modeOpen);
+
+ var rv = getPickerChoice(picker);
+ if (rv.ok)
+ futils.lastOpenDir = picker.file.parent;
+
+ return rv;
+}
+
+/**
+ * Displays a standard directory selection dialog.
+ *
+ * @param title Optional. The title for the dialog.
+ * @param defaultDir Optional. See |futils.getPicker| for details.
+ * @returns An |Object| with "ok" (Boolean), "file" (|nsIFile|) and
+ * "picker" (|nsIFilePicker|) properties.
+ */
+function pickGetFolder(title, defaultDir)
+{
+ if (!defaultDir && "lastOpenDir" in futils)
+ defaultDir = futils.lastOpenDir;
+
+ var picker = futils.getPicker(defaultDir);
+ picker.init(window, title ? title : futils.MSG_OPEN,
+ Components.interfaces.nsIFilePicker.modeGetFolder);
+
+ var rv = getPickerChoice(picker);
+ if (rv.ok)
+ futils.lastOpenDir = picker.file;
+
+ return rv;
+}
+
+function mkdir (localFile, perms)
+{
+ if (typeof perms == "undefined")
+ perms = 0o766 & ~futils.umask;
+
+ localFile.create(FTYPE_DIR, perms);
+}
+
+function getTempFile(path, name)
+{
+ var tempFile = new nsLocalFile(path);
+ tempFile.append(name);
+ tempFile.createUnique(0, 0o600);
+ return tempFile;
+}
+
+function nsLocalFile(path)
+{
+ const LOCALFILE_CTRID = "@mozilla.org/file/local;1";
+ const nsIFile = Components.interfaces.nsIFile;
+
+ var localFile =
+ Components.classes[LOCALFILE_CTRID].createInstance(nsIFile);
+ localFile.initWithPath(path);
+ return localFile;
+}
+
+function fopen (path, mode, perms, tmp)
+{
+ return new LocalFile(path, mode, perms, tmp);
+}
+
+function LocalFile(file, mode, perms, tmp)
+{
+ const classes = Components.classes;
+ const interfaces = Components.interfaces;
+
+ const LOCALFILE_CTRID = "@mozilla.org/file/local;1";
+ const FILEIN_CTRID = "@mozilla.org/network/file-input-stream;1";
+ const FILEOUT_CTRID = "@mozilla.org/network/file-output-stream;1";
+ const SCRIPTSTREAM_CTRID = "@mozilla.org/scriptableinputstream;1";
+
+ const nsIFile = interfaces.nsIFile;
+ const nsIFileOutputStream = interfaces.nsIFileOutputStream;
+ const nsIFileInputStream = interfaces.nsIFileInputStream;
+ const nsIScriptableInputStream = interfaces.nsIScriptableInputStream;
+
+ if (typeof perms == "undefined")
+ perms = 0o666 & ~futils.umask;
+
+ if (typeof mode == "string")
+ {
+ switch (mode)
+ {
+ case ">":
+ mode = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
+ break;
+ case ">>":
+ mode = MODE_WRONLY | MODE_CREATE | MODE_APPEND;
+ break;
+ case "<":
+ mode = MODE_RDONLY;
+ break;
+ default:
+ throw "Invalid mode ``" + mode + "''";
+ }
+ }
+
+ if (typeof file == "string")
+ {
+ this.localFile = new nsLocalFile(file);
+ }
+ else if (isinstance(file, nsIFile))
+ {
+ this.localFile = file;
+ }
+ else
+ {
+ throw "bad type for argument |file|.";
+ }
+
+ this.path = this.localFile.path;
+
+ if (mode & (MODE_WRONLY | MODE_RDWR))
+ {
+ this.outputStream =
+ classes[FILEOUT_CTRID].createInstance(nsIFileOutputStream);
+ this.outputStream.init(this.localFile, mode, perms, 0);
+ }
+
+ if (mode & (MODE_RDONLY | MODE_RDWR))
+ {
+ this.baseInputStream =
+ classes[FILEIN_CTRID].createInstance(nsIFileInputStream);
+ this.baseInputStream.init(this.localFile, mode, perms, tmp);
+ this.inputStream =
+ classes[SCRIPTSTREAM_CTRID].createInstance(nsIScriptableInputStream);
+ this.inputStream.init(this.baseInputStream);
+ }
+}
+
+LocalFile.prototype.write =
+function fo_write(buf)
+{
+ if (!("outputStream" in this))
+ throw "file not open for writing.";
+
+ return this.outputStream.write(buf, buf.length);
+}
+
+// Will return null if there is no more data in the file.
+// Will block until it has some data to return.
+// Will return an empty string if there is data, but it couldn't be read.
+LocalFile.prototype.read =
+function fo_read(max)
+{
+ if (!("inputStream" in this))
+ throw "file not open for reading.";
+
+ if (typeof max == "undefined")
+ max = this.inputStream.available();
+
+ try
+ {
+ var rv = this.inputStream.read(max);
+ return (rv != "") ? rv : null;
+ }
+ catch (ex)
+ {
+ return "";
+ }
+}
+
+LocalFile.prototype.close =
+function fo_close()
+{
+ if ("outputStream" in this)
+ this.outputStream.close();
+ if ("inputStream" in this)
+ this.inputStream.close();
+}
+
+LocalFile.prototype.flush =
+function fo_close()
+{
+ return this.outputStream.flush();
+}
diff --git a/comm/suite/chatzilla/js/lib/http.js b/comm/suite/chatzilla/js/lib/http.js
new file mode 100644
index 0000000000..45ec3ddbbc
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/http.js
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+function CHTTPDoc (server, path)
+{
+
+ this.GET_TIMEOUT = 2 * 60; /* 2 minute timeout on gets */
+ this.state = "new";
+ this.server = server;
+ this.path = path;
+
+}
+
+CHTTPDoc.prototype.get =
+function http_get (ep)
+{
+
+ this.connection = new CBSConnection();
+ if (!this.connection.connect (this.server, 80, (void 0), true))
+ {
+ this.state = "connect-error";
+ var e = new CEvent ("httpdoc", "complete", this, "onComplete");
+ this.eventPump.addEvent (e);
+ return false;
+ }
+
+ this.eventPump = ep;
+ this.state = "opened";
+ this.data = "";
+ this.duration = 0;
+
+ var e = new CEvent ("httpdoc", "poll", this, "onPoll");
+ this.eventPump.addEvent (e);
+
+ this.connection.sendData ("GET " + this.path + "\n");
+ this.startDate = new Date();
+
+ return true;
+
+}
+
+CHTTPDoc.prototype.onPoll =
+function http_poll (e)
+{
+ var line = "";
+ var ex, c;
+ var need_more = false;
+
+ if (this.duration < this.GET_TIMEOUT)
+ try
+ {
+ line = this.connection.readData (50);
+ need_more = true;
+ }
+ catch (ex)
+ {
+ if (typeof (ex) == "undefined")
+ {
+ line = "";
+ need_more = true;
+ }
+ else
+ {
+ dd ("** Caught exception: '" + ex + "' while receiving " +
+ this.server + this.path);
+ this.state = "read-error";
+ }
+ }
+ else
+ this.state = "receive-timeout";
+
+ switch (this.state)
+ {
+ case "opened":
+ case "receive-header":
+ if (this.state == "opened")
+ this.headers = "";
+
+ c = line.search(/\<html\>/i);
+ if (c != -1)
+ {
+ this.data = line.substr (c, line.length);
+ this.state = "receive-data";
+ line = line.substr (0, c);
+
+ c = this.data.search(/\<\/html\>/i);
+ if (c != -1)
+ {
+ this.docType = stringTrim(this.docType);
+ this.state = "complete";
+ need_more = false;
+ }
+
+ }
+
+ this.headers += line;
+ c = this.headers.search (/\<\!doctype/i);
+ if (c != -1)
+ {
+ this.docType = this.headers.substr (c, line.length);
+ if (this.state == "opened")
+ this.state = "receive-doctype";
+ this.headers = this.headers.substr (0, c);
+ }
+
+ if (this.state == "opened")
+ this.state = "receive-header";
+ break;
+
+ case "receive-doctype":
+ var c = line.search (/\<html\>/i);
+ if (c != -1)
+ {
+ this.docType = stringTrim(this.docType);
+ this.data = line.substr (c, line.length);
+ this.state = "receive-data";
+ line = line.substr (0, c);
+ }
+
+ this.docType += line;
+ break;
+
+ case "receive-data":
+ this.data += line;
+ var c = this.data.search(/\<\/html\>/i);
+ if (c != -1)
+ this.state = "complete";
+ break;
+
+ case "read-error":
+ case "receive-timeout":
+ break;
+
+ default:
+ dd ("** INVALID STATE in HTTPDoc object (" + this.state + ") **");
+ need_more = false;
+ this.state = "error";
+ break;
+
+ }
+
+ if ((this.state != "complete") && (need_more))
+ var e = new CEvent ("httpdoc", "poll", this, "onPoll");
+ else
+ {
+ this.connection.disconnect();
+ if (this.data)
+ {
+ var m = this.data.match(/\<title\>(.*)\<\/title\>/i);
+ if (m != null)
+ this.title = m[1];
+ else
+ this.title = "";
+ }
+ var e = new CEvent ("httpdoc", "complete", this, "onComplete");
+ }
+
+ this.eventPump.addEvent (e);
+ this.duration = (new Date() - this.startDate) / 1000;
+
+ return true;
+
+}
+
+CHTTPDoc.prototype.onComplete =
+function http_complete(e)
+{
+
+ return true;
+
+}
+
+
+
diff --git a/comm/suite/chatzilla/js/lib/ident.js b/comm/suite/chatzilla/js/lib/ident.js
new file mode 100644
index 0000000000..5110d70165
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/ident.js
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+// RFC1413-ish identification server for ChatZilla
+// One Ident Server is used for all networks. When multiple networks are in the
+// process of connecting, it won't stop listening until they're all done.
+
+function IdentServer(parent)
+{
+ this.responses = new Array();
+ this.listening = false;
+ this.dns = getService("@mozilla.org/network/dns-service;1","nsIDNSService");
+
+ this.parent = parent;
+ this.eventPump = parent.eventPump;
+}
+
+IdentServer.prototype.start =
+function ident_start()
+{
+ if (this.listening)
+ return true;
+
+ if (!this.socket)
+ this.socket = new CBSConnection();
+
+ if (!this.socket.listen(113, this))
+ return false;
+
+ this.listening = true;
+ return true;
+}
+
+IdentServer.prototype.stop =
+function ident_stop()
+{
+ if (!this.socket || !this.listening)
+ return;
+
+ // No need to destroy socket, listen() will work again.
+ this.socket.close();
+ this.listening = false;
+}
+
+IdentServer.prototype.addNetwork =
+function ident_add(net, serv)
+{
+ var addr, dnsRecord = this.dns.resolve(serv.hostname, 0);
+
+ while (dnsRecord.hasMore())
+ {
+ addr = dnsRecord.getNextAddrAsString();
+ this.responses.push({net: net, ip: addr, port: serv.port,
+ username: net.INITIAL_NAME || net.INITIAL_NICK});
+ }
+
+ if (this.responses.length == 0)
+ return false;
+
+ // Start the ident server if necessary.
+ if (!this.listening)
+ return this.start();
+
+ return true;
+}
+
+IdentServer.prototype.removeNetwork =
+function ident_remove(net)
+{
+ var newResponses = new Array();
+
+ for (var i = 0; i < this.responses.length; ++i)
+ {
+ if (this.responses[i].net != net)
+ newResponses.push(this.responses[i]);
+ }
+ this.responses = newResponses;
+
+ // Only stop listening if responses is empty - no networks need us anymore.
+ if (this.responses.length == 0)
+ this.stop();
+}
+
+IdentServer.prototype.onSocketAccepted =
+function ident_gotconn(serv, transport)
+{
+ // Using the listening CBSConnection to accept would stop it listening.
+ // A new CBSConnection does exactly what we want.
+ var connection = new CBSConnection();
+ connection.accept(transport);
+
+ connection.startAsyncRead(new IdentListener(this, connection));
+}
+
+function IdentListener(server, connection)
+{
+ this.server = server;
+ this.connection = connection;
+}
+
+IdentListener.prototype.onStreamDataAvailable =
+function ident_listener_sda(request, inStream, sourceOffset, count)
+{
+ var ev = new CEvent("ident-listener", "data-available", this,
+ "onDataAvailable");
+ ev.line = this.connection.readData(0, count);
+ this.server.eventPump.routeEvent(ev);
+}
+
+IdentListener.prototype.onStreamClose =
+function ident_listener_sclose(status)
+{
+}
+
+IdentListener.prototype.onDataAvailable =
+function ident_listener_dataavailable(e)
+{
+ var incomplete = (e.line.substr(-2) != "\r\n");
+ var lines = e.line.split(/\r\n/);
+
+ if (this.savedLine)
+ {
+ lines[0] = this.savedLine + lines[0];
+ this.savedLine = "";
+ }
+
+ if (incomplete)
+ this.savedLine = lines.pop()
+
+ for (var i in lines)
+ {
+ var ev = new CEvent("ident-listener", "rawdata", this, "onRawData");
+ ev.line = lines[i];
+ this.server.eventPump.routeEvent(ev);
+ }
+}
+
+IdentListener.prototype.onRawData =
+function ident_listener_rawdata(e)
+{
+ var ports = e.line.match(/(\d+) *, *(\d+)/);
+ // <port-on-server> , <port-on-client>
+ // (where "server" is the ident server)
+
+ if (!ports)
+ {
+ this.connection.disconnect(); // same meaning as "ERROR : UNKNOWN-ERROR"
+ return;
+ }
+
+ e.type = "parsedrequest";
+ e.destObject = this;
+ e.destMethod = "onParsedRequest";
+ e.localPort = ports[1];
+ e.remotePort = ports[2];
+}
+
+IdentListener.prototype.onParsedRequest =
+function ident_listener_request(e)
+{
+ function response(str)
+ {
+ return e.localPort + " , " + e.remotePort + " : " + str + "\r\n";
+ };
+
+ function validPort(p)
+ {
+ return (p >= 1) && (p <= 65535);
+ };
+
+ if (!validPort(e.localPort) || !validPort(e.remotePort))
+ {
+ this.connection.sendData(response("ERROR : INVALID-PORT"));
+ this.connection.disconnect();
+ return;
+ }
+
+ var found, responses = this.server.responses;
+ for (var i = 0; i < responses.length; ++i)
+ {
+ if ((e.remotePort == responses[i].port) &&
+ (this.connection._transport.host == responses[i].ip))
+ {
+ // charset defaults to US-ASCII
+ // anything except an OS username should use OTHER
+ // however, ircu sucks, so we can't do that.
+ this.connection.sendData(response("USERID : CHATZILLA :" +
+ responses[i].username));
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ this.connection.sendData(response("ERROR : NO-USER"));
+
+ // Spec gives us a choice: drop the connection, or listen for more queries.
+ // Since IRC servers will only ever want one name, we disconnect.
+ this.connection.disconnect();
+}
diff --git a/comm/suite/chatzilla/js/lib/irc-debug.js b/comm/suite/chatzilla/js/lib/irc-debug.js
new file mode 100644
index 0000000000..b0e95fee2e
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/irc-debug.js
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+/*
+ * Hook used to trace events.
+ */
+function event_tracer (e)
+{
+ var name = "";
+ var data = ("debug" in e) ? e.debug : "";
+
+ switch (e.set)
+ {
+ case "server":
+ name = e.destObject.connection.host;
+ if (e.type == "rawdata")
+ data = "'" + e.data + "'";
+ if (e.type == "senddata")
+ {
+ var nextLine =
+ e.destObject.sendQueue[e.destObject.sendQueue.length - 1];
+ if ("logged" in nextLine)
+ return true; /* don't print again */
+
+ if (nextLine) {
+ data = "'" + nextLine.replace ("\n", "\\n") + "'";
+ nextLine.logged = true;
+ }
+ else
+ data = "!!! Nothing to send !!!";
+ }
+ break;
+
+ case "network":
+ case "channel":
+ case "user":
+ name = e.destObject.unicodeName;
+ break;
+
+ case "httpdoc":
+ name = e.destObject.server + e.destObject.path;
+ if (e.destObject.state != "complete")
+ data = "state: '" + e.destObject.state + "', received " +
+ e.destObject.data.length;
+ else
+ dd ("document done:\n" + dumpObjectTree (this));
+ break;
+
+ case "dcc-chat":
+ case "dcc-file":
+ name = e.destObject.localIP + ":" + e.destObject.port;
+ if (e.type == "rawdata")
+ data = "'" + e.data + "'";
+ break;
+
+ case "client":
+ if (e.type == "do-connect")
+ data = "attempt: " + e.attempt + "/" +
+ e.destObject.MAX_CONNECT_ATTEMPTS;
+ break;
+
+ default:
+ break;
+ }
+
+ if (name)
+ name = "[" + name + "]";
+
+ if (e.type == "info")
+ data = "'" + e.msg + "'";
+
+ var str = "Level " + e.level + ": '" + e.type + "', " +
+ e.set + name + "." + e.destMethod;
+ if (data)
+ str += "\ndata : " + data;
+
+ dd (str);
+
+ return true;
+
+}
diff --git a/comm/suite/chatzilla/js/lib/irc.js b/comm/suite/chatzilla/js/lib/irc.js
new file mode 100644
index 0000000000..5e7d210c44
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/irc.js
@@ -0,0 +1,4518 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+const JSIRC_ERR_NO_SOCKET = "JSIRCE:NS";
+const JSIRC_ERR_EXHAUSTED = "JSIRCE:E";
+const JSIRC_ERR_CANCELLED = "JSIRCE:C";
+const JSIRC_ERR_NO_SECURE = "JSIRCE:NO_SECURE";
+const JSIRC_ERR_OFFLINE = "JSIRCE:OFFLINE";
+const JSIRC_ERR_PAC_LOADING = "JSIRCE:PAC_LOADING";
+
+const JSIRCV3_SUPPORTED_CAPS = [
+ "account-notify",
+ "account-tag",
+ "away-notify",
+ "batch",
+ "cap-notify",
+ "chghost",
+ "echo-message",
+ "extended-join",
+ "invite-notify",
+ //"labeled-response",
+ "message-tags",
+ //"metadata",
+ "multi-prefix",
+ "sasl",
+ "server-time",
+ "tls",
+ "userhost-in-names",
+];
+
+function userIsMe (user)
+{
+
+ switch (user.TYPE)
+ {
+ case "IRCUser":
+ return (user == user.parent.me);
+ break;
+
+ case "IRCChanUser":
+ return (user.__proto__ == user.parent.parent.me);
+ break;
+
+ default:
+ return false;
+
+ }
+
+ return false;
+}
+
+/*
+ * Attached to event objects in onRawData
+ */
+function decodeParam(number, charsetOrObject)
+{
+ if (!charsetOrObject)
+ charsetOrObject = this.currentObject;
+
+ var rv = toUnicode(this.params[number], charsetOrObject);
+
+ return rv;
+}
+
+// JavaScript won't let you delete things declared with "var", workaround:
+window.i = 1;
+
+const NET_OFFLINE = i++; // Initial, disconected.
+const NET_WAITING = i++; // Waiting before trying.
+const NET_CONNECTING = i++; // Trying a connect...
+const NET_CANCELLING = i++; // Cancelling connect.
+const NET_ONLINE = i++; // Connected ok.
+const NET_DISCONNECTING = i++; // Disconnecting.
+
+delete window.i;
+
+function CIRCNetwork (name, serverList, eventPump, temporary)
+{
+ this.unicodeName = name;
+ this.viewName = name;
+ this.canonicalName = name;
+ this.collectionKey = ":" + name;
+ this.encodedName = name;
+ this.servers = new Object();
+ this.serverList = new Array();
+ this.ignoreList = new Object();
+ this.ignoreMaskCache = new Object();
+ this.state = NET_OFFLINE;
+ this.temporary = Boolean(temporary);
+
+ for (var i = 0; i < serverList.length; ++i)
+ {
+ var server = serverList[i];
+ var password = ("password" in server) ? server.password : null;
+ var isSecure = ("isSecure" in server) ? server.isSecure : false;
+ this.serverList.push(new CIRCServer(this, server.name, server.port, isSecure,
+ password));
+ }
+
+ this.eventPump = eventPump;
+ if ("onInit" in this)
+ this.onInit();
+}
+
+/** Clients should override this stuff themselves **/
+CIRCNetwork.prototype.INITIAL_NICK = "js-irc";
+CIRCNetwork.prototype.INITIAL_NAME = "INITIAL_NAME";
+CIRCNetwork.prototype.INITIAL_DESC = "INITIAL_DESC";
+CIRCNetwork.prototype.USE_SASL = false;
+CIRCNetwork.prototype.UPGRADE_INSECURE = false;
+CIRCNetwork.prototype.STS_MODULE = null;
+/* set INITIAL_CHANNEL to "" if you don't want a primary channel */
+CIRCNetwork.prototype.INITIAL_CHANNEL = "#jsbot";
+CIRCNetwork.prototype.INITIAL_UMODE = "+iw";
+
+CIRCNetwork.prototype.MAX_CONNECT_ATTEMPTS = 5;
+CIRCNetwork.prototype.PAC_RECONNECT_DELAY = 5 * 1000;
+CIRCNetwork.prototype.getReconnectDelayMs = function() { return 15000; }
+CIRCNetwork.prototype.stayingPower = false;
+
+// "http" = use HTTP proxy, "none" = none, anything else = auto.
+CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE = "";
+
+CIRCNetwork.prototype.TYPE = "IRCNetwork";
+
+/**
+ * Returns the IRC URL representation of this network.
+ *
+ * @param target A network-specific object to target the URL at. Instead of
+ * passing it in here, call the target's |getURL| method.
+ * @param flags An |Object| with flags (as properties) to be applied to the URL.
+ */
+CIRCNetwork.prototype.getURL =
+function net_geturl(target, flags)
+{
+ if (this.temporary)
+ return this.serverList[0].getURL(target, flags);
+
+ /* Determine whether to use the irc:// or ircs:// scheme */
+ var scheme = "irc";
+ if ((("primServ" in this) && this.primServ.isConnected &&
+ this.primServ.isSecure) ||
+ this.hasOnlySecureServers())
+ {
+ scheme = "ircs"
+ }
+
+ var obj = {host: this.unicodeName, scheme: scheme};
+
+ if (target)
+ obj.target = target;
+
+ if (flags)
+ {
+ for (var i = 0; i < flags.length; i++)
+ obj[flags[i]] = true;
+ }
+
+ return constructIRCURL(obj);
+}
+
+CIRCNetwork.prototype.getUser =
+function net_getuser (nick)
+{
+ if ("primServ" in this && this.primServ)
+ return this.primServ.getUser(nick);
+
+ return null;
+}
+
+CIRCNetwork.prototype.addServer =
+function net_addsrv(host, port, isSecure, password)
+{
+ this.serverList.push(new CIRCServer(this, host, port, isSecure, password));
+}
+
+/**
+ * Returns |true| iif a network has a secure server in its list.
+ */
+CIRCNetwork.prototype.hasSecureServer =
+function net_hasSecure()
+{
+ for (var i = 0; i < this.serverList.length; i++)
+ {
+ if (this.serverList[i].isSecure)
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Returns |true| iif a network only has secure servers in its list.
+ */
+CIRCNetwork.prototype.hasOnlySecureServers =
+function net_hasOnlySecure()
+{
+ for (var i = 0; i < this.serverList.length; i++)
+ {
+ if (!this.serverList[i].isSecure)
+ return false;
+ }
+
+ return true;
+}
+
+CIRCNetwork.prototype.clearServerList =
+function net_clearserverlist()
+{
+ /* Note: we don't have to worry about being connected, since primServ
+ * keeps the currently connected server alive if we still need it.
+ */
+ this.servers = new Object();
+ this.serverList = new Array();
+}
+
+/**
+ * Trigger an |onDoConnect| event after a delay.
+ */
+CIRCNetwork.prototype.delayedConnect =
+function net_delayedConnect(eventProperties)
+{
+ function reconnectFn(network, eventProperties)
+ {
+ network.immediateConnect(eventProperties);
+ };
+
+ if ((-1 != this.MAX_CONNECT_ATTEMPTS) &&
+ (this.connectAttempt >= this.MAX_CONNECT_ATTEMPTS))
+ {
+ this.state = NET_OFFLINE;
+
+ var ev = new CEvent("network", "error", this, "onError");
+ ev.debug = "Connection attempts exhausted, giving up.";
+ ev.errorCode = JSIRC_ERR_EXHAUSTED;
+ this.eventPump.addEvent(ev);
+
+ return;
+ }
+
+ this.state = NET_WAITING;
+ this.reconnectTimer = setTimeout(reconnectFn,
+ this.getReconnectDelayMs(),
+ this,
+ eventProperties);
+}
+
+/**
+ * Immediately trigger an |onDoConnect| event. Use |delayedConnect| for automatic
+ * repeat attempts, instead, to throttle the attempts to a reasonable pace.
+ */
+CIRCNetwork.prototype.immediateConnect =
+function net_immediateConnect(eventProperties)
+{
+ var ev = new CEvent("network", "do-connect", this, "onDoConnect");
+
+ if (typeof eventProperties != "undefined")
+ for (var key in eventProperties)
+ ev[key] = eventProperties[key];
+
+ this.eventPump.addEvent(ev);
+}
+
+CIRCNetwork.prototype.connect =
+function net_connect(requireSecurity)
+{
+ if ("primServ" in this && this.primServ.isConnected)
+ return true;
+
+ // We need to test for secure servers in the network object here,
+ // because without them all connection attempts will fail anyway.
+ if (requireSecurity && !this.hasSecureServer())
+ {
+ // No secure server, cope.
+ ev = new CEvent ("network", "error", this, "onError");
+ ev.server = this;
+ ev.debug = "No connection attempted: no secure servers in list";
+ ev.errorCode = JSIRC_ERR_NO_SECURE;
+ this.eventPump.addEvent(ev);
+
+ return false;
+ }
+
+ this.state = NET_CONNECTING;
+ this.connectAttempt = 0; // actual connection attempts
+ this.connectCandidate = 0; // incl. requireSecurity non-attempts
+ this.nextHost = 0;
+ this.requireSecurity = requireSecurity || false;
+ this.immediateConnect({"password": null});
+ return true;
+}
+
+/**
+ * Disconnects the network with a given reason.
+ */
+CIRCNetwork.prototype.quit =
+function net_quit (reason)
+{
+ if (this.isConnected())
+ this.primServ.logout(reason);
+}
+
+/**
+ * Cancels the network's connection (whatever its current state).
+ */
+CIRCNetwork.prototype.cancel =
+function net_cancel()
+{
+ // We're online, pull the plug on the current connection, or...
+ if (this.state == NET_ONLINE)
+ {
+ this.quit();
+ }
+ // We're waiting for the 001, too late to throw a reconnect, or...
+ else if (this.state == NET_CONNECTING)
+ {
+ this.state = NET_CANCELLING;
+ if ("primServ" in this && this.primServ.isConnected)
+ {
+ this.primServ.connection.disconnect();
+
+ var ev = new CEvent("network", "error", this, "onError");
+ ev.server = this.primServ;
+ ev.debug = "Connect sequence was canceled.";
+ ev.errorCode = JSIRC_ERR_CANCELLED;
+ this.eventPump.addEvent(ev);
+ }
+ }
+ // We're waiting for onDoConnect, so try a reconnect (which will fail us)
+ else if (this.state == NET_WAITING)
+ {
+ this.state = NET_CANCELLING;
+ // onDoConnect will throw the error events for us, as it will fail
+ this.immediateConnect();
+ }
+ else
+ {
+ dd("Network cancel in odd state: " + this.state);
+ }
+}
+
+CIRCNetwork.prototype.onDoConnect =
+function net_doconnect(e)
+{
+ const NS_ERROR_OFFLINE = 0x804b0010;
+ var c;
+
+ // Clear the timer, if there is one.
+ if ("reconnectTimer" in this)
+ {
+ clearTimeout(this.reconnectTimer);
+ delete this.reconnectTimer;
+ }
+
+ var ev;
+
+ if (this.state == NET_CANCELLING)
+ {
+ if ("primServ" in this && this.primServ.isConnected)
+ this.primServ.connection.disconnect();
+ else
+ this.state = NET_OFFLINE;
+
+ ev = new CEvent("network", "error", this, "onError");
+ ev.server = this.primServ;
+ ev.debug = "Connect sequence was canceled.";
+ ev.errorCode = JSIRC_ERR_CANCELLED;
+ this.eventPump.addEvent(ev);
+
+ return false;
+ }
+
+ if ("primServ" in this && this.primServ.isConnected)
+ return true;
+
+ this.connectAttempt++;
+ this.connectCandidate++;
+
+ this.state = NET_CONNECTING; /* connection is considered "made" when server
+ * sends a 001 message (see server.on001) */
+
+ var host = this.nextHost++;
+ if (host >= this.serverList.length)
+ {
+ this.nextHost = 1;
+ host = 0;
+ }
+
+ // If STS is enabled, check the cache for a secure port to connect to.
+ if (this.STS_MODULE.ENABLED && !this.serverList[host].isSecure)
+ {
+ var newPort = this.STS_MODULE.getUpgradePolicy(this.serverList[host].hostname);
+ if (newPort)
+ {
+ // If we're a temporary network, just change the server prior to connecting.
+ if (this.temporary)
+ {
+ this.serverList[host].port = newPort;
+ this.serverList[host].isSecure = true;
+ }
+ // Otherwise, find or create a server with the specified host and port.
+ else
+ {
+ var hostname = this.serverList[host].hostname;
+ var matches = this.serverList.filter(function(s) {
+ return s.hostname == hostname && s.port == newPort;
+ });
+ if (matches.length > 0)
+ {
+ host = arrayIndexOf(this.serverList, matches[0]);
+ }
+ else
+ {
+ this.addServer(hostname, newPort, true,
+ this.serverList[host].password);
+ host = this.serverList.length - 1;
+ }
+ }
+ }
+ }
+
+ if (this.serverList[host].isSecure || !this.requireSecurity)
+ {
+ ev = new CEvent ("network", "startconnect", this, "onStartConnect");
+ ev.debug = "Connecting to " + this.serverList[host].unicodeName + ":" +
+ this.serverList[host].port + ", attempt " + this.connectAttempt +
+ " of " + this.MAX_CONNECT_ATTEMPTS + "...";
+ ev.host = this.serverList[host].hostname;
+ ev.port = this.serverList[host].port;
+ ev.server = this.serverList[host];
+ ev.connectAttempt = this.connectAttempt;
+ ev.reconnectDelayMs = this.getReconnectDelayMs();
+ this.eventPump.addEvent (ev);
+
+ try
+ {
+ this.serverList[host].connect();
+ }
+ catch(ex)
+ {
+ this.state = NET_OFFLINE;
+
+ ev = new CEvent("network", "error", this, "onError");
+ ev.server = this;
+ ev.debug = "Exception opening socket: " + ex;
+ ev.errorCode = JSIRC_ERR_NO_SOCKET;
+ if ((typeof ex == "object") && (ex.result == NS_ERROR_OFFLINE))
+ ev.errorCode = JSIRC_ERR_OFFLINE;
+ if ((typeof ex == "string") && (ex == JSIRC_ERR_PAC_LOADING))
+ {
+ ev.errorCode = JSIRC_ERR_PAC_LOADING;
+ ev.retryDelay = CIRCNetwork.prototype.PAC_RECONNECT_DELAY;
+ /* PAC loading is not a problem with any specific server. We'll
+ * retry the connection in 5 seconds.
+ */
+ this.nextHost--;
+ this.state = NET_WAITING;
+ setTimeout(function(n) { n.immediateConnect() },
+ ev.retryDelay, this);
+ }
+ this.eventPump.addEvent(ev);
+ }
+ }
+ else
+ {
+ /* Server doesn't use SSL as requested, try next one.
+ * In the meantime, correct the connection attempt counter */
+ this.connectAttempt--;
+ this.immediateConnect();
+ }
+
+ return true;
+}
+
+/**
+ * Returns |true| iff this network has a socket-level connection.
+ */
+CIRCNetwork.prototype.isConnected =
+function net_connected (e)
+{
+ return ("primServ" in this && this.primServ.isConnected);
+}
+
+
+CIRCNetwork.prototype.ignore =
+function net_ignore (hostmask)
+{
+ var input = getHostmaskParts(hostmask);
+
+ if (input.mask in this.ignoreList)
+ return false;
+
+ this.ignoreList[input.mask] = input;
+ this.ignoreMaskCache = new Object();
+ return true;
+}
+
+CIRCNetwork.prototype.unignore =
+function net_ignore (hostmask)
+{
+ var input = getHostmaskParts(hostmask);
+
+ if (!(input.mask in this.ignoreList))
+ return false;
+
+ delete this.ignoreList[input.mask];
+ this.ignoreMaskCache = new Object();
+ return true;
+}
+
+function CIRCServer (parent, hostname, port, isSecure, password)
+{
+ var serverName = hostname + ":" + port;
+
+ var s;
+ if (serverName in parent.servers)
+ {
+ s = parent.servers[serverName];
+ }
+ else
+ {
+ s = this;
+ s.channels = new Object();
+ s.users = new Object();
+ }
+
+ s.unicodeName = serverName;
+ s.viewName = serverName;
+ s.canonicalName = serverName;
+ s.collectionKey = ":" + serverName;
+ s.encodedName = serverName;
+ s.hostname = hostname;
+ s.port = port;
+ s.parent = parent;
+ s.isSecure = isSecure;
+ s.password = password;
+ s.connection = null;
+ s.isConnected = false;
+ s.sendQueue = new Array();
+ s.lastSend = new Date("1/1/1980");
+ s.lastPingSent = null;
+ s.lastPing = null;
+ s.savedLine = "";
+ s.lag = -1;
+ s.usersStable = true;
+ s.supports = null;
+ s.channelTypes = null;
+ s.channelModes = null;
+ s.channelCount = -1;
+ s.userModes = null;
+ s.maxLineLength = 400;
+ s.caps = new Object();
+ s.capvals = new Object();
+
+ parent.servers[s.collectionKey] = s;
+ if ("onInit" in s)
+ s.onInit();
+ return s;
+}
+
+CIRCServer.prototype.MS_BETWEEN_SENDS = 1500;
+CIRCServer.prototype.READ_TIMEOUT = 100;
+CIRCServer.prototype.VERSION_RPLY = "JS-IRC Library v0.01, " +
+ "Copyright (C) 1999 Robert Ginda; rginda@ndcico.com";
+CIRCServer.prototype.OS_RPLY = "Unknown";
+CIRCServer.prototype.HOST_RPLY = "Unknown";
+CIRCServer.prototype.DEFAULT_REASON = "no reason";
+/* true means WHO command doesn't collect hostmask, username, etc. */
+CIRCServer.prototype.LIGHTWEIGHT_WHO = false;
+/* Unique identifier for WHOX commands. */
+CIRCServer.prototype.WHOX_TYPE = "314";
+/* -1 == never, 0 == prune onQuit, >0 == prune when >X ms old */
+CIRCServer.prototype.PRUNE_OLD_USERS = -1;
+
+CIRCServer.prototype.TYPE = "IRCServer";
+
+// Define functions to set modes so they're easily readable.
+// name is the name used on the CIRCChanMode object
+// getValue is a function returning the value the canonicalmode should be set to
+// given a certain modifier and appropriate data.
+CIRCServer.prototype.canonicalChanModes = {
+ i: {
+ name: "invite",
+ getValue: function (modifier) { return (modifier == "+"); }
+ },
+ m: {
+ name: "moderated",
+ getValue: function (modifier) { return (modifier == "+"); }
+ },
+ n: {
+ name: "publicMessages",
+ getValue: function (modifier) { return (modifier == "-"); }
+ },
+ t: {
+ name: "publicTopic",
+ getValue: function (modifier) { return (modifier == "-"); }
+ },
+ s: {
+ name: "secret",
+ getValue: function (modifier) { return (modifier == "+"); }
+ },
+ p: {
+ name: "pvt",
+ getValue: function (modifier) { return (modifier == "+"); }
+ },
+ k: {
+ name: "key",
+ getValue: function (modifier, data)
+ {
+ if (modifier == "+")
+ return data;
+ else
+ return "";
+ }
+ },
+ l: {
+ name: "limit",
+ getValue: function (modifier, data)
+ {
+ // limit is special - we return -1 if there is no limit.
+ if (modifier == "-")
+ return -1;
+ else
+ return data;
+ }
+ }
+};
+
+CIRCServer.prototype.toLowerCase =
+function serv_tolowercase(str)
+{
+ /* This is an implementation that lower-cases strings according to the
+ * prevailing CASEMAPPING setting for the server. Values for this are:
+ *
+ * o "ascii": The ASCII characters 97 to 122 (decimal) are defined as
+ * the lower-case characters of ASCII 65 to 90 (decimal). No other
+ * character equivalency is defined.
+ * o "strict-rfc1459": The ASCII characters 97 to 125 (decimal) are
+ * defined as the lower-case characters of ASCII 65 to 93 (decimal).
+ * No other character equivalency is defined.
+ * o "rfc1459": The ASCII characters 97 to 126 (decimal) are defined as
+ * the lower-case characters of ASCII 65 to 94 (decimal). No other
+ * character equivalency is defined.
+ *
+ */
+
+ function replaceFunction(chr)
+ {
+ return String.fromCharCode(chr.charCodeAt(0) + 32);
+ }
+
+ var mapping = "rfc1459";
+ if (this.supports)
+ mapping = this.supports.casemapping;
+
+ /* NOTE: There are NO breaks in this switch. This is CORRECT.
+ * Each mapping listed is a super-set of those below, thus we only
+ * transform the extra characters, and then fall through.
+ */
+ switch (mapping)
+ {
+ case "rfc1459":
+ str = str.replace(/\^/g, replaceFunction);
+ case "strict-rfc1459":
+ str = str.replace(/[\[\\\]]/g, replaceFunction);
+ case "ascii":
+ str = str.replace(/[A-Z]/g, replaceFunction);
+ }
+ return str;
+}
+
+// Iterates through the keys in an object and, if specified, the keys of
+// child objects.
+CIRCServer.prototype.renameProperties =
+function serv_renameproperties(obj, child)
+{
+ for (let key in obj)
+ {
+ let item = obj[key];
+ item.canonicalName = this.toLowerCase(item.encodedName);
+ item.collectionKey = ":" + item.canonicalName;
+ renameProperty(obj, key, item.collectionKey);
+ if (child && (child in item))
+ this.renameProperties(item[child], null);
+ }
+}
+
+// Encodes tag data to send.
+CIRCServer.prototype.encodeTagData =
+function serv_encodetagdata(obj)
+{
+ var dict = new Object();
+ dict[";"] = ":";
+ dict[" "] = "s";
+ dict["\\"] = "\\";
+ dict["\r"] = "r";
+ dict["\n"] = "n";
+
+ // Function for escaping key values.
+ function escapeTagValue(data)
+ {
+ var rv = "";
+ for (var i = 0; i < data.length; i++)
+ {
+ var ci = data[i];
+ var co = dict[data[i]];
+ if (co)
+ rv += "\\" + co;
+ else
+ rv += ci;
+ }
+
+ return rv;
+ }
+
+ var str = "";
+
+ for(var key in obj)
+ {
+ var val = obj[key];
+ str += key;
+ if (val)
+ {
+ str += "=";
+ str += escapeTagValue(val);
+ }
+ str += ";";
+ }
+
+ // Remove any trailing semicolons.
+ if (str[str.length - 1] == ";")
+ str = str.substring(0, str.length - 1);
+
+ return str;
+}
+
+// Decodes received tag data.
+CIRCServer.prototype.decodeTagData =
+function serv_decodetagdata(str)
+{
+ // Remove the leading '@' if we have one.
+ if (str[0] == "@")
+ str = str.substring(1);
+
+ var dict = new Object();
+ dict[":"] = ";";
+ dict["s"] = " ";
+ dict["\\"] = "\\";
+ dict["r"] = "\r";
+ dict["n"] = "\n";
+
+ // Function for unescaping key values.
+ function unescapeTagValue(data)
+ {
+ var rv = "";
+ for (let j = 0; j < data.length; j++)
+ {
+ let currentItem = data[j];
+ if (currentItem == "\\" && j < data.length - 1)
+ {
+ let nextItem = data[j + 1];
+ if (nextItem in dict)
+ rv += dict[nextItem];
+ else
+ rv += nextItem;
+ j++
+ }
+ else if (currentItem != "\\")
+ rv += currentItem;
+ }
+
+ return rv;
+ }
+
+ var obj = Object();
+
+ var tags = str.split(";");
+ for (var i = 0; i < tags.length; i++)
+ {
+ var [key, val] = tags[i].split("=");
+ val = unescapeTagValue(val);
+ obj[key] = val;
+ }
+
+ return obj;
+}
+
+// Returns the IRC URL representation of this server.
+CIRCServer.prototype.getURL =
+function serv_geturl(target, flags)
+{
+ var scheme = (this.isSecure ? "ircs" : "irc");
+ var obj = {host: this.hostname, scheme: scheme, isserver: true,
+ port: this.port, needpass: Boolean(this.password)};
+
+ if (target)
+ obj.target = target;
+
+ if (flags)
+ {
+ for (var i = 0; i < flags.length; i++)
+ obj[flags[i]] = true;
+ }
+
+ return constructIRCURL(obj);
+}
+
+CIRCServer.prototype.getUser =
+function chan_getuser(nick)
+{
+ var tnick = ":" + this.toLowerCase(nick);
+
+ if (tnick in this.users)
+ return this.users[tnick];
+
+ tnick = ":" + this.toLowerCase(fromUnicode(nick, this));
+
+ if (tnick in this.users)
+ return this.users[tnick];
+
+ return null;
+}
+
+CIRCServer.prototype.getChannel =
+function chan_getchannel(name)
+{
+ var tname = ":" + this.toLowerCase(name);
+
+ if (tname in this.channels)
+ return this.channels[tname];
+
+ tname = ":" + this.toLowerCase(fromUnicode(name, this));
+
+ if (tname in this.channels)
+ return this.channels[tname];
+
+ return null;
+}
+
+CIRCServer.prototype.connect =
+function serv_connect()
+{
+ if (this.connection != null)
+ throw "Server already has a connection pending or established";
+
+ var config = { isSecure: this.isSecure };
+ if (this.parent.PROXY_TYPE_OVERRIDE)
+ config.proxy = this.parent.PROXY_TYPE_OVERRIDE;
+
+ this.connection = new CBSConnection();
+ this.connection.connect(this.hostname, this.port, config, this);
+}
+
+// This may be called synchronously or asynchronously by CBSConnection.connect.
+CIRCServer.prototype.onSocketConnection =
+function serv_onsocketconnection(host, port, config, exception)
+{
+ if (this.parent.state == NET_CANCELLING)
+ {
+ this.connection.disconnect();
+ this.connection = null;
+ this.parent.state = NET_OFFLINE;
+
+ var ev = new CEvent("network", "error", this.parent, "onError");
+ ev.server = this;
+ ev.debug = "Connect sequence was canceled.";
+ ev.errorCode = JSIRC_ERR_CANCELLED;
+ this.parent.eventPump.addEvent(ev);
+ }
+ else if (!exception)
+ {
+ var ev = new CEvent("server", "connect", this, "onConnect");
+ ev.server = this;
+ this.parent.eventPump.addEvent(ev);
+ this.isConnected = true;
+
+ this.connection.startAsyncRead(this);
+ }
+ else
+ {
+ var ev = new CEvent("server", "disconnect", this, "onDisconnect");
+ ev.server = this;
+ ev.reason = "error";
+ ev.exception = exception;
+ ev.disconnectStatus = NS_ERROR_ABORT;
+ this.parent.eventPump.addEvent(ev);
+ }
+}
+
+/*
+ * What to do when the client connects to it's primary server
+ */
+CIRCServer.prototype.onConnect =
+function serv_onconnect (e)
+{
+ this.parent.primServ = e.server;
+
+ this.sendData("CAP LS 302\n");
+ this.pendingCapNegotiation = true;
+
+ this.caps = new Object();
+ this.capvals = new Object();
+
+ this.login(this.parent.INITIAL_NICK, this.parent.INITIAL_NAME,
+ this.parent.INITIAL_DESC);
+ return true;
+}
+
+CIRCServer.prototype.onStreamDataAvailable =
+function serv_sda (request, inStream, sourceOffset, count)
+{
+ var ev = new CEvent ("server", "data-available", this,
+ "onDataAvailable");
+
+ ev.line = this.connection.readData(0, count);
+
+ /* route data-available as we get it. the data-available handler does
+ * not do much, so we can probably get away with this without starving
+ * the UI even under heavy input traffic.
+ */
+ this.parent.eventPump.routeEvent(ev);
+}
+
+CIRCServer.prototype.onStreamClose =
+function serv_sockdiscon(status)
+{
+ var ev = new CEvent ("server", "disconnect", this, "onDisconnect");
+ ev.server = this;
+ ev.disconnectStatus = status;
+ if (ev.disconnectStatus == NS_ERROR_BINDING_ABORTED)
+ ev.disconnectStatus = NS_ERROR_ABORT;
+
+ this.parent.eventPump.addEvent (ev);
+}
+
+
+CIRCServer.prototype.flushSendQueue =
+function serv_flush()
+{
+ this.sendQueue.length = 0;
+ dd("sendQueue flushed.");
+
+ return true;
+}
+
+CIRCServer.prototype.login =
+function serv_login(nick, name, desc)
+{
+ nick = nick.replace(/ /g, "_");
+ name = name.replace(/ /g, "_");
+
+ if (!nick)
+ nick = "nick";
+
+ if (!name)
+ name = nick;
+
+ if (!desc)
+ desc = nick;
+
+ this.me = new CIRCUser(this, nick, null, name);
+ if (this.password)
+ this.sendData("PASS " + this.password + "\n");
+ this.changeNick(this.me.unicodeName);
+ this.sendData("USER " + name + " * * :" +
+ fromUnicode(desc, this) + "\n");
+}
+
+CIRCServer.prototype.logout =
+function serv_logout(reason)
+{
+ if (reason == null || typeof reason == "undefined")
+ reason = this.DEFAULT_REASON;
+
+ this.quitting = true;
+
+ this.connection.sendData("QUIT :" +
+ fromUnicode(reason, this.parent) + "\n");
+ this.connection.disconnect();
+}
+
+CIRCServer.prototype.sendAuthResponse =
+function serv_authresponse(resp)
+{
+ // Encode the response and break into 400-byte parts.
+ var resp = btoa(resp);
+ var part = null;
+ var n = 0;
+ do
+ {
+ part = resp.substring(0, 400);
+ n = part.length;
+ resp = resp.substring(400);
+
+ this.sendData("AUTHENTICATE " + part + '\n');
+ }
+ while (resp.length > 0);
+
+ // Send empty auth response if last part was exactly 400 bytes long.
+ if (n == 400)
+ {
+ this.sendData("AUTHENTICATE +\n");
+ }
+}
+
+CIRCServer.prototype.sendAuthAbort =
+function serv_authabort()
+{
+ // Abort an in-progress SASL authentication.
+ this.sendData("AUTHENTICATE *\n");
+}
+
+CIRCServer.prototype.sendMonitorList =
+function serv_monitorlist(nicks, isAdd)
+{
+ if (!nicks.length)
+ return;
+
+ var prefix;
+ if (isAdd)
+ prefix = "MONITOR + ";
+ else
+ prefix = "MONITOR - ";
+
+ /* Send monitor list updates in chunks less than
+ maxLineLength in size. */
+ var nicks_string = nicks.join(",");
+ while (nicks_string.length > this.maxLineLength)
+ {
+ var nicks_part = nicks_string.substring(0, this.maxLineLength);
+ var i = nicks_part.lastIndexOf(",");
+ nicks_part = nicks_string.substring(0, i);
+ nicks_string = nicks_string.substring(i + 1);
+ this.sendData(prefix + nicks_part + "\n");
+ }
+ this.sendData(prefix + nicks_string + "\n");
+}
+
+CIRCServer.prototype.addTarget =
+function serv_addtarget(name)
+{
+ if (arrayIndexOf(this.channelTypes, name[0]) != -1) {
+ return this.addChannel(name);
+ } else {
+ return this.addUser(name);
+ }
+}
+
+CIRCServer.prototype.addChannel =
+function serv_addchan(unicodeName, charset)
+{
+ return new CIRCChannel(this, unicodeName, fromUnicode(unicodeName, charset));
+}
+
+CIRCServer.prototype.addUser =
+function serv_addusr(unicodeName, name, host)
+{
+ return new CIRCUser(this, unicodeName, null, name, host);
+}
+
+CIRCServer.prototype.getChannelsLength =
+function serv_chanlen()
+{
+ var i = 0;
+
+ for (var p in this.channels)
+ i++;
+
+ return i;
+}
+
+CIRCServer.prototype.getUsersLength =
+function serv_chanlen()
+{
+ var i = 0;
+
+ for (var p in this.users)
+ i++;
+
+ return i;
+}
+
+CIRCServer.prototype.sendData =
+function serv_senddata (msg)
+{
+ this.queuedSendData (msg);
+}
+
+CIRCServer.prototype.queuedSendData =
+function serv_senddata (msg)
+{
+ if (this.sendQueue.length == 0)
+ this.parent.eventPump.addEvent (new CEvent ("server", "senddata",
+ this, "onSendData"));
+ arrayInsertAt (this.sendQueue, 0, new String(msg));
+}
+
+// Utility method for splitting large lines prior to sending.
+CIRCServer.prototype.splitLinesForSending =
+function serv_splitlines(line, prettyWrap)
+{
+ let lines = String(line).split("\n");
+ let realLines = [];
+ for (let i = 0; i < lines.length; i++)
+ {
+ if (lines[i])
+ {
+ while (lines[i].length > this.maxLineLength)
+ {
+ var extraLine = lines[i].substr(0, this.maxLineLength - 5);
+ var pos = extraLine.lastIndexOf(" ");
+
+ if ((pos >= 0) && (pos >= this.maxLineLength - 15))
+ {
+ // Smart-split.
+ extraLine = lines[i].substr(0, pos);
+ lines[i] = lines[i].substr(extraLine.length + 1);
+ if (prettyWrap)
+ {
+ extraLine += "...";
+ lines[i] = "..." + lines[i];
+ }
+ }
+ else
+ {
+ // Dumb-split.
+ extraLine = lines[i].substr(0, this.maxLineLength);
+ lines[i] = lines[i].substr(extraLine.length);
+ }
+ realLines.push(extraLine);
+ }
+ realLines.push(lines[i]);
+ }
+ }
+ return realLines;
+}
+
+CIRCServer.prototype.messageTo =
+function serv_messto(code, target, msg, ctcpCode)
+{
+ let lines = this.splitLinesForSending(msg, true);
+
+ let i = 0;
+ let pfx = "";
+ let sfx = "";
+
+ if (ctcpCode)
+ {
+ pfx = "\01" + ctcpCode;
+ sfx = "\01";
+ }
+
+ // We may have no message at all with CTCP commands.
+ if (!lines.length && ctcpCode)
+ lines.push("");
+
+ for (i in lines)
+ {
+ if ((lines[i] != "") || ctcpCode)
+ {
+ var line = code + " " + target + " :" + pfx;
+ if (lines[i] != "")
+ {
+ if (ctcpCode)
+ line += " ";
+ line += lines[i] + sfx;
+ }
+ else
+ line += sfx;
+ //dd ("-*- irc sending '" + line + "'");
+ this.sendData(line + "\n");
+ }
+ }
+}
+
+CIRCServer.prototype.sayTo =
+function serv_sayto (target, msg)
+{
+ this.messageTo("PRIVMSG", target, msg);
+}
+
+CIRCServer.prototype.noticeTo =
+function serv_noticeto (target, msg)
+{
+ this.messageTo("NOTICE", target, msg);
+}
+
+CIRCServer.prototype.actTo =
+function serv_actto (target, msg)
+{
+ this.messageTo("PRIVMSG", target, msg, "ACTION");
+}
+
+CIRCServer.prototype.ctcpTo =
+function serv_ctcpto (target, code, msg, method)
+{
+ msg = msg || "";
+ method = method || "PRIVMSG";
+
+ code = code.toUpperCase();
+ if (code == "PING" && !msg)
+ msg = Number(new Date());
+ this.messageTo(method, target, msg, code);
+}
+
+CIRCServer.prototype.changeNick =
+function serv_changenick(newNick)
+{
+ this.sendData("NICK " + fromUnicode(newNick, this) + "\n");
+}
+
+CIRCServer.prototype.updateLagTimer =
+function serv_uptimer()
+{
+ this.connection.sendData("PING :LAGTIMER\n");
+ this.lastPing = this.lastPingSent = new Date();
+}
+
+CIRCServer.prototype.userhost =
+function serv_userhost(target)
+{
+ this.sendData("USERHOST " + fromUnicode(target, this) + "\n");
+}
+
+CIRCServer.prototype.userip =
+function serv_userip(target)
+{
+ this.sendData("USERIP " + fromUnicode(target, this) + "\n");
+}
+
+CIRCServer.prototype.who =
+function serv_who(target)
+{
+ this.sendData("WHO " + fromUnicode(target, this) + "\n");
+}
+
+/**
+ * Abstracts the whois command.
+ *
+ * @param target intended user(s).
+ */
+CIRCServer.prototype.whois =
+function serv_whois (target)
+{
+ this.sendData("WHOIS " + fromUnicode(target, this) + "\n");
+}
+
+CIRCServer.prototype.whowas =
+function serv_whowas(target, limit)
+{
+ if (typeof limit == "undefined")
+ limit = 1;
+ else if (limit == 0)
+ limit = "";
+
+ this.sendData("WHOWAS " + fromUnicode(target, this) + " " + limit + "\n");
+}
+
+CIRCServer.prototype.onDisconnect =
+function serv_disconnect(e)
+{
+ function stateChangeFn(network, state) {
+ network.state = state;
+ };
+
+ function delayedConnectFn(network) {
+ network.delayedConnect();
+ };
+
+ /* If we're not connected and get this, it means we have almost certainly
+ * encountered a read or write error on the socket post-disconnect. There's
+ * no point propagating this any further, as we've already notified the
+ * user of the disconnect (with the right error).
+ */
+ if (!this.isConnected)
+ return;
+
+ // Don't reconnect from a certificate error.
+ var certError = (getNSSErrorClass(e.disconnectStatus) == ERROR_CLASS_BAD_CERT);
+
+ // Don't reconnect if our connection was aborted.
+ var wasAborted = (e.disconnectStatus == NS_ERROR_ABORT);
+ var dontReconnect = certError || wasAborted;
+
+ if (((this.parent.state == NET_CONNECTING) && !dontReconnect) ||
+ /* fell off while connecting, try again */
+ (this.parent.primServ == this) && (this.parent.state == NET_ONLINE) &&
+ (!("quitting" in this) && this.parent.stayingPower && !dontReconnect))
+ { /* fell off primary server, reconnect to any host in the serverList */
+ setTimeout(delayedConnectFn, 0, this.parent);
+ }
+ else
+ {
+ setTimeout(stateChangeFn, 0, this.parent, NET_OFFLINE);
+ }
+
+ e.server = this;
+ e.set = "network";
+ e.destObject = this.parent;
+
+ e.quitting = this.quitting;
+
+ for (var c in this.channels)
+ {
+ this.channels[c].users = new Object();
+ this.channels[c].active = false;
+ }
+
+ if (this.isStartTLS)
+ {
+ this.isSecure = false;
+ delete this.isStartTLS;
+ }
+
+ delete this.batches;
+
+ this.connection = null;
+ this.isConnected = false;
+
+ delete this.quitting;
+}
+
+CIRCServer.prototype.onSendData =
+function serv_onsenddata (e)
+{
+ if (!this.isConnected || (this.parent.state == NET_CANCELLING))
+ {
+ dd ("Can't send to disconnected socket");
+ this.flushSendQueue();
+ return false;
+ }
+
+ var d = new Date();
+
+ // Wheee, some sanity checking! (there's been at least one case of lastSend
+ // ending up in the *future* at this point, which kinda busts things)
+ if (this.lastSend > d)
+ this.lastSend = 0;
+
+ if (((d - this.lastSend) >= this.MS_BETWEEN_SENDS) &&
+ this.sendQueue.length > 0)
+ {
+ var s = this.sendQueue.pop();
+
+ if (s)
+ {
+ try
+ {
+ this.connection.sendData(s);
+ }
+ catch(ex)
+ {
+ dd("Exception in queued send: " + ex);
+ this.flushSendQueue();
+
+ var ev = new CEvent("server", "disconnect",
+ this, "onDisconnect");
+ ev.server = this;
+ ev.reason = "error";
+ ev.exception = ex;
+ ev.disconnectStatus = NS_ERROR_ABORT;
+ this.parent.eventPump.addEvent(ev);
+
+ return false;
+ }
+ this.lastSend = d;
+ }
+
+ }
+ else
+ {
+ this.parent.eventPump.addEvent(new CEvent("event-pump", "yield",
+ null, ""));
+ }
+
+ if (this.sendQueue.length > 0)
+ this.parent.eventPump.addEvent(new CEvent("server", "senddata",
+ this, "onSendData"));
+ return true;
+}
+
+CIRCServer.prototype.onPoll =
+function serv_poll(e)
+{
+ var lines;
+ var ex;
+ var ev;
+
+ try
+ {
+ if (this.parent.state != NET_CANCELLING)
+ line = this.connection.readData(this.READ_TIMEOUT);
+ }
+ catch (ex)
+ {
+ dd ("*** Caught exception " + ex + " reading from server " +
+ this.hostname);
+ ev = new CEvent ("server", "disconnect", this, "onDisconnect");
+ ev.server = this;
+ ev.reason = "error";
+ ev.exception = ex;
+ ev.disconnectStatus = NS_ERROR_ABORT;
+ this.parent.eventPump.addEvent (ev);
+ return false;
+ }
+
+ this.parent.eventPump.addEvent (new CEvent ("server", "poll", this,
+ "onPoll"));
+
+ if (line)
+ {
+ ev = new CEvent ("server", "data-available", this, "onDataAvailable");
+ ev.line = line;
+ this.parent.eventPump.routeEvent(ev);
+ }
+
+ return true;
+}
+
+CIRCServer.prototype.onDataAvailable =
+function serv_ppline(e)
+{
+ var line = e.line;
+
+ if (line == "")
+ return false;
+
+ var incomplete = (line[line.length - 1] != '\n');
+ var lines = line.split("\n");
+
+ if (this.savedLine)
+ {
+ lines[0] = this.savedLine + lines[0];
+ this.savedLine = "";
+ }
+
+ if (incomplete)
+ this.savedLine = lines.pop();
+
+ for (var i in lines)
+ {
+ var ev = new CEvent("server", "rawdata", this, "onRawData");
+ ev.data = lines[i].replace(/\r/g, "");
+ if (ev.data)
+ {
+ if (ev.data.match(/^(?::[^ ]+ )?(?:32[123]|352|354|315) /i))
+ this.parent.eventPump.addBulkEvent(ev);
+ else
+ this.parent.eventPump.addEvent(ev);
+ }
+ }
+
+ return true;
+}
+
+/*
+ * onRawData begins shaping the event by parsing the IRC message at it's
+ * simplest level. After onRawData, the event will have the following
+ * properties:
+ * name value
+ *
+ * set............"server"
+ * type..........."parsedata"
+ * destMethod....."onParsedData"
+ * destObject.....server (this)
+ * server.........server (this)
+ * connection.....CBSConnection (this.connection)
+ * source.........the <prefix> of the message (if it exists)
+ * user...........user object initialized with data from the message <prefix>
+ * params.........array containing the parameters of the message
+ * code...........the first parameter (most messages have this)
+ *
+ * See Section 2.3.1 of RFC 1459 for details on <prefix>, <middle> and
+ * <trailing> tokens.
+ */
+CIRCServer.prototype.onRawData =
+function serv_onRawData(e)
+{
+ var ary;
+ var l = e.data;
+
+ if (l.length == 0)
+ {
+ dd ("empty line on onRawData?");
+ return false;
+ }
+
+ if (l[0] == "@")
+ {
+ e.tagdata = l.substring(0, l.indexOf(" "));
+ e.tags = this.decodeTagData(e.tagdata);
+ l = l.substring(l.indexOf(" ") + 1);
+ }
+ else
+ {
+ e.tagdata = new Object();
+ e.tags = new Object();
+ }
+
+ if (l[0] == ":")
+ {
+ // Must split only on REAL spaces here, not just any old whitespace.
+ ary = l.match(/:([^ ]+) +(.*)/);
+ e.source = ary[1];
+ l = ary[2];
+ ary = e.source.match(/([^ ]+)!([^ ]+)@(.*)/);
+ if (ary)
+ {
+ e.user = new CIRCUser(this, null, ary[1], ary[2], ary[3]);
+ }
+ else
+ {
+ ary = e.source.match(/([^ ]+)@(.*)/);
+ if (ary)
+ {
+ e.user = new CIRCUser(this, null, ary[1], null, ary[2]);
+ }
+ else
+ {
+ ary = e.source.match(/([^ ]+)!(.*)/);
+ if (ary)
+ e.user = new CIRCUser(this, null, ary[1], ary[2], null);
+ }
+ }
+ }
+
+ if (("user" in e) && e.user && e.tags.account)
+ {
+ e.user.account = e.tags.account;
+ }
+
+ e.ignored = false;
+ if (("user" in e) && e.user && ("ignoreList" in this.parent))
+ {
+ // Assumption: if "ignoreList" is in this.parent, we assume that:
+ // a) it's an array.
+ // b) ignoreMaskCache also exists, and
+ // c) it's an array too.
+
+ if (!(e.source in this.parent.ignoreMaskCache))
+ {
+ for (var m in this.parent.ignoreList)
+ {
+ if (hostmaskMatches(e.user, this.parent.ignoreList[m]))
+ {
+ e.ignored = true;
+ break;
+ }
+ }
+ /* Save this exact source in the cache, with results of tests. */
+ this.parent.ignoreMaskCache[e.source] = e.ignored;
+ }
+ else
+ {
+ e.ignored = this.parent.ignoreMaskCache[e.source];
+ }
+ }
+
+ e.server = this;
+
+ var sep = l.indexOf(" :");
+
+ if (sep != -1) /* <trailing> param, if there is one */
+ {
+ var trail = l.substr (sep + 2, l.length);
+ e.params = l.substr(0, sep).split(/ +/);
+ e.params[e.params.length] = trail;
+ }
+ else
+ {
+ e.params = l.split(/ +/);
+ }
+
+ e.decodeParam = decodeParam;
+ e.code = e.params[0].toUpperCase();
+
+ // Ignore all private (inc. channel) messages, notices and invites here.
+ if (e.ignored && ((e.code == "PRIVMSG") || (e.code == "NOTICE") ||
+ (e.code == "INVITE") || (e.code == "TAGMSG")))
+ return true;
+
+ // If the message is part of a batch, store it for later.
+ if (this.batches && e.tags["batch"] && e.code != "BATCH")
+ {
+ var reftag = e.tags["batch"];
+ // Check if the batch is already open.
+ // If not, ignore the incoming message.
+ if (this.batches[reftag])
+ this.batches[reftag].messages.push(e);
+ return false;
+ }
+
+ e.type = "parseddata";
+ e.destObject = this;
+ e.destMethod = "onParsedData";
+
+ return true;
+}
+
+/*
+ * onParsedData forwards to next event, based on |e.code|
+ */
+CIRCServer.prototype.onParsedData =
+function serv_onParsedData(e)
+{
+ e.type = this.toLowerCase(e.code);
+ if (!e.code[0])
+ {
+ dd (dumpObjectTree (e));
+ return false;
+ }
+
+ e.destMethod = "on" + e.code[0].toUpperCase() +
+ e.code.substr (1, e.code.length).toLowerCase();
+
+ if (typeof this[e.destMethod] == "function")
+ e.destObject = this;
+ else if (typeof this["onUnknown"] == "function")
+ e.destMethod = "onUnknown";
+ else if (typeof this.parent[e.destMethod] == "function")
+ {
+ e.set = "network";
+ e.destObject = this.parent;
+ }
+ else
+ {
+ e.set = "network";
+ e.destObject = this.parent;
+ e.destMethod = "onUnknown";
+ }
+
+ return true;
+}
+
+/* User changed topic */
+CIRCServer.prototype.onTopic =
+function serv_topic (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[1]);
+ e.channel.topicBy = e.user.unicodeName;
+ e.channel.topicDate = new Date();
+ e.channel.topic = toUnicode(e.params[2], e.channel);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+/* Successful login */
+CIRCServer.prototype.on001 =
+function serv_001 (e)
+{
+ this.parent.connectAttempt = 0;
+ this.parent.connectCandidate = 0;
+ //Mark capability negotiation as finished, if we haven't already.
+ delete this.parent.pendingCapNegotiation;
+ this.parent.state = NET_ONLINE;
+ // nextHost is incremented after picking a server. Push it back here.
+ this.parent.nextHost--;
+
+ /* servers won't send a nick change notification if user was forced
+ * to change nick while logging in (eg. nick already in use.) We need
+ * to verify here that what the server thinks our name is, matches what
+ * we think it is. If not, the server wins.
+ */
+ if (e.params[1] != e.server.me.encodedName)
+ {
+ renameProperty(e.server.users, e.server.me.collectionKey,
+ ":" + this.toLowerCase(e.params[1]));
+ e.server.me.changeNick(toUnicode(e.params[1], this));
+ }
+
+ /* Set up supports defaults here.
+ * This is so that we don't waste /huge/ amounts of RAM for the network's
+ * servers just because we know about them. Until we connect, that is.
+ * These defaults are taken from the draft 005 RPL_ISUPPORTS here:
+ * http://www.ietf.org/internet-drafts/draft-brocklesby-irc-isupport-02.txt
+ */
+ this.supports = new Object();
+ this.supports.modes = 3;
+ this.supports.maxchannels = 10;
+ this.supports.nicklen = 9;
+ this.supports.casemapping = "rfc1459";
+ this.supports.channellen = 200;
+ this.supports.chidlen = 5;
+ /* Make sure it's possible to tell if we've actually got a 005 message. */
+ this.supports.rpl_isupport = false;
+ this.channelTypes = [ '#', '&' ];
+ /* This next one isn't in the isupport draft, but instead is defaulting to
+ * the codes we understand. It should be noted, some servers include the
+ * mode characters (o, h, v) in the 'a' list, although the draft spec says
+ * they should be treated as type 'b'. Luckly, in practise this doesn't
+ * matter, since both 'a' and 'b' types always take a parameter in the
+ * MODE message, and parsing is not affected. */
+ this.channelModes = {
+ a: ['b'],
+ b: ['k'],
+ c: ['l'],
+ d: ['i', 'm', 'n', 'p', 's', 't']
+ };
+ // Default to support of v/+ and o/@ only.
+ this.userModes = [
+ { mode: 'o', symbol: '@' },
+ { mode: 'v', symbol: '+' }
+ ];
+ // Assume the server supports no extra interesting commands.
+ this.servCmds = {};
+
+ if (this.parent.INITIAL_UMODE)
+ {
+ e.server.sendData("MODE " + e.server.me.encodedName + " :" +
+ this.parent.INITIAL_UMODE + "\n");
+ }
+
+ this.parent.users = this.users;
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* server features */
+CIRCServer.prototype.on005 =
+function serv_005 (e)
+{
+ var oldCaseMapping = this.supports["casemapping"];
+ /* Drop params 0 and 1. */
+ for (var i = 2; i < e.params.length; i++) {
+ var itemStr = e.params[i];
+ /* Items may be of the forms:
+ * NAME
+ * -NAME
+ * NAME=value
+ * Value may be empty on occasion.
+ * No value allowed for -NAME items.
+ */
+ var item = itemStr.match(/^(-?)([A-Z]+)(=(.*))?$/i);
+ if (! item)
+ continue;
+
+ var name = item[2].toLowerCase();
+ if (("3" in item) && item[3])
+ {
+ // And other items are stored as-is, though numeric items
+ // get special treatment to make our life easier later.
+ if (("4" in item) && item[4].match(/^\d+$/))
+ this.supports[name] = Number(item[4]);
+ else
+ this.supports[name] = item[4];
+ }
+ else
+ {
+ // Boolean-type items stored as 'true'.
+ this.supports[name] = !(("1" in item) && item[1] == "-");
+ }
+ }
+ // Update all users and channels if the casemapping changed.
+ if (this.supports["casemapping"] != oldCaseMapping)
+ {
+ this.renameProperties(this.users, null);
+ this.renameProperties(this.channels, "users");
+ }
+
+ // Supported 'special' items:
+ // CHANTYPES (--> channelTypes{}),
+ // PREFIX (--> userModes[{mode,symbol}]),
+ // CHANMODES (--> channelModes{a:[], b:[], c:[], d:[]}).
+
+ var m;
+ if ("chantypes" in this.supports)
+ {
+ this.channelTypes = [];
+ for (m = 0; m < this.supports.chantypes.length; m++)
+ this.channelTypes.push( this.supports.chantypes[m] );
+ }
+
+ if ("prefix" in this.supports)
+ {
+ var mlist = this.supports.prefix.match(/^\((.*)\)(.*)$/i);
+ if ((! mlist) || (mlist[1].length != mlist[2].length))
+ {
+ dd ("** Malformed PREFIX entry in 005 SUPPORTS message **");
+ }
+ else
+ {
+ this.userModes = [];
+ for (m = 0; m < mlist[1].length; m++)
+ this.userModes.push( { mode: mlist[1][m],
+ symbol: mlist[2][m] } );
+ }
+ }
+
+ if ("chanmodes" in this.supports)
+ {
+ var cmlist = this.supports.chanmodes.split(/,/);
+ if ((!cmlist) || (cmlist.length < 4))
+ {
+ dd ("** Malformed CHANMODES entry in 005 SUPPORTS message **");
+ }
+ else
+ {
+ // 4 types - list, set-unset-param, set-only-param, flag.
+ this.channelModes = {
+ a: cmlist[0].split(''),
+ b: cmlist[1].split(''),
+ c: cmlist[2].split(''),
+ d: cmlist[3].split('')
+ };
+ }
+ }
+
+ if ("cmds" in this.supports)
+ {
+ // Map this.supports.cmds [comma-list] into this.servCmds [props].
+ var cmdlist = this.supports.cmds.split(/,/);
+ for (var i = 0; i < cmdlist.length; i++)
+ this.servCmds[cmdlist[i].toLowerCase()] = true;
+ }
+
+ this.supports.rpl_isupport = true;
+
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+/* users */
+CIRCServer.prototype.on251 =
+function serv_251(e)
+{
+ // 251 is the first message we get after 005, so it's now safe to do
+ // things that might depend upon server features.
+
+ if (("namesx" in this.supports) && this.supports.namesx)
+ {
+ // "multi-prefix" is the same as "namesx" but PROTOCTL doesn't reply.
+ this.caps["multi-prefix"] = true;
+ this.sendData("PROTOCTL NAMESX\n");
+ }
+
+ if (this.parent.INITIAL_CHANNEL)
+ {
+ this.parent.primChan = this.addChannel(this.parent.INITIAL_CHANNEL);
+ this.parent.primChan.join();
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* channels */
+CIRCServer.prototype.on254 =
+function serv_254(e)
+{
+ this.channelCount = e.params[2];
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* user away message */
+CIRCServer.prototype.on301 =
+function serv_301(e)
+{
+ e.user = new CIRCUser(this, null, e.params[2]);
+ e.user.awayMessage = e.decodeParam(3, e.user);
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* whois name */
+CIRCServer.prototype.on311 =
+function serv_311 (e)
+{
+ e.user = new CIRCUser(this, null, e.params[2], e.params[3], e.params[4]);
+ e.user.desc = e.decodeParam(6, e.user);
+ e.destObject = this.parent;
+ e.set = "network";
+
+ this.pendingWhoisLines = e.user;
+}
+
+/* whois server */
+CIRCServer.prototype.on312 =
+function serv_312 (e)
+{
+ e.user = new CIRCUser(this, null, e.params[2]);
+ e.user.connectionHost = e.params[3];
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* whois idle time */
+CIRCServer.prototype.on317 =
+function serv_317 (e)
+{
+ e.user = new CIRCUser(this, null, e.params[2]);
+ e.user.idleSeconds = e.params[3];
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* whois channel list */
+CIRCServer.prototype.on319 =
+function serv_319(e)
+{
+ e.user = new CIRCUser(this, null, e.params[2]);
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* end of whois */
+CIRCServer.prototype.on318 =
+function serv_318(e)
+{
+ e.user = new CIRCUser(this, null, e.params[2]);
+
+ if ("pendingWhoisLines" in this)
+ delete this.pendingWhoisLines;
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* ircu's 330 numeric ("X is logged in as Y") */
+CIRCServer.prototype.on330 =
+function serv_330(e)
+{
+ e.user = new CIRCUser(this, null, e.params[2]);
+ var account = (e.params[3] == "*" ? null : e.params[3]);
+ this.users[e.user.collectionKey].account = account;
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* TOPIC reply - no topic set */
+CIRCServer.prototype.on331 =
+function serv_331 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.channel.topic = "";
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+/* TOPIC reply - topic set */
+CIRCServer.prototype.on332 =
+function serv_332 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.channel.topic = toUnicode(e.params[3], e.channel);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+/* topic information */
+CIRCServer.prototype.on333 =
+function serv_333 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.channel.topicBy = toUnicode(e.params[3], this);
+ e.channel.topicDate = new Date(Number(e.params[4]) * 1000);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+/* who reply */
+CIRCServer.prototype.on352 =
+function serv_352 (e)
+{
+ e.userHasChanges = false;
+ if (this.LIGHTWEIGHT_WHO)
+ {
+ e.user = new CIRCUser(this, null, e.params[6]);
+ }
+ else
+ {
+ e.user = new CIRCUser(this, null, e.params[6], e.params[3], e.params[4]);
+ e.user.connectionHost = e.params[5];
+ if (8 in e.params)
+ {
+ var ary = e.params[8].match(/(?:(\d+)\s)?(.*)/);
+ e.user.hops = ary[1];
+ var desc = fromUnicode(ary[2], e.user);
+ if (e.user.desc != desc)
+ {
+ e.userHasChanges = true;
+ e.user.desc = desc;
+ }
+ }
+ }
+ var away = (e.params[7][0] == "G");
+ if (e.user.isAway != away)
+ {
+ e.userHasChanges = true;
+ e.user.isAway = away;
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+/* extended who reply */
+CIRCServer.prototype.on354 =
+function serv_354(e)
+{
+ // Discard if the type is not ours.
+ if (e.params[2] != this.WHOX_TYPE)
+ return;
+
+ e.userHasChanges = false;
+ if (this.LIGHTWEIGHT_WHO)
+ {
+ e.user = new CIRCUser(this, null, e.params[7]);
+ }
+ else
+ {
+ e.user = new CIRCUser(this, null, e.params[7], e.params[4], e.params[5]);
+ e.user.connectionHost = e.params[6];
+ // Hops is a separate parameter in WHOX.
+ e.user.hops = e.params[9];
+ var account = (e.params[10] == "0" ? null : e.params[10]);
+ e.user.account = account;
+ if (11 in e.params)
+ {
+ var desc = e.decodeParam(11, e.user);
+ if (e.user.desc != desc)
+ {
+ e.userHasChanges = true;
+ e.user.desc = desc;
+ }
+ }
+ }
+ var away = (e.params[8][0] == "G");
+ if (e.user.isAway != away)
+ {
+ e.userHasChanges = true;
+ e.user.isAway = away;
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+/* end of who */
+CIRCServer.prototype.on315 =
+function serv_315 (e)
+{
+ e.user = new CIRCUser(this, null, e.params[1]);
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+/* names reply */
+CIRCServer.prototype.on353 =
+function serv_353 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[3]);
+ if (e.channel.usersStable)
+ {
+ e.channel.users = new Object();
+ e.channel.usersStable = false;
+ }
+
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ var nicks = e.params[4].split (" ");
+ var mList = this.userModes;
+
+ for (var n in nicks)
+ {
+ var nick = nicks[n];
+ if (nick == "")
+ break;
+
+ var modes = new Array();
+ var multiPrefix = (("namesx" in this.supports) && this.supports.namesx)
+ || (("multi-prefix" in this.caps)
+ && this.caps["multi-prefix"]);
+ do
+ {
+ var found = false;
+ for (var m in mList)
+ {
+ if (nick[0] == mList[m].symbol)
+ {
+ nick = nick.substr(1);
+ modes.push(mList[m].mode);
+ found = true;
+ break;
+ }
+ }
+ } while (found && multiPrefix);
+
+ var ary = nick.match(/([^ ]+)!([^ ]+)@(.*)/);
+ var user = null;
+ var host = null;
+
+ if (this.caps["userhost-in-names"] && ary)
+ {
+ nick = ary[1];
+ user = ary[2];
+ host = ary[3];
+ }
+
+ new CIRCChanUser(e.channel, null, nick, modes, true, user, host);
+ }
+
+ return true;
+}
+
+/* end of names */
+CIRCServer.prototype.on366 =
+function serv_366 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+ e.channel.usersStable = true;
+
+ return true;
+}
+
+/* channel time stamp? */
+CIRCServer.prototype.on329 =
+function serv_329 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+ e.channel.timeStamp = new Date (Number(e.params[3]) * 1000);
+
+ return true;
+}
+
+/* channel mode reply */
+CIRCServer.prototype.on324 =
+function serv_324 (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = this;
+ e.type = "chanmode";
+ e.destMethod = "onChanMode";
+
+ return true;
+}
+
+/* channel ban entry */
+CIRCServer.prototype.on367 =
+function serv_367(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+ e.ban = e.params[3];
+ e.user = new CIRCUser(this, null, e.params[4]);
+ e.banTime = new Date (Number(e.params[5]) * 1000);
+
+ if (typeof e.channel.bans[e.ban] == "undefined")
+ {
+ e.channel.bans[e.ban] = {host: e.ban, user: e.user, time: e.banTime };
+ var ban_evt = new CEvent("channel", "ban", e.channel, "onBan");
+ ban_evt.tags = e.tags;
+ ban_evt.channel = e.channel;
+ ban_evt.ban = e.ban;
+ ban_evt.source = e.user;
+ this.parent.eventPump.addEvent(ban_evt);
+ }
+
+ return true;
+}
+
+/* channel ban list end */
+CIRCServer.prototype.on368 =
+function serv_368(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ /* This flag is cleared in a timeout (which occurs right after the current
+ * message has been processed) so that the new event target (the channel)
+ * will still have the flag set when it executes.
+ */
+ if ("pendingBanList" in e.channel)
+ setTimeout(function() { delete e.channel.pendingBanList; }, 0);
+
+ return true;
+}
+
+/* channel except entry */
+CIRCServer.prototype.on348 =
+function serv_348(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+ e.except = e.params[3];
+ e.user = new CIRCUser(this, null, e.params[4]);
+ e.exceptTime = new Date (Number(e.params[5]) * 1000);
+
+ if (typeof e.channel.excepts[e.except] == "undefined")
+ {
+ e.channel.excepts[e.except] = {host: e.except, user: e.user,
+ time: e.exceptTime };
+ }
+
+ return true;
+}
+
+/* channel except list end */
+CIRCServer.prototype.on349 =
+function serv_349(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ if ("pendingExceptList" in e.channel)
+ setTimeout(function (){ delete e.channel.pendingExceptList; }, 0);
+
+ return true;
+}
+
+/* don't have operator perms */
+CIRCServer.prototype.on482 =
+function serv_482(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ /* Some servers (e.g. Hybrid) don't let you get the except list without ops,
+ * so we might be waiting for this list forever otherwise.
+ */
+ if ("pendingExceptList" in e.channel)
+ setTimeout(function (){ delete e.channel.pendingExceptList; }, 0);
+
+ return true;
+}
+
+/* userhost reply */
+CIRCServer.prototype.on302 =
+function serv_302(e)
+{
+ var list = e.params[2].split(/\s+/);
+
+ for (var i = 0; i < list.length; i++)
+ {
+ // <reply> ::= <nick>['*'] '=' <'+'|'-'><hostname>
+ // '*' == IRCop. '+' == here, '-' == away.
+ var data = list[i].match(/^(.*)(\*?)=([-+])(.*)@(.*)$/);
+ if (data)
+ this.addUser(data[1], data[4], data[5]);
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+/* CAP response */
+CIRCServer.prototype.onCap =
+function my_cap (e)
+{
+ // We expect some sort of identifier.
+ if (e.params.length < 2)
+ return;
+
+ if (e.params[2] == "LS")
+ {
+ /* We're getting a list of all server capabilities. Set them all to
+ * null (if they don't exist) to indicate we don't know if they're
+ * enabled or not (but this will evaluate to false which matches that
+ * capabilities are only enabled on request).
+ */
+ var caps = e.params[3].split(/\s+/);
+ var multiline = (e.params[3] == "*");
+ if (multiline)
+ caps = e.params[4].split(/\s+/);
+
+ for (var i = 0; i < caps.length; i++)
+ {
+ var [cap, value] = caps[i].split(/=(.+)/);
+ cap = cap.replace(/^-/, "").trim();
+ if (!(cap in this.caps))
+ this.caps[cap] = null;
+ if (value)
+ this.capvals[cap] = value;
+ }
+
+ // Don't do anything until the end of the response.
+ if (multiline)
+ return true;
+
+ //Only request capabilities we support if we are connecting.
+ if (this.pendingCapNegotiation)
+ {
+ // If we have an STS upgrade policy, immediately disconnect
+ // and reconnect on the secure port.
+ if (this.parent.STS_MODULE.ENABLED && ("sts" in this.caps) && !this.isSecure)
+ {
+ var policy = this.parent.STS_MODULE.parseParameters(this.capvals["sts"]);
+ if (policy && policy.port)
+ {
+ e.stsUpgradePort = policy.port;
+ e.destObject = this.parent;
+ e.set = "network";
+ return false;
+ }
+ }
+
+ // Request STARTTLS if we are configured to do so.
+ if (!this.isSecure && ("tls" in this.caps) && this.parent.UPGRADE_INSECURE)
+ this.sendData("STARTTLS\n");
+
+ var caps_req = JSIRCV3_SUPPORTED_CAPS.filter(i => (i in this.caps));
+
+ // Don't send requests for these caps.
+ let caps_noreq = ["tls", "sts", "echo-message"];
+
+ if (!this.parent.USE_SASL)
+ caps_noreq.push("sasl");
+
+ caps_req = caps_req.filter(i => caps_noreq.indexOf(i) === -1);
+
+ if (caps_req.length > 0)
+ {
+ caps_req = caps_req.join(" ");
+ e.server.sendData("CAP REQ :" + caps_req + "\n");
+ }
+ else
+ {
+ e.server.sendData("CAP END\n");
+ delete this.pendingCapNegotiation;
+ }
+ }
+ }
+ else if (e.params[2] == "LIST")
+ {
+ /* Received list of enabled capabilities. Just use this as a sanity
+ * check. */
+ var caps = e.params[3].trim().split(/\s+/);
+ var multiline = (e.params[3] == "*");
+ if (multiline)
+ caps = e.params[4].trim().split(/\s+/);
+
+ for (var i = 0; i < caps.length; i++)
+ {
+ this.caps[caps[i]] = true;
+ }
+
+ // Don't do anything until the end of the response.
+ if (multiline)
+ return true;
+ }
+ else if (e.params[2] == "ACK")
+ {
+ /* One or more capability changes have been successfully applied. An enabled
+ * capability is just "cap" whilst a disabled capability is "-cap".
+ */
+ var caps = e.params[3].trim().split(/\s+/);
+ e.capsOn = new Array();
+ e.capsOff = new Array();
+ for (var i = 0; i < caps.length; i++)
+ {
+ var cap = caps[i].replace(/^-/,"").trim();
+ var enabled = caps[i][0] != "-";
+ if (enabled)
+ e.capsOn.push(cap);
+ else
+ e.capsOff.push(cap);
+ this.caps[cap] = enabled;
+ }
+
+ // Try SASL authentication if we are configured to do so.
+ if (caps.indexOf("sasl") != -1)
+ {
+ var ev = new CEvent("server", "sasl-start", this, "onSASLStart");
+ ev.server = this;
+ if (this.capvals["sasl"])
+ ev.mechs = this.capvals["sasl"].toLowerCase().split(/,/);
+ ev.destObject = this.parent;
+ this.parent.eventPump.routeEvent(ev);
+
+ if (this.pendingCapNegotiation)
+ return true;
+ }
+
+ if (this.pendingCapNegotiation)
+ {
+ e.server.sendData("CAP END\n");
+ delete this.pendingCapNegotiation;
+
+ //Don't show the raw message while connecting.
+ return true;
+ }
+ }
+ else if (e.params[2] == "NAK")
+ {
+ // A capability change has failed.
+ var caps = e.params[3].trim().split(/\s+/);
+ e.caps = new Array();
+ for (var i = 0; i < caps.length; i++)
+ {
+ var cap = caps[i].replace(/^-/, "").trim();
+ e.caps.push(cap);
+ }
+
+ if (this.pendingCapNegotiation)
+ {
+ e.server.sendData("CAP END\n");
+ delete this.pendingCapNegotiation;
+
+ //Don't show the raw message while connecting.
+ return true;
+ }
+ }
+ else if (e.params[2] == "NEW")
+ {
+ // A capability is now available, so request it if we can.
+ var caps = e.params[3].split(/\s+/);
+ e.newcaps = [];
+ for (var i = 0; i < caps.length; i++)
+ {
+ var [cap, value] = caps[i].split(/=(.+)/);
+ cap = cap.trim();
+ this.caps[cap] = null;
+ e.newcaps.push(cap);
+ if (value)
+ this.capvals[cap] = value;
+ }
+
+ var caps_req = JSIRCV3_SUPPORTED_CAPS.filter(i => (i in e.newcaps));
+
+ // Don't send requests for these caps.
+ caps_noreq = ["tls", "sts", "sasl", "echo-message"];
+ caps_req = caps_req.filter(i => caps_noreq.indexOf(i) === -1);
+
+ if (caps_req.length > 0)
+ {
+ caps_req = caps_req.join(" ");
+ e.server.sendData("CAP REQ :" + caps_req + "\n");
+ }
+ }
+ else if (e.params[2] == "DEL")
+ {
+ // A capability is no longer available.
+ var caps = e.params[3].split(/\s+/);
+ var caps_nodel = ["sts"];
+ for (var i = 0; i < caps.length; i++)
+ {
+ var cap = caps[i].split(/=(.+)/)[0];
+ cap = cap.trim();
+
+ if (arrayContains(caps_nodel, cap))
+ continue;
+
+ this.caps[cap] = null;
+ }
+ }
+ else
+ {
+ dd("Unknown CAP reply " + e.params[2]);
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* BATCH start or end */
+CIRCServer.prototype.onBatch =
+function serv_batch(e)
+{
+ // We should at least get a ref tag.
+ if (e.params.length < 2)
+ return false;
+
+ e.reftag = e.params[1].substring(1);
+ switch (e.params[1][0])
+ {
+ case "+":
+ e.starting = true;
+ break;
+ case "-":
+ e.starting = false;
+ break;
+ default:
+ // Invalid reference tag.
+ return false;
+ }
+ var isPlayback = (this.batches && this.batches[e.reftag] &&
+ this.batches[e.reftag].playback);
+
+ if (!isPlayback)
+ {
+ if (e.starting)
+ {
+ // We're starting a batch, so we also need a type.
+ if (e.params.length < 3)
+ return false;
+
+ if (!this.batches)
+ this.batches = new Object();
+ // The batch object holds the messages queued up as part
+ // of this batch, and a boolean value indicating whether
+ // it is being played back.
+ var newBatch = new Object();
+ newBatch.messages = [e];
+ newBatch.type = e.params[2].toUpperCase();
+ if (e.params[3] && (e.params[3] in this.channels))
+ {
+ newBatch.destObject = this.channels[e.params[3]];
+ }
+ else if (e.params[3] && (e.params[3] in this.users))
+ {
+ newBatch.destObject = this.users[e.params[3]];
+ }
+ else
+ {
+ newBatch.destObject = this.parent;
+ }
+ newBatch.playback = false;
+ this.batches[e.reftag] = newBatch;
+ }
+ else
+ {
+ if (!this.batches[e.reftag])
+ {
+ // Got a close tag without an open tag, so ignore it.
+ return false;
+ }
+
+ var batch = this.batches[e.reftag];
+
+ // Closing the batch, prepare for playback.
+ batch.messages.push(e);
+ batch.playback = true;
+ if (e.tags["batch"])
+ {
+ // We are an inner batch. Append the message queue
+ // to the outer batch's message queue.
+ var parentRef = e.tags["batch"];
+ var parentMsgs = this.batches[parentRef].messages;
+ parentMsgs = parentMsgs.concat(batch.messages);
+ }
+ else
+ {
+ // We are an outer batch. Playback!
+ for (var i = 0; i < batch.messages.length; i++)
+ {
+ var ev = batch.messages[i];
+ ev.type = "parseddata";
+ ev.destObject = this;
+ ev.destMethod = "onParsedData";
+ this.parent.eventPump.routeEvent(ev);
+ }
+ }
+ }
+ return false;
+ }
+ else
+ {
+ // Batch command is ready for handling.
+ e.batchtype = this.batches[e.reftag].type;
+ e.destObject = this.batches[e.reftag].destObject;
+ if (e.destObject.TYPE == "CIRCChannel")
+ {
+ e.set = "channel";
+ }
+ else
+ {
+ e.set = "network";
+ }
+
+ if (!e.starting)
+ {
+ // If we've reached the end of a batch in playback,
+ // do some cleanup.
+ delete this.batches[e.reftag];
+ if (Object.entries(this.batches).length == 0)
+ delete this.batches;
+ }
+
+ // Massage the batchtype into a method name for handlers:
+ // netsplit - onNetsplitBatch
+ // some-batch-type - onSomeBatchTypeBatch
+ // example.com/example - onExampleComExampleBatch
+ var batchCode = e.batchtype.split(/[\.\/-]/).map(function(s)
+ {
+ return s[0].toUpperCase() + s.substr(1).toLowerCase();
+ }).join("");
+ e.destMethod = "on" + batchCode + "Batch";
+
+ if (!e.destObject[e.destMethod])
+ e.destMethod = "onUnknownBatch";
+ }
+}
+
+/* SASL authentication responses */
+CIRCServer.prototype.on902 = /* Nick locked */
+CIRCServer.prototype.on903 = /* Auth success */
+CIRCServer.prototype.on904 = /* Auth failed */
+CIRCServer.prototype.on905 = /* Command too long */
+CIRCServer.prototype.on906 = /* Aborted */
+CIRCServer.prototype.on907 = /* Already authenticated */
+CIRCServer.prototype.on908 = /* Mechanisms */
+function cap_on900(e)
+{
+ if (this.pendingCapNegotiation)
+ {
+ delete this.pendingCapNegotiation;
+ this.sendData("CAP END\n");
+ }
+
+ if (e.code == "908")
+ {
+ // Update our list of SASL mechanics.
+ this.capvals["sasl"] = e.params[2];
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* STARTTLS responses */
+CIRCServer.prototype.on670 = /* Success */
+function cap_on670(e)
+{
+ this.caps["tls"] = true;
+ e.server.connection.startTLS();
+ e.server.isSecure = true;
+ e.server.isStartTLS = true;
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+CIRCServer.prototype.on691 = /* Failure */
+function cap_on691(e)
+{
+ this.caps["tls"] = false;
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* User away status changed */
+CIRCServer.prototype.onAway =
+function serv_away(e)
+{
+ e.user.isAway = e.params[1] ? true : false;
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* User host changed */
+CIRCServer.prototype.onChghost =
+function serv_chghost(e)
+{
+ this.users[e.user.collectionKey].name = e.params[1];
+ this.users[e.user.collectionKey].host = e.params[2];
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+/* user changed the mode */
+CIRCServer.prototype.onMode =
+function serv_mode (e)
+{
+ e.destObject = this;
+ /* modes are not allowed in +channels -> no need to test that here.. */
+ if (arrayIndexOf(this.channelTypes, e.params[1][0]) != -1)
+ {
+ e.channel = new CIRCChannel(this, null, e.params[1]);
+ if ("user" in e && e.user)
+ e.user = new CIRCChanUser(e.channel, e.user.unicodeName);
+ e.type = "chanmode";
+ e.destMethod = "onChanMode";
+ }
+ else
+ {
+ e.type = "usermode";
+ e.destMethod = "onUserMode";
+ }
+
+ return true;
+}
+
+CIRCServer.prototype.onUserMode =
+function serv_usermode (e)
+{
+ e.user = new CIRCUser(this, null, e.params[1])
+ e.user.modestr = e.params[2];
+ e.destObject = this.parent;
+ e.set = "network";
+
+ // usermode usually happens on connect, after the MOTD, so it's a good
+ // place to kick off the lag timer.
+ this.updateLagTimer();
+
+ return true;
+}
+
+CIRCServer.prototype.onChanMode =
+function serv_chanmode (e)
+{
+ var modifier = "";
+ var params_eaten = 0;
+ var BASE_PARAM;
+
+ if (e.code.toUpperCase() == "MODE")
+ BASE_PARAM = 2;
+ else
+ if (e.code == "324")
+ BASE_PARAM = 3;
+ else
+ {
+ dd ("** INVALID CODE in ChanMode event **");
+ return false;
+ }
+
+ var mode_str = e.params[BASE_PARAM];
+ params_eaten++;
+
+ e.modeStr = mode_str;
+ e.usersAffected = new Array();
+
+ var nick;
+ var user;
+ var umList = this.userModes;
+ var cmList = this.channelModes;
+ var modeMap = this.canonicalChanModes;
+ var canonicalModeValue;
+
+ for (var i = 0; i < mode_str.length ; i++)
+ {
+ /* Take care of modifier first. */
+ if ((mode_str[i] == '+') || (mode_str[i] == '-'))
+ {
+ modifier = mode_str[i];
+ continue;
+ }
+
+ var done = false;
+ for (var m in umList)
+ {
+ if ((mode_str[i] == umList[m].mode) && (modifier != ""))
+ {
+ nick = e.params[BASE_PARAM + params_eaten];
+ user = new CIRCChanUser(e.channel, null, nick,
+ [ modifier + umList[m].mode ]);
+ params_eaten++;
+ e.usersAffected.push (user);
+ done = true;
+ break;
+ }
+ }
+ if (done)
+ continue;
+
+ // Update legacy canonical modes if necessary.
+ if (mode_str[i] in modeMap)
+ {
+ // Get the data in case we need it, but don't increment the counter.
+ var datacounter = BASE_PARAM + params_eaten;
+ var data = (datacounter in e.params) ? e.params[datacounter] : null;
+ canonicalModeValue = modeMap[mode_str[i]].getValue(modifier, data);
+ e.channel.mode[modeMap[mode_str[i]].name] = canonicalModeValue;
+ }
+
+ if (arrayContains(cmList.a, mode_str[i]))
+ {
+ var data = e.params[BASE_PARAM + params_eaten++];
+ if (modifier == "+")
+ {
+ e.channel.mode.modeA[data] = true;
+ }
+ else
+ {
+ if (data in e.channel.mode.modeA)
+ {
+ delete e.channel.mode.modeA[data];
+ }
+ else
+ {
+ dd("** Trying to remove channel mode '" + mode_str[i] +
+ "'/'" + data + "' which does not exist in list.");
+ }
+ }
+ }
+ else if (arrayContains(cmList.b, mode_str[i]))
+ {
+ var data = e.params[BASE_PARAM + params_eaten++];
+ if (modifier == "+")
+ {
+ e.channel.mode.modeB[mode_str[i]] = data;
+ }
+ else
+ {
+ // Save 'null' even though we have some data.
+ e.channel.mode.modeB[mode_str[i]] = null;
+ }
+ }
+ else if (arrayContains(cmList.c, mode_str[i]))
+ {
+ if (modifier == "+")
+ {
+ var data = e.params[BASE_PARAM + params_eaten++];
+ e.channel.mode.modeC[mode_str[i]] = data;
+ }
+ else
+ {
+ e.channel.mode.modeC[mode_str[i]] = null;
+ }
+ }
+ else if (arrayContains(cmList.d, mode_str[i]))
+ {
+ e.channel.mode.modeD[mode_str[i]] = (modifier == "+");
+ }
+ else
+ {
+ dd("** UNKNOWN mode symbol '" + mode_str[i] + "' in ChanMode event **");
+ }
+ }
+
+ e.destObject = e.channel;
+ e.set = "channel";
+ return true;
+}
+
+CIRCServer.prototype.onNick =
+function serv_nick (e)
+{
+ var newNick = e.params[1];
+ var newKey = ":" + this.toLowerCase(newNick);
+ var oldKey = e.user.collectionKey;
+ var ev;
+
+ renameProperty (this.users, oldKey, newKey);
+ e.oldNick = e.user.unicodeName;
+ e.user.changeNick(toUnicode(newNick, this));
+
+ for (var c in this.channels)
+ {
+ if (this.channels[c].active &&
+ ((oldKey in this.channels[c].users) || e.user == this.me))
+ {
+ var cuser = this.channels[c].users[oldKey];
+ renameProperty (this.channels[c].users, oldKey, newKey);
+
+ // User must be a channel user, update sort name for userlist,
+ // before we route the event further:
+ cuser.updateSortName();
+
+ ev = new CEvent ("channel", "nick", this.channels[c], "onNick");
+ ev.tags = e.tags;
+ ev.channel = this.channels[c];
+ ev.user = cuser;
+ ev.server = this;
+ ev.oldNick = e.oldNick;
+ this.parent.eventPump.routeEvent(ev);
+ }
+ }
+
+ if (e.user == this.me)
+ {
+ /* if it was me, tell the network about the nick change as well */
+ ev = new CEvent ("network", "nick", this.parent, "onNick");
+ ev.tags = e.tags;
+ ev.user = e.user;
+ ev.server = this;
+ ev.oldNick = e.oldNick;
+ this.parent.eventPump.routeEvent(ev);
+ }
+
+ e.destObject = e.user;
+ e.set = "user";
+
+ return true;
+}
+
+CIRCServer.prototype.onQuit =
+function serv_quit (e)
+{
+ var reason = e.decodeParam(1);
+
+ for (var c in e.server.channels)
+ {
+ if (e.server.channels[c].active &&
+ e.user.collectionKey in e.server.channels[c].users)
+ {
+ var ev = new CEvent ("channel", "quit", e.server.channels[c],
+ "onQuit");
+ ev.tags = e.tags;
+ ev.user = e.server.channels[c].users[e.user.collectionKey];
+ ev.channel = e.server.channels[c];
+ ev.server = ev.channel.parent;
+ ev.reason = reason;
+ this.parent.eventPump.routeEvent(ev);
+ delete e.server.channels[c].users[e.user.collectionKey];
+ }
+ }
+
+ this.users[e.user.collectionKey].lastQuitMessage = reason;
+ this.users[e.user.collectionKey].lastQuitDate = new Date();
+
+ // 0 == prune onQuit.
+ if (this.PRUNE_OLD_USERS == 0)
+ delete this.users[e.user.collectionKey];
+
+ e.reason = reason;
+ e.destObject = e.user;
+ e.set = "user";
+
+ return true;
+}
+
+CIRCServer.prototype.onPart =
+function serv_part (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[1]);
+ e.reason = (e.params.length > 2) ? e.decodeParam(2, e.channel) : "";
+ e.user = new CIRCChanUser(e.channel, e.user.unicodeName);
+ if (userIsMe(e.user))
+ {
+ e.channel.active = false;
+ e.channel.joined = false;
+ }
+ e.channel.removeUser(e.user.encodedName);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+CIRCServer.prototype.onKick =
+function serv_kick (e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[1]);
+ e.lamer = new CIRCChanUser(e.channel, null, e.params[2]);
+ delete e.channel.users[e.lamer.collectionKey];
+ if (userIsMe(e.lamer))
+ {
+ e.channel.active = false;
+ e.channel.joined = false;
+ }
+ e.reason = e.decodeParam(3, e.channel);
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+CIRCServer.prototype.onJoin =
+function serv_join(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[1]);
+ // Passing undefined here because CIRCChanUser doesn't like "null"
+ e.user = new CIRCChanUser(e.channel, e.user.unicodeName, null,
+ undefined, true);
+
+ if (e.params[2] && e.params[3])
+ {
+ var account = (e.params[2] == "*" ? null : e.params[2]);
+ var desc = e.decodeParam([3], e.user);
+ this.users[e.user.collectionKey].account = account;
+ this.users[e.user.collectionKey].desc = desc;
+ }
+
+ if (userIsMe(e.user))
+ {
+ var delayFn1 = function(t) {
+ if (!e.channel.active)
+ return;
+
+ // Give us the channel mode!
+ e.server.sendData("MODE " + e.channel.encodedName + "\n");
+ };
+ // Between 1s - 3s.
+ setTimeout(delayFn1, 1000 + 2000 * Math.random(), this);
+
+ var delayFn2 = function(t) {
+ if (!e.channel.active)
+ return;
+
+ // Get a full list of bans and exceptions, if supported.
+ if (arrayContains(t.channelModes.a, "b"))
+ {
+ e.server.sendData("MODE " + e.channel.encodedName + " +b\n");
+ e.channel.pendingBanList = true;
+ }
+ if (arrayContains(t.channelModes.a, "e"))
+ {
+ e.server.sendData("MODE " + e.channel.encodedName + " +e\n");
+ e.channel.pendingExceptList = true;
+ }
+
+ //If away-notify is active, query the list of users for away status.
+ if (e.server.caps["away-notify"])
+ {
+ // If the server supports extended who, use it.
+ // This lets us initialize the account property.
+ if (e.server.supports["whox"])
+ e.server.who(e.channel.unicodeName + " %acdfhnrstu," + e.server.WHOX_TYPE);
+ else
+ e.server.who(e.channel.unicodeName);
+ }
+ };
+ // Between 10s - 20s.
+ setTimeout(delayFn2, 10000 + 10000 * Math.random(), this);
+
+ /* Clean up the topic, since servers don't always send RPL_NOTOPIC
+ * (no topic set) when joining a channel without a topic. In fact,
+ * the RFC even fails to mention sending a RPL_NOTOPIC after a join!
+ */
+ e.channel.topic = "";
+ e.channel.topicBy = null;
+ e.channel.topicDate = null;
+
+ // And we're in!
+ e.channel.active = true;
+ e.channel.joined = true;
+ }
+
+ e.destObject = e.channel;
+ e.set = "channel";
+
+ return true;
+}
+
+CIRCServer.prototype.onAccount =
+function serv_acct(e)
+{
+ var account = (e.params[1] == "*" ? null : e.params[1]);
+ this.users[e.user.collectionKey].account = account;
+
+ return true;
+}
+
+CIRCServer.prototype.onPing =
+function serv_ping (e)
+{
+ /* non-queued send, so we can calcualte lag */
+ this.connection.sendData("PONG :" + e.params[1] + "\n");
+ this.updateLagTimer();
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+CIRCServer.prototype.onPong =
+function serv_pong (e)
+{
+ if (e.params[2] != "LAGTIMER")
+ return true;
+
+ if (this.lastPingSent)
+ this.lag = (new Date() - this.lastPingSent) / 1000;
+
+ this.lastPingSent = null;
+
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+CIRCServer.prototype.onInvite =
+function serv_invite(e)
+{
+ e.channel = new CIRCChannel(this, null, e.params[2]);
+
+ e.destObject = this.parent;
+ e.set = "network";
+}
+
+CIRCServer.prototype.onNotice =
+CIRCServer.prototype.onPrivmsg =
+CIRCServer.prototype.onTagmsg =
+function serv_notice_privmsg (e)
+{
+ var targetName = e.params[1];
+
+ if (this.userModes)
+ {
+ // Strip off one (and only one) user mode prefix.
+ for (var i = 0; i < this.userModes.length; i++)
+ {
+ if (targetName[0] == this.userModes[i].symbol)
+ {
+ e.msgPrefix = this.userModes[i];
+ targetName = targetName.substr(1);
+ break;
+ }
+ }
+ }
+
+ /* setting replyTo provides a standard place to find the target for */
+ /* replies associated with this event. */
+ if (arrayIndexOf(this.channelTypes, targetName[0]) != -1)
+ {
+ e.channel = new CIRCChannel(this, null, targetName);
+ if ("user" in e)
+ e.user = new CIRCChanUser(e.channel, e.user.unicodeName);
+ e.replyTo = e.channel;
+ e.set = "channel";
+ }
+ else if (!("user" in e))
+ {
+ e.set = "network";
+ e.destObject = this.parent;
+ return true;
+ }
+ else
+ {
+ e.set = "user";
+ e.replyTo = e.user; /* send replies to the user who sent the message */
+ }
+
+ /* The capability identify-msg adds a + or - in front the message to
+ * indicate their network registration status.
+ */
+ if (("identify-msg" in this.caps) && this.caps["identify-msg"])
+ {
+ e.identifyMsg = false;
+ var flag = e.params[2].substring(0,1);
+ if (flag == "+")
+ {
+ e.identifyMsg = true;
+ e.params[2] = e.params[2].substring(1);
+ }
+ else if (flag == "-")
+ {
+ e.params[2] = e.params[2].substring(1);
+ }
+ else
+ {
+ // Just print to console on failure - or we'd spam the user
+ dd("Warning: IDENTIFY-MSG is on, but there's no message flags");
+ }
+ }
+
+ // TAGMSG doesn't have a message parameter, so just pass it on.
+ if (e.code == "TAGMSG")
+ {
+ e.destObject = e.replyTo;
+ return true;
+ }
+
+ if (e.params[2].search (/^\x01[^ ]+.*\x01$/) != -1)
+ {
+ if (e.code == "NOTICE")
+ {
+ e.type = "ctcp-reply";
+ e.destMethod = "onCTCPReply";
+ }
+ else // e.code == "PRIVMSG"
+ {
+ e.type = "ctcp";
+ e.destMethod = "onCTCP";
+ }
+ e.set = "server";
+ e.destObject = this;
+ }
+ else
+ {
+ e.msg = e.decodeParam(2, e.replyTo);
+ e.destObject = e.replyTo;
+ }
+
+ return true;
+}
+
+CIRCServer.prototype.onWallops =
+function serv_wallops(e)
+{
+ if (("user" in e) && e.user)
+ {
+ e.msg = e.decodeParam(1, e.user);
+ e.replyTo = e.user;
+ }
+ else
+ {
+ e.msg = e.decodeParam(1);
+ e.replyTo = this;
+ }
+
+ e.destObject = this.parent;
+ e.set = "network";
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPReply =
+function serv_ctcpr (e)
+{
+ var ary = e.params[2].match (/^\x01([^ ]+) ?(.*)\x01$/i);
+
+ if (ary == null)
+ return false;
+
+ e.CTCPData = ary[2] ? ary[2] : "";
+
+ e.CTCPCode = ary[1].toLowerCase();
+ e.type = "ctcp-reply-" + e.CTCPCode;
+ e.destMethod = "onCTCPReply" + ary[1][0].toUpperCase() +
+ ary[1].substr (1, ary[1].length).toLowerCase();
+
+ if (typeof this[e.destMethod] != "function")
+ { /* if there's no place to land the event here, try to forward it */
+ e.destObject = this.parent;
+ e.set = "network";
+
+ if (typeof e.destObject[e.destMethod] != "function")
+ { /* if there's no place to forward it, send it to unknownCTCP */
+ e.type = "unk-ctcp-reply";
+ e.destMethod = "onUnknownCTCPReply";
+ if (e.destMethod in this)
+ {
+ e.set = "server";
+ e.destObject = this;
+ }
+ else
+ {
+ e.set = "network";
+ e.destObject = this.parent;
+ }
+ }
+ }
+ else
+ e.destObject = this;
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCP =
+function serv_ctcp (e)
+{
+ var ary = e.params[2].match (/^\x01([^ ]+) ?(.*)\x01$/i);
+
+ if (ary == null)
+ return false;
+
+ e.CTCPData = ary[2] ? ary[2] : "";
+
+ e.CTCPCode = ary[1].toLowerCase();
+ if (e.CTCPCode.search (/^reply/i) == 0)
+ {
+ dd ("dropping spoofed reply.");
+ return false;
+ }
+
+ e.CTCPCode = toUnicode(e.CTCPCode, e.replyTo);
+ e.CTCPData = toUnicode(e.CTCPData, e.replyTo);
+
+ e.type = "ctcp-" + e.CTCPCode;
+ e.destMethod = "onCTCP" + ary[1][0].toUpperCase() +
+ ary[1].substr (1, ary[1].length).toLowerCase();
+
+ if (typeof this[e.destMethod] != "function")
+ { /* if there's no place to land the event here, try to forward it */
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+
+ if (typeof e.replyTo[e.destMethod] != "function")
+ { /* if there's no place to forward it, send it to unknownCTCP */
+ e.type = "unk-ctcp";
+ e.destMethod = "onUnknownCTCP";
+ }
+ }
+ else
+ e.destObject = this;
+
+ var ev = new CEvent("server", "ctcp-receive", this, "onReceiveCTCP");
+ ev.tags = e.tags;
+ ev.server = this;
+ ev.CTCPCode = e.CTCPCode;
+ ev.CTCPData = e.CTCPData;
+ ev.type = e.type;
+ ev.user = e.user;
+ ev.destObject = this.parent;
+ this.parent.eventPump.addEvent(ev);
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPClientinfo =
+function serv_ccinfo (e)
+{
+ var clientinfo = new Array();
+
+ if (e.CTCPData)
+ {
+ var cmdName = "onCTCP" + e.CTCPData[0].toUpperCase() +
+ e.CTCPData.substr (1, e.CTCPData.length).toLowerCase();
+ var helpName = cmdName.replace(/^onCTCP/, "CTCPHelp");
+
+ // Check we support the command.
+ if (cmdName in this)
+ {
+ // Do we have help for it?
+ if (helpName in this)
+ {
+ var msg;
+ if (typeof this[helpName] == "function")
+ msg = this[helpName]();
+ else
+ msg = this[helpName];
+
+ e.user.ctcp("CLIENTINFO", msg, "NOTICE");
+ }
+ else
+ {
+ e.user.ctcp("CLIENTINFO",
+ getMsg(MSG_ERR_NO_CTCP_HELP, e.CTCPData), "NOTICE");
+ }
+ }
+ else
+ {
+ e.user.ctcp("CLIENTINFO",
+ getMsg(MSG_ERR_NO_CTCP_CMD, e.CTCPData), "NOTICE");
+ }
+ return true;
+ }
+
+ for (var fname in this)
+ {
+ var ary = fname.match(/^onCTCP(.+)/);
+ if (ary && ary[1].search(/^Reply/) == -1)
+ clientinfo.push (ary[1].toUpperCase());
+ }
+
+ e.user.ctcp("CLIENTINFO", clientinfo.join(" "), "NOTICE");
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPAction =
+function serv_cact (e)
+{
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+}
+
+CIRCServer.prototype.onCTCPFinger =
+function serv_cfinger (e)
+{
+ e.user.ctcp("FINGER", this.parent.INITIAL_DESC, "NOTICE");
+ return true;
+}
+
+CIRCServer.prototype.onCTCPTime =
+function serv_cping (e)
+{
+ e.user.ctcp("TIME", new Date(), "NOTICE");
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPVersion =
+function serv_cver (e)
+{
+ var lines = e.server.VERSION_RPLY.split ("\n");
+
+ for (var i in lines)
+ e.user.ctcp("VERSION", lines[i], "NOTICE");
+
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPSource =
+function serv_csrc (e)
+{
+ e.user.ctcp("SOURCE", this.SOURCE_RPLY, "NOTICE");
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPOs =
+function serv_os(e)
+{
+ e.user.ctcp("OS", this.OS_RPLY, "NOTICE");
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPHost =
+function serv_host(e)
+{
+ e.user.ctcp("HOST", this.HOST_RPLY, "NOTICE");
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPPing =
+function serv_cping (e)
+{
+ /* non-queued send */
+ this.connection.sendData("NOTICE " + e.user.encodedName + " :\01PING " +
+ e.CTCPData + "\01\n");
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+
+ return true;
+}
+
+CIRCServer.prototype.onCTCPDcc =
+function serv_dcc (e)
+{
+ var ary = e.CTCPData.match (/([^ ]+)? ?(.*)/);
+
+ e.DCCData = ary[2];
+ e.type = "dcc-" + ary[1].toLowerCase();
+ e.destMethod = "onDCC" + ary[1][0].toUpperCase() +
+ ary[1].substr (1, ary[1].length).toLowerCase();
+
+ if (typeof this[e.destMethod] != "function")
+ { /* if there's no place to land the event here, try to forward it */
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+ }
+ else
+ e.destObject = this;
+
+ return true;
+}
+
+CIRCServer.prototype.onDCCChat =
+function serv_dccchat (e)
+{
+ var ary = e.DCCData.match (/(chat) (\d+) (\d+)/i);
+
+ if (ary == null)
+ return false;
+
+ e.id = ary[2];
+ // Longword --> dotted IP conversion.
+ var host = Number(e.id);
+ e.host = ((host >> 24) & 0xFF) + "." +
+ ((host >> 16) & 0xFF) + "." +
+ ((host >> 8) & 0xFF) + "." +
+ (host & 0xFF);
+ e.port = Number(ary[3]);
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+
+ return true;
+}
+
+CIRCServer.prototype.onDCCSend =
+function serv_dccsend (e)
+{
+ var ary = e.DCCData.match(/([^ ]+) (\d+) (\d+) (\d+)/);
+
+ /* Just for mIRC: filenames with spaces may be enclosed in double-quotes.
+ * (though by default it replaces spaces with underscores, but we might as
+ * well cope). */
+ if ((ary[1][0] == '"') || (ary[1][ary[1].length - 1] == '"'))
+ ary = e.DCCData.match(/"(.+)" (\d+) (\d+) (\d+)/);
+
+ if (ary == null)
+ return false;
+
+ e.file = ary[1];
+ e.id = ary[2];
+ // Longword --> dotted IP conversion.
+ var host = Number(e.id);
+ e.host = ((host >> 24) & 0xFF) + "." +
+ ((host >> 16) & 0xFF) + "." +
+ ((host >> 8) & 0xFF) + "." +
+ (host & 0xFF);
+ e.port = Number(ary[3]);
+ e.size = Number(ary[4]);
+ e.destObject = e.replyTo;
+ e.set = (e.replyTo == e.user) ? "user" : "channel";
+
+ return true;
+}
+
+function CIRCChannel(parent, unicodeName, encodedName)
+{
+ // Both unicodeName and encodedName are optional, but at least one must be
+ // present.
+
+ if (!encodedName && !unicodeName)
+ throw "Hey! Come on, I need either an encoded or a Unicode name.";
+ if (!encodedName)
+ encodedName = fromUnicode(unicodeName, parent);
+
+ let collectionKey = ":" + parent.toLowerCase(encodedName);
+ if (collectionKey in parent.channels)
+ return parent.channels[collectionKey];
+
+ this.parent = parent;
+ this.encodedName = encodedName;
+ this.canonicalName = collectionKey.substr(1);
+ this.collectionKey = collectionKey;
+ this.unicodeName = unicodeName || toUnicode(encodedName, this);
+ this.viewName = this.unicodeName;
+
+ this.users = new Object();
+ this.bans = new Object();
+ this.excepts = new Object();
+ this.mode = new CIRCChanMode(this);
+ this.usersStable = true;
+ /* These next two flags represent a subtle difference in state:
+ * active - in the channel, from the server's point of view.
+ * joined - in the channel, from the user's point of view.
+ * e.g. parting the channel clears both, but being disconnected only
+ * clears |active| - the user still wants to be in the channel, even
+ * though they aren't physically able to until we've reconnected.
+ */
+ this.active = false;
+ this.joined = false;
+
+ this.parent.channels[this.collectionKey] = this;
+ if ("onInit" in this)
+ this.onInit();
+
+ return this;
+}
+
+CIRCChannel.prototype.TYPE = "IRCChannel";
+CIRCChannel.prototype.topic = "";
+
+// Returns the IRC URL representation of this channel.
+CIRCChannel.prototype.getURL =
+function chan_geturl()
+{
+ var target = this.encodedName;
+ var flags = this.mode.key ? ["needkey"] : [];
+
+ if ((target[0] == "#") && (target.length > 1) &&
+ arrayIndexOf(this.parent.channelTypes, target[1]) == -1)
+ {
+ /* First character is "#" (which we're allowed to omit), and the
+ * following character is NOT a valid prefix, so it's safe to remove.
+ */
+ target = target.substr(1);
+ }
+ return this.parent.parent.getURL(target, flags);
+}
+
+CIRCChannel.prototype.rehome =
+function chan_rehome(newParent)
+{
+ delete this.parent.channels[this.collectionKey];
+ this.parent = newParent;
+ this.parent.channels[this.collectionKey] = this;
+}
+
+CIRCChannel.prototype.addUser =
+function chan_adduser (unicodeName, modes)
+{
+ return new CIRCChanUser(this, unicodeName, null, modes);
+}
+
+CIRCChannel.prototype.getUser =
+function chan_getuser(nick)
+{
+ // Try assuming it's an encodedName first.
+ let tnick = ":" + this.parent.toLowerCase(nick);
+ if (tnick in this.users)
+ return this.users[tnick];
+
+ // Ok, failed, so try assuming it's a unicodeName.
+ tnick = ":" + this.parent.toLowerCase(fromUnicode(nick, this.parent));
+ if (tnick in this.users)
+ return this.users[tnick];
+
+ return null;
+}
+
+CIRCChannel.prototype.removeUser =
+function chan_removeuser(nick)
+{
+ // Try assuming it's an encodedName first.
+ let key = ":" + this.parent.toLowerCase(nick);
+ if (key in this.users)
+ delete this.users[key]; // see ya
+
+ // Ok, failed, so try assuming it's a unicodeName.
+ key = ":" + this.parent.toLowerCase(fromUnicode(nick, this.parent));
+ if (key in this.users)
+ delete this.users[key];
+}
+
+CIRCChannel.prototype.getUsersLength =
+function chan_userslen (mode)
+{
+ var i = 0;
+ var p;
+ this.opCount = 0;
+ this.halfopCount = 0;
+ this.voiceCount = 0;
+
+ if (typeof mode == "undefined")
+ {
+ for (p in this.users)
+ {
+ if (this.users[p].isOp)
+ this.opCount++;
+ if (this.users[p].isHalfOp)
+ this.halfopCount++;
+ if (this.users[p].isVoice)
+ this.voiceCount++;
+ i++;
+ }
+ }
+ else
+ {
+ for (p in this.users)
+ if (arrayContains(this.users[p].modes, mode))
+ i++;
+ }
+
+ return i;
+}
+
+CIRCChannel.prototype.iAmOp =
+function chan_amop()
+{
+ return this.active && this.users[this.parent.me.collectionKey].isOp;
+}
+
+CIRCChannel.prototype.iAmHalfOp =
+function chan_amhalfop()
+{
+ return this.active && this.users[this.parent.me.collectionKey].isHalfOp;
+}
+
+CIRCChannel.prototype.iAmVoice =
+function chan_amvoice()
+{
+ return this.active && this.users[this.parent.me.collectionKey].isVoice;
+}
+
+CIRCChannel.prototype.setTopic =
+function chan_topic (str)
+{
+ this.parent.sendData ("TOPIC " + this.encodedName + " :" +
+ fromUnicode(str, this) + "\n");
+}
+
+CIRCChannel.prototype.say =
+function chan_say (msg)
+{
+ this.parent.sayTo(this.encodedName, fromUnicode(msg, this));
+}
+
+CIRCChannel.prototype.act =
+function chan_say (msg)
+{
+ this.parent.actTo(this.encodedName, fromUnicode(msg, this));
+}
+
+CIRCChannel.prototype.notice =
+function chan_notice (msg)
+{
+ this.parent.noticeTo(this.encodedName, fromUnicode(msg, this));
+}
+
+CIRCChannel.prototype.ctcp =
+function chan_ctcpto (code, msg, type)
+{
+ msg = msg || "";
+ type = type || "PRIVMSG";
+
+ this.parent.ctcpTo(this.encodedName, fromUnicode(code, this),
+ fromUnicode(msg, this), type);
+}
+
+CIRCChannel.prototype.join =
+function chan_join (key)
+{
+ if (!key)
+ key = "";
+
+ this.parent.sendData ("JOIN " + this.encodedName + " " + key + "\n");
+ return true;
+}
+
+CIRCChannel.prototype.part =
+function chan_part (reason)
+{
+ if (!reason)
+ reason = "";
+ this.parent.sendData ("PART " + this.encodedName + " :" +
+ fromUnicode(reason, this) + "\n");
+ this.users = new Object();
+ return true;
+}
+
+/**
+ * Invites a user to a channel.
+ *
+ * @param nick the user name to invite.
+ */
+CIRCChannel.prototype.invite =
+function chan_inviteuser (nick)
+{
+ var rawNick = fromUnicode(nick, this.parent);
+ this.parent.sendData("INVITE " + rawNick + " " + this.encodedName + "\n");
+ return true;
+}
+
+CIRCChannel.prototype.findUsers =
+function chan_findUsers(mask)
+{
+ var ary = [];
+ var unchecked = 0;
+ mask = getHostmaskParts(mask);
+ for (var nick in this.users)
+ {
+ var user = this.users[nick];
+ if (!user.host || !user.name)
+ unchecked++;
+ else if (hostmaskMatches(user, mask))
+ ary.push(user);
+ }
+ return { users: ary, unchecked: unchecked };
+}
+
+/**
+ * Stores a channel's current mode settings.
+ *
+ * You should never need to create an instance of this prototype; access the
+ * channel mode information through the |CIRCChannel.mode| property.
+ *
+ * @param parent The |CIRCChannel| to which this mode belongs.
+ */
+function CIRCChanMode (parent)
+{
+ this.parent = parent;
+
+ this.modeA = new Object();
+ this.modeB = new Object();
+ this.modeC = new Object();
+ this.modeD = new Object();
+
+ this.invite = false;
+ this.moderated = false;
+ this.publicMessages = true;
+ this.publicTopic = true;
+ this.secret = false;
+ this.pvt = false;
+ this.key = "";
+ this.limit = -1;
+}
+
+CIRCChanMode.prototype.TYPE = "IRCChanMode";
+
+// Returns the complete mode string, as constructed from its component parts.
+CIRCChanMode.prototype.getModeStr =
+function chan_modestr (f)
+{
+ var str = "";
+ var modeCparams = "";
+
+ /* modeA are 'list' ones, and so should not be shown.
+ * modeB are 'param' ones, like +k key, so we wont show them either.
+ * modeC are 'on-param' ones, like +l limit, which we will show.
+ * modeD are 'boolean' ones, which we will definitely show.
+ */
+
+ // Add modeD:
+ for (var m in this.modeD)
+ {
+ if (this.modeD[m])
+ str += m;
+ }
+
+ // Add modeC, save parameters for adding all the way at the end:
+ for (var m in this.modeC)
+ {
+ if (this.modeC[m])
+ {
+ str += m;
+ modeCparams += " " + this.modeC[m];
+ }
+ }
+
+ // Add parameters:
+ if (str)
+ str = "+" + str + modeCparams;
+
+ return str;
+}
+
+// Sends the given mode string to the server with the channel pre-filled.
+CIRCChanMode.prototype.setMode =
+function chanm_mode (modestr)
+{
+ this.parent.parent.sendData ("MODE " + this.parent.encodedName + " " +
+ modestr + "\n");
+
+ return true;
+}
+
+// Sets (|n| > 0) or clears (|n| <= 0) the user count limit.
+CIRCChanMode.prototype.setLimit =
+function chanm_limit (n)
+{
+ if ((typeof n == "undefined") || (n <= 0))
+ {
+ this.parent.parent.sendData("MODE " + this.parent.encodedName +
+ " -l\n");
+ }
+ else
+ {
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " +l " +
+ Number(n) + "\n");
+ }
+
+ return true;
+}
+
+// Locks the channel with a given key.
+CIRCChanMode.prototype.lock =
+function chanm_lock (k)
+{
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " +k " +
+ k + "\n");
+ return true;
+}
+
+// Unlocks the channel with a given key.
+CIRCChanMode.prototype.unlock =
+function chan_unlock (k)
+{
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " -k " +
+ k + "\n");
+ return true;
+}
+
+// Sets or clears the moderation mode.
+CIRCChanMode.prototype.setModerated =
+function chan_moderate (f)
+{
+ var modifier = (f) ? "+" : "-";
+
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " " +
+ modifier + "m\n");
+ return true;
+}
+
+// Sets or clears the allow public messages mode.
+CIRCChanMode.prototype.setPublicMessages =
+function chan_pmessages (f)
+{
+ var modifier = (f) ? "-" : "+";
+
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " " +
+ modifier + "n\n");
+ return true;
+}
+
+// Sets or clears the public topic mode.
+CIRCChanMode.prototype.setPublicTopic =
+function chan_ptopic (f)
+{
+ var modifier = (f) ? "-" : "+";
+
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " " +
+ modifier + "t\n");
+ return true;
+}
+
+// Sets or clears the invite-only mode.
+CIRCChanMode.prototype.setInvite =
+function chan_invite (f)
+{
+ var modifier = (f) ? "+" : "-";
+
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " " +
+ modifier + "i\n");
+ return true;
+}
+
+// Sets or clears the private channel mode.
+CIRCChanMode.prototype.setPvt =
+function chan_pvt (f)
+{
+ var modifier = (f) ? "+" : "-";
+
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " " +
+ modifier + "p\n");
+ return true;
+}
+
+// Sets or clears the secret channel mode.
+CIRCChanMode.prototype.setSecret =
+function chan_secret (f)
+{
+ var modifier = (f) ? "+" : "-";
+
+ this.parent.parent.sendData("MODE " + this.parent.encodedName + " " +
+ modifier + "s\n");
+ return true;
+}
+
+function CIRCUser(parent, unicodeName, encodedName, name, host)
+{
+ // Both unicodeName and encodedName are optional, but at least one must be
+ // present.
+
+ if (!encodedName && !unicodeName)
+ throw "Hey! Come on, I need either an encoded or a Unicode name.";
+ if (!encodedName)
+ encodedName = fromUnicode(unicodeName, parent);
+
+ let collectionKey = ":" + parent.toLowerCase(encodedName);
+ if (collectionKey in parent.users)
+ {
+ let existingUser = parent.users[collectionKey];
+ if (name)
+ existingUser.name = name;
+ if (host)
+ existingUser.host = host;
+ return existingUser;
+ }
+
+ this.parent = parent;
+ this.encodedName = encodedName;
+ this.canonicalName = collectionKey.substr(1);
+ this.collectionKey = collectionKey;
+ this.unicodeName = unicodeName || toUnicode(encodedName, this.parent);
+ this.viewName = this.unicodeName;
+
+ this.name = name;
+ this.host = host;
+ this.desc = "";
+ this.account = null;
+ this.connectionHost = null;
+ this.isAway = false;
+ this.modestr = this.parent.parent.INITIAL_UMODE;
+
+ this.parent.users[this.collectionKey] = this;
+ if ("onInit" in this)
+ this.onInit();
+
+ return this;
+}
+
+CIRCUser.prototype.TYPE = "IRCUser";
+
+// Returns the IRC URL representation of this user.
+CIRCUser.prototype.getURL =
+function usr_geturl()
+{
+ return this.parent.parent.getURL(this.encodedName, ["isnick"]);
+}
+
+CIRCUser.prototype.rehome =
+function usr_rehome(newParent)
+{
+ delete this.parent.users[this.collectionKey];
+ this.parent = newParent;
+ this.parent.users[this.collectionKey] = this;
+}
+
+CIRCUser.prototype.changeNick =
+function usr_changenick(unicodeName)
+{
+ this.unicodeName = unicodeName;
+ this.viewName = this.unicodeName;
+ this.encodedName = fromUnicode(this.unicodeName, this.parent);
+ this.canonicalName = this.parent.toLowerCase(this.encodedName);
+ this.collectionKey = ":" + this.canonicalName;
+}
+
+CIRCUser.prototype.getHostMask =
+function usr_hostmask (pfx)
+{
+ pfx = (typeof pfx != "undefined") ? pfx : "*!" + this.name + "@*.";
+ var idx = this.host.indexOf(".");
+ if (idx == -1)
+ return pfx + this.host;
+
+ return (pfx + this.host.substr(idx + 1, this.host.length));
+}
+
+CIRCUser.prototype.getBanMask =
+function usr_banmask()
+{
+ if (!this.host)
+ return this.unicodeName + "!*@*";
+
+ return "*!*@" + this.host;
+}
+
+CIRCUser.prototype.say =
+function usr_say (msg)
+{
+ this.parent.sayTo(this.encodedName, fromUnicode(msg, this));
+}
+
+CIRCUser.prototype.notice =
+function usr_notice (msg)
+{
+ this.parent.noticeTo(this.encodedName, fromUnicode(msg, this));
+}
+
+CIRCUser.prototype.act =
+function usr_act (msg)
+{
+ this.parent.actTo(this.encodedName, fromUnicode(msg, this));
+}
+
+CIRCUser.prototype.ctcp =
+function usr_ctcp (code, msg, type)
+{
+ msg = msg || "";
+ type = type || "PRIVMSG";
+
+ this.parent.ctcpTo(this.encodedName, fromUnicode(code, this),
+ fromUnicode(msg, this), type);
+}
+
+CIRCUser.prototype.whois =
+function usr_whois ()
+{
+ this.parent.whois(this.unicodeName);
+}
+
+/*
+ * channel user
+ */
+function CIRCChanUser(parent, unicodeName, encodedName, modes, userInChannel, name, host)
+{
+ // Both unicodeName and encodedName are optional, but at least one must be
+ // present.
+
+ if (!encodedName && !unicodeName)
+ throw "Hey! Come on, I need either an encoded or a Unicode name.";
+ else if (encodedName && !unicodeName)
+ unicodeName = toUnicode(encodedName, parent);
+ else if (!encodedName && unicodeName)
+ encodedName = fromUnicode(unicodeName, parent);
+
+ // We should have both unicode and encoded names by now.
+ let collectionKey = ":" + parent.parent.toLowerCase(encodedName);
+
+ if (collectionKey in parent.users)
+ {
+ let existingUser = parent.users[collectionKey];
+ if (modes)
+ {
+ // If we start with a single character mode, assume we're replacing
+ // the list. (i.e. the list is either all +/- modes, or all normal)
+ if ((modes.length >= 1) && (modes[0].search(/^[-+]/) == -1))
+ {
+ // Modes, but no +/- prefixes, so *replace* mode list.
+ existingUser.modes = modes;
+ }
+ else
+ {
+ // We have a +/- mode list, so carefully update the mode list.
+ for (var m in modes)
+ {
+ // This will remove '-' modes, and all other modes will be
+ // added.
+ var mode = modes[m][1];
+ if (modes[m][0] == "-")
+ {
+ if (arrayContains(existingUser.modes, mode))
+ {
+ var i = arrayIndexOf(existingUser.modes, mode);
+ arrayRemoveAt(existingUser.modes, i);
+ }
+ }
+ else
+ {
+ if (!arrayContains(existingUser.modes, mode))
+ existingUser.modes.push(mode);
+ }
+ }
+ }
+ }
+ existingUser.isFounder = (arrayContains(existingUser.modes, "q")) ?
+ true : false;
+ existingUser.isAdmin = (arrayContains(existingUser.modes, "a")) ?
+ true : false;
+ existingUser.isOp = (arrayContains(existingUser.modes, "o")) ?
+ true : false;
+ existingUser.isHalfOp = (arrayContains(existingUser.modes, "h")) ?
+ true : false;
+ existingUser.isVoice = (arrayContains(existingUser.modes, "v")) ?
+ true : false;
+ existingUser.updateSortName();
+ return existingUser;
+ }
+
+ var protoUser = new CIRCUser(parent.parent, unicodeName, encodedName, name, host);
+
+ this.__proto__ = protoUser;
+ this.getURL = cusr_geturl;
+ this.setOp = cusr_setop;
+ this.setHalfOp = cusr_sethalfop;
+ this.setVoice = cusr_setvoice;
+ this.setBan = cusr_setban;
+ this.kick = cusr_kick;
+ this.kickBan = cusr_kban;
+ this.say = cusr_say;
+ this.notice = cusr_notice;
+ this.act = cusr_act;
+ this.whois = cusr_whois;
+ this.updateSortName = cusr_updatesortname;
+ this.parent = parent;
+ this.TYPE = "IRCChanUser";
+
+ this.modes = new Array();
+ if (typeof modes != "undefined")
+ this.modes = modes;
+ this.isFounder = (arrayContains(this.modes, "q")) ? true : false;
+ this.isAdmin = (arrayContains(this.modes, "a")) ? true : false;
+ this.isOp = (arrayContains(this.modes, "o")) ? true : false;
+ this.isHalfOp = (arrayContains(this.modes, "h")) ? true : false;
+ this.isVoice = (arrayContains(this.modes, "v")) ? true : false;
+ this.updateSortName();
+
+ if (userInChannel)
+ parent.users[this.collectionKey] = this;
+
+ return this;
+}
+
+function cusr_updatesortname()
+{
+ // Check for the highest mode the user has (for sorting the userlist)
+ const userModes = this.parent.parent.userModes;
+ var modeLevel = 0;
+ var mode;
+ for (var i = 0; i < this.modes.length; i++)
+ {
+ for (var j = 0; j < userModes.length; j++)
+ {
+ if (userModes[j].mode == this.modes[i])
+ {
+ if (userModes.length - j > modeLevel)
+ {
+ modeLevel = userModes.length - j;
+ mode = userModes[j];
+ }
+ break;
+ }
+ }
+ }
+ // Counts numerically down from 9.
+ this.sortName = (9 - modeLevel) + "-" + this.unicodeName;
+}
+
+function cusr_geturl()
+{
+ // Don't ask.
+ return this.parent.parent.parent.getURL(this.encodedName, ["isnick"]);
+}
+
+function cusr_setop(f)
+{
+ var server = this.parent.parent;
+ var me = server.me;
+
+ var modifier = (f) ? " +o " : " -o ";
+ server.sendData("MODE " + this.parent.encodedName + modifier + this.encodedName + "\n");
+
+ return true;
+}
+
+function cusr_sethalfop (f)
+{
+ var server = this.parent.parent;
+ var me = server.me;
+
+ var modifier = (f) ? " +h " : " -h ";
+ server.sendData("MODE " + this.parent.encodedName + modifier + this.encodedName + "\n");
+
+ return true;
+}
+
+function cusr_setvoice (f)
+{
+ var server = this.parent.parent;
+ var me = server.me;
+
+ var modifier = (f) ? " +v " : " -v ";
+ server.sendData("MODE " + this.parent.encodedName + modifier + this.encodedName + "\n");
+
+ return true;
+}
+
+function cusr_kick (reason)
+{
+ var server = this.parent.parent;
+ var me = server.me;
+
+ reason = typeof reason == "string" ? reason : "";
+
+ server.sendData("KICK " + this.parent.encodedName + " " + this.encodedName + " :" +
+ fromUnicode(reason, this) + "\n");
+
+ return true;
+}
+
+function cusr_setban (f)
+{
+ var server = this.parent.parent;
+ var me = server.me;
+
+ if (!this.host)
+ return false;
+
+ var modifier = (f) ? " +b " : " -b ";
+ modifier += fromUnicode(this.getBanMask(), server) + " ";
+
+ server.sendData("MODE " + this.parent.encodedName + modifier + "\n");
+
+ return true;
+}
+
+function cusr_kban (reason)
+{
+ var server = this.parent.parent;
+ var me = server.me;
+
+ if (!this.host)
+ return false;
+
+ reason = (typeof reason != "undefined") ? reason : this.encodedName;
+ var modifier = " -o+b " + this.encodedName + " " +
+ fromUnicode(this.getBanMask(), server) + " ";
+
+ server.sendData("MODE " + this.parent.encodedName + modifier + "\n" +
+ "KICK " + this.parent.encodedName + " " +
+ this.encodedName + " :" + reason + "\n");
+
+ return true;
+}
+
+function cusr_say (msg)
+{
+ this.__proto__.say (msg);
+}
+
+function cusr_notice (msg)
+{
+ this.__proto__.notice (msg);
+}
+
+function cusr_act (msg)
+{
+ this.__proto__.act (msg);
+}
+
+function cusr_whois ()
+{
+ this.__proto__.whois ();
+}
+
+
+// IRC URL parsing and generating
+
+function parseIRCURL(url)
+{
+ var specifiedHost = "";
+
+ var rv = new Object();
+ rv.spec = url;
+ rv.scheme = url.split(":")[0];
+ rv.host = null;
+ rv.target = "";
+ rv.port = (rv.scheme == "ircs" ? 6697 : 6667);
+ rv.msg = "";
+ rv.pass = null;
+ rv.key = null;
+ rv.charset = null;
+ rv.needpass = false;
+ rv.needkey = false;
+ rv.isnick = false;
+ rv.isserver = false;
+
+ if (url.search(/^(ircs?:\/?\/?)$/i) != -1)
+ return rv;
+
+ /* split url into <host>/<everything-else> pieces */
+ var ary = url.match(/^ircs?:\/\/([^\/\s]+)?(\/[^\s]*)?$/i);
+ if (!ary || !ary[1])
+ {
+ dd("parseIRCURL: initial split failed");
+ return null;
+ }
+ var host = ary[1];
+ var rest = arrayHasElementAt(ary, 2) ? ary[2] : "";
+
+ /* split <host> into server (or network) / port */
+ ary = host.match(/^([^\:]+|\[[^\]]+\])(\:\d+)?$/i);
+ if (!ary)
+ {
+ dd("parseIRCURL: host/port split failed");
+ return null;
+ }
+
+ // 1 = hostname or IPv4 address, 2 = port.
+ specifiedHost = rv.host = ary[1].toLowerCase();
+ rv.isserver = arrayHasElementAt(ary, 2) || /\.|:/.test(specifiedHost);
+ if (arrayHasElementAt(ary, 2))
+ rv.port = parseInt(ary[2].substr(1));
+
+ if (rest)
+ {
+ ary = rest.match(/^\/([^\?\s\/,]*)?\/?(,[^\?]*)?(\?.*)?$/);
+ if (!ary)
+ {
+ dd("parseIRCURL: rest split failed ``" + rest + "''");
+ return null;
+ }
+
+ rv.target = arrayHasElementAt(ary, 1) ? ecmaUnescape(ary[1]) : "";
+
+ if (rv.target.search(/[\x07,\s]/) != -1)
+ {
+ dd("parseIRCURL: invalid characters in channel name");
+ return null;
+ }
+
+ var params = arrayHasElementAt(ary, 2) ? ary[2].toLowerCase() : "";
+ var query = arrayHasElementAt(ary, 3) ? ary[3] : "";
+
+ if (params)
+ {
+ params = params.split(",");
+ while (params.length)
+ {
+ var param = params.pop();
+ // split doesn't take out empty bits:
+ if (param == "")
+ continue;
+ switch (param)
+ {
+ case "isnick":
+ rv.isnick = true;
+ if (!rv.target)
+ {
+ dd("parseIRCURL: isnick w/o target");
+ /* isnick w/o a target is bogus */
+ return null;
+ }
+ break;
+
+ case "isserver":
+ rv.isserver = true;
+ if (!specifiedHost)
+ {
+ dd("parseIRCURL: isserver w/o host");
+ /* isserver w/o a host is bogus */
+ return null;
+ }
+ break;
+
+ case "needpass":
+ case "needkey":
+ rv[param] = true;
+ break;
+
+ default:
+ /* If we didn't understand it, ignore but warn: */
+ dd("parseIRCURL: Unrecognized param '" + param +
+ "' in URL!");
+ }
+ }
+ }
+
+ if (query)
+ {
+ ary = query.substr(1).split("&");
+ while (ary.length)
+ {
+ var arg = ary.pop().split("=");
+ /*
+ * we don't want to accept *any* query, or folks could
+ * say things like "target=foo", and overwrite what we've
+ * already parsed, so we only use query args we know about.
+ */
+ switch (arg[0].toLowerCase())
+ {
+ case "msg":
+ rv.msg = ecmaUnescape(arg[1]).replace("\n", "\\n");
+ break;
+
+ case "pass":
+ rv.needpass = true;
+ rv.pass = ecmaUnescape(arg[1]).replace("\n", "\\n");
+ break;
+
+ case "key":
+ rv.needkey = true;
+ rv.key = ecmaUnescape(arg[1]).replace("\n", "\\n");
+ break;
+
+ case "charset":
+ rv.charset = ecmaUnescape(arg[1]).replace("\n", "\\n");
+ break;
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+function constructIRCURL(obj)
+{
+ function parseQuery(obj)
+ {
+ var rv = new Array();
+ if ("msg" in obj)
+ rv.push("msg=" + ecmaEscape(obj.msg.replace("\\n", "\n")));
+ if ("pass" in obj)
+ rv.push("pass=" + ecmaEscape(obj.pass.replace("\\n", "\n")));
+ if ("key" in obj)
+ rv.push("key=" + ecmaEscape(obj.key.replace("\\n", "\n")));
+ if ("charset" in obj)
+ rv.push("charset=" + ecmaEscape(obj.charset.replace("\\n", "\n")));
+
+ return rv.length ? "?" + rv.join("&") : "";
+ };
+ function parseFlags(obj)
+ {
+ var rv = new Array();
+ var haveTarget = ("target" in obj) && obj.target;
+ if (("needpass" in obj) && obj.needpass)
+ rv.push(",needpass");
+ if (("needkey" in obj) && obj.needkey && haveTarget)
+ rv.push(",needkey");
+ if (("isnick" in obj) && obj.isnick && haveTarget)
+ rv.push(",isnick");
+
+ return rv.join("");
+ };
+
+ var flags = "";
+ var scheme = ("scheme" in obj) ? obj.scheme : "irc";
+ if (!("host" in obj) || !obj.host)
+ return scheme + "://";
+
+ var url = scheme + "://" + obj.host;
+
+ // Add port if non-standard:
+ if (("port" in obj) && (((scheme == "ircs") && (obj.port != 6697)) ||
+ ((scheme == "irc") && (obj.port != 6667))))
+ {
+ url += ":" + obj.port;
+ }
+ // Need to add ",isserver" if there's no port and no dots in the hostname:
+ else if (("isserver" in obj) && obj.isserver &&
+ (obj.host.indexOf(".") == -1))
+ {
+ flags += ",isserver";
+ }
+ url += "/";
+
+ if (("target" in obj) && obj.target)
+ {
+ if (obj.target.search(/[\x07,\s]/) != -1)
+ {
+ dd("parseIRCObject: invalid characters in channel/nick name");
+ return null;
+ }
+ url += ecmaEscape(obj.target).replace(/\//g, "%2f");
+ }
+
+ return url + flags + parseFlags(obj) + parseQuery(obj);
+}
+
+/* Canonicalizing an IRC URL removes all items which aren't necessary to
+ * identify the target. For example, an IRC URL with ?pass=password and one
+ * without (but otherwise identical) are refering to the same target, so
+ * ?pass= is removed.
+ */
+function makeCanonicalIRCURL(url)
+{
+ var canonicalProps = { scheme: true, host: true, port: true,
+ target: true, isserver: true, isnick: true };
+
+ var urlObject = parseIRCURL(url);
+ if (!urlObject)
+ return ""; // Input wasn't a valid IRC URL.
+ for (var prop in urlObject)
+ {
+ if (!(prop in canonicalProps))
+ delete urlObject[prop];
+ }
+ return constructIRCURL(urlObject);
+}
diff --git a/comm/suite/chatzilla/js/lib/json-serializer.js b/comm/suite/chatzilla/js/lib/json-serializer.js
new file mode 100644
index 0000000000..93333f2bb1
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/json-serializer.js
@@ -0,0 +1,103 @@
+/* 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/. */
+
+/* This is a simple set of functions for serializing and parsing JS objects
+ * to and from files.
+ */
+
+function JSONSerializer(file) {
+ if (typeof file == "string")
+ this._file = new nsLocalFile(file);
+ else
+ this._file = file;
+ this._open = false;
+}
+
+JSONSerializer.prototype = {
+ /*
+ * Opens the serializer on the file specified when created, in either the read
+ * ("<") or write (">") directions. When the file is open, only the
+ * appropriate direction of serialization/deserialization may be performed.
+ *
+ * Note: serialize and deserialize automatically open the file if it is not
+ * open.
+ *
+ * @param dir The string representing the direction of serialization.
+ * @returns Value indicating whether the file was opened successfully.
+ */
+ open: function(dir) {
+ if (!ASSERT((dir == ">") || (dir == "<"), "Bad serialization direction!")) {
+ return false;
+ }
+ if (this._open) {
+ return false;
+ }
+
+ this._fileStream = new LocalFile(this._file, dir);
+ if ((typeof this._fileStream == "object") && this._fileStream) {
+ this._open = true;
+ }
+
+ return this._open;
+ },
+
+ /*
+ * Closes the file stream and ends reading or writing.
+ *
+ * @returns Value indicating whether the file was closed successfully.
+ */
+ close: function() {
+ if (this._open) {
+ this._fileStream.close();
+ delete this._fileStream;
+ this._open = false;
+ }
+ return true;
+ },
+
+ /*
+ * Serializes a single object into the file stream. All properties of the
+ * object are stored in the stream, including properties that contain other
+ * objects.
+ *
+ * @param obj JS object to serialize to the file.
+ */
+ serialize: function(obj) {
+ if (!this._open) {
+ this.open(">");
+ }
+ if (!ASSERT(this._open, "Unable to open the file for writing!")) {
+ return;
+ }
+
+ this._fileStream.write(JSON.stringify(obj, null, 2));
+ },
+
+ /*
+ * Reads in enough of the file to deserialize (realize) a single object. The
+ * object deserialized is returned; all sub-properties of the object are
+ * deserialized with it.
+ *
+ * @returns JS object parsed from the file.
+ */
+ deserialize: function() {
+ if (!this._open) {
+ this.open("<");
+ }
+ if (!ASSERT(this._open, "Unable to open the file for reading!"))
+ return false;
+
+ let rv = null;
+ try {
+ rv = JSON.parse(this._fileStream.read());
+ }
+ catch(ex) {
+ dd("Syntax error while deserializing file!");
+ dd(ex.message);
+ dd(ex.stack);
+ }
+
+ return rv;
+ },
+};
diff --git a/comm/suite/chatzilla/js/lib/menu-manager.js b/comm/suite/chatzilla/js/lib/menu-manager.js
new file mode 100644
index 0000000000..6fd0686833
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/menu-manager.js
@@ -0,0 +1,848 @@
+/* -*- 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/. */
+
+//
+function MenuManager(commandManager, menuSpecs, contextFunction, commandStr)
+{
+ var menuManager = this;
+
+ this.commandManager = commandManager;
+ this.menuSpecs = menuSpecs;
+ this.contextFunction = contextFunction;
+ this.commandStr = commandStr;
+ this.repeatId = 0;
+ this.cxStore = new Object();
+
+ this.onPopupShowing =
+ function mmgr_onshow(event) { return menuManager.showPopup(event); };
+ this.onPopupHiding =
+ function mmgr_onhide(event) { return menuManager.hidePopup(event); };
+ this.onMenuCommand =
+ function mmgr_oncmd(event) { return menuManager.menuCommand(event); };
+
+ /* The code using us may override these with functions which will be called
+ * after all our internal processing is done. Both are called with the
+ * arguments 'event' (DOM), 'cx' (JS), 'popup' (DOM).
+ */
+ this.onCallbackPopupShowing = null;
+ this.onCallbackPopupHiding = null;
+}
+
+MenuManager.prototype.appendMenuItems =
+function mmgr_append(menuId, items)
+{
+ for (var i = 0; i < items.length; ++i)
+ this.menuSpecs[menuId].items.push(items[i]);
+}
+
+MenuManager.prototype.createContextMenus =
+function mmgr_initcxs (document)
+{
+ for (var id in this.menuSpecs)
+ {
+ if (id.indexOf("context:") == 0)
+ this.createContextMenu(document, id);
+ }
+}
+
+MenuManager.prototype.createContextMenu =
+function mmgr_initcx (document, id)
+{
+ if (!document.getElementById(id))
+ {
+ if (!ASSERT(id in this.menuSpecs, "unknown context menu " + id))
+ return;
+
+ var dp = document.getElementById("dynamic-popups");
+ var popup = this.appendPopupMenu (dp, null, id, id);
+ var items = this.menuSpecs[id].items;
+ this.createMenuItems (popup, null, items);
+
+ if (!("uiElements" in this.menuSpecs[id]))
+ this.menuSpecs[id].uiElements = [popup];
+ else if (!arrayContains(this.menuSpecs[id].uiElements, popup))
+ this.menuSpecs[id].uiElements.push(popup);
+ }
+}
+
+
+MenuManager.prototype.createMenus =
+function mmgr_createtb(document, menuid)
+{
+ var menu = document.getElementById(menuid);
+ for (var id in this.menuSpecs)
+ {
+ var domID;
+ if ("domID" in this.menuSpecs[id])
+ domID = this.menuSpecs[id].domID;
+ else
+ domID = id;
+
+ if (id.indexOf(menuid + ":") == 0)
+ this.createMenu(menu, null, id, domID);
+ }
+}
+
+MenuManager.prototype.createMainToolbar =
+function mmgr_createtb(document, id)
+{
+ var toolbar = document.getElementById(id);
+ var spec = this.menuSpecs[id];
+ for (var i in spec.items)
+ {
+ this.appendToolbarItem (toolbar, null, spec.items[i]);
+ }
+
+ toolbar.className = "toolbar-primary chromeclass-toolbar";
+}
+
+MenuManager.prototype.updateMenus =
+function mmgr_updatemenus(document, menus)
+{
+ // Cope with one string (update just the one menu)...
+ if (isinstance(menus, String))
+ {
+ menus = [menus];
+ }
+ // Or nothing/nonsense (update everything).
+ else if ((typeof menus != "object") || !isinstance(menus, Array))
+ {
+ menus = [];
+ for (var k in this.menuSpecs)
+ {
+ if ((/^(mainmenu|context)/).test(k))
+ menus.push(k);
+ }
+ }
+
+ var menuBar = document.getElementById("mainmenu");
+
+ // Loop through this array and update everything we need to.
+ for (var i = 0; i < menus.length; i++)
+ {
+ var id = menus[i];
+ if (!(id in this.menuSpecs))
+ continue;
+ var menu = this.menuSpecs[id];
+ var domID;
+ if ("domID" in this.menuSpecs[id])
+ domID = this.menuSpecs[id].domID;
+ else
+ domID = id;
+
+ // Context menus need to be deleted in order to be regenerated...
+ if ((/^context/).test(id))
+ {
+ var cxMenuNode;
+ if ((cxMenuNode = document.getElementById(id)))
+ cxMenuNode.parentNode.removeChild(cxMenuNode);
+ this.createContextMenu(document, id);
+ }
+ else if ((/^mainmenu/).test(id) &&
+ !("uiElements" in this.menuSpecs[id]))
+ {
+ this.createMenu(menuBar, null, id, domID);
+ continue;
+ }
+ else if ((/^(mainmenu|popup)/).test(id) &&
+ ("uiElements" in this.menuSpecs[id]))
+ {
+ for (var j = 0; j < menu.uiElements.length; j++)
+ {
+ var node = menu.uiElements[j];
+ domID = node.parentNode.id;
+ // Clear the menu node.
+ while (node.lastChild)
+ node.removeChild(node.lastChild);
+
+ this.createMenu(node.parentNode.parentNode,
+ node.parentNode.nextSibling,
+ id, domID);
+ }
+ }
+
+
+ }
+}
+
+
+/**
+ * Internal use only.
+ *
+ * Registers event handlers on a given menu.
+ */
+MenuManager.prototype.hookPopup =
+function mmgr_hookpop (node)
+{
+ node.addEventListener ("popupshowing", this.onPopupShowing, false);
+ node.addEventListener ("popuphiding", this.onPopupHiding, false);
+}
+
+/**
+ * Internal use only.
+ *
+ * |showPopup| is called from the "onpopupshowing" event of menus managed
+ * by the CommandManager. If a command is disabled, represents a command
+ * that cannot be "satisfied" by the current command context |cx|, or has an
+ * "enabledif" attribute that eval()s to false, then the menuitem is disabled.
+ * In addition "checkedif" and "visibleif" attributes are eval()d and
+ * acted upon accordingly.
+ */
+MenuManager.prototype.showPopup =
+function mmgr_showpop (event)
+{
+ /* returns true if the command context has the properties required to
+ * execute the command associated with |menuitem|.
+ */
+ function satisfied()
+ {
+ if (menuitem.hasAttribute("isSeparator") ||
+ !menuitem.hasAttribute("commandname"))
+ {
+ return true;
+ }
+
+ if (menuitem.hasAttribute("repeatfor"))
+ return false;
+
+ if (!("menuManager" in cx))
+ {
+ dd ("no menuManager in cx");
+ return false;
+ }
+
+ var name = menuitem.getAttribute("commandname");
+ var commandManager = cx.menuManager.commandManager;
+ var commands = commandManager.commands;
+
+ if (!ASSERT (name in commands,
+ "menu contains unknown command '" + name + "'"))
+ {
+ return false;
+ }
+
+ var rv = commandManager.isCommandSatisfied(cx, commands[name]);
+ delete cx.parseError;
+ return rv;
+ };
+
+ /* Convenience function for "enabledif", etc, attributes. */
+ function has (prop)
+ {
+ return (prop in cx);
+ };
+
+ /* evals the attribute named |attr| on the node |node|. */
+ function evalIfAttribute (node, attr)
+ {
+ var ex;
+ var expr = node.getAttribute(attr);
+ if (!expr)
+ return true;
+
+ expr = expr.replace (/\Wand\W/gi, " && ");
+ expr = expr.replace (/\Wor\W/gi, " || ");
+
+ try
+ {
+ return eval("(" + expr + ")");
+ }
+ catch (ex)
+ {
+ dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
+ attr + "': '" + expr + "'\n" + ex);
+ }
+ return true;
+ };
+
+ /* evals the attribute named |attr| on the node |node|. */
+ function evalAttribute(node, attr)
+ {
+ var ex;
+ var expr = node.getAttribute(attr);
+ if (!expr)
+ return null;
+
+ try
+ {
+ return eval(expr);
+ }
+ catch (ex)
+ {
+ dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
+ attr + "': '" + expr + "'\n" + ex);
+ }
+ return null;
+ };
+
+ var cx;
+ var popup = event.originalTarget;
+ var menuName = popup.getAttribute("menuName");
+
+ /* If the host provided a |contextFunction|, use it now. Remember the
+ * return result as this.cx for use if something from this menu is actually
+ * dispatched. */
+ if (typeof this.contextFunction == "function")
+ {
+ cx = this.cx = this.contextFunction(menuName, event);
+ }
+ else
+ {
+ cx = this.cx = { menuManager: this, originalEvent: event };
+ }
+
+ // Keep the context around by menu name. Removed in hidePopup.
+ this.cxStore[menuName] = cx;
+
+ var menuitem = popup.firstChild;
+ do
+ {
+ if (!menuitem.hasAttribute("repeatfor"))
+ continue;
+
+ // Remove auto-generated items (located prior to real item).
+ while (menuitem.previousSibling &&
+ menuitem.previousSibling.hasAttribute("repeatgenerated"))
+ {
+ menuitem.parentNode.removeChild(menuitem.previousSibling);
+ }
+
+ if (!("repeatList" in cx))
+ cx.repeatList = new Object();
+
+ /* Get the array of new items to add by evaluating "repeatfor" with
+ * "cx" in scope. Usually will return an already-calculated Array
+ * either from "cx" or somewhere in the object model.
+ */
+ var ary = evalAttribute(menuitem, "repeatfor");
+
+ if ((typeof ary != "object") || !isinstance(ary, Array))
+ ary = [];
+
+ /* The item itself should only be shown if there's no items in the
+ * array - this base item is always disabled.
+ */
+ if (ary.length > 0)
+ menuitem.setAttribute("hidden", "true");
+ else
+ menuitem.removeAttribute("hidden");
+
+ // Save the array in the context object.
+ cx.repeatList[menuitem.getAttribute("repeatid")] = ary;
+
+ /* Get the maximum number of items we're allowed to show from |ary| by
+ * evaluating "repeatlimit" with "cx" in scope. This could be a fixed
+ * limit or dynamically calculated (e.g. from prefs).
+ */
+ var limit = evalAttribute(menuitem, "repeatlimit");
+ // Make sure we've got a number at all...
+ if (typeof limit != "number")
+ limit = ary.length;
+ // ...and make sure it's no higher than |ary.length|.
+ limit = Math.min(ary.length, limit);
+
+ var cmd = menuitem.getAttribute("commandname");
+ var props = { repeatgenerated: true, repeatindex: -1,
+ repeatid: menuitem.getAttribute("repeatid"),
+ repeatmap: menuitem.getAttribute("repeatmap") };
+
+ /* Clone non-repeat attributes. All attributes except those starting
+ * with 'repeat', and those matching 'hidden' or 'disabled' are saved
+ * to |props|, which is then supplied to |appendMenuItem| later.
+ */
+ for (var i = 0; i < menuitem.attributes.length; i++)
+ {
+ var name = menuitem.attributes[i].nodeName;
+ if (!name.match(/^(repeat|(hidden|disabled)$)/))
+ props[name] = menuitem.getAttribute(name);
+ }
+
+ var lastGroup = "";
+ for (i = 0; i < limit; i++)
+ {
+ /* Check for groupings. For each item we add, if "repeatgroup" gives
+ * a different value, we insert a separator.
+ */
+ if (menuitem.getAttribute("repeatgroup"))
+ {
+ cx.index = i;
+ ary = cx.repeatList[menuitem.getAttribute("repeatid")];
+ var item = ary[i];
+ /* Apply any updates to "cx" for this item by evaluating
+ * "repeatmap" with "cx" and "item" in scope. This may just
+ * copy some attributes from "item" to "cx" or it may do more.
+ */
+ evalAttribute(menuitem, "repeatmap");
+ /* Get the item's group by evaluating "repeatgroup" with "cx"
+ * and "item" in scope. Usually will return an appropriate
+ * property from "item".
+ */
+ var group = evalAttribute(menuitem, "repeatgroup");
+
+ if ((i > 0) && (lastGroup != group))
+ this.appendMenuSeparator(popup, menuitem, props);
+
+ lastGroup = group;
+ }
+
+ props.repeatindex = i;
+ this.appendMenuItem(popup, menuitem, cmd, props);
+ }
+ } while ((menuitem = menuitem.nextSibling));
+
+ menuitem = popup.firstChild;
+ do
+ {
+ if (menuitem.hasAttribute("repeatgenerated") &&
+ menuitem.hasAttribute("repeatmap"))
+ {
+ cx.index = menuitem.getAttribute("repeatindex");
+ ary = cx.repeatList[menuitem.getAttribute("repeatid")];
+ var item = ary[cx.index];
+ /* Apply any updates to "cx" for this item by evaluating
+ * "repeatmap" with "cx" and "item" in scope. This may just
+ * copy some attributes from "item" to "cx" or it may do more.
+ */
+ evalAttribute(menuitem, "repeatmap");
+ }
+
+ /* should it be visible? */
+ if (menuitem.hasAttribute("visibleif"))
+ {
+ if (evalIfAttribute(menuitem, "visibleif"))
+ menuitem.removeAttribute ("hidden");
+ else
+ {
+ menuitem.setAttribute ("hidden", "true");
+ continue;
+ }
+ }
+
+ /* it's visible, maybe it has a dynamic label? */
+ if (menuitem.hasAttribute("format"))
+ {
+ var label = replaceVars(menuitem.getAttribute("format"), cx);
+ if (label.indexOf("\$") != -1)
+ label = menuitem.getAttribute("backupLabel");
+ menuitem.setAttribute("label", label);
+ }
+
+ /* ok, it's visible, maybe it should be disabled? */
+ if (satisfied())
+ {
+ if (menuitem.hasAttribute("enabledif"))
+ {
+ if (evalIfAttribute(menuitem, "enabledif"))
+ menuitem.removeAttribute ("disabled");
+ else
+ menuitem.setAttribute ("disabled", "true");
+ }
+ else
+ menuitem.removeAttribute ("disabled");
+ }
+ else
+ {
+ menuitem.setAttribute ("disabled", "true");
+ }
+
+ /* should it have a check? */
+ if (menuitem.hasAttribute("checkedif"))
+ {
+ if (evalIfAttribute(menuitem, "checkedif"))
+ menuitem.setAttribute ("checked", "true");
+ else
+ menuitem.removeAttribute ("checked");
+ }
+ } while ((menuitem = menuitem.nextSibling));
+
+ if (typeof this.onCallbackPopupShowing == "function")
+ this.onCallbackPopupShowing(event, cx, popup);
+
+ return true;
+}
+
+/**
+ * Internal use only.
+ *
+ * |hidePopup| is called from the "onpopuphiding" event of menus
+ * managed by the CommandManager. Clean up this.cxStore, but
+ * not this.cx because that messes up nested menus.
+ */
+MenuManager.prototype.hidePopup =
+function mmgr_hidepop(event)
+{
+ var popup = event.originalTarget;
+ var menuName = popup.getAttribute("menuName");
+
+ if (typeof this.onCallbackPopupHiding == "function")
+ this.onCallbackPopupHiding(event, this.cxStore[menuName], popup);
+
+ delete this.cxStore[menuName];
+
+ return true;
+}
+
+MenuManager.prototype.menuCommand =
+function mmgr_menucmd(event)
+{
+ /* evals the attribute named |attr| on the node |node|. */
+ function evalAttribute(node, attr)
+ {
+ var ex;
+ var expr = node.getAttribute(attr);
+ if (!expr)
+ return null;
+
+ try
+ {
+ return eval(expr);
+ }
+ catch (ex)
+ {
+ dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
+ attr + "': '" + expr + "'\n" + ex);
+ }
+ return null;
+ };
+
+ var menuitem = event.originalTarget;
+ var cx = this.cx;
+ /* We need to re-run the repeat-map if the user has selected a special
+ * repeat-generated menu item, so that the context object is correct.
+ */
+ if (menuitem.hasAttribute("repeatgenerated") &&
+ menuitem.hasAttribute("repeatmap"))
+ {
+ cx.index = menuitem.getAttribute("repeatindex");
+ var ary = cx.repeatList[menuitem.getAttribute("repeatid")];
+ var item = ary[cx.index];
+ /* Apply any updates to "cx" for this item by evaluating
+ * "repeatmap" with "cx" and "item" in scope. This may just
+ * copy some attributes from "item" to "cx" or it may do more.
+ */
+ evalAttribute(menuitem, "repeatmap");
+ }
+
+ eval(this.commandStr);
+};
+
+
+/**
+ * Appends a sub-menu to an existing menu.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param domId ID of the sub-menu to add.
+ * @param label Text to use for this sub-menu.
+ * @param accesskey Accesskey to use for the sub-menu.
+ * @param attribs Object containing CSS attributes to set on the element.
+ */
+MenuManager.prototype.appendSubMenu =
+function mmgr_addsmenu(parentNode, beforeNode, menuName, domId, label,
+ accesskey, attribs)
+{
+ var document = parentNode.ownerDocument;
+
+ /* sometimes the menu is already there, for overlay purposes. */
+ var menu = document.getElementById(domId);
+
+ if (!menu)
+ {
+ menu = document.createElement ("menu");
+ menu.setAttribute ("id", domId);
+ }
+
+ var menupopup = menu.firstChild;
+
+ if (!menupopup)
+ {
+ menupopup = document.createElement ("menupopup");
+ menupopup.setAttribute ("id", domId + "-popup");
+ menu.appendChild(menupopup);
+ menupopup = menu.firstChild;
+ }
+
+ menupopup.setAttribute ("menuName", menuName);
+
+ menu.setAttribute("accesskey", accesskey);
+ label = label.replace("&", "");
+ menu.setAttribute ("label", label);
+ menu.setAttribute ("isSeparator", true);
+
+ // Only attach the menu if it's not there already. This can't be in the
+ // if (!menu) block because the updateMenus code clears toplevel menus,
+ // orphaning the submenus, to (parts of?) which we keep handles in the
+ // uiElements array. See the updateMenus code.
+ if (!menu.parentNode)
+ parentNode.insertBefore(menu, beforeNode);
+
+ if (typeof attribs == "object")
+ {
+ for (var p in attribs)
+ menu.setAttribute (p, attribs[p]);
+ }
+
+ this.hookPopup (menupopup);
+
+ return menupopup;
+}
+
+/**
+ * Appends a popup to an existing popupset.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param id ID of the popup to add.
+ * @param label Text to use for this popup. Popup menus don't normally have
+ * labels, but we set a "label" attribute anyway, in case
+ * the host wants it for some reason. Any "&" characters will
+ * be stripped.
+ * @param attribs Object containing CSS attributes to set on the element.
+ */
+MenuManager.prototype.appendPopupMenu =
+function mmgr_addpmenu (parentNode, beforeNode, menuName, id, label, attribs)
+{
+ var document = parentNode.ownerDocument;
+ var popup = document.createElement ("menupopup");
+ popup.setAttribute ("id", id);
+ if (label)
+ popup.setAttribute ("label", label.replace("&", ""));
+ if (typeof attribs == "object")
+ {
+ for (var p in attribs)
+ popup.setAttribute (p, attribs[p]);
+ }
+
+ popup.setAttribute ("menuName", menuName);
+
+ parentNode.insertBefore(popup, beforeNode);
+ this.hookPopup (popup);
+
+ return popup;
+}
+
+/**
+ * Appends a menuitem to an existing menu or popup.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param command A reference to the CommandRecord this menu item will represent.
+ * @param attribs Object containing CSS attributes to set on the element.
+ */
+MenuManager.prototype.appendMenuItem =
+function mmgr_addmenu (parentNode, beforeNode, commandName, attribs)
+{
+ var menuManager = this;
+
+ var document = parentNode.ownerDocument;
+ if (commandName == "-")
+ return this.appendMenuSeparator(parentNode, beforeNode, attribs);
+
+ var parentId = parentNode.getAttribute("id");
+
+ if (!ASSERT(commandName in this.commandManager.commands,
+ "unknown command " + commandName + " targeted for " +
+ parentId))
+ {
+ return null;
+ }
+
+ var command = this.commandManager.commands[commandName];
+ var menuitem = document.createElement ("menuitem");
+ menuitem.setAttribute ("id", parentId + ":" + commandName);
+ menuitem.setAttribute ("commandname", command.name);
+ // Add keys if this isn't a context menu:
+ if (parentId.indexOf("context") != 0)
+ menuitem.setAttribute("key", "key:" + command.name);
+ menuitem.setAttribute("accesskey", command.accesskey);
+ var label = command.label.replace("&", "");
+ menuitem.setAttribute ("label", label);
+ if (command.format)
+ {
+ menuitem.setAttribute("format", command.format);
+ menuitem.setAttribute("backupLabel", label);
+ }
+
+ if ((typeof attribs == "object") && attribs)
+ {
+ for (var p in attribs)
+ menuitem.setAttribute (p, attribs[p]);
+ if ("repeatfor" in attribs)
+ menuitem.setAttribute("repeatid", this.repeatId++);
+ }
+
+ command.uiElements.push(menuitem);
+ parentNode.insertBefore (menuitem, beforeNode);
+ /* It seems, bob only knows why, that this must be done AFTER the node is
+ * added to the document.
+ */
+ menuitem.addEventListener("command", this.onMenuCommand, false);
+
+ return menuitem;
+}
+
+/**
+ * Appends a menuseparator to an existing menu or popup.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param attribs Object containing CSS attributes to set on the element.
+ */
+MenuManager.prototype.appendMenuSeparator =
+function mmgr_addsep (parentNode, beforeNode, attribs)
+{
+ var document = parentNode.ownerDocument;
+ var menuitem = document.createElement ("menuseparator");
+ menuitem.setAttribute ("isSeparator", true);
+ if (typeof attribs == "object")
+ {
+ for (var p in attribs)
+ menuitem.setAttribute (p, attribs[p]);
+ }
+ parentNode.insertBefore (menuitem, beforeNode);
+
+ return menuitem;
+}
+
+/**
+ * Appends a toolbaritem to an existing box element.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param command A reference to the CommandRecord this toolbaritem will
+ * represent.
+ * @param attribs Object containing CSS attributes to set on the element.
+ */
+MenuManager.prototype.appendToolbarItem =
+function mmgr_addtb (parentNode, beforeNode, commandName, attribs)
+{
+ if (commandName == "-")
+ return this.appendToolbarSeparator(parentNode, beforeNode, attribs);
+
+ var parentId = parentNode.getAttribute("id");
+
+ if (!ASSERT(commandName in this.commandManager.commands,
+ "unknown command " + commandName + " targeted for " +
+ parentId))
+ {
+ return null;
+ }
+
+ var command = this.commandManager.commands[commandName];
+ var document = parentNode.ownerDocument;
+ var tbitem = document.createElement ("toolbarbutton");
+
+ var id = parentNode.getAttribute("id") + ":" + commandName;
+ tbitem.setAttribute ("id", id);
+ tbitem.setAttribute ("class", "toolbarbutton-1");
+ if (command.tip)
+ tbitem.setAttribute ("tooltiptext", command.tip);
+ tbitem.setAttribute ("label", command.label.replace("&", ""));
+ tbitem.setAttribute ("oncommand",
+ "dispatch('" + commandName + "');");
+ if (typeof attribs == "object")
+ {
+ for (var p in attribs)
+ tbitem.setAttribute (p, attribs[p]);
+ }
+
+ command.uiElements.push(tbitem);
+ parentNode.insertBefore (tbitem, beforeNode);
+
+ return tbitem;
+}
+
+/**
+ * Appends a toolbarseparator to an existing box.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param attribs Object containing CSS attributes to set on the element.
+ */
+MenuManager.prototype.appendToolbarSeparator =
+function mmgr_addmenu (parentNode, beforeNode, attribs)
+{
+ var document = parentNode.ownerDocument;
+ var tbitem = document.createElement ("toolbarseparator");
+ tbitem.setAttribute ("isSeparator", true);
+ if (typeof attribs == "object")
+ {
+ for (var p in attribs)
+ tbitem.setAttribute (p, attribs[p]);
+ }
+ parentNode.appendChild (tbitem);
+
+ return tbitem;
+}
+
+/**
+ * Creates menu DOM nodes from a menu specification.
+ * @param parentNode DOM Node to insert into
+ * @param beforeNode DOM Node already contained by parentNode, to insert before
+ * @param menuSpec array of menu items
+ */
+MenuManager.prototype.createMenu =
+function mmgr_newmenu (parentNode, beforeNode, menuName, domId, attribs)
+{
+ if (typeof domId == "undefined")
+ domId = menuName;
+
+ if (!ASSERT(menuName in this.menuSpecs, "unknown menu name " + menuName))
+ return null;
+
+ var menuSpec = this.menuSpecs[menuName];
+ if (!("accesskey" in menuSpec))
+ menuSpec.accesskey = getAccessKey(menuSpec.label);
+
+ var subMenu = this.appendSubMenu(parentNode, beforeNode, menuName, domId,
+ menuSpec.label, menuSpec.accesskey,
+ attribs);
+
+ // Keep track where we're adding popup nodes derived from some menuSpec
+ if (!("uiElements" in this.menuSpecs[menuName]))
+ this.menuSpecs[menuName].uiElements = [subMenu];
+ else if (!arrayContains(this.menuSpecs[menuName].uiElements, subMenu))
+ this.menuSpecs[menuName].uiElements.push(subMenu);
+
+ this.createMenuItems (subMenu, null, menuSpec.items);
+ return subMenu;
+}
+
+MenuManager.prototype.createMenuItems =
+function mmgr_newitems (parentNode, beforeNode, menuItems)
+{
+ function itemAttribs()
+ {
+ return (1 in menuItems[i]) ? menuItems[i][1] : null;
+ };
+
+ var parentId = parentNode.getAttribute("id");
+
+ for (var i in menuItems)
+ {
+ var itemName = menuItems[i][0];
+ if (itemName[0] == ">")
+ {
+ itemName = itemName.substr(1);
+ if (!ASSERT(itemName in this.menuSpecs,
+ "unknown submenu " + itemName + " referenced in " +
+ parentId))
+ {
+ continue;
+ }
+ this.createMenu (parentNode, beforeNode, itemName,
+ parentId + ":" + itemName, itemAttribs());
+ }
+ else if (itemName in this.commandManager.commands)
+ {
+ this.appendMenuItem (parentNode, beforeNode, itemName,
+ itemAttribs());
+ }
+ else if (itemName == "-")
+ {
+ this.appendMenuSeparator (parentNode, beforeNode, itemAttribs());
+ }
+ else
+ {
+ dd ("unknown command " + itemName + " referenced in " + parentId);
+ }
+ }
+}
+
diff --git a/comm/suite/chatzilla/js/lib/message-manager.js b/comm/suite/chatzilla/js/lib/message-manager.js
new file mode 100644
index 0000000000..56020d48f6
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/message-manager.js
@@ -0,0 +1,356 @@
+/* -*- 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/. */
+
+//
+function MessageManager(entities)
+{
+ const UC_CTRID = "@mozilla.org/intl/scriptableunicodeconverter";
+ const nsIUnicodeConverter =
+ Components.interfaces.nsIScriptableUnicodeConverter;
+
+ this.ucConverter =
+ Components.classes[UC_CTRID].getService(nsIUnicodeConverter);
+ this.defaultBundle = null;
+ this.bundleList = new Array();
+ // Provide a fallback so we don't break getMsg and related constants later.
+ this.entities = entities || {};
+}
+
+// ISO-2022-JP (often used for Japanese on IRC) doesn't contain any support
+// for hankaku kana (half-width katakana), so we support the option to convert
+// it to zenkaku kana (full-width katakana). This does not affect any other
+// encoding at this time.
+MessageManager.prototype.enableHankakuToZenkaku = false;
+
+MessageManager.prototype.loadBrands =
+function mm_loadbrands()
+{
+ var entities = this.entities;
+ var app = getService("@mozilla.org/xre/app-info;1", "nsIXULAppInfo");
+ if (app)
+ {
+ // Use App info if possible
+ entities.brandShortName = app.name;
+ entities.brandFullName = app.name + " " + app.version;
+ entities.brandVendorName = app.vendor;
+ return;
+ }
+
+ var brandBundle;
+ var path = "chrome://branding/locale/brand.properties";
+ try
+ {
+ brandBundle = this.addBundle(path);
+ }
+ catch (exception)
+ {
+ // May be an older mozilla version, try another location.
+ path = "chrome://global/locale/brand.properties";
+ brandBundle = this.addBundle(path);
+ }
+
+ entities.brandShortName = brandBundle.GetStringFromName("brandShortName");
+ entities.brandVendorName = brandBundle.GetStringFromName("vendorShortName");
+ // Not all versions of Suite / Fx have this defined; Cope:
+ try
+ {
+ entities.brandFullName = brandBundle.GetStringFromName("brandFullName");
+ }
+ catch(exception)
+ {
+ entities.brandFullName = entities.brandShortName;
+ }
+
+ // Remove all of this junk, or it will be the default bundle for getMsg...
+ this.bundleList.pop();
+}
+
+MessageManager.prototype.addBundle =
+function mm_addbundle(bundlePath, targetWindow)
+{
+ var bundle = srGetStrBundle(bundlePath);
+ this.bundleList.push(bundle);
+
+ // The bundle will load if the file doesn't exist. This will fail though.
+ // We want to be clean and remove the bundle again.
+ try
+ {
+ this.importBundle(bundle, targetWindow, this.bundleList.length - 1);
+ }
+ catch (exception)
+ {
+ // Clean up and return the exception.
+ this.bundleList.pop();
+ throw exception;
+ }
+ return bundle;
+}
+
+MessageManager.prototype.importBundle =
+function mm_importbundle(bundle, targetWindow, index)
+{
+ var me = this;
+ function replaceEntities(matched, entity)
+ {
+ if (entity in me.entities)
+ return me.entities[entity];
+
+ return matched;
+ };
+ const nsIPropertyElement = Components.interfaces.nsIPropertyElement;
+
+ if (!targetWindow)
+ targetWindow = window;
+
+ if (typeof index == "undefined")
+ index = arrayIndexOf(this.bundleList, bundle);
+
+ var pfx;
+ if (index == 0)
+ pfx = "";
+ else
+ pfx = index + ":";
+
+ var enumer = bundle.getSimpleEnumeration();
+
+ while (enumer.hasMoreElements())
+ {
+ var prop = enumer.getNext().QueryInterface(nsIPropertyElement);
+ var ary = prop.key.match (/^(msg|msn)/);
+ if (ary)
+ {
+ var constValue;
+ var constName = prop.key.toUpperCase().replace (/\./g, "_");
+ if (ary[1] == "msn" || prop.value.search(/%(\d+\$)?s/i) != -1)
+ constValue = pfx + prop.key;
+ else
+ constValue = prop.value.replace (/^\"/, "").replace (/\"$/, "");
+
+ constValue = constValue.replace(/\&(\w+)\;/g, replaceEntities);
+ targetWindow[constName] = constValue;
+ }
+ }
+
+ if (this.bundleList.length == 1)
+ this.defaultBundle = bundle;
+}
+
+MessageManager.prototype.convertHankakuToZenkaku =
+function mm_converthankakutozenkaku(msg)
+{
+ const basicMapping = [
+ /* 0xFF60 */ 0xFF60,0x3002,0x300C,0x300D,0x3001,0x30FB,0x30F2,0x30A1,
+ /* 0xFF68 */ 0x30A3,0x30A5,0x30A7,0x30A9,0x30E3,0x30E5,0x30E7,0x30C3,
+ /* 0xFF70 */ 0x30FC,0x30A2,0x30A4,0x30A6,0x30A8,0x30AA,0x30AB,0x30AD,
+ /* 0xFF78 */ 0x30AF,0x30B1,0x30B3,0x30B5,0x30B7,0x30B9,0x30BB,0x30BD,
+ /* 0xFF80 */ 0x30BF,0x30C1,0x30C4,0x30C6,0x30C8,0x30CA,0x30CB,0x30CC,
+ /* 0xFF88 */ 0x30CD,0x30CE,0x30CF,0x30D2,0x30D5,0x30D8,0x30DB,0x30DE,
+ /* 0xFF90 */ 0x30DF,0x30E0,0x30E1,0x30E2,0x30E4,0x30E6,0x30E8,0x30E9,
+ /* 0xFF98 */ 0x30EA,0x30EB,0x30EC,0x30ED,0x30EF,0x30F3,0x309B,0x309C
+ ];
+
+ const HANKAKU_BASE1 = 0xFF60;
+ const HANKAKU_BASE2 = 0xFF80;
+ const HANKAKU_MASK = 0xFFE0;
+
+ const MOD_NIGORI = 0xFF9E;
+ const NIGORI_MIN1 = 0xFF76;
+ const NIGORI_MAX1 = 0xFF84;
+ const NIGORI_MIN2 = 0xFF8A;
+ const NIGORI_MAX2 = 0xFF8E;
+ const NIGORI_MODIFIER = 1;
+
+ const MOD_MARU = 0xFF9F;
+ const MARU_MIN = 0xFF8A;
+ const MARU_MAX = 0xFF8E;
+ const MARU_MODIFIER = 2;
+
+ var i, src, srcMod, dest;
+ var rv = "";
+
+ for (i = 0; i < msg.length; i++)
+ {
+ // Get both this character and the next one, which could be a modifier.
+ src = msg.charCodeAt(i);
+ if (i < msg.length - 1)
+ srcMod = msg.charCodeAt(i + 1);
+
+ // Is the source characher hankaku?
+ if ((HANKAKU_BASE1 == (src & HANKAKU_MASK)) ||
+ (HANKAKU_BASE2 == (src & HANKAKU_MASK)))
+ {
+ // Do the basic character mapping first.
+ dest = basicMapping[src - HANKAKU_BASE1];
+
+ // If the source character is in the nigori or maru ranges and
+ // the following character is the associated modifier, we apply
+ // the modification and skip over the modifier.
+ if (i < msg.length - 1)
+ {
+ if ((MOD_NIGORI == srcMod) &&
+ (((src >= NIGORI_MIN1) && (src <= NIGORI_MAX1)) ||
+ ((src >= NIGORI_MIN2) && (src <= NIGORI_MAX2))))
+ {
+ dest += NIGORI_MODIFIER;
+ i++;
+ }
+ else if ((MOD_MARU == srcMod) &&
+ (src >= MARU_MIN) && (src <= MARU_MAX))
+ {
+ dest += MARU_MODIFIER;
+ i++;
+ }
+ }
+
+ rv += String.fromCharCode(dest);
+ }
+ else
+ {
+ rv += msg[i];
+ }
+ }
+
+ return rv;
+}
+
+MessageManager.prototype.checkCharset =
+function mm_checkset(charset)
+{
+ try
+ {
+ this.ucConverter.charset = charset;
+ }
+ catch (ex)
+ {
+ return false;
+ }
+
+ return true;
+}
+
+MessageManager.prototype.toUnicode =
+function mm_tounicode(msg, charset)
+{
+ if (!charset)
+ return msg;
+
+ try
+ {
+ this.ucConverter.charset = charset;
+ msg = this.ucConverter.ConvertToUnicode(msg);
+ }
+ catch (ex)
+ {
+ //dd ("caught exception " + ex + " converting " + msg + " to charset " +
+ // charset);
+ }
+
+ return msg;
+}
+
+MessageManager.prototype.fromUnicode =
+function mm_fromunicode(msg, charset)
+{
+ if (!charset)
+ return msg;
+
+ if (this.enableHankakuToZenkaku && (charset.toLowerCase() == "iso-2022-jp"))
+ msg = this.convertHankakuToZenkaku(msg);
+
+ try
+ {
+ // This can actually fail in bizare cases. Cope.
+ if (charset != this.ucConverter.charset)
+ this.ucConverter.charset = charset;
+
+ if ("Finish" in this.ucConverter)
+ {
+ msg = this.ucConverter.ConvertFromUnicode(msg) +
+ this.ucConverter.Finish();
+ }
+ else
+ {
+ msg = this.ucConverter.ConvertFromUnicode(msg + " ");
+ msg = msg.substr(0, msg.length - 1);
+ }
+ }
+ catch (ex)
+ {
+ //dd ("caught exception " + ex + " converting " + msg + " to charset " +
+ // charset);
+ }
+
+ return msg;
+}
+
+MessageManager.prototype.getMsg =
+function mm_getmsg (msgName, params, deflt)
+{
+ try
+ {
+ var bundle;
+ var ary = msgName.match (/(\d+):(.+)/);
+ if (ary)
+ {
+ return (this.getMsgFrom(this.bundleList[ary[1]], ary[2], params,
+ deflt));
+ }
+
+ return this.getMsgFrom(this.bundleList[0], msgName, params, deflt);
+ }
+ catch (ex)
+ {
+ ASSERT (0, "Caught exception getting message: " + msgName + "/" +
+ params);
+ return deflt ? deflt : msgName;
+ }
+}
+
+MessageManager.prototype.getMsgFrom =
+function mm_getfrom (bundle, msgName, params, deflt)
+{
+ var me = this;
+ function replaceEntities(matched, entity)
+ {
+ if (entity in me.entities)
+ return me.entities[entity];
+
+ return matched;
+ };
+
+ try
+ {
+ var rv;
+
+ if (params && isinstance(params, Array))
+ rv = bundle.formatStringFromName (msgName, params, params.length);
+ else if (params || params == 0)
+ rv = bundle.formatStringFromName (msgName, [params], 1);
+ else
+ rv = bundle.GetStringFromName (msgName);
+
+ /* strip leading and trailing quote characters, see comment at the
+ * top of venkman.properties.
+ */
+ rv = rv.replace(/^\"/, "");
+ rv = rv.replace(/\"$/, "");
+ rv = rv.replace(/\&(\w+)\;/g, replaceEntities);
+
+ return rv;
+ }
+ catch (ex)
+ {
+ if (typeof deflt == "undefined")
+ {
+ ASSERT (0, "caught exception getting value for ``" + msgName +
+ "''\n" + ex + "\n");
+ return msgName;
+ }
+ return deflt;
+ }
+
+ return null;
+}
diff --git a/comm/suite/chatzilla/js/lib/pref-manager.js b/comm/suite/chatzilla/js/lib/pref-manager.js
new file mode 100644
index 0000000000..3d0b09ff86
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/pref-manager.js
@@ -0,0 +1,443 @@
+/* -*- 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/. */
+
+const PREF_RELOAD = true;
+const PREF_WRITETHROUGH = true;
+const PREF_CHARSET = "utf-8"; // string prefs stored in this charset
+
+function PrefRecord(name, defaultValue, label, help, group)
+{
+ this.name = name;
+ this.defaultValue = defaultValue;
+ this.help = help;
+ this.label = label ? label : name;
+ this.group = group ? group : "";
+ // Prepend the group 'general' if there isn't one already.
+ if (this.group.match(/^(\.|$)/))
+ this.group = "general" + this.group;
+ this.realValue = null;
+}
+
+function PrefManager (branchName, defaultBundle)
+{
+ var prefManager = this;
+
+ function pm_observe (prefService, topic, prefName)
+ {
+ prefManager.onPrefChanged(prefName);
+ };
+
+ const PREF_CTRID = "@mozilla.org/preferences-service;1";
+ const nsIPrefService = Components.interfaces.nsIPrefService;
+ const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+
+ this.prefService =
+ Components.classes[PREF_CTRID].getService(nsIPrefService);
+ this.prefBranch = this.prefService.getBranch(branchName);
+ this.prefSaveTime = 0;
+ this.prefSaveTimer = 0;
+ this.branchName = branchName;
+ this.defaultValues = new Object();
+ this.prefs = new Object();
+ this.prefNames = new Array();
+ this.prefRecords = new Object();
+ this.observer = { observe: pm_observe, branch: branchName };
+ this.observers = new Array();
+
+ this.nsIPrefBranch =
+ this.prefBranch.QueryInterface(nsIPrefBranch);
+ this.nsIPrefBranch.addObserver("", this.observer, false);
+
+ this.defaultBundle = defaultBundle;
+
+ this.valid = true;
+}
+
+// Delay between change and save.
+PrefManager.prototype.PREF_SAVE_DELAY = 5000; // 5 seconds.
+/* The timer is reset for each change. Only reset if it hasn't been delayed by
+ * this much already, or we could put off a save indefinitely.
+ */
+PrefManager.prototype.PREF_MAX_DELAY = 15000; // 15 seconds.
+
+//
+PrefManager.prototype.destroy =
+function pm_destroy()
+{
+ if (this.valid)
+ {
+ this.nsIPrefBranch.removeObserver("", this.observer);
+ this.valid = false;
+ }
+}
+
+PrefManager.prototype.getBranch =
+function pm_getbranch(suffix)
+{
+ return this.prefService.getBranch(this.prefBranch.root + suffix);
+}
+
+PrefManager.prototype.getBranchManager =
+function pm_getbranchmgr(suffix)
+{
+ return new PrefManager(this.prefBranch.root + suffix);
+}
+
+PrefManager.prototype.addObserver =
+function pm_addobserver(observer)
+{
+ if (!("onPrefChanged" in observer))
+ throw "Bad observer!";
+
+ this.observers.push(observer);
+}
+
+PrefManager.prototype.removeObserver =
+function pm_removeobserver(observer)
+{
+ for (var i = 0; i < this.observers.length; i++)
+ {
+ if (this.observers[i] == observer)
+ {
+ arrayRemoveAt(this.observers, i);
+ break;
+ }
+ }
+}
+
+PrefManager.prototype.delayedSave =
+function pm_delayedsave()
+{
+ // this.prefSaveTimer
+ var now = Number(new Date());
+
+ /* If the time == 0, there is no delayed save in progress, and we should
+ * start one. If it isn't 0, check the delayed save was started within the
+ * allowed time - this means that if we keep putting off a save, it will
+ * go through eventually, as we will stop resetting it.
+ */
+ if ((this.prefSaveTime == 0) ||
+ (now - this.prefSaveTime < this.PREF_MAX_DELAY))
+ {
+ if (this.prefSaveTime == 0)
+ this.prefSaveTime = now;
+ if (this.prefSaveTimer != 0)
+ clearTimeout(this.prefSaveTimer);
+ this.prefSaveTimer = setTimeout(function(o) { o.forceSave() },
+ this.PREF_SAVE_DELAY, this);
+ }
+}
+
+PrefManager.prototype.forceSave =
+function pm_forcesave()
+{
+ this.prefSaveTime = 0;
+ this.prefSaveTimer = 0;
+ try {
+ this.prefService.savePrefFile(null);
+ } catch(ex) {
+ dd("Exception saving preferences: " + formatException(ex));
+ }
+}
+
+PrefManager.prototype.onPrefChanged =
+function pm_prefchanged(prefName, realValue, oldValue)
+{
+ var r, oldValue;
+ // We're only interested in prefs we actually know about.
+ if (!(prefName in this.prefRecords) || !(r = this.prefRecords[prefName]))
+ return;
+
+ if (r.realValue != null)
+ oldValue = r.realValue;
+ else if (typeof r.defaultValue == "function")
+ oldValue = r.defaultValue(prefName);
+ else
+ oldValue = r.defaultValue;
+
+ var realValue = this.getPref(prefName, PREF_RELOAD);
+
+ for (var i = 0; i < this.observers.length; i++)
+ this.observers[i].onPrefChanged(prefName, realValue, oldValue);
+}
+
+PrefManager.prototype.listPrefs =
+function pm_listprefs (prefix)
+{
+ var list = new Array();
+ var names = this.prefNames;
+ for (var i = 0; i < names.length; ++i)
+ {
+ if (!prefix || names[i].indexOf(prefix) == 0)
+ list.push (names[i]);
+ }
+
+ return list;
+}
+
+PrefManager.prototype.readPrefs =
+function pm_readprefs ()
+{
+ const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+
+ var list = this.prefBranch.getChildList("", {});
+ for (var i = 0; i < list.length; ++i)
+ {
+ if (!(list[i] in this))
+ {
+ var type = this.prefBranch.getPrefType (list[i]);
+ var defaultValue;
+
+ switch (type)
+ {
+ case nsIPrefBranch.PREF_INT:
+ defaultValue = 0;
+ break;
+
+ case nsIPrefBranch.PREF_BOOL:
+ defaultValue = false;
+ break;
+
+ default:
+ defaultValue = "";
+ }
+
+ this.addPref(list[i], defaultValue);
+ }
+ }
+}
+
+PrefManager.prototype.isKnownPref =
+function pm_ispref(prefName)
+{
+ return (prefName in this.prefRecords);
+}
+
+PrefManager.prototype.addPrefs =
+function pm_addprefs(prefSpecs)
+{
+ var bundle = "stringBundle" in prefSpecs ? prefSpecs.stringBundle : null;
+ for (var i = 0; i < prefSpecs.length; ++i)
+ {
+ this.addPref(prefSpecs[i][0], prefSpecs[i][1],
+ 3 in prefSpecs[i] ? prefSpecs[i][3] : null, bundle,
+ 2 in prefSpecs[i] ? prefSpecs[i][2] : null);
+ }
+}
+
+PrefManager.prototype.updateArrayPref =
+function pm_arrayupdate(prefName)
+{
+ var record = this.prefRecords[prefName];
+ if (!ASSERT(record, "Unknown pref: " + prefName))
+ return;
+
+ if (record.realValue == null)
+ record.realValue = record.defaultValue;
+
+ if (!ASSERT(isinstance(record.realValue, Array), "Pref is not an array"))
+ return;
+
+ this.prefBranch.setCharPref(prefName, this.arrayToString(record.realValue));
+ this.delayedSave();
+}
+
+PrefManager.prototype.stringToArray =
+function pm_s2a(string)
+{
+ if (string.search(/\S/) == -1)
+ return [];
+
+ var ary = string.split(/\s*;\s*/);
+ for (var i = 0; i < ary.length; ++i)
+ ary[i] = toUnicode(unescape(ary[i]), PREF_CHARSET);
+
+ return ary;
+}
+
+PrefManager.prototype.arrayToString =
+function pm_a2s(ary)
+{
+ var escapedAry = new Array()
+ for (var i = 0; i < ary.length; ++i)
+ escapedAry[i] = escape(fromUnicode(ary[i], PREF_CHARSET));
+
+ return escapedAry.join("; ");
+}
+
+PrefManager.prototype.getPref =
+function pm_getpref(prefName, reload)
+{
+ var prefManager = this;
+
+ function updateArrayPref() { prefManager.updateArrayPref(prefName); };
+
+ var record = this.prefRecords[prefName];
+ if (!ASSERT(record, "Unknown pref: " + prefName))
+ return null;
+
+ var defaultValue;
+
+ if (typeof record.defaultValue == "function")
+ {
+ // deferred pref, call the getter, and don't cache the result.
+ defaultValue = record.defaultValue(prefName);
+ }
+ else
+ {
+ if (!reload && record.realValue != null)
+ return record.realValue;
+
+ defaultValue = record.defaultValue;
+ }
+
+ var realValue = defaultValue;
+
+ try
+ {
+ if (typeof defaultValue == "boolean")
+ {
+ realValue = this.prefBranch.getBoolPref(prefName);
+ }
+ else if (typeof defaultValue == "number")
+ {
+ realValue = this.prefBranch.getIntPref(prefName);
+ }
+ else if (isinstance(defaultValue, Array))
+ {
+ realValue = this.prefBranch.getCharPref(prefName);
+ realValue = this.stringToArray(realValue);
+ realValue.update = updateArrayPref;
+ }
+ else if (typeof defaultValue == "string" ||
+ defaultValue == null)
+ {
+ realValue = toUnicode(this.prefBranch.getCharPref(prefName),
+ PREF_CHARSET);
+ }
+ }
+ catch (ex)
+ {
+ // if the pref doesn't exist, ignore the exception.
+ }
+
+ record.realValue = realValue;
+ return realValue;
+}
+
+PrefManager.prototype.setPref =
+function pm_setpref(prefName, value)
+{
+ var prefManager = this;
+
+ function updateArrayPref() { prefManager.updateArrayPref(prefName); };
+
+ var record = this.prefRecords[prefName];
+ if (!ASSERT(record, "Unknown pref: " + prefName))
+ return null;
+
+ var defaultValue = record.defaultValue;
+
+ if (typeof defaultValue == "function")
+ defaultValue = defaultValue(prefName);
+
+ if ((record.realValue == null && value == defaultValue) ||
+ record.realValue == value)
+ {
+ // no realvalue, and value is the same as default value ... OR ...
+ // no change at all. just bail.
+ return record.realValue;
+ }
+
+ if (value == defaultValue)
+ {
+ this.clearPref(prefName);
+ return value;
+ }
+
+ if (typeof defaultValue == "boolean")
+ {
+ this.prefBranch.setBoolPref(prefName, value);
+ }
+ else if (typeof defaultValue == "number")
+ {
+ this.prefBranch.setIntPref(prefName, value);
+ }
+ else if (isinstance(defaultValue, Array))
+ {
+ var str = this.arrayToString(value);
+ this.prefBranch.setCharPref(prefName, str);
+ value.update = updateArrayPref;
+ }
+ else
+ {
+ this.prefBranch.setCharPref(prefName, fromUnicode(value, PREF_CHARSET));
+ }
+ this.delayedSave();
+
+ // Always update this after changing the preference.
+ record.realValue = value;
+
+ return value;
+}
+
+PrefManager.prototype.clearPref =
+function pm_reset(prefName)
+{
+ try {
+ this.prefBranch.clearUserPref(prefName);
+ } catch(ex) {
+ // Do nothing, the pref didn't exist.
+ }
+ this.delayedSave();
+
+ // Always update this after changing the preference.
+ this.prefRecords[prefName].realValue = null;
+}
+
+PrefManager.prototype.addPref =
+function pm_addpref(prefName, defaultValue, setter, bundle, group)
+{
+ var prefManager = this;
+ if (!bundle)
+ bundle = this.defaultBundle;
+
+ function updateArrayPref() { prefManager.updateArrayPref(prefName); };
+ function prefGetter() { return prefManager.getPref(prefName); };
+ function prefSetter(value) { return prefManager.setPref(prefName, value); };
+
+ if (!ASSERT(!(prefName in this.defaultValues),
+ "Preference already exists: " + prefName))
+ {
+ return;
+ }
+
+ if (!setter)
+ setter = prefSetter;
+
+ if (isinstance(defaultValue, Array))
+ defaultValue.update = updateArrayPref;
+
+ var label = getMsgFrom(bundle, "pref." + prefName + ".label", null, prefName);
+ var help = getMsgFrom(bundle, "pref." + prefName + ".help", null,
+ MSG_NO_HELP);
+ if (group != "hidden")
+ {
+ if (label == prefName)
+ dd("WARNING: !!! Preference without label: " + prefName);
+ if (help == MSG_NO_HELP)
+ dd("WARNING: Preference without help text: " + prefName);
+ }
+
+ this.prefRecords[prefName] = new PrefRecord (prefName, defaultValue,
+ label, help, group);
+
+ this.prefNames.push(prefName);
+ this.prefNames.sort();
+
+ this.prefs.__defineGetter__(prefName, prefGetter);
+ this.prefs.__defineSetter__(prefName, setter);
+}
diff --git a/comm/suite/chatzilla/js/lib/protocol-handlers.jsm b/comm/suite/chatzilla/js/lib/protocol-handlers.jsm
new file mode 100644
index 0000000000..a24ca1a0de
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/protocol-handlers.jsm
@@ -0,0 +1,250 @@
+/* -*- 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/. */
+
+const EXPORTED_SYMBOLS = [
+ "ChatZillaProtocols",
+ "IRCProtocolHandlerFactory",
+ "IRCSProtocolHandlerFactory",
+ "IRCPROT_HANDLER_CID",
+ "IRCSPROT_HANDLER_CID"
+];
+
+const { classes: Cc, interfaces: Ci, results: Cr } = Components;
+
+const STANDARDURL_CONTRACTID =
+ "@mozilla.org/network/standard-url;1";
+const IOSERVICE_CONTRACTID =
+ "@mozilla.org/network/io-service;1";
+
+const IRCPROT_HANDLER_CONTRACTID =
+ "@mozilla.org/network/protocol;1?name=irc";
+const IRCSPROT_HANDLER_CONTRACTID =
+ "@mozilla.org/network/protocol;1?name=ircs";
+this.IRCPROT_HANDLER_CID =
+ Components.ID("{f21c35f4-1dd1-11b2-a503-9bf8a539ea39}");
+this.IRCSPROT_HANDLER_CID =
+ Components.ID("{f21c35f4-1dd1-11b2-a503-9bf8a539ea3a}");
+
+const IRC_MIMETYPE = "application/x-irc";
+const IRCS_MIMETYPE = "application/x-ircs";
+
+//XXXgijs: Because necko is annoying and doesn't expose this error flag, we
+// define our own constant for it. Throwing something else will show
+// ugly errors instead of seeminly doing nothing.
+const NS_ERROR_MODULE_NETWORK_BASE = 0x804b0000;
+const NS_ERROR_NO_CONTENT = NS_ERROR_MODULE_NETWORK_BASE + 17;
+
+
+function spawnChatZilla(uri) {
+ var cpmm;
+ // Ci.nsISyncMessageSender went in Gecko 61.
+ if (Ci.nsISyncMessageSender) {
+ cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ } else {
+ cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService();
+ }
+ cpmm.sendAsyncMessage("ChatZilla:SpawnChatZilla", { uri });
+}
+
+
+function IRCProtocolHandler(isSecure)
+{
+ this.isSecure = isSecure;
+}
+
+var protocolFlags = Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY;
+if ("URI_DANGEROUS_TO_LOAD" in Ci.nsIProtocolHandler) {
+ protocolFlags |= Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE;
+}
+if ("URI_NON_PERSISTABLE" in Ci.nsIProtocolHandler) {
+ protocolFlags |= Ci.nsIProtocolHandler.URI_NON_PERSISTABLE;
+}
+if ("URI_DOES_NOT_RETURN_DATA" in Ci.nsIProtocolHandler) {
+ protocolFlags |= Ci.nsIProtocolHandler.URI_DOES_NOT_RETURN_DATA;
+}
+
+IRCProtocolHandler.prototype =
+{
+ protocolFlags: protocolFlags,
+
+ allowPort(port, scheme)
+ {
+ // Allow all ports to connect, so long as they are irc: or ircs:
+ return (scheme === 'irc' || scheme === 'ircs');
+ },
+
+ newURI(spec, charset, baseURI)
+ {
+ const port = this.isSecure ? 6697 : 6667;
+
+ if (!Cc.hasOwnProperty("@mozilla.org/network/standard-url-mutator;1")) {
+ const cls = Cc[STANDARDURL_CONTRACTID];
+ const url = cls.createInstance(Ci.nsIStandardURL);
+
+ url.init(Ci.nsIStandardURL.URLTYPE_STANDARD, port, spec, charset, baseURI);
+
+ return url.QueryInterface(Ci.nsIURI);
+ }
+ return Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .init(Ci.nsIStandardURL.URLTYPE_STANDARD, port, spec, charset, baseURI)
+ .finalize();
+ },
+
+ newChannel(URI)
+ {
+ const ios = Cc[IOSERVICE_CONTRACTID].getService(Ci.nsIIOService);
+ if (!ios.allowPort(URI.port, URI.scheme))
+ throw Cr.NS_ERROR_FAILURE;
+
+ return new BogusChannel(URI, this.isSecure);
+ },
+};
+
+
+this.IRCProtocolHandlerFactory =
+{
+ createInstance(outer, iid)
+ {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ if (!iid.equals(Ci.nsIProtocolHandler) && !iid.equals(Ci.nsISupports))
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ const protHandler = new IRCProtocolHandler(false);
+ protHandler.scheme = "irc";
+ protHandler.defaultPort = 6667;
+ return protHandler;
+ },
+};
+
+
+this.IRCSProtocolHandlerFactory =
+{
+ createInstance(outer, iid)
+ {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ if (!iid.equals(Ci.nsIProtocolHandler) && !iid.equals(Ci.nsISupports))
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ const protHandler = new IRCProtocolHandler(true);
+ protHandler.scheme = "ircs";
+ protHandler.defaultPort = 6697;
+ return protHandler;
+ },
+};
+
+
+/* Bogus IRC channel used by the IRCProtocolHandler */
+function BogusChannel(URI, isSecure)
+{
+ this.URI = URI;
+ this.originalURI = URI;
+ this.isSecure = isSecure;
+ this.contentType = this.isSecure ? IRCS_MIMETYPE : IRC_MIMETYPE;
+}
+
+BogusChannel.prototype =
+{
+ /* nsISupports */
+ QueryInterface(iid)
+ {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIChannel) ||
+ iid.equals(Ci.nsIRequest))
+ {
+ return this;
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ /* nsIChannel */
+ loadAttributes: null,
+ contentLength: 0,
+ owner: null,
+ loadGroup: null,
+ notificationCallbacks: null,
+ securityInfo: null,
+
+ open(observer, context)
+ {
+ spawnChatZilla(this.URI.spec);
+ // We don't throw this (a number, not a real 'resultcode') because it
+ // upsets xpconnect if we do (error in the js console).
+ Components.returnCode = NS_ERROR_NO_CONTENT;
+ },
+
+ asyncOpen(observer, context)
+ {
+ spawnChatZilla(this.URI.spec);
+ // We don't throw this (a number, not a real 'resultcode') because it
+ // upsets xpconnect if we do (error in the js console).
+ Components.returnCode = NS_ERROR_NO_CONTENT;
+ },
+
+ asyncRead(listener, context)
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /* nsIRequest */
+ isPending()
+ {
+ return true;
+ },
+
+ status: Cr.NS_OK,
+
+ cancel(status)
+ {
+ this.status = status;
+ },
+
+ suspend()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ resume()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+};
+
+
+this.ChatZillaProtocols =
+{
+ init()
+ {
+ const compMgr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ compMgr.registerFactory(IRCPROT_HANDLER_CID,
+ "IRC protocol handler",
+ IRCPROT_HANDLER_CONTRACTID,
+ IRCProtocolHandlerFactory);
+ compMgr.registerFactory(IRCSPROT_HANDLER_CID,
+ "IRC protocol handler",
+ IRCSPROT_HANDLER_CONTRACTID,
+ IRCSProtocolHandlerFactory);
+ },
+
+ initObsolete(compMgr, fileSpec, location, type)
+ {
+ compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
+ compMgr.registerFactoryLocation(IRCPROT_HANDLER_CID,
+ "IRC protocol handler",
+ IRCPROT_HANDLER_CONTRACTID,
+ fileSpec, location, type);
+ compMgr.registerFactoryLocation(IRCSPROT_HANDLER_CID,
+ "IRCS protocol handler",
+ IRCSPROT_HANDLER_CONTRACTID,
+ fileSpec, location, type);
+ },
+};
diff --git a/comm/suite/chatzilla/js/lib/sts.js b/comm/suite/chatzilla/js/lib/sts.js
new file mode 100644
index 0000000000..25052b0985
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/sts.js
@@ -0,0 +1,210 @@
+/* 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/. */
+
+/**
+ * The base CIRCSTS object.
+ */
+function CIRCSTS()
+{
+ this.policyList = new Object();
+ this.preloadList = new Object();
+}
+
+CIRCSTS.prototype.ENABLED = false;
+CIRCSTS.prototype.USE_PRELOAD_LIST = true;
+
+/**
+ * Initializes the STS module.
+ * @param policyFile |nsIFile| for storing the STS cache.
+ */
+CIRCSTS.prototype.init = function(policyFile)
+{
+ this.policyFile = policyFile;
+ this.readCacheFromFile();
+}
+
+/**
+ * Reads the policy cache from disk.
+ */
+CIRCSTS.prototype.readCacheFromFile = function()
+{
+ if (!this.policyFile.exists())
+ return;
+
+ cacheReader = new JSONSerializer(this.policyFile);
+ this.policyList = cacheReader.deserialize();
+ cacheReader.close();
+}
+
+/**
+ * Writes the policy cache to disk.
+ */
+CIRCSTS.prototype.writeCacheToFile = function()
+{
+ cacheWriter = new JSONSerializer(this.policyFile);
+ cacheWriter.serialize(this.policyList);
+ cacheWriter.close();
+}
+
+/**
+ * Utility method for determining if an expiration time has passed.
+ * An expire time of zero is never considered to be expired (as is
+ * the case for knockout entries).
+ *
+ * @param expireTime the time to evaluate, in seconds since the UNIX
+ * epoch.
+ * @return boolean value indicating whether the expiration
+ * time has passed.
+ */
+CIRCSTS.prototype.isPolicyExpired = function(expireTime)
+{
+ if (!expireTime)
+ {
+ return false;
+ }
+
+ if (Date.now() > expireTime)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Utility method for parsing the raw CAP value for an STS policy, typically
+ * received via CAP LS or CAP NEW.
+ *
+ * @param params the comma-separated list of parameters in
+ * key[=value] format.
+ * @return the received parameters in JSON.
+ *
+ */
+CIRCSTS.prototype.parseParameters = function(params)
+{
+ var rv = new Object();
+ var keys = params.toLowerCase().split(",");
+ for (var i = 0; i < keys.length; i++)
+ {
+ var [key, value] = keys[i].split("=");
+ rv[key] = value;
+ }
+
+ return rv;
+}
+
+/**
+ * Remove a policy from the cache.
+ *
+ * @param host the host to remove a policy for.
+ */
+CIRCSTS.prototype.removePolicy = function(host)
+{
+ // If this host is in the preload list, we have to store a knockout entry.
+ // To do this, we set the port and expiration time to zero.
+ if (this.getPreloadPolicy(host))
+ {
+ this.policyList[host] = {
+ port: 0,
+ expireTime: 0
+ };
+ }
+ else
+ {
+ delete this.policyList[host];
+ }
+
+ this.writeCacheToFile();
+}
+
+/**
+ * Retrieves a policy from the preload list for the specified host.
+ *
+ * @param host the host to retrieve a policy for.
+ * @return the policy from the preload list, if any.
+ */
+CIRCSTS.prototype.getPreloadPolicy = function(host)
+{
+ if (this.USE_PRELOAD_LIST && (host in this.preloadList))
+ {
+ return this.preloadList[host];
+ }
+
+ return null;
+}
+
+/**
+ * Checks whether there is an upgrade policy in place for a given host
+ * and, if so, returns the port to connect to.
+ *
+ * @param host the host to query an upgrade policy for.
+ * @return the secure port to connect to, if any.
+ */
+CIRCSTS.prototype.getUpgradePolicy = function(host)
+{
+ if (!this.ENABLED)
+ return null;
+
+ var cdata = this.policyList[host];
+ var pdata = this.getPreloadPolicy(host);
+
+ if (cdata)
+ {
+ // We have a cached policy.
+ if (!this.isPolicyExpired(cdata.expireTime))
+ {
+ // Return null if it's a knockout entry.
+ return cdata.port || null;
+ }
+ else if (!pdata)
+ {
+ // Remove the policy if it is expired and not in the preload list.
+ this.removePolicy(host);
+ }
+ }
+
+ if (pdata)
+ {
+ return pdata.port;
+ }
+
+ return null;
+}
+
+/**
+ * Processes a given policy and caches it. This may also be used to renew a
+ * persistance policy. This should ONLY be called if we are using a secure
+ * connection. Insecure connections MUST switch to a secure port first.
+ *
+ * @param host the host to store an STS policy for.
+ * @param port the currently connected secure port.
+ * @param duration the duration of the new policy.
+ * @param duration the duration of the new policy, in seconds. This may be
+ * zero if the policy needs to be removed.
+ */
+CIRCSTS.prototype.setPolicy = function(host, port, duration)
+{
+ if (!this.ENABLED)
+ return;
+
+ port = Number(port);
+ duration = Number(duration);
+
+ // If duration is zero, that's an indication to immediately remove the
+ // policy, so here's a shortcut.
+ if (!duration)
+ {
+ this.removePolicy(host);
+ return;
+ }
+
+ var expireTime = Date.now() + duration*1000;
+
+ this.policyList[host] = {
+ port: port,
+ expireTime: expireTime
+ };
+
+ this.writeCacheToFile();
+}
diff --git a/comm/suite/chatzilla/js/lib/text-logger.js b/comm/suite/chatzilla/js/lib/text-logger.js
new file mode 100644
index 0000000000..731dbd2eaf
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/text-logger.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+
+/**
+ * Serializer for lists of data that can be printed line-by-line.
+ * If you pass an autoLimit, it will automatically call limit() once the number
+ * of appended items exceeds the limit (so the number of items will never
+ * exceed limit*2).
+ */
+
+function TextLogger(path, autoLimit)
+{
+ // Check if we can open the path. This will throw if it doesn't work
+ var f = fopen(path, ">>");
+ f.close();
+ this.path = path;
+
+ this.appended = 0;
+ if (typeof autoLimit == "number")
+ this.autoLimit = autoLimit;
+ else
+ this.autoLimit = -1;
+
+ // Limit the amount of data in the file when constructing, when asked to.
+ if (this.autoLimit != -1)
+ this.limit();
+}
+
+/**
+ * Append data (an array or single item) to the file
+ */
+TextLogger.prototype.append =
+function tl_append(data)
+{
+ if (!isinstance(data, Array))
+ data = [data];
+
+ // If we go over the limit, don't write everything twice:
+ if ((this.autoLimit != -1) &&
+ (data.length + this.appended > this.autoLimit))
+ {
+ // Collect complete set of data instead:
+ var dataInFile = this.read();
+ var newData = dataInFile.concat(data);
+ // Get the last |autoLimit| items: yay JS negative indexing!
+ newData = newData.slice(-this.autoLimit);
+ this.limit(newData);
+ return true;
+ }
+
+ var file = fopen(this.path, ">>");
+ for (var i = 0; i < data.length; i++)
+ file.write(ecmaEscape(data[i]) + "\n");
+ file.close();
+ this.appended += data.length;
+
+ return true;
+}
+
+/**
+ * Limit the data already in the file to the data provided, or the count given.
+ */
+TextLogger.prototype.limit =
+function tl_limit(dataOrCount)
+{
+ // Find data and count:
+ var data = null, count = -1;
+ if (isinstance(dataOrCount, Array))
+ {
+ data = dataOrCount;
+ count = data.length;
+ }
+ else if (typeof dataOrCount == "number")
+ {
+ count = dataOrCount;
+ data = this.read();
+ }
+ else if (this.autoLimit != -1)
+ {
+ count = this.autoLimit;
+ data = this.read();
+ }
+ else
+ {
+ throw "Can't limit the length of the file without a limit..."
+ }
+
+ // Write the right data out. Note that we use the back of the array, not
+ // the front (start from data.length - count), without dropping below 0:
+ var start = Math.max(data.length - count, 0);
+ var file = fopen(this.path, ">");
+ for (var i = start; i < data.length; i++)
+ file.write(ecmaEscape(data[i]) + "\n");
+ file.close();
+ this.appended = 0;
+
+ return true;
+}
+
+/**
+ * Reads out the data currently in the file, and returns an array.
+ */
+TextLogger.prototype.read =
+function tl_read()
+{
+ var rv = new Array(), parsedLines = new Array(), buffer = "";
+ var file = fopen(this.path, "<");
+ while (true)
+ {
+ var newData = file.read();
+ if (newData)
+ buffer += newData;
+ else if (buffer.length == 0)
+ break;
+
+ // Got more data in the buffer, so split into lines. Unless we're
+ // done, the last one might not be complete yet, so save that one.
+ // We split rather strictly on line ends, because empty lines should
+ // be preserved.
+ var lines = buffer.split(/\r?\n/);
+ if (!newData)
+ buffer = "";
+ else
+ buffer = lines.pop();
+
+ rv = rv.concat(lines);
+ }
+ // Unescape here...
+ for (var i = 0; i < rv.length; i++)
+ rv[i] = ecmaUnescape(rv[i]);
+ return rv;
+}
diff --git a/comm/suite/chatzilla/js/lib/text-serializer.js b/comm/suite/chatzilla/js/lib/text-serializer.js
new file mode 100644
index 0000000000..d9d2a3c7af
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/text-serializer.js
@@ -0,0 +1,348 @@
+/* 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/. */
+
+/* The serialized file format is pretty generic... each line (using any line
+ * separator, so we don't mind being moved between platforms) consists of
+ * a command name, and some parameters (optionally). The commands 'start'
+ * and 'end' mark the chunks of properties for each object - in this case
+ * motifs. Every command inside a start/end block is considered a property
+ * for the object. There are some rules, but we are generally pretty flexible.
+ *
+ * Example file:
+ * START <Array>
+ * START 0
+ * "message" "Food%3a%20Mmm...%20food..."
+ * END
+ * START 1
+ * "message" "Busy%3a%20Working."
+ * END
+ * START 2
+ * "message" "Not%20here."
+ * END
+ * END
+ *
+ * The whitespace at the start of the inner lines is generated by the
+ * serialisation process, but is ignored when parsing - it is only to make
+ * the file more readable.
+ *
+ * The START command may be followed by one or both of a class name (enclosed
+ * in angle brackets, as above) and a property name (the first non-<>-enclosed
+ * word). Top-level START commands must not have a property name, although a
+ * class name is fine. Only the following class names are supported:
+ * - Object (the default)
+ * - Array
+ *
+ * For arrays, there are some limitations; saving an array cannot save any
+ * properties that are not numerics, due to limitations in JS' for...in
+ * enumeration. Thus, for loading, only items with numeric property names are
+ * allowed. If an item is STARTed inside an array, and specifies no property
+ * name, it will be push()ed into the array instead.
+ */
+
+function TextSerializer(file)
+{
+ this._initialized = false;
+ if (typeof file == "string")
+ this._file = new nsLocalFile(file);
+ else
+ this._file = file;
+ this._open = false;
+ this._buffer = "";
+ this._lines = [];
+ this.lineEnd = "\n";
+ this._initialized = true;
+}
+
+/* open(direction)
+ *
+ * Opens the serializer on the file specified when created, in either the read
+ * ("<") or write (">") directions. When the file is open, only the appropriate
+ * direction of serialization/deserialization may be performed.
+ *
+ * Note: serialize and deserialize automatically open the file if it is not
+ * open.
+ */
+TextSerializer.prototype.open =
+function ts_open(dir)
+{
+ if (!ASSERT((dir == ">") || (dir == "<"), "Bad serialization direction!"))
+ return false;
+ if (this._open)
+ return false;
+
+ this._fileStream = new LocalFile(this._file, dir);
+ if ((typeof this._fileStream == "object") && this._fileStream)
+ this._open = true;
+
+ return this._open;
+}
+
+/* close()
+ *
+ * Closes the file stream and ends reading or writing.
+ */
+TextSerializer.prototype.close =
+function ts_close()
+{
+ if (this._open)
+ {
+ this._fileStream.close();
+ delete this._fileStream;
+ this._open = false;
+ }
+ return true;
+}
+
+/* serialize(object)
+ *
+ * Serializes a single object into the file stream. All properties of the object
+ * are stored in the stream, including properties that contain other objects.
+ */
+TextSerializer.prototype.serialize =
+function ts_serialize(obj)
+{
+ if (!this._open)
+ this.open(">");
+ if (!ASSERT(this._open, "Unable to open the file for writing!"))
+ return;
+
+ var me = this;
+
+ function writeObjProps(o, indent)
+ {
+ function writeProp(name, val)
+ {
+ me._fileStream.write(indent + "\"" + ecmaEscape(name) + "\" " + val +
+ me.lineEnd);
+ };
+
+ for (var p in o)
+ {
+ switch (typeof o[p])
+ {
+ case "string":
+ writeProp(p, '"' + ecmaEscape(o[p]) + '"');
+ break;
+
+ case "number":
+ case "boolean":
+ case "null": // (just in case)
+ case "undefined":
+ // These all serialise to what we want.
+ writeProp(p, o[p]);
+ break;
+
+ case "function":
+ if (isinstance(o[p], RegExp))
+ writeProp(p, ecmaEscape("" + o[p]));
+ // Can't serialize non-RegExp functions (yet).
+ break;
+
+ case "object":
+ if (o[p] == null)
+ {
+ // typeof null == "object", just to catch us out.
+ writeProp(p, "null");
+ }
+ else
+ {
+ var className = "";
+ if (isinstance(o[p], Array))
+ className = "<Array> ";
+
+ me._fileStream.write(indent + "START " + className +
+ ecmaEscape(p) + me.lineEnd);
+ writeObjProps(o[p], indent + " ");
+ me._fileStream.write(indent + "END" + me.lineEnd);
+ }
+ break;
+
+ default:
+ // Can't handle anything else!
+ }
+ }
+ };
+
+ if (isinstance(obj, Array))
+ this._fileStream.write("START <Array>" + this.lineEnd);
+ else
+ this._fileStream.write("START" + this.lineEnd);
+ writeObjProps(obj, " ");
+ this._fileStream.write("END" + this.lineEnd);
+}
+
+/* deserialize()
+ *
+ * Reads in enough of the file to deserialize (realize) a single object. The
+ * object deserialized is returned; all sub-properties of the object are
+ * deserialized with it.
+ */
+TextSerializer.prototype.deserialize =
+function ts_deserialize()
+{
+ if (!this._open)
+ this.open("<");
+ if (!ASSERT(this._open, "Unable to open the file for reading!"))
+ return false;
+
+ var obj = null;
+ var rv = null;
+ var objs = new Array();
+
+ while (true)
+ {
+ if (this._lines.length == 0)
+ {
+ var newData = this._fileStream.read();
+ if (newData)
+ this._buffer += newData;
+ else if (this._buffer.length == 0)
+ break;
+
+ // Got more data in the buffer, so split into lines. Unless we're
+ // done, the last one might not be complete yet, so save that one.
+ var lines = this._buffer.split(/[\r\n]+/);
+ if (!newData)
+ this._buffer = "";
+ else
+ this._buffer = lines.pop();
+
+ this._lines = this._lines.concat(lines);
+ if (this._lines.length == 0)
+ break;
+ }
+
+ // Split each line into "command params...".
+ var parts = this._lines[0].match(/^\s*(\S+)(?:\s+(.*))?$/);
+ var command = parts[1];
+ var params = parts[2];
+
+ // 'start' and 'end' commands are special.
+ switch (command.toLowerCase())
+ {
+ case "start":
+ var paramList = new Array();
+ if (params)
+ paramList = params.split(/\s+/g);
+
+ var className = "";
+ if ((paramList.length > 0) && /^<\w+>$/i.test(paramList[0]))
+ {
+ className = paramList[0].substr(1, paramList[0].length - 2);
+ paramList.shift();
+ }
+
+ if (!rv)
+ {
+ /* The top-level objects are not allowed a property name
+ * in their START command (it is meaningless).
+ */
+ ASSERT(paramList.length == 0, "Base object with name!");
+ // Construct the top-level object.
+ if (className)
+ rv = obj = new window[className]();
+ else
+ rv = obj = new Object();
+ }
+ else
+ {
+ var n;
+ if (paramList.length == 0)
+ {
+ /* Create a new object level, but with no name. This is
+ * only valid if the parent level is an array.
+ */
+ if (!ASSERT(isinstance(obj, Array), "Parent not Array!"))
+ return null;
+ if (className)
+ n = new window[className]();
+ else
+ n = new Object();
+ objs.push(obj);
+ obj.push(n);
+ obj = n;
+ }
+ else
+ {
+ /* Create a new object level, store the reference on the
+ * parent, and set the new object as the current.
+ */
+ if (className)
+ n = new window[className]();
+ else
+ n = new Object();
+ objs.push(obj);
+ obj[ecmaUnescape(paramList[0])] = n;
+ obj = n;
+ }
+ }
+
+ this._lines.shift();
+ break;
+
+ case "end":
+ this._lines.shift();
+ if (rv && (objs.length == 0))
+ {
+ // We're done for the day.
+ return rv;
+ }
+ // Return to the previous object level.
+ obj = objs.pop();
+ if (!ASSERT(obj, "Waaa! no object level to return to!"))
+ return rv;
+ break;
+
+ default:
+ this._lines.shift();
+ // The property name may be enclosed in quotes.
+ if (command[0] == '"')
+ command = command.substr(1, command.length - 2);
+ // But it is always escaped.
+ command = ecmaUnescape(command);
+
+ if (!obj)
+ {
+ /* If we find a line that is NOT starting a new object, and
+ * we don't have a current object, we just assume the START
+ * command was missed.
+ */
+ rv = obj = new Object();
+ }
+ if (params[0] == '"') // String
+ {
+ // Remove quotes, then unescape.
+ params = params.substr(1, params.length - 2);
+ obj[command] = ecmaUnescape(params);
+ }
+ else if (params[0] == "/") // RegExp
+ {
+ var p = params.match(/^\/(.*)\/(\w*)$/);
+ if (ASSERT(p, "RepExp entry malformed, ignored!"))
+ {
+ var re = new RegExp(ecmaUnescape(p[1]), p[2]);
+ obj[command] = re;
+ }
+ }
+ else if (params == "null") // null
+ {
+ obj[command] = null;
+ }
+ else if (params == "undefined") // undefined
+ {
+ obj[command] = undefined;
+ }
+ else if ((params == "true") || (params == "false")) // boolean
+ {
+ obj[command] = (params == "true");
+ }
+ else // Number
+ {
+ obj[command] = Number(params);
+ }
+ break;
+ }
+ }
+ return null;
+}
diff --git a/comm/suite/chatzilla/js/lib/utils.js b/comm/suite/chatzilla/js/lib/utils.js
new file mode 100644
index 0000000000..d8aa88baef
--- /dev/null
+++ b/comm/suite/chatzilla/js/lib/utils.js
@@ -0,0 +1,1490 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+// Namespaces we happen to need:
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+var utils = new Object();
+
+var DEBUG = true;
+var dd, warn, TEST, ASSERT;
+
+if (DEBUG) {
+ var _dd_pfx = "";
+ var _dd_singleIndent = " ";
+ var _dd_indentLength = _dd_singleIndent.length;
+ var _dd_currentIndent = "";
+ var _dd_lastDumpWasOpen = false;
+ var _dd_timeStack = new Array();
+ var _dd_disableDepth = Number.MAX_VALUE;
+ var _dd_currentDepth = 0;
+ dd = function _dd(str) {
+ if (typeof str != "string") {
+ dump(str + "\n");
+ } else if (str == "") {
+ dump("<empty-string>\n");
+ } else if (str[str.length - 1] == "{") {
+ ++_dd_currentDepth;
+ if (_dd_currentDepth >= _dd_disableDepth)
+ return;
+ if (str.indexOf("OFF") == 0)
+ _dd_disableDepth = _dd_currentDepth;
+ _dd_timeStack.push (new Date());
+ if (_dd_lastDumpWasOpen)
+ dump("\n");
+ dump (_dd_pfx + _dd_currentIndent + str);
+ _dd_currentIndent += _dd_singleIndent;
+ _dd_lastDumpWasOpen = true;
+ } else if (str[0] == "}") {
+ if (--_dd_currentDepth >= _dd_disableDepth)
+ return;
+ _dd_disableDepth = Number.MAX_VALUE;
+ var sufx = (new Date() - _dd_timeStack.pop()) / 1000 + " sec";
+ _dd_currentIndent =
+ _dd_currentIndent.substr(0, _dd_currentIndent.length -
+ _dd_indentLength);
+ if (_dd_lastDumpWasOpen)
+ dump(str + " " + sufx + "\n");
+ else
+ dump(_dd_pfx + _dd_currentIndent + str + " " +
+ sufx + "\n");
+ _dd_lastDumpWasOpen = false;
+ } else {
+ if (_dd_currentDepth >= _dd_disableDepth)
+ return;
+ if (_dd_lastDumpWasOpen)
+ dump("\n");
+ dump(_dd_pfx + _dd_currentIndent + str + "\n");
+ _dd_lastDumpWasOpen = false;
+ }
+ }
+ warn = function (msg) { dd("** WARNING " + msg + " **"); }
+ TEST = ASSERT = function _assert(expr, msg) {
+ if (!expr) {
+ var m = "** ASSERTION FAILED: " + msg + " **\n" +
+ getStackTrace();
+ try {
+ alert(m);
+ } catch(ex) {}
+ dd(m);
+ return false;
+ } else {
+ return true;
+ }
+ }
+} else {
+ dd = warn = TEST = ASSERT = function (){};
+}
+
+function dumpObject (o, pfx, sep)
+{
+ var p;
+ var s = "";
+
+ sep = (typeof sep == "undefined") ? " = " : sep;
+ pfx = (typeof pfx == "undefined") ? "" : pfx;
+
+ for (p in o)
+ {
+ if (typeof (o[p]) != "function")
+ s += pfx + p + sep + o[p] + "\n";
+ else
+ s += pfx + p + sep + "function\n";
+ }
+
+ return s;
+
+}
+
+/* Dumps an object in tree format, recurse specifiec the the number of objects
+ * to recurse, compress is a boolean that can uncompress (true) the output
+ * format, and level is the number of levels to intitialy indent (only useful
+ * internally.) A sample dumpObjectTree (o, 1) is shown below.
+ *
+ * + parent (object)
+ * + users (object)
+ * | + jsbot (object)
+ * | + mrjs (object)
+ * | + nakkezzzz (object)
+ * | *
+ * + bans (object)
+ * | *
+ * + topic (string) 'ircclient.js:59: nothing is not defined'
+ * + getUsersLength (function) 9 lines
+ * *
+ */
+function dumpObjectTree (o, recurse, compress, level)
+{
+ var s = "";
+ var pfx = "";
+
+ if (typeof recurse == "undefined")
+ recurse = 0;
+ if (typeof level == "undefined")
+ level = 0;
+ if (typeof compress == "undefined")
+ compress = true;
+
+ for (var i = 0; i < level; i++)
+ pfx += (compress) ? "| " : "| ";
+
+ var tee = (compress) ? "+ " : "+- ";
+
+ for (i in o)
+ {
+ var t, ex;
+
+ try
+ {
+ t = typeof o[i];
+ }
+ catch (ex)
+ {
+ t = "ERROR";
+ }
+
+ switch (t)
+ {
+ case "function":
+ var sfunc = String(o[i]).split("\n");
+ if (sfunc[2] == " [native code]")
+ sfunc = "[native code]";
+ else
+ if (sfunc.length == 1)
+ sfunc = String(sfunc);
+ else
+ sfunc = sfunc.length + " lines";
+ s += pfx + tee + i + " (function) " + sfunc + "\n";
+ break;
+
+ case "object":
+ s += pfx + tee + i + " (object)";
+ if (o[i] == null)
+ {
+ s += " null\n";
+ break;
+ }
+
+ s += "\n";
+
+ if (!compress)
+ s += pfx + "|\n";
+ if ((i != "parent") && (recurse))
+ s += dumpObjectTree (o[i], recurse - 1,
+ compress, level + 1);
+ break;
+
+ case "string":
+ if (o[i].length > 200)
+ s += pfx + tee + i + " (" + t + ") " +
+ o[i].length + " chars\n";
+ else
+ s += pfx + tee + i + " (" + t + ") '" + o[i] + "'\n";
+ break;
+
+ case "ERROR":
+ s += pfx + tee + i + " (" + t + ") ?\n";
+ break;
+
+ default:
+ s += pfx + tee + i + " (" + t + ") " + o[i] + "\n";
+
+ }
+
+ if (!compress)
+ s += pfx + "|\n";
+
+ }
+
+ s += pfx + "*\n";
+
+ return s;
+
+}
+
+function ecmaEscape(str)
+{
+ function replaceNonPrintables(ch)
+ {
+ var rv = ch.charCodeAt().toString(16);
+ if (rv.length == 1)
+ rv = "0" + rv;
+ else if (rv.length == 3)
+ rv = "u0" + rv;
+ else if (rv.length == 4)
+ rv = "u" + rv;
+
+ return "%" + rv;
+ };
+
+ // Replace any character that is not in the 69 character set
+ // [ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./]
+ // with an escape sequence. Two digit sequences in the form %XX are used
+ // for characters whose codepoint is less than 255, %uXXXX for all others.
+ // See section B.2.1 of ECMA-262 rev3 for more information.
+ return str.replace(/[^A-Za-z0-9@*_+.\-\/]/g, replaceNonPrintables);
+}
+
+function ecmaUnescape(str)
+{
+ function replaceEscapes(seq)
+ {
+ var ary = seq.match(/([\da-f]{1,2})(.*)|u([\da-f]{1,4})/i);
+ if (!ary)
+ return "<ERROR>";
+
+ var rv;
+ if (ary[1])
+ {
+ // two digit escape, possibly with cruft after
+ rv = String.fromCharCode(parseInt(ary[1], 16)) + ary[2];
+ }
+ else
+ {
+ // four digits, no cruft
+ rv = String.fromCharCode(parseInt(ary[3], 16));
+ }
+
+ return rv;
+ };
+
+ // Replace the escape sequences %X, %XX, %uX, %uXX, %uXXX, and %uXXXX with
+ // the characters they represent, where X is a hexadecimal digit.
+ // See section B.2.2 of ECMA-262 rev3 for more information.
+ return str.replace(/%u?([\da-f]{1,4})/ig, replaceEscapes);
+}
+
+function encodeForXMLAttribute(value) {
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;")
+ .replace(/'/g, "&apos;");
+}
+
+function replaceVars(str, vars)
+{
+ // replace "string $with a $variable", with
+ // "string " + vars["with"] + " with a " + vars["variable"]
+
+ function doReplace(symbol)
+ {
+ var name = symbol.substr(1);
+ if (name in vars)
+ return vars[name];
+
+ return "$" + name;
+ };
+
+ return str.replace(/(\$\w[\w\d\-]+)/g, doReplace);
+}
+
+function formatException(ex)
+{
+ if (isinstance(ex, Error))
+ {
+ return getMsg(MSG_FMT_JSEXCEPTION, [ex.name, ex.message, ex.fileName,
+ ex.lineNumber]);
+ }
+ if ((typeof ex == "object") && ("filename" in ex))
+ {
+ return getMsg(MSG_FMT_JSEXCEPTION, [ex.name, ex.message, ex.filename,
+ ex.lineNumber]);
+ }
+
+ return String(ex);
+}
+
+/*
+ * Clones an existing object (Only the enumerable properties
+ * of course.) use as a function..
+ * var c = Clone (obj);
+ * or a constructor...
+ * var c = new Clone (obj);
+ */
+function Clone (obj)
+{
+ var robj = new Object();
+
+ if ("__proto__" in obj)
+ {
+ // Special clone for Spidermonkey.
+ for (var p in obj)
+ {
+ if (obj.hasOwnProperty(p))
+ robj[p] = obj[p];
+ }
+ robj.__proto__ = obj.__proto__;
+ }
+ else
+ {
+ for (var p in obj)
+ robj[p] = obj[p];
+ }
+
+ return robj;
+
+}
+
+function Copy(source, dest, overwrite)
+{
+ if (!dest)
+ dest = new Object();
+
+ for (var p in source)
+ {
+ if (overwrite || !(p in dest))
+ dest[p] = source[p];
+ }
+
+ return dest;
+}
+
+/*
+ * matches a real object against one or more pattern objects.
+ * if you pass an array of pattern objects, |negate| controls whether to check
+ * if the object matches ANY of the patterns, or NONE of the patterns.
+ */
+function matchObject (o, pattern, negate)
+{
+ negate = Boolean(negate);
+
+ function _match (o, pattern)
+ {
+ if (isinstance(pattern, Function))
+ return pattern(o);
+
+ for (var p in pattern)
+ {
+ var val;
+ /* nice to have, but slow as molases, allows you to match
+ * properties of objects with obj$prop: "foo" syntax */
+ /*
+ if (p[0] == "$")
+ val = eval ("o." +
+ p.substr(1,p.length).replace (/\$/g, "."));
+ else
+ */
+ val = o[p];
+
+ if (isinstance(pattern[p], Function))
+ {
+ if (!pattern[p](val))
+ return false;
+ }
+ else
+ {
+ var ary = (new String(val)).match(pattern[p]);
+ if (ary == null)
+ return false;
+ else
+ o.matchresult = ary;
+ }
+ }
+
+ return true;
+
+ }
+
+ if (!isinstance(pattern, Array))
+ return Boolean (negate ^ _match(o, pattern));
+
+ for (var i in pattern)
+ if (_match (o, pattern[i]))
+ return !negate;
+
+ return negate;
+
+}
+
+function equalsObject(o1, o2)
+{
+ for (var p in o1)
+ {
+ if (!(p in o2) || (o1[p] != o2[p]))
+ return false;
+ }
+ for (p in o2)
+ {
+ // If the property did exist in o1, the previous loop tested it:
+ if (!(p in o1))
+ return false;
+ }
+ return true;
+}
+
+function utils_lcfn(text)
+{
+ return text.toLowerCase();
+}
+
+function matchEntry (partialName, list, lcFn)
+{
+
+ if ((typeof partialName == "undefined") ||
+ (String(partialName) == ""))
+ {
+ var ary = new Array();
+ for (var i in list)
+ ary.push(i);
+ return ary;
+ }
+
+ if (typeof lcFn != "function")
+ lcFn = utils_lcfn;
+
+ ary = new Array();
+
+ for (i in list)
+ {
+ if (lcFn(list[i]).indexOf(lcFn(partialName)) == 0)
+ ary.push(i);
+ }
+
+ return ary;
+
+}
+
+function encodeChar(ch)
+{
+ return "%" + ch.charCodeAt(0).toString(16);
+}
+
+function escapeFileName(fileName)
+{
+ // Escape / \ : * ? " < > | so they don't cause trouble.
+ return fileName.replace(/[\/\\\:\*\?"<>\|]/g, encodeChar);
+}
+
+function getCommonPfx (list, lcFn)
+{
+ var pfx = list[0];
+ var l = list.length;
+
+ if (typeof lcFn != "function")
+ lcFn = utils_lcfn;
+
+ for (var i = 0; i < l; i++)
+ {
+ for (var c = 0; c < pfx.length; ++c)
+ {
+ if (c >= list[i].length)
+ {
+ pfx = pfx.substr(0, c);
+ break;
+ }
+ else
+ {
+ if (lcFn(pfx[c]) != lcFn(list[i][c]))
+ pfx = pfx.substr(0, c);
+ }
+ }
+ }
+
+ return pfx;
+
+}
+
+function openTopWin (url)
+{
+ return openDialog (getBrowserURL(), "_blank", "chrome,all,dialog=no", url);
+}
+
+function getWindowByType (windowType)
+{
+ const MEDIATOR_CONTRACTID =
+ "@mozilla.org/appshell/window-mediator;1";
+ const nsIWindowMediator = Components.interfaces.nsIWindowMediator;
+
+ var windowManager =
+ Components.classes[MEDIATOR_CONTRACTID].getService(nsIWindowMediator);
+
+ return windowManager.getMostRecentWindow(windowType);
+}
+
+function toOpenWindowByType(inType, url, features)
+{
+ var topWindow = getWindowByType(inType);
+
+ if (typeof features == "undefined")
+ features = "chrome,extrachrome,menubar,resizable," +
+ "scrollbars,status,toolbar";
+
+ if (topWindow)
+ topWindow.focus();
+ else
+ window.open(url, "_blank", features);
+}
+
+function renameProperty (obj, oldname, newname)
+{
+
+ if (oldname == newname)
+ return;
+
+ obj[newname] = obj[oldname];
+ delete obj[oldname];
+
+}
+
+function newObject(contractID, iface)
+{
+ var rv;
+ var cls = Components.classes[contractID];
+
+ if (!cls)
+ return null;
+
+ switch (typeof iface)
+ {
+ case "undefined":
+ rv = cls.createInstance();
+ break;
+
+ case "string":
+ rv = cls.createInstance(Components.interfaces[iface]);
+ break;
+
+ case "object":
+ rv = cls.createInstance(iface);
+ break;
+
+ default:
+ rv = null;
+ break;
+ }
+
+ return rv;
+
+}
+
+function getService(contractID, iface)
+{
+ var rv;
+ var cls = Components.classes[contractID];
+
+ if (!cls)
+ return null;
+
+ switch (typeof iface)
+ {
+ case "undefined":
+ rv = cls.getService();
+ break;
+
+ case "string":
+ rv = cls.getService(Components.interfaces[iface]);
+ break;
+
+ case "object":
+ rv = cls.getService(iface);
+ break;
+
+ default:
+ rv = null;
+ break;
+ }
+
+ return rv;
+
+}
+
+function getNSSErrorClass(errorCode)
+{
+ var nssErrSvc = getService("@mozilla.org/nss_errors_service;1", "nsINSSErrorsService");
+
+ try
+ {
+ return nssErrSvc.getErrorClass(errorCode);
+ }
+ catch
+ {
+ return 0;
+ }
+}
+
+function getContentWindow(frame)
+{
+ try
+ {
+ if (!frame || !("contentWindow" in frame))
+ return false;
+
+ // The "in" operator does not detect wrappedJSObject, so don't bother.
+ if (frame.contentWindow.wrappedJSObject)
+ return frame.contentWindow.wrappedJSObject;
+ return frame.contentWindow;
+ }
+ catch (ex)
+ {
+ // throws exception is contentWindow is gone
+ return null;
+ }
+}
+
+function getContentDocument(frame)
+{
+ try
+ {
+ if (!frame || !("contentDocument" in frame))
+ return false;
+
+ // The "in" operator does not detect wrappedJSObject, so don't bother.
+ if (frame.contentDocument.wrappedJSObject)
+ return frame.contentDocument.wrappedJSObject;
+ return frame.contentDocument;
+ }
+ catch (ex)
+ {
+ // throws exception is contentDocument is gone
+ return null;
+ }
+}
+
+function getPriv (priv)
+{
+ var rv = true;
+
+ try
+ {
+ netscape.security.PrivilegeManager.enablePrivilege(priv);
+ }
+ catch (e)
+ {
+ dd ("getPriv: unable to get privlege '" + priv + "': " + e);
+ rv = false;
+ }
+
+ return rv;
+
+}
+
+function len(o)
+{
+ var l = 0;
+
+ for (var p in o)
+ ++l;
+
+ return l;
+}
+
+function keys (o)
+{
+ var rv = new Array();
+
+ for (var p in o)
+ rv.push(p);
+
+ return rv;
+
+}
+
+function stringTrim (s)
+{
+ if (!s)
+ return "";
+ s = s.replace (/^\s+/, "");
+ return s.replace (/\s+$/, "");
+
+}
+
+/* the offset should be in seconds, it will be rounded to 2 decimal places */
+function formatDateOffset (offset, format)
+{
+ var seconds = roundTo(offset % 60, 2);
+ var minutes = Math.floor(offset / 60);
+ var hours = Math.floor(minutes / 60);
+ minutes = minutes % 60;
+ var days = Math.floor(hours / 24);
+ hours = hours % 24;
+
+ if (!format)
+ {
+ var ary = new Array();
+
+ if (days == 1)
+ ary.push(MSG_DAY);
+ else if (days > 0)
+ ary.push(getMsg(MSG_DAYS, days));
+
+ if (hours == 1)
+ ary.push(MSG_HOUR);
+ else if (hours > 0)
+ ary.push(getMsg(MSG_HOURS, hours));
+
+ if (minutes == 1)
+ ary.push(MSG_MINUTE);
+ else if (minutes > 0)
+ ary.push(getMsg(MSG_MINUTES, minutes));
+
+ if (seconds == 1)
+ ary.push(MSG_SECOND);
+ else if (seconds > 0 || offset == 0)
+ ary.push(getMsg(MSG_SECONDS, seconds));
+
+ format = ary.join(", ");
+ }
+ else
+ {
+ format = format.replace ("%d", days);
+ format = format.replace ("%h", hours);
+ format = format.replace ("%m", minutes);
+ format = format.replace ("%s", seconds);
+ }
+
+ return format;
+}
+
+function arrayHasElementAt(ary, i)
+{
+ return typeof ary[i] != "undefined";
+}
+
+function arrayContains (ary, elem)
+{
+ return (arrayIndexOf (ary, elem) != -1);
+}
+
+function arrayIndexOf (ary, elem)
+{
+ for (var i in ary)
+ if (ary[i] == elem)
+ return i;
+
+ return -1;
+}
+
+function arrayInsertAt (ary, i, o)
+{
+ ary.splice (i, 0, o);
+}
+
+function arrayRemoveAt (ary, i)
+{
+ ary.splice (i, 1);
+}
+
+function objectContains(o, p)
+{
+ return Object.hasOwnProperty.call(o, p);
+}
+
+/* length should be an even number >= 6 */
+function abbreviateWord (str, length)
+{
+ if (str.length <= length || length < 6)
+ return str;
+
+ var left = str.substr (0, (length / 2) - 1);
+ var right = str.substr (str.length - (length / 2) + 1);
+
+ return left + "..." + right;
+}
+
+/*
+ * Inserts the string |hyphen| into string |str| every |pos| characters.
+ * If there are any wordbreaking characters in |str| within -/+5 characters of
+ * of a |pos| then the hyphen is inserted there instead, in order to produce a
+ * "cleaner" break.
+ */
+function hyphenateWord (str, pos, hyphen)
+{
+ if (str.length <= pos)
+ return str;
+ if (typeof hyphen == "undefined")
+ hyphen = " ";
+
+ /* search for a nice place to break the word, fuzzfactor of +/-5, centered
+ * around |pos| */
+ var splitPos =
+ str.substring(pos - 5, pos + 5).search(/[^A-Za-z0-9]/);
+
+ splitPos = (splitPos != -1) ? pos - 4 + splitPos : pos;
+ var left = str.substr (0, splitPos);
+ var right = hyphenateWord(str.substr (splitPos), pos, hyphen);
+
+ return left + hyphen + right;
+}
+
+/*
+ * Like hyphenateWord, except individual chunks of the word are returned as
+ * elements of an array.
+ */
+function splitLongWord (str, pos)
+{
+ if (str.length <= pos)
+ return [str];
+
+ var ary = new Array();
+ var right = str;
+
+ while (right.length > pos)
+ {
+ /* search for a nice place to break the word, fuzzfactor of +/-5,
+ * centered around |pos| */
+ var splitPos =
+ right.substring(pos - 5, pos + 5).search(/[^A-Za-z0-9]/);
+
+ splitPos = (splitPos != -1) ? pos - 4 + splitPos : pos;
+ ary.push(right.substr (0, splitPos));
+ right = right.substr (splitPos);
+ }
+
+ ary.push (right);
+
+ return ary;
+}
+
+function getRandomElement (ary)
+{
+
+ return ary[Math.floor(Math.random() * ary.length)];
+
+}
+
+function roundTo (num, prec)
+{
+
+ return Math.round(num * Math.pow (10, prec)) / Math.pow (10, prec);
+
+}
+
+function randomRange (min, max)
+{
+
+ if (typeof min == "undefined")
+ min = 0;
+
+ if (typeof max == "undefined")
+ max = 1;
+
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+
+}
+
+// Creates a random string of |len| characters from a-z, A-Z, 0-9.
+function randomString(len) {
+ var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var rv = "";
+
+ for (var i = 0; i < len; i++)
+ rv += chars.substr(Math.floor(Math.random() * chars.length), 1);
+
+ return rv;
+}
+
+function getStackTrace ()
+{
+ var frame = Components.stack.caller;
+ var str = "<top>";
+
+ while (frame)
+ {
+ var name = frame.name ? frame.name : "[anonymous]";
+ str += "\n" + name + "@" + frame.lineNumber;
+ frame = frame.caller;
+ }
+
+ return str;
+
+}
+
+function getInterfaces (cls)
+{
+ var rv = new Object();
+ var e;
+
+ for (var i in Components.interfaces)
+ {
+ try
+ {
+ var ifc = Components.interfaces[i];
+ cls.QueryInterface(ifc);
+ rv[i] = ifc;
+ }
+ catch (e)
+ {
+ /* nada */
+ }
+ }
+
+ return rv;
+
+}
+
+/**
+ * Calls a named function for each element in an array, sending
+ * the same parameter each call.
+ *
+ * @param ary an array of objects
+ * @param func_name string name of function to call.
+ * @param data data object to pass to each object.
+ */
+function mapObjFunc(ary, func_name, data)
+{
+ /*
+ * WARNING: Caller assumes resonsibility to verify ary
+ * and func_name
+ */
+
+ for (var i in ary)
+ ary[i][func_name](data);
+}
+
+/**
+ * Passes each element of an array to a given function object.
+ *
+ * @param func a function object.
+ * @param ary an array of values.
+ */
+function map(func, ary) {
+
+ /*
+ * WARNING: Caller assumnes responsibility to verify
+ * func and ary.
+ */
+
+ for (var i in ary)
+ func(ary[i]);
+
+}
+
+function getSpecialDirectory(name)
+{
+ if (!("directoryService" in utils))
+ {
+ const DS_CTR = "@mozilla.org/file/directory_service;1";
+ const nsIProperties = Components.interfaces.nsIProperties;
+
+ utils.directoryService =
+ Components.classes[DS_CTR].getService(nsIProperties);
+ }
+
+ return utils.directoryService.get(name, Components.interfaces.nsIFile);
+}
+
+function getFileFromURLSpec(url)
+{
+ var handler = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ return handler.getFileFromURLSpec(url);
+}
+
+function getURLSpecFromFile(file)
+{
+ if (!file)
+ return null;
+
+ if (typeof file == "string")
+ {
+ let fileObj = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsIFile);
+ fileObj.initWithPath(file);
+ file = fileObj;
+ }
+
+ var fileHandler = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ return fileHandler.getURLSpecFromFile(file);
+}
+
+function alert(msg, parent, title)
+{
+ var PROMPT_CTRID = "@mozilla.org/embedcomp/prompt-service;1";
+ var nsIPromptService = Components.interfaces.nsIPromptService;
+ var ps = Components.classes[PROMPT_CTRID].getService(nsIPromptService);
+ if (!parent)
+ parent = window;
+ if (!title)
+ title = MSG_ALERT;
+ ps.alert (parent, title, msg);
+}
+
+function confirm(msg, parent, title)
+{
+ var PROMPT_CTRID = "@mozilla.org/embedcomp/prompt-service;1";
+ var nsIPromptService = Components.interfaces.nsIPromptService;
+ var ps = Components.classes[PROMPT_CTRID].getService(nsIPromptService);
+ if (!parent)
+ parent = window;
+ if (!title)
+ title = MSG_CONFIRM;
+ return ps.confirm (parent, title, msg);
+}
+
+function confirmEx(msg, buttons, defaultButton, checkText,
+ checkVal, parent, title)
+{
+ /* Note that on versions before Mozilla 0.9, using 3 buttons,
+ * the revert or dontsave button, or custom button titles will NOT work.
+ *
+ * The buttons should be listed in the 'accept', 'cancel' and 'extra' order,
+ * and the exact button order is host app- and platform-dependant.
+ * For example, on Windows this is usually [button 1] [button 3] [button 2],
+ * and on Linux [button 3] [button 2] [button 1].
+ */
+ var PROMPT_CTRID = "@mozilla.org/embedcomp/prompt-service;1";
+ var nsIPromptService = Components.interfaces.nsIPromptService;
+ var ps = Components.classes[PROMPT_CTRID].getService(nsIPromptService);
+
+ var buttonConstants = {
+ ok: ps.BUTTON_TITLE_OK,
+ cancel: ps.BUTTON_TITLE_CANCEL,
+ yes: ps.BUTTON_TITLE_YES,
+ no: ps.BUTTON_TITLE_NO,
+ save: ps.BUTTON_TITLE_SAVE,
+ revert: ps.BUTTON_TITLE_REVERT,
+ dontsave: ps.BUTTON_TITLE_DONT_SAVE
+ };
+ var buttonFlags = 0;
+ var buttonText = [null, null, null];
+
+ if (!isinstance(buttons, Array))
+ throw "buttons parameter must be an Array";
+ if ((buttons.length < 1) || (buttons.length > 3))
+ throw "the buttons array must have 1, 2 or 3 elements";
+
+ for (var i = 0; i < buttons.length; i++)
+ {
+ var buttonFlag = ps.BUTTON_TITLE_IS_STRING;
+ if ((buttons[i][0] == "!") && (buttons[i].substr(1) in buttonConstants))
+ buttonFlag = buttonConstants[buttons[i].substr(1)];
+ else
+ buttonText[i] = buttons[i];
+
+ buttonFlags += ps["BUTTON_POS_" + i] * buttonFlag;
+ }
+
+ // ignore anything but a proper number
+ var defaultIsNumber = (typeof defaultButton == "number");
+ if (defaultIsNumber && arrayHasElementAt(buttons, defaultButton))
+ buttonFlags += ps["BUTTON_POS_" + defaultButton + "_DEFAULT"];
+
+ if (!parent)
+ parent = window;
+ if (!title)
+ title = MSG_CONFIRM;
+ if (!checkVal)
+ checkVal = new Object();
+
+ var rv = ps.confirmEx(parent, title, msg, buttonFlags, buttonText[0],
+ buttonText[1], buttonText[2], checkText, checkVal);
+ return rv;
+}
+
+function prompt(msg, initial, parent, title)
+{
+ var PROMPT_CTRID = "@mozilla.org/embedcomp/prompt-service;1";
+ var nsIPromptService = Components.interfaces.nsIPromptService;
+ var ps = Components.classes[PROMPT_CTRID].getService(nsIPromptService);
+ if (!parent)
+ parent = window;
+ if (!title)
+ title = MSG_PROMPT;
+ var rv = { value: initial };
+
+ if (!ps.prompt (parent, title, msg, rv, null, {value: null}))
+ return null;
+
+ return rv.value;
+}
+
+function promptPassword(msg, initial, parent, title)
+{
+ var PROMPT_CTRID = "@mozilla.org/embedcomp/prompt-service;1";
+ var nsIPromptService = Components.interfaces.nsIPromptService;
+ var ps = Components.classes[PROMPT_CTRID].getService(nsIPromptService);
+ if (!parent)
+ parent = window;
+ if (!title)
+ title = MSG_PROMPT;
+ var rv = { value: initial };
+
+ if (!ps.promptPassword (parent, title, msg, rv, null, {value: null}))
+ return null;
+
+ return rv.value;
+}
+
+function viewCert(cert, parent)
+{
+ var cd = getService("@mozilla.org/nsCertificateDialogs;1",
+ "nsICertificateDialogs");
+ if (!parent)
+ parent = window;
+ cd.viewCert(parent, cert);
+}
+
+function addOrUpdateLogin(url, type, username, password)
+{
+ username = username.toLowerCase();
+ var newinfo = newObject("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo");
+ newinfo.init(url, null, type, username, password, "", "");
+ var oldinfo = getLogin(url, type, username);
+
+ if (oldinfo) {
+ Services.logins.modifyLogin(oldinfo, newinfo);
+ return true; //updated
+ }
+
+ Services.logins.addLogin(newinfo);
+ return false; //added
+}
+
+function getLogin(url, realm, username)
+{
+ username = username.toLowerCase();
+
+ let logins = Services.logins.findLogins({}, url, null, realm);
+ for (let login of logins) {
+ if (login.username == username) {
+ return login;
+ }
+ }
+
+ return null;
+}
+
+function getHostmaskParts(hostmask)
+{
+ var rv;
+ // A bit cheeky this, we try the matches here, and then branch
+ // according to the ones we like.
+ var ary1 = hostmask.match(/([^ ]*)!([^ ]*)@(.*)/);
+ var ary2 = hostmask.match(/([^ ]*)@(.*)/);
+ var ary3 = hostmask.match(/([^ ]*)!(.*)/);
+ if (ary1)
+ rv = { nick: ary1[1], user: ary1[2], host: ary1[3] };
+ else if (ary2)
+ rv = { nick: "*", user: ary2[1], host: ary2[2] };
+ else if (ary3)
+ rv = { nick: ary3[1], user: ary3[2], host: "*" };
+ else
+ rv = { nick: hostmask, user: "*", host: "*" };
+ // Make sure we got something for all fields.
+ if (!rv.nick)
+ rv.nick = "*";
+ if (!rv.user)
+ rv.user = "*";
+ if (!rv.host)
+ rv.host = "*";
+ // And re-construct the 'parsed' hostmask.
+ rv.mask = rv.nick + "!" + rv.user + "@" + rv.host;
+ return rv;
+}
+
+function makeMaskRegExp(text)
+{
+ function escapeChars(c)
+ {
+ if (c == "*")
+ return ".*";
+ if (c == "?")
+ return ".";
+ return "\\" + c;
+ }
+ // Anything that's not alpha-numeric gets escaped.
+ // "*" and "?" are 'escaped' to ".*" and ".".
+ // Optimisation; * translates as 'match all'.
+ return new RegExp("^" + text.replace(/[^\w\d]/g, escapeChars) + "$", "i");
+}
+
+function hostmaskMatches(user, mask)
+{
+ // Need to match .nick, .user, and .host.
+ if (!("nickRE" in mask))
+ {
+ // We cache all the regexp objects, but use null if the term is
+ // just "*", so we can skip having the object *and* the .match
+ // later on.
+ if (mask.nick == "*")
+ mask.nickRE = null;
+ else
+ mask.nickRE = makeMaskRegExp(mask.nick);
+
+ if (mask.user == "*")
+ mask.userRE = null;
+ else
+ mask.userRE = makeMaskRegExp(mask.user);
+
+ if (mask.host == "*")
+ mask.hostRE = null;
+ else
+ mask.hostRE = makeMaskRegExp(mask.host);
+ }
+
+ var lowerNick;
+ if (user.TYPE == "IRCChanUser")
+ lowerNick = user.parent.parent.toLowerCase(user.unicodeName);
+ else
+ lowerNick = user.parent.toLowerCase(user.unicodeName);
+
+ if ((!mask.nickRE || lowerNick.match(mask.nickRE)) &&
+ (!mask.userRE || user.name.match(mask.userRE)) &&
+ (!mask.hostRE || user.host.match(mask.hostRE)))
+ return true;
+ return false;
+}
+
+function isinstance(inst, base)
+{
+ /* Returns |true| if |inst| was constructed by |base|. Not 100% accurate,
+ * but plenty good enough for us. This is to work around the fix for bug
+ * 254067 which makes instanceof fail if the two sides are 'from'
+ * different windows (something we don't care about).
+ */
+ return (inst && base &&
+ ((inst instanceof base) ||
+ (inst.constructor && (inst.constructor.name == base.name))));
+}
+
+function isDefaultPrevented(ev)
+{
+ if ("defaultPrevented" in ev)
+ return ev.defaultPrevented;
+ return ev.getPreventDefault();
+}
+
+function scaleNumberBy1024(number)
+{
+ var scale = 0;
+ while ((number >= 1000) && (scale < 6))
+ {
+ scale++;
+ number /= 1024;
+ }
+
+ return [scale, number];
+}
+
+function getSISize(size)
+{
+ var data = scaleNumberBy1024(size);
+
+ if (data[1] < 10)
+ data[1] = data[1].toFixed(2);
+ else if (data[1] < 100)
+ data[1] = data[1].toFixed(1);
+ else
+ data[1] = data[1].toFixed(0);
+
+ return getMsg(MSG_SI_SIZE, [data[1], getMsg("msg.si.size." + data[0])]);
+}
+
+function getSISpeed(speed)
+{
+ var data = scaleNumberBy1024(speed);
+
+ if (data[1] < 10)
+ data[1] = data[1].toFixed(2);
+ else if (data[1] < 100)
+ data[1] = data[1].toFixed(1);
+ else
+ data[1] = data[1].toFixed(0);
+
+ return getMsg(MSG_SI_SPEED, [data[1], getMsg("msg.si.speed." + data[0])]);
+}
+
+// Returns -1 if version 1 is newer, +1 if version 2 is newer, and 0 for same.
+function compareVersions(ver1, ver2)
+{
+ var ver1parts = ver1.split(".");
+ var ver2parts = ver2.split(".");
+
+ while ((ver1parts.length > 0) && (ver2parts.length > 0))
+ {
+ if (ver1parts[0] < ver2parts[0])
+ return 1;
+ if (ver1parts[0] > ver2parts[0])
+ return -1;
+ ver1parts.shift();
+ ver2parts.shift();
+ }
+ if (ver1parts.length > 0)
+ return -1;
+ if (ver2parts.length > 0)
+ return 1;
+ return 0;
+}
+
+// Zero-pad Numbers (or pad with something else if you wish)
+function padNumber(num, digits, pad)
+{
+ pad = pad || "0";
+ var rv = num.toString();
+ while (rv.length < digits)
+ rv = pad + rv;
+ return rv;
+}
+
+const timestr = {
+ A: { method: "getDay" },
+ a: { method: "getDay" },
+ B: { method: "getMonth" },
+ b: { method: "getMonth" },
+ c: { replace: null },
+ D: { replace: "%m/%d/%y" },
+ d: { method: "getDate", pad: 2 },
+ e: { method: "getDate", pad: 2, padwith: " " },
+ F: { replace: "%Y-%m-%d" },
+ h: { replace: "%b" },
+ H: { method: "getHours", pad: 2 },
+ k: { method: "getHours", pad: 2, padwith: " " },
+ M: { method: "getMinutes", pad: 2 },
+ p: { AM: null, PM: null },
+ P: { AM: null, PM: null },
+ r: { replace: null },
+ R: { replace: "%H:%M" },
+ S: { method: "getSeconds", pad: 2 },
+ T: { replace: "%H:%M:%S" },
+ w: { method: "getDay" },
+ x: { replace: null },
+ X: { replace: null },
+ Y: { method: "getFullYear" },
+ initialized: false
+}
+
+function strftime(format, time)
+{
+ /* Javascript implementation of standard C strftime */
+
+ if (!timestr.initialized)
+ {
+ timestr.A.values = getMsg("datetime.day.long").split("^");
+ timestr.a.values = getMsg("datetime.day.short").split("^");
+ timestr.B.values = getMsg("datetime.month.long").split("^");
+ timestr.b.values = getMsg("datetime.month.short").split("^");
+ // Just make sure the locale isn't playing silly with us.
+ ASSERT(timestr.A.values.length == 7, "datetime.day.long bad!");
+ ASSERT(timestr.a.values.length == 7, "datetime.day.short bad!");
+ ASSERT(timestr.B.values.length == 12, "datetime.month.long bad!");
+ ASSERT(timestr.b.values.length == 12, "datetime.month.short bad!");
+
+ timestr.p.AM = getMsg("datetime.uam");
+ timestr.p.PM = getMsg("datetime.upm");
+ timestr.P.AM = getMsg("datetime.lam");
+ timestr.P.PM = getMsg("datetime.lpm");
+
+ timestr.c.replace = getMsg("datetime.presets.lc");
+ timestr.r.replace = getMsg("datetime.presets.lr");
+ timestr.x.replace = getMsg("datetime.presets.lx");
+ timestr.X.replace = getMsg("datetime.presets.ux");
+
+ timestr.initialized = true;
+ }
+
+
+ function getDayOfYear(dateobj)
+ {
+ var yearobj = new Date(dateobj.getFullYear(), 0, 1, 0, 0, 0, 0);
+ return Math.floor((dateobj - yearobj) / 86400000) + 1;
+ };
+
+ time = time || new Date();
+ if (!isinstance(time, Date))
+ throw "Expected date object";
+
+ var ary;
+ while ((ary = format.match(/(^|[^%])%(\w)/)))
+ {
+ var start = ary[1] ? (ary.index + 1) : ary.index;
+ var rpl = "";
+ if (ary[2] in timestr)
+ {
+ var tbranch = timestr[ary[2]];
+ if (("method" in tbranch) && ("values" in tbranch))
+ rpl = tbranch.values[time[tbranch.method]()];
+ else if ("method" in tbranch)
+ rpl = time[tbranch.method]().toString();
+ else if ("replace" in tbranch)
+ rpl = tbranch.replace;
+
+ if ("pad" in tbranch)
+ {
+ var padwith = ("padwith" in tbranch) ? tbranch.padwith : "0";
+ rpl = padNumber(rpl, tbranch.pad, padwith);
+ }
+ }
+ if (!rpl)
+ {
+ switch (ary[2])
+ {
+ case "C":
+ var century = Math.floor(time.getFullYear() / 100);
+ rpl = padNumber(century, 2);
+ break;
+ case "I":
+ case "l":
+ var hour = (time.getHours() + 11) % 12 + 1;
+ var padwith = (ary[2] == "I") ? "0" : " ";
+ rpl = padNumber(hour, 2, padwith);
+ break;
+ case "j":
+ rpl = padNumber(getDayOfYear(time), 3);
+ break;
+ case "m":
+ rpl = padNumber(time.getMonth() + 1, 2);
+ break;
+ case "p":
+ case "P":
+ var bit = (time.getHours() < 12) ? "AM" : "PM";
+ rpl = timestr[ary[2]][bit];
+ break;
+ case "s":
+ rpl = Math.round(time.getTime() / 1000);
+ break;
+ case "u":
+ rpl = (time.getDay() + 6) % 7 + 1;
+ break;
+ case "y":
+ rpl = time.getFullYear().toString().substr(2);
+ break;
+ case "z":
+ var mins = time.getTimezoneOffset();
+ rpl = (mins > 0) ? "-" : "+";
+ mins = Math.abs(mins);
+ var hours = Math.floor(mins / 60);
+ rpl += padNumber(hours, 2) + padNumber(mins - (hours * 60), 2);
+ break;
+ }
+ }
+ if (!rpl)
+ rpl = "%%" + ary[2];
+ format = format.substr(0, start) + rpl + format.substr(start + 2);
+ }
+ return format.replace(/%%/, "%");
+}
+
+// This used to be strres.js, copied here to help remove that...
+var strBundleService = null;
+function srGetStrBundle(path)
+{
+ const STRBSCID = "@mozilla.org/intl/stringbundle;1";
+ const STRBSIF = "nsIStringBundleService";
+ var strBundle = null;
+ if (!strBundleService)
+ {
+ try
+ {
+ strBundleService = getService(STRBSCID, STRBSIF);
+ }
+ catch (ex)
+ {
+ dump("\n--** strBundleService failed: " + ex + "\n");
+ return null;
+ }
+ }
+
+ strBundle = strBundleService.createBundle(path);
+ if (!strBundle)
+ dump("\n--** strBundle createInstance failed **--\n");
+
+ return strBundle;
+}
+
+// No-op window.getAttention if it's not found, this is for in-a-tab mode.
+if (typeof getAttention == "undefined")
+ getAttention = function() {};
diff --git a/comm/suite/chatzilla/js/tests/DP.js b/comm/suite/chatzilla/js/tests/DP.js
new file mode 100644
index 0000000000..5320b38c35
--- /dev/null
+++ b/comm/suite/chatzilla/js/tests/DP.js
@@ -0,0 +1,612 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+/*
+ * Dissociated Press javascript for the jsbot
+ * see: http://wombat.doc.ic.ac.uk/foldoc/foldoc.cgi?query=dissociated%20press
+ */
+
+DP_DEBUG = false;
+
+if (DP_DEBUG)
+ dpprint = dd;
+else
+ dpprint = (function () {});
+
+function CDPressMachine()
+{
+
+ this.wordPivots = new Object();
+ this.cleanCounter = 0;
+ this.cleanCount = 0;
+
+}
+
+CDPressMachine.CLEAN_CYCLE = 1000; // list will be trimmed after this many
+ // or never if < 1 addPhrase()s
+CDPressMachine.CLEAN_THRESHOLD = 2; // anything <= this will be trimmed
+CDPressMachine.RANDOMIZE_DEPTH = 10; // not used yet
+CDPressMachine.MIN_PHRASE_LENGTH = 3; // requested minimum phrase length
+CDPressMachine.MAX_PHRASE_LENGTH = 8; // requested maximum phrase length
+CDPressMachine.LENGTH_RETRIES = 3 // number of retries per word
+ // (to reach maxlen)
+CDPressMachine.WORD_PATTERN = /[\x21-\x7e]+/; // pattern for words
+
+/**
+ * Adds a phrase to the engine
+ */
+CDPressMachine.prototype.addPhrase =
+function DPM_addPhrase (strPhrase, weight)
+{
+ if (strPhrase == "")
+ return;
+
+ this.cleanCounter++;
+ if ((CDPressMachine.CLEAN_CYCLE >= 1) &&
+ (this.cleanCounter >= CDPressMachine.CLEAN_CYCLE))
+ {
+ dpprint ("** cleaning list");
+
+ this.cleanCounter = 0;
+ this.trimList (CDPressMachine.CLEAN_THRESHOLD);
+ this.cleancount++;
+ }
+
+ strPhrase = strPhrase.toLowerCase();
+
+ /* split the phrase */
+ var aryWordMatches = strPhrase.split (" ");
+ var previousWord = aryWordMatches[aryWordMatches.length - 1];
+ previousWord = previousWord.match(CDPressMachine.WORD_PATTERN);
+ var nextWord = "";
+
+ /* loop through each word */
+ for (var i=-1; i < aryWordMatches.length; i++)
+ {
+ var currentWord = nextWord;
+ var currentWordPivot = this.wordPivots[currentWord];
+
+ if (typeof currentWordPivot == "undefined")
+ currentWordPivot =
+ (this.wordPivots[currentWord] = new CWordPivot (currentWord));
+
+ currentWordPivot.previousList.addLink (previousWord, weight);
+
+ if (i < aryWordMatches.length - 1)
+ {
+ nextWord = aryWordMatches[i + 1];
+ if (nextWord == (String.fromCharCode(1) + "action"))
+ nextWord = escape(nextWord.toUpperCase());
+ else
+ nextWord = nextWord.match(CDPressMachine.WORD_PATTERN);
+
+ if (nextWord == null)
+ nextWord = ""; //this is weak
+
+ currentWordPivot.nextList.addLink (nextWord, weight);
+ }
+ else
+ currentWordPivot.nextList.addLink ("");
+
+ previousWord = currentWord;
+
+ }
+
+}
+
+CDPressMachine.prototype.addPhrases =
+function DPM_addPhrases(phrases)
+{
+
+ for (var i in phrases)
+ this.addPhrase (phrases[i]);
+
+}
+
+/**
+ * Gets a phrase from the engine, starting from seedWord.
+ * if dir is greater than 0, then seedWord will be the first in
+ * the phrase, otherwise it will be the last
+ */
+CDPressMachine.prototype.getPhraseDirected =
+function DPM_getPhraseDirected(seedWord, dir)
+{
+ var word = (typeof seedWord != "undefined") ? seedWord : "";
+ var tempword = word;
+ var rval = "";
+ var c = 0, retry = 0;
+
+ dpprint ("DPM_getPhraseDirected: '" + word + "' " + dir);
+
+ if (typeof this.wordPivots[word] == "undefined")
+ return;
+
+ do
+ {
+ if (typeof this.wordPivots[word] == "undefined")
+ {
+ dd ("** DP Error: Word '" + word + "' is not a pivot **");
+ return;
+ }
+
+ if (dir > 0) // pick a word
+ word= this.wordPivots[word].nextList.getRandomLink().link;
+ else
+ word= this.wordPivots[word].previousList.getRandomLink().link;
+
+ if (word != "") // if it isn't blank
+ {
+ dpprint ("DPM_getPhraseDirected: got word '" + word + "'");
+
+ if (c < CDPressMachine.MIN_PHRASE_LENGTH)
+ retry = 0;
+
+ if (c > CDPressMachine.MAX_PHRASE_LENGTH)
+ if (((dir > 0) && (this.wordPivots[word].nextList.list[""])) ||
+ ((dir <= 0) &&
+ (this.wordPivots[word].previousList.list[""])))
+ {
+ dpprint ("DPM_getPhraseDirected: forcing last word");
+ word="";
+ rval = rval.substring (0, rval.length - 1);
+ break;
+ }
+
+ if (dir > 0)
+ rval += word + " "; // put it in the rslt
+ else
+ rval = word + " " + rval;
+
+ c++; // count the word
+ }
+ else // otherwise
+ {
+ dpprint ("DPM_getPhraseDirected: last word");
+ // if it's too short
+ // and were not out of retrys
+ if ((c < CDPressMachine.MIN_PHRASE_LENGTH) &&
+ (retry++ < CDPressMachine.LENGTH_RETRIES))
+ word = tempword; // try again
+ else
+ // otherwise, we're done
+ rval = rval.substring (0, rval.length - 1);
+ }
+
+ tempword = word;
+
+ } while (word != "");
+
+ rval = unescape (rval);
+
+ return rval;
+
+}
+
+CDPressMachine.prototype.getPhraseForward =
+function DPM_getPhraseForward(firstWord)
+{
+ return this.getPhraseDirected (firstWord, 1)
+}
+
+CDPressMachine.prototype.getPhraseReverse =
+function DPM_getPhraseReverse(lastWord)
+{
+ return this.getPhraseDirected (lastWord, -1)
+}
+
+/**
+ * locates a random pivot by following CDPressMachine.RANDOMIZE_DEPTH
+ * links from |word|.
+ */
+CDPressMachine.prototype.getRandomPivot =
+function DPM_getRandomPivot (word)
+{
+
+ /**
+ * XXXrgg: erm, this is currently pointless, but could be neat later
+ * if max phrase length's were implemented.
+ */
+ if (false)
+ {
+ var depth = parseInt (Math.round
+ (CDPressMachine.RANDOMIZE_DEPTH * Math.random()));
+ word = "";
+ for (var i = 0;
+ i < depth, word =
+ this.wordPivots[word].nextList.getRandomLink().link;
+ i++); /* empty loop */
+
+ }
+
+}
+
+CDPressMachine.prototype.getPhrase =
+function DPM_getPhrase(word)
+{
+ var rval = this.getPhraseContaining (word);
+
+ return rval;
+
+}
+
+/**
+ * Returns a phrase with |word| somewhere in it.
+ */
+CDPressMachine.prototype.getPhraseContaining =
+function DPM_getPhraseContaining(word)
+{
+ if (typeof word == "undefined")
+ word = "";
+ else
+ word = word.toString();
+
+ dpprint ("* DPM_getPhraseContaining: '" + word + "'");
+
+ var rval, spc;
+ var post, pre = this.getPhraseReverse (word);
+ if (word != "")
+ var post = this.getPhraseForward (word);
+
+ dpprint ("* DPM_getPhraseContaining: pre = '" + pre + "' post = '" +
+ post + "'");
+ dpprint ("* DPM_getPhraseContaining: " + (post == "" && pre == ""));
+
+ if (word)
+ {
+ word = unescape (word);
+ spc = " ";
+ }
+ else
+ spc = "";
+
+ if (pre)
+ {
+ if (post)
+ rval = pre + spc + word + spc + post;
+ else
+ rval = pre + spc + word;
+ }
+ else
+ {
+ if (post)
+ rval = word + spc + post;
+ else
+ if (post == "" && pre == "")
+ rval = word;
+ }
+
+ if (rval && (rval.charCodeAt(0) == 1))
+ rval += String.fromCharCode(1);
+
+ dpprint ("* DPM_getPhraseContaining: returning '" + rval + "'");
+
+ return rval;
+
+}
+
+CDPressMachine.prototype.getPhraseWeight =
+function DPM_getPhraseWeight (phrase)
+{
+ var ary = this.getPhraseWeights (phrase);
+ var w = 0;
+
+ while (ary.length > 0)
+ w += ary.pop();
+
+ return w;
+}
+
+CDPressMachine.prototype.getPhraseWeights =
+function DPM_getPhraseWeights (phrase)
+{
+ var words, ary = new Array();
+ var lastword = "";
+ var link, pivot;
+
+ if (!phrase)
+ return ary;
+
+ words = phrase.split (" ");
+
+ for (var i = 0; i < words.length; i++)
+ {
+
+ if (i == 0)
+ {
+ lastWord = "";
+ nextWord = words[i + 1];
+ }
+ else if (i == words.length - 1)
+ {
+ lastWord = words[i - 1];
+ nextWord = "";
+ }
+ else
+ {
+ lastWord = words[i - 1];
+ nextWord = words[i + 1];
+ }
+
+ pivot = this.wordPivots[words[i]];
+
+ if (pivot)
+ {
+ link = pivot.previousList.list[lastWord];
+
+ if (link)
+ ary.push(link.weight);
+ else
+ ary.push(0);
+
+ link = pivot.nextList.list[nextWord];
+
+ if (link)
+ ary.push(link.weight);
+ else
+ ary.push(0);
+ }
+ else
+ {
+ ary.push(0);
+ ary.push(0);
+ }
+
+ }
+
+ return ary;
+
+}
+
+CDPressMachine.prototype.getPivot =
+function DPM_getPivot(word)
+{
+
+ return this.wordPivots[word];
+
+}
+
+CDPressMachine.prototype.trimList =
+function DPM_trimList(threshold)
+{
+ var el;
+ var c;
+
+ for (el in this.wordPivots)
+ {
+ c = this.wordPivots[el].nextList.trimList (threshold);
+ if (c == 0)
+ delete this.wordPivots[el];
+ else
+ {
+ c = this.wordPivots[el].previousList.trimList (threshold);
+ if (c == 0)
+ delete this.wordPivots[el];
+ }
+
+ }
+
+}
+
+CDPressMachine.prototype.getMachineStatus =
+function DPM_getMachineStatus()
+{
+ var o = new Object();
+
+ o.pivotcount = 0;
+ o.linkcount = 0;
+ o.linksperpivot = 0;
+ o.maxweight = 0;
+ o.minweight = Number.MAX_VALUE;
+ o.averageweight = 0;
+ o.cleancounter = this.cleanCounter;
+ o.cleancount = this.cleanCount;
+
+ for (var pivot in this.wordPivots)
+ {
+ o.pivotcount++;
+
+ for (var link in this.wordPivots[pivot].previousList.list)
+ {
+ var l = this.wordPivots[pivot].previousList.list[link];
+
+ o.linkcount++;
+
+ o.maxweight = Math.max (o.maxweight, l.weight);
+ o.minweight = Math.min (o.minweight, l.weight);
+
+ (o.averageweight == 0) ?
+ o.averageweight = l.weight :
+ o.averageweight = (l.weight + o.averageweight) / 2;
+
+ }
+ }
+
+ o.linksperpivot = o.linkcount / o.pivotcount;
+
+ return o;
+
+}
+
+////////////////////////
+function CWordPivot (word)
+{
+
+ dpprint ("* new pivot : '" + word + "'");
+ this.word = word;
+ this.nextList = new CPhraseLinkList(word, "next");
+ this.previousList = new CPhraseLinkList(word, "previous");
+
+}
+
+///////////////////////
+
+function CPhraseLinkList (parentWord, listID)
+{
+
+ if (DP_DEBUG)
+ {
+ this.parentWord = parentWord;
+ this.listID = listID;
+ }
+
+ this.list = new Object();
+
+}
+
+CPhraseLinkList.prototype.addLink =
+function PLL_addLink (link, weight)
+{
+ var existingLink = this.list[link];
+
+ dpprint ("* adding link to '" + link + "' from '" + this.parentWord +
+ "' in list '" + this.listID + "'");
+
+ if (typeof weight == "undefined")
+ weight = 1;
+
+ if (typeof existingLink == "undefined")
+ this.list[link] = new CPhraseLink (link, weight);
+ else
+ if (!(typeof existingLink.adjust == "function"))
+ dd("existingLink.adjust is a '" + existingLink.adjust + "' " +
+ "not a function! link is '" + link +"'");
+ else
+ existingLink.adjust (weight);
+
+}
+
+CPhraseLinkList.prototype.getRandomLink =
+function PLL_getRandomLink ()
+{
+ var tot = 0;
+ var lastMatch = "";
+ var aryChoices = new Array();
+ var fDone = false;
+
+ dpprint ("* PLL_getRandomLink: from '" + this.parentWord + "'");
+
+ for (el in this.list)
+ {
+ tot += this.list[el].weight;
+
+ for (var i = 0; i< aryChoices.length; i++)
+ if (this.list[el].weight <= aryChoices[i].weight)
+ break;
+
+ arrayInsertAt (aryChoices, i, this.list[el]);
+ }
+
+ if (DP_DEBUG)
+ for (var i = 0; i < aryChoices.length; i++)
+ dpprint ("** potential word '" + aryChoices[i].link + "', weight " +
+ aryChoices[i].weight);
+
+ var choice = parseInt (Math.round(((tot - 1) * Math.random()) + 1));
+
+ dpprint ("* PLL_getRandomLink: tot = " + tot + ", choice = " + choice);
+
+ tot = 0;
+ for (i = 0; i < aryChoices.length; i++)
+ {
+ if ((tot += aryChoices[i].weight) >= choice)
+ {
+ lastMatch = aryChoices[i];
+ break;
+ }
+ }
+
+ if (lastMatch == "")
+ lastMatch = aryChoices[aryChoices.length - 1];
+
+ if (!lastMatch)
+ lastMatch = {link: ""}
+
+ dpprint ("* PLL_getRandomLink: returning: " + lastMatch);
+
+ return lastMatch;
+
+}
+
+CPhraseLinkList.prototype.getListWeights =
+function PLL_getListWeights ()
+{
+ var ary = new Array();
+
+ for (var el in this.list)
+ ary.push (this.list[el].weight);
+
+ return ary;
+
+}
+
+CPhraseLinkList.prototype.getListLinks =
+function PLL_getListLinks ()
+{
+ var ary = new Array();
+
+ for (var el in this.list)
+ ary.push (this.list[el].link);
+
+ return ary;
+
+}
+
+CPhraseLinkList.prototype.trimList =
+function PLL_trimList (threshold)
+{
+ var el;
+ var c;
+
+ dpprint ("trimming '" + this.parentWord + "'s list to " + threshold);
+
+ for (el in this.list)
+ {
+ c++;
+
+ if (this.list[el].weight <= threshold)
+ {
+ dpprint ("removing '" + el + "' from '" + this.parentWord + "'s '" +
+ this.listID + "' list, because it's weight is " +
+ this.list[el].weight);
+
+ delete this.list[el];
+ c--;
+ }
+ }
+
+ return c;
+
+}
+
+////////////////////////
+
+function CPhraseLink (link, weight)
+{
+ if (typeof weight == "undefined")
+ this.weight = 1;
+ else
+ this.weight = weight;
+
+ this.link = link;
+
+}
+
+CPhraseLink.prototype.adjust =
+function PL_adjust(weight)
+{
+
+ if ((this.weight += weight) < 1)
+ this.weight = 1;
+
+}
+
+CPhraseLink.prototype.weight =
+function PL_weight ()
+{
+
+ return this.weight;
+
+}
diff --git a/comm/suite/chatzilla/js/tests/ircbot.js b/comm/suite/chatzilla/js/tests/ircbot.js
new file mode 100644
index 0000000000..e3683d034c
--- /dev/null
+++ b/comm/suite/chatzilla/js/tests/ircbot.js
@@ -0,0 +1,407 @@
+/* -*- Mode: C++; tab-width: 8; 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 LIB_PATH = "../lib/";
+
+bot = new Object();
+bot.ownerPatterns = new Array();
+bot.personality = new Object();
+bot.personality.hooks = new Array();
+bot.prefix = "!js ";
+
+function loadDeps()
+{
+ load(LIB_PATH + "utils.js");
+ load(LIB_PATH + "events.js");
+ load(LIB_PATH + "connection.js");
+ load(LIB_PATH + "http.js");
+ load(LIB_PATH + "dcc.js");
+ load(LIB_PATH + "irc.js");
+ load(LIB_PATH + "irc-debug.js");
+ load(LIB_PATH + "message-manager.js");
+
+ bot.messageManager = new MessageManager();
+
+ load(LIB_PATH + "connection-xpcom.js");
+
+ return true;
+}
+
+// FIXME: Find somewhere better for these guys. //
+function toUnicode(msg, charsetOrView)
+{
+ if (!msg)
+ return msg;
+
+ var charset;
+ if (typeof charsetOrView == "string")
+ charset = charsetOrView;
+ else
+ return msg;
+
+ return bot.messageManager.toUnicode(msg, charset);
+}
+
+function fromUnicode(msg, charsetOrView)
+{
+ if (!msg)
+ return msg;
+
+ var charset;
+ if (typeof charsetOrView == "string")
+ charset = charsetOrView;
+ else
+ return msg;
+
+ return bot.messageManager.fromUnicode(msg, charset);
+}
+// FIXME: END //
+
+
+function initStatic()
+{
+ CIRCNetwork.prototype.INITIAL_NICK = "jsbot";
+ CIRCNetwork.prototype.INITIAL_NAME = "XPJSBot";
+ CIRCNetwork.prototype.INITIAL_DESC = "XPCOM Javascript bot";
+ CIRCNetwork.prototype.INITIAL_CHANNEL = "#jsbot";
+
+ CIRCNetwork.prototype.stayingPower = true;
+ CIRCNetwork.prototype.on433 = my_433;
+ CIRCChannel.prototype.onPrivmsg = my_chan_privmsg;
+ CIRCUser.prototype.onDCCChat = my_user_dccchat;
+ CIRCDCCChat.prototype.onRawData = my_dccchat_rawdata;
+}
+
+/*
+ * One time initilization stuff
+ */
+function init(obj)
+{
+ obj.eventPump = new CEventPump(100);
+
+ obj.networks = new Object();
+ obj.networks["libera.chat"] =
+ new CIRCNetwork("libera.chat", [{name: "libera.chat", port: 6667}],
+ obj.eventPump);
+
+ obj.networks["efnet"] =
+ new CIRCNetwork ("efnet", [{name: "irc.mcs.net", port: 6667},
+ {name: "irc.cs.cmu.edu", port: 6667}],
+ obj.eventPump);
+
+ obj.primNet = obj.networks["efnet"];
+}
+
+/*
+ * Kick off the mainloop for the first time
+ */
+function go()
+{
+ if (!loadDeps())
+ return false;
+
+ // The utils.js formatException relies on localization, we can't. Fix:
+ formatException = function formatException(ex)
+ {
+ if (isinstance(ex, Error) ||
+ ((typeof ex == "object") && ("filename" in ex)))
+ {
+ return [ex.name, ex.message, ex.fileName, ex.lineNumber].join(", ");
+ }
+
+ return String(ex);
+ };
+
+ initStatic();
+ init(bot);
+ if (DEBUG)
+ {
+ /* hook all events EXCEPT server.poll and *.event-end types
+ * (the 4th param inverts the match) */
+ bot.eventPump.addHook([{type: "poll", set: /^(server|dcc-chat)$/},
+ {type: "event-end"}], event_tracer,
+ "event-tracer", true /* negate */);
+ }
+
+ if (typeof initPersonality == "function")
+ initPersonality();
+
+ bot.primNet.connect();
+ rego();
+
+ return true;
+}
+
+/*
+ * If you didn't compile libjs with JS_HAS_ERROR_EXCEPTIONS, any error the
+ * bot encounters will exit the mainloop and drop you back to a shell ("js>")
+ * prompt. You can continue the mainloop by executing this function.
+ */
+function rego()
+{
+ /* mainloop */
+ while (bot.eventPump.queue.length > 0)
+ {
+ bot.eventPump.stepEvents();
+ if (typeof gc == "function")
+ {
+ if ((typeof bot.lastGc == "undefined") ||
+ (Number(new Date()) - bot.lastGc > 60000))
+ {
+ gc();
+ bot.lastGc = Number(new Date());
+ }
+ }
+ }
+ dd("No events to process.");
+
+ return true;
+}
+
+function addOwner(pattern)
+{
+ bot.ownerPatterns.push(pattern);
+}
+
+function userIsOwner(user)
+{
+ if (!user.host)
+ {
+ /* we havn't got any information on this user yet. They havn't spoken
+ * yet, and we havn't /whoi's them yet. Say no for now, but do the
+ * /whois so we'll know for sure next time.
+ */
+ if (user.TYPE == "IRCChanUser")
+ user.parent.parent.sendData("WHOIS " + user.unicodeName + "\n");
+ else
+ user.parent.sendData("WHOIS " + user.unicodeName + "\n");
+ return false;
+ }
+
+ var userString = user.unicodeName + "!" + user.name + "@" + user.host;
+ dd("userIsOwner: checking userString `" + userString + "' against:");
+
+ for (var p in bot.ownerPatterns)
+ {
+ if (userString.search(bot.ownerPatterns[p]) != -1)
+ {
+ dd(String(bot.ownerPatterns[p]) + " passed.");
+ return true;
+ }
+ else
+ {
+ dd(String(bot.ownerPatterns[p]) + " fails.");
+ }
+ }
+
+ return false;
+}
+
+function psn_isAddressedToMe(e)
+{
+ if (!e.server)
+ return false;
+
+ if ((e.type.search(/privmsg|ctcp-action/)) || (e.set != "channel"))
+ return false;
+
+ var msg = e.decodeParam(2);
+
+ if (msg.indexOf(bot.prefix) == 0)
+ return false;
+
+ /*
+ dd ("-*- checking to see if message '" + msg + "' is addressed to me.");
+ */
+
+ var regex = new RegExp("^\\s*" + e.server.me.unicodeName + "\\W+(.*)", "i");
+ var ary = msg.match(regex);
+
+ //dd ("address match: " + ary);
+
+ if (ary != null)
+ {
+ e.statement = ary[1];
+ return true;
+ }
+
+ //XXXgijs: Shouldn't this be in mingus.js?
+ bot.personality.dp.addPhrase(msg);
+ return false;
+}
+
+function psn_onAddressedMsg(e)
+{
+
+ bot.eventPump.onHook(e, bot.personality.hooks);
+ return false;
+}
+
+bot.personality.addHook =
+function psn_addhook(pattern, f, name, neg, enabled)
+{
+ if (pattern instanceof RegExp)
+ pattern = {statement: pattern};
+
+ return bot.eventPump.addHook(pattern, f, name, neg, enabled,
+ bot.personality.hooks);
+}
+
+function bot_eval(e, script)
+{
+ try
+ {
+ var v = eval(script);
+ }
+ catch (ex)
+ {
+ e.replyTo.say(e.user.unicodeName + ": " + String(ex));
+ return false;
+ }
+
+ if (typeof v != "undefined")
+ {
+ if (v != null)
+ v = String(v);
+ else
+ v = "null";
+
+ var rsp = e.user.unicodeName + ", your result is,";
+
+ if (v.indexOf("\n") != -1)
+ rsp += "\n";
+ else
+ rsp += " ";
+
+ e.replyTo.say(rsp + v);
+ }
+}
+
+/*
+ * The following my_* are attached to their proper objects in the init()
+ * function. This is because the CIRC* objects are not defined at load time
+ * (they get defined when loadDeps() loads the irc library) and so connecting
+ * them here would cause an error.
+ */
+
+/*
+ * What to do when a privmsg is received on a channel
+ */
+function my_chan_privmsg(e)
+{
+ var user = e.user;
+ var msg = e.decodeParam(2);
+ if ((msg.indexOf(bot.prefix) == 0) && userIsOwner(user))
+ {
+ /* if last char is a continuation character, then... */
+ if (msg[msg.length - 1] == "\\")
+ {
+ user.accumulatedScript = msg.substring(bot.prefix.length,
+ msg.length - 1);
+ return false; // prevent other hooks from processing this...
+ }
+ else
+ {
+ return bot_eval(e, msg.substring(bot.prefix.length,
+ msg.length));
+ }
+ }
+ else if ((typeof(user.accumulatedScript) != "undefined") &&
+ userIsOwner(user))
+ /* if we were accumulating a message, add here,
+ * and finish if not ends with '\'. */
+ {
+ var lastLine = (msg[msg.length - 1] != "\\");
+ var line = msg.substring(0, msg.length - (lastLine ? 0 : 1));
+ user.accumulatedScript += line;
+ if (lastLine)
+ {
+ var script = user.accumulatedScript;
+ delete user.accumulatedScript;
+ return bot_eval(e, script);
+ }
+ }
+}
+
+/*
+ * What to do when a dcc chat request reaches a user object
+ */
+function my_user_dccchat(e)
+{
+ if (!e.user.canDCC)
+ {
+ e.user.notice("\01DCC REJECT CHAT chat\01");
+ return false;
+ }
+
+ var c = new CIRCDCCChat(bot.eventPump);
+
+ if (!c.connect(e.user.host, e.port))
+ {
+ e.user.notice("\01DCC REJECT CHAT chat\01");
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * What to do when our requested nickname is in use
+ */
+function my_433(e)
+{
+ if (e.params[2] != CIRCNetwork.prototype.INITIAL_NICK)
+ {
+ /* server didn't like the last nick we tried, probably too long.
+ * not much more we can do, bail out. */
+ e.server.disconnect();
+ }
+
+ CIRCNetwork.prototype.INITIAL_NICK += "_";
+ e.server.sendData("nick " + CIRCNetwork.prototype.INITIAL_NICK + "\n");
+}
+
+/*
+ * What to do when raw data is received on a dcc chat connection
+ */
+function my_dccchat_rawdata(e)
+{
+ try
+ {
+ var v = eval(e.data);
+ }
+ catch (ex)
+ {
+ this.say(String(ex));
+ return false;
+ }
+
+ if (typeof v != "undefined")
+ {
+ if (v != null)
+ v = String(v);
+ else
+ v = "null";
+
+ this.say(v);
+ }
+}
+
+/*
+ * Wrapper around CHTTPDoc to make is simpler to use
+ */
+function loadHTTP(host, path, onComplete)
+{
+ var htdoc = new CHTTPDoc(host, path);
+
+ htdoc.onComplete = onComplete;
+ htdoc.get(bot.eventPump);
+
+ return htdoc;
+}
+
+
+
diff --git a/comm/suite/chatzilla/js/tests/mingus.js b/comm/suite/chatzilla/js/tests/mingus.js
new file mode 100644
index 0000000000..2590ed919e
--- /dev/null
+++ b/comm/suite/chatzilla/js/tests/mingus.js
@@ -0,0 +1,362 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+bot.personality.guessPrefixes = ["I guess ", "maybe ", "probably ", "I think ",
+ "could be that ", "", ""];
+bot.personality.guessActionPrefixes = ["guesses ", "postulates ", "figures ",
+ "tries ","pretends ", "", ""];
+
+function initMingus()
+{
+ //XXX You should add the owner(s) of the bot here. You can do so with a
+ // regular expression which matches their hostmask, like so:
+ // addOwner(/rginda.*!.*@.*netscape\.com$/i);
+
+ bot.primNet = bot.networks["libera.chat"];
+
+ load("DP.js");
+ CIRCNetwork.prototype.INITIAL_NICK = "mingus";
+ CIRCNetwork.prototype.INITIAL_NAME = "mingus";
+ CIRCNetwork.prototype.INITIAL_DESC = "real men do it with prototypes";
+ CIRCNetwork.prototype.INITIAL_CHANNEL = "#chatzilla";
+
+ CIRCChannel.prototype.onJoin =
+ function my_chan_join(e)
+ {
+ if (userIsOwner(e.user))
+ e.user.setOp(true);
+ };
+
+ bot.eventPump.addHook(psn_isAddressedToMe, psn_onAddressedMsg,
+ "addressed-to-me-hook");
+ bot.personality.dp = new CDPressMachine();
+ /*
+ bot.personality.dp.addPhrase ("I am " +
+ CIRCNetwork.prototype.INITIAL_NICK +
+ ", hear me roar.");
+ */
+ bot.personality.dp.addPhrase("\01ACTION is back.");
+
+ /* dp hooks start */
+
+ var f = function(e)
+ {
+ var catchall = (e.hooks[e.hooks.length - 1].name == "catchall");
+ var answer = "";
+
+ if (catchall)
+ {
+ var ary = e.statement.split(" ");
+ for (var i = 0; i < 3; i++)
+ {
+ var randomElem = getRandomElement(ary);
+ answer = bot.personality.dp.getPhraseContaining(randomElem);
+ if (answer)
+ break;
+ }
+ }
+
+ if (!answer)
+ answer = bot.personality.dp.getPhrase();
+
+ if (answer[answer.length - 1] == "\01")
+ {
+ if (answer[0] != "\01")
+ {
+ if (catchall)
+ {
+ var prefes = bot.personality.guessActionPrefes;
+ answer = "\01ACTION " + getRandomElement(prefes) + answer;
+ }
+ else
+ {
+ answer = "\01ACTION " + answer;
+ }
+ }
+ }
+ else
+ {
+ if (!answer)
+ answer = "I don't know anything";
+
+ if (catchall)
+ {
+ answer = getRandomElement(bot.personality.guessPrefixes) +
+ answer;
+ }
+ }
+
+ if (answer[0] != "\01")
+ e.replyTo.say(e.user.unicodeName + ", " + answer);
+ else
+ e.replyTo.say(answer);
+
+ return false;
+ };
+
+/* first hook added is last checked */
+ bot.personality.addHook(/.*/i, f, "catchall");
+ bot.personality.addHook(/speak$/i, f, "speak");
+ bot.personality.addHook(/talk$/i, f, "hook");
+ bot.personality.addHook(/say something$/i, f, "say-something");
+
+ f = function(e)
+ {
+ var subject = e.matchresult[1].match(CDPressMachine.WORD_PATTERN);
+ if (subject == null)
+ subject = "";
+ else
+ subject = subject.toString();
+
+ var escapedSubject = escape(subject.toLowerCase());
+ var answer = bot.personality.dp.getPhraseContaining(escapedSubject);
+
+ if (!answer)
+ answer = "I don't know anything about " + e.matchresult[1];
+
+ if (answer.charCodeAt(0) != 1)
+ e.replyTo.say(e.user.unicodeName + ", " + answer);
+ else
+ e.replyTo.say(answer);
+
+ return false;
+ };
+
+ bot.personality.addHook(/speak about (\S+)/i, f);
+ bot.personality.addHook(/talk about (\S+)/i, f);
+ bot.personality.addHook(/say something about (\S+)/i, f);
+
+ f = function(e)
+ {
+ var answer = bot.personality.dp.getPhraseContaining("%01ACTION");
+
+ if (!answer)
+ answer = "I can't do a thing.";
+
+ e.replyTo.say(answer);
+
+ return false;
+ };
+
+ bot.personality.addHook(/do something/i, f);
+
+ f = function(e)
+ {
+ var ary = bot.personality.dp.getPhraseWeights(e.matchresult[1]);
+ var c = bot.personality.dp.getPhraseWeight(e.matchresult[1]);
+
+ e.replyTo.say(e.user.unicodeName + ", that phrase weighs " +
+ c + ": " + ary);
+
+ return false;
+ };
+
+ bot.personality.addHook(/weigh (.+)/i, f);
+
+ f = function(e)
+ {
+ var word = e.matchresult[1].toLowerCase();
+ var pivot = bot.personality.dp.getPivot(word);
+ var result = "";
+
+ if (pivot)
+ {
+ var list, w, l;
+
+ list = pivot.previousList;
+
+ w = list.getListWeights();
+ l = list.getListLinks();
+
+ if (w.length != l.length)
+ e.replyTo.say("warning: previous list mismatched.");
+
+ for (var i = 0; i < Math.max(w.length, l.length); i++)
+ result += ("`" + l[i] + "'" + w[i] + " ");
+
+ if (result.length > 250)
+ result += "\n";
+
+ result += ( "[" + word + "]" );
+
+ if (result.length > 250)
+ result += "\n";
+
+ list = pivot.nextList;
+
+ w = list.getListWeights();
+ l = list.getListLinks();
+
+ if (w.length != l.length)
+ e.replyTo.say("warning: next list mismatched.");
+
+ for (var i = 0; i < Math.max(w.length, l.length); i++)
+ result += (" `" + l[i] + "'" + w[i]);
+ }
+ else
+ {
+ result = "- [" + word + "] -";
+ }
+
+ e.replyTo.say(result);
+
+ return false;
+ };
+
+ bot.personality.addHook(/pivot (.*)/i, f);
+
+/* dp hooks end */
+
+ f = function(e)
+ {
+ print("I can hear you.");
+ e.replyTo.say(e.user.unicodeName + ", yes, I am.");
+
+ return false;
+ };
+
+ bot.personality.addHook(/are you alive(\?)?/i, f);
+
+
+ f = function(e)
+ {
+ if (!userIsOwner(e.user))
+ {
+ e.replyTo.say("nope.");
+ return;
+ }
+
+ chan = e.matchresult[1];
+
+ if (chan.charAt(0) != "#")
+ chan = "#" + chan;
+
+ e.server.sendData("join " + chan + "\n");
+
+ return false;
+ };
+
+ bot.personality.addHook(/join\s+(\S+)\.*/i, f);
+
+ f = function(e)
+ {
+ if (!userIsOwner(e.user))
+ {
+ e.channel.say("nope.");
+ return false;
+ }
+
+ chan = e.matchresult[1];
+
+ if (chan.charAt(0) != "#")
+ chan = "#" + chan;
+
+ e.server.sendData("part " + chan + "\n");
+
+ return false;
+ };
+
+ bot.personality.addHook(/part\s+(\S+)\.*/i, f);
+ bot.personality.addHook(/leave\s+(\S+)\.*/i, f);
+
+ f = function (e)
+ {
+ e.replyTo.say("mmmmmmm. Thanks " + e.user.unicodeName + ".");
+ return false;
+ };
+
+ bot.personality.addHook(/botsnack/i, f);
+
+ f = function(e)
+ {
+ e.replyTo.act("blushes");
+ return false;
+ };
+
+ bot.personality.addHook(/you rock/i, f);
+
+ f = function(e)
+ {
+ if (e.matchresult[1] == "me")
+ e.replyTo.act("hugs " + e.user.unicodeName);
+ else
+ e.replyTo.act("hugs " + e.matchresult[1]);
+ return false;
+ };
+
+ bot.personality.addHook(/hug (.*)/i, f);
+
+ f = function(e)
+ {
+ if (e.matchresult[1] == "me")
+ e.replyTo.say(e.user.unicodeName + ", :*");
+ else
+ e.replyTo.say(e.matchresult[1] + ", :*");
+ return false;
+ };
+
+ bot.personality.addHook(/kiss (\w+)/, f);
+
+ f = function (e)
+ {
+ e.replyTo.say(e.user.unicodeName + ", I'll try :(");
+ return false;
+ };
+
+ bot.personality.addHook
+ (/(shut up)|(shaddup)|(be quiet)|(keep quiet)|(sssh)|(stfu)/i, f);
+
+ f = function(e)
+ {
+ if (!userIsOwner(e.user))
+ {
+ e.replyTo.say("No.");
+ }
+ else
+ {
+ for (var n in bot.networks)
+ bot.networks[n].quit("Goodnight.");
+ }
+ return false;
+ };
+
+ bot.personality.addHook(/(go to bed)|(go to sleep)|(sleep)/i, f);
+
+ f = function(e)
+ {
+ e.replyTo.say(":)");
+ return false;
+ };
+
+ bot.personality.addHook
+ (/(smile)|(rotfl)|(lmao)|(rotflmao)|(look happy)|(you(.)?re smart)/i, f);
+/* (/(smile)|(rotfl)|(lmao)|(rotflmao)|(you(.)?re funny)|(look happy)|(you(.)?re smart)/i, f); */
+
+ f = function(e)
+ {
+ e.replyTo.say(":(");
+ return false;
+ };
+
+ bot.personality.addHook(/(frown)|(don(.)?t like you)|(look sad)/i, f);
+
+ f = function(e)
+ {
+ e.replyTo.say(">:|");
+ return false;
+ };
+
+ bot.personality.addHook(/(look mad)|(beat you up)/i, f);
+
+ f = function(e)
+ {
+ e.replyTo.say(":/");
+ return false;
+ };
+
+ bot.personality.addHook(/(look confused)|(i like windows)/i, f);
+}
+
diff --git a/comm/suite/chatzilla/js/tests/mybot.js b/comm/suite/chatzilla/js/tests/mybot.js
new file mode 100644
index 0000000000..260943b3c1
--- /dev/null
+++ b/comm/suite/chatzilla/js/tests/mybot.js
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+if (typeof escape == "undefined")
+ escape = unescape = function (s) {return s;}
+
+
+function runMyBot()
+{
+ load ("ircbot.js");
+ go();
+}
+
+function initPersonality()
+{
+ load ("mingus.js");
+ initMingus();
+}
diff --git a/comm/suite/chatzilla/js/tests/test_matchobject.js b/comm/suite/chatzilla/js/tests/test_matchobject.js
new file mode 100644
index 0000000000..c44380a366
--- /dev/null
+++ b/comm/suite/chatzilla/js/tests/test_matchobject.js
@@ -0,0 +1,41 @@
+
+function test_matchObject()
+{
+ var f = true;
+
+ obj1 = {foo:"hey", bar:"ho"}
+ obj2 = {a:"1", b:"2"}
+
+ p1 = {foo:"hey"}
+ p2 = {bar:"ho"}
+ p3 = {a:"1"}
+ p4 = {b:"2"}
+
+ /* true, single pattern, and it matches */
+ f &= matchObject (obj1, p1);
+ /* false, single pattern matches, negated */
+ f &= !matchObject (obj1, p1, true);
+
+ /* false, single pattern doesn't match */
+ f &= !matchObject (obj1, p3);
+ /* true, single pattern doesn't match, negated */
+ f &= matchObject (obj1, p3, true);
+
+ /* true, p1 matches */
+ f &= matchObject (obj1, [p1, p3]);
+ /* false, p1 matches, negated */
+ f &= !matchObject (obj1, [p1, p3], true);
+
+ /* true, both paterns match */
+ f &= matchObject (obj2, [p3, p4]);
+ /* false, both patterns match, negated */
+ f &= !matchObject (obj2, [p3, p4], true);
+
+ /* false, neither pattern matches */
+ f &= !matchObject (obj1, [p3, p4]);
+ /* true, neither pattern matches, negated */
+ f &= matchObject (obj1, [p3, p4], true);
+
+ return Boolean(f); /* you've got to find any problems by hand :) */
+
+} \ No newline at end of file
diff --git a/comm/suite/chatzilla/js/tests/toys.js b/comm/suite/chatzilla/js/tests/toys.js
new file mode 100644
index 0000000000..a9fd7f7e9c
--- /dev/null
+++ b/comm/suite/chatzilla/js/tests/toys.js
@@ -0,0 +1,35 @@
+
+function rainbow(str)
+{
+ str = String(str);
+ var c = str.length;
+ var rv = "";
+
+ for (var i = 0; i < c; i++)
+ {
+ var color = randomRange (2, 6);
+ rv += unescape ("%03" + color + str[i]);
+ }
+
+ return rv;
+
+}
+
+function fade(str)
+{
+ var colors = new Array(1, 14, 10, 15, 0);
+ var cIndex = 0;
+ var msg = "";
+ for (var i = 0; i < str.length; i++)
+ {
+ msg += "%03" + colors[cIndex] + str[i];
+ if ((++cIndex) == 5)
+ {
+ cIndex = 0;
+ }
+ }
+
+ return unescape(msg);
+
+ }
+