diff options
Diffstat (limited to 'comm/suite/chatzilla/js')
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, "&").replace(/</g, "<") + .replace(/>/g, ">").replace(/"/g, """) + .replace(/'/g, "'"); +} + +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); + + } + |