diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/chatzilla | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/chatzilla')
130 files changed, 44530 insertions, 0 deletions
diff --git a/comm/suite/chatzilla/ChangeLog b/comm/suite/chatzilla/ChangeLog new file mode 100644 index 0000000000..ad095e6a0a --- /dev/null +++ b/comm/suite/chatzilla/ChangeLog @@ -0,0 +1,432 @@ +################################################################################# +# 12/03/01, ChatZilla 0.8.5, <rginda@netscape.com> + +Shipped with 0.9.7. Again, thanks to Samuel Sieb <samuel@sieb.net> for +contributing many of the fixes (the most obvious of which is mIRC color code +support), reviews, and driving the release candidate process. Thanks to Casey +Hutchinson <nnooiissee@yahoo.com> for mac accelerator cleanup, help and edit +menus, xul refactoring work, statusbar, and motivation. Thanks to +KOIKE Kazuhiko, Katsuhiko Momoi, Shoji Matsumoto, and others who helped out in +bug 41564 to get charsets working. + +* strict warning cleanup. +* add input/output charset support. use /charset <charsetname> to + change the current charset, settings will be saved. +* add "open at startup" item to view menus. +* implement urls for /query views (isnick urls.) +* make nicknames in output window isnick urls. +* add mozilla statusbar. +* add chatzilla icon to mozilla statusbar. +* move "statusbar" to top of message window, rename to "header". +* add mirc color code handling. +* show long topics in multiple lines in header, instead of cropping. +* add bugzilla munger rule, links "bug 12345" and "bug #12345" to appropriate + bugzilla bugs. +* add channel munger rule to link #channelname to the channel. +* add edit and help menus. +* make /attach accept urls and url "fragments", + /attach moznet, /attach moznet/chatzilla, and /attach irc://moznet/chatzilla + are now all valid commands. +* add prefs for when to make noise; new msg, msg in existing query view, and/or + /stalk matches. +* allow for space separated list of sounds to play, like "beep beep", or + "file:/foo.wav" +* fix bogus "connection refused" error when disconnecting. +* add editable topic in header area. +* implement server passowrds (they were way broke.) +* add ctrl-tab and ctrl-shift-tab handlers to switch views. +* fix tab complete behavior, so it works for words in the middle of the line. +* add /about command to get version info. +* add "collapse consecutive messages" option, useful for motifs with faces. +* mac s/ctrl/meta/ fixes. +* add default completions per view, as described in bug 63621. +* add whowas help info. +* format responses from WHO queries. +* add network view maxlines pref. +* only print "attampting to connect" message once per network. +* add target in Kick item of message pane context menu. +* remove timestamp tooltips, use status text instead. +* show message source in status bar, after timestamp. +* remove usage warning from /quit command. +* change max connect attempts from 20 to 5. +* remove unused listbox.js file from chatzilla.jar. +* add help menu. +* allow params in /ctcp commands. + + +================================================================================= += Bugs fixed + +41564,63621,71549,86032,88438,91399,92080,95913,97288,98244,99612,100159,100704,101942,102377,103104,103598,104114,105343,105604,105881,105882,106349,107148,108061,110006,111546,111565 + +http://bugzilla.mozilla.org/buglist.cgi?bug_id=41564%2C63621%2C71549%2C86032%2C88438%2C91399%2C92080%2C95913%2C97288%2C98244%2C99612%2C100159%2C100704%2C101942%2C102377%2C103104%2C103598%2C104114%2C105343%2C105604%2C105881%2C105882%2C106349%2C107148%2C108061%2C110006%2C111546%2C111565 + +28 bugs found. +ID Opened Summary +63621 2000-12-22 [RFE] Shortcut to reply to the last IRC message +98244 2001-09-04 Add "Leave this channel" to channel popup menu +101942 2001-09-27 whowas command said unknown even though it exists +92080 2001-07-24 Enter key to navigate menus also sends commmand +105604 2001-10-18 chatzilla should have a status bar (and component bar) +99612 2001-09-14 Chatzilla window id should not be "main-window" +102377 2001-09-29 Chatzilla does not handle server passwords +103104 2001-10-04 implement isnick urls +103598 2001-10-07 JavaScript strict mode patch for Chatzilla +91399 2001-07-18 Missing "screen buffer length" pref setting in prefs.js +104114 2001-10-10 Can't localize some strings in Chatzilla. +100704 2001-09-20 chatzilla needs an easy way to edit the topic +105882 2001-10-21 Chatzilla keyboard shortcuts use wrong modifier key +100159 2001-09-17 hiding a minimized userlist expands it instead +105343 2001-10-17 chatzilla should have /about command +106349 2001-10-23 [rfe]show a check beside selected motif in the view menu. +107148 2001-10-27 chatzilla menus should move to an overlay +105881 2001-10-21 `Edit' menu is missing in Chatzilla +88438 2001-06-29 2xAlt-Tab will bring up unexpected "user-match"-message +110006 2001-11-13 view/hide status bar is not saved +71549 2001-03-09 [RFE] add mIRC and IrcII color code support +86032 2001-06-14 [RFE] Ctrl+[shift]+tab and [shift]+F6 to switch channel view +95913 2001-08-18 Include Chatzilla icon in Component Bar +111565 2001-11-23 /query command no longer sends second argument +111546 2001-11-22 User list doesn't repopulate correctly after rejoining chann +108061 2001-11-01 /stalk won't stalk nicknames +41564 2000-06-05 Internationalize ChatZilla to handle different language scri +97288 2001-08-28 Chatzilla gets disconnected w/o indicating anything is wrong + +################################################################################# +# 8/23/01, ChatZilla 0.8.4, <rginda@netscape.com> + +Shipped with Mozilla 0.9.4, 0.9.5, and 0.9.6, introduced motifs, channel and +mailto links, and other things that were only availble in the 0.8.1a/b releases, +which were not checked in. Thanks to Samuel Sieb <samuel@sieb.net> for +contributing many of the fixes, and reviewing the rest. + +================================================================================= += Bugs fixed + +superbug: 103386 + +41337,44447,69296,72272,72702,73652,73767,74408,79322,85573,89692,89717,92327,92403,95781,96567 + +http://bugzilla.mozilla.org/buglist.cgi?bugidtype=include&bug_id=41337%2C44447%2C69296%2C72272%2C72702%2C73652%2C73767%2C74408%2C79322%2C85573%2C89692%2C89717%2C92327%2C92403%2C95781%2C96567 + +SuperBug: http://bugzilla.mozilla.org/show_bug.cgi?id=89713 + +16 bugs found. +ID Opened Summary +92403 2001-07-26 mozilla -help displays >>>>>>>>>>>> chatzilla-service loaded +96567 2001-08-22 long nicks cause extra-high lines +95781 2001-08-17 Chatzilla text is black-on-black +74408 2001-04-02 Typing in Chatzilla should give Textbox at bottom focus + automatically. +89692 2001-07-06 /away command only uses first word for reason +85573 2001-06-12 [RFE] support for mailto: +72272 2001-03-16 irc urls don't detect server/network names correctly +72702 2001-03-20 need UI for adding irc:// bookmarks +89717 2001-07-06 header text not visible for op and voice +73652 2001-03-27 Missing support for !-Channels +41337 2000-06-02 /join should join the current window if it is a channel and you + are not currently in it +79322 2001-05-07 [RFE] Make chatzilla user scriptable +73767 2001-03-28 Confusing behaviour if nick already in use +44447 2000-07-02 no ctcp ping results +69296 2001-02-18 irc URL with # doesn't work +92327 2001-07-25 [RFE] Have ops' names in red + +################################################################################# +# 07/01/01, ChatZilla 0.8.3, <rginda@netscape.com> + +Revision 0.8.3 removed hardcoded strings, in favor of the Mozilla i18n/l10n api. +RTL languages may still have some issues. Many thanks to Chiaki Koufugata +<chiaki@mozilla.gr.jp> for doing the heavy lifting. + +================================================================================= += Bugs fixed + +http://bugzilla.mozilla.org/show_bug.cgi?id=27805 +Bug 27805, "ChatZilla needs i18n" + +################################################################################# +# 06/03/01, ChatZilla 0.8.2, <rginda@netscape.com> + +================================================================================= += Bugs fixed +79311, 75226, 80993 + +================================================================================= += Changes by file name... +chatzilla-service.js: +- Add stub allowPort method. +- Fix signature for handleContent method, bug 80993 + +connection-xpcom.js: +- factor chatzilla specific code out of this file. Callbacks into chatzilla specific code are now used, making this file more generic. +- according to darinf (the current necko guy), using openOutputStream with asyncRead is a bad thing. Most of the changes in this file involve migrating from usage of openOutputStream to asyncWrite. +- Changes also include fixing the function declaration syntax to match the rest of the code (two lines, named functions.) + +irc-debug.js: +- check nextLine before using it. + +irc.js: +- changes to work with new socket interface. +- correct isOp detection in setTopic +- remove checks for undefined exceptions +- route data-available immediately. inserting a data-available event to be routed later caused disconnect events to be received out of order. + +utils.js: +- fix HAS_XPCOM test (XPCDOM broke it.) +- add jsenv.HAS_NSPR_EVENTQ + +mybot.js: +- not built - +- add dummy escape/unescape if it isn't there + +handlers.js: +- return false the first time through onClose(), and disconnect from all servers. This makes sure we keep the window around long enough to send the QUIT messages. +- close window if client.userClose is set and we disconnected from the last server. + +chatzilla.xul: +- hook up onclose event. +- apply patch from 75226, fixes initial splitter position. + +static.js: +- copy client.userAgent code from chatzilla 0.8.1x +- implement getConnectionCount() + +################################################################################# +# ChatZilla 0.8.1a and 0.8.1b released only in XPI format, changes to be merged +# in later. +################################################################################# + +################################################################################# +# 03/08/01, ChatZilla 0.8, <rginda@netscape.com> + +================================================================================= += Bugs fixed + +The superbug for ChatZilla 0.8 is 71468, which fixes the following bugs: +22890, 40959, 41798, 42905, 43850, 44077, 54143, 56312, 59036, 59374, 65633, 65634, 65861, 66040, 71310, and 71378. + +Also, a workaround for bug 70335 is included, and bug 45576 *may* be fixed, we'll have to wait and see + +Link to superbug: http://bugzilla.mozilla.org/show_bug.cgi?id=71468 +Link to list of bugs fixed: http://bugzilla.mozilla.org/buglist.cgi?bug_id=22890%2C+40959%2C+41798%2C+42905%2C+43850%2C+44077%2C+54143%2C+56312%2C+59036%2C+59374%2C+65633%2C+65634%2C+65861%2C+66040%2C+71310%2C+71378 + +================================================================================= += Changes by file name... + +These changes are listed as they appear, when reading a diff between what's in cvs now, and what's in my local tree. Before checking in, I'll tag the current chatzilla code as chatzilla0.7, and after, chatzilla0.8. Just diff those two tags to get something to look at alongside this ChangeLog. Or see the superbug for ChatZilla 0.8, 71468, which has a copy of the diff attached to it. + +--------------------------------------------------------------------------------- +- NEW FILES: + +outputwindow.html: +- used as the base html for the output window, allowed bug 41798, bug 54143, and bug 59374 to be fixed. + +ChangeLog: +- this file, tracks changes between ChatZilla versions. I added the comments from the previous large checkin (retro-branded as ChatZilla 0.7) as well. + +TODO: +- List of things to do in the future. + +--------------------------------------------------------------------------------- +- MODIFIED FILES: + +README: +- Started user guide, only the TOC at the moment. + +connection-xpcom.js: +- Don't call close on output streams, it throws a NS_ERROR_NOT_IMPLEMENTED. dougt says just let the refcount go to 0 and it'll be fine. +- Tell the IRCServer object it has lost it's connection when the stream gets an onStopRequest (bug 22890 and 42905) + +events.js: +- dump exception on error (to get the filename and linenumber info, if it's there) + +irc-debug.js: +- show data for new "info" type of event. + +irc.js: +- add a servers collection to the IRCNetwork. I'm not exactly sure why I didn't do this from the beginning. It allows us to recover and reuse server objects in the event of a disconnect/reconnect. (bug 22890 and 66040) +- set default number of reconnect attempts to something more tolerable. (bug 22890 and 66040) +- move connection attempt/ next host to attempt information from the event object onto the actual network object. The new reconnect logic needs it in places where the event is not available. (bug 22890 and 66040) +- spew out info event when max connect attempts are exhausted. (bug 22890 and 66040) +- spew out info event when a connection attempt starts. (bug 22890 and 66040) +- add try/catch around server creation, to be safe. +- spew info event when moving on to the next connection attempt. (bug 22890 and 66040) +- modify CIRCServer connection to check for a dupe server in it's parent.servers collection, re-init/return that object if it exists, otherwise make a new one. +- try to reconnect if the connection was broken before we got a 001 message from the server, regardless of whether ot not the parent IRCNetwork has it's "stayingPower" flag set. +- forward the disconnect event from the server to the network, to keep the network properly informed. +- if an unknown message arrives at the server, send it to the server's onUnknown handler if it exists. If the server has no onUnknown handler, send it to the parent network's handler for that type of message, if it has one. If the parent network has no handler for this message, send it to the network's onUnknown handler as a last ditch. (the event code will fail silently if the network has no onUnknown handler.) +- when we get an 001, reset connection attempt cound, and record the successful connection. +- if the channel's users collection is considered "stable" when a new names list begins to arrive, mark it as unstable before wiping it out. the list will be marked stable again when the end of names (code 366) message arrives. +- refer to this.foo instead of e.server.foo. they are the same object, but this.foo is one less lookup. this change should happen to the rest of this file eventually. +- Tell the network when the user changes their nick. This is so nick changes that occur when you're not on any channel don't go unnoticed. +- round off excess decimals in the server lag. +- add opCount and voiceCount vars to the IRCChannel during a channel.getUsersCount() call. + +utils.js: +- make keys() return an array instead of a string. this function was not called by anything before this change. +- add formatDateOffset() function to the result of a Date subtraction into english. +- add arraySpeak() function to join an array into an english list. +- add arrayContains() function to search an array for an element. +- add arrayIndexOf() function to get the index of an element within the array. +- modify hyphenateWord() function to search for a goo place to break the word with a -/+ 5 character fuzz factor. +- add splitLongWord() function, similar to hyphenateWord, except returns an array. +- fix stoopid paren bug in roundTo() function. + +chatzilla.xul: +- add broadcasters for the file menu items. +- add keys for the file menu items. +- add file menu. (bug 43850) +- move options menu under file menu. +- remove old view toolbar. +- add crop="right" attribute to the userlist table to prevent horizontal scrollbars. (bug 56312) +- whitespace fixes. +- add input splitter for multiline-input mode. +- add multiline-input control for multiline-input mode. +- add crop="right" to statusbar elements (bug 59036 maybe 45576) + +commands.js: +- alphabetize commands. +- add client, cancel, disconnect, infobar, list, networks, notify, query, and status commands. (bug 44077) + +handlers.js: +- don't call setOutputStyle() onLoad (it's been deleted.) +- we can now call setClientOutput() and initStatic() onLoad, because the output window is a separate .html file. This means the the xul onLoad will not be called until the .html file is loaded, and so we can be sure that the content model for the output window will be stable. +- global change from 'quotes like this' to ``quotes like this''. +- set client.debugMode variable when debug messages are on. This flag is used to determine how we display irc message codes. +- remove onToggleToolbarIcons() because we don't have a toolbar anymore. +- don't allow the last view to be deleted. +- move to new sort-service, but leave the old one in for now. This keeps us from breaking 0.8 users. +- add keyUp handler for the multiline-input control. +- clear the input control's value in the singleline input control's keyUp handler, instead of onInputCompleteLine(), because we don't want to clear the multiline control. +- switch to multiline mode if the singleline control hears a ctl+up. +- don't spew an error message if autocomplete can't locate a match on [tab] +- remove old (and broken) multiline code. +- add onNotifyTimeout() handler to take care of sending ISON messages. ISON is used to ask the server if certain users are on, used for the /notify command. +- add onInputCancel() handler (for the /cancel command) to cancel and pending /attach command. +- send unknown / commands directly to the server, (after warning the user that the command was unknown.) +- add onInputSimpleCommand(). Any / commands that can be sent directly to the server can use this function, instead of creating a new one for every "simple" command. +- add onInputStatus() (for the /status command.) Prints connection status, including connection duration, lag, and channels joined. +- revamp onInputTestdisplay(). It now tests all types of messages, and all munger rules, from multiple sources, to multiple targets. Much more useful than it used to be. +- add onInputNetworks() (for /networks.) Lists all networks as a set of irc:// links. +- show an error message if you attempt to /attach to a network you are already attached to. +- fail a /delete if the user provided a parameter. /delete deletes the *current* view, failing if a param is given prevents accidental deletion. +- Change onInputNames() from a function that toggles the infobar (userlist) visibility to a function that sends a NAMES command to the server. +- add onInputInfobar() to do what onInputName() used to do. +- add extra help message to onInputCommands(). +- add status messages to onInputAttach(). +- fix onInputMe() so it works on /query views as well as channels. +- add onInputQuery() function (for /query.) +- fix onInputMsg() so it does not force a new query window to open. +- change parameter of temporary doEval() function form s to __s, so as not to interfere with eval'd expressions. +- split EVAL message types into EVAL-IN and EVAL-OUT, so you can see was was eval()d, in the event of an error. +- change onInputTopic() from a function that would consult the irc library for topic information to a function send asks the server to update the topic information in the irc library. the code that handles TOPIC replies now tales care of printing this information out. +- add onInputNotify() function (for /notify.) +- add onInfo handler to networks. +- add onUnknown handler to networks. +- add 005 to the list of initial server messages that are *always* displayed on the network view. +- force handlers attached to the my_showtonet() function to display on the network view (ignoring the client.SLOPPY_NETWORKS setting.) +- allow NOTICE messages to be affected by client.SPLOPPY_NETWORKS. +- add an on303 (ISON reply) handler to networks. +- add an on322 (LIST reply) handler to networks. +- use the new arraySpeak() and formatDateOffset() functions when printing WHOIS information. +- print error message when a network gets disconnected. (bug 42905) +- show nick change information on the network if it is the current view. +- display "end of NAMES" message if the channel's pendingNamesReply flag is set, then clear pendingNamesReply. channel.pendingNamesReply is set by the /names command (and cleared in on366) to control whether or not NAMES information (353 and 366 messages) should actually be displayed. (the irc library will request this information when a new channels is joined, and we don't want to display it in that case.) (bug 40959) +- print topic information on a 332. +- print topic setter information on a 333. +- print NAMES information on 353 if the channel's pendingNamesReply flag is set. (bug 40959) +- global fixes to .display callsites. new syntax is obj.display (<msg>, <type>, <source>, <dest>); +- beep when a message from a user is received. + +readprefs.js: +- remove toolbar.icons pref, we don't have a toolbar anymore. +- set client.debugMode based on debug messages pref. + +static.js: +- removed some unused client.PREFERENCE type variables. +- added client.SLOPPY_NETWORKS switch (see comment.) +- added client.HIDE_CODES switch (see comment.) +- added a message code -> display glyph map for use when client.HIDE_CODES is true. +- remove the isPermanent flag from the client view, allows users to delete the client view. +- set client.IGNORE_MOTD to false. +- bump MAX_MESSAGES up a few notches for various view types. +- set onresize handler for the window. (bug 41798) +- create an nsISound in the client object, so we can beep for /msgs. +- hook up multiline-input control keyUp handler. +- remove icons-in-toolbar related code. +- remove network list from the hello message, but... +- call onInputNetworks() during init. +- start the notify interval. +- global change from 'quotes like this' to ``quotes like this''. +- remove dead servers from the efnet server list. +- add opennet network. (bug 65633) +- removed the "you-talking-to-me" munger rule, this is now done in display(). +- made bold, underline, teletype, italic regexps more better. +- remove matchMyNick function, this is now done in display(). +- add a "chatzilla-link" class to all links. +- style rheet matches as chatzilla-rheet and chatzilla-link. +- keep smileys that start with the number 8 from matching. (bug 65861) +- fix insertHyphenatedWord() to use splitLongWord() and empty <img> tags, instead of using hyphenateWord() to insert spaces in long words. the empty <img> lets the layout engine break long words, without adding spaces. +- added skin switching voodoo to mainStep, it's a dumb ass hack, read the comment. (bug 59374) +- removed setOutputStyle() function, we can't do this now that we load the basic output document as a .html file. +- simplified setClientOutput() function. all the extra work is not needed now that we load the basic output document as a .html file. +- global fixes to .display callsites. new syntax is obj.display (<msg>, <type>, <source>, <dest>); +- added multilineLineInputMode() function, to set the input mode state. +- added stringToMsg() function. takes a string and converts it into munged DOM nodes ready to be added to the message window. +- set the "selected" attribute on the current view tab button. +- add scrollDown() function, it's now used from addHistory and onresize. +- put a <tbody> in the <table> before adding content. (bug 70335) +- create <xul:tab>s instead of <xul:menubutton>s for the views. +- add client.load() function to load js subscripts (won't work until bug 48974 is fixed.) +- combined client, network, channel, and user.display() implementations into a single function. + +munger.js: +- rename third parameter to .munge() method. +- break long words found inside text that matched other munger rules. + +chatzilla.css: +- remove unused ids and classes. +- made the status data color darker. (bug 65634) + +output-default.css: +- removed unused classes. +- added huge comment (read it.) +- changed color scheme. + +face-*.gif: +- made the smiley faces smaller so they fit better with the small font. + +################################################################################# +# 10/23/00, ChatZilla 0.7, <rginda@netscape.com> + +================================================================================= += Bugs fixed + +The superbug for ChatZilla 0.7 is 57633, which fixes the following bugs: +40636, 41343, 51352, 54145, 56708, 57104, and 57138 + +Link to superbug: http://bugzilla.mozilla.org/show_bug.cgi?id=57633 +List of bugs dependent on the superbug: http://bugzilla.mozilla.org/buglist.cgi?field0-0-0=dependson&type0-0-0=equals&value0-0-0=57633&cmdtype=doit + +================================================================================= += Changes ... + += fixed 41343, tab complete still works after part. += fixed 56708, link to rheet.wav when someone says rheet. += fixed 40636, delete text when simleys are matched += fixed 57138, toolbars not collapsing correctly. += fixed 57104, link regex was kinda lame. += fixed 54145, link completion char should be a pref. += fixed 51352, chatzilla should remember your nick. += cleaned up strict mode warnings. += adjusted menu layout to make it easier to grok (i hope.) += added "save settings now" and "save settings on exit" menu items. += fixed invalid xul tags (<label/>) to avoid nasty warnings. += implemented save settings functions. += added prefs for default network, startup urls, nickname autocomplete suffix, += delete view after part, save settings on exit, stalk list, smiley text deletion. += removed pref fot startup network/channel (replaced by startup urls.) += added graphic for scc's ear emoticon (* += made ui state (toolbar, userlist, statusbar visiblity) persist. + diff --git a/comm/suite/chatzilla/DYK b/comm/suite/chatzilla/DYK new file mode 100644 index 0000000000..eb96dc228f --- /dev/null +++ b/comm/suite/chatzilla/DYK @@ -0,0 +1,97 @@ + + + ChatZilla: Did You Know? + + + + + +The basics... + ++ The networks listed in the *client* view on startup are clickable links. + Instead of typing /attach <network-name>, you can just click on one of the + links. + ++ The view tabs are color coded, green means that something has happened in that + view since you last looked at it. Red means that someone has said something + to you. + ++ ChatZilla will automatically highlight phrases that contain your nickname. + ++ You can add to the list of words that cause messages to be highlighted with + the /stalk command. You can remove words from the list with the /unstalk + command. + ++ You can quickly switch to another view using the function keys. F1 will + switch to the first view, F2 to the second, etc. + ++ You can turn off the userlist, statusbar, and/or view tabs using the + File->Options menu. + ++ You can see a list of commands by typing /commands. + ++ You can get help on any command by typing /help <command-name>. + ++ You can delete any view with the /delete command. Once deleted, the text from + that view will be lost. Deleteing a channel view is not the same as leaving + the channel. You will still be a member of the channel if you only /delete it. + It will reappear (without the previous contents) the next time activity occurs. + ++ You can remove a channel or query view from the tab list without dropping its + contents. The /hide command will remove the view's tab, but keep the contents. + The next time something happens on that view, it will re-appear with the + previous text still intact. + ++ You can recover deleted or hidden views. To recover a deleted *client* view, + use /client. To recover a network view use the /attach command, to recover + channel view use /join, and to recover a query view use /query. + +The cool commands... + ++ /networks will display the list of networks as a list of clickable links. + ++ /status will show your connection status. If you use /status from a channel, + you'll get information about the network and server you are connected to, + including connection duration, and last ping and lag (if available.) You will + also get status on the channel you are viewing, including number of operators, + number voiced, and topic. + + Using /status from a network view will give you information on every channel + you are connected to. + + Using /status from the *client* view will give you information on every network + you are connected to, as well as every channel on that network. + ++ /disconnect can be used to disconnect from a network without quitting + ChatZilla. This can be useful if you are attached to more than one network + but want to leave only one of them. + +The more advanced stuff... + ++ The input area can be switched to multiline mode by pressing [CTRL]+[UP]. + Press [CTRL]+[ENTER] to sent the text to the server. In multiline mode, + the text you type not scanned for commands, so stray / characters will not + affect your post. To return to single line mode, press [CRTL]+[DOWN]. + ++ The place where messages appear can be styled to your own taste, if you + know a little css <http://www.htmlhelp.org/reference/css/>. See the comment + at the top of <http://lxr.mozilla.org/mozilla/source/extensions/irc/xul/skin/output-default.css> + for details. + ++ You can manually bookmark an irc channel by creating a new irc:// bookmark. + From the "Manage bookmarks" window, create a new bookmark and enter + "irc://moznet/chatzilla", to bookmark the #chatzilla channel, for example. + You can also use a server name, as in "irc://irc.mcs.com/javascript". + ++ irc:// urls work from normal web pages as well. You can link to your + irc channel from you home page. See + <http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt> for a description + of irc:// urls. + ++ Turning off File->Options->Enable Smileys actually turns off all of the text + matching rules, including link detection, and bold/underline/italic. It will + also disable the part of ChatZilla that makes it possible to break long words + (ie, you'll end up with a horizontal scrollbar eventually.) This + option was added back when a few bugs in the JavaScript engine caused the + text matching rules to do Bad Things. You probably don't really want to turn + this off. diff --git a/comm/suite/chatzilla/Makefile.in b/comm/suite/chatzilla/Makefile.in new file mode 100644 index 0000000000..ca4424f50f --- /dev/null +++ b/comm/suite/chatzilla/Makefile.in @@ -0,0 +1,6 @@ +# 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/. + +LOCALE_TOPDIR=$(commtopsrcdir) +LOCALE_RELATIVEDIR=suite/chatzilla diff --git a/comm/suite/chatzilla/README b/comm/suite/chatzilla/README new file mode 100644 index 0000000000..9a809aae9a --- /dev/null +++ b/comm/suite/chatzilla/README @@ -0,0 +1,70 @@ + Using ChatZilla + + + + + + + + + + + +Table Of Contents + +Part 1: Introduction to ChatZilla. + + 1.1: Introduction + 1.1.1: What is IRC? + 1.1.2: What is ChatZilla? + 1.1.3: Where do I get more information on IRC? + + 1.2: The User Interface. + 1.2.1: Menu structure. + 1.2.2: User list. + 1.2.3: Output area. + 1.2.4: View tabs. + 1.2.5: Status bar. + + 1.3: Features of the input area. + 1.3.1: Responding to the last person who spoke to you. + 1.3.2: Autocompleting nicknames. + 1.3.3: Autocompleting commands. + 1.3.4: Multiline input. + +Part 2: Navigating IRC with ChatZilla. + + 2.1: Connecting to an IRC network. + 2.1.1: Listing available networks. + 2.1.2: Attaching to a network. + 2.1.3: Canceling an attach that isn't going well. + 2.1.4: Connecting to a specific server. + 2.1.5: Disconnecting when you're finished. + + 2.2: Channels. + 2.2.1: Finding a channel. + 2.2.2: Joining. + 2.2.3: Modes, Topics, and Kicks. + 2.2.4: Parting. + + 2.3: Private messages. + 2.3.1: Receiving messages. + 2.3.2: Sending messages. + 2.3.3: Query windows. + +Part 3: Scripting ChatZilla. + + 3.1: Scripting basics. + 3.1.1: The /eval command. + 3.1.2: Writing text to the output window. + 3.1.3: Writing DOM nodes to the output window. + 3.1.1: Exploring objects with dumpObjectTree(); + + 3.2: More Scripting. + 3.2.1: External scripts. + 3.2.2: Hooking into IRC events. + +Appendix A: + A: Styling the output window. + (copy of the comment in output-window.css) + diff --git a/comm/suite/chatzilla/TODO b/comm/suite/chatzilla/TODO new file mode 100644 index 0000000000..9ebedc7556 --- /dev/null +++ b/comm/suite/chatzilla/TODO @@ -0,0 +1,8 @@ +ChatZilla 0.8.1 +smarter [tab] code (tab on empty line responds to last /msg or yournick:) +flubbed /msg detection. +optional time stamps +isnick irc:// urls + +ChatZilla 0.9 +Lots of prefs work, including a pref panel (finally!)
\ No newline at end of file diff --git a/comm/suite/chatzilla/jar.mn b/comm/suite/chatzilla/jar.mn new file mode 100644 index 0000000000..a9ae89fa42 --- /dev/null +++ b/comm/suite/chatzilla/jar.mn @@ -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/. + +chatzilla.jar: +% content chatzilla %content/chatzilla/ +% skin chatzilla modern/1.0 %skin/modern/chatzilla/ +% overlay chrome://navigator/content/navigator.xul chrome://chatzilla/content/browserOverlay.xul +% overlay chrome://communicator/content/tasksOverlay.xul chrome://chatzilla/content/chatzillaOverlay.xul +% overlay chrome://communicator/content/pref/preferences.xul chrome://chatzilla/content/prefsOverlay.xul +% overlay chrome://communicator/content/pref/pref-appearance.xul chrome://chatzilla/content/prefsOverlay.xul +% overlay chrome://chatzilla/content/chatzilla.xul chrome://communicator/content/utilityOverlay.xul +% overlay chrome://chatzilla/content/menus.xul chrome://communicator/content/tasksOverlay.xul +% style chrome://communicator/content/customizeToolbar.xul chrome://chatzilla/skin/browserOverlay.css + content/chatzilla/lib/js/utils.js (js/lib/utils.js) + content/chatzilla/lib/js/events.js (js/lib/events.js) + content/chatzilla/lib/js/connection-xpcom.js (js/lib/connection-xpcom.js) + content/chatzilla/lib/js/command-manager.js (js/lib/command-manager.js) + content/chatzilla/lib/js/pref-manager.js (js/lib/pref-manager.js) + content/chatzilla/lib/js/message-manager.js (js/lib/message-manager.js) + content/chatzilla/lib/js/menu-manager.js (js/lib/menu-manager.js) + content/chatzilla/lib/js/ident.js (js/lib/ident.js) + content/chatzilla/lib/js/irc.js (js/lib/irc.js) + content/chatzilla/lib/js/irc-debug.js (js/lib/irc-debug.js) + content/chatzilla/lib/js/file-utils.js (js/lib/file-utils.js) + content/chatzilla/lib/js/dcc.js (js/lib/dcc.js) + content/chatzilla/lib/js/json-serializer.js (js/lib/json-serializer.js) + content/chatzilla/lib/js/sts.js (js/lib/sts.js) + content/chatzilla/lib/js/text-serializer.js (js/lib/text-serializer.js) + content/chatzilla/lib/js/text-logger.js (js/lib/text-logger.js) + content/chatzilla/lib/js/chatzilla-protocol-script.js (js/lib/chatzilla-protocol-script.js) + content/chatzilla/lib/js/protocol-handlers.jsm (js/lib/protocol-handlers.jsm) + content/chatzilla/lib/xul/munger.js (xul/lib/munger.js) + content/chatzilla/lib/xul/tree-utils.js (xul/lib/tree-utils.js) + content/chatzilla/chatzilla.xul (xul/content/chatzilla.xul) + content/chatzilla/scripts.xul (xul/content/scripts.xul) + content/chatzilla/menus.xul (xul/content/menus.xul) + content/chatzilla/popups.xul (xul/content/popups.xul) + content/chatzilla/channels.xul (xul/content/channels.xul) + content/chatzilla/channels.js (xul/content/channels.js) + content/chatzilla/output-window.html (xul/content/output-window.html) + content/chatzilla/output-window.js (xul/content/output-window.js) + content/chatzilla/commands.js (xul/content/commands.js) + content/chatzilla/handlers.js (xul/content/handlers.js) + content/chatzilla/prefs.js (xul/content/prefs.js) + content/chatzilla/messages.js (xul/content/messages.js) + content/chatzilla/menus.js (xul/content/menus.js) + content/chatzilla/mungers.js (xul/content/mungers.js) +* content/chatzilla/static.js (xul/content/static.js) + content/chatzilla/networks.js (xul/content/networks.js) + content/chatzilla/networks-edit.css (xul/content/networks-edit.css) + content/chatzilla/networks-edit.js (xul/content/networks-edit.js) + content/chatzilla/networks-edit.xul (xul/content/networks-edit.xul) + content/chatzilla/networks-server.js (xul/content/networks-server.js) + content/chatzilla/networks-server.xul (xul/content/networks-server.xul) + content/chatzilla/dynamic.css (xul/content/dynamic.css) + content/chatzilla/output-base.css (xul/content/output-base.css) + content/chatzilla/chatzillaOverlay.xul (xul/content/chatzillaOverlay.xul) + content/chatzilla/chatzillaOverlay.js (xul/content/chatzillaOverlay.js) + content/chatzilla/browserOverlay.xul (xul/content/browserOverlay.xul) + content/chatzilla/prefsOverlay.xul (xul/content/prefsOverlay.xul) + content/chatzilla/pref-irc-toolkit.xul (xul/content/pref-irc-toolkit.xul) + content/chatzilla/config.xul (xul/content/config.xul) + content/chatzilla/config-add.xul (xul/content/config-add.xul) + content/chatzilla/config.js (xul/content/config.js) + content/chatzilla/config-add.js (xul/content/config-add.js) + content/chatzilla/config.css (xul/content/config.css) + content/chatzilla/install-plugin/install-plugin.js (xul/content/install-plugin/install-plugin.js) + content/chatzilla/install-plugin/install-plugin.xul (xul/content/install-plugin/install-plugin.xul) + content/chatzilla/about/about.xul (xul/content/about/about.xul) + content/chatzilla/about/about.js (xul/content/about/about.js) + skin/modern/chatzilla/chatzilla.css (xul/skin/chatzilla.css) + skin/modern/chatzilla/chatzillaOverlay.css (xul/skin/chatzillaOverlay.css) + skin/modern/chatzilla/browserOverlay.css (xul/skin/browserOverlay.css) + skin/modern/chatzilla/channels.css (xul/skin/channels.css) + skin/modern/chatzilla/install-plugin.css (xul/skin/install-plugin.css) + skin/modern/chatzilla/networks-edit.css (xul/skin/networks-edit.css) + skin/modern/chatzilla/about.css (xul/skin/about.css) + skin/modern/chatzilla/output-default.css (xul/skin/output-default.css) + skin/modern/chatzilla/output-light.css (xul/skin/output-light.css) + skin/modern/chatzilla/output-dark.css (xul/skin/output-dark.css) + skin/modern/chatzilla/images/logo.png (xul/skin/images/logo.png) + skin/modern/chatzilla/images/no-symbol.png (xul/skin/images/no-symbol.png) + skin/modern/chatzilla/images/no-graphic.png (xul/skin/images/no-graphic.png) + skin/modern/chatzilla/images/founder-symbol.png (xul/skin/images/founder-symbol.png) + skin/modern/chatzilla/images/founder-graphic.png (xul/skin/images/founder-graphic.png) + skin/modern/chatzilla/images/admin-symbol.png (xul/skin/images/admin-symbol.png) + skin/modern/chatzilla/images/admin-graphic.png (xul/skin/images/admin-graphic.png) + skin/modern/chatzilla/images/op-symbol.png (xul/skin/images/op-symbol.png) + skin/modern/chatzilla/images/op-graphic.png (xul/skin/images/op-graphic.png) + skin/modern/chatzilla/images/halfop-symbol.png (xul/skin/images/halfop-symbol.png) + skin/modern/chatzilla/images/halfop-graphic.png (xul/skin/images/halfop-graphic.png) + skin/modern/chatzilla/images/voice-symbol.png (xul/skin/images/voice-symbol.png) + skin/modern/chatzilla/images/voice-graphic.png (xul/skin/images/voice-graphic.png) + skin/modern/chatzilla/images/chatzilla-16.png (xul/skin/images/chatzilla-16.png) + skin/modern/chatzilla/images/multiline-expand.png (xul/skin/images/multiline-expand.png) + skin/modern/chatzilla/images/multiline-contract.png (xul/skin/images/multiline-contract.png) + skin/modern/chatzilla/images/input-send.png (xul/skin/images/input-send.png) + skin/modern/chatzilla/images/drop-indicator-bottom.png (xul/skin/images/drop-indicator-bottom.png) + skin/modern/chatzilla/images/logging-on.png (xul/skin/images/logging-on.png) + skin/modern/chatzilla/images/logging-off.png (xul/skin/images/logging-off.png) + skin/modern/chatzilla/images/spbubble-on.png (xul/skin/images/spbubble-on.png) + skin/modern/chatzilla/images/spbubble-off.png (xul/skin/images/spbubble-off.png) 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); + + } + diff --git a/comm/suite/chatzilla/locales/Makefile.in b/comm/suite/chatzilla/locales/Makefile.in new file mode 100644 index 0000000000..6b58d5d8b2 --- /dev/null +++ b/comm/suite/chatzilla/locales/Makefile.in @@ -0,0 +1,6 @@ +# 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/. + +LOCALE_TOPDIR=$(commtopsrcdir) +LOCALE_RELATIVEDIR=suite/chatzilla/locales diff --git a/comm/suite/chatzilla/locales/all-locales b/comm/suite/chatzilla/locales/all-locales new file mode 100644 index 0000000000..e6d17c0b74 --- /dev/null +++ b/comm/suite/chatzilla/locales/all-locales @@ -0,0 +1,23 @@ +cs +de +el +en-GB +es-AR +es-ES +fi +fr +hu +it +ja +ja-JP-mac +ka +nb-NO +nl +pl +pt-BR +pt-PT +ru +sk +sv-SE +zh-CN +zh-TW diff --git a/comm/suite/chatzilla/locales/en-US/chrome/about.dtd b/comm/suite/chatzilla/locales/en-US/chrome/about.dtd new file mode 100644 index 0000000000..981817cda7 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/about.dtd @@ -0,0 +1,17 @@ +<!-- 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/. --> + +<!ENTITY window.title "About ChatZilla"> + +<!ENTITY chatzilla.label "ChatZilla"> +<!ENTITY version.unknown.label "Version unknown"> +<!ENTITY version.known.label "Version %S"> +<!ENTITY description.label "A clean, easy to use and highly extensible Internet Relay Chat (IRC) client."> + +<!ENTITY homepage.label "Visit Home Page"> +<!ENTITY copyversion.label "Copy Version Details"> + +<!ENTITY section.core.label "Core Development Team:"> +<!ENTITY section.locale.label "Localization:"> +<!ENTITY section.contrib.label "Contributors:"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/browserOverlay.dtd b/comm/suite/chatzilla/locales/en-US/chrome/browserOverlay.dtd new file mode 100644 index 0000000000..5e774cc75c --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/browserOverlay.dtd @@ -0,0 +1,5 @@ +<!-- 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/. --> + +<!ENTITY czButton.label "ChatZilla"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/channels.dtd b/comm/suite/chatzilla/locales/en-US/chrome/channels.dtd new file mode 100644 index 0000000000..4cbc892041 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/channels.dtd @@ -0,0 +1,33 @@ +<!-- 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/. --> + +<!ENTITY window.title "Join Channel"> + +<!ENTITY network.label "Network:"> +<!ENTITY network.accesskey "N"> + +<!ENTITY channel.label "Channel:"> +<!ENTITY channel.accesskey "C"> + +<!ENTITY topics.label "Search topics as well as channel names"> +<!ENTITY topics.accesskey "t"> + +<!ENTITY join.label "Join"> +<!ENTITY join.accesskey "J"> + +<!ENTITY minusers.label "Min users:"> +<!ENTITY minusers.accesskey "M"> + +<!ENTITY maxusers.label "Max users:"> +<!ENTITY maxusers.accesskey "x"> + +<!ENTITY refreshNow.label "Refresh Now"> +<!ENTITY refreshNow.accesskey "R"> + +<!ENTITY network.hint.label "Enter any network or server name (you can include a port) and a channel to join."> + +<!ENTITY col.network "Network"> +<!ENTITY col.name "Name"> +<!ENTITY col.users "Users"> +<!ENTITY col.topic "Topic"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/chatzilla.dtd b/comm/suite/chatzilla/locales/en-US/chrome/chatzilla.dtd new file mode 100644 index 0000000000..19e74be9c3 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/chatzilla.dtd @@ -0,0 +1,19 @@ +<!-- 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/. --> + +<!ENTITY Menubar.tooltip "Main Menu"> +<!ENTITY Toolbar.tooltip "Main Toolbar"> + + +<!ENTITY multiline-expand.tooltip "Switch to multi-line input (Ctrl+Up)"> +<!ENTITY multiline-contract.tooltip "Switch to single-line input (Ctrl+Down)"> +<!ENTITY multiline-send.tooltip "Send this text (Ctrl+Enter)"> +<!ENTITY server-nick.tooltip "Change nickname or set away state. To focus the input box, press Escape."> + +<!ENTITY Underline.label "Underline"> +<!ENTITY Bold.label "Bold"> +<!ENTITY Reverse.label "Reverse video"> +<!ENTITY Normal.label "Normal"> +<!ENTITY Color.label "Color"> +<!ENTITY ForeBack.label "xx=Fore yy=Back"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/chatzilla.properties b/comm/suite/chatzilla/locales/en-US/chrome/chatzilla.properties new file mode 100644 index 0000000000..bfec752d34 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/chatzilla.properties @@ -0,0 +1,1674 @@ +# 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/. + +locale.authors = XXX REPLACE THIS VALUE WITH A SEMICOLON-SEPARATED LIST OF NAMES FOR YOUR LOCALIZATION TEAM XXX + +# Misc + +unknown=<unknown> +none=<none> +na=<n/a> + +# util.js + +msg.alert = Alert +msg.prompt = Prompt +msg.confirm = Confirm + +# command.js + +### Notes for localizers ### +# +# ChatZilla uses cmd.<command name>.* to construct the command's help, +# help usage and any UI labels. +# +# Note also that, for every command, an accesskey may be specified: +# EITHER by prefixing the desired accesskey with "&" in the .label string, +# OR by specifying a .accesskey string, which is useful if the desired +# accesskey does not occur in the label. +# +# The following are therefore equivalent: +# cmd.foo.label = &Foo +# and +# cmd.foo.label = Foo +# cmd.foo.accesskey = F +# +# +# All localised strings may contain certain entities for branding purposes. +# The three standard brand entities (brandShortName, brandFullName, vendorName) +# can all be used like this: +# foo.bar = Some text used in &brandFullName;! +# +### End of notes ### + +cmd.about.label = About ChatZilla +cmd.about.help = Display information about this version of ChatZilla. + +cmd.alias.helpUsage = [<alias-name> [<command-list>]] +cmd.alias.help = Defines <alias-name> as an alias for the semicolon (';') delimited list of commands specified by <command-list>. If <command-list> is a minus ('-') character, the alias will be removed; if omitted, the alias will be displayed. If <alias-name> is not provided, all aliases will be listed. + +cmd.attach.helpUsage = <irc-url> +cmd.attach.help = Attaches to the IRC URL specified by <irc-url>. If you are already attached, the view for <irc-url> is made current. If that view has been deleted, it is recreated. You may omit the irc:// portion of the <irc-url>. Examples are; /attach libera.chat, /attach libera.chat/firefox and /attach libera.chat/SeaMonkey,isnick. + +cmd.away.label = Away (default) +# LOCALIZATION NOTE (cmd.away.format): +# Do not localize $reason +cmd.away.format = Away ($reason) +cmd.away.helpUsage = [<reason>] +cmd.away.help = If <reason> is specified, sets you away with that message. Used without <reason>, you are marked away with a default message. + +cmd.back.label = Back +cmd.back.help = Marks you as no longer away. + +cmd.ban.label = Ban +# LOCALIZATION NOTE (cmd.ban.format): +# Do not localize $channelName +cmd.ban.format = Ban from $channelName +cmd.ban.helpUsage = [<nickname>] +cmd.ban.help = Bans a single user, or mask of users, from the current channel. A user's nickname may be specified, or a proper host mask can be used. Used without a nickname or mask, shows the list of bans currently in effect. + +cmd.cancel.help = Cancels an /attach or /server command, or a file transfer. Use /cancel on a network view when ChatZilla is repeatedly trying to attach to a network that is not responding, to tell ChatZilla to give up before the normal number of retries. Use /cancel on a file transfer view to stop the transfer. + +cmd.charset.helpUsage = [<new-charset>] +cmd.charset.help = Sets the character encoding mode for the current view to <new-charset>, or displays the current character encoding mode if <new-charset> is not provided. + +cmd.channel-motif.helpUsage = [<motif> [<channel>]] +cmd.channel-motif.help = Sets the CSS file used for the message tab for this specific channel. <motif> can be a URL to a .css file, or the shortcut "dark" or "light". If <motif> is a minus ('-') character, the motif will revert to the network motif. If <channel> is not provided, the current channel will be assumed. See the ChatZilla homepage at <http://www.mozilla.org/projects/rt-messaging/chatzilla/> for more information on how to style ChatZilla. See also |motif|. + +cmd.channel-pref.helpUsage = [<pref-name> [<pref-value>]] +cmd.channel-pref.help = Sets the value of the preference named <pref-name> to the value of <pref-value> on the current channel. If <pref-value> is not provided, the current value of <pref-name> will be displayed. If both <pref-name> and <pref-value> are omitted, all preferences will be displayed. If <pref-value> is a minus ('-') character, then the preference will revert back to its default value. + +cmd.clear-view.label = Cl&ear Tab +cmd.clear-view.helpUsage = [<view>] +cmd.clear-view.help = Clear the current view, discarding *all* content. +cmd.clear-view.key = accel L + +cmd.client.help = Make the ``*client*'' view current. If the ``*client*'' view has been deleted, it will be recreated. + +cmd.cmd-undo.label = &Undo +cmd.cmd-undo.key = accel Z +cmd.cmd-redo.label = &Redo +cmd.cmd-redo.key = accel Y +cmd.cmd-cut.label = Cu&t +cmd.cmd-cut.key = accel X +cmd.cmd-copy.label = &Copy +cmd.cmd-copy.key = accel C +cmd.cmd-paste.label = &Paste +cmd.cmd-paste.key = accel V +cmd.cmd-delete.label = &Delete +cmd.cmd-delete.key = VK_DELETE +cmd.cmd-selectall.label = Select &All +cmd.cmd-selectall.key = accel A +cmd.cmd-copy-link-url.label = Copy Link Location + +cmd.cmd-mozilla-prefs.label = &&brandShortName; Preferences… +cmd.cmd-prefs.label = Pr&eferences… +cmd.cmd-chatzilla-prefs.label = ChatZilla Pr&eferences… +cmd.cmd-chatzilla-opts.label = &Options… + +cmd.commands.helpUsage = [<pattern>] +cmd.commands.help = Lists all command names matching <pattern>, or all command names if pattern is not specified. + +cmd.custom-away.label = Away (custom)… + +cmd.ctcp.helpUsage = <target> <code> [<params>] +cmd.ctcp.help = Sends the CTCP code <code> to the target (user or channel) <target>. If <params> are specified they are sent along as well. + +cmd.default-charset.helpUsage = [<new-charset>] +cmd.default-charset.help = Sets the global default character encoding mode to <new-charset>, or displays the current global default character encoding mode if <new-charset> is not provided. + +cmd.delayed.helpUsage = <delay> <rest> +cmd.delayed.help = After |delay| seconds, run the command specified in |rest|. + +cmd.describe.helpUsage = <target> <action> +cmd.describe.help = Performs an 'action' at the |target|, either a channel or a user. + +cmd.dcc-accept.helpUsage = [<nickname> [<type> [<file>]]] +cmd.dcc-accept.help = Accepts an incoming DCC Chat or Send offer. If a |nickname| is not specified, the last offer that arrived will be accepted (for security reasons, this will not work in the first 10 seconds after an offer is received). You can also use a regular expression for either <nickname> or <file>. + +cmd.dcc-accept-list.help = Displays the DCC auto-accept list for the current network. + +cmd.dcc-accept-list-add.helpUsage = <nickname> +cmd.dcc-accept-list-add.help = Add someone to your DCC auto-accept list for the current network. + +cmd.dcc-accept-list-remove.helpUsage = <nickname> +cmd.dcc-accept-list-remove.help = Remove someone from your DCC auto-accept list for the current network. + +cmd.dcc-chat.helpUsage = [<nickname>] +cmd.dcc-chat.help = Sends a DCC Chat offer to |nickname| on the current server. On a query view, |nickname| may be omitted to send the offer to the query view's user. +cmd.dcc-chat.label = Direct Chat + +# LOCALIZATION NOTE (cmd.dcc-close.format): +# Do not localize $userName +cmd.dcc-close.format = Disconnect From $userName +cmd.dcc-close.label = &Disconnect +cmd.dcc-close.helpUsage = [<nickname> [<type> [<file>]]] +cmd.dcc-close.help = Closes an existing DCC connection. |nickname| may be omitted if run from a DCC view, in which case the DCC connection for that view will be closed. |type| and |file| may be needed to identify the connection. You can also use a regular expression for either <nickname> or <file>. + +cmd.dcc-decline.helpUsage = [<nickname>] +cmd.dcc-decline.help = Declines an incoming DCC Chat or Send offer. If a |nickname| is not specified, the last offer that arrived will be declined. You can also use a regular expression for <nickname>. + +cmd.dcc-list.helpUsage = [<type>] +cmd.dcc-list.help = Lists the currently known about DCC offers and connections. This may be limited to just "chat" or "send" using the |type| parameter. + +cmd.dcc-send.helpUsage = [<nickname> [<file>]] +cmd.dcc-send.help = Offers a file to |nickname|. On a query view, |nickname| may be omitted to send the offer to the query view's user. A file may be specified directly by passing |file| or, if omitted, selected from a browse dialog. +cmd.dcc-send.label = Send File… + +cmd.dcc-show-file.helpUsage = <file> +cmd.dcc-show-file.help = Opens the folder containing the file you downloaded. + +cmd.delete-view.key = accel W +cmd.delete-view.label = &Close Tab +cmd.delete-view.helpUsage = [<view>] +cmd.delete-view.help = Clear the current view, discarding *all* content, and drop its icon from the tab strip. If a channel view is deleted this way, you also leave the channel. + +cmd.dehop.label = Remove Half-operator Status +cmd.dehop.helpUsage = <nickname> [<...>] +cmd.dehop.help = Removes half-operator status from <nickname> on current channel. Requires operator status. + +cmd.deop.label = Remove Operator Status +cmd.deop.helpUsage = <nickname> [<...>] +cmd.deop.help = Removes operator status from <nickname> on current channel. Requires operator status. + +cmd.desc.helpUsage = [<description>] +cmd.desc.help = Changes the 'ircname' line returned when someone performs a /whois on you. You must specify this *before* connecting to the network. If you omit <description>, the current description is shown. + +cmd.devoice.label = Remove Voice Status +cmd.devoice.helpUsage = <nickname> [<...>] +cmd.devoice.help = Removes voice status from <nickname> on current channel. Requires operator (or half-operator) status. + +# LOCALIZATION NOTE (cmd.disconnect.format): +# Do not localize $networkName +cmd.disconnect.format = Disconnect From $networkName +cmd.disconnect.label = &Disconnect +cmd.disconnect.helpUsage = [<reason>] +cmd.disconnect.help = Disconnects from the server represented by the active view when the command is executed providing the reason <reason> or the default reason if <reason> is not specified. + +cmd.disconnect-all.label = &Disconnect From All Networks +cmd.disconnect-all.helpUsage = [<reason>] +cmd.disconnect-all.key = accel D +cmd.disconnect-all.help = Disconnects from all networks providing the reason <reason> or the default reason if <reason> is not specified. + +cmd.echo.helpUsage = <message> +cmd.echo.help = Displays <message> in the current view, but does not send it to the server. + +cmd.edit-networks.label = &Networks… +cmd.edit-networks.help = Opens the network editor, where you can edit the list of available networks and servers. + +cmd.enable-plugin.helpUsage = <plugin> +cmd.enable-plugin.help = Meant to be used to re-enable a plugin after calling |disable-plugin|, this command calls the plugin's enablePlugin function. There are no guarantees that the plugin will properly enable itself. + +cmd.eval.helpUsage = <expression> +cmd.eval.help = Evaluates <expression> as JavaScript code. Not for the faint of heart. + +cmd.evalsilent.helpUsage = <expression> +cmd.evalsilent.help = Identical to the /eval command, except the [EVAL-IN] and [EVAL-OUT] lines are not displayed. + +cmd.except.helpUsage = [<nickname>] +cmd.except.help = Excepts a user from channel bans. A user's nickname may be specified, or a proper host mask can be used. Used without a nickname or mask, shows the list of exceptions currently in effect. + +cmd.exit.label = E&xit ChatZilla +cmd.exit.helpUsage = [<reason>] +cmd.exit.help = Disconnects from all active servers and networks, providing the reason <reason>, or the default reason if <reason> is not specified. Exits ChatZilla after disconnecting. + +cmd.faq.label = ChatZilla FAQ + +cmd.find.label = &Find… +cmd.find.key = accel F +cmd.find-again.label = Find A&gain +cmd.find-again.key = accel G + +cmd.focus-input.key = VK_ESCAPE + +cmd.font-family.helpUsage = [<font>] +cmd.font-family.help = Sets or views the font family being used on the current view. Omit <font> to see the current font family. The value |default| will use your global font family, |serif|, |sans-serif| and |monospace| will use your global font settings, other values will set a font directly. +cmd.font-family-default.label = Default &Font +cmd.font-family-serif.label = Se&rif +cmd.font-family-sans-serif.label = S&ans Serif +cmd.font-family-monospace.label = Mo&nospace +# LOCALIZATION NOTE (cmd.font-family.format): +# Do not localize $fontFamily +cmd.font-family-other.format = Other ($fontFamily)… +cmd.font-family-other.label = O&ther… + +cmd.font-size.helpUsage = [<font-size>] +cmd.font-size.help = Sets or views the font size being used on the current view. Omit <font-size> to see the current font size. The size value is specified in points (pt). The value |default| will use your global font size, and the values |bigger| and |smaller| increase or reduce the size by a fixed amount each time. +cmd.font-size-bigger.label = Make Text &Bigger +cmd.font-size-bigger.key = accel + +cmd.font-size-bigger2.key = accel = +cmd.font-size-smaller.label = Make Text &Smaller +cmd.font-size-smaller.key = accel - +cmd.font-size-default.label = Default Si&ze +cmd.font-size-small.label = Sma&ll +cmd.font-size-medium.label = &Medium +cmd.font-size-large.label = Lar&ge +# LOCALIZATION NOTE (cmd.font-size-other.format): +# Do not localize $fontSize +cmd.font-size-other.format = Other ($fontSize pt)… +cmd.font-size-other.label = &Other… + +cmd.goto-startup.label = Open Auto-connect +cmd.goto-startup.help = Open all of your configured auto-connect URLs. + +cmd.goto-url.label = Open Link +# LOCALIZATION NOTE (cmd.goto-url.format): +# Do not localize $label +cmd.goto-url.format = $label +cmd.goto-url-newwin.label = Open Link in New Window +cmd.goto-url-newtab.label = Open Link in New Tab + +cmd.header.label = Header +cmd.header.key = accel shift H +cmd.header.help = Toggles visibility of the header bar. + +cmd.help.helpUsage = [<pattern>] +cmd.help.help = Displays help on all commands matching <pattern>, if <pattern> is not given, displays help on all commands. + +cmd.hide-view.label = &Hide Tab +cmd.hide-view.helpUsage = [<view>] +cmd.hide-view.help = Drop the current view's icon from the tab strip, but save its contents. The icon will reappear the next time there is activity on the view. + +cmd.homepage.label = ChatZilla Homepage + +cmd.hop.label = Give Half-operator Status +cmd.hop.helpUsage = <nickname> [<...>] +cmd.hop.help = Gives half-operator status to <nickname> on current channel. Requires operator status. + +# LOCALIZATION NOTE (cmd.reconnect.format): +# Do not localize $networkName +cmd.reconnect.format = Reconnect To $networkName +cmd.reconnect.label = &Reconnect +cmd.reconnect.helpUsage = [<reason>] +cmd.reconnect.help = Reconnects to the network represented by the active view when the command is executed providing the reason <reason> when disconnecting, or the default reason if <reason> is not specified. + +cmd.reconnect-all.label = &Reconnect To All Networks +cmd.reconnect-all.helpUsage = [<reason>] +cmd.reconnect-all.help = Reconnects to all networks providing the reason <reason> when disconnecting, or the default reason if <reason> is not specified. + +cmd.toggle-ui.helpUsage = <thing> +cmd.toggle-ui.help = Toggles the visibility of various pieces of the user interface. <thing> must be one of: tabstrip, userlist, header, status. + +cmd.rtl.help = Switches text direction to Right-to-Left. +cmd.ltr.help = Switches text direction to Left-to-Right. +cmd.irtl.help = Switches input area direction to Right-to-Left. +cmd.iltr.help = Switches input area direction to Left-to-Right. + +cmd.toggle-text-dir.label = S&witch Text Direction +cmd.toggle-text-dir.key = accel shift X + +cmd.toggle-usort.label = Sort Users By Mode +cmd.toggle-ccm.label = Collapse Co&nsecutive Messages +cmd.toggle-copy.label = Copy &Important Messages +cmd.toggle-umode.label = Show Mode as Symbol +cmd.toggle-timestamps.label = Show &Timestamps + +cmd.unban.label = Un-ban +# LOCALIZATION NOTE (cmd.unban.format): +# Do not localize $channelName +cmd.unban.format = Un-ban from $channelName +cmd.unban.helpUsage = <nickname> +cmd.unban.help = Removes the ban on a single user, or removes a specific ban mask from the channel's ban list. + +cmd.unexcept.helpUsage = <nickname> +cmd.unexcept.help = Removes a channel ban exception. + +cmd.user.helpUsage = [<username> <description>] +cmd.user.help = Sets your username to <username> and your description (``Real Name'') to <description>. Equivalent to using the |name| and |desc| command. The new name and description will be used the next time you connect to the network. You can use this command without parameters to show the current username and description. + +cmd.userlist.label = User List +cmd.userlist.key = accel shift L +cmd.userlist.help = Toggles the visibility of the user list. + +cmd.identify.helpUsage = [<password>] +cmd.identify.help = Identify with nickname services on the current server. If <password> is not provided, you will be asked to enter the password in a prompt with a masked textfield (so nobody will be able to read it when you type it). + +cmd.ignore.helpUsage = [<mask>] +cmd.ignore.help = Add someone to your ignore list for the current network. A nickname will suffice for <mask>, but you can also use a hostmask. With no parameters, it shows a list of all currently ignored users. + +cmd.install-plugin.helpUsage = [<url> [<name>]] +cmd.install-plugin.help = Installs a ChatZilla plugin for you. +cmd.install-plugin.label = &Install Plugin… + +cmd.invite.helpUsage = <nickname> [<channel-name>] +cmd.invite.help = Invites <nickname> to <channel-name> or current channel if not supplied. Requires operator status if +i is set. + +cmd.j.helpUsage = [<channel-name> [<key>]] +cmd.j.help = This command is an alias for /join. + +cmd.join.label = &Join Channel… +cmd.join.key = accel J +cmd.join.helpUsage = [<channel-name> [<key>]] +cmd.join.help = Joins the global (name starts with #), local (name starts with &), or modeless (name starts with a +) channel named <channel-name>. If no prefix is given, # is assumed. Provides the key <key> if specified. + +cmd.join-charset.helpUsage = [<channel-name> <charset> [<key>]] +cmd.join-charset.help = Joins the global (name starts with #), local (name starts with &), or modeless (name starts with a +) channel named <channel-name>. Messages will be encoded and decoded according to the character encoding specified by <charset>. The <charset> parameter is independent of the default character encoding, which can be selected with the /charset command. If no prefix is given, # is assumed. Provides the key <key> if specified. + +# LOCALIZATION NOTE (cmd.kick.format): +# Do not localize $channelName +cmd.kick.format = Kick from $channelName +cmd.kick.label = Kick +cmd.kick.helpUsage = <nickname> [<reason>] +cmd.kick.help = Kicks <nickname> off the current channel. Requires operator status. + +# LOCALIZATION NOTE (cmd.kick-ban.format): +# Do not localize $channelName +cmd.kick-ban.format = Kickban from $channelName +cmd.kick-ban.label = Kickban +cmd.kick-ban.helpUsage = <nickname> [<reason>] +cmd.kick-ban.help = Bans *!username@hostmask from the current channel, then kicks them off. Requires operator status. + +cmd.knock.helpUsage = <channel-name> [<reason>] +cmd.knock.help = Requests an invitation from the specified channel with optional reason. This command is not supported by all servers. + +# LOCALIZATION NOTE (cmd.label-user.format): +# Do not localize $nickname +cmd.label-user.format = «$nickname» +cmd.label-user.label = <unknown> + +# LOCALIZATION NOTE (cmd.label-user-multi.format): +# Do not localize $userCount +cmd.label-user-multi.format = «$userCount users» +cmd.label-user-multi.label = <unknown> + +# LOCALIZATION NOTE (cmd.leave.format): +# Do not localize $channelName +cmd.leave.format = Leave $channelName +cmd.leave.label = &Leave +cmd.leave.helpUsage = [<channel-name>] [<reason>] +cmd.leave.help = Leaves the current channel. Use /delete to force the view to go away, losing its contents, or /hide to temporarily hide it, preserving its contents. Many servers do not support the optional <reason> parameter. Your preferences are used to determine whether to delete the tab. If you are dispatching this command from a script, you may override this behaviour with the <delete-when-done> parameter. + +cmd.marker.help = Scrolls to the last read message marker. If the marker is not visible, this is equivalent to using ``marker-set''. +cmd.marker-clear.help = Hides the last read message marker. +cmd.marker-set.help = Moves the last read message marker to the last line of the view and scrolls to it. + +cmd.links.help = Displays the "links" to the current server. This is a list of the other servers in the network which are directly connected to the one you are connected to. + +cmd.list.helpUsage = [<channel-name>] +cmd.list.help = Lists channel name, user count, and topic information for the network/server you are attached to. If you omit the optional channel argument, all channels will be listed. On large networks, the server may disconnect you for asking for a complete list. + +cmd.list-plugins.helpUsage = [<plugin>] +cmd.list-plugins.help = If <plugin> is not provided, this command lists information on all loaded plugins. If <plugin> is provided, only its information will be displayed. If this command is dispatched from the console, you may specify <plugin> by either the plugin id, or index. + +cmd.load.helpUsage = <url> +cmd.load.help = Executes the contents of the url specified by <url>. See also: The |initialScripts| pref. + +cmd.reload-plugin.helpUsage = <plugin> +cmd.reload-plugin.help = Reloads the plugin from the same url it was loaded from last time. This will only work if the currently loaded version of the plugin can be disabled. + +cmd.log.helpUsage = [<state>] +cmd.log.help = Turns logging on or off for the current channel. If <state> is provided and is |true|, |on|, |yes|, or |1|, logging will be turned on. Values |false|, |off|, |no| and |0| will turn logging off. Omit <state> to see the current logging state. The state will be saved in prefs, so that if logging is on when you close ChatZilla, it will resume logging the next time you join the channel. + +cmd.rlist.helpUsage = <regexp> +cmd.rlist.help = Lists channel name, user count, and topic information for the network/server you are attached to, filtered by the regular expression. + +cmd.map.help = Similar to /links, but provides a graphical "Network Map" of the IRC network. Mainly used for routing purposes. + +cmd.match-users.helpUsage = <mask> +cmd.match-users.help = Shows a list of all users whose hostmask matches <mask>. + +cmd.me.helpUsage = <action> +cmd.me.help = Sends the text <action> to the channel as a statement in the third person. Try it and see! + +cmd.motd.help = Displays the "Message of the Day", which usually contains information about the network and current server, as well as any usage policies. + +cmd.mode.helpUsage = [<target>] [<modestr> [<param> [<...>]]] +cmd.mode.help = Changes the channel or user mode of <target> using <modestr> and any subsequent <param> if added. When used from a channel view, <target> may be omitted. For a list of modes you may use, see http://irchelp.org. + +cmd.motif.helpUsage = [<motif>] +cmd.motif.help = Sets the default CSS file used for the message tabs. <motif> can be a URL to a .css file, or the shortcut "dark" or "light". See the ChatZilla homepage at <http://www.mozilla.org/projects/rt-messaging/chatzilla/> for more information on how to style ChatZilla. See also |network-motif|, |channel-motif|, |user-motif|. + +cmd.motif-dark.label = Dar&k Motif +cmd.motif-light.label = &Light Motif + +cmd.msg.helpUsage = <nickname> <message> +cmd.msg.help = Sends the private message <message> to <nickname>. + +cmd.name.helpUsage = [<username>] +cmd.name.help = Changes the username displayed before your hostmask if the server you're connecting to allows it. Some servers will only trust the username reply from the ident service. You must specify this *before* connecting to the network. If you omit <username>, the current username will be shown. + +cmd.names.helpUsage = [<channel-name>] +cmd.names.help = Lists the users in a channel. + +cmd.network.helpUsage = <network-name> +cmd.network.help = Sets the current network to <network-name> + +cmd.networks.help = Lists all available networks as clickable links. + +cmd.network-motif.helpUsage = [<motif> [<network>]] +cmd.network-motif.help = Sets the CSS file used for the message tab for the network <network>. <motif> can be a URL to a .css file, or the shortcut "dark" or "light". If <motif> is a minus ('-') character, the motif will revert to the global motif. If <network> is not provided, the current network is assumed. See the ChatZilla homepage at <http://www.mozilla.org/projects/rt-messaging/chatzilla/> for more information on how to style ChatZilla. See also |motif|. + +cmd.network-pref.helpUsage = [<pref-name> [<pref-value>]] +cmd.network-pref.help = Sets the value of the preference named <pref-name> to the value of <pref-value> on the current network. If <pref-value> is not provided, the current value of <pref-name> will be displayed. If both <pref-name> and <pref-value> are omitted, all preferences will be displayed. If <pref-value> is a minus ('-') character, then the preference will revert back to its default value. + +cmd.nick.label = Change nickname… +cmd.nick.helpUsage = [<nickname>] +cmd.nick.help = Changes your nickname. If |nickname| is omited, a prompt is shown. + +cmd.notify.helpUsage = [<nickname> [<...>]] +cmd.notify.help = With no parameters, /notify shows you the online/offline status of all the users on your notify list. If one or more <nickname> parameters are supplied, the nickname(s) will be added to your notify list if they are not yet on it, or removed from it if they are. + +cmd.notice.helpUsage = <nickname> <message> +cmd.notice.help = Sends the notice <message> to <nickname>. + +cmd.op.label = Give Operator Status +cmd.op.helpUsage = <nickname> [<...>] +cmd.op.help = Gives operator status to <nickname> on current channel. Requires operator status. + +cmd.open-at-startup.helpUsage = [<toggle>] +cmd.open-at-startup.help = Used to add the current view to the list of views that will be automatically opened at startup. If <toggle> is not provided, the status of the current view will be displayed. <toggle> can be one of: yes, on, true, 1, no, off, false, 0, or toggle, to toggle the current state. + +cmd.oper.helpUsage = <opername> [<password>] +cmd.oper.help = Requests IRC Operator status from the current server. If <password> is not provided, you will be asked to enter the password in a prompt with a masked textfield (so nobody will be able to read it when you type it). + +cmd.print.label = &Print… +cmd.print.key = accel P +cmd.print.help = Opens the print dialog for the current view. + +cmd.save.label = Save View &As… +cmd.save.key = accel S +cmd.save.helpUsage = [<filename> [<savetype>]] +cmd.save.help = Save the current view as file <filename>. If <filename> is omitted, a Save As… dialog will be shown. <savetype> can be either |complete|, |htmlonly| or |text|. If it is omitted, it is deduced from the file extension. Files with the extension .html, .xhtml, .xhtm or .htm will be saved as complete views, .txt files as text files. Any other extensions will throw an error if <savetype> is not provided. + +cmd.say.helpUsage = <message> +cmd.say.help = Sends a message to the current view. This command is used automatically by ChatZilla when you type text that does not begin with the "/" character. + +cmd.stats.helpUsage = [<params>] +cmd.stats.help = Request server statistics. Use this command with no parameters to get a server-specific list of available parameters for use with this command. + +cmd.time.helpUsage = [<nickname>] +cmd.time.help = Asks <nickname> what time it is on their machine. Their IRC client may or may not show them that you've asked for this information. ChatZilla currently does not. If you do not specify <nickname>, ChatZilla will ask the server for the time it is on the server. +cmd.time.label = Get Local Time + +cmd.timestamps.helpUsage = [<toggle>] +cmd.timestamps.help = Sets the visibility of timestamps in the current view. If <toggle> is provided and is |true|, |on|, |yes|, or |1|, timestamps will be turned on. Values |false|, |off|, |no| and |0| will turn timestamps off, and |toggle| will toggle the state. Omit <toggle> to see the current state. + +# LOCALIZATION NOTE (cmd.toggle-oas.format): +# Do not localize $viewType +cmd.toggle-oas.format = Open This $viewType at Startup +cmd.toggle-oas.label = Open at &Startup + +cmd.pass.helpUsage = <password> +cmd.pass.help = Sends a password to the server for use when connecting to password-protected servers. + +cmd.ping.helpUsage = <nickname> +cmd.ping.help = Ping takes its name from the technique of measuring distance with sonar. In IRC, it is used to measure the time it takes to send a message to someone, and receive a response. Specify a channel to ping everyone in that channel. Some IRC clients will display ping requests to the user. ChatZilla does not. +cmd.ping.label = Ping User + +cmd.plugin-pref.helpUsage = <plugin> [<pref-name> [<pref-value>]] +cmd.plugin-pref.help = Sets the value of the plugin's preference named <pref-name> to the value of <pref-value>. If <pref-value> is not provided, the current value of <pref-name> will be displayed. If both <pref-name> and <pref-value> are omitted, all preferences for <plugin> will be displayed. If <pref-value> is a minus ('-') character, then the preference will revert back to its default value. + +cmd.pref.helpUsage = [<pref-name> [<pref-value>]] +cmd.pref.help = Sets the value of the preference named <pref-name> to the value of <pref-value>. If <pref-value> is not provided, the current value of <pref-name> will be displayed. If both <pref-name> and <pref-value> are omitted, all preferences will be displayed. If <pref-value> is a minus ('-') character, then the preference will revert back to its default value. + +cmd.query.label = Open Private Chat +cmd.query.helpUsage = <nickname> [<message>] +cmd.query.help = Opens a one-on-one chat with <nickname>. If <message> is supplied, it is sent as the initial private message to <nickname>. + +cmd.quit.label = &Quit ChatZilla +cmd.quit.helpUsage = [<reason>] +cmd.quit.help = Quit ChatZilla. + +cmd.quote.helpUsage = <irc-command> +cmd.quote.help = Sends a raw command to the IRC server, not a good idea if you don't know what you're doing. see IRC RFC1459 <http://www.irchelp.org/irchelp/rfc1459.html> for complete details. + +cmd.rejoin.helpUsage = [<reason>] +cmd.rejoin.help = Rejoins the channel displayed in the current view. Only works from a channel view. +# LOCALIZATION NOTE (cmd.rejoin.format): +# Do not localize $channelName +cmd.rejoin.format = Rejoin $channelName +cmd.rejoin.label = Rejoin + +cmd.rename.helpUsage = [<label>] +cmd.rename.help = Change the label of the current tab to <label>. +cmd.rename.label = Rename Tab… + +cmd.server.helpUsage = <hostname> [<port> [<password>]] +cmd.server.help = Connects to server <hostname> on <port>, or 6667 if <port> is not specified. Provides the password <password> if specified. If you are already connected, the view for <hostname> is made current. If that view has been deleted, it is recreated. + +cmd.sslserver.helpUsage = <hostname> [<port> [<password>]] +cmd.sslserver.help = Connects to server using SSL <hostname> on <port>, or 6697 if <port> is not specified. Provides the password <password> if specified. If you are already connected, the view for <hostname> is made current. If that view has been deleted, it is recreated. + +cmd.squery.helpUsage = <service> [<commands>] +cmd.squery.help = Sends the commands <commands> to the service <service>. + +cmd.stalk.helpUsage = [<text>] +cmd.stalk.help = Add <text> to list of words for which you would like to see alerts. Whenever a person with a nickname matching <text> speaks, or someone says a phrase containing <text>, your ChatZilla window will become active (on some operating systems) and its taskbar icon will flash (on some operating systems.) If <text> is omitted the list of stalk words is displayed. + +cmd.status.help = Shows status information for the current view. + +cmd.statusbar.label = Status Bar +cmd.statusbar.key = accel shift S +cmd.statusbar.help = Toggles the visibility of the status bar. + +cmd.supports.help = Lists the capabilities of the current server, as reported by the 005 numeric. + +cmd.testdisplay.help = Displays a sample text. Used to preview styles. + +cmd.topic.helpUsage = [<new-topic>] +cmd.topic.help = If <new-topic> is specified and you are a chanop, or the channel is not in 'private topic' mode (+t), the topic will be changed to <new-topic>. If <new-topic> is *not* specified, the current topic will be displayed. + +cmd.tabstrip.label = Tab Strip +cmd.tabstrip.key = accel shift S +cmd.tabstrip.help = Toggles the visibility of the channel tab strip. + +cmd.unalias.helpUsage = <alias-name> +cmd.unalias.help = Removes the named alias. + +cmd.unignore.helpUsage = <mask> +cmd.unignore.help = Removes someone from your ignore list for the current network. A nickname will suffice for <mask>, but you can also use a hostmask. + +cmd.uninstall-plugin.helpUsage = <plugin> +cmd.uninstall-plugin.help = Uninstalls a ChatZilla plugin for you. + +cmd.unstalk.helpUsage = <text> +cmd.unstalk.help = Remove word from list of terms for which you would like to see alerts. + +cmd.urls.helpUsage = [<number>] +cmd.urls.help = Displays the last few URLs seen by ChatZilla. Specify <number> to change how many it displays, or omit to display the default 10. + +cmd.userhost.helpUsage = <nickname> [<...>] +cmd.userhost.help = Requests the hostmask of every <nickname> given. + +cmd.userip.helpUsage = <nickname> [<...>] +cmd.userip.help = Requests the IP-address of every <nickname> given. + +cmd.disable-plugin.helpUsage = <plugin> +cmd.disable-plugin.help = This command calls the plugin's disablePlugin function, if it exists. There are no guarantees that the plugin will properly disable itself. + +cmd.usermode.helpUsage = [<new-mode>] +cmd.usermode.help = Changes or displays the current user mode. + +cmd.user-motif.helpUsage = [<motif> [<user>]] +cmd.user-motif.help = Sets the CSS file used for the message tab for the user <user>. <motif> can be a URL to a .css file, or the shortcut "dark" or "light". If <motif> is a minus ('-') character, the motif will revert to the network motif. If <user> is not provided, the current user is assumed. See the ChatZilla homepage at <http://www.mozilla.org/projects/rt-messaging/chatzilla/> for more information on how to style ChatZilla. See also |motif|. + +cmd.user-pref.helpUsage = [<pref-name> [<pref-value>]] +cmd.user-pref.help = Sets the value of the preference named <pref-name> to the value of <pref-value> on the current user. If <pref-value> is not provided, the current value of <pref-name> will be displayed. If both <pref-name> and <pref-value> are omitted, all preferences will be displayed. If <pref-value> is a minus ('-') character, then the preference will revert back to its default value. + +cmd.websearch.help = Runs a web search for the currently-selected text. +cmd.websearch.helpUsage = <selected-text> +# LOCALIZATION NOTE (cmd.websearch.format): +# Do not localize $selectedText +cmd.websearch.format = Search the web for "$selectedText"" +cmd.websearch.label = Search the web + +cmd.version.label = Get Version Information +cmd.version.helpUsage = [<nickname>] +cmd.version.help = Asks <nickname> what irc client they're running. Their IRC client may or may not show them that you've asked for this information. ChatZilla currently does not. If you do not specify <nickname>, ChatZilla will ask the server for the version of the IRCserver software it is running. + +cmd.voice.label = Give Voice Status +cmd.voice.helpUsage = <nickname> [<...>] +cmd.voice.help = Gives voice status to <nickname> on current channel. Requires operator (or half-operator) status. + +cmd.who.helpUsage = <rest> +cmd.who.help = List users who have name, host, or description information matching <rest>. + +cmd.whois.label = Who is +cmd.whois.helpUsage = <nickname> [<...>] +cmd.whois.help = Displays information about the user <nickname>, including 'real name', server connected to, idle time, and signon time. Note that some servers will lie about the idle time. The correct idle time can usually be obtained by using |wii| instead of |whois|. + +cmd.wii.helpUsage = <nickname> [<...>] +cmd.wii.help = Displays the same information as |whois|, but asks the server to include the user's real idle time. + +cmd.whowas.label = Who was +cmd.whowas.helpUsage = <nickname> [<limit>] +cmd.whowas.help = Displays the last known information about the user <nickname>, including 'real name', for a user that has left the server. + +## dispatch-related error messages ## +msg.err.internal.dispatch = Internal error dispatching command ``%1$S''. +msg.err.internal.hook = Internal error processing hook ``%1$S''. +msg.err.invalid.param = Invalid value for parameter %1$S (%2$S). +msg.err.disabled = Sorry, ``%1$S'' is currently disabled. +msg.err.notimplemented = Sorry, ``%1$S'' has not been implemented. +msg.err.required.param = Missing required parameter %1$S. +msg.err.ambigcommand = Ambiguous command, ``%1$S'', %2$S commands match [%3$S]. +msg.err.required.nr.param = Missing %1$S parameters. This alias requires at least %2$S parameters. +msg.err.max.dispatch.depth = Reached max dispatch depth while attempting to dispatch ``%1$S''. + +## ChatZilla error messages ## +msg.err.invalid.regex = Invalid Regular Expression. For help with regular expressions, see http://en.wikipedia.org/wiki/Regular_expression#Syntax. +msg.err.invalid.pref = Invalid value for preference %1$S (%2$S). +msg.err.invalid.file = Invalid file <%1$S> renamed to <%2$S>. +msg.err.failure = Operation Failed: %1$S. +msg.err.scriptload = Error loading subscript from <%1$S>. +msg.err.pluginapi.noid = Plugin <%1$S> does not have an id. +msg.err.pluginapi.faultyid = Plugin <%1$S> does not have a valid id. Plugin ids may only contain alphanumeric characters, underscores (_) and dashes (-). +msg.err.pluginapi.noenable = Plugin <%1$S> does not have an enable() method. +msg.err.pluginapi.nodisable = Plugin <%1$S> does not have a disable() method. +msg.err.invalid.scheme = Invalid scheme in url <%1$S>. +msg.err.item.not.found = Startup script item <%1$S> does not exist or is inaccessible. +msg.err.unknown.pref = The preference ``%1$S'' is not known to ChatZilla. +msg.err.unknown.network = The network ``%S'' is not known to ChatZilla. +msg.err.unknown.channel = The channel ``%S'' is not known to ChatZilla. +msg.err.unknown.user = The user ``%S'' is not known to ChatZilla. +msg.err.unknown.command = The command ``%S'' is not known to ChatZilla. +msg.err.unknown.stalk = Not stalking %S. +msg.err.unknown.motif = The motif ``%S'' is not known to ChatZilla. +msg.err.invalid.charset = Invalid character encoding mode ``%S''. +msg.err.improper.view = ``%S'' cannot be used from this view. +msg.err.not.connected = Not connected. +msg.err.last.view = Cannot delete last view. +msg.err.last.view.hide = Cannot hide last view. +msg.err.bad.ircurl = Invalid IRC URL ``%S''. +msg.err.need.network = Command ``%1$S'' must be run in the context of a network. +msg.err.need.server = Command ``%1$S'' must be run in the context of an attached server. +msg.err.need.channel = Command ``%1$S'' must be run in the context of a channel. +msg.err.need.user = Command ``%1$S'' must be run in the context of a user. +msg.err.need.recip = Command ``%1$S'' must be run in the context of a user or a channel. +msg.err.no.default = Please do not just type into this tab, use an actual command instead. +msg.err.no.match = No match for ``%S''. +msg.err.no.socket = Error creating socket. +msg.err.no.secure = The network ``%S'' has no secure servers defined. +msg.err.cancelled = Connection process canceled. +msg.err.offline = &brandShortName; is in ``offline mode''. No network connections can be made in this mode. +msg.err.badalias = Malformed alias: %S" +msg.err.no.ctcp.cmd = %S is not a valid CTCP function for this client +msg.err.no.ctcp.help = %S does not have any help information +msg.err.unable.to.print = The current view does not support printing. +msg.err.unsupported.command = The server does not support the ``%S'' command. +msg.err.invalid.mode = The mode string you entered (``%S'') is invalid. A valid mode string consists of one or more sequences of a + or - followed by one or more alphabetical characters. +msg.err.away.save = Saving the list of away messages failed (%S). +msg.err.inputhistory.not.writable = Unable to save input history to ``%S''. +msg.err.urls.not.writable = Unable to save URL log to ``%S''. +msg.err.invalid.url = ``%S'' is not a valid url nor an alias for a url, and therefore could not be loaded. +msg.err.no.channel = When running the ``%S'' command, you should either provide a channel name, or run the command in the context of a channel. +msg.err.no.idleservice = ChatZilla can't determine when you're away in your version of &brandShortName;. The auto-away feature will now be disabled. + +msg.warn.pac.loading = The automatic proxy configuration file has not loaded yet; ChatZilla will retry shortly. + +# Ask for nick pass if not explicitly given in the command: +msg.need.identify.password = Please enter the Nickname Services password for this nickname. + +# Ask for oper pass if not explicitly given in the command: +msg.need.oper.password = Please enter a password for obtaining IRC Operator privileges. + +# Better IRC error messages +msg.irc.381 = You are now an IRC Operator. +msg.irc.401 = The nickname ``%S'' does not exist. +msg.irc.402 = The server ``%S'' does not exist. +msg.irc.403 = The channel ``%S'' does not exist. +msg.irc.421 = The command ``%S'' is not known to the server. +msg.irc.464 = Incorrect password, please try again with the correct password. +msg.irc.464.login = Please specify your password using the "/pass" command to continue connecting. +msg.irc.471 = This channel has reached its set capacity; you cannot join it. +msg.irc.473 = This channel is invite-only. You must have an invite from an existing member of the channel to join. +msg.irc.474 = You are banned from this channel. +msg.irc.475 = This channel needs a key. You must provide the correct key to join the channel. See "/help join" for details on joining a channel with a key. +msg.irc.476 = You provided a channel mask which the server considers to be invalid. +msg.irc.477 = This channel requires that you have registered and identified yourself with the network's nickname registration services (e.g. NickServ). Please see the documentation of this network's nickname registration services that should be found in the MOTD (/motd to display it). +msg.irc.491 = Only few of mere mortals may try to enter the twilight zone (your host did not match any configured 'O-lines'). + +# This is an extended version that is only used if the server support /knock. +msg.irc.471.knock = %S You might be able to use "/knock %S" to ask the channel operators to invite you in. [[Knock][Asks the channel operators to let you in][%S]] +msg.irc.473.knock = %S Use "/knock %S" to ask the channel operators to invite you in. [[Knock][Asks the channel operators to let you in][%S]] +msg.irc.475.knock = %S You might be able to use "/knock %S" to ask the channel operators to invite you in. [[Knock][Asks the channel operators to let you in][%S]] + +msg.val.on = on +msg.val.off = off + +msg.plugin.enabled = Plugin ``%S'' is now enabled. +msg.plugin.disabled = Plugin ``%S'' is now disabled. +msg.plugin.uninstalled = Plugin ``%S'' has been uninstalled. + +msg.leave.inputbox = There is nothing to tab-complete. Use F6 to cycle through the user list, input box and the chat output. + +## formatting ## +msg.fmt.usage = "%1$S %2$S" +msg.fmt.jsexception = "%1$S: %2$S @ <%3$S> %4$S" +# 1: error number, 2: error text, 3: file name, 4: line number, 5: function name +# 1: pref name 2: value +msg.fmt.pref = Preference ``%1$S'' is ``%2$S''. +msg.fmt.netpref = Network preference ``%1$S'' is ``%2$S''. +msg.fmt.chanpref = Channel preference ``%1$S'' is ``%2$S''. +msg.fmt.userpref = User preference ``%1$S'' is ``%2$S''. +msg.fmt.pluginpref = Plugin preference ``%1$S'' is ``%2$S''. + +msg.fmt.plugin1 = Plugin at index %S, loaded from <%S>. +msg.fmt.plugin2 = id: %S, version: %S, enabled: %S, status: %S. +msg.fmt.plugin3 = Description: %S. + +msg.fmt.usercount = "%S, %S@, %S%%, %S+" +msg.fmt.alias = "%S = %S" +msg.fmt.seconds = "%S seconds + +msg.fmt.matchlist = "%S matches for ``%S'': [%S] +msg.fmt.ctcpreply = CTCP %S reply ``%S'' from %S" +msg.fmt.ctcprecv = CTCP %S request (%S) from %S" +msg.fmt.chanlist = "%S %S %S" +msg.fmt.logged.on = "%S is logged in as %S" + +# 1: local short date/time, 2: nick info +msg.fmt.status = "%S %S" + +msg.unknown = <unknown> +msg.none = <none> +msg.na = <n/a> + +msg.always = always +msg.and = and +msg.primary = primary +msg.secondary = secondary +msg.you = you +msg.network = Network +msg.server = Server +msg.channel = Channel +msg.user = User +msg.client = Client +msg.view = View +msg.tab = Tab +msg.loading = Loading +msg.error = Error +msg.here = here +msg.gone = gone +msg.connecting = Connecting +msg.connected = Connected +msg.disconnected = Disconnected + +msg.days = "%S days +msg.hours = "%S hours +msg.minutes = "%S minutes +msg.seconds = "%S seconds +msg.day = 1 day +msg.hour = 1 hour +msg.minute = 1 minute +msg.second = 1 second + + +msg.rsp.hello = [HELLO] +msg.rsp.help = [HELP] +msg.rsp.usage = [USAGE] +msg.rsp.error = [ERROR] +msg.rsp.warn = [WARNING] +msg.rsp.info = [INFO] +msg.rsp.evin = [EVAL-IN] +msg.rsp.evout = [EVAL-OUT] +msg.rsp.disconnect = [QUIT] + +# For these menu labels, too, an accesskey may be specified using a .accesskey +# string, or by prefixing the desired letter with "&" in the label. +# The accesskey string should have the form: msg.mnu.<menuname>.accesskey +msg.mnu.chatzilla = &ChatZilla +msg.mnu.irc = &IRC +msg.mnu.edit = &Edit +msg.mnu.help = &Help +msg.mnu.view = &View +msg.mnu.views = &Views +msg.mnu.motifs = Co&lor Scheme +msg.mnu.opcommands = &Operator Commands +msg.mnu.usercommands = &User Commands +msg.mnu.fonts = &Font Family and Size + +msg.client.name = *client* +msg.cant.disable = Unable to disable plugin %S. +msg.cant.enable = Unable to enable plugin %S. +msg.is.disabled = Plugin %S is already disabled. +msg.is.enabled = Plugin %S is already enabled. +msg.no.help = Help not available. +msg.no.cmdmatch = No commands match ``%1$S''. +msg.no.plugins = There are no plugins loaded. +msg.cmdmatch = Commands matching ``%1$S'' are [%2$S]. +msg.default.alias.help = This command is an alias for |%1$S|. +msg.extra.params = Extra parameters ``%1$S'' ignored. +msg.version.reply = ChatZilla %S [%S] +msg.source.reply = http://chatzilla.hacksrus.com/ +msg.nothing.to.cancel = No connection or /list in progress, nothing to cancel. +msg.cancelling = Cancelling connection to ``%S''… +msg.cancelling.list = Cancelling /list request… +msg.current.charset = Using ``%S'' as default character encoding. +msg.current.charset.view = Using ``%S'' as character encoding for this view. +msg.current.css = Using <%S> as default motif. +msg.current.css.net = Using <%S> as default motif for this network. +msg.current.css.chan = Using <%S> as motif for this channel. +msg.current.css.user = Using <%S> as motif for this user. +msg.no.dynamic.style = Sorry, but your version of &brandShortName; doesn't support styling the entire application with a motif. This functionality will now be disabled. +msg.subscript.loaded = Subscript <%1$S> loaded with result ``%2$S''. +msg.user.info = Default nickname, ``%S'', username ``%S'', and description ``%S''. +msg.connection.info = "%S: User %S connected via %S:%S (%S server). +msg.server.info = "%S: Connected for %S, last ping: %S, server roundtrip (lag): %S seconds. +msg.connect.via = Connected via %S" +msg.user.mode = User mode for %S is now %S" +msg.not.connected = "%S: Not connected. +msg.insecure.server = Your connection to the server ``%S'' is not secure. +msg.secure.connection = Signed by %S" +msg.security.info = Displays security information about the current connection +msg.going.offline = &brandShortName; is trying to go into offline mode. This will disconnect you from ALL the networks and channels you're connected to. +msg.really.go.offline = Go Offline +msg.dont.go.offline = Don't Go Offline +msg.offlinestate.offline = You are offline. Click the icon to go online. +msg.offlinestate.online = You are online. Click the icon to go offline. +msg.member = Member +msg.operator = Operator member +msg.voiced = Voiced member +msg.voiceop = Operator and voiced member +msg.no.mode = no mode +msg.topic.info = "%S, %S: Topic, ``%S'' +msg.notopic.info = "%S, %S: No topic. +msg.channel.info = "%S: %S of %S (%S) <%S> +msg.channel.details = "%S/%S: %S users total, %S operators, %S voiced. +msg.nonmember = "%S: No longer a member of %S. +msg.end.status = End of status. +msg.networks.heada = Available networks are [ +msg.networks.headb2 = ]. [[Edit][Edit the list of available networks.][%S]] +msg.messages.cleared = Messages Cleared. +msg.match.unchecked = (%S users were not checked) +msg.matching.nicks = The following users matched your query: %S. %S +msg.no.matching.nicks = There were no users matching your query. %S +msg.commands.header = Type /help <command-name> for information about a specific command. +msg.matching.commands = Currently implemented commands matching the pattern ``%S'' are [%S].\nType /help <command-name> for information about a specific command. +msg.all.commands = Currently implemented commands are [%S]. +msg.help.intro = Help is available from many places:\n - |/commands| lists all the built-in commands in ChatZilla. Use |/help <command-name>| to get help on individual commands.\n - The IRC Help website <http://www.irchelp.org/> provides introductory material for new IRC users. \n - The ChatZilla website <http://chatzilla.hacksrus.com/> provides more information about IRC and ChatZilla, including the ChatZilla FAQ <http://chatzilla.hacksrus.com/faq>, which answers many common questions about using ChatZilla. +msg.about.version = "%S [[Details][Opens the about dialog for more details][%S]] +msg.about.homepage = Please visit the ChatZilla homepage at <http://chatzilla.hacksrus.com/> for more information. +msg.newnick.you = YOU are now known as %S +msg.newnick.notyou = "%S is now known as %S +msg.view.hidden = "%S (hidden) + +msg.sts.upgrade = "Upgrade policy in effect, switching to secure port %S." +msg.sts.upgrade.new = "A new security policy is in effect for this network. ChatZilla will automatically switch to TLS the next time you connect. [[Reconnect now][Reconnect to %S][%S]]" + +msg.localeurl.homepage = http://chatzilla.hacksrus.com/ +msg.localeurl.faq = http://chatzilla.hacksrus.com/faq/ + +msg.no.notify.list = Your notify list is empty. +msg.notify.addone = "%S has been added to your notify list. +msg.notify.addsome = "%S have been added to your notify list. +msg.notify.delone = "%S has been removed from your notify list. +msg.notify.delsome = "%S have been removed from your notify list. +msg.notify.list = You are watching %S. +msg.notify.full = Your notify list is full. + +msg.not.an.alias = No such alias: %S. +msg.alias.removed = Removed alias: %S. +msg.alias.created = Created alias: %S = %S. +msg.no.aliases = No aliases are defined. + +msg.no.stalk.list = No stalking victims. +msg.stalk.list = Currently stalking [%S]. +msg.stalk.add = Now stalking %S. +msg.stalk.del = No longer stalking %S. +msg.stalking.already = Already stalking %S. + +msg.status = Status +msg.title.net.on = User %S on ``%S'' (%S:%S) +msg.title.net.off = User %S, not connected to network ``%S'' +msg.title.nonick = <unregistered-user> +msg.title.no.topic = No Topic +msg.title.no.mode = No Mode +msg.title.channel = "%S on %S (%S): %S" +msg.title.user = Conversation with %S %S" +msg.title.dccchat = DCC Conversation with %S" +msg.title.dccfile.send = "%S%% of ``%S'' sent to %S" +msg.title.dccfile.get = "%S%% of ``%S'' received from %S" +msg.title.unknown = ChatZilla! +msg.title.activity = "%S -- Activity [%S] + +msg.output.url = URL +msg.output.knownnets = Known Networks +msg.output.connnets = Connected Networks +msg.output.notconn = Not Connected +msg.output.lag = Lag +msg.output.mode = Mode +msg.output.users = Users +msg.output.topic = Topic +msg.output.via = Connected via +msg.output.to = Connected to +msg.output.file = File +msg.output.progress = Progress +msg.output.cancel = Cancel + +msg.logging.off = Logging is off. +msg.logging.on = Logging is on. Log output is going to file <%S>. +msg.logfile.closed = Logfile closed. +msg.logfile.error = Unable to open file <%S>. Logging disabled. +msg.logfile.opened = Now logging to <%S>. +msg.logfile.closing = Closing log file <%S>. +msg.logfile.write.error = Unable to write to file <%S>. Logging disabled. +msg.logging.icon.off = Logging is off. Click the icon to start logging this view. +msg.logging.icon.on = Logging is on. Click the icon to stop logging this view. + +msg.alert.icon.off = Message notifications are off. Click the icon to start showing notifications for new messages. +msg.alert.icon.on = Message notifications are on. Click the icon to stop showing notifications for new messages. + +msg.already.connected = You are already connected to ``%S''. +msg.enter.nick = Please select a nickname +msg.network.connecting = Attempting to connect to ``%S''. Use /cancel to abort. + +msg.jumpto.button = [[%1$S][Jump to this message in %1$S][%2$S]] +msg.jumpto.err.nochan = ``%S'' is no longer open. +msg.jumpto.err.noanchor = The anchor cannot be found. + +msg.banlist.item = "%S banned %S from %S on %S. +msg.banlist.button = [[Remove][Remove this ban][%S]] +msg.banlist.end = End of %S ban list. +msg.exceptlist.item = "%S excepted %S from bans in %S on %S. +msg.exceptlist.button = [[Remove][Remove this ban exception][%S]] +msg.exceptlist.end = End of %S exception list. + +msg.batch.netsplit.start = Netsplit (%S %S) +msg.batch.netsplit.end = End of netsplit. +msg.batch.netjoin.start = Net reconnect (%S %S) +msg.batch.netjoin.end = End of net reconnect. +msg.batch.chathistory.start = Chat history for %S +msg.batch.chathistory.end = End of chat history. +msg.batch.unknown.start = Batch %S (%S) +msg.batch.unknown.end = End of batch. + +msg.channel.needops = You need to be an operator in %S to do that. + +msg.ctcphelp.clientinfo = CLIENTINFO gives information on available CTCP commands +msg.ctcphelp.action = ACTION performs an action at the user +msg.ctcphelp.time = TIME gives the local date and time for the client +msg.ctcphelp.version = VERSION returns the client's version +msg.ctcphelp.source = SOURCE returns an address where you can obtain the client +msg.ctcphelp.os = OS returns the client's host's operating system and version +msg.ctcphelp.host = HOST returns the client's host application name and version +msg.ctcphelp.ping = PING echos the parameter passed to the client +msg.ctcphelp.dcc = DCC requests a direct client connection + +# DCC CHAT messages. +msg.dccchat.sent.request = Sent DCC Chat offer to ``%S'' from YOU (%S:%S) %S. +msg.dccchat.got.request = Got DCC Chat offer from ``%S'' (%S:%S) %S. +msg.dccchat.accepting = Auto-accepting DCC Chat offer from ``%S'' (%S:%S) in %S seconds %S. +msg.dccchat.accepting.now = Auto-accepting DCC Chat offer from ``%S'' (%S:%S). +msg.dccchat.accepted = Accepted DCC Chat with ``%S'' (%S:%S). +msg.dccchat.declined = Declined DCC Chat with ``%S'' (%S:%S). +msg.dccchat.aborted = Aborted DCC Chat with ``%S'' (%S:%S). +msg.dccchat.failed = Failed DCC Chat with ``%S'' (%S:%S). +msg.dccchat.opened = DCC Chat with ``%S'' (%S:%S) connected. +msg.dccchat.closed = DCC Chat with ``%S'' (%S:%S) disconnected. + +# DCC FILE messages. +msg.dccfile.sent.request = Sent DCC File Transfer offer to ``%S'' from YOU (%S:%S) of ``%S'' (%S) %S. +msg.dccfile.got.request = Got DCC File Transfer offer from ``%S'' (%S:%S) of ``%S'' (%S) %S. +msg.dccfile.accepting = Auto-accepting DCC File Transfer offer from ``%S'' (%S:%S) of ``%S'' (%S) in %S seconds %S. +msg.dccfile.accepting.now = Auto-accepting DCC File Transfer offer from ``%S'' (%S:%S) of ``%S'' (%S). +# 1 = file, 2 = to/from, 3 = nick, 4 = IP, 5 = port. +msg.dccfile.accepted = Accepted DCC File Transfer of ``%S'' %S ``%S'' (%S:%S). +msg.dccfile.declined = Declined DCC File Transfer of ``%S'' %S ``%S'' (%S:%S). +msg.dccfile.aborted = Aborted DCC File Transfer of ``%S'' %S ``%S'' (%S:%S). +msg.dccfile.failed = Failed DCC File Transfer of ``%S'' %S ``%S'' (%S:%S). +msg.dccfile.opened = DCC File Transfer of ``%S'' %S ``%S'' (%S:%S) started. +msg.dccfile.closed.sent = DCC File Transfer of ``%S'' %S ``%S'' (%S:%S) finished. +# 6 = path, 7 = command for opening the folder +msg.dccfile.closed.saved = DCC File Transfer of ``%S'' %S ``%S'' (%S:%S) finished. File saved to ``%S''. [[Open Containing Folder][Open the folder containing the downloaded file][%S]] +msg.dccfile.closed.saved.mac = DCC File Transfer of ``%S'' %S ``%S'' (%S:%S) finished. File saved to ``%S''. [[Show In Finder][Show the folder containing the file in Finder][%S]] + +# 1 = percent, 2 = current pos, 3 = total size, 4 = speed. +msg.dccfile.progress = %S%% complete, %S of %S, %S. +msg.dccfile.send = Pick file to send +msg.dccfile.save.to = Save incoming file (%S) +msg.dccfile.err.notfound = The file specified could not be found. +msg.dccfile.err.notafile = The path specified is not a normal file. +msg.dccfile.err.notreadable = The file specified cannot be read. + +# General DCC messages. +msg.dcc.pending.matches = "%S pending incoming DCC offers matched. +msg.dcc.accepted.matches = "%S DCC connections matched. +msg.dcc.matches.help = You must specify enough of the user's nickname to uniquely identify the request, or include the request type and even the filename if necessary. + +msg.dcc.not.enabled = DCC is disabled. If you need DCC functionality, you may turn it on from the Preferences window. +msg.dcc.err.nouser = Must specify |nickname| or run the command from a query view. +msg.dcc.err.accept.time = You cannot use the short form of |/dcc-accept| within the first 10 seconds of receiving a DCC request. +msg.dcc.err.notdcc = Must specify |nickname| or run the command from a DCC view. + +# /dcc-list words and phrases. +msg.dcclist.dir.in = incoming +msg.dcclist.dir.out = outgoing (offer) +msg.dcclist.to = to +msg.dcclist.from = from +## Params: index, state, direction (incoming/outgoing), DCC type, direction (to/from), user (ip:port), commands. +msg.dcclist.line = %S: %S %S DCC %S %S %S (%S:%S) %S +## Params: waiting, running, done. +msg.dcclist.summary = DCC sessions: %S pending, %S connected, %S failed. + +msg.dccaccept.disabled = Currently not auto-accepting DCC on this network. +msg.dccaccept.list = Currently auto-accepting DCC on this network from [%S]. +msg.dccaccept.add = Now auto-accepting DCC on this network from %S. +msg.dccaccept.del = No longer auto-accepting DCC on this network from %S. +msg.dccaccept.adderr = You are already auto-accepting DCC on this network from %S. +msg.dccaccept.delerr = %S not found on your DCC auto-accept list for this network. + +msg.dcc.command.accept = [[Accept][Accept this DCC offer][%S]] +msg.dcc.command.decline = [[Decline][Decline (refuse) this DCC offer][%S]] +msg.dcc.command.cancel = [[Cancel][Cancels this DCC offer][%S]] +msg.dcc.command.close = [[Close][Close (disconnect) this DCC offer][%S]] + +# DCC state names. +msg.dcc.state.abort = Aborted +msg.dcc.state.request = Requested +msg.dcc.state.accept = Accepted +msg.dcc.state.connect = Connected +# 1 = percent, 2 = current pos, 3 = total size, 4 = speed. +msg.dcc.state.connectPro = Connected (%S%% complete, %S of %S, %S) +msg.dcc.state.disconnect = Done +msg.dcc.state.decline = Declined +msg.dcc.state.fail = Failed + +# SI general format (1$ == number, 2$ == scale suffix). +msg.si.size = %1$S %2$S +msg.si.speed = %1$S %2$S + +# SI suffixes for sizes. +msg.si.size.0 = B +msg.si.size.1 = KiB +msg.si.size.2 = MiB +msg.si.size.3 = GiB +msg.si.size.4 = TiB +msg.si.size.5 = PiB +msg.si.size.6 = EiB + +# SI suffixes for speeds. +msg.si.speed.0 = B/s +msg.si.speed.1 = KiB/s +msg.si.speed.2 = MiB/s +msg.si.speed.3 = GiB/s +msg.si.speed.4 = TiB/s +msg.si.speed.5 = PiB/s +msg.si.speed.6 = EiB/s + +msg.ident.error = Error enabling Ident Server: %S" + +msg.host.password = Enter a password for the server %S: +msg.sasl.password = Enter a password for SASL authentication with username %S: +msg.url.key = Enter key for url %S: + +msg.startup.added = <%1$S> will now open at startup. +msg.startup.removed = <%1$S> will no longer open at startup. +msg.startup.exists = <%1$S> is currently opened at startup. +msg.startup.notfound = <%1$S> is not currently opened at startup. + +msg.collapse.button = [[%S][%S][toggle-group %S]] +msg.collapse.test = Sample collapsible message group. +msg.collapse.show = Show +msg.collapse.hide = Hide +msg.collapse.showtitle = Show message group +msg.collapse.hidetitle = Hide message group + +msg.test.hello = Sample HELLO message, <http://testurl.com/foo.html>. +msg.test.info = Sample INFO message, <http://testurl.com/foo.html>. +msg.test.error = Sample ERROR message, <http://testurl.com/foo.html>. +msg.test.help = Sample HELP message, <http://testurl.com/foo.html>. +msg.test.usage = Sample USAGE message, <http://testurl.com/foo.html>. +msg.test.status = Sample STATUS message, <http://testurl.com/foo.html>. +msg.test.privmsg = Normal message from %S to %S, <http://testurl.com/foo.html>. +msg.test.action = Action message from %S to %S, <http://testurl.com/foo.html>. +msg.test.notice = Notice message from %S to %S, <http://testurl.com/foo.html>. +msg.test.url = Sample URL <http://www.mozilla.org> message. +msg.test.styles = Sample text styles *bold*, _underline_, /italic/, |teletype| message. +msg.test.emoticon = Sample emoticon :) :( :~( :0 :/ :P :| (* message. +msg.test.rheet = Sample Rheeeeeeeeeet! message. +msg.test.topic = Sample Topic message, <http://testurl.com/foo.html>. +msg.test.join = Sample Join message, <http://testurl.com/foo.html>. +msg.test.part = Sample Part message, <http://testurl.com/foo.html>. +msg.test.kick = Sample Kick message, <http://testurl.com/foo.html>. +msg.test.quit = Sample Quit message, <http://testurl.com/foo.html>. +msg.test.stalk = "%S : Sample /stalk match, <http://testurl.com/foo.html>. +msg.test.ctlchr = Sample control char >%01<\\1 -- >%05<\\5 -- >%10<\\10 +msg.test.color = Sample color %033c%034o%034l%033o%033r%034%20%036t%036e%032s%034t%0f message. +msg.test.quote = Sample ``double quote'' message. + +msg.welcome = Welcome to ChatZilla…\nBelow is a short selection of information to help you get started using ChatZilla. +msg.welcome.url = Because ChatZilla was launched from a URL, the target has been opened for you. You can find it on the tab bar, next to this view. +msg.tabdnd.drop = Would you like to use the file ``%S'' as your new motif? +msg.default.status = Welcome to ChatZilla! + +msg.closing = Disconnecting from IRC. Click close again to exit now. +msg.confirm.quit = You are still connected to some networks, are you sure you want to quit ChatZilla?\nConfirming will close the window, and disconnect from all the networks and channels you're connected to. +msg.quit.anyway = &Quit Anyway +msg.dont.quit = &Don't Quit +msg.warn.on.exit = Warn me when quitting while still connected + +msg.login.confirm = Do you want to save the password for ``%S''? +msg.login.prompt = Prompt to save passwords +msg.login.save = Yes +msg.login.dont = No +msg.login.added = Saved password for ``%S''. +msg.login.updated = Changed password for ``%S''. +msg.login.err.unknown.type = Unknown login type ``%S''. + +msg.whois.name = "%S <%S@%S> ``%S'' +msg.whois.channels = "%S: member of %S" +msg.whois.server = "%S: attached to %S ``%S'' +msg.whois.idle = "%S: idle for %S (on since %S) +msg.whois.away = "%S: away with message ``%S'' +msg.whois.end = End of WHOIS information for %S. + +msg.ignore.list.1 = Currently not ignoring anyone. +msg.ignore.list.2 = Currently ignoring [%S]. +msg.ignore.add = You are now ignoring %S. +msg.ignore.adderr = You are already ignoring %S. +msg.ignore.del = You are no longer ignoring %S. +msg.ignore.delerr = "%S not found in your ignore list. + +msg.you.invite = You have invited %S to %S. +msg.invite.you = "%S (%S@%S) has invited you to [[%S][Accept invitation to channel %S][goto-url %S]]. +msg.invite.someone = "%S has invited %S to %S". + +msg.nick.in.use = The nickname ``%S'' is already in use, use the /nick command to pick a new one. +msg.retry.nick = The nickname ``%S'' is already in use, trying ``%S''. +msg.nick.prompt = Enter a nickname to use: + +msg.tab.name.prompt = Enter a label for this tab: + +msg.list.rerouted = List reply will appear on the ``%S'' view. +msg.list.end = Displayed %S of %S channels. +msg.list.chancount = This server has %S channels. Listing them all will probably take a long time, and may lead to ChatZilla becoming unresponsive or being disconnected by the server. [[List Channels][List all channels][%S]] + +msg.who.end = End of WHO results for ``%S'', %S user(s) found. +msg.who.match = User %S, (%S@%S) ``%S'' (%S), member of %S, is connected to <irc://%S/>, %S hop(s). + +msg.connection.attempt = Connecting to %S (%S)… [[Cancel][Cancel connecting to %S][%S]] +msg.connection.refused = Connection to %S (%S) refused. [[Help][Get more information about this error online][faq connection.refused]] +msg.connection.abort.offline = The connection to %S (%S) was aborted because you went into offline mode. +msg.connection.abort.unknown = The connection to %S (%S) was aborted with error %S. +msg.connection.timeout = Connection to %S (%S) timed out. [[Help][Get more information about this error online][faq connection.timeout]] +msg.unknown.host = Unknown host ``%S'' connecting to %S (%S). [[Help][Get more information about this error online][faq connection.unknown.host]] +msg.invalid.cert = "%S has an invalid security certificate. If you trust this server, [[add an exception][Opens the dialog to add a security certificate exception][%S]]. +msg.connection.closed = Connection to %S (%S) closed. [[Help][Get more information about this error online][faq connection.closed]] +msg.connection.reset = Connection to %S (%S) reset. [[Help][Get more information about this error online][faq connection.reset]] +msg.connection.interrupt = Connection to %S (%S) was interrupted. +msg.connection.quit = Disconnected from %S (%S). [[Reconnect][Reconnect to %S][%S]] +msg.close.status = Connection to %S (%S) closed with status %S. + +msg.proxy.connection.refused = The proxy server you configured is refusing the connection. +msg.unknown.proxy.host = Unknown proxy host connecting to %S (%S). + +# In these messages, the first replacement string is a connection error from above. +msg.connection.exhausted = "%S Connection attempts exhausted, giving up. +msg.reconnecting.in = "%S Reconnecting in %S. [[Cancel][Cancel reconnecting to %S][%S]] +msg.reconnecting.in.left = "%S %S attempts left, reconnecting in %S. [[Cancel][Cancel reconnecting to %S][%S]] +msg.reconnecting.in.left1 = "%S 1 attempt left, reconnecting in %S. [[Cancel][Cancel reconnecting to %S][%S]] + +msg.reconnecting = Reconnecting… +msg.confirm.disconnect.all = Are you sure you want to disconnect from ALL networks? +msg.no.connected.nets = You are not connected to any networks. +msg.no.reconnectable.nets = There are no networks to reconnect to. + +msg.ping.reply = Ping reply from %S in %S. +msg.ping.reply.invalid = Malformed ping reply from %S. +msg.prefix.response = "%S, your result is, + +msg.topic.changed = "%S has changed the topic to ``%S'' +msg.topic = Topic for %S is ``%S'' +msg.no.topic = No topic for channel %S" +msg.topic.date = Topic for %S was set by %S on %S" + +msg.you.joined = YOU (%S) have joined %S" +msg.someone.joined = "%S (%S@%S) has joined %S" +msg.you.left = YOU (%S) have left %S" +msg.you.left.reason = YOU (%S) have left %S (%S) +msg.someone.left = "%S has left %S" +msg.someone.left.reason = "%S has left %S (%S) +msg.youre.gone = YOU (%S) have been booted from %S by %S (%S) +msg.someone.gone = "%S was booted from %S by %S (%S) + +msg.mode.all = Mode for %S is %S" +msg.mode.changed = Mode %S by %S" + +msg.away.on = You are now marked as away (%S). Click the nickname button or use the |/back| command to return from being away. +msg.idle.away.on = You have automatically been marked as away (%S) after %S minutes of inactivity. +msg.away.off = You are no longer marked as away. +msg.away.prompt = Enter an away message to use: +msg.away.default = I'm not here right now. +msg.away.idle.default = I'm not here right now. + +msg.you.quit = YOU (%S) have left %S (%S) +msg.someone.quit = "%S has left %S (%S) + +msg.unknown.ctcp = Unknown CTCP %S (%S) from %S" + +msg.fonts.family.fmt = Font family is ``%S'' +msg.fonts.family.pick = Enter the font family you wish to use: +msg.fonts.size.fmt = Font size is %Spt +msg.fonts.size.default = Font size is default +msg.fonts.size.pick = Enter the font size you wish to use. Note: Non-numeric inputs will result in the default size. + +msg.supports.chanTypes = Supported channel types: %S" +msg.supports.chanModesA = Supported channel modes (A: lists): %S" +msg.supports.chanModesB = Supported channel modes (B: param): %S" +msg.supports.chanModesC = Supported channel modes (C: on-param): %S" +msg.supports.chanModesD = Supported channel modes (D: boolean): %S" +msg.supports.userMode = "%S (%S) +msg.supports.userModes = Supported channel user modes: %S" +msg.supports.flagsOn = This server DOES support: %S" +msg.supports.flagsOff = This server DOESN'T support: %S" +msg.supports.miscOption = "%S=%S" +msg.supports.miscOptions = Server settings/limits: %S" +msg.supports.caps = Supported capabilities: %S" +msg.supports.capsOn = Enabled capabilities: %S" + +msg.caps.on = Capability %S enabled. +msg.caps.off = Capability %S disabled. +msg.caps.error = Capability %S is invalid. + +msg.conf.mode.on = Conference Mode has been enabled for this view; joins, leaves, quits and nickname changes will be hidden. +msg.conf.mode.stayon = Conference Mode is enabled for this view; joins, leaves, quits and nickname changes are hidden. +msg.conf.mode.off = Conference Mode has been disabled for this view; joins, leaves, quits and nickname changes will be shown. + +# Join Network/Channel dialog +msg.cd.updated = Network's channel list cached on %S" +msg.cd.updated.format = %e %B %Y +msg.cd.updated.never = Network's channel list not cached +msg.cd.create = <create new channel> +msg.cd.filtering = Filtered %S of %S channels… +msg.cd.showing = Showing %S of %S channels. +msg.cd.wait.list = Waiting for current list operation to finish… +msg.cd.fetching = Fetching channel list… +msg.cd.fetched = Fetched %S channels… +msg.cd.error.list = There was an error loading the channel list. +msg.cd.loaded = Loaded %S channels… + + +msg.urls.none = There are no stored URLs. +msg.urls.header = Listing the %S most recent stored URLs (most recent first): +msg.urls.item = URL %S: %S" + +msg.save.completeview = View, Complete +msg.save.htmlonlyview = View, HTML Only +msg.save.plaintextview = View, Plain Text +msg.save.files.folder = %S_files +msg.save.dialogtitle = Save View ``%S'' As… +msg.save.err.no.ext = You must specify either a normal extension or <savetype>. Nothing was saved. +msg.save.err.invalid.path = The path ``%S'' is not a valid path or URL to save to. Only local file paths and file:/// urls are accepted. +msg.save.err.invalid.ext = The extension ``%S'' cannot be used without supplying a <savetype>. Use either |.xhtml|, |.xhtm|, |.html|, |.htm| or |.txt| as a file extension, or supply <savetype>. +msg.save.err.invalid.savetype = The savetype ``%S'' is not a valid type of file to save to. Use either |complete|, |htmlonly| or |text|. +msg.save.err.failed = Saving the view ``%1$S'' to ``%2$S'' failed:\n ``%3$S'' +msg.save.fileexists = The file ``%S'' already exists.\n Click OK to overwrite it, click Cancel to keep the original file. +msg.save.successful = The view ``%1$S'' has been saved to <%2$S>. + +# Plugin installation +msg.install.plugin.err.download = An error occurred downloading the plugin: %S" +msg.install.plugin.err.remove.temp = An error occurred removing the temporary files: %S" +msg.install.plugin.err.no.name = Unable to pick a plugin name from the source, please specify one instead. +msg.install.plugin.err.protocol = Sorry, the source location has been specified with an unknown protocol. Only 'file', 'http' and 'https' are supported. +msg.install.plugin.err.install.to = Unable to find a suitable install location (initialScripts). Please fix the initialScripts preference, for example by resetting it, using the command: |/pref initialScripts - |. Careful, this will remove any plugin you installed elsewhere from this list! +msg.install.plugin.err.check.sd = An error occurred checking the source and destination: %S" +msg.install.plugin.err.many.initjs = This ChatZilla plugin appears to have multiple 'init.js' files and thus cannot be installed. +msg.install.plugin.err.mixed.base = This ChatZilla plugin has a base path for 'init.js' which is not used for all other files. This plugin will probably not function in this state. +msg.install.plugin.err.already.inst = This ChatZilla plugin appears to already be installed. +msg.install.plugin.err.extract = An error occurred extracting the compressed source: %S" +msg.install.plugin.err.installing = An error occurred installing the source: %S" +msg.install.plugin.err.format = The source specified is not a format understood by the plugin installer. +msg.install.plugin.err.removing = An error occurred loading or enabling the plugin. Removing the plugin. +msg.install.plugin.err.spec.name = The plugin name must be specified! + +msg.install.plugin.select.source = Select a script to install… + +msg.install.plugin.warn.name = Changed plugin name for install from '%S' to '%S' to match source code. +msg.install.plugin.downloading = Downloading plugin from '%S'… +msg.install.plugin.installing = Installing from '%S' to '%S'… +msg.install.plugin.done = Done. ChatZilla plugin '%S' installed! + +# Munger +munger.mailto=Mailto +munger.link=URLs +munger.channel-link=IRC channel +munger.bugzilla-link=Bugzilla link +munger.face=Face +munger.ear=Ear +munger.quote=Double Quotes +munger.rheet=Rheet +munger.bold=Bold +munger.italic=Italic +munger.talkback-link=Talkback link +munger.teletype=Teletype +munger.underline=Underline +munger.ctrl-char=Control Chars + + +# Date/Time representations for strftime + +datetime.day.long = Sunday^Monday^Tuesday^Wednesday^Thursday^Friday^Saturday +datetime.day.short = Sun^Mon^Tue^Wed^Thu^Fri^Sat +datetime.month.long = January^February^March^April^May^June^July^August^September^October^November^December +datetime.month.short = Jan^Feb^Mar^Apr^May^Jun^Jul^Aug^Sep^Oct^Nov^Dec + +datetime.uam = AM +datetime.lam = am +datetime.upm = PM +datetime.lpm = pm + +datetime.presets.lc = %Y-%m-%d %H:%M:%S +datetime.presets.lr = %I:%M:%S %p +datetime.presets.lx = %Y-%m-%d +datetime.presets.ux = %H:%M:%S + + +# Messages used in config.js, part of the pref window. + +# We only allow one pref window open at once, this occurs when a 2nd is opened. +msg.prefs.alreadyOpen = ChatZilla's preferences are already open; you may not open a second copy. + +msg.prefs.err.save = An exception occurred trying to save the preferences: %S. + +msg.prefs.browse = Browse… +msg.prefs.browse.title = ChatZilla Browse +msg.prefs.move.up = Move up +msg.prefs.move.down = Move down +msg.prefs.add = Add… +msg.prefs.edit = Edit +msg.prefs.delete = Delete + +msg.prefs.list.add = Enter item to add: +msg.prefs.list.edit = Edit the item as needed: +msg.prefs.list.delete = Are you sure you want to remove the item ``%S''? + +msg.prefs.object.delete = Are you sure you want to remove the object ``%S'' and all its preferences? +msg.prefs.object.reset = Are you sure you want to reset all the preferences for ``%S'' to their defaults? + +# First is for adding prefix/suffix to the overall header, and the next three +# are for the different objects (first is network name, second is channel/user +# name). +msg.prefs.fmt.header = "%S" +msg.prefs.fmt.display.network = Network %S" +msg.prefs.fmt.display.channel = Network %S, channel %S" +msg.prefs.fmt.display.user = Network %S, user %S" + +# Name for "global" object. +msg.prefs.global = Global Settings + +# Localized names for all the prefs and tooltip "help" messages. +# NOTE: "Bugzilla", "ChatZilla" and "mIRC" are product names. +pref.activityFlashDelay.label = Activity flash delay +pref.activityFlashDelay.help = When a tab that has had activity gets more activity, the tab is flashed. This preference is the length of the flash in milliseconds: 0 disables it. +pref.alert.globalEnabled.label = Globally enabled +pref.alert.globalEnabled.help = When enabled, all alerts configured may be shown. When disabled, no alerts will be shown. Provides nothing more than a global toggle. +pref.alert.enabled.label = Enabled +pref.alert.enabled.help = When enabled, popups are shown for this view. +pref.alert.nonFocusedOnly.label = Only when window not active +pref.alert.nonFocusedOnly.help = When enabled, all message notifications are suppressed when the window is active. Otherwise, message notifications for non-active views will be shown. Unchecking is suggested for channel moderators or for low traffic channels. +pref.alert.channel.event.label = Alert for Channel Event +pref.alert.channel.event.help = Shows message notifications for joins, parts, kicks, usermodes, and any other system messages. Suggested for channel moderators or for low traffic channels. +pref.alert.channel.chat.label = Alert for Channel Chat +pref.alert.channel.chat.help = Show message notifications for normal chat messages. It may be annoying for high traffic channels. Suggested for moderators or for low traffic channels. +pref.alert.channel.stalk.label = Alert for Channel Stalk +pref.alert.channel.stalk.help = Shows message notifications for messages containing stalk words. +pref.alert.user.chat.label = Alert for User Chat +pref.alert.user.chat.help = Shows message notifications for private messages. +pref.aliases.label = Command aliases +pref.aliases.help = Allows you to make shortcuts to various commands or sequences of commands. Each item is of the form "<name> = <command-list>". The command-list is a list of commands (without the leading "/") along with their parameters, each separated by ";". The name of the alias will automatically be turned into a command when ChatZilla starts. +pref.autoAwayCap.label = Auto away-check user limit +pref.autoAwayCap.help = ChatZilla automatically checks which users are here and which are away in each channel you are a member of, however, this causes significant lag on larger channels. Any channel bigger than this limit won't be checked. +pref.autoAwayPeriod.label = Auto away-check period length +pref.autoAwayPeriod.help = ChatZilla automatically checks which users are here and which are away in each channel you are a member of. This specifies how many minutes should pass between checks. Checking is automatically disabled if the server supports the "away-notify" extension. +pref.autoMarker.label = Automatically display the last read message marker +pref.autoMarker.help = ChatZilla can automatically mark the last read message on views you are not looking at, or if the window is in the background. If set to false the line marker can still be set manually. +pref.autoRejoin.label = Rejoin when kicked +pref.autoRejoin.help = If this is turned on, ChatZilla will try (only once) to rejoin a channel you got kicked from. Note, some channels dislike auto-rejoin, and will ban you, so be careful. +pref.away.label = Away status +pref.away.help = +pref.awayIdleTime.label = Auto-away timeout +pref.awayIdleTime.help = After how many minutes of inactivity ChatZilla will set your status to "away". Set to 0 to disable it. +pref.awayIdleMsg.label = Auto-away message +pref.awayIdleMsg.help = The away message ChatZilla will use when you go away. +pref.awayNick.label = Nickname (away) +pref.awayNick.help = This nickname will automatically be used when you mark yourself away, if different from 'Nickname'. You may leave this blank to not change nickname when going away. +pref.bugKeyword.label = Bug Keywords +pref.bugKeyword.help = You can define multiple issue tracker keywords as a regular expression perhaps by separating them with "|" e.g. bug|issue|case|ticket +pref.bugURL.label = Bugzilla URL +pref.bugURL.help = The URL used for links to bugs. "%s" is replaced with the bug number or alias. The text "bug " followed by a number or "#" and a 1-20 letter word (bug alias) will get turned into a link using this URL. +pref.bugURL.comment.label = Bugzilla URL for Comments +pref.bugURL.comment.help = The URL or suffix used for links to specific comments within bugs. With a full URL, "%1$s" is replaced with the bug number or alias and "%2$s" with the comment number, respectively. With a suffix, "%s" is replaced with the comment number. The text "bug " followed by a number or "#" and a 1-20 letter word (bug alias) followed by " comment " followed by another number will get turned into a link using this URL or suffix. +pref.charset.label = Character encoding +pref.charset.help = For multiple clients to correctly read messages with non-ASCII characters on IRC, they need to use the same character encoding. +pref.collapseMsgs.label = Collapse messages +pref.collapseMsgs.help = Makes multiple messages from one person only show their nickname against the first, which can look cleaner than having the nickname repeated. +pref.collapseActions.label = Collapse actions when collapsing messages +pref.collapseActions.help = Makes multiple actions from one person only show their nickname against the first, which can look cleaner than having the nickname repeated. +pref.conference.limit.label = Conference mode limit +pref.conference.limit.help = When the number of users in a channel sufficiently exceeds this limit, ChatZilla switches the channel into "conference mode", during which JOIN, PART, QUIT and NICK messages for other users are hidden. When the user count drops sufficiently below the limit, normal operation is resumed automatically. Setting this to 0 will never use conference mode, likewise setting this to 1 will always use it. +pref.connectTries.label = Connection attempts +pref.connectTries.help = The number of times ChatZilla attempts to connect to a server or network. Set to -1 for unlimited attempts. +pref.copyMessages.label = Copy important messages +pref.copyMessages.help = Any message marked as "important" will be copied to the network view. It allows you to quickly see messages that were addressed to you when you were away from the computer. +pref.dcc.enabled.label = DCC Enabled +pref.dcc.enabled.help = When disabled, no DCC-related commands will do anything, and all DCC requests from other users will be ignored. +pref.dcc.autoAccept.list.label = Auto-accept list +pref.dcc.autoAccept.list.help = List of nicknames to automatically accept DCC chat/file offers from. Hostmasks are also accepted, using "*" as a wildcard. If this list is empty, all DCC requests must be manually accepted or declined. +pref.dcc.downloadsFolder.label = Downloads folder +pref.dcc.downloadsFolder.help = Specifies the default destination for files received via DCC. +pref.dcc.listenPorts.label = Listen Ports +pref.dcc.listenPorts.help = List of ports that other users can connect to remotely. Each item may be a single port number, or a range specified as "lower-upper". Leave empty to use a random, OS-picked port. Each time you offer a DCC connection to someone, the next port listed is picked. +pref.dcc.useServerIP.label = Get local IP from server +pref.dcc.useServerIP.help = When turned on, ChatZilla will ask the server for your IP address when connecting. This allows DCC to obtain the correct IP address when operating behind a gateway or NAT-based system. +pref.debugMode.label = Debug mode +pref.debugMode.help = This preference is for debugging ChatZilla and can generate a lot of debug output (usually to the console). It is a list of letters, signifying what you want debug messages about. "c" for context menus (dumps data when opening a context menu), "d" for dispatch (dumps data when dispatching commands), and "t" for trace/hook (dumps data about hooks and the event queue processing) debugging. +pref.defaultQuitMsg.label = Default quit message +pref.defaultQuitMsg.help = Specifies a default quit message to use when one is not explicitly specified. Leave blank to use the basic ChatZilla one, which simply states what version you have. +pref.desc.label = Description +pref.desc.help = Sets the "description" (aka "real name") field shown in your /whois information. It is commonly used to include one's real name, but you are not required to enter anything. +pref.deleteOnPart.label = Delete channel views on part +pref.deleteOnPart.help = Causes /leave and /part to also close the channel view. +pref.displayHeader.label = Show header +pref.displayHeader.help = Display the chat header on this view. This contains information like the URL of the current view, and the topic and modes for a channel view. +pref.font.family.label = Font Family +pref.font.family.help = Selects the font in which ChatZilla will display messages. The value "default" will use your global font family; "serif", "sans-serif" and "monospace" will use your global font settings; other values will be treated as font names. +pref.font.size.label = Font Size (pt) +pref.font.size.help = Selects the integer font size you want ChatZilla to display messages with. The value 0 will use your global font size, and other integer values will be interpreted as the size in points (pt). +pref.guessCommands.label = Guess unknown commands +pref.guessCommands.help = If you enter a command (starts with "/") that ChatZilla doesn't understand, then it can try "guessing" by sending the command to the server. You can turn this off if you don't want ChatZilla to try this. +pref.hasPrefs.label = Object has prefs +pref.hasPrefs.help = Indicates the object has preferences saved. Never shown in preferences window. :) +pref.identd.enabled.label = Enable Identification Server during connection process +pref.identd.enabled.help = Allows ChatZilla to connect to servers that require an ident response. +pref.initialURLs.label = Locations +pref.initialURLs.help = A list of locations (irc: and ircs: URLs) to which ChatZilla should connect when starting. These will not be processed if ChatZilla was started by clicking on a hyperlink. +pref.initialScripts.label = Script files +pref.initialScripts.help = A list of script files (file: URLs) for ChatZilla to load when it starts. URLs may be relative to the profile directory. If a URL points to a directory, "init.js" from that directory and each subdirectory is loaded. +pref.inputSpellcheck.label = Spellcheck the inputbox +pref.inputSpellcheck.help = Whether or not the inputbox will be spellchecked. Only works on recent &brandShortName; builds. +pref.log.label = Log this view +pref.log.help = Makes ChatZilla log this view. The log file is usually stored in your profile, which can be overridden with "Profile path" (for the base path) or "Log file name" for a specific view's log. +pref.logFileName.label = Log file name +pref.logFileName.help = The log file used for this view. If the view is currently open and logging, changing this option won't take effect until the next time it starts logging. +pref.logFile.client.label = Log file for client +pref.logFile.client.help = Specifies the name of the log file for the client view. This is appended to the 'log folder' to create a full path. +pref.logFile.network.label = Log file for networks +pref.logFile.network.help = Specifies the name of the log file for network views. This is appended to the 'log folder' to create a full path. +pref.logFile.channel.label = Log file for channels +pref.logFile.channel.help = Specifies the name of the log file for channel views. This is appended to the 'log folder' to create a full path. +pref.logFile.user.label = Log file for users +pref.logFile.user.help = Specifies the name of the log file for user/query views. This is appended to the 'log folder' to create a full path. +pref.logFile.dccuser.label = Log file for DCC +pref.logFile.dccuser.help = Specifies the name of the log file for DCC chat/file views. This is appended to the 'log folder' to create a full path. +pref.logFolder.label = Log folder +pref.logFolder.help = Specifies the base location for all logs. The various "Log file for" preferences specify the exact names for the different types of log file. +pref.login.promptToSave.label = Prompt to save passwords +pref.login.promptToSave.help = Enable this preference if you wish to save passwords with the password manager. Passwords that can be saved include server passwords, channel keys, nickname identification passwords and oper passwords. Passwords can only be added via this prompt. +pref.motif.dark.label = Dark motif +pref.motif.dark.help = The dark motif selectable from the View > Color Scheme menu. +pref.motif.light.label = Light motif +pref.motif.light.help = The light motif selectable from the View > Color Scheme menu. +pref.motif.current.label = Current motif +pref.motif.current.help = The currently selected motif file. A Motif is a CSS file that describes how do display the chat view, and can be used to customize the display. +pref.multiline.label = Multiline input mode +pref.multiline.help = Sets whether ChatZilla is using the multiline input box or the single-line one. +pref.munger.bold.label = Bold +pref.munger.bold.help = Makes ChatZilla display text between astersks (e.g. *bold*) in an actually bold face. +pref.munger.bugzilla-link.label = Bugzilla links +pref.munger.bugzilla-link.help = Makes ChatZilla hyperlink "bug <number>" to the specified bug, using the "Bugzilla URL" as the base link. +pref.munger.channel-link.label = Channel links +pref.munger.channel-link.help = Makes ChatZilla convert "#channel" into a link to the channel. +pref.munger.colorCodes.label = mIRC colors +pref.munger.colorCodes.help = Enables the display of colors on the chat text, as well as other mIRC codes (bold and underline). When disabled, ChatZilla will simply hide mIRC codes. +pref.munger.ctrl-char.label = Control characters +pref.munger.ctrl-char.help = Makes ChatZilla display control characters it doesn't understand. +pref.munger.face.label = Faces (emoticons) +pref.munger.face.help = Makes ChatZilla display images for common smilies, such as :-) and ;-). +pref.munger.italic.label = Italic +pref.munger.italic.help = Makes ChatZilla italicize text between forward slashes. (e.g. /italic/) +pref.munger.link.label = Web links +pref.munger.link.help = Makes ChatZilla hyperlink text that looks like a URL. +pref.munger.mailto.label = Mail links +pref.munger.mailto.help = Makes ChatZilla hyperlink text that looks like an e-mail address. +pref.munger.quote.label = Neater quotes +pref.munger.quote.help = Makes ChatZilla replace `` with \u201C and '' with \u201D. +pref.munger.rheet.label = Rheet +pref.munger.rheet.help = Makes ChatZilla hyperlink "rheet": a very Mozilla.org-centric feature. +pref.munger.talkback-link.label = Talkback links +pref.munger.talkback-link.help = Makes ChatZilla hyperlink "TB<numbers><character>" to the specified talkback stack trace. +pref.munger.teletype.label = Teletype +pref.munger.teletype.help = Makes ChatZilla display |teletype| actually in teletype (a fixed-width font). +pref.munger.underline.label = Underline +pref.munger.underline.help = Makes ChatZilla underline text between underscores. (e.g. _underline_) +pref.munger.word-hyphenator.label = Hyphenate long words +pref.munger.word-hyphenator.help = Makes ChatZilla insert "hyphenation points" into long words and URLs so they can wrap to the screen size. +pref.newTabLimit.label = Max auto-created views +pref.newTabLimit.help = Sets the number of views (such as query views) that may be created automatically by ChatZilla. Once the limit is reached, private messages will show up on the current view instead. Set this to 0 for unlimited or 1 to disallow all auto-created views. +pref.nickCompleteStr.label = Nickname completion string +pref.nickCompleteStr.help = This string is appended to a nickname when tab-completed at the start of a line. +pref.nickname.label = Nickname +pref.nickname.help = This is the name seen by everyone else when on IRC. You can use anything you like, but it can't contain particularly "weird" characters, so keep to alpha-numeric characters. +pref.nicknameList.label = Nickname List +pref.nicknameList.help = This is a list of nicknames you want ChatZilla to try if the one you were using happens to be already in use. Your normal nickname need not be listed. +pref.notify.aggressive.label = Aggressive notify +pref.notify.aggressive.help = When someone sends you a private message, says your nickname, or mentions one of your "stalk words", ChatZilla considers the message to be worth getting your attention. This preference sets whether it's allowed to flash the window or bring it to the front (varies by OS) in order to get your attention. +pref.notifyList.label = Notify list +pref.notifyList.help = A list of nicknames to periodically check to see if they are online or not. Every 5 minutes, ChatZilla will check this list, and inform you if anyone is now online or has gone offline. If the server supports monitor lists, this list is synchronized with the server, and you will be notified immediately of any status changes. +pref.outgoing.colorCodes.label = Enable sending color codes +pref.outgoing.colorCodes.help = Allows you to send color and other mIRC codes, such as bold and underline, using special %-sequences. When enabled, simply type "%" to see a popup of the various choices. +pref.outputWindowURL.label = Output Window +pref.outputWindowURL.help = You probably don't want to change this. The chat view loads this URL to display the actual messages, header, etc., and the file must correctly define certain items or you'll get JavaScript errors and a blank chat window! +pref.profilePath.label = Profile path +pref.profilePath.help = This is the base location for ChatZilla-related files. By default, ChatZilla loads scripts from the "scripts" subdirectory, and stores log files in the "logs" subdirectory. +pref.proxy.typeOverride.label = Proxy Type +pref.proxy.typeOverride.help = Override the normal proxy choice by specifying "http" to use your browser's HTTP Proxy or "none" to force no proxy to be used (not even the SOCKS proxy). Note that this usually only works when the browser is set to use a manual proxy configuration. +pref.reconnect.label = Reconnect when disconnected unexpectedly +pref.reconnect.help = When your connection is lost unexpectedly, ChatZilla can automatically reconnect to the server for you. +pref.sasl.plain.enabled.label = Use SASL authentication +pref.sasl.plain.enabled.help = While connecting, ChatZilla can authenticate with SASL using your username and saved password. +pref.showModeSymbols.label = Show user mode symbols +pref.showModeSymbols.help = The userlist can either show mode symbols ("@" for op, "%" for half-op, "+" for voice), or it can show colored dots (green for op, dark blue for half-op, cyan for voice, and black for normal). Turn this preference on to show mode symbols instead of colored dots. +pref.sortUsersByMode.label = Sort users by mode +pref.sortUsersByMode.help = Causes the userlist to be sorted by mode, op first, then half-op (if supported on the server), then voice, followed by everyone else. +pref.sound.enabled.label = Enabled +pref.sound.enabled.help = Tick this preference to allow sound, or untick to turn off all sounds. Provides nothing more than a global toggle. +pref.sound.overlapDelay.label = Overlap Delay +pref.sound.overlapDelay.help = Sets the period of time during which the same event will not trigger the sound to be played. For example, the default value of 2000ms (2 seconds) means if two stalk matches occur within 2 seconds of each other, only the first will cause the sound to be played. +##pref.sound.surpressActive.label = Suppress Sounds for active view +##pref.sound.surpressActive.help = Stops sounds generated by the active view from playing if ChatZilla is the active window. Sounds from other views, or when ChatZilla is not active, will always play. +pref.sound.channel.start.label = Sound for Channel Start +pref.sound.channel.start.help = +pref.sound.channel.event.label = Sound for Channel Event +pref.sound.channel.event.help = +pref.sound.channel.chat.label = Sound for Channel Chat +pref.sound.channel.chat.help = +pref.sound.channel.stalk.label = Sound for Channel Stalk +pref.sound.channel.stalk.help = +pref.sound.user.start.label = Sound for User Start +pref.sound.user.start.help = +pref.sound.user.stalk.label = Sound for User Chat +pref.sound.user.stalk.help = +pref.stalkWholeWords.label = Stalk whole words only +pref.stalkWholeWords.help = This preferences toggles ChatZilla's handling of stalk words between finding matching words, or simple substrings. For example, "ChatZilla is cool" will match the stalk word "zilla" only if this preferences is off. +pref.stalkWords.label = Stalk words +pref.stalkWords.help = A list of words that will cause a line to be marked "important" and will try to get your attention if "Aggressive notify" is turned on. +pref.upgrade-insecure.label = Enable opportunistic encryption +pref.upgrade-insecure.help = When opening an unencrypted connection to a server, ChatZilla can request that the server upgrade the existing connection to use TLS if possible. Using this option is not recommended if the server supports a dedicated TLS port or strict transport security (STS). +pref.sts.enabled.label = Enable strict transport security +pref.sts.enabled.help = When opening an unencrypted connection to a server, ChatZilla will automatically switch to to a TLS port if the server advertises an STS policy. +pref.urls.store.max.label = Max stored URLs +pref.urls.store.max.help = Sets the maximum number of URLs collected and stored by ChatZilla. The "/urls" command displays the last 10 stored, or more if you do "/urls 20", for example. +pref.userlistLeft.label = Display the userlist on the left +pref.userlistLeft.help = Display the userlist on the left. Uncheck to display the userlist on the right instead. +pref.username.label = Username +pref.username.help = Your username is used to construct your "host mask", which is a string representing you. It includes your connection's host name and this username. It is sometimes used for setting auto-op, bans, and other things specific to one person. +pref.usermode.label = Usermode +pref.usermode.help = Your usermode is an option string that is sent to the IRC network. It is composed of a plus sign ("+") followed by one or more letters, each of which represents an option. The letter "i" represents "invisible mode". When you are invisible, your nickname will not appear in channel userlists for people who are not in the channel with you. The letter "s" allows you to see server messages like nickname collisions. For a more complete list of available options, look up usermode on www.irchelp.org. +pref.warnOnClose.label = Warn me when quitting while still connected +pref.warnOnClose.help = When quitting while still connected to networks, a message appears asking you if you are sure you want to quit. Uncheck this to disable it. + +# Preference group labels # + +pref.group.general.label = General +pref.group.general.connect.label = Connection +pref.group.general.ident.label = Identification +pref.group.general.log.label = Logging +pref.group.general.palert.label = Message notifications +pref.group.global.palertconfig.label = Message notifications configuration +pref.group.appearance.label = Appearance +pref.group.appearance.misc.label = Miscellaneous +pref.group.appearance.motif.label = Motifs +pref.group.appearance.timestamps.label = Timestamps +pref.group.appearance.timestamps.help = The Format preference uses strftime replacements. For example, "%A %l:%M:%S%P" might become "Thursday 1:37:42pm". +pref.group.appearance.userlist.label = Userlist +pref.group.dcc.label = DCC +pref.group.dcc.ports.label = Ports +pref.group.dcc.autoAccept.label = Auto-accept +pref.group.munger.label = Formatting +pref.group.startup.label = Startup +pref.group.startup.initialURLs.label = Locations +pref.group.startup.initialScripts.label = Script files +pref.group.lists.label = Lists +pref.group.lists.stalkWords.label = Stalk words +pref.group.lists.aliases.label = Command aliases +pref.group.lists.notifyList.label = Notify list +pref.group.lists.nicknameList.label = Nickname List +pref.group.lists.autoperform.label = Auto-perform +pref.group.global.label = Global +pref.group.global.header.label = Headers +pref.group.global.header.help = Sets the default visibility for headers of views. Each view can override this default if necessary. +pref.group.global.log.label = Log these view types +pref.group.global.log.help = Sets the default logging state for views. Each view can override this default if necessary. +pref.group.global.maxLines.label = Scrollback size +pref.group.global.maxLines.help = The number of lines of text to keep in this view type. Once the limit is reached, the oldest lines are removed as new lines are added. +pref.group.global.security.label = Security +pref.group.global.sounds.label = Sound Configuration +pref.group.general.sounds.help = +pref.group.general.soundEvts.label = Sound Events +pref.group.general.soundEvts.help = Sounds for certain client events. These preferences are a space-separated list of either "beep" or file: URLs. + +# These are the prefs that get grouped # + +pref.autoperform.label = Auto-perform +pref.autoperform.help = Enter any commands to be run when connecting to this network/joining this channel/opening this user's private chat. The commands are run in the order listed. +pref.autoperform.channel.label = Channel +pref.autoperform.channel.help = Enter any commands to be run when joining any channel. +pref.autoperform.client.label = Client +pref.autoperform.client.help = Enter any commands to be run when starting ChatZilla. +pref.autoperform.network.label = Network +pref.autoperform.network.help = Enter any commands to be run when connecting to any network. +pref.autoperform.user.label = User +pref.autoperform.user.help = Enter any commands to be run when opening any user's private chat. + +pref.networkHeader.label = Networks +pref.networkHeader.help = +pref.channelHeader.label = Channels +pref.channelHeader.help = +pref.userHeader.label = Users +pref.userHeader.help = +pref.dccUserHeader.label = DCC +pref.dccUserHeader.help = + +pref.networkLog.label = Networks +pref.networkLog.help = +pref.channelLog.label = Channels +pref.channelLog.help = +pref.userLog.label = Users +pref.userLog.help = +pref.dccUserLog.label = DCC +pref.dccUserLog.help = + +pref.clientMaxLines.label = Client +pref.clientMaxLines.help = +pref.networkMaxLines.label = Networks +pref.networkMaxLines.help = +pref.channelMaxLines.label = Channels +pref.channelMaxLines.help = +pref.userMaxLines.label = Users +pref.userMaxLines.help = +pref.dccUserMaxLines.label = DCC +pref.dccUserMaxLines.help = + +pref.timestamps.display.label = Format +pref.timestamps.display.help = +pref.timestamps.label = Enabled +pref.timestamps.help = + +pref.msgBeep.label = New query view +pref.msgBeep.help = +pref.queryBeep.label = Query message +pref.queryBeep.help = +pref.stalkBeep.label = Important message +pref.stalkBeep.help = diff --git a/comm/suite/chatzilla/locales/en-US/chrome/chatzillaOverlay.dtd b/comm/suite/chatzilla/locales/en-US/chrome/chatzillaOverlay.dtd new file mode 100644 index 0000000000..af9e16bc10 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/chatzillaOverlay.dtd @@ -0,0 +1,7 @@ +<!-- 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/. --> + +<!ENTITY ircCmd.label "IRC Chat"> +<!ENTITY ircCmd.accesskey "i"> +<!ENTITY ircCmd.commandkey "6"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/config.dtd b/comm/suite/chatzilla/locales/en-US/chrome/config.dtd new file mode 100644 index 0000000000..60be5d78fa --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/config.dtd @@ -0,0 +1,40 @@ +<!-- 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/. --> + +<!ENTITY window.title "ChatZilla Preferences"> + +<!ENTITY loading.label "Please wait, loading…"> + +<!ENTITY homepage.url "http://chatzilla.hacksrus.com/"> +<!ENTITY homepage.label "ChatZilla Homepage"> + +<!ENTITY object.add.label "Add…"> +<!ENTITY object.add.hint "Add a new network, channel or user to set preferences on"> +<!ENTITY object.add.accesskey "A"> + +<!ENTITY object.del.label "Remove"> +<!ENTITY object.del.hint "Remove the current object, clearing all of its preferences"> +<!ENTITY object.del.accesskey "R"> + +<!ENTITY object.reset.label "Reset"> +<!ENTITY object.reset.hint "Reset this object's preferences to the defaults"> +<!ENTITY object.reset.accesskey "s"> + +<!ENTITY dialog.apply "Apply"> + +<!ENTITY network "Network"> +<!ENTITY channel "Channel"> +<!ENTITY user "User"> + +<!ENTITY config.add.title "Add Preference Object"> + +<!ENTITY config.type.label "Type:"> +<!ENTITY config.type.hint "Type of object to add preferences for"> +<!ENTITY config.type.accesskey "T"> +<!ENTITY config.network.label "Network:"> +<!ENTITY config.network.hint "Known name of the network, or the server name"> +<!ENTITY config.network.accesskey "N"> +<!ENTITY config.target.label "Target:"> +<!ENTITY config.target.hint "Target channel or user name"> +<!ENTITY config.target.accesskey "g"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/install-plugin.dtd b/comm/suite/chatzilla/locales/en-US/chrome/install-plugin.dtd new file mode 100644 index 0000000000..e92b76728b --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/install-plugin.dtd @@ -0,0 +1,17 @@ +<!-- 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/. --> + +<!ENTITY windowtitle "Install Plugin…"> + +<!ENTITY name.label "Name:"> +<!ENTITY name.accesskey "N"> + +<!ENTITY name.autopick.label "Automatically pick a name for me"> +<!ENTITY name.autopick.accesskey "A"> + +<!ENTITY source.label "Source:"> +<!ENTITY source.accesskey "c"> + +<!ENTITY browse.label "Browse…"> +<!ENTITY browse.accesskey "B"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/networks.dtd b/comm/suite/chatzilla/locales/en-US/chrome/networks.dtd new file mode 100644 index 0000000000..470efed305 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/networks.dtd @@ -0,0 +1,57 @@ +<!-- 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/. --> + +<!-- Used in networks-edit dialog --> +<!ENTITY networksEditDialog.title "Networks Editor"> +<!-- LOCALIZATION NOTE + - width should be 62ch + length of longest serverList*.label --> +<!ENTITY networksEditDialog.size "width: 75ch; height: 28em;"> + +<!ENTITY restoreButton.label "Restore Defaults"> +<!ENTITY restoreButton.accesskey "f"> +<!ENTITY connectNetwork.label "Connect to Network"> +<!ENTITY connectNetwork.accesskey "N"> + +<!ENTITY networkListAdd.label "Add Network…"> +<!ENTITY networkListAdd.accesskey "A"> +<!ENTITY networkListAdd.tooltip "Add a new network"> +<!ENTITY networkListEdit.label "Edit Network…"> +<!ENTITY networkListEdit.accesskey "E"> +<!ENTITY networkListEdit.tooltip "Edit the selected network"> +<!ENTITY networkListRemove.label "Remove Network"> +<!ENTITY networkListRemove.accesskey "R"> +<!ENTITY networkListRemove.tooltip "Remove the selected network"> + +<!ENTITY serverListUp.label "Move Up"> +<!ENTITY serverListUp.accesskey "U"> +<!ENTITY serverListUp.tooltip "Move the selected server up the list"> +<!ENTITY serverListDown.label "Move Down"> +<!ENTITY serverListDown.accesskey "D"> +<!ENTITY serverListDown.tooltip "Move the selected server down the list"> +<!ENTITY serverListAdd.label "Add Server…"> +<!ENTITY serverListAdd.accesskey "S"> +<!ENTITY serverListAdd.tooltip "Add a new server"> +<!ENTITY serverListEdit.label "Edit Server…"> +<!ENTITY serverListEdit.accesskey "i"> +<!ENTITY serverListEdit.tooltip "Edit the selected server"> +<!ENTITY serverListRemove.label "Remove Server"> +<!ENTITY serverListRemove.accesskey "m"> +<!ENTITY serverListRemove.tooltip "Remove the selected server"> + +<!-- LOCALIZATION NOTE: + - *.label are shared between networks-edit and networks-server dialogs + - *.accesskey are only used in network-servers dialog --> +<!ENTITY serverDetails.label "Details of the selected server:"> +<!ENTITY serverName.label "Server:"> +<!ENTITY serverName.accesskey "S"> +<!ENTITY serverPort.label "Port:"> +<!ENTITY serverPort.accesskey "P"> +<!ENTITY connectionSecurity.label "Connection Security:"> +<!ENTITY connectionSecurity.accesskey "n"> + +<!-- Used in networks-server dialog --> +<!ENTITY serverEditDialog.title "IRC Server"> +<!ENTITY settings.caption "Settings"> +<!ENTITY security.caption "Security"> +<!ENTITY serverPortDefault.label "Default:"> diff --git a/comm/suite/chatzilla/locales/en-US/chrome/networks.properties b/comm/suite/chatzilla/locales/en-US/chrome/networks.properties new file mode 100644 index 0000000000..a36d4879fc --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/networks.properties @@ -0,0 +1,28 @@ +# 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/. + +network-headerDefault=Networks +network-headerName=Network %S +network-saveErrorTitle=Save Error +network-saveError=An exception occurred trying to save the networks list: \n %S. + +network-addTitle=Add Network +network-add=Enter network to add: +network-editTitle=Edit Network +network-edit=Network name: +network-removeTitle=Remove Network +network-remove=Are you sure that you want to remove the network: \n %S? +network-nameErrorTitle=Network Exists +network-nameError=A network %S already exists. + +network-confirmRestoreDefaultsTitle=Restore Default Network List +network-confirmRestoreDefaults=Are you sure you want to restore the default network list, and overwrite any changes you have made? + +server-ConnectionSecurityType-0=None +server-ConnectionSecurityType-3=SSL/TLS +server-removeTitle=Remove Server +server-remove=Are you sure that you want to remove the server: \n %S? + +invalidServerName=Invalid Server Name +enterValidServerName=Please enter a valid server name. diff --git a/comm/suite/chatzilla/locales/en-US/chrome/pref-irc.dtd b/comm/suite/chatzilla/locales/en-US/chrome/pref-irc.dtd new file mode 100644 index 0000000000..6be6ff1bc8 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/chrome/pref-irc.dtd @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<!ENTITY pref-irc.window.title "ChatZilla"> + +<!ENTITY pref-irc.open.title "ChatZilla's Preferences"> +<!ENTITY pref-irc.open.desc "ChatZilla's preferences are too extensive to fit into this preference window so, instead, you can open its preferences window from here."> +<!ENTITY pref-irc.open.label "Open ChatZilla's Preferences"> +<!ENTITY pref-irc.open.accesskey "O"> +<!ENTITY startup.chat.label "ChatZilla"> +<!ENTITY startup.chat.accesskey "Z"> diff --git a/comm/suite/chatzilla/locales/en-US/defines.inc b/comm/suite/chatzilla/locales/en-US/defines.inc new file mode 100644 index 0000000000..1c706834f3 --- /dev/null +++ b/comm/suite/chatzilla/locales/en-US/defines.inc @@ -0,0 +1,11 @@ +#filter emptyLines + +#define MOZ_LANGPACK_CREATOR The ChatZilla Team + +#define MOZ_LANGPACK_HOMEPAGE http://chatzilla.hacksrus.com/ + +# If non-English locales wish to credit multiple contributors, uncomment this +# variable definition and use the format specified. +# #define MOZ_LANGPACK_CONTRIBUTORS <em:contributor>Joe Solon</em:contributor> <em:contributor>Suzy Solon</em:contributor> + +#unfilter emptyLines diff --git a/comm/suite/chatzilla/locales/jar.mn b/comm/suite/chatzilla/locales/jar.mn new file mode 100644 index 0000000000..9ff9bf79b3 --- /dev/null +++ b/comm/suite/chatzilla/locales/jar.mn @@ -0,0 +1,20 @@ +# 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/. + +#filter substitution + +@AB_CD@.jar: +# the "% locale" line gets put into the .manifest used by toolkit-based apps +% locale chatzilla @AB_CD@ %locale/@AB_CD@/chatzilla/ + locale/@AB_CD@/chatzilla/chatzillaOverlay.dtd (%chrome/chatzillaOverlay.dtd) + locale/@AB_CD@/chatzilla/browserOverlay.dtd (%chrome/browserOverlay.dtd) + locale/@AB_CD@/chatzilla/pref-irc.dtd (%chrome/pref-irc.dtd) + locale/@AB_CD@/chatzilla/chatzilla.dtd (%chrome/chatzilla.dtd) + locale/@AB_CD@/chatzilla/chatzilla.properties (%chrome/chatzilla.properties) + locale/@AB_CD@/chatzilla/config.dtd (%chrome/config.dtd) + locale/@AB_CD@/chatzilla/channels.dtd (%chrome/channels.dtd) + locale/@AB_CD@/chatzilla/networks.dtd (%chrome/networks.dtd) + locale/@AB_CD@/chatzilla/networks.properties (%chrome/networks.properties) + locale/@AB_CD@/chatzilla/about.dtd (%chrome/about.dtd) + locale/@AB_CD@/chatzilla/install-plugin.dtd (%chrome/install-plugin.dtd) diff --git a/comm/suite/chatzilla/locales/l10n.ini b/comm/suite/chatzilla/locales/l10n.ini new file mode 100644 index 0000000000..d4bacc82f7 --- /dev/null +++ b/comm/suite/chatzilla/locales/l10n.ini @@ -0,0 +1,10 @@ +; 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/. + +[general] +depth = ../../.. +all = suite/chatzilla/locales/all-locales + +[compare] +dirs = suite/chatzilla diff --git a/comm/suite/chatzilla/locales/l10n.toml b/comm/suite/chatzilla/locales/l10n.toml new file mode 100644 index 0000000000..8da66121cd --- /dev/null +++ b/comm/suite/chatzilla/locales/l10n.toml @@ -0,0 +1,44 @@ +# 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/. + +basepath = "../../.." + +locales = [ + "cs", + "de", + "el", + "en-GB", + "es-AR", + "es-ES", + "fi", + "fr", + "hu", + "it", + "ja", + "ja-JP-mac", + "ka", + "nb-NO", + "nl", + "pl", + "pt-BR", + "pt-PT", + "ru", + "sk", + "sv-SE", + "zh-CN", + "zh-TW", +] + +[env] + l = "{l10n_base}/{locale}/" + +[[paths]] + reference = "suite/chatzilla/locales/en-US/**" + l10n = "{l}suite/chatzilla/**" + +# ignore MOZ_LANGPACK_CONTRIBUTORS +[[filters]] + path = "{l}suite/chatzilla/defines.inc" + key = "MOZ_LANGPACK_CONTRIBUTORS" + action = "ignore" diff --git a/comm/suite/chatzilla/locales/moz.build b/comm/suite/chatzilla/locales/moz.build new file mode 100644 index 0000000000..e0eb66aace --- /dev/null +++ b/comm/suite/chatzilla/locales/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ['jar.mn'] diff --git a/comm/suite/chatzilla/moz.build b/comm/suite/chatzilla/moz.build new file mode 100644 index 0000000000..6e463475ee --- /dev/null +++ b/comm/suite/chatzilla/moz.build @@ -0,0 +1,15 @@ +# vim: set filetype=python: +# 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/. + +DEFINES['CHATZILLA_VERSION'] = '0.9.97' + +DIRS += ['locales'] + +JAR_MANIFESTS += ["jar.mn"] + +EXTRA_COMPONENTS += [ + "js/lib/chatzilla-service.js", + "js/lib/chatzilla-service.manifest", +] diff --git a/comm/suite/chatzilla/xul/content/about/about.js b/comm/suite/chatzilla/xul/content/about/about.js new file mode 100644 index 0000000000..b68c64aa8f --- /dev/null +++ b/comm/suite/chatzilla/xul/content/about/about.js @@ -0,0 +1,112 @@ +/* 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 ownerClient = null; + +// To be able to load static.js, we need a few things defined first: +function CIRCNetwork() {} +function CIRCServer() {} +function CIRCChannel() {} +function CIRCUser() {} +function CIRCChanUser() {} +function CIRCDCCUser() {} +function CIRCDCCChat() {} +function CIRCDCCFile() {} +function CIRCDCCFileTransfer() {} +function CIRCSTS() {} + +// Our friend from messages.js: +function getMsg(msgName, params, deflt) +{ + return client.messageManager.getMsg(msgName, params, deflt); +} + +function onLoad() +{ + const propsPath = "chrome://chatzilla/locale/chatzilla.properties"; + + // Find our owner, if we have one. + ownerClient = window.arguments ? window.arguments[0].client : null; + if (ownerClient) + ownerClient.aboutDialog = window; + + client.entities = new Object(); + client.messageManager = new MessageManager(client.entities); + client.messageManager.loadBrands(); + client.defaultBundle = client.messageManager.addBundle(propsPath); + + var version = getVersionInfo(); + client.userAgent = getMsg(MSG_VERSION_REPLY, [version.cz, version.ua]); + + var verLabel = document.getElementById("version"); + var verString = verLabel.getAttribute("format").replace("%S", version.cz); + verLabel.setAttribute("value", verString); + verLabel.setAttribute("condition", __cz_condition); + + var localizers = document.getElementById("localizers"); + var localizerNames = getMsg("locale.authors", null, ""); + if (localizerNames && (localizerNames.substr(0, 11) != "XXX REPLACE")) + { + localizerNames = localizerNames.split(/\s*;\s*/); + + for (var i = 0; i < localizerNames.length; i++) { + var loc = document.createElement("label"); + loc.setAttribute("value", localizerNames[i]); + localizers.appendChild(loc); + } + } + else + { + var localizersHeader = document.getElementById("localizers-header"); + localizersHeader.style.display = "none"; + localizers.style.display = "none"; + } + + if (window.opener) + { + // Force the window to be the right size now, not later. + window.sizeToContent(); + + // Position it centered over, but never up or left of parent. + var opener = window.opener; + var sx = Math.max((opener.outerWidth - window.outerWidth ) / 2, 0); + var sy = Math.max((opener.outerHeight - window.outerHeight) / 2, 0); + window.moveTo(opener.screenX + sx, opener.screenY + sy); + } + + /* Find and focus the dialog's default button (OK), otherwise the focus + * lands on the first focusable content - the homepage link. Links in XUL + * look horrible when focused. + */ + var binding = document.documentElement; + var defaultButton = binding.getButton(binding.defaultButton); + if (defaultButton) + setTimeout(function() { defaultButton.focus() }, 0); +} + +function onUnload() +{ + if (ownerClient) + delete ownerClient.aboutDialog; +} + +function copyVersion() +{ + const cbID = Components.interfaces.nsIClipboard.kGlobalClipboard; + var cb = getService("@mozilla.org/widget/clipboard;1", "nsIClipboard"); + var tr = newObject("@mozilla.org/widget/transferable;1", "nsITransferable"); + var str = newObject("@mozilla.org/supports-string;1", "nsISupportsString"); + + str.data = client.userAgent; + tr.setTransferData("text/unicode", str, str.data.length * 2); + cb.setData(tr, null, cbID); +} + +function openHomepage() +{ + if (ownerClient) + ownerClient.dispatch("goto-url", {url: MSG_SOURCE_REPLY}); + else + window.opener.open(MSG_SOURCE_REPLY, "_blank"); +} diff --git a/comm/suite/chatzilla/xul/content/about/about.xul b/comm/suite/chatzilla/xul/content/about/about.xul new file mode 100644 index 0000000000..54dc7b1bd4 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/about/about.xul @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/about.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/skin/about.css" type="text/css"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="irc:chatzilla:about" + buttons="accept" + onload="onLoad()" + onunload="onUnload()" + title="&window.title;"> + + <script src="chrome://chatzilla/content/lib/js/utils.js"/> + <script src="chrome://chatzilla/content/about/about.js"/> + <script src="chrome://chatzilla/content/lib/js/message-manager.js"/> + <script src="chrome://chatzilla/content/static.js"/> + + <vbox class="box-padded" flex="1"> + <hbox> + <image id="logo"/> + <vbox flex="1"> + <hbox> + <label class="header large-text" id="name" value="&chatzilla.label;"/> + <spacer flex="1"/> + <label class="header" id="version" value="&version.unknown.label;" format="&version.known.label;"/> + </hbox> + <hbox> + <label class="text-link" onclick="openHomepage()" value="&homepage.label;"/> + <spacer flex="1"/> + <label class="text-link" onclick="copyVersion()" value="©version.label;"/> + </hbox> + </vbox> + </hbox> + <description id="description">&description.label;</description> + <label class="contributors-label header" value="§ion.core.label;"/> + <vbox class="contributors"> + <label>Robert Ginda</label> + <label>Gijs Kruitbosch</label> + <label>James Ross</label> + <label>Samuel Sieb</label> + </vbox> + <label class="contributors-label header" id="localizers-header" value="§ion.locale.label;"/> + <vbox class="contributors" id="localizers"> + <!-- These are inserted from onLoad(), as read from locale file. --> + </vbox> + <label class="contributors-label header" value="§ion.contrib.label;"/> + <vbox class="contributors"> + <label>Lim Chee Aun (graphics)</label> + </vbox> + </vbox> + <separator id="groove" class="groove"/> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/browserOverlay.xul b/comm/suite/chatzilla/xul/content/browserOverlay.xul new file mode 100644 index 0000000000..a6e31a3158 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/browserOverlay.xul @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/browserOverlay.dtd" > + +<!-- This is the overlay that adds a "Chatzilla" button to the toolbar palette. --> + +<?xml-stylesheet href="chrome://chatzilla/skin/browserOverlay.css" + type="text/css"?> + +<overlay id="ChatzillaBrowserToolbar" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://chatzilla/content/chatzillaOverlay.js"/> + +<toolbarpalette id="BrowserToolbarPalette"> + <toolbarbutton id="chatzilla-open" oncommand="toIRC()" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&czButton.label;" tooltiptext="&czButton.label;"/> +</toolbarpalette> +</overlay> diff --git a/comm/suite/chatzilla/xul/content/channels.js b/comm/suite/chatzilla/xul/content/channels.js new file mode 100644 index 0000000000..912e62c14a --- /dev/null +++ b/comm/suite/chatzilla/xul/content/channels.js @@ -0,0 +1,875 @@ +/* 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 client; +var network; +var channels = new Array(); +var tree = { view: null, newItem: null, share: new Object() }; +var xul = new Object(); + + +// Create list of operations. These are handled by common code. +const OPS = new Array(); +OPS.push({ key: "noop", ignore: true }); +OPS.push({ key: "list", canStop: false }); +OPS.push({ key: "load", canStop: true }); +OPS.push({ key: "filter", canStop: true }); + + +// Define constants for each operation. +// JavaScript won't let you delete things declared with "var", workaround: +// NOTE: This order MUST be the same as those above! +window.s = 0; +const OP_LIST = ++s; // A /list operation on the server. +const OP_LOAD = ++s; // Loading the saved file. +const OP_FILTER = ++s; // Filtering the loaded list. + + +// Define constants for the valid states of each operation. +// All states before STATE_START must be idle (stopped) states. +// All states from STATE_START onwards must be busy (running) states. +s = 0; +const STATE_IDLE = ++s; // Not doing this operation. +const STATE_ERROR = ++s; // Error occurred: don't try do to any more. +const STATE_START = ++s; // Starting an operation. +const STATE_RUN = ++s; // Running... +const STATE_STOP = ++s; // Clean-up/ending operation. +delete window.s; + + +// Store all the operation data here. +var data = { + list: { state: STATE_IDLE }, + load: { state: STATE_IDLE }, + filter: { state: STATE_IDLE } +}; + + +// This should keep things responsive enough, for the user to click buttons and +// edit the filter text and options, without giving up too much time to letting +// Gecko catch up. +const PROCESS_TIME_MAX = 200; +const PROCESS_DELAY = 50; + +const colIDToSortKey = { chanColName: "name", + chanColUsers: "users", + chanColTopic: "topic" }; +const sortKeyToColID = { name: "chanColName", + users: "chanColUsers", + topic: "chanColTopic" }; + +function onLoad() +{ + function ondblclick(event) { tree.view.onRouteDblClick(event); }; + function onkeypress(event) { tree.view.onRouteKeyPress(event); }; + function onfocus(event) { tree.view.onRouteFocus(event); }; + function onblur(event) { tree.view.onRouteBlur(event); }; + + function doJoin() + { + if (joinChannel()) + window.close(); + }; + + client = window.arguments[0].client; + client.joinDialog = window; + + window.dd = client.mainWindow.dd; + window.ASSERT = client.mainWindow.ASSERT; + window.toUnicode = client.mainWindow.toUnicode; + window.getMsg = client.mainWindow.getMsg; + window.MSG_CHANNEL_OPENED = client.mainWindow.MSG_CHANNEL_OPENED; + window.MSG_FMT_JSEXCEPTION = client.mainWindow.MSG_FMT_JSEXCEPTION; + window.MT_INFO = client.mainWindow.MT_INFO; + + // Import "MSG_CD_*"... + for (var m in client.mainWindow) + { + if (m.substr(0, 7) == "MSG_CD_") + window[m] = client.mainWindow[m]; + } + + // Cache all the XUL DOM elements. + var elements = ["network", "networks", "channel", "includeTopic", + "lastUpdated", "join", "minUsers", "maxUsers", "refresh", + "bottomPanel", "channels", "loadContainer", "loadLabel", + "loadBarDeck", "loadBar"]; + for (var i = 0; i < elements.length; i++) + xul[elements[i]] = document.getElementById(elements[i]); + + // Set the <dialog>'s class so we can do platform-specific CSS. + var dialog = document.getElementById("chatzilla-window"); + dialog.className = "platform-" + client.platform; + + // Set up the channel tree view. + tree.view = new XULTreeView(tree.share); + tree.view.onRowCommand = doJoin; + tree.view.cycleHeader = changeSort; + xul.channels.treeBoxObject.view = tree.view; + + // If the new "search" binding is not working (i.e. doesn't exist)... + if (!("searchButton" in xul.channel)) + { + // ...restore the text boxes to their former selves. + xul.channel.setAttribute("timeout", "500"); + xul.channel.setAttribute("type", "timed"); + xul.minUsers.setAttribute("timeout", "500"); + xul.minUsers.setAttribute("type", "timed"); + xul.maxUsers.setAttribute("timeout", "500"); + xul.maxUsers.setAttribute("type", "timed"); + } + + // Sort by user count, descending. + changeSort("chanColUsers"); + + xul.channels.addEventListener("dblclick", ondblclick, false); + xul.channels.addEventListener("keypress", onkeypress, false); + xul.channels.addEventListener("focus", onfocus, false); + xul.channels.addEventListener("blur", onblur, false); + + tree.newItem = new ChannelEntry("", "", MSG_CD_CREATE); + tree.newItem.first = true; + tree.view.childData.appendChild(tree.newItem); + + var opener = window.arguments[0].opener; + if (opener) + { + // Force the window to be the right size now, not later. + window.sizeToContent(); + + // Position it centered over, but never up or left of parent. + var sx = Math.max((opener.outerWidth - window.outerWidth ) / 2, 0); + var sy = Math.max((opener.outerHeight - window.outerHeight) / 2, 0); + window.moveTo(opener.screenX + sx, opener.screenY + sy); + } + + setNetwork(window.arguments[0].network); + setTimeout(updateOperations, PROCESS_DELAY); + if (network) + xul.channel.focus(); + else + xul.network.focus(); +} + +function onUnload() +{ + delete client.joinDialog; +} + +function onKeyPress(event) +{ + if (event.keyCode == event.DOM_VK_RETURN) + { + if (joinChannel()) + window.close(); + event.stopPropagation(); + event.preventDefault(); + } + else if (event.keyCode == event.DOM_VK_UP) + { + if (tree.view.selectedIndex > 0) + { + tree.view.selectedIndex = tree.view.selectedIndex - 1; + ensureRowIsVisible(); + } + event.preventDefault(); + } + else if (event.keyCode == event.DOM_VK_DOWN) + { + if (tree.view.selectedIndex < tree.view.rowCount - 1) + { + tree.view.selectedIndex = tree.view.selectedIndex + 1; + ensureRowIsVisible(); + } + event.preventDefault(); + } +} + +function onShowingNetworks() +{ + while (xul.networks.lastChild) + xul.networks.removeChild(xul.networks.lastChild); + + /* Show any network meeting at least 1 requirement: + * - Non-temporary (i.e. real network). + * - Currently connected. + * - Has visible tab in main window. + */ + var networks = new Array(); + for (var n in client.networks) + { + if (!client.networks[n].temporary + || client.networks[n].isConnected() + || client.mainWindow.getTabForObject(client.networks[n])) + { + networks.push(client.networks[n].unicodeName); + } + } + networks.sort(); + for (var i = 0; i < networks.length; i++) + { + var menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", networks[i]); + xul.networks.appendChild(menuitem); + } +} + +function onSelectionChange() +{ + update(); +} + +function onFilter() +{ + update(); + if (network) + startOperation(OP_FILTER); +} + +function setNetwork(newNetwork, noUpdate) +{ + xul.network.value = newNetwork ? newNetwork.unicodeName : ""; + update(); +} + +function update() +{ + let newNetwork = client.getNetwork(xul.network.value); + if (network != newNetwork) + { + network = newNetwork; + if (network) + startOperation(OP_LOAD); + } + + if (network) + { + var index = tree.view.selectedIndex; + var rows = tree.view.childData; + var row = index == -1 ? null : rows.locateChildByVisualRow(index); + var listFile = getListFile(); + var listMod = 0; + if (listFile.exists() && (listFile.fileSize > 0)) + listMod = listFile.lastModifiedTime; + + xul.join.disabled = network.isConnected() && (!row || !row.name); + xul.lastUpdated.value = listMod ? getMsg(MSG_CD_UPDATED, [strftime(MSG_CD_UPDATED_FORMAT, new Date(listMod))]) : MSG_CD_UPDATED_NEVER; + xul.refresh.disabled = !network.isConnected() || + (getOperationState(OP_LIST) == STATE_START) || + (getOperationState(OP_LIST) == STATE_RUN); + xul.bottomPanel.selectedIndex = 1; + } + else + { + xul.join.disabled = !xul.network.value; + xul.lastUpdated.value = ""; + xul.refresh.disabled = true; + xul.bottomPanel.selectedIndex = 0; + } +} + +function joinChannel() +{ + update(); + if (xul.join.disabled) + return false; + + /* Calculate the row index AS IF the 'create' row is visible. We're going + * to use this so that the index chosen by the user is always consistent, + * whatever the visibility of the 'create' row - an index of 0 is ALWAYS + * the 'create' row, and >= 1 is ALWAYS the searched rows. + */ + var index = tree.view.selectedIndex; + var row = tree.view.childData.locateChildByVisualRow(index); + var realIndex = index + (tree.newItem.isHidden ? 1 : 0); + + client.dispatch("attach", { ircUrl: xul.network.value + "/" + row.name }); + + return true; +} + +function focusSearch() +{ + xul.channel.focus(); +} + +function refreshList() +{ + startOperation(OP_LIST); +} + +function updateProgress(label, pro) +{ + if (label) + { + xul.loadLabel.value = label; + } + else + { + var msg = getMsg(MSG_CD_SHOWING, + [(tree.view.rowCount - (tree.newItem.isHidden ? 0 : 1)), + channels.length]); + xul.loadLabel.value = msg; + } + + xul.loadBarDeck.selectedIndex = (typeof pro == "undefined") ? 1 : 0; + + if ((typeof pro == "undefined") || (pro == -1)) + { + xul.loadBar.mode = "undetermined"; + } + else + { + xul.loadBar.mode = "determined"; + xul.loadBar.value = pro; + } +} + +function changeSort(col) +{ + if (typeof col == "object") + col = col.id; + + col = colIDToSortKey[col]; + // Users default to descending, others ascending. + var dir = (col == "users" ? -1 : 1); + + if (col == tree.share.sortColumn) + dir = -tree.share.sortDirection; + + var colID = sortKeyToColID[tree.share.sortColumn]; + var colNode = document.getElementById(colID); + if (colNode) + { + colNode.removeAttribute("sortActive"); + colNode.removeAttribute("sortDirection"); + } + + tree.view.childData.setSortColumn(col, dir); + + colID = sortKeyToColID[tree.share.sortColumn]; + colNode = document.getElementById(colID); + if (colNode) + { + colNode.setAttribute("sortActive", "true"); + var sortDir = (dir > 0 ? "ascending" : "descending"); + colNode.setAttribute("sortDirection", sortDir); + } +} + + +// ***** BEGIN OPERATIONS CODE ***** + + +/* Return the static data about an operation (e.g. whether it can be + * stopped, etc.). The data returned is always the same for a given op code. + */ +function getOperation(op) +{ + ASSERT(op in OPS, "Invalid op-code: " + op); + return OPS[op]; +} + +/* Returns the live data about an operation (e.g. current state). Accepts + * either the op ID or the static data (as returned from getOperation(op)). + */ +function getOperationData(op) +{ + if (typeof op == "object") + return data[op.key]; + return data[getOperation(op).key]; +} + +// Returns the current state of an operation; accepts same as getOperationData. +function getOperationState(op) +{ + return getOperationData(op).state; +} + +function startOperation(op) +{ + var ops = getOperation(op); + if (ops.ignore) + return; + + var dbg = "startOperation(" + ops.key + ")"; + var opData = getOperationData(ops); + + // STATE_ERROR operations must not do anything. Assert and bail. + if (!ASSERT(opData.state != STATE_ERROR, dbg + " in STATE_ERROR")) + return; + + // Check we can stop a non-idle operation. + if (!ASSERT((opData.state == STATE_IDLE) || ops.canStop, + dbg + " not in STATE_IDLE and can't stop")) + { + return; + } + + // Stop the current operation. + if (opData.state != STATE_IDLE) + stopOperation(op); + + // Begin! + var opData = getOperationData(op); + opData.state = STATE_START; + processOperation(op); + ASSERT(opData.state == STATE_RUN, dbg + " didn't enter STATE_RUN"); +} + +function updateOperations() +{ + for (var i = 1; i < OPS.length; i++) + { + var state = getOperationState(i); + if ((state == STATE_RUN) || (state == STATE_STOP)) + processOperation(i); + } + + setTimeout(updateOperations, PROCESS_DELAY); +} + +function processOperation(op) +{ + var ops = getOperation(op); + if (ops.ignore) + return; + + var dbg = "processOperation(" + ops.key + ")"; + var opData = getOperationData(ops); + + var fn = "processOp"; + fn += ops.key[0].toUpperCase() + ops.key.substr(1); + if (opData.state == STATE_START) + fn += "Start"; + else if (opData.state == STATE_RUN) + fn += "Run"; + else if (opData.state == STATE_STOP) + fn += "Stop"; + // assert and return if we're in a different state: + else if (!ASSERT(false, dbg + " invalid state: " + opData.state)) + return; + + try + { + var newState = window[fn](opData); + if (typeof newState != "undefined") + opData.state = newState; + } + catch(ex) + { + /* If an error has occurred, we display it (updateProgress) and then + * halt our operations to prevent further damage. + */ + dd("Exception in channels.js: " + dbg + ": " + fn + ": " + formatException(ex)); + updateProgress(formatException(ex)); + opData.state = STATE_ERROR; + } +} + +function stopOperation(op) +{ + var ops = getOperation(op); + if (ops.ignore) + return; + + var dbg = "stopOperation(" + ops.key + ")"; + var opData = getOperationData(ops); + + // STATE_ERROR operations must not do anything. Assert and bail. + if (!ASSERT(opData.state != STATE_ERROR, dbg + " in STATE_ERROR")) + return; + + // Nothing to do for STATE_IDLE. We shouldn't really be here, so assert. + if (!ASSERT(opData.state != STATE_IDLE, dbg + " in STATE_IDLE")) + return; + + // Force the end and process synchronously. + opData.state = STATE_STOP; + processOperation(op); + ASSERT(opData.state == STATE_IDLE, dbg + " didn't enter STATE_IDLE"); +} + +// ***** END OPERATIONS CODE ***** + + +// ***** BEGIN OPERATION HANDLERS ***** + +function processOpListStart(opData) +{ + ASSERT(network, "No network"); + ASSERT(network.isConnected(), "Network is disconnected"); + + // Updates the refresh button. + update(); + + // Show a general message until we get some data. + updateProgress(MSG_CD_FETCHING, -1); + + // Get the file we're going to save to, and start the /list. + var file = getListFile(); + network.list("", file.path); + + return STATE_RUN; +} + +function processOpListRun(opData) +{ + // Update the progress and end if /list done for "list only" state. + updateProgress(getMsg(MSG_CD_FETCHED, network._list.count), -1); + + // Stop if the network's /list has finished. + return (network._list.done ? STATE_STOP : STATE_RUN); +} + +function processOpListStop(opData) +{ + // Updates the refresh button. + update(); + + // Check that /list finished okay if we're just doing a list. + if ("error" in network._list) + { + updateProgress(MSG_CD_ERROR_LIST); + } + else + { + updateProgress(); + if (getOperationState(OP_LOAD) == STATE_IDLE) + startOperation(OP_LOAD); + } + + return STATE_IDLE; +} + +function processOpLoadStart(opData) +{ + ASSERT(network, "No network"); + + // Nuke contents. + tree.view.selectedIndex = -1; + if (tree.view.childData.childData.length > 1) + tree.view.childData.removeChildrenAtIndex(1, tree.view.childData.childData.length - 1); + + var file = getListFile(); + if (!file.exists()) + { + // We tried to do a load, but the file does not exist. Start a list to + // fill up the file. + startOperation(OP_LIST); + + // File still doesn't exist, just give up. + if (!file.exists()) + return STATE_IDLE; + } + + // Nuke more stuff. + channels = new Array(); + + // And... here we go. + opData.loadFile = new LocalFile(file, "<"); + opData.loadPendingData = ""; + opData.loadChunk = 10000; + opData.loadedSoFar = 0; + + return STATE_RUN; +} + +function processOpLoadRun(opData) +{ + // All states before STATE_START are "not running" states. + var opListRunning = (getOperationState(OP_LIST) >= STATE_START); + + var end = Number(new Date()) + PROCESS_TIME_MAX; + while (Number(new Date()) < end) + { + var nlIndex = opData.loadPendingData.indexOf("\n"); + if (nlIndex == -1) + { + opData.loadedSoFar += opData.loadChunk; + var newChunk = opData.loadFile.read(opData.loadChunk); + if (newChunk) + opData.loadPendingData += newChunk; + nlIndex = opData.loadPendingData.indexOf("\n"); + if (nlIndex == -1) + break; + } + + var line = opData.loadPendingData.substr(0, nlIndex); + opData.loadPendingData = opData.loadPendingData.substr(nlIndex + 1); + + line = toUnicode(line, "UTF-8"); + var ary = line.match(/^([^ ]+) ([^ ]+) (.*)$/); + if (ary) + { + var chan = new ChannelEntry(ary[1], ary[2], ary[3]); + channels.push(chan); + } + } + + var dataLeft = opData.loadFile.inputStream.available(); + + // We shouldn't update the display when listing as well, as we're not + // going to show anything useful (always 100% or near to it, and + // replaces the 'fetching' message). + if (!opListRunning) + { + var pro = opData.loadedSoFar / (opData.loadedSoFar + dataLeft); + pro = Math.round(100 * pro); + updateProgress(getMsg(MSG_CD_LOADED, channels.length), pro); + } + + // Done if there is no more data, and we're not *expecting* any more. + if ((dataLeft == 0) && !opListRunning) + return STATE_STOP; + + return STATE_RUN; +} + +function processOpLoadStop(opData) +{ + if (channels.length > 0) + tree.view.childData.appendChildren(channels); + opData.loadFile.close(); + delete opData.loadFile; + delete opData.loadPendingData; + delete opData.loadChunk; + delete opData.loadedSoFar; + delete opData.loadNeverComplete; + updateProgress(); + + startOperation(OP_FILTER); + + return STATE_IDLE; +} + +function processOpFilterStart(opData) +{ + // Catch filtering with the same options on the same channels: + var newOptions = {network: xul.network.value.toLowerCase(), + text: xul.channel.value.toLowerCase(), + min: xul.minUsers.value * 1, + max: xul.maxUsers.value * 1, + listLen: channels.length, + searchTopics: xul.includeTopic.checked}; + + if (("filterOptions" in window) && + equalsObject(window.filterOptions, newOptions)) + { + return STATE_IDLE; + } + + window.filterOptions = newOptions; + + opData.text = newOptions.text; + opData.searchTopics = newOptions.searchTopics; + opData.minUsers = newOptions.min; + opData.maxUsers = newOptions.max; + opData.exactMatch = null; + opData.currentIndex = 0; + opData.channelText = opData.text; + + // Log the filter, indicating which features the user is using. + var filters = new Array(); + if (opData.channelText) + filters.push("name"); + if (opData.searchTopics) + filters.push("topics"); + if (opData.minUsers) + filters.push("min-users"); + if (opData.maxUsers) + filters.push("max-users"); + + if (opData.channelText && + (arrayIndexOf(["#", "&", "+", "!"], opData.channelText[0]) == -1)) + { + opData.channelText = "#" + opData.channelText; + } + else + { + // Log that user has specified an explicit prefix. + filters.push("prefix"); + } + + // Update special "create channel" row, and select it. + tree.newItem.name = opData.channelText; + tree.newItem.unHide(); + + // Scroll to the top and select the "create channel" row. + tree.view.selectedIndex = 0; + xul.channels.treeBoxObject.invalidateRow(0); + xul.channels.treeBoxObject.scrollToRow(0); + ensureRowIsVisible(); + + updateProgress(getMsg(MSG_CD_FILTERING, [0, channels.length]), 0); + + return STATE_RUN; +} + +function processOpFilterRun(opData) +{ + var end = Number(new Date()) + PROCESS_TIME_MAX; + var more = false; + + // Save selection because freeze/thaw screws it up. + // Note that we only save the item if it isn't the "create channel" row. + var index = tree.view.selectedIndex; + var item = null; + if (index > 0) + item = tree.view.childData.locateChildByVisualRow(index); + + tree.view.freeze(); + for (var i = opData.currentIndex; i < channels.length; i++) + { + var c = channels[i]; + + var match = (c.nameLC.indexOf(opData.text) != -1) || + (opData.searchTopics && + (c.topicLC.indexOf(opData.text) != -1)); + + if (opData.minUsers && (c.users < opData.minUsers)) + match = false; + if (opData.maxUsers && (c.users > opData.maxUsers)) + match = false; + + if (match) + c.unHide(); + else + c.hide(); + + if (match && (c.nameLC == opData.channelText)) + opData.exactMatch = c; + + opData.currentIndex = i; + if ((new Date()) > end) + { + more = true; + break; + } + } + tree.view.thaw(); + + // No item selected by user, so use our exact match instead. + if (!item && opData.exactMatch) + item = opData.exactMatch; + + // Restore selected item. + if (item) + tree.view.selectedIndex = item.calculateVisualRow(); + else + tree.view.selectedIndex = 0; + + ensureRowIsVisible(); + + updateProgress(getMsg(MSG_CD_FILTERING, + [opData.currentIndex, channels.length]), + 100 * opData.currentIndex / channels.length); + + return (more ? STATE_RUN : STATE_STOP); +} + +function processOpFilterStop(opData) +{ + if (opData.exactMatch) + { + tree.newItem.hide(); + } + // If nothing is selected, select the "create channel" row. + else if (tree.view.selectedIndex < 0) + { + tree.view.selectedIndex = 0; + } + + ensureRowIsVisible(); + + delete opData.text; + delete opData.searchTopics; + delete opData.minUsers; + delete opData.maxUsers; + delete opData.exactMatch; + delete opData.currentIndex; + delete opData.channelText; + updateProgress(); + + return STATE_IDLE; +} + + +// ***** END OPERATION HANDLERS ***** + + +function ensureRowIsVisible() +{ + if (tree.view.selectedIndex >= 0) + xul.channels.treeBoxObject.ensureRowIsVisible(tree.view.selectedIndex); + else + xul.channels.treeBoxObject.ensureRowIsVisible(0); +} + +function getListFile(temp) +{ + ASSERT(network, "No network"); + var file = new LocalFile(network.prefs["logFileName"]); + if (temp) + file.localFile.leafName = "list.temp"; + else + file.localFile.leafName = "list.txt"; + return file.localFile; +} + + +// Tree ChannelEntry objects // +function ChannelEntry(name, users, topic) +{ + this.setColumnPropertyName("chanColName", "name"); + this.setColumnPropertyName("chanColUsers", "users"); + this.setColumnPropertyName("chanColTopic", "topic"); + + // Nuke color codes and bold etc. + topic = topic.replace(/[\x1F\x02\x0F\x16]/g, ""); + topic = topic.replace(/\x03\d{1,2}(?:,\d{1,2})?/g, ""); + + this.name = name; + this.users = users; + this.topic = topic; + + this.nameLC = this.name.toLowerCase(); + this.topicLC = this.topic.toLowerCase(); +} + +ChannelEntry.prototype = new XULTreeViewRecord(tree.share); + +ChannelEntry.prototype.sortCompare = +function chanentry_sortcmp(a, b) +{ + var sc = a._share.sortColumn; + var sd = a._share.sortDirection; + + // Make sure the special 'first' row is always first. + if ("first" in a) + return -1; + if ("first" in b) + return 1; + + if (sc == "users") + { + // Force a numeric comparison. + a = 1 * a[sc]; + b = 1 * b[sc]; + } + else + { + // Case-insensitive, please. + a = a[sc].toLowerCase(); + b = b[sc].toLowerCase(); + } + + if (a < b) + return -1 * sd; + + if (a > b) + return 1 * sd; + + return 0; +} diff --git a/comm/suite/chatzilla/xul/content/channels.xul b/comm/suite/chatzilla/xul/content/channels.xul new file mode 100644 index 0000000000..7770a7a0d9 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/channels.xul @@ -0,0 +1,108 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/channels.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/skin/channels.css" type="text/css"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + windowtype="irc:chatzilla:channels" + id="chatzilla-window" + buttons="cancel" + onload="onLoad()" + onunload="onUnload()" + ondialogaccept="return joinChannel()" + title="&window.title;"> + + <script src="chrome://chatzilla/content/lib/js/utils.js"/> + <script src="chrome://chatzilla/content/lib/js/file-utils.js"/> + <script src="chrome://chatzilla/content/lib/xul/tree-utils.js"/> + <script src="channels.js"/> + + <vbox flex="1"> + <hbox id="topPanel"> + <grid flex="1"> + <columns> + <column/><column flex="1"/><column id="rightPanel"/> + </columns> + <rows> + <row align="center"> + <label value="&network.label;" accesskey="&network.accesskey;" + control="network"/> + <menulist id="network" editable="true" tabindex="1" + oncommand="onFilter()" onblur="onFilter()" + onkeypress="onKeyPress(event)" onkeyup="update()"> + <menupopup id="networks" onpopupshowing="onShowingNetworks()"> + </menupopup> + </menulist> + <button id="join" disabled="true" default="true" tabindex="4" + label="&join.label;" accesskey="&join.accesskey;" + oncommand="if (joinChannel()) window.close()"/> + </row> + <row align="center"> + <label value="&channel.label;" accesskey="&channel.accesskey;" + control="channel"/> + <textbox id="channel" type="search" tabindex="2" + oncommand="onFilter()" onkeypress="onKeyPress(event)"/> + <hbox align="center"> + <label value="&minusers.label;" accesskey="&minusers.accesskey;" + control="minUsers"/> + <textbox id="minUsers" type="search" flex="1" tabindex="5" + oncommand="onFilter()"/> + </hbox> + </row> + <row align="center"> + <spacer/> + <checkbox id="includeTopic" checked="true" tabindex="3" + label="&topics.label;" accesskey="&topics.accesskey;" + oncommand="onFilter(); focusSearch()"/> + <hbox align="center"> + <label value="&maxusers.label;" accesskey="&maxusers.accesskey;" + control="maxUsers"/> + <textbox id="maxUsers" type="search" flex="1" tabindex="6" + oncommand="onFilter()"/> + </hbox> + </row> + <row align="center"> + <spacer/> + <label id="lastUpdated"/> + <button id="refresh" + tabindex="7" + label="&refreshNow.label;" + accesskey="&refreshNow.accesskey;" + oncommand="refreshList(); focusSearch();"/> + </row> + </rows> + </grid> + </hbox> + <deck id="bottomPanel" flex="1" selectedindex="0"> + <hbox pack="center" align="center"> + <label value="&network.hint.label;"/> + </hbox> + <vbox> + <tree id="channels" flex="1" hidecolumnpicker="true" seltype="single" tabindex="8" + onselect="onSelectionChange()"> + <treecols> + <treecol label="&col.name;" width="100" id="chanColName"/> + <splitter class="tree-splitter"/> + <treecol label="&col.users;" width="50" id="chanColUsers"/> + <splitter class="tree-splitter"/> + <treecol label="&col.topic;" flex="1" id="chanColTopic"/> + </treecols> + <treechildren flex="1"/> + </tree> + <hbox id="loadContainer"> + <label id="loadLabel" flex="1" crop="right"/> + <deck id="loadBarDeck"> + <progressmeter id="loadBar" mode="undetermined" /> + <box/> + </deck> + </hbox> + </vbox> + </deck> + </vbox> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/chatzilla.xul b/comm/suite/chatzilla/xul/content/chatzilla.xul new file mode 100644 index 0000000000..a543c1e6ae --- /dev/null +++ b/comm/suite/chatzilla/xul/content/chatzilla.xul @@ -0,0 +1,141 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE window SYSTEM "chrome://chatzilla/locale/chatzilla.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/skin/chatzilla.css" type="text/css"?> + +<?xul-overlay href="chrome://chatzilla/content/scripts.xul"?> +<?xul-overlay href="chrome://chatzilla/content/popups.xul"?> +<?xul-overlay href="chrome://chatzilla/content/menus.xul"?> + +<window id="chatzilla-window" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:aaa="http://www.w3.org/2005/07/aaa" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + orient="vertical" onload="onLoad();" onunload="onUnload();" + onclose="return onClose();" onmouseover="onMouseOver(event);" + persist="width height screenX screenY sizemode" windowtype="irc:chatzilla"> + + <script src="chrome://global/content/contentAreaUtils.js"/> + <script src="chrome://communicator/content/findUtils.js"/> + + <html:link rel="icon" href="chrome://chatzilla/skin/images/logo.png" style="display:none"/> + + <overlaytarget id="scripts-overlay-target"/> + <overlaytarget id="popup-overlay-target"/> + <overlaytarget id="menu-overlay-target"/> + <stringbundleset id="chatzilla-stringbundle"> + <stringbundle id="findBundle" src="chrome://global/locale/finddialog.properties"/> + </stringbundleset> + + <vbox id="upper-box" flex="1"> + <hbox id="tabpanels-contents-box" flex="1"> + <vbox id="user-list-box" width="125" persist="collapsed width"> + + <tree id="user-list" flex="1" hidecolumnpicker="true" + ondblclick="onUserDoubleClick(event);" + context="context:userlist" aaa:live="off" aria-live="off" + aaa:relevant="additions removals text" + aria-relevant="additions removals text"> + <treecols> + <treecol id="usercol" hideheader="true" flex="1"/> + </treecols> + <treechildren/> + </tree> + + </vbox> <!-- user-list-box --> + + <splitter id="main-splitter" collapse="before" persist="collapsed left"> + <grippy/> + </splitter> + + <vbox flex="1" id="browser-box"> + <deck id="output-deck" flex="1"/> + </vbox> + + </hbox> <!-- tabpanels-contents-box --> + + <vbox id="tabstrip-box" flex="0" crop="right"> + <hbox id="view-tabs" persist="collapsed" flex="1" + ondragover="tabsDNDObserver.onDragOver(event);" + ondragexit="tabsDNDObserver.onDragExit(event);" + ondrop="tabsDNDObserver.onDrop(event);"> + <tabs id="views-tbar-inner" flex="1" + onselect="onTabSelect(event)" setfocus="false"> + <tab hidden="true"/> <!-- dummy tab to keep the freaking xbl from + causing an exception --> + </tabs> + <spacer id="views-tbar-spacer"/> + </hbox> + <hbox id="tabs-drop-indicator-bar" collapsed="true"> + <hbox id="tabs-drop-indicator" mousethrough="always"/> + </hbox> + </vbox> + + </vbox> <!-- upper-box --> + + <splitter id="input-splitter" orient="vertical" collapse="after" + collapsed="true"/> + + <hbox id="input-widgets" align="center"> + <button id="server-nick" type="menu" label="" tooltiptext="&server-nick.tooltip;"/> + <hbox id="multiline-box" flex="1" collapsed="true"> + <box id="multiline-hug-box" flex="1"> + <textbox id="multiline-input" multiline="true" flex="1" height="100px" + class="multiline-input-widget" onfocus="onInputFocus();" + tabindex="-1"/> + </box> + <vbox> + <toolbarbutton id="button-input" flex="1" + oncommand="onMultilineSend(event);" + tooltiptext="&multiline-send.tooltip;" /> + <toolbarbutton id="button-multiline-contract" + oncommand="dispatch('pref multiline false');" + tooltiptext="&multiline-contract.tooltip;" /> + </vbox> + </hbox> + <hbox id="singleline-box" flex="1" collapsed="true"> + <box id="singleline-hug-box" flex="1"> + <textbox id="input" class="input-widget" flex="1" + onfocus="onInputFocus();" tabindex="-1"/> + </box> + <toolbarbutton id="button-multiline-expand" + oncommand="dispatch('pref multiline true');" + tooltiptext="&multiline-expand.tooltip;"/> + </hbox> + </hbox> + + <statusbar id="status-bar" + class="chromeclass-status" + persist="collapsed"> + <statusbarpanel id="component-bar"/> + <statusbarpanel id="status-text" label="" flex="1" crop="right"/> + <statusbarpanel id="status-progress-panel" class="statusbarpanel-progress"> + <progressmeter id="status-progress-bar" + class="progressmeter-statusbar" + mode="undetermined" + value="0"/> + </statusbarpanel> + <statusbarpanel id="security-button" + class="statusbarpanel-iconic-text" + dir="reverse" + label="" + oncommand="displayCertificateInfo();"/> + <statusbarpanel id="alert-status" + class="statusbarpanel-iconic" + oncommand="updateAlertIcon(true);"/> + <statusbarpanel id="logging-status" + class="statusbarpanel-iconic" + oncommand="onLoggingIcon();"/> + <statusbarpanel id="offline-status" + class="statusbarpanel-iconic" + oncommand="client.offlineObserver.toggleOffline();"/> + </statusbar> + +</window> diff --git a/comm/suite/chatzilla/xul/content/chatzillaOverlay.js b/comm/suite/chatzilla/xul/content/chatzillaOverlay.js new file mode 100644 index 0000000000..7841b40f20 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/chatzillaOverlay.js @@ -0,0 +1,11 @@ +/* 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 toIRC() +{ + + toOpenWindowByType("irc:chatzilla", "chrome://chatzilla/content/chatzilla.xul"); + +} + diff --git a/comm/suite/chatzilla/xul/content/chatzillaOverlay.xul b/comm/suite/chatzilla/xul/content/chatzillaOverlay.xul new file mode 100644 index 0000000000..755e4fd813 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/chatzillaOverlay.xul @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/chatzillaOverlay.dtd" > + +<!-- This is the overlay that addes "Chatzilla" to the (global) task menu. --> + +<?xml-stylesheet href="chrome://chatzilla/skin/chatzillaOverlay.css" type="text/css"?> + +<overlay id="ChatzillaTaskMenuID" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/x-javascript" src="chrome://chatzilla/content/chatzillaOverlay.js"/> + +<keyset id="tasksKeys"> + <key id="key_irc" key="&ircCmd.commandkey;" command="Tasks:IRC" modifiers="accel"/> +</keyset> + +<commandset id="tasksCommands"> + <command id="Tasks:IRC" oncommand="toIRC();"/> +</commandset> + +<menupopup id="windowPopup"> + <menuitem + label="&ircCmd.label;" + accesskey="&ircCmd.accesskey;" + key="key_irc" + command="Tasks:IRC" + id="tasksMenuIRC" + class="menuitem-iconic" + insertafter="tasksMenuAddressBook,tasksMenuEditor,IMMenuItem,tasksMenuNavigator"/> +</menupopup> + +<statusbarpanel id="component-bar"> + <toolbarbutton class="taskbutton" id="mini-irc" oncommand="toIRC()" + insertafter="mini-addr,mini-comp,mini-aim,mini-nav" tooltiptext="&ircCmd.label;"/> +</statusbarpanel> + +</overlay> diff --git a/comm/suite/chatzilla/xul/content/commands.js b/comm/suite/chatzilla/xul/content/commands.js new file mode 100644 index 0000000000..a99ca52c3e --- /dev/null +++ b/comm/suite/chatzilla/xul/content/commands.js @@ -0,0 +1,4760 @@ +/* -*- 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 CMD_CONSOLE = 0x01; +const CMD_NEED_NET = 0x02; +const CMD_NEED_SRV = 0x04; +const CMD_NEED_CHAN = 0x08; +const CMD_NEED_USER = 0x10; + +function initCommands() +{ + // Keep this in sync with the command.js section in chatzilla.properties. + var cmdary = + [/* "real" commands */ + ["about", cmdAbout, CMD_CONSOLE], + ["alias", cmdAlias, CMD_CONSOLE, + "[<alias-name> [<command-list>]]"], + ["attach", cmdAttach, CMD_CONSOLE, + "<irc-url>"], + ["away", cmdAway, CMD_CONSOLE, + "[<reason>]"], + ["back", cmdAway, CMD_CONSOLE], + ["ban", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE, + "[<nickname>]"], + ["cancel", cmdCancel, CMD_CONSOLE], + ["charset", cmdCharset, CMD_CONSOLE, + "[<new-charset>]"], + ["channel-motif", cmdMotif, CMD_NEED_CHAN | CMD_CONSOLE, + "[<motif> [<channel>]]"], + ["channel-pref", cmdPref, CMD_NEED_CHAN | CMD_CONSOLE, + "[<pref-name> [<pref-value>]]"], + ["cmd-undo", "cmd-docommand cmd_undo", 0], + ["cmd-redo", "cmd-docommand cmd_redo", 0], + ["cmd-cut", "cmd-docommand cmd_cut", 0], + ["cmd-copy", "cmd-docommand cmd_copy", 0], + ["cmd-paste", "cmd-docommand cmd_paste", 0], + ["cmd-delete", "cmd-docommand cmd_delete", 0], + ["cmd-selectall", "cmd-docommand cmd_selectAll", 0], + ["cmd-copy-link-url", "cmd-docommand cmd_copyLink", 0, + "<url>"], + ["cmd-mozilla-prefs", "cmd-docommand cmd_mozillaPrefs", 0], + ["cmd-prefs", "cmd-docommand cmd_chatzillaPrefs", 0], + ["cmd-chatzilla-prefs", "cmd-docommand cmd_chatzillaPrefs", 0], + ["cmd-chatzilla-opts", "cmd-docommand cmd_chatzillaPrefs", 0], + ["cmd-docommand", cmdDoCommand, 0, + "<cmd-name>"], + ["create-tab-for-view", cmdCreateTabForView, 0, + "<view>"], + ["custom-away", cmdAway, 0], + ["op", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<...>]"], + ["dcc-accept", cmdDCCAccept, CMD_CONSOLE, + "[<nickname> [<type> [<file>]]]"], + ["dcc-accept-list", cmdDCCAutoAcceptList, CMD_NEED_NET | CMD_CONSOLE], + ["dcc-accept-list-add", cmdDCCAutoAcceptAdd, + CMD_NEED_NET | CMD_CONSOLE, + "<nickname>"], + ["dcc-accept-list-remove", cmdDCCAutoAcceptDel, + CMD_NEED_NET | CMD_CONSOLE, + "<nickname>"], + ["dcc-chat", cmdDCCChat, CMD_NEED_SRV | CMD_CONSOLE, + "[<nickname>]"], + ["dcc-close", cmdDCCClose, CMD_CONSOLE, + "[<nickname> [<type> [<file>]]]"], + ["dcc-decline", cmdDCCDecline, CMD_CONSOLE, + "[<nickname>]"], + ["dcc-list", cmdDCCList, CMD_CONSOLE, + "[<type>]"], + ["dcc-send", cmdDCCSend, CMD_NEED_SRV | CMD_CONSOLE, + "[<nickname> [<file>]]"], + ["dcc-show-file", cmdDCCShowFile, CMD_CONSOLE, + "<file>"], + ["delayed", cmdDelayed, CMD_CONSOLE, + "<delay> <rest>"], + ["deop", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<...>]"], + ["describe", cmdDescribe, CMD_NEED_SRV | CMD_CONSOLE, + "<target> <action>"], + ["hop", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<...>]"], + ["dehop", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<...>]"], + ["voice", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<...>]"], + ["devoice", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<...>]"], + ["clear-view", cmdClearView, CMD_CONSOLE, + "[<view>]"], + ["client", cmdClient, CMD_CONSOLE], + ["commands", cmdCommands, CMD_CONSOLE, + "[<pattern>]"], + ["ctcp", cmdCTCP, CMD_NEED_SRV | CMD_CONSOLE, + "<target> <code> [<params>]"], + ["default-charset", cmdCharset, CMD_CONSOLE, + "[<new-charset>]"], + ["delete-view", cmdDeleteView, CMD_CONSOLE, + "[<view>]"], + ["desc", cmdDesc, CMD_CONSOLE, + "[<description>]"], + ["disable-plugin", cmdDisablePlugin, CMD_CONSOLE], + ["disconnect", cmdDisconnect, CMD_NEED_SRV | CMD_CONSOLE, + "[<reason>]"], + ["disconnect-all", cmdDisconnectAll, CMD_CONSOLE, + "[<reason>]"], + ["echo", cmdEcho, CMD_CONSOLE, + "<message>"], + ["edit-networks", cmdEditNetworks, CMD_CONSOLE], + ["enable-plugin", cmdEnablePlugin, CMD_CONSOLE, + "<plugin>"], + ["eval", cmdEval, CMD_CONSOLE, + "<expression>"], + ["evalsilent", cmdEval, CMD_CONSOLE, + "<expression>"], + ["except", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE, + "[<nickname>]"], + ["find", cmdFind, 0, + "[<rest>]"], + ["find-again", cmdFindAgain, 0], + ["focus-input", cmdFocusInput, 0], + ["font-family", cmdFont, CMD_CONSOLE, + "[<font>]"], + ["font-family-other", cmdFont, 0], + ["font-size", cmdFont, CMD_CONSOLE, + "[<font-size>]"], + ["font-size-other", cmdFont, 0], + ["goto-startup", cmdGotoStartup, CMD_CONSOLE], + ["goto-url", cmdGotoURL, 0, + "<url> [<anchor>]"], + ["goto-url-newwin", cmdGotoURL, 0, + "<url> [<anchor>]"], + ["goto-url-newtab", cmdGotoURL, 0, + "<url> [<anchor>]"], + ["help", cmdHelp, CMD_CONSOLE, + "[<pattern>]"], + ["hide-view", cmdHideView, CMD_CONSOLE, + "[<view>]"], + ["identify", cmdIdentify, CMD_NEED_SRV | CMD_CONSOLE, + "[<password>]"], + ["idle-away", cmdAway, 0], + ["idle-back", cmdAway, 0], + ["ignore", cmdIgnore, CMD_NEED_NET | CMD_CONSOLE, + "[<mask>]"], + ["input-text-direction", cmdInputTextDirection, 0, + "<dir>"], + ["install-plugin", cmdInstallPlugin, CMD_CONSOLE, + "[<url> [<name>]]"], + ["invite", cmdInvite, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<channel-name>]"], + ["join", cmdJoin, CMD_NEED_SRV | CMD_CONSOLE, + "[<channel-name> [<key>]]"], + ["join-charset", cmdJoin, CMD_NEED_SRV | CMD_CONSOLE, + "[<channel-name> <charset> [<key>]]"], + ["jump-to-anchor", cmdJumpToAnchor, CMD_NEED_NET, + "<anchor> [<channel-name>]"], + ["kick", cmdKick, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<reason>]"], + ["kick-ban", cmdKick, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname> [<reason>]"], + ["knock", cmdKnock, CMD_NEED_SRV | CMD_CONSOLE, + "<channel-name> [<reason>]"], + ["leave", cmdLeave, CMD_NEED_NET | CMD_CONSOLE, + "[<channel-name>] [<reason>]"], + ["links", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE], + ["list", cmdList, CMD_NEED_SRV | CMD_CONSOLE, + "[<channel-name>]"], + ["list-plugins", cmdListPlugins, CMD_CONSOLE, + "[<plugin>]"], + ["load", cmdLoad, CMD_CONSOLE, + "<url>"], + ["log", cmdLog, CMD_CONSOLE, + "[<state>]"], + ["map", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE], + ["marker", cmdMarker, CMD_CONSOLE], + ["marker-clear", cmdMarker, CMD_CONSOLE], + ["marker-set", cmdMarker, CMD_CONSOLE], + ["match-users", cmdMatchUsers, CMD_NEED_CHAN | CMD_CONSOLE, + "<mask>"], + ["me", cmdMe, CMD_CONSOLE, + "<action>"], + ["motd", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE], + ["mode", cmdMode, CMD_NEED_SRV | CMD_CONSOLE, + "[<target>] [<modestr> [<param> [<...>]]]"], + ["motif", cmdMotif, CMD_CONSOLE, + "[<motif>]"], + ["msg", cmdMsg, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> <message>"], + ["name", cmdName, CMD_CONSOLE, + "[<username>]"], + ["names", cmdNames, CMD_NEED_SRV | CMD_CONSOLE, + "[<channel-name>]"], + ["network", cmdNetwork, CMD_CONSOLE, + "<network-name>"], + ["network-motif", cmdMotif, CMD_NEED_NET | CMD_CONSOLE, + "[<motif> [<network>]]"], + ["network-pref", cmdPref, CMD_NEED_NET | CMD_CONSOLE, + "[<pref-name> [<pref-value>]]"], + ["networks", cmdNetworks, CMD_CONSOLE], + ["nick", cmdNick, CMD_CONSOLE, + "[<nickname>]"], + ["notice", cmdNotice, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> <message>"], + ["notify", cmdNotify, CMD_NEED_SRV | CMD_CONSOLE, + "[<nickname> [<...>]]"], + ["open-at-startup", cmdOpenAtStartup, CMD_CONSOLE, + "[<toggle>]"], + ["oper", cmdOper, CMD_NEED_SRV | CMD_CONSOLE, + "<opername> [<password>]"], + ["ping", cmdPing, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname>"], + ["plugin-pref", cmdPref, CMD_CONSOLE, + "<plugin> [<pref-name> [<pref-value>]]"], + ["pref", cmdPref, CMD_CONSOLE, + "[<pref-name> [<pref-value>]]"], + ["print", cmdPrint, CMD_CONSOLE], + ["query", cmdQuery, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<message>]"], + ["quit", cmdQuit, CMD_CONSOLE, + "[<reason>]"], + ["quote", cmdQuote, CMD_NEED_NET | CMD_CONSOLE, + "<irc-command>"], + ["rename", cmdRename, CMD_CONSOLE, + "[<label>]"], + ["reload-plugin", cmdReload, CMD_CONSOLE, + "<plugin>"], + ["rlist", cmdRlist, CMD_NEED_SRV | CMD_CONSOLE, + "<regexp>"], + ["reconnect", cmdReconnect, CMD_NEED_NET | CMD_CONSOLE, + "[<reason>]"], + ["reconnect-all", cmdReconnectAll, CMD_CONSOLE, + "[<reason>]"], + ["rejoin", cmdRejoin, + CMD_NEED_SRV | CMD_NEED_CHAN | CMD_CONSOLE, + "[<reason>]"], + ["reload-ui", cmdReloadUI, 0], + ["save", cmdSave, CMD_CONSOLE, + "[<filename> [<savetype>]]"], + ["say", cmdSay, CMD_CONSOLE, + "<message>"], + ["server", cmdServer, CMD_CONSOLE, + "<hostname> [<port> [<password>]]"], + ["set-current-view", cmdSetCurrentView, 0, + "<view>"], + ["stats", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE, + "[<params>]"], + ["squery", cmdSquery, CMD_NEED_SRV | CMD_CONSOLE, + "<service> [<commands>]"], + ["sslserver", cmdServer, CMD_CONSOLE, + "<hostname> [<port> [<password>]]"], + ["ssl-exception", cmdSSLException, 0, + "[<hostname> <port> [<connect>]]"], + ["stalk", cmdStalk, CMD_CONSOLE, + "[<text>]"], + ["supports", cmdSupports, CMD_NEED_SRV | CMD_CONSOLE], + ["sync-font", cmdSync, 0], + ["sync-header", cmdSync, 0], + ["sync-log", cmdSync, 0], + ["sync-motif", cmdSync, 0], + ["sync-timestamp", cmdSync, 0], + ["sync-window", cmdSync, 0], + ["testdisplay", cmdTestDisplay, CMD_CONSOLE], + ["text-direction", cmdTextDirection, 0, + "<dir>"], + ["time", cmdTime, CMD_NEED_SRV | CMD_CONSOLE, + "[<nickname>]"], + ["timestamps", cmdTimestamps, CMD_CONSOLE, + "[<toggle>]"], + ["toggle-ui", cmdToggleUI, CMD_CONSOLE, + "<thing>"], + ["toggle-pref", cmdTogglePref, 0, + "<pref-name>"], + ["toggle-group", cmdToggleGroup, 0, + "<group-id>"], + ["topic", cmdTopic, CMD_NEED_CHAN | CMD_CONSOLE, + "[<new-topic>]"], + ["unalias", cmdAlias, CMD_CONSOLE, + "<alias-name>"], + ["unignore", cmdIgnore, CMD_NEED_NET | CMD_CONSOLE, + "<mask>"], + ["unban", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE, + "<nickname>"], + ["unexcept", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE], + ["uninstall-plugin", cmdUninstallPlugin, CMD_CONSOLE, + "<plugin>"], + ["unstalk", cmdUnstalk, CMD_CONSOLE, + "<text>"], + ["urls", cmdURLs, CMD_CONSOLE, + "[<number>]"], + ["user", cmdUser, CMD_CONSOLE, + "[<username> <description>]"], + ["userhost", cmdUserhost, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<...>]"], + ["userip", cmdUserip, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<...>]"], + ["usermode", cmdUsermode, CMD_CONSOLE, + "[<new-mode>]"], + ["user-motif", cmdMotif, CMD_NEED_USER | CMD_CONSOLE, + "[<motif> [<user>]]"], + ["user-pref", cmdPref, CMD_NEED_USER | CMD_CONSOLE, + "[<pref-name> [<pref-value>]]"], + ["version", cmdVersion, CMD_NEED_SRV | CMD_CONSOLE, + "[<nickname>]"], + ["websearch", cmdWebSearch, CMD_CONSOLE, + "<selected-text>"], + ["who", cmdWho, CMD_NEED_SRV | CMD_CONSOLE, + "<rest>"], + ["whois", cmdWhoIs, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<...>]"], + ["whowas", cmdWhoWas, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<limit>]"], + ["wii", cmdWhoIsIdle, CMD_NEED_SRV | CMD_CONSOLE, + "<nickname> [<...>]"], + + /* aliases */ + ["exit", "quit", CMD_CONSOLE, + "[<reason>]"], + ["j", "join", CMD_CONSOLE, + "[<channel-name> [<key>]]"], + ["pass", "quote PASS", CMD_CONSOLE, + "<password>"], + ["part", "leave", CMD_CONSOLE], + ["raw", "quote", CMD_CONSOLE], + // Shortcuts to useful URLs: + ["faq", "goto-url-newtab faq", 0], + ["homepage", "goto-url-newtab homepage", 0], + // Used to display a nickname in the menu only. + ["label-user", "echo", 0, + "<unspecified>"], + ["label-user-multi", "echo", 0, + "<unspecified>"], + // These are all the font family/size menu commands... + ["font-family-default", "font-family default", 0], + ["font-family-serif", "font-family serif", 0], + ["font-family-sans-serif", "font-family sans-serif", 0], + ["font-family-monospace", "font-family monospace", 0], + ["font-size-default", "font-size default", 0], + ["font-size-small", "font-size small", 0], + ["font-size-medium", "font-size medium", 0], + ["font-size-large", "font-size large", 0], + ["font-size-bigger", "font-size bigger", 0], + // This next command is not visible; it maps to Ctrl-=, which is what + // you get when the user tries to do Ctrl-+ (previous command's key). + ["font-size-bigger2", "font-size bigger", 0], + ["font-size-smaller", "font-size smaller", 0], + ["toggle-oas", "open-at-startup toggle", 0], + ["toggle-ccm", "toggle-pref collapseMsgs", 0], + ["toggle-copy", "toggle-pref copyMessages", 0], + ["toggle-usort", "toggle-pref sortUsersByMode", 0], + ["toggle-umode", "toggle-pref showModeSymbols", 0], + ["toggle-timestamps","timestamps toggle", 0], + ["motif-dark", "motif dark", 0], + ["motif-light", "motif light", 0], + ["sync-output", "evalsilent syncOutputFrame(this)", 0], + ["userlist", "toggle-ui userlist", CMD_CONSOLE], + ["tabstrip", "toggle-ui tabstrip", CMD_CONSOLE], + ["statusbar", "toggle-ui status", CMD_CONSOLE], + ["header", "toggle-ui header", CMD_CONSOLE], + + // text-direction aliases + ["rtl", "text-direction rtl", CMD_CONSOLE], + ["ltr", "text-direction ltr", CMD_CONSOLE], + ["toggle-text-dir", "text-direction toggle", 0], + ["irtl", "input-text-direction rtl", CMD_CONSOLE], + ["iltr", "input-text-direction ltr", CMD_CONSOLE], + // Services aliases + ["cs", "quote cs", 0], + ["ms", "quote ms", 0], + ["ns", "quote ns", 0] + ]; + + // set the stringbundle associated with these commands. + cmdary.stringBundle = client.defaultBundle; + + client.commandManager = new CommandManager(client.defaultBundle); + client.commandManager.defaultFlags = CMD_CONSOLE; + client.commandManager.isCommandSatisfied = isCommandSatisfied; + client.commandManager.defineCommands(cmdary); + + var restList = ["reason", "action", "text", "message", "params", "font", + "expression", "ircCommand", "prefValue", "newTopic", "file", + "password", "commandList", "commands", "description", + "selectedText"]; + var stateList = ["connect"]; + + client.commandManager.argTypes.__aliasTypes__(restList, "rest"); + client.commandManager.argTypes.__aliasTypes__(stateList, "state"); + client.commandManager.argTypes["plugin"] = parsePlugin; +} + +function isCommandSatisfied(e, command) +{ + if (typeof command == "undefined") + command = e.command; + else if (typeof command == "string") + command = this.commands[command]; + + if (command.flags & CMD_NEED_USER) + { + if (!("user" in e) || !e.user) + { + e.parseError = getMsg(MSG_ERR_NEED_USER, command.name); + return false; + } + } + + if (command.flags & CMD_NEED_CHAN) + { + if (!("channel" in e) || !e.channel) + { + e.parseError = getMsg(MSG_ERR_NEED_CHANNEL, command.name); + return false; + } + } + + if (command.flags & CMD_NEED_SRV) + { + if (!("server" in e) || !e.server) + { + e.parseError = getMsg(MSG_ERR_NEED_SERVER, command.name); + return false; + } + + if (e.network.state != NET_ONLINE) + { + e.parseError = MSG_ERR_NOT_CONNECTED; + return false; + } + } + + if (command.flags & (CMD_NEED_NET | CMD_NEED_SRV | CMD_NEED_CHAN)) + { + if (!("network" in e) || !e.network) + { + e.parseError = getMsg(MSG_ERR_NEED_NETWORK, command.name); + return false; + } + } + + return CommandManager.prototype.isCommandSatisfied(e, command); +} + +CIRCChannel.prototype.dispatch = +CIRCNetwork.prototype.dispatch = +CIRCUser.prototype.dispatch = +CIRCDCCChat.prototype.dispatch = +CIRCDCCFileTransfer.prototype.dispatch = +client.dispatch = +function this_dispatch(text, e, isInteractive, flags) +{ + e = getObjectDetails(this, e); + return dispatch(text, e, isInteractive, flags); +} + +function dispatch(text, e, isInteractive, flags) +{ + if (typeof isInteractive == "undefined") + isInteractive = false; + + if (!e) + e = new Object(); + + if (!("sourceObject" in e)) + e.__proto__ = getObjectDetails(client.currentObject); + + if (!("isInteractive" in e)) + e.isInteractive = isInteractive; + + if (!("inputData" in e)) + e.inputData = ""; + + /* split command from arguments */ + var ary = text.match(/(\S+) ?(.*)/); + if (!ary) + { + display(getMsg(MSG_ERR_UNKNOWN_COMMAND, "")); + return null; + } + + e.commandText = ary[1]; + if (ary[2]) + e.inputData = stringTrim(ary[2]); + + /* list matching commands */ + ary = client.commandManager.list(e.commandText, flags, true); + var rv = null; + var i; + + switch (ary.length) + { + case 0: + /* no match, try again */ + if (e.server && e.server.isConnected && + client.prefs["guessCommands"]) + { + /* Want to keep the source details. */ + var e2 = getObjectDetails(e.sourceObject); + e2.inputData = e.commandText + " " + e.inputData; + return dispatch("quote", e2); + } + + display(getMsg(MSG_ERR_UNKNOWN_COMMAND, e.commandText), MT_ERROR); + break; + + case 1: + /* one match, good for you */ + var cm = client.commandManager; + + if (cm.currentDispatchDepth >= cm.maxDispatchDepth) + { + /* We've reatched the max dispatch depth, so we need to unwind + * the entire stack of commands. + */ + cm.dispatchUnwinding = true; + } + // Don't start any new commands while unwinding. + if (cm.dispatchUnwinding) + break; + + cm.currentDispatchDepth++; + + var ex; + try + { + rv = dispatchCommand(ary[0], e, flags); + } + catch (ex) + { + display(getMsg(MSG_ERR_INTERNAL_DISPATCH, ary[0].name), + MT_ERROR); + display(formatException(ex), MT_ERROR); + if (typeof ex == "object" && "stack" in ex) + dd(formatException(ex) + "\n" + ex.stack); + else + dd(formatException(ex), MT_ERROR); + } + + cm.currentDispatchDepth--; + if (cm.dispatchUnwinding && (cm.currentDispatchDepth == 0)) + { + /* Last level to unwind, and this is where we display the + * message. We need to leave it until here because displaying + * a message invokes a couple of commands itself, and we need + * to not be right on the dispatch limit for that. + */ + cm.dispatchUnwinding = false; + display(getMsg(MSG_ERR_MAX_DISPATCH_DEPTH, ary[0].name), + MT_ERROR); + } + break; + + default: + /* more than one match, show the list */ + var str = ""; + for (i in ary) + str += (str) ? ", " + ary[i].name : ary[i].name; + display(getMsg(MSG_ERR_AMBIGCOMMAND, + [e.commandText, ary.length, str]), MT_ERROR); + } + + return rv; +} + +function dispatchCommand (command, e, flags) +{ + function displayUsageError (e, details) + { + if (!("isInteractive" in e) || !e.isInteractive) + { + var caller = Components.stack.caller.caller; + if (caller.name == "dispatch") + caller = caller.caller; + var error = new Error (details); + error.fileName = caller.filename; + error.lineNumber = caller.lineNumber; + error.name = caller.name; + display (formatException(error), MT_ERROR); + } + else + { + display (details, MT_ERROR); + } + + //display (getMsg(MSG_FMT_USAGE, [e.command.name, e.command.usage]), + // MT_USAGE); + return null; + }; + + function callHooks (command, isBefore) + { + var names, hooks; + + if (isBefore) + hooks = command.beforeHooks; + else + hooks = command.afterHooks; + + for (var h in hooks) + { + if ("dbgDispatch" in client && client.dbgDispatch) + { + dd ("calling " + (isBefore ? "before" : "after") + + " hook " + h); + } + try + { + hooks[h](e); + } + catch (ex) + { + if (e.command.name != "hook-session-display") + { + display(getMsg(MSG_ERR_INTERNAL_HOOK, h), MT_ERROR); + display(formatException(ex), MT_ERROR); + } + else + { + dd(getMsg(MSG_ERR_INTERNAL_HOOK, h)); + } + + dd("Caught exception calling " + + (isBefore ? "before" : "after") + " hook " + h); + dd(formatException(ex)); + if (typeof ex == "object" && "stack" in ex) + dd(ex.stack); + else + dd(getStackTrace()); + } + } + }; + + e.command = command; + + if (!e.command.enabled) + { + /* disabled command */ + display (getMsg(MSG_ERR_DISABLED, e.command.name), + MT_ERROR); + return null; + } + + function parseAlias(aliasLine, e) { + /* Only 1 of these will be presented to the user. Math.max is used to + supply the 'worst' error */ + const ALIAS_ERR_REQ_PRMS = 1; + const ALIAS_ERR_REQ_SRV = 2; + const ALIAS_ERR_REQ_RECIP = 3; + + /* double slashes because of the string to regexp conversion, which + turns these into single slashes */ + const SIMPLE_REPLACE = "\\$\\((\\d+)\\)"; + const CUMUL_REPLACE = "\\$\\((\\d+)\\+\\)"; + const RANGE_REPLACE = "\\$\\((\\d+)\\-(\\d+)\\)"; + const NICK_REPLACE = "\\$\\((nick)\\)"; + const RECIP_REPLACE = "\\$\\((recip)\\)"; + const ALL_REPLACE = "\\$\\((all)\\)"; + if (!aliasLine.match(/\$/)) + { + if (e.inputData) + display(getMsg(MSG_EXTRA_PARAMS, e.inputData), MT_WARN); + return aliasLine; + } + + function replaceAll(match, single, cumulative, start, end, nick, recip, all) + { + if (single) + { + // Simple 1-parameter replace + if (arrayHasElementAt(parameters, single - 1)) + { + paramsUsed = Math.max(paramsUsed, single); + return parameters[single-1]; + } + maxParamsAsked = Math.max(maxParamsAsked, single); + errorMsg = Math.max(ALIAS_ERR_REQ_PRMS, errorMsg); + return match; + } + if (cumulative) + { + // Cumulative Replace: parameters cumulative and up + if (arrayHasElementAt(parameters, cumulative - 1)) + { + paramsUsed = parameters.length; + // there are never leftover parameters for $(somenumber+) + return parameters.slice(cumulative - 1).join(" "); + } + maxParamsAsked = Math.max(maxParamsAsked, cumulative); + errorMsg = Math.max(ALIAS_ERR_REQ_PRMS, errorMsg); + return match; + } + if (start && end) + { + // Ranged replace: parameters start through end + //'decrement to correct 0-based index. + if (start > end) + { + var iTemp = end; + end = start; + start = iTemp; + // We obviously have a very stupid user, but we're nice + } + start--; + if (arrayHasElementAt(parameters, start) && + arrayHasElementAt(parameters, end - 1)) + { + paramsUsed = Math.max(paramsUsed,end); + return parameters.slice(start, end).join(" "); + } + maxParamsAsked = Math.max(maxParamsAsked, end); + errorMsg = Math.max(ALIAS_ERR_REQ_PRMS, errorMsg); + return match; + } + if (nick) + { + // Replace with own nickname + if (e.network && e.server && e.network.state == NET_ONLINE) + return e.server.me.unicodeName; + + errorMsg = Math.max(ALIAS_ERR_REQ_SRV, errorMsg); + return null; + } + if (recip) + { + // Replace with current recipient + if (e.channel) + return e.channel.unicodeName; + + if (e.user) + return e.user.unicodeName; + + errorMsg = ALIAS_ERR_REQ_RECIP; + return null; + } + // Replace with all parameters + paramsUsed = parameters.length; + return parameters.join(" "); + }; + + // If the replace function has a problem, this is an error constant: + var errorMsg = 0; + + var paramsUsed = 0; + var maxParamsAsked = 0; + + /* set parameters array and escaping \ and ; in parameters so the + * parameters don't get split up by the command list split later on */ + e.inputData = e.inputData.replace(/([\\;])/g, "\\$1"); + var parameters = e.inputData.match(/\S+/g); + if (!parameters) + parameters = []; + + // replace in the command line. + var expr = [SIMPLE_REPLACE, CUMUL_REPLACE, RANGE_REPLACE, NICK_REPLACE, + RECIP_REPLACE, ALL_REPLACE].join("|"); + aliasLine = aliasLine.replace(new RegExp(expr, "gi"), replaceAll); + + if (errorMsg) + { + switch (errorMsg) + { + case ALIAS_ERR_REQ_PRMS: + display(getMsg(MSG_ERR_REQUIRED_NR_PARAM, + [maxParamsAsked - parameters.length, + maxParamsAsked]), MT_ERROR); + break; + case ALIAS_ERR_REQ_SRV: + display(getMsg(MSG_ERR_NEED_SERVER, e.command.name), + MT_ERROR); + break; + case ALIAS_ERR_REQ_RECIP: + display(getMsg(MSG_ERR_NEED_RECIP, e.command.name), + MT_ERROR); + break; + } + return null; + } + + // return the revised command line. + if (paramsUsed < parameters.length) + { + var pmstring = parameters.slice(paramsUsed, + parameters.length).join(" "); + display(getMsg(MSG_EXTRA_PARAMS, pmstring), MT_WARN); + } + return aliasLine; + }; + + function callBeforeHooks() + { + if ("beforeHooks" in client.commandManager) + callHooks(client.commandManager, true); + if ("beforeHooks" in e.command) + callHooks(e.command, true); + }; + + function callAfterHooks() + { + if ("afterHooks" in e.command) + callHooks(e.command, false); + if ("afterHooks" in client.commandManager) + callHooks(client.commandManager, false); + }; + + var h, i; + + if (typeof e.command.func == "function") + { + /* dispatch a real function */ + + client.commandManager.parseArguments(e); + if ("parseError" in e) + return displayUsageError(e, e.parseError); + + if (("dbgDispatch" in client) && client.dbgDispatch) + { + var str = ""; + for (i = 0; i < e.command.argNames.length; ++i) + { + var name = e.command.argNames[i]; + if (name in e) + str += " " + name + ": " + e[name]; + else if (name != ":") + str += " ?" + name; + } + dd(">>> " + e.command.name + str + " <<<"); + } + + callBeforeHooks(); + try + { + e.returnValue = e.command.func(e); + } + finally + { + callAfterHooks(); + /* set client.lastEvent *after* dispatching, so the dispatched + * function actually get's a chance to see the last event. */ + if (("dbgDispatch" in client) && client.dbgDispatch) + client.lastEvent = e; + } + } + else if (typeof e.command.func == "string") + { + /* dispatch an alias (semicolon delimited list of subcommands) */ + + var commandList; + //Don't make use of e.inputData if we have multiple commands in 1 alias + if (e.command.func.match(/\$\(.*\)|(?:^|[^\\])(?:\\\\)*;/)) + commandList = parseAlias(e.command.func, e); + else + commandList = e.command.func + " " + e.inputData; + + if (commandList == null) + return null; + commandList = commandList.split(";"); + + i = 0; + while (i < commandList.length) { + if (commandList[i].match(/(?:^|[^\\])(?:\\\\)*$/) || + (i == commandList.length - 1)) + { + commandList[i] = commandList[i].replace(/\\(.)/g, "$1"); + i++; + } + else + { + commandList[i] = commandList[i] + ";" + commandList[i + 1]; + commandList.splice(i + 1, 1); + } + } + + callBeforeHooks(); + try + { + for (i = 0; i < commandList.length; ++i) + { + var newEvent = Clone(e); + delete newEvent.command; + commandList[i] = stringTrim(commandList[i]); + dispatch(commandList[i], newEvent, flags); + } + } + finally + { + callAfterHooks(); + } + } + else + { + display(getMsg(MSG_ERR_NOTIMPLEMENTED, e.command.name), MT_ERROR); + return null; + } + + return ("returnValue" in e) ? e.returnValue : null; +} + +/* parse function for <plugin> parameters */ +function parsePlugin(e, name) +{ + var ary = e.unparsedData.match(/(?:(\S+))(?:\s+(.*))?$/); + if (!ary) + return false; + + var plugin; + + if (ary[1]) + { + plugin = getPluginById(ary[1]); + if (!plugin) + return false; + + } + + e.unparsedData = ary[2] || ""; + e[name] = plugin; + return true; +} + +function getToggle (toggle, currentState) +{ + if (toggle == "toggle") + toggle = !currentState; + + return toggle; +} + +/****************************************************************************** + * command definitions from here on down. + */ + +function cmdDisablePlugin(e) +{ + disablePlugin(e.plugin, false); +} + +function cmdEnablePlugin(e) +{ + if (e.plugin.enabled) + { + display(getMsg(MSG_IS_ENABLED, e.plugin.id)); + return; + } + + if (e.plugin.API > 0) + { + if (!e.plugin.enable()) + { + display(getMsg(MSG_CANT_ENABLE, e.plugin.id)); + e.plugin.prefs["enabled"] = false; + return; + } + e.plugin.prefs["enabled"] = true; + } + else if (!("enablePlugin" in e.plugin.scope)) + { + display(getMsg(MSG_CANT_ENABLE, e.plugin.id)); + return; + } + else + { + e.plugin.scope.enablePlugin(); + } + + display(getMsg(MSG_PLUGIN_ENABLED, e.plugin.id)); + e.plugin.enabled = true; +} + +function cmdBanOrExcept(e) +{ + var modestr; + switch (e.command.name) + { + case "ban": + modestr = "+bbbb"; + break; + + case "unban": + modestr = "-bbbb"; + break; + + case "except": + modestr = "+eeee"; + break; + + case "unexcept": + modestr = "-eeee"; + break; + + default: + ASSERT(0, "Dispatch from unknown name " + e.command.name); + return; + } + + /* If we're unbanning, or banning in odd cases, we may actually be talking + * about a user who is not in the channel, so we need to check the server + * for information as well. + */ + if (!e.user && e.nickname) + e.user = e.channel.getUser(e.nickname); + if (!e.user && e.nickname) + e.user = e.server.getUser(e.nickname); + + var masks = new Array(); + + if (e.userList) + { + for (var i = 0; i < e.userList.length; i++) + masks.push(fromUnicode(e.userList[i].getBanMask(), e.server)); + } + else if (e.user) + { + // We have a real user object, so get their proper 'ban mask'. + masks = [fromUnicode(e.user.getBanMask(), e.server)]; + } + else if (e.nickname) + { + /* If we have either ! or @ in the nickname assume the user has given + * us a complete mask and pass it directly, otherwise assume it is + * only the nickname and use * for username/host. + */ + masks = [fromUnicode(e.nickname, e.server)]; + if (!/[!@]/.test(e.nickname)) + masks[0] = masks[0] + "!*@*"; + } + else + { + // Nothing specified, so we want to list the bans/excepts. + masks = [""]; + } + + // Collapses into groups we can do individually. + masks = combineNicks(masks); + + for (var i = 0; i < masks.length; i++) + { + e.server.sendData("MODE " + e.channel.encodedName + " " + + modestr.substr(0, masks[i].count + 1) + + " " + masks[i] + "\n"); + } +} + +function cmdCancel(e) +{ + if (e.network && e.network.isRunningList()) + { + // We're running a /list, terminate the output so we return to sanity. + display(MSG_CANCELLING_LIST); + return e.network.abortList(); + } + + if (e.network && ((e.network.state == NET_CONNECTING) || + (e.network.state == NET_WAITING))) + { + // We're trying to connect to a network, and want to cancel. Do so: + if (e.deleteWhenDone) + e.network.deleteWhenDone = true; + + display(getMsg(MSG_CANCELLING, e.network.unicodeName)); + return e.network.cancel(); + } + + // If we're transferring a file, abort it. + var source = e.sourceObject; + if ((source.TYPE == "IRCDCCFileTransfer") && source.isActive()) + return source.abort(); + + display(MSG_NOTHING_TO_CANCEL, MT_ERROR); +} + +function cmdChanUserMode(e) +{ + var modestr; + switch (e.command.name) + { + case "op": + modestr = "+oooo"; + break; + + case "deop": + modestr = "-oooo"; + break; + + case "hop": + modestr = "+hhhh"; + break; + + case "dehop": + modestr = "-hhhh"; + break; + + case "voice": + modestr = "+vvvv"; + break; + + case "devoice": + modestr = "-vvvv"; + break; + + default: + ASSERT(0, "Dispatch from unknown name " + e.command.name); + return; + } + + var nicks; + var user; + var nickList = new Array(); + // Prefer pre-canonicalised list, then a * passed to the command directly, + // then a normal list, then finally a singular item (canon. or otherwise). + if (e.canonNickList) + { + nicks = combineNicks(e.canonNickList); + } + else if (e.nickname && (e.nickname == "*")) + { + var me = e.server.me; + var mode = modestr.substr(1, 1); + var adding = modestr[0] == "+"; + for (userKey in e.channel.users) + { + var user = e.channel.users[userKey]; + /* Never change our own mode and avoid trying to change someone + * else in a no-op manner (e.g. voicing an already voiced user). + */ + if ((user.encodedName != me.encodedName) && + (arrayContains(user.modes, mode) ^ adding)) + { + nickList.push(user.encodedName); + } + } + nicks = combineNicks(nickList); + } + else if (e.nicknameList) + { + for (var i = 0; i < e.nicknameList.length; i++) + { + user = e.channel.getUser(e.nicknameList[i]); + if (!user) + { + display(getMsg(MSG_ERR_UNKNOWN_USER, e.nicknameList[i]), MT_ERROR); + return; + } + nickList.push(user.encodedName); + } + nicks = combineNicks(nickList); + } + else if (e.nickname) + { + user = e.channel.getUser(e.nickname); + if (!user) + { + display(getMsg(MSG_ERR_UNKNOWN_USER, e.nickname), MT_ERROR); + return; + } + var str = new String(user.encodedName); + str.count = 1; + nicks = [str]; + } + else + { + // Panic? + dd("Help! Channel user mode command with no users...?"); + } + + for (var i = 0; i < nicks.length; ++i) + { + e.server.sendData("MODE " + e.channel.encodedName + " " + + modestr.substr(0, nicks[i].count + 1) + + " " + nicks[i] + "\n"); + } +} + +function cmdCharset(e) +{ + var pm; + + if (e.command.name == "default-charset") + { + pm = client.prefManager; + msg = MSG_CURRENT_CHARSET; + } + else + { + pm = e.sourceObject.prefManager; + msg = MSG_CURRENT_CHARSET_VIEW; + } + + if (e.newCharset) + { + if (e.newCharset == "-") + { + pm.clearPref("charset"); + } + else + { + if(!checkCharset(e.newCharset)) + { + display(getMsg(MSG_ERR_INVALID_CHARSET, e.newCharset), + MT_ERROR); + return; + } + pm.prefs["charset"] = e.newCharset; + } + } + + display(getMsg(msg, pm.prefs["charset"])); + + // If we're on a channel, get the topic again so it can be re-decoded. + if (e.newCharset && e.server && e.channel) + e.server.sendData("TOPIC " + e.channel.encodedName + "\n"); +} + +function cmdCreateTabForView(e) +{ + return getTabForObject(e.view, true); +} + +function cmdDelayed(e) +{ + function _dispatch() + { + // Clear inputData so that commands without arguments work properly + e.inputData = ""; + dispatch(e.rest, e, e.isInteractive); + } + setTimeout(_dispatch, e.delay * 1000); +} + +function cmdSync(e) +{ + var fun; + + switch (e.command.name) + { + case "sync-font": + fun = function () + { + if (view.prefs["displayHeader"]) + view.setHeaderState(false); + view.changeCSS(view.getFontCSS("data"), "cz-fonts"); + if (view.prefs["displayHeader"]) + view.setHeaderState(true); + }; + break; + + case "sync-header": + fun = function () + { + view.setHeaderState(view.prefs["displayHeader"]); + }; + break; + + case "sync-motif": + fun = function () + { + view.changeCSS(view.prefs["motif.current"]); + updateAppMotif(view.prefs["motif.current"]); + // Refresh the motif settings. + view.updateMotifSettings(); + }; + break; + + case "sync-timestamp": + fun = function () + { + updateTimestamps(view); + }; + break; + + case "sync-window": + fun = function () + { + if (window && window.location && + window.location.href != view.prefs["outputWindowURL"]) + { + syncOutputFrame(view); + } + }; + break; + + case "sync-log": + fun = function () + { + if (view.prefs["log"] ^ Boolean(view.logFile)) + { + if (view.prefs["log"]) + client.openLogFile(view, true); + else + client.closeLogFile(view, true); + updateLoggingIcon(); + } + }; + break; + } + + var view = e.sourceObject; + var window; + if (("frame" in view) && view.frame) + window = getContentWindow(view.frame); + + try + { + fun(); + } + catch(ex) + { + dd("Exception in " + e.command.name + " for " + e.sourceObject.unicodeName + ": " + ex); + } +} + +function cmdSimpleCommand(e) +{ + e.server.sendData(e.command.name + " " + e.inputData + "\n"); +} + +function cmdSquery(e) +{ + var data; + + if (e.commands) + data = "SQUERY " + e.service + " :" + e.commands + "\n"; + else + data = "SQUERY " + e.service + "\n"; + + e.server.sendData(data); +} + +function cmdHelp(e) +{ + if (!e.pattern) + { + if ("hello" in e) + display(MSG_HELP_INTRO, "HELLO"); + else + display(MSG_HELP_INTRO); + return; + } + + var ary = client.commandManager.list(e.pattern, CMD_CONSOLE, true); + + if (ary.length == 0) + { + display(getMsg(MSG_ERR_UNKNOWN_COMMAND, e.pattern), MT_ERROR); + return; + } + + for (var i in ary) + { + display(getMsg(MSG_FMT_USAGE, [ary[i].name, ary[i].helpUsage]), + MT_USAGE); + display(ary[i].help, MT_HELP); + } + + return; +} + +function cmdTestDisplay(e) +{ + startMsgGroup("testdisplay", MSG_COLLAPSE_TEST); + display(MSG_TEST_HELLO, MT_HELLO); + display(MSG_TEST_INFO, MT_INFO); + display(MSG_TEST_ERROR, MT_ERROR); + display(MSG_TEST_HELP, MT_HELP); + display(MSG_TEST_USAGE, MT_USAGE); + display(MSG_TEST_STATUS, MT_STATUS); + + if (e.server && e.server.me) + { + var me = e.server.me; + var sampleUser = {TYPE: "IRCUser", + encodedName: "ircmonkey", collectionKey: ":ircmonkey", + unicodeName: "IRCMonkey", viewName: "IRCMonkey", + host: "", name: "IRCMonkey"}; + var sampleChannel = {TYPE: "IRCChannel", + encodedName: "#mojo", collectionKey: ":#mojo", + unicodeName: "#Mojo", viewName: "#Mojo", + name: "#Mojo"}; + + function test (from, to) + { + var fromText = (from != me) ? from.TYPE + " ``" + from.name + "''" : + MSG_YOU; + var toText = (to != me) ? to.TYPE + " ``" + to.name + "''" : + MSG_YOU; + + display (getMsg(MSG_TEST_PRIVMSG, [fromText, toText]), + "PRIVMSG", from, to); + display (getMsg(MSG_TEST_ACTION, [fromText, toText]), + "ACTION", from, to); + display (getMsg(MSG_TEST_NOTICE, [fromText, toText]), + "NOTICE", from, to); + } + + test (sampleUser, me); /* from user to me */ + test (me, sampleUser); /* me to user */ + + display(MSG_TEST_URL, "PRIVMSG", sampleUser, me); + display(MSG_TEST_STYLES, "PRIVMSG", sampleUser, me); + display(MSG_TEST_EMOTICON, "PRIVMSG", sampleUser, me); + display(MSG_TEST_RHEET, "PRIVMSG", sampleUser, me); + display(unescape(MSG_TEST_CTLCHR), "PRIVMSG", sampleUser, me); + display(unescape(MSG_TEST_COLOR), "PRIVMSG", sampleUser, me); + display(MSG_TEST_QUOTE, "PRIVMSG", sampleUser, me); + + if (e.channel) + { + test (sampleUser, sampleChannel); /* user to channel */ + test (me, sampleChannel); /* me to channel */ + display(MSG_TEST_TOPIC, "TOPIC", sampleUser, sampleChannel); + display(MSG_TEST_JOIN, "JOIN", sampleUser, sampleChannel); + display(MSG_TEST_PART, "PART", sampleUser, sampleChannel); + display(MSG_TEST_KICK, "KICK", sampleUser, sampleChannel); + display(MSG_TEST_QUIT, "QUIT", sampleUser, sampleChannel); + display(getMsg(MSG_TEST_STALK, me.unicodeName), + "PRIVMSG", sampleUser, sampleChannel); + display(MSG_TEST_STYLES, "PRIVMSG", me, sampleChannel); + } + } + endMsgGroup(); +} + +function cmdNetwork(e) +{ + let network = client.getNetwork(e.networkName); + + if (!network) + { + display (getMsg(MSG_ERR_UNKNOWN_NETWORK, e.networkName), MT_ERROR); + return; + } + + dispatch("create-tab-for-view", { view: network }); + dispatch("set-current-view", { view: network }); +} + +function cmdNetworks(e) +{ + var wrapper = newInlineText(MSG_NETWORKS_HEADA); + + var netnames = keys(client.networks).sort(); + + for (let i = 0; i < netnames.length; i++) + { + let net = client.networks[netnames[i]]; + let hasSecure = networkHasSecure(net.serverList); + + var linkData = { + "data": net.unicodeName, + "href": (hasSecure ? "ircs://" : "irc://") + net.canonicalName + }; + wrapper.appendChild(newInlineText(linkData, "chatzilla-link", "a")); + + if (i < netnames.length - 1) + wrapper.appendChild(document.createTextNode(", ")); + } + + // Display an "Edit" link. + var spanb = document.createElementNS(XHTML_NS, "html:span"); + + client.munger.getRule(".inline-buttons").enabled = true; + var msg = getMsg(MSG_NETWORKS_HEADB2, "edit-networks"); + client.munger.munge(msg, spanb, getObjectDetails(client.currentObject)); + client.munger.getRule(".inline-buttons").enabled = false; + + wrapper.appendChild(spanb); + display(wrapper, MT_INFO); +} + +function cmdEditNetworks(e) +{ + toOpenWindowByType("irc:chatzilla:networks", + "chrome://chatzilla/content/networks-edit.xul", + "chrome,resizable,dialog", client); +} + +function cmdServer(e) +{ + let scheme = (e.command.name == "sslserver") ? "ircs" : "irc"; + + var ary = e.hostname.match(/^(.*):(\d+)$/); + if (ary) + { + // Foolish user obviously hasn't read the instructions, but we're nice. + e.password = e.port; + e.port = ary[2]; + e.hostname = ary[1]; + } + + gotoIRCURL({scheme: scheme, host: e.hostname, port: e.port, + pass: e.password, isserver: true}); +} + +function cmdSSLException(e) +{ + var opts = "chrome,centerscreen,modal"; + var location = e.hostname ? e.hostname + ':' + e.port : undefined; + var args = {location: location, prefetchCert: true}; + + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "", opts, args); + + if (!args.exceptionAdded) + return; + + if (e.connect) + { + // When we come via the inline button, we just want to reconnect + if (e.source == "mouse") + dispatch("reconnect"); + else + dispatch("sslserver " + e.hostname + " " + e.port); + } +} + + +function cmdQuit(e) +{ + // if we're not connected to anything, just close the window + if (!("getConnectionCount" in client) || (client.getConnectionCount() == 0)) + { + client.userClose = true; + window.close(); + return; + } + + // Otherwise, try to close gracefully: + client.wantToQuit(e.reason, true); +} + +function cmdDisconnect(e) +{ + if ((typeof e.reason != "string") || !e.reason) + e.reason = e.network.prefs["defaultQuitMsg"]; + if (!e.reason) + e.reason = client.userAgent; + + e.network.quit(e.reason); +} + +function cmdDisconnectAll(e) +{ + var netReason; + if (confirmEx(MSG_CONFIRM_DISCONNECT_ALL, ["!yes", "!no"]) != 0) + return; + + var conNetworks = client.getConnectedNetworks(); + if (conNetworks.length <= 0) + { + display(MSG_NO_CONNECTED_NETS, MT_ERROR); + return; + } + + for (var i = 0; i < conNetworks.length; i++) + { + netReason = e.reason; + if ((typeof netReason != "string") || !netReason) + netReason = conNetworks[i].prefs["defaultQuitMsg"]; + netReason = (netReason ? netReason : client.userAgent); + conNetworks[i].quit(netReason); + } +} + +function cmdDeleteView(e) +{ + if (!e.view) + e.view = e.sourceObject; + + if (("lockView" in e.view) && e.view.lockView) + { + setTabState(e.view, "attention"); + return; + } + + if (e.view.TYPE == "IRCChannel" && e.view.joined) + { + e.view.dispatch("part", { deleteWhenDone: true }); + return; + } + + if (e.view.TYPE.substr(0, 6) == "IRCDCC") + { + if (e.view.isActive()) + e.view.abort(); + // abort() calls disconnect() if it is appropriate. + // Fall through: we don't delete on disconnect. + } + + if (e.view.TYPE == "IRCNetwork" && (e.view.state == NET_CONNECTING || + e.view.state == NET_WAITING)) + { + e.view.dispatch("cancel", { deleteWhenDone: true }); + return; + } + + if (client.viewsArray.length < 2) + { + display(MSG_ERR_LAST_VIEW, MT_ERROR); + return; + } + + var tb = getTabForObject(e.view); + if (tb) + { + var i = deleteTab (tb); + if (i != -1) + { + if (e.view.logFile) + { + e.view.logFile.close(); + e.view.logFile = null; + } + delete e.view.messageCount; + delete e.view.messages; + deleteFrame(e.view); + + var oldView = client.currentObject; + if (client.currentObject == e.view) + { + if (i >= client.viewsArray.length) + i = client.viewsArray.length - 1; + oldView = client.viewsArray[i].source + } + client.currentObject = null; + oldView.dispatch("set-current-view", { view: oldView }); + } + } +} + +function cmdHideView(e) +{ + if (!e.view) + e.view = e.sourceObject; + + if (client.viewsArray.length < 2) + { + display(MSG_ERR_LAST_VIEW_HIDE, MT_ERROR); + return; + } + + if ("messages" in e.view) + { + // Detach messages from output window content. + if (e.view.messages.parentNode) + e.view.messages.parentNode.removeChild(e.view.messages); + + /* XXX Bug 335998: Adopt the messages into our own internal document + * so that when the real one the messages were in gets incorrectly + * GC-collected (see bug) the nodes still have an ownerDocument. + */ + client.adoptNode(e.view.messages, client.hiddenDocument); + } + + var tb = getTabForObject(e.view); + + if (tb) + { + var i = deleteTab (tb); + if (i != -1) + { + deleteFrame(e.view); + + var oldView = client.currentObject; + if (client.currentObject == e.view) + { + if (i >= client.viewsArray.length) + i = client.viewsArray.length - 1; + oldView = client.viewsArray[i].source + } + client.currentObject = null; + oldView.dispatch("set-current-view", { view: oldView }); + } + } +} + +function cmdClearView(e) +{ + if (!e.view) + e.view = e.sourceObject; + + e.view.messages = null; + e.view.messageCount = 0; + + e.view.displayHere(MSG_MESSAGES_CLEARED); + + syncOutputFrame(e.view); +} + +function cmdDesc(e) +{ + if (e.network != null) // somewhere on a network + { + dispatch("network-pref", {prefValue: e.description, prefName: "desc", + network: e.network, + isInteractive: e.isInteractive}); + } + else // no network, change the general pref + { + dispatch("pref", {prefName: "desc", prefValue: e.description, + isInteractive: e.isInteractive}); + } +} + +function cmdName(e) +{ + if (e.network != null) // somewhere on a network + { + dispatch("network-pref", {prefName: "username", prefValue: e.username, + network: e.network, + isInteractive: e.isInteractive}); + } + else // no network, change the general pref + { + dispatch("pref", {prefName: "username", prefValue: e.username, + isInteractive: e.isInteractive}); + } +} + +function cmdNames(e) +{ + if (e.hasOwnProperty("channelName")) + { + e.channel = new CIRCChannel(e.server, e.channelName); + } + else + { + if (!e.channel) + { + display(getMsg(MSG_ERR_REQUIRED_PARAM, "channel-name"), MT_ERROR); + return; + } + } + + e.channel.pendingNamesReply = true; + e.server.sendData("NAMES " + e.channel.encodedName + "\n"); +} + +function cmdReconnect(e) +{ + if (e.network.isConnected()) + { + // Set reconnect flag + e.network.reconnect = true; + if (typeof e.reason != "string") + e.reason = MSG_RECONNECTING; + // Now we disconnect. + e.network.quit(e.reason); + } + else + { + e.network.connect(e.network.requireSecurity); + } +} + +function cmdReconnectAll(e) +{ + var reconnected = false; + for (var net in client.networks) + { + if (client.networks[net].isConnected() || + ("messages" in client.networks[net])) + { + client.networks[net].dispatch("reconnect", { reason: e.reason }); + reconnected = true; + } + } + if (!reconnected) + display(MSG_NO_RECONNECTABLE_NETS, MT_ERROR); +} + +function cmdRejoin(e) +{ + if (e.channel.joined) + { + if (!e.reason) + e.reason = ""; + e.channel.dispatch("part", { reason: e.reason, deleteWhenDone: false }); + } + + e.channel.join(e.channel.mode.key); +} + +function cmdRename(e) +{ + var tab = getTabForObject(e.sourceObject); + if (!tab) + { + feedback(e, getMsg(MSG_ERR_INTERNAL_DISPATCH, "rename")); + return; + } + var label = e.label || prompt(MSG_TAB_NAME_PROMPT, tab.label); + if (!label) + { + return; + } + e.sourceObject.prefs["tabLabel"] = label; +} + + +function cmdTogglePref (e) +{ + var state = !client.prefs[e.prefName]; + client.prefs[e.prefName] = state; + feedback(e, getMsg (MSG_FMT_PREF, + [e.prefName, state ? MSG_VAL_ON : MSG_VAL_OFF])); +} + +function cmdToggleGroup(e) +{ + var document = getContentDocument(e.sourceObject.frame); + var msgs = document.querySelectorAll("[msg-groups*=\"" + e.groupId + "\"]"); + if (!msgs.length) + return; + + var isHidden = (msgs[0].style.display == "none"); + for (i = 0; i < msgs.length; i++) + { + if (isHidden) + msgs[i].style.display = ""; + else + msgs[i].style.display = "none"; + } + + var els = msgs[0].previousSibling.querySelectorAll(".chatzilla-link"); + var button = els[els.length - 1]; + if (button.text == MSG_COLLAPSE_HIDE) + { + button.text = MSG_COLLAPSE_SHOW; + button.title = MSG_COLLAPSE_SHOWTITLE; + } + else + { + button.text = MSG_COLLAPSE_HIDE; + button.title = MSG_COLLAPSE_HIDETITLE; + } +} + +function cmdToggleUI(e) +{ + var ids = new Array(); + + switch (e.thing) + { + case "tabstrip": + ids = ["view-tabs"]; + break; + + case "userlist": + ids = ["main-splitter", "user-list-box"]; + break; + + case "header": + client.currentObject.prefs["displayHeader"] = + !client.currentObject.prefs["displayHeader"]; + return; + + case "status": + ids = ["status-bar"]; + break; + + default: + ASSERT (0,"Unknown element ``" + e.thing + + "'' passed to onToggleVisibility."); + return; + } + + var newState; + var elem = document.getElementById(ids[0]); + var sourceObject = e.sourceObject; + var newState = !elem.collapsed; + + for (var i in ids) + { + elem = document.getElementById(ids[i]); + elem.collapsed = newState; + } + + updateTitle(); + dispatch("focus-input"); +} + +function cmdCommands(e) +{ + display(MSG_COMMANDS_HEADER); + + var matchResult = client.commandManager.listNames(e.pattern, CMD_CONSOLE); + matchResult = matchResult.join(", "); + + if (e.pattern) + display(getMsg(MSG_MATCHING_COMMANDS, [e.pattern, matchResult])); + else + display(getMsg(MSG_ALL_COMMANDS, matchResult)); +} + +function cmdAttach(e) +{ + if (e.ircUrl.search(/ircs?:\/\//i) != 0) + e.ircUrl = "irc://" + e.ircUrl; + + var parsedURL = parseIRCURL(e.ircUrl); + if (!parsedURL) + { + display(getMsg(MSG_ERR_BAD_IRCURL, e.ircUrl), MT_ERROR); + return; + } + + gotoIRCURL(e.ircUrl); +} + +function cmdMatchUsers(e) +{ + var matches = e.channel.findUsers(e.mask); + var uc = matches.unchecked; + var msgNotChecked = ""; + + // Get a pretty list of nicknames: + var nicknames = []; + for (var i = 0; i < matches.users.length; i++) + nicknames.push(matches.users[i].unicodeName); + + var nicknameStr = arraySpeak(nicknames); + + // Were we unable to check one or more of the users? + if (uc != 0) + msgNotChecked = getMsg(MSG_MATCH_UNCHECKED, uc); + + if (matches.users.length == 0) + display(getMsg(MSG_NO_MATCHING_NICKS, msgNotChecked)); + else + display(getMsg(MSG_MATCHING_NICKS, [nicknameStr, msgNotChecked])); +} + +function cmdMe(e) +{ + if (!("act" in e.sourceObject)) + { + display(getMsg(MSG_ERR_IMPROPER_VIEW, "me"), MT_ERROR); + return; + } + _sendMsgTo(e.action, "ACTION", e.sourceObject); +} + +function cmdDescribe(e) +{ + var target = e.server.addTarget(e.target); + _sendMsgTo(e.action, "ACTION", target, e.sourceObject); +} + +function cmdMode(e) +{ + var chan; + + // Make sure the user can leave the channel name out from a channel view. + if ((!e.target || /^[\+\-].+/.test(e.target)) && + !(chan && e.server.getChannel(chan))) + { + if (e.channel) + { + chan = e.channel.canonicalName; + if (e.param && e.modestr) + { + e.paramList.unshift(e.modestr); + } + else if (e.modestr) + { + e.paramList = [e.modestr]; + e.param = e.modestr; + } + e.modestr = e.target; + } + else + { + display(getMsg(MSG_ERR_REQUIRED_PARAM, "target"), MT_ERROR); + return; + } + } + else + { + chan = fromUnicode(e.target, e.server); + } + + // Check whether our mode string makes sense + if (!e.modestr) + { + e.modestr = ""; + if (!e.channel && arrayContains(e.server.channelTypes, chan[0])) + e.channel = new CIRCChannel(e.server, null, chan); + if (e.channel) + e.channel.pendingModeReply = true; + } + else if (!(/^([+-][a-z]+)+$/i).test(e.modestr)) + { + display(getMsg(MSG_ERR_INVALID_MODE, e.modestr), MT_ERROR); + return; + } + + var params = (e.param) ? " " + e.paramList.join(" ") : ""; + e.server.sendData("MODE " + chan + " " + fromUnicode(e.modestr, e.server) + + params + "\n"); +} + +function cmdMotif(e) +{ + var pm; + var msg; + + if (e.command.name == "channel-motif") + { + pm = e.channel.prefManager; + msg = MSG_CURRENT_CSS_CHAN; + } + else if (e.command.name == "network-motif") + { + pm = e.network.prefManager; + msg = MSG_CURRENT_CSS_NET; + } + else if (e.command.name == "user-motif") + { + pm = e.user.prefManager; + msg = MSG_CURRENT_CSS_USER; + } + else + { + pm = client.prefManager; + msg = MSG_CURRENT_CSS; + } + + if (e.motif) + { + if (e.motif == "-") + { + // delete local motif in favor of default + pm.clearPref("motif.current"); + e.motif = pm.prefs["motif.current"]; + } + else if (e.motif.search(/^(file|https?|ftp):/i) != -1) + { + // specific css file + pm.prefs["motif.current"] = e.motif; + } + else + { + // motif alias + var prefName = "motif." + e.motif; + if (client.prefManager.isKnownPref(prefName)) + { + e.motif = client.prefManager.prefs[prefName]; + } + else + { + display(getMsg(MSG_ERR_UNKNOWN_MOTIF, e.motif), MT_ERROR); + return; + } + + pm.prefs["motif.current"] = e.motif; + } + + } + + display (getMsg(msg, pm.prefs["motif.current"])); +} + +function cmdList(e) +{ + if (!e.channelName) + { + e.channelName = ""; + var c = e.server.channelCount; + if ((c > client.SAFE_LIST_COUNT) && !("listWarned" in e.network)) + { + client.munger.getRule(".inline-buttons").enabled = true; + display(getMsg(MSG_LIST_CHANCOUNT, [c, "list"]), MT_WARN); + client.munger.getRule(".inline-buttons").enabled = false; + e.network.listWarned = true; + return; + } + } + + e.network.list(e.channelName); +} + +function cmdListPlugins(e) +{ + function listPlugin(plugin, i) + { + var enabled; + if ((plugin.API > 0) || ("disablePlugin" in plugin.scope)) + enabled = plugin.enabled; + else + enabled = MSG_ALWAYS; + + display(getMsg(MSG_FMT_PLUGIN1, [i, plugin.url])); + display(getMsg(MSG_FMT_PLUGIN2, + [plugin.id, plugin.version, enabled, plugin.status])); + display(getMsg(MSG_FMT_PLUGIN3, plugin.description)); + } + + if (e.plugin) + { + listPlugin(e.plugin, 0); + return; + } + + var i = 0; + for (var k in client.plugins) + listPlugin(client.plugins[k], i++); + + if (i == 0) + display(MSG_NO_PLUGINS); +} + +function cmdRlist(e) +{ + try + { + var re = new RegExp(e.regexp, "i"); + } + catch (ex) + { + display(MSG_ERR_INVALID_REGEX, MT_ERROR); + return; + } + + var c = e.server.channelCount; + if ((c > client.SAFE_LIST_COUNT) && !("listWarned" in e.network)) + { + client.munger.getRule(".inline-buttons").enabled = true; + display(getMsg(MSG_LIST_CHANCOUNT, [c, "rlist " + e.regexp]), MT_WARN); + client.munger.getRule(".inline-buttons").enabled = false; + e.network.listWarned = true; + return; + } + e.network.list(re); +} + +function cmdReloadUI(e) +{ + if (!("getConnectionCount" in client) || + client.getConnectionCount() == 0) + { + window.location.href = window.location.href; + } +} + +function cmdQuery(e) +{ + // We'd rather *not* trigger the user.start event this time. + blockEventSounds("user", "start"); + var user = openQueryTab(e.server, e.nickname); + dispatch("set-current-view", { view: user }); + + if (e.message) + _sendMsgTo(e.message, "PRIVMSG", user); + + return user; +} + +function cmdSay(e) +{ + if (!("say" in e.sourceObject)) + { + display(getMsg(MSG_ERR_IMPROPER_VIEW, "say"), MT_ERROR); + return; + } + + _sendMsgTo(e.message, "PRIVMSG", e.sourceObject) +} + +function cmdMsg(e) +{ + var target = e.server.addTarget(e.nickname); + _sendMsgTo(e.message, "PRIVMSG", target, e.sourceObject); +} + +function _sendMsgTo(message, msgType, target, displayObj) +{ + if (!displayObj) + displayObj = target; + + + var msg = filterOutput(message, msgType, target); + + var o = getObjectDetails(target); + var lines = o.server ? o.server.splitLinesForSending(msg, true) : [msg]; + + for (var i = 0; i < lines.length; i++) + { + msg = lines[i]; + if (!(o.server && o.server.caps["echo-message"])) + { + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + displayObj.display(msg, msgType, "ME!", target); + client.munger.getRule(".mailto").enabled = false; + } + if (msgType == "PRIVMSG") + target.say(msg); + else if (msgType == "NOTICE") + target.notice(msg); + else if (msgType == "ACTION") + target.act(msg); + } +} + +function cmdNick(e) +{ + if (!e.nickname) + { + var curNick; + if (e.server && e.server.isConnected) + curNick = e.server.me.unicodeName; + else if (e.network) + curNick = e.network.prefs["nickname"]; + else + curNick = client.prefs["nickname"]; + + e.nickname = prompt(MSG_NICK_PROMPT, curNick); + if (e.nickname == null) + return; + e.nickname = e.nickname.replace(/ /g, "_"); + } + + if (e.server && e.server.isConnected) + e.server.changeNick(e.nickname); + + if (e.network) + { + /* We want to save in all non-online cases, including NET_CONNECTING, + * as we will only get a NICK reply if we are completely connected. + */ + if (e.network.state == NET_ONLINE) + { + e.network.pendingNickChange = e.nickname; + } + else + { + e.network.prefs["nickname"] = e.nickname; + e.network.preferredNick = e.nickname; + } + } + else + { + client.prefs["nickname"] = e.nickname; + updateTitle(client); + } +} + +function cmdNotice(e) +{ + var target = e.server.addTarget(e.nickname); + _sendMsgTo(e.message, "NOTICE", target, e.sourceObject); +} + +function cmdQuote(e) +{ + /* Check we are connected, or at least pretending to be connected, so this + * can actually send something. The only thing that's allowed to send + * before the 001 is PASS, so if the command is not that and the net is not + * online, we stop too. + */ + if ((e.network.state != NET_ONLINE) && + (!e.server.isConnected || !e.ircCommand.match(/^\s*PASS/i))) + { + feedback(e, MSG_ERR_NOT_CONNECTED); + return; + } + e.server.sendData(fromUnicode(e.ircCommand) + "\n", e.sourceObject); +} + +function cmdEval(e) +{ + var sourceObject = e.sourceObject; + + try + { + sourceObject.doEval = function (__s) { return eval(__s); } + if (e.command.name == "eval") + sourceObject.display(e.expression, MT_EVALIN); + var rv = String(sourceObject.doEval (e.expression)); + if (e.command.name == "eval") + sourceObject.display(rv, MT_EVALOUT); + + } + catch (ex) + { + sourceObject.display(String(ex), MT_ERROR); + } +} + +function cmdFocusInput(e) +{ + const WWATCHER_CTRID = "@mozilla.org/embedcomp/window-watcher;1"; + const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher; + + var watcher = + Components.classes[WWATCHER_CTRID].getService(nsIWindowWatcher); + if (watcher.activeWindow == window) + client.input.focus(); + else + document.commandDispatcher.focusedElement = client.input; +} + +function cmdGotoStartup(e) +{ + openStartupURLs(); +} + +function cmdGotoURL(e) +{ + if (/^ircs?:/.test(e.url)) + { + gotoIRCURL(e.url); + return; + } + + if (/^x-irc-dcc-(chat|file):[0-9a-fA-F]+$/.test(e.url)) + { + var view = client.dcc.findByID(e.url.substr(15)); + if (view) + dispatch("set-current-view", {view: view}); + return; + } + + if (/^x-cz-command:/.test(e.url)) + { + var ary = e.url.match(/^x-cz-command:(.*)$/i); + e.sourceObject.dispatch(decodeURI(ary[1]), + {isInteractive: true, source: e.source}); + return; + } + + try + { + var uri = Services.io.newURI(e.url, "UTF-8"); + } + catch (ex) + { + // Given "goto-url faq bar", expand to "http://.../faq/#bar" + var localeURLKey = "msg.localeurl." + e.url; + var hash = (("anchor" in e) && e.anchor) ? "#" + e.anchor : ""; + if (localeURLKey != getMsg(localeURLKey)) + dispatch(e.command.name + " " + getMsg(localeURLKey) + hash); + else + display(getMsg(MSG_ERR_INVALID_URL, e.url), MT_ERROR); + + dispatch("focus-input"); + return; + } + + var browserWin = getWindowByType("navigator:browser"); + var location = browserWin ? browserWin.gBrowser.currentURI.spec : null; + var action = e.command.name; + let where = "current"; + + // We don't want to replace ChatZilla running in a tab. + if ((action == "goto-url-newwin") || + ((action == "goto-url") && location && + location.startsWith("chrome://chatzilla/content/"))) + { + where = "window"; + } + + if (action == "goto-url-newtab") + { + where = e.shiftKey ? "tabshifted" : "tab"; + } + + try + { + let loadInBackground = + Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground"); + openLinkIn(e.url, where, { inBackground: loadInBackground }); + } + catch (ex) + { + dd(formatException(ex)); + } + dispatch("focus-input"); +} + +function cmdCTCP(e) +{ + var obj = e.server.addTarget(e.target); + obj.ctcp(e.code, e.params); +} + +function cmdJoin(e) +{ + /* This check makes sure we only check if the *user* entered anything, and + * ignore any contextual information, like the channel the command was + * run on. + */ + if ((!e.hasOwnProperty("channelName") || !e.channelName) && + !e.channelToJoin) + { + if (client.joinDialog) + { + client.joinDialog.setNetwork(e.network); + client.joinDialog.focus(); + return; + } + + window.openDialog("chrome://chatzilla/content/channels.xul", "", + "resizable=yes", + { client: client, network: e.network || null, + opener: window }); + return null; + } + + var chan; + if (!e.channelToJoin) + { + if (!("charset" in e)) + { + e.charset = null; + } + else if (e.charset && !checkCharset(e.charset)) + { + display (getMsg(MSG_ERR_INVALID_CHARSET, e.charset), MT_ERROR); + return null; + } + + if (e.channelName.search(",") != -1) + { + // We can join multiple channels! Woo! + var chans = e.channelName.split(","); + var keys = []; + if (e.key) + keys = e.key.split(","); + for (var c in chans) + { + chan = dispatch("join", { network: e.network, + server: e.server, + charset: e.charset, + channelName: chans[c], + key: keys.shift() }); + } + return chan; + } + + if ((arrayIndexOf(["#", "&", "+", "!"], e.channelName[0]) == -1) && + (arrayIndexOf(e.server.channelTypes, e.channelName[0]) == -1)) + { + e.channelName = e.server.channelTypes[0] + e.channelName; + } + + var charset = e.charset ? e.charset : e.network.prefs["charset"]; + chan = e.server.addChannel(e.channelName, charset); + if (e.charset) + chan.prefs["charset"] = e.charset; + } + else + { + chan = e.channelToJoin; + } + + e.key = client.tryToGetLogin(chan.getURL(), "chan", "*", e.key, false, ""); + chan.join(e.key); + + /* !-channels are "safe" channels, and get a server-generated prefix. For + * this reason, we shouldn't do anything client-side until the server + * replies (since the reply will have the appropriate prefix). */ + if (chan.unicodeName[0] != "!") + { + dispatch("create-tab-for-view", { view: chan }); + dispatch("set-current-view", { view: chan }); + } + + return chan; +} + +function cmdLeave(e) +{ + function leaveChannel(channelName) + { + var channelToLeave; + // This function will return true if we should continue processing + // channel names. If we discover that we were passed an invalid channel + // name, but have a channel on the event, we'll just leave that channel + // with the full message (including what we thought was a channel name) + // and return false in order to not process the rest of what we thought + // was a channel name. If there's a genuine error, e.g. because the user + // specified a non-existing channel and isn't in a channel either, we + // will also return a falsy value + var shouldContinue = true; + if (arrayIndexOf(e.server.channelTypes, channelName[0]) == -1) + { + // No valid prefix character. Check they really meant a channel... + var valid = false; + for (var i = 0; i < e.server.channelTypes.length; i++) + { + // Hmm, not ideal... + var chan = e.server.getChannel(e.server.channelTypes[i] + + channelName); + if (chan) + { + // Yes! They just missed that single character. + channelToLeave = chan; + valid = true; + break; + } + } + + // We can only let them get away here if we've got a channel. + if (!valid) + { + if (e.channel) + { + /* Their channel name was invalid, but we have a channel + * view, so we'll assume they did "/leave part msg". + * NB: we use e.channelName here to get the full channel + * name before we (may have) split it. + */ + e.reason = e.channelName + (e.reason ? " " + e.reason : ""); + channelToLeave = e.channel; + shouldContinue = false; + } + else + { + display(getMsg(MSG_ERR_UNKNOWN_CHANNEL, channelName), + MT_ERROR); + return; + } + } + } + else + { + // Valid prefix, so get real channel (if it exists...). + channelToLeave = e.server.getChannel(channelName); + if (!channelToLeave) + { + display(getMsg(MSG_ERR_UNKNOWN_CHANNEL, channelName), + MT_ERROR); + return; + } + } + + if (!("deleteWhenDone" in e)) + e.deleteWhenDone = client.prefs["deleteOnPart"]; + + /* If it's not active, we're not actually in it, even though the view is + * still here. + */ + if (channelToLeave.active) + { + channelToLeave.deleteWhenDone = e.deleteWhenDone; + + if (!e.reason) + e.reason = ""; + + e.server.sendData("PART " + channelToLeave.encodedName + " :" + + fromUnicode(e.reason, channelToLeave) + "\n"); + } + else + { + /* We can leave the channel when not active + * this will close the view and prevent rejoin after a reconnect + */ + if (channelToLeave.joined) + channelToLeave.joined = false; + + if (e.deleteWhenDone) + channelToLeave.dispatch("delete-view"); + } + + return shouldContinue; + }; + + if (!e.server) + { + display(getMsg(MSG_ERR_IMPROPER_VIEW, e.command.name), MT_ERROR); + return; + } + + if (!e.hasOwnProperty("channelName") && e.channel) + e.channelName = e.channel.unicodeName; + + if (e.hasOwnProperty("channelName")) + { + if (!e.channelName) + { + // No channel specified and command not sent from a channel view + display(getMsg(MSG_ERR_NEED_CHANNEL, e.command.name), MT_ERROR); + return; + } + + + var channels = e.channelName.split(","); + for (var i = 0; i < channels.length; i++) + { + // Skip empty channel names: + if (!channels[i]) + continue; + + // If we didn't successfully leave, stop processing the + // rest of the channels: + if (!leaveChannel(channels[i])) + break; + } + } +} + +function cmdMarker(e) +{ + if (!client.initialized) + return; + + var view = e.sourceObject; + if (!("setActivityMarker" in e.sourceObject)) + return; + + var marker = e.sourceObject.getActivityMarker(); + if ((e.command.name == "marker") && (marker == null)) + { + // Marker is not currently set but user wants to scroll to it, + // so we just call set like normal. + e.command.name = "marker-set"; + } + + switch(e.command.name) + { + case "marker": /* Scroll to the marker. */ + e.sourceObject.scrollToElement("marker", "center"); + break; + case "marker-set": /* Set (or reset) the marker. */ + e.sourceObject.setActivityMarker(true); + e.sourceObject.scrollToElement("marker", "center"); + break; + case "marker-clear": /* Clear the marker. */ + e.sourceObject.setActivityMarker(false); + break; + default: + view.display(MSG_ERR_UNKNOWN_COMMAND, e.command.name); + } +} + +function cmdReload(e) +{ + dispatch("load " + e.plugin.url); +} + +function cmdLoad(e) +{ + if (!e.scope) + e.scope = new Object(); + + if (!("plugin" in e.scope)) + { + e.scope.plugin = { url: e.url, id: MSG_UNKNOWN, version: -1, + description: "", status: MSG_LOADING, enabled: false, + PluginAPI: 1, cwd: e.url.match(/^(.*?)[^\/]+$/)[1]}; + + } + + var plugin = e.scope.plugin; + plugin.scope = e.scope; + + try + { + var rvStr; + var rv = rvStr = client.load(e.url, e.scope); + let oldPlugin = getPluginByURL(e.url); + if (oldPlugin && !disablePlugin(oldPlugin, true)) + { + display(getMsg(MSG_ERR_SCRIPTLOAD, e.url)); + return null; + } + + if ("init" in plugin) + { + // Sanity check plugin's methods and properties: + var okay = false; + if (!("id" in plugin) || (plugin.id == MSG_UNKNOWN)) + display(getMsg(MSG_ERR_PLUGINAPI_NOID, e.url)); + else if (!(plugin.id.match(/^[A-Za-z0-9-_]+$/))) + display(getMsg(MSG_ERR_PLUGINAPI_FAULTYID, e.url)); + else if (!("enable" in plugin)) + display(getMsg(MSG_ERR_PLUGINAPI_NOENABLE, e.url)); + else if (!("disable" in plugin)) + display(getMsg(MSG_ERR_PLUGINAPI_NODISABLE, e.url)); + else + okay = true; + + if (!okay) + { + display (getMsg(MSG_ERR_SCRIPTLOAD, e.url)); + return null; + } + + plugin.API = 1; + plugin.prefary = [["enabled", true, "hidden"]]; + rv = rvStr = plugin.init(e.scope); + + var branch = "extensions.irc.plugins." + plugin.id + "."; + var prefManager = new PrefManager(branch, client.defaultBundle); + prefManager.addPrefs(plugin.prefary); + plugin.prefManager = prefManager; + plugin.prefs = prefManager.prefs; + if ("onPrefChanged" in plugin) + prefManager.addObserver(plugin); + client.prefManager.addObserver(prefManager); + client.prefManagers.push(prefManager); + } + else + { + plugin.API = 0; + if ("initPlugin" in e.scope) + rv = rvStr = e.scope.initPlugin(e.scope); + plugin.enabled = true; + } + plugin.status = "loaded"; + + if (typeof rv == "function") + rvStr = "function"; + + if (!plugin.id) + plugin.id = 'plugin' + randomString(8); + + client.plugins[plugin.id] = plugin; + + feedback(e, getMsg(MSG_SUBSCRIPT_LOADED, [e.url, rvStr]), MT_INFO); + + if ((plugin.API > 0) && plugin.prefs["enabled"]) + dispatch("enable-plugin " + plugin.id); + return {rv: rv}; + } + catch (ex) + { + display (getMsg(MSG_ERR_SCRIPTLOAD, e.url)); + display (formatException(ex), MT_ERROR); + } + + return null; +} + +function cmdWho(e) +{ + e.network.pendingWhoReply = true; + e.server.LIGHTWEIGHT_WHO = false; + e.server.who(e.rest); +} + +function cmdWhoIs(e) +{ + if (!isinstance(e.network.whoisList, Object)) + e.network.whoisList = {}; + + for (var i = 0; i < e.nicknameList.length; i++) + { + if ((i < e.nicknameList.length - 1) && + (e.server.toLowerCase(e.nicknameList[i]) == + e.server.toLowerCase(e.nicknameList[i + 1]))) + { + e.server.whois(e.nicknameList[i] + " " + e.nicknameList[i]); + i++; + } + else + { + e.server.whois(e.nicknameList[i]); + } + e.network.whoisList[e.server.toLowerCase(e.nicknameList[i])] = null; + } +} + +function cmdWhoIsIdle(e) +{ + for (var i = 0; i < e.nicknameList.length; i++) + e.server.whois(e.nicknameList[i] + " " + e.nicknameList[i]); +} + +function cmdWhoWas(e) +{ + e.server.whowas(e.nickname, e.limit); +} + +function cmdTopic(e) +{ + if (!e.newTopic) + e.server.sendData("TOPIC " + e.channel.encodedName + "\n"); + else + e.channel.setTopic(e.newTopic); +} + +function cmdAbout(e) +{ + if (e.source) + { + if ("aboutDialog" in client) + return client.aboutDialog.focus(); + + window.openDialog("chrome://chatzilla/content/about/about.xul", "", + "chrome,dialog", { client: client }); + } + else + { + var ver = CIRCServer.prototype.VERSION_RPLY; + client.munger.getRule(".inline-buttons").enabled = true; + display(getMsg(MSG_ABOUT_VERSION, [ver, "about"])); + display(MSG_ABOUT_HOMEPAGE); + client.munger.getRule(".inline-buttons").enabled = false; + } +} + +function cmdAlias(e) +{ + var aliasDefs = client.prefs["aliases"]; + function getAlias(commandName) + { + for (var i = 0; i < aliasDefs.length; ++i) + { + var ary = aliasDefs[i].match(/^(.*?)\s*=\s*(.*)$/); + if (ary[1] == commandName) + return [i, ary[2]]; + } + + return null; + }; + + var ary; + + if ((e.commandList == "-") || (e.command.name == "unalias")) + { + /* remove alias */ + ary = getAlias(e.aliasName); + if (!ary) + { + display(getMsg(MSG_NOT_AN_ALIAS, e.aliasName), MT_ERROR); + return; + } + + // Command Manager is updated when the preference changes. + arrayRemoveAt(aliasDefs, ary[0]); + aliasDefs.update(); + + feedback(e, getMsg(MSG_ALIAS_REMOVED, e.aliasName)); + } + else if (e.aliasName && e.commandList) + { + /* add/change alias */ + ary = getAlias(e.aliasName); + if (ary) + aliasDefs[ary[0]] = e.aliasName + " = " + e.commandList; + else + aliasDefs.push(e.aliasName + " = " + e.commandList); + + // Command Manager is updated when the preference changes. + aliasDefs.update(); + + feedback(e, getMsg(MSG_ALIAS_CREATED, [e.aliasName, e.commandList])); + } + else if (e.aliasName) + { + /* display alias */ + ary = getAlias(e.aliasName); + if (!ary) + display(getMsg(MSG_NOT_AN_ALIAS, e.aliasName), MT_ERROR); + else + display(getMsg(MSG_FMT_ALIAS, [e.aliasName, ary[1]])); + } + else + { + /* list aliases */ + if (aliasDefs.length == 0) + { + display(MSG_NO_ALIASES); + } + else + { + for (var i = 0; i < aliasDefs.length; ++i) + { + ary = aliasDefs[i].match(/^(.*?)\s*=\s*(.*)$/); + if (ary) + display(getMsg(MSG_FMT_ALIAS, [ary[1], ary[2]])); + else + display(getMsg(MSG_ERR_BADALIAS, aliasDefs[i])); + } + } + } +} + +function cmdAway(e) +{ + function sendToAllNetworks(command, reason) + { + for (var n in client.networks) + { + var net = client.networks[n]; + if (net.primServ && (net.state == NET_ONLINE)) + { + // If we can override the network's away state, or they are + // already idly-away, or they're not away to begin with: + if (overrideAway || net.isIdleAway || !net.prefs["away"]) + { + net.dispatch(command, {reason: reason }); + net.isIdleAway = (e.command.name == "idle-away"); + } + } + } + }; + + // Idle away shouldn't override away state set by the user. + var overrideAway = (e.command.name.indexOf("idle") != 0); + + if ((e.command.name == "away") || (e.command.name == "custom-away") || + (e.command.name == "idle-away")) + { + /* going away */ + if (e.command.name == "custom-away") + { + e.reason = prompt(MSG_AWAY_PROMPT); + // prompt() returns null for cancelling, a string otherwise (even if empty). + if (e.reason == null) + return; + } + // No parameter, or user entered nothing in the prompt. + if (!e.reason) + e.reason = MSG_AWAY_DEFAULT; + + // Update away list (remove from current location). + for (var i = 0; i < client.awayMsgs.length; i++) + { + if (client.awayMsgs[i].message == e.reason) + { + client.awayMsgs.splice(i, 1); + break; + } + } + // Always put new item at start. + var newMsg = { message: e.reason }; + client.awayMsgs.unshift(newMsg); + // Make sure we've not exceeded the limit set. + if (client.awayMsgs.length > client.awayMsgCount) + client.awayMsgs.splice(client.awayMsgCount); + // And now, to save the list! + try + { + var awayFile = new nsLocalFile(client.prefs["profilePath"]); + awayFile.append("awayMsgs.txt"); + var awayLoader = new TextSerializer(awayFile); + if (awayLoader.open(">")) + { + awayLoader.serialize(client.awayMsgs); + awayLoader.close(); + } + } + catch(ex) + { + display(getMsg(MSG_ERR_AWAY_SAVE, formatException(ex)), MT_ERROR); + } + + // Actually do away stuff, is this on a specific network? + if (e.server) + { + var normalNick = e.network.prefs["nickname"]; + var awayNick = e.network.prefs["awayNick"]; + if (e.network.state == NET_ONLINE) + { + // Postulate that if normal nick and away nick are the same, + // user doesn't want to change nicks: + if (awayNick && (normalNick != awayNick)) + e.server.changeNick(awayNick); + e.server.sendData("AWAY :" + fromUnicode(e.reason, e.network) + + "\n"); + } + if (awayNick && (normalNick != awayNick)) + e.network.preferredNick = awayNick; + e.network.prefs["away"] = e.reason; + } + else + { + // Client view, do command for all networks. + sendToAllNetworks("away", e.reason); + client.prefs["away"] = e.reason; + + // Don't tell people how to get back if they're idle: + var idleMsgParams = [e.reason, client.prefs["awayIdleTime"]]; + if (e.command.name == "idle-away") + var msg = getMsg(MSG_IDLE_AWAY_ON, idleMsgParams); + else + msg = getMsg(MSG_AWAY_ON, e.reason); + + // Display on the *client* tab, or on the current tab iff + // there's nowhere else they'll hear about it: + if (("frame" in client) && client.frame) + client.display(msg); + else if (!client.getConnectedNetworks()) + display(msg); + } + } + else + { + /* returning */ + if (e.server) + { + if (e.network.state == NET_ONLINE) + { + var curNick = e.server.me.unicodeName; + var awayNick = e.network.prefs["awayNick"]; + if (awayNick && (curNick == awayNick)) + e.server.changeNick(e.network.prefs["nickname"]); + e.server.sendData("AWAY\n"); + } + // Go back to old nick, even if not connected: + if (awayNick && (curNick == awayNick)) + e.network.preferredNick = e.network.prefs["nickname"]; + e.network.prefs["away"] = ""; + } + else + { + client.prefs["away"] = ""; + // Client view, do command for all networks. + sendToAllNetworks("back"); + if (("frame" in client) && client.frame) + client.display(MSG_AWAY_OFF); + else if (!client.getConnectedNetworks()) + display(MSG_AWAY_OFF); + } + } +} + +function cmdOpenAtStartup(e) +{ + var origURL = e.sourceObject.getURL(); + var url = makeCanonicalIRCURL(origURL); + var list = client.prefs["initialURLs"]; + ensureCachedCanonicalURLs(list); + var index = arrayIndexOf(list.canonicalURLs, url); + + if (e.toggle == null) + { + if (index == -1) + display(getMsg(MSG_STARTUP_NOTFOUND, url)); + else + display(getMsg(MSG_STARTUP_EXISTS, url)); + return; + } + + e.toggle = getToggle(e.toggle, (index != -1)); + + if (e.toggle) + { + // yes, please open at startup + if (index == -1) + { + list.push(origURL); + list.update(); + display(getMsg(MSG_STARTUP_ADDED, url)); + } + else + { + display(getMsg(MSG_STARTUP_EXISTS, url)); + } + } + else + { + // no, please don't open at startup + if (index != -1) + { + arrayRemoveAt(list, index); + list.update(); + display(getMsg(MSG_STARTUP_REMOVED, url)); + } + else + { + display(getMsg(MSG_STARTUP_NOTFOUND, url)); + } + } +} + +function cmdOper(e) +{ + e.password = client.tryToGetLogin(e.server.getURL(), "oper", e.opername, + e.password, true, MSG_NEED_OPER_PASSWORD); + + if (!e.password) + return; + + e.server.sendData("OPER " + fromUnicode(e.opername, e.server) + " " + + fromUnicode(e.password, e.server) + "\n"); +} + +function cmdPing (e) +{ + e.network.dispatch("ctcp", { target: e.nickname, code: "PING" }); +} + +function cmdPref (e) +{ + var msg; + var pm; + + if (e.command.name == "network-pref") + { + pm = e.network.prefManager; + msg = MSG_FMT_NETPREF; + } + else if (e.command.name == "channel-pref") + { + pm = e.channel.prefManager; + msg = MSG_FMT_CHANPREF; + } + else if (e.command.name == "plugin-pref") + { + pm = e.plugin.prefManager; + msg = MSG_FMT_PLUGINPREF; + } + else if (e.command.name == "user-pref") + { + pm = e.user.prefManager; + msg = MSG_FMT_USERPREF; + } + else + { + pm = client.prefManager; + msg = MSG_FMT_PREF; + } + + var ary = pm.listPrefs(e.prefName); + if (ary.length == 0) + { + display (getMsg(MSG_ERR_UNKNOWN_PREF, [e.prefName]), + MT_ERROR); + return false; + } + + if (e.prefValue == "-") + e.deletePref = true; + + if (e.deletePref) + { + if (!(e.prefName in pm.prefRecords)) + { + display(getMsg(MSG_ERR_UNKNOWN_PREF, [e.prefName]), MT_ERROR); + return false; + } + + try + { + pm.clearPref(e.prefName); + } + catch (ex) + { + // ignore exception generated by clear of nonexistant pref + if (!("result" in ex) || + ex.result != Components.results.NS_ERROR_UNEXPECTED) + { + throw ex; + } + } + + var prefValue = pm.prefs[e.prefName]; + feedback (e, getMsg(msg, [e.prefName, pm.prefs[e.prefName]])); + return true; + } + + if (e.prefValue) + { + if (!(e.prefName in pm.prefRecords)) + { + display(getMsg(MSG_ERR_UNKNOWN_PREF, [e.prefName]), MT_ERROR); + return false; + } + + var r = pm.prefRecords[e.prefName]; + var def, type; + + if (typeof r.defaultValue == "function") + def = r.defaultValue(e.prefName); + else + def = r.defaultValue; + + type = typeof def; + + switch (type) + { + case "number": + e.prefValue = Number(e.prefValue); + break; + case "boolean": + e.prefValue = (e.prefValue.toLowerCase() == "true"); + break; + case "string": + break; + default: + if (isinstance(e.prefValue, Array)) + e.prefValue = e.prefValue.join("; "); + if (isinstance(def, Array)) + e.prefValue = pm.stringToArray(e.prefValue); + break; + } + + pm.prefs[e.prefName] = e.prefValue; + if (isinstance(e.prefValue, Array)) + e.prefValue = e.prefValue.join("; "); + feedback (e, getMsg(msg, [e.prefName, e.prefValue])); + } + else + { + for (var i = 0; i < ary.length; ++i) + { + var value; + if (isinstance(pm.prefs[ary[i]], Array)) + value = pm.prefs[ary[i]].join("; "); + else + value = pm.prefs[ary[i]]; + + feedback(e, getMsg(msg, [ary[i], value])); + } + } + + return true; +} + +function cmdPrint(e) +{ + if (("frame" in e.sourceObject) && e.sourceObject.frame && + getContentWindow(e.sourceObject.frame)) + { + getContentWindow(e.sourceObject.frame).print(); + } + else + { + display(MSG_ERR_UNABLE_TO_PRINT); + } +} + +function cmdVersion(e) +{ + if (e.nickname) + e.network.dispatch("ctcp", { target: e.nickname, code: "VERSION"}); + else + e.server.sendData(fromUnicode("VERSION") + "\n", e.sourceObject); +} + +function cmdEcho(e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + display(e.message); + client.munger.getRule(".mailto").enabled = false; +} + +function cmdInvite(e) +{ + var channel; + + if (e.channelName) + { + channel = e.server.getChannel(e.channelName); + if (!channel) + { + display(getMsg(MSG_ERR_UNKNOWN_CHANNEL, e.channelName), MT_ERROR); + return; + } + } + else if (e.channel) + { + channel = e.channel; + } + else + { + display(getMsg(MSG_ERR_NO_CHANNEL, e.command.name), MT_ERROR); + return; + } + + channel.invite(e.nickname); +} + +function cmdKick(e) +{ + if (e.userList) + { + if (e.command.name == "kick-ban") + { + e.sourceObject.dispatch("ban", { userList: e.userList, + canonNickList: e.canonNickList, + user: e.user, + nickname: e.user.encodedName }); + } + + /* Note that we always do /kick below; the /ban is covered above. + * Also note that we are required to pass the nickname, to satisfy + * the dispatching of the command (which is defined with a required + * <nickname> parameter). It's not required for /ban, above, but it + * seems prudent to include it anyway. + */ + for (var i = 0; i < e.userList.length; i++) + { + var e2 = { user: e.userList[i], + nickname: e.userList[i].encodedName }; + e.sourceObject.dispatch("kick", e2); + } + return; + } + + if (!e.user) + e.user = e.channel.getUser(e.nickname); + + if (!e.user) + { + display(getMsg(MSG_ERR_UNKNOWN_USER, e.nickname), MT_ERROR); + return; + } + + if (e.command.name == "kick-ban") + e.sourceObject.dispatch("ban", { nickname: e.user.encodedName }); + + e.user.kick(e.reason); +} + +function cmdKnock(e) +{ + var rest = (e.reason ? " :" + fromUnicode(e.reason, e.server) : "") + "\n"; + e.server.sendData("KNOCK " + fromUnicode(e.channelName, e.server) + rest); +} + +function cmdClient(e) +{ + if (!("messages" in client)) + { + client.display(MSG_WELCOME, "HELLO"); + dispatch("set-current-view", { view: client }); + dispatch("help", { hello: true }); + dispatch("networks"); + } + else + { + dispatch("set-current-view", { view: client }); + } +} + +function cmdNotify(e) +{ + var net = e.network; + var supports_monitor = net.primServ.supports["monitor"]; + + if (!e.nickname) + { + if (net.prefs["notifyList"].length > 0) + { + if (supports_monitor) + { + // Just get the status of the monitor list from the server. + net.primServ.sendData("MONITOR S\n"); + } + else + { + /* delete the lists and force a ISON check, this will + * print the current online/offline status when the server + * responds */ + delete net.onList; + delete net.offList; + onNotifyTimeout(); + } + } + else + { + display(MSG_NO_NOTIFY_LIST); + } + } + else + { + var adds = new Array(); + var subs = new Array(); + + for (var i in e.nicknameList) + { + var nickname = e.server.toLowerCase(e.nicknameList[i]); + var list = net.prefs["notifyList"]; + list = e.server.toLowerCase(list.join(";")).split(";"); + var idx = arrayIndexOf (list, nickname); + if (idx == -1) + { + net.prefs["notifyList"].push (nickname); + adds.push(nickname); + } + else + { + arrayRemoveAt (net.prefs["notifyList"], idx); + subs.push(nickname); + } + } + net.prefs["notifyList"].update(); + + var msgname; + + if (adds.length > 0) + { + if (supports_monitor) + net.primServ.sendMonitorList(adds, true); + + msgname = (adds.length == 1) ? MSG_NOTIFY_ADDONE : + MSG_NOTIFY_ADDSOME; + display(getMsg(msgname, arraySpeak(adds))); + } + + if (subs.length > 0) + { + if (supports_monitor) + net.primServ.sendMonitorList(subs, false); + + msgname = (subs.length == 1) ? MSG_NOTIFY_DELONE : + MSG_NOTIFY_DELSOME; + display(getMsg(msgname, arraySpeak(subs))); + } + + delete net.onList; + delete net.offList; + if (!supports_monitor) + onNotifyTimeout(); + } +} + +function cmdStalk(e) +{ + var list = client.prefs["stalkWords"]; + + if (!e.text) + { + if (list.length == 0) + display(MSG_NO_STALK_LIST); + else + { + function alphabetize(a, b) + { + var A = a.toLowerCase(); + var B = b.toLowerCase(); + if (A < B) return -1; + if (B < A) return 1; + return 0; + } + + list.sort(alphabetize); + display(getMsg(MSG_STALK_LIST, list.join(", "))); + } + return; + } + + var notStalkingWord = true; + var loweredText = e.text.toLowerCase(); + + for (var i = 0; i < list.length; ++i) + if (list[i].toLowerCase() == loweredText) + notStalkingWord = false; + + if (notStalkingWord) + { + list.push(e.text); + list.update(); + display(getMsg(MSG_STALK_ADD, e.text)); + } + else + { + display(getMsg(MSG_STALKING_ALREADY, e.text)); + } +} + +function cmdUnstalk(e) +{ + e.text = e.text.toLowerCase(); + var list = client.prefs["stalkWords"]; + + for (var i = 0; i < list.length; ++i) + { + if (list[i].toLowerCase() == e.text) + { + list.splice(i, 1); + list.update(); + display(getMsg(MSG_STALK_DEL, e.text)); + return; + } + } + + display(getMsg(MSG_ERR_UNKNOWN_STALK, e.text), MT_ERROR); +} + +function cmdUser(e) +{ + dispatch("name", {username: e.username, network: e.network, + isInteractive: e.isInteractive}); + dispatch("desc", {description: e.description, network: e.network, + isInteractive: e.isInteractive}); +} + +function cmdUserhost(e) +{ + var nickList = combineNicks(e.nicknameList, 5); + for (var i = 0; i < nickList.length; i++) + { + e.server.userhost(nickList[i]); + } +} + +function cmdUserip(e) +{ + // Check if the server supports this + if (!e.server.servCmds.userip) + { + display(getMsg(MSG_ERR_UNSUPPORTED_COMMAND, "USERIP"), MT_ERROR); + return; + } + var nickList = combineNicks(e.nicknameList, 5); + for (var i = 0; i < nickList.length; i++) + e.server.userip(nickList[i]); +} + +function cmdUsermode(e) +{ + if (e.newMode) + { + if (e.sourceObject.network) + e.sourceObject.network.prefs["usermode"] = e.newMode; + else + client.prefs["usermode"] = e.newMode; + } + else + { + if (e.server && e.server.isConnected) + { + e.server.sendData("mode " + e.server.me.encodedName + "\n"); + } + else + { + var prefs; + + if (e.network) + prefs = e.network.prefs; + else + prefs = client.prefs; + + display(getMsg(MSG_USER_MODE, + [prefs["nickname"], prefs["usermode"]]), + MT_MODE); + } + } +} + +function cmdLog(e) +{ + var view = e.sourceObject; + + if (e.state != null) + { + e.state = getToggle(e.state, view.prefs["log"]) + view.prefs["log"] = e.state; + } + else + { + if (view.prefs["log"]) + display(getMsg(MSG_LOGGING_ON, getLogPath(view))); + else + display(MSG_LOGGING_OFF); + } +} + +function cmdSave(e) +{ + var OutputProgressListener = + { + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) + { + // Use this to access onStateChange flags + var requestSpec; + try + { + var channel = aRequest.QueryInterface(nsIChannel); + requestSpec = channel.URI.spec; + } + catch (ex) { } + + // Detect end of file saving of any file: + if (aStateFlags & nsIWebProgressListener.STATE_STOP) + { + if (aStatus == kErrorBindingAborted) + aStatus = 0; + + // We abort saving for all errors except if image src file is + // not found + var abortSaving = (aStatus != 0 && aStatus != kFileNotFound); + if (abortSaving) + { + // Cancel saving + wbp.cancelSave(); + display(getMsg(MSG_SAVE_ERR_FAILED, aMessage), MT_ERROR); + return; + } + + if (aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK + && wbp.currentState == nsIWBP.PERSIST_STATE_FINISHED) + { + // Let the user know: + pm = [e.sourceObject.viewName, getURLSpecFromFile(file)]; + display(getMsg(MSG_SAVE_SUCCESSFUL, pm), MT_INFO); + } + /* Check if we've finished. WebBrowserPersist screws up when we + * don't save additional files. Cope when saving html only or + * text. + */ + else if (!requestSpec && saveType > 0) + { + if (wbp) + wbp.progressListener = null; + pm = [e.sourceObject.viewName, getURLSpecFromFile(file)]; + display(getMsg(MSG_SAVE_SUCCESSFUL, pm), MT_INFO); + } + } + }, + + onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, + aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress) {}, + onLocationChange: function(aWebProgress, aRequest, aLocation) {}, + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange: function(aWebProgress, aRequest, state) {}, + + QueryInterface: function(aIID) + { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) + || aIID.equals(Components.interfaces.nsISupports) + || aIID.equals(Components.interfaces.nsISupportsWeakReference)) + { + return this; + } + + throw Components.results.NS_NOINTERFACE; + } + }; + + const kFileNotFound = 2152857618; + const kErrorBindingAborted = 2152398850; + + const nsIWBP = Components.interfaces.nsIWebBrowserPersist; + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + const nsIChannel = Components.interfaces.nsIChannel; + + var wbp = newObject("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", + nsIWBP); + wbp.progressListener = OutputProgressListener; + + var file, saveType, saveFolder, docToBeSaved, title; + var flags, fileType, charLimit; + var dialogTitle, rv, pm; + + // We want proper descriptions and no "All Files" option. + const TYPELIST = [[MSG_SAVE_COMPLETEVIEW,"*.htm;*.html"], + [MSG_SAVE_HTMLONLYVIEW,"*.htm;*.html"], + [MSG_SAVE_PLAINTEXTVIEW,"*.txt"], "$noAll"]; + // constants and variables for the wbp.saveDocument call + var saveTypes = + { + complete: 0, + htmlonly: 1, + text: 2 + }; + + if (!e.filename) + { + dialogTitle = getMsg(MSG_SAVE_DIALOGTITLE, e.sourceObject.viewName); + rv = pickSaveAs(dialogTitle, TYPELIST, e.sourceObject.viewName + + ".html"); + if (!rv.ok) + return; + saveType = rv.picker.filterIndex; + file = rv.file; + e.filename = rv.file.path; + } + else + { + try + { + // Try to use this as a path + file = nsLocalFile(e.filename); + } + catch (ex) + { + // try to use it as a URL + try + { + file = getFileFromURLSpec(e.filename); + } + catch(ex) + { + // What is the user thinking? It's not rocket science... + display(getMsg(MSG_SAVE_ERR_INVALID_PATH, e.filename), + MT_ERROR); + return; + } + } + + // Get extension and determine savetype + if (!e.savetype) + { + var extension = file.path.substr(file.path.lastIndexOf(".")); + if (extension == ".txt") + { + saveType = saveTypes["text"]; + } + else if (extension.match(/\.x?html?$/)) + { + saveType = saveTypes["complete"]; + } + else + { + // No saveType and no decent extension --> out! + var errMsg; + if (extension.indexOf(".") < 0) + errMsg = MSG_SAVE_ERR_NO_EXT; + else + errMsg = getMsg(MSG_SAVE_ERR_INVALID_EXT, extension); + display(errMsg, MT_ERROR); + return; + } + } + else + { + if (!(e.savetype in saveTypes)) + { + // no valid saveType + display(getMsg(MSG_SAVE_ERR_INVALID_SAVETYPE, e.savetype), + MT_ERROR); + return; + } + saveType = saveTypes[e.savetype]; + } + + var askforreplace = (e.isInteractive && file.exists()); + if (askforreplace && !confirm(getMsg(MSG_SAVE_FILEEXISTS, e.filename))) + return; + } + + // We don't want to convert anything, leave everything as is and replace + // old files, as the user has been prompted about that already. + wbp.persistFlags |= nsIWBP.PERSIST_FLAGS_NO_CONVERSION + | nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES + | nsIWBP.PERSIST_FLAGS_NO_BASE_TAG_MODIFICATIONS + | nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES + | nsIWBP.PERSIST_FLAGS_DONT_FIXUP_LINKS + | nsIWBP.PERSIST_FLAGS_DONT_CHANGE_FILENAMES; + + // Set the document from the current view, and set a usable title + docToBeSaved = getContentDocument(e.sourceObject.frame); + var headElement = docToBeSaved.getElementsByTagName("HEAD")[0]; + var titleElements = docToBeSaved.getElementsByTagName("title"); + // Remove an existing title, there shouldn't be more than one. + if (titleElements.length > 0) + titleElements[0].parentNode.removeChild(titleElements[0]); + title = docToBeSaved.createElement("title"); + title.appendChild(docToBeSaved.createTextNode(document.title + + " (" + new Date() + ")")); + headElement.appendChild(title); + // Set standard flags, file type, saveFolder and character limit + flags = nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; + fileType = "text/html"; + saveFolder = null; + charLimit = 0; + + // Do saveType specific stuff + switch (saveType) + { + case saveTypes["complete"]: + // Get the directory into which to save associated files. + saveFolder = file.clone(); + var baseName = saveFolder.leafName; + baseName = baseName.substring(0, baseName.lastIndexOf(".")); + saveFolder.leafName = getMsg(MSG_SAVE_FILES_FOLDER, baseName); + break; + // html only does not need any additional configuration + case saveTypes["text"]: + // set flags for Plain Text + flags = nsIWBP.ENCODE_FLAGS_FORMATTED; + flags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; + flags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; + // set the file type and set character limit to 80 + fileType = "text/plain"; + charLimit = 80; + break; + } + + try + { + wbp.saveDocument(docToBeSaved, file, saveFolder, fileType, flags, + charLimit); + } + catch (ex) + { + pm = [e.sourceObject.viewName, e.filename, ex.message]; + display(getMsg(MSG_SAVE_ERR_FAILED, pm), MT_ERROR); + } + // Error handling and finishing message is done by the listener +} + +function cmdSupports(e) +{ + var server = e.server; + var data = server.supports; + + if ("channelTypes" in server) + display(getMsg(MSG_SUPPORTS_CHANTYPES, + server.channelTypes.join(", "))); + if ("channelModes" in server) + { + display(getMsg(MSG_SUPPORTS_CHANMODESA, + server.channelModes.a.join(", "))); + display(getMsg(MSG_SUPPORTS_CHANMODESB, + server.channelModes.b.join(", "))); + display(getMsg(MSG_SUPPORTS_CHANMODESC, + server.channelModes.c.join(", "))); + display(getMsg(MSG_SUPPORTS_CHANMODESD, + server.channelModes.d.join(", "))); + } + + if ("userModes" in server) + { + var list = new Array(); + for (var m in server.userModes) + { + list.push(getMsg(MSG_SUPPORTS_USERMODE, [ + server.userModes[m].mode, + server.userModes[m].symbol + ])); + } + display(getMsg(MSG_SUPPORTS_USERMODES, list.join(", "))); + } + + var listB1 = new Array(); + var listB2 = new Array(); + var listN = new Array(); + for (var k in data) + { + if (typeof data[k] == "boolean") + { + if (data[k]) + listB1.push(k); + else + listB2.push(k); + } + else + { + listN.push(getMsg(MSG_SUPPORTS_MISCOPTION, [ k, data[k] ] )); + } + } + listB1.sort(); + listB2.sort(); + listN.sort(); + display(getMsg(MSG_SUPPORTS_FLAGSON, listB1.join(", "))); + display(getMsg(MSG_SUPPORTS_FLAGSOFF, listB2.join(", "))); + display(getMsg(MSG_SUPPORTS_MISCOPTIONS, listN.join(", "))); + + var listCaps = new Array(); + var listCapsEnabled = new Array(); + for (var cap in server.caps) + { + listCaps.push(cap); + if (server.caps[cap]) + listCapsEnabled.push(cap); + } + if (listCaps.length > 0) + { + listCaps.sort(); + listCapsEnabled.sort(); + display(getMsg(MSG_SUPPORTS_CAPS, listCaps.join(", "))); + display(getMsg(MSG_SUPPORTS_CAPSON, listCapsEnabled.join(", "))); + } +} + +function cmdDoCommand(e) +{ + if (e.cmdName == "cmd_mozillaPrefs") + { + // Open SeaMonkey preferences. + goPreferences("navigator_pane"); + } + else if (e.cmdName == "cmd_chatzillaPrefs") + { + var prefWin = getWindowByType("irc:chatzilla:config"); + if (!prefWin) + { + window.openDialog('chrome://chatzilla/content/config.xul', '', + 'chrome,resizable,dialog=no', window); + } + else + { + prefWin.focus(); + } + } + else if (e.cmdName == "cmd_selectAll") + { + var userList = document.getElementById("user-list"); + var elemFocused = document.commandDispatcher.focusedElement; + + if (userList.view && (elemFocused == userList)) + userList.view.selection.selectAll(); + else + doCommand("cmd_selectAll"); + } + else + { + doCommand(e.cmdName); + } +} + +function cmdTime(e) +{ + if (e.nickname) + e.network.dispatch("ctcp", { target: e.nickname, code: "TIME"}); + else + e.server.sendData(fromUnicode("TIME") + "\n", e.sourceObject); +} + +function cmdTimestamps(e) +{ + var view = e.sourceObject; + + if (e.toggle != null) + { + e.toggle = getToggle(e.toggle, view.prefs["timestamps"]) + view.prefs["timestamps"] = e.toggle; + } + else + { + display(getMsg(MSG_FMT_PREF, ["timestamps", + view.prefs["timestamps"]])); + } +} + +function cmdSetCurrentView(e) +{ + if ("lockView" in e.view) + delete e.view.lockView; + + setCurrentObject(e.view); +} + +function cmdJumpToAnchor(e) +{ + if (e.hasOwnProperty("channelName")) + { + e.channel = new CIRCChannel(e.server, e.channelName); + } + else if (!e.channel) + { + display(getMsg(MSG_ERR_REQUIRED_PARAM, "channel-name"), MT_ERROR); + return; + } + if (!e.channel.frame) + { + display(getMsg(MSG_JUMPTO_ERR_NOCHAN, e.channel.unicodeName), MT_ERROR); + return; + } + + var document = getContentDocument(e.channel.frame); + var row = document.getElementById(e.anchor); + + if (!row) + { + display(getMsg(MSG_JUMPTO_ERR_NOANCHOR), MT_ERROR); + return; + } + + dispatch("set-current-view", {view: e.channel}); + e.channel.scrollToElement(row, "center"); +} + +function cmdIdentify(e) +{ + e.password = client.tryToGetLogin(e.server.parent.getURL(), "nick", + e.server.me.name, e.password, true, + MSG_NEED_IDENTIFY_PASSWORD); + if (!e.password) + return; + + e.server.sendData("NS IDENTIFY " + fromUnicode(e.password, e.server) + + "\n"); +} + +function cmdIgnore(e) +{ + if (("mask" in e) && e.mask) + { + e.mask = e.server.toLowerCase(e.mask); + + if (e.command.name == "ignore") + { + if (e.network.ignore(e.mask)) + display(getMsg(MSG_IGNORE_ADD, e.mask)); + else + display(getMsg(MSG_IGNORE_ADDERR, e.mask)); + } + else + { + if (e.network.unignore(e.mask)) + display(getMsg(MSG_IGNORE_DEL, e.mask)); + else + display(getMsg(MSG_IGNORE_DELERR, e.mask)); + } + // Update pref: + var ignoreList = keys(e.network.ignoreList); + e.network.prefs["ignoreList"] = ignoreList; + e.network.prefs["ignoreList"].update(); + } + else + { + var list = new Array(); + for (var m in e.network.ignoreList) + list.push(m); + if (list.length == 0) + display(MSG_IGNORE_LIST_1); + else + display(getMsg(MSG_IGNORE_LIST_2, arraySpeak(list))); + } +} + +function cmdFont(e) +{ + var view = client; + var pref, val, pVal; + + if (e.command.name == "font-family") + { + pref = "font.family"; + val = e.font; + + // Save new value, then display pref value. + if (val) + view.prefs[pref] = val; + + display(getMsg(MSG_FONTS_FAMILY_FMT, view.prefs[pref])); + } + else if (e.command.name == "font-size") + { + pref = "font.size"; + val = e.fontSize; + + // Ok, we've got an input. + if (val) + { + // Get the current value, use user's default if needed. + pVal = view.prefs[pref]; + if (pVal == 0) + pVal = getDefaultFontSize(); + + // Handle user's input... + switch(val) { + case "default": + val = 0; + break; + + case "small": + val = getDefaultFontSize() - 2; + break; + + case "medium": + val = getDefaultFontSize(); + break; + + case "large": + val = getDefaultFontSize() + 2; + break; + + case "smaller": + val = pVal - 2; + break; + + case "bigger": + val = pVal + 2; + break; + + default: + if (isNaN(val)) + val = 0; + else + val = Number(val); + } + // Save the new value. + view.prefs[pref] = val; + } + + // Show the user what the pref is set to. + if (view.prefs[pref] == 0) + display(MSG_FONTS_SIZE_DEFAULT); + else + display(getMsg(MSG_FONTS_SIZE_FMT, view.prefs[pref])); + } + else if (e.command.name == "font-family-other") + { + val = prompt(MSG_FONTS_FAMILY_PICK, view.prefs["font.family"]); + if (!val) + return; + + dispatch("font-family", { font: val }); + } + else if (e.command.name == "font-size-other") + { + pVal = view.prefs["font.size"]; + if (pVal == 0) + pVal = getDefaultFontSize(); + + val = prompt(MSG_FONTS_SIZE_PICK, pVal); + if (!val) + return; + + dispatch("font-size", { fontSize: val }); + } +} + +function cmdDCCChat(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + if (!e.nickname && !e.user) + return display(MSG_DCC_ERR_NOUSER); + + var user; + if (e.nickname) + user = e.server.addUser(e.nickname); + else + user = e.server.addUser(e.user.unicodeName); + + var u = client.dcc.addUser(user); + var c = client.dcc.addChat(u, client.dcc.getNextPort()); + c.request(); + + client.munger.getRule(".inline-buttons").enabled = true; + var cmd = getMsg(MSG_DCC_COMMAND_CANCEL, "dcc-close " + c.id); + display(getMsg(MSG_DCCCHAT_SENT_REQUEST, c._getParams().concat(cmd)), + "DCC-CHAT"); + client.munger.getRule(".inline-buttons").enabled = false; + + return true; +} + +function cmdDCCClose(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + // If there is no nickname specified, use current view. + if (!e.nickname) + { + // Both DCC chat and file transfers can be aborted like this. + if (e.sourceObject.TYPE.substr(0, 6) == "IRCDCC") + { + if (e.sourceObject.isActive()) + return e.sourceObject.abort(); + return true; + } + // ...if there is one. + return display(MSG_DCC_ERR_NOTDCC); + } + + var o = client.dcc.findByID(e.nickname); + if (o) + // Direct ID --> object request. + return o.abort(); + + if (e.type) + e.type = [e.type.toLowerCase()]; + else + e.type = ["chat", "file"]; + + // Go ask the DCC code for some matching requets. + var list = client.dcc.getMatches + (e.nickname, e.file, e.type, [DCC_DIR_GETTING, DCC_DIR_SENDING], + [DCC_STATE_REQUESTED, DCC_STATE_ACCEPTED, DCC_STATE_CONNECTED]); + + // Disconnect if only one match. + if (list.length == 1) + return list[0].abort(); + + // Oops, couldn't figure the user's requets out, so give them some help. + display(getMsg(MSG_DCC_ACCEPTED_MATCHES, [list.length])); + display(MSG_DCC_MATCHES_HELP); + return true; +} + +function cmdDCCSend(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + const DIRSVC_CID = "@mozilla.org/file/directory_service;1"; + const nsIProperties = Components.interfaces.nsIProperties; + + if (!e.nickname && !e.user) + return display(MSG_DCC_ERR_NOUSER); + + // Accept the request passed in... + var file; + if (!e.file) + { + var pickerRv = pickOpen(MSG_DCCFILE_SEND); + if (!pickerRv.ok) + return false; + file = pickerRv.file; + } + else + { + // Wrap in try/catch because nsIFile creation throws a freaking + // error if it doesn't get a FULL path. + try + { + file = nsLocalFile(e.file); + } + catch(ex) + { + // Ok, try user's home directory. + var fl = Components.classes[DIRSVC_CID].getService(nsIProperties); + file = fl.get("Home", Components.interfaces.nsIFile); + + // Another freaking try/catch wrapper. + try + { + // NOTE: This is so pathetic it can't cope with any path + // separators in it, so don't even THINK about lobing a + // relative path at it. + file.append(e.file); + + // Wow. We survived. + } + catch (ex) + { + return display(MSG_DCCFILE_ERR_NOTFOUND); + } + } + } + if (!file.exists()) + return display(MSG_DCCFILE_ERR_NOTFOUND); + if (!file.isFile()) + return display(MSG_DCCFILE_ERR_NOTAFILE); + if (!file.isReadable()) + return display(MSG_DCCFILE_ERR_NOTREADABLE); + + var user; + if (e.nickname) + user = e.server.addUser(e.nickname); + else + user = e.server.addUser(e.user.unicodeName); + + var u = client.dcc.addUser(user); + var c = client.dcc.addFileTransfer(u, client.dcc.getNextPort()); + c.request(file); + + client.munger.getRule(".inline-buttons").enabled = true; + var cmd = getMsg(MSG_DCC_COMMAND_CANCEL, "dcc-close " + c.id); + display(getMsg(MSG_DCCFILE_SENT_REQUEST, [c.user.unicodeName, c.localIP, + c.port, c.filename, + getSISize(c.size), cmd]), + "DCC-FILE"); + client.munger.getRule(".inline-buttons").enabled = false; + + return true; +} + +function cmdDCCList(e) { + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + var counts = { pending: 0, connected: 0, failed: 0 }; + var k; + + // Get all the DCC sessions. + var list = client.dcc.getMatches(); + + for (k = 0; k < list.length; k++) { + var c = list[k]; + var type = c.TYPE.substr(6, c.TYPE.length - 6); + + var dir = MSG_UNKNOWN; + var tf = MSG_UNKNOWN; + if (c.state.dir == DCC_DIR_SENDING) + { + dir = MSG_DCCLIST_DIR_OUT; + tf = MSG_DCCLIST_TO; + } + else if (c.state.dir == DCC_DIR_GETTING) + { + dir = MSG_DCCLIST_DIR_IN; + tf = MSG_DCCLIST_FROM; + } + + var state = MSG_UNKNOWN; + var cmds = ""; + switch (c.state.state) + { + case DCC_STATE_REQUESTED: + state = MSG_DCC_STATE_REQUEST; + if (c.state.dir == DCC_DIR_GETTING) + { + cmds = getMsg(MSG_DCC_COMMAND_ACCEPT, "dcc-accept " + c.id) + " " + + getMsg(MSG_DCC_COMMAND_DECLINE, "dcc-decline " + c.id); + } + else + { + cmds = getMsg(MSG_DCC_COMMAND_CANCEL, "dcc-close " + c.id); + } + counts.pending++; + break; + case DCC_STATE_ACCEPTED: + state = MSG_DCC_STATE_ACCEPT; + counts.connected++; + break; + case DCC_STATE_DECLINED: + state = MSG_DCC_STATE_DECLINE; + break; + case DCC_STATE_CONNECTED: + state = MSG_DCC_STATE_CONNECT; + cmds = getMsg(MSG_DCC_COMMAND_CLOSE, "dcc-close " + c.id); + if (c.TYPE == "IRCDCCFileTransfer") + { + state = getMsg(MSG_DCC_STATE_CONNECTPRO, + [c.progress, + getSISize(c.position), getSISize(c.size), + getSISpeed(c.speed)]); + } + counts.connected++; + break; + case DCC_STATE_DONE: + state = MSG_DCC_STATE_DISCONNECT; + break; + case DCC_STATE_ABORTED: + state = MSG_DCC_STATE_ABORT; + counts.failed++; + break; + case DCC_STATE_FAILED: + state = MSG_DCC_STATE_FAIL; + counts.failed++; + break; + } + client.munger.getRule(".inline-buttons").enabled = true; + display(getMsg(MSG_DCCLIST_LINE, [k + 1, state, dir, type, tf, + c.unicodeName, c.remoteIP, c.port, + cmds])); + client.munger.getRule(".inline-buttons").enabled = false; + } + display(getMsg(MSG_DCCLIST_SUMMARY, [counts.pending, counts.connected, + counts.failed])); + return true; +} + +function cmdDCCAutoAcceptList(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + var list = e.network.prefs["dcc.autoAccept.list"]; + + if (list.length == 0) + display(MSG_DCCACCEPT_DISABLED); + else + display(getMsg(MSG_DCCACCEPT_LIST, arraySpeak(list))); + + return true; +} + +function cmdDCCAutoAcceptAdd(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + var list = e.network.prefs["dcc.autoAccept.list"]; + + if (!e.user && e.server) + e.user = e.server.getUser(e.nickname); + + var mask = e.user ? "*!" + e.user.name + "@" + e.user.host : e.nickname; + var index = arrayIndexOf(list, mask); + if (index == -1) + { + list.push(mask); + list.update(); + display(getMsg(MSG_DCCACCEPT_ADD, mask)); + } + else + { + display(getMsg(MSG_DCCACCEPT_ADDERR, + e.user ? e.user.unicodeName : e.nickname)); + } + return true; +} + +function cmdDCCAutoAcceptDel(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + var list = e.network.prefs["dcc.autoAccept.list"]; + + if (!e.user && e.server) + e.user = e.server.getUser(e.nickname); + + var maskObj, newList = new Array(); + for (var m = 0; m < list.length; ++m) + { + maskObj = getHostmaskParts(list[m]); + if (e.nickname == list[m] || + (e.user && hostmaskMatches(e.user, maskObj, e.server))) + { + display(getMsg(MSG_DCCACCEPT_DEL, list[m])); + } + else + { + newList.push(list[m]); + } + } + + if (list.length > newList.length) + e.network.prefs["dcc.autoAccept.list"] = newList; + else + display(getMsg(MSG_DCCACCEPT_DELERR, + e.user ? e.user.unicodeName : e.nickname)); + + return true; +} + +function cmdDCCAccept(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + function accept(c) + { + if (c.TYPE == "IRCDCCChat") + { + if (!c.accept()) + return false; + + display(getMsg(MSG_DCCCHAT_ACCEPTED, c._getParams()), "DCC-CHAT"); + return true; + } + + // Accept the request passed in... + var filename = c.filename; + var ext = "*"; + var m = filename.match(/...\.([a-z]+)$/i); + if (m) + ext = "*." + m[1]; + + var pickerRv = pickSaveAs(getMsg(MSG_DCCFILE_SAVE_TO, filename), + ["$all", ext], filename); + if (!pickerRv.ok) + return false; + + if (!c.accept(pickerRv.file)) + return false; + + display(getMsg(MSG_DCCFILE_ACCEPTED, c._getParams()), "DCC-FILE"); + return true; + }; + + // If there is no nickname specified, use the "last" item. + // This is the last DCC request that arrvied. + if (!e.nickname && client.dcc.last) + { + if ((new Date() - client.dcc.lastTime) >= 10000) + return accept(client.dcc.last); + return display(MSG_DCC_ERR_ACCEPT_TIME); + } + + var o = client.dcc.findByID(e.nickname); + if (o) + // Direct ID --> object request. + return accept(o); + + if (e.type) + e.type = [e.type.toLowerCase()]; + else + e.type = ["chat", "file"]; + + // Go ask the DCC code for some matching requets. + var list = client.dcc.getMatches(e.nickname, e.file, e.type, + [DCC_DIR_GETTING], [DCC_STATE_REQUESTED]); + // Accept if only one match. + if (list.length == 1) + return accept(list[0]); + + // Oops, couldn't figure the user's request out, so give them some help. + display(getMsg(MSG_DCC_PENDING_MATCHES, [list.length])); + display(MSG_DCC_MATCHES_HELP); + return true; +} + +function cmdDCCDecline(e) +{ + if (!client.prefs["dcc.enabled"]) + return display(MSG_DCC_NOT_ENABLED); + + function decline(c) + { + // Decline the request passed in... + c.decline(); + if (c.TYPE == "IRCDCCChat") + display(getMsg(MSG_DCCCHAT_DECLINED, c._getParams()), "DCC-CHAT"); + else + display(getMsg(MSG_DCCFILE_DECLINED, c._getParams()), "DCC-FILE"); + }; + + // If there is no nickname specified, use the "last" item. + // This is the last DCC request that arrvied. + if (!e.nickname && client.dcc.last) + return decline(client.dcc.last); + + var o = client.dcc.findByID(e.nickname); + if (o) + // Direct ID --> object request. + return decline(o); + + if (!e.type) + e.type = ["chat", "file"]; + + // Go ask the DCC code for some matching requets. + var list = client.dcc.getMatches(e.nickname, e.file, e.type, + [DCC_DIR_GETTING], [DCC_STATE_REQUESTED]); + // Decline if only one match. + if (list.length == 1) + return decline(list[0]); + + // Oops, couldn't figure the user's requets out, so give them some help. + display(getMsg(MSG_DCC_PENDING_MATCHES, [list.length])); + display(MSG_DCC_MATCHES_HELP); + return true; +} + +function cmdDCCShowFile(e) +{ + var f = getFileFromURLSpec(e.file); + if (f) + f = nsLocalFile(f.path); + if (f && f.parent && f.parent.exists()) + { + try + { + f.reveal(); + } + catch (ex) + { + dd(formatException(ex)); + } + } +} + +function cmdTextDirection(e) +{ + var direction; + var sourceObject = getContentDocument(e.sourceObject.frame).body; + + switch (e.dir) + { + case "toggle": + if (sourceObject.getAttribute("dir") == "rtl") + direction = 'ltr'; + else + direction = 'rtl'; + break; + case "rtl": + direction = 'rtl'; + break; + default: + // that is "case "ltr":", + // but even if !e.dir OR e.dir is an invalid value -> set to + // default direction + direction = 'ltr'; + } + client.input.setAttribute("dir", direction); + sourceObject.setAttribute("dir", direction); + + return true; +} + +function cmdInputTextDirection(e) +{ + var direction; + + switch (e.dir) + { + case "rtl": + client.input.setAttribute("dir", "rtl"); + break + default: + // that is "case "ltr":", but even if !e.dir OR e.dir is an + //invalid value -> set to default direction + client.input.setAttribute("dir", "ltr"); + } + + return true; +} + +function cmdInstallPlugin(e) +{ + var ipURL = "chrome://chatzilla/content/install-plugin/install-plugin.xul"; + var ctx = {}; + var pluginDownloader = + { + onStartRequest: function _onStartRequest(request, context) + { + var tempName = "plugin-install.temp"; + if (urlMatches) + tempName += urlMatches[2]; + + ctx.outFile = getTempFile(client.prefs["profilePath"], tempName); + ctx.outFileH = fopen(ctx.outFile, ">"); + }, + onDataAvailable: function _onDataAvailable(request, context, stream, + offset, count) + { + if (!ctx.inputStream) + ctx.inputStream = toSInputStream(stream, true); + + ctx.outFileH.write(ctx.inputStream.readBytes(count)); + }, + onStopRequest: function _onStopRequest(request, context, statusCode) + { + ctx.outFileH.close(); + + if (statusCode == 0) + { + client.installPlugin(e.name, ctx.outFile); + } + else + { + display(getMsg(MSG_INSTALL_PLUGIN_ERR_DOWNLOAD, statusCode), + MT_ERROR); + } + + try + { + ctx.outFile.remove(false); + } + catch (ex) + { + display(getMsg(MSG_INSTALL_PLUGIN_ERR_REMOVE_TEMP, ex), + MT_ERROR); + } + } + }; + + if (!e.url) + { + if ("installPluginDialog" in client) + return client.installPluginDialog.focus(); + + window.openDialog(ipURL, "", "chrome,dialog", client); + return; + } + + var urlMatches = e.url.match(/([^\/]+?)((\..{0,3}){0,2})$/); + if (!e.name) + { + if (urlMatches) + { + e.name = urlMatches[1]; + } + else + { + display(MSG_INSTALL_PLUGIN_ERR_NO_NAME, MT_ERROR); + return; + } + } + + // Do real install here. + switch (e.url.match(/^[^:]+/)[0]) + { + case "file": + client.installPlugin(e.name, e.url); + break; + + case "http": + case "https": + try + { + var channel = Services.io.newChannel( + e.url, "UTF-8", null, null, + Services.scriptSecurityManager.getSystemPrincipal(), null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + display(getMsg(MSG_INSTALL_PLUGIN_DOWNLOADING, e.url), + MT_INFO); + channel.asyncOpen(pluginDownloader, { e: e }); + } + catch (ex) + { + display(getMsg(MSG_INSTALL_PLUGIN_ERR_DOWNLOAD, ex), MT_ERROR); + return; + } + break; + + default: + display(MSG_INSTALL_PLUGIN_ERR_PROTOCOL, MT_ERROR); + } +} + +function cmdUninstallPlugin(e) +{ + if (e.plugin) + { + client.uninstallPlugin(e.plugin); + } +} + +function cmdFind(e) +{ + if (!e.rest) + { + findInPage(getFindData(e)); + return; + } + + // Used from the inputbox, set the search string and find the first + // occurrence using find-again. + const FINDSVC_ID = "@mozilla.org/find/find_service;1"; + var findService = getService(FINDSVC_ID, "nsIFindService"); + // Make sure it searches the entire document, but don't lose the old setting + var oldWrap = findService.wrapFind; + findService.wrapFind = true; + findService.searchString = e.rest; + findAgainInPage(getFindData(e)); + // Restore wrap setting: + findService.wrapFind = oldWrap; +} + +function cmdFindAgain(e) +{ + if (canFindAgainInPage()) + findAgainInPage(getFindData(e)); +} + +function cmdURLs(e) +{ + var urls = client.urlLogger.read().reverse(); + + if (urls.length == 0) + { + display(MSG_URLS_NONE); + } + else + { + /* Temporarily remove the URL logger to avoid changing the list when + * displaying it. + */ + var logger = client.urlLogger; + delete client.urlLogger; + + var num = e.number || client.prefs["urls.display"]; + if (num > urls.length) + num = urls.length; + display(getMsg(MSG_URLS_HEADER, num)); + + for (var i = 0; i < num; i++) + display(getMsg(MSG_URLS_ITEM, [i + 1, urls[i]])); + + client.urlLogger = logger; + } +} + +function cmdWebSearch(e) +{ + let submission = Services.search.currentEngine + .getSubmission(e.selectedText); + let newTabPref = Services.prefs.getBoolPref("browser.search.opentabforcontextsearch"); + dispatch(newTabPref ? "goto-url-newtab" : "goto-url-newwin", + {url: submission.uri.asciiSpec, + shiftKey: e.shiftKey}); +} diff --git a/comm/suite/chatzilla/xul/content/config-add.js b/comm/suite/chatzilla/xul/content/config-add.js new file mode 100644 index 0000000000..81034e2071 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/config-add.js @@ -0,0 +1,55 @@ +/* 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 rv, rad, box1, box2; + +function changeType() +{ + box2.disabled = (rad.value == "net"); +} + +function onOK() +{ + rv.ok = true; + + rv.type = rad.value; + rv.net = box1.value; + rv.chan = box2.value; + + return true; +} + +function onCancel() +{ + rv.ok = false; + + return true; +} + +function onLoad() +{ + rad = document.getElementById("prefType"); + box1 = document.getElementById("prefName1"); + box2 = document.getElementById("prefName2"); + + rv = window.arguments[0]; + + if (!("type" in rv)) + rv.type = ""; + if (!("net" in rv)) + rv.net = ""; + if (!("chan" in rv)) + rv.chan = ""; + rv.ok = false; + + if (rv.type == "net") + rad.selectedIndex = 0; + if (rv.type == "chan") + rad.selectedIndex = 1; + if (rv.type == "user") + rad.selectedIndex = 2; + + box1.value = rv.net || ""; + box2.value = rv.chan || ""; +} diff --git a/comm/suite/chatzilla/xul/content/config-add.xul b/comm/suite/chatzilla/xul/content/config-add.xul new file mode 100644 index 0000000000..84834ca5a7 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/config-add.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/config.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="config.css" type="text/css"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + windowtype="irc:chatzilla:config:add" + buttons="accept,cancel" + ondialogaccept="onOK();" + ondialogcancel="onCancel();" + onload="onLoad();" + title="&config.add.title;"> + + <script src="config-add.js"/> + + <vbox> + <hbox align="center"> + <label value="&config.type.label;" accesskey="&config.type.accesskey;" + tooltiptext="&config.type.hint;" control="prefType"/> + <!-- Beware the hacks, number 264: add |value| attribute to make .value + work when the dialog loads (|selected| selects an item, but doesn't + set .value). --> + <radiogroup orient="horizontal" id="prefType" value="chan" + onselect="changeType();"> + <radio value="net" label="&network;"/> + <radio value="chan" label="&channel;" selected="true"/> + <radio value="user" label="&user;"/> + </radiogroup> + </hbox> + <separator class="groove"/> + <grid> + <columns><column/><column flex="1"/></columns> + <rows> + <row align="center"> + <label value="&config.network.label;" control="prefName1" + tooltiptext="&config.network.hint;" + accesskey="&config.network.accesskey;"/> + <textbox id="prefName1"/> + </row> + <row align="center"> + <label value="&config.target.label;" control="prefName2" + tooltiptext="&config.target.hint;" + accesskey="&config.target.accesskey;"/> + <textbox id="prefName2"/> + </row> + </rows> + </grid> + </vbox> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/config.css b/comm/suite/chatzilla/xul/content/config.css new file mode 100644 index 0000000000..dfbd77fa5b --- /dev/null +++ b/comm/suite/chatzilla/xul/content/config.css @@ -0,0 +1,33 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +/* OSX uses lotsa padding on tabs. Need to allow window to expand. */ + +/* Instead of setting on dialog, we're going to set it on the two main + * components of the window, so that large changes in the size of bits of the + * dialog don't upset things. + */ + +/* Set min-width on the left-side tree. */ +#pref-objects { + min-width: 25ex; +} + +/* Set min-width and min-height on tabs container. */ +#pref-object-deck { + min-width: 65ex; + min-height: 32em; + width: 65ex; + height: 32em; +} + +scroller { + overflow: auto; +} + +listbox { + min-height: 7em; +} diff --git a/comm/suite/chatzilla/xul/content/config.js b/comm/suite/chatzilla/xul/content/config.js new file mode 100644 index 0000000000..12eefa9da3 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/config.js @@ -0,0 +1,1775 @@ +/* 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 MEDIATOR_CONTRACTID = "@mozilla.org/appshell/window-mediator;1"; + +const nsIWindowMediator = Components.interfaces.nsIWindowMediator; + +const CONFIG_WINDOWTYPE = "irc:chatzilla:config"; + +/* Now we create and set up some required items from other Chatzilla JS files + * that we really have no reason to load, but the ones we do need won't work + * without these... + */ +var ASSERT = function(cond, msg) { if (!cond) { alert(msg); } return cond; } +var client; + +function CIRCNetwork() {} +function CIRCServer() {} +function CIRCChannel() {} +function CIRCChanUser() {} +function CIRCUser() {} +function CIRCDCC() {} +function CIRCDCCUser() {} +function CIRCDCCChat() {} +function CIRCDCCFileTransfer() {} +function CIRCSTS() {} + +function getObjectDetails(obj) +{ + var rv = new Object(); + rv.sourceObject = obj; + rv.TYPE = obj.TYPE; + rv.parent = ("parent" in obj) ? obj.parent : null; + rv.user = null; + rv.channel = null; + rv.server = null; + rv.network = null; + + switch (obj.TYPE) + { + case "PrefNetwork": + rv.network = obj; + if ("primServ" in rv.network) + rv.server = rv.network.primServ; + else + rv.server = null; + break; + + case "PrefChannel": + rv.channel = obj; + rv.server = rv.channel.parent; + rv.network = rv.server.parent; + break; + + case "PrefUser": + rv.user = obj; + rv.server = rv.user.parent; + rv.network = rv.server.parent; + break; + } + + return rv; +} + +/* Global object for the prefs. The 'root' of all the objects to do with the + * prefs. + */ +function PrefGlobal() +{ + this.networks = new Object(); + this.commandManager = new Object(); + this.commandManager.defineCommand = function() {}; + this.commandManager.removeCommand = function() {}; + this.entities = new Object(); + this.hostCompat = new Object(); +} +PrefGlobal.prototype.TYPE = "PrefGlobal"; + +/* Represents a single network in the hierarchy. + * + * |force| - If true, sets a pref on this object. This makes sure the object + * is "known" next time we load up (since we look for any prefs). + * + * |show| - If true, the object still exists even if the magic pref is not set. + * Thus, allows an object to exist without any prefs set. + */ +function PrefNetwork(parent, name, force, show) +{ + if (":" + name in parent.networks) + return parent.networks[":" + name]; + + this.parent = parent; + this.unicodeName = name; + this.viewName = name; + this.canonicalName = name; + this.collectionKey = ":" + name; + this.encodedName = name; + this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_NETWORK, this.unicodeName); + this.servers = new Object(); + this.primServ = new PrefServer(this, "dummy server"); + this.channels = this.primServ.channels; + this.users = this.primServ.users; + this.prefManager = getNetworkPrefManager(this); + this.prefs = this.prefManager.prefs; + this.prefManager.onPrefChanged = function(){}; + + if (force) + this.prefs["hasPrefs"] = true; + + if (this.prefs["hasPrefs"] || show) + this.parent.networks[this.collectionKey] = this; + + return this; +}; +PrefNetwork.prototype.TYPE = "PrefNetwork"; + +/* Cleans up the mess. */ +PrefNetwork.prototype.clear = +function pnet_clear() +{ + this.prefs["hasPrefs"] = false; + delete this.parent.networks[this.collectionKey]; +} + +/* A middle-management object. + * + * Exists only to satisfy the IRC library pref functions that expect this + * particular hierarchy. + */ +function PrefServer(parent, name) +{ + this.parent = parent; + this.unicodeName = name; + this.viewName = name; + this.canonicalName = name; + this.collectionKey = ":" + name; + this.encodedName = name; + this.prettyName = this.unicodeName; // Not used, thus not localised. + this.channels = new Object(); + this.users = new Object(); + this.parent.servers[this.collectionKey] = this; + return this; +}; +PrefServer.prototype.TYPE = "PrefServer"; + +/* Represents a single channel in the hierarchy. + * + * |force| and |show| the same as PrefNetwork. + */ +function PrefChannel(parent, name, force, show) +{ + if (":" + name in parent.channels) + return parent.channels[":" + name]; + + this.parent = parent; + this.unicodeName = name; + this.viewName = name; + this.canonicalName = name; + this.collectionKey = ":" + name; + this.encodedName = name; + this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_CHANNEL, + [this.parent.parent.unicodeName, this.unicodeName]); + this.prefManager = getChannelPrefManager(this); + this.prefs = this.prefManager.prefs; + this.prefManager.onPrefChanged = function(){}; + + if (force) + this.prefs["hasPrefs"] = true; + + if (this.prefs["hasPrefs"] || show) + this.parent.channels[this.collectionKey] = this; + + return this; +}; +PrefChannel.prototype.TYPE = "PrefChannel"; + +/* Cleans up the mess. */ +PrefChannel.prototype.clear = +function pchan_clear() +{ + this.prefs["hasPrefs"] = false; + delete this.parent.channels[this.collectionKey]; +} + +/* Represents a single user in the hierarchy. + * + * |force| and |show| the same as PrefNetwork. + */ +function PrefUser(parent, name, force, show) +{ + if (":" + name in parent.users) + return parent.users[":" + name]; + + this.parent = parent; + this.unicodeName = name; + this.viewName = name; + this.canonicalName = name; + this.collectionKey = ":" + name; + this.encodedName = name; + this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_USER, + [this.parent.parent.unicodeName, this.unicodeName]); + this.prefManager = getUserPrefManager(this); + this.prefs = this.prefManager.prefs; + this.prefManager.onPrefChanged = function(){}; + + if (force) + this.prefs["hasPrefs"] = true; + + if (this.prefs["hasPrefs"] || show) + this.parent.users[this.collectionKey] = this; + + return this; +}; +PrefUser.prototype.TYPE = "PrefUser"; + +/* Cleans up the mess. */ +PrefUser.prototype.clear = +function puser_clear() +{ + this.prefs["hasPrefs"] = false; + delete this.parent.users[this.collectionKey]; +} + +// Stores a list of |PrefObject|s. +function PrefObjectList() +{ + this.objects = new Array(); + + return this; +} + +// Add an object, and init it's private data. +PrefObjectList.prototype.addObject = +function polist_addObject(pObject) +{ + this.objects.push(pObject); + return pObject.privateData = new ObjectPrivateData(pObject, this.objects.length - 1); +} + +/* Removes an object, without changing the index. */ +PrefObjectList.prototype.deleteObject = +function polist_addObject(index) +{ + this.objects[index].privateData.clear(); + this.objects[index].clear(); + this.objects[index] = { privateData: null }; +} + +// Get a specific object. +PrefObjectList.prototype.getObject = +function polist_getObject(index) +{ + return this.objects[index].privateData; +} + +// Gets the private data for an object. +PrefObjectList.getPrivateData = +function polist_getPrivateData(object) +{ + return object.privateData; +} + +// Stores the pref object's private data. +function ObjectPrivateData(parent, index) +{ + this.parent = parent; // Real pref object. + this.prefs = new Object(); + this.groups = new Object(); + + this.arrayIndex = index; + this.deckIndex = -1; + this.dataLoaded = false; + + var treeObj = document.getElementById("pref-tree-object"); + this.tree = document.getElementById("pref-tree"); + this.treeContainer = document.createElement("treeitem"); + this.treeNode = document.createElement("treerow"); + this.treeCell = document.createElement("treecell"); + + this.treeContainer.setAttribute("prefobjectindex", this.arrayIndex); + this.treeCell.setAttribute("label", this.parent.unicodeName); + + switch (this.parent.TYPE) + { + case "PrefChannel": + case "PrefUser": + var p = this.parent.parent.parent; // Network object. + var pData = PrefObjectList.getPrivateData(p); + + if (!("treeChildren" in pData) || !pData.treeChildren) + { + pData.treeChildren = document.createElement("treechildren"); + pData.treeContainer.appendChild(pData.treeChildren); + treeObj.view.toggleOpenState(treeObj.view.rowCount - 1); + } + pData.treeContainer.setAttribute("container", "true"); + pData.treeChildren.appendChild(this.treeContainer); + break; + + default: + this.tree.appendChild(this.treeContainer); + break; + } + + this.treeContainer.appendChild(this.treeNode); + this.treeNode.appendChild(this.treeCell); + + return this; +} + +// Creates all the XUL elements needed to show this pref object. +ObjectPrivateData.prototype.loadXUL = +function opdata_loadXUL(tabOrder) +{ + var t = this; + + /* Function that sorts the preferences by their label, else they look + * fairly random in order. + * + * Sort keys: not grouped, sub-group name, boolean, pref label. + */ + function sortByLabel(a, b) { + if (t.prefs[a].subGroup || t.prefs[b].subGroup) + { + // Non-grouped go first. + if (!t.prefs[a].subGroup) + return -1; + if (!t.prefs[b].subGroup) + return 1; + + // Sub-group names. + if (t.prefs[a].subGroup < t.prefs[b].subGroup) + return -1; + if (t.prefs[a].subGroup > t.prefs[b].subGroup) + return 1; + } + + // Booleans go first. + if ((t.prefs[a].type == "boolean") && (t.prefs[b].type != "boolean")) + return -1; + if ((t.prefs[a].type != "boolean") && (t.prefs[b].type == "boolean")) + return 1; + + // ...then label. + if (t.prefs[a].label < t.prefs[b].label) + return -1; + if (t.prefs[a].label > t.prefs[b].label) + return 1; + return 0; + }; + + if (this.deckIndex >= 0) + return; + + this.deck = document.getElementById("pref-object-deck"); + this.tabbox = document.createElement("tabbox"); + this.tabs = document.createElement("tabs"); + this.tabPanels = document.createElement("tabpanels"); + + this.tabbox.setAttribute("flex", 1); + this.tabPanels.setAttribute("flex", 1); + + this.tabbox.appendChild(this.tabs); + this.tabbox.appendChild(this.tabPanels); + this.deck.appendChild(this.tabbox); + + this.deckIndex = this.deck.childNodes.length - 1; + + this.loadData(); + + var prefList = keys(this.prefs); + prefList.sort(sortByLabel); + + for (var i = 0; i < tabOrder.length; i++) + { + var pto = tabOrder[i]; + var needTab = pto.fixed; + if (!needTab) + { + // Not a "always visible" tab, check we need it. + for (var j = 0; j < prefList.length; j++) + { + if (this.prefs[prefList[j]].mainGroup == pto.name) + { + needTab = true; + break; + } + } + } + if (needTab) + this.addGroup(pto.name); + } + + for (i = 0; i < prefList.length; i++) + this.prefs[prefList[i]].loadXUL(); + + if (this.tabs.childNodes.length > 0) + this.tabbox.selectedIndex = 0; +} + +// Loads all the prefs. +ObjectPrivateData.prototype.loadData = +function opdata_loadData() +{ + if (this.dataLoaded) + return; + + this.dataLoaded = true; + + // Now get the list of pref names, and add them... + var prefList = this.parent.prefManager.prefNames; + + for (var i in prefList) + this.addPref(prefList[i]); +} + +// Clears up all the XUL objects and data. +ObjectPrivateData.prototype.clear = +function opdata_clear() +{ + //dd("Removing prefs for " + this.parent.displayName + " {"); + if (!this.dataLoaded) + this.loadData(); + for (var i in this.prefs) + this.prefs[i].clear(); + //dd("}"); + + if (this.deckIndex >= 0) + { + this.deck.removeChild(this.tabbox); + this.treeContainer.removeAttribute("container"); + this.treeContainer.parentNode.removeChild(this.treeContainer); + } +} + +// Resets all the prefs to their original values. +ObjectPrivateData.prototype.reset = +function opdata_reset() +{ + for (var i in this.prefs) + if (this.prefs[i].type != "hidden") + this.prefs[i].reset(); +} + +// Adds a pref to the internal data structures. +ObjectPrivateData.prototype.addPref = +function opdata_addPref(name) +{ + return this.prefs[name] = new PrefData(this, name); +} + +// Adds a group to a pref object's data. +ObjectPrivateData.prototype.addGroup = +function opdata_addPref(name) +{ + // Special group for prefs we don't want shown (nothing sinister here). + if (name == "hidden") + return null; + + if (!(name in this.groups)) + this.groups[name] = new PrefMainGroup(this, name); + + return this.groups[name]; +} + +// Represents a single pref on a single object within the pref window. +function PrefData(parent, name) +{ + // We want to keep all this "worked out" info, so make a hash of all + // the prefs on the pwData [Pref Window Data] property of the object. + + // First, lets find out what kind of pref we've got: + this.parent = parent; // Private data for pref object. + this.name = name; + this.manager = this.parent.parent.prefManager; // PrefManager. + this.record = this.manager.prefRecords[name]; // PrefRecord. + this.def = this.record.defaultValue; // Default value. + this.type = typeof this.def; // Pref type. + this.val = this.manager.prefs[name]; // Current value. + this.startVal = this.val; // Start value. + this.label = this.record.label; // Display name. + this.help = this.record.help; // Help text. + this.group = this.record.group; // Group identifier. + this.labelFor = "none"; // Auto-grouped label. + + // Handle defered prefs (call defer function, and use resulting + // value/type instead). + if (this.type == "function") + this.def = this.def(this.name); + this.type = typeof this.def; + + // And those arrays... this just makes our life easier later by having + // a particular name for array prefs. + if (isinstance(this.def, Array)) + this.type = "array"; + + if (this.group == "hidden") + this.type = "hidden"; + + // Convert "a.b" into sub-properties... + var m = this.group.match(/^([^.]*)(\.(.*))?$/) + ASSERT(m, "Failed group match!"); + this.mainGroup = m[1]; + this.subGroup = m[3]; + + return this; +} + +/* Creates all the XUL elements to display this one pref. */ +PrefData.prototype.loadXUL = +function pdata_loadXUL() +{ + if (this.type == "hidden") + return; + + // Create the base box for the pref. + this.box = document.createElement("box"); + this.box.orient = "horizontal"; + this.box.setAttribute("align", "center"); + + switch (this.type) + { + case "string": + label = document.createElement("label"); + label.setAttribute("value", this.label); + label.width = 100; + label.flex = 1; + this.box.appendChild(label); + + this.edit = document.createElement("textbox"); + // We choose the size based on the length of the default. + if (this.def.length < 8) + this.edit.setAttribute("size", "10"); + else if (this.def.length < 20) + this.edit.setAttribute("size", "25"); + else + this.edit.flex = 1; + + var editCont = document.createElement("hbox"); + editCont.flex = 1000; + editCont.appendChild(this.edit); + this.box.appendChild(editCont); + + // But if it's a file/URL... + if (this.def.match(/^([a-z]+:\/|[a-z]:\\)/i)) + { + // ...we make it as big as possible. + this.edit.removeAttribute("size"); + this.edit.flex = 1; + + if (!this.name.match(/path$/i) && + (this.def.match(/^(file|chrome):\//i) || + this.name.match(/filename$/i))) + { + // So long as the pref name doesn't end in "path", and + // it's chrome:, file: or a local file, we add the button. + var ext = ""; + var m = this.def.match(/\.([a-z0-9]+)$/); + if (m) + ext = "*." + m[1]; + + // We're cheating again here, if it ends "filename" it's + // a local file path. + var type = (this.name.match(/filename$/i) ? "file" : "fileurl"); + type = (this.name.match(/folder$/i) ? "folder" : type); + appendButton(this.box, "onPrefBrowse", + { label: MSG_PREFS_BROWSE, spec: ext, + kind: type }); + } + } + break; + + case "number": + label = document.createElement("label"); + label.setAttribute("value", this.label); + label.width = 100; + label.flex = 1; + this.box.appendChild(label); + + this.edit = document.createElement("textbox"); + this.edit.setAttribute("size", "5"); + this.edit.setAttribute("type", "number"); + this.edit.setAttribute("min", "-1"); + + editCont = document.createElement("hbox"); + editCont.flex = 1000; + editCont.appendChild(this.edit); + this.box.appendChild(editCont); + break; + + case "boolean": + this.edit = document.createElement("checkbox"); + this.edit.setAttribute("label", this.label); + this.box.appendChild(this.edit); + break; + + case "array": + this.box.removeAttribute("align"); + + var oBox = document.createElement("box"); + oBox.orient = "vertical"; + oBox.flex = 1; + this.box.appendChild(oBox); + + if (this.help) + { + label = document.createElement("label"); + label.appendChild(document.createTextNode(this.help)); + oBox.appendChild(label); + } + + this.edit = document.createElement("listbox"); + this.edit.flex = 1; + this.edit.setAttribute("style", "height: 1em;"); + this.edit.setAttribute("kind", "url"); + if (this.def.length > 0 && this.def[0].match(/^file:\//)) + this.edit.setAttribute("kind", "fileurl"); + this.edit.setAttribute("onselect", "gPrefWindow.onPrefListSelect(this);"); + this.edit.setAttribute("ondblclick", "gPrefWindow.onPrefListEdit(this);"); + oBox.appendChild(this.edit); + + var box = document.createElement("box"); + box.orient = "vertical"; + this.box.appendChild(box); + + // NOTE: This order is important - getRelatedItem needs to be + // kept in sync with this order. Perhaps a better way is needed... + appendButton(box, "onPrefListUp", { label: MSG_PREFS_MOVE_UP, + "class": "up" }); + appendButton(box, "onPrefListDown", { label: MSG_PREFS_MOVE_DOWN, + "class": "down" }); + appendSeparator(box); + appendButton(box, "onPrefListAdd", { label: MSG_PREFS_ADD }); + appendButton(box, "onPrefListEdit", { label: MSG_PREFS_EDIT }); + appendButton(box, "onPrefListDelete", { label: MSG_PREFS_DELETE }); + break; + + default: + // This is really more of an error case, since we really should + // know about all the valid pref types. + var label = document.createElement("label"); + label.setAttribute("value", "[not editable] " + this.type); + this.box.appendChild(label); + } + + this.loadData(); + + if (this.edit) + { + this.edit.setAttribute("prefobjectindex", this.parent.arrayIndex); + this.edit.setAttribute("prefname", this.name); + // Associate textbox with label for accessibility. + if (label) + { + this.edit.id = this.manager.branchName + this.name; + label.setAttribute("control", this.edit.id); + } + } + + if (!ASSERT("groups" in this.parent, "Must have called " + + "[ObjectPrivateData].loadXUL before trying to display prefs.")) + return; + + this.parent.addGroup(this.mainGroup); + if (this.subGroup) + this.parent.groups[this.mainGroup].addGroup(this.subGroup); + + if (!this.subGroup) + this.parent.groups[this.mainGroup].box.appendChild(this.box); + else + this.parent.groups[this.mainGroup].groups[this.subGroup].box.appendChild(this.box); + + // Setup tooltip stuff... + if (this.help && (this.type != "array")) + { + this.box.setAttribute("tooltiptitle", this.label); + this.box.setAttribute("tooltipcontent", this.help); + this.box.setAttribute("onmouseover", "gPrefWindow.onPrefMouseOver(this);"); + this.box.setAttribute("onmousemove", "gPrefWindow.onPrefMouseMove(this);"); + this.box.setAttribute("onmouseout", "gPrefWindow.onPrefMouseOut(this);"); + } +} + +/* Loads the pref's data into the edit component. */ +PrefData.prototype.loadData = +function pdata_loadData() +{ + /* Note about .value and .setAttribute as used here: + * + * XBL doesn't kick in until CSS is calculated on a node, so the code makes + * a compromise and uses these two methods as appropriate. Initally this + * is called is before the node has been placed in the document DOM tree, + * and thus hasn't been "magiced" by XBL and so .value is meaningless to + * it. After initally being set as an attribute, it's added to the DOM, + * XBL kicks in, and after that .value is the only way to change the value. + */ + switch (this.type) + { + case "string": + if (this.edit.hasAttribute("value")) + this.edit.value = this.val; + else + this.edit.setAttribute("value", this.val); + break; + + case "number": + if (this.edit.hasAttribute("value")) + this.edit.value = this.val; + else + this.edit.setAttribute("value", this.val); + break; + + case "boolean": + if (this.edit.hasAttribute("checked")) + this.edit.checked = this.val; + else + this.edit.setAttribute("checked", this.val); + break; + + case "array": + // Remove old entires. + while (this.edit.firstChild) + this.edit.removeChild(this.edit.firstChild); + + // Add new ones. + for (var i = 0; i < this.val.length; i++) + { + var item = document.createElement("listitem"); + item.value = this.val[i]; + item.crop = "center"; + item.setAttribute("label", this.val[i]); + this.edit.appendChild(item); + } + + // Make sure buttons are up-to-date. + gPrefWindow.onPrefListSelect(this.edit); + break; + + default: + } +} + +/* Cleans up the mess. */ +PrefData.prototype.clear = +function pdata_clear() +{ + //dd("Clearing pref " + this.name); + if (("box" in this) && this.box) + { + this.box.parentNode.removeChild(this.box); + delete this.box; + } + try { + this.manager.clearPref(this.name); + } catch(ex) {} +} + +/* Resets the pref to it's default. */ +PrefData.prototype.reset = +function pdata_reset() +{ + //try { + // this.manager.clearPref(this.name); + //} catch(ex) {} + this.val = this.def; + this.loadData(); +} + +/* Saves the pref... or would do. */ +PrefData.prototype.save = +function pdata_save() +{ + //FIXME// +} + +// Represents a "main group", i.e. a tab for a single pref object. +function PrefMainGroup(parent, name) +{ + // Init this group's object. + this.parent = parent; // Private data for pref object. + this.name = name; + this.groups = new Object(); + this.label = getMsg("pref.group." + this.name + ".label", null, this.name); + this.tab = document.createElement("tab"); + this.tabPanel = document.createElement("tabpanel"); + this.box = this.sb = document.createElement("scroller"); + + this.tab.setAttribute("label", this.label); + this.tabPanel.setAttribute("orient", "vertical"); + this.sb.setAttribute("orient", "vertical"); + this.sb.setAttribute("flex", 1); + + this.parent.tabs.appendChild(this.tab); + this.parent.tabPanels.appendChild(this.tabPanel); + this.tabPanel.appendChild(this.sb); + + return this; +} +// Adds a sub group to this main group. +PrefMainGroup.prototype.addGroup = +function pmgroup_addGroup(name) +{ + // If the sub group doesn't exist, make it exist. + if (!(name in this.groups)) + this.groups[name] = new PrefSubGroup(this, name); + + return this.groups[name]; +} + +// Represents a "sub group", i.e. a groupbox on a tab, for a single main group. +function PrefSubGroup(parent, name) +{ + this.parent = parent; // Main group. + this.name = name; + this.fullGroup = this.parent.name + "." + this.name; + this.label = getMsg("pref.group." + this.fullGroup + ".label", null, this.name); + this.help = getMsg("pref.group." + this.fullGroup + ".help", null, ""); + this.gb = document.createElement("groupbox"); + this.cap = document.createElement("caption"); + this.box = document.createElement("box"); + + this.cap.setAttribute("label", this.label); + this.gb.appendChild(this.cap); + this.box.orient = "vertical"; + + // If there's some help text, we place it as the first thing inside + // the <groupbox>, as an explanation for the entire subGroup. + if (this.help) + { + this.helpLabel = document.createElement("label"); + this.helpLabel.appendChild(document.createTextNode(this.help)); + this.gb.appendChild(this.helpLabel); + } + this.gb.appendChild(this.box); + this.parent.box.appendChild(this.gb); + + return this; +} + +// Actual pref window itself. +function PrefWindow() +{ + // Not loaded until the pref list and objects have been created in |onLoad|. + this.loaded = false; + + /* PREF TAB ORDER: Determins the order, and fixed tabs, found on the UI. + * + * It is an array of mainGroup names, and a flag indicating if the tab + * should be created even when there's no prefs for it. + * + * This is for consistency, although none of the ChatZilla built-in pref + * objects leave fixed tabs empty currently. + */ + this.prefTabOrder = [{ fixed: true, name: "general"}, + { fixed: true, name: "appearance"}, + { fixed: false, name: "lists"}, + { fixed: false, name: "dcc"}, + { fixed: false, name: "startup"}, + { fixed: false, name: "global"}, + { fixed: false, name: "munger"} + ]; + + /* PREF OBJECTS: Stores all the objects we've using that have prefs. + * + * Each object gets a "privateData" object, which is then used by the pref + * window code for storing all of it's data on the object. + */ + this.prefObjects = null; + + /* TOOLTIPS: Special tooltips for preference items. + * + * Timer: return value from |setTimeout| whenever used. There is only + * ever one timer going for the tooltips. + * Showing: stores visibility of the tooltip. + * ShowDelay: ms delay which them mouse must be still to show tooltips. + * HideDelay: ms delay before the tooltips hide themselves. + */ + this.tooltipTimer = 0; + this.tooltipShowing = false; + this.tooltipShowDelay = 1000; + this.tooltipHideDelay = 20000; + this.tooltipBug418798 = false; +} +PrefWindow.prototype.TYPE = "PrefWindow"; + +/* Updates the tooltip state, either showing or hiding it. */ +PrefWindow.prototype.setTooltipState = +function pwin_setTooltipState(visible) +{ + // Shortcut: if we're already in the right state, don't bother. + if (this.tooltipShowing == visible) + return; + + var tt = document.getElementById("czPrefTip"); + + // If we're trying to show it, and we have a reference object + // (this.tooltipObject), we are ok to go. + if (visible && this.tooltipObject) + { + /* Get the boxObject for the reference object, as we're going to + * place to tooltip explicitly based on this. + */ + var tipobjBox = this.tooltipObject.boxObject; + + // Adjust the width to that of the reference box. + tt.sizeTo(tipobjBox.width, tt.boxObject.height); + /* show tooltip using the reference object, and it's screen + * position. NOTE: Most of these parameters are supposed to affect + * things, and they do seem to matter, but don't work anything like + * the documentation. Be careful changing them. + */ + tt.showPopup(this.tooltipObject, -1, -1, "tooltip", "bottomleft", "topleft"); + + // Set the timer to hide the tooltip some time later... + // (note the fun inline function) + this.tooltipTimer = setTimeout(setTooltipState, this.tooltipHideDelay, + this, false); + this.tooltipShowing = true; + } + else + { + /* We're here because either we are meant to be hiding the tooltip, + * or we lacked the information needed to show one. So hide it. + */ + tt.hidePopup(); + this.tooltipShowing = false; + } +} + +/** Window event handlers **/ + +/* Initalises, and loads all the data/utilities and prefs. */ +PrefWindow.prototype.onLoad = +function pwin_onLoad() +{ + // Get ourselves a base object for the object hierarchy. + client = new PrefGlobal(); + + // Kick off the localisation load. + initMessages(); + + // Use localised name. + client.viewName = MSG_PREFS_GLOBAL; + client.unicodeName = client.viewName; + client.prettyName = client.viewName; + + // Use the window mediator service to prevent mutliple instances. + var windowMediator = Components.classes[MEDIATOR_CONTRACTID]; + var windowManager = windowMediator.getService(nsIWindowMediator); + var enumerator = windowManager.getEnumerator(CONFIG_WINDOWTYPE); + + // We only want one open at a time because don't (currently) cope with + // pref-change notifications. In fact, it's not easy to cope with. + // Save it for some time later. :) + + enumerator.getNext(); + if (enumerator.hasMoreElements()) + { + alert(MSG_PREFS_ALREADYOPEN); + window.close(); + return; + } + + // Make sure we know what host we're on. + initApplicationCompatibility(); + + // Kick off the core pref initalisation code. + initPrefs(); + + // Turn off all notifications, or it'll get confused when any pref + // does change. + client.prefManager.onPrefChanged = function(){}; + + // The list of objects we're tacking the prefs of. + this.prefObjects = new PrefObjectList(); + + /* Oh, this is such an odd way to do this. But hey, it works. :) + * What we do is ask the pref branch for the client object to give us + * a list of all the preferences under it. This just gets us all the + * Chatzilla prefs. Then, we filter them so that we only deal with + * ones for networks, channels and users. This means, even if only + * one pref is set for a channel, we get it's network and channel + * object created here. + */ + var prefRE = new RegExp("^networks.([^.]+)" + + "(?:\\.(users|channels)?\\.([^.]+))?\\."); + var rv = new Object(); + var netList = client.prefManager.prefBranch.getChildList("networks.", rv); + for (var i in netList) + { + var m = netList[i].match(prefRE); + if (!m) + continue; + + var netName = unMungeNetworkName(m[1]); + /* We're forcing the network into existance (3rd param) if the + * pref is actually set, as opposed to just being known about (the + * pref branch will list all *known* prefs, not just those set). + * + * NOTE: |force| will, if |true|, set a property on the object so it + * will still exist next time we're here. If |false|, the + * the object will only come into existance if this magical + * [hidden] pref is set. + */ + var force = client.prefManager.prefBranch.prefHasUserValue(netList[i]); + + // Don't bother with the new if it's already there (time saving). + if (!(":" + netName in client.networks)) + new PrefNetwork(client, netName, force); + + if ((2 in m) && (3 in m) && (":" + netName in client.networks)) + { + let net = client.networks[":" + netName]; + + // Create a channel object if appropriate. + if (m[2] == "channels") + new PrefChannel(net.primServ, unMungeNetworkName(m[3]), force); + + // Create a user object if appropriate. + if (m[2] == "users") + new PrefUser(net.primServ, unMungeNetworkName(m[3]), force); + } + } + + // Store out object that represents the current view. + var currentView = null; + + // Enumerate source window's tab list... + if (("arguments" in window) && (0 in window.arguments) && ("client" in window.arguments[0])) + { + /* Make sure we survive this, external data could be bad. :) */ + try + { + var czWin = window.arguments[0]; + var s; + var n, c, u; + this.ownerClient = czWin.client; + this.ownerClient.configWindow = window; + + /* Go nick the source window's view list. We can then show items in + * the tree for the currently connected/shown networks, channels + * and users even if they don't have any known prefs yet. + * + * NOTE: the |false, true| params tell the objects to not force + * any prefs into existance, but still show in the tree. + */ + for (i = 0; i < czWin.client.viewsArray.length; i++) + { + var view = czWin.client.viewsArray[i].source; + + // Network view... + if (view.TYPE == "IRCNetwork") + { + n = new PrefNetwork(client, view.unicodeName, false, true); + if (view == czWin.client.currentObject) + currentView = n; + } + + if (view.TYPE.match(/^IRC(Channel|User)$/)) + { + n = new PrefNetwork(client, view.parent.parent.unicodeName, + false, true); + s = n.primServ; + } + + // Channel view... + if (view.TYPE == "IRCChannel") + { + c = new PrefChannel(s, view.unicodeName, false, true); + if (view == czWin.client.currentObject) + currentView = c; + } + + // User view... + if (view.TYPE == "IRCUser") + { + u = new PrefUser(s, view.unicodeName, false, true); + if (view == czWin.client.currentObject) + currentView = u; + } + } + } + catch(ex) + {} + } + + // Add the client object... + this.prefObjects.addObject(client); + // ...and everyone else. + var i, j; + /* We sort the keys (property names, i.e. network names). This means the UI + * will show them in lexographical order, which is good. + */ + var sortedNets = keys(client.networks).sort(); + for (i = 0; i < sortedNets.length; i++) { + net = client.networks[sortedNets[i]]; + this.prefObjects.addObject(net); + + var sortedChans = keys(net.channels).sort(); + for (j = 0; j < sortedChans.length; j++) + this.prefObjects.addObject(net.channels[sortedChans[j]]); + + var sortedUsers = keys(net.users).sort(); + for (j = 0; j < sortedUsers.length; j++) + this.prefObjects.addObject(net.users[sortedUsers[j]]); + } + + // Select the first item in the list. + var prefTree = document.getElementById("pref-tree-object"); + if ("selection" in prefTree.treeBoxObject) + prefTree.treeBoxObject.selection.select(0); + else + prefTree.view.selection.select(0); + + // Find and select the current view. + for (i = 0; i < this.prefObjects.objects.length; i++) + { + if (this.prefObjects.objects[i] == currentView) + { + if ("selection" in prefTree.treeBoxObject) + prefTree.treeBoxObject.selection.select(i); + else + prefTree.view.selection.select(i); + } + } + + this.onSelectObject(); + + // We're done, without error, so it's safe to show the stuff. + document.getElementById("loadDeck").selectedIndex = 1; + // This allows [OK] to actually save, without this it'll just close. + this.loaded = true; + + // Force the window to be the right size now, not later. + window.sizeToContent(); + // XXX: If we're on mac, make it wider because the default theme's + // tabs are huge: + if (client.platform == "Mac") + window.resizeBy(140, 0); + + // Center window. + if (("arguments" in window) && (0 in window.arguments)) + { + var ow = window.arguments[0]; + + window.moveTo(ow.screenX + Math.max((ow.outerWidth - window.outerWidth ) / 2, 0), + ow.screenY + Math.max((ow.outerHeight - window.outerHeight) / 2, 0)); + } +} + +/* Closing the window. Clean up. */ +PrefWindow.prototype.onClose = +function pwin_onClose() +{ + if (this.ownerClient) + delete this.ownerClient.configWindow; + if (this.loaded) + destroyPrefs(); +} + +/* OK button. */ +PrefWindow.prototype.onOK = +function pwin_onOK() +{ + if (this.onApply()) + window.close(); + return true; +} + +/* Apply button. */ +PrefWindow.prototype.onApply = +function pwin_onApply() +{ + // If the load failed, better not to try to save. + if (!this.loaded) + return false; + + try { + // Get an array of all the (XUL) items we have to save. + var list = getPrefTags(); + + //if (!confirm("There are " + list.length + " pref tags to save. OK?")) return false; + + for (var i = 0; i < list.length; i++) + { + // Save this one pref... + var index = list[i].getAttribute("prefobjectindex"); + var name = list[i].getAttribute("prefname"); + // Get the private data for the object out, since everything is + // based on this. + var po = this.prefObjects.getObject(index); + var pref = po.prefs[name]; + + var value; + // We just need to force the value from the DOMString form to + // the right form, so we don't get anything silly happening. + switch (pref.type) + { + case "string": + value = list[i].value; + break; + case "number": + value = 1 * list[i].value; + break; + case "boolean": + value = list[i].checked; + break; + case "array": + var l = new Array(); + for (var j = 0; j < list[i].childNodes.length; j++) + l.push(list[i].childNodes[j].value); + value = l; + break; + default: + throw "Unknown pref type: " + pref.type + "!"; + } + /* Fun stuff. We don't want to save if the user hasn't changed + * it. This is so that if the default is defered, and changed, + * but this hasn't, we keep the defered default. Which is a + * Good Thing. :) + */ + if (((pref.type != "array") && (value != pref.startVal)) || + ((pref.type == "array") && + (value.join(";") != pref.startVal.join(";")))) + { + po.parent.prefs[pref.name] = value; + } + // Update our saved value, so the above check works the 2nd + // time the user clicks Apply. + pref.val = value; + pref.startVal = pref.val; + } + + return true; + } + catch (e) + { + alert(getMsg(MSG_PREFS_ERR_SAVE, e)); + return false; + } +} + +/* Cancel button. */ +PrefWindow.prototype.onCancel = +function pwin_onCancel() +{ + window.close(); + return true; +} + +/** Tooltips' event handlers **/ + +PrefWindow.prototype.onPrefMouseOver = +function pwin_onPrefMouseOver(object) +{ + this.tooltipObject = object; + this.tooltipTitle = object.getAttribute("tooltiptitle"); + this.tooltipText = object.getAttribute("tooltipcontent"); + // Reset the timer now we're over a new pref. + clearTimeout(this.tooltipTimer); + this.tooltipTimer = setTimeout(setTooltipState, this.tooltipShowDelay, + this, true); +} + +PrefWindow.prototype.onPrefMouseMove = +function pwin_onPrefMouseMove(object) +{ + // If the tooltip isn't showing, we need to reset the timer. + if (!this.tooltipShowing) + { + clearTimeout(this.tooltipTimer); + this.tooltipTimer = setTimeout(setTooltipState, this.tooltipShowDelay, + this, true); + } +} + +PrefWindow.prototype.onPrefMouseOut = +function pwin_onPrefMouseOut(object) +{ + // Left the pref! Hide tooltip, and clear timer. + this.setTooltipState(false); + clearTimeout(this.tooltipTimer); +} + +PrefWindow.prototype.onTooltipPopupShowing = +function pwin_onTooltipPopupShowing(popup) +{ + if (!this.tooltipText) + return false; + + var fChild = popup.firstChild; + var diff = popup.boxObject.height - fChild.boxObject.height; + + // Setup the labels... + var ttt = document.getElementById("czPrefTipTitle"); + ttt.firstChild.nodeValue = this.tooltipTitle; + var ttl = document.getElementById("czPrefTipLabel"); + ttl.firstChild.nodeValue = this.tooltipText; + + /* In Gecko 1.9, the popup has done no layout at this point, unlike in + * earlier versions. As a result, the box object of all the elements + * within it are 0x0. It also means the height of the labels isn't + * updated. To deal with this, we avoid calling sizeTo with the box + * object (as it's 0) and instead just force the popup height to 0 - + * otherwise it will only ever get bigger each time, never smaller. + */ + if (popup.boxObject.width == 0) + this.tooltipBug418798 = true; + + if (this.tooltipBug418798) + popup.height = 0; + else + popup.sizeTo(popup.boxObject.width, fChild.boxObject.height + diff); + + return true; +} + +/** Components' event handlers **/ + +// Selected an item in the tree. +PrefWindow.prototype.onSelectObject = +function pwin_onSelectObject() +{ + var prefTree = document.getElementById("pref-tree-object"); + var rv = new Object(); + if ("selection" in prefTree.treeBoxObject) + prefTree.treeBoxObject.selection.getRangeAt(0, rv, {}); + else + prefTree.view.selection.getRangeAt(0, rv, {}); + var prefTreeIndex = rv.value; + + var delButton = document.getElementById("object-delete"); + if (prefTreeIndex > 0) + delButton.removeAttribute("disabled"); + else + delButton.setAttribute("disabled", "true"); + + var prefItem = prefTree.contentView.getItemAtIndex(prefTreeIndex); + + var pObjectIndex = prefItem.getAttribute("prefobjectindex"); + var pObject = this.prefObjects.getObject(pObjectIndex); + + if (!ASSERT(pObject, "Object not found for index! " + + prefItem.getAttribute("prefobjectindex"))) + return; + + pObject.loadXUL(this.prefTabOrder); + + var header = document.getElementById("pref-header"); + header.setAttribute("title", getMsg(MSG_PREFS_FMT_HEADER, + pObject.parent.prettyName)); + + var deck = document.getElementById("pref-object-deck"); + deck.selectedIndex = pObject.deckIndex; + + this.currentObject = pObject; +} + +// Browse button for file prefs. +PrefWindow.prototype.onPrefBrowse = +function pwin_onPrefBrowse(button) +{ + var spec = "$all"; + if (button.hasAttribute("spec")) + spec = button.getAttribute("spec") + " " + spec; + + var type = button.getAttribute("kind"); + var edit = button.previousSibling.lastChild; + + var rv; + if (type == "folder") + { + try + { + // if the user set the pref to an invalid folder, this will throw: + var current = getFileFromURLSpec(edit.value); + } + catch (ex) + { + // Just setting it to null will work: + current = null; + } + rv = pickGetFolder(MSG_PREFS_BROWSE_TITLE, current); + } + else + { + rv = pickOpen(MSG_PREFS_BROWSE_TITLE, spec); + } + + if (!rv.ok) + return; + + edit.value = (type == "file") ? rv.file.path : rv.picker.fileURL.spec; +}, + +// Selection changed on listbox. +PrefWindow.prototype.onPrefListSelect = +function pwin_onPrefListSelect(object) +{ + var list = getRelatedItem(object, "list"); + var buttons = new Object(); + buttons.up = getRelatedItem(object, "button-up"); + buttons.down = getRelatedItem(object, "button-down"); + buttons.add = getRelatedItem(object, "button-add"); + buttons.edit = getRelatedItem(object, "button-edit"); + buttons.del = getRelatedItem(object, "button-delete"); + + if (("selectedItems" in list) && list.selectedItems && + list.selectedItems.length) + { + buttons.edit.removeAttribute("disabled"); + buttons.del.removeAttribute("disabled"); + } + else + { + buttons.edit.setAttribute("disabled", "true"); + buttons.del.setAttribute("disabled", "true"); + } + + if (!("selectedItems" in list) || !list.selectedItems || + list.selectedItems.length == 0 || list.selectedIndex == 0) + { + buttons.up.setAttribute("disabled", "true"); + } + else + { + buttons.up.removeAttribute("disabled"); + } + + if (!("selectedItems" in list) || !list.selectedItems || + list.selectedItems.length == 0 || + list.selectedIndex == list.childNodes.length - 1) + { + buttons.down.setAttribute("disabled", "true"); + } + else + { + buttons.down.removeAttribute("disabled"); + } +} + +// Up button for lists. +PrefWindow.prototype.onPrefListUp = +function pwin_onPrefListUp(object) +{ + var list = getRelatedItem(object, "list"); + + var selected = list.selectedItems[0]; + var before = selected.previousSibling; + if (before) + { + before.parentNode.insertBefore(selected, before); + list.selectItem(selected); + list.ensureElementIsVisible(selected); + } +} + +// Down button for lists. +PrefWindow.prototype.onPrefListDown = +function pwin_onPrefListDown(object) +{ + var list = getRelatedItem(object, "list"); + + var selected = list.selectedItems[0]; + if (selected.nextSibling) + { + if (selected.nextSibling.nextSibling) + list.insertBefore(selected, selected.nextSibling.nextSibling); + else + list.appendChild(selected); + + list.selectItem(selected); + } +} + +// Add button for lists. +PrefWindow.prototype.onPrefListAdd = +function pwin_onPrefListAdd(object) +{ + var list = getRelatedItem(object, "list"); + var newItem; + + switch (list.getAttribute("kind")) + { + case "url": + var item = prompt(MSG_PREFS_LIST_ADD); + if (item) + { + newItem = document.createElement("listitem"); + newItem.setAttribute("label", item); + newItem.value = item; + list.appendChild(newItem); + this.onPrefListSelect(object); + } + break; + case "file": + case "fileurl": + var spec = "$all"; + + var rv = pickOpen(MSG_PREFS_BROWSE_TITLE, spec); + if (rv.ok) + { + var data = { file: rv.file.path, fileurl: rv.picker.fileURL.spec }; + var kind = list.getAttribute("kind"); + + newItem = document.createElement("listitem"); + newItem.setAttribute("label", data[kind]); + newItem.value = data[kind]; + list.appendChild(newItem); + this.onPrefListSelect(object); + } + + break; + } +} + +// Edit button for lists. +PrefWindow.prototype.onPrefListEdit = +function pwin_onPrefListEdit(object) +{ + var list = getRelatedItem(object, "list"); + + switch (list.getAttribute("kind")) + { + case "url": + case "file": + case "fileurl": + // We're letting the user edit file types here, since it saves us + // a lot of work, and we can't let them pick a file OR a directory, + // so they pick a file and can edit it off to use a directory. + var listItem = list.selectedItems[0]; + var newValue = prompt(MSG_PREFS_LIST_EDIT, listItem.value); + if (newValue) + { + listItem.setAttribute("label", newValue); + listItem.value = newValue; + } + break; + } +} + +// Delete button for lists. +PrefWindow.prototype.onPrefListDelete = +function pwin_onPrefListDelete(object) +{ + var list = getRelatedItem(object, "list"); + + var listItem = list.selectedItems[0]; + if (confirm(getMsg(MSG_PREFS_LIST_DELETE, listItem.value))) + list.removeChild(listItem); +} + +/* Add... button. */ +PrefWindow.prototype.onAddObject = +function pwin_onAddObject() +{ + var rv = new Object(); + + /* Try to nobble the current selection and pre-fill as needed. */ + switch (this.currentObject.parent.TYPE) + { + case "PrefNetwork": + rv.type = "net"; + rv.net = this.currentObject.parent.unicodeName; + break; + case "PrefChannel": + rv.type = "chan"; + rv.net = this.currentObject.parent.parent.parent.unicodeName; + rv.chan = this.currentObject.parent.unicodeName; + break; + case "PrefUser": + rv.type = "user"; + rv.net = this.currentObject.parent.parent.parent.unicodeName; + rv.chan = this.currentObject.parent.unicodeName; + break; + } + + // Show add dialog, passing the data object along. + window.openDialog("config-add.xul", "cz-config-add", "chrome,dialog,modal", rv); + + if (!rv.ok) + return; + + /* Ok, so what type did they want again? + * + * NOTE: The param |true| in the object creation calls is for |force|. It + * causes the hidden pref to be set for the objects so they are shown + * every time this window opens, until the user deletes them. + */ + switch (rv.type) + { + case "net": + this.prefObjects.addObject(new PrefNetwork(client, rv.net, true)); + break; + case "chan": + if (!(":" + rv.net in client.networks)) + this.prefObjects.addObject(new PrefNetwork(client, rv.net, true)); + this.prefObjects.addObject(new PrefChannel(client.networks[":" + rv.net].primServ, rv.chan, true)); + break; + case "user": + if (!(":" + rv.net in client.networks)) + this.prefObjects.addObject(new PrefNetwork(client, rv.net, true)); + this.prefObjects.addObject(new PrefUser(client.networks[":" + rv.net].primServ, rv.chan, true)); + break; + default: + // Oops. Not good, if we got here. + alert("Unknown pref type: " + rv.type); + } +} + +/* Delete button. */ +PrefWindow.prototype.onDeleteObject = +function pwin_onDeleteObject() +{ + // Save current node before we re-select. + var sel = this.currentObject; + + // Check they want to go ahead. + if (!confirm(getMsg(MSG_PREFS_OBJECT_DELETE, sel.parent.unicodeName))) + return; + + // Select a new item BEFORE removing the current item, so the <tree> + // doesn't freak out on us. + var prefTree = document.getElementById("pref-tree-object"); + if ("selection" in prefTree.treeBoxObject) + prefTree.treeBoxObject.selection.select(0); + else + prefTree.view.selection.select(0); + + // If it's a network, nuke all the channels and users too. + if (sel.parent.TYPE == "PrefNetwork") + { + var chans = sel.parent.channels; + for (k in chans) + PrefObjectList.getPrivateData(chans[k]).clear(); + + var users = sel.parent.users; + for (k in users) + PrefObjectList.getPrivateData(users[k]).clear(); + } + sel.clear(); + + this.onSelectObject(); +} + +/* Reset button. */ +PrefWindow.prototype.onResetObject = +function pwin_onResetObject() +{ + // Save current node before we re-select. + var sel = this.currentObject; + + // Check they want to go ahead. + if (!confirm(getMsg(MSG_PREFS_OBJECT_RESET, sel.parent.unicodeName))) + return; + + // Reset the prefs. + sel.reset(); +} + +// End of PrefWindow. // + +/*** Base functions... ***/ + +/* Gets a "related" items, such as the buttons associated with a list. */ +function getRelatedItem(object, thing) +{ + switch (object.nodeName) + { + case "listbox": + switch (thing) { + case "list": + return object; + case "button-up": + return object.parentNode.nextSibling.childNodes[0]; + case "button-down": + return object.parentNode.nextSibling.childNodes[1]; + case "button-add": + return object.parentNode.nextSibling.childNodes[3]; + case "button-edit": + return object.parentNode.nextSibling.childNodes[4]; + case "button-delete": + return object.parentNode.nextSibling.childNodes[5]; + } + break; + case "button": + var n = object.parentNode.previousSibling.lastChild; + if (n) + return getRelatedItem(n, thing); + break; + } + return null; +} + +// Wrap this call so we have the right |this|. +function setTooltipState(w, s) +{ + w.setTooltipState(s); +} + +// Reverses the Pref Manager's munging of network names. +function unMungeNetworkName(name) +{ + name = ecmaUnescape(name); + return name.replace(/_/g, ":").replace(/-/g, "."); +} + +// Adds a button to a container, setting up the command in a simple way. +function appendButton(cont, oncommand, attr) +{ + var btn = document.createElement("button"); + if (attr) + for (var a in attr) + btn.setAttribute(a, attr[a]); + if (oncommand) + btn.setAttribute("oncommand", "gPrefWindow." + oncommand + "(this);"); + else + btn.setAttribute("disabled", "true"); + cont.appendChild(btn); +} + +// Like appendButton, but just drops in a separator. +function appendSeparator(cont, attr) +{ + var spacer = document.createElement("separator"); + if (attr) + for (var a in attr) + spacer.setAttribute(a, attr[a]); + cont.appendChild(spacer); +} + +/* This simply collects together all the <textbox>, <checkbox> and <listbox> + * elements that have the attribute "prefname". Thus, we generate a list of + * all elements that are for prefs. + */ +function getPrefTags() +{ + var rv = new Array(); + var i, list; + + list = document.getElementsByTagName("textbox"); + for (i = 0; i < list.length; i++) + { + if (list[i].hasAttribute("prefname")) + rv.push(list[i]); + } + list = document.getElementsByTagName("checkbox"); + for (i = 0; i < list.length; i++) + { + if (list[i].hasAttribute("prefname")) + rv.push(list[i]); + } + list = document.getElementsByTagName("listbox"); + for (i = 0; i < list.length; i++) + { + if (list[i].hasAttribute("prefname")) + rv.push(list[i]); + } + + return rv; +} + +// Sets up the "extra1" button (Apply). +function setupButtons() +{ + // Hacky-hacky-hack. Looks like the new toolkit does provide a solution, + // but we need to support SeaMonkey too. :) + + var dialog = document.documentElement; + dialog.getButton("extra1").label = dialog.getAttribute("extra1Label"); +} + +// And finally, we want one of these. +var gPrefWindow = new PrefWindow(); diff --git a/comm/suite/chatzilla/xul/content/config.xul b/comm/suite/chatzilla/xul/content/config.xul new file mode 100644 index 0000000000..47cacbfd3d --- /dev/null +++ b/comm/suite/chatzilla/xul/content/config.xul @@ -0,0 +1,82 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/config.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="config.css" type="text/css"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + windowtype="irc:chatzilla:config" + id="chatzilla-window" + buttons="accept,cancel,extra1" + extra1Label="&dialog.apply;" + ondialogaccept="gPrefWindow.onOK();" + ondialogcancel="gPrefWindow.onCancel();" + ondialogextra1="gPrefWindow.onApply();" + onload="setupButtons(); gPrefWindow.onLoad();" + onunload="gPrefWindow.onClose();" + title="&window.title;"> + + <script src="chrome://chatzilla/content/lib/js/utils.js"/> + <script src="chrome://chatzilla/content/lib/js/file-utils.js"/> + <script src="chrome://chatzilla/content/lib/js/pref-manager.js"/> + <script src="chrome://chatzilla/content/lib/js/message-manager.js"/> + <script src="chrome://chatzilla/content/prefs.js"/> + <script src="chrome://chatzilla/content/messages.js"/> + <script src="config.js"/> + <script src="chrome://chatzilla/content/static.js"/> + + <tooltip id="czPrefTip" orient="vertical" + onpopupshowing="return gPrefWindow.onTooltipPopupShowing(this);"> + <vbox> + <label id="czPrefTipTitle" class="header">.</label> + <label id="czPrefTipLabel">.</label> + </vbox> + <spacer flex="1"/> + </tooltip> + + <deck id="loadDeck" flex="1"> + <vbox flex="1" align="center" pack="center"> + <label class="loading" value="&loading.label;"/> + </vbox> + <hbox flex="1"> + <vbox id="pref-objects"> + <tree id="pref-tree-object" flex="1" seltype="single" + hidecolumnpicker="true" onselect="gPrefWindow.onSelectObject();"> + <treecols> + <treecol id="pref-col-name" primary="true" flex="1" + hideheader="true"/> + </treecols> + <treechildren id="pref-tree"/> + </tree> + <hbox> + <!-- + <button label="&object.add.label;" tooltiptext="&object.add.hint;" + accesskey="&object.add.accesskey;" flex="1" + oncommand="gPrefWindow.onAddObject();"/> + --> + <button label="&object.del.label;" tooltiptext="&object.del.hint;" + accesskey="&object.del.accesskey;" flex="1" + oncommand="gPrefWindow.onDeleteObject();" id="object-delete"/> + </hbox> + </vbox> + <vbox flex="1"> + <dialogheader id="pref-header" title=""/> + <deck flex="1" id="pref-object-deck"/> + <hbox align="center"> + <button label="&object.reset.label;" tooltiptext="&object.reset.hint;" + accesskey="&object.reset.accesskey;" + oncommand="gPrefWindow.onResetObject();"/> + <spacer flex="1"/> + <html:a onclick="" target="_blank" href="&homepage.url;" + style="display: block; color: blue; text-decoration: + underline;">&homepage.label;</html:a> + </hbox> + </vbox> + </hbox> + </deck> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/dynamic.css b/comm/suite/chatzilla/xul/content/dynamic.css new file mode 100644 index 0000000000..d585c07ab2 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/dynamic.css @@ -0,0 +1,7 @@ +/* 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/. */ + +/* empty css file. rules are appended to this dynamically */ + + diff --git a/comm/suite/chatzilla/xul/content/handlers.js b/comm/suite/chatzilla/xul/content/handlers.js new file mode 100644 index 0000000000..74e0f1c856 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/handlers.js @@ -0,0 +1,3960 @@ +/* -*- 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/. */ + +window.onresize = +function onresize() +{ + for (var i = 0; i < client.deck.childNodes.length; i++) + scrollDown(client.deck.childNodes[i], true); +} + +function onInputFocus() +{ +} + +function showErrorDlg(message) +{ + const XUL_MIME = "application/vnd.mozilla.xul+xml"; + const XUL_KEY = "http://www.mozilla.org/keymaster/" + + "gatekeeper/there.is.only.xul"; + + const TITLE = "ChatZilla run-time error"; + const HEADER = "There was a run-time error with ChatZilla. " + + "Please report the following information:"; + + const OL_JS = "document.getElementById('tb').value = '%S';"; + const TB_STYLE = ' multiline="true" readonly="true"' + + ' style="width: 60ex; height: 20em;"'; + + const ERROR_DLG = '<?xml version="1.0"?>' + + '<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>' + + '<dialog xmlns="' + XUL_KEY + '" buttons="accept" ' + + 'title="' + TITLE + '" onload="' + OL_JS + '">' + + '<label>' + HEADER + '</label><textbox' + TB_STYLE + ' id="tb"/>' + + '</dialog>'; + + var content = message.replace(/\n/g, "\\n"); + content = content.replace(/'/g, "\\'").replace(/"/g, """); + content = content.replace(/</g, "<").replace(/>/g, ">"); + content = ERROR_DLG.replace("%S", content); + content = encodeURIComponent(content); + content = "data:" + XUL_MIME + "," + content; + + setTimeout(function() { + window.openDialog(content, "_blank", "chrome,modal"); + }, 100); +} + +function onLoad() +{ + dd ("Initializing ChatZilla {"); + try + { + init(); + } + catch (ex) + { + dd("caught exception while initializing:\n" + dumpObjectTree(ex)); + var exception = formatException(ex) + (ex.stack && "\n" + ex.stack); + showErrorDlg(exception + "\n" + dumpObjectTree(ex)); + } + + dd("}"); + mainStep(); +} + +function initHandlers() +{ + var node; + node = document.getElementById("input"); + node.addEventListener("keypress", onInputKeyPress, false); + node = document.getElementById("multiline-input"); + node.addEventListener("keypress", onMultilineInputKeyPress, false); + node.active = false; + + window.onkeypress = onWindowKeyPress; + + window.isFocused = false; + window.addEventListener("focus", onWindowFocus, true); + window.addEventListener("blur", onWindowBlue, true); + + client.inputPopup = null; + + // Should fail silently pre-moz1.4 + doCommandWithParams("cmd_clipboardDragDropHook", + {addhook: CopyPasteHandler}); +} + +function onClose() +{ + // Assume close needs authorization from user. + var close = false; + + // Close has already been authorized. + if ("userClose" in client && client.userClose) + close = true; + + // Not connected, no need for authorization. + if (!("getConnectionCount" in client) || (client.getConnectionCount() == 0)) + close = true; + + if (!close) + { + // Authorization needed from user. + client.wantToQuit(); + return false; + } + + return true; +} + +function onUnload() +{ + dd("Shutting down ChatZilla."); + + /* Disable every loaded & enabled plugin to give them all a chance to + * clean up anything beyond the ChatZilla window (e.g. libraries). All + * errors are disregarded as there's nothing we can do at this point. + * Wipe the plugin list afterwards for safety. + */ + for (var k in client.plugins) + { + if ((client.plugins[k].API > 0) && client.plugins[k].enabled) + { + try + { + client.plugins[k].disable(); + } + catch(ex) {} + } + } + client.plugins = new Object(); + + // Close all dialogs. + if ("joinDialog" in client) + client.joinDialog.close(); + if ("configWindow" in client) + client.configWindow.close(); + if ("installPluginDialog" in client) + client.installPluginDialog.close(); + if ("aboutDialog" in client) + client.aboutDialog.close(); + + // We don't trust anybody. + client.hiddenDocument = null; + uninitOfflineIcon(); + uninitIdleAutoAway(client.prefs["awayIdleTime"]); + destroy(); +} + +function onNotImplemented() +{ + alert (getMsg("onNotImplementedMsg")); +} + +/* tab click */ +function onTabClick(e, id) +{ + if (e.which != 2) + return; + + var tbi = document.getElementById(id); + var view = client.viewsArray[tbi.getAttribute("viewKey")]; + + if (e.which == 2) + { + dispatch("hide", { view: view.source, source: "mouse" }); + return; + } +} + +function onTabSelect(e) +{ + var tabs = e.target; + + /* Hackaround, bug 314230. XBL likes focusing a tab before onload has fired. + * That means the tab we're trying to select here will be the hidden one, + * which doesn't have a viewKey. We catch that case. + */ + if (!tabs.selectedItem.hasAttribute("viewKey")) + return; + + var key = tabs.selectedItem.getAttribute("viewKey"); + var view = client.viewsArray[key]; + dispatch("set-current-view", {view:view.source}); +} + +function onMessageViewClick(e) +{ + if ((e.which != 1) && (e.which != 2)) + return true; + + var cx = getMessagesContext(null, e.target); + cx.source = "mouse"; + cx.shiftKey = e.shiftKey; + var command = getEventCommand(e); + if (!client.commandManager.isCommandSatisfied(cx, command)) + return false; + + dispatch(command, cx); + dispatch("focus-input"); + e.preventDefault(); + return true; +} + +function onMessageViewMouseDown(e) +{ + if ((typeof startScrolling != "function") || + ((e.which != 1) && (e.which != 2))) + { + return true; + } + + var cx = getMessagesContext(null, e.target); + var command = getEventCommand(e); + if (!client.commandManager.isCommandSatisfied(cx, command)) + startScrolling(e); + return true; +} + +function onMessageViewContextMenu(e) +{ + var elem = e.target; + var menu = document.getElementById("context:messages"); + while (elem) + { + if (elem.localName && elem.localName.toLowerCase() == "input") + { + menu = document.getElementById("context:edit"); + break; + } + elem = elem.parentNode; + } + document.popupNode = e.target; + if ("openPopupAtScreen" in menu) + menu.openPopupAtScreen(e.screenX, e.screenY, true); + else + menu.showPopup(null, e.screenX + 2, e.screenY + 2, "context", "", ""); + e.stopPropagation(); + e.preventDefault(); +} + +function getEventCommand(e) +{ + let where = Services.prefs.getIntPref("browser.link.open_newwindow"); + if ((where != 3) && ((e.which == 2) || e.ctrlKey) && + Services.prefs.getBoolPref("browser.tabs.opentabfor.middleclick", true)) + where = 3; + + if (where == 2) + return "goto-url-newwin"; + if (where == 3) + return "goto-url-newtab"; + return "goto-url"; +} + +function onMouseOver (e) +{ + var i = 0; + var target = e.target; + var status = ""; + while (!status && target && i < 5) + { + if ("getAttribute" in target) + { + status = target.getAttribute("href"); + if (!status) + status = target.getAttribute("status-text"); + } + ++i; + target = target.parentNode; + } + + // Setting client.status to "" will revert it to the default automatically. + client.status = status; +} + +function onMultilineInputKeyPress (e) +{ + if ((e.ctrlKey || e.metaKey) && e.keyCode == 13) + { + /* meta-enter, execute buffer */ + onMultilineSend(e); + } + else + { + if ((e.ctrlKey || e.metaKey) && e.keyCode == 40) + { + /* ctrl/meta-down, switch to single line mode */ + dispatch ("pref multiline false"); + } + } +} + +function onMultilineSend(e) +{ + var multiline = document.getElementById("multiline-input"); + e.line = multiline.value; + if (e.line.search(/\S/) == -1) + return; + onInputCompleteLine (e); + multiline.value = ""; + if (("multiLineForPaste" in client) && client.multiLineForPaste) + client.prefs["multiline"] = false; +} + +function onTooltip(event) +{ + const XLinkNS = "http://www.w3.org/1999/xlink"; + + var tipNode = event.originalTarget; + var titleText = null; + var XLinkTitleText = null; + + var element = document.tooltipNode; + while (element && (element != document.documentElement)) + { + if (element.nodeType == Node.ELEMENT_NODE) + { + var text; + if (element.hasAttribute("title")) + text = element.getAttribute("title"); + else if (element.hasAttributeNS(XLinkNS, "title")) + text = element.getAttributeNS(XLinkNS, "title"); + + if (text) + { + tipNode.setAttribute("label", text); + return true; + } + } + + element = element.parentNode; + } + + return false; +} + +function onInputKeyPress (e) +{ + if (client.prefs["outgoing.colorCodes"]) + setTimeout(onInputKeyPressCallback, 100, e.target); + + switch (e.keyCode) + { + case 9: /* tab */ + if (!e.ctrlKey && !e.metaKey) + { + onTabCompleteRequest(e); + e.preventDefault(); + } + return; + + case 77: /* Hackaround for carbon on mac sending us this instead of 13 + * for ctrl+enter. 77 = "M", and ctrl+M was originally used + * to send a CR in a terminal. */ + // Fallthrough if ctrl was pressed, otherwise break out to default: + if (!e.ctrlKey) + break; + + case 13: /* CR */ + e.line = e.target.value; + e.target.value = ""; + if (e.line.search(/\S/) == -1) + return; + if (e.ctrlKey) + e.line = client.COMMAND_CHAR + "say " + e.line; + onInputCompleteLine (e); + return; + + case 37: /* left */ + if (e.altKey && e.metaKey) + cycleView(-1); + return; + + case 38: /* up */ + if (e.ctrlKey || e.metaKey) + { + /* ctrl/meta-up, switch to multi line mode */ + dispatch ("pref multiline true"); + } + else + { + if (client.lastHistoryReferenced == -2) + { + client.lastHistoryReferenced = -1; + e.target.value = client.incompleteLine; + } + else if (client.lastHistoryReferenced < + client.inputHistory.length - 1) + { + e.target.value = + client.inputHistory[++client.lastHistoryReferenced]; + } + } + e.preventDefault(); + return; + + case 39: /* right */ + if (e.altKey && e.metaKey) + cycleView(+1); + return; + + case 40: /* down */ + if (client.lastHistoryReferenced > 0) + e.target.value = + client.inputHistory[--client.lastHistoryReferenced]; + else if (client.lastHistoryReferenced == -1) + { + e.target.value = ""; + client.lastHistoryReferenced = -2; + } + else + { + client.lastHistoryReferenced = -1; + e.target.value = client.incompleteLine; + } + e.preventDefault(); + return; + } + client.lastHistoryReferenced = -1; + client.incompleteLine = e.target.value; +} + +function onTabCompleteRequest (e) +{ + var elem = document.commandDispatcher.focusedElement; + var singleInput = document.getElementById("input"); + if (document.getBindingParent(elem) != singleInput) + return; + + var selStart = singleInput.selectionStart; + var selEnd = singleInput.selectionEnd; + var line = singleInput.value; + + if (!line) + { + if ("defaultCompletion" in client.currentObject) + singleInput.value = client.currentObject.defaultCompletion; + // If there was nothing to complete, help the user: + if (!singleInput.value) + display(MSG_LEAVE_INPUTBOX, MT_INFO); + return; + } + + if (selStart != selEnd) + { + /* text is highlighted, just move caret to end and exit */ + singleInput.selectionStart = singleInput.selectionEnd = line.length; + return; + } + + var wordStart = line.substr(0, selStart).search(/\s\S*$/); + if (wordStart == -1) + wordStart = 0; + else + ++wordStart; + + var wordEnd = line.substr(selStart).search(/\s/); + if (wordEnd == -1) + wordEnd = line.length; + else + wordEnd += selStart; + + // Double tab on nothing, inform user how to get out of the input box + if (wordEnd == wordStart) + { + display(MSG_LEAVE_INPUTBOX, MT_INFO); + return; + } + + if ("performTabMatch" in client.currentObject) + { + var word = line.substring(wordStart, wordEnd); + var wordLower = word.toLowerCase(); + var d = getObjectDetails(client.currentObject); + if (d.server) + wordLower = d.server.toLowerCase(word); + + var co = client.currentObject; + + // We need some special knowledge of how to lower-case strings. + var lcFn; + if ("getLCFunction" in co) + lcFn = co.getLCFunction(); + + var matches = co.performTabMatch(line, wordStart, wordEnd, wordLower, + selStart, lcFn); + /* if we get null back, we're supposed to fail silently */ + if (!matches) + return; + + var doubleTab = false; + var date = new Date(); + if ((date - client.lastTabUp) <= client.DOUBLETAB_TIME) + doubleTab = true; + else + client.lastTabUp = date; + + if (doubleTab) + { + /* if the user hit tab twice quickly, */ + if (matches.length > 0) + { + /* then list possible completions, */ + display(getMsg(MSG_FMT_MATCHLIST, + [matches.length, word, + matches.sort().join(", ")])); + } + else + { + /* or display an error if there are none. */ + display(getMsg(MSG_ERR_NO_MATCH, word), MT_ERROR); + } + } + else if (matches.length >= 1) + { + var match; + if (matches.length == 1) + match = matches[0]; + else + match = getCommonPfx(matches, lcFn); + singleInput.value = line.substr(0, wordStart) + match + + line.substr(wordEnd); + if (wordEnd < line.length) + { + /* if the word we completed was in the middle if the line + * then move the cursor to the end of the completed word. */ + var newpos = wordStart + match.length; + if (matches.length == 1) + { + /* word was fully completed, move one additional space */ + ++newpos; + } + singleInput.selectionEnd = e.target.selectionStart = newpos; + } + } + } + +} + +function onWindowKeyPress(e) +{ + var code = Number(e.keyCode); + var w; + var newOfs; + var userList = document.getElementById("user-list"); + var elemFocused = document.commandDispatcher.focusedElement; + + const isMac = client.platform == "Mac"; + const isLinux = client.platform == "Linux"; + const isWindows = client.platform == "Windows"; + const isOS2 = client.platform == "OS/2"; + const isUnknown = !(isMac || isLinux || isWindows || isOS2); + + switch (code) + { + case 9: /* Tab */ + // Control-Tab => next tab (all platforms) + // Control-Shift-Tab => previous tab (all platforms) + if (e.ctrlKey && !e.altKey && !e.metaKey) + { + cycleView(e.shiftKey ? -1: 1); + e.preventDefault(); + } + break; + + case 33: /* Page Up */ + case 34: /* Page Down */ + // Control-Page Up => previous tab (all platforms) + // Control-Page Down => next tab (all platforms) + if ((e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) || + (e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey)) + { + cycleView(2 * code - 67); + e.preventDefault(); + break; + } + + if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey && + (elemFocused != userList)) + { + w = client.currentFrame; + newOfs = w.pageYOffset + (w.innerHeight * 0.75) * + (2 * code - 67); + if (newOfs > 0) + w.scrollTo(w.pageXOffset, newOfs); + else + w.scrollTo(w.pageXOffset, 0); + e.preventDefault(); + } + break; + + case 37: /* Left Arrow */ + case 39: /* Right Arrow */ + // Command-Alt-Left Arrow => previous tab (Mac only) + // Command-Alt-Right Arrow => next tab (Mac only) + if (isMac && e.metaKey && e.altKey && !e.ctrlKey && !e.shiftKey) + { + cycleView(code - 38); + e.preventDefault(); + } + break; + + case 219: /* [ */ + case 221: /* ] */ + // Command-Shift-[ => previous tab (Mac only) + // Command-Shift-] => next tab (Mac only) + if (isMac && e.metaKey && e.shiftKey && !e.altKey && !e.ctrlKey) + { + cycleView(code - 220); + e.preventDefault(); + } + break; + + case 117: /* F6 */ + // F6 => focus next (all platforms) + // Shift-F6 => focus previous (all platforms) + if (!e.altKey && !e.ctrlKey && !e.metaKey) + { + advanceKeyboardFocus(e.shiftKey ? -1 : 1); + e.preventDefault(); + } + break; + } + + // Code is zero if we have a typeable character triggering the event. + if (code != 0) + return; + + // OS X only: Command-{ and Command-} + // Newer geckos seem to only provide these keys in charCode, not keyCode + if (isMac && e.metaKey && e.shiftKey && !e.altKey && !e.ctrlKey) + { + if (e.charCode == 123 || e.charCode == 125) + { + cycleView(e.charCode - 124); + e.preventDefault(); + return; + } + } + + // Numeric shortcuts + + // The following code is copied from: + // /mozilla/browser/base/content/browser.js + // Revision: 1.748 + // Lines: 1397-1421 + + // \d in a RegExp will find any Unicode character with the "decimal digit" + // property (Nd) + var regExp = /\d/; + if (!regExp.test(String.fromCharCode(e.charCode))) + return; + + // Some Unicode decimal digits are in the range U+xxx0 - U+xxx9 and some are + // in the range U+xxx6 - U+xxxF. Find the digit 1 corresponding to our + // character. + var digit1 = (e.charCode & 0xFFF0) | 1; + if (!regExp.test(String.fromCharCode(digit1))) + digit1 += 6; + + var idx = e.charCode - digit1; + + if ((0 <= idx) && (idx <= 8)) + { + var modifier = (e.altKey ? 0x1 : 0) | + (e.ctrlKey ? 0x2 : 0) | + (e.shiftKey ? 0x4 : 0) | + (e.metaKey ? 0x8 : 0); + + var modifierMask; + if (client.prefs["tabGotoKeyModifiers"]) + modifierMask = client.prefs["tabGotoKeyModifiers"]; + else + modifierMask = 0x1; // alt + + if ((modifier & modifierMask) == modifierMask) + { + // Pressing 1-8 takes you to that tab, while pressing 9 takes you + // to the last tab always. + if (idx == 8) + idx = client.viewsArray.length - 1; + + if ((idx in client.viewsArray) && client.viewsArray[idx].source) + { + var newView = client.viewsArray[idx].source; + dispatch("set-current-view", { view: newView }); + } + e.preventDefault(); + return; + } + } +} + +function onWindowFocus(e) +{ + window.isFocused = true; +} + +function onWindowBlue(e) +{ + window.isFocused = false; + + // If we're tracking last read lines, set a mark on the current view + // when losing focus. + if (client.currentObject && client.currentObject.prefs["autoMarker"]) + client.currentObject.dispatch("marker-set"); +} + +function onInputCompleteLine(e) +{ + if (!client.inputHistory.length || client.inputHistory[0] != e.line) + { + client.inputHistory.unshift(e.line); + if (client.inputHistoryLogger) + client.inputHistoryLogger.append(e.line); + } + + if (client.inputHistory.length > client.MAX_HISTORY) + client.inputHistory.pop(); + + client.lastHistoryReferenced = -1; + client.incompleteLine = ""; + + if (e.line[0] == client.COMMAND_CHAR) + { + if (client.prefs["outgoing.colorCodes"]) + e.line = replaceColorCodes(e.line); + dispatch(e.line.substr(1), null, true); + } + else /* plain text */ + { + /* color codes */ + if (client.prefs["outgoing.colorCodes"]) + e.line = replaceColorCodes(e.line); + client.sayToCurrentTarget(e.line, true); + } +} + +function onNotifyTimeout() +{ + for (var n in client.networks) + { + var net = client.networks[n]; + if (net.isConnected()) { + if ((net.prefs["notifyList"].length > 0) && + (!net.primServ.supports["monitor"])) { + let isonList = net.prefs["notifyList"]; + net.primServ.sendData ("ISON " + isonList.join(" ") + "\n"); + } else { + /* Just send a ping to see if we're alive. */ + net.primServ.sendData ("PING :ALIVECHECK\n"); + } + } + } +} + +function onWhoTimeout() +{ + function checkWho() + { + var checkNext = (net.lastWhoCheckChannel == null); + for (var c in net.primServ.channels) + { + var chan = net.primServ.channels[c]; + + if (checkNext && chan.active && + chan.getUsersLength() < client.prefs["autoAwayCap"]) + { + net.primServ.LIGHTWEIGHT_WHO = true; + net.primServ.who(chan.unicodeName); + net.lastWhoCheckChannel = chan; + net.lastWhoCheckTime = Number(new Date()); + return; + } + + if (chan == net.lastWhoCheckChannel) + checkNext = true; + } + if (net.lastWhoCheckChannel) + { + net.lastWhoCheckChannel = null; + checkWho(); + } + }; + + for (var n in client.networks) + { + var net = client.networks[n]; + var period = net.prefs["autoAwayPeriod"]; + // The time since the last check, with a 5s error margin to + // stop us from not checking because the timer fired a tad early: + var waited = Number(new Date()) - net.lastWhoCheckTime + 5000; + if (net.isConnected() && (period != 0) && (period * 60000 < waited) && + !net.primServ.caps["away-notify"]) + checkWho(); + } +} + +function onInputKeyPressCallback (el) +{ + function doPopup(popup) + { + if (client.inputPopup && client.inputPopup != popup) + client.inputPopup.hidePopup(); + + client.inputPopup = popup; + if (popup) + { + if (el.nodeName == "textbox") + { + popup.showPopup(el, -1, -1, "tooltip", "topleft", "bottomleft"); + } + else + { + var box = el.ownerDocument.getBoxObjectFor(el); + var pos = { x: client.mainWindow.screenX + box.screenX + 5, + y: client.mainWindow.screenY + box.screenY + box.height + 25 }; + popup.moveTo(pos.x, pos.y); + popup.showPopup(el, 0, 0, "tooltip"); + } + } + } + + var text = " " + el.value.substr(0, el.selectionStart); + if (el.selectionStart != el.selectionEnd) + text = ""; + + if (text.match(/[^%]%C[0-9]{0,2},?[0-9]{0,2}$/)) + doPopup(document.getElementById("colorTooltip")); + else if (text.match(/[^%]%$/)) + doPopup(document.getElementById("percentTooltip")); + else + doPopup(null); +} + +function onUserDoubleClick(event) +{ + if ((event.button != 0) || + event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) + { + return; + } + var userList = document.getElementById("user-list"); + if (!userList.view || !userList.view.selection) + return; + var currentIndex = userList.view.selection.currentIndex; + if (currentIndex < 0) + return; + var nickname = getNicknameForUserlistRow(currentIndex); + dispatch("query", {nickname: nickname, source: "mouse"}); +} + +client.onFindEnd = +CIRCNetwork.prototype.onFindEnd = +CIRCChannel.prototype.onFindEnd = +CIRCUser.prototype.onFindEnd = +CIRCDCCChat.prototype.onFindEnd = +CIRCDCCFileTransfer.prototype.onFindEnd = +function this_onfindend(e) +{ + this.scrollToElement("selection", "inview"); +} + +CIRCChannel.prototype._updateConferenceMode = +function my_updateconfmode() +{ + const minDiff = client.CONFERENCE_LOW_PASS; + + var enabled = this.prefs["conference.enabled"]; + var userLimit = this.prefs["conference.limit"]; + var userCount = this.getUsersLength(); + + if (userLimit == 0) + { + // userLimit == 0 --> always off. + if (enabled) + this.prefs["conference.enabled"] = false; + } + else if (userLimit == 1) + { + // userLimit == 1 --> always on. + if (!enabled) + this.prefs["conference.enabled"] = true; + } + else if (enabled && (userCount < userLimit - minDiff)) + { + this.prefs["conference.enabled"] = false; + } + else if (!enabled && (userCount > userLimit + minDiff)) + { + this.prefs["conference.enabled"] = true; + } +} + +CIRCServer.prototype.CTCPHelpClientinfo = +function serv_ccinfohelp() +{ + return MSG_CTCPHELP_CLIENTINFO; +} + +CIRCServer.prototype.CTCPHelpAction = +function serv_ccinfohelp() +{ + return MSG_CTCPHELP_ACTION; +} + +CIRCServer.prototype.CTCPHelpTime = +function serv_ccinfohelp() +{ + return MSG_CTCPHELP_TIME; +} + +CIRCServer.prototype.CTCPHelpVersion = +function serv_ccinfohelp() +{ + return MSG_CTCPHELP_VERSION; +} + +CIRCServer.prototype.CTCPHelpSource = +function serv_csrchelp() +{ + return MSG_CTCPHELP_SOURCE; +} + +CIRCServer.prototype.CTCPHelpOs = +function serv_oshelp() +{ + return MSG_CTCPHELP_OS; +} + +CIRCServer.prototype.CTCPHelpHost = +function serv_hosthelp() +{ + return MSG_CTCPHELP_HOST; +} + +CIRCServer.prototype.CTCPHelpPing = +function serv_ccinfohelp() +{ + return MSG_CTCPHELP_PING; +} + +CIRCServer.prototype.CTCPHelpDcc = +function serv_ccinfohelp() +{ + return MSG_CTCPHELP_DCC; +} + +/** + * Calculates delay before the next automatic connection attempt. + * + * If the number of connection attempts is limited, use fixed interval + * MIN_RECONNECT_MS. For unlimited attempts (-1), use exponential backoff: the + * interval between connection attempts to the network (not individual + * servers) is doubled after each attempt, up to MAX_RECONNECT_MS. + */ +CIRCNetwork.prototype.getReconnectDelayMs = +function my_getReconnectDelayMs() +{ + var nServers = this.serverList.length; + + if ((-1 != this.MAX_CONNECT_ATTEMPTS) || + (0 != this.connectCandidate % nServers)) + { + return this.MIN_RECONNECT_MS; + } + + var networkRound = Math.ceil(this.connectCandidate / nServers); + + var rv = this.MIN_RECONNECT_MS * Math.pow(2, networkRound - 1); + + // clamp rv between MIN/MAX_RECONNECT_MS + rv = Math.min(Math.max(rv, this.MIN_RECONNECT_MS), this.MAX_RECONNECT_MS); + + return rv; +} + +CIRCNetwork.prototype.onInit = +function net_oninit () +{ + this.logFile = null; + this.lastServer = null; +} + +CIRCNetwork.prototype.onInfo = +function my_netinfo (e) +{ + this.display(e.msg, "INFO", undefined, undefined, e.tags); +} + +CIRCNetwork.prototype.onUnknown = +function my_unknown (e) +{ + if ("pendingWhoisLines" in e.server) + { + /* whois lines always have the nick in param 2 */ + e.user = new CIRCUser(e.server, null, e.params[2]); + + e.destMethod = "onUnknownWhois"; + e.destObject = this; + return; + } + + e.params.shift(); /* remove the code */ + e.params.shift(); /* and the dest. nick (always me) */ + + // Handle random IRC numerics automatically. + var msg = getMsg("msg.irc." + e.code, null, ""); + if (msg) + { + if (arrayIndexOf(e.server.channelTypes, e.params[0][0]) != -1) + { + // Message about a channel (e.g. join failed). + e.channel = new CIRCChannel(e.server, null, e.params[0]); + } + + var targetDisplayObj = this; + if (e.channel && ("messages" in e.channel)) + targetDisplayObj = e.channel; + + // Check for /knock support for the +i message. + if (((e.code == 471) || (e.code == 473) || (e.code == 475)) && + ("knock" in e.server.servCmds)) + { + var args = [msg, e.channel.unicodeName, + "knock " + e.channel.unicodeName]; + msg = getMsg("msg.irc." + e.code + ".knock", args, ""); + client.munger.getRule(".inline-buttons").enabled = true; + targetDisplayObj.display(msg, undefined, undefined, undefined, + e.tags); + client.munger.getRule(".inline-buttons").enabled = false; + } + else + { + targetDisplayObj.display(msg, undefined, undefined, undefined, + e.tags); + } + + if (e.channel) + { + if (e.channel.busy) + { + e.channel.busy = false; + updateProgress(); + } + } + else + { + // Network type error? + if (this.busy) + { + this.busy = false; + updateProgress(); + } + } + return; + } + + /* if it looks like some kind of "end of foo" code, and we don't + * already have a mapping for it, make one up */ + var length = e.params.length; + if (!(e.code in client.responseCodeMap) && + (e.params[length - 1].search (/^end of/i) != -1)) + { + client.responseCodeMap[e.code] = "---"; + } + + this.display(toUnicode(e.params.join(" "), this), e.code.toUpperCase(), + undefined, undefined, e.tags); +} + +CIRCNetwork.prototype.lastWhoCheckChannel = null; +CIRCNetwork.prototype.lastWhoCheckTime = 0; +CIRCNetwork.prototype.on001 = /* Welcome! */ +CIRCNetwork.prototype.on002 = /* your host is */ +CIRCNetwork.prototype.on003 = /* server born-on date */ +CIRCNetwork.prototype.on004 = /* server id */ +CIRCNetwork.prototype.on005 = /* server features */ +CIRCNetwork.prototype.on250 = /* highest connection count */ +CIRCNetwork.prototype.on251 = /* users */ +CIRCNetwork.prototype.on252 = /* opers online (in params[2]) */ +CIRCNetwork.prototype.on254 = /* channels found (in params[2]) */ +CIRCNetwork.prototype.on255 = /* link info */ +CIRCNetwork.prototype.on265 = /* local user details */ +CIRCNetwork.prototype.on266 = /* global user details */ +CIRCNetwork.prototype.on375 = /* start of MOTD */ +CIRCNetwork.prototype.on372 = /* MOTD line */ +CIRCNetwork.prototype.on376 = /* end of MOTD */ +CIRCNetwork.prototype.on422 = /* no MOTD */ +CIRCNetwork.prototype.on670 = /* STARTTLS Success */ +CIRCNetwork.prototype.on691 = /* STARTTLS Failure */ +CIRCNetwork.prototype.on902 = /* SASL Nick locked */ +CIRCNetwork.prototype.on903 = /* SASL Auth success */ +CIRCNetwork.prototype.on904 = /* SASL Auth failed */ +CIRCNetwork.prototype.on905 = /* SASL Command too long */ +CIRCNetwork.prototype.on906 = /* SASL Aborted */ +CIRCNetwork.prototype.on907 = /* SASL Already authenticated */ +CIRCNetwork.prototype.on908 = /* SASL Mechanisms */ +function my_showtonet (e) +{ + var p = (3 in e.params) ? e.params[2] + " " : ""; + var str = ""; + + switch (e.code) + { + case "004": + case "005": + str = e.params.slice(3).join(" "); + break; + + case "001": + // Code moved to lower down to speed this bit up. :) + var c, u; + // If we've switched servers, *first* we must rehome our objects. + if (this.lastServer && (this.lastServer != this.primServ)) + { + for (c in this.lastServer.channels) + this.lastServer.channels[c].rehome(this.primServ); + for (u in this.lastServer.users) + this.lastServer.users[u].rehome(this.primServ); + + // This makes sure we have the *right* me object. + this.primServ.me.rehome(this.primServ); + } + + // Update the list of ignored users from the prefs: + var ignoreAry = this.prefs["ignoreList"]; + for (var j = 0; j < ignoreAry.length; ++j) + this.ignoreList[ignoreAry[j]] = getHostmaskParts(ignoreAry[j]); + + // Update everything. + // Welcome to history. + addURLToHistory(this.getURL()); + updateTitle(this); + this.updateHeader(); + client.updateHeader(); + updateSecurityIcon(); + updateStalkExpression(this); + + client.ident.removeNetwork(this); + + // Figure out what nick we *really* want: + if (this.prefs["away"] && this.prefs["awayNick"]) + this.preferredNick = this.prefs["awayNick"]; + else + this.preferredNick = this.prefs["nickname"]; + + // Pretend this never happened. + delete this.pendingNickChange; + + str = e.decodeParam(2); + + break; + + case "251": /* users */ + this.doAutoPerform(); + + // Set our initial monitor list + if ((this.primServ.supports["monitor"]) && + (this.prefs["notifyList"].length > 0)) + { + this.primServ.sendMonitorList(this.prefs["notifyList"], true); + } + + this.isIdleAway = client.isIdleAway; + if (this.prefs["away"]) + this.dispatch("away", { reason: this.prefs["away"] }); + + if (this.lastServer) + { + // Re-join channels from previous connection. + for (c in this.primServ.channels) + { + var chan = this.primServ.channels[c]; + if (chan.joined) + chan.join(chan.mode.key); + } + } + this.lastServer = this.primServ; + + if ("pendingURLs" in this) + { + var target = this.pendingURLs.pop(); + while (target) + { + gotoIRCURL(target.url, target.e); + target = this.pendingURLs.pop(); + } + delete this.pendingURLs; + } + + // Do this after the JOINs, so they are quicker. + // This is not time-critical code. + if (client.prefs["dcc.enabled"] && this.prefs["dcc.useServerIP"]) + { + var delayFn = function(t) { + // This is the quickest way to get out host/IP. + t.pendingUserhostReply = true; + t.primServ.sendData("USERHOST " + + t.primServ.me.encodedName + "\n"); + }; + setTimeout(delayFn, 1000 * Math.random(), this); + } + + // Had some collision during connect. + if (this.primServ.me.unicodeName != this.preferredNick) + { + this.reclaimLeft = this.RECLAIM_TIMEOUT; + this.reclaimName(); + } + + if ("onLogin" in this) + { + ev = new CEvent("network", "login", this, "onLogin"); + client.eventPump.addEvent(ev); + } + + str = e.decodeParam(e.params.length - 1); + break; + + case "376": /* end of MOTD */ + case "422": /* no MOTD */ + this.busy = false; + updateProgress(); + + /* Some servers (wrongly) dont send 251, so try + auto-perform after the MOTD as well */ + this.doAutoPerform(); + /* no break */ + + case "372": + case "375": + case "376": + if (this.IGNORE_MOTD) + return; + /* no break */ + + default: + str = e.decodeParam(e.params.length - 1); + break; + } + + this.displayHere(p + str, e.code.toUpperCase(), undefined, undefined, + e.tags); +} + +CIRCNetwork.prototype.onUnknownCTCPReply = +function my_ctcprunk (e) +{ + this.display(getMsg(MSG_FMT_CTCPREPLY, + [toUnicode(e.CTCPCode, this), + toUnicode(e.CTCPData, this), e.user.unicodeName]), + "CTCP_REPLY", e.user, e.server.me, e.tags); +} + +CIRCNetwork.prototype.onNotice = +function my_notice(e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.display(e.decodeParam(2), "NOTICE", this, e.server.me, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCNetwork.prototype.onPrivmsg = +function my_privmsg(e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.display(e.decodeParam(2), "PRIVMSG", this, e.server.me, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +/* userhost reply */ +CIRCNetwork.prototype.on302 = +function my_302(e) +{ + if (client.prefs["dcc.enabled"] && this.prefs["dcc.useServerIP"] && + ("pendingUserhostReply" in this)) + { + var me = new RegExp("^" + this.primServ.me.encodedName + "\\*?=", "i"); + if (e.params[2].match(me)) + client.dcc.addHost(this.primServ.me.host, true); + + delete this.pendingUserhostReply; + return true; + } + + e.destMethod = "onUnknown"; + e.destObject = this; + + return true; +} + +CIRCNetwork.prototype.on303 = /* ISON (aka notify) reply */ +function my_303 (e) +{ + function lower(text) + { + return e.server.toLowerCase(text); + }; + + var onList = new Array(); + // split() gives an array of one item ("") when splitting "", which we + // don't want, so only do the split if there's something to split. + if (e.params[2]) + onList = stringTrim(e.server.toLowerCase(e.params[2])).split(/\s+/); + var offList = new Array(); + var newArrivals = new Array(); + var newDepartures = new Array(); + var o = getObjectDetails(client.currentObject); + var displayTab; + var i; + + if ("network" in o && o.network == this && client.currentObject != this) + displayTab = client.currentObject; + + for (i = 0; i < this.prefs["notifyList"].length; i++) + { + if (!arrayContains(onList, lower(this.prefs["notifyList"][i]))) + /* user is not on */ + offList.push(lower(this.prefs["notifyList"][i])); + } + + if ("onList" in this) + { + for (i in onList) + if (!arrayContains(this.onList, onList[i])) + /* we didn't know this person was on */ + newArrivals.push(onList[i]); + } + else + this.onList = newArrivals = onList; + + if ("offList" in this) + { + for (i in offList) + if (!arrayContains(this.offList, offList[i])) + /* we didn't know this person was off */ + newDepartures.push(offList[i]); + } + else + this.offList = newDepartures = offList; + + if (newArrivals.length > 0) + { + this.displayHere (arraySpeak (newArrivals, "is", "are") + + " online.", "NOTIFY-ON", undefined, undefined, + e.tags); + if (displayTab) + displayTab.displayHere (arraySpeak (newArrivals, "is", "are") + + " online.", "NOTIFY-ON", undefined, + undefined, e.tags); + } + + if (newDepartures.length > 0) + { + this.displayHere (arraySpeak (newDepartures, "is", "are") + + " offline.", "NOTIFY-OFF", undefined, undefined, + e.tags); + if (displayTab) + displayTab.displayHere (arraySpeak (newDepartures, "is", "are") + + " offline.", "NOTIFY-OFF", undefined, + undefined, e.tags); + } + + this.onList = onList; + this.offList = offList; + +} + +CIRCNetwork.prototype.on730 = /* RPL_MONONLINE */ +CIRCNetwork.prototype.on731 = /* RPL_MONOFFLINE */ +function my_monnotice(e) +{ + var userList = e.params[2].split(","); + var nickList = []; + var o = getObjectDetails(client.currentObject); + var displayTab; + var i; + var msg; + + if ("network" in o && o.network == this && client.currentObject != this) + displayTab = client.currentObject; + + for (i = 0; i < userList.length; i++) + { + var nick = e.server.toLowerCase(userList[i].split("!")[0]); + + // Make sure this nick is in the notify list. + if (this.prefs["notifyList"].indexOf(nick) < 0) + { + this.prefs["notifyList"].push(nick); + this.prefs["notifyList"].update(); + } + nickList.push(nick); + } + + if (e.code == "730") // RPL_MONONLINE + msg = arraySpeak (nickList, "is", "are") + " online."; + else // RPL_MONOFFLINE + msg = arraySpeak (nickList, "is", "are") + " offline."; + this.displayHere(msg, e.code, undefined, undefined, e.tags); + if (displayTab) + displayTab.displayHere(msg, e.code, undefined, undefined, e.tags); +} + +CIRCNetwork.prototype.on732 = /* RPL_MONLIST */ +function my_732(e) +{ + if (!this.pendingNotifyList) + this.pendingNotifyList = []; + var nickList = e.server.toLowerCase(e.params[2]).split(",") + this.pendingNotifyList = this.pendingNotifyList.concat(nickList); +} + +CIRCNetwork.prototype.on733 = /* RPL_ENDOFMONLIST */ +function my_733(e) +{ + if (this.pendingNotifyList) + { + this.prefs["notifyList"] = this.pendingNotifyList; + this.prefs["notifyList"].update(); + this.display(getMsg(MSG_NOTIFY_LIST, arraySpeak(this.pendingNotifyList))); + delete this.pendingNotifyList; + if (e.params[2]) + this.display(e.params[2], e.code, undefined, undefined, e.tags); + } + else + { + this.prefs["notifyList"] = []; + this.prefs["notifyList"].update(); + display(MSG_NO_NOTIFY_LIST, e.code, undefined, undefined, e.tags); + } +} + +CIRCNetwork.prototype.on734 = /* ERR_MONLISTFULL */ +function my_734(e) +{ + var nickList = e.server.toLowerCase(e.params[3]).split(",") + var i; + var msgname; + + for (i = 0; i < nickList.length; i++) + { + var j = this.prefs["notifyList"].indexOf(nickList[i]); + if (j >= 0) + arrayRemoveAt(this.prefs["notifyList"], j); + } + this.prefs["notifyList"].update(); + + if (e.params[4]) + this.display(e.params[4], e.code, undefined, undefined, e.tags) + else + this.display(MSG_NOTIFY_FULL); + + msgname = (nickList.length == 1) ? MSG_NOTIFY_DELONE : + MSG_NOTIFY_DELSOME; + this.display(getMsg(msgname, arraySpeak(nickList))); +} + +/* away off reply */ +CIRCNetwork.prototype.on305 = +function my_305(e) +{ + this.display(MSG_AWAY_OFF, e.code, undefined, undefined, e.tags); + + return true; +} + +/* away on reply */ +CIRCNetwork.prototype.on306 = +function my_306(e) +{ + var idleMsgParams = [this.prefs["away"], client.prefs["awayIdleTime"]]; + if (!this.isIdleAway) + this.display(getMsg(MSG_AWAY_ON, this.prefs["away"]), e.code, + undefined, undefined, e.tags); + else + this.display(getMsg(MSG_IDLE_AWAY_ON, idleMsgParams), e.code, + undefined, undefined, e.tags); + + return true; +} + + +CIRCNetwork.prototype.on263 = /* 'try again' */ +function my_263 (e) +{ + /* Urgh, this one's a pain. We need to abort whatever we tried, and start + * it again if appropriate. + * + * Known causes of this message: + * - LIST, with or without a parameter. + */ + + if (("_list" in this) && !this._list.done && (this._list.count == 0)) + { + // We attempted a LIST, and we think it failed. :) + this._list.done = true; + this._list.error = e.decodeParam(2); + // Return early for this one if we're saving it. + if ("file" in this._list) + return true; + } + + e.destMethod = "onUnknown"; + e.destObject = this; + return true; +} + +CIRCNetwork.prototype.isRunningList = +function my_running_list() +{ + /* The list is considered "running" when a cancel is effective. This means + * that even if _list.done is true (finished recieving data), we will still + * be "running" whilst we have undisplayed items. + */ + return (("_list" in this) && + (!this._list.done || (this._list.length > this._list.displayed)) && + !this._list.cancelled); +} + +CIRCNetwork.prototype.list = +function my_list(word, file) +{ + const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE; + + if (("_list" in this) && !this._list.done) + return false; + + this._list = new Array(); + this._list.string = word; + this._list.regexp = null; + this._list.done = false; + this._list.count = 0; + if (file) + { + var lfile = new LocalFile(file); + if (!lfile.localFile.exists()) + { + // futils.umask may be 0022. Result is 0644. + lfile.localFile.create(NORMAL_FILE_TYPE, 0o666 & ~futils.umask); + } + this._list.file = new LocalFile(lfile.localFile, ">"); + } + + if (isinstance(word, RegExp)) + { + this._list.regexp = word; + this._list.string = ""; + word = ""; + } + + if (word) + this.primServ.sendData("LIST " + fromUnicode(word, this) + "\n"); + else + this.primServ.sendData("LIST\n"); + + return true; +} + +CIRCNetwork.prototype.listInit = +function my_list_init () +{ + function checkEndList (network) + { + if (network._list.count == network._list.lastLength) + { + network.on323(); + } + else + { + network._list.lastLength = network._list.count; + network._list.endTimeout = + setTimeout(checkEndList, 5000, network); + } + } + + function outputList (network) + { + const CHUNK_SIZE = 5; + var list = network._list; + if (list.cancelled) + { + if (list.done) + { + /* The server is no longer throwing stuff at us, so now + * we can safely kill the list. + */ + network.display(getMsg(MSG_LIST_END, + [list.displayed, list.count])); + delete network._list; + } + else + { + /* We cancelled the list, but we're still getting data. + * Handle that data, but don't display, and do it more + * slowly, so we cause less lag. + */ + setTimeout(outputList, 1000, network); + } + return; + } + if (list.length > list.displayed) + { + var start = list.displayed; + var end = list.length; + if (end - start > CHUNK_SIZE) + end = start + CHUNK_SIZE; + for (var i = start; i < end; ++i) + network.displayHere(getMsg(MSG_FMT_CHANLIST, list[i]), "322", + undefined, undefined, list[i][3]); + list.displayed = end; + } + if (list.done && (list.displayed == list.length)) + { + if (list.event323) + { + var length = list.event323.params.length; + network.displayHere(list.event323.params[length - 1], "323", + undefined, undefined, list.event323.tags); + } + network.displayHere(getMsg(MSG_LIST_END, + [list.displayed, list.count])); + } + else + { + setTimeout(outputList, 250, network); + } + } + + if (!("_list" in this)) + { + this._list = new Array(); + this._list.string = MSG_UNKNOWN; + this._list.regexp = null; + this._list.done = false; + this._list.count = 0; + } + + if (!("file" in this._list)) + { + this._list.displayed = 0; + if (client.currentObject != this) + display (getMsg(MSG_LIST_REROUTED, this.unicodeName)); + setTimeout(outputList, 250, this); + } + this._list.lastLength = 0; + this._list.endTimeout = setTimeout(checkEndList, 5000, this); +} + +CIRCNetwork.prototype.abortList = +function my_abortList() +{ + this._list.cancelled = true; +} + +CIRCNetwork.prototype.on321 = /* LIST reply header */ +function my_321 (e) +{ + this.listInit(); + + if (!("file" in this._list)) + this.displayHere (e.params[2] + " " + e.params[3], "321"); +} + +CIRCNetwork.prototype.on323 = /* end of LIST reply */ +function my_323 (e) +{ + if (this._list.endTimeout) + { + clearTimeout(this._list.endTimeout); + delete this._list.endTimeout; + } + if (("file" in this._list)) + this._list.file.close(); + + this._list.done = true; + this._list.event323 = e; +} + +CIRCNetwork.prototype.on322 = /* LIST reply */ +function my_listrply (e) +{ + if (!("_list" in this) || !("lastLength" in this._list)) + this.listInit(); + + ++this._list.count; + + /* If the list has been cancelled, don't bother adding all this info + * anymore. Do increase the count (above), otherwise we never truly notice + * the list being finished. + */ + if (this._list.cancelled) + return; + + var chanName = e.decodeParam(2); + var topic = e.decodeParam(4); + if (!this._list.regexp || chanName.match(this._list.regexp) || + topic.match(this._list.regexp)) + { + if (!("file" in this._list)) + { + this._list.push([chanName, e.params[3], topic, e.tags]); + } + else + { + this._list.file.write(fromUnicode(chanName, "UTF-8") + " " + + e.params[3] + " " + + fromUnicode(topic, "UTF-8") + "\n"); + } + } +} + +CIRCNetwork.prototype.on401 = /* ERR_NOSUCHNICK */ +CIRCNetwork.prototype.on402 = /* ERR_NOSUCHSERVER */ +CIRCNetwork.prototype.on403 = /* ERR_NOSUCHCHANNEL */ +function my_401(e) +{ + var server, channel, user; + + /* Note that servers generally only send 401 and 402, sharing the former + * between nicknames and channels, but we're ready for anything. + */ + if (e.code == 402) + server = e.decodeParam(2); + else if (arrayIndexOf(e.server.channelTypes, e.params[2][0]) != -1) + channel = new CIRCChannel(e.server, null, e.params[2]); + else + user = new CIRCUser(e.server, null, e.params[2]); + + if (user && this.whoisList && (user.collectionKey in this.whoisList)) + { + // If this is from a /whois, send a /whowas and don't display anything. + this.primServ.whowas(user.unicodeName, 1); + this.whoisList[user.collectionKey] = false; + return; + } + + if (user) + user.display(getMsg(MSG_IRC_401, [user.unicodeName]), e.code, + undefined, undefined, e.tags); + else if (server) + this.display(getMsg(MSG_IRC_402, [server]), e.code, + undefined, undefined, e.tags); + else if (channel) + channel.display(getMsg(MSG_IRC_403, [channel.unicodeName]), e.code, + undefined, undefined, e.tags); + else + dd("on401: unreachable code."); +} + +/* 464; "invalid or missing password", occurs as a reply to both OPER and + * sometimes initially during user registration. */ +CIRCNetwork.prototype.on464 = +function my_464(e) +{ + if (this.state == NET_CONNECTING) + { + // If we are in the process of connecting we are needing a login + // password, subtly different from after user registration. + this.display(MSG_IRC_464_LOGIN, e.code, undefined, undefined, e.tags); + } + else + { + e.destMethod = "onUnknown"; + e.destObject = this; + } +} + +/* end of WHO */ +CIRCNetwork.prototype.on315 = +function my_315 (e) +{ + var matches; + if ("whoMatches" in this) + matches = this.whoMatches; + else + matches = 0; + + if ("pendingWhoReply" in this) + this.display(getMsg(MSG_WHO_END, [e.params[2], matches]), e.code, + undefined, undefined, e.tags); + + if ("whoUpdates" in this) + { + var userlist = document.getElementById("user-list"); + for (var c in this.whoUpdates) + { + for (var i = 0; i < this.whoUpdates[c].length; i++) + { + var index = this.whoUpdates[c][i].chanListEntry.childIndex; + userlist.treeBoxObject.invalidateRow(index); + } + this.primServ.channels[c].updateUsers(this.whoUpdates[c]); + } + delete this.whoUpdates; + } + + delete this.pendingWhoReply; + delete this.whoMatches; +} + +CIRCNetwork.prototype.on352 = +function my_352 (e) +{ + //0-352 1-sender 2-channel 3-ident 4-host + //5-server 6-nick 7-H/G 8-hops and realname + if ("pendingWhoReply" in this) + { + var status; + if (e.user.isAway) + status = MSG_GONE; + else + status = MSG_HERE; + + this.display(getMsg(MSG_WHO_MATCH, + [e.params[6], e.params[3], e.params[4], + e.user.desc, status, e.decodeParam(2), + e.params[5], e.user.hops]), e.code, e.user, + undefined, e.tags); + } + + updateTitle(e.user); + if ("whoMatches" in this) + ++this.whoMatches; + else + this.whoMatches = 1; + + if (!("whoUpdates" in this)) + this.whoUpdates = new Object(); + + if (e.userHasChanges) + { + for (var c in e.server.channels) + { + var chan = e.server.channels[c]; + if (chan.active && (e.user.collectionKey in chan.users)) + { + if (!(c in this.whoUpdates)) + this.whoUpdates[c] = new Array(); + this.whoUpdates[c].push(chan.users[e.user.collectionKey]); + } + } + } +} + +CIRCNetwork.prototype.on354 = +function my_354(e) +{ + //0-352 1-sender 2-type 3-channel 4-ident 5-host + //6-server 7-nick 8-H/G 9-hops 10-account 11-realname + if ("pendingWhoReply" in this) + { + var status; + if (e.user.isAway) + status = MSG_GONE; + else + status = MSG_HERE; + + this.display(getMsg(MSG_WHO_MATCH, + [e.params[7], e.params[4], e.params[5], + e.user.desc, status, e.decodeParam(3), + e.params[6], e.user.hops]), e.code, e.user, + undefined, e.tags); + } + + updateTitle(e.user); + if ("whoMatches" in this) + ++this.whoMatches; + else + this.whoMatches = 1; + + if (!("whoUpdates" in this)) + this.whoUpdates = new Object(); + + if (e.userHasChanges) + { + for (var c in e.server.channels) + { + var chan = e.server.channels[c]; + if (chan.active && (e.user.collectionKey in chan.users)) + { + if (!(c in this.whoUpdates)) + this.whoUpdates[c] = new Array(); + this.whoUpdates[c].push(chan.users[e.user.collectionKey]); + } + } + } +} + +CIRCNetwork.prototype.on301 = /* user away message */ +function my_301(e) +{ + if (e.user.awayMessage != e.user.lastShownAwayMessage) + { + var params = [e.user.unicodeName, e.user.awayMessage]; + e.user.display(getMsg(MSG_WHOIS_AWAY, params), e.code, + undefined, undefined, e.tags); + e.user.lastShownAwayMessage = e.user.awayMessage; + } +} + +CIRCNetwork.prototype.on311 = /* whois name */ +CIRCNetwork.prototype.on319 = /* whois channels */ +CIRCNetwork.prototype.on312 = /* whois server */ +CIRCNetwork.prototype.on317 = /* whois idle time */ +CIRCNetwork.prototype.on318 = /* whois end of whois*/ +CIRCNetwork.prototype.on330 = /* ircu's 330 numeric ("X is logged in as Y") */ +CIRCNetwork.prototype.onUnknownWhois = /* misc whois line */ +function my_whoisreply (e) +{ + var text = "egads!"; + var nick = e.params[2]; + var lowerNick = this.primServ.toLowerCase(nick); + var user; + + if (this.whoisList && (e.code != 318) && (lowerNick in this.whoisList)) + this.whoisList[lowerNick] = true; + + if (e.user) + { + user = e.user; + nick = user.unicodeName; + } + + switch (Number(e.code)) + { + case 311: + // Clear saved away message so it appears and can be reset. + if (e.user) + e.user.lastShownAwayMessage = ""; + + text = getMsg(MSG_WHOIS_NAME, + [nick, e.params[3], e.params[4], + e.decodeParam(6)]); + break; + + case 319: + var ary = stringTrim(e.decodeParam(3)).split(" "); + text = getMsg(MSG_WHOIS_CHANNELS, [nick, arraySpeak(ary)]); + break; + + case 312: + text = getMsg(MSG_WHOIS_SERVER, + [nick, e.params[3], e.params[4]]); + break; + + case 317: + text = getMsg(MSG_WHOIS_IDLE, + [nick, formatDateOffset(Number(e.params[3])), + new Date(Number(e.params[4]) * 1000)]); + break; + + case 318: + // If the user isn't here, then we sent a whowas in on401. + // Don't display the "end of whois" message. + if (this.whoisList && (lowerNick in this.whoisList) && + !this.whoisList[lowerNick]) + { + delete this.whoisList[lowerNick]; + return; + } + if (this.whoisList) + delete this.whoisList[lowerNick]; + + text = getMsg(MSG_WHOIS_END, nick); + if (user) + user.updateHeader(); + break; + + case 330: + text = getMsg(MSG_FMT_LOGGED_ON, [e.decodeParam(2), e.params[3]]); + break; + + default: + text = toUnicode(e.params.splice(2, e.params.length).join(" "), + this); + } + + if (e.user) + e.user.display(text, e.code, undefined, undefined, e.tags); + else + this.display(text, e.code, undefined, undefined, e.tags); +} + +CIRCNetwork.prototype.on341 = /* invite reply */ +function my_341 (e) +{ + this.display (getMsg(MSG_YOU_INVITE, [e.decodeParam(2), e.decodeParam(3)]), + "341", undefined, undefined, e.tags); +} + +CIRCNetwork.prototype.onInvite = /* invite message */ +function my_invite (e) +{ + var invitee = e.params[1]; + if (invitee == e.server.me.unicodeName) + { + client.munger.getRule(".inline-buttons").enabled = true; + this.display(getMsg(MSG_INVITE_YOU, [e.user.unicodeName, e.user.name, + e.user.host, + e.channel.unicodeName, + e.channel.unicodeName, + e.channel.getURL()]), + "INVITE", undefined, undefined, e.tags); + client.munger.getRule(".inline-buttons").enabled = false; + + if ("messages" in e.channel) + e.channel.join(); + } + else + { + this.display(getMsg(MSG_INVITE_SOMEONE, [e.user.unicodeName, + invitee, + e.channel.unicodeName]), + "INVITE", undefined, undefined, e.tags); + } +} + +CIRCNetwork.prototype.on433 = /* nickname in use */ +function my_433 (e) +{ + var nick = toUnicode(e.params[2], this); + + if ("pendingReclaimCheck" in this) + { + delete this.pendingReclaimCheck; + return; + } + + if (this.state == NET_CONNECTING) + { + // Force a number, thanks. + var nickIndex = 1 * arrayIndexOf(this.prefs["nicknameList"], nick); + var newnick = null; + + dd("433: failed with " + nick + " (" + nickIndex + ")"); + + var tryList = true; + + if ((("_firstNick" in this) && (this._firstNick == -1)) || + (this.prefs["nicknameList"].length == 0) || + ((nickIndex != -1) && (this.prefs["nicknameList"].length < 2))) + { + tryList = false; + } + + if (tryList) + { + nickIndex = (nickIndex + 1) % this.prefs["nicknameList"].length; + + if (("_firstNick" in this) && (this._firstNick == nickIndex)) + { + // We're back where we started. Give up with this method. + this._firstNick = -1; + tryList = false; + } + } + + if (tryList) + { + newnick = this.prefs["nicknameList"][nickIndex]; + dd(" trying " + newnick + " (" + nickIndex + ")"); + + // Save first index we've tried. + if (!("_firstNick" in this)) + this._firstNick = nickIndex; + } + else if (this.NICK_RETRIES > 0) + { + newnick = this.INITIAL_NICK + "_"; + this.NICK_RETRIES--; + dd(" trying " + newnick); + } + + if (newnick) + { + this.INITIAL_NICK = newnick; + this.display(getMsg(MSG_RETRY_NICK, [nick, newnick]), "433", + undefined, undefined, e.tags); + this.primServ.changeNick(newnick); + } + else + { + this.display(getMsg(MSG_NICK_IN_USE, nick), "433", + undefined, undefined, e.tags); + } + } + else + { + this.display(getMsg(MSG_NICK_IN_USE, nick), "433", + undefined, undefined, e.tags); + } +} + +CIRCNetwork.prototype.onStartConnect = +function my_sconnect (e) +{ + this.busy = true; + updateProgress(); + if ("_firstNick" in this) + delete this._firstNick; + + client.munger.getRule(".inline-buttons").enabled = true; + this.display(getMsg(MSG_CONNECTION_ATTEMPT, + [this.getURL(), e.server.getURL(), this.unicodeName, + "cancel"]), "INFO"); + client.munger.getRule(".inline-buttons").enabled = false; + + if (this.prefs["identd.enabled"]) + { + try + { + client.ident.addNetwork(this, e.server); + } + catch (ex) + { + display(getMsg(MSG_IDENT_ERROR, formatException(ex)), MT_ERROR); + } + } + + this.NICK_RETRIES = this.prefs["nicknameList"].length + 3; + + // When connection begins, autoperform has not been sent + this.autoPerformSent = false; +} + +CIRCNetwork.prototype.onError = +function my_neterror (e) +{ + var msg; + var type = MT_ERROR; + + if (typeof e.errorCode != "undefined") + { + switch (e.errorCode) + { + case JSIRC_ERR_NO_SOCKET: + msg = MSG_ERR_NO_SOCKET; + break; + + case JSIRC_ERR_EXHAUSTED: + // error already displayed in onDisconnect + break; + + case JSIRC_ERR_OFFLINE: + msg = MSG_ERR_OFFLINE; + break; + + case JSIRC_ERR_NO_SECURE: + msg = getMsg(MSG_ERR_NO_SECURE, this.unicodeName); + break; + + case JSIRC_ERR_CANCELLED: + msg = MSG_ERR_CANCELLED; + type = MT_INFO; + break; + + case JSIRC_ERR_PAC_LOADING: + msg = MSG_WARN_PAC_LOADING; + type = MT_WARN; + break; + } + } + else + { + msg = e.params[e.params.length - 1]; + } + + dispatch("sync-header"); + updateTitle(); + + if (this.state == NET_OFFLINE) + { + this.busy = false; + updateProgress(); + } + + client.ident.removeNetwork(this); + + if (msg) + this.display(msg, type); + + if (e.errorCode == JSIRC_ERR_PAC_LOADING) + return; + + if (this.deleteWhenDone) + this.dispatch("delete-view"); + + delete this.deleteWhenDone; +} + + +CIRCNetwork.prototype.onDisconnect = +function my_netdisconnect (e) +{ + var msg, msgNetwork; + var msgType = MT_ERROR; + var retrying = true; + + if (typeof e.disconnectStatus != "undefined") + { + switch (e.disconnectStatus) + { + case 0: + msg = getMsg(MSG_CONNECTION_CLOSED, + [this.getURL(), e.server.getURL()]); + break; + + case NS_ERROR_CONNECTION_REFUSED: + msg = getMsg(MSG_CONNECTION_REFUSED, + [this.getURL(), e.server.getURL()]); + break; + + case NS_ERROR_NET_TIMEOUT: + msg = getMsg(MSG_CONNECTION_TIMEOUT, + [this.getURL(), e.server.getURL()]); + break; + + case NS_ERROR_NET_RESET: + msg = getMsg(MSG_CONNECTION_RESET, + [this.getURL(), e.server.getURL()]); + break; + + case NS_ERROR_NET_INTERRUPT: + msg = getMsg(MSG_CONNECTION_INTERRUPT, + [this.getURL(), e.server.getURL()]); + break; + + case NS_ERROR_UNKNOWN_HOST: + msg = getMsg(MSG_UNKNOWN_HOST, + [e.server.hostname, this.getURL(), + e.server.getURL()]); + break; + + case NS_ERROR_UNKNOWN_PROXY_HOST: + msg = getMsg(MSG_UNKNOWN_PROXY_HOST, + [this.getURL(), e.server.getURL()]); + break; + + case NS_ERROR_PROXY_CONNECTION_REFUSED: + msg = MSG_PROXY_CONNECTION_REFUSED; + break; + + case NS_ERROR_OFFLINE: + msg = MSG_ERR_OFFLINE; + retrying = false; + break; + + case NS_ERROR_ABORT: + if (Services.io.offline) + { + msg = getMsg(MSG_CONNECTION_ABORT_OFFLINE, + [this.getURL(), e.server.getURL()]); + } + else + { + msg = getMsg(MSG_CONNECTION_ABORT_UNKNOWN, + [this.getURL(), e.server.getURL(), + formatException(e.exception)]); + } + retrying = false; + break; + + default: + var errClass = getNSSErrorClass(e.disconnectStatus); + // Check here if it's a cert error. + // The exception adding dialog will explain the reasons. + if (errClass == ERROR_CLASS_BAD_CERT) + { + var cmd = "ssl-exception"; + cmd += " " + e.server.hostname + " " + e.server.port; + cmd += " true"; + msg = getMsg(MSG_INVALID_CERT, [this.getURL(), cmd]); + retrying = false; + break; + } + + // If it's a protocol error, we can still display a useful message. + var statusMsg = e.disconnectStatus; + if (errClass == ERROR_CLASS_SSL_PROTOCOL) + { + var nssErrSvc = getService("@mozilla.org/nss_errors_service;1", + "nsINSSErrorsService"); + var errMsg = nssErrSvc.getErrorMessage(e.disconnectStatus); + errMsg = errMsg.replace(/\.$/, ""); + statusMsg += " (" + errMsg + ")"; + } + + msg = getMsg(MSG_CLOSE_STATUS, + [this.getURL(), e.server.getURL(), + statusMsg]); + break; + } + } + else + { + msg = getMsg(MSG_CONNECTION_CLOSED, + [this.getURL(), e.server.getURL()]); + } + + // e.quitting signals the disconnect was intended: don't use "ERROR". + if (e.quitting) + { + msgType = "DISCONNECT"; + msg = getMsg(MSG_CONNECTION_QUIT, + [this.getURL(), e.server.getURL(), this.unicodeName, + "reconnect"]); + msgNetwork = msg; + } + // We won't reconnect if the error was really bad, or if the user doesn't + // want us to do so. + else if (!retrying || !this.stayingPower) + { + msgNetwork = msg; + } + else + { + var delayStr = formatDateOffset(this.getReconnectDelayMs() / 1000); + if (this.MAX_CONNECT_ATTEMPTS == -1) + { + msgNetwork = getMsg(MSG_RECONNECTING_IN, + [msg, delayStr, this.unicodeName, "cancel"]); + } + else if (this.connectAttempt < this.MAX_CONNECT_ATTEMPTS) + { + var left = this.MAX_CONNECT_ATTEMPTS - this.connectAttempt; + if (left == 1) + { + msgNetwork = getMsg(MSG_RECONNECTING_IN_LEFT1, + [msg, delayStr, this.unicodeName, + "cancel"]); + } + else + { + msgNetwork = getMsg(MSG_RECONNECTING_IN_LEFT, + [msg, left, delayStr, this.unicodeName, + "cancel"]); + } + } + else + { + msgNetwork = getMsg(MSG_CONNECTION_EXHAUSTED, msg); + } + } + + /* If we were connected ok, put an error on all tabs. If we were only + * /trying/ to connect, and failed, just put it on the network tab. + */ + client.munger.getRule(".inline-buttons").enabled = true; + if (this.state == NET_ONLINE) + { + for (var v in client.viewsArray) + { + var obj = client.viewsArray[v].source; + if (obj == this) + { + obj.displayHere(msgNetwork, msgType); + } + else if (obj != client) + { + var details = getObjectDetails(obj); + if ("server" in details && details.server == e.server) + obj.displayHere(msg, msgType); + } + } + } + else + { + this.busy = false; + updateProgress(); + + // Don't do anything if we're cancelling. + if (this.state != NET_CANCELLING) + { + this.displayHere(msgNetwork, msgType); + } + } + client.munger.getRule(".inline-buttons").enabled = false; + + for (var c in this.primServ.channels) + { + var channel = this.primServ.channels[c]; + channel._clearUserList(); + } + + dispatch("sync-header"); + updateTitle(); + updateProgress(); + updateSecurityIcon(); + + client.ident.removeNetwork(this); + + if ("userClose" in client && client.userClose && + client.getConnectionCount() == 0) + window.close(); + + // Renew the STS policy. + if (e.server.isSecure && ("sts" in e.server.caps) && client.sts.ENABLED) + { + var policy = client.sts.parseParameters(e.server.capvals["sts"]); + client.sts.setPolicy(e.server.hostname, e.server.port, policy.duration); + } + + if (("reconnect" in this) && this.reconnect) + { + if ("stsUpgradePort" in this) + { + e.server.port = this.stsUpgradePort; + e.server.isSecure = true; + delete this.stsUpgradePort; + } + this.connect(this.requireSecurity); + delete this.reconnect; + } +} + +CIRCNetwork.prototype.onCTCPReplyPing = +function my_replyping (e) +{ + // see bug 326523 + if (stringTrim(e.CTCPData).length != 13) + { + this.display(getMsg(MSG_PING_REPLY_INVALID, e.user.unicodeName), + "INFO", e.user, "ME!", e.tags); + return; + } + + var delay = formatDateOffset((new Date() - new Date(Number(e.CTCPData))) / + 1000); + this.display(getMsg(MSG_PING_REPLY, [e.user.unicodeName, delay]), "INFO", + e.user, "ME!", e.tags); +} + +CIRCNetwork.prototype.on221 = +CIRCNetwork.prototype.onUserMode = +function my_umode (e) +{ + if ("user" in e && e.user) + { + e.user.updateHeader(); + this.display(getMsg(MSG_USER_MODE, [e.user.unicodeName, e.params[2]]), + MT_MODE, undefined, undefined, e.tags); + } + else + { + this.display(getMsg(MSG_USER_MODE, [e.params[1], e.params[2]]), + MT_MODE, undefined, undefined, e.tags); + } +} + +CIRCNetwork.prototype.onNick = +function my_cnick (e) +{ + if (!ASSERT(userIsMe(e.user), "network nick event for third party")) + return; + + if (("pendingNickChange" in this) && + (this.pendingNickChange == e.user.unicodeName)) + { + this.prefs["nickname"] = e.user.unicodeName; + this.preferredNick = e.user.unicodeName; + delete this.pendingNickChange; + } + + if (getTabForObject(this)) + { + this.displayHere(getMsg(MSG_NEWNICK_YOU, e.user.unicodeName), + "NICK", "ME!", e.user, e.tags); + } + + this.updateHeader(); + updateStalkExpression(this); +} + +CIRCNetwork.prototype.onPing = +function my_netping (e) +{ + this.updateHeader(this); +} + +CIRCNetwork.prototype.onPong = +function my_netpong (e) +{ + this.updateHeader(this); +} + +CIRCNetwork.prototype.onWallops = +function my_netwallops(e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + if (e.user) + this.display(e.msg, "WALLOPS/WALLOPS", e.user, this, e.tags); + else + this.display(e.msg, "WALLOPS/WALLOPS", undefined, this, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +/* unknown command reply */ +CIRCNetwork.prototype.on421 = +function my_421(e) +{ + this.display(getMsg(MSG_IRC_421, e.decodeParam(2)), MT_ERROR, undefined, + undefined, e.tags); + return true; +} + +/* cap reply */ +CIRCNetwork.prototype.onCap = +function my_cap(e) +{ + if (e.params[2] == "LS") + { + // Handle the STS upgrade policy if we have one. + if (e.server.pendingCapNegotiation && e.stsUpgradePort) + { + this.display(getMsg(MSG_STS_UPGRADE, e.stsUpgradePort)); + this.reconnect = true; + this.stsUpgradePort = e.stsUpgradePort; + this.quit(MSG_RECONNECTING); + return true; + } + + // Don't show the raw message until we've registered. + if (this.state == NET_ONLINE) + { + + var listCaps = new Array(); + for (var cap in e.server.caps) + { + var value = e.server.capvals[cap]; + if (value) + cap += "=" + value; + listCaps.push(cap); + } + if (listCaps.length > 0) + { + listCaps.sort(); + this.display(getMsg(MSG_SUPPORTS_CAPS, listCaps.join(", "))); + } + } + + // Update the STS duration policy. + if (e.server.isSecure && ("sts" in e.server.caps) && client.sts.ENABLED) + { + var policy = client.sts.parseParameters(e.server.capvals["sts"]); + client.sts.setPolicy(e.server.hostname, e.server.port, policy.duration); + } + } + else if (e.params[2] == "LIST") + { + var listCapsEnabled = new Array(); + for (var cap in e.server.caps) + { + if (e.server.caps[cap]) + { + listCapsEnabled.push(cap); + } + } + if (listCapsEnabled.length > 0) + { + listCapsEnabled.sort(); + this.display(getMsg(MSG_SUPPORTS_CAPSON, + listCapsEnabled.join(", "))); + } + } + else if (e.params[2] == "ACK") + { + if (e.capsOn.length) + this.display(getMsg(MSG_CAPS_ON, e.capsOn.join(", "))); + if (e.capsOff.length) + this.display(getMsg(MSG_CAPS_OFF, e.capsOff.join(", "))); + } + else if (e.params[2] == "NAK") + { + this.display(getMsg(MSG_CAPS_ERROR, e.caps.join(", "))); + } + else if (e.params[2] == "NEW") + { + // Handle a new STS policy + if (client.sts.ENABLED && (arrayContains(e.newcaps, "sts"))) + { + var policy = client.sts.parseParameters(e.server.capvals["sts"]); + if (!e.server.isSecure && policy.port) + { + // Inform the user of the new upgrade policy and + // offer an option to reconnect. + client.munger.getRule(".inline-buttons").enabled = true; + this.display(getMsg(MSG_STS_UPGRADE_NEW, [this.unicodeName, "reconnect"])); + client.munger.getRule(".inline-buttons").enabled = false; + } + else if (e.server.isSecure && policy.duration) + { + // Renew the policy's duration. + client.sts.setPolicy(e.server.hostname, e.server.port, policy.duration); + } + } + } + return true; +} + +// Notify the user of received CTCP requests. +CIRCNetwork.prototype.onReceiveCTCP = +function my_ccrecv(e) +{ + // Do nothing if we receive these. + if ((e.type == "ctcp-action") || + (e.type == "ctcp-dcc") || + (e.type == "unk-ctcp")) + return true; + + this.display(getMsg(MSG_FMT_CTCPRECV, + [toUnicode(e.CTCPCode, this), + toUnicode(e.CTCPData, this), e.user.unicodeName]), + "CTCP_REQUEST", e.user, e.server.me, e.tags); + + return true; +} + +/* SASL authentication start */ +CIRCNetwork.prototype.onSASLStart = +function my_saslstart(e) +{ + if (!e.mechs || e.mechs.indexOf("plain") !== -1) + e.server.sendData("AUTHENTICATE PLAIN\n"); +} + +/* SASL authentication response */ +CIRCNetwork.prototype.onAuthenticate = +function my_auth(e) +{ + if (e.params[1] !== "+") + return; + + var username = e.server.me.encodedName; + var password = client.tryToGetLogin(e.server.parent.getURL(), "sasl", + e.server.me.name, null, true, + getMsg(MSG_SASL_PASSWORD, username)); + if (!password) + { + // Abort authentication. + e.server.sendAuthAbort(); + return; + } + + var auth = username + '\0' + username + '\0' + password; + e.server.sendAuthResponse(auth); +} + +CIRCNetwork.prototype.onNetsplitBatch = +function my_netsplit_batch(e) +{ + for (var c in this.primServ.channels) + { + if (e.starting) + { + this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_NETSPLIT_START, + [e.params[3], + e.params[4]]), + e.batchtype); + } + else + { + this.display(MSG_BATCH_NETSPLIT_END, e.batchtype); + this.endMsgGroup(); + } + } +} + +CIRCNetwork.prototype.onNetjoinBatch = +function my_netjoin_batch(e) +{ + for (var c in this.primServ.channels) + { + if (e.starting) + { + this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_NETJOIN_START, + [e.params[3], + e.params[4]]), + e.batchtype); + } + else + { + this.display(MSG_BATCH_NETJOIN_END, e.batchtype); + this.endMsgGroup(); + } + } +} + +CIRCChannel.prototype.onChathistoryBatch = +function my_chathistory_batch(e) +{ + if (e.starting) + { + this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_CHATHISTORY_START, + [e.params[3]]), + e.batchtype); + } + else + { + this.display(MSG_BATCH_CHATHISTORY_END, e.batchtype); + this.endMsgGroup(); + } +} + +CIRCNetwork.prototype.onUnknownBatch = +CIRCChannel.prototype.onUnknownBatch = +CIRCUser.prototype.onUnknownBatch = +function my_unknown_batch(e) +{ + if (e.starting) + { + this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_UNKNOWN, + [e.batchtype, + e.params.slice(3)]), + "BATCH"); + } + else + { + this.display(MSG_BATCH_UNKNOWN_END, e.batchtype); + this.endMsgGroup(); + } +} + +/* user away status */ +CIRCNetwork.prototype.onAway = +function my_away(e) +{ + var userlist = document.getElementById("user-list"); + for (var c in e.server.channels) + { + var chan = e.server.channels[c]; + if (chan.active && (e.user.collectionKey in chan.users)) + { + let index = chan.users[e.user.collectionKey].chanListEntry.childIndex; + userlist.treeBoxObject.invalidateRow(index); + e.server.channels[c].updateUsers([e.user.collectionKey]); + } + } +} + +/* user host changed */ +CIRCNetwork.prototype.onChghost = +function my_chghost(e) +{ + e.user.updateHeader(); +} + +CIRCNetwork.prototype.reclaimName = +function my_reclaimname() +{ + var network = this; + + function callback() { + network.reclaimName(); + }; + + if ("pendingReclaimCheck" in this) + delete this.pendingReclaimCheck; + + // Function to attempt to get back the nickname the user wants. + if ((this.state != NET_ONLINE) || !this.primServ) + return false; + + if (this.primServ.me.unicodeName == this.preferredNick) + return false; + + this.reclaimLeft -= this.RECLAIM_WAIT; + + if (this.reclaimLeft <= 0) + return false; + + this.pendingReclaimCheck = true; + this.INITIAL_NICK = this.preferredNick; + this.primServ.changeNick(this.preferredNick); + + setTimeout(callback, this.RECLAIM_WAIT); + + return true; +} + +CIRCNetwork.prototype.doAutoPerform = +function net_autoperform() +{ + if (("autoPerformSent" in this) && (this.autoPerformSent == false)) + { + var cmdary = client.prefs["autoperform.network"].concat(this.prefs["autoperform"]); + for (var i = 0; i < cmdary.length; ++i) + { + if (cmdary[i][0] == "/") + this.dispatch(cmdary[i].substr(1)); + else + this.dispatch(cmdary[i]); + } + this.autoPerformSent = true; + } +} + + +/* We want to override the base implementations. */ +CIRCChannel.prototype._join = CIRCChannel.prototype.join; +CIRCChannel.prototype._part = CIRCChannel.prototype.part; + +CIRCChannel.prototype.join = +function chan_join(key) +{ + var joinFailedFn = function _joinFailedFn(t) + { + delete t.joinTimer; + t.busy = false; + updateProgress(); + } + if (!this.joined) + { + this.joinTimer = setTimeout(joinFailedFn, 30000, this); + this.busy = true; + updateProgress(); + } + this._join(key); +} + +CIRCChannel.prototype.part = +function chan_part(reason) +{ + var partFailedFn = function _partFailedFn(t) + { + delete t.partTimer; + t.busy = false; + updateProgress(); + } + this.partTimer = setTimeout(partFailedFn, 30000, this); + this.busy = true; + updateProgress(); + this._part(reason); +} + +client.setActivityMarker = +CIRCNetwork.prototype.setActivityMarker = +CIRCChannel.prototype.setActivityMarker = +CIRCUser.prototype.setActivityMarker = +CIRCDCCChat.prototype.setActivityMarker = +CIRCDCCFileTransfer.prototype.setActivityMarker = +function view_setactivitymarker(state) +{ + if (!client.initialized) + return; + + // Always clear the activity marker first. + var markedRow = this.getActivityMarker(); + if (markedRow) + { + markedRow.classList.remove("chatzilla-line-marker"); + } + + if (state) + { + // Mark the last row. + var target = this.messages.firstChild.lastChild; + if (!target) + return; + target.classList.add("chatzilla-line-marker"); + } +} + +client.getActivityMarker = +CIRCNetwork.prototype.getActivityMarker = +CIRCChannel.prototype.getActivityMarker = +CIRCUser.prototype.getActivityMarker = +CIRCDCCChat.prototype.getActivityMarker = +CIRCDCCFileTransfer.prototype.getActivityMarker = +function view_getactivitymarker() +{ + return this.messages.querySelector(".chatzilla-line-marker"); +} +CIRCChannel.prototype.onInit = +function chan_oninit () +{ + this.logFile = null; + this.pendingNamesReply = false; + this.importantMessages = 0; +} + +CIRCChannel.prototype.onPrivmsg = +function my_cprivmsg (e) +{ + var msg = e.decodeParam(2); + var msgtype = "PRIVMSG"; + if ("msgPrefix" in e) + msgtype += "/" + e.msgPrefix.symbol; + + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.display(msg, msgtype, e.user, this, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +/* end of names */ +CIRCChannel.prototype.on366 = +function my_366 (e) +{ + // First clear up old users: + var removals = new Array(); + while (this.userList.childData.childData.length > 0) + { + var userToRemove = this.userList.childData.childData[0]._userObj; + this.removeFromList(userToRemove); + removals.push(userToRemove); + } + this.removeUsers(removals); + + var entries = new Array(), updates = new Array(); + for (var u in this.users) + { + entries.push(new UserEntry(this.users[u], this.userListShare)); + updates.push(this.users[u]); + } + this.addUsers(updates); + + this.userList.childData.appendChildren(entries); + + if (this.pendingNamesReply) + { + this.parent.parent.display (e.channel.unicodeName + ": " + + e.params[3], "366", undefined, undefined, + e.tags); + } + this.pendingNamesReply = false; + + // Update conference mode now we have a complete user list. + this._updateConferenceMode(); +} + +CIRCChannel.prototype.onTopic = /* user changed topic */ +CIRCChannel.prototype.on332 = /* TOPIC reply */ +function my_topic (e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + if (e.code == "TOPIC") + this.display (getMsg(MSG_TOPIC_CHANGED, [this.topicBy, this.topic]), + "TOPIC", undefined, undefined, e.tags); + + if (e.code == "332") + { + if (this.topic) + { + this.display (getMsg(MSG_TOPIC, + [this.unicodeName, this.topic]), + "TOPIC", undefined, undefined, e.tags); + } + else + { + this.display(getMsg(MSG_NO_TOPIC, this.unicodeName), "TOPIC", + undefined, undefined, e.tags); + } + } + + this.updateHeader(); + updateTitle(this); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCChannel.prototype.on333 = /* Topic setter information */ +function my_topicinfo (e) +{ + this.display (getMsg(MSG_TOPIC_DATE, [this.unicodeName, this.topicBy, + this.topicDate]), "TOPIC", + undefined, undefined, e.tags); +} + +CIRCChannel.prototype.on353 = /* names reply */ +function my_topic (e) +{ + if (this.pendingNamesReply) + { + this.parent.parent.display (e.channel.unicodeName + ": " + + e.params[4], "NAMES", undefined, undefined, + e.tags); + } +} + +CIRCChannel.prototype.on367 = /* channel ban stuff */ +function my_bans(e) +{ + if ("pendingBanList" in this) + return; + + var msg = getMsg(MSG_BANLIST_ITEM, + [e.user.unicodeName, e.ban, this.unicodeName, e.banTime]); + if (this.iAmHalfOp() || this.iAmOp()) + msg += " " + getMsg(MSG_BANLIST_BUTTON, "mode -b " + e.ban); + + client.munger.getRule(".inline-buttons").enabled = true; + this.display(msg, "BAN", undefined, undefined, e.tags); + client.munger.getRule(".inline-buttons").enabled = false; +} + +CIRCChannel.prototype.on368 = +function my_endofbans(e) +{ + if ("pendingBanList" in this) + return; + + this.display(getMsg(MSG_BANLIST_END, this.unicodeName), "BAN", undefined, + undefined, e.tags); +} + +CIRCChannel.prototype.on348 = /* channel except stuff */ +function my_excepts(e) +{ + if ("pendingExceptList" in this) + return; + + var msg = getMsg(MSG_EXCEPTLIST_ITEM, [e.user.unicodeName, e.except, + this.unicodeName, e.exceptTime]); + if (this.iAmHalfOp() || this.iAmOp()) + msg += " " + getMsg(MSG_EXCEPTLIST_BUTTON, "mode -e " + e.except); + + client.munger.getRule(".inline-buttons").enabled = true; + this.display(msg, "EXCEPT", undefined, undefined, e.tags); + client.munger.getRule(".inline-buttons").enabled = false; +} + +CIRCChannel.prototype.on349 = +function my_endofexcepts(e) +{ + if ("pendingExceptList" in this) + return; + + this.display(getMsg(MSG_EXCEPTLIST_END, this.unicodeName), "EXCEPT", + undefined, undefined, e.tags); +} + +CIRCChannel.prototype.on482 = +function my_needops(e) +{ + if ("pendingExceptList" in this) + return; + + this.display(getMsg(MSG_CHANNEL_NEEDOPS, this.unicodeName), MT_ERROR, + undefined, undefined, e.tags); +} + +CIRCChannel.prototype.onNotice = +function my_notice (e) +{ + var msgtype = "NOTICE"; + if ("msgPrefix" in e) + msgtype += "/" + e.msgPrefix.symbol; + + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.display(e.decodeParam(2), msgtype, e.user, this, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCChannel.prototype.onCTCPAction = +function my_caction (e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.display(e.CTCPData, "ACTION", e.user, this, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCChannel.prototype.onUnknownCTCP = +function my_unkctcp (e) +{ + this.display (getMsg(MSG_UNKNOWN_CTCP, [e.CTCPCode, e.CTCPData, + e.user.unicodeName]), + "BAD-CTCP", e.user, this, e.tags); +} + +CIRCChannel.prototype.onJoin = +function my_cjoin (e) +{ + dispatch("create-tab-for-view", { view: e.channel }); + + if (userIsMe(e.user)) + { + var params = [e.user.unicodeName, e.channel.unicodeName]; + this.display(getMsg(MSG_YOU_JOINED, params), "JOIN", + e.server.me, this, e.tags); + /* Tell the user that conference mode is on, lest they forget (if it + * subsequently turns itself off, they'll get a message anyway). + */ + if (this.prefs["conference.enabled"]) + this.display(MSG_CONF_MODE_STAYON); + addURLToHistory(this.getURL()); + + if ("joinTimer" in this) + { + clearTimeout(this.joinTimer); + delete this.joinTimer; + this.busy = false; + updateProgress(); + } + + /* !-channels are "safe" channels, and get a server-generated prefix. + * For this reason, creating the channel is delayed until this point. + */ + if (e.channel.unicodeName[0] == "!") + dispatch("set-current-view", { view: e.channel }); + + this.doAutoPerform(); + } + else + { + if (!this.prefs["conference.enabled"]) + { + this.display(getMsg(MSG_SOMEONE_JOINED, + [e.user.unicodeName, e.user.name, e.user.host, + e.channel.unicodeName]), + "JOIN", e.user, this, e.tags); + } + + /* Only do this for non-me joins so us joining doesn't reset it (when + * we join the usercount is always 1). Also, do this after displaying + * the join message so we don't get cryptic effects such as a user + * joining causes *only* a "Conference mode enabled" message. + */ + this._updateConferenceMode(); + } + + /* We don't want to add ourself here, since the names reply we'll be + * getting right after the join will include us as well! (FIXME) + */ + if (!userIsMe(e.user)) + { + this.addUsers([e.user]); + var entry = new UserEntry(e.user, this.userListShare); + this.userList.childData.appendChild(entry); + this.userList.childData.reSort(); + } + this.updateHeader(); +} + +CIRCChannel.prototype.onPart = +function my_cpart(e) +{ + this.removeUsers([e.user]); + this.updateHeader(); + + if (userIsMe(e.user)) + { + var msg = e.reason ? MSG_YOU_LEFT_REASON : MSG_YOU_LEFT; + var params = [e.user.unicodeName, e.channel.unicodeName, e.reason]; + this.display(getMsg(msg, params), "PART", e.user, this, e.tags); + this._clearUserList(); + + if ("partTimer" in this) + { + clearTimeout(this.partTimer); + delete this.partTimer; + this.busy = false; + updateProgress(); + } + + if (this.deleteWhenDone) + this.dispatch("delete-view"); + + delete this.deleteWhenDone; + } + else + { + /* We're ok to update this before the message, because the only thing + * that can happen is *disabling* of conference mode. + */ + this._updateConferenceMode(); + + if (!this.prefs["conference.enabled"]) + { + var msg = e.reason ? MSG_SOMEONE_LEFT_REASON : MSG_SOMEONE_LEFT; + var params = [e.user.unicodeName, e.channel.unicodeName, e.reason]; + this.display(getMsg(msg, params), "PART", e.user, this, e.tags); + } + + this.removeFromList(e.user); + } +} + +CIRCChannel.prototype.onKick = +function my_ckick (e) +{ + if (userIsMe (e.lamer)) + { + if (e.user) + { + this.display (getMsg(MSG_YOURE_GONE, + [e.lamer.unicodeName, e.channel.unicodeName, + e.user.unicodeName, e.reason]), + "KICK", e.user, this, e.tags); + } + else + { + this.display (getMsg(MSG_YOURE_GONE, + [e.lamer.unicodeName, e.channel.unicodeName, + MSG_SERVER, e.reason]), + "KICK", (void 0), this, e.tags); + } + + this._clearUserList(); + /* Try 1 re-join attempt if allowed. */ + if (this.prefs["autoRejoin"]) + this.join(this.mode.key); + } + else + { + var enforcerProper, enforcerNick; + if (e.user && userIsMe(e.user)) + { + enforcerProper = "YOU"; + enforcerNick = "ME!"; + } + else if (e.user) + { + enforcerProper = e.user.unicodeName; + enforcerNick = e.user.encodedName; + } + else + { + enforcerProper = MSG_SERVER; + enforcerNick = MSG_SERVER; + } + + this.display(getMsg(MSG_SOMEONE_GONE, + [e.lamer.unicodeName, e.channel.unicodeName, + enforcerProper, e.reason]), + "KICK", e.user, this, e.tags); + + this.removeFromList(e.lamer); + } + + this.removeUsers([e.lamer]); + this.updateHeader(); +} + +CIRCChannel.prototype.removeFromList = +function my_removeFromList(user) +{ + // Remove the user from the list and 'disconnect' the user from their entry: + var idx = user.chanListEntry.childIndex; + this.userList.childData.removeChildAtIndex(idx); + + delete user.chanListEntry._userObj; + delete user.chanListEntry; +} + +CIRCChannel.prototype.onChanMode = +function my_cmode (e) +{ + if (e.code == "MODE") + { + var msg = e.decodeParam(1); + for (var i = 2; i < e.params.length; i++) + msg += " " + e.decodeParam(i); + + var source = e.user ? e.user.unicodeName : e.source; + this.display(getMsg(MSG_MODE_CHANGED, [msg, source]), + "MODE", (e.user || null), this, e.tags); + } + else if ("pendingModeReply" in this) + { + var msg = e.decodeParam(3); + for (var i = 4; i < e.params.length; i++) + msg += " " + e.decodeParam(i); + + var view = ("messages" in this && this.messages) ? this : e.network; + view.display(getMsg(MSG_MODE_ALL, [this.unicodeName, msg]), "MODE", + undefined, undefined, e.tags); + delete this.pendingModeReply; + } + var updates = new Array(); + for (var u in e.usersAffected) + updates.push(e.usersAffected[u]); + this.updateUsers(updates); + + this.updateHeader(); + updateTitle(this); + if (client.currentObject == this) + updateUserList(); +} + +CIRCChannel.prototype.onNick = +function my_cnick (e) +{ + if (userIsMe (e.user)) + { + if (getTabForObject(this)) + { + this.displayHere(getMsg(MSG_NEWNICK_YOU, e.user.unicodeName), + "NICK", "ME!", e.user, e.tags); + } + this.parent.parent.updateHeader(); + } + else if (!this.prefs["conference.enabled"]) + { + this.display(getMsg(MSG_NEWNICK_NOTYOU, [e.oldNick, + e.user.unicodeName]), + "NICK", e.user, this, e.tags); + } + + this.updateUsers([e.user]); + if (client.currentObject == this) + updateUserList(); +} + +CIRCChannel.prototype.onQuit = +function my_cquit (e) +{ + if (userIsMe(e.user)) + { + /* I dont think this can happen */ + var pms = [e.user.unicodeName, e.server.parent.unicodeName, e.reason]; + this.display(getMsg(MSG_YOU_QUIT, pms),"QUIT", e.user, this, e.tags); + this._clearUserList(); + } + else + { + // See onPart for why this is ok before the message. + this._updateConferenceMode(); + + if (!this.prefs["conference.enabled"]) + { + this.display(getMsg(MSG_SOMEONE_QUIT, + [e.user.unicodeName, + e.server.parent.unicodeName, e.reason]), + "QUIT", e.user, this, e.tags); + } + } + + this.removeUsers([e.user]); + this.removeFromList(e.user); + + this.updateHeader(); +} + +CIRCChannel.prototype.doAutoPerform = +function my_cautoperform() +{ + var cmdary = client.prefs["autoperform.channel"].concat(this.prefs["autoperform"]); + for (var i = 0; i < cmdary.length; ++i) + { + if (cmdary[i][0] == "/") + this.dispatch(cmdary[i].substr(1)); + else + this.dispatch(cmdary[i]); + } +} + +CIRCChannel.prototype._clearUserList = +function _my_clearuserlist() +{ + if (this.userList && this.userList.childData && + this.userList.childData.childData) + { + this.userList.freeze(); + var len = this.userList.childData.childData.length; + while (len > 0) + { + var entry = this.userList.childData.childData[--len]; + this.userList.childData.removeChildAtIndex(len); + delete entry._userObj.chanListEntry; + delete entry._userObj; + } + this.userList.thaw(); + } +} + +CIRCUser.prototype.onInit = +function user_oninit () +{ + this.logFile = null; + this.lastShownAwayMessage = ""; +} + +CIRCUser.prototype.onPrivmsg = +function my_cprivmsg(e) +{ + var sourceObj = e.user; + var destObj = e.server.me; + var displayObj = this; + + if (!("messages" in this)) + { + var limit = client.prefs["newTabLimit"]; + if (limit == 0 || client.viewsArray.length < limit) + { + if (e.user != e.server.me) + { + openQueryTab(e.server, e.user.unicodeName); + } + else + { + // This is a self-message, i.e. we received a message that + // looks like it came from us. Display it accordingly. + sourceObj = e.server.me; + destObj = openQueryTab(e.server, e.params[1]); + displayObj = destObj; + } + } + } + + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + displayObj.display(e.decodeParam(2), "PRIVMSG", sourceObj, destObj, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCUser.prototype.onNick = +function my_unick (e) +{ + if (userIsMe(e.user)) + { + this.parent.parent.updateHeader(); + updateTitle(); + } + else if ("messages" in this && this.messages) + { + this.display(getMsg(MSG_NEWNICK_NOTYOU, [e.oldNick, e.user.unicodeName]), + "NICK", e.user, this, e.tags); + } + + this.updateHeader(); + var tab = getTabForObject(this); + if (tab) + tab.setAttribute("label", this.unicodeName); +} + +CIRCUser.prototype.onNotice = +function my_notice (e) +{ + var msg = e.decodeParam(2); + var displayMailto = client.prefs["munger.mailto"]; + + var ary = msg.match(/^\[([^ ]+)\]\s+/); + if (ary) + { + var channel = e.server.getChannel(ary[1]); + if (channel) + { + client.munger.getRule(".mailto").enabled = displayMailto; + channel.display(msg, "NOTICE", this, e.server.me, e.tags); + client.munger.getRule(".mailto").enabled = false; + return; + } + } + + var sourceObj = this; + var destObj = e.server.me; + var displayObj = this; + + if (e.user == e.server.me) + { + // This is a self-message, i.e. we received a message that + // looks like it came from us. Display it accordingly. + var sourceObj = e.server.me; + var destObj = e.server.addTarget(e.params[1]); + var displayObj = e.server.parent; + } + + client.munger.getRule(".mailto").enabled = displayMailto; + displayObj.display(msg, "NOTICE", sourceObj, destObj, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCUser.prototype.onCTCPAction = +function my_uaction(e) +{ + if (!("messages" in this)) + { + var limit = client.prefs["newTabLimit"]; + if (limit == 0 || client.viewsArray.length < limit) + openQueryTab(e.server, e.user.unicodeName); + } + + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.display(e.CTCPData, "ACTION", this, e.server.me, e.tags); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCUser.prototype.onUnknownCTCP = +function my_unkctcp (e) +{ + this.parent.parent.display (getMsg(MSG_UNKNOWN_CTCP, + [e.CTCPCode, e.CTCPData, + e.user.unicodeName]), + "BAD-CTCP", this, e.server.me, e.tags); +} + +function onDCCAutoAcceptTimeout(o, folder) +{ + // user may have already accepted or declined + if (o.state.state != DCC_STATE_REQUESTED) + return; + + if (o.TYPE == "IRCDCCChat") + { + o.accept(); + display(getMsg(MSG_DCCCHAT_ACCEPTED, o._getParams()), "DCC-CHAT"); + } + else + { + var dest, leaf, tries = 0; + while (true) + { + leaf = escapeFileName(o.filename); + if (++tries > 1) + { + // A file with the same name as the offered file already exists + // in the user's download folder. Add [x] before the extension. + // The extension is the last dot to the end of the string, + // unless it is one of the special-cased compression extensions, + // in which case the second to last dot is used. The second + // extension can only contain letters, to avoid mistakes like + // "patch-version1[2].0.gz". If no file extension is present, + // the [x] is just appended to the filename. + leaf = leaf.replace(/(\.[a-z]*\.(gz|bz2|z)|\.[^\.]*|)$/i, + "[" + tries + "]$&"); + } + + dest = getFileFromURLSpec(folder); + dest.append(leaf); + if (!dest.exists()) + break; + } + o.accept(dest); + display(getMsg(MSG_DCCFILE_ACCEPTED, o._getParams()), "DCC-FILE"); + } +} + +CIRCUser.prototype.onDCCChat = +function my_dccchat(e) +{ + if (!client.prefs["dcc.enabled"]) + return; + + var u = client.dcc.addUser(e.user, e.host); + var c = client.dcc.addChat(u, e.port); + + var str = MSG_DCCCHAT_GOT_REQUEST; + var cmds = getMsg(MSG_DCC_COMMAND_ACCEPT, "dcc-accept " + c.id) + " " + + getMsg(MSG_DCC_COMMAND_DECLINE, "dcc-decline " + c.id); + + var allowList = this.parent.parent.prefs["dcc.autoAccept.list"]; + for (var m = 0; m < allowList.length; ++m) + { + if (hostmaskMatches(e.user, getHostmaskParts(allowList[m]))) + { + var acceptDelay = client.prefs["dcc.autoAccept.delay"]; + if (acceptDelay == 0) + { + str = MSG_DCCCHAT_ACCEPTING_NOW; + } + else + { + str = MSG_DCCCHAT_ACCEPTING; + cmds = [(acceptDelay / 1000), cmds]; + } + setTimeout(onDCCAutoAcceptTimeout, acceptDelay, c); + break; + } + } + + client.munger.getRule(".inline-buttons").enabled = true; + this.parent.parent.display(getMsg(str, c._getParams().concat(cmds)), + "DCC-CHAT", undefined, undefined, e.tags); + client.munger.getRule(".inline-buttons").enabled = false; + + // Pass the event over to the DCC Chat object. + e.set = "dcc-chat"; + e.destObject = c; + e.destMethod = "onGotRequest"; +} + +CIRCUser.prototype.onDCCSend = +function my_dccsend(e) +{ + if (!client.prefs["dcc.enabled"]) + return; + + var u = client.dcc.addUser(e.user, e.host); + var f = client.dcc.addFileTransfer(u, e.port, e.file, e.size); + + var str = MSG_DCCFILE_GOT_REQUEST; + var cmds = getMsg(MSG_DCC_COMMAND_ACCEPT, "dcc-accept " + f.id) + " " + + getMsg(MSG_DCC_COMMAND_DECLINE, "dcc-decline " + f.id); + + var allowList = this.parent.parent.prefs["dcc.autoAccept.list"]; + for (var m = 0; m < allowList.length; ++m) + { + if (hostmaskMatches(e.user, getHostmaskParts(allowList[m]), + this.parent)) + { + var acceptDelay = client.prefs["dcc.autoAccept.delay"]; + if (acceptDelay == 0) + { + str = MSG_DCCFILE_ACCEPTING_NOW; + } + else + { + str = MSG_DCCFILE_ACCEPTING; + cmds = [(acceptDelay / 1000), cmds]; + } + setTimeout(onDCCAutoAcceptTimeout, acceptDelay, + f, this.parent.parent.prefs["dcc.downloadsFolder"]); + break; + } + } + + client.munger.getRule(".inline-buttons").enabled = true; + this.parent.parent.display(getMsg(str,[e.user.unicodeName, + e.host, e.port, e.file, + getSISize(e.size)].concat(cmds)), + "DCC-FILE", undefined, undefined, e.tags); + client.munger.getRule(".inline-buttons").enabled = false; + + // Pass the event over to the DCC File object. + e.set = "dcc-file"; + e.destObject = f; + e.destMethod = "onGotRequest"; +} + +CIRCUser.prototype.onDCCReject = +function my_dccreject(e) +{ + if (!client.prefs["dcc.enabled"]) + return; + + //FIXME: Uh... cope. // + + // Pass the event over to the DCC Chat object. + //e.set = "dcc-file"; + //e.destObject = f; + //e.destMethod = "onGotReject"; +} + +CIRCUser.prototype.doAutoPerform = +function my_autoperform() +{ + var cmdary = client.prefs["autoperform.user"].concat(this.prefs["autoperform"]); + for (var i = 0; i < cmdary.length; ++i) + { + if (cmdary[i][0] == "/") + this.dispatch(cmdary[i].substr(1)); + else + this.dispatch(cmdary[i]); + } +} + +CIRCDCCChat.prototype.onInit = +function my_dccinit(e) +{ +} + +CIRCDCCChat.prototype._getParams = +function my_dccgetparams() +{ + return [this.unicodeName, this.remoteIP, this.port]; +} + +CIRCDCCChat.prototype.onPrivmsg = +function my_dccprivmsg(e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.displayHere(toUnicode(e.line, this), "PRIVMSG", e.user, "ME!"); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCDCCChat.prototype.onCTCPAction = +function my_uaction(e) +{ + client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"]; + this.displayHere(e.CTCPData, "ACTION", e.user, "ME!"); + client.munger.getRule(".mailto").enabled = false; +} + +CIRCDCCChat.prototype.onUnknownCTCP = +function my_unkctcp(e) +{ + this.displayHere(getMsg(MSG_UNKNOWN_CTCP, [e.CTCPCode, e.CTCPData, + e.user.unicodeName]), + "BAD-CTCP", e.user, "ME!"); +} + +CIRCDCCChat.prototype.onConnect = +function my_dccconnect(e) +{ + playEventSounds("dccchat", "connect"); + this.displayHere(getMsg(MSG_DCCCHAT_OPENED, this._getParams()), "DCC-CHAT"); +} + +CIRCDCCChat.prototype.onAbort = +function my_dccabort(e) +{ + this.display(getMsg(MSG_DCCCHAT_ABORTED, this._getParams()), "DCC-CHAT"); +} + +CIRCDCCChat.prototype.onFail = +function my_dccfail(e) +{ + this.display(getMsg(MSG_DCCCHAT_FAILED, this._getParams()), "DCC-CHAT"); +} + +CIRCDCCChat.prototype.onDisconnect = +function my_dccdisconnect(e) +{ + playEventSounds("dccchat", "disconnect"); + this.display(getMsg(MSG_DCCCHAT_CLOSED, this._getParams()), "DCC-CHAT"); +} + + +CIRCDCCFileTransfer.prototype.onInit = +function my_dccfileinit(e) +{ + this.busy = false; + updateProgress(); +} + +CIRCDCCFileTransfer.prototype._getParams = +function my_dccfilegetparams() +{ + var dir = MSG_UNKNOWN; + + if (this.state.dir == DCC_DIR_GETTING) + dir = MSG_DCCLIST_FROM; + + if (this.state.dir == DCC_DIR_SENDING) + dir = MSG_DCCLIST_TO; + + return [this.filename, dir, this.unicodeName, + this.remoteIP, this.port]; +} + +CIRCDCCFileTransfer.prototype.onConnect = +function my_dccfileconnect(e) +{ + this.displayHere(getMsg(MSG_DCCFILE_OPENED, this._getParams()), "DCC-FILE"); + this.busy = true; + this.speed = 0; + updateProgress(); + this._lastUpdate = new Date(); + this._lastPosition = 0; + this._lastSpeedTime = new Date(); +} + +CIRCDCCFileTransfer.prototype.onProgress = +function my_dccfileprogress(e) +{ + var now = new Date(); + var pcent = this.progress; + + var tab = getTabForObject(this); + + // If we've moved 100KiB or waited 10s, update the progress bar. + if ((this.position > this._lastPosition + 102400) || + (now - this._lastUpdate > 10000)) + { + updateProgress(); + updateTitle(); + + if (tab) + tab.setAttribute("label", this.viewName + " (" + pcent + "%)"); + + var change = (this.position - this._lastPosition); + var speed = change / ((now - this._lastSpeedTime) / 1000); // B/s + this._lastSpeedTime = now; + + /* Use an average of the last speed, and this speed, so we get a little + * smoothing to it. + */ + this.speed = (this.speed + speed) / 2; + this.updateHeader(); + this._lastPosition = this.position; + } + + // If it's also been 10s or more since we last displayed a msg... + if (now - this._lastUpdate > 10000) + { + this._lastUpdate = now; + + var args = [pcent, getSISize(this.position), getSISize(this.size), + getSISpeed(this.speed)]; + + // We supress this message if the view is hidden. + if (tab) + this.displayHere(getMsg(MSG_DCCFILE_PROGRESS, args), "DCC-FILE"); + } +} + +CIRCDCCFileTransfer.prototype.onAbort = +function my_dccfileabort(e) +{ + this.busy = false; + updateProgress(); + updateTitle(); + this.display(getMsg(MSG_DCCFILE_ABORTED, this._getParams()), "DCC-FILE"); +} + +CIRCDCCFileTransfer.prototype.onFail = +function my_dccfilefail(e) +{ + this.busy = false; + updateProgress(); + updateTitle(); + this.display(getMsg(MSG_DCCFILE_FAILED, this._getParams()), "DCC-FILE"); +} + +CIRCDCCFileTransfer.prototype.onDisconnect = +function my_dccfiledisconnect(e) +{ + this.busy = false; + updateProgress(); + this.updateHeader(); + updateTitle(); + + var msg, tab = getTabForObject(this); + if (tab) + tab.setAttribute("label", this.viewName + " (DONE)"); + + if (this.state.dir == DCC_DIR_GETTING) + { + var localURL = getURLSpecFromFile(this.localPath); + var cmd = "dcc-show-file " + localURL; + var msgId = (client.platform == "Mac") ? MSG_DCCFILE_CLOSED_SAVED_MAC : + MSG_DCCFILE_CLOSED_SAVED; + msg = getMsg(msgId, this._getParams().concat(localURL, cmd)); + } + else + { + msg = getMsg(MSG_DCCFILE_CLOSED_SENT, this._getParams()); + } + client.munger.getRule(".inline-buttons").enabled = true; + this.display(msg, "DCC-FILE"); + client.munger.getRule(".inline-buttons").enabled = false; +} + +var CopyPasteHandler = new Object(); + +CopyPasteHandler.allowDrop = +CopyPasteHandler.allowStartDrag = +CopyPasteHandler.onCopyOrDrag = +function phand_bogus() +{ + return true; +} + +CopyPasteHandler.onPasteOrDrop = +function phand_onpaste(e, data) +{ + // XXXbug 329487: The effect of onPasteOrDrop's return value is actually the + // exact opposite of the definition in the IDL. + + // Don't mess with the multiline box at all. + if (client.prefs["multiline"]) + return true; + + var str = new Object(); + var strlen = new Object(); + data.getTransferData("text/unicode", str, strlen); + str.value.QueryInterface(Components.interfaces.nsISupportsString); + str.value.data = str.value.data.replace(/(^\s*[\r\n]+|[\r\n]+\s*$)/g, ""); + + // XXX part of what follows is a very ugly hack to make links (with a title) + // not open the multiline box. We 'should' be able to ask the transferable + // what flavours it supports, but testing showed that by the time we can ask + // for that info, it's forgotten about everything apart from text/unicode. + var lines = str.value.data.split("\n"); + var m = lines[0].match(client.linkRE); + + if ((str.value.data.indexOf("\n") == -1) || + (m && (m[0] == lines[0]) && (lines.length == 2))) + { + // If, after stripping leading/trailing empty lines, the string is a + // single line, or it's a link with a title, put it back in + // the transferable and return. + data.setTransferData("text/unicode", str.value, + str.value.data.length * 2); + return true; + } + + // If it's a drop, move the text cursor to the mouse position. + if (e && ("rangeOffset" in e)) + client.input.setSelectionRange(e.rangeOffset, e.rangeOffset); + + str = client.input.value.substr(0, client.input.selectionStart) + + str.value.data + client.input.value.substr(client.input.selectionEnd); + client.prefs["multiline"] = true; + // We want to auto-collapse after send, so the user is not thrown off by the + // "strange" input box if they didn't specifically ask for it: + client.multiLineForPaste = true; + client.input.value = str; + return false; +} + +CopyPasteHandler.QueryInterface = +function phand_qi(iid) +{ + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIClipboardDragDropHooks)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; +} + +function UserEntry(userObj, channelListShare) +{ + var self = this; + function getUName() + { + return userObj.unicodeName; + }; + function getSortFn() + { + if (client.prefs["sortUsersByMode"]) + return ule_sortByMode; + return ule_sortByName; + }; + + // This object is used to represent a user in the userlist. To work with our + // JS tree view, it needs a bunch of stuff that is set through the + // constructor and the prototype (see also a couple of lines down). Here we + // call the original constructor to do some work for us: + XULTreeViewRecord.call(this, channelListShare); + + // This magic function means the unicodeName is used for display: + this.setColumnPropertyName("usercol", getUName); + + // We need this for sorting by mode (op, hop, voice, etc.) + this._userObj = userObj; + + // When the user leaves, we need to have the entry so we can remove it: + userObj.chanListEntry = this; + + // Gross hack: we set up the sort function by getter so we don't have to go + // back (array sort -> xpc -> our pref lib -> xpc -> pref interfaces) for + // every bloody compare. Now it will be a function that doesn't need prefs + // after being retrieved, which is much much faster. + this.__defineGetter__("sortCompare", getSortFn); +} + +// See explanation in the constructor. +UserEntry.prototype = XULTreeViewRecord.prototype; + +function ule_sortByName(a, b) +{ + if (a._userObj.unicodeName == b._userObj.unicodeName) + return 0; + var aName = a._userObj.unicodeName.toLowerCase(); + var bName = b._userObj.unicodeName.toLowerCase(); + return (aName < bName ? -1 : 1); +} + +function ule_sortByMode(a, b) +{ + if (a._userObj.sortName == b._userObj.sortName) + return 0; + var aName = a._userObj.sortName.toLowerCase(); + var bName = b._userObj.sortName.toLowerCase(); + return (aName < bName ? -1 : 1); +} diff --git a/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js new file mode 100644 index 0000000000..3116d9a2f3 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js @@ -0,0 +1,97 @@ +/* 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 client; +var plugin; + +function onLoad() +{ + client = window.arguments[0]; + client.installPluginDialog = window; + window.getMsg = client.messageManager.getMsg; + window.MSG_ALERT = client.mainWindow.MSG_ALERT; + + hookEvent("chk-name-auto", "command", changeAutoName); + hookEvent("txt-source", "input", sourceChange); + hookEvent("btn-browse", "command", browseForSource); + + // Center on CZ: + var ow = client.mainWindow; + window.sizeToContent(); + window.moveTo(ow.screenX + Math.max((ow.outerWidth - window.outerWidth ) / 2, 0), + ow.screenY + Math.max((ow.outerHeight - window.outerHeight) / 2, 0)); +} + +function changeAutoName(event) +{ + var useAutoName = document.getElementById("chk-name-auto"); + var pluginName = document.getElementById("txt-name"); + if (useAutoName.checked) + { + pluginName.setAttribute("disabled", "true"); + sourceChange(null); + } + else + { + pluginName.removeAttribute("disabled"); + } +} + +function sourceChange(event) +{ + var useAutoName = document.getElementById("chk-name-auto"); + var pluginName = document.getElementById("txt-name"); + var sourceLoc = document.getElementById("txt-source"); + + if (useAutoName.checked) + { + var ary = sourceLoc.value.match(/([^\/]+?)(\..{0,3}){0,2}$/); + pluginName.value = (ary ? ary[1] : sourceLoc.value); + } +} + +function browseForSource(event) +{ + var rv = pickOpen(client.mainWindow.MSG_INSTALL_PLUGIN_SELECT_SOURCE, + "*.js;*.zip;*.jar"); + + if (("file" in rv) && rv.file) + { + rv.path = rv.file.path; + rv.spec = rv.picker.fileURL.spec; + } + + if (rv.reason == 0) + { + var sourceLoc = document.getElementById("txt-source"); + sourceLoc.value = rv.spec; + sourceChange(null); + } +} + +function doOK() +{ + var pluginName = document.getElementById("txt-name"); + var pluginSource = document.getElementById("txt-source"); + if (!pluginName.value) + { + alert(client.mainWindow.MSG_INSTALL_PLUGIN_ERR_SPEC_NAME); + return false; + } + + client.dispatch("install-plugin", {name: pluginName.value, + url: pluginSource.value}); + delete client.installPluginDialog; +} + +function doCancel() +{ + delete client.installPluginDialog; +} + +function hookEvent(id, event, handler) +{ + var item = document.getElementById(id); + item.addEventListener(event, handler, false); +} diff --git a/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul new file mode 100644 index 0000000000..90de4f6105 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE window SYSTEM "chrome://chatzilla/locale/install-plugin.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/skin/install-plugin.css" type="text/css"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" onload="onLoad();" + title="&windowtitle;" + ondialogaccept="return doOK()" + ondialogcancel="doCancel()"> + <script src="install-plugin.js"/> + <script src="../lib/js/file-utils.js"/> + <script src="../lib/js/utils.js"/> + <grid> + <columns><column/><column flex="1"/></columns> + <rows> + <row align="center"> + <label id="lbl-source" accesskey="&source.accesskey;" + control="txt-source" value="&source.label;"/> + <textbox id="txt-source"/> + <button id="btn-browse" label="&browse.label;" + accesskey="&browse.accesskey;"/> + </row> + <row align="center"> + <label id="lbl-name" control="txt-name" + value="&name.label;" accesskey="&name.accesskey;"/> + <textbox id="txt-name" disabled="true"/> + </row> + <row align="center"> + <spacer/> + <checkbox id="chk-name-auto" checked="true" + label="&name.autopick.label;" + accesskey="&name.autopick.accesskey;"/> + </row> + </rows> + </grid> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/menus.js b/comm/suite/chatzilla/xul/content/menus.js new file mode 100644 index 0000000000..1fecac268d --- /dev/null +++ b/comm/suite/chatzilla/xul/content/menus.js @@ -0,0 +1,513 @@ +/* -*- 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 initMenus() +{ + function isMotif(name) + { + return "client.prefs['motif.current'] == " + + "client.prefs['motif." + name + "']"; + }; + + function isFontFamily(name) + { + return "cx.sourceObject.prefs['font.family'] == '" + name + "'"; + }; + + function isFontFamilyCustom() + { + return "!cx.sourceObject.prefs['font.family']." + + "match(/^(default|(sans-)?serif|monospace)$/)"; + }; + + function isFontSize(size) + { + return "cx.fontSize == cx.fontSizeDefault + " + size; + }; + + function isFontSizeCustom() + { + // It's "custom" if it's set (non-zero/not default), not the default + // size (medium) and not +/-2 (small/large). + return "'fontSize' in cx && cx.fontSize != 0 && " + + "cx.fontSizeDefault != cx.fontSize && " + + "Math.abs((cx.fontSizeDefault - cx.fontSize) / 2) != 1"; + }; + + function onMenuCommand(event, window) + { + var commandName = event.originalTarget.getAttribute("commandname"); + var params = new Object(); + if ("cx" in client.menuManager && client.menuManager.cx) + params = client.menuManager.cx; + params.sourceWindow = window; + params.source = "menu"; + params.shiftKey = event.shiftKey; + + dispatch(commandName, params, true); + + delete client.menuManager.cx; + }; + + client.onMenuCommand = onMenuCommand; + client.menuSpecs = new Object(); + var menuManager = new MenuManager(client.commandManager, + client.menuSpecs, + getCommandContext, + "client.onMenuCommand(event, window);"); + client.menuManager = menuManager; + + client.menuSpecs["maintoolbar"] = { + items: + [ + ["disconnect"], + ["quit"], + ["part"] + ] + }; + + // OS values + var Win = "(client.platform == 'Windows')"; + var NotWin = "(client.platform != 'Windows')"; + var Linux = "(client.platform == 'Linux')"; + var NotLinux = "(client.platform != 'Linux')"; + var Mac = "(client.platform == 'Mac')"; + var NotMac = "(client.platform != 'Mac')"; + + // IRC specific values + var ViewClient = "(cx.TYPE == 'IRCClient')"; + var ViewNetwork = "(cx.TYPE == 'IRCNetwork')"; + var ViewChannel = "(cx.TYPE == 'IRCChannel')"; + var ViewUser = "(cx.TYPE == 'IRCUser')"; + var ViewDCC = "(cx.TYPE.substr(0, 6) == 'IRCDCC')"; + + // IRC specific combinations + var ChannelActive = "(" + ViewChannel + " and cx.channel.active)"; + var ChannelInactive = "(" + ViewChannel + " and !cx.channel.active)"; + var DCCActive = "(" + ViewDCC + " and cx.sourceObject.isActive())"; + var NetConnected = "(cx.network and cx.network.isConnected())"; + var NetDisconnected = "(cx.network and !cx.network.isConnected())"; + + client.menuSpecs["mainmenu:chatzilla"] = { + label: MSG_MNU_CHATZILLA, + accesskey: getAccessKeyForMenu('MSG_MNU_CHATZILLA'), + getContext: getDefaultContext, + items: + [ + ["cmd-prefs"], + ["install-plugin"], + ["goto-startup"], + ["-"], + ["print"], + ["save"], + ["-", {visibleif: NotMac}], + ["exit", {visibleif: Win}], + ["quit", {visibleif: NotMac + " and " + NotWin}] + ] + }; + + client.menuSpecs["mainmenu:irc"] = { + label: MSG_MNU_IRC, + accesskey: getAccessKeyForMenu('MSG_MNU_IRC'), + getContext: getDefaultContext, + items: + [ + ["join"], + ["-"], + ["edit-networks"], + ["-"], + [">popup:views"], + [">popup:nickname"], + ["-"], + ["clear-view"], + ["hide-view", {enabledif: "client.viewsArray.length > 1"}], + ["toggle-oas", + {type: "checkbox", + checkedif: "isStartupURL(cx.sourceObject.getURL())"}], + ["-"], + ["leave", {visibleif: ChannelActive}], + ["rejoin", {visibleif: ChannelInactive}], + ["dcc-close", {visibleif: DCCActive}], + ["delete-view", {visibleif: "!" + ChannelActive + " and !" + DCCActive}], + ["disconnect", {visibleif: NetConnected}], + ["reconnect", {visibleif: NetDisconnected}], + ["-"], + ["toggle-text-dir"] + ] + }; + + client.menuSpecs["popup:views"] = { + label: MSG_MNU_VIEWS, + accesskey: getAccessKeyForMenu('MSG_MNU_VIEWS'), + getContext: getViewsContext, + items: + [ + ["goto-url", {type: "radio", + checkedif: "cx.url == cx.sourceObject.getURL()", + repeatfor: "cx.views", + repeatgroup: "item.group", + repeatmap: "cx.url = item.url; cx.label = item.label"}] + ] + }; + + client.menuSpecs["mainmenu:edit"] = { + label: MSG_MNU_EDIT, + accesskey: getAccessKeyForMenu('MSG_MNU_EDIT'), + getContext: getDefaultContext, + items: + [ + ["cmd-undo", {enabledif: "getCommandEnabled('cmd_undo')"}], + ["cmd-redo", {enabledif: "getCommandEnabled('cmd_redo')"}], + ["-"], + ["cmd-cut", {enabledif: "getCommandEnabled('cmd_cut')"}], + ["cmd-copy", {enabledif: "getCommandEnabled('cmd_copy')"}], + ["cmd-paste", {enabledif: "getCommandEnabled('cmd_paste')"}], + ["cmd-delete", {enabledif: "getCommandEnabled('cmd_delete')"}], + ["-"], + ["cmd-selectall", {enabledif: "getCommandEnabled('cmd_selectAll')"}], + ["-"], + ["find"], + ["find-again", {enabledif: "canFindAgainInPage()"}], + ["-"], + ["cmd-mozilla-prefs"] + ] + }; + + client.menuSpecs["popup:motifs"] = { + label: MSG_MNU_MOTIFS, + accesskey: getAccessKeyForMenu('MSG_MNU_MOTIFS'), + items: + [ + ["motif-dark", + {type: "checkbox", + checkedif: isMotif("dark")}], + ["motif-light", + {type: "checkbox", + checkedif: isMotif("light")}], + ] + }; + + client.menuSpecs["mainmenu:view"] = { + label: MSG_MNU_VIEW, + accesskey: getAccessKeyForMenu('MSG_MNU_VIEW'), + getContext: getDefaultContext, + items: + [ + ["tabstrip", + {type: "checkbox", + checkedif: "isVisible('view-tabs')"}], + ["header", + {type: "checkbox", + checkedif: "cx.sourceObject.prefs['displayHeader']"}], + ["userlist", + {type: "checkbox", + checkedif: "isVisible('user-list-box')"}], + ["statusbar", + {type: "checkbox", + checkedif: "isVisible('status-bar')"}], + ["-"], + [">popup:motifs"], + [">popup:fonts"], + ["-"], + ["toggle-ccm", + {type: "checkbox", + checkedif: "client.prefs['collapseMsgs']"}], + ["toggle-copy", + {type: "checkbox", + checkedif: "client.prefs['copyMessages']"}], + ["toggle-timestamps", + {type: "checkbox", + checkedif: "cx.sourceObject.prefs['timestamps']"}] + ] + }; + + /* Mac expects a help menu with this ID, and there is nothing we can do + * about it. */ + client.menuSpecs["mainmenu:help"] = { + label: MSG_MNU_HELP, + accesskey: getAccessKeyForMenu('MSG_MNU_HELP'), + domID: "menu_Help", + items: + [ + ["-"], + ["homepage"], + ["faq"], + ["-"], + ["about", {id: "aboutName"}] + ] + }; + + client.menuSpecs["popup:fonts"] = { + label: MSG_MNU_FONTS, + accesskey: getAccessKeyForMenu('MSG_MNU_FONTS'), + getContext: getFontContext, + items: + [ + ["font-size-bigger", {}], + ["font-size-smaller", {}], + ["-"], + ["font-size-default", + {type: "checkbox", checkedif: "!cx.fontSize"}], + ["font-size-small", + {type: "checkbox", checkedif: isFontSize(-2)}], + ["font-size-medium", + {type: "checkbox", checkedif: isFontSize(0)}], + ["font-size-large", + {type: "checkbox", checkedif: isFontSize(+2)}], + ["font-size-other", + {type: "checkbox", checkedif: isFontSizeCustom()}], + ["-"], + ["font-family-default", + {type: "checkbox", checkedif: isFontFamily("default")}], + ["font-family-serif", + {type: "checkbox", checkedif: isFontFamily("serif")}], + ["font-family-sans-serif", + {type: "checkbox", checkedif: isFontFamily("sans-serif")}], + ["font-family-monospace", + {type: "checkbox", checkedif: isFontFamily("monospace")}], + ["font-family-other", + {type: "checkbox", checkedif: isFontFamilyCustom()}] + ] + }; + + // Me is op. + var isop = "(cx.channel.iAmOp()) && "; + // Me is op or half-op. + var isopish = "(cx.channel.iAmOp() || cx.channel.iAmHalfOp()) && "; + // Server has half-ops. + var shop = "(cx.server.supports.prefix.indexOf('h') > 0) && "; + // User is Me or Me is op. + var isoporme = "((cx.user == cx.server.me) || cx.channel.iAmOp()) && "; + + client.menuSpecs["popup:opcommands"] = { + label: MSG_MNU_OPCOMMANDS, + accesskey: getAccessKeyForMenu('MSG_MNU_OPCOMMANDS'), + items: + [ + ["op", {visibleif: isop + "!cx.user.isOp"}], + ["deop", {visibleif: isop + "cx.user.isOp"}], + ["hop", {visibleif: isop + "!cx.user.isHalfOp"}], + ["dehop", {visibleif: isoporme + "cx.user.isHalfOp"}], + ["voice", {visibleif: isopish + "!cx.user.isVoice"}], + ["devoice", {visibleif: isopish + "cx.user.isVoice"}], + ["-"], + ["ban", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}], + ["unban", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}], + ["kick", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}], + ["kick-ban", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}] + ] + }; + + + client.menuSpecs["popup:usercommands"] = { + label: MSG_MNU_USERCOMMANDS, + accesskey: getAccessKeyForMenu('MSG_MNU_USERCOMMANDS'), + items: + [ + ["query", {visibleif: "cx.channel && cx.user"}], + ["whois", {visibleif: "cx.user"}], + ["whowas", {visibleif: "cx.nickname && !cx.user"}], + ["ping", {visibleif: "cx.user"}], + ["time", {visibleif: "cx.user"}], + ["version", {visibleif: "cx.user"}], + ["-", {visibleif: "cx.user"}], + ["dcc-chat", {visibleif: "cx.user"}], + ["dcc-send", {visibleif: "cx.user"}], + ] + }; + + + client.menuSpecs["context:userlist"] = { + getContext: getUserlistContext, + items: + [ + ["toggle-usort", {type: "checkbox", + checkedif: "client.prefs['sortUsersByMode']"}], + ["toggle-umode", {type: "checkbox", + checkedif: "client.prefs['showModeSymbols']"}], + ["-", {visibleif: "cx.nickname"}], + ["label-user", {visibleif: "cx.nickname && (cx.userCount == 1)", + header: true}], + ["label-user-multi", {visibleif: "cx.nickname && (cx.userCount != 1)", + header: true}], + [">popup:opcommands", {visibleif: "cx.nickname", + enabledif: isopish + "true"}], + [">popup:usercommands", {visibleif: "cx.nickname", + enabledif: "cx.userCount == 1"}], + ] + }; + + var urlenabled = "has('url')"; + var urlexternal = "has('url') && cx.url.search(/^ircs?:/i) == -1"; + var textselected = "getCommandEnabled('cmd_copy')"; + + client.menuSpecs["context:messages"] = { + getContext: getMessagesContext, + items: + [ + ["goto-url", {visibleif: urlenabled}], + ["goto-url-newwin", {visibleif: urlexternal}], + ["goto-url-newtab", {visibleif: urlexternal}], + ["cmd-copy-link-url", {visibleif: urlenabled}], + ["cmd-copy", {visibleif: "!" + urlenabled, enabledif: textselected }], + ["cmd-selectall", {visibleif: "!" + urlenabled }], + ["websearch", {visibleif: textselected}], + ["-", {visibleif: "cx.nickname"}], + ["label-user", {visibleif: "cx.nickname", header: true}], + [">popup:opcommands", {visibleif: "cx.channel && cx.nickname", + enabledif: isopish + "cx.user"}], + [">popup:usercommands", {visibleif: "cx.nickname"}], + ["-"], + ["clear-view"], + ["hide-view", {enabledif: "client.viewsArray.length > 1"}], + ["toggle-oas", + {type: "checkbox", + checkedif: "isStartupURL(cx.sourceObject.getURL())"}], + ["-"], + ["leave", {visibleif: ChannelActive}], + ["rejoin", {visibleif: ChannelInactive}], + ["dcc-close", {visibleif: DCCActive}], + ["delete-view", {visibleif: "!" + ChannelActive + " and !" + DCCActive}], + ["disconnect", {visibleif: NetConnected}], + ["reconnect", {visibleif: NetDisconnected}], + ["-"], + ["toggle-text-dir"] + ] + }; + + client.menuSpecs["context:tab"] = { + getContext: getTabContext, + items: + [ + ["clear-view"], + ["hide-view", {enabledif: "client.viewsArray.length > 1"}], + ["toggle-oas", + {type: "checkbox", + checkedif: "isStartupURL(cx.sourceObject.getURL())"}], + ["-"], + ["leave", {visibleif: ChannelActive}], + ["rejoin", {visibleif: ChannelInactive}], + ["dcc-close", {visibleif: DCCActive}], + ["delete-view", {visibleif: "!" + ChannelActive + " and !" + DCCActive}], + ["disconnect", {visibleif: NetConnected}], + ["reconnect", {visibleif: NetDisconnected}], + ["-"], + ["rename"], + ["-"], + ["toggle-text-dir"] + ] + }; + + client.menuSpecs["context:edit"] = { + getContext: getDefaultContext, + items: + [ + ["cmd-undo", {enabledif: "getCommandEnabled('cmd_undo')"}], + ["-"], + ["cmd-cut", {enabledif: "getCommandEnabled('cmd_cut')"}], + ["cmd-copy", {enabledif: "getCommandEnabled('cmd_copy')"}], + ["cmd-paste", {enabledif: "getCommandEnabled('cmd_paste')"}], + ["cmd-delete", {enabledif: "getCommandEnabled('cmd_delete')"}], + ["-"], + ["cmd-selectall", {enabledif: "getCommandEnabled('cmd_selectAll')"}] + ] + } + + // Gross hacks to figure out if we're away: + var netAway = "cx.network.prefs['away']"; + var cliAway = "client.prefs['away']"; + var awayCheckNet = "(cx.network and (" + netAway + " == item.message))"; + var awayCheckCli = "(!cx.network and (" + cliAway + " == item.message))"; + var awayChecked = awayCheckNet + " or " + awayCheckCli; + var areBack = "(cx.network and !" + netAway + ") or " + + "(!cx.network and !" + cliAway + ")"; + + client.menuSpecs["mainmenu:nickname"] = { + label: client.prefs["nickname"], + domID: "server-nick", + getContext: getDefaultContext, + items: + [ + ["nick"], + ["-"], + ["back", {type: "checkbox", checkedif: areBack}], + ["away", {type: "checkbox", + checkedif: awayChecked, + repeatfor: "client.awayMsgs", + repeatmap: "cx.reason = item.message" }], + ["-"], + ["custom-away"] + ] + }; + + client.menuSpecs["popup:nickname"] = { + label: MSG_STATUS, + accesskey: getAccessKeyForMenu('MSG_STATUS'), + getContext: getDefaultContext, + items: client.menuSpecs["mainmenu:nickname"].items + }; + +} + +function createMenus() +{ + client.menuManager.createMenus(document, "mainmenu"); + client.menuManager.createContextMenus(document); +} + +function getCommandContext (id, event) +{ + var cx = { originalEvent: event }; + + if (id in client.menuSpecs) + { + if ("getContext" in client.menuSpecs[id]) + cx = client.menuSpecs[id].getContext(cx); + else if ("cx" in client.menuManager) + { + //dd ("using existing context"); + cx = client.menuManager.cx; + } + else + { + //no context. + } + } + else + { + dd ("getCommandContext: unknown menu id " + id); + } + + if (typeof cx == "object") + { + if (!("menuManager" in cx)) + cx.menuManager = client.menuManager; + if (!("contextSource" in cx)) + cx.contextSource = id; + if ("dbgContexts" in client && client.dbgContexts) + dd ("context '" + id + "'\n" + dumpObjectTree(cx)); + } + + return cx; +} + +/** + * Gets an accesskey for the menu with label string ID labelString. + * At first, we attempt to extract it from the label string, otherwise + * we fall back to using a separate string. + * + * @param labelString the id for the locale string corresponding to the label + * @return the accesskey for the menu. + */ +function getAccessKeyForMenu(labelString) +{ + var rv = getAccessKey(window[labelString]); + if (!rv) + rv = window[labelString + "_ACCESSKEY"] || ""; + return rv; +} + + diff --git a/comm/suite/chatzilla/xul/content/menus.xul b/comm/suite/chatzilla/xul/content/menus.xul new file mode 100644 index 0000000000..3cf2e34d13 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/menus.xul @@ -0,0 +1,97 @@ +<?xml version="1.0"?> + +<!-- + - + - 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/. --> + +<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/chatzilla.dtd" > + +<overlay id="chatzilla-menu-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <overlaytarget id="menu-overlay-target"> + + <!-- parents for the command manager-managed objects --> + <keyset id="dynamic-keys"/> + <popupset id="dynamic-popups"/> + + <!-- tooltip thingy --> + <tooltip id="html-tooltip-node" onpopupshowing="return onTooltip(event);"/> + <tooltip id="xul-tooltip-node" onpopupshowing="return onTooltip(event);"/> + + <!-- Commands --> + <commandset id="chatzilla-commands"> + + <!-- Edit commands --> + <commandset id="selectEditMenuItems"/> + <commandset id="globalEditMenuItems"/> + <commandset id="undoEditMenuItems"/> + <commandset id="clipboardEditMenuItems"/> + <command id="cmd_undo"/> + <command id="cmd_redo"/> + <command id="cmd_cut"/> + <command id="cmd_copy"/> + <command id="cmd_paste"/> + <command id="cmd_delete"/> + <command id="cmd_selectAll"/> + + <!-- Tasks commands, from overlay --> + <commandset id="tasksCommands"/> + </commandset> + + <!-- Keys --> + + <keyset id="chatzillaKeys"> + <key id="key:reloadui" modifiers="accel alt" key="R" + oncommand="if (typeof cmdReloadUI =='function') cmdReloadUI(); else window.location.href = window.location.href;"/> + + <!-- Edit keys --> + <key id="key_undo"/> + <key id="key_redo"/> + <key id="key_cut"/> + <key id="key_copy"/> + <key id="key_paste"/> + <key id="key_delete"/> + <key id="key_selectAll"/> + + <!-- Tasks keys, from overlay --> + <keyset id="tasksKeys"/> + </keyset> + + <!-- Main menu bar --> + <toolbox flex="1" id="main-toolbox"> + <menubar id="mainmenu" persist="collapsed" + grippytooltiptext="&Menubar.tooltip;"> + + <!-- ChatZilla menu placeholder, see menus.js --> + <menu id="mainmenu:chatzilla"><menupopup/></menu> + + <!-- IRC menu placeholder, see menus.js --> + <menu id="mainmenu:irc"><menupopup/></menu> + + <!-- Edit menu placeholder, see menus.js --> + <menu id="mainmenu:edit"><menupopup/></menu> + + <!-- View menu placeholder, see menus.js --> + <menu id="mainmenu:view"><menupopup/></menu> + + <!-- Tasks menu --> + <menu id="tasksMenu"/> + + <!-- Window menu --> + <menu id="windowMenu"/> + + <!-- Help menu --> + <!-- Mac expects a help menu with this ID, and there is nothing we can + do about it. --> + <menu id="menu_Help"/> + </menubar> + + </toolbox> + + </overlaytarget> + +</overlay> + diff --git a/comm/suite/chatzilla/xul/content/messages.js b/comm/suite/chatzilla/xul/content/messages.js new file mode 100644 index 0000000000..b0eb1822ee --- /dev/null +++ b/comm/suite/chatzilla/xul/content/messages.js @@ -0,0 +1,104 @@ +/* -*- 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 initMessages() +{ + var path = "chrome://chatzilla/locale/chatzilla.properties"; + + client.messageManager = new MessageManager(client.entities); + client.messageManager.enableHankakuToZenkaku = true; + client.messageManager.loadBrands(); + client.defaultBundle = client.messageManager.addBundle(path); + + client.viewName = client.unicodeName = MSG_CLIENT_NAME; + client.responseCodeMap = + { + "HELLO": MSG_RSP_HELLO, + "HELP" : MSG_RSP_HELP, + "USAGE": MSG_RSP_USAGE, + "ERROR": MSG_RSP_ERROR, + "WARNING": MSG_RSP_WARN, + "INFO": MSG_RSP_INFO, + "EVAL-IN": MSG_RSP_EVIN, + "EVAL-OUT": MSG_RSP_EVOUT, + "DISCONNECT": MSG_RSP_DISCONNECT, + "JOIN": "-->|", + "PART": "<--|", + "QUIT": "|<--", + "NICK": "=-=", + "TOPIC": "=-=", + "KICK": "=-=", + "MODE": "=-=", + "END_STATUS": "---", + "DCC-CHAT": "[DCC]", + "DCC-FILE": "[DCC]", + "315": "---", /* end of WHO */ + "318": "---", /* end of WHOIS */ + "366": "---", /* end of NAMES */ + "376": "---" /* end of MOTD */ + }; +} + +function checkCharset(charset) +{ + return client.messageManager.checkCharset(charset); +} + +function toUnicode (msg, charsetOrView) +{ + if (!msg) + return msg; + + var charset; + if (typeof charsetOrView == "object") + charset = charsetOrView.prefs["charset"]; + else if (typeof charsetOrView == "string") + charset = charsetOrView; + else + charset = client.currentObject.prefs["charset"]; + + return client.messageManager.toUnicode(msg, charset); +} + +function fromUnicode (msg, charsetOrView) +{ + if (!msg) + return msg; + + var charset; + if (typeof charsetOrView == "object") + charset = charsetOrView.prefs["charset"]; + else if (typeof charsetOrView == "string") + charset = charsetOrView; + else + charset = client.currentObject.prefs["charset"]; + + return client.messageManager.fromUnicode(msg, charset); +} + +function getMsg(msgName, params, deflt) +{ + return client.messageManager.getMsg(msgName, params, deflt); +} + +function getMsgFrom(bundle, msgName, params, deflt) +{ + return client.messageManager.getMsgFrom(bundle, msgName, params, deflt); +} + +/* message types, don't localize */ +const MT_ATTENTION = "ATTENTION"; +const MT_ERROR = "ERROR"; +const MT_HELLO = "HELLO"; +const MT_HELP = "HELP"; +const MT_MODE = "MODE"; +const MT_WARN = "WARNING"; +const MT_INFO = "INFO"; +const MT_USAGE = "USAGE"; +const MT_STATUS = "STATUS"; +const MT_EVALIN = "EVAL-IN"; +const MT_EVALOUT = "EVAL-OUT"; + diff --git a/comm/suite/chatzilla/xul/content/mungers.js b/comm/suite/chatzilla/xul/content/mungers.js new file mode 100644 index 0000000000..dce8e9980f --- /dev/null +++ b/comm/suite/chatzilla/xul/content/mungers.js @@ -0,0 +1,904 @@ +/* -*- 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/. */ + +/* This file contains the munger functions and rules used by ChatZilla. + * It's generally a bad idea to call munger functions inside ChatZilla for + * anything but munging (chat) output. + */ + +function initMunger() +{ + /* linkRE: the general URL linkifier regular expression: + * + * - start with whitespace, non-word, or begining-of-line + * - then match: + * - EITHER scheme (word + hyphen), colon, then lots of non-whitespace + * - OR "www" followed by at least 2 sets of: + * - "." plus some non-whitespace, non-"." characters + * - must end match with a word-break + * - include a "/" or "=" beyond break if present + * - end with whitespace, non-word, or end-of-line + */ + client.linkRE = + /(?:\W|^)((?:(\w[\w-]+):[^\s]+|www(\.[^.\s]+){2,})\b[\/=\)]?)(?=\s|\W|$)/; + + // Colours: \x03, with optional foreground and background colours + client.colorRE = /(\x03((\d{1,2})(,\d{1,2}|)|))/; + + client.whitespaceRE = new RegExp("(\\S{" + client.MAX_WORD_DISPLAY + ",})"); + + const LOW_PRIORITY = 5; + const NORMAL_PRIORITY = 10; + const HIGH_PRIORITY = 15; + const HIGHER_PRIORITY = 20; + + var munger = client.munger = new CMunger(insertText); + // Special internal munger! + munger.addRule(".inline-buttons", /(\[\[.*?\]\])/, insertInlineButton, + HIGH_PRIORITY, LOW_PRIORITY, false); + munger.addRule("quote", /(``|'')/, insertQuote, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("bold", /(?:[\s(\[]|^)(\*[^*()]*\*)(?:[\s\]).,;!\?]|$)/, + "chatzilla-bold", NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("underline", /(?:[\s(\[]|^)(\_[^_()]*\_)(?:[\s\]).,;!\?]|$)/, + "chatzilla-underline", NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("italic", /(?:\s|^)(\/[^\/()]*\/)(?:[\s.,]|$)/, + "chatzilla-italic", NORMAL_PRIORITY, NORMAL_PRIORITY); + /* allow () chars inside |code()| blocks */ + munger.addRule("teletype", /(?:\s|^)(\|[^|]*\|)(?:[\s.,]|$)/, + "chatzilla-teletype", NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule(".mirc-colors", client.colorRE, mircChangeColor, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule(".mirc-bold", /(\x02)/, mircToggleBold, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule(".mirc-underline", /(\x1f)/, mircToggleUnder, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule(".mirc-color-reset", /(\x0f)/, mircResetColor, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule(".mirc-reverse", /(\x16)/, mircReverseColor, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule(".ansi-escape-sgr", /(\x1b\[([\d;]*)m)/, + ansiEscapeSGR, NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("ctrl-char", /([\x01-\x1f])/, showCtrlChar, + NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("link", client.linkRE, insertLink, NORMAL_PRIORITY, HIGH_PRIORITY); + + // This has a higher starting priority so as to get it to match before the + // normal link, which won't know about mailto and then fail. + munger.addRule(".mailto", + /(?:\W|^)((mailto:)?[^:;\\<>\[\]()\'\"\s\u201d]+@[^.<>\[\]()\'\"\s\u201d]+\.[^<>\[\]()\'\"\s\u201d]+)/i, + insertMailToLink, NORMAL_PRIORITY, HIGHER_PRIORITY, false); + + addBugzillaLinkMungerRule(client.prefs["bugKeyword"], NORMAL_PRIORITY, NORMAL_PRIORITY); + + munger.addRule("channel-link", + /(?:[^\w#]|^)[@%+]?(#[^<>,\[\](){}\"\s\u201d]*[^:,.<>\[\](){}\'\"\s\u201d])/i, + insertChannelLink, NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("talkback-link", /(?:\W|^)(TB\d{8,}[A-Z]?)(?:\W|$)/, + insertTalkbackLink, NORMAL_PRIORITY, NORMAL_PRIORITY); + + munger.addRule("face", + /((^|\s)(?:[>O]?[B8=:;(xX%][~']?[-^v"]?(?:[)|(PpSs0oO#\?\*\[\]\/\\]|D+)|>[-^v]?\)|[oO9][._][oO9])(\s|$))/, + insertSmiley, NORMAL_PRIORITY, NORMAL_PRIORITY); + munger.addRule("rheet", /(?:\W|^)(rhee+t\!*)(?:\s|$)/i, insertRheet, 10, 10); + munger.addRule("word-hyphenator", client.whitespaceRE, + insertHyphenatedWord, LOW_PRIORITY, NORMAL_PRIORITY); + + client.enableColors = client.prefs["munger.colorCodes"]; + var branch = client.prefManager.prefBranch; + for (var entry in munger.entries) + { + if (!isinstance(munger.entries[entry], Object)) + continue; + + for (var rule in munger.entries[entry]) + { + if (rule[0] == ".") + continue; + + try + { + munger.entries[entry][rule].enabled = + branch.getBoolPref("munger." + rule); + } + catch (ex) + { + // nada + } + } + } +} + +function addBugzillaLinkMungerRule(keywords, priority, startPriority) +{ + client.munger.addRule("bugzilla-link", + new RegExp("(?:\\W|^)(("+keywords+")\\s+(?:#?\\d+|#[^\\s,]{1,20})(?:\\s+comment\\s+#?\\d+)?)","i"), + insertBugzillaLink, priority, startPriority); + +} + +function insertLink(matchText, containerTag, data, mungerEntry) +{ + var href; + var linkText; + + var trailing; + ary = matchText.match(/([.,?\)]+)$/); + if (ary) + { + linkText = RegExp.leftContext; + trailing = ary[1]; + + // We special-case links that end with (something), often found on wikis + // if "trailing" starts with ) and there's an unclosed ( in the + // "linkText"; then we put the final ) back in + if ((trailing.indexOf(")") == 0) && (linkText.match(/\([^\)]*$/))) + { + + linkText += ")"; + trailing = trailing.substr(1); + } + } + else + { + linkText = matchText; + } + + var ary = linkText.match(/^(\w[\w-]+):/); + if (ary) + { + if (!client.checkURLScheme(ary[1])) + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, data); + mungerEntry.enabled = true; + return; + } + + href = linkText; + } + else + { + href = "http://" + linkText; + } + + /* This gives callers to the munger control over URLs being logged; the + * channel topic munger uses this, as well as the "is important" checker. + * If either of |dontLogURLs| or |noStateChange| is present and true, we + * don't log. + */ + if ((!("dontLogURLs" in data) || !data.dontLogURLs) && + (!("noStateChange" in data) || !data.noStateChange) && + client.urlLogger) + { + client.urlLogger.append(href); + } + + var anchor = document.createElementNS(XHTML_NS, "html:a"); + var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g; + anchor.setAttribute("href", href.replace(mircRE, "")); + + // Carry over formatting. + var otherFormatting = calcClass(data); + if (otherFormatting) + anchor.setAttribute("class", "chatzilla-link " + otherFormatting); + else + anchor.setAttribute("class", "chatzilla-link"); + + anchor.setAttribute("target", "_content"); + mungerEntry.enabled = false; + data.inLink = true; + client.munger.munge(linkText, anchor, data); + mungerEntry.enabled = true; + delete data.inLink; + containerTag.appendChild(anchor); + if (trailing) + insertText(trailing, containerTag, data); + +} + +function insertMailToLink(matchText, containerTag, eventData, mungerEntry) +{ + if (("inLink" in eventData) && eventData.inLink) + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, eventData); + mungerEntry.enabled = true; + return; + } + + var href; + + if (matchText.toLowerCase().indexOf("mailto:") != 0)
+ href = "mailto:" + matchText; + else + href = matchText; + + var anchor = document.createElementNS(XHTML_NS, "html:a"); + var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g; + anchor.setAttribute("href", href.replace(mircRE, "")); + + // Carry over formatting. + var otherFormatting = calcClass(eventData); + if (otherFormatting) + anchor.setAttribute("class", "chatzilla-link " + otherFormatting); + else + anchor.setAttribute("class", "chatzilla-link"); + + //anchor.setAttribute ("target", "_content"); + mungerEntry.enabled = false; + eventData.inLink = true; + client.munger.munge(matchText, anchor, eventData); + mungerEntry.enabled = true; + delete eventData.inLink; + containerTag.appendChild(anchor); + +} + +function insertChannelLink(matchText, containerTag, eventData, mungerEntry) +{ + if (("inLink" in eventData) && eventData.inLink) + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, eventData); + mungerEntry.enabled = true; + return; + } + + var bogusChannels = + /^#(include|error|define|if|ifdef|else|elsif|endif)$/i; + + if (!("network" in eventData) || !eventData.network || + matchText.search(bogusChannels) != -1) + { + containerTag.appendChild(document.createTextNode(matchText)); + return; + } + + var linkText = removeColorCodes(matchText); + var encodedLinkText = fromUnicode(linkText, eventData.sourceObject); + var anchor = document.createElementNS(XHTML_NS, "html:a"); + anchor.setAttribute("href", eventData.network.getURL(encodedLinkText)); + + // Carry over formatting. + var otherFormatting = calcClass(eventData); + if (otherFormatting) + anchor.setAttribute("class", "chatzilla-link " + otherFormatting); + else + anchor.setAttribute("class", "chatzilla-link"); + + mungerEntry.enabled = false; + eventData.inLink = true; + client.munger.munge(matchText, anchor, eventData); + mungerEntry.enabled = true; + delete eventData.inLink; + containerTag.appendChild(anchor); +} + +function insertTalkbackLink(matchText, containerTag, eventData, mungerEntry) +{ + if (("inLink" in eventData) && eventData.inLink) + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, eventData); + mungerEntry.enabled = true; + return; + } + + var anchor = document.createElementNS(XHTML_NS, "html:a"); + + anchor.setAttribute("href", "http://talkback-public.mozilla.org/" + + "search/start.jsp?search=2&type=iid&id=" + matchText); + + // Carry over formatting. + var otherFormatting = calcClass(eventData); + if (otherFormatting) + anchor.setAttribute("class", "chatzilla-link " + otherFormatting); + else + anchor.setAttribute("class", "chatzilla-link"); + + mungerEntry.enabled = false; + client.munger.munge(matchText, anchor, eventData); + mungerEntry.enabled = true; + containerTag.appendChild(anchor); +} + +function insertBugzillaLink (matchText, containerTag, eventData, mungerEntry) +{ + if (("inLink" in eventData) && eventData.inLink) + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, eventData); + mungerEntry.enabled = true; + return; + } + + var prefs = client.prefs; + if (eventData.channel) + prefs = eventData.channel.prefs; + else if (eventData.network) + prefs = eventData.network.prefs; + + var bugURL = prefs["bugURL"]; + var bugURLcomment = prefs["bugURL.comment"]; + + if (bugURL.length > 0) + { + var idOrAlias = matchText.match(new RegExp("(?:"+client.prefs["bugKeyword"]+")\\s+#?(\\d+|[^\\s,]{1,20})","i"))[1]; + bugURL = bugURL.replace("%s", idOrAlias); + + var commentNum = matchText.match(/comment\s+#?(\d+)/i); + if (commentNum) + { + /* If the comment is a complete URL, use only that, replacing %1$s + * and %2$s with the bug number and comment number, respectively. + * Otherwise, append the comment preference to the main one, + * replacing just %s in each. + */ + if (bugURLcomment.match(/^\w+:/)) + { + bugURL = bugURLcomment; + bugURL = bugURL.replace("%1$s", idOrAlias); + bugURL = bugURL.replace("%2$s", commentNum[1]); + } + else + { + bugURL += bugURLcomment.replace("%s", commentNum[1]); + } + } + + var anchor = document.createElementNS(XHTML_NS, "html:a"); + anchor.setAttribute("href", bugURL); + // Carry over formatting. + var otherFormatting = calcClass(eventData); + if (otherFormatting) + anchor.setAttribute("class", "chatzilla-link " + otherFormatting); + else + anchor.setAttribute("class", "chatzilla-link"); + + anchor.setAttribute("target", "_content"); + mungerEntry.enabled = false; + eventData.inLink = true; + client.munger.munge(matchText, anchor, eventData); + mungerEntry.enabled = true; + delete eventData.inLink; + containerTag.appendChild(anchor); + } + else + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, eventData); + mungerEntry.enabled = true; + } +} + +function insertRheet(matchText, containerTag, eventData, mungerEntry) +{ + if (("inLink" in eventData) && eventData.inLink) + { + mungerEntry.enabled = false; + client.munger.munge(matchText, containerTag, eventData); + mungerEntry.enabled = true; + return; + } + + var anchor = document.createElementNS(XHTML_NS, "html:a"); + anchor.setAttribute("href", + "http://ftp.mozilla.org/pub/mozilla.org/mozilla/libraries/bonus-tracks/rheet.wav"); + anchor.setAttribute("class", "chatzilla-rheet chatzilla-link"); + //anchor.setAttribute ("target", "_content"); + insertText(matchText, anchor, eventData); + containerTag.appendChild(anchor); +} + +function insertQuote (matchText, containerTag) +{ + if (matchText == "``") + containerTag.appendChild(document.createTextNode("\u201c")); + else + containerTag.appendChild(document.createTextNode("\u201d")); + containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr")); +} + +function insertSmiley(emoticon, containerTag, eventData, mungerEntry) +{ + let smilies = { + "face-alien": "\uD83D\uDC7D", + "face-lol": "\uD83D\uDE02", + "face-laugh": "\uD83D\uDE04", + "face-sweat_smile": "\uD83D\uDE05", + "face-innocent": "\uD83D\uDE07", + "face-evil": "\uD83D\uDE08", + "face-wink": "\uD83D\uDE09", + "face-smile": "\uD83D\uDE0A", + "face-cool": "\uD83D\uDE0E", + "face-neutral": "\uD83D\uDE10", + "face-thinking": "\uD83D\uDE14", + "face-confused": "\uD83D\uDE15", + "face-kissing": "\uD83D\uDE17", + "face-tongue": "\uD83D\uDE1B", + "face-worried": "\uD83D\uDE1F", + "face-angry": "\uD83D\uDE20", + "face-cry": "\uD83D\uDE22", + "face-surprised": "\uD83D\uDE2D", + "face-eek": "\uD83D\uDE31", + "face-red": "\uD83D\uDE33", + "face-dizzy": "\uD83D\uDE35", + "face-sad": "\uD83D\uDE41", + "face-rolleyes": "\uD83D\uDE44", + "face-zipped": "\uD83E\uDD10", + "face-rofl": "\uD83E\uDD23", + "face-woozy": "\uD83E\uDD74", + }; + + let type; + + if (emoticon.search(/\>[-^v]?\)/) != -1) + type = "face-alien"; + else if (emoticon.search(/\>[=:;][-^v]?[(|]|[Xx][-^v]?[(\[]/) != -1) + type = "face-angry"; + else if (emoticon.search(/[=:;][-^v]?[Ss]/) != -1) + type = "face-confused"; + else if (emoticon.search(/[B8][-^v]?[)\]]/) != -1) + type = "face-cool"; + else if (emoticon.search(/[=:;][~'][-^v]?\(/) != -1) + type = "face-cry"; + else if (emoticon.search(/o[._]O|O[._]o/) != -1) + type = "face-dizzy"; + else if (emoticon.search(/o[._]o|O[._]O/) != -1) + type = "face-eek"; + else if (emoticon.search(/\>[=:;][-^v]?D/) != -1) + type = "face-evil"; + else if (emoticon.search(/O[=:][-^v]?[)]/) != -1) + type = "face-innocent"; + else if (emoticon.search(/[=:;][-^v]?[*]/) != -1) + type = "face-kissing"; + else if (emoticon.search(/[=:;][-^v]?DD/) != -1) + type = "face-lol"; + else if (emoticon.search(/[=:;][-^v]?D/) != -1) + type = "face-laugh"; + else if (emoticon.search(/\([-^v]?D|[xX][-^v]?D/) != -1) + type = "face-rofl"; + else if (emoticon.search(/[=:;][-^v]?\|/) != -1) + type = "face-neutral"; + else if (emoticon.search(/[=:;][-^v]?\?/) != -1) + type = "face-thinking"; + else if (emoticon.search(/[=:;]"[)\]]/) != -1) + type = "face-red"; + else if (emoticon.search(/9[._]9/) != -1) + type = "face-rolleyes"; + else if (emoticon.search(/[=:;][-^v]?[(\[]/) != -1) + type = "face-sad"; + else if (emoticon.search(/[=:][-^v]?[)]/) != -1) + type = "face-smile"; + else if (emoticon.search(/[=:;][-^v]?[0oO]/) != -1) + type = "face-surprised"; + else if (emoticon.search(/[=:][-^v]?[\]]/) != -1) + type = "face-sweat_smile"; + else if (emoticon.search(/[=:;][-^v]?[pP]/) != -1) + type = "face-tongue"; + else if (emoticon.search(/;[-^v]?[)\]]/) != -1) + type = "face-wink"; + else if (emoticon.search(/%[-^v][)\]]/) != -1) + type = "face-woozy"; + else if (emoticon.search(/[=:;][-^v]?[\/\\]/) != -1) + type = "face-worried"; + else if (emoticon.search(/[=:;][-^v]?[#]/) != -1) + type = "face-zipped"; + + let glyph = smilies[type]; + if (!glyph) { + // We didn't actually match anything, so it'll be a too-generic match + // from the munger RegExp. + mungerEntry.enabled = false; + client.munger.munge(emoticon, containerTag, eventData); + mungerEntry.enabled = true; + return; + } + + // Add spaces to beginning / end where appropriate. + if (emoticon.search(/^\s/) != -1) + glyph = " " + glyph; + if (emoticon.search(/\s$/) != -1) + glyph = glyph + " "; + + // Create a span to hold the emoticon. + let span = document.createElementNS(XHTML_NS, "html:span"); + span.appendChild(document.createTextNode(glyph)); + span.setAttribute("class", "chatzilla-emote-txt"); + // Add the title attribute (to show the original text in a tooltip) in case + // the replacement was done incorrectly. + span.setAttribute("title", emoticon); + span.setAttribute("type", type); + containerTag.appendChild(span); +} + +function mircChangeColor (colorInfo, containerTag, data) +{ + /* If colors are disabled, the caller doesn't want colors specifically, or + * the caller doesn't want any state-changing effects, we drop out. + */ + if (!client.enableColors || + (("noMircColors" in data) && data.noMircColors) || + (("noStateChange" in data) && data.noStateChange)) + { + return; + } + + // Entry 0 will contain all colors specified, + // entry 1 will have any specified foreground color or be undefined, + // entry 2 will have any specified background color or be undefined. + // Valid color codes are 0-99 with 99 having special meaning. + let ary = colorInfo.match(/^\x03(?:(\d\d?)(?:,(\d\d?))?)?/); + + // If no foreground color specified or somehow the array does not have 3 + // entries then it has invalid syntax. + if (ary.length != 3 || !ary[1]) { + delete data.currFgColor; + delete data.currBgColor; + return; + } + + let fgColor = Number(ary[1]); + + if (fgColor != 99) { + data.currFgColor = (fgColor % 16).toString().padStart(2, "0"); + } else { + delete data.currFgColor; + } + + // If no background color then default to 99. + let bgColor = Number(ary[2] || "99"); + + if (bgColor != 99) { + data.currBgColor = (bgColor % 16).toString().padStart(2, "0"); + } else { + delete data.currBgColor; + } + + // Only set hasColorInfo if we have something set. + if (fgColor != 99 || bgColor != 99) { + data.hasColorInfo = true; + } +} + +function mircToggleBold (colorInfo, containerTag, data) +{ + if (!client.enableColors || + (("noMircColors" in data) && data.noMircColors) || + (("noStateChange" in data) && data.noStateChange)) + { + return; + } + + if ("isBold" in data) + delete data.isBold; + else + data.isBold = true; + data.hasColorInfo = true; +} + +function mircToggleUnder (colorInfo, containerTag, data) +{ + if (!client.enableColors || + (("noMircColors" in data) && data.noMircColors) || + (("noStateChange" in data) && data.noStateChange)) + { + return; + } + + if ("isUnderline" in data) + delete data.isUnderline; + else + data.isUnderline = true; + data.hasColorInfo = true; +} + +function mircResetColor (text, containerTag, data) +{ + if (!client.enableColors || + (("noMircColors" in data) && data.noMircColors) || + (("noStateChange" in data) && data.noStateChange) || + !("hasColorInfo" in data)) + { + return; + } + + removeColorInfo(data); +} + +function mircReverseColor (text, containerTag, data) +{ + if (!client.enableColors || + (("noMircColors" in data) && data.noMircColors) || + (("noStateChange" in data) && data.noStateChange)) + { + return; + } + + var tempColor = ("currFgColor" in data ? data.currFgColor : ""); + + if ("currBgColor" in data) + data.currFgColor = data.currBgColor; + else + delete data.currFgColor; + if (tempColor) + data.currBgColor = tempColor; + else + delete data.currBgColor; + data.hasColorInfo = true; +} + +function ansiEscapeSGR(text, containerTag, data) +{ + if (!client.enableColors || + (("noANSIColors" in data) && data.noANSIColors) || + (("noStateChange" in data) && data.noStateChange)) + { + return; + } + + /* ANSI SGR (Select Graphic Rendition) escape support. Matched text may + * have any number of effects, each a number separated by a semicolon. If + * there are no effects listed, it is treated as effect "0" (reset/normal). + */ + + text = text.substr(2, text.length - 3) || "0"; + + const ansiToMircColor = [ + "01", "05", "03", "07", "02", "06", "10", "15", + "14", "04", "09", "08", "12", "13", "11", "00" + ]; + + var effects = text.split(";"); + for (var i = 0; i < effects.length; i++) + { + data.hasColorInfo = true; + + switch (Number(effects[i])) + { + case 0: // Reset/normal. + removeColorInfo(data); + break; + + case 1: // Intensity: bold. + data.isBold = true; + break; + + case 3: // Italic: on. + data.isItalic = true; + break; + + case 4: // Underline: single. + data.isUnderline = true; + break; + + case 9: // Strikethrough: on. + data.isStrikethrough = true; + break; + + case 22: // Intensity: normal. + delete data.isBold; + break; + + case 23: // Italic: off. + delete data.isItalic; + break; + + case 24: // Underline: off. + delete data.isUnderline; + break; + + case 29: // Strikethrough: off. + delete data.isStrikethrough; + break; + + case 53: // Overline: on. + data.isOverline = true; + break; + + case 55: // Overline: off. + delete data.isOverline; + break; + + case 30: // FG: Black. + case 31: // FG: Red. + case 32: // FG: Green. + case 33: // FG: Yellow. + case 34: // FG: Blue. + case 35: // FG: Magenta. + case 36: // FG: Cyan. + case 37: // FG: While (light grey). + data.currFgColor = ansiToMircColor[effects[i] - 30]; + break; + + case 39: // FG: default. + delete data.currFgColor; + break; + + case 40: // BG: Black. + case 41: // BG: Red. + case 42: // BG: Green. + case 43: // BG: Yellow. + case 44: // BG: Blue. + case 45: // BG: Magenta. + case 46: // BG: Cyan. + case 47: // BG: While (light grey). + data.currBgColor = ansiToMircColor[effects[i] - 40]; + break; + + case 49: // BG: default. + delete data.currBgColor; + break; + + case 90: // FG: Bright Black (dark grey). + case 91: // FG: Bright Red. + case 92: // FG: Bright Green. + case 93: // FG: Bright Yellow. + case 94: // FG: Bright Blue. + case 95: // FG: Bright Magenta. + case 96: // FG: Bright Cyan. + case 97: // FG: Bright While. + data.currFgColor = ansiToMircColor[effects[i] - 90 + 8]; + break; + + case 100: // BG: Bright Black (dark grey). + case 101: // BG: Bright Red. + case 102: // BG: Bright Green. + case 103: // BG: Bright Yellow. + case 104: // BG: Bright Blue. + case 105: // BG: Bright Magenta. + case 106: // BG: Bright Cyan. + case 107: // BG: Bright While. + data.currBgColor = ansiToMircColor[effects[i] - 100 + 8]; + break; + } + } +} + +function removeColorInfo(data) +{ + delete data.currFgColor; + delete data.currBgColor; + delete data.isBold; + delete data.isItalic; + delete data.isOverline; + delete data.isStrikethrough; + delete data.isUnderline; + delete data.hasColorInfo; +} + +function showCtrlChar(c, containerTag) +{ + var span = document.createElementNS(XHTML_NS, "html:span"); + span.setAttribute("class", "chatzilla-control-char"); + if (c == "\t") + { + containerTag.appendChild(document.createTextNode(c)); + return; + } + + var ctrlStr = c.charCodeAt(0).toString(16); + if (ctrlStr.length < 2) + ctrlStr = "0" + ctrlStr; + span.appendChild(document.createTextNode("0x" + ctrlStr)); + containerTag.appendChild(span); + containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr")); +} + +function insertText(text, containerTag, data) +{ + var newClass = ""; + if (data && ("hasColorInfo" in data)) + newClass = calcClass(data); + if (!newClass) + delete data.hasColorInfo; + + if (newClass) + { + var spanTag = document.createElementNS(XHTML_NS, "html:span"); + spanTag.setAttribute("class", newClass); + containerTag.appendChild(spanTag); + containerTag = spanTag; + } + + var arg; + while ((arg = text.match(client.whitespaceRE))) + { + // Find the start of the match so we can insert the preceding text. + var start = text.indexOf(arg[0]); + if (start > 0) + containerTag.appendChild(document.createTextNode(text.substr(0, start))); + + // Process the long word itself. + insertHyphenatedWord(arg[1], containerTag, { dontStyleText: true }); + + // Continue with the rest of the text. + text = text.substr(start + arg[0].length); + } + + // Insert any left-over text on the end. + if (text) + containerTag.appendChild(document.createTextNode(text)); +} + +function insertHyphenatedWord(longWord, containerTag, data) +{ + var wordParts = splitLongWord(longWord, client.MAX_WORD_DISPLAY); + + if (!data || !("dontStyleText" in data)) + { + var newClass = ""; + if (data && ("hasColorInfo" in data)) + newClass = calcClass(data); + if (!newClass) + delete data.hasColorInfo; + + if (newClass) + { + var spanTag = document.createElementNS(XHTML_NS, "html:span"); + spanTag.setAttribute("class", newClass); + containerTag.appendChild(spanTag); + containerTag = spanTag; + } + } + + var wbr = document.createElementNS(XHTML_NS, "html:wbr"); + for (var i = 0; i < wordParts.length; ++i) + { + containerTag.appendChild(document.createTextNode(wordParts[i])); + containerTag.appendChild(wbr.cloneNode(true)); + } +} + +function insertInlineButton(text, containerTag, data) +{ + var ary = text.match(/\[\[([^\]]+)\]\[([^\]]+)\]\[([^\]]+)\]\]/); + + if (!ary) + { + containerTag.appendChild(document.createTextNode(text)); + return; + } + + var label = ary[1]; + var title = ary[2]; + var command = ary[3]; + + var link = document.createElementNS(XHTML_NS, "html:a"); + link.setAttribute("href", "x-cz-command:" + encodeURI(command)); + link.setAttribute("title", title); + link.setAttribute("class", "chatzilla-link"); + link.appendChild(document.createTextNode(label)); + + containerTag.appendChild(document.createTextNode("[")); + containerTag.appendChild(link); + containerTag.appendChild(document.createTextNode("]")); +} + +function calcClass(data) +{ + var className = ""; + if ("hasColorInfo" in data) + { + if ("currFgColor" in data) + className += " chatzilla-fg" + data.currFgColor; + if ("currBgColor" in data) + className += " chatzilla-bg" + data.currBgColor; + if ("isBold" in data) + className += " chatzilla-bold"; + if ("isItalic" in data) + className += " chatzilla-italic"; + if ("isOverline" in data) + className += " chatzilla-overline"; + if ("isStrikethrough" in data) + className += " chatzilla-strikethrough"; + if ("isUnderline" in data) + className += " chatzilla-underline"; + } + return className; +} + diff --git a/comm/suite/chatzilla/xul/content/networks-edit.css b/comm/suite/chatzilla/xul/content/networks-edit.css new file mode 100644 index 0000000000..f1f36e47e1 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/networks-edit.css @@ -0,0 +1,15 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +/* Set min-width on the network list. */ +#networkList { + min-width: 26ch; +} + +/* Set min-width on the server list. */ +#serverList { + min-width: 28ch; +} diff --git a/comm/suite/chatzilla/xul/content/networks-edit.js b/comm/suite/chatzilla/xul/content/networks-edit.js new file mode 100644 index 0000000000..4952783cc9 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/networks-edit.js @@ -0,0 +1,390 @@ +/* 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 ASSERT = function(cond, msg) { if (!cond) { alert(msg); } return cond; } +var client; + +// To be able to load static.js, we need a few things defined first: +function CIRCNetwork() {} +function CIRCServer() {} +function CIRCChannel() {} +function CIRCUser() {} +function CIRCChanUser() {} +function CIRCDCCUser() {} +function CIRCDCCChat() {} +function CIRCDCCFile() {} +function CIRCDCCFileTransfer() {} +function CIRCSTS() {} + +// Actual network window itself. +var gNetworkWindow = { + mBundle: null, + mServerList: null, + mNetworkList: null, + + /* Stores all the network and server objects we're using. + */ + networkList: null, + + alert: function(aSubject, aVar) { + let title = this.mBundle.getString(aSubject + "Title"); + let msg = this.mBundle.getFormattedString(aSubject, [aVar]); + Services.prompt.alert(window, title, msg); + }, + + confirmEx: function(aSubject, aVar) { + let title = this.mBundle.getString(aSubject + "Title"); + let msg = aVar ? this.mBundle.getFormattedString(aSubject, [aVar]) + : this.mBundle.getString(aSubject); + return Services.prompt.confirmEx(window, title, msg, + Services.prompt.STD_YES_NO_BUTTONS, null, + null, null, null, { }); + }, + + prompt: function(aSubject, aInitial) { + let title = this.mBundle.getString(aSubject + "Title"); + let msg = this.mBundle.getString(aSubject); + let rv = { value: aInitial }; + + if (!Services.prompt.prompt(window, title, msg, rv, null, {value: null})) { + return null; + } + + return rv.value.toLowerCase().trim(); + }, + + refreshNetworks: function(aNetwork) { + // Remove all children. + while (this.mNetworkList.hasChildNodes()) { + this.mNetworkList.lastChild.remove(); + } + + let hasChildren = false; + let network; + // Populate the network item list. + for (let name in this.networkList) { + let label = document.createElement("label"); + label.setAttribute("value", name); + let listitem = document.createElement("listitem"); + listitem.appendChild(label); + listitem.id = name; + if (aNetwork && (aNetwork == name)) { + network = listitem; + } + this.mNetworkList.appendChild(listitem); + hasChildren = true; + } + + if (hasChildren) { + // If a network name was given and found select it, + // otherwise select the first item. + this.mNetworkList.selectItem(network || this.mNetworkList.firstChild); + } else { + this.onSelectNetwork(); + } + this.updateNetworkButtons(hasChildren); + }, + + updateNetworkButtons: function(aSelected) { + let editButton = document.getElementById("networkListEditButton"); + let removeButton = document.getElementById("networkListRemoveButton"); + if (!aSelected) { + editButton.setAttribute("disabled", "true"); + removeButton.setAttribute("disabled", "true"); + } else { + editButton.removeAttribute("disabled"); + removeButton.removeAttribute("disabled"); + } + }, + + // Loads the networks list. + onLoad: function() { + client = window.arguments[0]; + + // Needed for ASSERT. + initMessages(); + + this.mBundle = document.getElementById("bundle_networks"); + this.mServerList = document.getElementById("serverList"); + this.mNetworkList = document.getElementById("networkList"); + + // The list of objects we're tracking. + this.networkList = networksToNetworkList(); + this.refreshNetworks(); + + // Force the window to be the right size now, not later. + window.sizeToContent(); + }, + + // Closing the window. Clean up. + onClose: function() { + }, + + // OK button. + onOK: function() { + // Save the list and update client.networks + try { + networksSaveList(this.networkList); + } + catch (e) { + this.alert("network-saveError", e); + return false; + } + + networksSyncFromList(this.networkList); + window.close(); + client.updateHeader(); + client.dispatch("networks"); + return true; + }, + + // Cancel button. + onCancel: function() { + window.close(); + return true; + }, + + // Restore Defaults button. + onRestore: function() { + // Ask for confirmation. + if (this.confirmEx("network-confirmRestoreDefaults") != 0) { + return; + } + + // Repopulate the network list. + this.networkList = networksGetDefaults(); + this.refreshNetworks(); + }, + + // Connect to Network button. + onConnect: function() { + let selection = this.mNetworkList.selectedItem; + if (!selection) + return; + + let network = this.networkList[selection.id]; + if (this.onOK()) { + if (networkHasSecure(network.servers)) { + client.dispatch("sslserver " + network.name); + } else { + client.dispatch("server " + network.name); + } + } + }, + + // Select a network listitem. + onSelectNetwork: function(aId = 0) { + let header = document.getElementById("network-header"); + + // Remove all children. + while (this.mServerList.hasChildNodes()) { + this.mServerList.lastChild.remove(); + } + + let selection = this.mNetworkList.selectedItem; + if (!selection) { + header.setAttribute("title", + this.mBundle.getString("network-headerDefault")); + this.updateServerButtons(null, true); + return; + } + + // Make sure selected network item is visible. + this.mNetworkList.ensureElementIsVisible(selection); + + let hasChildren = false; + let network = this.networkList[selection.id]; + for (let i = 0; i < network.servers.length; i++) { + let server = network.servers[i]; + let label = document.createElement("label"); + label.setAttribute("value", server.hostname + ":" + server.port); + let listitem = document.createElement("listitem"); + listitem.appendChild(label); + listitem.setAttribute("server_id", i); + listitem.id = network.name + "-" + i; + this.mServerList.appendChild(listitem); + hasChildren = true; + } + + if (hasChildren) { + // Select the given id if it exists otherwise the first item. + this.mServerList.selectedIndex = aId; + } else { + this.onSelectServer(); + } + + header.setAttribute("title", + this.mBundle.getFormattedString("network-headerName", + [network.name])); + }, + + // Network Add button. + onAddNetwork: function() { + let name = this.prompt("network-add"); + if (!name) { + return; + } + + if (name in this.networkList) { + this.alert("network-nameError", name); + return; + } + + // Create new network entry. + this.networkList[name] = { name: name, displayName: name, servers: [] }; + + this.refreshNetworks(name); + }, + + // Network Edit button. + onEditNetwork: function() { + let oldName = this.mNetworkList.selectedItem.id; + let name = this.prompt("network-edit", oldName); + if (!name || (name == oldName)) { + return; + } + + if (name in this.networkList) { + this.alert("network-nameError", name); + return; + } + + // Create new network entry. + this.networkList[name] = { name: name, displayName: name, + servers: this.networkList[oldName].servers }; + // Remove old network entry. + delete this.networkList[oldName]; + + this.refreshNetworks(name); + }, + + // Network Remove button. + onRemoveNetwork: function() { + let selected = this.mNetworkList.selectedItem; + + // Confirm definitely want to remove this network. + if (this.confirmEx("network-remove", selected.id) != 0) { + return; + } + + // Remove network entry. + delete this.networkList[selected.id]; + + this.refreshNetworks(); + }, + + // Move up / down buttons. + onMoveServer: function(aDir) { + let item = this.mServerList.selectedItem; + let network = this.mNetworkList.selectedItem.id; + let id = parseInt(item.getAttribute("server_id")); + let server = this.networkList[network].servers[id]; + this.networkList[network].servers.splice(id, 1); + this.networkList[network].servers.splice(id + aDir, 0, server); + + // Refresh the server list and select the server that has been moved. + this.onSelectNetwork(id + aDir); + }, + + // Add Server button. + onAddServer: function() { + this.openServerEditor(null); + }, + + // Edit Server button. + onEditServer: function() { + let item = this.mServerList.selectedItem; + if (!item) { + return; + } + this.openServerEditor(item); + }, + + // Remove Server button. + onRemoveServer: function() { + let item = this.mServerList.selectedItem; + let network = this.mNetworkList.selectedItem.id; + let id = item.getAttribute("server_id"); + let server = this.networkList[network].servers[id]; + let name = server.hostname + ":" + server.port; + + // Confirm definitely want to remove this network. + if (this.confirmEx("server-remove", name) != 0) { + return; + } + + this.networkList[network].servers.splice(id, 1); + this.onSelectNetwork(); + }, + + onSelectServer: function() { + let server = this.mServerList.selectedItem; + this.updateServerButtons(server, false); + this.updateServerInfoBox(server); + }, + + openServerEditor: function(aItem) { + let network = this.mNetworkList.selectedItem.id; + let id; + let server; + if (aItem) { + id = aItem.getAttribute("server_id"); + server = this.networkList[network].servers[id]; + } + + let args = { server: server, result: false }; + window.openDialog("chrome://chatzilla/content/networks-server.xul", + "serverEdit", "chrome,titlebar,modal,centerscreen", args); + // Now update the server which was just added / edited and select it. + if (args.result) { + if (server) { + this.networkList[network].servers[id] = args.server; + } else { + id = this.networkList[network].servers.length; + this.networkList[network].servers.push(args.server); + } + this.refreshNetworks(network); + this.mServerList.selectedIndex = id; + } + }, + + updateServerButtons: function(aServer, aNone) { + this.disableButton("serverListUpButton", aNone || !aServer || + !aServer.previousSibling); + this.disableButton("serverListDownButton", aNone || !aServer || + !aServer.nextSibling); + this.disableButton("serverListAddButton", aNone); + this.disableButton("serverListEditButton", aNone || !aServer); + this.disableButton("serverListRemoveButton", aNone || !aServer); + }, + + disableButton: function(aButtonId, aDisable) { + let button = document.getElementById(aButtonId); + if (aDisable) { + button.setAttribute("disabled", "true"); + } else { + button.removeAttribute("disabled"); + } + }, + + updateServerInfoBox: function(aServer) { + let name = document.getElementById("nameValue"); + let port = document.getElementById("portValue"); + let connection = document.getElementById("connectionSecurityValue"); + if (!aServer) { + name.value = ""; + port.value = ""; + connection.value = ""; + return; + } + + let network = this.mNetworkList.selectedItem.id; + let id = aServer.getAttribute("server_id"); + let server = this.networkList[network].servers[id]; + let type = "server-ConnectionSecurityType-" + (server.isSecure ? "3" : "0"); + name.value = server.hostname; + port.value = server.port; + connection.value = this.mBundle.getString(type); + }, +}; diff --git a/comm/suite/chatzilla/xul/content/networks-edit.xul b/comm/suite/chatzilla/xul/content/networks-edit.xul new file mode 100644 index 0000000000..97483230ba --- /dev/null +++ b/comm/suite/chatzilla/xul/content/networks-edit.xul @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/networks.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/skin/networks-edit.css" + type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/content/networks-edit.css" + type="text/css"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="chatzilla-window" + title="&networksEditDialog.title;" + size="&networksEditDialog.size;" + windowtype="irc:chatzilla:networks" + onload="gNetworkWindow.onLoad();" + onunload="gNetworkWindow.onClose();" + buttons="accept,cancel,extra1,extra2" + buttonlabelextra1="&connectNetwork.label;" + buttonaccesskeyextra1="&connectNetwork.accesskey;" + buttonlabelextra2="&restoreButton.label;" + buttonaccesskeyextra2="&restoreButton.accesskey;" + ondialogaccept="return gNetworkWindow.onOK();" + ondialogcancel="return gNetworkWindow.onCancel();" + ondialogextra1="gNetworkWindow.onConnect();" + ondialogextra2="gNetworkWindow.onRestore();" + persist="screenX screenY width height"> + + <script src="chrome://chatzilla/content/lib/js/utils.js"/> + <script src="chrome://chatzilla/content/lib/js/file-utils.js"/> + <script src="chrome://chatzilla/content/lib/js/json-serializer.js"/> + <script src="chrome://chatzilla/content/lib/js/message-manager.js"/> + <script src="chrome://chatzilla/content/lib/js/text-serializer.js"/> + <script src="chrome://chatzilla/content/messages.js"/> + <script src="chrome://chatzilla/content/networks.js"/> + <script src="chrome://chatzilla/content/networks-edit.js"/> + <script src="chrome://chatzilla/content/static.js"/> + + <stringbundle id="bundle_networks" + src="chrome://chatzilla/locale/networks.properties"/> + + <hbox flex="1"> + <vbox id="networkListBox"> + <listbox id="networkList" + onselect="gNetworkWindow.onSelectNetwork();" + ondblclick="gNetworkWindow.onEditNetwork();" + seltype="single" + flex="1"/> + <button id="networkListAddButton" + label="&networkListAdd.label;" + accesskey="&networkListAdd.accesskey;" + tooltiptext="&networkListAdd.tooltip;" + oncommand="gNetworkWindow.onAddNetwork();"/> + <button id="networkListEditButton" + label="&networkListEdit.label;" + accesskey="&networkListEdit.accesskey;" + tooltiptext="&networkListEdit.tooltip;" + oncommand="gNetworkWindow.onEditNetwork();"/> + <button id="networkListRemoveButton" + label="&networkListRemove.label;" + accesskey="&networkListRemove.accesskey;" + tooltiptext="&networkListRemove.tooltip;" + oncommand="gNetworkWindow.onRemoveNetwork();"/> + </vbox> + <vbox id="serverListBox" flex="1"> + <dialogheader id="network-header" title=""/> + <hbox flex="1"> + <listbox id="serverList" + onselect="gNetworkWindow.onSelectServer();" + ondblclick="gNetworkWindow.onEditServer();" + seltype="single" + flex="1"/> + <vbox id="serverListButtons"> + <button id="serverListUpButton" + disabled="true" + label="&serverListUp.label;" + accesskey="&serverListUp.accesskey;" + tooltiptext="&serverListUp.tooltip;" + oncommand="gNetworkWindow.onMoveServer(-1);"/> + <button id="serverListDownButton" + disabled="true" + label="&serverListDown.label;" + accesskey="&serverListDown.accesskey;" + tooltiptext="&serverListDown.tooltip;" + oncommand="gNetworkWindow.onMoveServer(1);"/> + <spacer flex="1"/> + <button id="serverListAddButton" + label="&serverListAdd.label;" + accesskey="&serverListAdd.accesskey;" + tooltiptext="&serverListAdd.tooltip;" + oncommand="gNetworkWindow.onAddServer();"/> + <button id="serverListEditButton" + label="&serverListEdit.label;" + accesskey="&serverListEdit.accesskey;" + tooltiptext="&serverListEdit.tooltip;" + oncommand="gNetworkWindow.onEditServer();"/> + <separator/> + <button id="serverListRemoveButton" + disabled="true" + label="&serverListRemove.label;" + accesskey="&serverListRemove.accesskey;" + tooltiptext="&serverListRemove.tooltip;" + oncommand="gNetworkWindow.onRemoveServer();"/> + </vbox> + </hbox> + + <separator/> + + <label class="header">&serverDetails.label;</label> + <hbox id="serverInfoBox"> + <stack flex="1" class="inset"> + <spacer id="backgroundBox"/> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <hbox pack="end"> + <label id="nameLabel" + value="&serverName.label;" + control="nameValue"/> + </hbox> + <textbox id="nameValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="portLabel" + value="&serverPort.label;" + control="portValue"/> + </hbox> + <textbox id="portValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="connectionSecurityLabel" + value="&connectionSecurity.label;" + control="connectionSecurityValue"/> + </hbox> + <textbox id="connectionSecurityValue" + readonly="true" + class="plain"/> + </row> + </rows> + </grid> + </stack> + </hbox> + </vbox> + </hbox> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/networks-server.js b/comm/suite/chatzilla/xul/content/networks-server.js new file mode 100644 index 0000000000..fde299bf89 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/networks-server.js @@ -0,0 +1,94 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { Services } = + ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { isLegalHostNameOrIP = + ChromeUtils.import("resource:///modules/hostnameUtils.jsm"); + +var gNetworkServer; +var gNetworksBundle; +var gNameValue; +var gPortValue; +var gDefaultPort; +var gSocketType; + +function onLoad(aEvent) { + gNetworkServer = window.arguments[0].server; + + gNetworksBundle = document.getElementById("bundle_networks"); + gNameValue = document.getElementById("nameValue"); + gPortValue = document.getElementById("portValue"); + gDefaultPort = document.getElementById("defaultPort"); + gSocketType = document.getElementById("socketType"); + + // Set labels on socketType menuitems. + document.getElementById("socketSecurityType-0").label = + gNetworksBundle.getString("server-ConnectionSecurityType-0"); + document.getElementById("socketSecurityType-3").label = + gNetworksBundle.getString("server-ConnectionSecurityType-3"); + + if (gNetworkServer) { + gNameValue.value = gNetworkServer.hostname; + gPortValue.value = gNetworkServer.port; + gSocketType.value = gNetworkServer.isSecure ? 3 : 0; + } + sslChanged(false); +} + +function onAccept() { + let hostname = cleanUpHostName(gNameValue.value.toLowerCase()); + if (!isLegalHostNameOrIP(hostname)) { + let alertTitle = gNetworksBundle.getString("invalidServerName"); + let alertMsg = gNetworksBundle.getString("enterValidServerName"); + Services.prompt.alert(window, alertTitle, alertMsg); + + window.arguments[0].result = false; + return false; + } + + // If we didn't have a server to initialize with, we must create one. + if (!gNetworkServer) { + gNetworkServer = {}; + } + + gNetworkServer.hostname = hostname; + gNetworkServer.port = gPortValue.value; + gNetworkServer.isSecure = gSocketType.value == 3; + + window.arguments[0].server = gNetworkServer; + window.arguments[0].result = true; + return true; +} + +/** + * Resets the default port to IRC or IRCS, dependending on the |gSocketType| + * value, and sets the port to use to this default, if that's appropriate. + * + * @param aUserAction false for dialog initialization, + * true for user action. + */ +function sslChanged(aUserAction) { + const DEFAULT_IRC_PORT = "6667"; + const DEFAULT_IRCS_PORT = "6697"; + let otherDefaultPort; + let prevDefaultPort = gDefaultPort.value; + + if (gSocketType.value == 3) { + gDefaultPort.value = DEFAULT_IRCS_PORT; + otherDefaultPort = DEFAULT_IRC_PORT; + } else { + gDefaultPort.value = DEFAULT_IRC_PORT; + otherDefaultPort = DEFAULT_IRCS_PORT; + } + + // If the port is not set, or the user is causing the default port to change, + // and the port is set to the default for the other protocol, + // then set the port to the default for the new protocol. + if ((gPortValue.value == 0) || + (aUserAction && (gDefaultPort.value != prevDefaultPort) && + (gPortValue.value == otherDefaultPort))) + gPortValue.value = gDefaultPort.value; +} diff --git a/comm/suite/chatzilla/xul/content/networks-server.xul b/comm/suite/chatzilla/xul/content/networks-server.xul new file mode 100644 index 0000000000..af2341dae0 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/networks-server.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/networks.dtd"> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://chatzilla/content/" type="text/css"?> + +<dialog title="&serverEditDialog.title;" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoad();" + ondialogaccept="return onAccept();"> + + <script src="chrome://chatzilla/content/networks-server.js"/> + + <stringbundle id="bundle_networks" + src="chrome://chatzilla/locale/networks.properties"/> + + <vbox id="serverEditor"> + <groupbox> + <caption label="&settings.caption;"/> + <grid flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&serverName.label;" + accesskey="&serverName.accesskey;" + control="nameValue"/> + <textbox id="nameValue" + flex="1" + class="uri-element"/> + </row> + <row align="center"> + <label value="&serverPort.label;" + accesskey="&serverPort.accesskey;" + control="portValue"/> + <hbox align="center"> + <textbox id="portValue" + type="number" + min="0" + max="65535" + size="5"/> + <label value="&serverPortDefault.label;"/> + <label id="defaultPort"/> + </hbox> + </row> + </rows> + </grid> + </groupbox> + + <separator class="thin"/> + + <groupbox> + <caption label="&security.caption;"/> + + <grid flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&connectionSecurity.label;" + accesskey="&connectionSecurity.accesskey;" + control="socketType"/> + <menulist id="socketType" oncommand="sslChanged(true);"> + <menupopup id="socketTypePopup"> + <menuitem id="socketSecurityType-0" value="0"/> + <menuitem id="socketSecurityType-3" value="3"/> + </menupopup> + </menulist> + </row> + </rows> + </grid> + </groupbox> + </vbox> +</dialog> diff --git a/comm/suite/chatzilla/xul/content/networks.js b/comm/suite/chatzilla/xul/content/networks.js new file mode 100644 index 0000000000..bffeab7c4a --- /dev/null +++ b/comm/suite/chatzilla/xul/content/networks.js @@ -0,0 +1,228 @@ +/* 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 initNetworks() +{ + let migrated = Services.prefs.getBoolPref("extensions.irc.network_migrated", + false); + let networksFile = new nsLocalFile(client.prefs["profilePath"]); + networksFile.append("networks." + (migrated ? "json" : "txt")); + + let createDefault = !networksFile.exists(); + let networkList = {}; + // Populate networkList with defaults if no file exists or migrating from + // previous networks.txt file usage. + if (createDefault || !migrated) + { + networkList = networksGetDefaults(); + } + + if (!createDefault) + { + let userNetworkList = []; + + let networksLoader = migrated ? new JSONSerializer(networksFile) + : new TextSerializer(networksFile); + if (networksLoader.open("<")) + { + let item = networksLoader.deserialize(); + if (isinstance(item, Array)) + userNetworkList = item; + else + dd("Malformed networks file!"); + networksLoader.close(); + } + + // When migrating this merges the user's network list with the default + // ones otherwise this populates the empty networkList. + for (let network of userNetworkList) + { + let lowerNetName = network.name.toLowerCase(); + if ((lowerNetName in networkList) && ("isDeleted" in network)) + { + delete networkList[lowerNetName]; + } + else if (!("isDeleted" in network)) + { + networkList[lowerNetName] = network; + networkList[lowerNetName].name = lowerNetName; + } + } + } + + + if (!migrated) + { + Services.prefs.setBoolPref("extensions.irc.network_migrated", true); + } + + // Sync to client.networks. + networksSyncFromList(networkList); + + // If we created a new file with the defaults, save it. + if (createDefault || !migrated) + networksSaveList(networkList); +} + +function networksGetDefaults() +{ + var networks = new Object(); + + // Set up default network list. + networks["libera.chat"] = { + displayName: "libera.chat", + servers: [{hostname: "irc.libera.chat", port:6697, isSecure: true}, + {hostname: "irc.libera.chat", port:6667}]}; + networks["slashnet"] = { + displayName: "slashnet", + servers: [{hostname: "irc.slashnet.org", port:6667}]}; + networks["dalnet"] = { + displayName: "dalnet", + servers: [{hostname: "irc.dal.net", port:6667}, + {hostname: "irc.dal.net", port:6697, isSecure: true}, + {hostname: "irc.au.dal.net", port:6667}, + {hostname: "irc.eu.dal.net", port:6667}, + {hostname: "irc.us.dal.net", port:6667}]}; + networks["undernet"] = { + displayName: "undernet", + servers: [{hostname: "irc.undernet.org", port:6667}, + {hostname: "eu.undernet.org", port:6667}, + {hostname: "us.undernet.org", port:6667}]}; + networks["webbnet"] = { + displayName: "webbnet", + servers: [{hostname: "irc.webbnet.info", port:6667}]}; + networks["quakenet"] = { + displayName: "quakenet", + servers: [{hostname: "irc.quakenet.org", port:6667}, + {hostname: "se.quakenet.org", port:6667}, + {hostname: "uk.quakenet.org", port:6667}, + {hostname: "us.quakenet.org", port:6667}]}; + networks["ircnet"] = { + displayName: "ircnet", + servers: [{hostname: "open.ircnet.net", port:6667}, + {hostname: "au.ircnet.org", port:6667}, + {hostname: "eu.ircnet.org", port:6667}, + {hostname: "us.ircnet.org", port:6667}]}; + networks["efnet"] = { + displayName: "efnet", + servers: [{hostname: "irc.efnet.org", port: 6667}]}; + networks["hispano"] = { + displayName: "hispano", + servers: [{hostname: "irc.irc-hispano.org", port: 6667}]}; + networks["freenode"] = { + displayName: "freenode", + servers: [{hostname: "chat.freenode.net", port:6697, isSecure: true}, + {hostname: "chat.freenode.net", port:7000, isSecure: true}, + {hostname: "chat.freenode.net", port:6667}]}; + + for (var name in networks) + networks[name].name = name; + + return networks; +} + +function networksToNetworkList() +{ + var networkList = {}; + + // Create a networkList from client.networks. + for (let name in client.networks) + { + let net = client.networks[name]; + // Skip temporary networks, as they're created to wrap standalone + // servers only. + if (net.temporary) + continue; + + let listNet = { name: net.canonicalName, displayName: net.unicodeName, + servers: [] }; + + // Populate server list (no merging here). + for (let i = 0; i < net.serverList.length; i++) + { + let serv = net.serverList[i]; + let listServ = { hostname: serv.hostname, port: serv.port, + isSecure: serv.isSecure }; + listNet.servers.push(listServ); + } + networkList[net.canonicalName] = listNet; + } + + return networkList; +} + +function networksSyncFromList(networkList) +{ + // Copy to and update client.networks from networkList. + for (let name in networkList) + { + let listNet = networkList[name]; + + // Create new network object if necessary. + if (!client.getNetwork(name)) + client.addNetwork(name, []); + + // Get network object and make sure server list is empty. + let net = client.getNetwork(name); + net.clearServerList(); + + // Update server list. + for (let listServ of listNet.servers) + { + // Make sure these exist. + if (!("isSecure" in listServ)) + listServ.isSecure = false; + + // NOTE: this must match the name given by CIRCServer. + let servName = ":" + listServ.hostname + ":" + listServ.port; + + if (!(servName in net.servers)) + { + net.addServer(listServ.hostname, listServ.port, + listServ.isSecure); + } + let serv = net.servers[servName]; + + serv.isSecure = listServ.isSecure; + } + } + + // Remove network objects that aren't in networkList. + for (let name in client.networks) + { + // Skip temporary networks, as they don't matter. + let net = client.networks[name]; + if (net.temporary) + continue; + if (!(net.canonicalName in networkList)) + client.removeNetwork(net.canonicalName); + } +} + +function networksSaveList(networkList) +{ + var networksFile = new nsLocalFile(client.prefs["profilePath"]); + networksFile.append("networks.json"); + var networksLoader = new JSONSerializer(networksFile); + if (networksLoader.open(">")) + { + networksLoader.serialize(Object.values(networkList)); + networksLoader.close(); + } +} + +function networkHasSecure(serverList) +{ + // Test to see if the network has a secure server. + let hasSecure = false; + for (let s in serverList) + { + if (serverList[s].isSecure) + { + hasSecure = true; + break; + } + } + return hasSecure; +} diff --git a/comm/suite/chatzilla/xul/content/output-base.css b/comm/suite/chatzilla/xul/content/output-base.css new file mode 100644 index 0000000000..bdf9db3f2f --- /dev/null +++ b/comm/suite/chatzilla/xul/content/output-base.css @@ -0,0 +1,528 @@ +/* -*- Mode: Text; 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/. */ + +/* + * This file contains the CSS rules for the output window in ChatZilla. + * The output window is layed out as a table with two columns. The first + * column holds a message type or a nickname, depending on what view the + * message is contained by, and what kind of message it is. The second column + * contains the text of the message. For most message types, ChatZilla displays + * ascii-art instead of the actual code. For example, messages of type "JOIN" + * are displayed as "-->|", and most unclassified message types are displayed + * as "===". If you turn on debug messages (using the options->debug messages + * menuitem) ChatZilla will always show the actual message type. This can be + * helpful when styling a particular response from the IRC server. See the + * description of the msg-type attribute below. + * + * You can modify these styles on your local system by placing your desired + * styles in a file called chatzilla.css in your <profile>/chrome directory. + * (the file won't be there already, you have to create it.) Add the line + * + * @import url(chatzilla.css); + * + * to your userContent.css (also in your <profile>/chrome directory, and + * also not there unless you created it already.) End all CSS rules in your + * new chatzilla.css with !important to override any styles declared here. + * For example, on a Linux system, you would create a file called + * /home/<username>/.mozilla/<username>/chrome/userContent.css (if it + * doesn't already exist), and add the line @import url(chatzilla.css) to it. + * Next, create /home/<username>/.mozilla/<username>/chrome/chatzilla.css, and + * add the text: + * + * .msg { + * font-size: 14pt !important; + * } + * + * .chatzilla-body { + * background: green !important; + * } + * + * Close your browser and restart. When you bring up ChatZilla, it should have + * a 14pt font and a green background. + * + * To learn how to make more useful changes to the ChatZilla output style, read + * on. + * + * All of the output in the message window is contained in an html <TABLE>. + * New messages are composed of <TR> and <TD> elements inside this <TABLE>. + * The message table and it's children have the following CSS classes assigned + * to them: + * + * + .msg-table is used as the class for the surrounding <TABLE>. + * Styles applied to this class will affect all parts of all messages. + * + * + .msg-nested-table is used as the class for the surrounding <TABLE> for + * messages sent from users with long nicknames. A new table is created, and + * nested inside a <TR colspan="2"> of the .msg-table. The rows of this + * table have their classes set as if they were direct children of the + * .msg-table. Placing messages from users with long nicknames in a nested + * table keeps the nickname column from getting too wide. + * + * + .msg is used as the class for the surrounding <TR>. This means that + * any styles applied here will affect the entire message. + * + * + .msg-timestamp is used as the class for the <TD> that has all the time + * information on it. Styles on this class will affect the time stamps + * against messages (but not the format of the time). + * + * + .msg-type is used as the class for the <TD> surrounding the message type + * portion of messages. Styles applied here will only affect message + * types. ie. "-->|", or "[HELP]". + * + * + .msg-user is used as the class for the <TD> surrounding the nickname + * portion of messages. ChatZilla makes use of the :before and :after + * pseudoclasses to decorate nicknames with different characters depending + * on their message type. For example, when a user performs a /me, their + * nickname may be surrounded by asterisks. + * + * + .msg-data is used as the class for the <TD> surrounding the actual text + * of the message. + * + * In addition to CSS class properties, portions of a message may have one + * or mode of the following attributes set: + * + * + view-type is the type of view the message is contained in. The types + * are: + * "IRCClient" for the *client* view + * "IRCNetwork" for network and server views + * "IRCChannel" for channel views + * "IRCUser" for query views + * + * + msg-type is the message type described above. There are too many types + * to list here. Turn on debug messages to force message types to print + * in the left column of the output. + * + * + msg-user is the nickname (in lowercase) of the nickname who sent the + * message. If you sent the message, msg-user will be set to "ME!", so + * that you can style your messages regardless of your nickname. + * + * + msg-dest is the name of the object that the message is directed to. It + * could be a channel name, or a nickname (both in lowercase.) + * + * + dest-type is the type of object the message is directed to. The types + * are: + * "IRCChannel" for messages sent to a channel. + * "IRCUser" for messages sent directly to another user, including + * private messages that appear in a network or channel view (when + * a dedicated query view does not exist.) + * + * + mark is either the text "even" or "odd". When the first user speaks on + * a channel, that message is marked as "even". Messages will continue to + * be marked "even" until a different user speaks, when the mark switches + * to "odd". Each view maintains it's own mark state. An example of how + * ChatZilla marks messages would be: + * + * EVEN: <moe> this deep fat fry-o-later is great. + * EVEN: <moe> It'll deep fat fry a whole buffalo in 30 seconds. + * ODD: <homer> but I'm hungry *now*! + * + * + important is either the text "true", or it is not set at all. If + * important is true, then the message triggered ChatZilla /stalk function. + * This occurs when someone with a nickname matching a pattern in your + * /stalk list speaks, when someone says a word that matches a pattern in + * your /stalk list, or when someone says your nickname. + */ + +#splash-wrapper { + display: flex; + height: 100vh; + justify-content: center; + align-items: center; + overflow: hidden; +} + +/****************************************************************************** + * basic classes * + ******************************************************************************/ + +.chatzilla-body { /* The topmost container in the ChatZilla */ + margin: 0px 0px 0px 0px; /* output window. */ + background: #FFFFFF; + color: #000000; +} + +a:link { + color: #0000EE; +} +a:active { + color: #EE0000; +} +a:visited { + color: #551A8B; +} + +/* links */ +a.chatzilla-link { + text-decoration: none; + direction: ltr; + unicode-bidi: embed; +} + +/* link hover effect */ +a.chatzilla-link:hover { + text-decoration: underline; +} + +/* line marker */ +.chatzilla-line-marker { + box-shadow: 0px 2px black; +} + +/* basic styles */ +.chatzilla-highlight[name="Large"] { + font-size: larger; +} + +.chatzilla-highlight[name="Small"] { + font-size: smaller; +} + +.chatzilla-highlight[name="Bold text"], +.chatzilla-bold, a.chatzilla-bold.chatzilla-link { + font-weight: bold; +} + +.chatzilla-italic { + font-style: italic; +} + +/* In CSS, text-decoration is a list of decorations to apply to the text. + * However, as it is just one property, there is no way to apply it additively; + * instead, we're forced to have all the 7 combinations (of 8 - one is none). + */ + +.chatzilla-overline { + text-decoration: overline; +} + +.chatzilla-strikethrough { + text-decoration: line-through; +} + +.chatzilla-underline, +a.chatzilla-underline.chatzilla-link { + text-decoration: underline; +} + +.chatzilla-overline.chatzilla-strikethrough { + text-decoration: overline line-through; +} + +.chatzilla-overline.chatzilla-underline, +a.chatzilla-overline.chatzilla-underline.chatzilla-link { + text-decoration: overline underline; +} + +.chatzilla-strikethrough.chatzilla-underline, +a.chatzilla-strikethrough.chatzilla-underline.chatzilla-link { + text-decoration: line-through underline; +} + +.chatzilla-overline.chatzilla-strikethrough.chatzilla-underline, +a.chatzilla-overline.chatzilla-strikethrough.chatzilla-underline.chatzilla-link { + text-decoration: overline line-through underline; +} + +.chatzilla-teletype { + font-family: monospace; +} + +.chatzilla-rheet { + font-weight: bold; +} + +.chatzilla-decor { + display: none; +} + +/* mirc colors */ +.chatzilla-fg00, a.chatzilla-fg00.chatzilla-link { + color: #FFFFFF; +} + +.chatzilla-fg01, a.chatzilla-fg01.chatzilla-link { + color: #000000; +} + +.chatzilla-fg02, a.chatzilla-fg02.chatzilla-link { + color: #00007F; +} + +.chatzilla-fg03, a.chatzilla-fg03.chatzilla-link { + color: #009300; +} + +.chatzilla-fg04, a.chatzilla-fg04.chatzilla-link { + color: #FF0000; +} + +.chatzilla-fg05, a.chatzilla-fg05.chatzilla-link { + color: #7F0000; +} + +.chatzilla-fg06, a.chatzilla-fg06.chatzilla-link { + color: #9C009C; +} + +.chatzilla-fg07, a.chatzilla-fg07.chatzilla-link { + color: #FC7F00; +} + +.chatzilla-fg08, a.chatzilla-fg08.chatzilla-link { + color: #FFFF00; +} + +.chatzilla-fg09, a.chatzilla-fg09.chatzilla-link { + color: #00FC00; +} + +.chatzilla-fg10, a.chatzilla-fg10.chatzilla-link { + color: #009393; +} + +.chatzilla-fg11, a.chatzilla-fg11.chatzilla-link { + color: #00FFFF; +} + +.chatzilla-fg12, a.chatzilla-fg12.chatzilla-link { + color: #0000FC; +} + +.chatzilla-fg13, a.chatzilla-fg13.chatzilla-link { + color: #FF00FF; +} + +.chatzilla-fg14, a.chatzilla-fg14.chatzilla-link { + color: #7F7F7F; +} + +.chatzilla-fg15, a.chatzilla-fg15.chatzilla-link { + color: #D2D2D2; +} + +.chatzilla-bg00, a.chatzilla-bg00.chatzilla-link { + background-color: #FFFFFF; +} + +.chatzilla-bg01, a.chatzilla-bg01.chatzilla-link { + background-color: #000000; +} + +.chatzilla-bg02, a.chatzilla-bg02.chatzilla-link { + background-color: #00007F; +} + +.chatzilla-bg03, a.chatzilla-bg03.chatzilla-link { + background-color: #009300; +} + +.chatzilla-bg04, a.chatzilla-bg04.chatzilla-link { + background-color: #FF0000; +} + +.chatzilla-bg05, a.chatzilla-bg05.chatzilla-link { + background-color: #7F0000; +} + +.chatzilla-bg06, a.chatzilla-bg06.chatzilla-link { + background-color: #9C009C; +} + +.chatzilla-bg07, a.chatzilla-bg07.chatzilla-link { + background-color: #FC7F00; +} + +.chatzilla-bg08, a.chatzilla-bg08.chatzilla-link { + background-color: #FFFF00; +} + +.chatzilla-bg09, a.chatzilla-bg09.chatzilla-link { + background-color: #00FC00; +} + +.chatzilla-bg10, a.chatzilla-bg10.chatzilla-link { + background-color: #009393; +} + +.chatzilla-bg11, a.chatzilla-bg11.chatzilla-link { + background-color: #00FFFF; +} + +.chatzilla-bg12, a.chatzilla-bg12.chatzilla-link { + background-color: #0000FC; +} + +.chatzilla-bg13, a.chatzilla-bg13.chatzilla-link { + background-color: #FF00FF; +} + +.chatzilla-bg14, a.chatzilla-bg14.chatzilla-link { + background-color: #7F7F7F; +} + +.chatzilla-bg15, a.chatzilla-bg15.chatzilla-link { + background-color: #D2D2D2; +} + +.chatzilla-control-char:before { + content: "[\\"; +} + +.chatzilla-control-char:after { + content: "]"; +} + +/* smiley faces */ +.chatzilla-emote-txt { /* emoticon text inside */ + font-size: larger; +} + +/****************************************************************************** + * message class base definitions * + ******************************************************************************/ + +.msg-table { /* <TABLE> containing all of the */ + width: 100%; /* messages. */ +} + +.msg-nested-table { /* <TABLE> nested inside */ + width: 100%; /* .msg-table for users with long */ + margin: 0px; /* nicknames. */ + border: 0px; + border-spacing: 0px; + padding: 0px; +} + +.msg { /* .msg = a single message in the */ + width: 100%; /* output window */ +} + +.msg-timestamp { /* .msg-timestamp = timestamp for */ + font-style: normal !important; /* the message, done using */ + vertical-align: top; /* :before and content. */ + white-space: nowrap; +} + +.msg-type { /* .msg-type = message type */ + font-variant: small-caps; /* indicator */ + font-size: 90%; + padding-right: 10px; + text-align: right; + vertical-align: top; + white-space: nowrap; +} + +.msg-user { /* msg-user = nickname portion of */ + text-align: right; /* a message (channel and query */ + vertical-align: top; /* views) */ + white-space: nowrap; +} + +.msg-data { /* .msg-data = the text portion */ + padding: 1px 1px 1px 3px; /* of a message */ + width: 100%; + white-space: pre-wrap; +} + + +/****************************************************************************** + * message class specific definitions * + ******************************************************************************/ + +/* msg-user is the nickname of the person who spoke, or "ME!" if you said it. + * msg-type is the type of the message, taken from the irc message. If you + * turn on debug messages (options->debug messages), the msg-types will be + * displayed to the left of the messages for all messages except: + * PRIVMSG: when a user sends you, or a channel you are on a message. + * ACTION: when a user performs a /me. + * NOTIFY: when a server or user sends you a notification. + */ +.msg[msg-user="|"] .msg-data, /* messages from common "bulk */ +.msg[msg-user="||"] .msg-data, /* paste" nicks */ +.msg[msg-user="|||"] .msg-data, +.msg[msg-user="]"] .msg-data, +.msg[msg-user="["] .msg-data, +.msg[msg-type="372"] .msg-data, /* MOTD */ +.msg[msg-type="EVAL-IN"] .msg-data, /* /eval results */ +.msg[msg-type="EVAL-OUT"] .msg-data { + font-size: 90%; + font-family: monospace; +} + +.msg[msg-type="USAGE"] .msg-data { + font-style: italic; +} + +.msg[msg-type="HELP"] .msg-data { + font-weight: normal; +} + +.msg[msg-type="ACTION"] .msg-user { + font-style: italic; +} + +.msg[important="true"] .msg-user { + font-weight: bold; +} + +/****************************************************************************** + * nickname decorations * + ******************************************************************************/ + +/* :before and :after pseudoclasses form the decorations around nicknames */ +.msg-user:before { + content: "<"; +} +.msg-user:after { + content: ">"; +} +.msg[important="true"] .msg-user:before { + font-weight: bold; +} +.msg[important="true"] .msg-user:after { + font-weight: bold; +} +.msg[msg-user$="ME!"] .msg-user:before { + content: "<"; +} +.msg[msg-user$="ME!"] .msg-user:after { + content: ">"; +} +.msg[msg-type="ACTION"] .msg-user:before, +.msg[msg-type="ACTION"] .msg-user:after { + content: ""; +} +.msg[msg-type="NOTICE"] .msg-user:before { + content: "["; +} +.msg[msg-type="NOTICE"] .msg-user:after { + content: "]"; +} + +/* private messages in a query window */ +.msg[view-type="IRCUser"] .msg-user:before { + content: "{"; +} +.msg[view-type="IRCUser"] .msg-user:after { + content: "}"; +} +.msg[view-type="IRCUser"][msg-dest$="ME!"] .msg-user:before { + content: "{"; +} +.msg[view-type="IRCUser"][msg-dest$="ME!"] .msg-user:after { + content: "}"; +} + +/* messages 'to' or 'from' somewhere other than where displayed */ +.msg[to-other] .msg-user:before { + content: "to("; +} +.msg[from-other] .msg-user:before { + content: "from("; +} +.msg[to-other] .msg-user:after, +.msg[from-other] .msg-user:after { + content: ")"; +} diff --git a/comm/suite/chatzilla/xul/content/output-window.html b/comm/suite/chatzilla/xul/content/output-window.html new file mode 100644 index 0000000000..2155c282c3 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/output-window.html @@ -0,0 +1,209 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style type="text/css"> + [hidden="true"] { + display: none; + } + + .header-outer { + position: fixed; + top: 0px; + left: 0px; + right: 0px; + margin: 0px; + padding: 0px; + } + + .header { + background-color: white; + color: black; + margin: 2px; + border: 1px black solid; + } + + .h-table, + #net-url, + #ch-url, + #dcc-chat-title { + width: 100%; + } + + #splash { + font-size: 24pt; + font-weight: bold; + text-align: center; + } + + #cli-version-container { + text-align: center; + width: 100%; + } + + #usr-descnodes, + #ch-topicnodes { + line-height: 110%; + } + + #ch-usercount, + #ch-modestr, + #net-lag, + #dcc-file-progress { + white-space: nowrap; + } + + .label { + font-weight: bold; + text-align: right; + vertical-align: top; + white-space: nowrap; + padding-right: 0.5em; + } + + .value { + vertical-align: top; + padding-right: 1em; + } + + #usr-title, + #usr-descnodes { + text-align: center; + } + </style> + + <script type="application/x-javascript" src="chrome://chatzilla/content/output-window.js"></script> + </head> + + <body class="chatzilla-body"> + + <div class="header-outer"> + + <div class="header" id="cli-container" hidden="true"> + <table class="h-table"> + <tbody> + <tr> + <td class="label" localize="output.knownnets"></td> + <td class="value" id="cli-netcount"></td> + <td class="label" id="cli-version-container" + condition="yellow">ChatZilla <span id="cli-version">error</span></td> + <td class="label" localize="output.connnets"></td> + <td class="value" id="cli-connectcount" localize="none"></td> + </tr> + </tbody> + </table> + </div> + + <div class="header" id="net-container" hidden="true"> + <table class="h-table"> + <tbody> + <tr> + <td class="label" id="net-url-l" localize="output.url"></td> + <td class="value crop-right" id="net-url"> + <a id="net-url-anchor" class="chatzilla-link" + href="irc://foo/bar">irc://foo/bar</a> + </td> + <td class="value" id="net-status" + condition="red" localize="output.notconn"></td> + <td class="label" id="net-lag-l" localize="output.lag"></td> + <td class="value" id="net-lag" localize="unknown"></td> + </tr> + </tbody> + </table> + </div> + + <div class="header" id="ch-container" hidden="true"> + <table class="h-table"> + <tbody> + <tr> + <td class="label" id="ch-url-l" localize="output.url"></td> + <td class="value crop-right" id="ch-url"> + <a id="ch-url-anchor" class="chatzilla-link" + href="irc://foo/bar">irc://foo/bar</a> + </td> + <td class="label" id="ch-modestr-l" localize="output.mode"></td> + <td class="value" id="ch-modestr" localize="none"></td> + <td class="label" id="ch-usercount-l" localize="output.users"></td> + <td class="value" id="ch-usercount" localize="none"></td> + </tr> + <tr onclick="onTopicNodesClick(event);" style="cursor:default"> + <td class="label" id="ch-topicnodes-l" localize="output.topic"></td> + <td class="value" colspan="6"> + <span id="ch-topicnodes" localize="none"></span> + <input hidden="true" id="ch-topicinput" style="width:90%" + onblur="cancelTopicEdit();" + onkeypress="onTopicKeypress(event);"/> + <input type="button" hidden="true" id="ch-topiccancel" + onclick="setTimeout(cancelTopicEdit, 0, true);" + localize="output.cancel"/> + </td> + </tr> + </tbody> + </table> + </div> + + <div class="header" id="usr-container" hidden="true"> + <table class="h-table"> + <tbody> + <tr> + <td class="label" localize="output.url"></td> + <td class="value crop-right" width="100%"> + <a id="usr-url-anchor" class="chatzilla-link" + href="irc://foo/bar">irc://foo/bar</a> + </td> + <td class="label" id="usr-serverstr-l" localize="output.via"></td> + <td class="value" id="usr-serverstr" localize="none"></td> + </tr> + <tr> + <td id="usr-title" colspan="4" localize="none"></td> + </tr> + <tr> + <td id="usr-descnodes" colspan="4" localize="none"></td> + </tr> + </tbody> + </table> + </div> + + <div class="header" id="dcc-chat-container" hidden="true"> + <table class="h-table"> + <tbody> + <tr> + <td id="dcc-chat-title" localize="none"></td> + <td class="label" id="dcc-chat-remotestr-l" localize="output.to"></td> + <td class="value" id="dcc-chat-remotestr" localize="none"></td> + </tr> + </tbody> + </table> + </div> + + <div class="header" id="dcc-file-container" hidden="true"> + <table class="h-table"> + <tbody> + <tr> + <td class="label" localize="output.file"></td> + <td class="value crop-right" id="dcc-file-file" width="100%"></td> + <td class="label" localize="output.progress"></td> + <td class="value" id="dcc-file-progress" localize="unknown"></td> + </tr> + <tr> + <td colspan="4" class="progress-bg"> + <table id="dcc-file-progressbar" width="0%"><tbody><tr> + <td class="progress-fg"> </td> + </tr></tbody></table> + </td> + </tr> + </tbody> + </table> + </div> + </div> + + <div id="messages-outer" hidden="true"> + <div id="splash-wrapper"><div id="splash"></div></div> + <div id="output"></div> + </div> + </body> +</html> diff --git a/comm/suite/chatzilla/xul/content/output-window.js b/comm/suite/chatzilla/xul/content/output-window.js new file mode 100644 index 0000000000..8f670ec3ec --- /dev/null +++ b/comm/suite/chatzilla/xul/content/output-window.js @@ -0,0 +1,588 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var initialized = false; + +var view; +var client; +var mainWindow; +var clickHandler; + +var dd; +var getMsg; +var getObjectDetails; + +var header = null; +var headers = { + IRCClient: { + prefix: "cli-", + fields: ["container", "netcount", "version-container", "version", + "connectcount"], + update: updateClient + }, + + IRCNetwork: { + prefix: "net-", + fields: ["container", "url-anchor", "status", "lag"], + update: updateNetwork + }, + + IRCChannel: { + prefix: "ch-", + fields: ["container", "url-anchor", "modestr", "usercount", + "topicnodes", "topicinput", "topiccancel"], + update: updateChannel + }, + + IRCUser: { + prefix: "usr-", + fields: ["container", "url-anchor", "serverstr", "title", + "descnodes"], + update: updateUser + }, + + IRCDCCChat: { + prefix: "dcc-chat-", + fields: ["container", "remotestr", "title"], + update: updateDCCChat + }, + + IRCDCCFileTransfer: { + prefix: "dcc-file-", + fields: ["container", "file", "progress", "progressbar"], + update: updateDCCFile + } +}; + +var initOutputWindow = stock_initOutputWindow; + +function stock_initOutputWindow(newClient, newView, newClickHandler) +{ + function initHeader() + { + /* it's better if we wait a half a second before poking at these + * dom nodes. */ + setHeaderState(view.prefs["displayHeader"]); + updateHeader(); + var div = document.getElementById("messages-outer"); + div.removeAttribute("hidden"); + window.scrollTo(0, window.document.body.clientHeight); + }; + + client = newClient; + view = newView; + clickHandler = newClickHandler; + mainWindow = client.mainWindow; + + client.messageManager.importBundle(client.defaultBundle, window); + + getMsg = mainWindow.getMsg; + getObjectDetails = mainWindow.getObjectDetails; + dd = mainWindow.dd; + + // Wheee... localize stuff! + //var nodes = document.getElementsByAttribute("localize", "*"); + var nodes = document.getElementsByTagName("*"); + for (var i = 0; i < nodes.length; i++) + { + if (nodes[i].hasAttribute("localize")) + { + var msg = nodes[i].getAttribute("localize"); + msg = getMsg("msg." + msg); + if (nodes[i].nodeName.toLowerCase() == "input") + nodes[i].value = msg; + else + nodes[i].appendChild(document.createTextNode(msg)); + } + } + + changeCSS("chrome://chatzilla/content/output-base.css", "cz-css-base"); + changeCSS(view.prefs["motif.current"]); + updateMotifSettings(); + + var output = document.getElementById("output"); + output.appendChild(adoptNode(view.messages)); + + if (view.TYPE in headers) + { + header = cacheNodes(headers[view.TYPE].prefix, + headers[view.TYPE].fields); + // Turn off accessibility announcements: they're useless as all these + // changes are in the "log" as well, normally. + // We're setting the attribute here instead of in the HTML to cope with + // custom output windows and so we set it only on the Right header + // for this view. + header["container"].setAttribute("aria-live", "off"); + header.update = headers[view.TYPE].update; + } + + var name; + if ("unicodeName" in view) + name = view.unicodeName; + else + name = view.name; + updateSplash(name); + + setTimeout(initHeader, 500); + + initialized = true; +} + +function onTopicNodesClick(e) +{ + if (!clickHandler(e)) + { + if (e.which != 1) + return; + + startTopicEdit(); + } + + e.stopPropagation(); +} + +function onTopicKeypress(e) +{ + switch (e.keyCode) + { + case 13: /* enter */ + var topic = header["topicinput"].value; + topic = mainWindow.replaceColorCodes(topic); + view.setTopic(topic); + cancelTopicEdit(true); + view.dispatch("focus-input"); + break; + + case 27: /* esc */ + cancelTopicEdit(true); + view.dispatch("focus-input"); + break; + + default: + client.mainWindow.onInputKeyPress(e); + } +} + +function startTopicEdit() +{ + var me = view.getUser(view.parent.me.unicodeName); + if (!me || (!view.mode.publicTopic && !me.isOp && !me.isHalfOp) || + !hasAttribute("topicinput", "hidden")) + { + return; + } + + header["topicinput"].value = mainWindow.decodeColorCodes(view.topic); + + header["topicnodes"].setAttribute("hidden", "true") + header["topicinput"].removeAttribute("hidden"); + header["topiccancel"].removeAttribute("hidden"); + header["topicinput"].focus(); + header["topicinput"].selectionStart = 0; +} + +function cancelTopicEdit(force) +{ + var originalTopic = mainWindow.decodeColorCodes(view.topic); + if (!hasAttribute("topicnodes", "hidden") || + (!force && (header["topicinput"].value != originalTopic))) + { + return; + } + + header["topicinput"].setAttribute("hidden", "true"); + header["topiccancel"].setAttribute("hidden", "true"); + header["topicnodes"].removeAttribute("hidden"); +} + +function cacheNodes(pfx, ary, nodes) +{ + if (!nodes) + nodes = new Object(); + + for (var i = 0; i < ary.length; ++i) + nodes[ary[i]] = document.getElementById(pfx + ary[i]); + + return nodes; +} + +function changeCSS(url, id) +{ + if (!id) + id = "main-css"; + + var node = document.getElementById(id); + + if (!node) + { + node = document.createElement("link"); + node.setAttribute("id", id); + node.setAttribute("rel", "stylesheet"); + node.setAttribute("type", "text/css"); + var head = document.getElementsByTagName("head")[0]; + head.appendChild(node); + } + else + { + if (node.getAttribute("href") == url) + return; + } + + node.setAttribute("href", url); + window.scrollTo(0, window.document.body.clientHeight); +} + +function scrollToElement(element, position) +{ + /* The following values can be used for element: + * selection - current selected text. + * marker - the activity marker. + * [any DOM node] - anything :) + * + * The following values can be used for position: + * top - scroll so it is at the top. + * center - scroll so it is in the middle. + * bottom - scroll so it is at the bottom. + * inview - scroll so it is in view. + */ + switch (element) + { + case "selection": + var sel = window.getSelection(); + if (sel) + element = sel.anchorNode; + else + element = null; + break; + + case "marker": + if ("getActivityMarker" in view) + element = view.getActivityMarker(); + else + element = null; + break; + } + if (!element) + return; + + // Calculate element's position in document. + var pos = { top: 0, center: 0, bottom: 0 }; + // Find first parent with offset data. + while (element && !("offsetParent" in element)) + element = element.parentNode; + var elt = element; + // Calc total offset data. + while (elt) + { + pos.top += 0 + elt.offsetTop; + elt = elt.offsetParent; + } + pos.center = pos.top + element.offsetHeight / 2; + pos.bottom = pos.top + element.offsetHeight; + + // Store the positions to align the element with. + var cont = { top: 0, center: window.innerHeight / 2, + bottom: window.innerHeight }; + if (!hasAttribute("container", "hidden")) + { + /* Offset height doesn't include the margins, so we get to do that + * ourselves via getComputedStyle(). We're assuming that will return + * a px value, which is all but guaranteed. + */ + var headerHeight = header["container"].offsetHeight; + var css = getComputedStyle(header["container"], null); + headerHeight += parseInt(css.marginTop) + parseInt(css.marginBottom); + cont.top += headerHeight; + cont.center += headerHeight / 2; + } + + // Pick between 'top' and 'bottom' for 'inview' position. + if (position == "inview") + { + if (pos.top - window.scrollY < cont.top) + position = "top"; + else if (pos.bottom - window.scrollY > cont.bottom) + position = "bottom"; + else + return; + } + + window.scrollTo(0, pos[position] - cont[position]); +} + +function updateMotifSettings(existingTimeout) +{ + // Try... catch with a repeat to cope with the style sheet not being loaded + const TIMEOUT = 100; + try + { + existingTimeout += TIMEOUT; + view.motifSettings = getMotifSettings(); + } + catch(ex) + { + if (existingTimeout >= 30000) // Stop after trying for 30 seconds + return; + if (ex.name == "NS_ERROR_DOM_INVALID_ACCESS_ERR") //not ready, try again + setTimeout(updateMotifSettings, TIMEOUT, existingTimeout); + else // something else, panic! + dd(ex); + } +} + +function getMotifSettings() +{ + var re = new RegExp("czsettings\\.(\\w*)", "i"); + var rules = document.getElementById("main-css").sheet.cssRules; + var rv = new Object(); + var ary; + // Copy any settings, which are available in the motif using the + // "CZSETTINGS" selector. We only store the regexp match after checking + // the rule type because selectorText is not defined on other rule types. + for (var i = 0; i < rules.length; i++) + { + if ((rules[i].type == CSSRule.STYLE_RULE) && + ((ary = rules[i].selectorText.match(re)) != null)) + { + rv[ary[1]] = true; + } + } + return rv; +} + +function adoptNode(node) +{ + return client.adoptNode(node, document); +} + +function setText(field, text, checkCondition) +{ + if (!header[field].firstChild) + header[field].appendChild(document.createTextNode("")); + + if (typeof text != "string") + { + text = MSG_UNKNOWN; + if (checkCondition) + setAttribute(field, "condition", "red"); + } + else if (checkCondition) + { + setAttribute(field, "condition", "green"); + } + + header[field].firstChild.data = text; +} + +function setAttribute(field, name, value) +{ + if (!value) + value = "true"; + + header[field].setAttribute(name, value); +} + +function removeAttribute(field, name) +{ + header[field].removeAttribute(name); +} + +function hasAttribute(field, name) +{ + return header[field].hasAttribute(name); +} + +function setHeaderState(state) +{ + if (header) + { + if (state) + { + removeAttribute("container", "hidden"); + updateHeader(); + } + else + { + setAttribute("container", "hidden"); + } + } +} + +function updateHeader() +{ + document.title = view.getURL(); + + if (!header || hasAttribute("container", "hidden")) + return; + + for (var id in header) + { + var value; + + if (id == "url-anchor") + { + value = view.getURL(); + setAttribute("url-anchor", "href", value); + setText("url-anchor", value); + } + else if (id in view) + { + setText(id, view[id]); + } + } + + if (header.update) + header.update(); +} + +function updateClient() +{ + var n = 0, c = 0; + for (name in client.networks) + { + ++n; + if (client.networks[name].isConnected()) + ++c; + } + + setAttribute("version-container", "title", client.userAgent); + setAttribute("version-container", "condition", mainWindow.__cz_condition); + setText("version", mainWindow.__cz_version); + setText("netcount", String(n)); + setText("connectcount", String(c)); +} + +function updateNetwork() +{ + if (view.state == mainWindow.NET_CONNECTING) + { + setText("status", MSG_CONNECTING); + setAttribute("status","condition", "yellow"); + removeAttribute("status", "title"); + setText("lag", MSG_UNKNOWN); + } + else if (view.isConnected()) + { + setText("status", MSG_CONNECTED); + setAttribute("status","condition", "green"); + setAttribute("status", "title", + getMsg(MSG_CONNECT_VIA, view.primServ.unicodeName)); + var lag = view.primServ.lag; + if (lag != -1) + setText("lag", getMsg(MSG_FMT_SECONDS, lag.toFixed(2))); + else + setText("lag", MSG_UNKNOWN); + + } + else + { + setText("status", MSG_DISCONNECTED); + setAttribute("status","condition", "red"); + removeAttribute("status", "title"); + setText("lag", MSG_UNKNOWN); + } +} + +function updateChannel() +{ + header["topicnodes"].removeChild(header["topicnodes"].firstChild); + + if (view.active) + { + var str = view.mode.getModeStr(); + if (!str) + str = MSG_NO_MODE; + setText("modestr", str); + setAttribute("modestr", "condition", "green"); + + setText("usercount", getMsg(MSG_FMT_USERCOUNT, + [view.getUsersLength(), view.opCount, + view.halfopCount, view.voiceCount])); + setAttribute("usercount", "condition", "green"); + + if (view.topic) + { + var data = getObjectDetails(view); + data.dontLogURLs = true; + var mailto = client.prefs["munger.mailto"]; + client.munger.getRule(".mailto").enabled = mailto; + var nodes = client.munger.munge(view.topic, null, data); + client.munger.getRule(".mailto").enabled = false; + header["topicnodes"].appendChild(adoptNode(nodes)); + } + else + { + setText("topicnodes", MSG_NONE); + } + } + else + { + setText("modestr", MSG_UNKNOWN); + setAttribute("modestr", "condition", "red"); + setText("usercount", MSG_UNKNOWN); + setAttribute("usercount", "condition", "red"); + setText("topicnodes", MSG_UNKNOWN); + } + +} + +function updateUser() +{ + var source; + if (view.name) + source = "<" + view.name + "@" + view.host + ">"; + else + source = MSG_UNKNOWN; + + if (view.parent.isConnected) + setText("serverstr", view.connectionHost, true); + else + setText("serverstr", null, true); + + setText("title", getMsg(MSG_TITLE_USER, [view.unicodeName, source])); + + header["descnodes"].removeChild(header["descnodes"].firstChild); + if (typeof view.desc != "undefined") + { + var data = getObjectDetails(view); + data.dontLogURLs = true; + var nodes = client.munger.munge(view.desc, null, data); + header["descnodes"].appendChild(adoptNode(nodes)); + } + else + { + setText("descnodes", ""); + } +} + +function updateDCCChat() +{ + if (view.state.state == 4) + setText("remotestr", view.remoteIP + ":" + view.port, true); + else + setText("remotestr", null, true); + + setText("title", getMsg(MSG_TITLE_DCCCHAT, view.user.unicodeName)); +} + +function updateDCCFile() +{ + var pcent = view.progress; + + setText("file", view.filename); + setText("progress", getMsg(MSG_DCCFILE_PROGRESS, + [pcent, mainWindow.getSISize(view.position), + mainWindow.getSISize(view.size), + mainWindow.getSISpeed(view.speed)])); + + setAttribute("progressbar", "width", pcent + "%"); +} + +function updateSplash(content) +{ + var splash = document.getElementById("splash"); + splash.appendChild(document.createTextNode(content)); +}
\ No newline at end of file diff --git a/comm/suite/chatzilla/xul/content/popups.xul b/comm/suite/chatzilla/xul/content/popups.xul new file mode 100644 index 0000000000..aed0200cdf --- /dev/null +++ b/comm/suite/chatzilla/xul/content/popups.xul @@ -0,0 +1,124 @@ +<?xml version="1.0"?> + +<!-- + - + - 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/. --> + +<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/chatzilla.dtd" > + +<?xml-stylesheet href="chrome://chatzilla/content/output-base.css" type="text/css"?> + +<overlay id="chatzilla-popup-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <overlaytarget id="popup-overlay-target"> + + <tooltip id="percentTooltip"> + <grid> + <columns> + <column /> + <column /> + </columns> + <rows> + <row> + <label value="%U"/> + <label value="&Underline.label;"/> + </row> + <row> + <label value="%B"/> + <label value="&Bold.label;"/> + </row> + <row> + <label value="%R"/> + <label value="&Reverse.label;"/> + </row> + <row> + <label value="%O"/> + <label value="&Normal.label;"/> + </row> + <row> + <label value="%C"/> + <label value="&Color.label;"/> + </row> + <row> + <label value="%%C"/> + <label value="%C"/> + </row> + </rows> + </grid> + </tooltip> + <tooltip id="colorTooltip" orient="vertical"> + <label value="%Cxx[,yy] &ForeBack.label;"/> + <grid> + <columns> + <column /> + <column /> + <column /> + <column /> + <column /> + <column /> + <column /> + <column /> + </columns> + <rows> + <row> + <box class="colorGrid chatzilla-bg00 chatzilla-fg01"> + <label value="0"/> + </box> + <box class="colorGrid chatzilla-bg01 chatzilla-fg00"> + <label value="1"/> + </box> + <box class="colorGrid chatzilla-bg02 chatzilla-fg00"> + <label value="2"/> + </box> + <box class="colorGrid chatzilla-bg03 chatzilla-fg00"> + <label value="3"/> + </box> + <box class="colorGrid chatzilla-bg04 chatzilla-fg00"> + <label value="4"/> + </box> + <box class="colorGrid chatzilla-bg05 chatzilla-fg00"> + <label value="5"/> + </box> + <box class="colorGrid chatzilla-bg06 chatzilla-fg00"> + <label value="6"/> + </box> + <box class="colorGrid chatzilla-bg07 chatzilla-fg00"> + <label value="7"/> + </box> + </row> + <row> + <box class="colorGrid chatzilla-bg08 chatzilla-fg01"> + <label value="8"/> + </box> + <box class="colorGrid chatzilla-bg09 chatzilla-fg01"> + <label value="9"/> + </box> + <box class="colorGrid chatzilla-bg10 chatzilla-fg01"> + <label value="10"/> + </box> + <box class="colorGrid chatzilla-bg11 chatzilla-fg01"> + <label value="11"/> + </box> + <box class="colorGrid chatzilla-bg12 chatzilla-fg01"> + <label value="12"/> + </box> + <box class="colorGrid chatzilla-bg13 chatzilla-fg01"> + <label value="13"/> + </box> + <box class="colorGrid chatzilla-bg14 chatzilla-fg01"> + <label value="14"/> + </box> + <box class="colorGrid chatzilla-bg15 chatzilla-fg01"> + <label value="15"/> + </box> + </row> + </rows> + </grid> + </tooltip> + + </overlaytarget> + +</overlay> diff --git a/comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul b/comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul new file mode 100644 index 0000000000..e69fb1a165 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul @@ -0,0 +1,24 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?> +<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/pref-irc.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="chatzilla_pane" + label="&pref-irc.window.title;"> + + <label>&pref-irc.open.desc;</label> + <separator/> + <hbox pack="center"> + <button label="&pref-irc.open.label;" accesskey="&pref-irc.open.accesskey;" + oncommand="this.disabled = true; + window.openDialog('chrome://chatzilla/content/config.xul', + '', 'chrome,modal,resizable'); + this.disabled = false;"/> + </hbox> + </prefpane> +</overlay> diff --git a/comm/suite/chatzilla/xul/content/prefs.js b/comm/suite/chatzilla/xul/content/prefs.js new file mode 100644 index 0000000000..f4ef4fc7de --- /dev/null +++ b/comm/suite/chatzilla/xul/content/prefs.js @@ -0,0 +1,1213 @@ +/* -*- 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 DEFAULT_NICK = "IRCMonkey" + +function initPrefs() +{ + function makeLogNameClient() + { + return makeLogName(client, "client"); + }; + + client.prefManager = new PrefManager("extensions.irc.", + client.defaultBundle); + client.prefManagers = [client.prefManager]; + + client.prefs = client.prefManager.prefs; + + var profilePath = getSpecialDirectory("ProfD"); + profilePath.append("chatzilla"); + + client.prefManager.addPref("profilePath", profilePath.path, null, null, + "hidden"); + + profilePath = new nsLocalFile(client.prefs["profilePath"]); + + if (!profilePath.exists()) + mkdir(profilePath); + + client.prefManager.profilePath = profilePath; + + var scriptPath = profilePath.clone(); + scriptPath.append("scripts"); + if (!scriptPath.exists()) + mkdir(scriptPath); + + var logPath = profilePath.clone(); + logPath.append("logs"); + if (!logPath.exists()) + mkdir(logPath); + client.prefManager.logPath = logPath; + + var downloadsPath = profilePath.clone(); + downloadsPath.append("downloads"); + if (!downloadsPath.exists()) + mkdir(downloadsPath); + + var logDefault = client.prefManager.logPath.clone(); + logDefault.append(escapeFileName("client.log")); + + // Set up default nickname, if possible. + var defaultNick = DEFAULT_NICK; + var en = getService("@mozilla.org/process/environment;1", "nsIEnvironment"); + if (en) + { + /* Get the enviroment variables used by various OSes: + * USER - Linux, macOS and other *nix-types. + * USERNAME - Windows. + * LOGNAME - *nix again. + */ + const vars = ["USER", "USERNAME", "LOGNAME"]; + + for (var i = 0; i < vars.length; i++) + { + var nick = en.get(vars[i]); + if (nick) + { + defaultNick = nick.replace(/ /g, "_"); + break; + } + } + } + + // Set a property so network ident prefs get the same group later: + client.prefManager.identGroup = ".connect"; + // Linux and OS X won't let non-root listen on port 113. + if ((client.platform == "Linux") || (client.platform == "Mac")) + client.prefManager.identGroup = "hidden"; + + var prefs = + [ + ["activityFlashDelay", 200, "hidden"], + ["alert.overlapDelay", 50, "hidden"], + ["alert.floodDensity", 290, "hidden"], + ["alert.floodDispersion", 200, "hidden"], + ["alert.enabled", true, ".palert"], + ["alert.globalEnabled", true, "global.palertconfig"], + ["alert.clickable", true, "hidden"], + ["alert.nonFocusedOnly", true, "global.palertconfig"], + ["alert.channel.event", false, ".palert"], + ["alert.channel.chat", false, ".palert"], + ["alert.channel.stalk", true, ".palert"], + ["alert.user.chat", true, ".palert"], + ["aliases", [], "lists.aliases"], + ["autoAwayCap", 300, "global"], + ["autoAwayPeriod", 2, "appearance.misc"], + ["autoMarker", false, "appearance.misc"], + ["autoperform.channel", [], "lists.autoperform"], + ["autoperform.client", [], "lists.autoperform"], + ["autoperform.network", [], "lists.autoperform"], + ["autoperform.user", ["whois"], "lists.autoperform"], + ["autoRejoin", false, ".connect"], + ["away", "", "hidden"], + ["awayIdleMsg", "", ".ident"], + ["awayIdleTime", 0, ".ident"], + ["awayNick", "", ".ident"], + ["bugKeyword", "bug", "appearance.misc"], + ["bugURL", "https://bugzilla.mozilla.org/show_bug.cgi?id=%s", + "appearance.misc"], + ["bugURL.comment", "#c%s", "appearance.misc"], + ["channelHeader", true, "global.header"], + ["channelLog", false, "global.log"], + ["channelMaxLines", 500, "global.maxLines"], + ["charset", "utf-8", ".connect"], + ["clientMaxLines", 200, "global.maxLines"], + ["collapseActions", true, "appearance.misc"], + ["collapseMsgs", false, "appearance.misc"], + ["conference.limit", 150, "appearance.misc"], + ["connectTries", -1, ".connect"], + ["copyMessages", true, "global"], + ["dcc.autoAccept.delay", 10000, "hidden"], + ["dcc.downloadsFolder", getURLSpecFromFile(downloadsPath.path), + "dcc"], + ["dcc.enabled", true, "dcc"], + ["dcc.listenPorts", [], "dcc.ports"], + ["dcc.useServerIP", true, "dcc"], + ["dccUserHeader", true, "global.header"], + ["dccUserLog", false, "global.log"], + ["dccUserMaxLines", 500, "global.maxLines"], + ["debugMode", "", "hidden"], + ["defaultQuitMsg", "", ".connect"], + ["deleteOnPart", true, "global"], + ["desc", "New Now Know How", ".ident"], + ["displayHeader", true, "appearance.misc"], + ["font.family", "default", "appearance.misc"], + ["font.size", 0, "appearance.misc"], + ["guessCommands", true, "hidden"], + ["hasPrefs", false, "hidden"], + ["identd.enabled", false, client.prefManager.identGroup], + ["initialScripts", ["scripts/"], "startup.initialScripts"], + ["initialURLs", [], "startup.initialURLs"], + ["inputSpellcheck", true, "global"], + ["log", false, + ".log"], + ["logFile.channel", "$(network)/channels/$(channel).$y-$m-$d.log", + "hidden"], + ["logFile.client", "client.$y-$m-$d.log", + "hidden"], + ["logFile.dccuser", "dcc/$(user)/$(user).$y-$m-$d.log", + "hidden"], + ["logFile.network", "$(network)/$(network).$y-$m-$d.log", + "hidden"], + ["logFile.user", "$(network)/users/$(user).$y-$m-$d.log", + "hidden"], + ["logFileName", makeLogNameClient, + "hidden"], + ["logFolder", getURLSpecFromFile(logPath.path), ".log"], + ["login.promptToSave", true, "global.security"], + ["motif.current", "chrome://chatzilla/skin/output-light.css", + "appearance.motif"], + ["motif.dark", "chrome://chatzilla/skin/output-dark.css", + "appearance.motif"], + ["motif.light", "chrome://chatzilla/skin/output-light.css", + "appearance.motif"], + ["multiline", false, "hidden"], + ["munger.bold", true, "munger"], + ["munger.bugzilla-link", true, "munger"], + ["munger.channel-link",true, "munger"], + ["munger.colorCodes", true, "munger"], + ["munger.ctrl-char", true, "munger"], + ["munger.face", true, "munger"], + ["munger.italic", true, "munger"], + ["munger.link", true, "munger"], + ["munger.mailto", true, "munger"], + ["munger.quote", true, "munger"], + ["munger.rheet", true, "munger"], + ["munger.talkback-link", true, "munger"], + ["munger.teletype", true, "munger"], + ["munger.underline", true, "munger"], + ["munger.word-hyphenator", true, "munger"], + ["networkHeader", true, "global.header"], + ["networkLog", false, "global.log"], + ["networkMaxLines", 200, "global.maxLines"], + ["newTabLimit", 30, "global"], + ["nickCompleteStr", ":", "global"], + ["nickname", defaultNick, ".ident"], + ["nicknameList", [], "lists.nicknameList"], + ["notify.aggressive", true, "global"], + ["outgoing.colorCodes", true, "global"], + ["outputWindowURL", "chrome://chatzilla/content/output-window.html", + "hidden"], + ["proxy.typeOverride", "", ".connect"], + ["reconnect", true, ".connect"], + ["sasl.plain.enabled", false, ".ident"], + ["showModeSymbols", false, "appearance.userlist"], + ["sortUsersByMode", true, "appearance.userlist"], + // Chat == "Activity" activity. + // Event == "Superfluous" activity. + // Stalk == "Attention" activity. + // Start == When view it opened. + ["sound.channel.chat", "", ".soundEvts"], + ["sound.channel.event", "", ".soundEvts"], + ["sound.channel.stalk", "beep", ".soundEvts"], + ["sound.channel.start", "", ".soundEvts"], + ["sound.enabled", true, "global.sounds"], + ["sound.overlapDelay", 2000, "global.sounds"], + ["sound.user.stalk", "beep", ".soundEvts"], + ["sound.user.start", "beep beep", ".soundEvts"], + ["stalkWholeWords", true, "lists.stalkWords"], + ["stalkWords", [], "lists.stalkWords"], + ["sts.enabled", true, ".connect"], + ["tabLabel", "", "hidden"], + ["tabGotoKeyModifiers", 0, "hidden"], + ["timestamps", false, "appearance.timestamps"], + ["timestamps.display", "[%H:%M]", "appearance.timestamps"], + ["timestamps.log", "[%Y-%m-%d %H:%M:%S]", "hidden"], + ["upgrade-insecure", false, ".connect"], + ["urls.display", 10, "hidden"], + ["urls.store.max", 100, "global"], + ["userHeader", true, "global.header"], + ["userlistLeft", true, "appearance.userlist"], + ["userLog", false, "global.log"], + ["userMaxLines", 200, "global.maxLines"], + ["usermode", "+i", ".ident"], + ["username", "chatzilla", ".ident"], + ["warnOnClose", true, "global"] + ]; + + client.prefManager.addPrefs(prefs); + client.prefManager.addObserver({ onPrefChanged: onPrefChanged }); + + CIRCNetwork.prototype.stayingPower = client.prefs["reconnect"]; + CIRCNetwork.prototype.MAX_CONNECT_ATTEMPTS = client.prefs["connectTries"]; + CIRCNetwork.prototype.INITIAL_NICK = client.prefs["nickname"]; + CIRCNetwork.prototype.INITIAL_NAME = client.prefs["username"]; + CIRCNetwork.prototype.INITIAL_DESC = client.prefs["desc"]; + CIRCNetwork.prototype.INITIAL_UMODE = client.prefs["usermode"]; + CIRCNetwork.prototype.MAX_MESSAGES = client.prefs["networkMaxLines"]; + CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE = client.prefs["proxy.typeOverride"]; + CIRCNetwork.prototype.USE_SASL = client.prefs["sasl.plain.enabled"]; + CIRCNetwork.prototype.UPGRADE_INSECURE = client.prefs["upgrade-insecure"]; + CIRCNetwork.prototype.STS_MODULE.ENABLED = client.prefs["sts.enabled"]; + CIRCChannel.prototype.MAX_MESSAGES = client.prefs["channelMaxLines"]; + CIRCUser.prototype.MAX_MESSAGES = client.prefs["userMaxLines"]; + CIRCDCCChat.prototype.MAX_MESSAGES = client.prefs["dccUserMaxLines"]; + CIRCDCCFileTransfer.prototype.MAX_MESSAGES = client.prefs["dccUserMaxLines"]; + CIRCDCC.prototype.listenPorts = client.prefs["dcc.listenPorts"]; + client.MAX_MESSAGES = client.prefs["clientMaxLines"]; + client.charset = client.prefs["charset"]; + + initAliases(); +} + +function makeLogName(obj, type) +{ + 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; + }; + + function encode(text) + { + text = text.replace(/[^-A-Z0-9_#!.,'@~\[\]{}()%$"]/gi, replaceNonPrintables); + + return encodeURIComponent(text); + }; + + /* /\$\(([^)]+)\)|\$(\w)/g * + * <-----> <--> * + * longName shortName * + */ + function replaceParam(match, longName, shortName) + { + if (typeof longName != "undefined" && longName) + { + // Remember to encode these, don't want some dodgy # breaking stuff. + if (longName in longCodes) + return encode(longCodes[longName]); + + dd("Unknown long code: " + longName); + } + else if (typeof shortName != "undefined" && shortName) + { + if (shortName in shortCodes) + return encode(shortCodes[shortName]); + + dd("Unknown short code: " + shortName); + } + else + { + dd("Unknown match: " + match); + } + + return match; + }; + + var base = client.prefs["logFolder"]; + var specific = client.prefs["logFile." + type]; + + // Make sure we got ourselves a slash, or we'll be in trouble with the + // concatenation. + if (!base.match(/\/$/)) + base = base + "/"; + var file = base + specific; + + // Get details for $-replacement variables. + var info = getObjectDetails(obj); + + // Store the most specific time short code on the object. + obj.smallestLogInterval = ""; + if (file.indexOf("$y") != -1) + obj.smallestLogInterval = "y"; + if (file.indexOf("$m") != -1) + obj.smallestLogInterval = "m"; + if (file.indexOf("$d") != -1) + obj.smallestLogInterval = "d"; + if (file.indexOf("$h") != -1) + obj.smallestLogInterval = "h"; + + // Three longs codes: $(network), $(channel) and $(user). + // Each is available only if appropriate for the object. + var longCodes = new Object(); + if (info.network) + longCodes["network"] = info.network.unicodeName; + if (info.channel) + longCodes["channel"] = info.channel.unicodeName; + if (info.user) + longCodes["user"] = info.user.unicodeName; + + // 4 short codes: $y, $m, $d, $h. + // These are time codes, each replaced with a fixed-length number. + var d = new Date(); + var shortCodes = { y: padNumber(d.getFullYear(), 4), + m: padNumber(d.getMonth() + 1, 2), + d: padNumber(d.getDate(), 2), + h: padNumber(d.getHours(), 2) + }; + + // Replace all $-variables in one go. + file = file.replace(/\$\(([^)]+)\)|\$(\w)/g, replaceParam); + + // Convert from file: URL to local OS format. + try + { + file = getFileFromURLSpec(file).path; + } + catch(ex) + { + dd("Error converting '" + base + specific + "' to a local file path."); + } + + return file; +} + +function pref_mungeName(name) +{ + var safeName = name.replace(/\./g, "-").replace(/:/g, "_").toLowerCase(); + return ecmaEscape(safeName); +} + +function getNetworkPrefManager(network) +{ + function defer(prefName) + { + return client.prefs[prefName]; + }; + + function makeLogNameNetwork() + { + return makeLogName(network, "network"); + }; + + function onPrefChanged(prefName, newValue, oldValue) + { + onNetworkPrefChanged (network, prefName, newValue, oldValue); + }; + + var logDefault = client.prefManager.logPath.clone(); + logDefault.append(escapeFileName(pref_mungeName(network.encodedName)) + ".log"); + + var prefs = + [ + ["alert.enabled", defer, ".palert"], + ["alert.channel.event",defer, ".palert"], + ["alert.channel.chat", defer, ".palert"], + ["alert.channel.stalk",defer, ".palert"], + ["alert.user.chat", defer, ".palert"], + ["autoAwayPeriod", defer, "appearance.misc"], + ["autoMarker", defer, "appearance.misc"], + ["autoperform", [], "lists.autoperform"], + ["autoRejoin", defer, ".connect"], + ["away", defer, "hidden"], + ["awayNick", defer, ".ident"], + ["bugURL", defer, "appearance.misc"], + ["bugURL.comment", defer, "appearance.misc"], + ["charset", defer, ".connect"], + ["collapseActions", defer, "appearance.misc"], + ["collapseMsgs", defer, "appearance.misc"], + ["conference.limit", defer, "appearance.misc"], + ["connectTries", defer, ".connect"], + ["dcc.autoAccept.list", [], "dcc.autoAccept"], + ["dcc.downloadsFolder", defer, "dcc"], + ["dcc.useServerIP", defer, "dcc"], + ["defaultQuitMsg", defer, ".connect"], + ["desc", defer, ".ident"], + ["displayHeader", client.prefs["networkHeader"], "appearance.misc"], + ["font.family", defer, "appearance.misc"], + ["font.size", defer, "appearance.misc"], + ["hasPrefs", false, "hidden"], + ["identd.enabled", defer, client.prefManager.identGroup], + ["ignoreList", [], "hidden"], + ["log", client.prefs["networkLog"], ".log"], + ["logFileName", makeLogNameNetwork, "hidden"], + ["motif.current", defer, "appearance.motif"], + ["nickname", defer, ".ident"], + ["nicknameList", defer, "lists.nicknameList"], + ["notifyList", [], "lists.notifyList"], + ["outputWindowURL", defer, "hidden"], + ["proxy.typeOverride", defer, ".connect"], + ["reconnect", defer, ".connect"], + ["sasl.plain.enabled", defer, ".ident"], + ["sound.channel.chat", defer, ".soundEvts"], + ["sound.channel.event", defer, ".soundEvts"], + ["sound.channel.stalk", defer, ".soundEvts"], + ["sound.channel.start", defer, ".soundEvts"], + ["sound.user.stalk", defer, ".soundEvts"], + ["sound.user.start", defer, ".soundEvts"], + ["tabLabel", "", "hidden"], + ["timestamps", defer, "appearance.timestamps"], + ["timestamps.display", defer, "appearance.timestamps"], + ["timestamps.log", defer, "hidden"], + ["upgrade-insecure", defer, ".connect"], + ["usermode", defer, ".ident"], + ["username", defer, ".ident"] + ]; + + var branch = "extensions.irc.networks." + pref_mungeName(network.encodedName) + + "."; + var prefManager = new PrefManager(branch, client.defaultBundle); + prefManager.addPrefs(prefs); + prefManager.addObserver({ onPrefChanged: onPrefChanged }); + client.prefManager.addObserver(prefManager); + + var value = prefManager.prefs["nickname"]; + if (value != CIRCNetwork.prototype.INITIAL_NICK) + network.INITIAL_NICK = value; + + value = prefManager.prefs["username"]; + if (value != CIRCNetwork.prototype.INITIAL_NAME) + network.INITIAL_NAME = value; + + value = prefManager.prefs["desc"]; + if (value != CIRCNetwork.prototype.INITIAL_DESC) + network.INITIAL_DESC = value; + + value = prefManager.prefs["usermode"]; + if (value != CIRCNetwork.prototype.INITIAL_UMODE) + network.INITIAL_UMODE = value; + + value = prefManager.prefs["proxy.typeOverride"]; + if (value != CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE) + network.PROXY_TYPE_OVERRIDE = value; + + value = prefManager.prefs["sasl.plain.enabled"]; + if (value != CIRCNetwork.prototype.USE_SASL) + network.USE_SASL = value; + + value = prefManager.prefs["upgrade-insecure"]; + if (value != CIRCNetwork.prototype.UPGRADE_INSECURE) + network.UPGRADE_INSECURE = value; + + network.stayingPower = prefManager.prefs["reconnect"]; + network.MAX_CONNECT_ATTEMPTS = prefManager.prefs["connectTries"]; + + client.prefManagers.push(prefManager); + + return prefManager; +} + +function getChannelPrefManager(channel) +{ + var network = channel.parent.parent; + + function defer(prefName) + { + return network.prefs[prefName]; + }; + + function makeLogNameChannel() + { + return makeLogName(channel, "channel"); + }; + + function onPrefChanged(prefName, newValue, oldValue) + { + onChannelPrefChanged (channel, prefName, newValue, oldValue); + }; + + var logDefault = client.prefManager.logPath.clone(); + var filename = pref_mungeName(network.encodedName) + "," + + pref_mungeName(channel.encodedName); + + logDefault.append(escapeFileName(filename) + ".log"); + + var prefs = + [ + ["alert.enabled", defer, ".palert"], + ["alert.channel.event",defer, ".palert"], + ["alert.channel.chat", defer, ".palert"], + ["alert.channel.stalk",defer, ".palert"], + ["autoperform", [], "lists.autoperform"], + ["autoRejoin", defer, ".connect"], + ["autoMarker", defer, "appearance.misc"], + ["bugURL", defer, "appearance.misc"], + ["bugURL.comment", defer, "appearance.misc"], + ["charset", defer, ".connect"], + ["collapseActions", defer, "appearance.misc"], + ["collapseMsgs", defer, "appearance.misc"], + ["conference.enabled", false, "hidden"], + ["conference.limit", defer, "appearance.misc"], + ["displayHeader", client.prefs["channelHeader"], "appearance.misc"], + ["font.family", defer, "appearance.misc"], + ["font.size", defer, "appearance.misc"], + ["hasPrefs", false, "hidden"], + ["log", client.prefs["channelLog"], ".log"], + ["logFileName", makeLogNameChannel, "hidden"], + ["motif.current", defer, "appearance.motif"], + ["outputWindowURL", defer, "hidden"], + ["sound.channel.chat", defer, ".soundEvts"], + ["sound.channel.event", defer, ".soundEvts"], + ["sound.channel.stalk", defer, ".soundEvts"], + ["sound.channel.start", defer, ".soundEvts"], + ["tabLabel", "", "hidden"], + ["timestamps", defer, "appearance.timestamps"], + ["timestamps.display", defer, "appearance.timestamps"], + ["timestamps.log", defer, "hidden"] + ]; + + var branch = "extensions.irc.networks." + pref_mungeName(network.encodedName) + + ".channels." + pref_mungeName(channel.encodedName) + "." + var prefManager = new PrefManager(branch, client.defaultBundle); + prefManager.addPrefs(prefs); + prefManager.addObserver({ onPrefChanged: onPrefChanged }); + network.prefManager.addObserver(prefManager); + + client.prefManagers.push(prefManager); + + return prefManager; +} + +function getUserPrefManager(user) +{ + var network = user.parent.parent; + + function defer(prefName) + { + return network.prefs[prefName]; + }; + + function makeLogNameUser() + { + return makeLogName(user, "user"); + }; + + function onPrefChanged(prefName, newValue, oldValue) + { + onUserPrefChanged (user, prefName, newValue, oldValue); + }; + + var logDefault = client.prefManager.logPath.clone(); + var filename = pref_mungeName(network.encodedName); + filename += "," + pref_mungeName(user.encodedName); + logDefault.append(escapeFileName(filename) + ".log"); + + var prefs = + [ + ["alert.enabled", defer, ".palert"], + ["alert.user.chat", defer, ".palert"], + ["autoperform", [], "lists.autoperform"], + ["charset", defer, ".connect"], + ["collapseActions", defer, "appearance.misc"], + ["collapseMsgs", defer, "appearance.misc"], + ["displayHeader", client.prefs["userHeader"], "appearance.misc"], + ["font.family", defer, "appearance.misc"], + ["font.size", defer, "appearance.misc"], + ["hasPrefs", false, "hidden"], + ["log", client.prefs["userLog"], ".log"], + ["logFileName", makeLogNameUser, "hidden"], + ["motif.current", defer, "appearance.motif"], + ["outputWindowURL", defer, "hidden"], + ["sound.user.stalk", defer, ".soundEvts"], + ["sound.user.start", defer, ".soundEvts"], + ["tabLabel", "", "hidden"], + ["timestamps", defer, "appearance.timestamps"], + ["timestamps.display", defer, "appearance.timestamps"], + ["timestamps.log", defer, "hidden"] + ]; + + var branch = "extensions.irc.networks." + pref_mungeName(network.encodedName) + + ".users." + pref_mungeName(user.encodedName) + "."; + var prefManager = new PrefManager(branch, client.defaultBundle); + prefManager.addPrefs(prefs); + prefManager.addObserver({ onPrefChanged: onPrefChanged }); + network.prefManager.addObserver(prefManager); + + client.prefManagers.push(prefManager); + + return prefManager; +} + +function getDCCUserPrefManager(user) +{ + function defer(prefName) + { + return client.prefs[prefName]; + }; + + function makeLogNameUser() + { + return makeLogName(user, "dccuser"); + }; + + function onPrefChanged(prefName, newValue, oldValue) + { + onDCCUserPrefChanged(user, prefName, newValue, oldValue); + }; + + var prefs = + [ + ["alert.enabled", defer, ".palert"], + ["alert.user.chat", defer, ".palert"], + ["charset", defer, ".connect"], + ["collapseMsgs", defer, "appearance.misc"], + ["displayHeader", client.prefs["dccUserHeader"], "appearance.misc"], + ["font.family", defer, "appearance.misc"], + ["font.size", defer, "appearance.misc"], + ["hasPrefs", false, "hidden"], + ["log", client.prefs["dccUserLog"], ".log"], + ["logFileName", makeLogNameUser, "hidden"], + ["motif.current", defer, "appearance.motif"], + ["outputWindowURL", defer, "hidden"], + ["tabLabel", "", "hidden"], + ["timestamps", defer, "appearance.timestamps"], + ["timestamps.display", defer, "appearance.timestamps"], + ["timestamps.log", defer, "hidden"] + ]; + + var branch = "extensions.irc.dcc.users." + + pref_mungeName(user.canonicalName) + "."; + var prefManager = new PrefManager(branch, client.defaultBundle); + prefManager.addPrefs(prefs); + prefManager.addObserver({ onPrefChanged: onPrefChanged }); + client.prefManager.addObserver(prefManager); + + client.prefManagers.push(prefManager); + + return prefManager; +} + +function destroyPrefs() +{ + if ("prefManagers" in client) + { + for (var i = 0; i < client.prefManagers.length; ++i) + client.prefManagers[i].destroy(); + client.prefManagers = []; + } +} + +function onPrefChanged(prefName, newValue, oldValue) +{ + if (newValue == oldValue) + return; + + switch (prefName) + { + case "awayIdleTime": + uninitIdleAutoAway(oldValue); + initIdleAutoAway(newValue); + break; + + case "bugKeyword": + client.munger.delRule("bugzilla-link"); + addBugzillaLinkMungerRule(newValue, 10, 10); + break; + + case "channelMaxLines": + CIRCChannel.prototype.MAX_MESSAGES = newValue; + break; + + case "charset": + client.charset = newValue; + break; + + case "clientMaxLines": + client.MAX_MESSAGES = newValue; + break; + + case "connectTries": + CIRCNetwork.prototype.MAX_CONNECT_ATTEMPTS = newValue; + break; + + case "dcc.listenPorts": + CIRCDCC.prototype.listenPorts = newValue; + break; + + case "dccUserMaxLines": + CIRCDCCFileTransfer.prototype.MAX_MESSAGES = newValue; + CIRCDCCChat.prototype.MAX_MESSAGES = newValue; + break; + + case "font.family": + case "font.size": + client.dispatch("sync-font"); + break; + + case "proxy.typeOverride": + CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE = newValue; + break; + + case "reconnect": + CIRCNetwork.prototype.stayingPower = newValue; + break; + + case "showModeSymbols": + if (newValue) + setListMode("symbol"); + else + setListMode("graphic"); + break; + + case "sasl.plain.enabled": + CIRCNetwork.prototype.USE_SASL = newValue; + break; + + case "upgrade-insecure": + CIRCNetwork.prototype.UPGRADE_INSECURE = newValue; + break; + + case "sts.enabled": + CIRCNetwork.prototype.STS_MODULE.ENABLED = newValue; + break; + + case "nickname": + CIRCNetwork.prototype.INITIAL_NICK = newValue; + break; + + case "username": + CIRCNetwork.prototype.INITIAL_NAME = newValue; + break; + + case "usermode": + CIRCNetwork.prototype.INITIAL_UMODE = newValue; + break; + + case "userMaxLines": + CIRCUser.prototype.MAX_MESSAGES = newValue; + break; + + case "userlistLeft": + updateUserlistSide(newValue); + break; + + case "debugMode": + setDebugMode(newValue); + break; + + case "desc": + CIRCNetwork.prototype.INITIAL_DESC = newValue; + break; + + case "stalkWholeWords": + case "stalkWords": + updateAllStalkExpressions(); + break; + + case "sortUsersByMode": + if (client.currentObject.TYPE == "IRCChannel") + updateUserList(); + + case "motif.current": + client.dispatch("sync-motif"); + break; + + case "multiline": + multilineInputMode(newValue); + delete client.multiLineForPaste; + break; + + case "munger.colorCodes": + client.enableColors = newValue; + break; + + case "networkMaxLines": + CIRCNetwork.prototype.MAX_MESSAGES = newValue; + break; + + case "outputWindowURL": + client.dispatch("sync-window"); + break; + + case "displayHeader": + client.dispatch("sync-header"); + break; + + case "tabLabel": + onTabLabelUpdate(client, newValue); + break; + + case "timestamps": + case "timestamps.display": + case "collapseActions": + case "collapseMsgs": + client.dispatch("sync-timestamp"); + break; + + case "log": + client.dispatch("sync-log"); + break; + + case "alert.globalEnabled": + updateAlertIcon(false); + break; + + case "alert.floodDensity": + if (client.alert && client.alert.floodProtector) + client.alert.floodProtector.floodDensity = newValue; + break; + + case "alert.floodDispersion": + if (client.alert && client.alert.floodProtector) + client.alert.floodProtector.floodDispersion = newValue; + break; + + case "aliases": + updateAliases(); + break; + + case "inputSpellcheck": + updateSpellcheck(newValue); + break; + + case "urls.store.max": + if (client.urlLogger) + { + client.urlLogger.autoLimit = newValue; + client.urlLogger.limit(newValue); + } + break; + + default: + // Make munger prefs apply without a restart + var m, rule; + if ((m = prefName.match(/^munger\.(\S+)$/)) && + (rule = client.munger.getRule(m[1]))) + { + rule.enabled = newValue; + } + } +} + +function onNetworkPrefChanged(network, prefName, newValue, oldValue) +{ + if (network != client.networks[network.collectionKey]) + { + /* this is a stale observer, remove it */ + network.prefManager.destroy(); + return; + } + + if (newValue == oldValue) + return; + + network.updateHeader(); + + switch (prefName) + { + case "nickname": + network.INITIAL_NICK = newValue; + break; + + case "username": + network.INITIAL_NAME = newValue; + break; + + case "usermode": + network.INITIAL_UMODE = newValue; + if (network.isConnected()) + { + network.primServ.sendData("mode " + network.server.me + " :" + + newValue + "\n"); + } + break; + + case "desc": + network.INITIAL_DESC = newValue; + break; + + case "proxy.typeOverride": + network.PROXY_TYPE_OVERRIDE = newValue; + break; + + case "reconnect": + network.stayingPower = newValue; + break; + + case "font.family": + case "font.size": + network.dispatch("sync-font"); + break; + + case "motif.current": + network.dispatch("sync-motif"); + break; + + case "notifyList": + if (!network.primServ.supports["monitor"]) + break; + var adds = newValue.filter((el) => + { return oldValue.indexOf(el) < 0; }); + var subs = oldValue.filter((el) => + { return newValue.indexOf(el) < 0; }); + if (adds.length > 0) + network.primServ.sendMonitorList(adds, true); + if (subs.length > 0) + network.primServ.sendMonitorList(subs, false); + break; + + case "outputWindowURL": + network.dispatch("sync-window"); + break; + + case "displayHeader": + network.dispatch("sync-header"); + break; + + case "tabLabel": + onTabLabelUpdate(network, newValue); + break; + + case "timestamps": + case "timestamps.display": + case "collapseActions": + case "collapseMsgs": + network.dispatch("sync-timestamp"); + break; + + case "log": + network.dispatch("sync-log"); + break; + + case "connectTries": + network.MAX_CONNECT_ATTEMPTS = newValue; + break; + + case "sasl.plain.enabled": + network.USE_SASL = newValue; + break; + + case "upgrade-insecure": + network.UPGRADE_INSECURE = newValue; + break; + } +} + +function onChannelPrefChanged(channel, prefName, newValue, oldValue) +{ + var network = channel.parent.parent; + + if (network != client.networks[network.collectionKey] || + channel.parent != network.primServ || + channel != network.primServ.channels[channel.collectionKey]) + { + /* this is a stale observer, remove it */ + channel.prefManager.destroy(); + return; + } + + if (newValue == oldValue) + return; + + channel.updateHeader(); + + switch (prefName) + { + case "conference.enabled": + // Wouldn't want to display a message to a hidden view. + if ("messages" in channel) + { + if (newValue) + channel.display(MSG_CONF_MODE_ON); + else + channel.display(MSG_CONF_MODE_OFF); + } + break; + + case "conference.limit": + channel._updateConferenceMode(); + break; + + case "font.family": + case "font.size": + channel.dispatch("sync-font"); + break; + + case "motif.current": + channel.dispatch("sync-motif"); + break; + + case "outputWindowURL": + channel.dispatch("sync-window"); + break; + + case "displayHeader": + channel.dispatch("sync-header"); + break; + + case "tabLabel": + onTabLabelUpdate(channel, newValue); + break; + + case "timestamps": + case "timestamps.display": + case "collapseActions": + case "collapseMsgs": + channel.dispatch("sync-timestamp"); + break; + + case "log": + channel.dispatch("sync-log"); + break; + } +} + +function onUserPrefChanged(user, prefName, newValue, oldValue) +{ + var network = user.parent.parent; + + if (network != client.networks[network.collectionKey] || + user.parent != network.primServ || + user != network.primServ.users[user.collectionKey]) + { + /* this is a stale observer, remove it */ + user.prefManager.destroy(); + return; + } + + if (newValue == oldValue) + return; + + user.updateHeader(); + + switch (prefName) + { + case "font.family": + case "font.size": + user.dispatch("sync-font"); + break; + + case "motif.current": + user.dispatch("sync-motif"); + break; + + case "outputWindowURL": + user.dispatch("sync-window"); + break; + + case "displayHeader": + user.dispatch("sync-header"); + break; + + case "tabLabel": + onTabLabelUpdate(user, newValue); + break; + + case "timestamps": + case "timestamps.display": + case "collapseActions": + case "collapseMsgs": + user.dispatch("sync-timestamp"); + break; + + case "log": + user.dispatch("sync-log"); + break; + } +} + +function onDCCUserPrefChanged(user, prefName, newValue, oldValue) +{ + if (client.dcc.users[user.key] != user) + { + /* this is a stale observer, remove it */ + user.prefManager.destroy(); + return; + } + + if (newValue == oldValue) + return; + + // DCC Users are a pain, they can have multiple views! + function updateDCCView(view) + { + switch (prefName) + { + case "font.family": + case "font.size": + view.dispatch("sync-font"); + break; + + case "motif.current": + view.dispatch("sync-motif"); + break; + + case "outputWindowURL": + view.dispatch("sync-window"); + break; + + case "displayHeader": + view.dispatch("sync-header"); + break; + + case "tabLabel": + onTabLabelUpdate(user, newValue); + break; + + case "timestamps": + case "timestamps.display": + case "collapseActions": + case "collapseMsgs": + view.dispatch("sync-timestamp"); + break; + + case "log": + view.dispatch("sync-log"); + break; + } + }; + + for (var i = 0; client.dcc.chats.length; i++) + { + var chat = client.dcc.chats[i]; + if (chat.user == user) + updateDCCView(chat); + } +} + +function initAliases() +{ + client.commandManager.aliasList = new Object(); + updateAliases(); +} + +function updateAliases() +{ + var aliasDefs = client.prefs["aliases"]; + + // Flag all aliases as 'removed' first. + for (var name in client.commandManager.aliasList) + client.commandManager.aliasList[name] = false; + + for (var i = 0; i < aliasDefs.length; ++i) + { + var ary = aliasDefs[i].match(/^(.*?)\s*=\s*(.*)$/); + if (ary) + { + var name = ary[1]; + var list = ary[2]; + + // Remove the alias, if it exists, or we'll keep stacking them. + if (name in client.commandManager.aliasList) + client.commandManager.removeCommand({name: name}); + client.commandManager.defineCommand(name, list); + // Flag this alias as 'used'. + client.commandManager.aliasList[name] = true; + } + else + { + dd("Malformed alias: " + aliasDefs[i]); + } + } + + // Purge any aliases that were defined but are no longer in the pref. + for (var name in client.commandManager.aliasList) + { + if (!client.commandManager.aliasList[name]) + { + client.commandManager.removeCommand({name: name}); + delete client.commandManager.aliasList[name]; + } + } +} + +function onTabLabelUpdate(sourceObject, newValue) +{ + var tab = getTabForObject(sourceObject); + if (tab) + { + tab.label = newValue || sourceObject.viewName; + tab.setAttribute("tooltiptext", sourceObject.viewName); + } +} + diff --git a/comm/suite/chatzilla/xul/content/prefsOverlay.xul b/comm/suite/chatzilla/xul/content/prefsOverlay.xul new file mode 100644 index 0000000000..946299216f --- /dev/null +++ b/comm/suite/chatzilla/xul/content/prefsOverlay.xul @@ -0,0 +1,31 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/pref-irc.dtd"> + +<overlay id="ovCZPrefs" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <treechildren id="prefsPanelChildren"> + <treeitem id="chatzillaItem" + insertbefore="securityItem" + label="&pref-irc.window.title;" + prefpane="chatzilla_pane" + url="chrome://chatzilla/content/pref-irc-toolkit.xul"/> + </treechildren> + + <preferences id="appearance_preferences"> + <preference id="general.startup.chat" + name="general.startup.chat" + type="bool"/> + </preferences> + <!-- Startup checkbox --> + <groupbox id="generalStartupPreferences"> + <checkbox id="generalStartupChat" label="&startup.chat.label;" + accesskey="&startup.chat.accesskey;" pref="true" preftype="bool" + prefstring="general.startup.chat" prefattribute="checked" + wsm_persist="true" preference="general.startup.chat"/> + </groupbox> +</overlay> diff --git a/comm/suite/chatzilla/xul/content/scripts.xul b/comm/suite/chatzilla/xul/content/scripts.xul new file mode 100644 index 0000000000..6207970a07 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/scripts.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> + +<!-- + - + - 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/. --> + +<overlay id="chatzilla-scripts-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <overlaytarget id="scripts-overlay-target"> + + <script src="chrome://global/content/globalOverlay.js"/> + + <script><![CDATA[ + /* utils.js overloads the standard JS messages with prompt service + * calls, which require the locale support to have loaded. This next + * line is needed so that the onLoad function in handlers.js can + * display the "error loading ChatZilla" message even if the locale + * support is what failed to load. + */ + window.baseAlert = window.alert; + ]]></script> + <script src="chrome://chatzilla/content/lib/js/utils.js"/> + <script src="chrome://chatzilla/content/lib/js/connection-xpcom.js"/> + <script src="chrome://chatzilla/content/lib/js/events.js"/> + <script src="chrome://chatzilla/content/lib/js/command-manager.js"/> + <script src="chrome://chatzilla/content/lib/js/pref-manager.js"/> + <script src="chrome://chatzilla/content/lib/js/message-manager.js"/> + <script src="chrome://chatzilla/content/lib/js/menu-manager.js"/> + <script src="chrome://chatzilla/content/lib/js/irc.js"/> + <script src="chrome://chatzilla/content/lib/js/irc-debug.js"/> + <script src="chrome://chatzilla/content/lib/js/file-utils.js"/> + <script src="chrome://chatzilla/content/lib/js/dcc.js"/> + <script src="chrome://chatzilla/content/lib/js/ident.js"/> + <script src="chrome://chatzilla/content/lib/js/json-serializer.js"/> + <script src="chrome://chatzilla/content/lib/js/sts.js"/> + <script src="chrome://chatzilla/content/lib/js/text-serializer.js"/> + <script src="chrome://chatzilla/content/lib/js/text-logger.js"/> + <script src="chrome://chatzilla/content/lib/xul/munger.js"/> + <script src="chrome://chatzilla/content/lib/xul/tree-utils.js"/> + + <script src="chrome://chatzilla/content/static.js"/> + <script src="chrome://chatzilla/content/commands.js"/> + <script src="chrome://chatzilla/content/menus.js"/> + <script src="chrome://chatzilla/content/prefs.js"/> + <script src="chrome://chatzilla/content/messages.js"/> + <script src="chrome://chatzilla/content/mungers.js"/> + <script src="chrome://chatzilla/content/handlers.js"/> + <script src="chrome://chatzilla/content/networks.js"/> + + </overlaytarget> + +</overlay> diff --git a/comm/suite/chatzilla/xul/content/static.js b/comm/suite/chatzilla/xul/content/static.js new file mode 100644 index 0000000000..8ee7753210 --- /dev/null +++ b/comm/suite/chatzilla/xul/content/static.js @@ -0,0 +1,5639 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +ChromeUtils.defineModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +#expand const __cz_version = "__CHATZILLA_VERSION__"; +const __cz_condition = "green"; + +var warn; +var ASSERT; +var TEST; + +if (DEBUG) +{ + _dd_pfx = "cz: "; + warn = function (msg) { dumpln ("** WARNING " + msg + " **"); } + TEST = ASSERT = function _assert(expr, msg) { + if (!expr) { + dd("** ASSERTION FAILED: " + msg + " **\n" + + getStackTrace() + "\n"); + return false; + } else { + return true; + } + } +} +else + dd = warn = TEST = ASSERT = function (){}; + +var client = new Object(); + +client.TYPE = "IRCClient"; +client.COMMAND_CHAR = "/"; +client.STEP_TIMEOUT = 500; +client.MAX_MESSAGES = 200; +client.MAX_HISTORY = 50; +/* longest nick to show in display before forcing the message to a block level + * element */ +client.MAX_NICK_DISPLAY = 14; +/* longest word to show in display before abbreviating */ +client.MAX_WORD_DISPLAY = 20; + +client.NOTIFY_TIMEOUT = 5 * 60 * 1000; /* update notify list every 5 minutes */ + +// Check every minute which networks have away statuses that need an update. +client.AWAY_TIMEOUT = 60 * 1000; + +client.SLOPPY_NETWORKS = true; /* true if msgs from a network can be displayed + * on the current object if it is related to + * the network (ie, /whois results will appear + * on the channel you're viewing, if that channel + * is on the network that the results came from) + */ +client.DOUBLETAB_TIME = 500; +client.HIDE_CODES = true; /* true if you'd prefer to show numeric response + * codes as some default value (ie, "===") */ +client.DEFAULT_RESPONSE_CODE = "==="; + +/* Maximum number of channels we'll try to list without complaining */ +client.SAFE_LIST_COUNT = 500; + +/* Minimum number of users above or below the conference limit the user count + * must go, before it is changed. This allows the user count to fluctuate + * around the limit without continously going on and off. + */ +client.CONFERENCE_LOW_PASS = 10; + +client.viewsArray = new Array(); +client.activityList = new Object(); +client.hostCompat = new Object(); +client.inputHistory = new Array(); +client.lastHistoryReferenced = -1; +client.incompleteLine = ""; +client.lastTabUp = new Date(); +client.awayMsgs = new Array(); +client.awayMsgCount = 5; +client.statusMessages = new Array(); + +CIRCNetwork.prototype.INITIAL_CHANNEL = ""; +CIRCNetwork.prototype.STS_MODULE = new CIRCSTS(); +CIRCNetwork.prototype.MAX_MESSAGES = 100; +CIRCNetwork.prototype.IGNORE_MOTD = false; +CIRCNetwork.prototype.RECLAIM_WAIT = 15000; +CIRCNetwork.prototype.RECLAIM_TIMEOUT = 400000; +CIRCNetwork.prototype.MIN_RECONNECT_MS = 15 * 1000; // 15s +CIRCNetwork.prototype.MAX_RECONNECT_MS = 2 * 60 * 60 * 1000; // 2h + +CIRCServer.prototype.READ_TIMEOUT = 0; +CIRCServer.prototype.PRUNE_OLD_USERS = 0; // prune on user quit. + +CIRCUser.prototype.MAX_MESSAGES = 200; + +CIRCChannel.prototype.MAX_MESSAGES = 300; + +function init() +{ + if (("initialized" in client) && client.initialized) + return; + + client.initialized = false; + + client.networks = new Object(); + client.entities = new Object(); + client.eventPump = new CEventPump (200); + + if (DEBUG) + { + /* hook all events EXCEPT server.poll and *.event-end types + * (the 4th param inverts the match) */ + client.debugHook = + client.eventPump.addHook([{type: "poll", set:/^(server|dcc-chat)$/}, + {type: "event-end"}], event_tracer, + "event-tracer", true /* negate */, + false /* disable */); + } + + initApplicationCompatibility(); + initMessages(); + + initCommands(); + initPrefs(); + initMunger(); + initNetworks(); + initMenus(); + initStatic(); + initHandlers(); + + // Create DCC handler. + client.dcc = new CIRCDCC(client); + + client.ident = new IdentServer(client); + + // Initialize the STS module. + var stsFile = new nsLocalFile(client.prefs["profilePath"]); + stsFile.append("sts.json"); + + client.sts = CIRCNetwork.prototype.STS_MODULE; + client.sts.init(stsFile); + client.sts.ENABLED = client.prefs["sts.enabled"]; + + // Start log rotation checking first. This will schedule the next check. + checkLogFiles(); + // Start logging. Nothing should call display() before this point. + if (client.prefs["log"]) + client.openLogFile(client); + + // Make sure the userlist is on the correct side. + updateUserlistSide(client.prefs["userlistLeft"]); + + client.display(MSG_WELCOME, "HELLO"); + client.dispatch("set-current-view", { view: client }); + + /* + * Due to Firefox 44 changes regarding ES6 lexical scope, these 'const' + * items are no longer accessible from the global object ('window') but + * are required by the output window. The compromise is to copy them on + * to the global object so they can be used. + */ + window.__cz_version = __cz_version; + window.__cz_condition = __cz_condition; + window.NET_CONNECTING = NET_CONNECTING; + + importFromFrame("updateHeader"); + importFromFrame("setHeaderState"); + importFromFrame("changeCSS"); + importFromFrame("scrollToElement"); + importFromFrame("updateMotifSettings"); + importFromFrame("addUsers"); + importFromFrame("updateUsers"); + importFromFrame("removeUsers"); + + processStartupScripts(); + + client.commandManager.installKeys(document); + createMenus(); + + client.busy = false; + updateProgress(); + initOfflineIcon(); + updateAlertIcon(false); + client.isIdleAway = false; + initIdleAutoAway(client.prefs["awayIdleTime"]); + + client.initialized = true; + + dispatch("help", { hello: true }); + dispatch("networks"); + + setTimeout(function() { + dispatch("focus-input"); + }, 0); + setTimeout(processStartupAutoperform, 0); + setTimeout(processStartupURLs, 0); +} + +function initStatic() +{ + client.mainWindow = window; + + try + { + const nsISound = Components.interfaces.nsISound; + client.sound = + Components.classes["@mozilla.org/sound;1"].createInstance(nsISound); + + client.soundList = new Object(); + } + catch (ex) + { + dd("Sound failed to initialize: " + ex); + } + + try + { + const nsIAlertsService = Components.interfaces.nsIAlertsService; + client.alert = new Object(); + client.alert.service = + Components.classes["@mozilla.org/alerts-service;1"].getService(nsIAlertsService); + client.alert.alertList = new Object(); + client.alert.floodProtector = new FloodProtector( + client.prefs['alert.floodDensity'], + client.prefs['alert.floodDispersion']); + } + catch (ex) + { + dd("Alert service failed to initialize: " + ex); + client.alert = null; + } + + try + { + // Mmmm, fun. This ONLY affects the ChatZilla window, don't worry! + Date.prototype.toStringInt = Date.prototype.toString; + Date.prototype.toString = function() { + let dtf = new Services.intl.DateTimeFormat(undefined, + { dateStyle: "full", + timeStyle: "long" }); + return dtf.format(this); + } + } + catch (ex) + { + dd("Locale-correct date formatting failed to initialize: " + ex); + } + + // XXX Bug 335998: See cmdHideView for usage of this. + client.hiddenDocument = document.implementation.createDocument(null, null, null); + + multilineInputMode(client.prefs["multiline"]); + updateSpellcheck(client.prefs["inputSpellcheck"]); + + // Initialize userlist stuff + if (client.prefs["showModeSymbols"]) + setListMode("symbol"); + else + setListMode("graphic"); + + var tree = document.getElementById('user-list'); + tree.setAttribute("ondragstart", "userlistDNDObserver.onDragStart(event);"); + + setDebugMode(client.prefs["debugMode"]); + + var version = getVersionInfo(); + client.userAgent = getMsg(MSG_VERSION_REPLY, [version.cz, version.ua]); + CIRCServer.prototype.VERSION_RPLY = client.userAgent; + CIRCServer.prototype.HOST_RPLY = version.host; + CIRCServer.prototype.SOURCE_RPLY = MSG_SOURCE_REPLY; + + client.statusBar = new Object(); + + client.statusBar["server-nick"] = document.getElementById("server-nick"); + + client.tabs = document.getElementById("views-tbar-inner"); + client.tabDragBar = document.getElementById("tabs-drop-indicator-bar"); + client.tabDragMarker = document.getElementById("tabs-drop-indicator"); + + client.statusElement = document.getElementById("status-text"); + client.currentStatus = ""; + client.defaultStatus = MSG_DEFAULT_STATUS; + + client.progressPanel = document.getElementById("status-progress-panel"); + client.progressBar = document.getElementById("status-progress-bar"); + + client.logFile = null; + setInterval(onNotifyTimeout, client.NOTIFY_TIMEOUT); + // Call every minute, will check only the networks necessary. + setInterval(onWhoTimeout, client.AWAY_TIMEOUT); + + client.awayMsgs = [{ message: MSG_AWAY_DEFAULT }]; + var awayFile = new nsLocalFile(client.prefs["profilePath"]); + awayFile.append("awayMsgs.txt"); + if (awayFile.exists()) + { + var awayLoader = new TextSerializer(awayFile); + if (awayLoader.open("<")) + { + // Load the first item from the file. + var item = awayLoader.deserialize(); + if (isinstance(item, Array)) + { + // If the first item is an array, it is the entire thing. + client.awayMsgs = item; + } + else if (item != null) + { + /* Not an array, so we have the old format of a single object + * per entry. + */ + client.awayMsgs = [item]; + while ((item = awayLoader.deserialize())) + client.awayMsgs.push(item); + } + awayLoader.close(); + + /* we have to close the file before we can move it, + * hence the second if statement */ + if (item == null) + { + var invalidFile = new nsLocalFile(client.prefs["profilePath"]); + invalidFile.append("awayMsgs.invalid"); + invalidFile.createUnique(FTYPE_FILE, 0o600); + var msg = getMsg(MSG_ERR_INVALID_FILE, + [awayFile.leafName, invalidFile.leafName]); + setTimeout(function() { + client.display(msg, MT_WARN); + }, 0); + awayFile.moveTo(null, invalidFile.leafName); + } + } + } + + // Get back input history from previous session: + var inputHistoryFile = new nsLocalFile(client.prefs["profilePath"]); + inputHistoryFile.append("inputHistory.txt"); + try + { + client.inputHistoryLogger = new TextLogger(inputHistoryFile.path, + client.MAX_HISTORY); + } + catch (ex) + { + msg = getMsg(MSG_ERR_INPUTHISTORY_NOT_WRITABLE, inputHistoryFile.path); + setTimeout(function() { + client.display(msg, MT_ERROR); + }, 0); + dd(formatException(ex)); + client.inputHistoryLogger = null; + } + if (client.inputHistoryLogger) + client.inputHistory = client.inputHistoryLogger.read().reverse(); + + // Set up URL collector. + var urlsFile = new nsLocalFile(client.prefs["profilePath"]); + urlsFile.append("urls.txt"); + try + { + client.urlLogger = new TextLogger(urlsFile.path, + client.prefs["urls.store.max"]); + } + catch (ex) + { + msg = getMsg(MSG_ERR_URLS_NOT_WRITABLE, urlsFile.path); + setTimeout(function() { + client.display(msg, MT_ERROR); + }, 0); + dd(formatException(ex)); + client.urlLogger = null; + } + + // Migrate old list preference to file. + try + { + // Throws if the preference doesn't exist. + if (client.urlLogger) + var urls = client.prefManager.prefBranch.getCharPref("urls.list"); + } + catch (ex) + { + } + if (urls) + { + // Add the old URLs to the new file. + urls = client.prefManager.stringToArray(urls); + for (var i = 0; i < urls.length; i++) + client.urlLogger.append(urls[i]); + // Completely purge the old preference. + client.prefManager.prefBranch.clearUserPref("urls.list"); + } + + client.defaultCompletion = client.COMMAND_CHAR + "help "; + + client.deck = document.getElementById('output-deck'); +} + +function getVersionInfo() +{ + var version = new Object(); + version.cz = __cz_version; + + var app = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo); + version.hostName = app.vendor + " " + app.name; + version.hostVersion = app.version; + version.host = version.hostName + " " + version.hostVersion + ", " + + client.platform; + version.hostBuildID = app.platformBuildID; + version.ua = app.name + " " + app.version + "/" + version.hostBuildID; + + return version; +} + +function initApplicationCompatibility() +{ + // This function does nothing more than tweak the UI based on the platform. + + client.lineEnd = "\n"; + + // Set up simple platform information. + switch (AppConstants.platform) { + case "linux": + client.platform = "Linux"; + break; + case "macosx": + client.platform = "Mac"; + break; + case "win": + client.platform = "Windows"; + // Windows likes \r\n line endings, as notepad can't cope with just + // \n logs. + client.lineEnd = "\r\n"; + break; + default: + client.platform = "Unknown"; + } + + CIRCServer.prototype.OS_RPLY = navigator.oscpu + " (" + + navigator.platform + ")"; +} + +function getFindData(e) +{ + // findNext() wrapper to add our findStart/findEnd events. + function _cz_findNext() { + // Send start notification. + var ev = new CEvent("find", "findStart", e.sourceObject, "onFindStart"); + client.eventPump.routeEvent(ev); + + // Call the original findNext() and keep the result for later. + var rv = this.__proto__.findNext(); + + // Send end notification with result code. + var ev = new CEvent("find", "findEnd", e.sourceObject, "onFindEnd"); + ev.findResult = rv; + client.eventPump.routeEvent(ev); + + // Return the original findNext()'s result to keep up appearances. + return rv; + }; + + // Getter for webBrowserFind property. + function _cz_webBrowserFind() { + return this._cz_wbf; + }; + + var findData = new nsFindInstData(); + findData.browser = e.sourceObject.frame; + findData.rootSearchWindow = getContentWindow(e.sourceObject.frame); + findData.currentSearchWindow = getContentWindow(e.sourceObject.frame); + + /* Wrap up the webBrowserFind object so we get called for findNext(). Use + * __proto__ so that everything else is exactly like the original object. + */ + findData._cz_wbf = { findNext: _cz_findNext }; + findData._cz_wbf.__proto__ = findData.webBrowserFind; + + /* Replace the nsFindInstData getter for webBrowserFind to call our + * function which in turn returns our object (_cz_wbf). + */ + findData.__defineGetter__("webBrowserFind", _cz_webBrowserFind); + + /* Yay, evil hacks! findData.init doesn't care about the findService, it + * gets option settings from webBrowserFind. As we want the wrap option *on* + * when we use /find foo, we set it on the findService there. However, + * restoring the original value afterwards doesn't help, because init() here + * overrides that value. Unless we make .init do something else, of course: + */ + findData._init = findData.init; + findData.init = + function init() + { + this._init(); + const FINDSVC_ID = "@mozilla.org/find/find_service;1"; + var findService = getService(FINDSVC_ID, "nsIFindService"); + this.webBrowserFind.wrapFind = findService.wrapFind; + }; + + return findData; +} + +function importFromFrame(method) +{ + client.__defineGetter__(method, import_wrapper); + CIRCNetwork.prototype.__defineGetter__(method, import_wrapper); + CIRCChannel.prototype.__defineGetter__(method, import_wrapper); + CIRCUser.prototype.__defineGetter__(method, import_wrapper); + CIRCDCCChat.prototype.__defineGetter__(method, import_wrapper); + CIRCDCCFileTransfer.prototype.__defineGetter__(method, import_wrapper); + + function import_wrapper() + { + var dummy = function(){}; + + if (!("frame" in this)) + return dummy; + + try + { + var window = getContentWindow(this.frame); + if (window && "initialized" in window && window.initialized && + method in window) + { + return function import_wrapper_apply() + { + window[method].apply(this, arguments); + }; + } + } + catch (ex) + { + ASSERT(0, "Caught exception calling: " + method + "\n" + ex); + } + + return dummy; + }; +} + +function processStartupScripts() +{ + client.plugins = new Object(); + var scripts = client.prefs["initialScripts"]; + var basePath = getURLSpecFromFile(client.prefs["profilePath"]); + var baseURL = Services.io.newURI(basePath); + for (var i = 0; i < scripts.length; ++i) + { + try + { + var url = Services.io.newURI(scripts[i], null, baseURL); + var path = getFileFromURLSpec(url.spec); + } + catch(ex) + { + var params = ["initialScripts", scripts[i]]; + display(getMsg(MSG_ERR_INVALID_PREF, params), MT_ERROR); + dd(formatException(ex)); + continue; + } + + if (url.scheme != "file" && url.scheme != "chrome") + { + display(getMsg(MSG_ERR_INVALID_SCHEME, scripts[i]), MT_ERROR); + continue; + } + + if (!path.exists()) + { + display(getMsg(MSG_ERR_ITEM_NOT_FOUND, url.spec), MT_WARN); + continue; + } + + if (path.isDirectory()) + loadPluginDirectory(path); + else + loadLocalFile(path); + } +} + +function loadPluginDirectory(localPath, recurse) +{ + if (typeof recurse == "undefined") + recurse = 1; + + var initPath = localPath.clone(); + initPath.append("init.js"); + if (initPath.exists()) + loadLocalFile(initPath); + + if (recurse < 1) + return; + + var enumer = localPath.directoryEntries; + while (enumer.hasMoreElements()) + { + var entry = enumer.getNext(); + entry = entry.QueryInterface(Components.interfaces.nsIFile); + if (entry.isDirectory()) + loadPluginDirectory(entry, recurse - 1); + } +} + +function loadLocalFile(localFile) +{ + var url = getURLSpecFromFile(localFile); + var glob = new Object(); + dispatch("load", {url: url, scope: glob}); +} + +function getPluginById(id) +{ + return client.plugins[id] || null; +} + +function getPluginByURL(url) +{ + for (var k in client.plugins) + { + if (client.plugins[k].url == url) + return client.plugins[k]; + + } + + return null; +} + +function disablePlugin(plugin, destroy) +{ + if (!plugin.enabled) + { + display(getMsg(MSG_IS_DISABLED, plugin.id)); + return true; + } + + if (plugin.API > 0) + { + if (!plugin.disable()) + { + display(getMsg(MSG_CANT_DISABLE, plugin.id)); + return false; + } + + if (destroy) + { + client.prefManager.removeObserver(plugin.prefManager); + plugin.prefManager.destroy(); + } + else + { + plugin.prefs["enabled"] = false; + } + } + else if ("disablePlugin" in plugin.scope) + { + plugin.scope.disablePlugin(); + } + else + { + display(getMsg(MSG_CANT_DISABLE, plugin.id)); + return false; + } + + display(getMsg(MSG_PLUGIN_DISABLED, plugin.id)); + if (!destroy) + { + plugin.enabled = false; + } + return true; +} + +function processStartupAutoperform() +{ + var cmdary = client.prefs["autoperform.client"]; + for (var i = 0; i < cmdary.length; ++i) + { + if (cmdary[i][0] == "/") + client.dispatch(cmdary[i].substr(1)); + else + client.dispatch(cmdary[i]); + } +} + +function processStartupURLs() +{ + var wentSomewhere = false; + + if ("arguments" in window && + 0 in window.arguments && typeof window.arguments[0] == "object" && + "url" in window.arguments[0]) + { + var url = window.arguments[0].url; + if (url.search(/^ircs?:\/?\/?\/?$/i) == -1) + { + /* if the url is not irc: irc:/, irc://, or ircs equiv then go to it. */ + gotoIRCURL(url); + wentSomewhere = true; + } + } + /* check to see whether the URL has been passed via the command line + instead. */ + else if ("arguments" in window && + 0 in window.arguments && typeof window.arguments[0] == "string") + { + var url = window.arguments[0] + var urlMatches = url.match(/^ircs?:\/\/\/?(.*)$/) + if (urlMatches) + { + if (urlMatches[1]) + { + /* if the url is not "irc://", "irc:///" or an ircs equiv then + go to it. */ + gotoIRCURL(url); + wentSomewhere = true; + } + } + else if (url) + { + /* URL parameter is not blank, but does not not conform to the + irc[s] scheme. */ + display(getMsg(MSG_ERR_INVALID_SCHEME, url), MT_ERROR); + } + } + + /* if we had nowhere else to go, connect to any default urls */ + if (!wentSomewhere) + openStartupURLs(); + + if (client.viewsArray.length > 1 && !isStartupURL("irc://")) + dispatch("delete-view", { view: client }); + + /* XXX: If we have the "stop XBL breaking" hidden tab, remove it, to + * stop XBL breaking later. Oh, the irony. + */ + if (client.tabs.firstChild.hidden) + { + client.tabs.removeChild(client.tabs.firstChild); + updateTabAttributes(); + } +} + +function openStartupURLs() +{ + var ary = client.prefs["initialURLs"]; + for (var i = 0; i < ary.length; ++i) + { + if (ary[i] && ary[i] == "irc:///") + { + // Clean out "default network" entries, which we don't + // support any more; replace with the harmless irc:// URL. + ary[i] = "irc://"; + client.prefs["initialURLs"].update(); + } + if (ary[i] && ary[i] != "irc://") + gotoIRCURL(ary[i]); + } +} + +function destroy() +{ + destroyPrefs(); +} + +function addURLToHistory(url) { + url = Services.io.newURI(url, "UTF-8"); + PlacesUtils.history.insert({ + url, + visits: [{ + date: new Date(), + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }], + }); +} + +function addStatusMessage(message) +{ + const DELAY_SCALE = 100; + const DELAY_MINIMUM = 5000; + + var delay = message.length * DELAY_SCALE; + if (delay < DELAY_MINIMUM) + delay = DELAY_MINIMUM; + + client.statusMessages.push({ message: message, delay: delay }); + updateStatusMessages(); +} + +function updateStatusMessages() +{ + if (client.statusMessages.length == 0) + { + var status = client.currentStatus || client.defaultStatus; + client.statusElement.setAttribute("label", status); + client.statusElement.removeAttribute("notice"); + return; + } + + var now = Number(new Date()); + var currentMsg = client.statusMessages[0]; + if ("expires" in currentMsg) + { + if (now >= currentMsg.expires) + { + client.statusMessages.shift(); + setTimeout(updateStatusMessages, 0); + } + else + { + setTimeout(updateStatusMessages, 1000); + } + } + else + { + currentMsg.expires = now + currentMsg.delay; + client.statusElement.setAttribute("label", currentMsg.message); + client.statusElement.setAttribute("notice", "true"); + setTimeout(updateStatusMessages, currentMsg.delay); + } +} + + +function setStatus(str) +{ + client.currentStatus = str; + updateStatusMessages(); + return str; +} + +client.__defineSetter__("status", setStatus); + +function getStatus() +{ + return client.currentStatus; +} + +client.__defineGetter__("status", getStatus); + +function isVisible (id) +{ + var e = document.getElementById(id); + + if (!ASSERT(e,"Bogus id ``" + id + "'' passed to isVisible() **")) + return false; + + return (e.getAttribute ("collapsed") != "true"); +} + +client.getConnectedNetworks = +function getConnectedNetworks() +{ + var rv = []; + for (var n in client.networks) + { + if (client.networks[n].isConnected()) + rv.push(client.networks[n]); + } + return rv; +} + +function combineNicks(nickList, max) +{ + if (!max) + max = 4; + + var combinedList = []; + + for (var i = 0; i < nickList.length; i += max) + { + count = Math.min(max, nickList.length - i); + var nicks = nickList.slice(i, i + count); + var str = new String(nicks.join(" ")); + str.count = count; + combinedList.push(str); + } + + return combinedList; +} + +function updateAllStalkExpressions() +{ + var list = client.prefs["stalkWords"]; + + for (var name in client.networks) + { + if ("stalkExpression" in client.networks[name]) + updateStalkExpression(client.networks[name], list); + } +} + +function updateStalkExpression(network) +{ + function escapeChar(ch) + { + return "\\" + ch; + }; + + var list = client.prefs["stalkWords"]; + + var ary = new Array(); + + ary.push(network.primServ.me.unicodeName.replace(/[^\w\d]/g, escapeChar)); + + for (var i = 0; i < list.length; ++i) + ary.push(list[i].replace(/[^\w\d]/g, escapeChar)); + + var re; + if (client.prefs["stalkWholeWords"]) + re = "(^|[\\W\\s])((" + ary.join(")|(") + "))([\\W\\s]|$)"; + else + re = "(" + ary.join(")|(") + ")"; + + network.stalkExpression = new RegExp(re, "i"); +} + +function getDefaultFontSize() +{ + const PREF_CTRID = "@mozilla.org/preferences-service;1"; + const nsIPrefService = Components.interfaces.nsIPrefService; + const nsIPrefBranch = Components.interfaces.nsIPrefBranch; + + var prefSvc = Components.classes[PREF_CTRID].getService(nsIPrefService); + var prefBranch = prefSvc.getBranch(null); + + // PX size pref: font.size.variable.x-western + var pxSize = 16; + try + { + pxSize = prefBranch.getIntPref("font.size.variable.x-western"); + } + catch(ex) { } + + var dpi = 96; + try + { + // Get the DPI the fun way (make Mozilla do the work). + var b = document.createElement("box"); + b.style.width = "1in"; + dpi = window.getComputedStyle(b, null).width.match(/^\d+/); + } + catch(ex) + { + try + { + // Get the DPI the fun way (make Mozilla do the work). + b = document.createElementNS("box", XHTML_NS); + b.style.width = "1in"; + dpi = window.getComputedStyle(b, null).width.match(/^\d+/); + } + catch(ex) { } + } + + return Math.round((pxSize / dpi) * 72); +} + +function getDefaultContext(cx) +{ + if (!cx) + cx = new Object(); + /* Use __proto__ here and in all other get*Context so that the command can + * tell the difference between getObjectDetails and actual parameters. See + * cmdJoin for more details. + */ + cx.__proto__ = getObjectDetails(client.currentObject); + return cx; +} + +function getMessagesContext(cx, element) +{ + if (!cx) + cx = new Object(); + cx.__proto__ = getObjectDetails(client.currentObject); + if (!element) + element = document.popupNode; + + while (element) + { + switch (element.localName) + { + case "a": + var href = element.getAttribute("href"); + cx.url = href; + break; + + case "tr": + // NOTE: msg-user is the canonicalName. + cx.canonNick = element.getAttribute("msg-user"); + if (!cx.canonNick) + break; + + // Strip out a potential ME! suffix. + var ary = cx.canonNick.match(/([^ ]+)/); + cx.canonNick = ary[1]; + + if (!cx.network) + break; + + if (cx.channel) + cx.user = cx.channel.getUser(cx.canonNick); + else + cx.user = cx.network.getUser(cx.canonNick); + + if (cx.user) + cx.nickname = cx.user.unicodeName; + else + cx.nickname = toUnicode(cx.canonNick, cx.network); + break; + } + + element = element.parentNode; + } + + return cx; +} + +function getTabContext(cx, element) +{ + if (!cx) + cx = new Object(); + if (!element) + element = document.popupNode; + + while (element) + { + if (element.localName == "tab") + { + cx.__proto__ = getObjectDetails(element.view); + return cx; + } + element = element.parentNode; + } + + return cx; +} + +function getUserlistContext(cx) +{ + if (!cx) + cx = new Object(); + cx.__proto__ = getObjectDetails(client.currentObject); + if (!cx.channel) + return cx; + + var user, tree = document.getElementById("user-list"); + cx.userList = new Array(); + cx.canonNickList = new Array(); + cx.nicknameList = getSelectedNicknames(tree); + + for (var i = 0; i < cx.nicknameList.length; ++i) + { + user = cx.channel.getUser(cx.nicknameList[i]) + cx.userList.push(user); + cx.canonNickList.push(user.canonicalName); + if (i == 0) + { + cx.user = user; + cx.nickname = user.unicodeName; + cx.canonNick = user.canonicalName; + } + } + cx.userCount = cx.userList.length; + + return cx; +} + +function getViewsContext(cx) +{ + function addView(view) + { + // We only need the view to have messages, so we accept hidden views. + if (!("messages" in view)) + return; + + var url = view.getURL(); + if (url in urls) + return; + + var label = view.viewName; + if (!getTabForObject(view)) + label = getMsg(MSG_VIEW_HIDDEN, [label]); + + var types = ["IRCClient", "IRCNetwork", "IRCDCCChat", + "IRCDCCFileTransfer"]; + var typesNetwork = ["IRCNetwork", "IRCChannel", "IRCUser"]; + var group = String(arrayIndexOf(types, view.TYPE)); + if (arrayIndexOf(typesNetwork, view.TYPE) != -1) + group = "1-" + getObjectDetails(view).network.viewName; + + var sort = group + "-" + view.viewName; + if (view.TYPE == "IRCNetwork") + sort = group; + + cx.views.push({url: url, label: label, group: group, sort: sort}); + urls[url] = true + }; + + function sortViews(a, b) + { + if (a.sort < b.sort) + return -1; + if (a.sort > b.sort) + return 1; + return 0; + }; + + if (!cx) + cx = new Object(); + cx.__proto__ = getObjectDetails(client.currentObject); + + cx.views = new Array(); + var urls = new Object(); + + /* XXX The code here works its way through all the open views *and* any + * possibly visible objects in the object model. This is necessary because + * occasionally objects get removed from the object model while still + * having a view open. See bug 459318 for one such case. Note that we + * won't be able to correctly switch to the "lost" view but showing it is + * less confusing than not. + */ + + for (var i in client.viewsArray) + addView(client.viewsArray[i].source); + + addView(client); + for (var n in client.networks) + { + addView(client.networks[n]); + for (var s in client.networks[n].servers) { + var server = client.networks[n].servers[s]; + for (var c in server.channels) + addView(server.channels[c]); + for (var u in server.users) + addView(server.users[u]); + } + } + + for (var u in client.dcc.users) + addView(client.dcc.users[u]); + for (var i = 0; i < client.dcc.chats.length; i++) + addView(client.dcc.chats[i]); + for (var i = 0; i < client.dcc.files.length; i++) + addView(client.dcc.files[i]); + + cx.views.sort(sortViews); + + return cx; +} + +function getSelectedNicknames(tree) +{ + var rv = []; + if (!tree || !tree.view || !tree.view.selection) + return rv; + var rangeCount = tree.view.selection.getRangeCount(); + + // Loop through the selection ranges. + for (var i = 0; i < rangeCount; ++i) + { + var start = {}, end = {}; + tree.view.selection.getRangeAt(i, start, end); + + // If they == -1, we've got no selection, so bail. + if ((start.value == -1) && (end.value == -1)) + continue; + /* Workaround: Because we use select(-1) instead of clearSelection() + * (see bug 197667) the tree will then give us selection ranges + * starting from -1 instead of 0! (See bug 319066.) + */ + if (start.value == -1) + start.value = 0; + + // Loop through the contents of the current selection range. + for (var k = start.value; k <= end.value; ++k) + rv.push(getNicknameForUserlistRow(k)); + } + return rv; +} + +function getFontContext(cx) +{ + if (!cx) + cx = new Object(); + cx.__proto__ = getObjectDetails(client.currentObject); + cx.fontSizeDefault = getDefaultFontSize(); + var view = client; + + if ("prefs" in cx.sourceObject) + { + cx.fontFamily = view.prefs["font.family"]; + if (cx.fontFamily.match(/^(default|(sans-)?serif|monospace)$/)) + delete cx.fontFamily; + + cx.fontSize = view.prefs["font.size"]; + if (cx.fontSize == 0) + delete cx.fontSize; + } + + return cx; +} + +function msgIsImportant(msg, sourceNick, network) +{ + var plainMsg = removeColorCodes(msg); + + var re = network.stalkExpression; + if (plainMsg.search(re) != -1 || sourceNick && sourceNick.search(re) == 0) + return true; + + return false; +} + +function ensureCachedCanonicalURLs(array) +{ + if ("canonicalURLs" in array) + return; + + /* Caching this on the array is safe because the PrefManager constructs + * a new array if the preference changes, but otherwise keeps the same + * one around. + */ + array.canonicalURLs = new Array(); + for (var i = 0; i < array.length; i++) + array.canonicalURLs.push(makeCanonicalIRCURL(array[i])); +} + +function isStartupURL(url) +{ + // We canonicalize all URLs before we do the (string) comparison. + url = makeCanonicalIRCURL(url); + var list = client.prefs["initialURLs"]; + ensureCachedCanonicalURLs(list); + return arrayContains(list.canonicalURLs, url); +} + +function cycleView(amount) +{ + var len = client.viewsArray.length; + if (len <= 1) + return; + + var tb = getTabForObject (client.currentObject); + if (!tb) + return; + + var vk = Number(tb.getAttribute("viewKey")); + var destKey = (vk + amount) % len; /* wrap around */ + if (destKey < 0) + destKey += len; + + dispatch("set-current-view", { view: client.viewsArray[destKey].source }); +} + +// Plays the sound for a particular event on a type of object. +function playEventSounds(type, event, source) +{ + if (!client.sound || !client.prefs["sound.enabled"]) + return; + + // Converts .TYPE values into the event object names. + // IRCChannel => channel, IRCUser => user, etc. + if (type.match(/^IRC/)) + type = type.substr(3, type.length).toLowerCase(); + + // DCC Chat sessions should act just like user views. + if (type == "dccchat") + type = "user"; + + var ev = type + "." + event; + + if (ev in client.soundList) + return; + + var src = source ? source : client; + + if (!(("sound." + ev) in src.prefs)) + return; + + var s = src.prefs["sound." + ev]; + + if (!s) + return; + + if (client.prefs["sound.overlapDelay"] > 0) + { + client.soundList[ev] = true; + setTimeout(function() { + delete client.soundList[ev]; + }, client.prefs["sound.overlapDelay"]); + } + + if (event == "start") + { + blockEventSounds(type, "event"); + blockEventSounds(type, "chat"); + blockEventSounds(type, "stalk"); + } + + playSounds(s); +} + +// Blocks a particular type of event sound occuring. +function blockEventSounds(type, event) +{ + if (!client.sound || !client.prefs["sound.enabled"]) + return; + + // Converts .TYPE values into the event object names. + // IRCChannel => channel, IRCUser => user, etc. + if (type.match(/^IRC/)) + type = type.substr(3, type.length).toLowerCase(); + + var ev = type + "." + event; + + if (client.prefs["sound.overlapDelay"] > 0) + { + client.soundList[ev] = true; + setTimeout(function() { + delete client.soundList[ev]; + }, client.prefs["sound.overlapDelay"]); + } +} + +function playSounds(list) +{ + var ary = list.split (" "); + if (ary.length == 0) + return; + + playSound(ary[0]); + for (var i = 1; i < ary.length; ++i) + setTimeout(playSound, 250 * i, ary[i]); +} + +function playSound(file) +{ + if (!client.sound || !client.prefs["sound.enabled"] || !file) + return; + + if (file == "beep") + { + client.sound.beep(); + } + else + { + try + { + client.sound.play(Services.io.newURI(file)); + } + catch (ex) + { + // ignore exceptions from this pile of code. + } + } +} + +/* timer-based mainloop */ +function mainStep() +{ + try + { + var count = client.eventPump.stepEvents(); + if (count > 0) + setTimeout(mainStep, client.STEP_TIMEOUT); + else + setTimeout(mainStep, client.STEP_TIMEOUT / 5); + } + catch(ex) + { + dd("Exception in mainStep!"); + dd(formatException(ex)); + setTimeout(mainStep, client.STEP_TIMEOUT); + } +} + +function openQueryTab(server, nick) +{ + var user = server.addUser(nick); + addURLToHistory(user.getURL()); + if (!("messages" in user)) + { + var value = ""; + var same = true; + for (var c in server.channels) + { + var chan = server.channels[c]; + if (!(user.collectionKey in chan.users)) + continue; + /* This takes a boolean value for each channel (true - channel has + * same value as first), and &&-s them all together. Thus, |same| + * will tell us, at the end, if all the channels found have the + * same value for charset. + */ + if (value) + same = same && (value == chan.prefs["charset"]); + else + value = chan.prefs["charset"]; + } + /* If we've got a value, and it's the same accross all channels, + * we use it as the *default* for the charset pref. If not, it'll + * just keep the "defer" default which pulls it off the network. + */ + if (value && same) + { + user.prefManager.prefRecords["charset"].defaultValue = value; + } + + dispatch("create-tab-for-view", { view: user }); + + user.doAutoPerform(); + } + return user; +} + +function arraySpeak (ary, single, plural) +{ + var rv = ""; + var and = MSG_AND; + + switch (ary.length) + { + case 0: + break; + + case 1: + rv = ary[0]; + if (single) + rv += " " + single; + break; + + case 2: + rv = ary[0] + " " + and + " " + ary[1]; + if (plural) + rv += " " + plural; + break; + + default: + for (var i = 0; i < ary.length - 1; ++i) + rv += ary[i] + ", "; + rv += and + " " + ary[ary.length - 1]; + if (plural) + rv += " " + plural; + break; + } + + return rv; + +} + +function getObjectDetails (obj, rv) +{ + if (!rv) + rv = new Object(); + + if (!ASSERT(obj && typeof obj == "object", + "INVALID OBJECT passed to getObjectDetails (" + obj + "). **")) + { + return rv; + } + + rv.sourceObject = obj; + rv.TYPE = obj.TYPE; + rv.parent = ("parent" in obj) ? obj.parent : null; + rv.user = null; + rv.channel = null; + rv.server = null; + rv.network = null; + if (window && window.content && window.content.getSelection() != "") + rv.selectedText = window.content.getSelection(); + + switch (obj.TYPE) + { + case "IRCChannel": + rv.viewType = MSG_CHANNEL; + rv.channel = obj; + rv.channelName = obj.unicodeName; + rv.server = rv.channel.parent; + rv.network = rv.server.parent; + break; + + case "IRCUser": + rv.viewType = MSG_USER; + rv.user = obj; + rv.userName = rv.nickname = obj.unicodeName; + rv.server = rv.user.parent; + rv.network = rv.server.parent; + break; + + case "IRCChanUser": + rv.viewType = MSG_USER; + rv.user = obj; + rv.userName = rv.nickname = obj.unicodeName; + rv.channel = rv.user.parent; + rv.server = rv.channel.parent; + rv.network = rv.server.parent; + break; + + case "IRCNetwork": + rv.network = obj; + rv.viewType = MSG_NETWORK; + if ("primServ" in rv.network) + rv.server = rv.network.primServ; + else + rv.server = null; + break; + + case "IRCClient": + rv.viewType = MSG_TAB; + break; + + case "IRCDCCUser": + //rv.viewType = MSG_USER; + rv.user = obj; + rv.userName = obj.unicodeName; + break; + + case "IRCDCCChat": + //rv.viewType = MSG_USER; + rv.chat = obj; + rv.user = obj.user; + rv.userName = obj.unicodeName; + break; + + case "IRCDCCFileTransfer": + //rv.viewType = MSG_USER; + rv.file = obj; + rv.user = obj.user; + rv.userName = obj.unicodeName; + rv.fileName = obj.filename; + break; + + default: + /* no setup for unknown object */ + break; + } + + if (rv.network) + rv.networkName = rv.network.unicodeName; + + return rv; + +} + +function findDynamicRule (selector) +{ + var rules = frames[0].document.styleSheets[1].cssRules; + + if (isinstance(selector, RegExp)) + fun = "search"; + else + fun = "indexOf"; + + for (var i = 0; i < rules.length; ++i) + { + var rule = rules.item(i); + if (rule.selectorText && rule.selectorText[fun](selector) == 0) + return {sheet: frames[0].document.styleSheets[1], rule: rule, + index: i}; + } + + return null; +} + +function addDynamicRule (rule) +{ + var rules = frames[0].document.styleSheets[1]; + + var pos = rules.cssRules.length; + rules.insertRule (rule, pos); +} + +function getCommandEnabled(command) +{ + try { + var dispatcher = document.commandDispatcher; + var controller = dispatcher.getControllerForCommand(command); + + return controller.isCommandEnabled(command); + } + catch (e) + { + return false; + } +} + +function doCommand(command) +{ + try { + var dispatcher = document.commandDispatcher; + var controller = dispatcher.getControllerForCommand(command); + if (controller && controller.isCommandEnabled(command)) + controller.doCommand(command); + } + catch (e) + { + } +} + +function doCommandWithParams(command, params) +{ + try { + var dispatcher = document.commandDispatcher; + var controller = dispatcher.getControllerForCommand(command); + controller.QueryInterface(Components.interfaces.nsICommandController); + + if (!controller || !controller.isCommandEnabled(command)) + return; + + var cmdparams = newObject("@mozilla.org/embedcomp/command-params;1", + "nsICommandParams"); + for (var i in params) + cmdparams.setISupportsValue(i, params[i]); + + controller.doCommandWithParams(command, cmdparams); + } + catch (e) + { + } +} + +var testURLs = [ + "irc:", + "irc://", + "irc://foo", + "irc://foo/", + "irc://foo/,isserver", + "irc://foo/chatzilla", + "irc://foo/chatzilla/", + "irc://foo:6666", + "irc://foo:6666/", + "irc://irc.foo.org", + "irc://irc.foo.org/", + "irc://irc.foo.org/,needpass", + "irc://irc.foo.org/?msg=hello%20there", + "irc://irc.foo.org/?msg=hello%20there&ignorethis", + "irc://irc.foo.org/%23mozilla,needkey?msg=hello%20there&ignorethis", + "irc://libera.chat/", + "irc://libera.chat/,isserver", + "irc://[fe80::5d49:767b:4b68:1b17]", + "irc://[fe80::5d49:767b:4b68:1b17]/", + "irc://[fe80::5d49:767b:4b68:1b17]:6666", + "irc://[fe80::5d49:767b:4b68:1b17]:6666/" +]; + +var testFailURLs = [ + "irc:///", + "irc:///help", + "irc:///help,needkey", + "irc://irc.foo.org/,isnick", + "invalids" +]; + +function doURLTest() +{ + var passed = 0, total = testURLs.length + testFailURLs.length; + for (var i = 0; i < testURLs.length; i++) + { + var o = parseIRCURL(testURLs[i]); + if (!o) + display("Parse of '" + testURLs[i] + "' failed.", MT_ERROR); + else + passed++; + } + for (var i = 0; i < testFailURLs.length; i++) + { + var o = parseIRCURL(testFailURLs[i]); + if (o) + display("Parse of '" + testFailURLs[i] + "' unexpectedly succeeded.", MT_ERROR); + else + passed++; + } + display("Passed " + passed + " out of " + total + " tests (" + + passed / total * 100 + "%).", MT_INFO); +} + +var testIRCURLObjects = [ + [{}, "irc://"], + [{host: "undernet"}, "irc://undernet/"], + [{host: "irc.undernet.org"}, "irc://irc.undernet.org/"], + [{host: "irc.undernet.org", isserver: true}, "irc://irc.undernet.org/"], + [{host: "undernet", isserver: true}, "irc://undernet/,isserver"], + [{host: "irc.undernet.org", port: 6667}, "irc://irc.undernet.org/"], + [{host: "irc.undernet.org", port: 1}, "irc://irc.undernet.org:1/"], + [{host: "irc.undernet.org", port: 1, scheme: "ircs"}, + "ircs://irc.undernet.org:1/"], + [{host: "irc.undernet.org", port: 6697, scheme: "ircs"}, + "ircs://irc.undernet.org/"], + [{host: "undernet", needpass: true}, "irc://undernet/,needpass"], + [{host: "undernet", pass: "cz"}, "irc://undernet/?pass=cz"], + [{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"], + [{host: "undernet", target: "#foo"}, "irc://undernet/%23foo"], + [{host: "undernet", target: "#foo", needkey: true}, + "irc://undernet/%23foo,needkey"], + [{host: "undernet", target: "John", isnick: true}, + "irc://undernet/John,isnick"], + [{host: "undernet", target: "#foo", key: "cz"}, + "irc://undernet/%23foo?key=cz"], + [{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"], + [{host: "undernet", target: "John", msg: "spam!"}, + "irc://undernet/John?msg=spam%21"], + [{host: "undernet", target: "foo", isnick: true, msg: "spam!", pass: "cz"}, + "irc://undernet/foo,isnick?msg=spam%21&pass=cz"] +]; + +function doObjectURLtest() +{ + var passed = 0, total = testIRCURLObjects.length; + for (var i = 0; i < total; i++) + { + var obj = testIRCURLObjects[i][0]; + var url = testIRCURLObjects[i][1]; + var parsedURL = constructIRCURL(obj) + if (url != parsedURL) + { + display("Parsed IRC Object incorrectly! Expected '" + url + + "', got '" + parsedURL, MT_ERROR); + } + else + { + passed++; + } + } + display("Passed " + passed + " out of " + total + " tests (" + + passed / total * 100 + "%).", MT_INFO); +} + + +function gotoIRCURL(url, e) +{ + var urlspec = url; + if (typeof url == "string") + url = parseIRCURL(url); + + if (!url) + { + window.alert(getMsg(MSG_ERR_BAD_IRCURL, urlspec)); + return; + } + + if (!url.host) + { + /* focus the *client* view for irc:, irc:/, and irc:// (the only irc + * urls that don't have a host. (irc:/// implies a connect to the + * default network.) + */ + client.pendingViewContext = e; + dispatch("client"); + delete client.pendingViewContext; + return; + } + + let isSecure = url.scheme == "ircs"; + let network; + // Make sure host is in lower case. + url.host = url.host.toLowerCase(); + + // Convert a request for a server to a network if we know it. + if (url.isserver) + { + for (var n in client.networks) + { + network = client.networks[n]; + for (var s in network.servers) + { + let server = network.servers[s]; + if ((server.hostname == url.host) && + (server.isSecure == isSecure) && + (!url.port || (server.port == url.port))) + { + url.isserver = false; + url.host = network.canonicalName; + if (!url.port) + url.port = server.port; + break; + } + } + if (!url.isserver) + break; + } + } + + let name = url.host; + network = client.getNetwork(name); + + if (url.isserver) + { + let found = false; + if (network) { + for (let s in network.servers) { + let server = network.servers[s]; + if ((server.isSecure == isSecure) && + (!url.port || (server.port == url.port))) { + found = true; + if (!url.port) + url.port = server.port; + break; + } + } + } + + // If still no port set, use the default. + if (!url.port) + url.port = isSecure ? 6697 : 6667; + + if (!found) { + name += ":" + url.port; + + // If there is no temporary network for this server:port, create one. + if (!client.getNetwork(name)) { + let server = {name: url.host, port: url.port, isSecure: isSecure}; + client.addNetwork(name, [server], true); + } + network = client.getNetwork(name); + } + } + else + { + // There is no network called this, sorry. + if (!network) + { + display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name)); + return; + } + } + + // We should only prompt for a password if we're not connected. + if (network.state == NET_OFFLINE) + { + // Check for a network password. + url.pass = client.tryToGetLogin(network.getURL(), "serv", "*", + url.pass, url.needpass, + getMsg(MSG_HOST_PASSWORD, + network.getURL())); + } + + // Adjust secure setting for temporary networks (so user can override). + if (network.temporary) + network.serverList[0].isSecure = url.scheme == "ircs"; + + // Adjust password for all servers (so user can override). + if (url.pass) + { + for (var s in network.servers) + network.servers[s].password = url.pass; + } + + // Start the connection and pend anything else if we're not ready. + if (network.state != NET_ONLINE) + { + client.pendingViewContext = e; + if (!network.isConnected()) + { + client.connectToNetwork(network, url.scheme == "ircs"); + } + else + { + dispatch("create-tab-for-view", { view: network }); + dispatch("set-current-view", { view: network }); + } + delete client.pendingViewContext; + + if (!url.target) + return; + + // We're not completely online, so everything else is pending. + if (!("pendingURLs" in network)) + network.pendingURLs = new Array(); + network.pendingURLs.unshift({ url: url, e: e }); + return; + } + + // We're connected now, process the target. + if (url.target) + { + var targetObject; + var ev; + if (url.isnick) + { + /* url points to a person. */ + var nick = url.target; + var ary = url.target.split("!"); + if (ary) + nick = ary[0]; + + client.pendingViewContext = e; + targetObject = network.dispatch("query", {nickname: nick}); + delete client.pendingViewContext; + } + else + { + /* url points to a channel */ + var key; + var serv = network.primServ; + var target = url.target; + if (url.charset) + { + var chan = new CIRCChannel(serv, target, fromUnicode(target, url.charset)); + chan.prefs["charset"] = url.charset; + } + else + { + // Must do this the hard way... we have the server's format + // for the channel name here, and all our commands only work + // with the Unicode forms. + + /* If we don't have a valid prefix, stick a "#" on it. + * NOTE: This is always a "#" so that URLs may be compared + * properly without involving the server (e.g. off-line). + */ + if ((arrayIndexOf(["#", "&", "+", "!"], target[0]) == -1) && + (arrayIndexOf(serv.channelTypes, target[0]) == -1)) + { + target = "#" + target; + } + + var chan = new CIRCChannel(serv, null, target); + } + + if (url.needkey && !chan.joined) + { + if (url.key) + key = url.key; + else + key = window.promptPassword(getMsg(MSG_URL_KEY, url.spec)); + } + client.pendingViewContext = e; + d = {channelToJoin: chan, key: key}; + targetObject = network.dispatch("join", d); + delete client.pendingViewContext; + + if (!targetObject) + return; + } + + if (url.msg) + { + client.pendingViewContext = e; + var msg; + if (url.msg.indexOf("\01ACTION") == 0) + { + msg = filterOutput(url.msg, "ACTION", targetObject); + targetObject.display(msg, "ACTION", "ME!", + client.currentObject); + } + else + { + msg = filterOutput(url.msg, "PRIVMSG", targetObject); + targetObject.display(msg, "PRIVMSG", "ME!", + client.currentObject); + } + targetObject.say(msg); + dispatch("set-current-view", { view: targetObject }); + delete client.pendingViewContext; + } + } + else + { + client.pendingViewContext = e; + dispatch("create-tab-for-view", { view: network }); + dispatch("set-current-view", { view: network }); + delete client.pendingViewContext; + } +} + +function updateProgress() +{ + var busy; + var progress = -1; + + if ("busy" in client.currentObject) + busy = client.currentObject.busy; + + if ("progress" in client.currentObject) + progress = client.currentObject.progress; + + if (!busy) + progress = 0; + + client.progressPanel.collapsed = !busy; + client.progressBar.mode = (progress < 0 ? "undetermined" : "determined"); + if (progress >= 0) + client.progressBar.value = progress; +} + +function updateSecurityIcon() +{ + var o = getObjectDetails(client.currentObject); + var securityButton = window.document.getElementById("security-button"); + securityButton.label = ""; + securityButton.removeAttribute("level"); + securityButton.removeAttribute("tooltiptext"); + if (!o.server || !o.server.isConnected) // No server or connection? + { + securityButton.setAttribute("tooltiptext", MSG_SECURITY_INFO); + return; + } + + let tooltiptext = MSG_SECURITY_INFO; + switch (o.server.connection.getSecurityState()) { + case STATE_IS_SECURE: + securityButton.setAttribute("level", "high"); + + // Update the tooltip. + var issuer = o.server.connection.getCertificate().issuerOrganization; + tooltiptext = getMsg(MSG_SECURE_CONNECTION, issuer); + break; + case STATE_IS_BROKEN: + securityButton.setAttribute("level", "broken"); + break; + case STATE_IS_INSECURE: + default: + securityButton.setAttribute("level", "none"); + } + securityButton.label = o.server.hostname; + securityButton.setAttribute("tooltiptext", tooltiptext); +} + +function updateLoggingIcon() +{ + var state = client.currentObject.prefs["log"] ? "on" : "off"; + var icon = window.document.getElementById("logging-status"); + icon.setAttribute("loggingstate", state); + icon.setAttribute("tooltiptext", getMsg("msg.logging.icon." + state)); +} + +function updateAlertIcon(aToggle) { + let alertState = client.prefs["alert.globalEnabled"]; + if (aToggle) { + alertState = !alertState; + client.prefs["alert.globalEnabled"] = alertState; + } + let state = alertState ? "on" : "off"; + let icon = window.document.getElementById("alert-status"); + icon.setAttribute("alertstate", state); + icon.setAttribute("tooltiptext", getMsg("msg.alert.icon." + state)); +} + +function initOfflineIcon() +{ + const PRBool_CID = "@mozilla.org/supports-PRBool;1"; + const OS_CID = "@mozilla.org/observer-service;1"; + const nsISupportsPRBool = Components.interfaces.nsISupportsPRBool; + + client.offlineObserver = { + _element: document.getElementById("offline-status"), + state: function offline_state() + { + return (Services.io.offline ? "offline" : "online"); + }, + observe: function offline_observe(subject, topic, state) + { + if ((topic == "offline-requested") && + (client.getConnectionCount() > 0)) + { + var buttonAry = [MSG_REALLY_GO_OFFLINE, MSG_DONT_GO_OFFLINE]; + var rv = confirmEx(MSG_GOING_OFFLINE, buttonAry); + if (rv == 1) // Don't go offline, please! + { + subject.QueryInterface(nsISupportsPRBool); + subject.data = true; + } + } + else if (topic == "network:offline-status-changed") + { + this.updateOfflineUI(); + } + }, + updateOfflineUI: function offline_uiUpdate() + { + this._element.setAttribute("offline", Services.io.offline); + var tooltipMsgId = "MSG_OFFLINESTATE_" + this.state().toUpperCase(); + this._element.setAttribute("tooltiptext", window[tooltipMsgId]); + }, + toggleOffline: function offline_toggle() + { + // Check whether people are OK with us going offline: + if (!Services.io.offline && !this.canGoOffline()) + return; + + // Stop automatic management of the offline status, if existing. + Services.io.manageOfflineStatus = false; + + // Actually change the offline state. + Services.io.offline = !Services.io.offline; + }, + canGoOffline: function offline_check() + { + try + { + var canGoOffline = newObject(PRBool_CID, "nsISupportsPRBool"); + Services.obs.notifyObservers(canGoOffline, "offline-requested"); + // Someone called for a halt + if (canGoOffline.data) + return false; + } + catch (ex) + { + dd("Exception when trying to ask if we could go offline:" + ex); + } + return true; + } + }; + + Services.obs.addObserver(client.offlineObserver, "offline-requested"); + Services.obs.addObserver(client.offlineObserver, + "network:offline-status-changed"); + client.offlineObserver.updateOfflineUI(); +} + +function uninitOfflineIcon() +{ + Services.obs.removeObserver(client.offlineObserver, "offline-requested"); + Services.obs.removeObserver(client.offlineObserver, + "network:offline-status-changed"); +} + +client.idleObserver = { + QueryInterface: function io_qi(iid) + { + if (!iid || (!iid.equals(Components.interfaces.nsIObserver) && + !iid.equals(Components.interfaces.nsISupports))) + { + throw Components.results.NS_ERROR_NO_INTERFACE; + } + return this; + }, + observe: function io_observe(subject, topic, data) + { + if ((topic == "idle") && !client.prefs["away"]) + { + if (!client.prefs["awayIdleMsg"]) + client.prefs["awayIdleMsg"] = MSG_AWAY_IDLE_DEFAULT; + client.dispatch("idle-away", {reason: client.prefs["awayIdleMsg"]}); + client.isIdleAway = true; + } + else if ((topic == "back" || topic == "active") && client.isIdleAway) + { + client.dispatch("idle-back"); + client.isIdleAway = false; + } + } +}; + +function initIdleAutoAway(timeout) +{ + // Don't try to do anything if we are disabled + if (!timeout) + return; + + var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService"); + if (!is) + { + display(MSG_ERR_NO_IDLESERVICE, MT_WARN); + client.prefs["autoIdleTime"] = 0; + return; + } + + try + { + is.addIdleObserver(client.idleObserver, timeout * 60); + } + catch (ex) + { + display(formatException(ex), MT_ERROR); + } +} + +function uninitIdleAutoAway(timeout) +{ + // Don't try to do anything if we were disabled before + if (!timeout) + return; + + var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService"); + if (!is) + return; + + try + { + is.removeIdleObserver(client.idleObserver, timeout * 60); + } + catch (ex) + { + display(formatException(ex), MT_ERROR); + } +} + +function updateAppMotif(motifURL) +{ + var node = document.firstChild; + while (node && ((node.nodeType != node.PROCESSING_INSTRUCTION_NODE) || + !(/name="dyn-motif"/).test(node.data))) + { + node = node.nextSibling; + } + + motifURL = motifURL.replace(/"/g, "%22"); + var dataStr = "href=\"" + motifURL + "\" name=\"dyn-motif\""; + try + { + // No dynamic style node yet. + if (!node) + { + node = document.createProcessingInstruction("xml-stylesheet", dataStr); + document.insertBefore(node, document.firstChild); + } + else if (node.data != dataStr) + { + node.data = dataStr; + document.insertBefore(node, node.nextSibling); + } + } + catch (ex) + { + dd(formatException(ex)); + var err = ex.name; + // Mozilla 1.0 doesn't like document.insertBefore(..., + // document.firstChild); though it has a prototype for it - + // check for the right error: + if (err == "NS_ERROR_NOT_IMPLEMENTED") + { + display(MSG_NO_DYNAMIC_STYLE, MT_INFO); + updateAppMotif = function() {}; + } + } +} + +function updateSpellcheck(value) +{ + value = value.toString(); + document.getElementById("input").setAttribute("spellcheck", value); + document.getElementById("multiline-input").setAttribute("spellcheck", + value); +} + +function updateNetwork() +{ + var o = getObjectDetails (client.currentObject); + + var lag = MSG_UNKNOWN; + var nick = ""; + if (o.server) + { + if (o.server.me) + nick = o.server.me.unicodeName; + lag = (o.server.lag != -1) ? o.server.lag.toFixed(2) : MSG_UNKNOWN; + } + client.statusBar["header-url"].setAttribute("value", + client.currentObject.getURL()); + client.statusBar["header-url"].setAttribute("href", + client.currentObject.getURL()); + client.statusBar["header-url"].setAttribute("name", + client.currentObject.unicodeName); +} + +function updateTitle (obj) +{ + if (!(("currentObject" in client) && client.currentObject) || + (obj && obj != client.currentObject)) + return; + + var tstring = MSG_TITLE_UNKNOWN; + var o = getObjectDetails(client.currentObject); + var net = o.network ? o.network.unicodeName : ""; + var nick = ""; + client.statusBar["server-nick"].disabled = false; + + switch (client.currentObject.TYPE) + { + case "IRCNetwork": + var serv = "", port = ""; + if (client.currentObject.isConnected()) + { + serv = o.server.hostname; + port = o.server.port; + if (o.server.me) + nick = o.server.me.unicodeName; + tstring = getMsg(MSG_TITLE_NET_ON, [nick, net, serv, port]); + } + else + { + nick = client.currentObject.INITIAL_NICK; + tstring = getMsg(MSG_TITLE_NET_OFF, [nick, net]); + } + break; + + case "IRCChannel": + var chan = "", mode = "", topic = ""; + if ("me" in o.parent) + { + nick = o.parent.me.unicodeName; + if (o.parent.me.collectionKey in client.currentObject.users) + { + let cuser = client.currentObject.users[o.parent.me.collectionKey]; + if (cuser.isFounder) + nick = "~" + nick; + else if (cuser.isAdmin) + nick = "&" + nick; + else if (cuser.isOp) + nick = "@" + nick; + else if (cuser.isHalfOp) + nick = "%" + nick; + else if (cuser.isVoice) + nick = "+" + nick; + } + } + else + { + nick = MSG_TITLE_NONICK; + } + chan = o.channel.unicodeName; + mode = o.channel.mode.getModeStr(); + if (!mode) + mode = MSG_TITLE_NO_MODE; + topic = o.channel.topic ? o.channel.topic : MSG_TITLE_NO_TOPIC; + var re = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g; + topic = topic.replace(re, ""); + + tstring = getMsg(MSG_TITLE_CHANNEL, [nick, chan, mode, topic]); + break; + + case "IRCUser": + nick = client.currentObject.unicodeName; + var source = ""; + if (client.currentObject.name) + { + source = "<" + client.currentObject.name + "@" + + client.currentObject.host +">"; + } + tstring = getMsg(MSG_TITLE_USER, [nick, source]); + nick = "me" in o.parent ? o.parent.me.unicodeName : MSG_TITLE_NONICK; + break; + + case "IRCClient": + nick = client.prefs["nickname"]; + break; + + case "IRCDCCChat": + client.statusBar["server-nick"].disabled = true; + nick = o.chat.me.unicodeName; + tstring = getMsg(MSG_TITLE_DCCCHAT, o.userName); + break; + + case "IRCDCCFileTransfer": + client.statusBar["server-nick"].disabled = true; + nick = o.file.me.unicodeName; + var data = [o.file.progress, o.file.filename, o.userName]; + if (o.file.state.dir == 1) + tstring = getMsg(MSG_TITLE_DCCFILE_SEND, data); + else + tstring = getMsg(MSG_TITLE_DCCFILE_GET, data); + break; + } + + if (0 && !client.uiState["tabstrip"]) + { + var actl = new Array(); + for (var i in client.activityList) + actl.push ((client.activityList[i] == "!") ? + (Number(i) + 1) + "!" : (Number(i) + 1)); + if (actl.length > 0) + tstring = getMsg(MSG_TITLE_ACTIVITY, + [tstring, actl.join (", ")]); + } + + document.title = tstring; + client.statusBar["server-nick"].setAttribute("label", nick); +} + +// Where 'right' is orientation, not wrong/right: +function updateUserlistSide(shouldBeLeft) +{ + var listParent = document.getElementById("tabpanels-contents-box"); + var isLeft = (listParent.childNodes[0].id == "user-list-box"); + if (isLeft == shouldBeLeft) + return; + if (shouldBeLeft) // Move from right to left. + { + listParent.insertBefore(listParent.childNodes[1], listParent.childNodes[0]); + listParent.insertBefore(listParent.childNodes[2], listParent.childNodes[0]); + listParent.childNodes[1].setAttribute("collapse", "before"); + } + else // Move from left to right. + { + listParent.appendChild(listParent.childNodes[1]); + listParent.appendChild(listParent.childNodes[0]); + listParent.childNodes[1].setAttribute("collapse", "after"); + } + var userlist = document.getElementById("user-list") + if (client.currentObject && (client.currentObject.TYPE == "IRCChannel")) + userlist.view = client.currentObject.userList; +} + +function multilineInputMode (state) +{ + var multiInput = document.getElementById("multiline-input"); + var multiInputBox = document.getElementById("multiline-box"); + var singleInput = document.getElementById("input"); + var singleInputBox = document.getElementById("singleline-box"); + var splitter = document.getElementById("input-splitter"); + var iw = document.getElementById("input-widgets"); + var h; + + client._mlMode = state; + + if (state) /* turn on multiline input mode */ + { + + h = iw.getAttribute ("lastHeight"); + if (h) + iw.setAttribute ("height", h); /* restore the slider position */ + + singleInputBox.setAttribute ("collapsed", "true"); + splitter.setAttribute ("collapsed", "false"); + multiInputBox.setAttribute ("collapsed", "false"); + // multiInput should have the same direction as singleInput + multiInput.setAttribute("dir", singleInput.getAttribute("dir")); + multiInput.value = (client.input ? client.input.value : ""); + client.input = multiInput; + } + else /* turn off multiline input mode */ + { + h = iw.getAttribute ("height"); + iw.setAttribute ("lastHeight", h); /* save the slider position */ + iw.removeAttribute ("height"); /* let the slider drop */ + + splitter.setAttribute ("collapsed", "true"); + multiInputBox.setAttribute ("collapsed", "true"); + singleInputBox.setAttribute ("collapsed", "false"); + // singleInput should have the same direction as multiInput + singleInput.setAttribute("dir", multiInput.getAttribute("dir")); + singleInput.value = (client.input ? client.input.value : ""); + client.input = singleInput; + } + + client.input.focus(); +} + +function displayCertificateInfo() +{ + var o = getObjectDetails(client.currentObject); + if (!o.server) + return; + + if (!o.server.isSecure) + { + alert(getMsg(MSG_INSECURE_SERVER, o.server.hostname)); + return; + } + + viewCert(o.server.connection.getCertificate()); +} + +function onLoggingIcon() { + client.currentObject.dispatch("log", { state: "toggle" }); +} + +function newInlineText (data, className, tagName) +{ + if (typeof tagName == "undefined") + tagName = "html:span"; + + var a = document.createElementNS(XHTML_NS, tagName); + if (className) + a.setAttribute ("class", className); + + switch (typeof data) + { + case "string": + a.appendChild (document.createTextNode (data)); + break; + + case "object": + for (var p in data) + if (p != "data") + a.setAttribute (p, data[p]); + else + a.appendChild (document.createTextNode (data[p])); + break; + + case "undefined": + break; + + default: + ASSERT(0, "INVALID TYPE ('" + typeof data + "') passed to " + + "newInlineText."); + break; + + } + + return a; + +} + +function stringToMsg (message, obj) +{ + var ary = message.split ("\n"); + var span = document.createElementNS(XHTML_NS, "html:span"); + var data = getObjectDetails(obj); + + if (ary.length == 1) + client.munger.munge(ary[0], span, data); + else + { + for (var l = 0; l < ary.length - 1; ++l) + { + client.munger.munge(ary[l], span, data); + span.appendChild(document.createElementNS(XHTML_NS, "html:br")); + } + client.munger.munge(ary[l], span, data); + } + + return span; +} + +function getFrame() +{ + if (client.deck.childNodes.length == 0) + return undefined; + var panel = client.deck.selectedPanel; + return getContentWindow(panel); +} + +client.__defineGetter__ ("currentFrame", getFrame); + +function setCurrentObject (obj) +{ + if (!ASSERT(obj.messages, "INVALID OBJECT passed to setCurrentObject **")) + return; + + if ("currentObject" in client && client.currentObject == obj) + { + if (typeof client.pendingViewContext == "object") + dispatch("create-tab-for-view", { view: obj }); + return; + } + + // Set window.content to make screenreader apps find the chat content. + if (obj.frame && getContentWindow(obj.frame)) + window.content = getContentWindow(obj.frame); + + var tb, userList; + userList = document.getElementById("user-list"); + + if ("currentObject" in client && client.currentObject) + tb = getTabForObject(client.currentObject); + if (tb) + tb.setAttribute("state", "normal"); + + // If we're tracking last read lines, set a mark on the current view + // before switching to the new one. + if (tb && client.currentObject.prefs["autoMarker"]) + client.currentObject.dispatch("marker-set"); + + client.currentObject = obj; + + // Update userlist: + userList.view = null; + if (obj.TYPE == "IRCChannel") + { + userList.view = obj.userList; + updateUserList(); + } + + tb = dispatch("create-tab-for-view", { view: obj }); + if (tb) + { + tb.parentNode.selectedItem = tb; + tb.setAttribute("state", "current"); + } + + var vk = Number(tb.getAttribute("viewKey")); + delete client.activityList[vk]; + client.deck.selectedPanel = obj.frame; + + // Style userlist and the like: + updateAppMotif(obj.prefs["motif.current"]); + + updateTitle(); + updateProgress(); + updateSecurityIcon(); + updateLoggingIcon(); + + scrollDown(obj.frame, false); + + // Input area should have the same direction as the output area + if (("frame" in client.currentObject) && + client.currentObject.frame && + getContentDocument(client.currentObject.frame) && + ("body" in getContentDocument(client.currentObject.frame)) && + getContentDocument(client.currentObject.frame).body) + { + var contentArea = getContentDocument(client.currentObject.frame).body; + client.input.setAttribute("dir", contentArea.getAttribute("dir")); + } + client.input.focus(); +} + +function checkScroll(frame) +{ + var window = getContentWindow(frame); + if (!window || !window.document || !window.document.body) + return false; + + return (window.document.body.clientHeight - window.innerHeight - + window.pageYOffset) < 160; +} + +function scrollDown(frame, force) +{ + var window = getContentWindow(frame); + if (!window || !window.document || !window.document.body) + return; + + if (force || checkScroll(frame)) + window.scrollTo(0, window.document.body.clientHeight); +} + +function advanceKeyboardFocus(amount) +{ + var contentWin = getContentWindow(client.currentObject.frame); + var contentDoc = getContentDocument(client.currentObject.frame); + var userList = document.getElementById("user-list"); + + // Focus userlist, inputbox and outputwindow in turn: + var focusableElems = [userList, client.input.inputField, contentWin]; + + var elem = document.commandDispatcher.focusedElement; + // Finding focus in the content window is "hard". It's going to be null + // if the window itself is focused, and "some element" inside of it if the + // user starts tabbing through. + if (!elem || (elem.ownerDocument == contentDoc)) + elem = contentWin; + + var newIndex = (arrayIndexOf(focusableElems, elem) * 1 + 3 + amount) % 3; + focusableElems[newIndex].focus(); + + // Make it obvious this element now has focus. + var outlinedElem; + if (focusableElems[newIndex] == client.input.inputField) + outlinedElem = client.input.parentNode.id; + else if (focusableElems[newIndex] == userList) + outlinedElem = "user-list-box" + else + outlinedElem = "browser-box"; + + // Do magic, and make sure it gets undone at the right time: + if (("focusedElemTimeout" in client) && client.focusedElemTimeout) + clearTimeout(client.focusedElemTimeout); + outlineFocusedElem(outlinedElem); + client.focusedElemTimeout = setTimeout(outlineFocusedElem, 1000, ""); +} + +function outlineFocusedElem(id) +{ + var outlinedElements = ["user-list-box", "browser-box", "multiline-hug-box", + "singleline-hug-box"]; + for (var i = 0; i < outlinedElements.length; i++) + { + var elem = document.getElementById(outlinedElements[i]); + if (outlinedElements[i] == id) + elem.setAttribute("focusobvious", "true"); + else + elem.removeAttribute("focusobvious"); + } + client.focusedElemTimeout = 0; +} + +/* valid values for |what| are "superfluous", "activity", and "attention". + * final value for state is dependant on priority of the current state, and the + * new state. the priority is: normal < superfluous < activity < attention. + */ +function setTabState(source, what, callback) +{ + if (typeof source != "object") + { + if (!ASSERT(source in client.viewsArray, + "INVALID SOURCE passed to setTabState")) + return; + source = client.viewsArray[source].source; + } + + callback = callback || false; + + var tb = source.dispatch("create-tab-for-view", { view: source }); + var vk = Number(tb.getAttribute("viewKey")); + + var current = ("currentObject" in client && client.currentObject == source); + + /* We want to play sounds if they're from a non-current view, or we don't + * have focus at all. Also make sure stalk matches always play sounds. + * Also make sure we don't play on the 2nd half of the flash (Callback). + */ + if (!callback && (!window.isFocused || !current || (what == "attention"))) + { + if (what == "attention") + playEventSounds(source.TYPE, "stalk", source); + else if (what == "activity") + playEventSounds(source.TYPE, "chat", source); + else if (what == "superfluous") + playEventSounds(source.TYPE, "event", source); + } + + // Only change the tab's colour if it's not the active view. + if (!current) + { + var state = tb.getAttribute("state"); + if (state == what) + { + /* if the tab state has an equal priority to what we are setting + * then blink it */ + if (client.prefs["activityFlashDelay"] > 0) + { + tb.setAttribute("state", "normal"); + setTimeout(setTabState, client.prefs["activityFlashDelay"], + vk, what, true); + } + } + else + { + if (state == "normal" || state == "superfluous" || + (state == "activity" && what == "attention")) + { + /* if the tab state has a lower priority than what we are + * setting, change it to the new state */ + tb.setAttribute("state", what); + /* we only change the activity list if priority has increased */ + if (what == "attention") + client.activityList[vk] = "!"; + else if (what == "activity") + client.activityList[vk] = "+"; + else + { + /* this is functionally equivalent to "+" for now */ + client.activityList[vk] = "-"; + } + updateTitle(); + } + else + { + /* the current state of the tab has a higher priority than the + * new state. + * blink the new lower state quickly, then back to the old */ + if (client.prefs["activityFlashDelay"] > 0) + { + tb.setAttribute("state", what); + setTimeout(setTabState, + client.prefs["activityFlashDelay"], vk, + state, true); + } + } + } + } +} + +function notifyAttention (source) +{ + if (typeof source != "object") + source = client.viewsArray[source].source; + + if (client.currentObject != source) + { + var tb = dispatch("create-tab-for-view", { view: source }); + var vk = Number(tb.getAttribute("viewKey")); + + tb.setAttribute ("state", "attention"); + client.activityList[vk] = "!"; + updateTitle(); + } + + if (client.prefs["notify.aggressive"]) + window.getAttention(); + +} + +function setDebugMode(mode) +{ + if (mode.indexOf("t") != -1) + client.debugHook.enabled = true; + else + client.debugHook.enabled = false; + + if (mode.indexOf("c") != -1) + client.dbgContexts = true; + else + delete client.dbgContexts; + + if (mode.indexOf("d") != -1) + client.dbgDispatch = true; + else + delete client.dbgDispatch; +} + +function setListMode(mode) +{ + var elem = document.getElementById("user-list"); + if (mode) + elem.setAttribute("mode", mode); + else + elem.removeAttribute("mode"); + if (elem && elem.view && elem.treeBoxObject) + { + elem.treeBoxObject.clearStyleAndImageCaches(); + elem.treeBoxObject.invalidate(); + } +} + +function updateUserList() +{ + var node, chan; + + node = document.getElementById("user-list"); + if (!node.view) + return; + + if (("currentObject" in client) && client.currentObject && + client.currentObject.TYPE == "IRCChannel") + { + reSortUserlist(client.currentObject); + } +} + +function reSortUserlist(channel) +{ + if (!channel || !channel.userList) + return; + channel.userList.childData.reSort(); +} + +function getNicknameForUserlistRow(index) +{ + // This wouldn't be so hard if APIs didn't change so much... see bug 221619 + var userlist = document.getElementById("user-list"); + if (userlist.columns) + var col = userlist.columns.getNamedColumn("usercol"); + else + col = "usercol"; + return userlist.view.getCellText(index, col); +} + +function getFrameForDOMWindow(window) +{ + var frame; + for (var i = 0; i < client.deck.childNodes.length; i++) + { + frame = client.deck.childNodes[i]; + if (frame.contentWindow == window) + return frame; + } + return undefined; +} + +function replaceColorCodes(msg) +{ + // Find things that look like URLs and escape the color code inside of those + // to prevent munging the URLs resulting in broken links. Leave codes at + // the start of the URL alone. + msg = msg.replace(new RegExp(client.linkRE.source, "g"), function(url, _foo, scheme) { + if (scheme && !client.checkURLScheme(scheme)) + return url; + return url.replace(/%[BC][0-9A-Fa-f]/g, function(hex, index) { + // as JS does not support lookbehind and we don't want to consume the + // preceding character, we test for an existing %% manually + var needPercent = ("%" == url.substr(index - 1, 1)) || (index == 0); + return (needPercent ? "" : "%") + hex; + }); + }); + + // mIRC codes: underline, bold, Original (reset), colors, reverse colors. + msg = msg.replace(/(^|[^%])%U/g, "$1\x1f"); + msg = msg.replace(/(^|[^%])%B/g, "$1\x02"); + msg = msg.replace(/(^|[^%])%O/g, "$1\x0f"); + msg = msg.replace(/(^|[^%])%C/g, "$1\x03"); + msg = msg.replace(/(^|[^%])%R/g, "$1\x16"); + // %%[UBOCR] --> %[UBOCR]. + msg = msg.replace(/%(%[UBOCR])/g, "$1"); + + return msg; +} + +function decodeColorCodes(msg) +{ + // %[UBOCR] --> %%[UBOCR]. + msg = msg.replace(/(%[UBOCR])/g, "%$1"); + // Put %-codes back in place of special character codes. + msg = msg.replace(/\x1f/g, "%U"); + msg = msg.replace(/\x02/g, "%B"); + msg = msg.replace(/\x0f/g, "%O"); + msg = msg.replace(/\x03/g, "%C"); + msg = msg.replace(/\x16/g, "%R"); + + return msg; +} + +function removeColorCodes(msg) +{ + msg = msg.replace(/[\x1f\x02\x0f\x16]/g, ""); + // We need this to be global: + msg = msg.replace(new RegExp(client.colorRE.source, "g"), ""); + return msg; +} + +client.progressListener = new Object(); + +client.progressListener.QueryInterface = +function qi(iid) +{ + return this; +} + +client.progressListener.onStateChange = +function client_statechange (webProgress, request, stateFlags, status) +{ + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + const START = nsIWebProgressListener.STATE_START; + const STOP = nsIWebProgressListener.STATE_STOP; + const IS_NETWORK = nsIWebProgressListener.STATE_IS_NETWORK; + const IS_DOCUMENT = nsIWebProgressListener.STATE_IS_DOCUMENT; + const IS_REQUEST = nsIWebProgressListener.STATE_IS_REQUEST; + + var frame; + //dd("progressListener.onStateChange(" + stateFlags.toString(16) + ")"); + + // We only care about the initial start of loading, not the loading of + // and page sub-components (IS_DOCUMENT, etc.). + if ((stateFlags & START) && (stateFlags & IS_NETWORK) && + (stateFlags & IS_DOCUMENT)) + { + frame = getFrameForDOMWindow(webProgress.DOMWindow); + if (!frame) + { + dd("can't find frame for window (start)"); + try + { + webProgress.removeProgressListener(this); + } + catch(ex) + { + dd("Exception removing progress listener (start): " + ex); + } + } + } + // We only want to know when the *network* stops, not the page's + // individual components (STATE_IS_REQUEST/STATE_IS_DOCUMENT/somesuch). + else if ((stateFlags & STOP) && (stateFlags & IS_NETWORK)) + { + frame = getFrameForDOMWindow(webProgress.DOMWindow); + if (!frame) + { + dd("can't find frame for window (stop)"); + try + { + webProgress.removeProgressListener(this); + } + catch(ex) + { + dd("Exception removing progress listener (stop): " + ex); + } + } + else + { + var cwin = getContentWindow(frame); + if (cwin && "initOutputWindow" in cwin) + { + if (!("_called_initOutputWindow" in cwin)) + { + cwin._called_initOutputWindow = true; + cwin.initOutputWindow(client, frame.source, onMessageViewClick); + cwin.changeCSS(frame.source.getFontCSS("data"), "cz-fonts"); + scrollDown(frame, true); + //dd("initOutputWindow(" + frame.source.getURL() + ")"); + } + } + // XXX: For about:blank it won't find initOutputWindow. Cope. + else if (!cwin || !cwin.location || + (cwin.location.href != "about:blank")) + { + // This should totally never ever happen. It will if we get in a + // fight with xpcnativewrappers, though. Oops: + dd("Couldn't find a content window or its initOutputWindow " + + "function. This is BAD!"); + } + } + } + // Requests stopping are either the page, or its components loading. We're + // interested in its components. + else if ((stateFlags & STOP) && (stateFlags & IS_REQUEST)) + { + frame = getFrameForDOMWindow(webProgress.DOMWindow); + if (frame) + { + var cwin = getContentWindow(frame); + if (cwin && ("_called_initOutputWindow" in cwin)) + { + scrollDown(frame, false); + //dd("scrollDown(" + frame.source.getURL() + ")"); + } + } + + } +} + +client.progressListener.onProgressChange = +function client_progresschange (webProgress, request, currentSelf, totalSelf, + currentMax, selfMax) +{ +} + +client.progressListener.onLocationChange = +function client_locationchange (webProgress, request, uri) +{ +} + +client.progressListener.onStatusChange = +function client_statuschange (webProgress, request, status, message) +{ +} + +client.progressListener.onSecurityChange = +function client_securitychange (webProgress, request, state) +{ +} + +client.installPlugin = +function cli_installPlugin(name, source) +{ + function checkPluginInstalled(name, path) + { + var installed = path.exists(); + installed |= (name in client.plugins); + + if (installed) + { + display(MSG_INSTALL_PLUGIN_ERR_ALREADY_INST, MT_ERROR); + throw CZ_PI_ABORT; + } + }; + function getZipEntry(reader, entryEnum) + { + // nsIZipReader was rewritten... + var itemName = entryEnum.getNext(); + if (typeof itemName != "string") + name = itemName.QueryInterface(nsIZipEntry).name; + return itemName; + }; + function checkZipMore(items) + { + return (("hasMoreElements" in items) && items.hasMoreElements()) || + (("hasMore" in items) && items.hasMore()); + }; + + const DIRECTORY_TYPE = Components.interfaces.nsIFile.DIRECTORY_TYPE; + const CZ_PI_ABORT = "CZ_PI_ABORT"; + const nsIZipEntry = Components.interfaces.nsIZipEntry; + + var dest; + // Find a suitable location if there was none specified. + var destList = client.prefs["initialScripts"]; + if ((destList.length == 0) || + ((destList.length == 1) && /^\s*$/.test(destList[0]))) + { + // Reset to default because it is empty. + try + { + client.prefManager.clearPref("initialScripts"); + } + catch(ex) {/* If this really fails, we're mostly screwed anyway */} + destList = client.prefs["initialScripts"]; + } + + // URLs for initialScripts can be relative (the default is): + var profilePath = getURLSpecFromFile(client.prefs["profilePath"]); + profilePath = Services.io.newURI(profilePath); + for (var i = 0; i < destList.length; i++) + { + var destURL = Services.io.newURI(destList[i], null, profilePath); + var file = new nsLocalFile(getFileFromURLSpec(destURL.spec).path); + if (file.exists() && file.isDirectory()) { + // A directory that exists! We'll take it! + dest = file.clone(); + break; + } + } + if (!dest) { + display(MSG_INSTALL_PLUGIN_ERR_INSTALL_TO, MT_ERROR); + return; + } + + try { + if (typeof source == "string") + source = getFileFromURLSpec(source); + } + catch (ex) + { + display(getMsg(MSG_INSTALL_PLUGIN_ERR_CHECK_SD, ex), MT_ERROR); + return; + } + + display(getMsg(MSG_INSTALL_PLUGIN_INSTALLING, [source.path, dest.path]), + MT_INFO); + + var ary; + if (source.path.match(/\.(jar|zip)$/i)) + { + try + { + var zipReader = newObject("@mozilla.org/libjar/zip-reader;1", + "nsIZipReader"); + zipReader.open(source); + + // This is set to the base path found on ALL items in the zip file. + // when we extract, this WILL BE REMOVED from all paths. + var zipPathBase = ""; + // This always points to init.js, even if we're messing with paths. + var initPath = "init.js"; + + // Look for init.js within a directory... + var items = zipReader.findEntries("*/init.js"); + while (checkZipMore(items)) + { + var itemName = getZipEntry(zipReader, items); + // Do we already have one? + if (zipPathBase) + { + display(MSG_INSTALL_PLUGIN_ERR_MANY_INITJS, MT_WARN); + throw CZ_PI_ABORT; + } + zipPathBase = itemName.match(/^(.*\/)init.js$/)[1]; + initPath = itemName; + } + + if (zipPathBase) + { + // We have a base for init.js, assert that all files are inside + // it. If not, we drop the path and install exactly as-is + // instead (which will probably cause it to not work because the + // init.js isn't in the right place). + items = zipReader.findEntries("*"); + while (checkZipMore(items)) + { + itemName = getZipEntry(zipReader, items); + if (itemName.substr(0, zipPathBase.length) != zipPathBase) + { + display(MSG_INSTALL_PLUGIN_ERR_MIXED_BASE, MT_WARN); + zipPathBase = ""; + break; + } + } + } + + // Test init.js for a plugin ID. + var initJSFile = getTempFile(client.prefs["profilePath"], + "install-plugin.temp"); + zipReader.extract(initPath, initJSFile); + initJSFile.permissions = 438; // 0666 + var initJSFileH = fopen(initJSFile, "<"); + var initJSData = initJSFileH.read(); + initJSFileH.close(); + initJSFile.remove(false); + + //XXXgijs: this is fragile. Anyone got a better idea? + ary = initJSData.match(/plugin\.id\s*=\s*(['"])(.*?)(\1)/); + if (ary && (name != ary[2])) + { + display(getMsg(MSG_INSTALL_PLUGIN_WARN_NAME, [name, ary[2]]), + MT_WARN); + name = ary[2]; + } + + dest.append(name); + checkPluginInstalled(name, dest); + + dest.create(DIRECTORY_TYPE, 0o700); + + // Actually extract files... + var destInit; + items = zipReader.findEntries("*"); + while (checkZipMore(items)) + { + itemName = getZipEntry(zipReader, items); + if (!itemName.match(/\/$/)) + { + var dirs = itemName; + // Remove common path if there is one. + if (zipPathBase) + dirs = dirs.substring(zipPathBase.length); + dirs = dirs.split("/"); + + // Construct the full path for the extracted file... + var zipFile = dest.clone(); + for (var i = 0; i < dirs.length - 1; i++) + { + zipFile.append(dirs[i]); + if (!zipFile.exists()) + zipFile.create(DIRECTORY_TYPE, 0o700); + } + zipFile.append(dirs[dirs.length - 1]); + + if (zipFile.leafName == "init.js") + destInit = zipFile; + + zipReader.extract(itemName, zipFile); + zipFile.permissions = 438; // 0666 + } + } + + var rv = dispatch("load ", {url: getURLSpecFromFile(destInit)}); + if (rv) + { + display(getMsg(MSG_INSTALL_PLUGIN_DONE, name)); + } + else + { + display(MSG_INSTALL_PLUGIN_ERR_REMOVING, MT_ERROR); + dest.remove(true); + } + } + catch (ex) + { + if (ex != CZ_PI_ABORT) + display(getMsg(MSG_INSTALL_PLUGIN_ERR_EXTRACT, ex), MT_ERROR); + zipReader.close(); + return; + } + try + { + zipReader.close(); + } + catch (ex) + { + display(getMsg(MSG_INSTALL_PLUGIN_ERR_EXTRACT, ex), MT_ERROR); + } + } + else if (source.path.match(/\.(js)$/i)) + { + try + { + // Test init.js for a plugin ID. + var initJSFileH = fopen(source, "<"); + var initJSData = initJSFileH.read(); + initJSFileH.close(); + + ary = initJSData.match(/plugin\.id\s*=\s*(['"])(.*?)(\1)/); + if (ary && (name != ary[2])) + { + display(getMsg(MSG_INSTALL_PLUGIN_WARN_NAME, [name, ary[2]]), + MT_WARN); + name = ary[2]; + } + + dest.append(name); + checkPluginInstalled(name, dest); + + dest.create(DIRECTORY_TYPE, 0o700); + + dest.append("init.js"); + + var destFile = fopen(dest, ">"); + destFile.write(initJSData); + destFile.close(); + + var rv = dispatch("load", {url: getURLSpecFromFile(dest)}); + if (rv) + { + display(getMsg(MSG_INSTALL_PLUGIN_DONE, name)); + } + else + { + display(MSG_INSTALL_PLUGIN_ERR_REMOVING, MT_ERROR); + // We've appended "init.js" before, so go back up one level: + dest.parent.remove(true); + } + } + catch(ex) + { + if (ex != CZ_PI_ABORT) + { + display(getMsg(MSG_INSTALL_PLUGIN_ERR_INSTALLING, ex), + MT_ERROR); + } + } + } + else + { + display(MSG_INSTALL_PLUGIN_ERR_FORMAT, MT_ERROR); + } +} + +client.uninstallPlugin = +function cli_uninstallPlugin(plugin) +{ + if (!disablePlugin(plugin, true)) + return; + delete client.plugins[plugin.id]; + let file = getFileFromURLSpec(plugin.cwd); + if (file.exists() && file.isDirectory()) + { + // Delete the directory and contents. + file.remove(true); + } + display(getMsg(MSG_PLUGIN_UNINSTALLED, plugin.id)); +} + +function syncOutputFrame(obj, nesting) +{ + const nsIWebProgress = Components.interfaces.nsIWebProgress; + const WINDOW = nsIWebProgress.NOTIFY_STATE_WINDOW; + const NETWORK = nsIWebProgress.NOTIFY_STATE_NETWORK; + const ALL = nsIWebProgress.NOTIFY_ALL; + + var iframe = obj.frame; + + function tryAgain(nLevel) + { + syncOutputFrame(obj, nLevel); + }; + + if (typeof nesting != "number") + nesting = 0; + + if (nesting > 10) + { + dd("Aborting syncOutputFrame, taken too many tries."); + return; + } + + /* We leave the progress listener attached so try to remove it first, + * should we be called on an already-set-up view. + */ + try + { + iframe.removeProgressListener(client.progressListener, ALL); + } + catch (ex) + { + } + + try + { + if (getContentDocument(iframe) && ("webProgress" in iframe)) + { + var url = obj.prefs["outputWindowURL"]; + iframe.addProgressListener(client.progressListener, ALL); + iframe.loadURI(url); + } + else + { + setTimeout(tryAgain, 500, nesting + 1); + } + } + catch (ex) + { + dd("caught exception showing session view, will try again later."); + dd(dumpObjectTree(ex) + "\n"); + setTimeout(tryAgain, 500, nesting + 1); + } +} + +function createMessages(source) +{ + playEventSounds(source.TYPE, "start", source); + + source.messages = document.createElementNS(XHTML_NS, "html:table"); + source.messages.setAttribute("class", "msg-table"); + source.messages.setAttribute("view-type", source.TYPE); + source.messages.setAttribute("role", "log"); + source.messages.setAttribute("aria-live", "polite"); + + var tbody = document.createElementNS(XHTML_NS, "html:tbody"); + source.messages.appendChild (tbody); + source.messageCount = 0; +} + +/* Gets the <tab> element associated with a view object. + * If |create| is present, and true, tab is created if not found. + */ +function getTabForObject(source, create) +{ + var name; + + if (!ASSERT(source, "UNDEFINED passed to getTabForObject")) + return null; + + if (!ASSERT("viewName" in source, "INVALID OBJECT in getTabForObject")) + return null; + + name = source.viewName; + + var tb, id = "tb[" + name + "]"; + var matches = 1; + + for (var i in client.viewsArray) + { + if (client.viewsArray[i].source == source) + { + tb = client.viewsArray[i].tb; + break; + } + else + if (client.viewsArray[i].tb.getAttribute("id") == id) + id = "tb[" + name + "<" + (++matches) + ">]"; + } + + /* If we found a <tab>, are allowed to create it, and have a pending view + * context, then we're expected to move the existing tab according to said + * context. We do that by removing the tab here, and below the creation + * code (which is not run) we readd it in the correct location. + */ + if (tb && create && (typeof client.pendingViewContext == "object")) + { + /* If we're supposed to insert before ourselves, or the next <tab>, + * then bail out (since it's a no-op). + */ + var tabBefore = client.pendingViewContext.tabInsertBefore; + if (tabBefore) + { + var tbAfter = tb.nextSibling; + while (tbAfter && tbAfter.collapsed && tbAfter.hidden) + tbAfter = tbAfter.nextSibling; + if ((tabBefore == tb) || (tabBefore == tbAfter)) + return tb; + } + + var viewKey = Number(tb.getAttribute("viewKey")); + arrayRemoveAt(client.viewsArray, viewKey); + for (i = viewKey; i < client.viewsArray.length; i++) + client.viewsArray[i].tb.setAttribute("viewKey", i); + client.tabs.removeChild(tb); + } + else if (tb || (!tb && !create)) + { + /* Either: we have a tab and don't want it moved, or didn't find one + * and don't wish to create one. + */ + return tb; + } + + // Didn't found a <tab>, but we're allowed to create one. + if (!tb && create) + { + if (!("messages" in source) || source.messages == null) + createMessages(source); + + tb = document.createElement("tab"); + tb.setAttribute("ondragstart", "tabsDNDObserver.onDragStart(event);"); + tb.setAttribute("href", source.getURL()); + tb.setAttribute("name", source.unicodeName); + tb.setAttribute("onclick", "onTabClick(event, this.id);"); + // This wouldn't be here if there was a supported CSS property for it. + tb.setAttribute("crop", "center"); + tb.setAttribute("context", "context:tab"); + tb.setAttribute("class", "tab-bottom view-button"); + tb.setAttribute("id", id); + tb.setAttribute("state", "normal"); + name = source.prefs["tabLabel"] || name; + tb.setAttribute("label", name + (matches > 1 ? "<" + matches + ">" : "")); + tb.setAttribute("tooltiptext", source.viewName); + tb.view = source; + + var browser = document.createElement("browser"); + browser.setAttribute("class", "output-container"); + browser.setAttribute("type", "content"); + browser.setAttribute("flex", "1"); + browser.setAttribute("tooltip", "html-tooltip-node"); + browser.setAttribute("onclick", + "return onMessageViewClick(event)"); + browser.setAttribute("onmousedown", + "return onMessageViewMouseDown(event)"); + browser.setAttribute("oncontextmenu", + "return onMessageViewContextMenu(event)"); + browser.setAttribute("ondragover", + "contentDNDObserver.onDragOver(event);"); + browser.setAttribute("ondrop", "contentDNDObserver.onDrop(event);"); + browser.source = source; + source.frame = browser; + ASSERT(client.deck, "no deck?"); + client.deck.appendChild(browser); + syncOutputFrame(source); + + if (!("userList" in source) && (source.TYPE == "IRCChannel")) + { + source.userListShare = new Object(); + source.userList = new XULTreeView(source.userListShare); + source.userList.getRowProperties = ul_getrowprops; + source.userList.getCellProperties = ul_getcellprops; + source.userList.childData.setSortDirection(1); + } + } + + var beforeTab = null; + if (typeof client.pendingViewContext == "object") + { + var c = client.pendingViewContext; + /* If we have a <tab> to insert before, and it is still in the tabs, + * move the newly-created <tab> into the right place. + */ + if (c.tabInsertBefore && (c.tabInsertBefore.parentNode == client.tabs)) + beforeTab = c.tabInsertBefore; + } + + if (beforeTab) + { + var viewKey = beforeTab.getAttribute("viewKey"); + arrayInsertAt(client.viewsArray, viewKey, {source: source, tb: tb}); + for (i = viewKey; i < client.viewsArray.length; i++) + client.viewsArray[i].tb.setAttribute("viewKey", i); + client.tabs.insertBefore(tb, beforeTab); + } + else + { + client.viewsArray.push({source: source, tb: tb}); + tb.setAttribute("viewKey", client.viewsArray.length - 1); + client.tabs.appendChild(tb); + } + + updateTabAttributes(); + + return tb; +} + +function updateTabAttributes() +{ + /* XXX: Workaround for Gecko bugs 272646 and 261826. Note that this breaks + * the location of the spacers before and after the tabs but, due to our + * own <spacer>, their flex was not being utilised anyway. + */ + var tabOrdinal = 0; + for (var tab = client.tabs.firstChild; tab; tab = tab.nextSibling) + tab.ordinal = tabOrdinal++; + + /* XXX: Workaround for tabbox.xml not coping with updating attributes when + * tabs are moved. We correct the "first-tab", "last-tab", "beforeselected" + * and "afterselected" attributes. + * + * "last-tab" and "beforeselected" are updated on each valid (non-collapsed + * and non-hidden) tab found, to avoid having to work backwards as well as + * forwards. "first-tab" and "afterselected" are just set the once each. + * |foundSelected| tracks where we are in relation to the selected tab. + */ + var tabAttrs = { + "first-tab": null, + "last-tab": null, + "beforeselected": null, + "afterselected": null + }; + var foundSelected = "before"; + for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling) + { + if (tab.collapsed || tab.hidden) + continue; + + if (!tabAttrs["first-tab"]) + tabAttrs["first-tab"] = tab; + tabAttrs["last-tab"] = tab; + + if ((foundSelected == "before") && tab.selected) + foundSelected = "on"; + else if (foundSelected == "on") + foundSelected = "after"; + + if (foundSelected == "before") + tabAttrs["beforeselected"] = tab; + if ((foundSelected == "after") && !tabAttrs["afterselected"]) + tabAttrs["afterselected"] = tab; + } + + // After picking a tab for each attribute, apply them to the tabs. + for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling) + { + for (var attr in tabAttrs) + { + if (tabAttrs[attr] == tab) + tab.setAttribute(attr, "true"); + else + tab.removeAttribute(attr); + } + } +} + +// Properties getter for user list tree view +function ul_getrowprops(index) +{ + if ((index < 0) || (index >= this.childData.childData.length)) + { + return ""; + } + + // See bug 432482 - work around Gecko deficiency. + if (!this.selection.isSelected(index)) + { + return "unselected"; + } + + return ""; +} + +// Properties getter for user list tree view +function ul_getcellprops(index, column) +{ + if ((index < 0) || (index >= this.childData.childData.length)) + { + return ""; + } + + var resultProps = []; + + // See bug 432482 - work around Gecko deficiency. + if (!this.selection.isSelected(index)) + resultProps.push("unselected"); + + var userObj = this.childData.childData[index]._userObj; + + resultProps.push("voice-" + userObj.isVoice); + resultProps.push("op-" + userObj.isOp); + resultProps.push("halfop-" + userObj.isHalfOp); + resultProps.push("admin-" + userObj.isAdmin); + resultProps.push("founder-" + userObj.isFounder); + resultProps.push("away-" + userObj.isAway); + + return resultProps.join(" "); +} + +var contentDNDObserver = { + onDragOver(aEvent) { + if (aEvent.target == aEvent.dataTransfer.mozSourceNode) + return; + + if (Services.droppedLinkHandler.canDropLink(aEvent, true)) + aEvent.preventDefault(); + }, + + onDrop(aEvent) { + aEvent.stopPropagation(); + + var url = Services.droppedLinkHandler.dropLink(aEvent, {}); + if (!url || url.search(client.linkRE) == -1) + return; + + if (url.search(/\.css$/i) != -1 && confirm(getMsg(MSG_TABDND_DROP, url))) + dispatch("motif", {"motif": url}); + else if (url.search(/^ircs?:\/\//i) != -1) + dispatch("goto-url", {"url": url}); + }, +} + +var tabsDNDObserver = { + onDragOver(aEvent) { + if (aEvent.target == aEvent.dataTransfer.mozSourceNode) + return; + + // If we're not accepting the drag, don't show the marker either. + if (!Services.droppedLinkHandler.canDropLink(aEvent, true)) { + client.tabDragBar.collapsed = true; + return; + } + + aEvent.preventDefault(); + + /* Locate the tab we're about to drop onto. We split tabs in half, dropping + * on the side closest to the mouse, or after the last tab if the mouse is + * somewhere beyond all the tabs. + */ + var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr"); + var newPosition = client.tabs.firstChild.boxObject.x; + for (var dropTab = client.tabs.firstChild; dropTab; + dropTab = dropTab.nextSibling) + { + if (dropTab.collapsed || dropTab.hidden) + continue; + var bo = dropTab.boxObject; + if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) || + (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2))) + { + break; + } + newPosition = bo.x + bo.width; + } + + // Reposition the drop marker and show it. In that order. + client.tabDragMarker.style.MozMarginStart = newPosition + "px"; + client.tabDragBar.collapsed = false; + }, + + onDragExit(aEvent) { + aEvent.stopPropagation(); + + /* We've either stopped being part of a drag operation, or the dragging is + * somewhere away from us. + */ + client.tabDragBar.collapsed = true; + }, + + onDrop(aEvent) { + aEvent.stopPropagation(); + + // Dragging has finished. + client.tabDragBar.collapsed = true; + + // See comment above |var tabsDropObserver|. + var url = Services.droppedLinkHandler.dropLink(aEvent, {}); + if (!url || !(url.match(/^ircs?:/) || url.match(/^x-irc-dcc-(chat|file):/))) + return; + + // Find the tab to insertBefore() the new one. + var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr"); + for (var dropTab = client.tabs.firstChild; dropTab; + dropTab = dropTab.nextSibling) + { + if (dropTab.collapsed || dropTab.hidden) + continue; + var bo = dropTab.boxObject; + if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) || + (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2))) + { + break; + } + } + + // Check if the URL is already in the views. + for (var i = 0; i < client.viewsArray.length; i++) + { + var view = client.viewsArray[i].source; + if (view.getURL() == url) + { + client.pendingViewContext = { tabInsertBefore: dropTab }; + dispatch("create-tab-for-view", { view: view }); + delete client.pendingViewContext; + return; + } + } + + // URL not found in tabs, so force it into life - this may connect/rejoin. + if (url.substring(0, 3) == "irc") + gotoIRCURL(url, { tabInsertBefore: dropTab }); + }, + + onDragStart(aEvent) { + var tb = aEvent.currentTarget; + var href = tb.getAttribute("href"); + var name = tb.getAttribute("name"); + + /* x-moz-url has the format "<url>\n<name>", goodie */ + aEvent.dataTransfer.setData("text/x-moz-url", href + "\n" + name); + aEvent.dataTransfer.setData("text/unicode", href); + aEvent.dataTransfer.setData("text/plain", href); + aEvent.dataTransfer.setData("text/html", "<a href='" + href + "'>" + + name + "</a>"); + }, +} + +var userlistDNDObserver = { + onDragStart(aEvent) { + var col = {}; + var row = {}; + var cell = {}; + var tree = document.getElementById('user-list'); + tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY, + row, col, cell); + // Check whether we're actually on a normal row and cell + if (!cell.value || (row.value == -1)) + return; + + var nickname = getNicknameForUserlistRow(row.value); + aEvent.dataTransfer.setData("text/unicode", nickname); + aEvent.dataTransfer.setData("text/plain", nickname); + }, +} + +function deleteTab(tb) +{ + if (!ASSERT(tb.hasAttribute("viewKey"), + "INVALID OBJECT passed to deleteTab (" + tb + ")")) + { + return null; + } + + var key = Number(tb.getAttribute("viewKey")); + + // Re-index higher tabs. + for (var i = key + 1; i < client.viewsArray.length; i++) + client.viewsArray[i].tb.setAttribute("viewKey", i - 1); + arrayRemoveAt(client.viewsArray, key); + client.tabs.removeChild(tb); + setTimeout(updateTabAttributes, 0); + + return key; +} + +function deleteFrame(view) +{ + const nsIWebProgress = Components.interfaces.nsIWebProgress; + const ALL = nsIWebProgress.NOTIFY_ALL; + + // We leave the progress listener attached so try to remove it. + try + { + view.frame.removeProgressListener(client.progressListener, ALL); + } + catch (ex) + { + dd(formatException(ex)); + } + + client.deck.removeChild(view.frame); + delete view.frame; +} + +function filterOutput(msg, msgtype, dest) +{ + if ("outputFilters" in client) + { + for (var f in client.outputFilters) + { + if (client.outputFilters[f].enabled) + msg = client.outputFilters[f].func(msg, msgtype, dest); + } + } + + return msg; +} + +function updateTimestamps(view) +{ + if (!("messages" in view)) + return; + + view._timestampLast = ""; + var node = view.messages.firstChild.firstChild; + var nested; + while (node) + { + if(node.className == "msg-nested-tr") + { + nested = node.firstChild.firstChild.firstChild.firstChild; + while (nested) + { + updateTimestampFor(view, nested); + nested = nested.nextSibling; + } + } + else + { + updateTimestampFor(view, node); + } + node = node.nextSibling; + } +} + +function updateTimestampFor(view, displayRow, forceOldStamp) +{ + var time = new Date(1 * displayRow.getAttribute("timestamp")); + var tsCell = displayRow.firstChild; + if (!tsCell) + return; + + var fmt; + if (view.prefs["timestamps"]) + fmt = strftime(view.prefs["timestamps.display"], time); + + while (tsCell.lastChild) + tsCell.removeChild(tsCell.lastChild); + + var needStamp = fmt && (forceOldStamp || !view.prefs["collapseMsgs"] || + (fmt != view._timestampLast)); + if (needStamp) + tsCell.appendChild(document.createTextNode(fmt)); + if (!forceOldStamp) + view._timestampLast = fmt; +} + +client.updateMenus = +function c_updatemenus(menus) +{ + // Don't bother if the menus aren't even created yet. + if (!client.initialized) + return null; + + return this.menuManager.updateMenus(document, menus); +} + +client.checkURLScheme = +function c_checkURLScheme(url) +{ + if (!("schemes" in client)) + { + var pfx = "@mozilla.org/network/protocol;1?name="; + var len = pfx.length; + + client.schemes = new Object(); + for (var c in Components.classes) + { + if (c.indexOf(pfx) == 0) + client.schemes[c.substr(len)] = true; + } + } + return (url.toLowerCase() in client.schemes); +} + +client.adoptNode = +function cli_adoptnode(node, doc) +{ + try + { + doc.adoptNode(node); + } + catch(ex) + { + dd(formatException(ex)); + var err = ex.name; + // TypeError from before adoptNode was added; NOT_IMPL after. + if ((err == "TypeError") || (err == "NS_ERROR_NOT_IMPLEMENTED")) + client.adoptNode = cli_adoptnode_noop; + } + return node; +} + +function cli_adoptnode_noop(node, doc) +{ + return node; +} + +client.addNetwork = +function cli_addnet(name, serverList, temporary) +{ + let net = new CIRCNetwork(name, serverList, client.eventPump, temporary); + client.networks[net.collectionKey] = net; +} + +client.getNetwork = +function cli_getnet(name) +{ + return client.networks[":" + name] || null; +} + +client.removeNetwork = +function cli_removenet(name) +{ + let net = client.getNetwork(name); + + // Allow network a chance to clean up any mess. + if (typeof net.destroy == "function") + net.destroy(); + + delete client.networks[net.collectionKey]; +} + +client.connectToNetwork = +function cli_connect(networkOrName, requireSecurity) +{ + var network; + var name; + + + if (isinstance(networkOrName, CIRCNetwork)) + { + network = networkOrName; + } + else + { + name = networkOrName; + network = client.getNetwork(name); + + if (!network) + { + display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name), MT_ERROR); + return null; + } + } + name = network.unicodeName; + + dispatch("create-tab-for-view", { view: network }); + dispatch("set-current-view", { view: network }); + + if (network.isConnected()) + { + network.display(getMsg(MSG_ALREADY_CONNECTED, name)); + return network; + } + + if (network.state != NET_OFFLINE) + return network; + + if (network.prefs["nickname"] == DEFAULT_NICK) + network.prefs["nickname"] = prompt(MSG_ENTER_NICK, DEFAULT_NICK); + + if (!("connecting" in network)) + network.display(getMsg(MSG_NETWORK_CONNECTING, name)); + + network.connect(requireSecurity); + + network.updateHeader(); + client.updateHeader(); + updateTitle(); + + return network; +} + + +client.getURL = +function cli_geturl () +{ + return "irc://"; +} + +client.load = +function cli_load(url, scope) +{ + if (!("_loader" in client)) + { + const LOADER_CTRID = "@mozilla.org/moz/jssubscript-loader;1"; + const mozIJSSubScriptLoader = + Components.interfaces.mozIJSSubScriptLoader; + + var cls; + if ((cls = Components.classes[LOADER_CTRID])) + client._loader = cls.getService(mozIJSSubScriptLoader); + } + + if (client._loader.loadSubScriptWithOptions) + { + var opts = {target: scope, ignoreCache: true}; + return client._loader.loadSubScriptWithOptions(url, opts); + } + + return client._loader.loadSubScript(url, scope); +} + +client.sayToCurrentTarget = +function cli_say(msg, isInteractive) +{ + if ("say" in client.currentObject) + { + client.currentObject.dispatch("say", {message: msg}, isInteractive); + return; + } + + switch (client.currentObject.TYPE) + { + case "IRCClient": + dispatch("eval", {expression: msg}, isInteractive); + break; + + default: + if (msg != "") + display(MSG_ERR_NO_DEFAULT, MT_ERROR); + break; + } +} + +CIRCNetwork.prototype.__defineGetter__("prefs", net_getprefs); +function net_getprefs() +{ + if (!("_prefs" in this)) + { + this._prefManager = getNetworkPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefs; +} + +CIRCNetwork.prototype.__defineGetter__("prefManager", net_getprefmgr); +function net_getprefmgr() +{ + if (!("_prefManager" in this)) + { + this._prefManager = getNetworkPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefManager; +} + +CIRCServer.prototype.__defineGetter__("prefs", srv_getprefs); +function srv_getprefs() +{ + return this.parent.prefs; +} + +CIRCServer.prototype.__defineGetter__("prefManager", srv_getprefmgr); +function srv_getprefmgr() +{ + return this.parent.prefManager; +} + +CIRCChannel.prototype.__defineGetter__("prefs", chan_getprefs); +function chan_getprefs() +{ + if (!("_prefs" in this)) + { + this._prefManager = getChannelPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefs; +} + +CIRCChannel.prototype.__defineGetter__("prefManager", chan_getprefmgr); +function chan_getprefmgr() +{ + if (!("_prefManager" in this)) + { + this._prefManager = getChannelPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefManager; +} + +CIRCUser.prototype.__defineGetter__("prefs", usr_getprefs); +function usr_getprefs() +{ + if (!("_prefs" in this)) + { + this._prefManager = getUserPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefs; +} + +CIRCUser.prototype.__defineGetter__("prefManager", usr_getprefmgr); +function usr_getprefmgr() +{ + if (!("_prefManager" in this)) + { + this._prefManager = getUserPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefManager; +} + +CIRCDCCUser.prototype.__defineGetter__("prefs", dccusr_getprefs); +function dccusr_getprefs() +{ + if (!("_prefs" in this)) + { + this._prefManager = getDCCUserPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefs; +} + +CIRCDCCUser.prototype.__defineGetter__("prefManager", dccusr_getprefmgr); +function dccusr_getprefmgr() +{ + if (!("_prefManager" in this)) + { + this._prefManager = getDCCUserPrefManager(this); + this._prefs = this._prefManager.prefs; + } + + return this._prefManager; +} + +CIRCDCCChat.prototype.__defineGetter__("prefs", dccchat_getprefs); +function dccchat_getprefs() +{ + return this.user.prefs; +} + +CIRCDCCChat.prototype.__defineGetter__("prefManager", dccchat_getprefmgr); +function dccchat_getprefmgr() +{ + return this.user.prefManager; +} + +CIRCDCCFileTransfer.prototype.__defineGetter__("prefs", dccfile_getprefs); +function dccfile_getprefs() +{ + return this.user.prefs; +} + +CIRCDCCFileTransfer.prototype.__defineGetter__("prefManager", dccfile_getprefmgr); +function dccfile_getprefmgr() +{ + return this.user.prefManager; +} + +/* Displays a network-centric message on the most appropriate view. + * + * When |client.SLOPPY_NETWORKS| is |true|, messages will be displayed on the + * *current* view instead of the network view, if the current view is part of + * the same network. + */ +CIRCNetwork.prototype.display = +function net_display(message, msgtype, sourceObj, destObj, tags) +{ + var o = getObjectDetails(client.currentObject); + if (client.SLOPPY_NETWORKS && client.currentObject != this && + o.network == this && o.server && o.server.isConnected) + { + client.currentObject.display(message, msgtype, sourceObj, destObj, + tags); + } + else + { + this.displayHere(message, msgtype, sourceObj, destObj, tags); + } +} + +/* Displays a channel-centric message on the most appropriate view. + * + * If the channel view already exists (visible or hidden), messages are added + * to it; otherwise, messages go to the *network* view. + */ +CIRCChannel.prototype.display = +function chan_display(message, msgtype, sourceObj, destObj, tags) +{ + if ("messages" in this) + this.displayHere(message, msgtype, sourceObj, destObj, tags); + else + this.parent.parent.displayHere(message, msgtype, sourceObj, destObj, + tags); +} + +/* Displays a user-centric message on the most appropriate view. + * + * If the user view already exists (visible or hidden), messages are added to + * it; otherwise, it goes to the *current* view if the current view is part of + * the same network, or the *network* view if not. + */ +CIRCUser.prototype.display = +function usr_display(message, msgtype, sourceObj, destObj, tags) +{ + if ("messages" in this) + { + this.displayHere(message, msgtype, sourceObj, destObj, tags); + } + else + { + var o = getObjectDetails(client.currentObject); + if (o.server && o.server.isConnected && + o.network == this.parent.parent && + client.currentObject.TYPE != "IRCUser") + client.currentObject.display(message, msgtype, sourceObj, destObj, + tags); + else + this.parent.parent.displayHere(message, msgtype, sourceObj, + destObj, tags); + } +} + +/* Displays a DCC user/file transfer-centric message on the most appropriate view. + * + * If the DCC user/file transfer view already exists (visible or hidden), + * messages are added to it; otherwise, messages go to the *current* view. + */ +CIRCDCCChat.prototype.display = +CIRCDCCFileTransfer.prototype.display = +function dcc_display(message, msgtype, sourceObj, destObj) +{ + if ("messages" in this) + this.displayHere(message, msgtype, sourceObj, destObj); + else + client.currentObject.display(message, msgtype, sourceObj, destObj); +} + +function feedback(e, message, msgtype, sourceObj, destObj) +{ + if ("isInteractive" in e && e.isInteractive) + display(message, msgtype, sourceObj, destObj); +} + +CIRCChannel.prototype.feedback = +CIRCNetwork.prototype.feedback = +CIRCUser.prototype.feedback = +CIRCDCCChat.prototype.feedback = +CIRCDCCFileTransfer.prototype.feedback = +client.feedback = +function this_feedback(e, message, msgtype, sourceObj, destObj) +{ + if ("isInteractive" in e && e.isInteractive) + this.displayHere(message, msgtype, sourceObj, destObj); +} + +function display (message, msgtype, sourceObj, destObj, tags) +{ + client.currentObject.display (message, msgtype, sourceObj, destObj, tags); +} + +client.getFontCSS = +CIRCNetwork.prototype.getFontCSS = +CIRCChannel.prototype.getFontCSS = +CIRCUser.prototype.getFontCSS = +CIRCDCCChat.prototype.getFontCSS = +CIRCDCCFileTransfer.prototype.getFontCSS = +function this_getFontCSS(format) +{ + /* Wow, this is cool. We just put together a CSS-rule string based on the + * font preferences. *This* is what CSS is all about. :) + * We also provide a "data: URL" format, to simplify other code. + */ + var css; + var fs; + var fn; + + if (this.prefs["font.family"] != "default") + fn = "font-family: " + this.prefs["font.family"] + ";"; + else + fn = "font-family: inherit;"; + if (this.prefs["font.size"] != 0) + fs = "font-size: " + this.prefs["font.size"] + "pt;"; + else + fs = "font-size: medium;"; + + css = ".chatzilla-body { " + fs + fn + " }"; + + if (format == "data") + return "data:text/css," + encodeURIComponent(css); + return css; +} + +client.startMsgGroup = +CIRCNetwork.prototype.startMsgGroup = +CIRCChannel.prototype.startMsgGroup = +CIRCUser.prototype.startMsgGroup = +CIRCDCCChat.prototype.startMsgGroup = +CIRCDCCFileTransfer.prototype.startMsgGroup = +function __startMsgGroup(id, groupMsg, msgtype) +{ + // The given ID may not be unique, so append a timestamp to ensure it is. + var groupId = id + "-" + Date.now(); + + // Add the button to the end of the message. + var headerMsg = groupMsg + " " + getMsg(MSG_COLLAPSE_BUTTON, + [MSG_COLLAPSE_HIDE, + MSG_COLLAPSE_HIDETITLE, + groupId]); + + // Show the group header message. + client.munger.getRule(".inline-buttons").enabled = true; + this.displayHere(headerMsg, msgtype); + client.munger.getRule(".inline-buttons").enabled = false; + + // Add the group to a list of active message groups. + if (!this.msgGroups) + this.msgGroups = []; + this.msgGroups.push(groupId); + + // Return the actual ID in case the caller wants to use it later. + return groupId; +} + +function startMsgGroup(groupId, headerMsg, msgtype) +{ + client.currentObject.startMsgGroup(groupId, headerMsg, msgtype); +} + +client.endMsgGroup = +CIRCNetwork.prototype.endMsgGroup = +CIRCChannel.prototype.endMsgGroup = +CIRCUser.prototype.endMsgGroup = +CIRCDCCChat.prototype.endMsgGroup = +CIRCDCCFileTransfer.prototype.endMsgGroup = +function __endMsgGroup(groupId, message) +{ + if (!this.msgGroups) + return; + + // Remove the group from the list of active message groups. + this.msgGroups.pop(); + if (this.msgGroups.length == 0) + delete this.msgGroups; +} + +function endMsgGroup() +{ + client.currentObject.endMsgGroup(); +} + +client.display = +client.displayHere = +CIRCNetwork.prototype.displayHere = +CIRCChannel.prototype.displayHere = +CIRCUser.prototype.displayHere = +CIRCDCCChat.prototype.displayHere = +CIRCDCCFileTransfer.prototype.displayHere = +function __display(message, msgtype, sourceObj, destObj, tags) +{ + // We need a message type, assume "INFO". + if (!msgtype) + msgtype = MT_INFO; + + var msgprefix = ""; + if (msgtype.indexOf("/") != -1) + { + var ary = msgtype.match(/^(.*)\/(.*)$/); + msgtype = ary[1]; + msgprefix = ary[2]; + } + + var blockLevel = false; /* true if this row should be rendered at block + * level, (like, if it has a really long nickname + * that might disturb the rest of the layout) */ + var o = getObjectDetails(this); /* get the skinny on |this| */ + + // Get the 'me' object, so we can be sure to get the attributes right. + var me; + if ("me" in this) + me = this.me; + else if (o.server && "me" in o.server) + me = o.server.me; + + /* Allow for matching (but not identical) user objects here. This tends to + * happen with bouncers and proxies, when they send channel messages + * pretending to be from the user; the sourceObj is a CIRCChanUser + * instead of a CIRCUser so doesn't == 'me'. + */ + if (me) + { + if (sourceObj && (sourceObj.canonicalName == me.canonicalName)) + sourceObj = me; + if (destObj && (destObj.canonicalName == me.canonicalName)) + destObj = me; + } + + // Let callers get away with "ME!" and we have to substitute here. + if (sourceObj == "ME!") + sourceObj = me; + if (destObj == "ME!") + destObj = me; + + // Get the TYPE of the source object. + var fromType = (sourceObj && sourceObj.TYPE) ? sourceObj.TYPE : "unk"; + // Is the source a user? + var fromUser = (fromType.search(/IRC.*User/) != -1); + // Get some sort of "name" for the source. + var fromAttr = ""; + if (sourceObj) + { + if ("canonicalName" in sourceObj) + fromAttr = sourceObj.canonicalName; + else if ("name" in sourceObj) + fromAttr = sourceObj.name; + else + fromAttr = sourceObj.viewName; + } + + // Get the dest TYPE too... + var toType = (destObj) ? destObj.TYPE : "unk"; + // Is the dest a user? + var toUser = (toType.search(/IRC.*User/) != -1); + // Get a dest name too... + var toAttr = ""; + if (destObj) + { + if ("canonicalName" in destObj) + toAttr = destObj.canonicalName; + else if ("name" in destObj) + toAttr = destObj.name; + else + toAttr = destObj.viewName; + } + + // Is the message 'to' or 'from' somewhere other than this view + var toOther = ((sourceObj == me) && destObj && (destObj != this)); + var fromOther = (toUser && (destObj == me) && (sourceObj != this) && + // Need extra check for DCC users: + !((this.TYPE == "IRCDCCChat") && (this.user == sourceObj))); + + // Attach "ME!" if appropriate, so motifs can style differently. + if ((sourceObj == me) && !toOther) + fromAttr = fromAttr + " ME!"; + if (destObj && destObj == me) + toAttr = me.canonicalName + " ME!"; + + /* isImportant means to style the messages as important, and flash the + * window, getAttention means just flash the window. */ + var isImportant = false, getAttention = false, isSuperfluous = false; + var viewType = this.TYPE; + var code; + var time; + if (tags && ("time" in tags)) + time = new Date(tags.time); + else + time = new Date(); + + var timeStamp = strftime(this.prefs["timestamps.log"], time); + + // Statusbar text, and the line that gets saved to the log. + var statusString; + var logStringPfx = timeStamp + " "; + var logStrings = new Array(); + + if (fromUser) + { + statusString = getMsg(MSG_FMT_STATUS, + [timeStamp, + sourceObj.unicodeName + "!" + + sourceObj.name + "@" + sourceObj.host]); + } + else + { + var name; + if (sourceObj) + name = sourceObj.viewName; + else + name = this.viewName; + + statusString = getMsg(MSG_FMT_STATUS, + [timeStamp, name]); + } + + // The table row, and it's attributes. + var msgRow = document.createElementNS(XHTML_NS, "html:tr"); + msgRow.setAttribute("class", "msg"); + if (this.msgGroups) + msgRow.setAttribute("msg-groups", this.msgGroups.join(', ')); + msgRow.setAttribute("msg-type", msgtype); + msgRow.setAttribute("msg-prefix", msgprefix); + msgRow.setAttribute("msg-dest", toAttr); + msgRow.setAttribute("dest-type", toType); + msgRow.setAttribute("view-type", viewType); + msgRow.setAttribute("status-text", statusString); + msgRow.setAttribute("timestamp", Number(time)); + if (fromAttr) + { + if (fromUser) + { + msgRow.setAttribute("msg-user", fromAttr); + // Set some mode information for channel users + if (fromType == 'IRCChanUser') + msgRow.setAttribute("msg-user-mode", sourceObj.modes.join(" ")); + } + else + { + msgRow.setAttribute("msg-source", fromAttr); + } + } + if (toOther) + msgRow.setAttribute("to-other", toOther); + if (fromOther) + msgRow.setAttribute("from-other", fromOther); + + // Timestamp cell. + var msgRowTimestamp = document.createElementNS(XHTML_NS, "html:td"); + msgRowTimestamp.setAttribute("class", "msg-timestamp"); + + var canMergeData; + var msgRowSource, msgRowType, msgRowData; + if (fromUser && msgtype.match(/^(PRIVMSG|ACTION|NOTICE|WALLOPS)$/)) + { + var nick = sourceObj.unicodeName; + var decorSt = ""; + var decorEn = ""; + + // Set default decorations. + if (msgtype == "ACTION") + { + decorSt = "* "; + } + else + { + decorSt = "<"; + decorEn = ">"; + } + + var nickURL; + if ((sourceObj != me) && ("getURL" in sourceObj)) + nickURL = sourceObj.getURL(); + if (toOther && ("getURL" in destObj)) + nickURL = destObj.getURL(); + + if (sourceObj != me) + { + // Not from us... + if (destObj == me) + { + // ...but to us. Messages from someone else to us. + + getAttention = true; + this.defaultCompletion = "/msg " + nick + " "; + + // If this is a private message, and it's not in a query view, + // use *nick* instead of <nick>. + if ((msgtype != "ACTION") && (this.TYPE != "IRCUser")) + { + decorSt = "*"; + decorEn = "*"; + } + } + else + { + // ...or to us. Messages from someone else to channel or similar. + + if ((typeof message == "string") && me) + isImportant = msgIsImportant(message, nick, o.network); + else if (message.hasAttribute("isImportant") && me) + isImportant = true; + + if (isImportant) + { + this.defaultCompletion = nick + + client.prefs["nickCompleteStr"] + " "; + } + } + } + else + { + // Messages from us, to somewhere other than this view + if (toOther) + { + nick = destObj.unicodeName; + decorSt = ">"; + decorEn = "<"; + } + } + + // Log the nickname in the same format as we'll let the user copy. + // If the message has a prefix, show it after a "/". + if (msgprefix) + logStringPfx += decorSt + nick + "/" + msgprefix + decorEn + " "; + else + logStringPfx += decorSt + nick + decorEn + " "; + + if (!("lastNickDisplayed" in this) || this.lastNickDisplayed != nick) + { + this.lastNickDisplayed = nick; + this.mark = (("mark" in this) && this.mark == "even") ? "odd" : "even"; + } + + msgRowSource = document.createElementNS(XHTML_NS, "html:td"); + msgRowSource.setAttribute("class", "msg-user"); + + // Make excessive nicks get shunted. + if (nick && (nick.length > client.MAX_NICK_DISPLAY)) + blockLevel = true; + + if (decorSt) + msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor")); + if (nickURL) + { + var nick_anchor = document.createElementNS(XHTML_NS, "html:a"); + nick_anchor.setAttribute("class", "chatzilla-link"); + nick_anchor.setAttribute("href", nickURL); + nick_anchor.appendChild(newInlineText(nick)); + msgRowSource.appendChild(nick_anchor); + } + else + { + msgRowSource.appendChild(newInlineText(nick)); + } + if (msgprefix) + { + /* We don't style the "/" with chatzilla-decor because the one + * thing we don't want is it disappearing! + */ + msgRowSource.appendChild(newInlineText("/", "")); + msgRowSource.appendChild(newInlineText(msgprefix, + "chatzilla-prefix")); + } + if (decorEn) + msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor")); + canMergeData = this.prefs["collapseMsgs"]; + } + else if (msgprefix) + { + decorSt = "<"; + decorEn = ">"; + + logStringPfx += decorSt + "/" + msgprefix + decorEn + " "; + + msgRowSource = document.createElementNS(XHTML_NS, "html:td"); + msgRowSource.setAttribute("class", "msg-user"); + + msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor")); + msgRowSource.appendChild(newInlineText("/", "")); + msgRowSource.appendChild(newInlineText(msgprefix, "chatzilla-prefix")); + msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor")); + canMergeData = this.prefs["collapseMsgs"]; + } + else + { + isSuperfluous = true; + if (!client.debugHook.enabled && msgtype in client.responseCodeMap) + { + code = client.responseCodeMap[msgtype]; + } + else + { + if (!client.debugHook.enabled && client.HIDE_CODES) + code = client.DEFAULT_RESPONSE_CODE; + else + code = "[" + msgtype + "]"; + } + + /* Display the message code */ + msgRowType = document.createElementNS(XHTML_NS, "html:td"); + msgRowType.setAttribute("class", "msg-type"); + + msgRowType.appendChild(newInlineText(code)); + logStringPfx += code + " "; + } + + if (message) + { + msgRowData = document.createElementNS(XHTML_NS, "html:td"); + msgRowData.setAttribute("class", "msg-data"); + + var tmpMsgs = message; + if (typeof message == "string") + { + msgRowData.appendChild(stringToMsg(message, this)); + } + else + { + msgRowData.appendChild(message); + tmpMsgs = tmpMsgs.innerHTML.replace(/<[^<]*>/g, ""); + } + tmpMsgs = tmpMsgs.split(/\r?\n/); + for (var l = 0; l < tmpMsgs.length; l++) + logStrings[l] = logStringPfx + tmpMsgs[l]; + } + + if ("mark" in this) + msgRow.setAttribute("mark", this.mark); + + if (isImportant) + { + if ("importantMessages" in this) + { + var importantId = "important" + (this.importantMessages++); + msgRow.setAttribute("id", importantId); + } + msgRow.setAttribute("important", "true"); + msgRow.setAttribute("aria-live", "assertive"); + } + + // Timestamps first... + msgRow.appendChild(msgRowTimestamp); + // Now do the rest of the row, after block-level stuff. + if (msgRowSource) + msgRow.appendChild(msgRowSource); + else + msgRow.appendChild(msgRowType); + if (msgRowData) + msgRow.appendChild(msgRowData); + updateTimestampFor(this, msgRow); + + if (blockLevel) + { + /* putting a div here crashes mozilla, so fake it with nested tables + * for now */ + var tr = document.createElementNS(XHTML_NS, "html:tr"); + tr.setAttribute ("class", "msg-nested-tr"); + var td = document.createElementNS(XHTML_NS, "html:td"); + td.setAttribute ("class", "msg-nested-td"); + td.setAttribute ("colspan", "3"); + + tr.appendChild(td); + var table = document.createElementNS(XHTML_NS, "html:table"); + table.setAttribute ("class", "msg-nested-table"); + table.setAttribute("role", "presentation"); + + td.appendChild (table); + var tbody = document.createElementNS(XHTML_NS, "html:tbody"); + + tbody.appendChild(msgRow); + table.appendChild(tbody); + msgRow = tr; + } + + // Actually add the item. + addHistory (this, msgRow, canMergeData); + + // Update attention states... + if (isImportant || getAttention) + { + setTabState(this, "attention"); + if (client.prefs["notify.aggressive"]) + window.getAttention(); + } + else + { + if (isSuperfluous) + { + setTabState(this, "superfluous"); + } + else + { + setTabState(this, "activity"); + } + } + + // Copy Important Messages [to network view]. + if (isImportant && client.prefs["copyMessages"] && (o.network != this)) + { + if (importantId) + { + // Create the linked inline button + var msgspan = document.createElementNS(XHTML_NS, "html:span"); + msgspan.setAttribute("isImportant", "true"); + + var cmd = "jump-to-anchor " + importantId + " " + this.unicodeName; + var prefix = getMsg(MSG_JUMPTO_BUTTON, [this.unicodeName, cmd]); + + // Munge prefix as a button + client.munger.getRule(".inline-buttons").enabled = true; + client.munger.munge(prefix + " ", msgspan, o); + + // Munge rest of message normally + client.munger.getRule(".inline-buttons").enabled = false; + client.munger.munge(message, msgspan, o); + + o.network.displayHere(msgspan, msgtype, sourceObj, destObj); + } + else + { + o.network.displayHere(message, msgtype, sourceObj, destObj); + } + } + + // Log file time! + if (this.prefs["log"]) + { + if (!this.logFile) + client.openLogFile(this); + + try + { + var LE = client.lineEnd; + for (var l = 0; l < logStrings.length; l++) + this.logFile.write(fromUnicode(logStrings[l] + LE, "utf-8")); + } + catch (ex) + { + // Stop logging before showing any messages! + this.prefs["log"] = false; + dd("Log file write error: " + formatException(ex)); + this.displayHere(getMsg(MSG_LOGFILE_WRITE_ERROR, getLogPath(this)), + "ERROR"); + } + } + + /* We want to show alerts if they're from a non-current view (optional), + * or we don't have focus at all. + */ + if (client.prefs["alert.globalEnabled"] + && this.prefs["alert.enabled"] && client.alert && + (!window.isFocused + || (!client.prefs['alert.nonFocusedOnly'] && + !("currentObject" in client && client.currentObject == this) + ) + ) + ) + { + if (isImportant) + { + showEventAlerts(this.TYPE, "stalk", message, nick, o, this, msgtype); + } + else if (isSuperfluous) + { + showEventAlerts(this.TYPE, "event", message, nick, o, this, msgtype); + } + else + { + showEventAlerts(this.TYPE, "chat" , message, nick, o, this, msgtype); + } + } + +} + +function addHistory (source, obj, mergeData) +{ + if (!("messages" in source) || (source.messages == null)) + createMessages(source); + + var tbody = source.messages.firstChild; + var appendTo = tbody; + + var needScroll = false; + + if (mergeData) + { + var inobj = obj; + // This gives us the non-nested row when there is nesting. + if (inobj.className == "msg-nested-tr") + inobj = inobj.firstChild.firstChild.firstChild.firstChild; + + var thisUserCol = inobj.firstChild; + while (thisUserCol && !thisUserCol.className.match(/^(msg-user|msg-type)$/)) + thisUserCol = thisUserCol.nextSibling; + + var thisMessageCol = inobj.firstChild; + while (thisMessageCol && !(thisMessageCol.className == "msg-data")) + thisMessageCol = thisMessageCol.nextSibling; + + let columnInfo = findPreviousColumnInfo(source.messages); + let nickColumns = columnInfo.nickColumns; + let rowExtents = columnInfo.extents; + let nickColumnCount = nickColumns.length; + + let lastRowSpan = 0; + let sameNick = false; + let samePrefix = false; + let sameDest = false; + let haveSameType = false; + let isAction = false; + let collapseActions; + let needSameType = false; + // 1 or messages, check for doubles. + if (nickColumnCount > 0) + { + var lastRow = nickColumns[nickColumnCount - 1].parentNode; + // What was the span last time? + lastRowSpan = Number(nickColumns[0].getAttribute("rowspan")); + // Are we the same user as last time? + sameNick = (lastRow.getAttribute("msg-user") == + inobj.getAttribute("msg-user")); + // Do we have the same prefix as last time? + samePrefix = (lastRow.getAttribute("msg-prefix") == + inobj.getAttribute("msg-prefix")); + // Do we have the same destination as last time? + sameDest = (lastRow.getAttribute("msg-dest") == + inobj.getAttribute("msg-dest")); + // Is this message the same type as the last one? + haveSameType = (lastRow.getAttribute("msg-type") == + inobj.getAttribute("msg-type")); + // Is either of the messages an action? We may not want to collapse + // depending on the collapseActions pref + isAction = ((inobj.getAttribute("msg-type") == "ACTION") || + (lastRow.getAttribute("msg-type") == "ACTION")); + // Do we collapse actions? + collapseActions = source.prefs["collapseActions"]; + + // Does the motif collapse everything, regardless of type? + // NOTE: the collapseActions pref can override this for actions + needSameType = !(("motifSettings" in source) && + source.motifSettings && + ("collapsemore" in source.motifSettings)); + } + + if (sameNick && samePrefix && sameDest && + (haveSameType || !needSameType) && + (!isAction || collapseActions)) + { + obj = inobj; + if (columnInfo.nested) + appendTo = source.messages.firstChild.lastChild.firstChild.firstChild.firstChild; + + if (obj.getAttribute("important")) + { + nickColumns[nickColumnCount - 1].setAttribute("important", + true); + } + + // Remove nickname column from new row. + obj.removeChild(thisUserCol); + + // Expand previous grouping's nickname cell(s) to fill-in the gap. + for (var i = 0; i < nickColumns.length; ++i) + nickColumns[i].setAttribute("rowspan", rowExtents.length + 1); + } + } + + if ("frame" in source) + needScroll = checkScroll(source.frame); + if (obj) + appendTo.appendChild(client.adoptNode(obj, appendTo.ownerDocument)); + + if (source.MAX_MESSAGES) + { + if (typeof source.messageCount != "number") + source.messageCount = 1; + else + source.messageCount++; + + if (source.messageCount > source.MAX_MESSAGES) + removeExcessMessages(source); + } + + if (needScroll) + scrollDown(source.frame, true); +} + +function removeExcessMessages(source) +{ + var window = getContentWindow(source.frame); + var rows = source.messages.rows; + var lastItemOffset = rows[rows.length - 1].offsetTop; + var tbody = source.messages.firstChild; + while (source.messageCount > source.MAX_MESSAGES) + { + if (tbody.firstChild.className == "msg-nested-tr") + { + var table = tbody.firstChild.firstChild.firstChild; + var toBeRemoved = source.messageCount - source.MAX_MESSAGES; + // If we can remove the entire table, do that... + if (table.rows.length <= toBeRemoved) + { + tbody.removeChild(tbody.firstChild); + source.messageCount -= table.rows.length; + table = null; // Don't hang onto this. + continue; + } + // Otherwise, remove rows from this table instead: + tbody = table.firstChild; + } + var nextLastNode = tbody.firstChild.nextSibling; + // If the next node has only 2 childNodes, + // assume we're dealing with collapsed msgs, + // and move the nickname element: + if (nextLastNode.childNodes.length == 2) + { + var nickElem = tbody.firstChild.childNodes[1]; + var rowspan = nickElem.getAttribute("rowspan") - 1; + tbody.firstChild.removeChild(nickElem); + nickElem.setAttribute("rowspan", rowspan); + nextLastNode.insertBefore(nickElem, nextLastNode.lastChild); + } + tbody.removeChild(tbody.firstChild); + --source.messageCount; + } + var oldestItem = rows[0]; + if (oldestItem.className == "msg-nested-tr") + oldestItem = rows[0].firstChild.firstChild.firstChild.firstChild; + updateTimestampFor(source, oldestItem, true); + + // Scroll by as much as the lowest item has moved up: + lastItemOffset -= rows[rows.length - 1].offsetTop; + var y = window.pageYOffset; + if (!checkScroll(source.frame) && (y > lastItemOffset)) + window.scrollBy(0, -lastItemOffset); +} + +function findPreviousColumnInfo(table) +{ + // All the rows in the grouping (for merged rows). + var extents = new Array(); + // Get the last row in the table. + var tr = table.firstChild.lastChild; + // Bail if there's no rows. + if (!tr) + return {extents: [], nickColumns: [], nested: false}; + // Get message type. + if (tr.className == "msg-nested-tr") + { + var rv = findPreviousColumnInfo(tr.firstChild.firstChild); + rv.nested = true; + return rv; + } + // Now get the read one... + var className = (tr && tr.childNodes[1]) ? tr.childNodes[1].getAttribute("class") : ""; + // Keep going up rows until you find the first in a group. + // This will go up until it hits the top of a multiline/merged block. + while (tr && tr.childNodes[1] && className.search(/msg-user|msg-type/) == -1) + { + extents.push(tr); + tr = tr.previousSibling; + if (tr && tr.childNodes[1]) + className = tr.childNodes[1].getAttribute("class"); + } + + // If we ran out of rows, or it's not a talking line, we're outta here. + if (!tr || className != "msg-user") + return {extents: [], nickColumns: [], nested: false}; + + extents.push(tr); + + // Time to collect the nick data... + var nickCol = tr.firstChild; + // All the cells that contain nickname info. + var nickCols = new Array(); + while (nickCol) + { + // Just collect nickname column cells. + if (nickCol.getAttribute("class") == "msg-user") + nickCols.push(nickCol); + nickCol = nickCol.nextSibling; + } + + // And we're done. + return {extents: extents, nickColumns: nickCols, nested: false}; +} + +function getLogPath(obj) +{ + // If we're logging, return the currently-used URL. + if (obj.logFile) + return getURLSpecFromFile(obj.logFile.path); + // If not, return the ideal URL. + return getURLSpecFromFile(obj.prefs["logFileName"]); +} + +client.getConnectionCount = +function cli_gccount () +{ + var count = 0; + + for (var n in client.networks) + { + if (client.networks[n].isConnected()) + ++count; + } + + return count; +} + +client.quit = +function cli_quit (reason) +{ + var net, netReason; + for (var n in client.networks) + { + net = client.networks[n]; + if (net.isConnected()) + { + netReason = (reason ? reason : net.prefs["defaultQuitMsg"]); + netReason = (netReason ? netReason : client.userAgent); + net.quit(netReason); + } + } +} + +client.wantToQuit = +function cli_wantToQuit(reason, deliberate) +{ + + var close = true; + if (client.prefs["warnOnClose"] && !deliberate) + { + const buttons = [MSG_QUIT_ANYWAY, MSG_DONT_QUIT]; + var checkState = { value: true }; + var rv = confirmEx(MSG_CONFIRM_QUIT, buttons, 0, MSG_WARN_ON_EXIT, + checkState); + close = (rv == 0); + client.prefs["warnOnClose"] = checkState.value; + } + + if (close) + { + client.userClose = true; + display(MSG_CLOSING); + client.quit(reason); + } +} + +client.promptToSaveLogin = +function cli_promptToSaveLogin(url, type, username, password) +{ + var name = ""; + switch (type) + { + case "nick": + case "oper": + case "sasl": + name = username; + break; + case "serv": + case "chan": + name = url; + username = "*"; + break; + default: + display(getMsg(MSG_LOGIN_ERR_UNKNOWN_TYPE, type), MT_ERROR); + return; + } + + const buttons = [MSG_LOGIN_SAVE, MSG_LOGIN_DONT]; + var checkState = { value: true }; + var rv = confirmEx(getMsg(MSG_LOGIN_CONFIRM, name), buttons, 0, + MSG_LOGIN_PROMPT, checkState); + if (rv == 0) + { + client.prefs["login.promptToSave"] = checkState.value; + + var updated = addOrUpdateLogin(url, type, username, password); + if (updated) { + display(getMsg(MSG_LOGIN_UPDATED, name), MT_INFO); + } else { + display(getMsg(MSG_LOGIN_ADDED, name), MT_INFO); + } + } +} + +client.tryToGetLogin = +function cli_tryToGetLogin(url, type, username, existing, needpass, + promptstring) +{ + // Password is optional. If it is not given, we look for a saved password + // first. If there isn't one, we potentially use a safe prompt. + var info = getLogin(url, type, username); + var stored = (info && info.password) ? info.password : ""; + var promptToSave = false; + if (!existing && stored) { + existing = stored; + } else if (!existing && needpass) { + existing = promptPassword(promptstring, ""); + if (existing) + promptToSave = true; + } else if (existing && stored != existing) { + promptToSave = true; + } + + if (promptToSave && client.prefs["login.promptToSave"]) + client.promptToSaveLogin(url, type, username, existing); + + return existing; +} + +/* gets a tab-complete match for the line of text specified by |line|. + * wordStart is the position within |line| that starts the word being matched, + * wordEnd marks the end position. |cursorPos| marks the position of the caret + * in the textbox. + */ +client.performTabMatch = +function gettabmatch_usr (line, wordStart, wordEnd, word, cursorPos) +{ + if (wordStart != 0 || line[0] != client.COMMAND_CHAR) + return null; + + var matches = client.commandManager.listNames(word.substr(1), CMD_CONSOLE); + if (matches.length == 1 && wordEnd == line.length) + { + matches[0] = client.COMMAND_CHAR + matches[0] + " "; + } + else + { + for (var i in matches) + matches[i] = client.COMMAND_CHAR + matches[i]; + } + + return matches; +} + +client.openLogFile = +function cli_startlog(view, showMessage) +{ + function getNextLogFileDate() + { + var d = new Date(); + d.setMilliseconds(0); + d.setSeconds(0); + d.setMinutes(0); + switch (view.smallestLogInterval) + { + case "h": + return d.setHours(d.getHours() + 1); + case "d": + d.setHours(0); + return d.setDate(d.getDate() + 1); + case "m": + d.setHours(0); + d.setDate(1); + return d.setMonth(d.getMonth() + 1); + case "y": + d.setHours(0); + d.setDate(1); + d.setMonth(0); + return d.setFullYear(d.getFullYear() + 1); + } + //XXXhack: This should work... + return Infinity; + }; + + const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE; + + try + { + var file = new LocalFile(view.prefs["logFileName"]); + if (!file.localFile.exists()) + { + // futils.umask may be 0022. Result is 0644. + file.localFile.create(NORMAL_FILE_TYPE, 0o666 & ~futils.umask); + } + view.logFile = fopen(file.localFile, ">>"); + // If we're here, it's safe to say when we should re-open: + view.nextLogFileDate = getNextLogFileDate(); + } + catch (ex) + { + view.prefs["log"] = false; + dd("Log file open error: " + formatException(ex)); + view.displayHere(getMsg(MSG_LOGFILE_ERROR, getLogPath(view)), MT_ERROR); + return; + } + + if (showMessage) + view.displayHere(getMsg(MSG_LOGFILE_OPENED, getLogPath(view))); +} + +client.closeLogFile = +function cli_stoplog(view, showMessage) +{ + if (showMessage) + view.displayHere(getMsg(MSG_LOGFILE_CLOSING, getLogPath(view))); + + if (view.logFile) + { + view.logFile.close(); + view.logFile = null; + } +} + +function checkLogFiles() +{ + // For every view that has a logfile, check if we need a different file + // based on the current date and the logfile preference. We close the + // current logfile, and display will open the new one based on the pref + // when it's needed. + + var d = new Date(); + for (var n in client.networks) + { + var net = client.networks[n]; + if (net.logFile && (d > net.nextLogFileDate)) + client.closeLogFile(net); + if (("primServ" in net) && net.primServ && ("channels" in net.primServ)) + { + for (var c in net.primServ.channels) + { + var chan = net.primServ.channels[c]; + if (chan.logFile && (d > chan.nextLogFileDate)) + client.closeLogFile(chan); + } + } + if ("users" in net) + { + for (var u in net.users) + { + var user = net.users[u]; + if (user.logFile && (d > user.nextLogFileDate)) + client.closeLogFile(user); + } + } + } + + for (var dc in client.dcc.chats) + { + var dccChat = client.dcc.chats[dc]; + if (dccChat.logFile && (d > dccChat.nextLogFileDate)) + client.closeLogFile(dccChat); + } + for (var df in client.dcc.files) + { + var dccFile = client.dcc.files[df]; + if (dccFile.logFile && (d > dccFile.nextLogFileDate)) + client.closeLogFile(dccFile); + } + + // Don't forget about the client tab: + if (client.logFile && (d > client.nextLogFileDate)) + client.closeLogFile(client); + + /* We need to calculate the correct time for the next check. This is + * attempting to hit 2 seconds past the hour. We need the timezone offset + * here for when it is not a whole number of hours from UTC. + */ + var shiftedDate = d.getTime() + d.getTimezoneOffset() * 60000; + setTimeout(checkLogFiles, 3602000 - (shiftedDate % 3600000)); +} + +CIRCChannel.prototype.getLCFunction = +CIRCNetwork.prototype.getLCFunction = +CIRCUser.prototype.getLCFunction = +CIRCDCCChat.prototype.getLCFunction = +CIRCDCCFileTransfer.prototype.getLCFunction = +function getlcfn() +{ + var details = getObjectDetails(this); + var lcFn; + + if (details.server) + { + lcFn = function(text) + { + return details.server.toLowerCase(text); + } + } + + return lcFn; +} + +CIRCChannel.prototype.performTabMatch = +CIRCNetwork.prototype.performTabMatch = +CIRCUser.prototype.performTabMatch = +CIRCDCCChat.prototype.performTabMatch = +CIRCDCCFileTransfer.prototype.performTabMatch = +function gettabmatch_other (line, wordStart, wordEnd, word, cursorpos, lcFn) +{ + if (wordStart == 0 && line[0] == client.COMMAND_CHAR) + { + return client.performTabMatch(line, wordStart, wordEnd, word, + cursorpos); + } + + var matchList = new Array(); + var users; + var channels; + var userIndex = -1; + + var details = getObjectDetails(this); + + if (details.channel && word == details.channel.unicodeName[0]) + { + /* When we have #<tab>, we just want the current channel, + if possible. */ + matchList.push(details.channel.unicodeName); + } + else + { + /* Ok, not #<tab> or no current channel, so get the full list. */ + + if (details.channel) + users = details.channel.users; + + if (details.server) + { + channels = details.server.channels; + for (var c in channels) + matchList.push(channels[c].unicodeName); + if (!users) + users = details.server.users; + } + + if (users) + { + userIndex = matchList.length; + for (var n in users) + matchList.push(users[n].unicodeName); + } + } + + var matches = matchEntry(word, matchList, lcFn); + + var list = new Array(); + for (var i = 0; i < matches.length; i++) + list.push(matchList[matches[i]]); + + if (list.length == 1) + { + if (users && (userIndex >= 0) && (matches[0] >= userIndex)) + { + if (wordStart == 0) + list[0] += client.prefs["nickCompleteStr"]; + } + + if (wordEnd == line.length) + { + /* add a space if the word is at the end of the line. */ + list[0] += " "; + } + } + + return list; +} + +/* + * 290miliseconds for 1st derive is allowing about 3-4 events per + * second. 200ms for 2nd derivative allows max 200ms difference of + * frequency. This means when the flood is massive, this value is + * very closed to zero. But runtime issues should cause some delay + * in the core js, so zero value is not too good. We need increase + * this with a small, to make more strict. And when flood is done, + * we need detect it - based on arithmetic medium. When doesn't happen + * anything for a long time, perhaps for 2seconds the + * value - based on last 10 events - the 2nd value goes + * over 200ms average, so event should start again. + */ + +function FloodProtector (density, dispersion) +{ + this.lastHit = Number(new Date()); + + if (density) + this.floodDensity = density; + + if (dispersion) + this.floodDispersion = dispersion; +} + +FloodProtector.prototype.requestedTotal = 0; +FloodProtector.prototype.acceptedTotal = 0; +FloodProtector.prototype.firedTotal = 0; +FloodProtector.prototype.lastHit = 0; +FloodProtector.prototype.derivative1 = 100; +FloodProtector.prototype.derivative1Count = 100; +FloodProtector.prototype.derivative2 = 0; +FloodProtector.prototype.derivative2Count = 0; +FloodProtector.prototype.floodDensity = 290; +FloodProtector.prototype.floodDispersion = 200; + +FloodProtector.prototype.request = function () +{ + this.requestedTotal++; + var current = Number(new Date()); + var oldDerivative1 = this.derivative1; + this.derivative1 = current - this.lastHit; + this.derivative1Count = ((this.derivative1Count * 9) + this.derivative1) / 10; + this.derivative2 = Math.abs(this.derivative1 - oldDerivative1); + this.derivative2Count = ((this.derivative2Count * 9) + this.derivative2) / 10; + this.lastHit = current; +} + +FloodProtector.prototype.accept = function () +{ + this.acceptedTotal++; +} + +FloodProtector.prototype.fire = function () +{ + // There is no activity for 10 seconds - flood is possibly done. + // No need more recall. In other way the first normal activity + // overwrites it automatically earlier, if nessesary. + if ((Number(new Date()) - this.lastHit) > 10000) + return false; + + // The activity is not too frequent or not massive so should not be fire. + if ((this.derivative1Count > this.floodDensity) + || (this.derivative2Count > this.floodDispersion)) + { + return false; + } + + this.firedTotal++; + return true; + +} + + +function toasterPopupOverlapDelayReset (eventType) +{ + // it smells like a flood attack so rather wait more... + if (client.alert.floodProtector.fire()) + { + setTimeout( + toasterPopupOverlapDelayReset, + client.prefs['alert.overlapDelay'], eventType); + } + else + { + delete client.alert.alertList[eventType]; + } +} + +var alertClickerObserver = { + observe: function(subject, topic, data) + { + if (topic == "alertclickcallback") + { + var tb = document.getElementById(data); + if (tb && tb.view) + { + tb.view.dispatch("set-current-view", {view: tb.view}); + window.focus(); + } + } + }, + + // Gecko 1.7.* rulez + onAlertClickCallback: function(data) + { + var tb = document.getElementById(data); + if (tb && tb.view) + { + tb.view.dispatch("set-current-view", {view: tb.view}); + window.focus(); + } + }, + + onAlertFinished: function(data) + { + } +}; + + +// Show the alert for a particular event on a type of object. +function showEventAlerts (type, event, message, nick, o, thisp, msgtype) +{ + + // Converts .TYPE values into the event object names. + // IRCChannel => channel, IRCUser => user, etc. + type = type.replace(/^IRC/i,'').toLowerCase(); + + var source = type; + // DCC Chat sessions should act just like user views. + if (type == "dccchat") type = "user"; + + var ev = type + "." + event; + if (!(("alert."+ev) in thisp.prefs)) + return; + if (!thisp.prefs["alert."+ev]) + return; + + client.alert.floodProtector.request(); + if (ev in client.alert.alertList) + return; + + client.alert.floodProtector.accept(); + if(client.prefs['alert.overlapDelay'] > 0) + { + client.alert.alertList[ev] = true; + setTimeout(toasterPopupOverlapDelayReset, + client.prefs['alert.overlapDelay'], ev); + } + + var clickable = client.prefs['alert.clickable']; + var tabId = clickable ? getTabForObject(thisp,false).id : ""; + var listener = clickable ? alertClickerObserver : null; + + message = removeColorCodes(message); + if (nick) + { + if (msgtype == "ACTION") + { + message = "* " + nick + " " + message; + } + else + { + message = "<" + nick + "> " + message; + } + } + + if ((source == "channel") && o.channel) + { + source = o.channel.viewName; + } + else if ((source == "user") && o.network) + { + source = o.network.viewName; + } + + // We can't be sure if it is a macOS and Growl is now turned off or not + try + { + client.alert.service.showAlertNotification( + "chrome://chatzilla/skin/images/logo.png", + "ChatZilla - " + source + " - " + event, + message, clickable, tabId, listener); + } + catch(ex) + { + // yup. it is probably a MAC or NsIAlertsService is not initialized + } +} diff --git a/comm/suite/chatzilla/xul/lib/munger.js b/comm/suite/chatzilla/xul/lib/munger.js new file mode 100644 index 0000000000..cdbda15c54 --- /dev/null +++ b/comm/suite/chatzilla/xul/lib/munger.js @@ -0,0 +1,245 @@ +/* -*- 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/. */ + +/* Constructs a new munger entry, using a regexp or lambda match function, and + * a class name (to be applied by the munger itself) or lambda replace + * function, and the default enabled state and a start priority (used if two + * rules match at the same index), as well as a default tag (when the munger + * adds it based on the class name) name. + * + * Regular Expressions for matching should ensure that the first capturing + * group is the one that contains the matched text. Non-capturing groups, of + * zero-width or otherwise can be used before and after, to ensure the right + * things are matched (e.g. to ensure whitespace before something). + * + * Note that for RegExp matching, the munger will search for the matched text + * (from the first capturing group) from the leftmost point of the entire + * match. This means that if the text that matched the first group occurs in + * any part of the match before the group, the munger will apply to the wrong + * bit. This is not usually a problem, but if it is you should use a + * lambdaMatch function and be sure to return the new style return value, + * which specifically indicates the start. + * + * The lambda match and lambda replace functions have this signature: + * lambdaMatch(text, containerTag, data, mungerEntry) + * lambdaReplace(text, containerTag, data, mungerEntry) + * - text is the entire text to find a match in/that has matched + * - containerTag is the element containing the text (not useful?) + * - data is a generic object containing properties kept throughout + * - mungerEntry is the CMungerEntry object for the munger itself + * + * The lambdaReplace function is expected to do everything needed to put + * |text| into |containerTab| ready for display. + * + * The return value for lambda match functions should be either: + * - (old style) just the text that matched + * (the munger will search for this text, and uses the first match) + * - (new style) an object with properties: + * - start (start index, 0 = first character) + * - text (matched text) + * (note that |text| must start at index |start|) + * + * The return value for lambda replace functions are not used. + * + */ + +function CMungerEntry(name, regex, className, priority, startPriority, + enable, tagName) +{ + this.name = name; + if (name[0] != ".") + this.description = getMsg("munger." + name, null, null); + this.enabled = (typeof enable == "undefined" ? true : enable); + this.enabledDefault = this.enabled; + this.startPriority = (startPriority) ? startPriority : 0; + this.priority = priority; + this.tagName = (tagName) ? tagName : "html:span"; + + if (isinstance(regex, RegExp)) + this.regex = regex; + else + this.lambdaMatch = regex; + + if (typeof className == "function") + this.lambdaReplace = className; + else + this.className = className; +} + +function CMunger(textMunger) +{ + this.entries = new Array(); + this.tagName = "html:span"; + this.enabled = true; + if (textMunger) + this.insertPlainText = textMunger; +} + +CMunger.prototype.enabled = true; +CMunger.prototype.insertPlainText = insertText; + +CMunger.prototype.getRule = +function mng_getrule(name) +{ + for (var p in this.entries) + { + if (isinstance(this.entries[p], Object)) + { + if (name in this.entries[p]) + return this.entries[p][name]; + } + } + return null; +} + +CMunger.prototype.addRule = +function mng_addrule(name, regex, className, priority, startPriority, enable) +{ + if (typeof this.entries[priority] != "object") + this.entries[priority] = new Object(); + var entry = new CMungerEntry(name, regex, className, priority, + startPriority, enable); + this.entries[priority][name] = entry; +} + +CMunger.prototype.delRule = +function mng_delrule(name) +{ + for (var i in this.entries) + { + if (typeof this.entries[i] == "object") + { + if (name in this.entries[i]) + delete this.entries[i][name]; + } + } +} + +CMunger.prototype.munge = +function mng_munge(text, containerTag, data) +{ + + if (!containerTag) + containerTag = document.createElementNS(XHTML_NS, this.tagName); + + // Starting from the top, for each valid priority, check all the rules, + // return as soon as something matches. + if (this.enabled) + { + for (var i = this.entries.length - 1; i >= 0; i--) + { + if (i in this.entries) + { + if (this.mungePriority(i, text, containerTag, data)) + return containerTag; + } + } + } + + // If nothing matched, we don't have to do anything, + // just insert text (if any). + if (text) + this.insertPlainText(text, containerTag, data); + return containerTag; +} + +CMunger.prototype.mungePriority = +function mng_mungePriority(priority, text, containerTag, data) +{ + var matches = new Object(); + var entry; + // Find all the matches in this priority + for (entry in this.entries[priority]) + { + var munger = this.entries[priority][entry]; + if (!munger.enabled) + continue; + + var match = null; + if (typeof munger.lambdaMatch == "function") + { + var rval = munger.lambdaMatch(text, containerTag, data, munger); + if (typeof rval == "string") + match = { start: text.indexOf(rval), text: rval }; + else if (typeof rval == "object") + match = rval; + } + else + { + var ary = text.match(munger.regex); + if ((ary != null) && (ary[1])) + match = { start: text.indexOf(ary[1]), text: ary[1] }; + } + + if (match && (match.start >= 0)) + { + match.munger = munger; + matches[entry] = match; + } + } + + // Find the first matching entry... + var firstMatch = { start: text.length, munger: null }; + var firstPriority = 0; + for (entry in matches) + { + // If it matches before the existing first, or at the same spot but + // with a higher start-priority, this is a better match. + if (matches[entry].start < firstMatch.start || + ((matches[entry].start == firstMatch.start) && + (this.entries[priority][entry].startPriority > firstPriority))) + { + firstMatch = matches[entry]; + firstPriority = this.entries[priority][entry].startPriority; + } + } + + // Replace it. + if (firstMatch.munger) + { + var munger = firstMatch.munger; + firstMatch.end = firstMatch.start + firstMatch.text.length; + + // Need to deal with the text before the match, if there is any. + var beforeText = text.substr(0, firstMatch.start); + if (firstMatch.start > 0) + this.munge(beforeText, containerTag, data); + + if (typeof munger.lambdaReplace == "function") + { + // The munger rule itself should take care of munging the 'inside' + // of the match. + munger.lambdaReplace(firstMatch.text, containerTag, data, munger); + this.munge(text.substr(firstMatch.end), containerTag, data); + + return containerTag; + } + else + { + var tag = document.createElementNS(XHTML_NS, munger.tagName); + tag.setAttribute("class", munger.className + calcClass(data)); + + // Don't let this rule match again when we recurse. + munger.enabled = false; + this.munge(firstMatch.text, tag, data); + munger.enabled = true; + + containerTag.appendChild(tag); + + this.munge(text.substr(firstMatch.end), containerTag, data); + + return containerTag; + } + } + return null; +} + +function insertText(text, containerTag, data) +{ + var textNode = document.createTextNode(text); + containerTag.appendChild(textNode); +} + diff --git a/comm/suite/chatzilla/xul/lib/tree-utils.js b/comm/suite/chatzilla/xul/lib/tree-utils.js new file mode 100644 index 0000000000..1e47aebee6 --- /dev/null +++ b/comm/suite/chatzilla/xul/lib/tree-utils.js @@ -0,0 +1,1716 @@ +/* -*- 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/. */ + +/** + * An implemention of |nsITreeView| for a tree whose elements have no children. + * + * Code using BasicOView can override |getRowProperties|, |getColumnProperties|, + * |getCellProperties|, etc., as needed. + * + * Code using |BasicOView| will need to make the appropriate |myTree.tree + * .invalidate| calls when |myTree.data| changes. + * + * @syntax + * var myTree = new BasicOView() + * myTree.setColumnNames(["col 1", "col 2"]); + * myTree.data = [["row 1, col 1", "row 1, col 2"], + * ["row 2, col 1", "row 2, col 2"]]; + * treeBoxObject.view = myTree; + */ +function BasicOView() +{ + this.tree = null; +} + +/* functions *you* should call to initialize and maintain the tree state */ + +/* scroll the line specified by |line| to the center of the tree */ +BasicOView.prototype.centerLine = +function bov_ctrln (line) +{ + var first = this.tree.getFirstVisibleRow(); + var last = this.tree.getLastVisibleRow(); + this.scrollToRow(line - (last - first + 1) / 2); +} + +/* call this to set the association between column names and data columns */ +BasicOView.prototype.setColumnNames = +function bov_setcn (aryNames) +{ + this.columnNames = new Object(); + for (var i = 0; i < aryNames.length; ++i) + this.columnNames[aryNames[i]] = i; +} + +/* + * scroll the source so |line| is at either the top, center, or bottom + * of the view, depending on the value of |align|. + * + * line is the one based target line. + * if align is negative, the line will be scrolled to the top, if align is + * zero the line will be centered, and if align is greater than 0 the line + * will be scrolled to the bottom. 0 is the default. + */ +BasicOView.prototype.scrollTo = +function bov_scrollto (line, align) +{ + if (!this.tree) + return; + + var headerRows = 1; + + var first = this.tree.getFirstVisibleRow(); + var last = this.tree.getLastVisibleRow(); + var viz = last - first + 1 - headerRows; /* total number of visible rows */ + + /* all rows are visible, nothing to scroll */ + if (first == 0 && last >= this.rowCount) + return; + + /* tree lines are 0 based, we accept one based lines, deal with it */ + --line; + + /* safety clamp */ + if (line < 0) + line = 0; + if (line >= this.rowCount) + line = this.rowCount - 1; + + if (align < 0) + { + if (line > this.rowCount - viz) /* overscroll, can't put a row from */ + line = this.rowCount - viz; /* last page at the top. */ + this.tree.scrollToRow(line); + } + else if (align > 0) + { + if (line < viz) /* underscroll, can't put a row from the first page */ + line = 0; /* at the bottom. */ + else + line = line - viz + headerRows; + + this.tree.scrollToRow(line); + } + else + { + var half_viz = viz / 2; + /* lines past this line can't be centered without causing the tree + * to show more rows than we have. */ + var lastCenterable = this.rowCount - half_viz; + if (line > half_viz) + line = lastCenterable; + /* lines before this can't be centered without causing the tree + * to attempt to display negative rows. */ + else if (line < half_viz) + line = half_viz; + else + /* round the vizible rows down to a whole number, or we try to end up + * on a N + 0.5 row! */ + half_viz = Math.floor(half_viz); + + this.tree.scrollToRow(line - half_viz); + } +} + +BasicOView.prototype.__defineGetter__("selectedIndex", bov_getsel); +function bov_getsel() +{ + if (!this.tree || this.tree.view.selection.getRangeCount() < 1) + return -1; + + var min = new Object(); + this.tree.view.selection.getRangeAt(0, min, {}); + return min.value; +} + +BasicOView.prototype.__defineSetter__("selectedIndex", bov_setsel); +function bov_setsel(i) +{ + if (i == -1) + this.tree.view.selection.clearSelection(); + else + this.tree.view.selection.timedSelect (i, 500); + return i; +} + +/* + * functions the tree will call to retrieve the list state (nsITreeView.) + */ + +BasicOView.prototype.rowCount = 0; + +BasicOView.prototype.getCellProperties = +function bov_cellprops (row, col, properties) +{ + return ""; +} + +BasicOView.prototype.getColumnProperties = +function bov_colprops (col, properties) +{ + return ""; +} + +BasicOView.prototype.getRowProperties = +function bov_rowprops (index, properties) +{ + return ""; +} + +BasicOView.prototype.isContainer = +function bov_isctr (index) +{ + return false; +} + +BasicOView.prototype.isContainerOpen = +function bov_isctropen (index) +{ + return false; +} + +BasicOView.prototype.isContainerEmpty = +function bov_isctrempt (index) +{ + return false; +} + +BasicOView.prototype.isSeparator = +function bov_isseparator (index) +{ + return false; +} + +BasicOView.prototype.isSorted = +function bov_issorted (index) +{ + return false; +} + +BasicOView.prototype.canDrop = +function bov_drop (index, orientation) +{ + return false; +} + +BasicOView.prototype.drop = +function bov_drop (index, orientation) +{ + return false; +} + +BasicOView.prototype.getParentIndex = +function bov_getpi (index) +{ + if (index < 0) + return -1; + + return 0; +} + +BasicOView.prototype.hasNextSibling = +function bov_hasnxtsib (rowIndex, afterIndex) +{ + return (afterIndex < (this.rowCount - 1)); +} + +BasicOView.prototype.getLevel = +function bov_getlvl (index) +{ + return 0; +} + +BasicOView.prototype.getImageSrc = +function bov_getimgsrc (row, col) +{ +} + +BasicOView.prototype.getProgressMode = +function bov_getprgmode (row, col) +{ +} + +BasicOView.prototype.getCellValue = +function bov_getcellval (row, col) +{ +} + +BasicOView.prototype.getCellText = +function bov_getcelltxt (row, col) +{ + if (!this.columnNames) + return ""; + + if (typeof col == "object") + col = col.id; + + var ary = col.match (/:(.*)/); + if (ary) + col = ary[1]; + + var colName = this.columnNames[col]; + + if (typeof colName == "undefined") + return ""; + + return this.data[row][colName]; +} + +BasicOView.prototype.setTree = +function bov_seto (tree) +{ + this.tree = tree; +} + +BasicOView.prototype.toggleOpenState = +function bov_toggleopen (index) +{ +} + +BasicOView.prototype.cycleHeader = +function bov_cyclehdr (col) +{ +} + +BasicOView.prototype.selectionChanged = +function bov_selchg () +{ +} + +BasicOView.prototype.cycleCell = +function bov_cyclecell (row, col) +{ +} + +BasicOView.prototype.isEditable = +function bov_isedit (row, col) +{ + return false; +} + +BasicOView.prototype.isSelectable = +function bov_isselect (row, col) +{ + return false; +} + +BasicOView.prototype.setCellValue = +function bov_setct (row, col, value) +{ +} + +BasicOView.prototype.setCellText = +function bov_setct (row, col, value) +{ +} + +BasicOView.prototype.onRouteFocus = +function bov_rfocus (event) +{ + if ("onFocus" in this) + this.onFocus(event); +} + +BasicOView.prototype.onRouteBlur = +function bov_rblur (event) +{ + if ("onBlur" in this) + this.onBlur(event); +} + +BasicOView.prototype.onRouteDblClick = +function bov_rdblclick (event) +{ + if (!("onRowCommand" in this) || event.target.localName != "treechildren") + return; + + var rowIndex = this.tree.view.selection.currentIndex; + if (rowIndex == -1 || rowIndex > this.rowCount) + return; + var rec = this.childData.locateChildByVisualRow(rowIndex); + if (!rec) + { + ASSERT (0, "bogus row index " + rowIndex); + return; + } + + this.onRowCommand(rec, event); +} + +BasicOView.prototype.onRouteKeyPress = +function bov_rkeypress (event) +{ + var rec; + var rowIndex; + + if ("onRowCommand" in this && (event.keyCode == 13 || event.charCode == 32)) + { + if (!this.selection) + return; + + rowIndex = this.tree.view.selection.currentIndex; + if (rowIndex == -1 || rowIndex > this.rowCount) + return; + rec = this.childData.locateChildByVisualRow(rowIndex); + if (!rec) + { + ASSERT (0, "bogus row index " + rowIndex); + return; + } + + this.onRowCommand(rec, event); + } + else if ("onKeyPress" in this) + { + rowIndex = this.tree.view.selection.currentIndex; + if (rowIndex != -1 && rowIndex < this.rowCount) + { + rec = this.childData.locateChildByVisualRow(rowIndex); + if (!rec) + { + ASSERT (0, "bogus row index " + rowIndex); + return; + } + } + else + { + rec = null; + } + + this.onKeyPress(rec, event); + } +} + +BasicOView.prototype.performAction = +function bov_pact (action) +{ +} + +BasicOView.prototype.performActionOnRow = +function bov_pactrow (action) +{ +} + +BasicOView.prototype.performActionOnCell = +function bov_pactcell (action) +{ +} + +/** + * A single entry in an |XULTreeView|. + * + * These things take care of keeping the |XULTreeView| properly informed of + * changes in value and child count. You shouldn't have to maintain tree state + * at all - just update the |XULTreeViewRecord| objects. + * + * @param share An otherwise empty object to store cache data. You should use + * the same object as the |share| for the |XULTreeView| that you + * indend to contain these records. + * + */ +function XULTreeViewRecord(share) +{ + this._share = share; + this.visualFootprint = 1; + this.isHidden = true; /* records are considered hidden until they are + * inserted into a live tree */ +} + +XULTreeViewRecord.prototype.isContainerOpen = false; + +/* + * walk the parent tree to find our tree container. return null if there is + * none + */ +XULTreeViewRecord.prototype.findContainerTree = +function xtvr_gettree () +{ + if (!("parentRecord" in this)) + return null; + var parent = this.parentRecord; + + while (parent) + { + if ("_treeView" in parent) + return parent._treeView; + if ("parentRecord" in parent) + parent = parent.parentRecord; + else + parent = null; + } + + return null; +} + +XULTreeViewRecord.prototype.__defineGetter__("childIndex", xtvr_getChildIndex); +function xtvr_getChildIndex () +{ + //dd ("getChildIndex {"); + + if (!("parentRecord" in this)) + { + delete this._childIndex; + //dd ("} -1"); + return -1; + } + + if ("_childIndex" in this) + { + if ("childData" in this && this._childIndex in this.childData && + this.childData[this._childIndex] == this) + { + //dd ("} " + this._childIndex); + return this._childIndex; + } + } + + var childData = this.parentRecord.childData; + var len = childData.length; + for (var i = 0; i < len; ++i) + { + if (childData[i] == this) + { + this._childIndex = i; + //dd ("} " + this._childIndex); + return i; + } + } + + delete this._childIndex; + //dd ("} -1"); + return -1; +} + +XULTreeViewRecord.prototype.__defineSetter__("childIndex", xtvr_setChildIndex); +function xtvr_setChildIndex () +{ + dd("xtvr: childIndex is read only, ignore attempt to write to it\n"); + if (typeof getStackTrace == "function") + dd(getStackTrace()); +} + +/* count the number of parents, not including the root node */ +XULTreeViewRecord.prototype.__defineGetter__("level", xtvr_getLevel); +function xtvr_getLevel () +{ + if (!("parentRecord" in this)) + return -1; + + var rv = 0; + var parentRecord = this.parentRecord; + while ("parentRecord" in parentRecord && + (parentRecord = parentRecord.parentRecord)) ++rv; + return rv; +} + +/* + * associates a property name on this record, with a column in the tree. This + * method will set up a get/set pair for the property name you specify which + * will take care of updating the tree when the value changes. DO NOT try + * to change your mind later. Do not attach a different name to the same colID, + * and do not rename the colID. You have been warned. + */ +XULTreeViewRecord.prototype.setColumnPropertyName = +function xtvr_setcol (colID, propertyName) +{ + function xtvr_getValueShim () + { + return this._colValues[colID]; + } + function xtvr_setValueShim (newValue) + { + this._colValues[colID] = newValue; + return newValue; + } + + if (!("_colValues" in this)) + this._colValues = new Object(); + + if (typeof propertyName == "function") + { + this._colValues.__defineGetter__(colID, propertyName); + } + else + { + this.__defineGetter__(propertyName, xtvr_getValueShim); + this.__defineSetter__(propertyName, xtvr_setValueShim); + } +} + +XULTreeViewRecord.prototype.setColumnPropertyValue = +function xtvr_setcolv (colID, value) +{ + this._colValues[colID] = value; +} + +/* + * set the default sort column and reSort. + */ +XULTreeViewRecord.prototype.setSortColumn = +function xtvr_setcol (colID, dir) +{ + //dd ("setting sort column to " + colID); + this._share.sortColumn = colID; + this._share.sortDirection = (typeof dir == "undefined") ? 1 : dir; + this.reSort(); +} + +/* + * set the default sort direction. 1 is ascending, -1 is descending, 0 is no + * sort. setting this to 0 will *not* recover the natural insertion order, + * it will only affect newly added items. + */ +XULTreeViewRecord.prototype.setSortDirection = +function xtvr_setdir (dir) +{ + this._share.sortDirection = dir; +} + +/* + * invalidate this row in the tree + */ +XULTreeViewRecord.prototype.invalidate = +function xtvr_invalidate() +{ + var tree = this.findContainerTree(); + if (tree) + { + var row = this.calculateVisualRow(); + if (row != -1) + tree.tree.invalidateRow(row); + } +} + +/* + * invalidate any data in the cache. + */ +XULTreeViewRecord.prototype.invalidateCache = +function xtvr_killcache() +{ + this._share.rowCache = new Object(); + this._share.lastComputedIndex = -1; + this._share.lastIndexOwner = null; +} + +/* + * default comparator function for sorts. if you want a custom sort, override + * this method. We declare xtvr_sortcmp as a top level function, instead of + * a function expression so we can refer to it later. + */ +XULTreeViewRecord.prototype.sortCompare = xtvr_sortcmp; +function xtvr_sortcmp (a, b) +{ + var sc = a._share.sortColumn; + var sd = a._share.sortDirection; + + a = a[sc]; + b = b[sc]; + + if (a < b) + return -1 * sd; + + if (a > b) + return 1 * sd; + + return 0; +} + +/* + * this method will cause all child records to be reSorted. any records + * with the default sortCompare method will be sorted by the colID passed to + * setSortColumn. + * + * the local parameter is used internally to control whether or not the + * sorted rows are invalidated. don't use it yourself. + */ +XULTreeViewRecord.prototype.reSort = +function xtvr_resort (leafSort) +{ + if (!("childData" in this) || this.childData.length < 1 || + (this.childData[0].sortCompare == xtvr_sortcmp && + !("sortColumn" in this._share) || this._share.sortDirection == 0)) + { + /* if we have no children, or we have the default sort compare and no + * sort flags, then just exit */ + return; + } + + this.childData.sort(this.childData[0].sortCompare); + + for (var i = 0; i < this.childData.length; ++i) + { + if ("isContainerOpen" in this.childData[i] && + this.childData[i].isContainerOpen) + this.childData[i].reSort(true); + else + this.childData[i].sortIsInvalid = true; + } + + if (!leafSort) + { + this.invalidateCache(); + var tree = this.findContainerTree(); + if (tree && tree.tree) + { + var rowIndex = this.calculateVisualRow(); + /* + dd ("invalidating " + rowIndex + " - " + + (rowIndex + this.visualFootprint - 1)); + */ + tree.tree.invalidateRange (rowIndex, + rowIndex + this.visualFootprint - 1); + } + } + delete this.sortIsInvalid; +} + +/* + * call this to indicate that this node may have children at one point. make + * sure to call it before adding your first child. + */ +XULTreeViewRecord.prototype.reserveChildren = +function xtvr_rkids (always) +{ + if (!("childData" in this)) + this.childData = new Array(); + if (!("isContainerOpen" in this)) + this.isContainerOpen = false; + if (always) + this.alwaysHasChildren = true; + else + delete this.alwaysHasChildren; +} + +/* + * add a child to the end of the child list for this record. takes care of + * updating the tree as well. + */ +XULTreeViewRecord.prototype.appendChild = +function xtvr_appchild (child) +{ + if (!isinstance(child, XULTreeViewRecord)) + throw Components.results.NS_ERROR_INVALID_ARG; + + child.isHidden = false; + child.parentRecord = this; + this.childData.push(child); + + if ("isContainerOpen" in this && this.isContainerOpen) + { + //dd ("appendChild: " + xtv_formatRecord(child, "")); + if (this.calculateVisualRow() >= 0) + { + var tree = this.findContainerTree(); + if (tree && tree.frozen) + this.needsReSort = true; + else + this.reSort(true); /* reSort, don't invalidate. we're going + * to do that in the + * onVisualFootprintChanged call. */ + } + this.onVisualFootprintChanged(child.calculateVisualRow(), + child.visualFootprint); + } +} + +/* + * add a list of children to the end of the child list for this record. + * faster than multiple appendChild() calls. + */ +XULTreeViewRecord.prototype.appendChildren = +function xtvr_appchild (children) +{ + var delta = 0; + for (var i = 0; i < children.length; ++i) + { + var child = children[i]; + child.isHidden = false; + child.parentRecord = this; + this.childData.push(child); + delta += child.visualFootprint; + } + + if ("isContainerOpen" in this && this.isContainerOpen) + { + if (this.calculateVisualRow() >= 0) + { + this.reSort(true); /* reSort, don't invalidate. we're going to do + * that in the onVisualFootprintChanged call. */ + } + this.onVisualFootprintChanged(this.childData[0].calculateVisualRow(), + delta); + } +} + +/* + * Removes a single child from this record by index. + * @param index Index of the child record to remove. + */ +XULTreeViewRecord.prototype.removeChildAtIndex = +function xtvr_remchild(index) +{ + var len = this.childData.length; + if (!ASSERT(index >= 0 && index < len, "index out of bounds")) + return; + + var orphan = this.childData[index]; + var delta = -orphan.visualFootprint; + var changeStart = orphan.calculateVisualRow(); + delete orphan.parentRecord; + arrayRemoveAt(this.childData, index); + + if (!orphan.isHidden && "isContainerOpen" in this && this.isContainerOpen) + this.onVisualFootprintChanged(changeStart, delta); +} + +/* + * Removes a range of children from this record by index. Faster than multiple + * removeChildAtIndex() calls. + * @param index Index of the first child record to remove. + * @param count Number of child records to remove. + */ +XULTreeViewRecord.prototype.removeChildrenAtIndex = +function xtvr_remchildren(index, count) +{ + var len = this.childData.length; + if (!ASSERT(index >= 0 && index < len, "index out of bounds")) + return; + if (!ASSERT(count > 0 && index + count <= len, "count out of bounds")) + return; + + var delta = 0; + var changeStart = this.childData[index].calculateVisualRow(); + for (var i = 0; i < count; ++i) + { + var orphan = this.childData[index + i]; + if (!orphan.isHidden) + delta -= orphan.visualFootprint; + delete orphan.parentRecord; + } + this.childData.splice(index, count); + + if ("isContainerOpen" in this && this.isContainerOpen) + this.onVisualFootprintChanged(changeStart, delta); +} + +/* + * hide this record and all descendants. + */ +XULTreeViewRecord.prototype.hide = +function xtvr_hide() +{ + if (this.isHidden) + return; + + var tree = this.findContainerTree(); + if (tree && tree.frozen) + { + this.isHidden = true; + if ("parentRecord" in this) + this.parentRecord.onVisualFootprintChanged(0, -this.visualFootprint); + return; + } + + /* get the row before hiding */ + var row = this.calculateVisualRow(); + this.invalidateCache(); + this.isHidden = true; + /* go right to the parent so we don't muck with our own visualFootprint + * record, we'll need it to be correct if we're ever unHidden. */ + if ("parentRecord" in this) + this.parentRecord.onVisualFootprintChanged(row, -this.visualFootprint); +} + +/* + * unhide this record and all descendants. + */ +XULTreeViewRecord.prototype.unHide = +function xtvr_uhide() +{ + if (!this.isHidden) + return; + + var tree = this.findContainerTree(); + if (tree && tree.frozen) + { + this.isHidden = false; + if ("parentRecord" in this) + this.parentRecord.onVisualFootprintChanged(0, this.visualFootprint); + return; + } + + this.isHidden = false; + this.invalidateCache(); + var row = this.calculateVisualRow(); + if (this.parentRecord) + this.parentRecord.onVisualFootprintChanged(row, this.visualFootprint); +} + +/* + * open this record, exposing it's children. DONT call this method if the + * record has no children. + */ +XULTreeViewRecord.prototype.open = +function xtvr_open () +{ + if (this.isContainerOpen) + return; + + if ("onPreOpen" in this) + this.onPreOpen(); + + this.isContainerOpen = true; + var delta = 0; + for (var i = 0; i < this.childData.length; ++i) + { + if (!this.childData[i].isHidden) + delta += this.childData[i].visualFootprint; + } + + /* this reSort should only happen if the sort column changed */ + this.reSort(true); + this.visualFootprint += delta; + if ("parentRecord" in this) + { + this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow(), + 0); + this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow() + + 1, delta); + } +} + +/* + * close this record, hiding it's children. DONT call this method if the record + * has no children, or if it is already closed. + */ +XULTreeViewRecord.prototype.close = +function xtvr_close () +{ + if (!this.isContainerOpen) + return; + + this.isContainerOpen = false; + var delta = 1 - this.visualFootprint; + this.visualFootprint += delta; + if ("parentRecord" in this) + { + this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow(), + 0); + this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow() + + 1, delta); + } + + if ("onPostClose" in this) + this.onPostClose(); +} + +/* + * called when a node above this one grows or shrinks. we need to adjust + * our own visualFootprint to match the change, and pass the message on. + */ +XULTreeViewRecord.prototype.onVisualFootprintChanged = +function xtvr_vpchange (start, amount) +{ + /* if we're not hidden, but this notification came from a hidden node + * (start == -1), ignore it, it doesn't affect us. */ + if (start == -1 && !this.isHidden) + { + + //dd ("vfp change (" + amount + ") from hidden node ignored."); + return; + } + + this.visualFootprint += amount; + + if ("parentRecord" in this) + this.parentRecord.onVisualFootprintChanged(start, amount); +} + +/* + * calculate the "visual" row for this record. If the record isn't actually + * visible return -1. + * eg. + * Name Visual Row + * node1 0 + * node11 1 + * node12 2 + * node2 3 + * node21 4 + * node3 5 + */ +XULTreeViewRecord.prototype.calculateVisualRow = +function xtvr_calcrow () +{ + /* if this is the second time in a row that someone asked us, fetch the last + * result from the cache. */ + if (this._share.lastIndexOwner == this) + return this._share.lastComputedIndex; + + var vrow; + + /* if this is an uninserted or hidden node, or... */ + if (!("parentRecord" in this) || (this.isHidden) || + /* if parent isn't open, or... */ + (!this.parentRecord.isContainerOpen) || + /* parent isn't visible */ + ((vrow = this.parentRecord.calculateVisualRow()) == -1)) + { + /* then we're not visible, return -1 */ + //dd ("cvr: returning -1"); + return -1; + } + + /* parent is the root node XXX parent is not visible */ + if (vrow == null) + vrow = 0; + else + /* parent is not the root node, add one for the space they take up. */ + ++vrow; + + /* add in the footprint for all of the earlier siblings */ + var ci = this.childIndex; + for (var i = 0; i < ci; ++i) + { + if (!this.parentRecord.childData[i].isHidden) + vrow += this.parentRecord.childData[i].visualFootprint; + } + + /* save this calculation to the cache. */ + this._share.lastIndexOwner = this; + this._share.lastComputedIndex = vrow; + + return vrow; +} + +/* + * locates the child record for the visible row |targetRow|. DO NOT call this + * with a targetRow less than this record's visual row, or greater than this + * record's visual row + the number of visible children it has. + */ +XULTreeViewRecord.prototype.locateChildByVisualRow = +function xtvr_find (targetRow, myRow) +{ + if (targetRow in this._share.rowCache) + return this._share.rowCache[targetRow]; + + /* if this is true, we *are* the index */ + if (targetRow == myRow) + return (this._share.rowCache[targetRow] = this); + + /* otherwise, we've got to search the kids */ + var childStart = myRow; /* childStart represents the starting visual row + * for the child we're examining. */ + for (var i = 0; i < this.childData.length; ++i) + { + var child = this.childData[i]; + /* ignore hidden children */ + if (child.isHidden) + continue; + /* if this kid is the targetRow, we're done */ + if (childStart == targetRow) + return (this._share.rowCache[targetRow] = child); + /* if this kid contains the index, ask *it* to find the record */ + else if (targetRow <= childStart + child.visualFootprint) { + /* this *has* to succeed */ + var rv = child.locateChildByVisualRow(targetRow, childStart + 1); + //XXXASSERT (rv, "Can't find a row that *has* to be there."); + /* don't cache this, the previous call to locateChildByVisualRow + * just did. */ + return rv; + } + + /* otherwise, get ready to ask the next kid */ + childStart += child.visualFootprint; + } + + return null; +} + +/** + * Used to drop a label into an arbitrary place in an arbitrary tree. + * + * Normally, specializations of |XULTreeViewRecord| are tied to a specific + * tree because of implementation details. |XTLabelRecords| are specially + * designed (err, hacked) to work around these details - this makes them + * slower, but more generic. + * + * We set up a getter for |_share| that defers to the parent object. This lets + * |XTLabelRecords| work in any tree. + */ +function XTLabelRecord (columnName, label, blankCols) +{ + this.setColumnPropertyName (columnName, "label"); + this.label = label; + this.property = null; + + if (typeof blankCols == "object") + { + for (var i in blankCols) + this._colValues[blankCols[i]] = ""; + } +} + +XTLabelRecord.prototype = new XULTreeViewRecord (null); + +XTLabelRecord.prototype.__defineGetter__("_share", tolr_getshare); +function tolr_getshare() +{ + if ("parentRecord" in this) + return this.parentRecord._share; + + ASSERT (0, "XTLabelRecord cannot be the root of a visible tree."); + return null; +} + +// @internal +function XTRootRecord (tree, share) +{ + this._share = share; + this._treeView = tree; + this.visualFootprint = 0; + this.isHidden = false; + this.reserveChildren(); + this.isContainerOpen = true; +} + +/* no cache passed in here, we set it in the XTRootRecord contructor instead. */ +XTRootRecord.prototype = new XULTreeViewRecord (null); + +XTRootRecord.prototype.open = +XTRootRecord.prototype.close = +function torr_notimplemented() +{ + /* don't do this on a root node */ +} + +XTRootRecord.prototype.calculateVisualRow = +function torr_calcrow () +{ + return null; +} + +XTRootRecord.prototype.reSort = +function torr_resort () +{ + if ("_treeView" in this && this._treeView.frozen) + { + this._treeView.needsReSort = true; + return; + } + + if (!("childData" in this) || this.childData.length < 1 || + (this.childData[0].sortCompare == xtvr_sortcmp && + !("sortColumn" in this._share) || this._share.sortDirection == 0)) + { + /* if we have no children, or we have the default sort compare but we're + * missing a sort flag, then just exit */ + return; + } + + this.childData.sort(this.childData[0].sortCompare); + + for (var i = 0; i < this.childData.length; ++i) + { + if ("isContainerOpen" in this.childData[i] && + this.childData[i].isContainerOpen) + this.childData[i].reSort(true); + else + this.childData[i].sortIsInvalid = true; + } + + if ("_treeView" in this && this._treeView.tree) + { + /* + dd ("root node: invalidating 0 - " + this.visualFootprint + + " for sort"); + */ + this.invalidateCache(); + this._treeView.tree.invalidateRange (0, this.visualFootprint); + } +} + +XTRootRecord.prototype.locateChildByVisualRow = +function torr_find (targetRow) +{ + if (targetRow in this._share.rowCache) + return this._share.rowCache[targetRow]; + + var childStart = -1; /* childStart represents the starting visual row + * for the child we're examining. */ + for (var i = 0; i < this.childData.length; ++i) + { + var child = this.childData[i]; + /* ignore hidden children */ + if (child.isHidden) + continue; + /* if this kid is the targetRow, we're done */ + if (childStart == targetRow) + return (this._share.rowCache[targetRow] = child); + /* if this kid contains the index, ask *it* to find the record */ + else if (targetRow <= childStart + child.visualFootprint) { + /* this *has* to succeed */ + var rv = child.locateChildByVisualRow(targetRow, childStart + 1); + //XXXASSERT (rv, "Can't find a row that *has* to be there."); + /* don't cache this, the previous call to locateChildByVisualRow + * just did. */ + return rv; + } + + /* otherwise, get ready to ask the next kid */ + childStart += child.visualFootprint; + } + + return null; +} + +XTRootRecord.prototype.onVisualFootprintChanged = +function torr_vfpchange (start, amount) +{ + if (!this._treeView.frozen) + { + this.invalidateCache(); + this.visualFootprint += amount; + if ("_treeView" in this && "tree" in this._treeView && + this._treeView.tree) + { + if (amount != 0) + this._treeView.tree.rowCountChanged (start, amount); + else + this._treeView.tree.invalidateRow (start); + } + } + else + { + if ("changeAmount" in this._treeView) + this._treeView.changeAmount += amount; + else + this._treeView.changeAmount = amount; + if ("changeStart" in this._treeView) + this._treeView.changeStart = + Math.min (start, this._treeView.changeStart); + else + this._treeView.changeStart = start; + } +} + +/** + * An implemention of |nsITreeView| for a tree whose elements have multiple + * levels of children. + * + * Code using |XULTreeView| can override |getRowProperties|, |getColumnProperties|, + * |getCellProperties|, etc., as needed. + * + * @param share An otherwise empty object to store cache data. + */ +function XULTreeView(share) +{ + if (!share) + share = new Object(); + this.childData = new XTRootRecord(this, share); + this.childData.invalidateCache(); + this.tree = null; + this.share = share; + this.frozen = 0; +} + +/* functions *you* should call to initialize and maintain the tree state */ + +/* + * Changes to the tree contents will not cause the tree to be invalidated + * until |thaw()| is called. All changes will be pooled into a single invalidate + * call. + * + * Freeze/thaws are nestable, the tree will not update until the number of + * |thaw()| calls matches the number of freeze() calls. + */ +XULTreeView.prototype.freeze = +function xtv_freeze () +{ + if (++this.frozen == 1) + { + this.changeStart = 0; + this.changeAmount = 0; + } +} + +/* + * Reflect any changes to the tree content since the last freeze. + */ +XULTreeView.prototype.thaw = +function xtv_thaw () +{ + if (this.frozen == 0) + { + ASSERT (0, "not frozen"); + return; + } + + if (--this.frozen == 0 && "changeStart" in this) + { + this.childData.onVisualFootprintChanged(this.changeStart, + this.changeAmount); + } + + if ("needsReSort" in this) { + this.childData.reSort(); + delete this.needsReSort; + } + + + delete this.changeStart; + delete this.changeAmount; +} + +XULTreeView.prototype.saveBranchState = +function xtv_savebranch (target, source, recurse) +{ + var len = source.length; + for (var i = 0; i < len; ++i) + { + if (source[i].isContainerOpen) + { + target[i] = new Object(); + target[i].name = source[i]._colValues["col-0"]; + if (recurse) + this.saveBranchState (target[i], source[i].childData, true); + } + } +} + +XULTreeView.prototype.restoreBranchState = +function xtv_restorebranch (target, source, recurse) +{ + for (var i in source) + { + if (typeof source[i] == "object") + { + var name = source[i].name; + var len = target.length; + for (var j = 0; j < len; ++j) + { + if (target[j]._colValues["col-0"] == name && + "childData" in target[j]) + { + //dd ("opening " + name); + target[j].open(); + if (recurse) + { + this.restoreBranchState (target[j].childData, + source[i], true); + } + break; + } + } + } + } +} + +/* scroll the line specified by |line| to the center of the tree */ +XULTreeView.prototype.centerLine = +function xtv_ctrln (line) +{ + var first = this.tree.getFirstVisibleRow(); + var last = this.tree.getLastVisibleRow(); + this.scrollToRow(line - (last - first + 1) / 2); +} + +/* + * functions the tree will call to retrieve the list state (nsITreeView.) + */ + +// @internal +XULTreeView.prototype.__defineGetter__("rowCount", xtv_getRowCount); +function xtv_getRowCount () +{ + if (!this.childData) + return 0; + + return this.childData.visualFootprint; +} + +// @internal +XULTreeView.prototype.isContainer = +function xtv_isctr (index) +{ + var row = this.childData.locateChildByVisualRow (index); + + return Boolean(row && ("alwaysHasChildren" in row || "childData" in row)); +} + +// @internal +XULTreeView.prototype.__defineGetter__("selectedIndex", xtv_getsel); +function xtv_getsel() +{ + if (!this.tree || this.tree.view.selection.getRangeCount() < 1) + return -1; + + var min = new Object(); + this.tree.view.selection.getRangeAt(0, min, {}); + return min.value; +} + +// @internal +XULTreeView.prototype.__defineSetter__("selectedIndex", xtv_setsel); +function xtv_setsel(i) +{ + this.tree.view.selection.clearSelection(); + if (i != -1) + this.tree.view.selection.timedSelect (i, 500); + return i; +} + +// @internal +XULTreeView.prototype.scrollTo = BasicOView.prototype.scrollTo; + +// @internal +XULTreeView.prototype.isContainerOpen = +function xtv_isctropen (index) +{ + var row = this.childData.locateChildByVisualRow (index); + return row && row.isContainerOpen; +} + +// @internal +XULTreeView.prototype.toggleOpenState = +function xtv_toggleopen (index) +{ + var row = this.childData.locateChildByVisualRow (index); + //ASSERT(row, "bogus row"); + if (row) + { + if (row.isContainerOpen) + row.close(); + else + row.open(); + } +} + +// @internal +XULTreeView.prototype.isContainerEmpty = +function xtv_isctrempt (index) +{ + var row = this.childData.locateChildByVisualRow (index); + if ("alwaysHasChildren" in row) + return false; + + if (!row || !("childData" in row)) + return true; + + return !row.childData.length; +} + +// @internal +XULTreeView.prototype.isSeparator = +function xtv_isseparator (index) +{ + return false; +} + +// @internal +XULTreeView.prototype.getParentIndex = +function xtv_getpi (index) +{ + if (index < 0) + return -1; + + var row = this.childData.locateChildByVisualRow (index); + + var rv = row.parentRecord.calculateVisualRow(); + //dd ("getParentIndex: row " + index + " returning " + rv); + return (rv != null) ? rv : -1; +} + +// @internal +XULTreeView.prototype.hasNextSibling = +function xtv_hasnxtsib (rowIndex, afterIndex) +{ + var row = this.childData.locateChildByVisualRow (rowIndex); + return row.childIndex < row.parentRecord.childData.length - 1; +} + +// @internal +XULTreeView.prototype.getLevel = +function xtv_getlvl (index) +{ + var row = this.childData.locateChildByVisualRow (index); + if (!row) + return 0; + + return row.level; +} + +// @internal +XULTreeView.prototype.getImageSrc = +function xtv_getimgsrc (index, col) +{ +} + +// @internal +XULTreeView.prototype.getProgressMode = +function xtv_getprgmode (index, col) +{ +} + +// @internal +XULTreeView.prototype.getCellValue = +function xtv_getcellval (index, col) +{ +} + +// @internal +XULTreeView.prototype.getCellText = +function xtv_getcelltxt (index, col) +{ + var row = this.childData.locateChildByVisualRow (index); + //ASSERT(row, "bogus row " + index); + + if (typeof col == "object") + col = col.id; + + var ary = col.match (/:(.*)/); + if (ary) + col = ary[1]; + + if (row && row._colValues && col in row._colValues) + return row._colValues[col]; + else + return ""; +} + +// @internal +XULTreeView.prototype.getCellProperties = +function xtv_cellprops (row, col, properties) +{ + return ""; +} + +// @internal +XULTreeView.prototype.getColumnProperties = +function xtv_colprops (col, properties) +{ + return ""; +} + +// @internal +XULTreeView.prototype.getRowProperties = +function xtv_rowprops (index, properties) +{ + return ""; +} + +// @internal +XULTreeView.prototype.isSorted = +function xtv_issorted (index) +{ + return false; +} + +// @internal +XULTreeView.prototype.canDrop = +function xtv_drop (index, orientation) +{ + var row = this.childData.locateChildByVisualRow (index); + //ASSERT(row, "bogus row " + index); + return (row && ("canDrop" in row) && row.canDrop(orientation)); +} + +// @internal +XULTreeView.prototype.drop = +function xtv_drop (index, orientation) +{ + var row = this.childData.locateChildByVisualRow (index); + //ASSERT(row, "bogus row " + index); + return (row && ("drop" in row) && row.drop(orientation)); +} + +// @internal +XULTreeView.prototype.setTree = +function xtv_seto (tree) +{ + this.childData.invalidateCache(); + this.tree = tree; +} + +// @internal +XULTreeView.prototype.cycleHeader = +function xtv_cyclehdr (col) +{ +} + +// @internal +XULTreeView.prototype.selectionChanged = +function xtv_selchg () +{ +} + +// @internal +XULTreeView.prototype.cycleCell = +function xtv_cyclecell (row, col) +{ +} + +// @internal +XULTreeView.prototype.isEditable = +function xtv_isedit (row, col) +{ + return false; +} + +// @internal +XULTreeView.prototype.isSelectable = +function xtv_isselect (row, col) +{ + return false; +} + +// @internal +XULTreeView.prototype.setCellValue = +function xtv_setct (row, col, value) +{ +} + +// @internal +XULTreeView.prototype.setCellText = +function xtv_setct (row, col, value) +{ +} + +// @internal +XULTreeView.prototype.performAction = +function xtv_pact (action) +{ +} + +// @internal +XULTreeView.prototype.performActionOnRow = +function xtv_pactrow (action) +{ +} + +// @internal +XULTreeView.prototype.performActionOnCell = +function xtv_pactcell (action) +{ +} + +// @internal +XULTreeView.prototype.onRouteFocus = +function xtv_rfocus (event) +{ + if ("onFocus" in this) + this.onFocus(event); +} + +// @internal +XULTreeView.prototype.onRouteBlur = +function xtv_rblur (event) +{ + if ("onBlur" in this) + this.onBlur(event); +} + +// @internal +XULTreeView.prototype.onRouteDblClick = +function xtv_rdblclick (event) +{ + if (!("onRowCommand" in this) || event.target.localName != "treechildren") + return; + + var rowIndex = this.tree.view.selection.currentIndex; + if (rowIndex == -1 || rowIndex > this.rowCount) + return; + var rec = this.childData.locateChildByVisualRow(rowIndex); + if (!rec) + { + ASSERT (0, "bogus row index " + rowIndex); + return; + } + + this.onRowCommand(rec, event); +} + +// @internal +XULTreeView.prototype.onRouteKeyPress = +function xtv_rkeypress (event) +{ + var rec; + var rowIndex; + + if ("onRowCommand" in this && (event.keyCode == 13 || event.charCode == 32)) + { + if (!this.selection) + return; + + rowIndex = this.tree.view.selection.currentIndex; + if (rowIndex == -1 || rowIndex > this.rowCount) + return; + rec = this.childData.locateChildByVisualRow(rowIndex); + if (!rec) + { + ASSERT (0, "bogus row index " + rowIndex); + return; + } + + this.onRowCommand(rec, event); + } + else if ("onKeyPress" in this) + { + rowIndex = this.tree.view.selection.currentIndex; + if (rowIndex != -1 && rowIndex < this.rowCount) + { + rec = this.childData.locateChildByVisualRow(rowIndex); + if (!rec) + { + ASSERT (0, "bogus row index " + rowIndex); + return; + } + } + else + { + rec = null; + } + + this.onKeyPress(rec, event); + } +} + +/******************************************************************************/ + +function xtv_formatRecord (rec, indent) +{ + var str = ""; + + for (var i in rec._colValues) + str += rec._colValues[i] + ", "; + + str += "["; + + str += rec.calculateVisualRow() + ", "; + str += rec.childIndex + ", "; + str += rec.level + ", "; + str += rec.visualFootprint + ", "; + str += rec.isHidden + "]"; + + return (indent + str); +} + +function xtv_formatBranch (rec, indent, recurse) +{ + var str = ""; + for (var i = 0; i < rec.childData.length; ++i) + { + str += xtv_formatRecord (rec.childData[i], indent) + "\n"; + if (recurse) + { + if ("childData" in rec.childData[i]) + str += xtv_formatBranch(rec.childData[i], indent + " ", + --recurse); + } + } + + return str; +} + diff --git a/comm/suite/chatzilla/xul/skin/about.css b/comm/suite/chatzilla/xul/skin/about.css new file mode 100644 index 0000000000..e4d935fa39 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/about.css @@ -0,0 +1,44 @@ +/* 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/. */ + +dialog { + padding-top: 0px; + padding-left: 0px; + padding-right: 0px; + width: 30em; +} + +.box-padded { + background-color: Window; + color: WindowText; +} + +.large-text { + font-size: large; +} + +#logo { + background: url(chrome://chatzilla/skin/images/logo.png); + background-repeat: no-repeat; + width: 32px; + height: 32px; +} + +#version { + color: GrayText; +} + +.contributors-label { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.contributors label { + margin-top: 0px; + margin-bottom: 0px; +} + +#groove { + margin-top: 0px; +} diff --git a/comm/suite/chatzilla/xul/skin/browserOverlay.css b/comm/suite/chatzilla/xul/skin/browserOverlay.css new file mode 100644 index 0000000000..bd0abf011d --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/browserOverlay.css @@ -0,0 +1,7 @@ +/* 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/. */ + +#chatzilla-open { + list-style-image: url("chrome://chatzilla/skin/images/chatzilla-16.png"); +} diff --git a/comm/suite/chatzilla/xul/skin/channels.css b/comm/suite/chatzilla/xul/skin/channels.css new file mode 100644 index 0000000000..6fd6be8506 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/channels.css @@ -0,0 +1,24 @@ +/* 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/. */ + +#rightPanel { + width: 12em; +} + +#bottomPanel { + width: 50em; + height: 20em; +} + +.platform-Mac #rightPanel { + width: 18em; +} + +#loadContainer { + margin: 0; +} + +#loadBar { + -moz-appearance: none; +} diff --git a/comm/suite/chatzilla/xul/skin/chatzilla.css b/comm/suite/chatzilla/xul/skin/chatzilla.css new file mode 100644 index 0000000000..63da06a05c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/chatzilla.css @@ -0,0 +1,305 @@ +/* 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/. */ + +@import url(chrome://communicator/skin/); + +window { + width: 640px; + height: 480px; +} + +#menu-view-none { + display: none; +} + +#header-url:hover { + text-decoration: underline; +} + +#outer-box { + margin: 5px; +} + +.toolbarbutton-menu-dropmarker { + margin-left: 0.5em; +} + +#input-widget, +#input-widget-multiline { + border: thin silver inset; +} + +#button-multiline-expand { + list-style-image:url("chrome://chatzilla/skin/images/multiline-expand.png"); +} + +#button-multiline-contract { + list-style-image:url("chrome://chatzilla/skin/images/multiline-contract.png"); +} + +#button-input { + list-style-image:url("chrome://chatzilla/skin/images/input-send.png"); +} + +.highlight-menu-item:hover { + color: white !important; + background: darkslategrey !important; +} + +#user-list { + margin: 0; +} + +#view-tabs { + overflow: hidden; +} + +#views-tbar-spacer { + -moz-box-flex: 10000; +} + +#tabs-drop-indicator-bar { + position: relative; + margin-top: -3px; + -moz-margin-start: -5px; + height: 3px; +} + +#tabs-drop-indicator { + position: relative; + margin-right: -11px; + margin-bottom: -8px; + width: 11px; + height: 11px; + background: url(images/drop-indicator-bottom.png) 50% 50% no-repeat; +} + +.view-button { + /* Box model is 5px 5px 5px 6px in aggregate. Extra pixel on the left is for the separator. */ + -moz-appearance: none; + margin: 5px 0; + border: none; + border-left: 1px solid ButtonShadow; + padding: 0 5px; + background: Button; + color: ButtonText; + font: message-box; + text-shadow: none; + text-align: center; + max-width: 30ex; + -moz-box-flex: 1; + -moz-box-orient: vertical; + -moz-box-align: stretch; +} + +.view-button:first-child { + border-left-color: transparent; +} + +.view-button::before { + content: " "; + display: -moz-box; + margin: -5px -5px 0 -5px; + height: 4px; +} + +.view-button[state="superfluous"]::before { + background-color: darkblue; +} + +.view-button[state="activity"]::before { + background-color: darkgreen; +} + +.view-button[state="attention"]::before { + background-color: red; +} + +.view-button:hover, +.view-button[state="current"] { + margin: 0 0 0 1px; + border-left: none; + border-radius: 0 0 6px 6px; + padding: 5px; + background: Window; + color: WindowText; +} + +.view-button[state="current"] + .view-button, +.view-button:hover + .view-button:not([state="current"]) { + border-left-color: transparent; +} + +.view-button[state="current"] { + position: relative; /* So it visually appears above the tabs either side. */ + border: 2px solid ButtonShadow; + border-top: none; + padding: 5px 3px 3px 3px; +} + +treecol { + border: none; +} + +/* ::::: Trees ::::: */ + +treechildren::-moz-tree-row { + min-height: 18px; +} + +/* The userlist can be in one of two state. In "symbol", the user's + * channel mode is shown as a @ or + image, while in "graphic" mode, the + * image is one of the LED images. + */ + +/* no mode */ +treechildren::-moz-tree-image { + list-style-image: url(chrome://chatzilla/skin/images/no-symbol.png); +} + +#user-list[mode="graphic"] treechildren::-moz-tree-image { + list-style-image: url(chrome://chatzilla/skin/images/no-graphic.png); +} + +/* voice */ +treechildren::-moz-tree-image(voice-true) { + list-style-image: url(chrome://chatzilla/skin/images/voice-symbol.png); +} + +#user-list[mode="graphic"] treechildren::-moz-tree-image(voice-true) { + list-style-image: url(chrome://chatzilla/skin/images/voice-graphic.png); +} + +/* half-chanop */ +treechildren::-moz-tree-image(halfop-true) { + list-style-image: url(chrome://chatzilla/skin/images/halfop-symbol.png); +} + +#user-list[mode="graphic"] treechildren::-moz-tree-image(halfop-true) { + list-style-image: url(chrome://chatzilla/skin/images/halfop-graphic.png); +} + +/* chanop */ +treechildren::-moz-tree-image(op-true) { + list-style-image: url(chrome://chatzilla/skin/images/op-symbol.png); +} + +#user-list[mode="graphic"] treechildren::-moz-tree-image(op-true) { + list-style-image: url(chrome://chatzilla/skin/images/op-graphic.png); +} + +/* admin */ +treechildren::-moz-tree-image(admin-true) { + list-style-image: url(chrome://chatzilla/skin/images/admin-symbol.png); +} + +#user-list[mode="graphic"] treechildren::-moz-tree-image(admin-true) { + list-style-image: url(chrome://chatzilla/skin/images/admin-graphic.png); +} + +/* founder */ +treechildren::-moz-tree-image(founder-true) { + list-style-image: url(chrome://chatzilla/skin/images/founder-symbol.png); +} + +#user-list[mode="graphic"] treechildren::-moz-tree-image(founder-true) { + list-style-image: url(chrome://chatzilla/skin/images/founder-graphic.png); +} + +/* away */ +treechildren::-moz-tree-cell-text(away-true) { + color: GrayText; + font-style: italic; +} + +treechildren::-moz-tree-cell-text(away-false) { +} + +menuitem[header="true"] { + text-align: center; +} + +.colorGrid +{ + width: 24px; + height: 16px; + border: 1px solid black; + -moz-box-align: center; + -moz-box-pack: center; +} + +#colorTooltip +{ + padding: 0px; + -moz-box-align: center; + -moz-box-pack: center; +} + +/* Feel the hacks. */ +progressmeter[mode="undetermined"] { + -moz-appearance: none; +} + +[dir="ltr"] { + direction: ltr; +} + +[dir="rtl"] { + direction: rtl; +} + +#input-splitter { + margin: 4px 0 0 0; +} + +#input-widgets { + margin: 4px 0; +} + +#server-nick, #button-multiline-expand, #button-input, #button-multiline-contract { + margin: 0; +} + +#input, #multiline-input { + margin: 0 4px; +} + +/* Hack; Stop the status-bar from distorting without a security icon */ +#status-text { + min-height: 17px; +} + +#status-text[notice="true"] { + font-weight: bold; +} + +#security-button { + min-width: 20px; +} + +#security-button:not([level="none"]):not([level="high"]):not([level="broken"]) { + display: none; +} + +#alert-status[alertstate="off"] { + list-style-image: url("chrome://chatzilla/skin/images/spbubble-off.png"); +} + +#alert-status[alertstate="on"] { + list-style-image: url("chrome://chatzilla/skin/images/spbubble-on.png"); +} + +#logging-status[loggingstate="off"] { + list-style-image: url("chrome://chatzilla/skin/images/logging-off.png"); +} + +#logging-status[loggingstate="on"] { + list-style-image: url("chrome://chatzilla/skin/images/logging-on.png"); +} + +/* Focus styling for a11y reasons */ +#user-list-box[focusobvious="true"], #browser-box[focusobvious="true"], +#multiline-hug-box[focusobvious="true"], #singleline-hug-box[focusobvious="true"] { + outline: 2px solid highlight; +} diff --git a/comm/suite/chatzilla/xul/skin/chatzillaOverlay.css b/comm/suite/chatzilla/xul/skin/chatzillaOverlay.css new file mode 100644 index 0000000000..fef849df97 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/chatzillaOverlay.css @@ -0,0 +1,7 @@ +/* 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/. */ + +#mini-irc, #tasksMenuIRC { + list-style-image: url("chrome://chatzilla/skin/images/chatzilla-16.png"); +} diff --git a/comm/suite/chatzilla/xul/skin/images/admin-graphic.png b/comm/suite/chatzilla/xul/skin/images/admin-graphic.png Binary files differnew file mode 100644 index 0000000000..5f6c75ba82 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/admin-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/admin-symbol.png b/comm/suite/chatzilla/xul/skin/images/admin-symbol.png Binary files differnew file mode 100644 index 0000000000..11da8ea98a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/admin-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/arrow-down.png b/comm/suite/chatzilla/xul/skin/images/arrow-down.png Binary files differnew file mode 100644 index 0000000000..6910fd19e6 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/arrow-down.png diff --git a/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png b/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png Binary files differnew file mode 100644 index 0000000000..3f462580a0 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png diff --git a/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png b/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png Binary files differnew file mode 100644 index 0000000000..4196235cd3 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png diff --git a/comm/suite/chatzilla/xul/skin/images/founder-graphic.png b/comm/suite/chatzilla/xul/skin/images/founder-graphic.png Binary files differnew file mode 100644 index 0000000000..2328611847 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/founder-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/founder-symbol.png b/comm/suite/chatzilla/xul/skin/images/founder-symbol.png Binary files differnew file mode 100644 index 0000000000..842230a849 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/founder-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png b/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png Binary files differnew file mode 100644 index 0000000000..bc84fe7728 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png b/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png Binary files differnew file mode 100644 index 0000000000..ca5654cd8e --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/input-send.png b/comm/suite/chatzilla/xul/skin/images/input-send.png Binary files differnew file mode 100644 index 0000000000..fc1213f34a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/input-send.png diff --git a/comm/suite/chatzilla/xul/skin/images/logging-off.png b/comm/suite/chatzilla/xul/skin/images/logging-off.png Binary files differnew file mode 100644 index 0000000000..e9f32a18ad --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/logging-off.png diff --git a/comm/suite/chatzilla/xul/skin/images/logging-on.png b/comm/suite/chatzilla/xul/skin/images/logging-on.png Binary files differnew file mode 100644 index 0000000000..349e23be6a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/logging-on.png diff --git a/comm/suite/chatzilla/xul/skin/images/logo.png b/comm/suite/chatzilla/xul/skin/images/logo.png Binary files differnew file mode 100644 index 0000000000..f963f2c46c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/logo.png diff --git a/comm/suite/chatzilla/xul/skin/images/multiline-contract.png b/comm/suite/chatzilla/xul/skin/images/multiline-contract.png Binary files differnew file mode 100644 index 0000000000..7ae5acd1ec --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/multiline-contract.png diff --git a/comm/suite/chatzilla/xul/skin/images/multiline-expand.png b/comm/suite/chatzilla/xul/skin/images/multiline-expand.png Binary files differnew file mode 100644 index 0000000000..0ada94b4ef --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/multiline-expand.png diff --git a/comm/suite/chatzilla/xul/skin/images/no-graphic.png b/comm/suite/chatzilla/xul/skin/images/no-graphic.png Binary files differnew file mode 100644 index 0000000000..cb27b2df0a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/no-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/no-symbol.png b/comm/suite/chatzilla/xul/skin/images/no-symbol.png Binary files differnew file mode 100644 index 0000000000..567ec2abf0 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/no-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/op-graphic.png b/comm/suite/chatzilla/xul/skin/images/op-graphic.png Binary files differnew file mode 100644 index 0000000000..99a6c47ff8 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/op-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/op-symbol.png b/comm/suite/chatzilla/xul/skin/images/op-symbol.png Binary files differnew file mode 100644 index 0000000000..84de14972c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/op-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png Binary files differnew file mode 100644 index 0000000000..0dde51eeaf --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png diff --git a/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png Binary files differnew file mode 100644 index 0000000000..009a3d1f7b --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png diff --git a/comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg b/comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg new file mode 100644 index 0000000000..b472e0bcf6 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- 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/. --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + width="320" + height="160" + id="svg2"> + <defs + id="defs4" /> + <g + transform="translate(0,-892.36218)" + id="layer1"> + <path + d="M 95.147225,909.3621 133.18775,947.33552 125,957.36218 l -40,0 0,-40 10.147225,-8.00008 z" + id="rect2818-1" + style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 25,907.36218 60,0 c 20,0 10,10 10,40 30,0 40,-10 40,10 l 0,90.00002 -110,0 0,-140.00002 z" + id="rect2818" + style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 45,1027.3622 70,0" + id="path3615" + style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 45,1007.3622 70,0" + id="path3615-7" + style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 45,987.36218 70,0" + id="path3615-0" + style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 45,967.36218 70,0" + id="path3615-4" + style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 45,947.36218 70,0" + id="path3615-4-5" + style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 45,927.36218 50,0" + id="path3615-4-7" + style="fill:none;stroke:#000000;stroke-width:8.16496563;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 255.14722,909.3621 38.04053,37.97342 -8.18775,10.02666 -40,0 0,-40 10.14722,-8.00008 z" + id="rect2818-1-9" + style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 185,907.36218 60,0 c 20,0 10,10 10,40 30,0 40,-10 40,10 l 0,90.00002 -110,0 0,-140.00002 z" + id="rect2818-9" + style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + </g> +</svg> diff --git a/comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg b/comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg new file mode 100644 index 0000000000..a5b507228c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg @@ -0,0 +1,636 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- 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/. --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + width="48" + height="72" + id="svg4597"> + <defs + id="defs4599"> + <linearGradient + x1="3.932596" + y1="4.0576153" + x2="11.188456" + y2="12.377568" + id="linearGradient6792" + xlink:href="#linearGradient4491" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.8333333,0,0,0.8333333,41.811051,54.89456)" /> + <linearGradient + id="linearGradient4491"> + <stop + id="stop4493" + style="stop-color:#008000;stop-opacity:1" + offset="0" /> + <stop + id="stop4495" + style="stop-color:#008000;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="3.932596" + y1="4.0576153" + x2="11.188456" + y2="12.377568" + id="linearGradient4678" + xlink:href="#linearGradient4491" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.8333333,0,0,0.8333333,41.811051,54.89456)" /> + <filter + id="filter3268"> + <feGaussianBlur + inkscape:collect="always" + id="feGaussianBlur3270" + stdDeviation="0.17400778" /> + </filter> + <linearGradient + x1="5.8113861" + y1="12.271072" + x2="11.031619" + y2="7.3054457" + id="linearGradient5639" + xlink:href="#linearGradient5118" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient5118"> + <stop + id="stop5120" + style="stop-color:#808080;stop-opacity:1" + offset="0" /> + <stop + id="stop5126" + style="stop-color:#b6b6b6;stop-opacity:1" + offset="1" /> + <stop + id="stop5122" + style="stop-color:#808080;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="1.3302127" + y1="7.4045763" + x2="4.4953356" + y2="6.201149" + id="linearGradient5641" + xlink:href="#linearGradient3308" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3308"> + <stop + id="stop3310" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3312" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <filter + id="filter3362"> + <feGaussianBlur + inkscape:collect="always" + id="feGaussianBlur3364" + stdDeviation="0.05131204" /> + </filter> + <linearGradient + x1="5.7836757" + y1="2.3257139" + x2="5.6800237" + y2="5.6109982" + id="linearGradient5643" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3176"> + <stop + id="stop3178" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /> + <stop + id="stop3180" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /> + </linearGradient> + <filter + id="filter3290"> + <feGaussianBlur + inkscape:collect="always" + id="feGaussianBlur3292" + stdDeviation="0.040874578" /> + </filter> + <linearGradient + x1="5.8113861" + y1="12.520331" + x2="10.529106" + y2="7.3054457" + id="linearGradient5645" + xlink:href="#linearGradient5042" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient5042"> + <stop + id="stop5044" + style="stop-color:#0066ff;stop-opacity:1" + offset="0" /> + <stop + id="stop5050" + style="stop-color:#22a0ff;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + x1="1.3302127" + y1="7.4045763" + x2="4.4953356" + y2="6.201149" + id="linearGradient5647" + xlink:href="#linearGradient3308" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.7836757" + y1="2.3257139" + x2="5.6800237" + y2="5.6109982" + id="linearGradient5649" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.8113861" + y1="12.520331" + x2="11.026272" + y2="7.3054457" + id="linearGradient5651" + xlink:href="#linearGradient5221" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient5221"> + <stop + id="stop5223" + style="stop-color:#00c800;stop-opacity:1" + offset="0" /> + <stop + id="stop5229" + style="stop-color:#2fff12;stop-opacity:1" + offset="1" /> + <stop + id="stop5225" + style="stop-color:#00de00;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="1.3302127" + y1="7.4045763" + x2="4.4953356" + y2="6.201149" + id="linearGradient5653" + xlink:href="#linearGradient3308" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.7836757" + y1="2.3257139" + x2="5.6800237" + y2="5.6109982" + id="linearGradient5655" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="6.3511858" + y1="12.54519" + x2="11.05113" + y2="3.82529" + id="linearGradient5657" + xlink:href="#linearGradient4778" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4778"> + <stop + id="stop4780" + style="stop-color:#ff7700;stop-opacity:1" + offset="0" /> + <stop + id="stop4786" + style="stop-color:#ff9600;stop-opacity:1" + offset="0.96214288" /> + <stop + id="stop4782" + style="stop-color:#ff7700;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="1.3302127" + y1="7.4045763" + x2="4.4953356" + y2="6.201149" + id="linearGradient5659" + xlink:href="#linearGradient3308" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.766274" + y1="2.4264779" + x2="5.6800237" + y2="5.6109982" + id="linearGradient5661" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="6.0393691" + y1="12.525679" + x2="10.796444" + y2="7.3054457" + id="linearGradient5663" + xlink:href="#linearGradient3141" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3141"> + <stop + id="stop3143" + style="stop-color:#7c0063;stop-opacity:1" + offset="0" /> + <stop + id="stop3149" + style="stop-color:#a60084;stop-opacity:1" + offset="1" /> + <stop + id="stop3145" + style="stop-color:#7c0063;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="1.3302127" + y1="7.4045763" + x2="4.4953356" + y2="6.201149" + id="linearGradient5665" + xlink:href="#linearGradient3308" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.7836757" + y1="2.3257139" + x2="5.6800237" + y2="5.6109982" + id="linearGradient5667" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.8113861" + y1="12.520331" + x2="11.026272" + y2="7.3054457" + id="linearGradient5669" + xlink:href="#linearGradient5337-52-886-410" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient5337-52-886-410"> + <stop + id="stop4558" + style="stop-color:#005e00;stop-opacity:1" + offset="0" /> + <stop + id="stop4560" + style="stop-color:#058c00;stop-opacity:1" + offset="1" /> + <stop + id="stop4562" + style="stop-color:#005e00;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + x1="1.3302127" + y1="7.4045763" + x2="4.4953356" + y2="6.201149" + id="linearGradient5671" + xlink:href="#linearGradient3308" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.7836757" + y1="2.3257139" + x2="5.6800237" + y2="5.6109982" + id="linearGradient5673" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="5.7836757" + y1="2.3257139" + x2="5.6800237" + y2="5.6109982" + id="linearGradient6033" + xlink:href="#linearGradient3176" + gradientUnits="userSpaceOnUse" /> + </defs> + <g + id="layer2"> + <rect + width="48" + height="72" + x="0" + y="1.4988011e-15" + id="rect4768" + style="opacity:1;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:35.43307114;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + </g> + <g + id="layer1"> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHDSURB VCiRVZE9aFNhGIXPe7+bm6TXUlNM/KPUQtB2U1pw08WC+LModLBLpYuo2Tq4mHApCI7SRUGEgqmK Crp3KG6KWG0Q2iFoKhjvtalpk96f3O9+r0NISJ/Dmc573uUQM6PDq6nHxtCZkZv1NSfru37Fp+D1 jXd3bfRAncLKvfd36l+dXLjRPDk2O0Hm8EE0a7vhx+KHYuVzeXaenyoA0ABgee7t7R8vSw/t5Z+n jk6OkNIY0IDD40Oxi/evz6RPH/sGgABAnNfGE/anX4u11eoRBkCkQQURIk+C/QjGQAL9UV9mceHZ 2LnpC2/0vkFzZnNlIysgAAb+fKkg+Oci2Pa6pj0FY4suWZZ1SK+VndGWlKSD21LAdtmBZzfgOQ24 1QZ2Vh0YA2S6wJSupNqUkNgPg5suWqXfqJVs6BAIjzOUUnEt3p94EU+bXgiJtiPIrtsKEcHPMIQQ dW3ywbWqkUk+B9A96i2HiFA/EXA4Yawzc1EDgN3vW7com1iDTj3f2+W9Ycn+5aQrYmKhUCi0qHfp /JW5JfyVV9GITC0pgHQMdPbAOgl6lM/nn+xbuoNlWYNENM3Mgoh2UqnUUi6XCzr5f4P/6zdUCtoG AAAAAElFTkSuQmCC " + x="12" + y="0" + width="12" + height="12" + id="image4146" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAGzSURB VCiRZZFPaxNRFMV/782kqWltCmmqYCwJuCqCEPoFBCtIdyKYRcEvIPhvIwgZZhVwoxZcdOWqC4UW 0V0XfgBbqGCqFarSQuMkxJjGTGY6M2+eCzsY9cDhXrj87lkcobUmUeP+XHasVKhId7cQeWHTbUav Ckuf9xmSSIDDxxdvp/XXW+lJsyjKd2BihqjXihvryy/Vxkal9EKHABKg93Th7ni4+XBUO0Vx9jIE fQgHmNOzcqZSu9o/X962bVsCGDfPONkTvbfPRlQzh45BCFA+hB4oD1Lj5LLkfuxsXZi9tPjcTEXt 62nvSxED0BoONsHrgPsdvN82ww55oz1v2/ZpU/QbJaEBBWggjqG9C71v4DrQ24fWFlk5lQGumcoP D1CAOAaS6bvg1KFVBwH9uEgcxykZyZOv3aNMgDpOSRz/2XUEjprWQFfmH7zZc4L8qo7+AYb8sVvU 9dG5HSnligSQjb0b7wbnPhwF5n+ftw9Len1kwUXIR5ZlBWKoabF678raFJ35Cf0zMxBjOPKUfp8p f5KG+aRarS7/1XSiWq2W831/0TAMqZTqSilXLMsKkvsvIAPVEw1F5a4AAAAASUVORK5CYII= " + x="12" + y="12" + width="12" + height="12" + id="image4189" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHUSURB VCiRdZFPaxNRFMXPezNTdHQS61jpopUUqiIoEoiCuhAUqYWCG5EiSvwI4gfIZJeFi9KlCApKF/6p G7ELkUGKElottoK1pppx1ISEahibTpjOy3vXjdG48GzugXN/i3sPIyJ0lLmZSe47kBqvmt5AK4jr wddwppQtl9EtIgIR4ax7+mrm/X5vbOM4uXKGXtNLehDekSPuiWnkYXT2QEQ4545dS63a8d76Lrq/ eZfeygX6pEpUpyq9oTk69eRYKZ/PcyKCVjtSSy4mF27/sNZsBQmNcUQsQg8zoDMdSdYLNhjbxZW5 w+NHL97Thq6ksvM7ipcZA4gBZbmKFjbQYiEkk9jOEqjon7HoLw16D7/d0iuyOtRuK4D/vetVax6e /Ah/q4eS8Q6z7ClEr2YCOK+LSFSEIACq6xUc39HAc/UML3pcGAbHnvWDUEoZ3Iqtx+ZPMxaC0I4V hFC/J/3x8aZComYTgIC7F1y/r9I3DQL+ByVm++nQWnqFcz7FAcD/4meHl4aX9VD/F4oUdhYH6OTy SMgZn3AcJ2ZdTbPR66OPGlbjTHNL07Ta27C72U/pIP1B1/TJXC53AwC6AQBAoVCwoyi6pGkal1IG nPMpx3HiTv4Lj1QMG8iNFZwAAAAASUVORK5CYII= " + x="12" + y="24" + width="12" + height="12" + id="image4274" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG4SURB VCiRVZExixpRFIXPez7fDDvgIII2BhJsQgqXYOEPkPgLUmwgso2dRUJ+gNNprEIKizQJCAnEIkQW 8gNstbCLIRJtAgqKi77MjG+cuSnCDJsPLqe458C9HEZEiGk0GnalUrkiouJ+v9+sVqtvg8HgF+7A 4kCn03lp2/aLUql0v1wuwzAMbLfbaDQafR0Oh1fT6TRIAr1e75VlWa9t205Xq1Vks1lIKSGlhOd5 6Pf7P6Moeug4TpQyDMNmjH2QUuYAgHMOIgLnHIwxSClRKBRyk8nksl6vf07VarVrImrExuPxiPP5 jCiKQEQQQiAIAszn83vj8fi9cF33gWEY0Fr/e4oxrNdrKKWglMLhcMBut4NlWRe+7z8Vnuf9FkIk 5hilFLTW2Gw2yS9ElOZhGN4opXQYhtBaIwiCROM5nU5wXZcA3DIiQrPZ/FQsFp8xxiCEQDqdhpQy 0cViQaZpfuecPxYAMJvNronoMp/PPzJNMzkviiIsl0sSQvzhnL9xHEezO02zVqv1hXP+hDF2wTkH AMpkMj+EEG/b7fa7/5qO6Xa7Od/3n6dSKR6G4S3n/KPjODre/wWkwN2ZHNKyngAAAABJRU5ErkJg gg== " + x="12" + y="48" + width="12" + height="12" + id="image4317" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHJSURB VCiRVZG/axNxGMY/971cbNJK2oZEW6IoZhAFq1CdXKoUcaqDoBWhq1MR/4BcM6WbiJOIOEgGhzgo VBDtIDhIJO1gNcVijbGQ1Dbmp7mcd/c6mGD6wPO+L7yfZ3h5NRGhp8nJh6H41Inr391AzNquluvr xaWN7NxX+iUiiAiXbr27fTpR2DyfduXRR5GX30SefGg6U/PLGVgwepwmIszMv7+TGz25yMiQMXsK DoUgFoLxQWg7YC4ufbkQyR43TdPTS6WzoVV//PFuMBL2BDQNbA86LnQ8GPSDMRYL597mJ2Znzj31 7erD1zbV+BHdBhFYKUKtA9U2/LKgZkNDD1AbiE4nk8mDvq3WvqOOoSMdoHv/Rhm2m/CzDaUmfK6B f2A0CFz1/ak3t5zgf1i6pQ6s2ZDfAZ8fjjkNPM8z1H6n+iJQ+WE7Njg2uJ1/3bHB7dppewxbJQGq avn5lUJkJ5cRx9sL980jq69lYqycV0qlFUDhzcpcvJj5pLcqe2HLI7r2Si6Gsy2ltLumadpa36e1 yzfuP6t40emGFwoOGb85YJTkzOHyus+n30skEg8A+gMApFKpsGVZN3VdV67rVpVSadM07d7+L8s2 8Sj9Mf2NAAAAAElFTkSuQmCC " + x="12" + y="36" + width="12" + height="12" + id="image4360" + style="display:inline" /> + <text + x="25.851986" + y="22.37845" + id="text6605" + xml:space="preserve" + style="font-size:12px;font-style:normal;font-weight:normal;fill:#ff7f2a;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan + x="25.851986" + y="22.37845" + id="tspan6607" + style="fill:#ff7f2a;stroke:#4d4d4d">&</tspan></text> + <text + x="25.371283" + y="11.332599" + id="text6690" + xml:space="preserve" + style="font-size:15.14527416px;font-style:normal;font-weight:normal;fill:#ff00ff;fill-opacity:1;stroke:#800080;stroke-width:1.26210618px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:inline;font-family:FranklinGotTDemCon"><tspan + x="25.371283" + y="11.332599" + id="tspan6692" + style="stroke-width:1.26210618">~</tspan></text> + <text + x="23.484241" + y="32.947613" + id="text6738" + xml:space="preserve" + style="font-size:10.52353096px;font-style:normal;font-weight:normal;fill:#00ff00;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.0876961;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan + x="23.484241" + y="32.947613" + id="tspan6740" + style="fill:#00ff00;stroke:#4d4d4d;stroke-width:0.0876961;stroke-miterlimit:4;stroke-dasharray:none">@</tspan></text> + <text + x="24.187044" + y="46.661697" + id="text6788" + xml:space="preserve" + style="font-size:12.85570812px;font-style:normal;font-weight:normal;fill:#008dff;fill-opacity:1;stroke:url(#linearGradient6792);stroke-width:0.10713092;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan + x="24.187044" + y="46.661697" + id="tspan6790" + style="fill:#008dff;fill-opacity:1;stroke:url(#linearGradient6792);stroke-width:0.10713092;stroke-miterlimit:4;stroke-dasharray:none">%</tspan></text> + <text + x="22.939159" + y="62.14077" + id="text6845" + xml:space="preserve" + style="font-size:22.86951828px;font-style:normal;font-weight:normal;fill:#00ff00;fill-opacity:1;stroke:#e3dbdb;stroke-width:0.19057931;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan + x="22.939159" + y="62.14077" + id="tspan6847" + style="fill:#808080;stroke:#e3dbdb;stroke-width:0.19057931;stroke-miterlimit:4;stroke-dasharray:none">+</tspan></text> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADCSURB VCiRzZGhCsIAFEXPmxvDKAYtNoMg6A+YBD9C7P7GWBgD60DBYPUP/AQx2PwFlYGwNHEbY8/gBMVk ELzwyuUe7oUnqso3Mr5K/xTwxa8DmE/DFbcDTARpAQdF144650ACOyJaAHtgbpbhoYGxsbCsGjW5 cs1i4pkn3lbRNtAsgUeDIN2Cwk5JCQmpUpUGDSMhGeTkxMQA+dskQY6KjitUTjduo4xsqmhPkAsQ AKvXhl1B0XfUiUp+Wd6H5P8edwdEdUHuCYIzqwAAAABJRU5ErkJggg== " + x="36" + y="0" + width="12" + height="12" + id="image8696" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFKSURB VCiRY/z//z8DKYAFXSA9PZ1V89ct73u/OfdNWrLtE7o8I7INjTEuWp6in9YzMf7/wMn0X/zwO+6k jHkH9yFrYELmWAl8jmNj+v/ApO+0+bd/TEe1eX+EotuAouHzX+b7oqx/jKcnOuQKsv62ePKD9Sxe DTuf/lr8/CfLm0TZt5MOveNbFDnzyBycfuhIdFTwEPqy6utfxg+CLH+l3/9huX/iA3e9MtfPqGVv pGtWrVr1HSWUdLh+prMw/mNY9N3E2/DvbTM/0Xcb/vxjWMDL+u8dTDGKk/hY/3H9Y2D6PnPmzN8Z c/Yd3fuWb5q90BedR9/ZLmJ1Um+ii2mkxJs997+z7f7xl+mWItfP2Mff2Z9p8n7X2fqKvyRx9qHp KDYUz99zetdr3nAWRsavvCz/FY594G2zn3TCfPtr/p7f/xjvYthALAAALLyYRtlkYjwAAAAASUVO RK5CYII= " + x="36" + y="12" + width="12" + height="12" + id="image8739" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHVSURB VCiRY/z//z8DKYAFmSO7U9bjo8bHNMZ/jBqs31l//OH8c4/rGdeeZ9bPZsDUMMEYEqcl8j5qfOzk esm1WmGngkPu6lwT0TOik38J/PIWuyDWBlPH+P//fwbVTaqGL/VfLhA9IRr6RemL2W/e38kM/xke cb7jfMj5ivPaO513Cfw3+Kff972/kYmBgYHhvfL7CN7HvDO/y3zX+SH2o1B6n3QW2xe2Gy9NX1az fWV7y/GWY+0n5U/WcCf94fhjJHhJ8NhX6a/J3E+5J1zOunxdaaPSIo63HJ8UTikcYv7O/PU/y39+ uAaW7ywSHI847v9j/6chfE74OgMDA8Mz+2dmTL+ZLm2btO3nV+mv7pwvOE8ibOD6c+ut3VsHxl+M F19ZvArTWqpl8VbrbRPrV9a34mfFm5h+Mkml7U9bANfA/ZJ7/0fVjxEK+xWqvgt/V3xh+iJP7LzY 1N88v4X/cfzjFj4vXL3QdGE0AwMDAwM04hhFLoms5LvHt0VrsZZtaGgo2////xnMO81lZPbKpPA+ 5L0teVjS4////5BgZWBgYGBkZGSUOSgT9V7pfRz7J3ZJ5t/M/35z//7L9JvpvNhpsa7rcddvweMB G2BkZGT8j0USAHiWxx8oaaFMAAAAAElFTkSuQmCC " + x="36" + y="24" + width="12" + height="12" + id="image8782" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFeSURB VCiRlZDLK4RxFIaf8/1myi1JSRZyaSxFiWRDKEWyGkWSPwCFMdloTLkTwkbKhiTsrJSFRllZuqdE LCyUBSLzfcdiXGZYyLs5i3N730dUlf/IApDJmkQZ322X8VCPjHSmfTYl2JUtY+vzgkjMAvTPgZ0P z7m4Wqa/ziU0+LBSb5RvG65ISavgbakR5+GVeN+2bIjh0u/Baq7iaagYqmMtoW/XmIxk3JnpYN+q V22kzo99u6iB9cfoDJEPerKJqRhBwopzsSzDy4W4vaXc9bVDbUxo+aQkwW4PgAamLmRibxW93yd8 FcJd2gP6jIZm1O8/k59YZSxYhqlf4Km1iISVLeyDEJIUh8nLVF9Rm+sXaKnqhfNZOHKwkkoI73Rg pyRjytaisH7Mjg5WY8VnkdW0pAEN47wcQYEHU5ADr6dRWD9kynuxD2fUq3YExvEipnIYxME5H4A8 fmX4S+9JCYDPfaR61AAAAABJRU5ErkJggg== " + x="36" + y="36" + width="12" + height="12" + id="image8825" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADVSURB VCiRY/z//z8DCjhwgGXnly9RDAwMDO4+PksZGBj+IkszMaCBN5KSnNdu3Zpx7datac+fP2dHl8fQ QAjQXgPj+tWri1+8ehUJN4GRkeH1u3eGjAwM/4WEhC4wIAWKuLj4SpZvP37wv3z92hibaS9fvUIR 5+Xm3sV4bP9+ld+/f6vBBFlYWDiPnz27hImJ6b+psXH0v1+/fsLk2NnZbzOix8Obmzd5F2/d+pKB geFfRGSkmKSk5DeKPE2yBhZ0gR/c3H/ERUW3MzIy/v/69etfdHkAxMtOtq4bGL4AAAAASUVORK5C YII= " + x="36" + y="48" + width="12" + height="12" + id="image8868" + style="display:inline" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAXSURBVCiRY/z//z8DKYCJJNWjGkaQBgD1WAMVqVdojQAAAABJRU5ErkJggg== " + x="36" + y="60" + width="12" + height="12" + id="image4763" /> + <image + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHbSURB VCiRVZGxaxNRAId/7717d00T6Uk1pInGWqSQ4iS4NIMhoOhcsFPBTf+ADqEmJhBQHN1cIrhLEQQj aEFLoZApFCXaWtGAtl4TrpdLLrnL3XsdNBK/5bd83/QjUkr8hWbvZ6/3ae8mDdHZaT5tWGbn5YeH m1WMQaSU2KpvxVZfrD7ao5+XF1PpUHYui1R0Ac12U6xX17cTrXO3K08qvwCAAmD3nt998FHs3FE0 HtK5Dk5VGPZvzEfn6crSStqKHb8rFAoXAYBtHG9c2xdfH/eFo/rCR4AAlmthEAygMAUhPokdp36m vW+GG7XGW8Uj3q2u3w0HQkBIF7sHu7AGFlpOC+bARGIqgR7vEaq5SwDWlI7TmYMgAAMkABcumkdN mH0Th/YBwpMR7FlfsCAunwKQUSIscig9CSkAgj8RAWB3bdiuDUVlCFgAXeoDIcRZSlzymgd8CB+Q Q2B8MQR8L4DW1ZBUZo8AfKO1Sq0a9aJvgP/l8eiKcxUzEzOblNJPFADiMr6cEqm6GqhyXFYdDYvd tMyczmwDeFoqlX6S0dP5fP58o91YM4bGDY94MZ3rE8lQshOPxF8JIZ6Vy+X3/54ekcvlphhjFyil lzjnzPO8H5zz78Vi0Rg5J1kK4/V0dy9HAAAAAElFTkSuQmCC " + x="12" + y="60" + width="12" + height="12" + id="image5815" /> + <g + transform="translate(20,5e-7)" + id="g4788" + style="display:inline"> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9638661,0.1841601,-0.1841601,0.963866,-17.87869,58.359647)" + id="path3043" + style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" /> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9814535,0,0,0.9814535,-20.058185,58.569471)" + id="path3045" + style="fill:url(#linearGradient5669);fill-opacity:1;stroke:#003500;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z" + transform="matrix(0.5121728,0.8599464,-0.8599464,0.5121728,-11.048722,57.000296)" + id="path3047" + style="fill:none;stroke:url(#linearGradient5671);stroke-width:0.10042476;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" /> + <path + d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z" + transform="matrix(0.9498646,0.5484046,-0.683379,1.1836471,-16.039471,55.616584)" + id="path3049" + style="fill:url(#linearGradient6033);fill-opacity:1;stroke:none;filter:url(#filter3290)" /> + </g> + <g + transform="matrix(0.9705075,0,0,0.9705075,-0.146091,-0.1561012)" + id="g4816" + style="display:inline"> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.38882,-1.5621111)" + id="path4818" + style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" /> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)" + id="path4820" + style="fill:url(#linearGradient5663);fill-opacity:1;stroke:#7c0063;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z" + transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)" + id="path4822" + style="fill:none;stroke:url(#linearGradient5665);stroke-width:0.24374942;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" /> + <path + d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z" + transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)" + id="path4824" + style="fill:url(#linearGradient5667);fill-opacity:1;stroke:none;filter:url(#filter3290)" /> + </g> + <g + transform="matrix(0.9758889,0,0,0.9758889,-0.209913,11.88922)" + id="g4826" + style="display:inline"> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.3888195,-1.5621111)" + id="path4828" + style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" /> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)" + id="path4830" + style="fill:url(#linearGradient5657);fill-opacity:1;stroke:#de5500;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z" + transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)" + id="path4832" + style="fill:none;stroke:url(#linearGradient5659);stroke-width:0.10068177;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" /> + <path + d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z" + transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)" + id="path4834" + style="fill:url(#linearGradient5661);fill-opacity:1;stroke:none;filter:url(#filter3290)" /> + </g> + <g + transform="matrix(0.9758888,0,0,0.9758888,-0.209912,23.998424)" + id="g4836" + style="display:inline"> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.3888195,-1.5621111)" + id="path4838" + style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" /> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)" + id="path4840" + style="fill:url(#linearGradient5651);fill-opacity:1;stroke:#008000;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z" + transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.387535,-2.9550479)" + id="path4842" + style="fill:none;stroke:url(#linearGradient5653);stroke-width:0.10068177;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" /> + <path + d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z" + transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)" + id="path4844" + style="fill:url(#linearGradient5655);fill-opacity:1;stroke:none;filter:url(#filter3290)" /> + </g> + <g + transform="matrix(0.9758888,0,0,0.9758888,-0.209913,35.930851)" + id="g4846" + style="display:inline"> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.38882,-1.5621111)" + id="path4848" + style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" /> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)" + id="path4850" + style="fill:url(#linearGradient5645);fill-opacity:1;stroke:#000080;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z" + transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)" + id="path4852" + style="fill:none;stroke:url(#linearGradient5647);stroke-width:0.10042476;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" /> + <path + d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z" + transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)" + id="path4854" + style="fill:url(#linearGradient5649);fill-opacity:1;stroke:none;filter:url(#filter3290)" /> + </g> + <g + transform="matrix(0.9758889,0,0,0.9758889,-0.209913,47.863277)" + id="g4856" + style="display:inline"> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.3888195,-1.5621111)" + id="path4858" + style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" /> + <path + d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z" + transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)" + id="path4860" + style="fill:url(#linearGradient5639);fill-opacity:1;stroke:#333333;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + <path + d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z" + transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)" + id="path4862" + style="fill:none;stroke:url(#linearGradient5641);stroke-width:0.10068177;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" /> + <path + d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z" + transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)" + id="path4864" + style="fill:url(#linearGradient5643);fill-opacity:1;stroke:none;filter:url(#filter3290)" /> + </g> + </g> +</svg> diff --git a/comm/suite/chatzilla/xul/skin/images/spbubble-off.png b/comm/suite/chatzilla/xul/skin/images/spbubble-off.png Binary files differnew file mode 100644 index 0000000000..d4c3fe2b0e --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/spbubble-off.png diff --git a/comm/suite/chatzilla/xul/skin/images/spbubble-on.png b/comm/suite/chatzilla/xul/skin/images/spbubble-on.png Binary files differnew file mode 100644 index 0000000000..f23d76ae0a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/spbubble-on.png diff --git a/comm/suite/chatzilla/xul/skin/images/voice-graphic.png b/comm/suite/chatzilla/xul/skin/images/voice-graphic.png Binary files differnew file mode 100644 index 0000000000..77ee61dbad --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/voice-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/voice-symbol.png b/comm/suite/chatzilla/xul/skin/images/voice-symbol.png Binary files differnew file mode 100644 index 0000000000..e1eb3a2481 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/voice-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/install-plugin.css b/comm/suite/chatzilla/xul/skin/install-plugin.css new file mode 100644 index 0000000000..d2fa232d97 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/install-plugin.css @@ -0,0 +1,9 @@ +/* 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/. */ + +window { + width: 35em; + padding: 0.5em; +} + diff --git a/comm/suite/chatzilla/xul/skin/networks-edit.css b/comm/suite/chatzilla/xul/skin/networks-edit.css new file mode 100644 index 0000000000..e60efe2ab4 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/networks-edit.css @@ -0,0 +1,7 @@ +/* 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/. */ + +#backgroundBox { + background-color: ThreeDLightShadow; +} diff --git a/comm/suite/chatzilla/xul/skin/output-dark.css b/comm/suite/chatzilla/xul/skin/output-dark.css new file mode 100644 index 0000000000..39a4521e8c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/output-dark.css @@ -0,0 +1,226 @@ +/* -*- Mode: Text; 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/. */ + +/* + * a dark background/light text version of the output window. + * see output-base.css for details. + */ + +@import url(chrome://chatzilla/content/output-base.css); + +.chatzilla-body { /* The topmost container in the ChatZilla */ + background: black; /* output window. */ + color: lightgrey; +} + +a.chatzilla-link { + color: #fffdd6; +} + +a.chatzilla-link:visited { + color: lightgrey; +} + +.chatzilla-line-marker { + box-shadow: 0px 2px darkgreen !important; +} + +.header-outer { + background-color: black; +} + +.header { + color: lightslategrey; + background-color: #333333; + border-radius: 7px; +} + +.progress-fg { + background-color: silver; +} + +.value { + color: silver; +} + +/* Style userlist as white-on-black. */ +treechildren { + background: black; + color: white; +} + +/* For Mac, who's theme sucks. */ +treechildren::-moz-tree-row(unselected), +treechildren::-moz-tree-cell(unselected), +treechildren::-moz-tree-cell-text(unselected) { + border-color: black !important; + background: black !important; + color: white !important; +} + +#splash { + color: #444444; +} + +#usr-descnodes, +#ch-topicnodes { + color: white; +} + +[condition] { + font-weight: bold; +} + +[condition="red"] { + color: red; +} + +[condition="yellow"] { + color: yellow; +} + +[condition="green"] { + color: lightgreen; +} + +.msg[msg-type="JOIN"] .msg-data a.chatzilla-link, +.msg[msg-type="PART"] .msg-data a.chatzilla-link { + color: lightcyan; +} + +.msg[msg-type="KICK"] .msg-data a.chatzilla-link { + color: #ff5700; +} + +.chatzilla-rheet { + color: magenta !important; +} + +.chatzilla-highlight[name="Slate"] { + color: white; + background: #15272d; +} + +.chatzilla-highlight[name="Plum"] { + color: white; + background: #442144; +} + +.chatzilla-highlight[name="Brown"] { + color: white; + background: #562a14; +} + +.msg-type { /* .msg-type = message type */ + color: silver; /* indicator */ +} + +.msg-user a.chatzilla-link, +.msg-user { /* msg-user = nickname portion of */ + color: white !important; /* a message (channel and query */ +} /* views) */ + +.msg[mark="even"] .msg-data { /* use even/odd marks to create a */ + color: white; /* subtle brightness change when */ +} /* the speaker changes. */ + +.msg[msg-type="JOIN"] .msg-data, +.msg[msg-type="PART"] .msg-data { + color: lightblue; +} + +.msg[msg-type="PART"] .msg-data { + color: lightblue; +} + +.msg[msg-type="HELLO"] .msg-data { + color: yellow; +} + +.msg[msg-type="ERROR"] .msg-data, +.msg[msg-type="DISCONNECT"] .msg-data { + background: red; + color: white; +} + +.msg[msg-type="USAGE"] .msg-data { + color: white; +} + +.msg[msg-type="ACTION"] .msg-data { + color: #6ac9ee; +} + +.msg[msg-type="NICK"] .msg-data { + color: #96fa94; +} + +.msg[msg-type="NOTICE"] .msg-data, +.msg[msg-type="MODE"] .msg-data { + color: #60e066; +} + +.msg[msg-type="NOTICE"] .msg-data a.chatzilla-link, +.msg[msg-type="MODE"] .msg-data a.chatzilla-link { + color: #6dff74; +} + +.msg[msg-type="KICK"] .msg-data { + color: #d85d24; +} + +.msg[msg-type="QUIT"] .msg-data { + color: #f7b183; +} + +/* important="true" means that the message has text from your /stalk list in + * it, has your nickname in it, or was spoken by someone in your /stalk list. + */ +.msg[important="true"] .msg-user, +.msg[important="true"] .msg-data { + background: #333333 !important; +} + +.msg-user:before, +.msg-user:after { + color: blue; +} + +.msg[msg-user$="ME!"] .msg-user:before, +.msg[msg-user$="ME!"] .msg-user:after { + color: #6afc73; +} + +.msg[msg-type="ACTION"] .msg-user:before, +.msg[msg-type="ACTION"] .msg-user:after { + color: cyan; +} + +.msg[msg-type="NOTICE"] .msg-user:before, +.msg[msg-type="NOTICE"] .msg-user:after { + color: #6afc73; +} + +/* private messages *not* in a query window */ +.msg[dest-type="IRCUser"] .msg-user:before, +.msg[dest-type="IRCUser"] .msg-user:after { + color: #6afc73; +} + +.msg[msg-dest$="ME!"] .msg-user:before, +.msg[msg-dest$="ME!"] .msg-user:after { + color: magenta; +} + +/* private messages in a query window */ +.msg[view-type="IRCUser"] .msg-user:before, +.msg[view-type="IRCUser"] .msg-user:after { + color: white; +} + +.msg[view-type="IRCUser"][msg-user$="ME!"] .msg-user:before, +.msg[view-type="IRCUser"][msg-user$="ME!"] .msg-user:after { + color: #6afc73; +} diff --git a/comm/suite/chatzilla/xul/skin/output-default.css b/comm/suite/chatzilla/xul/skin/output-default.css new file mode 100644 index 0000000000..af5c850a12 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/output-default.css @@ -0,0 +1,67 @@ +/* -*- Mode: Text; 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/. */ + +/* + * a version of the output window that uses only your default foreground and + * backgrond colors. Message types are distinguished by font changes. + * see output-base.css for details. + */ + +@import url(chrome://chatzilla/content/output-base.css); + +.header-outer { + background-color: white; +} + +.progress-fg { + background-color: grey; +} + +.msg[msg-type="ACTION"] .msg-data { + font-style: italic; +} + +.msg[msg-type="JOIN"] .msg-type, +.msg[msg-type="PART"] .msg-type, +.msg[msg-type="QUIT"] .msg-type { + font-weight: bold; +} + +.msg[msg-type="QUIT"] .msg-data { + font-variant: small-caps; + font-weight: bold; +} + +.msg[msg-type="JOIN"] .msg-data, +.msg[msg-type="PART"] .msg-data { + font-variant: small-caps; +} + +.msg[msg-type="HELLO"] .msg-data, +.msg[msg-type="NICK"] .msg-type, +.msg[msg-type="NOTICE"] .msg-data { + font-weight: bold; +} + +.msg[msg-type="NICK"] .msg-data { + font-family: monospace; +} + +/* :before and :after pseudoclasses form the decorations around nicknames. */ +.msg-user:before, +.msg-user:after { + font-size: 100%; + font-family: monospace; + font-weight: bolder; +} + +.msg[dest-type="IRCUser"] .msg-user, +.msg[dest-type="IRCUser"][msg-dest$="ME!"] .msg-user { + font-style: italic; +} + +.msg[msg-user$="ME!"] .msg-user { + font-weight: bold; +} diff --git a/comm/suite/chatzilla/xul/skin/output-light.css b/comm/suite/chatzilla/xul/skin/output-light.css new file mode 100644 index 0000000000..c2a346333a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/output-light.css @@ -0,0 +1,217 @@ +/* -*- Mode: Text; 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/. */ + +/* + * a light background/dark text version of the output window. + * see output-base.css for details. + */ + +@import url(chrome://chatzilla/content/output-base.css); + +.chatzilla-body { /* The topmost container in the ChatZilla */ + background: white; /* output window. */ + color: #222222; +} + +a.chatzilla-link { + font-weight: bold; + color: #342ecc; +} + +.chatzilla-line-marker { + box-shadow: 0px 2px lightgray !important; +} + +.header-outer { + background-color: #d1d0ea; +} + +.header { + color: darkslategrey; + background-color: #EEEEEE; + border: 1px #777499 solid; + border-radius: 7px; +} + +#splash { + color: #DDDDDD; +} + +#usr-descnodes, +#ch-topicnodes { + color: black; +} + +.progress-fg { + background-color: darkslategrey; +} + +[condition] { + font-weight: bold; +} + +[condition="red"] { + color: red; +} + +[condition="yellow"] { + color: orange; +} + +[condition="green"] { + color: #2ec908; +} + +.msg[msg-type="PRIVMSG"] .msg-data, +.msg[msg-type="ACTION"] .msg-data { + background-color: #F0F0F0; +} + +.msg[msg-type="HELLO"] .msg-data a.chatzilla-link { + color: #d7d9dd; +} + +.msg[msg-type="JOIN"] .msg-data a.chatzilla-link, +.msg[msg-type="PART"] .msg-data a.chatzilla-link { + font-weight: bold; + color: #11c411; +} + +.msg[msg-type="ERROR"] .msg-data a.chatzilla-link, +.msg[msg-type="DISCONNECT"] .msg-data a.chatzilla-link { + font-weight: bold; + color: white; +} + +.msg[msg-type="KICK"] .msg-data a.chatzilla-link { + color: #aa0d08; +} + +.msg[msg-type="NOTICE"] .msg-data a.chatzilla-link { + color: #d64444; +} + +.msg[msg-type="QUIT"] .msg-data a.chatzilla-link { + color: #c46907; +} + +.chatzilla-rheet { + color: #e25e00 !important; +} + +.chatzilla-highlight[name="Slate"] { + color: black; + background: #b8c4e0; +} + +.chatzilla-highlight[name="Plum"] { + color: black; + background: #ddb8d0; +} + +.chatzilla-highlight[name="Brown"] { + color: black; + background: #ffbf77; +} + +.msg-type { /* .msg-type = message type */ + color: #686699; /* indicator */ + font-weight: bold; +} + +.msg-user a.chatzilla-link, +.msg-user { /* msg-user = nickname portion of */ + color: black !important; /* a message (channel and query */ + font-weight: bold; /* views) */ +} + +.msg[mark="even"] .msg-data { /* use even/odd marks to create a */ + color: #555555; /* subtle brightness change when */ +} /* the speaker changes. */ + +.msg[msg-type="JOIN"] .msg-data, +.msg[msg-type="PART"] .msg-data { + color: #0e9e0e; + background-color: #c3f7c3; + font-weight: bold; + border-radius: 5px 5px 5px 5px; +} + +.msg[msg-type="QUIT"] .msg-data { + background: #fff196; + color: #ff8d02; + font-weight: bold; + border-radius: 5px 5px 5px 5px; +} + +.msg[msg-type="HELLO"] .msg-data { + background: #1342a5; + color: white; + border-radius: 5px 5px 5px 5px; + font-weight: bold; +} + +.msg[msg-type="ERROR"] .msg-data, +.msg[msg-type="DISCONNECT"] .msg-data { + border-radius: 5px 5px 5px 5px; + background: #a8221e; + color: white; +} + +.msg[msg-type="USAGE"] .msg-data { + color: black; +} + +.msg[msg-type="ACTION"] .msg-data { + color: black; + font-style: italic; +} + +.msg[msg-type="NICK"] .msg-data { + color: #4e8387; + background-color: #d5e9ea; + font-weight: bold; +} + +.msg[msg-type="NOTICE"] .msg-data { + color: #ae4141; + font-weight: bold; +} + +.msg[msg-type="MODE"] .msg-data { + color: #2709ed; + font-weight: bold; +} + +.msg[msg-type="KICK"] .msg-data { + color: #ff1a0a; + background: #ffdbcc; + font-weight: bold; + border-radius: 5px 5px 5px 5px; +} + +/* important="true" means that the message has text from your /stalk list in + * it, has your nickname in it, or was spoken by someone in your /stalk list. + */ +.msg[important="true"] .msg-user { + background: #d4d8d4; + border-radius: 5px 0px 0px 5px; +} + +.msg[important="true"] .msg-data { + background: #eaefeb; +} + + +/* :before and :after pseudoclasses form the decorations around nicknames. */ +.msg-user:before, +.msg-user:after { + color: #777499; +} + +.msg[msg-user$="ME!"] .msg-user:before, /* the decoration around MY */ +.msg[msg-user$="ME!"] .msg-user:after { /* nick */ + color: #843c6c; +} diff --git a/comm/suite/chatzilla/xul/skin/output-loud.css b/comm/suite/chatzilla/xul/skin/output-loud.css new file mode 100644 index 0000000000..3bf7a94416 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/output-loud.css @@ -0,0 +1,202 @@ +/* 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/. */ + +body { + + margin: 0px 0px 0px 0px; + background: black; + +} + +.bold { + + font-weight: bold; + +} + +.italic { + + font-style: italic; + +} + +.underline { + + text-decoration: underline; + +} + +.strikethrough { + + text-decoration: line-through; + +} + +.teletype { + + font-family: monospace; + +} + +.smallcap { + + font-variant: small-caps; + +} + +.rheet { + + font-size: 14pt; + font-weight: bold; + color: magenta; + +} + +/* output from a chat session (contains msgs) */ +.chat-view { + + vertical-align: text-bottom; + +} + +/* common container for all portions of a message + * (contains msg-*s) */ +.msg { + + font-family: sans-serif; + +} + +.msg[user="!ME"] { + + background: lightgrey; + +} + +/* message data in output window */ +.msg-data { + + font-weight: bold; + color: lightgrey; + background: #1a2a44; + +} + +/* message data in output window */ + +.msg-data[user="!ME"]{ + + background: black; + +} + +.msg-data[msgtype="JOIN"], +.msg-data[msgtype="PART"] { + + width: 100%; + font-variant: small-caps; + background: lightgray; + color: black; + +} + +.msg-data[msgtype="HELLO"] { + + background: white; + color: darkgreen; + +} + +.msg-data[msgtype="ERROR"], +.msg-data[msgtype="DISCONNECT"] { + + background: red; + color: white; + +} + +.msg-data[msgtype="USAGE"] { + + font-style: italic; + color: white; + +} + +.msg-data[msgtype="HELP"] { + + font-weight: normal; + +} + +.msg-data[msgtype="ACTION"] { + + color: cyan; + +} + +.msg-data[msgtype="NOTICE"] { + + color: yellow; + +} + +.msg-data[msgtype="KICK"] { + + background: orange; + color: yellow; + +} + +.msg-data[msgtype="QUIT"] { + + background: lightgrey; + color: brown; + +} + +/* nickname field in output */ +.msg-user { + + text-align: center; + vertical-align: middle; + color: lightgrey; + font-weight: bold; + background: grey; + +} + +.msg-user[parity="odd"]{ + + background: black; + +} + +.msg-user[user="!ME"] { + + color : white; + +} + +.msg-user[msgtype="ACTION"] { + + font-style: italic; + +} + +/* Message type indicator in output window */ +.msg-type { + + text-align: center; + vertical-align: middle; + color: brown; + font-weight: bold; + background: lightgrey; + +} + +.msg-type[user="!ME"] { + + background: silver; + +} diff --git a/comm/suite/chatzilla/xul/skin/output-marble.css b/comm/suite/chatzilla/xul/skin/output-marble.css new file mode 100644 index 0000000000..0de0dc1409 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/output-marble.css @@ -0,0 +1,148 @@ +/* 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/. */ + +body { + + margin: 0px 0px 0px 0px; + background: url(chrome://chatzilla/skin/images/xtal.jpg); + +} + +.bold { + + font-weight: bold; + +} + +.italic { + + font-style: italic; + +} + +.underline { + + text-decoration: underline; + +} + +.strikethrough { + + text-decoration: line-through; + +} + +.teletype { + + font-family: monospace; + +} + +.smallcap { + + font-variant: small-caps; + +} + +.rheet { + + font-size: 14pt; + font-weight: bold; + color: magenta; + +} + +/* output from a chat session (contains msgs) */ +.chat-view { + + vertical-align: text-bottom; + +} + +/* common container for all portions of a message + * (contains msg-*s) */ +.msg { + + font-family: sans-serif; + +} + +.msg[user="!ME"] { + + background: lightgrey; + +} + +/* message data in output window */ +.msg-data { + + font-weight: bold; + +} + +.msg-data[msgtype="JOIN"], +.msg-data[msgtype="PART"] { + + font-variant: small-caps; + color: darkslategrey; + +} + +.msg-data[msgtype="ACTION"] { + + color: darkred; + +} + +.msg-data[msgtype="NOTICE"] { + + color: green; + +} + +.msg-data[msgtype="KICK"] { + + color: slategrey; + +} + +.msg-data[msgtype="QUIT"] { + + color: brown; + +} + +/* nickname field in output */ +.msg-user { + + text-align: center; + vertical-align: middle; + color: blue; + font-weight: bold; + background: grey; + +} + +.msg-user[user="!ME"] { + + color: green; + +} + +.msg-user[msgtype="ACTION"] { + + font-style: italic; + +} + +/* Message type indicator in output window */ +.msg-type { + + text-align: center; + vertical-align: middle; + color: brown; + font-weight: bold; + background: lightgrey; + +} |