From 251f94aa7abe89fa29b747cdf76fc8bf3eaa2807 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 7 Dec 2010 02:34:59 +0000 Subject: [PATCH] Evennia now runs on its own Twisted webserver (no need for testserver or Apache if you don't want to). Evennia now also has an ajax long-polling web client running from Twisted. The web client requires no extra dependencies beyond jQuery which is included. The src/server structure has been r cleaned up and rewritten to make it easier to add new protocols in the future - all new protocols need to inherit from server.session.Session, whi ch implements a set of hooks that Evennia uses to communicate. The current web client protocol is functional but does not implement any of rcaskey 's suggestions as of yet - it uses a separate data object passed through msg() to communicate between the server and the various protocols. Also the client itself could probably need cleanup and 'prettification'. The fact that the system runs a hybrid of Django and Twisted, getting the best of both worlds should allow for many possibilities in the future. /Griatch --- game/evennia.py | 12 + src/commands/default/general.py | 2 +- src/commands/default/tests.py | 15 +- src/commands/default/unloggedin.py | 31 +- src/comms/managers.py | 2 +- src/comms/models.py | 7 +- src/config/manager.py | 8 +- src/help/models.py | 1 - src/objects/models.py | 39 +- src/players/models.py | 24 +- src/players/player.py | 2 +- src/scripts/models.py | 3 +- src/scripts/scripts.py | 2 +- src/server/initial_setup.py | 29 +- src/server/server.py | 276 +- src/server/session.py | 482 +- src/server/sessionhandler.py | 293 +- src/server/telnet.py | 153 + src/server/webclient.py | 286 + src/server/webserver.py | 60 + src/settings_default.py | 42 +- src/utils/ansi.py | 14 +- src/utils/ansi2html.py | 137 + src/utils/create.py | 18 +- src/utils/utils.py | 18 +- src/web/media/admin/__init__.py | 0 src/web/media/admin/css/base.css | 1 + src/web/media/admin/css/changelists.css | 1 + src/web/media/admin/css/dashboard.css | 1 + src/web/media/admin/css/forms.css | 1 + src/web/media/admin/css/ie.css | 1 + src/web/media/admin/css/login.css | 1 + src/web/media/admin/css/rtl.css | 1 + src/web/media/admin/css/widgets.css | 1 + src/web/media/admin/img/admin/arrow-down.gif | 1 + src/web/media/admin/img/admin/arrow-up.gif | 1 + .../media/admin/img/admin/changelist-bg.gif | 1 + .../admin/img/admin/changelist-bg_rtl.gif | 1 + src/web/media/admin/img/admin/chooser-bg.gif | 1 + .../admin/img/admin/chooser_stacked-bg.gif | 1 + .../admin/img/admin/default-bg-reverse.gif | 1 + src/web/media/admin/img/admin/default-bg.gif | 1 + .../media/admin/img/admin/deleted-overlay.gif | 1 + src/web/media/admin/img/admin/icon-no.gif | 1 + .../media/admin/img/admin/icon-unknown.gif | 1 + src/web/media/admin/img/admin/icon-yes.gif | 1 + .../media/admin/img/admin/icon_addlink.gif | 1 + src/web/media/admin/img/admin/icon_alert.gif | 1 + .../media/admin/img/admin/icon_calendar.gif | 1 + .../media/admin/img/admin/icon_changelink.gif | 1 + src/web/media/admin/img/admin/icon_clock.gif | 1 + .../media/admin/img/admin/icon_deletelink.gif | 1 + src/web/media/admin/img/admin/icon_error.gif | 1 + .../media/admin/img/admin/icon_searchbox.png | 1 + .../media/admin/img/admin/icon_success.gif | 1 + .../admin/img/admin/inline-delete-8bit.png | 1 + .../media/admin/img/admin/inline-delete.png | 1 + .../admin/img/admin/inline-restore-8bit.png | 1 + .../media/admin/img/admin/inline-restore.png | 1 + .../admin/img/admin/inline-splitter-bg.gif | 1 + .../media/admin/img/admin/nav-bg-grabber.gif | 1 + .../media/admin/img/admin/nav-bg-reverse.gif | 1 + src/web/media/admin/img/admin/nav-bg.gif | 1 + .../media/admin/img/admin/selector-add.gif | 1 + .../media/admin/img/admin/selector-addall.gif | 1 + .../media/admin/img/admin/selector-remove.gif | 1 + .../admin/img/admin/selector-removeall.gif | 1 + .../media/admin/img/admin/selector-search.gif | 1 + .../admin/img/admin/selector_stacked-add.gif | 1 + .../img/admin/selector_stacked-remove.gif | 1 + src/web/media/admin/img/admin/tool-left.gif | 1 + .../media/admin/img/admin/tool-left_over.gif | 1 + src/web/media/admin/img/admin/tool-right.gif | 1 + .../media/admin/img/admin/tool-right_over.gif | 1 + src/web/media/admin/img/admin/tooltag-add.gif | 1 + .../admin/img/admin/tooltag-add_over.gif | 1 + .../admin/img/admin/tooltag-arrowright.gif | 1 + .../img/admin/tooltag-arrowright_over.gif | 1 + .../media/admin/img/gis/move_vertex_off.png | 1 + .../media/admin/img/gis/move_vertex_on.png | 1 + src/web/media/admin/js/LICENSE-JQUERY.txt | 1 + src/web/media/admin/js/SelectBox.js | 1 + src/web/media/admin/js/SelectFilter2.js | 1 + src/web/media/admin/js/__init__.py | 0 src/web/media/admin/js/actions.js | 1 + src/web/media/admin/js/actions.min.js | 1 + .../media/admin/js/admin/DateTimeShortcuts.js | 1 + .../admin/js/admin/RelatedObjectLookups.js | 1 + src/web/media/admin/js/admin/ordering.js | 1 + src/web/media/admin/js/calendar.js | 1 + src/web/media/admin/js/collapse.js | 1 + src/web/media/admin/js/collapse.min.js | 1 + src/web/media/admin/js/compress.py | 1 + src/web/media/admin/js/core.js | 1 + src/web/media/admin/js/dateparse.js | 1 + .../media/admin/js/getElementsBySelector.js | 1 + src/web/media/admin/js/inlines.js | 1 + src/web/media/admin/js/inlines.min.js | 1 + src/web/media/admin/js/jquery.init.js | 1 + src/web/media/admin/js/prepopulate.js | 1 + src/web/media/admin/js/prepopulate.min.js | 1 + src/web/media/admin/js/timeparse.js | 1 + src/web/media/admin/js/urlify.js | 1 + src/web/media/css/webclient.css | 79 + src/web/media/javascript/evennia_webclient.js | 212 + src/web/media/javascript/jquery-1.4.4.js | 7179 +++++++++++++++++ src/web/templates/prosimii/404.html | 11 + src/web/templates/prosimii/500.html | 10 + src/web/templates/prosimii/base.html | 6 +- src/web/templates/prosimii/index.html | 12 +- src/web/templates/prosimii/webclient.html | 37 + src/web/urls.py | 6 +- src/web/utils/general_context.py | 3 +- src/web/webclient/__init__.py | 0 src/web/webclient/models.py | 7 + src/web/webclient/urls.py | 10 + src/web/webclient/views.py | 23 + src/web/website/views.py | 15 +- 118 files changed, 9049 insertions(+), 593 deletions(-) create mode 100644 src/server/telnet.py create mode 100644 src/server/webclient.py create mode 100644 src/server/webserver.py create mode 100644 src/utils/ansi2html.py create mode 100644 src/web/media/admin/__init__.py create mode 120000 src/web/media/admin/css/base.css create mode 120000 src/web/media/admin/css/changelists.css create mode 120000 src/web/media/admin/css/dashboard.css create mode 120000 src/web/media/admin/css/forms.css create mode 120000 src/web/media/admin/css/ie.css create mode 120000 src/web/media/admin/css/login.css create mode 120000 src/web/media/admin/css/rtl.css create mode 120000 src/web/media/admin/css/widgets.css create mode 120000 src/web/media/admin/img/admin/arrow-down.gif create mode 120000 src/web/media/admin/img/admin/arrow-up.gif create mode 120000 src/web/media/admin/img/admin/changelist-bg.gif create mode 120000 src/web/media/admin/img/admin/changelist-bg_rtl.gif create mode 120000 src/web/media/admin/img/admin/chooser-bg.gif create mode 120000 src/web/media/admin/img/admin/chooser_stacked-bg.gif create mode 120000 src/web/media/admin/img/admin/default-bg-reverse.gif create mode 120000 src/web/media/admin/img/admin/default-bg.gif create mode 120000 src/web/media/admin/img/admin/deleted-overlay.gif create mode 120000 src/web/media/admin/img/admin/icon-no.gif create mode 120000 src/web/media/admin/img/admin/icon-unknown.gif create mode 120000 src/web/media/admin/img/admin/icon-yes.gif create mode 120000 src/web/media/admin/img/admin/icon_addlink.gif create mode 120000 src/web/media/admin/img/admin/icon_alert.gif create mode 120000 src/web/media/admin/img/admin/icon_calendar.gif create mode 120000 src/web/media/admin/img/admin/icon_changelink.gif create mode 120000 src/web/media/admin/img/admin/icon_clock.gif create mode 120000 src/web/media/admin/img/admin/icon_deletelink.gif create mode 120000 src/web/media/admin/img/admin/icon_error.gif create mode 120000 src/web/media/admin/img/admin/icon_searchbox.png create mode 120000 src/web/media/admin/img/admin/icon_success.gif create mode 120000 src/web/media/admin/img/admin/inline-delete-8bit.png create mode 120000 src/web/media/admin/img/admin/inline-delete.png create mode 120000 src/web/media/admin/img/admin/inline-restore-8bit.png create mode 120000 src/web/media/admin/img/admin/inline-restore.png create mode 120000 src/web/media/admin/img/admin/inline-splitter-bg.gif create mode 120000 src/web/media/admin/img/admin/nav-bg-grabber.gif create mode 120000 src/web/media/admin/img/admin/nav-bg-reverse.gif create mode 120000 src/web/media/admin/img/admin/nav-bg.gif create mode 120000 src/web/media/admin/img/admin/selector-add.gif create mode 120000 src/web/media/admin/img/admin/selector-addall.gif create mode 120000 src/web/media/admin/img/admin/selector-remove.gif create mode 120000 src/web/media/admin/img/admin/selector-removeall.gif create mode 120000 src/web/media/admin/img/admin/selector-search.gif create mode 120000 src/web/media/admin/img/admin/selector_stacked-add.gif create mode 120000 src/web/media/admin/img/admin/selector_stacked-remove.gif create mode 120000 src/web/media/admin/img/admin/tool-left.gif create mode 120000 src/web/media/admin/img/admin/tool-left_over.gif create mode 120000 src/web/media/admin/img/admin/tool-right.gif create mode 120000 src/web/media/admin/img/admin/tool-right_over.gif create mode 120000 src/web/media/admin/img/admin/tooltag-add.gif create mode 120000 src/web/media/admin/img/admin/tooltag-add_over.gif create mode 120000 src/web/media/admin/img/admin/tooltag-arrowright.gif create mode 120000 src/web/media/admin/img/admin/tooltag-arrowright_over.gif create mode 120000 src/web/media/admin/img/gis/move_vertex_off.png create mode 120000 src/web/media/admin/img/gis/move_vertex_on.png create mode 120000 src/web/media/admin/js/LICENSE-JQUERY.txt create mode 120000 src/web/media/admin/js/SelectBox.js create mode 120000 src/web/media/admin/js/SelectFilter2.js create mode 100644 src/web/media/admin/js/__init__.py create mode 120000 src/web/media/admin/js/actions.js create mode 120000 src/web/media/admin/js/actions.min.js create mode 120000 src/web/media/admin/js/admin/DateTimeShortcuts.js create mode 120000 src/web/media/admin/js/admin/RelatedObjectLookups.js create mode 120000 src/web/media/admin/js/admin/ordering.js create mode 120000 src/web/media/admin/js/calendar.js create mode 120000 src/web/media/admin/js/collapse.js create mode 120000 src/web/media/admin/js/collapse.min.js create mode 120000 src/web/media/admin/js/compress.py create mode 120000 src/web/media/admin/js/core.js create mode 120000 src/web/media/admin/js/dateparse.js create mode 120000 src/web/media/admin/js/getElementsBySelector.js create mode 120000 src/web/media/admin/js/inlines.js create mode 120000 src/web/media/admin/js/inlines.min.js create mode 120000 src/web/media/admin/js/jquery.init.js create mode 120000 src/web/media/admin/js/prepopulate.js create mode 120000 src/web/media/admin/js/prepopulate.min.js create mode 120000 src/web/media/admin/js/timeparse.js create mode 120000 src/web/media/admin/js/urlify.js create mode 100644 src/web/media/css/webclient.css create mode 100644 src/web/media/javascript/evennia_webclient.js create mode 100644 src/web/media/javascript/jquery-1.4.4.js create mode 100644 src/web/templates/prosimii/404.html create mode 100644 src/web/templates/prosimii/500.html create mode 100644 src/web/templates/prosimii/webclient.html create mode 100644 src/web/webclient/__init__.py create mode 100644 src/web/webclient/models.py create mode 100644 src/web/webclient/urls.py create mode 100644 src/web/webclient/views.py diff --git a/game/evennia.py b/game/evennia.py index 775180b505..727795f49d 100755 --- a/game/evennia.py +++ b/game/evennia.py @@ -107,6 +107,15 @@ def cycle_logfile(): os.remove(logfile_old) os.rename(logfile, logfile_old) + logfile = settings.HTTP_LOG_FILE.strip() + logfile_old = logfile + '.old' + if os.path.exists(logfile): + # Cycle the old logfiles to *.old + if os.path.exists(logfile_old): + # E.g. Windows don't support rename-replace + os.remove(logfile_old) + os.rename(logfile, logfile_old) + def start_daemon(parser, options, args): """ Start the server in daemon mode. This means that all logging output will @@ -136,6 +145,9 @@ def start_interactive(parser, options, args): print '\nStarting Evennia server in interactive mode (stop with keyboard interrupt) ...' print 'Logging to: Standard output.' + # we cycle logfiles (this will at most put all files to *.old) + # to handle html request logging files. + cycle_logfile() try: call([TWISTED_BINARY, '-n', diff --git a/src/commands/default/general.py b/src/commands/default/general.py index 712380d2ce..9d9a89e649 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -318,7 +318,7 @@ class CmdQuit(MuxCommand): sessions = self.caller.sessions for session in sessions: session.msg("Quitting. Hope to see you soon again.") - session.handle_close() + session.at_disconnect() class CmdWho(MuxCommand): """ diff --git a/src/commands/default/tests.py b/src/commands/default/tests.py index ac48067876..12f60e05d1 100644 --- a/src/commands/default/tests.py +++ b/src/commands/default/tests.py @@ -29,27 +29,20 @@ from src.server import session, sessionhandler # print all feedback from test commands (can become very verbose!) VERBOSE = False -class FakeSession(session.SessionProtocol): +class FakeSession(session.Session): """ A fake session that implements dummy versions of the real thing; this is needed to mimic a logged-in player. """ + protocol_key = "TestProtocol" def connectionMade(self): - self.prep_session() - sessionhandler.add_session(self) - def prep_session(self): - self.server, self.address = None, "0.0.0.0" - self.name, self.uid = None, None - self.logged_in = False - self.encoding = "utf-8" - self.cmd_last, self.cmd_last_visible, self.cmd_conn_time = time.time(), time.time(), time.time() - self.cmd_total = 0 + self.session_connect('0,0,0,0') def disconnectClient(self): pass def lineReceived(self, raw_string): pass - def msg(self, message, markup=True): + def msg(self, message, data=None): if VERBOSE: print message diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index 17adb9efb2..e0cf49d184 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -5,11 +5,13 @@ import traceback #from django.contrib.auth.models import User from django.conf import settings from django.contrib.auth.models import User +from src.server import sessionhandler from src.players.models import PlayerDB from src.objects.models import ObjectDB -from src.config.models import ConfigValue +from src.config.models import ConfigValue, ConnectScreen from src.comms.models import Channel -from src.utils import create, logger, utils + +from src.utils import create, logger, utils, ansi from src.commands.default.muxcommand import MuxCommand class CmdConnect(MuxCommand): @@ -94,7 +96,7 @@ class CmdConnect(MuxCommand): player.at_pre_login() character.at_pre_login() - session.login(player) + session.session_login(player) player.at_post_login() character.at_post_login() @@ -230,7 +232,7 @@ class CmdQuit(MuxCommand): "Simply close the connection." session = self.caller session.msg("Good bye! Disconnecting ...") - session.handle_close() + session.at_disconnect() class CmdUnconnectedLook(MuxCommand): """ @@ -243,8 +245,11 @@ class CmdUnconnectedLook(MuxCommand): def func(self): "Show the connect screen." try: - self.caller.game_connect_screen() - except Exception: + screen = ConnectScreen.objects.get_random_connect_screen() + string = ansi.parse_ansi(screen.text) + self.caller.msg(string) + except Exception, e: + self.caller.msg(e) self.caller.msg("Connect screen not found. Enter 'help' for aid.") class CmdUnconnectedHelp(MuxCommand): @@ -271,23 +276,19 @@ Commands available at this point: To login to the system, you need to do one of the following: 1) If you have no previous account, you need to use the 'create' - command followed by your desired character name (in quotes), your - e-mail address and finally a password of your choice. Like - this: + command like this: - > create "Anna the Barbarian" anna@myemail.com tuK3221mP + > create "Anna the Barbarian" anna@myemail.com c67jHL8p It's always a good idea (not only here, but everywhere on the net) to not use a regular word for your password. Make it longer than 3 characters (ideally 6 or more) and mix numbers and capitalization - into it. Now proceed to 2). + into it. 2) If you have an account already, either because you just created - one in 1) above, or you are returning, use the 'connect' command - followed by the e-mail and password you previously set. - Example: + one in 1) above or you are returning, use the 'connect' command: - > connect anna@myemail.com tuK3221mP + > connect anna@myemail.com c67jHL8p This should log you in. Run 'help' again once you're logged in to get more aid. Hope you enjoy your stay! diff --git a/src/comms/managers.py b/src/comms/managers.py index 995ce088e9..fdb04f78c1 100644 --- a/src/comms/managers.py +++ b/src/comms/managers.py @@ -3,7 +3,6 @@ These managers handles the """ from django.db import models -from src.players.models import PlayerDB from django.contrib.contenttypes.models import ContentType from src.utils.utils import is_iter @@ -21,6 +20,7 @@ def to_object(inp, objtype='player'): inp - the input object/string objtype - 'player' or 'channel' """ + from src.players.models import PlayerDB if objtype == 'player': if type(inp) == PlayerDB: return inp diff --git a/src/comms/models.py b/src/comms/models.py index dafa5c06c8..18ad861491 100644 --- a/src/comms/models.py +++ b/src/comms/models.py @@ -17,8 +17,8 @@ be able to delete connections on the fly). from django.db import models from src.utils.idmapper.models import SharedMemoryModel from src.players.models import PlayerDB -from src.comms import managers from src.server import sessionhandler +from src.comms import managers from src.permissions.permissions import has_perm from src.utils.utils import is_iter from src.utils.utils import dbref as is_dbref @@ -81,7 +81,7 @@ class Msg(SharedMemoryModel): permissions - perm strings """ - + from src.players.models import PlayerDB # # Msg database model setup # @@ -509,7 +509,7 @@ class Channel(SharedMemoryModel): # send message to all connected players for conn in conns: for session in \ - sessionhandler.find_sessions_from_username(conn.player.name): + sessionhandler.SESSIONS.sessions_from_player(conn.player): session.msg(msg) return True @@ -539,6 +539,7 @@ class ChannelConnection(SharedMemoryModel): The advantage of making it like this is that one can easily break the connection just by deleting this object. """ + from src.players.models import PlayerDB # Player connected to a channel db_player = models.ForeignKey(PlayerDB) # Channel the player is connected to diff --git a/src/config/manager.py b/src/config/manager.py index e3c89a9b88..8236eed40f 100644 --- a/src/config/manager.py +++ b/src/config/manager.py @@ -26,7 +26,7 @@ class ConfigValueManager(models.Manager): new_conf.db_value = db_value new_conf.save() - def get_configvalue(self, config_key): + def get_configvalue(self, config_key, default=None): """ Retrieve a configuration value. @@ -35,7 +35,7 @@ class ConfigValueManager(models.Manager): try: return self.get(db_key__iexact=config_key).db_value except self.model.DoesNotExist: - return None + return default # a simple wrapper for consistent naming in utils.search def config_search(self, ostring): @@ -46,7 +46,7 @@ class ConfigValueManager(models.Manager): """ return self.get_configvalue(ostring) - def conf(self, db_key=None, db_value=None, delete=False): + def conf(self, db_key=None, db_value=None, delete=False, default=None): """ Wrapper to access the Config database. This will act as a get/setter, lister or deleter @@ -64,7 +64,7 @@ class ConfigValueManager(models.Manager): elif db_value != None: self.set_configvalue(db_key, db_value) else: - return self.get_configvalue(db_key) + return self.get_configvalue(db_key, default=default) class ConnectScreenManager(models.Manager): diff --git a/src/help/models.py b/src/help/models.py index 3783048453..3a92b4b450 100644 --- a/src/help/models.py +++ b/src/help/models.py @@ -32,7 +32,6 @@ class HelpEntry(SharedMemoryModel): permissions - perm strings """ - # # HelpEntry Database Model setup diff --git a/src/objects/models.py b/src/objects/models.py index 3472412965..b967d86c17 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -111,10 +111,8 @@ class ObjectDB(TypedObject): has_player - bool if an active player is currently connected contents - other objects having this object as location - """ - # # ObjectDB Database model setup # @@ -354,8 +352,6 @@ class ObjectDB(TypedObject): return ObjectDB.objects.get_contents(self) contents = property(contents_get) - - # # Nicks - custom nicknames # @@ -379,7 +375,6 @@ class ObjectDB(TypedObject): # game). # - def set_nick(self, nick, realname=None): """ Map a nick to a realname. Be careful if mapping an @@ -514,33 +509,30 @@ class ObjectDB(TypedObject): break cmdhandler.cmdhandler(self.typeclass(self), raw_string) - def msg(self, message, from_obj=None, markup=True): + def msg(self, message, from_obj=None, data=None): """ Emits something to any sessions attached to the object. message (str): The message to send from_obj (obj): object that is sending. - markup (bool): Markup. Determines if the message is parsed - for special markup, such as ansi colors. If - false, all markup will be cleaned from the - message in the session.msg() and message - passed on as raw text. + data (object): an optional data object that may or may not + be used by the protocol. """ # This is an important function that must always work. # we use a different __getattribute__ to avoid recursive loops. if object.__getattribute__(self, 'player'): - object.__getattribute__(self, 'player').msg(message, markup) + object.__getattribute__(self, 'player').msg(message, data) - def emit_to(self, message, from_obj=None): + def emit_to(self, message, from_obj=None, data=None): "Deprecated. Alias for msg" - self.msg(message, from_obj) + self.msg(message, from_obj, data) - def msg_contents(self, message, exclude=None): + def msg_contents(self, message, exclude=None, from_obj=None, data=None): """ Emits something to all objects inside an object. - exclude is a list of objects not to send to. + exclude is a list of objects not to send to. See self.msg() for more info. """ contents = self.contents if exclude: @@ -549,11 +541,11 @@ class ObjectDB(TypedObject): contents = [obj for obj in contents if (obj not in exclude and obj not in exclude)] for obj in contents: - obj.msg(message) + obj.msg(message, from_obj=from_obj, data=data) - def emit_to_contents(self, message, exclude=None): + def emit_to_contents(self, message, exclude=None, from_obj=None, data=None): "Deprecated. Alias for msg_contents" - self.msg_contents(message, exclude) + self.msg_contents(message, exclude=exclude, from_obj=from_obj, data=data) def move_to(self, destination, quiet=False, emit_to_obj=None): @@ -736,12 +728,3 @@ class ObjectDB(TypedObject): # Deferred import to avoid circular import errors. from src.commands import cmdhandler - - -# from src.typeclasses import idmap -# class CachedObj(models.Model): -# key = models.CharField(max_length=255, null=True, blank=True) -# test = models.BooleanField(default=False) -# objects = idmap.CachingManager() -# def id(self): -# return id(self) diff --git a/src/players/models.py b/src/players/models.py index 510952084f..407ab0f7db 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -44,7 +44,7 @@ from django.conf import settings from django.db import models from django.contrib.auth.models import User from django.utils.encoding import smart_str -from src.server import sessionhandler +from src.server import sessionhandler from src.players import manager from src.typeclasses.models import Attribute, TypedObject from src.permissions import permissions @@ -216,11 +216,11 @@ class PlayerDB(TypedObject): name = property(name_get, name_set, name_del) key = property(name_get, name_set, name_del) - # sessions property (wraps sessionhandler) + # sessions property #@property def sessions_get(self): "Getter. Retrieve sessions related to this player/user" - return sessionhandler.find_sessions_from_username(self.name) + return sessionhandler.SESSIONS.sessions_from_player(self) #@sessions.setter def sessions_set(self, value): "Setter. Protects the sessions property from adding things" @@ -251,26 +251,20 @@ class PlayerDB(TypedObject): # PlayerDB class access methods # - def msg(self, message, from_obj=None, markup=True): + def msg(self, outgoing_string, from_obj=None, data=None): """ - This is the main route for sending data to the user. + Evennia -> User + This is the main route for sending data back to the user from the server. """ if from_obj: try: - from_obj.at_msg_send(message, self) + from_obj.at_msg_send(outgoing_string, self) except Exception: pass if self.character: - if self.character.at_msg_receive(message, from_obj): + if self.character.at_msg_receive(outgoing_string, from_obj): for session in object.__getattribute__(self, 'sessions'): - session.msg(message, markup) - - def emit_to(self, message, from_obj=None): - """ - Deprecated. Use msg instead. - """ - self.msg(message, from_obj) - + session.msg(outgoing_string, data) def swap_character(self, new_character, delete_old_character=False): """ diff --git a/src/players/player.py b/src/players/player.py index 44c82f47c8..af819886c7 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -57,7 +57,7 @@ class Player(TypeClass): them loose. """ pass - def at_disconnect(self): + def at_disconnect(self, reason=None): """ Called just before user is disconnected. diff --git a/src/scripts/models.py b/src/scripts/models.py index 5c99c76aa7..2d17a53c7b 100644 --- a/src/scripts/models.py +++ b/src/scripts/models.py @@ -26,7 +26,6 @@ Common examples of uses of Scripts: """ from django.conf import settings from django.db import models -from src.objects.models import ObjectDB from src.typeclasses.models import Attribute, TypedObject from src.scripts.manager import ScriptManager @@ -91,7 +90,7 @@ class ScriptDB(TypedObject): # optional description. db_desc = models.CharField(max_length=255, blank=True) # A reference to the database object affected by this Script, if any. - db_obj = models.ForeignKey(ObjectDB, null=True, blank=True) + db_obj = models.ForeignKey("objects.ObjectDB", null=True, blank=True) # how often to run Script (secs). -1 means there is no timer db_interval = models.IntegerField(default=-1) # start script right away or wait interval seconds first diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index 6e54f34f52..280d27d87a 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -234,7 +234,7 @@ class CheckSessions(Script): "called every 60 seconds" #print "session check!" #print "ValidateSessions run" - sessionhandler.check_all_sessions() + sessionhandler.validate_sessions() class ValidateScripts(Script): "Check script validation regularly" diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index 28c62936ed..00234a813a 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -9,15 +9,9 @@ Everything starts at handle_setup() from django.contrib.auth.models import User from django.core import management from django.conf import settings - from src.config.models import ConfigValue, ConnectScreen -from src.objects.models import ObjectDB -from src.comms.models import Channel, ChannelConnection -from src.players.models import PlayerDB from src.help.models import HelpEntry -from src.scripts import scripts from src.utils import create -from src.utils import gametime def create_config_values(): """ @@ -34,14 +28,14 @@ def create_connect_screens(): print " Creating startup screen(s) ..." - text = "%ch%cb==================================================================%cn" - text += "\r\n Welcome to %chEvennia%cn! Please type one of the following to begin:\r\n" + text = "{b=================================================================={n" + text += "\r\n Welcome to {wEvennia{n! Please type one of the following to begin:\r\n" text += "\r\n If you have an existing account, connect to it by typing:\r\n " - text += "%chconnect %cn\r\n If you need to create an account, " + text += "{wconnect {n\r\n If you need to create an account, " text += "type (without the <>'s):\r\n " - text += "%chcreate \"\" %cn\r\n" - text += "\r\n Enter %chhelp%cn for more info. %chlook%cn will re-show this screen.\r\n" - text += "%ch%cb==================================================================%cn\r\n" + text += "{wcreate \"\" {n\r\n" + text += "\r\n Enter {whelp{n for more info. {wlook{n will re-show this screen.\r\n" + text += "{b=================================================================={n\r\n" ConnectScreen(db_key="Default", db_text=text, db_is_active=True).save() def get_god_user(): @@ -116,6 +110,7 @@ def create_channels(): # connect the god user to all these channels by default. goduser = get_god_user() + from src.comms.models import ChannelConnection ChannelConnection.objects.create_connection(goduser, pchan) ChannelConnection.objects.create_connection(goduser, ichan) ChannelConnection.objects.create_connection(goduser, cchan) @@ -164,9 +159,10 @@ def create_system_scripts(): Setup the system repeat scripts. They are automatically started by the create_script function. """ + from src.scripts import scripts print " Creating and starting global scripts ..." - + # check so that all sessions are alive. script1 = create.create_script(scripts.CheckSessions) # validate all scripts in script table. @@ -184,7 +180,7 @@ def start_game_time(): (the uptime can also be found directly from the server though). """ print " Starting in-game time ..." - + from src.utils import gametime gametime.init_gametime() def handle_setup(last_step): @@ -230,11 +226,16 @@ def handle_setup(last_step): setup_func() except Exception: if last_step + num == 2: + from src.players.models import PlayerDB + from src.objects.models import ObjectDB + for obj in ObjectDB.objects.all(): obj.delete() for profile in PlayerDB.objects.all(): profile.delete() elif last_step + num == 3: + from src.comms.models import Channel, ChannelConnection + for chan in Channel.objects.all(): chan.delete() for conn in ChannelConnection.objects.all(): diff --git a/src/server/server.py b/src/server/server.py index 0cf98d3631..d5b69fc964 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -1,6 +1,11 @@ """ -This module implements the main Evennia -server process, the core of the game engine. +This module implements the main Evennia server process, the core of +the game engine. Only import this once! + +This module should be started with the 'twistd' executable since it +sets up all the networking features. (this is done by +game/evennia.py). + """ import time import sys @@ -12,75 +17,82 @@ if os.name == 'nt': from twisted.application import internet, service from twisted.internet import protocol, reactor +from twisted.web import server, static from django.db import connection from django.conf import settings - +from src.utils import reloads from src.config.models import ConfigValue -from src.server.session import SessionProtocol from src.server import sessionhandler from src.server import initial_setup -from src.utils import reloads + from src.utils.utils import get_evennia_version from src.comms import channelhandler -class EvenniaService(service.Service): + +#------------------------------------------------------------ +# Evennia Server settings +#------------------------------------------------------------ + +SERVERNAME = settings.SERVERNAME +VERSION = get_evennia_version() + +TELNET_PORTS = settings.TELNET_PORTS +WEBSERVER_PORTS = settings.WEBSERVER_PORTS + +TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS +WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS +WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED +IMC2_ENABLED = settings.IMC2_ENABLED +IRC_ENABLED = settings.IRC_ENABLED + +#------------------------------------------------------------ +# Evennia Main Server object +#------------------------------------------------------------ +class Evennia(object): + """ - The main server service task. + The main Evennia server handler. This object sets up the database and + tracks and interlinks all the twisted network services that make up + evennia. """ - def __init__(self): - # Holds the TCP services. - self.service_collection = None - self.game_running = True + + def __init__(self, application): + """ + Setup the server. + + application - an instantiated Twisted application + + """ sys.path.append('.') + + # create a store of services + self.services = service.IServiceCollection(application) + + print '\n' + '-'*50 # Database-specific startup optimizations. - if (settings.DATABASE_ENGINE == "sqlite3" - or hasattr(settings, 'DATABASE') - and settings.DATABASE.get('ENGINE', None) == 'django.db.backends.sqlite3'): - # run sqlite3 preps - self.sqlite3_prep() - - # Begin startup debug output. - print '\n' + '-'*50 - - last_initial_setup_step = \ - ConfigValue.objects.conf('last_initial_setup_step') - - if not last_initial_setup_step: - # None is only returned if the config does not exist, - # i.e. this is an empty DB that needs populating. - print ' Server started for the first time. Setting defaults.' - initial_setup.handle_setup(0) - print '-'*50 - - elif int(last_initial_setup_step) >= 0: - # last_setup_step >= 0 means the setup crashed - # on one of its modules and setup will resume, retrying - # the last failed module. When all are finished, the step - # is set to -1 to show it does not need to be run again. - print ' Resuming initial setup from step %s.' % \ - last_initial_setup_step - initial_setup.handle_setup(int(last_initial_setup_step)) - print '-'*50 + self.sqlite3_prep() + + # Run the initial setup if needed + self.run_initial_setup() # we have to null this here. - sessionhandler.change_session_count(0) + sessionhandler.SESSIONS.session_count(0) self.start_time = time.time() # initialize channelhandler channelhandler.CHANNELHANDLER.update() + # init all global scripts reloads.reload_scripts(init_mode=True) - # Make output to the terminal. - print ' %s (%s) started on port(s):' % \ - (settings.SERVERNAME, get_evennia_version()) - for port in settings.GAMEPORTS: - print ' * %s' % (port) - print '-'*50 + # Make info output to the terminal. + self.terminal_output() - + print '-'*50 + + self.game_running = True # Server startup methods @@ -89,14 +101,50 @@ class EvenniaService(service.Service): Optimize some SQLite stuff at startup since we can't save it to the database. """ - cursor = connection.cursor() - cursor.execute("PRAGMA cache_size=10000") - cursor.execute("PRAGMA synchronous=OFF") - cursor.execute("PRAGMA count_changes=OFF") - cursor.execute("PRAGMA temp_store=2") - + if (settings.DATABASE_ENGINE == "sqlite3" + or hasattr(settings, 'DATABASE') + and settings.DATABASE.get('ENGINE', None) + == 'django.db.backends.sqlite3'): + cursor = connection.cursor() + cursor.execute("PRAGMA cache_size=10000") + cursor.execute("PRAGMA synchronous=OFF") + cursor.execute("PRAGMA count_changes=OFF") + cursor.execute("PRAGMA temp_store=2") - # General methods + def run_initial_setup(self): + """ + This attempts to run the initial_setup script of the server. + It returns if this is not the first time the server starts. + """ + last_initial_setup_step = ConfigValue.objects.conf('last_initial_setup_step') + if not last_initial_setup_step: + # None is only returned if the config does not exist, + # i.e. this is an empty DB that needs populating. + print ' Server started for the first time. Setting defaults.' + initial_setup.handle_setup(0) + print '-'*50 + elif int(last_initial_setup_step) >= 0: + # a positive value means the setup crashed on one of its + # modules and setup will resume from this step, retrying + # the last failed module. When all are finished, the step + # is set to -1 to show it does not need to be run again. + print ' Resuming initial setup from step %s.' % \ + last_initial_setup_step + initial_setup.handle_setup(int(last_initial_setup_step)) + print '-'*50 + + def terminal_output(self): + """ + Outputs server startup info to the terminal. + """ + print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION) + if TELNET_ENABLED: + print " telnet: " + ", ".join([str(port) for port in TELNET_PORTS]) + if WEBSERVER_ENABLED: + clientstring = "" + if WEBCLIENT_ENABLED: + clientstring = '/client' + print " webserver%s: " % clientstring + ", ".join([str(port) for port in WEBSERVER_PORTS]) def shutdown(self, message=None): """ @@ -104,52 +152,88 @@ class EvenniaService(service.Service): """ if not message: message = 'The server has been shutdown. Please check back soon.' - sessionhandler.announce_all(message) - sessionhandler.disconnect_all_sessions() + sessionhandler.SESSIONS.disconnect_all_sessions(reason=message) reactor.callLater(0, reactor.stop) - def getEvenniaServiceFactory(self): - "Retrieve instances of the server" + +#------------------------------------------------------------ +# +# Start the Evennia game server and add all active services +# +#------------------------------------------------------------ + +# twistd requires us to define the variable 'application' so it knows +# what to execute from. +application = service.Application('Evennia') + +# The main evennia server program. This sets up the database +# and is where we store all the other services. +EVENNIA = Evennia(application) + +# We group all the various services under the same twisted app. +# These will gradually be started as they are initialized below. + +if TELNET_ENABLED: + + # start telnet game connections + + from src.server import telnet + + for port in TELNET_PORTS: factory = protocol.ServerFactory() - factory.protocol = SessionProtocol - factory.server = self - return factory + factory.protocol = telnet.TelnetProtocol + telnet_service = internet.TCPServer(port, factory) + telnet_service.setName('Evennia%s' % port) + EVENNIA.services.addService(telnet_service) - def start_services(self, application): - """ - Starts all of the TCP services. - """ - self.service_collection = service.IServiceCollection(application) - for port in settings.GAMEPORTS: - evennia_server = \ - internet.TCPServer(port, self.getEvenniaServiceFactory()) - evennia_server.setName('Evennia%s' %port) - evennia_server.setServiceParent(self.service_collection) - - if settings.IMC2_ENABLED: - from src.imc2.connection import IMC2ClientFactory - from src.imc2 import events as imc2_events - imc2_factory = IMC2ClientFactory() - svc = internet.TCPClient(settings.IMC2_SERVER_ADDRESS, - settings.IMC2_SERVER_PORT, - imc2_factory) - svc.setName('IMC2') - svc.setServiceParent(self.service_collection) - imc2_events.add_events() +if WEBSERVER_ENABLED: - if settings.IRC_ENABLED: - from src.irc.connection import IRC_BotFactory - irc = internet.TCPClient(settings.IRC_NETWORK, - settings.IRC_PORT, - IRC_BotFactory(settings.IRC_CHANNEL, - settings.IRC_NETWORK, - settings.IRC_NICKNAME)) - irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL)) - irc.setServiceParent(self.service_collection) + # a django-compatible webserver. + + from src.server.webserver import DjangoWebRoot + + # define the root url (/) as a wsgi resource recognized by Django + web_root = DjangoWebRoot() + # point our media resources to url /media + media_dir = os.path.join(settings.SRC_DIR, 'web', settings.MEDIA_URL.lstrip('/')) #TODO: Could be made cleaner? + web_root.putChild("media", static.File(media_dir)) + + if WEBCLIENT_ENABLED: + # create ajax client processes at /webclientdata + from src.server.webclient import WebClient + web_root.putChild("webclientdata", WebClient()) + + web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE) + for port in WEBSERVER_PORTS: + # create the webserver + webserver = internet.TCPServer(port, web_site) + webserver.setName('EvenniaWebServer%s' % port) + EVENNIA.services.addService(webserver) -# Twisted requires us to define an 'application' attribute. -application = service.Application('Evennia') -# The main mud service. Import this for access to the server methods. -mud_service = EvenniaService() -mud_service.start_services(application) +if IMC2_ENABLED: + + # IMC2 channel connections + + from src.imc2.connection import IMC2ClientFactory + from src.imc2 import events as imc2_events + imc2_factory = IMC2ClientFactory() + svc = internet.TCPClient(settings.IMC2_SERVER_ADDRESS, + settings.IMC2_SERVER_PORT, + imc2_factory) + svc.setName('IMC2') + EVENNIA.services.addService(svc) + imc2_events.add_events() + +if IRC_ENABLED: + + # IRC channel connections + + from src.irc.connection import IRC_BotFactory + irc = internet.TCPClient(settings.IRC_NETWORK, + settings.IRC_PORT, + IRC_BotFactory(settings.IRC_CHANNEL, + settings.IRC_NETWORK, + settings.IRC_NICKNAME)) + irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL)) + EVENNIA.services.addService(irc) diff --git a/src/server/session.py b/src/server/session.py index 9d02e3cc38..392b2cd5be 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -1,214 +1,183 @@ """ -This module contains classes related to Sessions. sessionhandler has the things -needed to manage them. +This defines a generic session class. + +All protocols should implement this class and its hook methods. """ -import time + +import time from datetime import datetime -from twisted.conch.telnet import StatefulTelnetProtocol +#from django.contrib.auth.models import User from django.conf import settings -from src.server import sessionhandler -from src.objects.models import ObjectDB +#from src.objects.models import ObjectDB from src.comms.models import Channel -from src.config.models import ConnectScreen +from src.utils import logger, reloads from src.commands import cmdhandler -from src.utils import ansi -from src.utils import reloads -from src.utils import logger -from src.utils import utils +from src.server import sessionhandler -ENCODINGS = settings.ENCODINGS +IDLE_TIMEOUT = settings.IDLE_TIMEOUT +IDLE_COMMAND = settings.IDLE_COMMAND -class SessionProtocol(StatefulTelnetProtocol): + + +class IOdata(object): """ - This class represents a player's session. Each player - gets a session assigned to them whenever - they connect to the game server. All communication - between game and player goes through here. + A simple storage object that allows for storing + new attributes on it at creation. + """ + def __init__(self, **kwargs): + "Give keyword arguments to store as new arguments on the object." + self.__dict__.update(**kwargs) + + +#------------------------------------------------------------ +# SessionBase class +#------------------------------------------------------------ + +class SessionBase(object): + """ + This class represents a player's session and is a template for + individual protocols to communicate with Evennia. + + Each player gets a session assigned to them whenever they connect + to the game server. All communication between game and player goes + through their session. + """ - def __str__(self): - """ - String representation of the user session class. We use - this a lot in the server logs and stuff. - """ - if self.logged_in: - symbol = '#' - else: - symbol = '?' - return "<%s> %s@%s" % (symbol, self.name, self.address,) + # use this to uniquely identify the protocol name, e.g. "telnet" or "comet" + protocol_key = "BaseProtocol" - def connectionMade(self): + def session_connect(self, address, suid=None): """ - What to do when we get a connection. - """ - # setup the parameters - self.prep_session() - # send info - logger.log_infomsg('New connection: %s' % self) - # add this new session to handler - sessionhandler.add_session(self) - # show a connect screen - self.game_connect_screen() + The setup of the session. An address (usually an IP address) on any form is required. - def getClientAddress(self): - """ - Returns the client's address and port in a tuple. For example - ('127.0.0.1', 41917) - """ - return self.transport.client + This should be called by the protocol at connection time. - def prep_session(self): + suid = this is a session id. Needed by some transport protocols. """ - This sets up the main parameters of - the session. The game will poll these - properties to check the status of the - connection and to be able to contact - the connected player. - """ - # main server properties - self.server = self.factory.server - self.address = self.getClientAddress() + self.address = address - # player setup + # user setup self.name = None self.uid = None + self.suid = suid self.logged_in = False self.encoding = "utf-8" + current_time = time.time() + # The time the user last issued a command. - self.cmd_last = time.time() + self.cmd_last = current_time # Player-visible idle time, excluding the IDLE command. - self.cmd_last_visible = time.time() + self.cmd_last_visible = current_time + # The time when the user connected. + self.conn_time = current_time # Total number of commands issued. self.cmd_total = 0 - # The time when the user connected. - self.conn_time = time.time() #self.channels_subscribed = {} + sessionhandler.SESSIONS.add_unloggedin_session(self) + # call hook method + self.at_connect() - def disconnectClient(self): + def session_login(self, player): """ - Manually disconnect the client. - """ - self.transport.loseConnection() + Private startup mechanisms that need to run at login - def connectionLost(self, reason): + player - the connected player """ - Execute this when a client abruplty loses their connection. - """ - logger.log_infomsg('Disconnected: %s' % self) - self.cemit_info('Disconnected: %s.' % self) - self.handle_close() + self.player = player + self.user = player.user + self.uid = self.user.id + self.name = self.user.username + self.logged_in = True + self.conn_time = time.time() - def lineReceived(self, raw_string): - """ - Communication Player -> Evennia - Any line return indicates a command for the purpose of the MUD. - So we take the user input and pass it to the Player and their currently - connected character. - """ + # Update account's last login time. + self.user.last_login = datetime.now() + self.user.save() + self.log('Logged in: %s' % self) - if self.encoding: - try: - raw_string = utils.to_unicode(raw_string, encoding=self.encoding) - self.execute_cmd(raw_string) - return - except Exception, e: - err = str(e) - print err - pass + # start (persistent) scripts on this object + reloads.reload_scripts(obj=self.player.character, init_mode=True) + + #add session to connected list + sessionhandler.SESSIONS.add_loggedin_session(self) - # malformed/wrong encoding defined on player-try some defaults - for encoding in ENCODINGS: - try: - raw_string = utils.to_unicode(raw_string, encoding=encoding) - err = None - break - except Exception, e: - err = str(e) - continue - if err: - self.sendLine(err) + #call hook + self.at_login() + + def session_disconnect(self, reason=None): + """ + Clean up the session, removing it from the game and doing some + accounting. This method is used also for non-loggedin + accounts. + + Note that this methods does not close the connection - this is protocol-dependent + and have to be done right after this function! + """ + if self.logged_in: + character = self.get_character() + if character: + character.player.at_disconnect(reason) + uaccount = character.player.user + uaccount.last_login = datetime.now() + uaccount.save() + self.logged_in = False + sessionhandler.SESSIONS.remove_session(self) + + def session_validate(self): + """ + Validate the session to make sure they have not been idle for too long + """ + if IDLE_TIMEOUT > 0 and (time.time() - self.cmd_last) > IDLE_TIMEOUT: + self.msg("Idle timeout exceeded, disconnecting.") + self.session_disconnect() + + def get_player(self): + """ + Get the player associated with this session + """ + if self.logged_in: + return self.player else: - self.execute_cmd(raw_string) - - def msg(self, message, markup=True): - """ - Communication Evennia -> Player - Sends a message to the session. - - markup - determines if formatting markup should be - parsed or not. Currently this means ANSI - colors, but could also be html tags for - web connections etc. - """ - if self.encoding: - try: - message = utils.to_str(message, encoding=self.encoding) - self.sendLine(ansi.parse_ansi(message, strip_ansi=not markup)) - return - except Exception: - pass - - # malformed/wrong encoding defined on player - try some defaults - for encoding in ENCODINGS: - try: - message = utils.to_str(message, encoding=encoding) - err = None - break - except Exception, e: - err = str(e) - continue - if err: - self.sendLine(err) - else: - self.sendLine(ansi.parse_ansi(message, strip_ansi=not markup)) + return None + # if self.logged_in: + # character = ObjectDB.objects.get_object_with_user(self.uid) + # if not character: + # string = "No player match for session uid: %s" % self.uid + # logger.log_errmsg(string) + # return None + # return character.player + # return None + def get_character(self): """ Returns the in-game character associated with a session. This returns the typeclass of the object. """ - if self.logged_in: - character = ObjectDB.objects.get_object_with_user(self.uid) - if not character: - string = "No character match for session uid: %s" % self.uid - logger.log_errmsg(string) - else: - return character + player = self.get_player() + if player: + return player.character return None - def execute_cmd(self, raw_string): + def log(self, message, channel=True): """ - Sends a command to this session's - character for processing. + Emits session info to the appropriate outputs and info channels. + """ + if channel: + try: + cchan = settings.CHANNEL_CONNECTINFO + cchan = Channel.objects.get_channel(cchan[0]) + cchan.msg("[%s]: %s" % (cchan.key, message)) + except Exception: + pass + logger.log_infomsg(message) - 'idle' is a special command that is - interrupted already here. It doesn't do - anything except silently updates the - last-active timer to avoid getting kicked - off for idleness. - """ - # handle the 'idle' command - if str(raw_string).strip() == 'idle': - self.update_counters(idle=True) - return - - # all other inputs, including empty inputs - character = self.get_character() - if character: - # normal operation. - character.execute_cmd(raw_string) - else: - # we are not logged in yet - cmdhandler.cmdhandler(self, raw_string, unloggedin=True) - # update our command counters and idle times. - self.update_counters() - - def update_counters(self, idle=False): + def update_session_counters(self, idle=False): """ Hit this when the user enters a command in order to update idle timers - and command counters. If silently is True, the public-facing idle time - is not updated. + and command counters. """ # Store the timestamp of the user's last command. self.cmd_last = time.time() @@ -217,80 +186,125 @@ class SessionProtocol(StatefulTelnetProtocol): self.cmd_total += 1 # Player-visible idle time, not used in idle timeout calcs. self.cmd_last_visible = time.time() - - def handle_close(self): + + def execute_cmd(self, command_string): """ - Break the connection and do some accounting. + Execute a command string. """ + + # handle the 'idle' command + if str(command_string).strip() == IDLE_COMMAND: + self.update_session_counters(idle=True) + return + + # all other inputs, including empty inputs character = self.get_character() + if character: - #call hook functions - character.at_disconnect() - character.player.at_disconnect() - uaccount = character.player.user - uaccount.last_login = datetime.now() - uaccount.save() - self.disconnectClient() - self.logged_in = False - sessionhandler.remove_session(self) - - def game_connect_screen(self): + #print "loggedin _execute_cmd: '%s' __ %s" % (command_string, character) + # normal operation. + character.execute_cmd(command_string) + else: + #print "unloggedin _execute_cmd: '%s' __ %s" % (command_string, character) + # we are not logged in yet; call cmdhandler directly + cmdhandler.cmdhandler(self, command_string, unloggedin=True) + + def get_data_obj(self, **kwargs): """ - Show the banner screen. Grab from the 'connect_screen' - config directive. If more than one connect screen is - defined in the ConnectScreen attribute, it will be - random which screen is used. + Create a data object, storing keyword arguments on itself as arguments. """ - screen = ConnectScreen.objects.get_random_connect_screen() - string = ansi.parse_ansi(screen.text) - self.msg(string) - + return IOdata(**kwargs) + + def __eq__(self, other): + return self.address == other.address + + def __str__(self): + """ + String representation of the user session class. We use + this a lot in the server logs. + """ + if self.logged_in: + symbol = '#' + else: + symbol = '?' + return "<%s> %s@%s" % (symbol, self.name, self.address,) + + def __unicode__(self): + """ + Unicode representation + """ + return u"%s" % str(self) + + + +#------------------------------------------------------------ +# Session class - inherit from this +#------------------------------------------------------------ + +class Session(SessionBase): + """ + The main class to inherit from. Overload the methods here. + """ + + # exchange this for a unique name you can use to identify the + # protocol type this session uses + protocol_key = "TemplateProtocol" + + # + # Hook methods + # + + def at_connect(self): + """ + This method is called by the connection mechanic after + connection has been made. The session is added to the + sessionhandler and basic accounting has been made at this + point. + + This is the place to put e.g. welcome screens specific to the + protocol. + """ + pass + + def at_login(self, player): + """ + This method is called by the login mechanic whenever the user + has finished authenticating. The user has been moved to the + right sessionhandler list and basic book keeping has been + done at this point (so logged_in=True). + """ + pass + + def at_disconnect(self, reason=None): + """ + This method is called just before cleaning up the session + (so still logged_in=True at this point). + """ + pass + + def at_data_in(self, string="", data=None): + """ + Player -> Evennia + """ + pass + + def at_data_out(self, string="", data=None): + """ + Evennia -> Player + + string - an string of any form to send to the player + data - a data structure of any form + + """ + pass + + # easy-access functions def login(self, player): - """ - After the user has authenticated, this actually - logs them in. At this point the session has - a User account tied to it. User is an django - object that handles stuff like permissions and - access, it has no visible precense in the game. - This User object is in turn tied to a game - Object, which represents whatever existence - the player has in the game world. This is the - 'character' referred to in this module. - """ - # set the session properties - - user = player.user - self.uid = user.id - self.name = user.username - self.logged_in = True - self.conn_time = time.time() - if player.db.encoding: - self.encoding = player.db.encoding - - if not settings.ALLOW_MULTISESSION: - # disconnect previous sessions. - sessionhandler.disconnect_duplicate_session(self) - - # start (persistent) scripts on this object - reloads.reload_scripts(obj=self.get_character(), init_mode=True) - - logger.log_infomsg("Logged in: %s" % self) - self.cemit_info('Logged in: %s' % self) - - # Update their account's last login time. - user.last_login = datetime.now() - user.save() - - def cemit_info(self, message): - """ - Channel emits info to the appropriate info channel. By default, this - is MUDConnections. - """ - try: - cchan = settings.CHANNEL_CONNECTINFO - cchan = Channel.objects.get_channel(cchan[0]) - cchan.msg("[%s]: %s" % (cchan.key, message)) - except Exception: - logger.log_infomsg(message) - - + "alias for at_login" + self.at_login(player) + def logout(self): + "alias for at_logout" + self.at_disconnect() + def msg(self, string='', data=None): + "alias for at_data_out" + self.at_data_out(string, data) diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 635e4bd4ce..9e26c12f3b 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -1,137 +1,184 @@ """ -Sessionhandler, stores and handles -a list of all player connections (sessions). +This module handles sessions of users connecting +to the server. + +Since Evennia supports several different connection +protocols, it is important to have a joint place +to store session info. It also makes it easier +to dispatch data. + +Whereas server.py handles all setup of the server +and database itself, this file handles all that +comes after initial startup. + +All new sessions (of whatever protocol) are responsible for +registering themselves with this module. + """ -import time +from django.conf import settings from django.contrib.auth.models import User from src.config.models import ConfigValue -from src.utils import logger -# Our list of connected sessions. -SESSIONS = [] +ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION -def add_session(session): - """ - Adds a session to the session list. - """ - SESSIONS.insert(0, session) - change_session_count(1) - logger.log_infomsg('Sessions active: %d' % (len(get_sessions(return_unlogged=True),))) - -def get_sessions(return_unlogged=False): - """ - Lists the connected session objects. - """ - if return_unlogged: - return SESSIONS - else: - return [sess for sess in SESSIONS if sess.logged_in] - -def get_session_id_list(return_unlogged=False): - """ - Lists the connected session object ids. - """ - if return_unlogged: - return SESSIONS - else: - return [sess.uid for sess in SESSIONS if sess.logged_in] +#------------------------------------------------------------ +# SessionHandler class +#------------------------------------------------------------ -def disconnect_all_sessions(): +class SessionHandler(object): """ - Cleanly disconnect all of the connected sessions. - """ - for sess in get_sessions(): - sess.handle_close() + This object holds the stack of sessions active in the game at + any time. -def disconnect_duplicate_session(session): - """ - Disconnects any existing session under the same object. This is used in - connection recovery to help with record-keeping. - """ - SESSIONS = get_sessions() - session_pobj = session.get_character() - for other_session in SESSIONS: - other_pobject = other_session.get_character() - if session_pobj == other_pobject and other_session != session: - other_session.msg("Your account has been logged in from elsewhere, disconnecting.") - other_session.disconnectClient() - return True - return False + A session register with the handler in two steps, first by + registering itself with the connect() method. This indicates an + non-authenticated session. Whenever the session is authenticated + the session together with the related player is sent to the login() + method. -def check_all_sessions(): - """ - Check all currently connected sessions and see if any are dead. - """ - idle_timeout = int(ConfigValue.objects.conf('idle_timeout')) - - if len(SESSIONS) <= 0: - return - - if idle_timeout <= 0: - return - - for sess in get_sessions(return_unlogged=True): - if (time.time() - sess.cmd_last) > idle_timeout: - sess.msg("Idle timeout exceeded, disconnecting.") - sess.handle_close() - -def change_session_count(num): - """ - Count number of connected users by use of a config value - - num can be a positive or negative value. If 0, the counter - will be reset to 0. """ - if num == 0: - # reset - ConfigValue.objects.conf('nr_sessions', 0) - - nr = ConfigValue.objects.conf('nr_sessions') - if nr == None: - nr = 0 - else: - nr = int(nr) - nr += num - ConfigValue.objects.conf('nr_sessions', str(nr)) + def __init__(self): + """ + Init the handler. We track two types of sessions, those + who have just connected (unloggedin) and those who have + logged in (authenticated). + """ + self.unloggedin = [] + self.loggedin = [] - -def remove_session(session): - """ - Removes a session from the session list. - """ - try: - SESSIONS.remove(session) - change_session_count(-1) - logger.log_infomsg('Sessions active: %d' % (len(get_sessions()),)) - except ValueError: - # the session was already removed. - logger.log_errmsg("Unable to remove session: %s" % (session,)) - return - -def find_sessions_from_username(username): - """ - Given a username, return any matching sessions. - """ - try: - uobj = User.objects.get(username=username) - uid = uobj.id - return [session for session in SESSIONS if session.uid == uid] - except User.DoesNotExist: - return None - -def sessions_from_object(targ_object): - """ - Returns a list of matching session objects, or None if there are no matches. - - targobject: (Object) The object to match. - """ - return [session for session in SESSIONS - if session.get_character() == targ_object] + def add_unloggedin_session(self, session): + """ + Call at first connect. This adds a not-yet authenticated session. + """ + self.unloggedin.insert(0, session) -def announce_all(message): - """ - Announces something to all connected players. - """ - for session in get_sessions(): - session.msg('%s' % message) + def add_loggedin_session(self, session): + """ + Log in the previously unloggedin session and the player we by + now should know is connected to it. After this point we + assume the session to be logged in one way or another. + """ + # prep the session with player/user info + + + if not ALLOW_MULTISESSION: + # disconnect previous sessions. + self.disconnect_duplicate_sessions(session) + + # store/move the session to the right list + try: + self.unloggedin.remove(session) + except ValueError: + pass + self.loggedin.insert(0, session) + self.session_count(1) + + def remove_session(self, session): + """ + Remove session from the handler + """ + removed = False + try: + self.unloggedin.remove(session) + except Exception: + try: + self.loggedin.remove(session) + except Exception: + return + self.session_count(-1) + + def get_sessions(self, include_unloggedin=False): + """ + Returns the connected session objects. + """ + if include_unloggedin: + return self.loggedin + self.unloggedin + else: + return self.loggedin + + def disconnect_all_sessions(self, reason=None): + """ + Cleanly disconnect all of the connected sessions. + """ + sessions = self.get_sessions(include_unloggedin=True) + for session in sessions: + session.session_disconnect(reason) + self.session_count(0) + + def disconnect_duplicate_sessions(self, session): + """ + Disconnects any existing sessions with the same game object. This is used in + connection recovery to help with record-keeping. + """ + reason = "Your account has been logged in from elsewhere. Disconnecting." + sessions = self.get_sessions() + session_character = self.get_character(session) + logged_out = 0 + for other_session in sessions: + other_character = self.get_character(other_session) + if session_character == other_character and other_session != session: + self.remove_session(other_session, reason=reason) + logged_out += 1 + self.session_count(-logged_out) + return logged_out + + def validate_sessions(self): + """ + Check all currently connected sessions (logged in and not) + and see if any are dead. + """ + for session in self.get_sessions(include_unloggedin=True): + session.session_validate() + + def session_count(self, num=None): + """ + Count up/down the number of connected, authenticated users. + If num is None, the current number of sessions is returned. + + num can be a positive or negative value to be added to the current count. + If 0, the counter will be reset to 0. + """ + if num == None: + # show the current value. This also syncs it. + return int(ConfigValue.objects.conf('nr_sessions', default=0)) + elif num == 0: + # reset value to 0 + ConfigValue.objects.conf('nr_sessions', 0) + else: + # add/remove session count from value + add = int(ConfigValue.objects.conf('nr_sessions', default=0)) + num = max(0, num + add) + ConfigValue.objects.conf('nr_sessions', str(num)) + + def sessions_from_player(self, player): + """ + Given a player, return any matching sessions. + """ + username = player.user.username + try: + uobj = User.objects.get(username=username) + except User.DoesNotExist: + return None + uid = uobj.id + return [session for session in self.loggedin if session.uid == uid] + + def sessions_from_character(self, character): + """ + Given a game character, return any matching sessions. + """ + player = character.player + if player: + return self.sessions_from_player(player) + return None + + def session_from_suid(self, suid): + """ + Given a session id, retrieve the session (this is primarily + intended to be called by web clients) + """ + return [sess for sess in self.get_sessions(include_unloggedin=True) if sess.suid and sess.suid == suid] + +SESSIONS = SessionHandler() + + diff --git a/src/server/telnet.py b/src/server/telnet.py new file mode 100644 index 0000000000..ca70cb6177 --- /dev/null +++ b/src/server/telnet.py @@ -0,0 +1,153 @@ +""" +This module implements the telnet protocol. + +This depends on a generic session module that implements +the actual login procedure of the game, tracks +sessions etc. + +""" + +from twisted.conch.telnet import StatefulTelnetProtocol +from django.conf import settings +from src.config.models import ConnectScreen +from src.server import session +from src.utils import ansi, utils + +ENCODINGS = settings.ENCODINGS + +class TelnetProtocol(StatefulTelnetProtocol, session.Session): + """ + Each player connecting over telnet (ie using most traditional mud + clients) gets a telnet protocol instance assigned to them. All + communication between game and player goes through here. + """ + + # identifier in case one needs to easily separate protocols at run-time. + protocol_key = "telnet" + + # telnet-specific hooks + + def connectionMade(self): + """ + This is called when the connection is first + established. + """ + # initialize the session + self.session_connect(self.getClientAddress()) + + def connectionLost(self, reason="Disconnecting. Goodbye for now."): + """ + This is executed when the connection is lost for + whatever reason. It should also be called from + self.at_disconnect() so one can close the connection + manually without having to know the name of this specific + method. + """ + self.session_disconnect(reason) + self.transport.loseConnection() + + def getClientAddress(self): + """ + Returns the client's address and port in a tuple. For example + ('127.0.0.1', 41917) + """ + return self.transport.client + + def lineReceived(self, string): + """ + Communication Player -> Evennia. Any line return indicates a + command for the purpose of the MUD. So we take the user input + and pass it on to the game engine. + """ + self.at_data_in(string) + + def lineSend(self, string): + """ + Communication Evennia -> Player + Any string sent should already have been + properly formatted and processed + before reaching this point. + + """ + self.sendLine(string) #this is the telnet-specific method for sending + + # session-general method hooks + + def at_connect(self): + """ + Show the banner screen. Grab from the 'connect_screen' + config directive. If more than one connect screen is + defined in the ConnectScreen attribute, it will be + random which screen is used. + """ + self.telnet_markup = True + # show screen + screen = ConnectScreen.objects.get_random_connect_screen() + string = ansi.parse_ansi(screen.text) + self.lineSend(string) + + def at_login(self): + """ + Called after authentication. self.logged_in=True at this point. + """ + if self.player.has_attribute('telnet_markup'): + self.telnet_markup = self.player.get_attribute("telnet_markup") + else: + self.telnet_markup = True + + def at_disconnect(self, reason="Connection closed. Goodbye for now."): + """ + Disconnect from server + """ + if reason: + self.lineSend(reason) + self.connectionLost(reasoon) + + def at_data_out(self, string, data=None): + """ + Data Evennia -> Player access hook. 'data' argument is ignored. + """ + if self.encoding: + try: + string = utils.to_str(string, encoding=self.encoding) + self.lineSend(ansi.parse_ansi(string, strip_ansi=not self.telnet_markup)) + return + except Exception: + pass + # malformed/wrong encoding defined on player - try some defaults + for encoding in ENCODINGS: + try: + string = utils.to_str(string, encoding=encoding) + err = None + break + except Exception, e: + err = str(e) + continue + if err: + self.lineSend(err) + else: + self.lineSend(ansi.parse_ansi(string, strip_ansi=not self.telnet_markup)) + + def at_data_in(self, string, data=None): + """ + Line from Player -> Evennia. 'data' argument is not used. + + """ + if self.encoding: + try: + string = utils.to_unicode(string, encoding=self.encoding) + self.execute_cmd(string) + return + except Exception, e: + err = str(e) + print err + # malformed/wrong encoding defined on player - try some defaults + for encoding in ENCODINGS: + try: + string = utils.to_unicode(string, encoding=encoding) + err = None + break + except Exception, e: + err = str(e) + continue + self.execute_cmd(self, string) diff --git a/src/server/webclient.py b/src/server/webclient.py new file mode 100644 index 0000000000..29bfda71a7 --- /dev/null +++ b/src/server/webclient.py @@ -0,0 +1,286 @@ +""" +Web client server resource. + +The Evennia web client consists of two components running +on twisted and django. They are both a part of the Evennia +website url tree (so the testing website might be located +on http://localhost:8020/, whereas the webclient can be +found on http://localhost:8020/webclient.) + +/webclient - this url is handled through django's template + system and serves the html page for the client + itself along with its javascript chat program. +/webclientdata - this url is called by the ajax chat using + POST requests (long-polling when necessary) + The WebClient resource in this module will + handle these requests and act as a gateway + to sessions connected over the webclient. +""" + +from twisted.web import server, resource +from twisted.internet import defer + +from django.utils import simplejson +from django.utils.functional import Promise +from django.utils.encoding import force_unicode +from django.conf import settings +from src.utils import utils +from src.utils.ansi2html import parse_html +from src.config.models import ConnectScreen +from src.server import session, sessionhandler + +SERVERNAME = settings.SERVERNAME +ENCODINGS = settings.ENCODINGS + +# defining a simple json encoder for returning +# django data to the client. Might need to +# extend this if one wants to send more +# complex database objects too. + +class LazyEncoder(simplejson.JSONEncoder): + def default(self, obj): + if isinstance(obj, Promise): + return force_unicode(obj) + return super(LazyEncoder, self).default(obj) +def jsonify(obj): + return simplejson.dumps(obj, ensure_ascii=False, cls=LazyEncoder) + + +# +# WebClient resource - this is called by the ajax client +# using POST requests to /webclientdata. +# + +class WebClient(resource.Resource): + """ + An ajax/comet long-polling transport protocol for + """ + isLeaf = True + allowedMethods = ('POST',) + + def __init__(self): + self.requests = {} + self.databuffer = {} + + def getChild(self, path, request): + """ + This is the place to put dynamic content. + """ + return self + + def _responseFailed(self, failure, suid, request): + "callback if a request is lost/timed out" + try: + self.requests.get(suid, []).remove(request) + except ValueError: + pass + + def lineSend(self, suid, string, data=None): + """ + This adds the data to the buffer and/or sends it to + the client as soon as possible. + """ + requests = self.requests.get(suid, None) + if requests: + request = requests.pop(0) + # we have a request waiting. Return immediately. + request.write(jsonify({'msg':string, 'data':data})) + request.finish() + self.requests[suid] = requests + else: + # no waiting request. Store data in buffer + dataentries = self.databuffer.get(suid, []) + dataentries.append(jsonify({'msg':string, 'data':data})) + self.databuffer[suid] = dataentries + + def disconnect(self, suid): + "Disconnect session" + sess = sessionhandler.SESSIONS.session_from_suid(suid) + if sess: + sess[0].session_disconnect() + if self.requests.has_key(suid): + for request in self.requests.get(suid, []): + request.finish() + del self.requests[suid] + if self.databuffer.has_key(suid): + del self.databuffer[suid] + + def mode_init(self, request): + """ + This is called by render_POST when the client + requests an init mode operation (at startup) + """ + csess = request.getSession() # obs, this is a cookie, not an evennia session! + #csees.expireCallbacks.append(lambda : ) + suid = csess.uid + remote_addr = request.getClientIP() + host_string = "%s (%s:%s)" % (SERVERNAME, request.getHost().host, request.getHost().port) + self.requests[suid] = [] + self.databuffer[suid] = [] + + sess = sessionhandler.SESSIONS.session_from_suid(suid) + if not sess: + sess = WebClientSession() + sess.client = self + sess.session_connect(remote_addr, suid) + sessionhandler.SESSIONS.add_unloggedin_session(sess) + return jsonify({'msg':host_string}) + + def mode_input(self, request): + """ + This is called by render_POST when the client + is sending data to the server. + """ + string = request.args.get('msg', [''])[0] + data = request.args.get('data', [None])[0] + suid = request.getSession().uid + sess = sessionhandler.SESSIONS.session_from_suid(suid) + if sess: + sess[0].at_data_in(string, data) + return '' + + def mode_receive(self, request): + """ + This is called by render_POST when the client is telling us + that it is ready to receive data as soon as it is + available. This is the basis of a long-polling (comet) + mechanism: the server will wait to reply until data is + available. + """ + suid = request.getSession().uid + dataentries = self.databuffer.get(suid, []) + if dataentries: + return dataentries.pop(0) + reqlist = self.requests.get(suid, []) + request.notifyFinish().addErrback(self._responseFailed, suid, request) + reqlist.append(request) + self.requests[suid] = reqlist + return server.NOT_DONE_YET + + def render_POST(self, request): + """ + This function is what Twisted calls with POST requests coming + in from the ajax client. The requests should be tagged with + different modes depending on what needs to be done, such as + initializing or sending/receving data through the request. It + uses a long-polling mechanism to avoid sending data unless + there is actual data available. + """ + dmode = request.args.get('mode', [None])[0] + if dmode == 'init': + # startup. Setup the server. + return self.mode_init(request) + elif dmode == 'input': + # input from the client to the server + return self.mode_input(request) + elif dmode == 'receive': + # the client is waiting to receive data. + return self.mode_receive(request) + else: + # this should not happen if client sends valid data. + return '' + +# +# A session type handling communication over the +# web client interface. +# + +class WebClientSession(session.Session): + """ + This represents a session running in a webclient. + """ + + def at_connect(self): + """ + Show the banner screen. Grab from the 'connect_screen' + config directive. If more than one connect screen is + defined in the ConnectScreen attribute, it will be + random which screen is used. + """ + # show screen + screen = ConnectScreen.objects.get_random_connect_screen() + string = parse_html(screen.text) + self.client.lineSend(self.suid, string) + + def at_login(self): + """ + Called after authentication. self.logged_in=True at this point. + """ + if self.player.has_attribute('telnet_markup'): + self.telnet_markup = self.player.get_attribute("telnet_markup") + else: + self.telnet_markup = True + + def at_disconnect(self, reason=None): + """ + Disconnect from server + """ + if reason: + self.lineSend(self.suid, reason) + self.client.disconnect(self.suid) + + def at_data_out(self, string='', data=None): + """ + Data Evennia -> Player access hook. + + data argument may be used depending on + the client-server implementation. + """ + + if data: + # treat data? + pass + + # string handling is similar to telnet + if self.encoding: + try: + string = utils.to_str(string, encoding=self.encoding) + #self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True)) + self.client.lineSend(self.suid, parse_html(string)) + return + except Exception: + pass + # malformed/wrong encoding defined on player - try some defaults + for encoding in ENCODINGS: + try: + string = utils.to_str(string, encoding=encoding) + err = None + break + except Exception, e: + err = str(e) + continue + if err: + self.client.lineSend(self.suid, err) + else: + #self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True)) + self.client.lineSend(self.suid, parse_html(string)) + def at_data_in(self, string, data=None): + """ + Input from Player -> Evennia (called by client). + Use of 'data' is up to the client - server implementation. + """ + + # treat data? + if data: + pass + + # the string part is identical to telnet + if self.encoding: + try: + string = utils.to_unicode(string, encoding=self.encoding) + self.execute_cmd(string) + return + except Exception, e: + err = str(e) + print err + # malformed/wrong encoding defined on player - try some defaults + for encoding in ENCODINGS: + try: + string = utils.to_unicode(string, encoding=encoding) + err = None + break + except Exception, e: + err = str(e) + continue + self.execute_cmd(string) + diff --git a/src/server/webserver.py b/src/server/webserver.py new file mode 100644 index 0000000000..004e6b5b6b --- /dev/null +++ b/src/server/webserver.py @@ -0,0 +1,60 @@ +""" +This implements resources for twisted webservers using the wsgi +interface of django. This alleviates the need of running e.g. an +apache server to serve Evennia's web presence (although you could do +that too if desired). + +The actual servers are started inside server.py as part of the Evennia +application. + +(Lots of thanks to http://githup.com/clemensha/twisted-wsgi-django for +a great example/aid on how to do this.) + +""" +from twisted.web import resource +from twisted.python import threadpool +from twisted.internet import reactor + +from twisted.web.wsgi import WSGIResource +from django.core.handlers.wsgi import WSGIHandler + +# +# Website server resource +# + +class DjangoWebRoot(resource.Resource): + """ + This creates a web root (/) that Django + understands by tweaking the way the + child instancee are recognized. + """ + + def __init__(self): + """ + Setup the django+twisted resource + """ + resource.Resource.__init__(self) + self.wsgi_resource = self._wsgi_resource() + + def _wsgi_resource(self): + """ + Sets up a threaded webserver resource by tying + django and twisted together. + """ + # Start the threading + pool = threadpool.ThreadPool() + pool.start() + # Set it up so the pool stops after e.g. Ctrl-C kills the server + reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) + # combine twisted's wsgi resource with django's wsgi handler + wsgi_resource = WSGIResource(reactor, pool, WSGIHandler()) + return wsgi_resource + + def getChild(self, path, request): + """ + To make things work we nudge the + url tree to make this the root. + """ + path0 = request.prepath.pop(0) + request.postpath.insert(0, path0) + return self.wsgi_resource diff --git a/src/settings_default.py b/src/settings_default.py index 86cb01c4e5..7e45a9b179 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -22,8 +22,21 @@ import os # This is the name of your server and/or site. # Can be anything. SERVERNAME = "Evennia" -# A list of ports the Evennia server listens on. Can be one or many. -GAMEPORTS = [4000] +# Activate telnet service +TELNET_ENABLED = True +# A list of ports the Evennia telnet server listens on +# Can be one or many. +TELNET_PORTS = [4000] +# Start the evennia django+twisted webserver so you can +# browse the evennia website and the admin interface +# (Obs - further web configuration can be found below +# in the section 'Config for Django web features') +WEBSERVER_ENABLED = True +# A list of ports the Evennia webserver listens on +WEBSERVER_PORTS = [8020] +# Start the evennia ajax client on /webclient +# (the webserver must also be running) +WEBCLIENT_ENABLED = True # Activate full persistence if you want everything in-game to be # stored to the database. With it set, you can do typeclass.attr=value # and value will be saved to the database under the name 'attr'. @@ -54,6 +67,9 @@ GAME_DIR = os.path.join(BASE_PATH, 'game') # Place to put log files LOG_DIR = os.path.join(GAME_DIR, 'logs') DEFAULT_LOG_FILE = os.path.join(LOG_DIR, 'evennia.log') +# Where to log server requests to the web server. This is VERY spammy, so this +# file should be removed at regular intervals. +HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log') # Local time zone for this installation. All choices can be found here: # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE TIME_ZONE = 'UTC' @@ -70,12 +86,18 @@ IMPORT_MUX_HELP = False # thrown off by sending the empty system command 'idle' to the server # at regular intervals. Set <=0 to deactivate idle timout completely. IDLE_TIMEOUT = 3600 -# If the PlayerAttribute 'encoding' is not set, or wrong encoding is -# given, this list is tried, in order, stopping on the first match. -# Add sets for languages/regions your players are likely to use. (see -# http://en.wikipedia.org/wiki/Character_encoding). +# The idle command can be sent to keep your session active without actually +# having to spam normal commands regularly. It gives no feedback, only updates +# the idle timer. +IDLE_COMMAND = "idle" +# The set of encodings tried. A Player object may set an attribute "encoding" on +# itself to match the client used. If not set, or wrong encoding is +# given, this list is tried, in order, aborting on the first match. +# Add sets for languages/regions your players are likely to use. +# (see http://en.wikipedia.org/wiki/Character_encoding) ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"] + ################################################### # Evennia Database config ################################################### @@ -314,8 +336,8 @@ IRC_NICKNAME = "" # While DEBUG is False, show a regular server error page on the web # stuff, email the traceback to the people in the ADMINS tuple # below. If True, show a detailed traceback for the web -# browser to display. Note however that this might leak memory when -# active, so make sure turn it off for a production server! +# browser to display. Note however that this will leak memory when +# active, so make sure to turn it off for a production server! DEBUG = False # While true, show "pretty" error messages for template syntax errors. TEMPLATE_DEBUG = DEBUG @@ -350,7 +372,7 @@ USE_I18N = False # you're running Django's built-in test server. Normally you want a webserver # that is optimized for serving static content to handle media files (apache, # lighttpd). -SERVE_MEDIA = True +SERVE_MEDIA = False # The master urlconf file that contains all of the sub-branches to the # applications. @@ -367,7 +389,7 @@ MEDIA_URL = '/media/' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/amedia/' +ADMIN_MEDIA_PREFIX = '/media/admin/' # The name of the currently selected web template. This corresponds to the # directory names shown in the webtemplates directory. ACTIVE_TEMPLATE = 'prosimii' diff --git a/src/utils/ansi.py b/src/utils/ansi.py index d219b6955f..38325e6963 100644 --- a/src/utils/ansi.py +++ b/src/utils/ansi.py @@ -72,7 +72,7 @@ class ANSIParser(object): # MUX-style mappings %cr %cn etc - mux_ansi_map = [ + self.mux_ansi_map = [ (r'%r', ANSITable.ansi["return"]), (r'%t', ANSITable.ansi["tab"]), (r'%b', ANSITable.ansi["space"]), @@ -102,7 +102,7 @@ class ANSIParser(object): hilite = ANSITable.ansi['hilite'] normal = ANSITable.ansi['normal'] - ext_ansi_map = [ + self.ext_ansi_map = [ (r'{r', hilite + ANSITable.ansi['red']), (r'{R', normal + ANSITable.ansi['red']), (r'{g', hilite + ANSITable.ansi['green']), @@ -121,8 +121,8 @@ class ANSIParser(object): (r'{X', normal + ANSITable.ansi['black']), #pure black (r'{n', normal) #reset ] - - self.ansi_map = mux_ansi_map + ext_ansi_map + + self.ansi_map = self.mux_ansi_map + self.ext_ansi_map # prepare regex matching self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1]) @@ -141,13 +141,15 @@ class ANSIParser(object): if not string: return '' string = str(string) - for sub in self.ansi_sub: + for sub in self.ansi_sub: # go through all available mappings and translate them string = sub[0].sub(sub[1], string) if strip_ansi: # remove all ANSI escape codes string = self.ansi_regex.sub("", string) return string + + ANSI_PARSER = ANSIParser() @@ -161,3 +163,5 @@ def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER): """ return parser.parse_ansi(string, strip_ansi=strip_ansi) + + diff --git a/src/utils/ansi2html.py b/src/utils/ansi2html.py new file mode 100644 index 0000000000..c773956cdc --- /dev/null +++ b/src/utils/ansi2html.py @@ -0,0 +1,137 @@ + +""" +ANSI -> html converter + +Credit for original idea and implementation +goes to Muhammad Alkarouri and his +snippet #577349 on http://code.activestate.com. + +(extensively modified by Griatch 2010) +""" + +import re +import cgi +from src.utils import ansi + +class ANSItoHTMLparser(object): + """ + This class describes a parser for converting from ansi to html. + """ + + # mapping html color name <-> ansi code. + # Obs order matters - longer ansi codes are replaced first. + colorcodes = [('white', '\033[1m\033[37m'), + ('cyan', '\033[1m\033[36m'), + ('blue', '\033[1m\033[34m'), + ('red', '\033[1m\033[31m'), + ('magenta', '\033[1m\033[35m'), + ('lime', '\033[1m\033[32m'), + ('yellow', '\033[1m\033[33m'), + ('gray', '\033[37m'), + ('teal', '\033[36m'), + ('navy', '\033[34m'), + ('maroon', '\033[31m'), + ('purple', '\033[35m'), + ('green', '\033[32m'), + ('olive', '\033[33m')] + normalcode = '\033[0m' + tabstop = 4 + + re_string = re.compile(r'(?P[<&>])|(?P^[ \t]+)|(?P\r\n|\r|\n)|(?P(^|\s)((http|ftp)://.*?))(\s|$)', + re.S|re.M|re.I) + + def re_color(self, text): + "Replace ansi colors with html color tags" + for colorname, code in self.colorcodes: + regexp = "(?:%s)(.*?)(?:%s)" % (code, self.normalcode) + regexp = regexp.replace('[', r'\[') + text = re.sub(regexp, r'''\1''' % colorname, text) + return text + + def re_bold(self, text): + "Replace ansi hilight with bold text" + regexp = "(?:%s)(.*?)(?:%s)" % ('\033[1m', self.normalcode) + regexp = regexp.replace('[', r'\[') + return re.sub(regexp, r'\1', text) + + def re_underline(self, text): + "Replace ansi underline with html equivalent" + regexp = "(?:%s)(.*?)(?:%s)" % ('\033[4m', self.normalcode) + regexp = regexp.replace('[', r'\[') + return re.sub(regexp, r'\1', text) + + def remove_bells(self, text): + "Remove ansi specials" + return text.replace('\07', '') + + def remove_backspaces(self, text): + "Removes special escape sequences" + backspace_or_eol = r'(.\010)|(\033\[K)' + n = 1 + while n > 0: + text, n = re.subn(backspace_or_eol, '', text, 1) + return text + + def convert_linebreaks(self, text): + "Extra method for cleaning linebreaks" + return text.replace(r'\n', r'
') + + def do_sub(self, m): + "Helper method to be passed to re.sub." + c = m.groupdict() + if c['htmlchars']: + return cgi.escape(c['htmlchars']) + if c['lineend']: + return '
' + elif c['space']: + t = m.group().replace('\t', ' '*self.tabstop) + t = t.replace(' ', ' ') + return t + elif c['space'] == '\t': + return ' '*self.tabstop + else: + url = m.group('protocal') + if url.startswith(' '): + prefix = ' ' + url = url[1:] + else: + prefix = '' + last = m.groups()[-1] + if last in ['\n', '\r', '\r\n']: + last = '
' + return '%s%s' % (prefix, url) + + def parse(self, text): + """ + Main access function, converts a text containing + ansi codes into html statements. + """ + + # parse everything to ansi first + text = ansi.parse_ansi(text) + + # convert all ansi to html + result = re.sub(self.re_string, self.do_sub, text) + result = self.re_color(result) + result = self.re_bold(result) + result = self.re_underline(result) + result = self.remove_bells(result) + result = self.convert_linebreaks(result) + result = self.remove_backspaces(result) + + # clean out eventual ansi that was missed + result = ansi.parse_ansi(result, strip_ansi=True) + + return result + +HTML_PARSER = ANSItoHTMLparser() + +# +# Access function +# + +def parse_html(string, parser=HTML_PARSER): + """ + Parses a string, replace ansi markup with html + """ + return parser.parse(string) diff --git a/src/utils/create.py b/src/utils/create.py index 79a02c231d..91d59e78ed 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -22,11 +22,8 @@ Models covered: from django.conf import settings from django.contrib.auth.models import User from django.db import IntegrityError -from src.players.models import PlayerDB -from src.help.models import HelpEntry -from src.comms.models import Msg, Channel -from src.comms import channelhandler -from src.comms.managers import to_object + + from src.permissions.permissions import set_perm from src.permissions.models import PermissionGroup from src.utils import logger @@ -212,6 +209,8 @@ def create_help_entry(key, entrytext, category="General", permissions=None): general help on the game, more extensive info, in-game setting information and so on. """ + + from src.help.models import HelpEntry try: new_help = HelpEntry() new_help.key = key @@ -293,7 +292,9 @@ def create_message(senderobj, message, channels=None, at the same time, it's up to the command definitions to limit this as desired. """ - + from src.comms.models import Msg + from src.comms.managers import to_object + def to_player(obj): "Make sure the object is a player object" if hasattr(obj, 'user'): @@ -345,6 +346,9 @@ def create_channel(key, aliases=None, description=None, listen/send/admin permissions are strings if permissions separated by commas. """ + + from src.comms.models import Channel + from src.comms import channelhandler try: new_channel = Channel() new_channel.key = key @@ -408,6 +412,8 @@ def create_player(name, email, password, # isn't already registered, and that the password is ok before # getting here. + from src.players.models import PlayerDB + if user: new_user = user else: diff --git a/src/utils/utils.py b/src/utils/utils.py index c0d2136960..54d5688bf7 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -18,14 +18,16 @@ def is_iter(iterable): they are actually iterable), since string iterations are usually not what we want to do with a string. """ - if isinstance(iterable, basestring): - # skip all forms of strings (str, unicode etc) - return False - try: - # check if object implements iter protocol - return iter(iterable) - except TypeError: - return False + return hasattr(iterable, '__iter__') + + # if isinstance(iterable, basestring): + # # skip all forms of strings (str, unicode etc) + # return False + # try: + # # check if object implements iter protocol + # return iter(iterable) + # except TypeError: + # return False def fill(text, width=78): """ diff --git a/src/web/media/admin/__init__.py b/src/web/media/admin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/web/media/admin/css/base.css b/src/web/media/admin/css/base.css new file mode 120000 index 0000000000..3d84978d2d --- /dev/null +++ b/src/web/media/admin/css/base.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/base.css \ No newline at end of file diff --git a/src/web/media/admin/css/changelists.css b/src/web/media/admin/css/changelists.css new file mode 120000 index 0000000000..944a445346 --- /dev/null +++ b/src/web/media/admin/css/changelists.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/changelists.css \ No newline at end of file diff --git a/src/web/media/admin/css/dashboard.css b/src/web/media/admin/css/dashboard.css new file mode 120000 index 0000000000..82b7230804 --- /dev/null +++ b/src/web/media/admin/css/dashboard.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/dashboard.css \ No newline at end of file diff --git a/src/web/media/admin/css/forms.css b/src/web/media/admin/css/forms.css new file mode 120000 index 0000000000..8002fcab2e --- /dev/null +++ b/src/web/media/admin/css/forms.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/forms.css \ No newline at end of file diff --git a/src/web/media/admin/css/ie.css b/src/web/media/admin/css/ie.css new file mode 120000 index 0000000000..3c551ce3d3 --- /dev/null +++ b/src/web/media/admin/css/ie.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/ie.css \ No newline at end of file diff --git a/src/web/media/admin/css/login.css b/src/web/media/admin/css/login.css new file mode 120000 index 0000000000..948c4b3c9a --- /dev/null +++ b/src/web/media/admin/css/login.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/login.css \ No newline at end of file diff --git a/src/web/media/admin/css/rtl.css b/src/web/media/admin/css/rtl.css new file mode 120000 index 0000000000..57e4acfa0c --- /dev/null +++ b/src/web/media/admin/css/rtl.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/rtl.css \ No newline at end of file diff --git a/src/web/media/admin/css/widgets.css b/src/web/media/admin/css/widgets.css new file mode 120000 index 0000000000..5e4fa021b6 --- /dev/null +++ b/src/web/media/admin/css/widgets.css @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/css/widgets.css \ No newline at end of file diff --git a/src/web/media/admin/img/admin/arrow-down.gif b/src/web/media/admin/img/admin/arrow-down.gif new file mode 120000 index 0000000000..bf39b21cf8 --- /dev/null +++ b/src/web/media/admin/img/admin/arrow-down.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/arrow-down.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/arrow-up.gif b/src/web/media/admin/img/admin/arrow-up.gif new file mode 120000 index 0000000000..ae505a1f3b --- /dev/null +++ b/src/web/media/admin/img/admin/arrow-up.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/arrow-up.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/changelist-bg.gif b/src/web/media/admin/img/admin/changelist-bg.gif new file mode 120000 index 0000000000..41fb5aa7a2 --- /dev/null +++ b/src/web/media/admin/img/admin/changelist-bg.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/changelist-bg.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/changelist-bg_rtl.gif b/src/web/media/admin/img/admin/changelist-bg_rtl.gif new file mode 120000 index 0000000000..f1fd264f1e --- /dev/null +++ b/src/web/media/admin/img/admin/changelist-bg_rtl.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/changelist-bg_rtl.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/chooser-bg.gif b/src/web/media/admin/img/admin/chooser-bg.gif new file mode 120000 index 0000000000..ffc5fc657f --- /dev/null +++ b/src/web/media/admin/img/admin/chooser-bg.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/chooser-bg.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/chooser_stacked-bg.gif b/src/web/media/admin/img/admin/chooser_stacked-bg.gif new file mode 120000 index 0000000000..c9696db9ef --- /dev/null +++ b/src/web/media/admin/img/admin/chooser_stacked-bg.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/chooser_stacked-bg.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/default-bg-reverse.gif b/src/web/media/admin/img/admin/default-bg-reverse.gif new file mode 120000 index 0000000000..1b94863223 --- /dev/null +++ b/src/web/media/admin/img/admin/default-bg-reverse.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/default-bg-reverse.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/default-bg.gif b/src/web/media/admin/img/admin/default-bg.gif new file mode 120000 index 0000000000..44c1c9ca0c --- /dev/null +++ b/src/web/media/admin/img/admin/default-bg.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/default-bg.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/deleted-overlay.gif b/src/web/media/admin/img/admin/deleted-overlay.gif new file mode 120000 index 0000000000..eb6c2f465c --- /dev/null +++ b/src/web/media/admin/img/admin/deleted-overlay.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/deleted-overlay.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon-no.gif b/src/web/media/admin/img/admin/icon-no.gif new file mode 120000 index 0000000000..e86465cac6 --- /dev/null +++ b/src/web/media/admin/img/admin/icon-no.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-no.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon-unknown.gif b/src/web/media/admin/img/admin/icon-unknown.gif new file mode 120000 index 0000000000..df58b43536 --- /dev/null +++ b/src/web/media/admin/img/admin/icon-unknown.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-unknown.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon-yes.gif b/src/web/media/admin/img/admin/icon-yes.gif new file mode 120000 index 0000000000..9a9db8cdf6 --- /dev/null +++ b/src/web/media/admin/img/admin/icon-yes.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-yes.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_addlink.gif b/src/web/media/admin/img/admin/icon_addlink.gif new file mode 120000 index 0000000000..84d6fa140a --- /dev/null +++ b/src/web/media/admin/img/admin/icon_addlink.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_addlink.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_alert.gif b/src/web/media/admin/img/admin/icon_alert.gif new file mode 120000 index 0000000000..c28964ca97 --- /dev/null +++ b/src/web/media/admin/img/admin/icon_alert.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_alert.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_calendar.gif b/src/web/media/admin/img/admin/icon_calendar.gif new file mode 120000 index 0000000000..7ab7ef8be3 --- /dev/null +++ b/src/web/media/admin/img/admin/icon_calendar.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_calendar.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_changelink.gif b/src/web/media/admin/img/admin/icon_changelink.gif new file mode 120000 index 0000000000..3ca07e3c65 --- /dev/null +++ b/src/web/media/admin/img/admin/icon_changelink.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_changelink.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_clock.gif b/src/web/media/admin/img/admin/icon_clock.gif new file mode 120000 index 0000000000..bede3c2e4b --- /dev/null +++ b/src/web/media/admin/img/admin/icon_clock.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_clock.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_deletelink.gif b/src/web/media/admin/img/admin/icon_deletelink.gif new file mode 120000 index 0000000000..4ce9ab6c54 --- /dev/null +++ b/src/web/media/admin/img/admin/icon_deletelink.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_deletelink.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_error.gif b/src/web/media/admin/img/admin/icon_error.gif new file mode 120000 index 0000000000..aefe696250 --- /dev/null +++ b/src/web/media/admin/img/admin/icon_error.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_error.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_searchbox.png b/src/web/media/admin/img/admin/icon_searchbox.png new file mode 120000 index 0000000000..a881508cfd --- /dev/null +++ b/src/web/media/admin/img/admin/icon_searchbox.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_searchbox.png \ No newline at end of file diff --git a/src/web/media/admin/img/admin/icon_success.gif b/src/web/media/admin/img/admin/icon_success.gif new file mode 120000 index 0000000000..7e5c03a59d --- /dev/null +++ b/src/web/media/admin/img/admin/icon_success.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_success.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/inline-delete-8bit.png b/src/web/media/admin/img/admin/inline-delete-8bit.png new file mode 120000 index 0000000000..1cbd59cb86 --- /dev/null +++ b/src/web/media/admin/img/admin/inline-delete-8bit.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-delete-8bit.png \ No newline at end of file diff --git a/src/web/media/admin/img/admin/inline-delete.png b/src/web/media/admin/img/admin/inline-delete.png new file mode 120000 index 0000000000..c65c52a23d --- /dev/null +++ b/src/web/media/admin/img/admin/inline-delete.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-delete.png \ No newline at end of file diff --git a/src/web/media/admin/img/admin/inline-restore-8bit.png b/src/web/media/admin/img/admin/inline-restore-8bit.png new file mode 120000 index 0000000000..e84fa4bfc2 --- /dev/null +++ b/src/web/media/admin/img/admin/inline-restore-8bit.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-restore-8bit.png \ No newline at end of file diff --git a/src/web/media/admin/img/admin/inline-restore.png b/src/web/media/admin/img/admin/inline-restore.png new file mode 120000 index 0000000000..6fffcb3a3c --- /dev/null +++ b/src/web/media/admin/img/admin/inline-restore.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-restore.png \ No newline at end of file diff --git a/src/web/media/admin/img/admin/inline-splitter-bg.gif b/src/web/media/admin/img/admin/inline-splitter-bg.gif new file mode 120000 index 0000000000..b7b36bce18 --- /dev/null +++ b/src/web/media/admin/img/admin/inline-splitter-bg.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-splitter-bg.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/nav-bg-grabber.gif b/src/web/media/admin/img/admin/nav-bg-grabber.gif new file mode 120000 index 0000000000..b25acf858d --- /dev/null +++ b/src/web/media/admin/img/admin/nav-bg-grabber.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg-grabber.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/nav-bg-reverse.gif b/src/web/media/admin/img/admin/nav-bg-reverse.gif new file mode 120000 index 0000000000..1da4c71de5 --- /dev/null +++ b/src/web/media/admin/img/admin/nav-bg-reverse.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg-reverse.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/nav-bg.gif b/src/web/media/admin/img/admin/nav-bg.gif new file mode 120000 index 0000000000..cb591378f1 --- /dev/null +++ b/src/web/media/admin/img/admin/nav-bg.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector-add.gif b/src/web/media/admin/img/admin/selector-add.gif new file mode 120000 index 0000000000..a394f24cde --- /dev/null +++ b/src/web/media/admin/img/admin/selector-add.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-add.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector-addall.gif b/src/web/media/admin/img/admin/selector-addall.gif new file mode 120000 index 0000000000..0de6e49be5 --- /dev/null +++ b/src/web/media/admin/img/admin/selector-addall.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-addall.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector-remove.gif b/src/web/media/admin/img/admin/selector-remove.gif new file mode 120000 index 0000000000..2096d6fd9d --- /dev/null +++ b/src/web/media/admin/img/admin/selector-remove.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-remove.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector-removeall.gif b/src/web/media/admin/img/admin/selector-removeall.gif new file mode 120000 index 0000000000..3f0553e656 --- /dev/null +++ b/src/web/media/admin/img/admin/selector-removeall.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-removeall.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector-search.gif b/src/web/media/admin/img/admin/selector-search.gif new file mode 120000 index 0000000000..a3e36fac4d --- /dev/null +++ b/src/web/media/admin/img/admin/selector-search.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-search.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector_stacked-add.gif b/src/web/media/admin/img/admin/selector_stacked-add.gif new file mode 120000 index 0000000000..cdb51a4a2b --- /dev/null +++ b/src/web/media/admin/img/admin/selector_stacked-add.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector_stacked-add.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/selector_stacked-remove.gif b/src/web/media/admin/img/admin/selector_stacked-remove.gif new file mode 120000 index 0000000000..db05846439 --- /dev/null +++ b/src/web/media/admin/img/admin/selector_stacked-remove.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/selector_stacked-remove.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tool-left.gif b/src/web/media/admin/img/admin/tool-left.gif new file mode 120000 index 0000000000..838e6b3bf0 --- /dev/null +++ b/src/web/media/admin/img/admin/tool-left.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-left.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tool-left_over.gif b/src/web/media/admin/img/admin/tool-left_over.gif new file mode 120000 index 0000000000..293c42ba36 --- /dev/null +++ b/src/web/media/admin/img/admin/tool-left_over.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-left_over.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tool-right.gif b/src/web/media/admin/img/admin/tool-right.gif new file mode 120000 index 0000000000..a263d78c92 --- /dev/null +++ b/src/web/media/admin/img/admin/tool-right.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-right.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tool-right_over.gif b/src/web/media/admin/img/admin/tool-right_over.gif new file mode 120000 index 0000000000..c5a37340ad --- /dev/null +++ b/src/web/media/admin/img/admin/tool-right_over.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-right_over.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tooltag-add.gif b/src/web/media/admin/img/admin/tooltag-add.gif new file mode 120000 index 0000000000..a4805fd414 --- /dev/null +++ b/src/web/media/admin/img/admin/tooltag-add.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-add.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tooltag-add_over.gif b/src/web/media/admin/img/admin/tooltag-add_over.gif new file mode 120000 index 0000000000..e9a0899e03 --- /dev/null +++ b/src/web/media/admin/img/admin/tooltag-add_over.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-add_over.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tooltag-arrowright.gif b/src/web/media/admin/img/admin/tooltag-arrowright.gif new file mode 120000 index 0000000000..44d27fe724 --- /dev/null +++ b/src/web/media/admin/img/admin/tooltag-arrowright.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-arrowright.gif \ No newline at end of file diff --git a/src/web/media/admin/img/admin/tooltag-arrowright_over.gif b/src/web/media/admin/img/admin/tooltag-arrowright_over.gif new file mode 120000 index 0000000000..de9c0fbe37 --- /dev/null +++ b/src/web/media/admin/img/admin/tooltag-arrowright_over.gif @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-arrowright_over.gif \ No newline at end of file diff --git a/src/web/media/admin/img/gis/move_vertex_off.png b/src/web/media/admin/img/gis/move_vertex_off.png new file mode 120000 index 0000000000..b4d3920f59 --- /dev/null +++ b/src/web/media/admin/img/gis/move_vertex_off.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/gis/move_vertex_off.png \ No newline at end of file diff --git a/src/web/media/admin/img/gis/move_vertex_on.png b/src/web/media/admin/img/gis/move_vertex_on.png new file mode 120000 index 0000000000..8b4ad62e9e --- /dev/null +++ b/src/web/media/admin/img/gis/move_vertex_on.png @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/img/gis/move_vertex_on.png \ No newline at end of file diff --git a/src/web/media/admin/js/LICENSE-JQUERY.txt b/src/web/media/admin/js/LICENSE-JQUERY.txt new file mode 120000 index 0000000000..5c143b70fb --- /dev/null +++ b/src/web/media/admin/js/LICENSE-JQUERY.txt @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/LICENSE-JQUERY.txt \ No newline at end of file diff --git a/src/web/media/admin/js/SelectBox.js b/src/web/media/admin/js/SelectBox.js new file mode 120000 index 0000000000..6df92435ac --- /dev/null +++ b/src/web/media/admin/js/SelectBox.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/SelectBox.js \ No newline at end of file diff --git a/src/web/media/admin/js/SelectFilter2.js b/src/web/media/admin/js/SelectFilter2.js new file mode 120000 index 0000000000..c3871f5be5 --- /dev/null +++ b/src/web/media/admin/js/SelectFilter2.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/SelectFilter2.js \ No newline at end of file diff --git a/src/web/media/admin/js/__init__.py b/src/web/media/admin/js/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/web/media/admin/js/actions.js b/src/web/media/admin/js/actions.js new file mode 120000 index 0000000000..92663dd62d --- /dev/null +++ b/src/web/media/admin/js/actions.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/actions.js \ No newline at end of file diff --git a/src/web/media/admin/js/actions.min.js b/src/web/media/admin/js/actions.min.js new file mode 120000 index 0000000000..2c5304c0ff --- /dev/null +++ b/src/web/media/admin/js/actions.min.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/actions.min.js \ No newline at end of file diff --git a/src/web/media/admin/js/admin/DateTimeShortcuts.js b/src/web/media/admin/js/admin/DateTimeShortcuts.js new file mode 120000 index 0000000000..c795647cb6 --- /dev/null +++ b/src/web/media/admin/js/admin/DateTimeShortcuts.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/admin/DateTimeShortcuts.js \ No newline at end of file diff --git a/src/web/media/admin/js/admin/RelatedObjectLookups.js b/src/web/media/admin/js/admin/RelatedObjectLookups.js new file mode 120000 index 0000000000..a58f4b93fc --- /dev/null +++ b/src/web/media/admin/js/admin/RelatedObjectLookups.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/admin/RelatedObjectLookups.js \ No newline at end of file diff --git a/src/web/media/admin/js/admin/ordering.js b/src/web/media/admin/js/admin/ordering.js new file mode 120000 index 0000000000..8a3185cd70 --- /dev/null +++ b/src/web/media/admin/js/admin/ordering.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/admin/ordering.js \ No newline at end of file diff --git a/src/web/media/admin/js/calendar.js b/src/web/media/admin/js/calendar.js new file mode 120000 index 0000000000..035ce12756 --- /dev/null +++ b/src/web/media/admin/js/calendar.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/calendar.js \ No newline at end of file diff --git a/src/web/media/admin/js/collapse.js b/src/web/media/admin/js/collapse.js new file mode 120000 index 0000000000..6875eab294 --- /dev/null +++ b/src/web/media/admin/js/collapse.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/collapse.js \ No newline at end of file diff --git a/src/web/media/admin/js/collapse.min.js b/src/web/media/admin/js/collapse.min.js new file mode 120000 index 0000000000..21ab6366b2 --- /dev/null +++ b/src/web/media/admin/js/collapse.min.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/collapse.min.js \ No newline at end of file diff --git a/src/web/media/admin/js/compress.py b/src/web/media/admin/js/compress.py new file mode 120000 index 0000000000..8120dced9d --- /dev/null +++ b/src/web/media/admin/js/compress.py @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/compress.py \ No newline at end of file diff --git a/src/web/media/admin/js/core.js b/src/web/media/admin/js/core.js new file mode 120000 index 0000000000..91b7ee6a5a --- /dev/null +++ b/src/web/media/admin/js/core.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/core.js \ No newline at end of file diff --git a/src/web/media/admin/js/dateparse.js b/src/web/media/admin/js/dateparse.js new file mode 120000 index 0000000000..10a66c0ac9 --- /dev/null +++ b/src/web/media/admin/js/dateparse.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/dateparse.js \ No newline at end of file diff --git a/src/web/media/admin/js/getElementsBySelector.js b/src/web/media/admin/js/getElementsBySelector.js new file mode 120000 index 0000000000..8174d84bd4 --- /dev/null +++ b/src/web/media/admin/js/getElementsBySelector.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/getElementsBySelector.js \ No newline at end of file diff --git a/src/web/media/admin/js/inlines.js b/src/web/media/admin/js/inlines.js new file mode 120000 index 0000000000..f0f6b64672 --- /dev/null +++ b/src/web/media/admin/js/inlines.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/inlines.js \ No newline at end of file diff --git a/src/web/media/admin/js/inlines.min.js b/src/web/media/admin/js/inlines.min.js new file mode 120000 index 0000000000..78180c9827 --- /dev/null +++ b/src/web/media/admin/js/inlines.min.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/inlines.min.js \ No newline at end of file diff --git a/src/web/media/admin/js/jquery.init.js b/src/web/media/admin/js/jquery.init.js new file mode 120000 index 0000000000..21bc512554 --- /dev/null +++ b/src/web/media/admin/js/jquery.init.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/jquery.init.js \ No newline at end of file diff --git a/src/web/media/admin/js/prepopulate.js b/src/web/media/admin/js/prepopulate.js new file mode 120000 index 0000000000..6cbd5dca85 --- /dev/null +++ b/src/web/media/admin/js/prepopulate.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/prepopulate.js \ No newline at end of file diff --git a/src/web/media/admin/js/prepopulate.min.js b/src/web/media/admin/js/prepopulate.min.js new file mode 120000 index 0000000000..2135ad66e2 --- /dev/null +++ b/src/web/media/admin/js/prepopulate.min.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/prepopulate.min.js \ No newline at end of file diff --git a/src/web/media/admin/js/timeparse.js b/src/web/media/admin/js/timeparse.js new file mode 120000 index 0000000000..63cca77774 --- /dev/null +++ b/src/web/media/admin/js/timeparse.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/timeparse.js \ No newline at end of file diff --git a/src/web/media/admin/js/urlify.js b/src/web/media/admin/js/urlify.js new file mode 120000 index 0000000000..bd3a5aa8ca --- /dev/null +++ b/src/web/media/admin/js/urlify.js @@ -0,0 +1 @@ +/usr/share/pyshared/django/contrib/admin/media/js/urlify.js \ No newline at end of file diff --git a/src/web/media/css/webclient.css b/src/web/media/css/webclient.css new file mode 100644 index 0000000000..1a88f0b604 --- /dev/null +++ b/src/web/media/css/webclient.css @@ -0,0 +1,79 @@ +/* +Style sheet for Evennia's web client. + +Possibly this should somehow be incoorporated with the +overall website theme in the future? +*/ + +body{ + background:#000; + color:#fff; + font-size:.9em; +} +/*Base style for new messages in the main message area.*/ +.msg{ + background:#000; + padding:.2em; + border-bottom:1px #000 solid +} +/*Utility messages (green)*/ +.sys{ + color:#0f0; +} +/*Messages echoed back after input*/ +.inp{ + color:#555; +} +/*Messages returned from the server (most messages) */ +.out{ + color:#aaa; +} +/*Error messages (red)*/ +.err{ + color:#f00; +} +/*Container surrounding entire chat*/ +#wrapper{ + margin-left:10px; +} +/*Main scrolling message area*/ +#messagewindow{ + overflow:auto; + height:400px; + bottom:10px +} +/*Input area containing input field and button*/ +#inputform{ + position:fixed; + bottom:0px; +} +/*Input field */ +#inputfield{ + background:#000; + color:#fff; + font-size:.9em; + width:120ex; + border:1px solid; + padding:4px; + border-color:#555; + outline-style:none; + margin-left:10px; +} +/*Input 'send' button*/ +#inputsend{ + background:#000; + color:#fff; + font-size:.9em; + border:1px solid; + padding: 3px; + border-color:#555; +} +/*No javascript warning*/ +#noscript{ + color:f00; +} +/*Example player count display */ +#playercount{ + margin-left:10px; + color: #555; +} \ No newline at end of file diff --git a/src/web/media/javascript/evennia_webclient.js b/src/web/media/javascript/evennia_webclient.js new file mode 100644 index 0000000000..aff7152ac1 --- /dev/null +++ b/src/web/media/javascript/evennia_webclient.js @@ -0,0 +1,212 @@ +/* + +Evennia ajax webclient (javascript component) + +The client is composed of several parts: + templates/webclient.html - the main page + webclient/views.py - the django view serving the template (based on urls.py pattern) + src/server/webclient.py - the server component receiving requests from the client + this file - the javascript component handling dynamic ajax content + +This implements an ajax mud client for use with Evennia, using jQuery +for simplicity. It communicates with the Twisted server on the address +/webclientdata through POST requests. Each request must at least +contain the 'mode' of the request to be handled by the protocol: + mode 'receive' - tell the server that we are ready to receive data. This is a + long-polling (comet-style) request since the server + will not reply until it actually has data available. + The returned data object has two variables 'msg' and 'data' + where msg should be output and 'data' is an arbitrary piece + of data the server and client understands (not used in default + client). + mode 'input' - the user has input data on some form. The POST request + should also contain variables 'msg' and 'data' where + the 'msg' is a string and 'data' is an arbitrary piece + of data from the client that the server knows how to + deal with (not used in this example client). + mode 'init' - starts the connection. All setup the server is requered to do + should happen at this point. The server returns a data object + with the 'msg' property containing the server address. + +*/ + + +// jQuery must be imported by the calling html page before this script +// (it comes with Evennia, in media/javascript/jquery-.js) +// There are plenty of help on using the jQuery library on http://jquery.com/ + +// Server communications + +function webclient_receive(){ + // This starts an asynchronous long-polling request. It will either timeout + // or receive data from the 'receivedata' url. In both cases a new request will + // immediately be started. + + $.ajax({ + type: "POST", + url: "/webclientdata", + async: true, // Turns off browser loading indicator + cache: false, // Forces browser reload independent of cache + timeout:30000, // Timeout in ms. After this time a new long-poll will be started. + dataType:"json", + data: {mode:'receive'}, + + // callback methods + + success: function(data){ // called when request to waitreceive completes + msg_display("out", data.msg); // Add response to the message area + webclient_receive(); // immediately start a new request + }, + error: function(XMLHttpRequest, textStatus, errorThrown){ + webclient_receive(); // A possible timeout. Resend request immediately + }, + }); +}; + +function webclient_input(){ + // Send an input from the player to the server + + var outmsg = $("#inputfield").val() // get data from form + + $.ajax({ + type: "POST", + url: "/webclientdata", + async: true, + cache: false, + timeout: 30000, + data: {mode:'input', msg:outmsg, data:'NoData'}, + + //callback methods + + success: function(data){ + //if (outmsg.length > 0 ) msg_display("inp", outmsg) // echo input on command line + history_add(outmsg); + HISTORY_POS = 0; + $('#inputform')[0].reset() // clear input field + }, + error: function(XMLHttpRequest, textStatus, errorThrown){ + msg_display("err", "Error: Server returned an error or timed out. Try resending."); + }, + }) +} + +function webclient_init(){ + // Start the connection by making sure the server is ready + + $.ajax({ + type: "POST", + url: "/webclientdata", + async: true, + cache: false, + timeout: 50000, + dataType:"json", + data: {mode:'init'}, + + // callback methods + + success: function(data){ // called when request to initdata completes + $("#connecting").remove() // remove the "connecting ..." message. + + setTimeout(function () { // a small timeout to stop 'loading' indicator in Chrome + $("#playercount").fadeOut('slow'); + }, 10000); + + // Report success + msg_display('sys',"Connected to " + data.msg + ".") + + // Wait for input + webclient_receive(); + }, + error: function(XMLHttpRequest, textStatus, errorThrown){ + msg_display("err", "Connection error ..." + " (" + errorThrown + ")"); + setTimeout('webclient_receive()', 15000); // try again after 15 seconds + }, + }); +} + +// Display messages + +function msg_display(type, msg){ + // Add a div to the message window. + // type givews the class of div to use. + $("#messagewindow").append( + "
"+ msg +"
"); + // scroll message window to bottom + $('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight}); +} + +// Input history mechanism + +var HISTORY_MAX_LENGTH = 21 +var HISTORY = new Array(); +HISTORY[0] = ''; +var HISTORY_POS = 0; + +function history_step_back() { + // step backwards in history stack + HISTORY_POS = Math.min(++HISTORY_POS, HISTORY.length-1); + return HISTORY[HISTORY.length-1 - HISTORY_POS]; +} +function history_step_fwd() { + // step forward in history stack + HISTORY_POS = Math.max(--HISTORY_POS, 0); + return HISTORY[HISTORY.length-1 - HISTORY_POS]; +} +function history_add(input) { + // add an entry to history + if (input != HISTORY[HISTORY.length-1]) { + if (HISTORY.length >= HISTORY_MAX_LENGTH) { + HISTORY.shift() // kill oldest history entry + } + HISTORY[HISTORY.length-1] = input + HISTORY[HISTORY.length] = '' + } +} + +// Catching keyboard shortcuts + +$(document).keypress( function(event) { + var code = event.keyCode ? event.keyCode : event.which + + // always focus input field + $("#inputfield")[0].focus(); + if (code == 13) { // Enter key + webclient_input(); + event.preventDefault(); + return false; + } + else { + if (code == 38) { // arrow up + $("#inputfield").val(function(index, value){ + return history_step_back(); + }); + } + else if (code == 40) { // arrow down + $("#inputfield").val(function(index, value){ + return history_step_fwd(); + }); + } + } +}); + +// handler to avoid double-clicks until the ajax request finishes +$("#inputsend").one("click", webclient_input) + +// Callback function - called when page has finished loading (gets things going) +$(document).ready(function(){ + + // remove the "no javascript" warning, since we obviously have javascript + $('#noscript').remove() + + // set sizes of elements and reposition them + var win_h = $(document).height(); + var win_w = $('#wrapper').width(); + var inp_h = $('#inputform').height(); + var inp_w = $('#inputsend').width() + $("#messagewindow").css({'height':win_h-inp_h - 20}); + $("#inputfield").css({'width':win_w-inp_w - 20}); + + setTimeout(function () { // a small timeout to stop 'loading' indicator in Chrome + webclient_init(); + }, 500); +}); \ No newline at end of file diff --git a/src/web/media/javascript/jquery-1.4.4.js b/src/web/media/javascript/jquery-1.4.4.js new file mode 100644 index 0000000000..a4f114586c --- /dev/null +++ b/src/web/media/javascript/jquery-1.4.4.js @@ -0,0 +1,7179 @@ +/*! + * jQuery JavaScript Library v1.4.4 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Nov 11 19:04:53 2010 -0500 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, + + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + rwhite = /\s/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Check for non-word characters + rnonword = /\W/, + + // Check for digits + rdigit = /\d/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // Has the ready events already been bound? + readyBound = false, + + // The functions to execute on DOM ready + readyList = [], + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $("TAG") + } else if ( !context && !rnonword.test( selector ) ) { + this.selector = selector; + this.context = document; + selector = document.getElementsByTagName( selector ); + return jQuery.merge( this, selector ); + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return (context || rootjQuery).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return jQuery( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if (selector.selector !== undefined) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.4.4", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + (this.selector ? " " : "") + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // If the DOM is already ready + if ( jQuery.isReady ) { + // Execute the function immediately + fn.call( document, jQuery ); + + // Otherwise, remember the function for later + } else if ( readyList ) { + // Add the function to the wait list + readyList.push( fn ); + } + + return this; + }, + + eq: function( i ) { + return i === -1 ? + this.slice( i ) : + this.slice( i, +i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || jQuery(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + // A third-party is pushing the ready event forwards + if ( wait === true ) { + jQuery.readyWait--; + } + + // Make sure that the DOM is not already loaded + if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + if ( readyList ) { + // Execute all of them + var fn, + i = 0, + ready = readyList; + + // Reset the list of functions + readyList = null; + + while ( (fn = ready[ i++ ]) ) { + fn.call( document, jQuery ); + } + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).unbind( "ready" ); + } + } + } + }, + + bindReady: function() { + if ( readyBound ) { + return; + } + + readyBound = true; + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNaN: function( obj ) { + return obj == null || !rdigit.test( obj ) || isNaN( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw msg; + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test(data.replace(rvalidescape, "@") + .replace(rvalidtokens, "]") + .replace(rvalidbraces, "")) ) { + + // Try to use the native JSON parser first + return window.JSON && window.JSON.parse ? + window.JSON.parse( data ) : + (new Function("return " + data))(); + + } else { + jQuery.error( "Invalid JSON: " + data ); + } + }, + + noop: function() {}, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && rnotwhite.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + + if ( jQuery.support.scriptEval ) { + script.appendChild( document.createTextNode( data ) ); + } else { + script.text = data; + } + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction(object); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // The extra typeof function check is to prevent crashes + // in Safari 2 (See: #3039) + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type(array); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var ret = [], value; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, length = elems.length; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + proxy: function( fn, proxy, thisObject ) { + if ( arguments.length === 2 ) { + if ( typeof proxy === "string" ) { + thisObject = fn; + fn = thisObject[ proxy ]; + proxy = undefined; + + } else if ( proxy && !jQuery.isFunction( proxy ) ) { + thisObject = proxy; + proxy = undefined; + } + } + + if ( !proxy && fn ) { + proxy = function() { + return fn.apply( thisObject || this, arguments ); + }; + } + + // Set the guid of unique handler to the same of original handler, so it can be removed + if ( fn ) { + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + } + + // So proxy can be declared as an argument + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can be optionally by executed if its a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return (new Date()).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +if ( indexOf ) { + jQuery.inArray = function( elem, array ) { + return indexOf.call( array, elem ); + }; +} + +// Verify that \s matches non-breaking spaces +// (IE fails on this test) +if ( !rwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +// Expose jQuery to the global object +return (window.jQuery = window.$ = jQuery); + +})(); + + +(function() { + + jQuery.support = {}; + + var root = document.documentElement, + script = document.createElement("script"), + div = document.createElement("div"), + id = "script" + jQuery.now(); + + div.style.display = "none"; + div.innerHTML = "
a"; + + var all = div.getElementsByTagName("*"), + a = div.getElementsByTagName("a")[0], + select = document.createElement("select"), + opt = select.appendChild( document.createElement("option") ); + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return; + } + + jQuery.support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText insted) + style: /red/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55$/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: div.getElementsByTagName("input")[0].value === "on", + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Will be defined later + deleteExpando: true, + optDisabled: false, + checkClone: false, + scriptEval: false, + noCloneEvent: true, + boxModel: null, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableHiddenOffsets: true + }; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as diabled) + select.disabled = true; + jQuery.support.optDisabled = !opt.disabled; + + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} + + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support.scriptEval = true; + delete window[ id ]; + } + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete script.test; + + } catch(e) { + jQuery.support.deleteExpando = false; + } + + root.removeChild( script ); + + if ( div.attachEvent && div.fireEvent ) { + div.attachEvent("onclick", function click() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + jQuery.support.noCloneEvent = false; + div.detachEvent("onclick", click); + }); + div.cloneNode(true).fireEvent("onclick"); + } + + div = document.createElement("div"); + div.innerHTML = ""; + + var fragment = document.createDocumentFragment(); + fragment.appendChild( div.firstChild ); + + // WebKit doesn't clone checked state correctly in fragments + jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + + // Figure out if the W3C box model works as expected + // document.body must exist before we can do this + jQuery(function() { + var div = document.createElement("div"); + div.style.width = div.style.paddingLeft = "1px"; + + document.body.appendChild( div ); + jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + + if ( "zoom" in div.style ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
"; + jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; + } + + div.innerHTML = "
t
"; + var tds = div.getElementsByTagName("td"); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + + tds[0].style.display = ""; + tds[1].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE < 8 fail this test) + jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; + div.innerHTML = ""; + + document.body.removeChild( div ).style.display = "none"; + div = tds = null; + }); + + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); + + // release memory in IE + root = script = div = all = a = null; +})(); + + + +var windowData = {}, + rbrace = /^(?:\{.*\}|\[.*\])$/; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + expando: "jQuery" + jQuery.now(), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + data: function( elem, name, data ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + elem = elem == window ? + windowData : + elem; + + var isNode = elem.nodeType, + id = isNode ? elem[ jQuery.expando ] : null, + cache = jQuery.cache, thisCache; + + if ( isNode && !id && typeof name === "string" && data === undefined ) { + return; + } + + // Get the data from the object directly + if ( !isNode ) { + cache = elem; + + // Compute a unique ID for the element + } else if ( !id ) { + elem[ jQuery.expando ] = id = ++jQuery.uuid; + } + + // Avoid generating a new cache unless none exists and we + // want to manipulate it. + if ( typeof name === "object" ) { + if ( isNode ) { + cache[ id ] = jQuery.extend(cache[ id ], name); + + } else { + jQuery.extend( cache, name ); + } + + } else if ( isNode && !cache[ id ] ) { + cache[ id ] = {}; + } + + thisCache = isNode ? cache[ id ] : cache; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) { + thisCache[ name ] = data; + } + + return typeof name === "string" ? thisCache[ name ] : thisCache; + }, + + removeData: function( elem, name ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + elem = elem == window ? + windowData : + elem; + + var isNode = elem.nodeType, + id = isNode ? elem[ jQuery.expando ] : elem, + cache = jQuery.cache, + thisCache = isNode ? cache[ id ] : id; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( thisCache ) { + // Remove the section of cache data + delete thisCache[ name ]; + + // If we've removed all the data, remove the element's cache + if ( isNode && jQuery.isEmptyObject(thisCache) ) { + jQuery.removeData( elem ); + } + } + + // Otherwise, we want to remove all of the element's data + } else { + if ( isNode && jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + + // Completely remove the data cache + } else if ( isNode ) { + delete cache[ id ]; + + // Remove all fields from the object + } else { + for ( var n in elem ) { + delete elem[ n ]; + } + } + } + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var data = null; + + if ( typeof key === "undefined" ) { + if ( this.length ) { + var attr = this[0].attributes, name; + data = jQuery.data( this[0] ); + + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = name.substr( 5 ); + dataAttr( this[0], name, data[ name ] ); + } + } + } + + return data; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + data = dataAttr( this[0], key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var $this = jQuery( this ), + args = [ parts[0], value ]; + + $this.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + $this.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + data = elem.getAttribute( "data-" + key ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + !jQuery.isNaN( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + + + + +jQuery.extend({ + queue: function( elem, type, data ) { + if ( !elem ) { + return; + } + + type = (type || "fx") + "queue"; + var q = jQuery.data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( !data ) { + return q || []; + } + + if ( !q || jQuery.isArray(data) ) { + q = jQuery.data( elem, type, jQuery.makeArray(data) ); + + } else { + q.push( data ); + } + + return q; + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(); + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift("inprogress"); + } + + fn.call(elem, function() { + jQuery.dequeue(elem, type); + }); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function( i ) { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + type = type || "fx"; + + return this.queue( type, function() { + var elem = this; + setTimeout(function() { + jQuery.dequeue( elem, type ); + }, time ); + }); + }, + + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + } +}); + + + + +var rclass = /[\n\t]/g, + rspaces = /\s+/, + rreturn = /\r/g, + rspecialurl = /^(?:href|src|style)$/, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rradiocheck = /^(?:radio|checkbox)$/i; + +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name, fn ) { + return this.each(function(){ + jQuery.attr( this, name, "" ); + if ( this.nodeType === 1 ) { + this.removeAttribute( name ); + } + }); + }, + + addClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.addClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( value && typeof value === "string" ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className ) { + elem.className = value; + + } else { + var className = " " + elem.className + " ", + setClass = elem.className; + + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { + setClass += " " + classNames[c]; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.removeClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + var className = (" " + elem.className + " ").replace(rclass, " "); + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[c] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this); + self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspaces ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery.data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " "; + for ( var i = 0, l = this.length; i < l; i++ ) { + if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + if ( !arguments.length ) { + var elem = this[0]; + + if ( elem ) { + if ( jQuery.nodeName( elem, "option" ) ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { + return elem.getAttribute("value") === null ? "on" : elem.value; + } + + + // Everything else, we just grab the value + return (elem.value || "").replace(rreturn, ""); + + } + + return undefined; + } + + var isFunction = jQuery.isFunction(value); + + return this.each(function(i) { + var self = jQuery(this), val = value; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call(this, i, self.val()); + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray(val) ) { + val = jQuery.map(val, function (value) { + return value == null ? "" : value + ""; + }); + } + + if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { + this.checked = jQuery.inArray( self.val(), val ) >= 0; + + } else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(val); + + jQuery( "option", this ).each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + this.selectedIndex = -1; + } + + } else { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + // don't set attributes on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery(elem)[name](value); + } + + var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), + // Whether we are setting (or getting) + set = value !== undefined; + + // Try to normalize/fix the name + name = notxml && jQuery.props[ name ] || name; + + // These attributes require special treatment + var special = rspecialurl.test( name ); + + // Safari mis-reports the default selected property of an option + // Accessing the parent's selectedIndex property fixes it + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + + // If applicable, access the attribute via the DOM 0 way + // 'in' checks fail in Blackberry 4.7 #6931 + if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { + if ( set ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } + + if ( value === null ) { + if ( elem.nodeType === 1 ) { + elem.removeAttribute( name ); + } + + } else { + elem[ name ] = value; + } + } + + // browsers index elements by id/name on forms, give priority to attributes. + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { + return elem.getAttributeNode( name ).nodeValue; + } + + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + if ( name === "tabIndex" ) { + var attributeNode = elem.getAttributeNode( "tabIndex" ); + + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + + return elem[ name ]; + } + + if ( !jQuery.support.style && notxml && name === "style" ) { + if ( set ) { + elem.style.cssText = "" + value; + } + + return elem.style.cssText; + } + + if ( set ) { + // convert the value to a string (all browsers do this but IE) see #1070 + elem.setAttribute( name, "" + value ); + } + + // Ensure that missing attributes return undefined + // Blackberry 4.7 returns "" from getAttribute #6938 + if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { + return undefined; + } + + var attr = !jQuery.support.hrefNormalized && notxml && special ? + // Some attributes require a special call on IE + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return attr === null ? undefined : attr; + } +}); + + + + +var rnamespaces = /\.(.*)$/, + rformElems = /^(?:textarea|input|select)$/i, + rperiod = /\./g, + rspace = / /g, + rescape = /[^\w\s.|`]/g, + fcleanup = function( nm ) { + return nm.replace(rescape, "\\$&"); + }, + focusCounts = { focusin: 0, focusout: 0 }; + +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code originated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { + + // Bind an event to an element + // Original by Dean Edwards + add: function( elem, types, handler, data ) { + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { + elem = window; + } + + if ( handler === false ) { + handler = returnFalse; + } else if ( !handler ) { + // Fixes bug #7229. Fix recommended by jdalton + return; + } + + var handleObjIn, handleObj; + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure + var elemData = jQuery.data( elem ); + + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; + } + + // Use a key less likely to result in collisions for plain JS objects. + // Fixes bug #7150. + var eventKey = elem.nodeType ? "events" : "__events__", + events = elemData[ eventKey ], + eventHandle = elemData.handle; + + if ( typeof events === "function" ) { + // On plain objects events is a fn that holds the the data + // which prevents this data from being JSON serialized + // the function does not need to be called, it just contains the data + eventHandle = events.handle; + events = events.events; + + } else if ( !events ) { + if ( !elem.nodeType ) { + // On plain objects, create a fn that acts as the holder + // of the values to avoid JSON serialization of event data + elemData[ eventKey ] = elemData = function(){}; + } + + elemData.events = events = {}; + } + + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + return typeof jQuery !== "undefined" && !jQuery.event.triggered ? + jQuery.event.handle.apply( eventHandle.elem, arguments ) : + undefined; + }; + } + + // Add elem as a property of the handle function + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = types.split(" "); + + var type, i = 0, namespaces; + + while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + + // Namespaced event handlers + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); + + } else { + namespaces = []; + handleObj.namespace = ""; + } + + handleObj.type = type; + if ( !handleObj.guid ) { + handleObj.guid = handler.guid; + } + + // Get the current list of functions bound to this event + var handlers = events[ type ], + special = jQuery.event.special[ type ] || {}; + + // Init the event handler queue + if ( !handlers ) { + handlers = events[ type ] = []; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add the function to the element's handler list + handlers.push( handleObj ); + + // Keep track of which events have been used, for global triggering + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, pos ) { + // don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + eventKey = elem.nodeType ? "events" : "__events__", + elemData = jQuery.data( elem ), + events = elemData && elemData[ eventKey ]; + + if ( !elemData || !events ) { + return; + } + + if ( typeof events === "function" ) { + elemData = events; + events = events.events; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } + } + + continue; + } + + special = jQuery.event.special[ type ] || {}; + + for ( j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); + } + + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + + if ( pos != null ) { + break; + } + } + } + + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( typeof elemData === "function" ) { + jQuery.removeData( elem, eventKey ); + + } else if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem ); + } + } + }, + + // bubbling is internal + trigger: function( event, data, elem /*, bubbling */ ) { + // Event object or event type + var type = event.type || event, + bubbling = arguments[3]; + + if ( !bubbling ) { + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + jQuery.extend( jQuery.Event(type), event ) : + // Just the event type (string) + jQuery.Event(type); + + if ( type.indexOf("!") >= 0 ) { + event.type = type = type.slice(0, -1); + event.exclusive = true; + } + + // Handle a global trigger + if ( !elem ) { + // Don't bubble custom events when global (to avoid too much overhead) + event.stopPropagation(); + + // Only trigger if we've ever bound an event for it + if ( jQuery.event.global[ type ] ) { + jQuery.each( jQuery.cache, function() { + if ( this.events && this.events[type] ) { + jQuery.event.trigger( event, data, this.handle.elem ); + } + }); + } + } + + // Handle triggering a single element + + // don't do events on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + // Clean up in case it is reused + event.result = undefined; + event.target = elem; + + // Clone the incoming data, if any + data = jQuery.makeArray( data ); + data.unshift( event ); + } + + event.currentTarget = elem; + + // Trigger the event, it is assumed that "handle" is a function + var handle = elem.nodeType ? + jQuery.data( elem, "handle" ) : + (jQuery.data( elem, "__events__" ) || {}).handle; + + if ( handle ) { + handle.apply( elem, data ); + } + + var parent = elem.parentNode || elem.ownerDocument; + + // Trigger an inline bound script + try { + if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { + if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { + event.result = false; + event.preventDefault(); + } + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (inlineError) {} + + if ( !event.isPropagationStopped() && parent ) { + jQuery.event.trigger( event, data, parent, true ); + + } else if ( !event.isDefaultPrevented() ) { + var old, + target = event.target, + targetType = type.replace( rnamespaces, "" ), + isClick = jQuery.nodeName( target, "a" ) && targetType === "click", + special = jQuery.event.special[ targetType ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + + try { + if ( target[ targetType ] ) { + // Make sure that we don't accidentally re-trigger the onFOO events + old = target[ "on" + targetType ]; + + if ( old ) { + target[ "on" + targetType ] = null; + } + + jQuery.event.triggered = true; + target[ targetType ](); + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (triggerError) {} + + if ( old ) { + target[ "on" + targetType ] = old; + } + + jQuery.event.triggered = false; + } + } + }, + + handle: function( event ) { + var all, handlers, namespaces, namespace_re, events, + namespace_sort = [], + args = jQuery.makeArray( arguments ); + + event = args[0] = jQuery.event.fix( event || window.event ); + event.currentTarget = this; + + // Namespaced event handlers + all = event.type.indexOf(".") < 0 && !event.exclusive; + + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace_sort = namespaces.slice(0).sort(); + namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.namespace = event.namespace || namespace_sort.join("."); + + events = jQuery.data(this, this.nodeType ? "events" : "__events__"); + + if ( typeof events === "function" ) { + events = events.events; + } + + handlers = (events || {})[ event.type ]; + + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + + // Filter the functions by class + if ( all || namespace_re.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + } + + return event.result; + }, + + props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // store a copy of the original event object + // and "clone" to set read-only properties + var originalEvent = event; + event = jQuery.Event( originalEvent ); + + for ( var i = this.props.length, prop; i; ) { + prop = this.props[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary + if ( !event.target ) { + // Fixes #1925 where srcElement might not be defined either + event.target = event.srcElement || document; + } + + // check if target is a textnode (safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var doc = document.documentElement, + body = document.body; + + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Add which for key events + if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { + event.which = event.charCode != null ? event.charCode : event.keyCode; + } + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) { + event.metaKey = event.ctrlKey; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button !== undefined ) { + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } + + return event; + }, + + // Deprecated, use jQuery.guid instead + guid: 1E8, + + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady, + teardown: jQuery.noop + }, + + live: { + add: function( handleObj ) { + jQuery.event.add( this, + liveConvert( handleObj.origType, handleObj.selector ), + jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); + }, + + remove: function( handleObj ) { + jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); + } + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src ) { + // Allow instantiation without the 'new' keyword + if ( !this.preventDefault ) { + return new jQuery.Event( src ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + // Event type + } else { + this.type = src; + } + + // timeStamp is buggy for some events on Firefox(#3843) + // So we won't rely on the native value + this.timeStamp = jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function( event ) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { + parent = parent.parentNode; + } + + if ( parent !== this ) { + // set the correct event type + event.type = event.data; + + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } +}, + +// In case of event delegation, we only need to rename the event.type, +// liveHandler will take care of the rest. +delegate = function( event ) { + event.type = event.data; + jQuery.event.handle.apply( this, arguments ); +}; + +// Create mouseenter and mouseleave events +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + setup: function( data ) { + jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); + }, + teardown: function( data ) { + jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + } + }; +}); + +// submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, + type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, + type = elem.type; + + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); + } + }; + +} + +// change delegation, happens here so we have bind. +if ( !jQuery.support.changeBubbles ) { + + var changeFilters, + + getVal = function( elem ) { + var type = elem.type, val = elem.value; + + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; + + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; + + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; + } + + return val; + }, + + testChange = function testChange( e ) { + var elem = e.target, data, val; + + if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { + return; + } + + data = jQuery.data( elem, "_change_data" ); + val = getVal(elem); + + // the current data will be also retrieved by beforeactivate + if ( e.type !== "focusout" || elem.type !== "radio" ) { + jQuery.data( elem, "_change_data", val ); + } + + if ( data === undefined || val === data ) { + return; + } + + if ( data != null || val ) { + e.type = "change"; + e.liveFired = undefined; + return jQuery.event.trigger( e, arguments[1], elem ); + } + }; + + jQuery.event.special.change = { + filters: { + focusout: testChange, + + beforedeactivate: testChange, + + click: function( e ) { + var elem = e.target, type = elem.type; + + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, + + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; + + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, + + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information + beforeactivate: function( e ) { + var elem = e.target; + jQuery.data( elem, "_change_data", getVal(elem) ); + } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return rformElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return rformElems.test( this.nodeName ); + } + }; + + changeFilters = jQuery.event.special.change.filters; + + // Handle when the input is .focus()'d + changeFilters.focus = changeFilters.beforeactivate; +} + +function trigger( type, elem, args ) { + args[0].type = type; + return jQuery.event.handle.apply( elem, args ); +} + +// Create "bubbling" focus and blur events +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + if ( focusCounts[fix]++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --focusCounts[fix] === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.trigger( e, null, e.target ); + } + }); +} + +jQuery.each(["bind", "one"], function( i, name ) { + jQuery.fn[ name ] = function( type, data, fn ) { + // Handle object literals + if ( typeof type === "object" ) { + for ( var key in type ) { + this[ name ](key, data, type[key], fn); + } + return this; + } + + if ( jQuery.isFunction( data ) || data === false ) { + fn = data; + data = undefined; + } + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }) : fn; + + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; + }; +}); + +jQuery.fn.extend({ + unbind: function( type, fn ) { + // Handle object literals + if ( typeof type === "object" && !type.preventDefault ) { + for ( var key in type ) { + this.unbind(key, type[key]); + } + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } + } + + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + + triggerHandler: function( type, data ) { + if ( this[0] ) { + var event = jQuery.Event( type ); + event.preventDefault(); + event.stopPropagation(); + jQuery.event.trigger( event, data, this[0] ); + return event.result; + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + i = 1; + + // link all the functions, so any of them can unbind this click handler + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); + } + + return this.click( jQuery.proxy( fn, function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + })); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + +jQuery.each(["live", "die"], function( i, name ) { + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); + + if ( typeof types === "object" && !types.preventDefault ) { + for ( var key in types ) { + context[ name ]( key, data, types[key], selector ); + } + + return this; + } + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + types = (types || "").split(" "); + + while ( (type = types[ i++ ]) != null ) { + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + + if ( name === "live" ) { + // bind live handler + for ( var j = 0, l = context.length; j < l; j++ ) { + jQuery.event.add( context[j], "live." + liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + } + + } else { + // unbind live handler + context.unbind( "live." + liveConvert( type, selector ), fn ); + } + } + + return this; + }; +}); + +function liveHandler( event ) { + var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + elems = [], + selectors = [], + events = jQuery.data( this, this.nodeType ? "events" : "__events__" ); + + if ( typeof events === "function" ) { + events = events.events; + } + + // Make sure we avoid non-left-click bubbling in Firefox (#3861) + if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { + return; + } + + if ( event.namespace ) { + namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); + + } else { + live.splice( j--, 1 ); + } + } + + match = jQuery( event.target ).closest( selectors, event.currentTarget ); + + for ( i = 0, l = match.length; i < l; i++ ) { + close = match[i]; + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { + elem = close.elem; + related = null; + + // Those two events require additional checking + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + event.type = handleObj.preType; + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; + } + + if ( !related || related !== elem ) { + elems.push({ elem: elem, handleObj: handleObj, level: close.level }); + } + } + } + } + + for ( i = 0, l = elems.length; i < l; i++ ) { + match = elems[i]; + + if ( maxLevel && match.level > maxLevel ) { + break; + } + + event.currentTarget = match.elem; + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + ret = match.handleObj.origHandler.apply( match.elem, arguments ); + + if ( ret === false || event.isPropagationStopped() ) { + maxLevel = match.level; + + if ( ret === false ) { + stop = false; + } + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + + return stop; +} + +function liveConvert( type, selector ) { + return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); +} + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.bind( name, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } +}); + +// Prevent memory leaks in IE +// Window isn't included so as not to unbind existing unload events +// More info: +// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ +if ( window.attachEvent && !window.addEventListener ) { + jQuery(window).bind("unload", function() { + for ( var id in jQuery.cache ) { + if ( jQuery.cache[ id ].handle ) { + // Try/Catch is to handle iframes being unloaded, see #4280 + try { + jQuery.event.remove( jQuery.cache[ id ].handle.elem ); + } catch(e) {} + } + } + }); +} + + +/*! + * Sizzle CSS Selector Engine - v1.0 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var match, + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = context.getElementsByTagName( "*" ); + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var found, item, + filter = Expr.filter[ type ], + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !/\W/.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + return context.getElementsByTagName( match[1] ); + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace(/\\/g, ""); + }, + + TAG: function( match, curLoop ) { + return match[1].toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + return "text" === elem.type; + }, + radio: function( elem ) { + return "radio" === elem.type; + }, + + checkbox: function( elem ) { + return "checkbox" === elem.type; + }, + + file: function( elem ) { + return "file" === elem.type; + }, + password: function( elem ) { + return "password" === elem.type; + }, + + submit: function( elem ) { + return "submit" === elem.type; + }, + + image: function( elem ) { + return "image" === elem.type; + }, + + reset: function( elem ) { + return "reset" === elem.type; + }, + + button: function( elem ) { + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( "Syntax error, unrecognized expression: " + name ); + } + }, + + CHILD: function( elem, match ) { + var type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + var first = match[2], + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // If the nodes are siblings (or identical) we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } + + return ret; +}; + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Make sure that attribute selectors are quoted + query = query.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + if ( context.nodeType === 9 ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var old = context.getAttribute( "id" ), + nid = old || id; + + if ( !old ) { + context.setAttribute( "id", nid ); + } + + try { + return makeArray( context.querySelectorAll( "#" + nid + " " + query ), extra ); + + } catch(pseudoError) { + } finally { + if ( !old ) { + context.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + if ( matches ) { + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + return matches.call( node, expr ); + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS; + +jQuery.fn.extend({ + find: function( selector ) { + var ret = this.pushStack( "", "find", selector ), + length = 0; + + for ( var i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( var n = length; n < ret.length; n++ ) { + for ( var r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && jQuery.filter( selector, this ).length > 0; + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + if ( jQuery.isArray( selectors ) ) { + var match, selector, + matches = {}, + level = 1; + + if ( cur && selectors.length ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { + selector = selectors[i]; + + if ( !matches[selector] ) { + matches[selector] = jQuery.expr.match.POS.test( selector ) ? + jQuery( selector, context || this.context ) : + selector; + } + } + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( selector in matches ) { + match = matches[selector]; + + if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { + ret.push({ selector: selector, elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + } + + return ret; + } + + var pos = POS.test( selectors ) ? + jQuery( selectors, context || this.context ) : null; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique(ret) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + if ( !elem || typeof elem === "string" ) { + return jQuery.inArray( this[0], + // If it receives a string, the selector is used + // If it receives nothing, the siblings are used + elem ? jQuery( elem ) : this.parent().children() ); + } + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context || this.context ) : + jQuery.makeArray( selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call(arguments).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return (elem === qualifier) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + }); +} + + + + +var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /\s]+\/)>/g, + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + + + + + + + + +
+
+ + Connecting ... + +

Javascript Error: The Evennia MUD client requires that you have Javascript activated.

+

Turn off eventual script blockers and/or switch to a web + browser supporting javascript.

+
+
+
+ Logged in Players: {{num_players_connected}}
+ +
+
+ + + diff --git a/src/web/urls.py b/src/web/urls.py index c748108353..de5024788a 100755 --- a/src/web/urls.py +++ b/src/web/urls.py @@ -33,10 +33,12 @@ urlpatterns = patterns('', # Admin interface url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), - #url(r'^admin/(.*)', admin.site.root, name='admin'), - + # favicon url(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url':'/media/images/favicon.ico'}), + + # ajax stuff + url(r'^webclient/',include('src.web.webclient.urls')), ) # If you'd like to serve media files via Django (strongly not recommended!), diff --git a/src/web/utils/general_context.py b/src/web/utils/general_context.py index c076b48398..1336c5aa5c 100644 --- a/src/web/utils/general_context.py +++ b/src/web/utils/general_context.py @@ -42,5 +42,6 @@ def general_context(request): 'evennia_entityapps': GAME_ENTITIES, 'evennia_setupapps': GAME_SETUP, 'evennia_connectapps': CONNECTIONS, - 'evennia_websiteapps':WEBSITE + 'evennia_websiteapps':WEBSITE, + "webclient_enabled" : settings.WEBCLIENT_ENABLED } diff --git a/src/web/webclient/__init__.py b/src/web/webclient/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/web/webclient/models.py b/src/web/webclient/models.py new file mode 100644 index 0000000000..89e59e2200 --- /dev/null +++ b/src/web/webclient/models.py @@ -0,0 +1,7 @@ +# +# Define database entities for the app. +# + +from django.db import models + + diff --git a/src/web/webclient/urls.py b/src/web/webclient/urls.py new file mode 100644 index 0000000000..a8a0e52d1a --- /dev/null +++ b/src/web/webclient/urls.py @@ -0,0 +1,10 @@ +""" +This structures the (simple) structure of the +webpage 'application'. +""" +from django.views.generic.simple import direct_to_template +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + url(r'^$', 'src.web.webclient.views.webclient'),) + #url(r'^$', direct_to_template, {'template': 'webclient.html'}),) diff --git a/src/web/webclient/views.py b/src/web/webclient/views.py new file mode 100644 index 0000000000..c233867393 --- /dev/null +++ b/src/web/webclient/views.py @@ -0,0 +1,23 @@ + +""" +This contains a simple view for rendering the webclient +page and serve it eventual static content. + +""" + +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.conf import settings +from src.server import sessionhandler +from src.config.models import ConfigValue + +def webclient(request): + """ + Webclient page template loading. + """ + + # as an example we send the number of connected players to the template + pagevars = {'num_players_connected': ConfigValue.objects.conf('nr_sessions')} + + context_instance = RequestContext(request) + return render_to_response('webclient.html', pagevars, context_instance) diff --git a/src/web/website/views.py b/src/web/website/views.py index 6e23161719..23dd333c3f 100644 --- a/src/web/website/views.py +++ b/src/web/website/views.py @@ -1,3 +1,11 @@ + +""" +This file contains the generic, assorted views that don't fall under one of +the other applications. Views are django's way of processing e.g. html +templates on the fly. + +""" + from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib.auth.models import User @@ -9,11 +17,6 @@ from src.typeclasses.models import TypedObject from src.players.models import PlayerDB from src.web.news.models import NewsEntry -""" -This file contains the generic, assorted views that don't fall under one of -the other applications. -""" - def page_index(request): """ Main root page. @@ -30,7 +33,7 @@ def page_index(request): exits = ObjectDB.objects.get_objs_with_attr('_destination') rooms = [room for room in ObjectDB.objects.filter(db_home=None) if room not in exits] - + pagevars = { "page_title": "Front Page", "news_entries": news_entries,