From 0e9b733bff40d284ea77a29a7c7ef82b960bd4b1 Mon Sep 17 00:00:00 2001 From: Kaan Kivilcim Date: Mon, 25 Aug 2014 15:31:28 +1000 Subject: Escape CGI environment variables in HTTP 404 responses --- paste/__init__.py | 17 + paste/auth/__init__.py | 9 + paste/auth/auth_tkt.py | 429 ++ paste/auth/basic.py | 122 + paste/auth/cas.py | 99 + paste/auth/cookie.py | 396 ++ paste/auth/digest.py | 239 + paste/auth/form.py | 149 + paste/auth/grantip.py | 114 + paste/auth/multi.py | 79 + paste/auth/open_id.py | 413 ++ paste/cascade.py | 133 + paste/cgiapp.py | 277 + paste/cgitb_catcher.py | 117 + paste/config.py | 120 + paste/cowbell/__init__.py | 104 + paste/cowbell/bell-ascending.png | Bin 0 -> 132993 bytes paste/cowbell/bell-descending.png | Bin 0 -> 124917 bytes paste/debug/__init__.py | 5 + paste/debug/debugapp.py | 79 + paste/debug/doctest_webapp.py | 435 ++ paste/debug/fsdiff.py | 419 ++ paste/debug/prints.py | 149 + paste/debug/profile.py | 228 + paste/debug/testserver.py | 93 + paste/debug/watchthreads.py | 347 ++ paste/debug/wdg_validate.py | 121 + paste/errordocument.py | 383 ++ paste/evalexception/__init__.py | 7 + paste/evalexception/evalcontext.py | 69 + paste/evalexception/media/MochiKit.packed.js | 7829 ++++++++++++++++++++++++++ paste/evalexception/media/debug.js | 161 + paste/evalexception/media/minus.jpg | Bin 0 -> 359 bytes paste/evalexception/media/plus.jpg | Bin 0 -> 361 bytes paste/evalexception/middleware.py | 613 ++ paste/exceptions/__init__.py | 6 + paste/exceptions/collector.py | 523 ++ paste/exceptions/errormiddleware.py | 458 ++ paste/exceptions/formatter.py | 565 ++ paste/exceptions/reporter.py | 141 + paste/exceptions/serial_number_generator.py | 125 + paste/fileapp.py | 354 ++ paste/fixture.py | 1730 ++++++ paste/flup_session.py | 108 + paste/gzipper.py | 108 + paste/httpexceptions.py | 660 +++ paste/httpheaders.py | 1105 ++++ paste/httpserver.py | 1416 +++++ paste/lint.py | 438 ++ paste/modpython.py | 253 + paste/pony.py | 57 + paste/progress.py | 222 + paste/proxy.py | 283 + paste/recursive.py | 406 ++ paste/registry.py | 589 ++ paste/reloader.py | 179 + paste/request.py | 418 ++ paste/response.py | 240 + paste/session.py | 343 ++ paste/transaction.py | 120 + paste/translogger.py | 122 + paste/url.py | 473 ++ paste/urlmap.py | 263 + paste/urlparser.py | 639 +++ paste/util/PySourceColor.py | 2102 +++++++ paste/util/UserDict24.py | 167 + paste/util/__init__.py | 4 + paste/util/classinit.py | 42 + paste/util/classinstance.py | 38 + paste/util/converters.py | 30 + paste/util/dateinterval.py | 104 + paste/util/datetimeutil.py | 361 ++ paste/util/doctest24.py | 2665 +++++++++ paste/util/filemixin.py | 53 + paste/util/finddata.py | 98 + paste/util/findpackage.py | 26 + paste/util/import_string.py | 95 + paste/util/intset.py | 511 ++ paste/util/ip4.py | 273 + paste/util/killthread.py | 30 + paste/util/looper.py | 155 + paste/util/mimeparse.py | 160 + paste/util/multidict.py | 404 ++ paste/util/quoting.py | 100 + paste/util/scgiserver.py | 172 + paste/util/string24.py | 531 ++ paste/util/subprocess24.py | 1152 ++++ paste/util/template.py | 759 +++ paste/util/threadedprint.py | 250 + paste/util/threadinglocal.py | 43 + paste/wsgilib.py | 596 ++ paste/wsgiwrappers.py | 587 ++ 92 files changed, 37607 insertions(+) create mode 100644 paste/__init__.py create mode 100644 paste/auth/__init__.py create mode 100644 paste/auth/auth_tkt.py create mode 100644 paste/auth/basic.py create mode 100644 paste/auth/cas.py create mode 100644 paste/auth/cookie.py create mode 100644 paste/auth/digest.py create mode 100644 paste/auth/form.py create mode 100644 paste/auth/grantip.py create mode 100644 paste/auth/multi.py create mode 100644 paste/auth/open_id.py create mode 100644 paste/cascade.py create mode 100644 paste/cgiapp.py create mode 100644 paste/cgitb_catcher.py create mode 100644 paste/config.py create mode 100644 paste/cowbell/__init__.py create mode 100644 paste/cowbell/bell-ascending.png create mode 100644 paste/cowbell/bell-descending.png create mode 100644 paste/debug/__init__.py create mode 100755 paste/debug/debugapp.py create mode 100755 paste/debug/doctest_webapp.py create mode 100644 paste/debug/fsdiff.py create mode 100644 paste/debug/prints.py create mode 100644 paste/debug/profile.py create mode 100755 paste/debug/testserver.py create mode 100644 paste/debug/watchthreads.py create mode 100644 paste/debug/wdg_validate.py create mode 100644 paste/errordocument.py create mode 100644 paste/evalexception/__init__.py create mode 100644 paste/evalexception/evalcontext.py create mode 100644 paste/evalexception/media/MochiKit.packed.js create mode 100644 paste/evalexception/media/debug.js create mode 100644 paste/evalexception/media/minus.jpg create mode 100644 paste/evalexception/media/plus.jpg create mode 100644 paste/evalexception/middleware.py create mode 100644 paste/exceptions/__init__.py create mode 100644 paste/exceptions/collector.py create mode 100644 paste/exceptions/errormiddleware.py create mode 100644 paste/exceptions/formatter.py create mode 100644 paste/exceptions/reporter.py create mode 100644 paste/exceptions/serial_number_generator.py create mode 100644 paste/fileapp.py create mode 100644 paste/fixture.py create mode 100644 paste/flup_session.py create mode 100644 paste/gzipper.py create mode 100644 paste/httpexceptions.py create mode 100644 paste/httpheaders.py create mode 100755 paste/httpserver.py create mode 100644 paste/lint.py create mode 100644 paste/modpython.py create mode 100644 paste/pony.py create mode 100755 paste/progress.py create mode 100644 paste/proxy.py create mode 100644 paste/recursive.py create mode 100644 paste/registry.py create mode 100644 paste/reloader.py create mode 100644 paste/request.py create mode 100644 paste/response.py create mode 100644 paste/session.py create mode 100644 paste/transaction.py create mode 100644 paste/translogger.py create mode 100644 paste/url.py create mode 100644 paste/urlmap.py create mode 100644 paste/urlparser.py create mode 100644 paste/util/PySourceColor.py create mode 100644 paste/util/UserDict24.py create mode 100644 paste/util/__init__.py create mode 100644 paste/util/classinit.py create mode 100644 paste/util/classinstance.py create mode 100644 paste/util/converters.py create mode 100644 paste/util/dateinterval.py create mode 100644 paste/util/datetimeutil.py create mode 100644 paste/util/doctest24.py create mode 100644 paste/util/filemixin.py create mode 100644 paste/util/finddata.py create mode 100644 paste/util/findpackage.py create mode 100644 paste/util/import_string.py create mode 100644 paste/util/intset.py create mode 100644 paste/util/ip4.py create mode 100644 paste/util/killthread.py create mode 100644 paste/util/looper.py create mode 100644 paste/util/mimeparse.py create mode 100644 paste/util/multidict.py create mode 100644 paste/util/quoting.py create mode 100644 paste/util/scgiserver.py create mode 100644 paste/util/string24.py create mode 100644 paste/util/subprocess24.py create mode 100644 paste/util/template.py create mode 100644 paste/util/threadedprint.py create mode 100644 paste/util/threadinglocal.py create mode 100644 paste/wsgilib.py create mode 100644 paste/wsgiwrappers.py (limited to 'paste') diff --git a/paste/__init__.py b/paste/__init__.py new file mode 100644 index 0000000..ba66606 --- /dev/null +++ b/paste/__init__.py @@ -0,0 +1,17 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + # don't prevent use of paste if pkg_resources isn't installed + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) + +try: + import modulefinder +except ImportError: + pass +else: + for p in __path__: + modulefinder.AddPackagePath(__name__, p) diff --git a/paste/auth/__init__.py b/paste/auth/__init__.py new file mode 100644 index 0000000..186e2ef --- /dev/null +++ b/paste/auth/__init__.py @@ -0,0 +1,9 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Package for authentication/identification of requests. + +The objective of this package is to provide single-focused middleware +components that implement a particular specification. Integration of +the components into a usable system is up to a higher-level framework. +""" diff --git a/paste/auth/auth_tkt.py b/paste/auth/auth_tkt.py new file mode 100644 index 0000000..da8ddbd --- /dev/null +++ b/paste/auth/auth_tkt.py @@ -0,0 +1,429 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +########################################################################## +# +# Copyright (c) 2005 Imaginary Landscape LLC and Contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +########################################################################## +""" +Implementation of cookie signing as done in `mod_auth_tkt +`_. + +mod_auth_tkt is an Apache module that looks for these signed cookies +and sets ``REMOTE_USER``, ``REMOTE_USER_TOKENS`` (a comma-separated +list of groups) and ``REMOTE_USER_DATA`` (arbitrary string data). + +This module is an alternative to the ``paste.auth.cookie`` module; +it's primary benefit is compatibility with mod_auth_tkt, which in turn +makes it possible to use the same authentication process with +non-Python code run under Apache. +""" + +import time as time_mod +try: + import hashlib +except ImportError: + # mimic hashlib (will work for md5, fail for secure hashes) + import md5 as hashlib +try: + from http.cookies import SimpleCookie +except ImportError: + # Python 2 + from Cookie import SimpleCookie +from paste import request +from urllib import quote as url_quote +from urllib import unquote as url_unquote + +DEFAULT_DIGEST = hashlib.md5 + + +class AuthTicket(object): + + """ + This class represents an authentication token. You must pass in + the shared secret, the userid, and the IP address. Optionally you + can include tokens (a list of strings, representing role names), + 'user_data', which is arbitrary data available for your own use in + later scripts. Lastly, you can override the timestamp, cookie name, + whether to secure the cookie and the digest algorithm (for details + look at ``AuthTKTMiddleware``). + + Once you provide all the arguments, use .cookie_value() to + generate the appropriate authentication ticket. .cookie() + generates a Cookie object, the str() of which is the complete + cookie header to be sent. + + CGI usage:: + + token = auth_tkt.AuthTick('sharedsecret', 'username', + os.environ['REMOTE_ADDR'], tokens=['admin']) + print('Status: 200 OK') + print('Content-type: text/html') + print(token.cookie()) + print("") + ... redirect HTML ... + + Webware usage:: + + token = auth_tkt.AuthTick('sharedsecret', 'username', + self.request().environ()['REMOTE_ADDR'], tokens=['admin']) + self.response().setCookie('auth_tkt', token.cookie_value()) + + Be careful not to do an HTTP redirect after login; use meta + refresh or Javascript -- some browsers have bugs where cookies + aren't saved when set on a redirect. + """ + + def __init__(self, secret, userid, ip, tokens=(), user_data='', + time=None, cookie_name='auth_tkt', + secure=False, digest_algo=DEFAULT_DIGEST): + self.secret = secret + self.userid = userid + self.ip = ip + if not isinstance(tokens, basestring): + tokens = ','.join(tokens) + self.tokens = tokens + self.user_data = user_data + if time is None: + self.time = time_mod.time() + else: + self.time = time + self.cookie_name = cookie_name + self.secure = secure + if isinstance(digest_algo, str): + # correct specification of digest from hashlib or fail + self.digest_algo = getattr(hashlib, digest_algo) + else: + self.digest_algo = digest_algo + + def digest(self): + return calculate_digest( + self.ip, self.time, self.secret, self.userid, self.tokens, + self.user_data, self.digest_algo) + + def cookie_value(self): + v = '%s%08x%s!' % (self.digest(), int(self.time), url_quote(self.userid)) + if self.tokens: + v += self.tokens + '!' + v += self.user_data + return v + + def cookie(self): + c = SimpleCookie() + c[self.cookie_name] = self.cookie_value().encode('base64').strip().replace('\n', '') + c[self.cookie_name]['path'] = '/' + if self.secure: + c[self.cookie_name]['secure'] = 'true' + return c + + +class BadTicket(Exception): + """ + Exception raised when a ticket can't be parsed. If we get + far enough to determine what the expected digest should have + been, expected is set. This should not be shown by default, + but can be useful for debugging. + """ + def __init__(self, msg, expected=None): + self.expected = expected + Exception.__init__(self, msg) + + +def parse_ticket(secret, ticket, ip, digest_algo=DEFAULT_DIGEST): + """ + Parse the ticket, returning (timestamp, userid, tokens, user_data). + + If the ticket cannot be parsed, ``BadTicket`` will be raised with + an explanation. + """ + if isinstance(digest_algo, str): + # correct specification of digest from hashlib or fail + digest_algo = getattr(hashlib, digest_algo) + digest_hexa_size = digest_algo().digest_size * 2 + ticket = ticket.strip('"') + digest = ticket[:digest_hexa_size] + try: + timestamp = int(ticket[digest_hexa_size:digest_hexa_size + 8], 16) + except ValueError as e: + raise BadTicket('Timestamp is not a hex integer: %s' % e) + try: + userid, data = ticket[digest_hexa_size + 8:].split('!', 1) + except ValueError: + raise BadTicket('userid is not followed by !') + userid = url_unquote(userid) + if '!' in data: + tokens, user_data = data.split('!', 1) + else: + # @@: Is this the right order? + tokens = '' + user_data = data + + expected = calculate_digest(ip, timestamp, secret, + userid, tokens, user_data, + digest_algo) + + if expected != digest: + raise BadTicket('Digest signature is not correct', + expected=(expected, digest)) + + tokens = tokens.split(',') + + return (timestamp, userid, tokens, user_data) + + +# @@: Digest object constructor compatible with named ones in hashlib only +def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, + digest_algo): + secret = maybe_encode(secret) + userid = maybe_encode(userid) + tokens = maybe_encode(tokens) + user_data = maybe_encode(user_data) + digest0 = digest_algo( + encode_ip_timestamp(ip, timestamp) + secret + userid + '\0' + + tokens + '\0' + user_data).hexdigest() + digest = digest_algo(digest0 + secret).hexdigest() + return digest + + +def encode_ip_timestamp(ip, timestamp): + ip_chars = ''.join(map(chr, map(int, ip.split('.')))) + t = int(timestamp) + ts = ((t & 0xff000000) >> 24, + (t & 0xff0000) >> 16, + (t & 0xff00) >> 8, + t & 0xff) + ts_chars = ''.join(map(chr, ts)) + return ip_chars + ts_chars + + +def maybe_encode(s, encoding='utf8'): + if isinstance(s, unicode): + s = s.encode(encoding) + return s + + +class AuthTKTMiddleware(object): + + """ + Middleware that checks for signed cookies that match what + `mod_auth_tkt `_ + looks for (if you have mod_auth_tkt installed, you don't need this + middleware, since Apache will set the environmental variables for + you). + + Arguments: + + ``secret``: + A secret that should be shared by any instances of this application. + If this app is served from more than one machine, they should all + have the same secret. + + ``cookie_name``: + The name of the cookie to read and write from. Default ``auth_tkt``. + + ``secure``: + If the cookie should be set as 'secure' (only sent over SSL) and if + the login must be over SSL. (Defaults to False) + + ``httponly``: + If the cookie should be marked as HttpOnly, which means that it's + not accessible to JavaScript. (Defaults to False) + + ``include_ip``: + If the cookie should include the user's IP address. If so, then + if they change IPs their cookie will be invalid. + + ``logout_path``: + The path under this middleware that should signify a logout. The + page will be shown as usual, but the user will also be logged out + when they visit this page. + + ``digest_algo``: + Digest algorithm specified as a name of the algorithm provided by + ``hashlib`` or as a compatible digest object constructor. + Defaults to ``md5``, as in mod_auth_tkt. The others currently + compatible with mod_auth_tkt are ``sha256`` and ``sha512``. + + If used with mod_auth_tkt, then these settings (except logout_path) should + match the analogous Apache configuration settings. + + This also adds two functions to the request: + + ``environ['paste.auth_tkt.set_user'](userid, tokens='', user_data='')`` + + This sets a cookie that logs the user in. ``tokens`` is a + string (comma-separated groups) or a list of strings. + ``user_data`` is a string for your own use. + + ``environ['paste.auth_tkt.logout_user']()`` + + Logs out the user. + """ + + def __init__(self, app, secret, cookie_name='auth_tkt', secure=False, + include_ip=True, logout_path=None, httponly=False, + no_domain_cookie=True, current_domain_cookie=True, + wildcard_cookie=True, digest_algo=DEFAULT_DIGEST): + self.app = app + self.secret = secret + self.cookie_name = cookie_name + self.secure = secure + self.httponly = httponly + self.include_ip = include_ip + self.logout_path = logout_path + self.no_domain_cookie = no_domain_cookie + self.current_domain_cookie = current_domain_cookie + self.wildcard_cookie = wildcard_cookie + if isinstance(digest_algo, str): + # correct specification of digest from hashlib or fail + self.digest_algo = getattr(hashlib, digest_algo) + else: + self.digest_algo = digest_algo + + def __call__(self, environ, start_response): + cookies = request.get_cookies(environ) + if self.cookie_name in cookies: + cookie_value = cookies[self.cookie_name].value + else: + cookie_value = '' + if cookie_value: + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + # mod_auth_tkt uses this dummy value when IP is not + # checked: + remote_addr = '0.0.0.0' + # @@: This should handle bad signatures better: + # Also, timeouts should cause cookie refresh + try: + timestamp, userid, tokens, user_data = parse_ticket( + self.secret, cookie_value, remote_addr, self.digest_algo) + tokens = ','.join(tokens) + environ['REMOTE_USER'] = userid + if environ.get('REMOTE_USER_TOKENS'): + # We want to add tokens/roles to what's there: + tokens = environ['REMOTE_USER_TOKENS'] + ',' + tokens + environ['REMOTE_USER_TOKENS'] = tokens + environ['REMOTE_USER_DATA'] = user_data + environ['AUTH_TYPE'] = 'cookie' + except BadTicket: + # bad credentials, just ignore without logging the user + # in or anything + pass + set_cookies = [] + + def set_user(userid, tokens='', user_data=''): + set_cookies.extend(self.set_user_cookie( + environ, userid, tokens, user_data)) + + def logout_user(): + set_cookies.extend(self.logout_user_cookie(environ)) + + environ['paste.auth_tkt.set_user'] = set_user + environ['paste.auth_tkt.logout_user'] = logout_user + if self.logout_path and environ.get('PATH_INFO') == self.logout_path: + logout_user() + + def cookie_setting_start_response(status, headers, exc_info=None): + headers.extend(set_cookies) + return start_response(status, headers, exc_info) + + return self.app(environ, cookie_setting_start_response) + + def set_user_cookie(self, environ, userid, tokens, user_data): + if not isinstance(tokens, basestring): + tokens = ','.join(tokens) + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + ticket = AuthTicket( + self.secret, + userid, + remote_addr, + tokens=tokens, + user_data=user_data, + cookie_name=self.cookie_name, + secure=self.secure) + # @@: Should we set REMOTE_USER etc in the current + # environment right now as well? + cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) + wild_domain = '.' + cur_domain + + cookie_options = "" + if self.secure: + cookie_options += "; secure" + if self.httponly: + cookie_options += "; HttpOnly" + + cookies = [] + if self.no_domain_cookie: + cookies.append(('Set-Cookie', '%s=%s; Path=/%s' % ( + self.cookie_name, ticket.cookie_value(), cookie_options))) + if self.current_domain_cookie: + cookies.append(('Set-Cookie', '%s=%s; Path=/; Domain=%s%s' % ( + self.cookie_name, ticket.cookie_value(), cur_domain, + cookie_options))) + if self.wildcard_cookie: + cookies.append(('Set-Cookie', '%s=%s; Path=/; Domain=%s%s' % ( + self.cookie_name, ticket.cookie_value(), wild_domain, + cookie_options))) + + return cookies + + def logout_user_cookie(self, environ): + cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) + wild_domain = '.' + cur_domain + expires = 'Sat, 01-Jan-2000 12:00:00 GMT' + cookies = [ + ('Set-Cookie', '%s=""; Expires="%s"; Path=/' % (self.cookie_name, expires)), + ('Set-Cookie', '%s=""; Expires="%s"; Path=/; Domain=%s' % + (self.cookie_name, expires, cur_domain)), + ('Set-Cookie', '%s=""; Expires="%s"; Path=/; Domain=%s' % + (self.cookie_name, expires, wild_domain)), + ] + return cookies + + +def make_auth_tkt_middleware( + app, + global_conf, + secret=None, + cookie_name='auth_tkt', + secure=False, + include_ip=True, + logout_path=None): + """ + Creates the `AuthTKTMiddleware + `_. + + ``secret`` is requird, but can be set globally or locally. + """ + from paste.deploy.converters import asbool + secure = asbool(secure) + include_ip = asbool(include_ip) + if secret is None: + secret = global_conf.get('secret') + if not secret: + raise ValueError( + "You must provide a 'secret' (in global or local configuration)") + return AuthTKTMiddleware( + app, secret, cookie_name, secure, include_ip, logout_path or None) diff --git a/paste/auth/basic.py b/paste/auth/basic.py new file mode 100644 index 0000000..69db128 --- /dev/null +++ b/paste/auth/basic.py @@ -0,0 +1,122 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Basic HTTP/1.0 Authentication + +This module implements ``Basic`` authentication as described in +HTTP/1.0 specification [1]_ . Do not use this module unless you +are using SSL or need to work with very out-dated clients, instead +use ``digest`` authentication. + +>>> from paste.wsgilib import dump_environ +>>> from paste.httpserver import serve +>>> # from paste.auth.basic import AuthBasicHandler +>>> realm = 'Test Realm' +>>> def authfunc(environ, username, password): +... return username == password +>>> serve(AuthBasicHandler(dump_environ, realm, authfunc)) +serving on... + +.. [1] http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA +""" +from paste.httpexceptions import HTTPUnauthorized +from paste.httpheaders import * + +class AuthBasicAuthenticator(object): + """ + implements ``Basic`` authentication details + """ + type = 'basic' + def __init__(self, realm, authfunc): + self.realm = realm + self.authfunc = authfunc + + def build_authentication(self): + head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) + return HTTPUnauthorized(headers=head) + + def authenticate(self, environ): + authorization = AUTHORIZATION(environ) + if not authorization: + return self.build_authentication() + (authmeth, auth) = authorization.split(' ', 1) + if 'basic' != authmeth.lower(): + return self.build_authentication() + auth = auth.strip().decode('base64') + username, password = auth.split(':', 1) + if self.authfunc(environ, username, password): + return username + return self.build_authentication() + + __call__ = authenticate + +class AuthBasicHandler(object): + """ + HTTP/1.0 ``Basic`` authentication middleware + + Parameters: + + ``application`` + + The application object is called only upon successful + authentication, and can assume ``environ['REMOTE_USER']`` + is set. If the ``REMOTE_USER`` is already set, this + middleware is simply pass-through. + + ``realm`` + + This is a identifier for the authority that is requesting + authorization. It is shown to the user and should be unique + within the domain it is being used. + + ``authfunc`` + + This is a mandatory user-defined function which takes a + ``environ``, ``username`` and ``password`` for its first + three arguments. It should return ``True`` if the user is + authenticated. + + """ + def __init__(self, application, realm, authfunc): + self.application = application + self.authenticate = AuthBasicAuthenticator(realm, authfunc) + + def __call__(self, environ, start_response): + username = REMOTE_USER(environ) + if not username: + result = self.authenticate(environ) + if isinstance(result, str): + AUTH_TYPE.update(environ, 'basic') + REMOTE_USER.update(environ, result) + else: + return result.wsgi_application(environ, start_response) + return self.application(environ, start_response) + +middleware = AuthBasicHandler + +__all__ = ['AuthBasicHandler'] + +def make_basic(app, global_conf, realm, authfunc, **kw): + """ + Grant access via basic authentication + + Config looks like this:: + + [filter:grant] + use = egg:Paste#auth_basic + realm=myrealm + authfunc=somepackage.somemodule:somefunction + + """ + from paste.util.import_string import eval_import + import types + authfunc = eval_import(authfunc) + assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function" + return AuthBasicHandler(app, realm, authfunc) + + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/paste/auth/cas.py b/paste/auth/cas.py new file mode 100644 index 0000000..44e4e98 --- /dev/null +++ b/paste/auth/cas.py @@ -0,0 +1,99 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +CAS 1.0 Authentication + +The Central Authentication System is a straight-forward single sign-on +mechanism developed by Yale University's ITS department. It has since +enjoyed widespread success and is deployed at many major universities +and some corporations. + + https://clearinghouse.ja-sig.org/wiki/display/CAS/Home + http://www.yale.edu/tp/auth/usingcasatyale.html + +This implementation has the goal of maintaining current path arguments +passed to the system so that it can be used as middleware at any stage +of processing. It has the secondary goal of allowing for other +authentication methods to be used concurrently. +""" +from six.moves.urllib.parse import urlencode +from paste.request import construct_url +from paste.httpexceptions import HTTPSeeOther, HTTPForbidden + +class CASLoginFailure(HTTPForbidden): + """ The exception raised if the authority returns 'no' """ + +class CASAuthenticate(HTTPSeeOther): + """ The exception raised to authenticate the user """ + +def AuthCASHandler(application, authority): + """ + middleware to implement CAS 1.0 authentication + + There are several possible outcomes: + + 0. If the REMOTE_USER environment variable is already populated; + then this middleware is a no-op, and the request is passed along + to the application. + + 1. If a query argument 'ticket' is found, then an attempt to + validate said ticket /w the authentication service done. If the + ticket is not validated; an 403 'Forbidden' exception is raised. + Otherwise, the REMOTE_USER variable is set with the NetID that + was validated and AUTH_TYPE is set to "cas". + + 2. Otherwise, a 303 'See Other' is returned to the client directing + them to login using the CAS service. After logon, the service + will send them back to this same URL, only with a 'ticket' query + argument. + + Parameters: + + ``authority`` + + This is a fully-qualified URL to a CAS 1.0 service. The URL + should end with a '/' and have the 'login' and 'validate' + sub-paths as described in the CAS 1.0 documentation. + + """ + assert authority.endswith("/") and authority.startswith("http") + def cas_application(environ, start_response): + username = environ.get('REMOTE_USER','') + if username: + return application(environ, start_response) + qs = environ.get('QUERY_STRING','').split("&") + if qs and qs[-1].startswith("ticket="): + # assume a response from the authority + ticket = qs.pop().split("=", 1)[1] + environ['QUERY_STRING'] = "&".join(qs) + service = construct_url(environ) + args = urlencode( + {'service': service,'ticket': ticket}) + requrl = authority + "validate?" + args + result = urlopen(requrl).read().split("\n") + if 'yes' == result[0]: + environ['REMOTE_USER'] = result[1] + environ['AUTH_TYPE'] = 'cas' + return application(environ, start_response) + exce = CASLoginFailure() + else: + service = construct_url(environ) + args = urlencode({'service': service}) + location = authority + "login?" + args + exce = CASAuthenticate(location) + return exce.wsgi_application(environ, start_response) + return cas_application + +middleware = AuthCASHandler + +__all__ = ['CASLoginFailure', 'CASAuthenticate', 'AuthCASHandler' ] + +if '__main__' == __name__: + authority = "https://secure.its.yale.edu/cas/servlet/" + from paste.wsgilib import dump_environ + from paste.httpserver import serve + from paste.httpexceptions import * + serve(HTTPExceptionHandler( + AuthCASHandler(dump_environ, authority))) diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py new file mode 100644 index 0000000..c636824 --- /dev/null +++ b/paste/auth/cookie.py @@ -0,0 +1,396 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Cookie "Saved" Authentication + +This authentication middleware saves the current REMOTE_USER, +REMOTE_SESSION, and any other environment variables specified in a +cookie so that it can be retrieved during the next request without +requiring re-authentication. This uses a session cookie on the client +side (so it goes away when the user closes their window) and does +server-side expiration. + +Following is a very simple example where a form is presented asking for +a user name (no actual checking), and dummy session identifier (perhaps +corresponding to a database session id) is stored in the cookie. + +:: + + >>> from paste.httpserver import serve + >>> from paste.fileapp import DataApp + >>> from paste.httpexceptions import * + >>> from paste.auth.cookie import AuthCookieHandler + >>> from paste.wsgilib import parse_querystring + >>> def testapp(environ, start_response): + ... user = dict(parse_querystring(environ)).get('user','') + ... if user: + ... environ['REMOTE_USER'] = user + ... environ['REMOTE_SESSION'] = 'a-session-id' + ... if environ.get('REMOTE_USER'): + ... page = 'Welcome %s (%s)' + ... page %= (environ['REMOTE_USER'], environ['REMOTE_SESSION']) + ... else: + ... page = ('
' + ... '
') + ... return DataApp(page, content_type="text/html")( + ... environ, start_response) + >>> serve(AuthCookieHandler(testapp)) + serving on... + +""" + +import hmac, base64, random, six, time, warnings +try: + from hashlib import sha1 +except ImportError: + # NOTE: We have to use the callable with hashlib (hashlib.sha1), + # otherwise hmac only accepts the sha module object itself + import sha as sha1 +from paste.request import get_cookies + +def make_time(value): + return time.strftime("%Y%m%d%H%M", time.gmtime(value)) +_signature_size = len(hmac.new(b'x', b'x', sha1).digest()) +_header_size = _signature_size + len(make_time(time.time())) + +# @@: Should this be using urllib.quote? +# build encode/decode functions to safely pack away values +_encode = [('\\', '\\x5c'), ('"', '\\x22'), + ('=', '\\x3d'), (';', '\\x3b')] +_decode = [(v, k) for (k, v) in _encode] +_decode.reverse() +def encode(s, sublist = _encode): + return six.moves.reduce((lambda a, b: a.replace(b[0], b[1])), sublist, str(s)) +decode = lambda s: encode(s, _decode) + +class CookieTooLarge(RuntimeError): + def __init__(self, content, cookie): + RuntimeError.__init__("Signed cookie exceeds maximum size of 4096") + self.content = content + self.cookie = cookie + +_all_chars = ''.join([chr(x) for x in range(0, 255)]) +def new_secret(): + """ returns a 64 byte secret """ + return ''.join(random.sample(_all_chars, 64)) + +class AuthCookieSigner(object): + """ + save/restore ``environ`` entries via digially signed cookie + + This class converts content into a timed and digitally signed + cookie, as well as having the facility to reverse this procedure. + If the cookie, after the content is encoded and signed exceeds the + maximum length (4096), then CookieTooLarge exception is raised. + + The timeout of the cookie is handled on the server side for a few + reasons. First, if a 'Expires' directive is added to a cookie, then + the cookie becomes persistent (lasting even after the browser window + has closed). Second, the user's clock may be wrong (perhaps + intentionally). The timeout is specified in minutes; and expiration + date returned is rounded to one second. + + Constructor Arguments: + + ``secret`` + + This is a secret key if you want to syncronize your keys so + that the cookie will be good across a cluster of computers. + It is recommended via the HMAC specification (RFC 2104) that + the secret key be 64 bytes since this is the block size of + the hashing. If you do not provide a secret key, a random + one is generated each time you create the handler; this + should be sufficient for most cases. + + ``timeout`` + + This is the time (in minutes) from which the cookie is set + to expire. Note that on each request a new (replacement) + cookie is sent, hence this is effectively a session timeout + parameter for your entire cluster. If you do not provide a + timeout, it is set at 30 minutes. + + ``maxlen`` + + This is the maximum size of the *signed* cookie; hence the + actual content signed will be somewhat less. If the cookie + goes over this size, a ``CookieTooLarge`` exception is + raised so that unexpected handling of cookies on the client + side are avoided. By default this is set at 4k (4096 bytes), + which is the standard cookie size limit. + + """ + def __init__(self, secret = None, timeout = None, maxlen = None): + self.timeout = timeout or 30 + if isinstance(timeout, six.string_types): + raise ValueError( + "Timeout must be a number (minutes), not a string (%r)" + % timeout) + self.maxlen = maxlen or 4096 + self.secret = secret or new_secret() + + def sign(self, content): + """ + Sign the content returning a valid cookie (that does not + need to be escaped and quoted). The expiration of this + cookie is handled server-side in the auth() function. + """ + cookie = base64.encodestring( + hmac.new(self.secret, content, sha1).digest() + + make_time(time.time() + 60*self.timeout) + + content) + cookie = cookie.replace("/", "_").replace("=", "~") + cookie = cookie.replace('\n', '').replace('\r', '') + if len(cookie) > self.maxlen: + raise CookieTooLarge(content, cookie) + return cookie + + def auth(self, cookie): + """ + Authenticate the cooke using the signature, verify that it + has not expired; and return the cookie's content + """ + decode = base64.decodestring( + cookie.replace("_", "/").replace("~", "=")) + signature = decode[:_signature_size] + expires = decode[_signature_size:_header_size] + content = decode[_header_size:] + if signature == hmac.new(self.secret, content, sha1).digest(): + if int(expires) > int(make_time(time.time())): + return content + else: + # This is the normal case of an expired cookie; just + # don't bother doing anything here. + pass + else: + # This case can happen if the server is restarted with a + # different secret; or if the user's IP address changed + # due to a proxy. However, it could also be a break-in + # attempt -- so should it be reported? + pass + +class AuthCookieEnviron(list): + """ + a list of environment keys to be saved via cookie + + An instance of this object, found at ``environ['paste.auth.cookie']`` + lists the `environ` keys that were restored from or will be added + to the digially signed cookie. This object can be accessed from an + `environ` variable by using this module's name. + """ + def __init__(self, handler, scanlist): + list.__init__(self, scanlist) + self.handler = handler + def append(self, value): + if value in self: + return + list.append(self, str(value)) + +class AuthCookieHandler(object): + """ + the actual handler that should be put in your middleware stack + + This middleware uses cookies to stash-away a previously authenticated + user (and perhaps other variables) so that re-authentication is not + needed. This does not implement sessions; and therefore N servers + can be syncronized to accept the same saved authentication if they + all use the same cookie_name and secret. + + By default, this handler scans the `environ` for the REMOTE_USER + and REMOTE_SESSION key; if found, it is stored. It can be + configured to scan other `environ` keys as well -- but be careful + not to exceed 2-3k (so that the encoded and signed cookie does not + exceed 4k). You can ask it to handle other environment variables + by doing: + + ``environ['paste.auth.cookie'].append('your.environ.variable')`` + + + Constructor Arguments: + + ``application`` + + This is the wrapped application which will have access to + the ``environ['REMOTE_USER']`` restored by this middleware. + + ``cookie_name`` + + The name of the cookie used to store this content, by default + it is ``PASTE_AUTH_COOKIE``. + + ``scanlist`` + + This is the initial set of ``environ`` keys to + save/restore to the signed cookie. By default is consists + only of ``REMOTE_USER`` and ``REMOTE_SESSION``; any tuple + or list of environment keys will work. However, be + careful, as the total saved size is limited to around 3k. + + ``signer`` + + This is the signer object used to create the actual cookie + values, by default, it is ``AuthCookieSigner`` and is passed + the remaining arguments to this function: ``secret``, + ``timeout``, and ``maxlen``. + + At this time, each cookie is individually signed. To store more + than the 4k of data; it is possible to sub-class this object to + provide different ``environ_name`` and ``cookie_name`` + """ + environ_name = 'paste.auth.cookie' + cookie_name = 'PASTE_AUTH_COOKIE' + signer_class = AuthCookieSigner + environ_class = AuthCookieEnviron + + def __init__(self, application, cookie_name=None, scanlist=None, + signer=None, secret=None, timeout=None, maxlen=None): + if not signer: + signer = self.signer_class(secret, timeout, maxlen) + self.signer = signer + self.scanlist = scanlist or ('REMOTE_USER','REMOTE_SESSION') + self.application = application + self.cookie_name = cookie_name or self.cookie_name + + def __call__(self, environ, start_response): + if self.environ_name in environ: + raise AssertionError("AuthCookie already installed!") + scanlist = self.environ_class(self, self.scanlist) + jar = get_cookies(environ) + if self.cookie_name in jar: + content = self.signer.auth(jar[self.cookie_name].value) + if content: + for pair in content.split(";"): + (k, v) = pair.split("=") + k = decode(k) + if k not in scanlist: + scanlist.append(k) + if k in environ: + continue + environ[k] = decode(v) + if 'REMOTE_USER' == k: + environ['AUTH_TYPE'] = 'cookie' + environ[self.environ_name] = scanlist + if "paste.httpexceptions" in environ: + warnings.warn("Since paste.httpexceptions is hooked in your " + "processing chain before paste.auth.cookie, if an " + "HTTPRedirection is raised, the cookies this module sets " + "will not be included in your response.\n") + + def response_hook(status, response_headers, exc_info=None): + """ + Scan the environment for keys specified in the scanlist, + pack up their values, signs the content and issues a cookie. + """ + scanlist = environ.get(self.environ_name) + assert scanlist and isinstance(scanlist, self.environ_class) + content = [] + for k in scanlist: + v = environ.get(k) + if v is not None: + if type(v) is not str: + raise ValueError( + "The value of the environmental variable %r " + "is not a str (only str is allowed; got %r)" + % (k, v)) + content.append("%s=%s" % (encode(k), encode(v))) + if content: + content = ";".join(content) + content = self.signer.sign(content) + cookie = '%s=%s; Path=/;' % (self.cookie_name, content) + if 'https' == environ['wsgi.url_scheme']: + cookie += ' secure;' + response_headers.append(('Set-Cookie', cookie)) + return start_response(status, response_headers, exc_info) + return self.application(environ, response_hook) + +middleware = AuthCookieHandler + +# Paste Deploy entry point: +def make_auth_cookie( + app, global_conf, + # Should this get picked up from global_conf somehow?: + cookie_name='PASTE_AUTH_COOKIE', + scanlist=('REMOTE_USER', 'REMOTE_SESSION'), + # signer cannot be set + secret=None, + timeout=30, + maxlen=4096): + """ + This middleware uses cookies to stash-away a previously + authenticated user (and perhaps other variables) so that + re-authentication is not needed. This does not implement + sessions; and therefore N servers can be syncronized to accept the + same saved authentication if they all use the same cookie_name and + secret. + + By default, this handler scans the `environ` for the REMOTE_USER + and REMOTE_SESSION key; if found, it is stored. It can be + configured to scan other `environ` keys as well -- but be careful + not to exceed 2-3k (so that the encoded and signed cookie does not + exceed 4k). You can ask it to handle other environment variables + by doing: + + ``environ['paste.auth.cookie'].append('your.environ.variable')`` + + Configuration: + + ``cookie_name`` + + The name of the cookie used to store this content, by + default it is ``PASTE_AUTH_COOKIE``. + + ``scanlist`` + + This is the initial set of ``environ`` keys to + save/restore to the signed cookie. By default is consists + only of ``REMOTE_USER`` and ``REMOTE_SESSION``; any + space-separated list of environment keys will work. + However, be careful, as the total saved size is limited to + around 3k. + + ``secret`` + + The secret that will be used to sign the cookies. If you + don't provide one (and none is set globally) then a random + secret will be created. Each time the server is restarted + a new secret will then be created and all cookies will + become invalid! This can be any string value. + + ``timeout`` + + The time to keep the cookie, expressed in minutes. This + is handled server-side, so a new cookie with a new timeout + is added to every response. + + ``maxlen`` + + The maximum length of the cookie that is sent (default 4k, + which is a typical browser maximum) + + """ + if isinstance(scanlist, six.string_types): + scanlist = scanlist.split() + if secret is None and global_conf.get('secret'): + secret = global_conf['secret'] + try: + timeout = int(timeout) + except ValueError: + raise ValueError('Bad value for timeout (must be int): %r' + % timeout) + try: + maxlen = int(maxlen) + except ValueError: + raise ValueError('Bad value for maxlen (must be int): %r' + % maxlen) + return AuthCookieHandler( + app, cookie_name=cookie_name, scanlist=scanlist, + secret=secret, timeout=timeout, maxlen=maxlen) + +__all__ = ['AuthCookieHandler', 'AuthCookieSigner', 'AuthCookieEnviron'] + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) + diff --git a/paste/auth/digest.py b/paste/auth/digest.py new file mode 100644 index 0000000..798f447 --- /dev/null +++ b/paste/auth/digest.py @@ -0,0 +1,239 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Digest HTTP/1.1 Authentication + +This module implements ``Digest`` authentication as described by +RFC 2617 [1]_ . + +Basically, you just put this module before your application, and it +takes care of requesting and handling authentication requests. This +module has been tested with several common browsers "out-in-the-wild". + +>>> from paste.wsgilib import dump_environ +>>> from paste.httpserver import serve +>>> # from paste.auth.digest import digest_password, AuthDigestHandler +>>> realm = 'Test Realm' +>>> def authfunc(environ, realm, username): +... return digest_password(realm, username, username) +>>> serve(AuthDigestHandler(dump_environ, realm, authfunc)) +serving on... + +This code has not been audited by a security expert, please use with +caution (or better yet, report security holes). At this time, this +implementation does not provide for further challenges, nor does it +support Authentication-Info header. It also uses md5, and an option +to use sha would be a good thing. + +.. [1] http://www.faqs.org/rfcs/rfc2617.html +""" +from paste.httpexceptions import HTTPUnauthorized +from paste.httpheaders import * +try: + from hashlib import md5 +except ImportError: + from md5 import md5 +import time, random +from six.moves.urllib.parse import quote as url_quote + +def _split_auth_string(auth_string): + """ split a digest auth string into individual key=value strings """ + prev = None + for item in auth_string.split(","): + try: + if prev.count('"') == 1: + prev = "%s,%s" % (prev, item) + continue + except AttributeError: + if prev == None: + prev = item + continue + else: + raise StopIteration + yield prev.strip() + prev = item + + yield prev.strip() + raise StopIteration + +def _auth_to_kv_pairs(auth_string): + """ split a digest auth string into key, value pairs """ + for item in _split_auth_string(auth_string): + (k, v) = item.split("=", 1) + if v.startswith('"') and len(v) > 1 and v.endswith('"'): + v = v[1:-1] + yield (k, v) + +def digest_password(realm, username, password): + """ construct the appropriate hashcode needed for HTTP digest """ + return md5("%s:%s:%s" % (username, realm, password)).hexdigest() + +class AuthDigestAuthenticator(object): + """ implementation of RFC 2617 - HTTP Digest Authentication """ + def __init__(self, realm, authfunc): + self.nonce = {} # list to prevent replay attacks + self.authfunc = authfunc + self.realm = realm + + def build_authentication(self, stale = ''): + """ builds the authentication error """ + nonce = md5( + "%s:%s" % (time.time(), random.random())).hexdigest() + opaque = md5( + "%s:%s" % (time.time(), random.random())).hexdigest() + self.nonce[nonce] = None + parts = {'realm': self.realm, 'qop': 'auth', + 'nonce': nonce, 'opaque': opaque } + if stale: + parts['stale'] = 'true' + head = ", ".join(['%s="%s"' % (k, v) for (k, v) in parts.items()]) + head = [("WWW-Authenticate", 'Digest %s' % head)] + return HTTPUnauthorized(headers=head) + + def compute(self, ha1, username, response, method, + path, nonce, nc, cnonce, qop): + """ computes the authentication, raises error if unsuccessful """ + if not ha1: + return self.build_authentication() + ha2 = md5('%s:%s' % (method, path)).hexdigest() + if qop: + chk = "%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2) + else: + chk = "%s:%s:%s" % (ha1, nonce, ha2) + if response != md5(chk).hexdigest(): + if nonce in self.nonce: + del self.nonce[nonce] + return self.build_authentication() + pnc = self.nonce.get(nonce,'00000000') + if nc <= pnc: + if nonce in self.nonce: + del self.nonce[nonce] + return self.build_authentication(stale = True) + self.nonce[nonce] = nc + return username + + def authenticate(self, environ): + """ This function takes a WSGI environment and authenticates + the request returning authenticated user or error. + """ + method = REQUEST_METHOD(environ) + fullpath = url_quote(SCRIPT_NAME(environ)) + url_quote(PATH_INFO(environ)) + authorization = AUTHORIZATION(environ) + if not authorization: + return self.build_authentication() + (authmeth, auth) = authorization.split(" ", 1) + if 'digest' != authmeth.lower(): + return self.build_authentication() + amap = dict(_auth_to_kv_pairs(auth)) + try: + username = amap['username'] + authpath = amap['uri'] + nonce = amap['nonce'] + realm = amap['realm'] + response = amap['response'] + assert authpath.split("?", 1)[0] in fullpath + assert realm == self.realm + qop = amap.get('qop', '') + cnonce = amap.get('cnonce', '') + nc = amap.get('nc', '00000000') + if qop: + assert 'auth' == qop + assert nonce and nc + except: + return self.build_authentication() + ha1 = self.authfunc(environ, realm, username) + return self.compute(ha1, username, response, method, authpath, + nonce, nc, cnonce, qop) + + __call__ = authenticate + +class AuthDigestHandler(object): + """ + middleware for HTTP Digest authentication (RFC 2617) + + This component follows the procedure below: + + 0. If the REMOTE_USER environment variable is already populated; + then this middleware is a no-op, and the request is passed + along to the application. + + 1. If the HTTP_AUTHORIZATION header was not provided or specifies + an algorithem other than ``digest``, then a HTTPUnauthorized + response is generated with the challenge. + + 2. If the response is malformed or or if the user's credientials + do not pass muster, another HTTPUnauthorized is raised. + + 3. If all goes well, and the user's credintials pass; then + REMOTE_USER environment variable is filled in and the + AUTH_TYPE is listed as 'digest'. + + Parameters: + + ``application`` + + The application object is called only upon successful + authentication, and can assume ``environ['REMOTE_USER']`` + is set. If the ``REMOTE_USER`` is already set, this + middleware is simply pass-through. + + ``realm`` + + This is a identifier for the authority that is requesting + authorization. It is shown to the user and should be unique + within the domain it is being used. + + ``authfunc`` + + This is a callback function which performs the actual + authentication; the signature of this callback is: + + authfunc(environ, realm, username) -> hashcode + + This module provides a 'digest_password' helper function + which can help construct the hashcode; it is recommended + that the hashcode is stored in a database, not the user's + actual password (since you only need the hashcode). + """ + def __init__(self, application, realm, authfunc): + self.authenticate = AuthDigestAuthenticator(realm, authfunc) + self.application = application + + def __call__(self, environ, start_response): + username = REMOTE_USER(environ) + if not username: + result = self.authenticate(environ) + if isinstance(result, str): + AUTH_TYPE.update(environ,'digest') + REMOTE_USER.update(environ, result) + else: + return result.wsgi_application(environ, start_response) + return self.application(environ, start_response) + +middleware = AuthDigestHandler + +__all__ = ['digest_password', 'AuthDigestHandler' ] + +def make_digest(app, global_conf, realm, authfunc, **kw): + """ + Grant access via digest authentication + + Config looks like this:: + + [filter:grant] + use = egg:Paste#auth_digest + realm=myrealm + authfunc=somepackage.somemodule:somefunction + + """ + from paste.util.import_string import eval_import + import types + authfunc = eval_import(authfunc) + assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function" + return AuthDigestHandler(app, realm, authfunc) + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/paste/auth/form.py b/paste/auth/form.py new file mode 100644 index 0000000..4e6aa49 --- /dev/null +++ b/paste/auth/form.py @@ -0,0 +1,149 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Authentication via HTML Form + +This is a very simple HTML form login screen that asks for the username +and password. This middleware component requires that an authorization +function taking the name and passsword and that it be placed in your +application stack. This class does not include any session management +code or way to save the user's authorization; however, it is easy enough +to put ``paste.auth.cookie`` in your application stack. + +>>> from paste.wsgilib import dump_environ +>>> from paste.httpserver import serve +>>> from paste.auth.cookie import AuthCookieHandler +>>> from paste.auth.form import AuthFormHandler +>>> def authfunc(environ, username, password): +... return username == password +>>> serve(AuthCookieHandler( +... AuthFormHandler(dump_environ, authfunc))) +serving on... + +""" +from paste.request import construct_url, parse_formvars + +TEMPLATE = """\ + + Please Login! + +

Please Login

+
+
+
Username:
+
+
Password:
+
+
+ +
+
+ + +""" + +class AuthFormHandler(object): + """ + HTML-based login middleware + + This causes a HTML form to be returned if ``REMOTE_USER`` is + not found in the ``environ``. If the form is returned, the + ``username`` and ``password`` combination are given to a + user-supplied authentication function, ``authfunc``. If this + is successful, then application processing continues. + + Parameters: + + ``application`` + + The application object is called only upon successful + authentication, and can assume ``environ['REMOTE_USER']`` + is set. If the ``REMOTE_USER`` is already set, this + middleware is simply pass-through. + + ``authfunc`` + + This is a mandatory user-defined function which takes a + ``environ``, ``username`` and ``password`` for its first + three arguments. It should return ``True`` if the user is + authenticated. + + ``template`` + + This is an optional (a default is provided) HTML + fragment that takes exactly one ``%s`` substution + argument; which *must* be used for the form's ``action`` + to ensure that this middleware component does not alter + the current path. The HTML form must use ``POST`` and + have two input names: ``username`` and ``password``. + + Since the authentication form is submitted (via ``POST``) + neither the ``PATH_INFO`` nor the ``QUERY_STRING`` are accessed, + and hence the current path remains _unaltered_ through the + entire authentication process. If authentication succeeds, the + ``REQUEST_METHOD`` is converted from a ``POST`` to a ``GET``, + so that a redirect is unnecessary (unlike most form auth + implementations) + """ + + def __init__(self, application, authfunc, template=None): + self.application = application + self.authfunc = authfunc + self.template = template or TEMPLATE + + def __call__(self, environ, start_response): + username = environ.get('REMOTE_USER','') + if username: + return self.application(environ, start_response) + + if 'POST' == environ['REQUEST_METHOD']: + formvars = parse_formvars(environ, include_get_vars=False) + username = formvars.get('username') + password = formvars.get('password') + if username and password: + if self.authfunc(environ, username, password): + environ['AUTH_TYPE'] = 'form' + environ['REMOTE_USER'] = username + environ['REQUEST_METHOD'] = 'GET' + environ['CONTENT_LENGTH'] = '' + environ['CONTENT_TYPE'] = '' + del environ['paste.parsed_formvars'] + return self.application(environ, start_response) + + content = self.template % construct_url(environ) + start_response("200 OK", [('Content-Type', 'text/html'), + ('Content-Length', str(len(content)))]) + return [content] + +middleware = AuthFormHandler + +__all__ = ['AuthFormHandler'] + +def make_form(app, global_conf, realm, authfunc, **kw): + """ + Grant access via form authentication + + Config looks like this:: + + [filter:grant] + use = egg:Paste#auth_form + realm=myrealm + authfunc=somepackage.somemodule:somefunction + + """ + from paste.util.import_string import eval_import + import types + authfunc = eval_import(authfunc) + assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function" + template = kw.get('template') + if template is not None: + template = eval_import(template) + assert isinstance(template, str), "template must resolve to a string" + + return AuthFormHandler(app, authfunc, template) + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/paste/auth/grantip.py b/paste/auth/grantip.py new file mode 100644 index 0000000..4ea6df5 --- /dev/null +++ b/paste/auth/grantip.py @@ -0,0 +1,114 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Grant roles and logins based on IP address. +""" +import six +from paste.util import ip4 + +class GrantIPMiddleware(object): + + """ + On each request, ``ip_map`` is checked against ``REMOTE_ADDR`` + and logins and roles are assigned based on that. + + ``ip_map`` is a map of {ip_mask: (username, roles)}. Either + ``username`` or ``roles`` may be None. Roles may also be prefixed + with ``-``, like ``'-system'`` meaning that role should be + revoked. ``'__remove__'`` for a username will remove the username. + + If ``clobber_username`` is true (default) then any user + specification will override the current value of ``REMOTE_USER``. + ``'__remove__'`` will always clobber the username. + + ``ip_mask`` is something that `paste.util.ip4:IP4Range + `_ can parse. Simple IP + addresses, IP/mask, ip<->ip ranges, and hostnames are allowed. + """ + + def __init__(self, app, ip_map, clobber_username=True): + self.app = app + self.ip_map = [] + for key, value in ip_map.items(): + self.ip_map.append((ip4.IP4Range(key), + self._convert_user_role(value[0], value[1]))) + self.clobber_username = clobber_username + + def _convert_user_role(self, username, roles): + if roles and isinstance(roles, six.string_types): + roles = roles.split(',') + return (username, roles) + + def __call__(self, environ, start_response): + addr = ip4.ip2int(environ['REMOTE_ADDR'], False) + remove_user = False + add_roles = [] + for range, (username, roles) in self.ip_map: + if addr in range: + if roles: + add_roles.extend(roles) + if username == '__remove__': + remove_user = True + elif username: + if (not environ.get('REMOTE_USER') + or self.clobber_username): + environ['REMOTE_USER'] = username + if (remove_user and 'REMOTE_USER' in environ): + del environ['REMOTE_USER'] + if roles: + self._set_roles(environ, add_roles) + return self.app(environ, start_response) + + def _set_roles(self, environ, roles): + cur_roles = environ.get('REMOTE_USER_TOKENS', '').split(',') + # Get rid of empty roles: + cur_roles = filter(None, cur_roles) + remove_roles = [] + for role in roles: + if role.startswith('-'): + remove_roles.append(role[1:]) + else: + if role not in cur_roles: + cur_roles.append(role) + for role in remove_roles: + if role in cur_roles: + cur_roles.remove(role) + environ['REMOTE_USER_TOKENS'] = ','.join(cur_roles) + + +def make_grantip(app, global_conf, clobber_username=False, **kw): + """ + Grant roles or usernames based on IP addresses. + + Config looks like this:: + + [filter:grant] + use = egg:Paste#grantip + clobber_username = true + # Give localhost system role (no username): + 127.0.0.1 = -:system + # Give everyone in 192.168.0.* editor role: + 192.168.0.0/24 = -:editor + # Give one IP the username joe: + 192.168.0.7 = joe + # And one IP is should not be logged in: + 192.168.0.10 = __remove__:-editor + + """ + from paste.deploy.converters import asbool + clobber_username = asbool(clobber_username) + ip_map = {} + for key, value in kw.items(): + if ':' in value: + username, role = value.split(':', 1) + else: + username = value + role = '' + if username == '-': + username = '' + if role == '-': + role = '' + ip_map[key] = value + return GrantIPMiddleware(app, ip_map, clobber_username) + + diff --git a/paste/auth/multi.py b/paste/auth/multi.py new file mode 100644 index 0000000..b378fa6 --- /dev/null +++ b/paste/auth/multi.py @@ -0,0 +1,79 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Authentication via Multiple Methods + +In some environments, the choice of authentication method to be used +depends upon the environment and is not "fixed". This middleware allows +N authentication methods to be registered along with a goodness function +which determines which method should be used. The following example +demonstrates how to use both form and digest authentication in a server +stack; by default it uses form-based authentication unless +``*authmeth=digest`` is specified as a query argument. + +>>> from paste.auth import form, cookie, digest, multi +>>> from paste.wsgilib import dump_environ +>>> from paste.httpserver import serve +>>> +>>> multi = multi.MultiHandler(dump_environ) +>>> def authfunc(environ, realm, user): +... return digest.digest_password(realm, user, user) +>>> multi.add_method('digest', digest.middleware, "Test Realm", authfunc) +>>> multi.set_query_argument('digest') +>>> +>>> def authfunc(environ, username, password): +... return username == password +>>> multi.add_method('form', form.middleware, authfunc) +>>> multi.set_default('form') +>>> serve(cookie.middleware(multi)) +serving on... + +""" + +class MultiHandler(object): + """ + Multiple Authentication Handler + + This middleware provides two othogonal facilities: + + - a manner to register any number of authentication middlewares + + - a mechanism to register predicates which cause one of the + registered middlewares to be used depending upon the request + + If none of the predicates returns True, then the application is + invoked directly without middleware + """ + def __init__(self, application): + self.application = application + self.default = application + self.binding = {} + self.predicate = [] + def add_method(self, name, factory, *args, **kwargs): + self.binding[name] = factory(self.application, *args, **kwargs) + def add_predicate(self, name, checker): + self.predicate.append((checker, self.binding[name])) + def set_default(self, name): + """ set default authentication method """ + self.default = self.binding[name] + def set_query_argument(self, name, key = '*authmeth', value = None): + """ choose authentication method based on a query argument """ + lookfor = "%s=%s" % (key, value or name) + self.add_predicate(name, + lambda environ: lookfor in environ.get('QUERY_STRING','')) + def __call__(self, environ, start_response): + for (checker, binding) in self.predicate: + if checker(environ): + return binding(environ, start_response) + return self.default(environ, start_response) + +middleware = MultiHandler + +__all__ = ['MultiHandler'] + +if "__main__" == __name__: + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) + diff --git a/paste/auth/open_id.py b/paste/auth/open_id.py new file mode 100644 index 0000000..967e699 --- /dev/null +++ b/paste/auth/open_id.py @@ -0,0 +1,413 @@ +# (c) 2005 Ben Bangert +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +""" +OpenID Authentication (Consumer) + +OpenID is a distributed authentication system for single sign-on originally +developed at/for LiveJournal.com. + + http://openid.net/ + +URL. You can have multiple identities in the same way you can have multiple +URLs. All OpenID does is provide a way to prove that you own a URL (identity). +And it does this without passing around your password, your email address, or +anything you don't want it to. There's no profile exchange component at all: +your profiile is your identity URL, but recipients of your identity can then +learn more about you from any public, semantically interesting documents +linked thereunder (FOAF, RSS, Atom, vCARD, etc.). + +``Note``: paste.auth.openid requires installation of the Python-OpenID +libraries:: + + http://www.openidenabled.com/ + +This module is based highly off the consumer.py that Python OpenID comes with. + +Using the OpenID Middleware +=========================== + +Using the OpenID middleware is fairly easy, the most minimal example using the +basic login form thats included:: + + # Add to your wsgi app creation + from paste.auth import open_id + + wsgi_app = open_id.middleware(wsgi_app, '/somewhere/to/store/openid/data') + +You will now have the OpenID form available at /oid on your site. Logging in will +verify that the login worked. + +A more complete login should involve having the OpenID middleware load your own +login page after verifying the OpenID URL so that you can retain the login +information in your webapp (session, cookies, etc.):: + + wsgi_app = open_id.middleware(wsgi_app, '/somewhere/to/store/openid/data', + login_redirect='/your/login/code') + +Your login code should then be configured to retrieve 'paste.auth.open_id' for +the users OpenID URL. If this key does not exist, the user has not logged in. + +Once the login is retrieved, it should be saved in your webapp, and the user +should be redirected to wherever they would normally go after a successful +login. +""" + +__all__ = ['AuthOpenIDHandler'] + +import cgi +import urlparse +import re +import six + +import paste.request +from paste import httpexceptions + +def quoteattr(s): + qs = cgi.escape(s, 1) + return '"%s"' % (qs,) + +# You may need to manually add the openid package into your +# python path if you don't have it installed with your system python. +# If so, uncomment the line below, and change the path where you have +# Python-OpenID. +# sys.path.append('/path/to/openid/') + +from openid.store import filestore +from openid.consumer import consumer +from openid.oidutil import appendArgs + +class AuthOpenIDHandler(object): + """ + This middleware implements OpenID Consumer behavior to authenticate a + URL against an OpenID Server. + """ + + def __init__(self, app, data_store_path, auth_prefix='/oid', + login_redirect=None, catch_401=False, + url_to_username=None): + """ + Initialize the OpenID middleware + + ``app`` + Your WSGI app to call + + ``data_store_path`` + Directory to store crypto data in for use with OpenID servers. + + ``auth_prefix`` + Location for authentication process/verification + + ``login_redirect`` + Location to load after successful process of login + + ``catch_401`` + If true, then any 401 responses will turn into open ID login + requirements. + + ``url_to_username`` + A function called like ``url_to_username(environ, url)``, which should + return a string username. If not given, the URL will be the username. + """ + store = filestore.FileOpenIDStore(data_store_path) + self.oidconsumer = consumer.OpenIDConsumer(store) + + self.app = app + self.auth_prefix = auth_prefix + self.data_store_path = data_store_path + self.login_redirect = login_redirect + self.catch_401 = catch_401 + self.url_to_username = url_to_username + + def __call__(self, environ, start_response): + if environ['PATH_INFO'].startswith(self.auth_prefix): + # Let's load everything into a request dict to pass around easier + request = dict(environ=environ, start=start_response, body=[]) + request['base_url'] = paste.request.construct_url(environ, with_path_info=False, + with_query_string=False) + + path = re.sub(self.auth_prefix, '', environ['PATH_INFO']) + request['parsed_uri'] = urlparse.urlparse(path) + request['query'] = dict(paste.request.parse_querystring(environ)) + + path = request['parsed_uri'][2] + if path == '/' or not path: + return self.render(request) + elif path == '/verify': + return self.do_verify(request) + elif path == '/process': + return self.do_process(request) + else: + return self.not_found(request) + else: + if self.catch_401: + return self.catch_401_app_call(environ, start_response) + return self.app(environ, start_response) + + def catch_401_app_call(self, environ, start_response): + """ + Call the application, and redirect if the app returns a 401 response + """ + was_401 = [] + def replacement_start_response(status, headers, exc_info=None): + if int(status.split(None, 1)) == 401: + # @@: Do I need to append something to go back to where we + # came from? + was_401.append(1) + def dummy_writer(v): + pass + return dummy_writer + else: + return start_response(status, headers, exc_info) + app_iter = self.app(environ, replacement_start_response) + if was_401: + try: + list(app_iter) + finally: + if hasattr(app_iter, 'close'): + app_iter.close() + redir_url = paste.request.construct_url(environ, with_path_info=False, + with_query_string=False) + exc = httpexceptions.HTTPTemporaryRedirect(redir_url) + return exc.wsgi_application(environ, start_response) + else: + return app_iter + + def do_verify(self, request): + """Process the form submission, initating OpenID verification. + """ + + # First, make sure that the user entered something + openid_url = request['query'].get('openid_url') + if not openid_url: + return self.render(request, 'Enter an identity URL to verify.', + css_class='error', form_contents=openid_url) + + oidconsumer = self.oidconsumer + + # Then, ask the library to begin the authorization. + # Here we find out the identity server that will verify the + # user's identity, and get a token that allows us to + # communicate securely with the identity server. + status, info = oidconsumer.beginAuth(openid_url) + + # If the URL was unusable (either because of network + # conditions, a server error, or that the response returned + # was not an OpenID identity page), the library will return + # an error code. Let the user know that that URL is unusable. + if status in [consumer.HTTP_FAILURE, consumer.PARSE_ERROR]: + if status == consumer.HTTP_FAILURE: + fmt = 'Failed to retrieve %s' + else: + fmt = 'Could not find OpenID information in %s' + + message = fmt % (cgi.escape(openid_url),) + return self.render(request, message, css_class='error', form_contents=openid_url) + elif status == consumer.SUCCESS: + # The URL was a valid identity URL. Now we construct a URL + # that will get us to process the server response. We will + # need the token from the beginAuth call when processing + # the response. A cookie or a session object could be used + # to accomplish this, but for simplicity here we just add + # it as a query parameter of the return-to URL. + return_to = self.build_url(request, 'process', token=info.token) + + # Now ask the library for the URL to redirect the user to + # his OpenID server. It is required for security that the + # return_to URL must be under the specified trust_root. We + # just use the base_url for this server as a trust root. + redirect_url = oidconsumer.constructRedirect( + info, return_to, trust_root=request['base_url']) + + # Send the redirect response + return self.redirect(request, redirect_url) + else: + assert False, 'Not reached' + + def do_process(self, request): + """Handle the redirect from the OpenID server. + """ + oidconsumer = self.oidconsumer + + # retrieve the token from the environment (in this case, the URL) + token = request['query'].get('token', '') + + # Ask the library to check the response that the server sent + # us. Status is a code indicating the response type. info is + # either None or a string containing more information about + # the return type. + status, info = oidconsumer.completeAuth(token, request['query']) + + css_class = 'error' + openid_url = None + if status == consumer.FAILURE and info: + # In the case of failure, if info is non-None, it is the + # URL that we were verifying. We include it in the error + # message to help the user figure out what happened. + openid_url = info + fmt = "Verification of %s failed." + message = fmt % (cgi.escape(openid_url),) + elif status == consumer.SUCCESS: + # Success means that the transaction completed without + # error. If info is None, it means that the user cancelled + # the verification. + css_class = 'alert' + if info: + # This is a successful verification attempt. If this + # was a real application, we would do our login, + # comment posting, etc. here. + openid_url = info + if self.url_to_username: + username = self.url_to_username(request['environ'], openid_url) + else: + username = openid_url + if 'paste.auth_tkt.set_user' in request['environ']: + request['environ']['paste.auth_tkt.set_user'](username) + if not self.login_redirect: + fmt = ("If you had supplied a login redirect path, you would have " + "been redirected there. " + "You have successfully verified %s as your identity.") + message = fmt % (cgi.escape(openid_url),) + else: + # @@: This stuff doesn't make sense to me; why not a remote redirect? + request['environ']['paste.auth.open_id'] = openid_url + request['environ']['PATH_INFO'] = self.login_redirect + return self.app(request['environ'], request['start']) + #exc = httpexceptions.HTTPTemporaryRedirect(self.login_redirect) + #return exc.wsgi_application(request['environ'], request['start']) + else: + # cancelled + message = 'Verification cancelled' + else: + # Either we don't understand the code or there is no + # openid_url included with the error. Give a generic + # failure message. The library should supply debug + # information in a log. + message = 'Verification failed.' + + return self.render(request, message, css_class, openid_url) + + def build_url(self, request, action, **query): + """Build a URL relative to the server base_url, with the given + query parameters added.""" + base = urlparse.urljoin(request['base_url'], self.auth_prefix + '/' + action) + return appendArgs(base, query) + + def redirect(self, request, redirect_url): + """Send a redirect response to the given URL to the browser.""" + response_headers = [('Content-type', 'text/plain'), + ('Location', redirect_url)] + request['start']('302 REDIRECT', response_headers) + return ["Redirecting to %s" % redirect_url] + + def not_found(self, request): + """Render a page with a 404 return code and a message.""" + fmt = 'The path %s was not understood by this server.' + msg = fmt % (request['parsed_uri'],) + openid_url = request['query'].get('openid_url') + return self.render(request, msg, 'error', openid_url, status='404 Not Found') + + def render(self, request, message=None, css_class='alert', form_contents=None, + status='200 OK', title="Python OpenID Consumer"): + """Render a page.""" + response_headers = [('Content-type', 'text/html')] + request['start'](str(status), response_headers) + + self.page_header(request, title) + if message: + request['body'].append("
" % (css_class,)) + request['body'].append(message) + request['body'].append("
") + self.page_footer(request, form_contents) + return request['body'] + + def page_header(self, request, title): + """Render the page header""" + request['body'].append('''\ + + %s + + +

%s

+

+ This example consumer uses the Python OpenID library. It + just verifies that the URL that you enter is your identity URL. +

+''' % (title, title)) + + def page_footer(self, request, form_contents): + """Render the page footer""" + if not form_contents: + form_contents = '' + + request['body'].append('''\ +
+
+ Identity URL: + + +
+
+ + +''' % (quoteattr(self.build_url(request, 'verify')), quoteattr(form_contents))) + + +middleware = AuthOpenIDHandler + +def make_open_id_middleware( + app, + global_conf, + # Should this default to something, or inherit something from global_conf?: + data_store_path, + auth_prefix='/oid', + login_redirect=None, + catch_401=False, + url_to_username=None, + apply_auth_tkt=False, + auth_tkt_logout_path=None): + from paste.deploy.converters import asbool + from paste.util import import_string + catch_401 = asbool(catch_401) + if url_to_username and isinstance(url_to_username, six.string_types): + url_to_username = import_string.eval_import(url_to_username) + apply_auth_tkt = asbool(apply_auth_tkt) + new_app = AuthOpenIDHandler( + app, data_store_path=data_store_path, auth_prefix=auth_prefix, + login_redirect=login_redirect, catch_401=catch_401, + url_to_username=url_to_username or None) + if apply_auth_tkt: + from paste.auth import auth_tkt + new_app = auth_tkt.make_auth_tkt_middleware( + new_app, global_conf, logout_path=auth_tkt_logout_path) + return new_app diff --git a/paste/cascade.py b/paste/cascade.py new file mode 100644 index 0000000..424794e --- /dev/null +++ b/paste/cascade.py @@ -0,0 +1,133 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +Cascades through several applications, so long as applications +return ``404 Not Found``. +""" +from paste import httpexceptions +from paste.util import converters +import tempfile +from cStringIO import StringIO + +__all__ = ['Cascade'] + +def make_cascade(loader, global_conf, catch='404', **local_conf): + """ + Entry point for Paste Deploy configuration + + Expects configuration like:: + + [composit:cascade] + use = egg:Paste#cascade + # all start with 'app' and are sorted alphabetically + app1 = foo + app2 = bar + ... + catch = 404 500 ... + """ + catch = map(int, converters.aslist(catch)) + apps = [] + for name, value in local_conf.items(): + if not name.startswith('app'): + raise ValueError( + "Bad configuration key %r (=%r); all configuration keys " + "must start with 'app'" + % (name, value)) + app = loader.get_app(value, global_conf=global_conf) + apps.append((name, app)) + apps.sort() + apps = [app for name, app in apps] + return Cascade(apps, catch=catch) + +class Cascade(object): + + """ + Passed a list of applications, ``Cascade`` will try each of them + in turn. If one returns a status code listed in ``catch`` (by + default just ``404 Not Found``) then the next application is + tried. + + If all applications fail, then the last application's failure + response is used. + + Instances of this class are WSGI applications. + """ + + def __init__(self, applications, catch=(404,)): + self.apps = applications + self.catch_codes = {} + self.catch_exceptions = [] + for error in catch: + if isinstance(error, str): + error = int(error.split(None, 1)[0]) + if isinstance(error, httpexceptions.HTTPException): + exc = error + code = error.code + else: + exc = httpexceptions.get_exception(error) + code = error + self.catch_codes[code] = exc + self.catch_exceptions.append(exc) + self.catch_exceptions = tuple(self.catch_exceptions) + + def __call__(self, environ, start_response): + """ + WSGI application interface + """ + failed = [] + def repl_start_response(status, headers, exc_info=None): + code = int(status.split(None, 1)[0]) + if code in self.catch_codes: + failed.append(None) + return _consuming_writer + return start_response(status, headers, exc_info) + + try: + length = int(environ.get('CONTENT_LENGTH', 0) or 0) + except ValueError: + length = 0 + if length > 0: + # We have to copy wsgi.input + copy_wsgi_input = True + if length > 4096 or length < 0: + f = tempfile.TemporaryFile() + if length < 0: + f.write(environ['wsgi.input'].read()) + else: + copy_len = length + while copy_len > 0: + chunk = environ['wsgi.input'].read(min(copy_len, 4096)) + if not chunk: + raise IOError("Request body truncated") + f.write(chunk) + copy_len -= len(chunk) + f.seek(0) + else: + f = StringIO(environ['wsgi.input'].read(length)) + environ['wsgi.input'] = f + else: + copy_wsgi_input = False + for app in self.apps[:-1]: + environ_copy = environ.copy() + if copy_wsgi_input: + environ_copy['wsgi.input'].seek(0) + failed = [] + try: + v = app(environ_copy, repl_start_response) + if not failed: + return v + else: + if hasattr(v, 'close'): + # Exhaust the iterator first: + list(v) + # then close: + v.close() + except self.catch_exceptions: + pass + if copy_wsgi_input: + environ['wsgi.input'].seek(0) + return self.apps[-1](environ, start_response) + +def _consuming_writer(s): + pass diff --git a/paste/cgiapp.py b/paste/cgiapp.py new file mode 100644 index 0000000..c37ba4c --- /dev/null +++ b/paste/cgiapp.py @@ -0,0 +1,277 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +Application that runs a CGI script. +""" +import os +import sys +import subprocess +from six.moves.urllib.parse import quote +try: + import select +except ImportError: + select = None + +from paste.util import converters + +__all__ = ['CGIError', 'CGIApplication'] + +class CGIError(Exception): + """ + Raised when the CGI script can't be found or doesn't + act like a proper CGI script. + """ + +class CGIApplication(object): + + """ + This object acts as a proxy to a CGI application. You pass in the + script path (``script``), an optional path to search for the + script (if the name isn't absolute) (``path``). If you don't give + a path, then ``$PATH`` will be used. + """ + + def __init__(self, + global_conf, + script, + path=None, + include_os_environ=True, + query_string=None): + if global_conf: + raise NotImplemented( + "global_conf is no longer supported for CGIApplication " + "(use make_cgi_application); please pass None instead") + self.script_filename = script + if path is None: + path = os.environ.get('PATH', '').split(':') + self.path = path + if '?' in script: + assert query_string is None, ( + "You cannot have '?' in your script name (%r) and also " + "give a query_string (%r)" % (script, query_string)) + script, query_string = script.split('?', 1) + if os.path.abspath(script) != script: + # relative path + for path_dir in self.path: + if os.path.exists(os.path.join(path_dir, script)): + self.script = os.path.join(path_dir, script) + break + else: + raise CGIError( + "Script %r not found in path %r" + % (script, self.path)) + else: + self.script = script + self.include_os_environ = include_os_environ + self.query_string = query_string + + def __call__(self, environ, start_response): + if 'REQUEST_URI' not in environ: + environ['REQUEST_URI'] = ( + quote(environ.get('SCRIPT_NAME', '')) + + quote(environ.get('PATH_INFO', ''))) + if self.include_os_environ: + cgi_environ = os.environ.copy() + else: + cgi_environ = {} + for name in environ: + # Should unicode values be encoded? + if (name.upper() == name + and isinstance(environ[name], str)): + cgi_environ[name] = environ[name] + if self.query_string is not None: + old = cgi_environ.get('QUERY_STRING', '') + if old: + old += '&' + cgi_environ['QUERY_STRING'] = old + self.query_string + cgi_environ['SCRIPT_FILENAME'] = self.script + proc = subprocess.Popen( + [self.script], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=cgi_environ, + cwd=os.path.dirname(self.script), + ) + writer = CGIWriter(environ, start_response) + if select and sys.platform != 'win32': + proc_communicate( + proc, + stdin=StdinReader.from_environ(environ), + stdout=writer, + stderr=environ['wsgi.errors']) + else: + stdout, stderr = proc.communicate(StdinReader.from_environ(environ).read()) + if stderr: + environ['wsgi.errors'].write(stderr) + writer.write(stdout) + if not writer.headers_finished: + start_response(writer.status, writer.headers) + return [] + +class CGIWriter(object): + + def __init__(self, environ, start_response): + self.environ = environ + self.start_response = start_response + self.status = '200 OK' + self.headers = [] + self.headers_finished = False + self.writer = None + self.buffer = '' + + def write(self, data): + if self.headers_finished: + self.writer(data) + return + self.buffer += data + while '\n' in self.buffer: + if '\r\n' in self.buffer and self.buffer.find('\r\n') < self.buffer.find('\n'): + line1, self.buffer = self.buffer.split('\r\n', 1) + else: + line1, self.buffer = self.buffer.split('\n', 1) + if not line1: + self.headers_finished = True + self.writer = self.start_response( + self.status, self.headers) + self.writer(self.buffer) + del self.buffer + del self.headers + del self.status + break + elif ':' not in line1: + raise CGIError( + "Bad header line: %r" % line1) + else: + name, value = line1.split(':', 1) + value = value.lstrip() + name = name.strip() + if name.lower() == 'status': + if ' ' not in value: + # WSGI requires this space, sometimes CGI scripts don't set it: + value = '%s General' % value + self.status = value + else: + self.headers.append((name, value)) + +class StdinReader(object): + + def __init__(self, stdin, content_length): + self.stdin = stdin + self.content_length = content_length + + def from_environ(cls, environ): + length = environ.get('CONTENT_LENGTH') + if length: + length = int(length) + else: + length = 0 + return cls(environ['wsgi.input'], length) + + from_environ = classmethod(from_environ) + + def read(self, size=None): + if not self.content_length: + return '' + if size is None: + text = self.stdin.read(self.content_length) + else: + text = self.stdin.read(min(self.content_length, size)) + self.content_length -= len(text) + return text + +def proc_communicate(proc, stdin=None, stdout=None, stderr=None): + """ + Run the given process, piping input/output/errors to the given + file-like objects (which need not be actual file objects, unlike + the arguments passed to Popen). Wait for process to terminate. + + Note: this is taken from the posix version of + subprocess.Popen.communicate, but made more general through the + use of file-like objects. + """ + read_set = [] + write_set = [] + input_buffer = '' + trans_nl = proc.universal_newlines and hasattr(open, 'newlines') + + if proc.stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + proc.stdin.flush() + if input: + write_set.append(proc.stdin) + else: + proc.stdin.close() + else: + assert stdin is None + if proc.stdout: + read_set.append(proc.stdout) + else: + assert stdout is None + if proc.stderr: + read_set.append(proc.stderr) + else: + assert stderr is None + + while read_set or write_set: + rlist, wlist, xlist = select.select(read_set, write_set, []) + + if proc.stdin in wlist: + # When select has indicated that the file is writable, + # we can write up to PIPE_BUF bytes without risk + # blocking. POSIX defines PIPE_BUF >= 512 + next, input_buffer = input_buffer, '' + next_len = 512-len(next) + if next_len: + next += stdin.read(next_len) + if not next: + proc.stdin.close() + write_set.remove(proc.stdin) + else: + bytes_written = os.write(proc.stdin.fileno(), next) + if bytes_written < len(next): + input_buffer = next[bytes_written:] + + if proc.stdout in rlist: + data = os.read(proc.stdout.fileno(), 1024) + if data == "": + proc.stdout.close() + read_set.remove(proc.stdout) + if trans_nl: + data = proc._translate_newlines(data) + stdout.write(data) + + if proc.stderr in rlist: + data = os.read(proc.stderr.fileno(), 1024) + if data == "": + proc.stderr.close() + read_set.remove(proc.stderr) + if trans_nl: + data = proc._translate_newlines(data) + stderr.write(data) + + try: + proc.wait() + except OSError as e: + if e.errno != 10: + raise + +def make_cgi_application(global_conf, script, path=None, include_os_environ=None, + query_string=None): + """ + Paste Deploy interface for :class:`CGIApplication` + + This object acts as a proxy to a CGI application. You pass in the + script path (``script``), an optional path to search for the + script (if the name isn't absolute) (``path``). If you don't give + a path, then ``$PATH`` will be used. + """ + if path is None: + path = global_conf.get('path') or global_conf.get('PATH') + include_os_environ = converters.asbool(include_os_environ) + return CGIApplication( + None, + script, path=path, include_os_environ=include_os_environ, + query_string=query_string) diff --git a/paste/cgitb_catcher.py b/paste/cgitb_catcher.py new file mode 100644 index 0000000..55a346f --- /dev/null +++ b/paste/cgitb_catcher.py @@ -0,0 +1,117 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +WSGI middleware + +Captures any exceptions and prints a pretty report. See the `cgitb +documentation `_ +for more. +""" + +import cgitb +import six +from six.moves import cStringIO as StringIO +import sys + +from paste.util import converters + +class NoDefault(object): + pass + +class CgitbMiddleware(object): + + def __init__(self, app, + global_conf=None, + display=NoDefault, + logdir=None, + context=5, + format="html"): + self.app = app + if global_conf is None: + global_conf = {} + if display is NoDefault: + display = global_conf.get('debug') + if isinstance(display, six.string_types): + display = converters.asbool(display) + self.display = display + self.logdir = logdir + self.context = int(context) + self.format = format + + def __call__(self, environ, start_response): + try: + app_iter = self.app(environ, start_response) + return self.catching_iter(app_iter, environ) + except: + exc_info = sys.exc_info() + start_response('500 Internal Server Error', + [('content-type', 'text/html')], + exc_info) + response = self.exception_handler(exc_info, environ) + return [response] + + def catching_iter(self, app_iter, environ): + if not app_iter: + raise StopIteration + error_on_close = False + try: + for v in app_iter: + yield v + if hasattr(app_iter, 'close'): + error_on_close = True + app_iter.close() + except: + response = self.exception_handler(sys.exc_info(), environ) + if not error_on_close and hasattr(app_iter, 'close'): + try: + app_iter.close() + except: + close_response = self.exception_handler( + sys.exc_info(), environ) + response += ( + '
Error in .close():
%s' + % close_response) + yield response + + def exception_handler(self, exc_info, environ): + dummy_file = StringIO() + hook = cgitb.Hook(file=dummy_file, + display=self.display, + logdir=self.logdir, + context=self.context, + format=self.format) + hook(*exc_info) + return dummy_file.getvalue() + +def make_cgitb_middleware(app, global_conf, + display=NoDefault, + logdir=None, + context=5, + format='html'): + """ + Wraps the application in the ``cgitb`` (standard library) + error catcher. + + display: + If true (or debug is set in the global configuration) + then the traceback will be displayed in the browser + + logdir: + Writes logs of all errors in that directory + + context: + Number of lines of context to show around each line of + source code + """ + from paste.deploy.converters import asbool + if display is not NoDefault: + display = asbool(display) + if 'debug' in global_conf: + global_conf['debug'] = asbool(global_conf['debug']) + return CgitbMiddleware( + app, global_conf=global_conf, + display=display, + logdir=logdir, + context=context, + format=format) diff --git a/paste/config.py b/paste/config.py new file mode 100644 index 0000000..c531579 --- /dev/null +++ b/paste/config.py @@ -0,0 +1,120 @@ +# (c) 2006 Ian Bicking, Philip Jenvey and contributors +# Written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +"""Paste Configuration Middleware and Objects""" +from paste.registry import RegistryManager, StackedObjectProxy + +__all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware'] + +class DispatchingConfig(StackedObjectProxy): + """ + This is a configuration object that can be used globally, + imported, have references held onto. The configuration may differ + by thread (or may not). + + Specific configurations are registered (and deregistered) either + for the process or for threads. + """ + # @@: What should happen when someone tries to add this + # configuration to itself? Probably the conf should become + # resolved, and get rid of this delegation wrapper + + def __init__(self, name='DispatchingConfig'): + super(DispatchingConfig, self).__init__(name=name) + self.__dict__['_process_configs'] = [] + + def push_thread_config(self, conf): + """ + Make ``conf`` the active configuration for this thread. + Thread-local configuration always overrides process-wide + configuration. + + This should be used like:: + + conf = make_conf() + dispatching_config.push_thread_config(conf) + try: + ... do stuff ... + finally: + dispatching_config.pop_thread_config(conf) + """ + self._push_object(conf) + + def pop_thread_config(self, conf=None): + """ + Remove a thread-local configuration. If ``conf`` is given, + it is checked against the popped configuration and an error + is emitted if they don't match. + """ + self._pop_object(conf) + + def push_process_config(self, conf): + """ + Like push_thread_config, but applies the configuration to + the entire process. + """ + self._process_configs.append(conf) + + def pop_process_config(self, conf=None): + self._pop_from(self._process_configs, conf) + + def _pop_from(self, lst, conf): + popped = lst.pop() + if conf is not None and popped is not conf: + raise AssertionError( + "The config popped (%s) is not the same as the config " + "expected (%s)" + % (popped, conf)) + + def _current_obj(self): + try: + return super(DispatchingConfig, self)._current_obj() + except TypeError: + if self._process_configs: + return self._process_configs[-1] + raise AttributeError( + "No configuration has been registered for this process " + "or thread") + current = current_conf = _current_obj + +CONFIG = DispatchingConfig() + +no_config = object() +class ConfigMiddleware(RegistryManager): + """ + A WSGI middleware that adds a ``paste.config`` key (by default) + to the request environment, as well as registering the + configuration temporarily (for the length of the request) with + ``paste.config.CONFIG`` (or any other ``DispatchingConfig`` + object). + """ + + def __init__(self, application, config, dispatching_config=CONFIG, + environ_key='paste.config'): + """ + This delegates all requests to `application`, adding a *copy* + of the configuration `config`. + """ + def register_config(environ, start_response): + popped_config = environ.get(environ_key, no_config) + current_config = environ[environ_key] = config.copy() + environ['paste.registry'].register(dispatching_config, + current_config) + + try: + app_iter = application(environ, start_response) + finally: + if popped_config is no_config: + environ.pop(environ_key, None) + else: + environ[environ_key] = popped_config + return app_iter + + super(self.__class__, self).__init__(register_config) + +def make_config_filter(app, global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + return ConfigMiddleware(app, conf) + +make_config_middleware = ConfigMiddleware.__doc__ diff --git a/paste/cowbell/__init__.py b/paste/cowbell/__init__.py new file mode 100644 index 0000000..43b7097 --- /dev/null +++ b/paste/cowbell/__init__.py @@ -0,0 +1,104 @@ +# Cowbell images: http://commons.wikimedia.org/wiki/Image:Cowbell-1.jpg +import os +import re +from paste.fileapp import FileApp +from paste.response import header_value, remove_header + +SOUND = "http://www.c-eye.net/eyeon/WalkenWAVS/explorestudiospace.wav" + +class MoreCowbell(object): + def __init__(self, app): + self.app = app + def __call__(self, environ, start_response): + path_info = environ.get('PATH_INFO', '') + script_name = environ.get('SCRIPT_NAME', '') + for filename in ['bell-ascending.png', 'bell-descending.png']: + if path_info == '/.cowbell/'+ filename: + app = FileApp(os.path.join(os.path.dirname(__file__), filename)) + return app(environ, start_response) + type = [] + body = [] + def repl_start_response(status, headers, exc_info=None): + ct = header_value(headers, 'content-type') + if ct and ct.startswith('text/html'): + type.append(ct) + remove_header(headers, 'content-length') + start_response(status, headers, exc_info) + return body.append + return start_response(status, headers, exc_info) + app_iter = self.app(environ, repl_start_response) + if type: + # Got text/html + body.extend(app_iter) + body = ''.join(body) + body = insert_head(body, self.javascript.replace('__SCRIPT_NAME__', script_name)) + body = insert_body(body, self.resources.replace('__SCRIPT_NAME__', script_name)) + return [body] + else: + return app_iter + + javascript = '''\ + +''' + + resources = '''\ + + +''' + +def insert_head(body, text): + end_head = re.search(r'', body, re.I) + if end_head: + return body[:end_head.start()] + text + body[end_head.end():] + else: + return text + body + +def insert_body(body, text): + end_body = re.search(r'', body, re.I) + if end_body: + return body[:end_body.start()] + text + body[end_body.end():] + else: + return body + text + +def make_cowbell(global_conf, app): + return MoreCowbell(app) + +if __name__ == '__main__': + from paste.debug.debugapp import SimpleApplication + app = MoreCowbell(SimpleApplication()) + from paste.httpserver import serve + serve(app) diff --git a/paste/cowbell/bell-ascending.png b/paste/cowbell/bell-ascending.png new file mode 100644 index 0000000..42f33db Binary files /dev/null and b/paste/cowbell/bell-ascending.png differ diff --git a/paste/cowbell/bell-descending.png b/paste/cowbell/bell-descending.png new file mode 100644 index 0000000..dac8012 Binary files /dev/null and b/paste/cowbell/bell-descending.png differ diff --git a/paste/debug/__init__.py b/paste/debug/__init__.py new file mode 100644 index 0000000..daef7cc --- /dev/null +++ b/paste/debug/__init__.py @@ -0,0 +1,5 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Package for debugging and development tools +""" diff --git a/paste/debug/debugapp.py b/paste/debug/debugapp.py new file mode 100755 index 0000000..8c7c7c2 --- /dev/null +++ b/paste/debug/debugapp.py @@ -0,0 +1,79 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +Various Applications for Debugging/Testing Purposes +""" + +import time +__all__ = ['SimpleApplication', 'SlowConsumer'] + + +class SimpleApplication(object): + """ + Produces a simple web page + """ + def __call__(self, environ, start_response): + body = "simple" + start_response("200 OK", [('Content-Type', 'text/html'), + ('Content-Length', str(len(body)))]) + return [body] + +class SlowConsumer(object): + """ + Consumes an upload slowly... + + NOTE: This should use the iterator form of ``wsgi.input``, + but it isn't implemented in paste.httpserver. + """ + def __init__(self, chunk_size = 4096, delay = 1, progress = True): + self.chunk_size = chunk_size + self.delay = delay + self.progress = True + + def __call__(self, environ, start_response): + size = 0 + total = environ.get('CONTENT_LENGTH') + if total: + remaining = int(total) + while remaining > 0: + if self.progress: + print("%s of %s remaining" % (remaining, total)) + if remaining > 4096: + chunk = environ['wsgi.input'].read(4096) + else: + chunk = environ['wsgi.input'].read(remaining) + if not chunk: + break + size += len(chunk) + remaining -= len(chunk) + if self.delay: + time.sleep(self.delay) + body = "%d bytes" % size + else: + body = ('\n' + '
\n' + '\n' + '\n' + '
\n') + print("bingles") + start_response("200 OK", [('Content-Type', 'text/html'), + ('Content-Length', len(body))]) + return [body] + +def make_test_app(global_conf): + return SimpleApplication() + +make_test_app.__doc__ = SimpleApplication.__doc__ + +def make_slow_app(global_conf, chunk_size=4096, delay=1, progress=True): + from paste.deploy.converters import asbool + return SlowConsumer( + chunk_size=int(chunk_size), + delay=int(delay), + progress=asbool(progress)) + +make_slow_app.__doc__ = SlowConsumer.__doc__ diff --git a/paste/debug/doctest_webapp.py b/paste/debug/doctest_webapp.py new file mode 100755 index 0000000..f399ac3 --- /dev/null +++ b/paste/debug/doctest_webapp.py @@ -0,0 +1,435 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +#!/usr/bin/env python2.4 +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +These are functions for use when doctest-testing a document. +""" + +try: + import subprocess +except ImportError: + from paste.util import subprocess24 as subprocess +import doctest +import os +import sys +import shutil +import re +import cgi +import rfc822 +from cStringIO import StringIO +from paste.util import PySourceColor + + +here = os.path.abspath(__file__) +paste_parent = os.path.dirname( + os.path.dirname(os.path.dirname(here))) + +def run(command): + data = run_raw(command) + if data: + print(data) + +def run_raw(command): + """ + Runs the string command, returns any output. + """ + proc = subprocess.Popen(command, shell=True, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, env=_make_env()) + data = proc.stdout.read() + proc.wait() + while data.endswith('\n') or data.endswith('\r'): + data = data[:-1] + if data: + data = '\n'.join( + [l for l in data.splitlines() if l]) + return data + else: + return '' + +def run_command(command, name, and_print=False): + output = run_raw(command) + data = '$ %s\n%s' % (command, output) + show_file('shell-command', name, description='shell transcript', + data=data) + if and_print and output: + print(output) + +def _make_env(): + env = os.environ.copy() + env['PATH'] = (env.get('PATH', '') + + ':' + + os.path.join(paste_parent, 'scripts') + + ':' + + os.path.join(paste_parent, 'paste', '3rd-party', + 'sqlobject-files', 'scripts')) + env['PYTHONPATH'] = (env.get('PYTHONPATH', '') + + ':' + + paste_parent) + return env + +def clear_dir(dir): + """ + Clears (deletes) the given directory + """ + shutil.rmtree(dir, True) + +def ls(dir=None, recurse=False, indent=0): + """ + Show a directory listing + """ + dir = dir or os.getcwd() + fns = os.listdir(dir) + fns.sort() + for fn in fns: + full = os.path.join(dir, fn) + if os.path.isdir(full): + fn = fn + '/' + print(' '*indent + fn) + if os.path.isdir(full) and recurse: + ls(dir=full, recurse=True, indent=indent+2) + +default_app = None +default_url = None + +def set_default_app(app, url): + global default_app + global default_url + default_app = app + default_url = url + +def resource_filename(fn): + """ + Returns the filename of the resource -- generally in the directory + resources/DocumentName/fn + """ + return os.path.join( + os.path.dirname(sys.testing_document_filename), + 'resources', + os.path.splitext(os.path.basename(sys.testing_document_filename))[0], + fn) + +def show(path_info, example_name): + fn = resource_filename(example_name + '.html') + out = StringIO() + assert default_app is not None, ( + "No default_app set") + url = default_url + path_info + out.write('%s
\n' + % (url, url)) + out.write('
\n') + proc = subprocess.Popen( + ['paster', 'serve' '--server=console', '--no-verbose', + '--url=' + path_info], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + env=_make_env()) + stdout, errors = proc.communicate() + stdout = StringIO(stdout) + headers = rfc822.Message(stdout) + content = stdout.read() + for header, value in headers.items(): + if header.lower() == 'status' and int(value.split()[0]) == 200: + continue + if header.lower() in ('content-type', 'content-length'): + continue + if (header.lower() == 'set-cookie' + and value.startswith('_SID_')): + continue + out.write('%s: %s
\n' + % (header, value)) + lines = [l for l in content.splitlines() if l.strip()] + for line in lines: + out.write(line + '\n') + if errors: + out.write('
%s
' + % errors) + out.write('
\n') + result = out.getvalue() + if not os.path.exists(fn): + f = open(fn, 'wb') + f.write(result) + f.close() + else: + f = open(fn, 'rb') + expected = f.read() + f.close() + if not html_matches(expected, result): + print('Pages did not match. Expected from %s:' % fn) + print('-'*60) + print(expected) + print('='*60) + print('Actual output:') + print('-'*60) + print(result) + +def html_matches(pattern, text): + regex = re.escape(pattern) + regex = regex.replace(r'\.\.\.', '.*') + regex = re.sub(r'0x[0-9a-f]+', '.*', regex) + regex = '^%s$' % regex + return re.search(regex, text) + +def convert_docstring_string(data): + if data.startswith('\n'): + data = data[1:] + lines = data.splitlines() + new_lines = [] + for line in lines: + if line.rstrip() == '.': + new_lines.append('') + else: + new_lines.append(line) + data = '\n'.join(new_lines) + '\n' + return data + +def create_file(path, version, data): + data = convert_docstring_string(data) + write_data(path, data) + show_file(path, version) + +def append_to_file(path, version, data): + data = convert_docstring_string(data) + f = open(path, 'a') + f.write(data) + f.close() + # I think these appends can happen so quickly (in less than a second) + # that the .pyc file doesn't appear to be expired, even though it + # is after we've made this change; so we have to get rid of the .pyc + # file: + if path.endswith('.py'): + pyc_file = path + 'c' + if os.path.exists(pyc_file): + os.unlink(pyc_file) + show_file(path, version, description='added to %s' % path, + data=data) + +def show_file(path, version, description=None, data=None): + ext = os.path.splitext(path)[1] + if data is None: + f = open(path, 'rb') + data = f.read() + f.close() + if ext == '.py': + html = ('
%s
' + % PySourceColor.str2html(data, PySourceColor.dark)) + else: + html = '
%s
' % cgi.escape(data, 1) + html = '%s
%s' % ( + description or path, html) + write_data(resource_filename('%s.%s.gen.html' % (path, version)), + html) + +def call_source_highlight(input, format): + proc = subprocess.Popen(['source-highlight', '--out-format=html', + '--no-doc', '--css=none', + '--src-lang=%s' % format], shell=False, + stdout=subprocess.PIPE) + stdout, stderr = proc.communicate(input) + result = stdout + proc.wait() + return result + + +def write_data(path, data): + dir = os.path.dirname(os.path.abspath(path)) + if not os.path.exists(dir): + os.makedirs(dir) + f = open(path, 'wb') + f.write(data) + f.close() + + +def change_file(path, changes): + f = open(os.path.abspath(path), 'rb') + lines = f.readlines() + f.close() + for change_type, line, text in changes: + if change_type == 'insert': + lines[line:line] = [text] + elif change_type == 'delete': + lines[line:text] = [] + else: + assert 0, ( + "Unknown change_type: %r" % change_type) + f = open(path, 'wb') + f.write(''.join(lines)) + f.close() + +class LongFormDocTestParser(doctest.DocTestParser): + + """ + This parser recognizes some reST comments as commands, without + prompts or expected output, like: + + .. run: + + do_this(... + ...) + """ + + _EXAMPLE_RE = re.compile(r""" + # Source consists of a PS1 line followed by zero or more PS2 lines. + (?: (?P + (?:^(?P [ ]*) >>> .*) # PS1 line + (?:\n [ ]* \.\.\. .*)*) # PS2 lines + \n? + # Want consists of any non-blank lines that do not start with PS1. + (?P (?:(?![ ]*$) # Not a blank line + (?![ ]*>>>) # Not a line starting with PS1 + .*$\n? # But any other line + )*)) + | + (?: # This is for longer commands that are prefixed with a reST + # comment like '.. run:' (two colons makes that a directive). + # These commands cannot have any output. + + (?:^\.\.[ ]*(?Prun):[ ]*\n) # Leading command/command + (?:[ ]*\n)? # Blank line following + (?P + (?:(?P [ ]+)[^ ].*$) + (?:\n [ ]+ .*)*) + ) + | + (?: # This is for shell commands + + (?P + (?:^(P [ ]*) [$] .*) # Shell line + (?:\n [ ]* [>] .*)*) # Continuation + \n? + # Want consists of any non-blank lines that do not start with $ + (?P (?:(?![ ]*$) + (?![ ]*[$]$) + .*$\n? + )*)) + """, re.MULTILINE | re.VERBOSE) + + def _parse_example(self, m, name, lineno): + r""" + Given a regular expression match from `_EXAMPLE_RE` (`m`), + return a pair `(source, want)`, where `source` is the matched + example's source code (with prompts and indentation stripped); + and `want` is the example's expected output (with indentation + stripped). + + `name` is the string's name, and `lineno` is the line number + where the example starts; both are used for error messages. + + >>> def parseit(s): + ... p = LongFormDocTestParser() + ... return p._parse_example(p._EXAMPLE_RE.search(s), '', 1) + >>> parseit('>>> 1\n1') + ('1', {}, '1', None) + >>> parseit('>>> (1\n... +1)\n2') + ('(1\n+1)', {}, '2', None) + >>> parseit('.. run:\n\n test1\n test2\n') + ('test1\ntest2', {}, '', None) + """ + # Get the example's indentation level. + runner = m.group('run') or '' + indent = len(m.group('%sindent' % runner)) + + # Divide source into lines; check that they're properly + # indented; and then strip their indentation & prompts. + source_lines = m.group('%ssource' % runner).split('\n') + if runner: + self._check_prefix(source_lines[1:], ' '*indent, name, lineno) + else: + self._check_prompt_blank(source_lines, indent, name, lineno) + self._check_prefix(source_lines[2:], ' '*indent + '.', name, lineno) + if runner: + source = '\n'.join([sl[indent:] for sl in source_lines]) + else: + source = '\n'.join([sl[indent+4:] for sl in source_lines]) + + if runner: + want = '' + exc_msg = None + else: + # Divide want into lines; check that it's properly indented; and + # then strip the indentation. Spaces before the last newline should + # be preserved, so plain rstrip() isn't good enough. + want = m.group('want') + want_lines = want.split('\n') + if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]): + del want_lines[-1] # forget final newline & spaces after it + self._check_prefix(want_lines, ' '*indent, name, + lineno + len(source_lines)) + want = '\n'.join([wl[indent:] for wl in want_lines]) + + # If `want` contains a traceback message, then extract it. + m = self._EXCEPTION_RE.match(want) + if m: + exc_msg = m.group('msg') + else: + exc_msg = None + + # Extract options from the source. + options = self._find_options(source, name, lineno) + + return source, options, want, exc_msg + + + def parse(self, string, name=''): + """ + Divide the given string into examples and intervening text, + and return them as a list of alternating Examples and strings. + Line numbers for the Examples are 0-based. The optional + argument `name` is a name identifying this string, and is only + used for error messages. + """ + string = string.expandtabs() + # If all lines begin with the same indentation, then strip it. + min_indent = self._min_indent(string) + if min_indent > 0: + string = '\n'.join([l[min_indent:] for l in string.split('\n')]) + + output = [] + charno, lineno = 0, 0 + # Find all doctest examples in the string: + for m in self._EXAMPLE_RE.finditer(string): + # Add the pre-example text to `output`. + output.append(string[charno:m.start()]) + # Update lineno (lines before this example) + lineno += string.count('\n', charno, m.start()) + # Extract info from the regexp match. + (source, options, want, exc_msg) = \ + self._parse_example(m, name, lineno) + # Create an Example, and add it to the list. + if not self._IS_BLANK_OR_COMMENT(source): + # @@: Erg, this is the only line I need to change... + output.append(doctest.Example( + source, want, exc_msg, + lineno=lineno, + indent=min_indent+len(m.group('indent') or m.group('runindent')), + options=options)) + # Update lineno (lines inside this example) + lineno += string.count('\n', m.start(), m.end()) + # Update charno. + charno = m.end() + # Add any remaining post-example text to `output`. + output.append(string[charno:]) + return output + + + +if __name__ == '__main__': + if sys.argv[1:] and sys.argv[1] == 'doctest': + doctest.testmod() + sys.exit() + if not paste_parent in sys.path: + sys.path.append(paste_parent) + for fn in sys.argv[1:]: + fn = os.path.abspath(fn) + # @@: OK, ick; but this module gets loaded twice + sys.testing_document_filename = fn + doctest.testfile( + fn, module_relative=False, + optionflags=doctest.ELLIPSIS|doctest.REPORT_ONLY_FIRST_FAILURE, + parser=LongFormDocTestParser()) + new = os.path.splitext(fn)[0] + '.html' + assert new != fn + os.system('rst2html.py %s > %s' % (fn, new)) diff --git a/paste/debug/fsdiff.py b/paste/debug/fsdiff.py new file mode 100644 index 0000000..156a2e4 --- /dev/null +++ b/paste/debug/fsdiff.py @@ -0,0 +1,419 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Module to find differences over time in a filesystem + +Basically this takes a snapshot of a directory, then sees what changes +were made. The contents of the files are not checked, so you can +detect that the content was changed, but not what the old version of +the file was. +""" + +import os +from fnmatch import fnmatch +from datetime import datetime + +try: + # Python 3 + import collections.UserDict as IterableUserDict +except ImportError: + try: + # Python 2.5-2.7 + from UserDict import IterableUserDict + except ImportError: + # Python <= 2.4 + from paste.util.UserDict24 import IterableUserDict +import operator +import re + +__all__ = ['Diff', 'Snapshot', 'File', 'Dir', 'report_expected_diffs', + 'show_diff'] + +class Diff(object): + + """ + Represents the difference between two snapshots + """ + + def __init__(self, before, after): + self.before = before + self.after = after + self._calculate() + + def _calculate(self): + before = self.before.data + after = self.after.data + self.deleted = {} + self.updated = {} + self.created = after.copy() + for path, f in before.items(): + if path not in after: + self.deleted[path] = f + continue + del self.created[path] + if f.mtime < after[path].mtime: + self.updated[path] = after[path] + + def __str__(self): + return self.report() + + def report(self, header=True, dates=False): + s = [] + if header: + s.append('Difference in %s from %s to %s:' % + (self.before.base_path, + self.before.calculated, + self.after.calculated)) + for name, files, show_size in [ + ('created', self.created, True), + ('deleted', self.deleted, True), + ('updated', self.updated, True)]: + if files: + s.append('-- %s: -------------------' % name) + files = files.items() + files.sort() + last = '' + for path, f in files: + t = ' %s' % _space_prefix(last, path, indent=4, + include_sep=False) + last = path + if show_size and f.size != 'N/A': + t += ' (%s bytes)' % f.size + if dates: + parts = [] + if self.before.get(path): + parts.append(self.before[path].mtime) + if self.after.get(path): + parts.append(self.after[path].mtime) + t += ' (mtime: %s)' % ('->'.join(map(repr, parts))) + s.append(t) + if len(s) == 1: + s.append(' (no changes)') + return '\n'.join(s) + +class Snapshot(IterableUserDict): + + """ + Represents a snapshot of a set of files. Has a dictionary-like + interface, keyed relative to ``base_path`` + """ + + def __init__(self, base_path, files=None, ignore_wildcards=(), + ignore_paths=(), ignore_hidden=True): + self.base_path = base_path + self.ignore_wildcards = ignore_wildcards + self.ignore_hidden = ignore_hidden + self.ignore_paths = ignore_paths + self.calculated = None + self.data = files or {} + if files is None: + self.find_files() + + ############################################################ + ## File finding + ############################################################ + + def find_files(self): + """ + Find all the files under the base path, and put them in + ``self.data`` + """ + self._find_traverse('', self.data) + self.calculated = datetime.now() + + def _ignore_file(self, fn): + if fn in self.ignore_paths: + return True + if self.ignore_hidden and os.path.basename(fn).startswith('.'): + return True + for pat in self.ignore_wildcards: + if fnmatch(fn, pat): + return True + return False + + def _ignore_file(self, fn): + if fn in self.ignore_paths: + return True + if self.ignore_hidden and os.path.basename(fn).startswith('.'): + return True + return False + + def _find_traverse(self, path, result): + full = os.path.join(self.base_path, path) + if os.path.isdir(full): + if path: + # Don't actually include the base path + result[path] = Dir(self.base_path, path) + for fn in os.listdir(full): + fn = os.path.join(path, fn) + if self._ignore_file(fn): + continue + self._find_traverse(fn, result) + else: + result[path] = File(self.base_path, path) + + def __repr__(self): + return '<%s in %r from %r>' % ( + self.__class__.__name__, self.base_path, + self.calculated or '(no calculation done)') + + def compare_expected(self, expected, comparison=operator.eq, + differ=None, not_found=None, + include_success=False): + """ + Compares a dictionary of ``path: content`` to the + found files. Comparison is done by equality, or the + ``comparison(actual_content, expected_content)`` function given. + + Returns dictionary of differences, keyed by path. Each + difference is either noted, or the output of + ``differ(actual_content, expected_content)`` is given. + + If a file does not exist and ``not_found`` is given, then + ``not_found(path)`` is put in. + """ + result = {} + for path in expected: + orig_path = path + path = path.strip('/') + if path not in self.data: + if not_found: + msg = not_found(path) + else: + msg = 'not found' + result[path] = msg + continue + expected_content = expected[orig_path] + file = self.data[path] + actual_content = file.bytes + if not comparison(actual_content, expected_content): + if differ: + msg = differ(actual_content, expected_content) + else: + if len(actual_content) < len(expected_content): + msg = 'differ (%i bytes smaller)' % ( + len(expected_content) - len(actual_content)) + elif len(actual_content) > len(expected_content): + msg = 'differ (%i bytes larger)' % ( + len(actual_content) - len(expected_content)) + else: + msg = 'diff (same size)' + result[path] = msg + elif include_success: + result[path] = 'same!' + return result + + def diff_to_now(self): + return Diff(self, self.clone()) + + def clone(self): + return self.__class__(base_path=self.base_path, + ignore_wildcards=self.ignore_wildcards, + ignore_paths=self.ignore_paths, + ignore_hidden=self.ignore_hidden) + +class File(object): + + """ + Represents a single file found as the result of a command. + + Has attributes: + + ``path``: + The path of the file, relative to the ``base_path`` + + ``full``: + The full path + + ``stat``: + The results of ``os.stat``. Also ``mtime`` and ``size`` + contain the ``.st_mtime`` and ``st_size`` of the stat. + + ``bytes``: + The contents of the file. + + You may use the ``in`` operator with these objects (tested against + the contents of the file), and the ``.mustcontain()`` method. + """ + + file = True + dir = False + + def __init__(self, base_path, path): + self.base_path = base_path + self.path = path + self.full = os.path.join(base_path, path) + self.stat = os.stat(self.full) + self.mtime = self.stat.st_mtime + self.size = self.stat.st_size + self._bytes = None + + def bytes__get(self): + if self._bytes is None: + f = open(self.full, 'rb') + self._bytes = f.read() + f.close() + return self._bytes + bytes = property(bytes__get) + + def __contains__(self, s): + return s in self.bytes + + def mustcontain(self, s): + __tracebackhide__ = True + bytes = self.bytes + if s not in bytes: + print('Could not find %r in:' % s) + print(bytes) + assert s in bytes + + def __repr__(self): + return '<%s %s:%s>' % ( + self.__class__.__name__, + self.base_path, self.path) + +class Dir(File): + + """ + Represents a directory created by a command. + """ + + file = False + dir = True + + def __init__(self, base_path, path): + self.base_path = base_path + self.path = path + self.full = os.path.join(base_path, path) + self.size = 'N/A' + self.mtime = 'N/A' + + def __repr__(self): + return '<%s %s:%s>' % ( + self.__class__.__name__, + self.base_path, self.path) + + def bytes__get(self): + raise NotImplementedError( + "Directory %r doesn't have content" % self) + + bytes = property(bytes__get) + + +def _space_prefix(pref, full, sep=None, indent=None, include_sep=True): + """ + Anything shared by pref and full will be replaced with spaces + in full, and full returned. + + Example:: + + >>> _space_prefix('/foo/bar', '/foo') + ' /bar' + """ + if sep is None: + sep = os.path.sep + pref = pref.split(sep) + full = full.split(sep) + padding = [] + while pref and full and pref[0] == full[0]: + if indent is None: + padding.append(' ' * (len(full[0]) + len(sep))) + else: + padding.append(' ' * indent) + full.pop(0) + pref.pop(0) + if padding: + if include_sep: + return ''.join(padding) + sep + sep.join(full) + else: + return ''.join(padding) + sep.join(full) + else: + return sep.join(full) + +def report_expected_diffs(diffs, colorize=False): + """ + Takes the output of compare_expected, and returns a string + description of the differences. + """ + if not diffs: + return 'No differences' + diffs = diffs.items() + diffs.sort() + s = [] + last = '' + for path, desc in diffs: + t = _space_prefix(last, path, indent=4, include_sep=False) + if colorize: + t = color_line(t, 11) + last = path + if len(desc.splitlines()) > 1: + cur_indent = len(re.search(r'^[ ]*', t).group(0)) + desc = indent(cur_indent+2, desc) + if colorize: + t += '\n' + for line in desc.splitlines(): + if line.strip().startswith('+'): + line = color_line(line, 10) + elif line.strip().startswith('-'): + line = color_line(line, 9) + else: + line = color_line(line, 14) + t += line+'\n' + else: + t += '\n' + desc + else: + t += ' '+desc + s.append(t) + s.append('Files with differences: %s' % len(diffs)) + return '\n'.join(s) + +def color_code(foreground=None, background=None): + """ + 0 black + 1 red + 2 green + 3 yellow + 4 blue + 5 magenta (purple) + 6 cyan + 7 white (gray) + + Add 8 to get high-intensity + """ + if foreground is None and background is None: + # Reset + return '\x1b[0m' + codes = [] + if foreground is None: + codes.append('[39m') + elif foreground > 7: + codes.append('[1m') + codes.append('[%im' % (22+foreground)) + else: + codes.append('[%im' % (30+foreground)) + if background is None: + codes.append('[49m') + else: + codes.append('[%im' % (40+background)) + return '\x1b' + '\x1b'.join(codes) + +def color_line(line, foreground=None, background=None): + match = re.search(r'^(\s*)', line) + return (match.group(1) + color_code(foreground, background) + + line[match.end():] + color_code()) + +def indent(indent, text): + return '\n'.join( + [' '*indent + l for l in text.splitlines()]) + +def show_diff(actual_content, expected_content): + actual_lines = [l.strip() for l in actual_content.splitlines() + if l.strip()] + expected_lines = [l.strip() for l in expected_content.splitlines() + if l.strip()] + if len(actual_lines) == len(expected_lines) == 1: + return '%r not %r' % (actual_lines[0], expected_lines[0]) + if not actual_lines: + return 'Empty; should have:\n'+expected_content + import difflib + return '\n'.join(difflib.ndiff(actual_lines, expected_lines)) diff --git a/paste/debug/prints.py b/paste/debug/prints.py new file mode 100644 index 0000000..6cc3f7d --- /dev/null +++ b/paste/debug/prints.py @@ -0,0 +1,149 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Middleware that displays everything that is printed inline in +application pages. + +Anything printed during the request will get captured and included on +the page. It will usually be included as a floating element in the +top right hand corner of the page. If you want to override this +you can include a tag in your template where it will be placed:: + +

+
+You might want to include ``style="white-space: normal"``, as all the
+whitespace will be quoted, and this allows the text to wrap if
+necessary.
+
+"""
+
+from cStringIO import StringIO
+import re
+import cgi
+from paste.util import threadedprint
+from paste import wsgilib
+from paste import response
+import six
+import sys
+
+_threadedprint_installed = False
+
+__all__ = ['PrintDebugMiddleware']
+
+class TeeFile(object):
+
+    def __init__(self, files):
+        self.files = files
+
+    def write(self, v):
+        if isinstance(v, unicode):
+            # WSGI is picky in this case
+            v = str(v)
+        for file in self.files:
+            file.write(v)
+
+class PrintDebugMiddleware(object):
+
+    """
+    This middleware captures all the printed statements, and inlines
+    them in HTML pages, so that you can see all the (debug-intended)
+    print statements in the page itself.
+
+    There are two keys added to the environment to control this:
+    ``environ['paste.printdebug_listeners']`` is a list of functions
+    that will be called everytime something is printed.
+
+    ``environ['paste.remove_printdebug']`` is a function that, if
+    called, will disable printing of output for that request.
+
+    If you have ``replace_stdout=True`` then stdout is replaced, not
+    captured.
+    """
+
+    log_template = (
+        '
'
+        'Log messages
' + '%s
') + + def __init__(self, app, global_conf=None, force_content_type=False, + print_wsgi_errors=True, replace_stdout=False): + # @@: global_conf should be handled separately and only for + # the entry point + self.app = app + self.force_content_type = force_content_type + if isinstance(print_wsgi_errors, six.string_types): + from paste.deploy.converters import asbool + print_wsgi_errors = asbool(print_wsgi_errors) + self.print_wsgi_errors = print_wsgi_errors + self.replace_stdout = replace_stdout + self._threaded_print_stdout = None + + def __call__(self, environ, start_response): + global _threadedprint_installed + if environ.get('paste.testing'): + # In a testing environment this interception isn't + # useful: + return self.app(environ, start_response) + if (not _threadedprint_installed + or self._threaded_print_stdout is not sys.stdout): + # @@: Not strictly threadsafe + _threadedprint_installed = True + threadedprint.install(leave_stdout=not self.replace_stdout) + self._threaded_print_stdout = sys.stdout + removed = [] + def remove_printdebug(): + removed.append(None) + environ['paste.remove_printdebug'] = remove_printdebug + logged = StringIO() + listeners = [logged] + environ['paste.printdebug_listeners'] = listeners + if self.print_wsgi_errors: + listeners.append(environ['wsgi.errors']) + replacement_stdout = TeeFile(listeners) + threadedprint.register(replacement_stdout) + try: + status, headers, body = wsgilib.intercept_output( + environ, self.app) + if status is None: + # Some error occurred + status = '500 Server Error' + headers = [('Content-type', 'text/html')] + start_response(status, headers) + if not body: + body = 'An error occurred' + content_type = response.header_value(headers, 'content-type') + if (removed or + (not self.force_content_type and + (not content_type + or not content_type.startswith('text/html')))): + if replacement_stdout == logged: + # Then the prints will be lost, unless... + environ['wsgi.errors'].write(logged.getvalue()) + start_response(status, headers) + return [body] + response.remove_header(headers, 'content-length') + body = self.add_log(body, logged.getvalue()) + start_response(status, headers) + return [body] + finally: + threadedprint.deregister() + + _body_re = re.compile(r']*>', re.I) + _explicit_re = re.compile(r']*id="paste-debug-prints".*?>', + re.I+re.S) + + def add_log(self, html, log): + if not log: + return html + text = cgi.escape(log) + text = text.replace('\n', '
') + text = text.replace(' ', '  ') + match = self._explicit_re.search(html) + if not match: + text = self.log_template % text + match = self._body_re.search(html) + if not match: + return text + html + else: + return html[:match.end()] + text + html[match.end():] diff --git a/paste/debug/profile.py b/paste/debug/profile.py new file mode 100644 index 0000000..036c805 --- /dev/null +++ b/paste/debug/profile.py @@ -0,0 +1,228 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Middleware that profiles the request and displays profiling +information at the bottom of each page. +""" + + +import sys +import os +import hotshot +import hotshot.stats +import threading +import cgi +import six +import time +from cStringIO import StringIO +from paste import response + +__all__ = ['ProfileMiddleware', 'profile_decorator'] + +class ProfileMiddleware(object): + + """ + Middleware that profiles all requests. + + All HTML pages will have profiling information appended to them. + The data is isolated to that single request, and does not include + data from previous requests. + + This uses the ``hotshot`` module, which affects performance of the + application. It also runs in a single-threaded mode, so it is + only usable in development environments. + """ + + style = ('clear: both; background-color: #ff9; color: #000; ' + 'border: 2px solid #000; padding: 5px;') + + def __init__(self, app, global_conf=None, + log_filename='profile.log.tmp', + limit=40): + self.app = app + self.lock = threading.Lock() + self.log_filename = log_filename + self.limit = limit + + def __call__(self, environ, start_response): + catch_response = [] + body = [] + def replace_start_response(status, headers, exc_info=None): + catch_response.extend([status, headers]) + start_response(status, headers, exc_info) + return body.append + def run_app(): + app_iter = self.app(environ, replace_start_response) + try: + body.extend(app_iter) + finally: + if hasattr(app_iter, 'close'): + app_iter.close() + self.lock.acquire() + try: + prof = hotshot.Profile(self.log_filename) + prof.addinfo('URL', environ.get('PATH_INFO', '')) + try: + prof.runcall(run_app) + finally: + prof.close() + body = ''.join(body) + headers = catch_response[1] + content_type = response.header_value(headers, 'content-type') + if content_type is None or not content_type.startswith('text/html'): + # We can't add info to non-HTML output + return [body] + stats = hotshot.stats.load(self.log_filename) + stats.strip_dirs() + stats.sort_stats('time', 'calls') + output = capture_output(stats.print_stats, self.limit) + output_callers = capture_output( + stats.print_callers, self.limit) + body += '
%s\n%s
' % ( + self.style, cgi.escape(output), cgi.escape(output_callers)) + return [body] + finally: + self.lock.release() + +def capture_output(func, *args, **kw): + # Not threadsafe! (that's okay when ProfileMiddleware uses it, + # though, since it synchronizes itself.) + out = StringIO() + old_stdout = sys.stdout + sys.stdout = out + try: + func(*args, **kw) + finally: + sys.stdout = old_stdout + return out.getvalue() + +def profile_decorator(**options): + + """ + Profile a single function call. + + Used around a function, like:: + + @profile_decorator(options...) + def ... + + All calls to the function will be profiled. The options are + all keywords, and are: + + log_file: + The filename to log to (or ``'stdout'`` or ``'stderr'``). + Default: stderr. + display_limit: + Only show the top N items, default: 20. + sort_stats: + A list of string-attributes to sort on. Default + ``('time', 'calls')``. + strip_dirs: + Strip directories/module names from files? Default True. + add_info: + If given, this info will be added to the report (for your + own tracking). Default: none. + log_filename: + The temporary filename to log profiling data to. Default; + ``./profile_data.log.tmp`` + no_profile: + If true, then don't actually profile anything. Useful for + conditional profiling. + """ + + if options.get('no_profile'): + def decorator(func): + return func + return decorator + def decorator(func): + def replacement(*args, **kw): + return DecoratedProfile(func, **options)(*args, **kw) + return replacement + return decorator + +class DecoratedProfile(object): + + lock = threading.Lock() + + def __init__(self, func, **options): + self.func = func + self.options = options + + def __call__(self, *args, **kw): + self.lock.acquire() + try: + return self.profile(self.func, *args, **kw) + finally: + self.lock.release() + + def profile(self, func, *args, **kw): + ops = self.options + prof_filename = ops.get('log_filename', 'profile_data.log.tmp') + prof = hotshot.Profile(prof_filename) + prof.addinfo('Function Call', + self.format_function(func, *args, **kw)) + if ops.get('add_info'): + prof.addinfo('Extra info', ops['add_info']) + exc_info = None + try: + start_time = time.time() + try: + result = prof.runcall(func, *args, **kw) + except: + exc_info = sys.exc_info() + end_time = time.time() + finally: + prof.close() + stats = hotshot.stats.load(prof_filename) + os.unlink(prof_filename) + if ops.get('strip_dirs', True): + stats.strip_dirs() + stats.sort_stats(*ops.get('sort_stats', ('time', 'calls'))) + display_limit = ops.get('display_limit', 20) + output = capture_output(stats.print_stats, display_limit) + output_callers = capture_output( + stats.print_callers, display_limit) + output_file = ops.get('log_file') + if output_file in (None, 'stderr'): + f = sys.stderr + elif output_file in ('-', 'stdout'): + f = sys.stdout + else: + f = open(output_file, 'a') + f.write('\n%s\n' % ('-'*60)) + f.write('Date: %s\n' % time.strftime('%c')) + f.write('Function call: %s\n' + % self.format_function(func, *args, **kw)) + f.write('Wall time: %0.2f seconds\n' + % (end_time - start_time)) + f.write(output) + f.write(output_callers) + if output_file not in (None, '-', 'stdout', 'stderr'): + f.close() + if exc_info: + # We captured an exception earlier, now we re-raise it + six.reraise(exc_info[0], exc_info[1], exc_info[2]) + return result + + def format_function(self, func, *args, **kw): + args = map(repr, args) + args.extend( + ['%s=%r' % (k, v) for k, v in kw.items()]) + return '%s(%s)' % (func.__name__, ', '.join(args)) + + +def make_profile_middleware( + app, global_conf, + log_filename='profile.log.tmp', + limit=40): + """ + Wrap the application in a component that will profile each + request. The profiling data is then appended to the output + of each page. + + Note that this serializes all requests (i.e., removing + concurrency). Therefore never use this in production. + """ + limit = int(limit) + return ProfileMiddleware( + app, log_filename=log_filename, limit=limit) diff --git a/paste/debug/testserver.py b/paste/debug/testserver.py new file mode 100755 index 0000000..4817161 --- /dev/null +++ b/paste/debug/testserver.py @@ -0,0 +1,93 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +WSGI Test Server + +This builds upon paste.util.baseserver to customize it for regressions +where using raw_interactive won't do. + + +""" +import time +from paste.httpserver import * + +class WSGIRegressionServer(WSGIServer): + """ + A threaded WSGIServer for use in regression testing. To use this + module, call serve(application, regression=True), and then call + server.accept() to let it handle one request. When finished, use + server.stop() to shutdown the server. Note that all pending requests + are processed before the server shuts down. + """ + defaulttimeout = 10 + def __init__ (self, *args, **kwargs): + WSGIServer.__init__(self, *args, **kwargs) + self.stopping = [] + self.pending = [] + self.timeout = self.defaulttimeout + # this is a local connection, be quick + self.socket.settimeout(2) + def serve_forever(self): + from threading import Thread + thread = Thread(target=self.serve_pending) + thread.start() + def reset_expires(self): + if self.timeout: + self.expires = time.time() + self.timeout + def close_request(self, *args, **kwargs): + WSGIServer.close_request(self, *args, **kwargs) + self.pending.pop() + self.reset_expires() + def serve_pending(self): + self.reset_expires() + while not self.stopping or self.pending: + now = time.time() + if now > self.expires and self.timeout: + # note regression test doesn't handle exceptions in + # threads very well; so we just print and exit + print("\nWARNING: WSGIRegressionServer timeout exceeded\n") + break + if self.pending: + self.handle_request() + time.sleep(.1) + def stop(self): + """ stop the server (called from tester's thread) """ + self.stopping.append(True) + def accept(self, count = 1): + """ accept another request (called from tester's thread) """ + assert not self.stopping + [self.pending.append(True) for x in range(count)] + +def serve(application, host=None, port=None, handler=None): + server = WSGIRegressionServer(application, host, port, handler) + print("serving on %s:%s" % server.server_address) + server.serve_forever() + return server + +if __name__ == '__main__': + from six.moves.urllib.request import urlopen + from paste.wsgilib import dump_environ + server = serve(dump_environ) + baseuri = ("http://%s:%s" % server.server_address) + + def fetch(path): + # tell the server to humor exactly one more request + server.accept(1) + # not needed; but this is what you do if the server + # may not respond in a resonable time period + import socket + socket.setdefaulttimeout(5) + # build a uri, fetch and return + return urlopen(baseuri + path).read() + + assert "PATH_INFO: /foo" in fetch("/foo") + assert "PATH_INFO: /womble" in fetch("/womble") + + # ok, let's make one more final request... + server.accept(1) + # and then schedule a stop() + server.stop() + # and then... fetch it... + urlopen(baseuri) diff --git a/paste/debug/watchthreads.py b/paste/debug/watchthreads.py new file mode 100644 index 0000000..c877942 --- /dev/null +++ b/paste/debug/watchthreads.py @@ -0,0 +1,347 @@ +""" +Watches the key ``paste.httpserver.thread_pool`` to see how many +threads there are and report on any wedged threads. +""" +import sys +import cgi +import time +import traceback +from cStringIO import StringIO +from thread import get_ident +from paste import httpexceptions +from paste.request import construct_url, parse_formvars +from paste.util.template import HTMLTemplate, bunch + +page_template = HTMLTemplate(''' + + + + {{title}} + + +

{{title}}

+ {{if kill_thread_id}} +
+ Thread {{kill_thread_id}} killed +
+ {{endif}} +
Pool size: {{nworkers}} + {{if actual_workers > nworkers}} + + {{actual_workers-nworkers}} extra + {{endif}} + ({{nworkers_used}} used including current request)
+ idle: {{len(track_threads["idle"])}}, + busy: {{len(track_threads["busy"])}}, + hung: {{len(track_threads["hung"])}}, + dying: {{len(track_threads["dying"])}}, + zombie: {{len(track_threads["zombie"])}}
+ +{{for thread in threads}} + + + + + + + + + + + + + + + + +
+ Thread + {{if thread.thread_id == this_thread_id}} + (this request) + {{endif}} + {{thread.thread_id}} + {{if allow_kill}} +
+ + +
+ {{endif}} +
+
Time processing request{{thread.time_html|html}}
URI{{if thread.uri == 'unknown'}} + unknown + {{else}}{{thread.uri_short}} + {{endif}} +
+ ▸ Show environ + + + + {{if thread.traceback}} + ▸ Show traceback + + + {{endif}} + +
+ +{{endfor}} + + + +''', name='watchthreads.page_template') + +class WatchThreads(object): + + """ + Application that watches the threads in ``paste.httpserver``, + showing the length each thread has been working on a request. + + If allow_kill is true, then you can kill errant threads through + this application. + + This application can expose private information (specifically in + the environment, like cookies), so it should be protected. + """ + + def __init__(self, allow_kill=False): + self.allow_kill = allow_kill + + def __call__(self, environ, start_response): + if 'paste.httpserver.thread_pool' not in environ: + start_response('403 Forbidden', [('Content-type', 'text/plain')]) + return ['You must use the threaded Paste HTTP server to use this application'] + if environ.get('PATH_INFO') == '/kill': + return self.kill(environ, start_response) + else: + return self.show(environ, start_response) + + def show(self, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + form = parse_formvars(environ) + if form.get('kill'): + kill_thread_id = form['kill'] + else: + kill_thread_id = None + thread_pool = environ['paste.httpserver.thread_pool'] + nworkers = thread_pool.nworkers + now = time.time() + + + workers = thread_pool.worker_tracker.items() + workers.sort(key=lambda v: v[1][0]) + threads = [] + for thread_id, (time_started, worker_environ) in workers: + thread = bunch() + threads.append(thread) + if worker_environ: + thread.uri = construct_url(worker_environ) + else: + thread.uri = 'unknown' + thread.thread_id = thread_id + thread.time_html = format_time(now-time_started) + thread.uri_short = shorten(thread.uri) + thread.environ = worker_environ + thread.traceback = traceback_thread(thread_id) + + page = page_template.substitute( + title="Thread Pool Worker Tracker", + nworkers=nworkers, + actual_workers=len(thread_pool.workers), + nworkers_used=len(workers), + script_name=environ['SCRIPT_NAME'], + kill_thread_id=kill_thread_id, + allow_kill=self.allow_kill, + threads=threads, + this_thread_id=get_ident(), + track_threads=thread_pool.track_threads()) + + return [page] + + def kill(self, environ, start_response): + if not self.allow_kill: + exc = httpexceptions.HTTPForbidden( + 'Killing threads has not been enabled. Shame on you ' + 'for trying!') + return exc(environ, start_response) + vars = parse_formvars(environ) + thread_id = int(vars['thread_id']) + thread_pool = environ['paste.httpserver.thread_pool'] + if thread_id not in thread_pool.worker_tracker: + exc = httpexceptions.PreconditionFailed( + 'You tried to kill thread %s, but it is not working on ' + 'any requests' % thread_id) + return exc(environ, start_response) + thread_pool.kill_worker(thread_id) + script_name = environ['SCRIPT_NAME'] or '/' + exc = httpexceptions.HTTPFound( + headers=[('Location', script_name+'?kill=%s' % thread_id)]) + return exc(environ, start_response) + +def traceback_thread(thread_id): + """ + Returns a plain-text traceback of the given thread, or None if it + can't get a traceback. + """ + if not hasattr(sys, '_current_frames'): + # Only 2.5 has support for this, with this special function + return None + frames = sys._current_frames() + if not thread_id in frames: + return None + frame = frames[thread_id] + out = StringIO() + traceback.print_stack(frame, file=out) + return out.getvalue() + +hide_keys = ['paste.httpserver.thread_pool'] + +def format_environ(environ): + if environ is None: + return environ_template.substitute( + key='---', + value='No environment registered for this thread yet') + environ_rows = [] + for key, value in sorted(environ.items()): + if key in hide_keys: + continue + try: + if key.upper() != key: + value = repr(value) + environ_rows.append( + environ_template.substitute( + key=cgi.escape(str(key)), + value=cgi.escape(str(value)))) + except Exception as e: + environ_rows.append( + environ_template.substitute( + key=cgi.escape(str(key)), + value='Error in repr(): %s' % e)) + return ''.join(environ_rows) + +def format_time(time_length): + if time_length >= 60*60: + # More than an hour + time_string = '%i:%02i:%02i' % (int(time_length/60/60), + int(time_length/60) % 60, + time_length % 60) + elif time_length >= 120: + time_string = '%i:%02i' % (int(time_length/60), + time_length % 60) + elif time_length > 60: + time_string = '%i sec' % time_length + elif time_length > 1: + time_string = '%0.1f sec' % time_length + else: + time_string = '%0.2f sec' % time_length + if time_length < 5: + return time_string + elif time_length < 120: + return '%s' % time_string + else: + return '%s' % time_string + +def shorten(s): + if len(s) > 60: + return s[:40]+'...'+s[-10:] + else: + return s + +def make_watch_threads(global_conf, allow_kill=False): + from paste.deploy.converters import asbool + return WatchThreads(allow_kill=asbool(allow_kill)) +make_watch_threads.__doc__ = WatchThreads.__doc__ + +def make_bad_app(global_conf, pause=0): + pause = int(pause) + def bad_app(environ, start_response): + import thread + if pause: + time.sleep(pause) + else: + count = 0 + while 1: + print("I'm alive %s (%s)" % (count, thread.get_ident())) + time.sleep(10) + count += 1 + start_response('200 OK', [('content-type', 'text/plain')]) + return ['OK, paused %s seconds' % pause] + return bad_app diff --git a/paste/debug/wdg_validate.py b/paste/debug/wdg_validate.py new file mode 100644 index 0000000..d3678fb --- /dev/null +++ b/paste/debug/wdg_validate.py @@ -0,0 +1,121 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Middleware that tests the validity of all generated HTML using the +`WDG HTML Validator `_ +""" + +from cStringIO import StringIO +try: + import subprocess +except ImportError: + from paste.util import subprocess24 as subprocess +from paste.response import header_value +import re +import cgi + +__all__ = ['WDGValidateMiddleware'] + +class WDGValidateMiddleware(object): + + """ + Middleware that checks HTML and appends messages about the validity of + the HTML. Uses: http://www.htmlhelp.com/tools/validator/ -- interacts + with the command line client. Use the configuration ``wdg_path`` to + override the path (default: looks for ``validate`` in $PATH). + + To install, in your web context's __init__.py:: + + def urlparser_wrap(environ, start_response, app): + return wdg_validate.WDGValidateMiddleware(app)( + environ, start_response) + + Or in your configuration:: + + middleware.append('paste.wdg_validate.WDGValidateMiddleware') + """ + + _end_body_regex = re.compile(r'', re.I) + + def __init__(self, app, global_conf=None, wdg_path='validate'): + self.app = app + self.wdg_path = wdg_path + + def __call__(self, environ, start_response): + output = StringIO() + response = [] + + def writer_start_response(status, headers, exc_info=None): + response.extend((status, headers)) + start_response(status, headers, exc_info) + return output.write + + app_iter = self.app(environ, writer_start_response) + try: + for s in app_iter: + output.write(s) + finally: + if hasattr(app_iter, 'close'): + app_iter.close() + page = output.getvalue() + status, headers = response + v = header_value(headers, 'content-type') or '' + if (not v.startswith('text/html') + and not v.startswith('text/xhtml') + and not v.startswith('application/xhtml')): + # Can't validate + # @@: Should validate CSS too... but using what? + return [page] + ops = [] + if v.startswith('text/xhtml+xml'): + ops.append('--xml') + # @@: Should capture encoding too + html_errors = self.call_wdg_validate( + self.wdg_path, ops, page) + if html_errors: + page = self.add_error(page, html_errors)[0] + headers.remove( + ('Content-Length', + str(header_value(headers, 'content-length')))) + headers.append(('Content-Length', str(len(page)))) + return [page] + + def call_wdg_validate(self, wdg_path, ops, page): + if subprocess is None: + raise ValueError( + "This middleware requires the subprocess module from " + "Python 2.4") + proc = subprocess.Popen([wdg_path] + ops, + shell=False, + close_fds=True, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT) + stdout = proc.communicate(page)[0] + proc.wait() + return stdout + + def add_error(self, html_page, html_errors): + add_text = ('
%s
' + % cgi.escape(html_errors)) + match = self._end_body_regex.search(html_page) + if match: + return [html_page[:match.start()] + + add_text + + html_page[match.start():]] + else: + return [html_page + add_text] + +def make_wdg_validate_middleware( + app, global_conf, wdg_path='validate'): + """ + Wraps the application in the WDG validator from + http://www.htmlhelp.com/tools/validator/ + + Validation errors are appended to the text of each page. + You can configure this by giving the path to the validate + executable (by default picked up from $PATH) + """ + return WDGValidateMiddleware( + app, global_conf, wdg_path=wdg_path) diff --git a/paste/errordocument.py b/paste/errordocument.py new file mode 100644 index 0000000..62224b3 --- /dev/null +++ b/paste/errordocument.py @@ -0,0 +1,383 @@ +# (c) 2005-2006 James Gardner +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +""" +Middleware to display error documents for certain status codes + +The middleware in this module can be used to intercept responses with +specified status codes and internally forward the request to an appropriate +URL where the content can be displayed to the user as an error document. +""" + +import warnings +import sys +from six.moves.urllib import parse as urlparse +from paste.recursive import ForwardRequestException, RecursiveMiddleware, RecursionLoop +from paste.util import converters +from paste.response import replace_header + +def forward(app, codes): + """ + Intercepts a response with a particular status code and returns the + content from a specified URL instead. + + The arguments are: + + ``app`` + The WSGI application or middleware chain. + + ``codes`` + A dictionary of integer status codes and the URL to be displayed + if the response uses that code. + + For example, you might want to create a static file to display a + "File Not Found" message at the URL ``/error404.html`` and then use + ``forward`` middleware to catch all 404 status codes and display the page + you created. In this example ``app`` is your exisiting WSGI + applicaiton:: + + from paste.errordocument import forward + app = forward(app, codes={404:'/error404.html'}) + + """ + for code in codes: + if not isinstance(code, int): + raise TypeError('All status codes should be type int. ' + '%s is not valid'%repr(code)) + + def error_codes_mapper(code, message, environ, global_conf, codes): + if code in codes: + return codes[code] + else: + return None + + #return _StatusBasedRedirect(app, error_codes_mapper, codes=codes) + return RecursiveMiddleware( + StatusBasedForward( + app, + error_codes_mapper, + codes=codes, + ) + ) + +class StatusKeeper(object): + def __init__(self, app, status, url, headers): + self.app = app + self.status = status + self.url = url + self.headers = headers + + def __call__(self, environ, start_response): + def keep_status_start_response(status, headers, exc_info=None): + for header, value in headers: + if header.lower() == 'set-cookie': + self.headers.append((header, value)) + else: + replace_header(self.headers, header, value) + return start_response(self.status, self.headers, exc_info) + parts = self.url.split('?') + environ['PATH_INFO'] = parts[0] + if len(parts) > 1: + environ['QUERY_STRING'] = parts[1] + else: + environ['QUERY_STRING'] = '' + #raise Exception(self.url, self.status) + try: + return self.app(environ, keep_status_start_response) + except RecursionLoop as e: + environ['wsgi.errors'].write('Recursion error getting error page: %s\n' % e) + keep_status_start_response('500 Server Error', [('Content-type', 'text/plain')], sys.exc_info()) + return ['Error: %s. (Error page could not be fetched)' + % self.status] + + +class StatusBasedForward(object): + """ + Middleware that lets you test a response against a custom mapper object to + programatically determine whether to internally forward to another URL and + if so, which URL to forward to. + + If you don't need the full power of this middleware you might choose to use + the simpler ``forward`` middleware instead. + + The arguments are: + + ``app`` + The WSGI application or middleware chain. + + ``mapper`` + A callable that takes a status code as the + first parameter, a message as the second, and accepts optional environ, + global_conf and named argments afterwards. It should return a + URL to forward to or ``None`` if the code is not to be intercepted. + + ``global_conf`` + Optional default configuration from your config file. If ``debug`` is + set to ``true`` a message will be written to ``wsgi.errors`` on each + internal forward stating the URL forwarded to. + + ``**params`` + Optional, any other configuration and extra arguments you wish to + pass which will in turn be passed back to the custom mapper object. + + Here is an example where a ``404 File Not Found`` status response would be + redirected to the URL ``/error?code=404&message=File%20Not%20Found``. This + could be useful for passing the status code and message into another + application to display an error document: + + .. code-block:: python + + from paste.errordocument import StatusBasedForward + from paste.recursive import RecursiveMiddleware + from urllib import urlencode + + def error_mapper(code, message, environ, global_conf, kw) + if code in [404, 500]: + params = urlencode({'message':message, 'code':code}) + url = '/error?'%(params) + return url + else: + return None + + app = RecursiveMiddleware( + StatusBasedForward(app, mapper=error_mapper), + ) + + """ + + def __init__(self, app, mapper, global_conf=None, **params): + if global_conf is None: + global_conf = {} + # @@: global_conf shouldn't really come in here, only in a + # separate make_status_based_forward function + if global_conf: + self.debug = converters.asbool(global_conf.get('debug', False)) + else: + self.debug = False + self.application = app + self.mapper = mapper + self.global_conf = global_conf + self.params = params + + def __call__(self, environ, start_response): + url = [] + writer = [] + + def change_response(status, headers, exc_info=None): + status_code = status.split(' ') + try: + code = int(status_code[0]) + except (ValueError, TypeError): + raise Exception( + 'StatusBasedForward middleware ' + 'received an invalid status code %s'%repr(status_code[0]) + ) + message = ' '.join(status_code[1:]) + new_url = self.mapper( + code, + message, + environ, + self.global_conf, + **self.params + ) + if not (new_url == None or isinstance(new_url, str)): + raise TypeError( + 'Expected the url to internally ' + 'redirect to in the StatusBasedForward mapper' + 'to be a string or None, not %r' % new_url) + if new_url: + url.append([new_url, status, headers]) + # We have to allow the app to write stuff, even though + # we'll ignore it: + return [].append + else: + return start_response(status, headers, exc_info) + + app_iter = self.application(environ, change_response) + if url: + if hasattr(app_iter, 'close'): + app_iter.close() + + def factory(app): + return StatusKeeper(app, status=url[0][1], url=url[0][0], + headers=url[0][2]) + raise ForwardRequestException(factory=factory) + else: + return app_iter + +def make_errordocument(app, global_conf, **kw): + """ + Paste Deploy entry point to create a error document wrapper. + + Use like:: + + [filter-app:main] + use = egg:Paste#errordocument + next = real-app + 500 = /lib/msg/500.html + 404 = /lib/msg/404.html + """ + map = {} + for status, redir_loc in kw.items(): + try: + status = int(status) + except ValueError: + raise ValueError('Bad status code: %r' % status) + map[status] = redir_loc + forwarder = forward(app, map) + return forwarder + +__pudge_all__ = [ + 'forward', + 'make_errordocument', + 'empty_error', + 'make_empty_error', + 'StatusBasedForward', +] + + +############################################################################### +## Deprecated +############################################################################### + +def custom_forward(app, mapper, global_conf=None, **kw): + """ + Deprectated; use StatusBasedForward instead. + """ + warnings.warn( + "errordocuments.custom_forward has been deprecated; please " + "use errordocuments.StatusBasedForward", + DeprecationWarning, 2) + if global_conf is None: + global_conf = {} + return _StatusBasedRedirect(app, mapper, global_conf, **kw) + +class _StatusBasedRedirect(object): + """ + Deprectated; use StatusBasedForward instead. + """ + def __init__(self, app, mapper, global_conf=None, **kw): + + warnings.warn( + "errordocuments._StatusBasedRedirect has been deprecated; please " + "use errordocuments.StatusBasedForward", + DeprecationWarning, 2) + + if global_conf is None: + global_conf = {} + self.application = app + self.mapper = mapper + self.global_conf = global_conf + self.kw = kw + self.fallback_template = """ + + + Error %(code)s + + +

Error %(code)s

+

%(message)s

+
+

+ Additionally an error occurred trying to produce an + error document. A description of the error was logged + to wsgi.errors. +

+ + + """ + + def __call__(self, environ, start_response): + url = [] + code_message = [] + try: + def change_response(status, headers, exc_info=None): + new_url = None + parts = status.split(' ') + try: + code = int(parts[0]) + except (ValueError, TypeError): + raise Exception( + '_StatusBasedRedirect middleware ' + 'received an invalid status code %s'%repr(parts[0]) + ) + message = ' '.join(parts[1:]) + new_url = self.mapper( + code, + message, + environ, + self.global_conf, + self.kw + ) + if not (new_url == None or isinstance(new_url, str)): + raise TypeError( + 'Expected the url to internally ' + 'redirect to in the _StatusBasedRedirect error_mapper' + 'to be a string or None, not %s'%repr(new_url) + ) + if new_url: + url.append(new_url) + code_message.append([code, message]) + return start_response(status, headers, exc_info) + app_iter = self.application(environ, change_response) + except: + try: + import sys + error = str(sys.exc_info()[1]) + except: + error = '' + try: + code, message = code_message[0] + except: + code, message = ['', ''] + environ['wsgi.errors'].write( + 'Error occurred in _StatusBasedRedirect ' + 'intercepting the response: '+str(error) + ) + return [self.fallback_template + % {'message': message, 'code': code}] + else: + if url: + url_ = url[0] + new_environ = {} + for k, v in environ.items(): + if k != 'QUERY_STRING': + new_environ['QUERY_STRING'] = urlparse.urlparse(url_)[4] + else: + new_environ[k] = v + class InvalidForward(Exception): + pass + def eat_start_response(status, headers, exc_info=None): + """ + We don't want start_response to do anything since it + has already been called + """ + if status[:3] != '200': + raise InvalidForward( + "The URL %s to internally forward " + "to in order to create an error document did not " + "return a '200' status code." % url_ + ) + forward = environ['paste.recursive.forward'] + old_start_response = forward.start_response + forward.start_response = eat_start_response + try: + app_iter = forward(url_, new_environ) + except InvalidForward as e: + code, message = code_message[0] + environ['wsgi.errors'].write( + 'Error occurred in ' + '_StatusBasedRedirect redirecting ' + 'to new URL: '+str(url[0]) + ) + return [ + self.fallback_template%{ + 'message':message, + 'code':code, + } + ] + else: + forward.start_response = old_start_response + return app_iter + else: + return app_iter diff --git a/paste/evalexception/__init__.py b/paste/evalexception/__init__.py new file mode 100644 index 0000000..a19cf85 --- /dev/null +++ b/paste/evalexception/__init__.py @@ -0,0 +1,7 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +An exception handler for interactive debugging +""" +from paste.evalexception.middleware import EvalException + diff --git a/paste/evalexception/evalcontext.py b/paste/evalexception/evalcontext.py new file mode 100644 index 0000000..42f2efa --- /dev/null +++ b/paste/evalexception/evalcontext.py @@ -0,0 +1,69 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +from six.moves import cStringIO as StringIO +import traceback +import threading +import pdb +import six +import sys + +exec_lock = threading.Lock() + +class EvalContext(object): + + """ + Class that represents a interactive interface. It has its own + namespace. Use eval_context.exec_expr(expr) to run commands; the + output of those commands is returned, as are print statements. + + This is essentially what doctest does, and is taken directly from + doctest. + """ + + def __init__(self, namespace, globs): + self.namespace = namespace + self.globs = globs + + def exec_expr(self, s): + out = StringIO() + exec_lock.acquire() + save_stdout = sys.stdout + try: + debugger = _OutputRedirectingPdb(save_stdout) + debugger.reset() + pdb.set_trace = debugger.set_trace + sys.stdout = out + try: + code = compile(s, '', "single", 0, 1) + six.exec_(code, self.globs, self.namespace) + debugger.set_continue() + except KeyboardInterrupt: + raise + except: + traceback.print_exc(file=out) + debugger.set_continue() + finally: + sys.stdout = save_stdout + exec_lock.release() + return out.getvalue() + +# From doctest +class _OutputRedirectingPdb(pdb.Pdb): + """ + A specialized version of the python debugger that redirects stdout + to a given stream when interacting with the user. Stdout is *not* + redirected when traced code is executed. + """ + def __init__(self, out): + self.__out = out + pdb.Pdb.__init__(self) + + def trace_dispatch(self, *args): + # Redirect stdout to the given stream. + save_stdout = sys.stdout + sys.stdout = self.__out + # Call Pdb's trace dispatch method. + try: + return pdb.Pdb.trace_dispatch(self, *args) + finally: + sys.stdout = save_stdout diff --git a/paste/evalexception/media/MochiKit.packed.js b/paste/evalexception/media/MochiKit.packed.js new file mode 100644 index 0000000..15027d9 --- /dev/null +++ b/paste/evalexception/media/MochiKit.packed.js @@ -0,0 +1,7829 @@ +/*** + + MochiKit.MochiKit 1.4.2 : PACKED VERSION + + THIS FILE IS AUTOMATICALLY GENERATED. If creating patches, please + diff against the source tree, not this file. + + See for documentation, downloads, license, etc. + + (c) 2005 Bob Ippolito. All rights Reserved. + +***/ + +if(typeof (dojo)!="undefined"){ +dojo.provide("MochiKit.Base"); +} +if(typeof (MochiKit)=="undefined"){ +MochiKit={}; +} +if(typeof (MochiKit.Base)=="undefined"){ +MochiKit.Base={}; +} +if(typeof (MochiKit.__export__)=="undefined"){ +MochiKit.__export__=(MochiKit.__compat__||(typeof (JSAN)=="undefined"&&typeof (dojo)=="undefined")); +} +MochiKit.Base.VERSION="1.4.2"; +MochiKit.Base.NAME="MochiKit.Base"; +MochiKit.Base.update=function(_1,_2){ +if(_1===null||_1===undefined){ +_1={}; +} +for(var i=1;i=0;i--){ +_18.unshift(o[i]); +} +}else{ +res.push(o); +} +} +return res; +},extend:function(_1b,obj,_1d){ +if(!_1d){ +_1d=0; +} +if(obj){ +var l=obj.length; +if(typeof (l)!="number"){ +if(typeof (MochiKit.Iter)!="undefined"){ +obj=MochiKit.Iter.list(obj); +l=obj.length; +}else{ +throw new TypeError("Argument not an array-like and MochiKit.Iter not present"); +} +} +if(!_1b){ +_1b=[]; +} +for(var i=_1d;i>b; +},zrshift:function(a,b){ +return a>>>b; +},eq:function(a,b){ +return a==b; +},ne:function(a,b){ +return a!=b; +},gt:function(a,b){ +return a>b; +},ge:function(a,b){ +return a>=b; +},lt:function(a,b){ +return al){ +_93=l; +} +} +_91=[]; +for(i=0;i<_93;i++){ +var _95=[]; +for(var j=1;j=0;i--){ +_b2=[_ae[i].apply(this,_b2)]; +} +return _b2[0]; +}; +},bind:function(_b4,_b5){ +if(typeof (_b4)=="string"){ +_b4=_b5[_b4]; +} +var _b6=_b4.im_func; +var _b7=_b4.im_preargs; +var _b8=_b4.im_self; +var m=MochiKit.Base; +if(typeof (_b4)=="function"&&typeof (_b4.apply)=="undefined"){ +_b4=m._wrapDumbFunction(_b4); +} +if(typeof (_b6)!="function"){ +_b6=_b4; +} +if(typeof (_b5)!="undefined"){ +_b8=_b5; +} +if(typeof (_b7)=="undefined"){ +_b7=[]; +}else{ +_b7=_b7.slice(); +} +m.extend(_b7,arguments,2); +var _ba=function(){ +var _bb=arguments; +var me=arguments.callee; +if(me.im_preargs.length>0){ +_bb=m.concat(me.im_preargs,_bb); +} +var _bd=me.im_self; +if(!_bd){ +_bd=this; +} +return me.im_func.apply(_bd,_bb); +}; +_ba.im_self=_b8; +_ba.im_func=_b6; +_ba.im_preargs=_b7; +return _ba; +},bindLate:function(_be,_bf){ +var m=MochiKit.Base; +if(typeof (_be)!="string"){ +return m.bind.apply(this,arguments); +} +var _c1=m.extend([],arguments,2); +var _c2=function(){ +var _c3=arguments; +var me=arguments.callee; +if(me.im_preargs.length>0){ +_c3=m.concat(me.im_preargs,_c3); +} +var _c5=me.im_self; +if(!_c5){ +_c5=this; +} +return _c5[me.im_func].apply(_c5,_c3); +}; +_c2.im_self=_bf; +_c2.im_func=_be; +_c2.im_preargs=_c1; +return _c2; +},bindMethods:function(_c6){ +var _c7=MochiKit.Base.bind; +for(var k in _c6){ +var _c9=_c6[k]; +if(typeof (_c9)=="function"){ +_c6[k]=_c7(_c9,_c6); +} +} +},registerComparator:function(_ca,_cb,_cc,_cd){ +MochiKit.Base.comparatorRegistry.register(_ca,_cb,_cc,_cd); +},_primitives:{"boolean":true,"string":true,"number":true},compare:function(a,b){ +if(a==b){ +return 0; +} +var _d0=(typeof (a)=="undefined"||a===null); +var _d1=(typeof (b)=="undefined"||b===null); +if(_d0&&_d1){ +return 0; +}else{ +if(_d0){ +return -1; +}else{ +if(_d1){ +return 1; +} +} +} +var m=MochiKit.Base; +var _d3=m._primitives; +if(!(typeof (a) in _d3&&typeof (b) in _d3)){ +try{ +return m.comparatorRegistry.match(a,b); +} +catch(e){ +if(e!=m.NotFound){ +throw e; +} +} +} +if(ab){ +return 1; +} +} +var _d4=m.repr; +throw new TypeError(_d4(a)+" and "+_d4(b)+" can not be compared"); +},compareDateLike:function(a,b){ +return MochiKit.Base.compare(a.getTime(),b.getTime()); +},compareArrayLike:function(a,b){ +var _d9=MochiKit.Base.compare; +var _da=a.length; +var _db=0; +if(_da>b.length){ +_db=1; +_da=b.length; +}else{ +if(_da=0;i--){ +sum+=o[i]; +} +}else{ +sum+=o; +} +} +if(_121<=0){ +throw new TypeError("mean() requires at least one argument"); +} +return sum/_121; +},median:function(){ +var data=MochiKit.Base.flattenArguments(arguments); +if(data.length===0){ +throw new TypeError("median() requires at least one argument"); +} +data.sort(compare); +if(data.length%2==0){ +var _125=data.length/2; +return (data[_125]+data[_125-1])/2; +}else{ +return data[(data.length-1)/2]; +} +},findValue:function(lst,_127,_128,end){ +if(typeof (end)=="undefined"||end===null){ +end=lst.length; +} +if(typeof (_128)=="undefined"||_128===null){ +_128=0; +} +var cmp=MochiKit.Base.compare; +for(var i=_128;i0))){ +var kv=MochiKit.DOM.formContents(_135); +_135=kv[0]; +_136=kv[1]; +}else{ +if(arguments.length==1){ +if(typeof (_135.length)=="number"&&_135.length==2){ +return arguments.callee(_135[0],_135[1]); +} +var o=_135; +_135=[]; +_136=[]; +for(var k in o){ +var v=o[k]; +if(typeof (v)=="function"){ +continue; +}else{ +if(MochiKit.Base.isArrayLike(v)){ +for(var i=0;i=stop){ +throw self.StopIteration; +} +_183+=step; +return rval; +}}; +},imap:function(fun,p,q){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +var _18d=m.map(self.iter,m.extend(null,arguments,1)); +var map=m.map; +var next=self.next; +return {repr:function(){ +return "imap(...)"; +},toString:m.forwardCall("repr"),next:function(){ +return fun.apply(this,map(next,_18d)); +}}; +},applymap:function(fun,seq,self){ +seq=MochiKit.Iter.iter(seq); +var m=MochiKit.Base; +return {repr:function(){ +return "applymap(...)"; +},toString:m.forwardCall("repr"),next:function(){ +return fun.apply(self,seq.next()); +}}; +},chain:function(p,q){ +var self=MochiKit.Iter; +var m=MochiKit.Base; +if(arguments.length==1){ +return self.iter(arguments[0]); +} +var _198=m.map(self.iter,arguments); +return {repr:function(){ +return "chain(...)"; +},toString:m.forwardCall("repr"),next:function(){ +while(_198.length>1){ +try{ +var _199=_198[0].next(); +return _199; +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +_198.shift(); +var _199=_198[0].next(); +return _199; +} +} +if(_198.length==1){ +var arg=_198.shift(); +this.next=m.bind("next",arg); +return this.next(); +} +throw self.StopIteration; +}}; +},takewhile:function(pred,seq){ +var self=MochiKit.Iter; +seq=self.iter(seq); +return {repr:function(){ +return "takewhile(...)"; +},toString:MochiKit.Base.forwardCall("repr"),next:function(){ +var rval=seq.next(); +if(!pred(rval)){ +this.next=function(){ +throw self.StopIteration; +}; +this.next(); +} +return rval; +}}; +},dropwhile:function(pred,seq){ +seq=MochiKit.Iter.iter(seq); +var m=MochiKit.Base; +var bind=m.bind; +return {"repr":function(){ +return "dropwhile(...)"; +},"toString":m.forwardCall("repr"),"next":function(){ +while(true){ +var rval=seq.next(); +if(!pred(rval)){ +break; +} +} +this.next=bind("next",seq); +return rval; +}}; +},_tee:function(_1a4,sync,_1a6){ +sync.pos[_1a4]=-1; +var m=MochiKit.Base; +var _1a8=m.listMin; +return {repr:function(){ +return "tee("+_1a4+", ...)"; +},toString:m.forwardCall("repr"),next:function(){ +var rval; +var i=sync.pos[_1a4]; +if(i==sync.max){ +rval=_1a6.next(); +sync.deque.push(rval); +sync.max+=1; +sync.pos[_1a4]+=1; +}else{ +rval=sync.deque[i-sync.min]; +sync.pos[_1a4]+=1; +if(i==sync.min&&_1a8(sync.pos)!=sync.min){ +sync.min+=1; +sync.deque.shift(); +} +} +return rval; +}}; +},tee:function(_1ab,n){ +var rval=[]; +var sync={"pos":[],"deque":[],"max":-1,"min":-1}; +if(arguments.length==1||typeof (n)=="undefined"||n===null){ +n=2; +} +var self=MochiKit.Iter; +_1ab=self.iter(_1ab); +var _tee=self._tee; +for(var i=0;i0&&_1bd>=stop)||(step<0&&_1bd<=stop)){ +throw MochiKit.Iter.StopIteration; +} +var rval=_1bd; +_1bd+=step; +return rval; +},repr:function(){ +return "range("+[_1bd,stop,step].join(", ")+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +},sum:function(_1c1,_1c2){ +if(typeof (_1c2)=="undefined"||_1c2===null){ +_1c2=0; +} +var x=_1c2; +var self=MochiKit.Iter; +_1c1=self.iter(_1c1); +try{ +while(true){ +x+=_1c1.next(); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +return x; +},exhaust:function(_1c5){ +var self=MochiKit.Iter; +_1c5=self.iter(_1c5); +try{ +while(true){ +_1c5.next(); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +},forEach:function(_1c7,func,obj){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +if(arguments.length>2){ +func=m.bind(func,obj); +} +if(m.isArrayLike(_1c7)&&!self.isIterable(_1c7)){ +try{ +for(var i=0;i<_1c7.length;i++){ +func(_1c7[i]); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +}else{ +self.exhaust(self.imap(func,_1c7)); +} +},every:function(_1cd,func){ +var self=MochiKit.Iter; +try{ +self.ifilterfalse(func,_1cd).next(); +return false; +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +return true; +} +},sorted:function(_1d0,cmp){ +var rval=MochiKit.Iter.list(_1d0); +if(arguments.length==1){ +cmp=MochiKit.Base.compare; +} +rval.sort(cmp); +return rval; +},reversed:function(_1d3){ +var rval=MochiKit.Iter.list(_1d3); +rval.reverse(); +return rval; +},some:function(_1d5,func){ +var self=MochiKit.Iter; +try{ +self.ifilter(func,_1d5).next(); +return true; +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +return false; +} +},iextend:function(lst,_1d9){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +if(m.isArrayLike(_1d9)&&!self.isIterable(_1d9)){ +for(var i=0;i<_1d9.length;i++){ +lst.push(_1d9[i]); +} +}else{ +_1d9=self.iter(_1d9); +try{ +while(true){ +lst.push(_1d9.next()); +} +} +catch(e){ +if(e!=self.StopIteration){ +throw e; +} +} +} +return lst; +},groupby:function(_1dd,_1de){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +if(arguments.length<2){ +_1de=m.operator.identity; +} +_1dd=self.iter(_1dd); +var pk=undefined; +var k=undefined; +var v; +function fetch(){ +v=_1dd.next(); +k=_1de(v); +} +function eat(){ +var ret=v; +v=undefined; +return ret; +} +var _1e5=true; +var _1e6=m.compare; +return {repr:function(){ +return "groupby(...)"; +},next:function(){ +while(_1e6(k,pk)===0){ +fetch(); +if(_1e5){ +_1e5=false; +break; +} +} +pk=k; +return [k,{next:function(){ +if(v==undefined){ +fetch(); +} +if(_1e6(k,pk)!==0){ +throw self.StopIteration; +} +return eat(); +}}]; +}}; +},groupby_as_array:function(_1e7,_1e8){ +var m=MochiKit.Base; +var self=MochiKit.Iter; +if(arguments.length<2){ +_1e8=m.operator.identity; +} +_1e7=self.iter(_1e7); +var _1eb=[]; +var _1ec=true; +var _1ed; +var _1ee=m.compare; +while(true){ +try{ +var _1ef=_1e7.next(); +var key=_1e8(_1ef); +} +catch(e){ +if(e==self.StopIteration){ +break; +} +throw e; +} +if(_1ec||_1ee(key,_1ed)!==0){ +var _1f1=[]; +_1eb.push([key,_1f1]); +} +_1f1.push(_1ef); +_1ec=false; +_1ed=key; +} +return _1eb; +},arrayLikeIter:function(_1f2){ +var i=0; +return {repr:function(){ +return "arrayLikeIter(...)"; +},toString:MochiKit.Base.forwardCall("repr"),next:function(){ +if(i>=_1f2.length){ +throw MochiKit.Iter.StopIteration; +} +return _1f2[i++]; +}}; +},hasIterateNext:function(_1f4){ +return (_1f4&&typeof (_1f4.iterateNext)=="function"); +},iterateNextIter:function(_1f5){ +return {repr:function(){ +return "iterateNextIter(...)"; +},toString:MochiKit.Base.forwardCall("repr"),next:function(){ +var rval=_1f5.iterateNext(); +if(rval===null||rval===undefined){ +throw MochiKit.Iter.StopIteration; +} +return rval; +}}; +}}); +MochiKit.Iter.EXPORT_OK=["iteratorRegistry","arrayLikeIter","hasIterateNext","iterateNextIter"]; +MochiKit.Iter.EXPORT=["StopIteration","registerIteratorFactory","iter","count","cycle","repeat","next","izip","ifilter","ifilterfalse","islice","imap","applymap","chain","takewhile","dropwhile","tee","list","reduce","range","sum","exhaust","forEach","every","sorted","reversed","some","iextend","groupby","groupby_as_array"]; +MochiKit.Iter.__new__=function(){ +var m=MochiKit.Base; +if(typeof (StopIteration)!="undefined"){ +this.StopIteration=StopIteration; +}else{ +this.StopIteration=new m.NamedError("StopIteration"); +} +this.iteratorRegistry=new m.AdapterRegistry(); +this.registerIteratorFactory("arrayLike",m.isArrayLike,this.arrayLikeIter); +this.registerIteratorFactory("iterateNext",this.hasIterateNext,this.iterateNextIter); +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +m.nameFunctions(this); +}; +MochiKit.Iter.__new__(); +if(MochiKit.__export__){ +reduce=MochiKit.Iter.reduce; +} +MochiKit.Base._exportSymbols(this,MochiKit.Iter); +MochiKit.Base._deps("Logging",["Base"]); +MochiKit.Logging.NAME="MochiKit.Logging"; +MochiKit.Logging.VERSION="1.4.2"; +MochiKit.Logging.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.Logging.toString=function(){ +return this.__repr__(); +}; +MochiKit.Logging.EXPORT=["LogLevel","LogMessage","Logger","alertListener","logger","log","logError","logDebug","logFatal","logWarning"]; +MochiKit.Logging.EXPORT_OK=["logLevelAtLeast","isLogMessage","compareLogMessage"]; +MochiKit.Logging.LogMessage=function(num,_1f9,info){ +this.num=num; +this.level=_1f9; +this.info=info; +this.timestamp=new Date(); +}; +MochiKit.Logging.LogMessage.prototype={repr:function(){ +var m=MochiKit.Base; +return "LogMessage("+m.map(m.repr,[this.num,this.level,this.info]).join(", ")+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +MochiKit.Base.update(MochiKit.Logging,{logLevelAtLeast:function(_1fc){ +var self=MochiKit.Logging; +if(typeof (_1fc)=="string"){ +_1fc=self.LogLevel[_1fc]; +} +return function(msg){ +var _1ff=msg.level; +if(typeof (_1ff)=="string"){ +_1ff=self.LogLevel[_1ff]; +} +return _1ff>=_1fc; +}; +},isLogMessage:function(){ +var _200=MochiKit.Logging.LogMessage; +for(var i=0;i=MochiKit.Logging.LogLevel.FATAL){ +_20f="FATAL"; +}else{ +if(_20f>=MochiKit.Logging.LogLevel.ERROR){ +_20f="ERROR"; +}else{ +if(_20f>=MochiKit.Logging.LogLevel.WARNING){ +_20f="WARNING"; +}else{ +if(_20f>=MochiKit.Logging.LogLevel.INFO){ +_20f="INFO"; +}else{ +_20f="DEBUG"; +} +} +} +} +} +var msg=new MochiKit.Logging.LogMessage(this.counter,_20f,MochiKit.Base.extend(null,arguments,1)); +this._messages.push(msg); +this.dispatchListeners(msg); +if(this.useNativeConsole){ +this.logToConsole(msg.level+": "+msg.info.join(" ")); +} +this.counter+=1; +while(this.maxSize>=0&&this._messages.length>this.maxSize){ +this._messages.shift(); +} +},getMessages:function(_212){ +var _213=0; +if(!(typeof (_212)=="undefined"||_212===null)){ +_213=Math.max(0,this._messages.length-_212); +} +return this._messages.slice(_213); +},getMessageText:function(_214){ +if(typeof (_214)=="undefined"||_214===null){ +_214=30; +} +var _215=this.getMessages(_214); +if(_215.length){ +var lst=map(function(m){ +return "\n ["+m.num+"] "+m.level+": "+m.info.join(" "); +},_215); +lst.unshift("LAST "+_215.length+" MESSAGES:"); +return lst.join(""); +} +return ""; +},debuggingBookmarklet:function(_218){ +if(typeof (MochiKit.LoggingPane)=="undefined"){ +alert(this.getMessageText()); +}else{ +MochiKit.LoggingPane.createLoggingPane(_218||false); +} +}}; +MochiKit.Logging.__new__=function(){ +this.LogLevel={ERROR:40,FATAL:50,WARNING:30,INFO:20,DEBUG:10}; +var m=MochiKit.Base; +m.registerComparator("LogMessage",this.isLogMessage,this.compareLogMessage); +var _21a=m.partial; +var _21b=this.Logger; +var _21c=_21b.prototype.baseLog; +m.update(this.Logger.prototype,{debug:_21a(_21c,"DEBUG"),log:_21a(_21c,"INFO"),error:_21a(_21c,"ERROR"),fatal:_21a(_21c,"FATAL"),warning:_21a(_21c,"WARNING")}); +var self=this; +var _21e=function(name){ +return function(){ +self.logger[name].apply(self.logger,arguments); +}; +}; +this.log=_21e("log"); +this.logError=_21e("error"); +this.logDebug=_21e("debug"); +this.logFatal=_21e("fatal"); +this.logWarning=_21e("warning"); +this.logger=new _21b(); +this.logger.useNativeConsole=true; +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +m.nameFunctions(this); +}; +if(typeof (printfire)=="undefined"&&typeof (document)!="undefined"&&document.createEvent&&typeof (dispatchEvent)!="undefined"){ +printfire=function(){ +printfire.args=arguments; +var ev=document.createEvent("Events"); +ev.initEvent("printfire",false,true); +dispatchEvent(ev); +}; +} +MochiKit.Logging.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.Logging); +MochiKit.Base._deps("DateTime",["Base"]); +MochiKit.DateTime.NAME="MochiKit.DateTime"; +MochiKit.DateTime.VERSION="1.4.2"; +MochiKit.DateTime.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.DateTime.toString=function(){ +return this.__repr__(); +}; +MochiKit.DateTime.isoDate=function(str){ +str=str+""; +if(typeof (str)!="string"||str.length===0){ +return null; +} +var iso=str.split("-"); +if(iso.length===0){ +return null; +} +var date=new Date(iso[0],iso[1]-1,iso[2]); +date.setFullYear(iso[0]); +date.setMonth(iso[1]-1); +date.setDate(iso[2]); +return date; +}; +MochiKit.DateTime._isoRegexp=/(\d{4,})(?:-(\d{1,2})(?:-(\d{1,2})(?:[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2})(?:\.(\d+))?)?(?:(Z)|([+-])(\d{1,2})(?::(\d{1,2}))?)?)?)?)?/; +MochiKit.DateTime.isoTimestamp=function(str){ +str=str+""; +if(typeof (str)!="string"||str.length===0){ +return null; +} +var res=str.match(MochiKit.DateTime._isoRegexp); +if(typeof (res)=="undefined"||res===null){ +return null; +} +var year,_227,day,hour,min,sec,msec; +year=parseInt(res[1],10); +if(typeof (res[2])=="undefined"||res[2]===""){ +return new Date(year); +} +_227=parseInt(res[2],10)-1; +day=parseInt(res[3],10); +if(typeof (res[4])=="undefined"||res[4]===""){ +return new Date(year,_227,day); +} +hour=parseInt(res[4],10); +min=parseInt(res[5],10); +sec=(typeof (res[6])!="undefined"&&res[6]!=="")?parseInt(res[6],10):0; +if(typeof (res[7])!="undefined"&&res[7]!==""){ +msec=Math.round(1000*parseFloat("0."+res[7])); +}else{ +msec=0; +} +if((typeof (res[8])=="undefined"||res[8]==="")&&(typeof (res[9])=="undefined"||res[9]==="")){ +return new Date(year,_227,day,hour,min,sec,msec); +} +var ofs; +if(typeof (res[9])!="undefined"&&res[9]!==""){ +ofs=parseInt(res[10],10)*3600000; +if(typeof (res[11])!="undefined"&&res[11]!==""){ +ofs+=parseInt(res[11],10)*60000; +} +if(res[9]=="-"){ +ofs=-ofs; +} +}else{ +ofs=0; +} +return new Date(Date.UTC(year,_227,day,hour,min,sec,msec)-ofs); +}; +MochiKit.DateTime.toISOTime=function(date,_22f){ +if(typeof (date)=="undefined"||date===null){ +return null; +} +var hh=date.getHours(); +var mm=date.getMinutes(); +var ss=date.getSeconds(); +var lst=[((_22f&&(hh<10))?"0"+hh:hh),((mm<10)?"0"+mm:mm),((ss<10)?"0"+ss:ss)]; +return lst.join(":"); +}; +MochiKit.DateTime.toISOTimestamp=function(date,_235){ +if(typeof (date)=="undefined"||date===null){ +return null; +} +var sep=_235?"T":" "; +var foot=_235?"Z":""; +if(_235){ +date=new Date(date.getTime()+(date.getTimezoneOffset()*60000)); +} +return MochiKit.DateTime.toISODate(date)+sep+MochiKit.DateTime.toISOTime(date,_235)+foot; +}; +MochiKit.DateTime.toISODate=function(date){ +if(typeof (date)=="undefined"||date===null){ +return null; +} +var _239=MochiKit.DateTime._padTwo; +var _23a=MochiKit.DateTime._padFour; +return [_23a(date.getFullYear()),_239(date.getMonth()+1),_239(date.getDate())].join("-"); +}; +MochiKit.DateTime.americanDate=function(d){ +d=d+""; +if(typeof (d)!="string"||d.length===0){ +return null; +} +var a=d.split("/"); +return new Date(a[2],a[0]-1,a[1]); +}; +MochiKit.DateTime._padTwo=function(n){ +return (n>9)?n:"0"+n; +}; +MochiKit.DateTime._padFour=function(n){ +switch(n.toString().length){ +case 1: +return "000"+n; +break; +case 2: +return "00"+n; +break; +case 3: +return "0"+n; +break; +case 4: +default: +return n; +} +}; +MochiKit.DateTime.toPaddedAmericanDate=function(d){ +if(typeof (d)=="undefined"||d===null){ +return null; +} +var _240=MochiKit.DateTime._padTwo; +return [_240(d.getMonth()+1),_240(d.getDate()),d.getFullYear()].join("/"); +}; +MochiKit.DateTime.toAmericanDate=function(d){ +if(typeof (d)=="undefined"||d===null){ +return null; +} +return [d.getMonth()+1,d.getDate(),d.getFullYear()].join("/"); +}; +MochiKit.DateTime.EXPORT=["isoDate","isoTimestamp","toISOTime","toISOTimestamp","toISODate","americanDate","toPaddedAmericanDate","toAmericanDate"]; +MochiKit.DateTime.EXPORT_OK=[]; +MochiKit.DateTime.EXPORT_TAGS={":common":MochiKit.DateTime.EXPORT,":all":MochiKit.DateTime.EXPORT}; +MochiKit.DateTime.__new__=function(){ +var base=this.NAME+"."; +for(var k in this){ +var o=this[k]; +if(typeof (o)=="function"&&typeof (o.NAME)=="undefined"){ +try{ +o.NAME=base+k; +} +catch(e){ +} +} +} +}; +MochiKit.DateTime.__new__(); +if(typeof (MochiKit.Base)!="undefined"){ +MochiKit.Base._exportSymbols(this,MochiKit.DateTime); +}else{ +(function(_245,_246){ +if((typeof (JSAN)=="undefined"&&typeof (dojo)=="undefined")||(MochiKit.__export__===false)){ +var all=_246.EXPORT_TAGS[":all"]; +for(var i=0;i_250){ +var i=_258.length-_250; +res=fmt.separator+_258.substring(i,_258.length)+res; +_258=_258.substring(0,i); +} +} +res=_258+res; +if(_24e>0){ +while(frac.length<_251){ +frac=frac+"0"; +} +res=res+fmt.decimal+frac; +} +return _253+res+_254; +}; +}; +MochiKit.Format.numberFormatter=function(_25c,_25d,_25e){ +if(typeof (_25d)=="undefined"){ +_25d=""; +} +var _25f=_25c.match(/((?:[0#]+,)?[0#]+)(?:\.([0#]+))?(%)?/); +if(!_25f){ +throw TypeError("Invalid pattern"); +} +var _260=_25c.substr(0,_25f.index); +var _261=_25c.substr(_25f.index+_25f[0].length); +if(_260.search(/-/)==-1){ +_260=_260+"-"; +} +var _262=_25f[1]; +var frac=(typeof (_25f[2])=="string"&&_25f[2]!="")?_25f[2]:""; +var _264=(typeof (_25f[3])=="string"&&_25f[3]!=""); +var tmp=_262.split(/,/); +var _266; +if(typeof (_25e)=="undefined"){ +_25e="default"; +} +if(tmp.length==1){ +_266=null; +}else{ +_266=tmp[1].length; +} +var _267=_262.length-_262.replace(/0/g,"").length; +var _268=frac.length-frac.replace(/0/g,"").length; +var _269=frac.length; +var rval=MochiKit.Format._numberFormatter(_25d,_260,_261,_25e,_264,_269,_267,_266,_268); +var m=MochiKit.Base; +if(m){ +var fn=arguments.callee; +var args=m.concat(arguments); +rval.repr=function(){ +return [self.NAME,"(",map(m.repr,args).join(", "),")"].join(""); +}; +} +return rval; +}; +MochiKit.Format.formatLocale=function(_26e){ +if(typeof (_26e)=="undefined"||_26e===null){ +_26e="default"; +} +if(typeof (_26e)=="string"){ +var rval=MochiKit.Format.LOCALE[_26e]; +if(typeof (rval)=="string"){ +rval=arguments.callee(rval); +MochiKit.Format.LOCALE[_26e]=rval; +} +return rval; +}else{ +return _26e; +} +}; +MochiKit.Format.twoDigitAverage=function(_270,_271){ +if(_271){ +var res=_270/_271; +if(!isNaN(res)){ +return MochiKit.Format.twoDigitFloat(res); +} +} +return "0"; +}; +MochiKit.Format.twoDigitFloat=function(_273){ +var res=roundToFixed(_273,2); +if(res.indexOf(".00")>0){ +return res.substring(0,res.length-3); +}else{ +if(res.charAt(res.length-1)=="0"){ +return res.substring(0,res.length-1); +}else{ +return res; +} +} +}; +MochiKit.Format.lstrip=function(str,_276){ +str=str+""; +if(typeof (str)!="string"){ +return null; +} +if(!_276){ +return str.replace(/^\s+/,""); +}else{ +return str.replace(new RegExp("^["+_276+"]+"),""); +} +}; +MochiKit.Format.rstrip=function(str,_278){ +str=str+""; +if(typeof (str)!="string"){ +return null; +} +if(!_278){ +return str.replace(/\s+$/,""); +}else{ +return str.replace(new RegExp("["+_278+"]+$"),""); +} +}; +MochiKit.Format.strip=function(str,_27a){ +var self=MochiKit.Format; +return self.rstrip(self.lstrip(str,_27a),_27a); +}; +MochiKit.Format.truncToFixed=function(_27c,_27d){ +var res=Math.floor(_27c).toFixed(0); +if(_27c<0){ +res=Math.ceil(_27c).toFixed(0); +if(res.charAt(0)!="-"&&_27d>0){ +res="-"+res; +} +} +if(res.indexOf("e")<0&&_27d>0){ +var tail=_27c.toString(); +if(tail.indexOf("e")>0){ +tail="."; +}else{ +if(tail.indexOf(".")<0){ +tail="."; +}else{ +tail=tail.substring(tail.indexOf(".")); +} +} +if(tail.length-1>_27d){ +tail=tail.substring(0,_27d+1); +} +while(tail.length-1<_27d){ +tail+="0"; +} +res+=tail; +} +return res; +}; +MochiKit.Format.roundToFixed=function(_280,_281){ +var _282=Math.abs(_280)+0.5*Math.pow(10,-_281); +var res=MochiKit.Format.truncToFixed(_282,_281); +if(_280<0){ +res="-"+res; +} +return res; +}; +MochiKit.Format.percentFormat=function(_284){ +return MochiKit.Format.twoDigitFloat(100*_284)+"%"; +}; +MochiKit.Format.EXPORT=["truncToFixed","roundToFixed","numberFormatter","formatLocale","twoDigitAverage","twoDigitFloat","percentFormat","lstrip","rstrip","strip"]; +MochiKit.Format.LOCALE={en_US:{separator:",",decimal:".",percent:"%"},de_DE:{separator:".",decimal:",",percent:"%"},pt_BR:{separator:".",decimal:",",percent:"%"},fr_FR:{separator:" ",decimal:",",percent:"%"},"default":"en_US"}; +MochiKit.Format.EXPORT_OK=[]; +MochiKit.Format.EXPORT_TAGS={":all":MochiKit.Format.EXPORT,":common":MochiKit.Format.EXPORT}; +MochiKit.Format.__new__=function(){ +var base=this.NAME+"."; +var k,v,o; +for(k in this.LOCALE){ +o=this.LOCALE[k]; +if(typeof (o)=="object"){ +o.repr=function(){ +return this.NAME; +}; +o.NAME=base+"LOCALE."+k; +} +} +for(k in this){ +o=this[k]; +if(typeof (o)=="function"&&typeof (o.NAME)=="undefined"){ +try{ +o.NAME=base+k; +} +catch(e){ +} +} +} +}; +MochiKit.Format.__new__(); +if(typeof (MochiKit.Base)!="undefined"){ +MochiKit.Base._exportSymbols(this,MochiKit.Format); +}else{ +(function(_289,_28a){ +if((typeof (JSAN)=="undefined"&&typeof (dojo)=="undefined")||(MochiKit.__export__===false)){ +var all=_28a.EXPORT_TAGS[":all"]; +for(var i=0;i1){ +fn=MochiKit.Base.partial.apply(null,arguments); +} +return this.addCallbacks(fn,fn); +},addCallback:function(fn){ +if(arguments.length>1){ +fn=MochiKit.Base.partial.apply(null,arguments); +} +return this.addCallbacks(fn,null); +},addErrback:function(fn){ +if(arguments.length>1){ +fn=MochiKit.Base.partial.apply(null,arguments); +} +return this.addCallbacks(null,fn); +},addCallbacks:function(cb,eb){ +if(this.chained){ +throw new Error("Chained Deferreds can not be re-used"); +} +this.chain.push([cb,eb]); +if(this.fired>=0){ +this._fire(); +} +return this; +},_fire:function(){ +var _299=this.chain; +var _29a=this.fired; +var res=this.results[_29a]; +var self=this; +var cb=null; +while(_299.length>0&&this.paused===0){ +var pair=_299.shift(); +var f=pair[_29a]; +if(f===null){ +continue; +} +try{ +res=f(res); +_29a=((res instanceof Error)?1:0); +if(res instanceof MochiKit.Async.Deferred){ +cb=function(res){ +self._resback(res); +self.paused--; +if((self.paused===0)&&(self.fired>=0)){ +self._fire(); +} +}; +this.paused++; +} +} +catch(err){ +_29a=1; +if(!(err instanceof Error)){ +err=new MochiKit.Async.GenericError(err); +} +res=err; +} +} +this.fired=_29a; +this.results[_29a]=res; +if(cb&&this.paused){ +res.addBoth(cb); +res.chained=true; +} +}}; +MochiKit.Base.update(MochiKit.Async,{evalJSONRequest:function(req){ +return MochiKit.Base.evalJSON(req.responseText); +},succeed:function(_2a2){ +var d=new MochiKit.Async.Deferred(); +d.callback.apply(d,arguments); +return d; +},fail:function(_2a4){ +var d=new MochiKit.Async.Deferred(); +d.errback.apply(d,arguments); +return d; +},getXMLHttpRequest:function(){ +var self=arguments.callee; +if(!self.XMLHttpRequest){ +var _2a7=[function(){ +return new XMLHttpRequest(); +},function(){ +return new ActiveXObject("Msxml2.XMLHTTP"); +},function(){ +return new ActiveXObject("Microsoft.XMLHTTP"); +},function(){ +return new ActiveXObject("Msxml2.XMLHTTP.4.0"); +},function(){ +throw new MochiKit.Async.BrowserComplianceError("Browser does not support XMLHttpRequest"); +}]; +for(var i=0;i<_2a7.length;i++){ +var func=_2a7[i]; +try{ +self.XMLHttpRequest=func; +return func(); +} +catch(e){ +} +} +} +return self.XMLHttpRequest(); +},_xhr_onreadystatechange:function(d){ +var m=MochiKit.Base; +if(this.readyState==4){ +try{ +this.onreadystatechange=null; +} +catch(e){ +try{ +this.onreadystatechange=m.noop; +} +catch(e){ +} +} +var _2ac=null; +try{ +_2ac=this.status; +if(!_2ac&&m.isNotEmpty(this.responseText)){ +_2ac=304; +} +} +catch(e){ +} +if(_2ac==200||_2ac==201||_2ac==204||_2ac==304||_2ac==1223){ +d.callback(this); +}else{ +var err=new MochiKit.Async.XMLHttpRequestError(this,"Request failed"); +if(err.number){ +d.errback(err); +}else{ +d.errback(err); +} +} +} +},_xhr_canceller:function(req){ +try{ +req.onreadystatechange=null; +} +catch(e){ +try{ +req.onreadystatechange=MochiKit.Base.noop; +} +catch(e){ +} +} +req.abort(); +},sendXMLHttpRequest:function(req,_2b0){ +if(typeof (_2b0)=="undefined"||_2b0===null){ +_2b0=""; +} +var m=MochiKit.Base; +var self=MochiKit.Async; +var d=new self.Deferred(m.partial(self._xhr_canceller,req)); +try{ +req.onreadystatechange=m.bind(self._xhr_onreadystatechange,req,d); +req.send(_2b0); +} +catch(e){ +try{ +req.onreadystatechange=null; +} +catch(ignore){ +} +d.errback(e); +} +return d; +},doXHR:function(url,opts){ +var self=MochiKit.Async; +return self.callLater(0,self._doXHR,url,opts); +},_doXHR:function(url,opts){ +var m=MochiKit.Base; +opts=m.update({method:"GET",sendContent:""},opts); +var self=MochiKit.Async; +var req=self.getXMLHttpRequest(); +if(opts.queryString){ +var qs=m.queryString(opts.queryString); +if(qs){ +url+="?"+qs; +} +} +if("username" in opts){ +req.open(opts.method,url,true,opts.username,opts.password); +}else{ +req.open(opts.method,url,true); +} +if(req.overrideMimeType&&opts.mimeType){ +req.overrideMimeType(opts.mimeType); +} +req.setRequestHeader("X-Requested-With","XMLHttpRequest"); +if(opts.headers){ +var _2bd=opts.headers; +if(!m.isArrayLike(_2bd)){ +_2bd=m.items(_2bd); +} +for(var i=0;i<_2bd.length;i++){ +var _2bf=_2bd[i]; +var name=_2bf[0]; +var _2c1=_2bf[1]; +req.setRequestHeader(name,_2c1); +} +} +return self.sendXMLHttpRequest(req,opts.sendContent); +},_buildURL:function(url){ +if(arguments.length>1){ +var m=MochiKit.Base; +var qs=m.queryString.apply(null,m.extend(null,arguments,1)); +if(qs){ +return url+"?"+qs; +} +} +return url; +},doSimpleXMLHttpRequest:function(url){ +var self=MochiKit.Async; +url=self._buildURL.apply(self,arguments); +return self.doXHR(url); +},loadJSONDoc:function(url){ +var self=MochiKit.Async; +url=self._buildURL.apply(self,arguments); +var d=self.doXHR(url,{"mimeType":"text/plain","headers":[["Accept","application/json"]]}); +d=d.addCallback(self.evalJSONRequest); +return d; +},wait:function(_2ca,_2cb){ +var d=new MochiKit.Async.Deferred(); +var m=MochiKit.Base; +if(typeof (_2cb)!="undefined"){ +d.addCallback(function(){ +return _2cb; +}); +} +var _2ce=setTimeout(m.bind("callback",d),Math.floor(_2ca*1000)); +d.canceller=function(){ +try{ +clearTimeout(_2ce); +} +catch(e){ +} +}; +return d; +},callLater:function(_2cf,func){ +var m=MochiKit.Base; +var _2d2=m.partial.apply(m,m.extend(null,arguments,1)); +return MochiKit.Async.wait(_2cf).addCallback(function(res){ +return _2d2(); +}); +}}); +MochiKit.Async.DeferredLock=function(){ +this.waiting=[]; +this.locked=false; +this.id=this._nextId(); +}; +MochiKit.Async.DeferredLock.prototype={__class__:MochiKit.Async.DeferredLock,acquire:function(){ +var d=new MochiKit.Async.Deferred(); +if(this.locked){ +this.waiting.push(d); +}else{ +this.locked=true; +d.callback(this); +} +return d; +},release:function(){ +if(!this.locked){ +throw TypeError("Tried to release an unlocked DeferredLock"); +} +this.locked=false; +if(this.waiting.length>0){ +this.locked=true; +this.waiting.shift().callback(this); +} +},_nextId:MochiKit.Base.counter(),repr:function(){ +var _2d5; +if(this.locked){ +_2d5="locked, "+this.waiting.length+" waiting"; +}else{ +_2d5="unlocked"; +} +return "DeferredLock("+this.id+", "+_2d5+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +MochiKit.Async.DeferredList=function(list,_2d7,_2d8,_2d9,_2da){ +MochiKit.Async.Deferred.apply(this,[_2da]); +this.list=list; +var _2db=[]; +this.resultList=_2db; +this.finishedCount=0; +this.fireOnOneCallback=_2d7; +this.fireOnOneErrback=_2d8; +this.consumeErrors=_2d9; +var cb=MochiKit.Base.bind(this._cbDeferred,this); +for(var i=0;i=0){ +var opt=elem.options[elem.selectedIndex]; +var v=opt.value; +if(!v){ +var h=opt.outerHTML; +if(h&&!h.match(/^[^>]+\svalue\s*=/i)){ +v=opt.text; +} +} +_2fa.push(name); +_2fb.push(v); +return null; +} +_2fa.push(name); +_2fb.push(""); +return null; +}else{ +var opts=elem.options; +if(!opts.length){ +_2fa.push(name); +_2fb.push(""); +return null; +} +for(var i=0;i]+\svalue\s*=/i)){ +v=opt.text; +} +} +_2fa.push(name); +_2fb.push(v); +} +return null; +} +} +if(_300==="FORM"||_300==="P"||_300==="SPAN"||_300==="DIV"){ +return elem.childNodes; +} +_2fa.push(name); +_2fb.push(elem.value||""); +return null; +} +return elem.childNodes; +}); +return [_2fa,_2fb]; +},withDocument:function(doc,func){ +var self=MochiKit.DOM; +var _309=self._document; +var rval; +try{ +self._document=doc; +rval=func(); +} +catch(e){ +self._document=_309; +throw e; +} +self._document=_309; +return rval; +},registerDOMConverter:function(name,_30c,wrap,_30e){ +MochiKit.DOM.domConverters.register(name,_30c,wrap,_30e); +},coerceToDOM:function(node,ctx){ +var m=MochiKit.Base; +var im=MochiKit.Iter; +var self=MochiKit.DOM; +if(im){ +var iter=im.iter; +var _315=im.repeat; +} +var map=m.map; +var _317=self.domConverters; +var _318=arguments.callee; +var _319=m.NotFound; +while(true){ +if(typeof (node)=="undefined"||node===null){ +return null; +} +if(typeof (node)=="function"&&typeof (node.length)=="number"&&!(node instanceof Function)){ +node=im?im.list(node):m.extend(null,node); +} +if(typeof (node.nodeType)!="undefined"&&node.nodeType>0){ +return node; +} +if(typeof (node)=="number"||typeof (node)=="boolean"){ +node=node.toString(); +} +if(typeof (node)=="string"){ +return self._document.createTextNode(node); +} +if(typeof (node.__dom__)=="function"){ +node=node.__dom__(ctx); +continue; +} +if(typeof (node.dom)=="function"){ +node=node.dom(ctx); +continue; +} +if(typeof (node)=="function"){ +node=node.apply(ctx,[ctx]); +continue; +} +if(im){ +var _31a=null; +try{ +_31a=iter(node); +} +catch(e){ +} +if(_31a){ +return map(_318,_31a,_315(ctx)); +} +}else{ +if(m.isArrayLike(node)){ +var func=function(n){ +return _318(n,ctx); +}; +return map(func,node); +} +} +try{ +node=_317.match(node,ctx); +continue; +} +catch(e){ +if(e!=_319){ +throw e; +} +} +return self._document.createTextNode(node.toString()); +} +return undefined; +},isChildNode:function(node,_31e){ +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +node=self.getElement(node); +} +if(typeof (_31e)=="string"){ +_31e=self.getElement(_31e); +} +if(typeof (node)=="undefined"||node===null){ +return false; +} +while(node!=null&&node!==self._document){ +if(node===_31e){ +return true; +} +node=node.parentNode; +} +return false; +},setNodeAttribute:function(node,attr,_322){ +var o={}; +o[attr]=_322; +try{ +return MochiKit.DOM.updateNodeAttributes(node,o); +} +catch(e){ +} +return null; +},getNodeAttribute:function(node,attr){ +var self=MochiKit.DOM; +var _327=self.attributeArray.renames[attr]; +var _328=self.attributeArray.ignoreAttr[attr]; +node=self.getElement(node); +try{ +if(_327){ +return node[_327]; +} +var _329=node.getAttribute(attr); +if(_329!=_328){ +return _329; +} +} +catch(e){ +} +return null; +},removeNodeAttribute:function(node,attr){ +var self=MochiKit.DOM; +var _32d=self.attributeArray.renames[attr]; +node=self.getElement(node); +try{ +if(_32d){ +return node[_32d]; +} +return node.removeAttribute(attr); +} +catch(e){ +} +return null; +},updateNodeAttributes:function(node,_32f){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +} +if(_32f){ +var _332=MochiKit.Base.updatetree; +if(self.attributeArray.compliant){ +for(var k in _32f){ +var v=_32f[k]; +if(typeof (v)=="object"&&typeof (elem[k])=="object"){ +if(k=="style"&&MochiKit.Style){ +MochiKit.Style.setStyle(elem,v); +}else{ +_332(elem[k],v); +} +}else{ +if(k.substring(0,2)=="on"){ +if(typeof (v)=="string"){ +v=new Function(v); +} +elem[k]=v; +}else{ +elem.setAttribute(k,v); +} +} +if(typeof (elem[k])=="string"&&elem[k]!=v){ +elem[k]=v; +} +} +}else{ +var _335=self.attributeArray.renames; +for(var k in _32f){ +v=_32f[k]; +var _336=_335[k]; +if(k=="style"&&typeof (v)=="string"){ +elem.style.cssText=v; +}else{ +if(typeof (_336)=="string"){ +elem[_336]=v; +}else{ +if(typeof (elem[k])=="object"&&typeof (v)=="object"){ +if(k=="style"&&MochiKit.Style){ +MochiKit.Style.setStyle(elem,v); +}else{ +_332(elem[k],v); +} +}else{ +if(k.substring(0,2)=="on"){ +if(typeof (v)=="string"){ +v=new Function(v); +} +elem[k]=v; +}else{ +elem.setAttribute(k,v); +} +} +} +} +if(typeof (elem[k])=="string"&&elem[k]!=v){ +elem[k]=v; +} +} +} +} +return elem; +},appendChildNodes:function(node){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +} +var _33a=[self.coerceToDOM(MochiKit.Base.extend(null,arguments,1),elem)]; +var _33b=MochiKit.Base.concat; +while(_33a.length){ +var n=_33a.shift(); +if(typeof (n)=="undefined"||n===null){ +}else{ +if(typeof (n.nodeType)=="number"){ +elem.appendChild(n); +}else{ +_33a=_33b(n,_33a); +} +} +} +return elem; +},insertSiblingNodesBefore:function(node){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +} +var _340=[self.coerceToDOM(MochiKit.Base.extend(null,arguments,1),elem)]; +var _341=elem.parentNode; +var _342=MochiKit.Base.concat; +while(_340.length){ +var n=_340.shift(); +if(typeof (n)=="undefined"||n===null){ +}else{ +if(typeof (n.nodeType)=="number"){ +_341.insertBefore(n,elem); +}else{ +_340=_342(n,_340); +} +} +} +return _341; +},insertSiblingNodesAfter:function(node){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +} +var _347=[self.coerceToDOM(MochiKit.Base.extend(null,arguments,1),elem)]; +if(elem.nextSibling){ +return self.insertSiblingNodesBefore(elem.nextSibling,_347); +}else{ +return self.appendChildNodes(elem.parentNode,_347); +} +},replaceChildNodes:function(node){ +var elem=node; +var self=MochiKit.DOM; +if(typeof (node)=="string"){ +elem=self.getElement(node); +arguments[0]=elem; +} +var _34b; +while((_34b=elem.firstChild)){ +elem.removeChild(_34b); +} +if(arguments.length<2){ +return elem; +}else{ +return self.appendChildNodes.apply(this,arguments); +} +},createDOM:function(name,_34d){ +var elem; +var self=MochiKit.DOM; +var m=MochiKit.Base; +if(typeof (_34d)=="string"||typeof (_34d)=="number"){ +var args=m.extend([name,null],arguments,1); +return arguments.callee.apply(this,args); +} +if(typeof (name)=="string"){ +var _352=self._xhtml; +if(_34d&&!self.attributeArray.compliant){ +var _353=""; +if("name" in _34d){ +_353+=" name=\""+self.escapeHTML(_34d.name)+"\""; +} +if(name=="input"&&"type" in _34d){ +_353+=" type=\""+self.escapeHTML(_34d.type)+"\""; +} +if(_353){ +name="<"+name+_353+">"; +_352=false; +} +} +var d=self._document; +if(_352&&d===document){ +elem=d.createElementNS("http://www.w3.org/1999/xhtml",name); +}else{ +elem=d.createElement(name); +} +}else{ +elem=name; +} +if(_34d){ +self.updateNodeAttributes(elem,_34d); +} +if(arguments.length<=2){ +return elem; +}else{ +var args=m.extend([elem],arguments,2); +return self.appendChildNodes.apply(this,args); +} +},createDOMFunc:function(){ +var m=MochiKit.Base; +return m.partial.apply(this,m.extend([MochiKit.DOM.createDOM],arguments)); +},removeElement:function(elem){ +var self=MochiKit.DOM; +var e=self.coerceToDOM(self.getElement(elem)); +e.parentNode.removeChild(e); +return e; +},swapDOM:function(dest,src){ +var self=MochiKit.DOM; +dest=self.getElement(dest); +var _35c=dest.parentNode; +if(src){ +src=self.coerceToDOM(self.getElement(src),_35c); +_35c.replaceChild(src,dest); +}else{ +_35c.removeChild(dest); +} +return src; +},getElement:function(id){ +var self=MochiKit.DOM; +if(arguments.length==1){ +return ((typeof (id)=="string")?self._document.getElementById(id):id); +}else{ +return MochiKit.Base.map(self.getElement,arguments); +} +},getElementsByTagAndClassName:function(_35f,_360,_361){ +var self=MochiKit.DOM; +if(typeof (_35f)=="undefined"||_35f===null){ +_35f="*"; +} +if(typeof (_361)=="undefined"||_361===null){ +_361=self._document; +} +_361=self.getElement(_361); +if(_361==null){ +return []; +} +var _363=(_361.getElementsByTagName(_35f)||self._document.all); +if(typeof (_360)=="undefined"||_360===null){ +return MochiKit.Base.extend(null,_363); +} +var _364=[]; +for(var i=0;i<_363.length;i++){ +var _366=_363[i]; +var cls=_366.className; +if(typeof (cls)!="string"){ +cls=_366.getAttribute("class"); +} +if(typeof (cls)=="string"){ +var _368=cls.split(" "); +for(var j=0;j<_368.length;j++){ +if(_368[j]==_360){ +_364.push(_366); +break; +} +} +} +} +return _364; +},_newCallStack:function(path,once){ +var rval=function(){ +var _36d=arguments.callee.callStack; +for(var i=0;i<_36d.length;i++){ +if(_36d[i].apply(this,arguments)===false){ +break; +} +} +if(once){ +try{ +this[path]=null; +} +catch(e){ +} +} +}; +rval.callStack=[]; +return rval; +},addToCallStack:function(_36f,path,func,once){ +var self=MochiKit.DOM; +var _374=_36f[path]; +var _375=_374; +if(!(typeof (_374)=="function"&&typeof (_374.callStack)=="object"&&_374.callStack!==null)){ +_375=self._newCallStack(path,once); +if(typeof (_374)=="function"){ +_375.callStack.push(_374); +} +_36f[path]=_375; +} +_375.callStack.push(func); +},addLoadEvent:function(func){ +var self=MochiKit.DOM; +self.addToCallStack(self._window,"onload",func,true); +},focusOnLoad:function(_378){ +var self=MochiKit.DOM; +self.addLoadEvent(function(){ +_378=self.getElement(_378); +if(_378){ +_378.focus(); +} +}); +},setElementClass:function(_37a,_37b){ +var self=MochiKit.DOM; +var obj=self.getElement(_37a); +if(self.attributeArray.compliant){ +obj.setAttribute("class",_37b); +}else{ +obj.setAttribute("className",_37b); +} +},toggleElementClass:function(_37e){ +var self=MochiKit.DOM; +for(var i=1;i/g,">"); +},toHTML:function(dom){ +return MochiKit.DOM.emitHTML(dom).join(""); +},emitHTML:function(dom,lst){ +if(typeof (lst)=="undefined"||lst===null){ +lst=[]; +} +var _3a1=[dom]; +var self=MochiKit.DOM; +var _3a3=self.escapeHTML; +var _3a4=self.attributeArray; +while(_3a1.length){ +dom=_3a1.pop(); +if(typeof (dom)=="string"){ +lst.push(dom); +}else{ +if(dom.nodeType==1){ +lst.push("<"+dom.tagName.toLowerCase()); +var _3a5=[]; +var _3a6=_3a4(dom); +for(var i=0;i<_3a6.length;i++){ +var a=_3a6[i]; +_3a5.push([" ",a.name,"=\"",_3a3(a.value),"\""]); +} +_3a5.sort(); +for(i=0;i<_3a5.length;i++){ +var _3a9=_3a5[i]; +for(var j=0;j<_3a9.length;j++){ +lst.push(_3a9[j]); +} +} +if(dom.hasChildNodes()){ +lst.push(">"); +_3a1.push(""); +var _3ab=dom.childNodes; +for(i=_3ab.length-1;i>=0;i--){ +_3a1.push(_3ab[i]); +} +}else{ +lst.push("/>"); +} +}else{ +if(dom.nodeType==3){ +lst.push(_3a3(dom.nodeValue)); +} +} +} +} +return lst; +},scrapeText:function(node,_3ad){ +var rval=[]; +(function(node){ +var cn=node.childNodes; +if(cn){ +for(var i=0;i0){ +var _3ca=m.filter; +_3c9=function(node){ +return _3ca(_3c9.ignoreAttrFilter,node.attributes); +}; +_3c9.ignoreAttr={}; +var _3cc=_3c8.attributes; +var _3cd=_3c9.ignoreAttr; +for(var i=0;i<_3cc.length;i++){ +var a=_3cc[i]; +_3cd[a.name]=a.value; +} +_3c9.ignoreAttrFilter=function(a){ +return (_3c9.ignoreAttr[a.name]!=a.value); +}; +_3c9.compliant=false; +_3c9.renames={"class":"className","checked":"defaultChecked","usemap":"useMap","for":"htmlFor","readonly":"readOnly","colspan":"colSpan","bgcolor":"bgColor","cellspacing":"cellSpacing","cellpadding":"cellPadding"}; +}else{ +_3c9=function(node){ +return node.attributes; +}; +_3c9.compliant=true; +_3c9.ignoreAttr={}; +_3c9.renames={}; +} +this.attributeArray=_3c9; +var _3d2=function(_3d3,arr){ +var _3d5=arr[0]; +var _3d6=arr[1]; +var _3d7=_3d6.split(".")[1]; +var str=""; +str+="if (!MochiKit."+_3d7+") { throw new Error(\""; +str+="This function has been deprecated and depends on MochiKit."; +str+=_3d7+".\");}"; +str+="return "+_3d6+".apply(this, arguments);"; +MochiKit[_3d3][_3d5]=new Function(str); +}; +for(var i=0;i0){ +abort(repr(expr)); +} +},buildMatchExpression:function(){ +var repr=MochiKit.Base.repr; +var _3e4=this.params; +var _3e5=[]; +var _3e6,i; +function childElements(_3e8){ +return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, "+_3e8+".childNodes)"; +} +if(_3e4.wildcard){ +_3e5.push("true"); +} +if(_3e6=_3e4.id){ +_3e5.push("element.id == "+repr(_3e6)); +} +if(_3e6=_3e4.tagName){ +_3e5.push("element.tagName.toUpperCase() == "+repr(_3e6)); +} +if((_3e6=_3e4.classNames).length>0){ +for(i=0;i<_3e6.length;i++){ +_3e5.push("MochiKit.DOM.hasElementClass(element, "+repr(_3e6[i])+")"); +} +} +if((_3e6=_3e4.pseudoClassNames).length>0){ +for(i=0;i<_3e6.length;i++){ +var _3e9=_3e6[i].match(/^([^(]+)(?:\((.*)\))?$/); +var _3ea=_3e9[1]; +var _3eb=_3e9[2]; +switch(_3ea){ +case "root": +_3e5.push("element.nodeType == 9 || element === element.ownerDocument.documentElement"); +break; +case "nth-child": +case "nth-last-child": +case "nth-of-type": +case "nth-last-of-type": +_3e9=_3eb.match(/^((?:(\d+)n\+)?(\d+)|odd|even)$/); +if(!_3e9){ +throw "Invalid argument to pseudo element nth-child: "+_3eb; +} +var a,b; +if(_3e9[0]=="odd"){ +a=2; +b=1; +}else{ +if(_3e9[0]=="even"){ +a=2; +b=0; +}else{ +a=_3e9[2]&&parseInt(_3e9)||null; +b=parseInt(_3e9[3]); +} +} +_3e5.push("this.nthChild(element,"+a+","+b+","+!!_3ea.match("^nth-last")+","+!!_3ea.match("of-type$")+")"); +break; +case "first-child": +_3e5.push("this.nthChild(element, null, 1)"); +break; +case "last-child": +_3e5.push("this.nthChild(element, null, 1, true)"); +break; +case "first-of-type": +_3e5.push("this.nthChild(element, null, 1, false, true)"); +break; +case "last-of-type": +_3e5.push("this.nthChild(element, null, 1, true, true)"); +break; +case "only-child": +_3e5.push(childElements("element.parentNode")+".length == 1"); +break; +case "only-of-type": +_3e5.push("MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, "+childElements("element.parentNode")+").length == 1"); +break; +case "empty": +_3e5.push("element.childNodes.length == 0"); +break; +case "enabled": +_3e5.push("(this.isUIElement(element) && element.disabled === false)"); +break; +case "disabled": +_3e5.push("(this.isUIElement(element) && element.disabled === true)"); +break; +case "checked": +_3e5.push("(this.isUIElement(element) && element.checked === true)"); +break; +case "not": +var _3ee=new MochiKit.Selector.Selector(_3eb); +_3e5.push("!( "+_3ee.buildMatchExpression()+")"); +break; +} +} +} +if(_3e6=_3e4.attributes){ +MochiKit.Base.map(function(_3ef){ +var _3f0="MochiKit.DOM.getNodeAttribute(element, "+repr(_3ef.name)+")"; +var _3f1=function(_3f2){ +return _3f0+".split("+repr(_3f2)+")"; +}; +_3e5.push(_3f0+" != null"); +switch(_3ef.operator){ +case "=": +_3e5.push(_3f0+" == "+repr(_3ef.value)); +break; +case "~=": +_3e5.push("MochiKit.Base.findValue("+_3f1(" ")+", "+repr(_3ef.value)+") > -1"); +break; +case "^=": +_3e5.push(_3f0+".substring(0, "+_3ef.value.length+") == "+repr(_3ef.value)); +break; +case "$=": +_3e5.push(_3f0+".substring("+_3f0+".length - "+_3ef.value.length+") == "+repr(_3ef.value)); +break; +case "*=": +_3e5.push(_3f0+".match("+repr(_3ef.value)+")"); +break; +case "|=": +_3e5.push(_3f1("-")+"[0].toUpperCase() == "+repr(_3ef.value.toUpperCase())); +break; +case "!=": +_3e5.push(_3f0+" != "+repr(_3ef.value)); +break; +case "": +case undefined: +break; +default: +throw "Unknown operator "+_3ef.operator+" in selector"; +} +},_3e6); +} +return _3e5.join(" && "); +},compileMatcher:function(){ +var code="return (!element.tagName) ? false : "+this.buildMatchExpression()+";"; +this.match=new Function("element",code); +},nthChild:function(_3f4,a,b,_3f7,_3f8){ +var _3f9=MochiKit.Base.filter(function(node){ +return node.nodeType==1; +},_3f4.parentNode.childNodes); +if(_3f8){ +_3f9=MochiKit.Base.filter(function(node){ +return node.tagName==_3f4.tagName; +},_3f9); +} +if(_3f7){ +_3f9=MochiKit.Iter.reversed(_3f9); +} +if(a){ +var _3fc=MochiKit.Base.findIdentical(_3f9,_3f4); +return ((_3fc+1-b)/a)%1==0; +}else{ +return b==MochiKit.Base.findIdentical(_3f9,_3f4)+1; +} +},isUIElement:function(_3fd){ +return MochiKit.Base.findValue(["input","button","select","option","textarea","object"],_3fd.tagName.toLowerCase())>-1; +},findElements:function(_3fe,axis){ +var _400; +if(axis==undefined){ +axis=""; +} +function inScope(_401,_402){ +if(axis==""){ +return MochiKit.DOM.isChildNode(_401,_402); +}else{ +if(axis==">"){ +return _401.parentNode===_402; +}else{ +if(axis=="+"){ +return _401===nextSiblingElement(_402); +}else{ +if(axis=="~"){ +var _403=_402; +while(_403=nextSiblingElement(_403)){ +if(_401===_403){ +return true; +} +} +return false; +}else{ +throw "Invalid axis: "+axis; +} +} +} +} +} +if(_400=MochiKit.DOM.getElement(this.params.id)){ +if(this.match(_400)){ +if(!_3fe||inScope(_400,_3fe)){ +return [_400]; +} +} +} +function nextSiblingElement(node){ +node=node.nextSibling; +while(node&&node.nodeType!=1){ +node=node.nextSibling; +} +return node; +} +if(axis==""){ +_3fe=(_3fe||MochiKit.DOM.currentDocument()).getElementsByTagName(this.params.tagName||"*"); +}else{ +if(axis==">"){ +if(!_3fe){ +throw "> combinator not allowed without preceeding expression"; +} +_3fe=MochiKit.Base.filter(function(node){ +return node.nodeType==1; +},_3fe.childNodes); +}else{ +if(axis=="+"){ +if(!_3fe){ +throw "+ combinator not allowed without preceeding expression"; +} +_3fe=nextSiblingElement(_3fe)&&[nextSiblingElement(_3fe)]; +}else{ +if(axis=="~"){ +if(!_3fe){ +throw "~ combinator not allowed without preceeding expression"; +} +var _406=[]; +while(nextSiblingElement(_3fe)){ +_3fe=nextSiblingElement(_3fe); +_406.push(_3fe); +} +_3fe=_406; +} +} +} +} +if(!_3fe){ +return []; +} +var _407=MochiKit.Base.filter(MochiKit.Base.bind(function(_408){ +return this.match(_408); +},this),_3fe); +return _407; +},repr:function(){ +return "Selector("+this.expression+")"; +},toString:MochiKit.Base.forwardCall("repr")}; +MochiKit.Base.update(MochiKit.Selector,{findChildElements:function(_409,_40a){ +var uniq=function(arr){ +var res=[]; +for(var i=0;i+~]$/)){ +_410=match[0]; +return _412; +}else{ +var _414=new MochiKit.Selector.Selector(expr); +var _415=MochiKit.Iter.reduce(function(_416,_417){ +return MochiKit.Base.extend(_416,_414.findElements(_417||_409,_410)); +},_412,[]); +_410=""; +return _415; +} +}; +var _418=_40f.replace(/(^\s+|\s+$)/g,"").split(/\s+/); +return uniq(MochiKit.Iter.reduce(_411,_418,[null])); +},_40a)); +},findDocElements:function(){ +return MochiKit.Selector.findChildElements(MochiKit.DOM.currentDocument(),arguments); +},__new__:function(){ +var m=MochiKit.Base; +this.$$=this.findDocElements; +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +m.nameFunctions(this); +}}); +MochiKit.Selector.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.Selector); +MochiKit.Base._deps("Style",["Base","DOM"]); +MochiKit.Style.NAME="MochiKit.Style"; +MochiKit.Style.VERSION="1.4.2"; +MochiKit.Style.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.Style.toString=function(){ +return this.__repr__(); +}; +MochiKit.Style.EXPORT_OK=[]; +MochiKit.Style.EXPORT=["setStyle","setOpacity","getStyle","getElementDimensions","elementDimensions","setElementDimensions","getElementPosition","elementPosition","setElementPosition","makePositioned","undoPositioned","makeClipping","undoClipping","setDisplayForElement","hideElement","showElement","getViewportDimensions","getViewportPosition","Dimensions","Coordinates"]; +MochiKit.Style.Dimensions=function(w,h){ +this.w=w; +this.h=h; +}; +MochiKit.Style.Dimensions.prototype.__repr__=function(){ +var repr=MochiKit.Base.repr; +return "{w: "+repr(this.w)+", h: "+repr(this.h)+"}"; +}; +MochiKit.Style.Dimensions.prototype.toString=function(){ +return this.__repr__(); +}; +MochiKit.Style.Coordinates=function(x,y){ +this.x=x; +this.y=y; +}; +MochiKit.Style.Coordinates.prototype.__repr__=function(){ +var repr=MochiKit.Base.repr; +return "{x: "+repr(this.x)+", y: "+repr(this.y)+"}"; +}; +MochiKit.Style.Coordinates.prototype.toString=function(){ +return this.__repr__(); +}; +MochiKit.Base.update(MochiKit.Style,{getStyle:function(elem,_421){ +var dom=MochiKit.DOM; +var d=dom._document; +elem=dom.getElement(elem); +_421=MochiKit.Base.camelize(_421); +if(!elem||elem==d){ +return undefined; +} +if(_421=="opacity"&&typeof (elem.filters)!="undefined"){ +var _424=(MochiKit.Style.getStyle(elem,"filter")||"").match(/alpha\(opacity=(.*)\)/); +if(_424&&_424[1]){ +return parseFloat(_424[1])/100; +} +return 1; +} +if(_421=="float"||_421=="cssFloat"||_421=="styleFloat"){ +if(elem.style["float"]){ +return elem.style["float"]; +}else{ +if(elem.style.cssFloat){ +return elem.style.cssFloat; +}else{ +if(elem.style.styleFloat){ +return elem.style.styleFloat; +}else{ +return "none"; +} +} +} +} +var _425=elem.style?elem.style[_421]:null; +if(!_425){ +if(d.defaultView&&d.defaultView.getComputedStyle){ +var css=d.defaultView.getComputedStyle(elem,null); +_421=_421.replace(/([A-Z])/g,"-$1").toLowerCase(); +_425=css?css.getPropertyValue(_421):null; +}else{ +if(elem.currentStyle){ +_425=elem.currentStyle[_421]; +if(/^\d/.test(_425)&&!/px$/.test(_425)&&_421!="fontWeight"){ +var left=elem.style.left; +var _428=elem.runtimeStyle.left; +elem.runtimeStyle.left=elem.currentStyle.left; +elem.style.left=_425||0; +_425=elem.style.pixelLeft+"px"; +elem.style.left=left; +elem.runtimeStyle.left=_428; +} +} +} +} +if(_421=="opacity"){ +_425=parseFloat(_425); +} +if(/Opera/.test(navigator.userAgent)&&(MochiKit.Base.findValue(["left","top","right","bottom"],_421)!=-1)){ +if(MochiKit.Style.getStyle(elem,"position")=="static"){ +_425="auto"; +} +} +return _425=="auto"?null:_425; +},setStyle:function(elem,_42a){ +elem=MochiKit.DOM.getElement(elem); +for(var name in _42a){ +switch(name){ +case "opacity": +MochiKit.Style.setOpacity(elem,_42a[name]); +break; +case "float": +case "cssFloat": +case "styleFloat": +if(typeof (elem.style["float"])!="undefined"){ +elem.style["float"]=_42a[name]; +}else{ +if(typeof (elem.style.cssFloat)!="undefined"){ +elem.style.cssFloat=_42a[name]; +}else{ +elem.style.styleFloat=_42a[name]; +} +} +break; +default: +elem.style[MochiKit.Base.camelize(name)]=_42a[name]; +} +} +},setOpacity:function(elem,o){ +elem=MochiKit.DOM.getElement(elem); +var self=MochiKit.Style; +if(o==1){ +var _42f=/Gecko/.test(navigator.userAgent)&&!(/Konqueror|AppleWebKit|KHTML/.test(navigator.userAgent)); +elem.style["opacity"]=_42f?0.999999:1; +if(/MSIE/.test(navigator.userAgent)){ +elem.style["filter"]=self.getStyle(elem,"filter").replace(/alpha\([^\)]*\)/gi,""); +} +}else{ +if(o<0.00001){ +o=0; +} +elem.style["opacity"]=o; +if(/MSIE/.test(navigator.userAgent)){ +elem.style["filter"]=self.getStyle(elem,"filter").replace(/alpha\([^\)]*\)/gi,"")+"alpha(opacity="+o*100+")"; +} +} +},getElementPosition:function(elem,_431){ +var self=MochiKit.Style; +var dom=MochiKit.DOM; +elem=dom.getElement(elem); +if(!elem||(!(elem.x&&elem.y)&&(!elem.parentNode===null||self.getStyle(elem,"display")=="none"))){ +return undefined; +} +var c=new self.Coordinates(0,0); +var box=null; +var _436=null; +var d=MochiKit.DOM._document; +var de=d.documentElement; +var b=d.body; +if(!elem.parentNode&&elem.x&&elem.y){ +c.x+=elem.x||0; +c.y+=elem.y||0; +}else{ +if(elem.getBoundingClientRect){ +box=elem.getBoundingClientRect(); +c.x+=box.left+(de.scrollLeft||b.scrollLeft)-(de.clientLeft||0); +c.y+=box.top+(de.scrollTop||b.scrollTop)-(de.clientTop||0); +}else{ +if(elem.offsetParent){ +c.x+=elem.offsetLeft; +c.y+=elem.offsetTop; +_436=elem.offsetParent; +if(_436!=elem){ +while(_436){ +c.x+=parseInt(_436.style.borderLeftWidth)||0; +c.y+=parseInt(_436.style.borderTopWidth)||0; +c.x+=_436.offsetLeft; +c.y+=_436.offsetTop; +_436=_436.offsetParent; +} +} +var ua=navigator.userAgent.toLowerCase(); +if((typeof (opera)!="undefined"&&parseFloat(opera.version())<9)||(ua.indexOf("AppleWebKit")!=-1&&self.getStyle(elem,"position")=="absolute")){ +c.x-=b.offsetLeft; +c.y-=b.offsetTop; +} +if(elem.parentNode){ +_436=elem.parentNode; +}else{ +_436=null; +} +while(_436){ +var _43b=_436.tagName.toUpperCase(); +if(_43b==="BODY"||_43b==="HTML"){ +break; +} +var disp=self.getStyle(_436,"display"); +if(disp.search(/^inline|table-row.*$/i)){ +c.x-=_436.scrollLeft; +c.y-=_436.scrollTop; +} +if(_436.parentNode){ +_436=_436.parentNode; +}else{ +_436=null; +} +} +} +} +} +if(typeof (_431)!="undefined"){ +_431=arguments.callee(_431); +if(_431){ +c.x-=(_431.x||0); +c.y-=(_431.y||0); +} +} +return c; +},setElementPosition:function(elem,_43e,_43f){ +elem=MochiKit.DOM.getElement(elem); +if(typeof (_43f)=="undefined"){ +_43f="px"; +} +var _440={}; +var _441=MochiKit.Base.isUndefinedOrNull; +if(!_441(_43e.x)){ +_440["left"]=_43e.x+_43f; +} +if(!_441(_43e.y)){ +_440["top"]=_43e.y+_43f; +} +MochiKit.DOM.updateNodeAttributes(elem,{"style":_440}); +},makePositioned:function(_442){ +_442=MochiKit.DOM.getElement(_442); +var pos=MochiKit.Style.getStyle(_442,"position"); +if(pos=="static"||!pos){ +_442.style.position="relative"; +if(/Opera/.test(navigator.userAgent)){ +_442.style.top=0; +_442.style.left=0; +} +} +},undoPositioned:function(_444){ +_444=MochiKit.DOM.getElement(_444); +if(_444.style.position=="relative"){ +_444.style.position=_444.style.top=_444.style.left=_444.style.bottom=_444.style.right=""; +} +},makeClipping:function(_445){ +_445=MochiKit.DOM.getElement(_445); +var s=_445.style; +var _447={"overflow":s.overflow,"overflow-x":s.overflowX,"overflow-y":s.overflowY}; +if((MochiKit.Style.getStyle(_445,"overflow")||"visible")!="hidden"){ +_445.style.overflow="hidden"; +_445.style.overflowX="hidden"; +_445.style.overflowY="hidden"; +} +return _447; +},undoClipping:function(_448,_449){ +_448=MochiKit.DOM.getElement(_448); +if(typeof (_449)=="string"){ +_448.style.overflow=_449; +}else{ +if(_449!=null){ +_448.style.overflow=_449["overflow"]; +_448.style.overflowX=_449["overflow-x"]; +_448.style.overflowY=_449["overflow-y"]; +} +} +},getElementDimensions:function(elem,_44b){ +var self=MochiKit.Style; +var dom=MochiKit.DOM; +if(typeof (elem.w)=="number"||typeof (elem.h)=="number"){ +return new self.Dimensions(elem.w||0,elem.h||0); +} +elem=dom.getElement(elem); +if(!elem){ +return undefined; +} +var disp=self.getStyle(elem,"display"); +if(disp=="none"||disp==""||typeof (disp)=="undefined"){ +var s=elem.style; +var _450=s.visibility; +var _451=s.position; +var _452=s.display; +s.visibility="hidden"; +s.position="absolute"; +s.display=self._getDefaultDisplay(elem); +var _453=elem.offsetWidth; +var _454=elem.offsetHeight; +s.display=_452; +s.position=_451; +s.visibility=_450; +}else{ +_453=elem.offsetWidth||0; +_454=elem.offsetHeight||0; +} +if(_44b){ +var _455="colSpan" in elem&&"rowSpan" in elem; +var _456=(_455&&elem.parentNode&&self.getStyle(elem.parentNode,"borderCollapse")=="collapse"); +if(_456){ +if(/MSIE/.test(navigator.userAgent)){ +var _457=elem.previousSibling?0.5:1; +var _458=elem.nextSibling?0.5:1; +}else{ +var _457=0.5; +var _458=0.5; +} +}else{ +var _457=1; +var _458=1; +} +_453-=Math.round((parseFloat(self.getStyle(elem,"paddingLeft"))||0)+(parseFloat(self.getStyle(elem,"paddingRight"))||0)+_457*(parseFloat(self.getStyle(elem,"borderLeftWidth"))||0)+_458*(parseFloat(self.getStyle(elem,"borderRightWidth"))||0)); +if(_455){ +if(/Gecko|Opera/.test(navigator.userAgent)&&!/Konqueror|AppleWebKit|KHTML/.test(navigator.userAgent)){ +var _459=0; +}else{ +if(/MSIE/.test(navigator.userAgent)){ +var _459=1; +}else{ +var _459=_456?0.5:1; +} +} +}else{ +var _459=1; +} +_454-=Math.round((parseFloat(self.getStyle(elem,"paddingTop"))||0)+(parseFloat(self.getStyle(elem,"paddingBottom"))||0)+_459*((parseFloat(self.getStyle(elem,"borderTopWidth"))||0)+(parseFloat(self.getStyle(elem,"borderBottomWidth"))||0))); +} +return new self.Dimensions(_453,_454); +},setElementDimensions:function(elem,_45b,_45c){ +elem=MochiKit.DOM.getElement(elem); +if(typeof (_45c)=="undefined"){ +_45c="px"; +} +var _45d={}; +var _45e=MochiKit.Base.isUndefinedOrNull; +if(!_45e(_45b.w)){ +_45d["width"]=_45b.w+_45c; +} +if(!_45e(_45b.h)){ +_45d["height"]=_45b.h+_45c; +} +MochiKit.DOM.updateNodeAttributes(elem,{"style":_45d}); +},_getDefaultDisplay:function(elem){ +var self=MochiKit.Style; +var dom=MochiKit.DOM; +elem=dom.getElement(elem); +if(!elem){ +return undefined; +} +var _462=elem.tagName.toUpperCase(); +return self._defaultDisplay[_462]||"block"; +},setDisplayForElement:function(_463,_464){ +var _465=MochiKit.Base.extend(null,arguments,1); +var _466=MochiKit.DOM.getElement; +for(var i=0;i<_465.length;i++){ +_464=_466(_465[i]); +if(_464){ +_464.style.display=_463; +} +} +},getViewportDimensions:function(){ +var d=new MochiKit.Style.Dimensions(); +var w=MochiKit.DOM._window; +var b=MochiKit.DOM._document.body; +if(w.innerWidth){ +d.w=w.innerWidth; +d.h=w.innerHeight; +}else{ +if(b&&b.parentElement&&b.parentElement.clientWidth){ +d.w=b.parentElement.clientWidth; +d.h=b.parentElement.clientHeight; +}else{ +if(b&&b.clientWidth){ +d.w=b.clientWidth; +d.h=b.clientHeight; +} +} +} +return d; +},getViewportPosition:function(){ +var c=new MochiKit.Style.Coordinates(0,0); +var d=MochiKit.DOM._document; +var de=d.documentElement; +var db=d.body; +if(de&&(de.scrollTop||de.scrollLeft)){ +c.x=de.scrollLeft; +c.y=de.scrollTop; +}else{ +if(db){ +c.x=db.scrollLeft; +c.y=db.scrollTop; +} +} +return c; +},__new__:function(){ +var m=MochiKit.Base; +var _470=["A","ABBR","ACRONYM","B","BASEFONT","BDO","BIG","BR","CITE","CODE","DFN","EM","FONT","I","IMG","KBD","LABEL","Q","S","SAMP","SMALL","SPAN","STRIKE","STRONG","SUB","SUP","TEXTAREA","TT","U","VAR"]; +this._defaultDisplay={"TABLE":"table","THEAD":"table-header-group","TBODY":"table-row-group","TFOOT":"table-footer-group","COLGROUP":"table-column-group","COL":"table-column","TR":"table-row","TD":"table-cell","TH":"table-cell","CAPTION":"table-caption","LI":"list-item","INPUT":"inline-block","SELECT":"inline-block"}; +if(/MSIE/.test(navigator.userAgent)){ +for(var k in this._defaultDisplay){ +var v=this._defaultDisplay[k]; +if(v.indexOf("table")==0){ +this._defaultDisplay[k]="block"; +} +} +} +for(var i=0;i<_470.length;i++){ +this._defaultDisplay[_470[i]]="inline"; +} +this.elementPosition=this.getElementPosition; +this.elementDimensions=this.getElementDimensions; +this.hideElement=m.partial(this.setDisplayForElement,"none"); +this.showElement=m.partial(this.setDisplayForElement,"block"); +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +m.nameFunctions(this); +}}); +MochiKit.Style.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.Style); +MochiKit.Base._deps("LoggingPane",["Base","Logging"]); +MochiKit.LoggingPane.NAME="MochiKit.LoggingPane"; +MochiKit.LoggingPane.VERSION="1.4.2"; +MochiKit.LoggingPane.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.LoggingPane.toString=function(){ +return this.__repr__(); +}; +MochiKit.LoggingPane.createLoggingPane=function(_474){ +var m=MochiKit.LoggingPane; +_474=!(!_474); +if(m._loggingPane&&m._loggingPane.inline!=_474){ +m._loggingPane.closePane(); +m._loggingPane=null; +} +if(!m._loggingPane||m._loggingPane.closed){ +m._loggingPane=new m.LoggingPane(_474,MochiKit.Logging.logger); +} +return m._loggingPane; +}; +MochiKit.LoggingPane.LoggingPane=function(_476,_477){ +if(typeof (_477)=="undefined"||_477===null){ +_477=MochiKit.Logging.logger; +} +this.logger=_477; +var _478=MochiKit.Base.update; +var _479=MochiKit.Base.updatetree; +var bind=MochiKit.Base.bind; +var _47b=MochiKit.Base.clone; +var win=window; +var uid="_MochiKit_LoggingPane"; +if(typeof (MochiKit.DOM)!="undefined"){ +win=MochiKit.DOM.currentWindow(); +} +if(!_476){ +var url=win.location.href.split("?")[0].replace(/[#:\/.><&%-]/g,"_"); +var name=uid+"_"+url; +var nwin=win.open("",name,"dependent,resizable,height=200"); +if(!nwin){ +alert("Not able to open debugging window due to pop-up blocking."); +return undefined; +} +nwin.document.write(""+"[MochiKit.LoggingPane]"+""); +nwin.document.close(); +nwin.document.title+=" "+win.document.title; +win=nwin; +} +var doc=win.document; +this.doc=doc; +var _482=doc.getElementById(uid); +var _483=!!_482; +if(_482&&typeof (_482.loggingPane)!="undefined"){ +_482.loggingPane.logger=this.logger; +_482.loggingPane.buildAndApplyFilter(); +return _482.loggingPane; +} +if(_483){ +var _484; +while((_484=_482.firstChild)){ +_482.removeChild(_484); +} +}else{ +_482=doc.createElement("div"); +_482.id=uid; +} +_482.loggingPane=this; +var _485=doc.createElement("input"); +var _486=doc.createElement("input"); +var _487=doc.createElement("button"); +var _488=doc.createElement("button"); +var _489=doc.createElement("button"); +var _48a=doc.createElement("button"); +var _48b=doc.createElement("div"); +var _48c=doc.createElement("div"); +var _48d=uid+"_Listener"; +this.colorTable=_47b(this.colorTable); +var _48e=[]; +var _48f=null; +var _490=function(msg){ +var _492=msg.level; +if(typeof (_492)=="number"){ +_492=MochiKit.Logging.LogLevel[_492]; +} +return _492; +}; +var _493=function(msg){ +return msg.info.join(" "); +}; +var _495=bind(function(msg){ +var _497=_490(msg); +var text=_493(msg); +var c=this.colorTable[_497]; +var p=doc.createElement("span"); +p.className="MochiKit-LogMessage MochiKit-LogLevel-"+_497; +p.style.cssText="margin: 0px; white-space: -moz-pre-wrap; white-space: -o-pre-wrap; white-space: pre-wrap; white-space: pre-line; word-wrap: break-word; wrap-option: emergency; color: "+c; +p.appendChild(doc.createTextNode(_497+": "+text)); +_48c.appendChild(p); +_48c.appendChild(doc.createElement("br")); +if(_48b.offsetHeight>_48b.scrollHeight){ +_48b.scrollTop=0; +}else{ +_48b.scrollTop=_48b.scrollHeight; +} +},this); +var _49b=function(msg){ +_48e[_48e.length]=msg; +_495(msg); +}; +var _49d=function(){ +var _49e,_49f; +try{ +_49e=new RegExp(_485.value); +_49f=new RegExp(_486.value); +} +catch(e){ +logDebug("Error in filter regex: "+e.message); +return null; +} +return function(msg){ +return (_49e.test(_490(msg))&&_49f.test(_493(msg))); +}; +}; +var _4a1=function(){ +while(_48c.firstChild){ +_48c.removeChild(_48c.firstChild); +} +}; +var _4a2=function(){ +_48e=[]; +_4a1(); +}; +var _4a3=bind(function(){ +if(this.closed){ +return; +} +this.closed=true; +if(MochiKit.LoggingPane._loggingPane==this){ +MochiKit.LoggingPane._loggingPane=null; +} +this.logger.removeListener(_48d); +try{ +try{ +_482.loggingPane=null; +} +catch(e){ +logFatal("Bookmarklet was closed incorrectly."); +} +if(_476){ +_482.parentNode.removeChild(_482); +}else{ +this.win.close(); +} +} +catch(e){ +} +},this); +var _4a4=function(){ +_4a1(); +for(var i=0;i<_48e.length;i++){ +var msg=_48e[i]; +if(_48f===null||_48f(msg)){ +_495(msg); +} +} +}; +this.buildAndApplyFilter=function(){ +_48f=_49d(); +_4a4(); +this.logger.removeListener(_48d); +this.logger.addListener(_48d,_48f,_49b); +}; +var _4a7=bind(function(){ +_48e=this.logger.getMessages(); +_4a4(); +},this); +var _4a8=bind(function(_4a9){ +_4a9=_4a9||window.event; +key=_4a9.which||_4a9.keyCode; +if(key==13){ +this.buildAndApplyFilter(); +} +},this); +var _4aa="display: block; z-index: 1000; left: 0px; bottom: 0px; position: fixed; width: 100%; background-color: white; font: "+this.logFont; +if(_476){ +_4aa+="; height: 10em; border-top: 2px solid black"; +}else{ +_4aa+="; height: 100%;"; +} +_482.style.cssText=_4aa; +if(!_483){ +doc.body.appendChild(_482); +} +_4aa={"cssText":"width: 33%; display: inline; font: "+this.logFont}; +_479(_485,{"value":"FATAL|ERROR|WARNING|INFO|DEBUG","onkeypress":_4a8,"style":_4aa}); +_482.appendChild(_485); +_479(_486,{"value":".*","onkeypress":_4a8,"style":_4aa}); +_482.appendChild(_486); +_4aa="width: 8%; display:inline; font: "+this.logFont; +_487.appendChild(doc.createTextNode("Filter")); +_487.onclick=bind("buildAndApplyFilter",this); +_487.style.cssText=_4aa; +_482.appendChild(_487); +_488.appendChild(doc.createTextNode("Load")); +_488.onclick=_4a7; +_488.style.cssText=_4aa; +_482.appendChild(_488); +_489.appendChild(doc.createTextNode("Clear")); +_489.onclick=_4a2; +_489.style.cssText=_4aa; +_482.appendChild(_489); +_48a.appendChild(doc.createTextNode("Close")); +_48a.onclick=_4a3; +_48a.style.cssText=_4aa; +_482.appendChild(_48a); +_48b.style.cssText="overflow: auto; width: 100%"; +_48c.style.cssText="width: 100%; height: "+(_476?"8em":"100%"); +_48b.appendChild(_48c); +_482.appendChild(_48b); +this.buildAndApplyFilter(); +_4a7(); +if(_476){ +this.win=undefined; +}else{ +this.win=win; +} +this.inline=_476; +this.closePane=_4a3; +this.closed=false; +return this; +}; +MochiKit.LoggingPane.LoggingPane.prototype={"logFont":"8pt Verdana,sans-serif","colorTable":{"ERROR":"red","FATAL":"darkred","WARNING":"blue","INFO":"black","DEBUG":"green"}}; +MochiKit.LoggingPane.EXPORT_OK=["LoggingPane"]; +MochiKit.LoggingPane.EXPORT=["createLoggingPane"]; +MochiKit.LoggingPane.__new__=function(){ +this.EXPORT_TAGS={":common":this.EXPORT,":all":MochiKit.Base.concat(this.EXPORT,this.EXPORT_OK)}; +MochiKit.Base.nameFunctions(this); +MochiKit.LoggingPane._loggingPane=null; +}; +MochiKit.LoggingPane.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.LoggingPane); +MochiKit.Base._deps("Color",["Base","DOM","Style"]); +MochiKit.Color.NAME="MochiKit.Color"; +MochiKit.Color.VERSION="1.4.2"; +MochiKit.Color.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.Color.toString=function(){ +return this.__repr__(); +}; +MochiKit.Color.Color=function(red,_4ac,blue,_4ae){ +if(typeof (_4ae)=="undefined"||_4ae===null){ +_4ae=1; +} +this.rgb={r:red,g:_4ac,b:blue,a:_4ae}; +}; +MochiKit.Color.Color.prototype={__class__:MochiKit.Color.Color,colorWithAlpha:function(_4af){ +var rgb=this.rgb; +var m=MochiKit.Color; +return m.Color.fromRGB(rgb.r,rgb.g,rgb.b,_4af); +},colorWithHue:function(hue){ +var hsl=this.asHSL(); +hsl.h=hue; +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},colorWithSaturation:function(_4b5){ +var hsl=this.asHSL(); +hsl.s=_4b5; +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},colorWithLightness:function(_4b8){ +var hsl=this.asHSL(); +hsl.l=_4b8; +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},darkerColorWithLevel:function(_4bb){ +var hsl=this.asHSL(); +hsl.l=Math.max(hsl.l-_4bb,0); +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},lighterColorWithLevel:function(_4be){ +var hsl=this.asHSL(); +hsl.l=Math.min(hsl.l+_4be,1); +var m=MochiKit.Color; +return m.Color.fromHSL(hsl); +},blendedColor:function(_4c1,_4c2){ +if(typeof (_4c2)=="undefined"||_4c2===null){ +_4c2=0.5; +} +var sf=1-_4c2; +var s=this.rgb; +var d=_4c1.rgb; +var df=_4c2; +return MochiKit.Color.Color.fromRGB((s.r*sf)+(d.r*df),(s.g*sf)+(d.g*df),(s.b*sf)+(d.b*df),(s.a*sf)+(d.a*df)); +},compareRGB:function(_4c7){ +var a=this.asRGB(); +var b=_4c7.asRGB(); +return MochiKit.Base.compare([a.r,a.g,a.b,a.a],[b.r,b.g,b.b,b.a]); +},isLight:function(){ +return this.asHSL().b>0.5; +},isDark:function(){ +return (!this.isLight()); +},toHSLString:function(){ +var c=this.asHSL(); +var ccc=MochiKit.Color.clampColorComponent; +var rval=this._hslString; +if(!rval){ +var mid=(ccc(c.h,360).toFixed(0)+","+ccc(c.s,100).toPrecision(4)+"%"+","+ccc(c.l,100).toPrecision(4)+"%"); +var a=c.a; +if(a>=1){ +a=1; +rval="hsl("+mid+")"; +}else{ +if(a<=0){ +a=0; +} +rval="hsla("+mid+","+a+")"; +} +this._hslString=rval; +} +return rval; +},toRGBString:function(){ +var c=this.rgb; +var ccc=MochiKit.Color.clampColorComponent; +var rval=this._rgbString; +if(!rval){ +var mid=(ccc(c.r,255).toFixed(0)+","+ccc(c.g,255).toFixed(0)+","+ccc(c.b,255).toFixed(0)); +if(c.a!=1){ +rval="rgba("+mid+","+c.a+")"; +}else{ +rval="rgb("+mid+")"; +} +this._rgbString=rval; +} +return rval; +},asRGB:function(){ +return MochiKit.Base.clone(this.rgb); +},toHexString:function(){ +var m=MochiKit.Color; +var c=this.rgb; +var ccc=MochiKit.Color.clampColorComponent; +var rval=this._hexString; +if(!rval){ +rval=("#"+m.toColorPart(ccc(c.r,255))+m.toColorPart(ccc(c.g,255))+m.toColorPart(ccc(c.b,255))); +this._hexString=rval; +} +return rval; +},asHSV:function(){ +var hsv=this.hsv; +var c=this.rgb; +if(typeof (hsv)=="undefined"||hsv===null){ +hsv=MochiKit.Color.rgbToHSV(this.rgb); +this.hsv=hsv; +} +return MochiKit.Base.clone(hsv); +},asHSL:function(){ +var hsl=this.hsl; +var c=this.rgb; +if(typeof (hsl)=="undefined"||hsl===null){ +hsl=MochiKit.Color.rgbToHSL(this.rgb); +this.hsl=hsl; +} +return MochiKit.Base.clone(hsl); +},toString:function(){ +return this.toRGBString(); +},repr:function(){ +var c=this.rgb; +var col=[c.r,c.g,c.b,c.a]; +return this.__class__.NAME+"("+col.join(", ")+")"; +}}; +MochiKit.Base.update(MochiKit.Color.Color,{fromRGB:function(red,_4de,blue,_4e0){ +var _4e1=MochiKit.Color.Color; +if(arguments.length==1){ +var rgb=red; +red=rgb.r; +_4de=rgb.g; +blue=rgb.b; +if(typeof (rgb.a)=="undefined"){ +_4e0=undefined; +}else{ +_4e0=rgb.a; +} +} +return new _4e1(red,_4de,blue,_4e0); +},fromHSL:function(hue,_4e4,_4e5,_4e6){ +var m=MochiKit.Color; +return m.Color.fromRGB(m.hslToRGB.apply(m,arguments)); +},fromHSV:function(hue,_4e9,_4ea,_4eb){ +var m=MochiKit.Color; +return m.Color.fromRGB(m.hsvToRGB.apply(m,arguments)); +},fromName:function(name){ +var _4ee=MochiKit.Color.Color; +if(name.charAt(0)=="\""){ +name=name.substr(1,name.length-2); +} +var _4ef=_4ee._namedColors[name.toLowerCase()]; +if(typeof (_4ef)=="string"){ +return _4ee.fromHexString(_4ef); +}else{ +if(name=="transparent"){ +return _4ee.transparentColor(); +} +} +return null; +},fromString:function(_4f0){ +var self=MochiKit.Color.Color; +var _4f2=_4f0.substr(0,3); +if(_4f2=="rgb"){ +return self.fromRGBString(_4f0); +}else{ +if(_4f2=="hsl"){ +return self.fromHSLString(_4f0); +}else{ +if(_4f0.charAt(0)=="#"){ +return self.fromHexString(_4f0); +} +} +} +return self.fromName(_4f0); +},fromHexString:function(_4f3){ +if(_4f3.charAt(0)=="#"){ +_4f3=_4f3.substring(1); +} +var _4f4=[]; +var i,hex; +if(_4f3.length==3){ +for(i=0;i<3;i++){ +hex=_4f3.substr(i,1); +_4f4.push(parseInt(hex+hex,16)/255); +} +}else{ +for(i=0;i<6;i+=2){ +hex=_4f3.substr(i,2); +_4f4.push(parseInt(hex,16)/255); +} +} +var _4f7=MochiKit.Color.Color; +return _4f7.fromRGB.apply(_4f7,_4f4); +},_fromColorString:function(pre,_4f9,_4fa,_4fb){ +if(_4fb.indexOf(pre)===0){ +_4fb=_4fb.substring(_4fb.indexOf("(",3)+1,_4fb.length-1); +} +var _4fc=_4fb.split(/\s*,\s*/); +var _4fd=[]; +for(var i=0;i<_4fc.length;i++){ +var c=_4fc[i]; +var val; +var _501=c.substring(c.length-3); +if(c.charAt(c.length-1)=="%"){ +val=0.01*parseFloat(c.substring(0,c.length-1)); +}else{ +if(_501=="deg"){ +val=parseFloat(c)/360; +}else{ +if(_501=="rad"){ +val=parseFloat(c)/(Math.PI*2); +}else{ +val=_4fa[i]*parseFloat(c); +} +} +} +_4fd.push(val); +} +return this[_4f9].apply(this,_4fd); +},fromComputedStyle:function(elem,_503){ +var d=MochiKit.DOM; +var cls=MochiKit.Color.Color; +for(elem=d.getElement(elem);elem;elem=elem.parentNode){ +var _506=MochiKit.Style.getStyle.apply(d,arguments); +if(!_506){ +continue; +} +var _507=cls.fromString(_506); +if(!_507){ +break; +} +if(_507.asRGB().a>0){ +return _507; +} +} +return null; +},fromBackground:function(elem){ +var cls=MochiKit.Color.Color; +return cls.fromComputedStyle(elem,"backgroundColor","background-color")||cls.whiteColor(); +},fromText:function(elem){ +var cls=MochiKit.Color.Color; +return cls.fromComputedStyle(elem,"color","color")||cls.blackColor(); +},namedColors:function(){ +return MochiKit.Base.clone(MochiKit.Color.Color._namedColors); +}}); +MochiKit.Base.update(MochiKit.Color,{clampColorComponent:function(v,_50d){ +v*=_50d; +if(v<0){ +return 0; +}else{ +if(v>_50d){ +return _50d; +}else{ +return v; +} +} +},_hslValue:function(n1,n2,hue){ +if(hue>6){ +hue-=6; +}else{ +if(hue<0){ +hue+=6; +} +} +var val; +if(hue<1){ +val=n1+(n2-n1)*hue; +}else{ +if(hue<3){ +val=n2; +}else{ +if(hue<4){ +val=n1+(n2-n1)*(4-hue); +}else{ +val=n1; +} +} +} +return val; +},hsvToRGB:function(hue,_513,_514,_515){ +if(arguments.length==1){ +var hsv=hue; +hue=hsv.h; +_513=hsv.s; +_514=hsv.v; +_515=hsv.a; +} +var red; +var _518; +var blue; +if(_513===0){ +red=_514; +_518=_514; +blue=_514; +}else{ +var i=Math.floor(hue*6); +var f=(hue*6)-i; +var p=_514*(1-_513); +var q=_514*(1-(_513*f)); +var t=_514*(1-(_513*(1-f))); +switch(i){ +case 1: +red=q; +_518=_514; +blue=p; +break; +case 2: +red=p; +_518=_514; +blue=t; +break; +case 3: +red=p; +_518=q; +blue=_514; +break; +case 4: +red=t; +_518=p; +blue=_514; +break; +case 5: +red=_514; +_518=p; +blue=q; +break; +case 6: +case 0: +red=_514; +_518=t; +blue=p; +break; +} +} +return {r:red,g:_518,b:blue,a:_515}; +},hslToRGB:function(hue,_520,_521,_522){ +if(arguments.length==1){ +var hsl=hue; +hue=hsl.h; +_520=hsl.s; +_521=hsl.l; +_522=hsl.a; +} +var red; +var _525; +var blue; +if(_520===0){ +red=_521; +_525=_521; +blue=_521; +}else{ +var m2; +if(_521<=0.5){ +m2=_521*(1+_520); +}else{ +m2=_521+_520-(_521*_520); +} +var m1=(2*_521)-m2; +var f=MochiKit.Color._hslValue; +var h6=hue*6; +red=f(m1,m2,h6+2); +_525=f(m1,m2,h6); +blue=f(m1,m2,h6-2); +} +return {r:red,g:_525,b:blue,a:_522}; +},rgbToHSV:function(red,_52c,blue,_52e){ +if(arguments.length==1){ +var rgb=red; +red=rgb.r; +_52c=rgb.g; +blue=rgb.b; +_52e=rgb.a; +} +var max=Math.max(Math.max(red,_52c),blue); +var min=Math.min(Math.min(red,_52c),blue); +var hue; +var _533; +var _534=max; +if(min==max){ +hue=0; +_533=0; +}else{ +var _535=(max-min); +_533=_535/max; +if(red==max){ +hue=(_52c-blue)/_535; +}else{ +if(_52c==max){ +hue=2+((blue-red)/_535); +}else{ +hue=4+((red-_52c)/_535); +} +} +hue/=6; +if(hue<0){ +hue+=1; +} +if(hue>1){ +hue-=1; +} +} +return {h:hue,s:_533,v:_534,a:_52e}; +},rgbToHSL:function(red,_537,blue,_539){ +if(arguments.length==1){ +var rgb=red; +red=rgb.r; +_537=rgb.g; +blue=rgb.b; +_539=rgb.a; +} +var max=Math.max(red,Math.max(_537,blue)); +var min=Math.min(red,Math.min(_537,blue)); +var hue; +var _53e; +var _53f=(max+min)/2; +var _540=max-min; +if(_540===0){ +hue=0; +_53e=0; +}else{ +if(_53f<=0.5){ +_53e=_540/(max+min); +}else{ +_53e=_540/(2-max-min); +} +if(red==max){ +hue=(_537-blue)/_540; +}else{ +if(_537==max){ +hue=2+((blue-red)/_540); +}else{ +hue=4+((red-_537)/_540); +} +} +hue/=6; +if(hue<0){ +hue+=1; +} +if(hue>1){ +hue-=1; +} +} +return {h:hue,s:_53e,l:_53f,a:_539}; +},toColorPart:function(num){ +num=Math.round(num); +var _542=num.toString(16); +if(num<16){ +return "0"+_542; +} +return _542; +},__new__:function(){ +var m=MochiKit.Base; +this.Color.fromRGBString=m.bind(this.Color._fromColorString,this.Color,"rgb","fromRGB",[1/255,1/255,1/255,1]); +this.Color.fromHSLString=m.bind(this.Color._fromColorString,this.Color,"hsl","fromHSL",[1/360,0.01,0.01,1]); +var _544=1/3; +var _545={black:[0,0,0],blue:[0,0,1],brown:[0.6,0.4,0.2],cyan:[0,1,1],darkGray:[_544,_544,_544],gray:[0.5,0.5,0.5],green:[0,1,0],lightGray:[2*_544,2*_544,2*_544],magenta:[1,0,1],orange:[1,0.5,0],purple:[0.5,0,0.5],red:[1,0,0],transparent:[0,0,0,0],white:[1,1,1],yellow:[1,1,0]}; +var _546=function(name,r,g,b,a){ +var rval=this.fromRGB(r,g,b,a); +this[name]=function(){ +return rval; +}; +return rval; +}; +for(var k in _545){ +var name=k+"Color"; +var _54f=m.concat([_546,this.Color,name],_545[k]); +this.Color[name]=m.bind.apply(null,_54f); +} +var _550=function(){ +for(var i=0;i1){ +var src=MochiKit.DOM.getElement(arguments[0]); +var sig=arguments[1]; +var obj=arguments[2]; +var func=arguments[3]; +for(var i=_592.length-1;i>=0;i--){ +var o=_592[i]; +if(o.source===src&&o.signal===sig&&o.objOrFunc===obj&&o.funcOrStr===func){ +self._disconnect(o); +if(!self._lock){ +_592.splice(i,1); +}else{ +self._dirty=true; +} +return true; +} +} +}else{ +var idx=m.findIdentical(_592,_590); +if(idx>=0){ +self._disconnect(_590); +if(!self._lock){ +_592.splice(idx,1); +}else{ +self._dirty=true; +} +return true; +} +} +return false; +},disconnectAllTo:function(_59b,_59c){ +var self=MochiKit.Signal; +var _59e=self._observers; +var _59f=self._disconnect; +var _5a0=self._lock; +var _5a1=self._dirty; +if(typeof (_59c)==="undefined"){ +_59c=null; +} +for(var i=_59e.length-1;i>=0;i--){ +var _5a3=_59e[i]; +if(_5a3.objOrFunc===_59b&&(_59c===null||_5a3.funcOrStr===_59c)){ +_59f(_5a3); +if(_5a0){ +_5a1=true; +}else{ +_59e.splice(i,1); +} +} +} +self._dirty=_5a1; +},disconnectAll:function(src,sig){ +src=MochiKit.DOM.getElement(src); +var m=MochiKit.Base; +var _5a7=m.flattenArguments(m.extend(null,arguments,1)); +var self=MochiKit.Signal; +var _5a9=self._disconnect; +var _5aa=self._observers; +var i,_5ac; +var _5ad=self._lock; +var _5ae=self._dirty; +if(_5a7.length===0){ +for(i=_5aa.length-1;i>=0;i--){ +_5ac=_5aa[i]; +if(_5ac.source===src){ +_5a9(_5ac); +if(!_5ad){ +_5aa.splice(i,1); +}else{ +_5ae=true; +} +} +} +}else{ +var sigs={}; +for(i=0;i<_5a7.length;i++){ +sigs[_5a7[i]]=true; +} +for(i=_5aa.length-1;i>=0;i--){ +_5ac=_5aa[i]; +if(_5ac.source===src&&_5ac.signal in sigs){ +_5a9(_5ac); +if(!_5ad){ +_5aa.splice(i,1); +}else{ +_5ae=true; +} +} +} +} +self._dirty=_5ae; +},signal:function(src,sig){ +var self=MochiKit.Signal; +var _5b3=self._observers; +src=MochiKit.DOM.getElement(src); +var args=MochiKit.Base.extend(null,arguments,2); +var _5b5=[]; +self._lock=true; +for(var i=0;i<_5b3.length;i++){ +var _5b7=_5b3[i]; +if(_5b7.source===src&&_5b7.signal===sig&&_5b7.connected){ +try{ +_5b7.listener.apply(src,args); +} +catch(e){ +_5b5.push(e); +} +} +} +self._lock=false; +if(self._dirty){ +self._dirty=false; +for(var i=_5b3.length-1;i>=0;i--){ +if(!_5b3[i].connected){ +_5b3.splice(i,1); +} +} +} +if(_5b5.length==1){ +throw _5b5[0]; +}else{ +if(_5b5.length>1){ +var e=new Error("Multiple errors thrown in handling 'sig', see errors property"); +e.errors=_5b5; +throw e; +} +} +}}); +MochiKit.Signal.EXPORT_OK=[]; +MochiKit.Signal.EXPORT=["connect","disconnect","signal","disconnectAll","disconnectAllTo"]; +MochiKit.Signal.__new__=function(win){ +var m=MochiKit.Base; +this._document=document; +this._window=win; +this._lock=false; +this._dirty=false; +try{ +this.connect(window,"onunload",this._unloadCache); +} +catch(e){ +} +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +m.nameFunctions(this); +}; +MochiKit.Signal.__new__(this); +if(MochiKit.__export__){ +connect=MochiKit.Signal.connect; +disconnect=MochiKit.Signal.disconnect; +disconnectAll=MochiKit.Signal.disconnectAll; +signal=MochiKit.Signal.signal; +} +MochiKit.Base._exportSymbols(this,MochiKit.Signal); +MochiKit.Base._deps("Position",["Base","DOM","Style"]); +MochiKit.Position.NAME="MochiKit.Position"; +MochiKit.Position.VERSION="1.4.2"; +MochiKit.Position.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.Position.toString=function(){ +return this.__repr__(); +}; +MochiKit.Position.EXPORT_OK=[]; +MochiKit.Position.EXPORT=[]; +MochiKit.Base.update(MochiKit.Position,{includeScrollOffsets:false,prepare:function(){ +var _5bb=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0; +var _5bc=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0; +this.windowOffset=new MochiKit.Style.Coordinates(_5bb,_5bc); +},cumulativeOffset:function(_5bd){ +var _5be=0; +var _5bf=0; +do{ +_5be+=_5bd.offsetTop||0; +_5bf+=_5bd.offsetLeft||0; +_5bd=_5bd.offsetParent; +}while(_5bd); +return new MochiKit.Style.Coordinates(_5bf,_5be); +},realOffset:function(_5c0){ +var _5c1=0; +var _5c2=0; +do{ +_5c1+=_5c0.scrollTop||0; +_5c2+=_5c0.scrollLeft||0; +_5c0=_5c0.parentNode; +}while(_5c0); +return new MochiKit.Style.Coordinates(_5c2,_5c1); +},within:function(_5c3,x,y){ +if(this.includeScrollOffsets){ +return this.withinIncludingScrolloffsets(_5c3,x,y); +} +this.xcomp=x; +this.ycomp=y; +this.offset=this.cumulativeOffset(_5c3); +if(_5c3.style.position=="fixed"){ +this.offset.x+=this.windowOffset.x; +this.offset.y+=this.windowOffset.y; +} +return (y>=this.offset.y&&y=this.offset.x&&x=this.offset.y&&this.ycomp=this.offset.x&&this.xcomp"+el.innerHTML+""; +},_roundTopCorners:function(el,_5f5,_5f6){ +var _5f7=this._createCorner(_5f6); +for(var i=0;i=0;i--){ +_5fc.appendChild(this._createCornerSlice(_5fa,_5fb,i,"bottom")); +} +el.style.paddingBottom=0; +el.appendChild(_5fc); +},_createCorner:function(_5fe){ +var dom=MochiKit.DOM; +return dom.DIV({style:{backgroundColor:_5fe.toString()}}); +},_createCornerSlice:function(_600,_601,n,_603){ +var _604=MochiKit.DOM.SPAN(); +var _605=_604.style; +_605.backgroundColor=_600.toString(); +_605.display="block"; +_605.height="1px"; +_605.overflow="hidden"; +_605.fontSize="1px"; +var _606=this._borderColor(_600,_601); +if(this.options.border&&n===0){ +_605.borderTopStyle="solid"; +_605.borderTopWidth="1px"; +_605.borderLeftWidth="0px"; +_605.borderRightWidth="0px"; +_605.borderBottomWidth="0px"; +_605.height="0px"; +_605.borderColor=_606.toString(); +}else{ +if(_606){ +_605.borderColor=_606.toString(); +_605.borderStyle="solid"; +_605.borderWidth="0px 1px"; +} +} +if(!this.options.compact&&(n==(this.options.numSlices-1))){ +_605.height="2px"; +} +this._setMargin(_604,n,_603); +this._setBorder(_604,n,_603); +return _604; +},_setOptions:function(_607){ +this.options={corners:"all",color:"fromElement",bgColor:"fromParent",blend:true,border:false,compact:false,__unstable__wrapElement:false}; +MochiKit.Base.update(this.options,_607); +this.options.numSlices=(this.options.compact?2:4); +},_whichSideTop:function(){ +var _608=this.options.corners; +if(this._hasString(_608,"all","top")){ +return ""; +} +var _609=(_608.indexOf("tl")!=-1); +var _60a=(_608.indexOf("tr")!=-1); +if(_609&&_60a){ +return ""; +} +if(_609){ +return "left"; +} +if(_60a){ +return "right"; +} +return ""; +},_whichSideBottom:function(){ +var _60b=this.options.corners; +if(this._hasString(_60b,"all","bottom")){ +return ""; +} +var _60c=(_60b.indexOf("bl")!=-1); +var _60d=(_60b.indexOf("br")!=-1); +if(_60c&&_60d){ +return ""; +} +if(_60c){ +return "left"; +} +if(_60d){ +return "right"; +} +return ""; +},_borderColor:function(_60e,_60f){ +if(_60e=="transparent"){ +return _60f; +}else{ +if(this.options.border){ +return this.options.border; +}else{ +if(this.options.blend){ +return _60f.blendedColor(_60e); +} +} +} +return ""; +},_setMargin:function(el,n,_612){ +var _613=this._marginSize(n)+"px"; +var _614=(_612=="top"?this._whichSideTop():this._whichSideBottom()); +var _615=el.style; +if(_614=="left"){ +_615.marginLeft=_613; +_615.marginRight="0px"; +}else{ +if(_614=="right"){ +_615.marginRight=_613; +_615.marginLeft="0px"; +}else{ +_615.marginLeft=_613; +_615.marginRight=_613; +} +} +},_setBorder:function(el,n,_618){ +var _619=this._borderSize(n)+"px"; +var _61a=(_618=="top"?this._whichSideTop():this._whichSideBottom()); +var _61b=el.style; +if(_61a=="left"){ +_61b.borderLeftWidth=_619; +_61b.borderRightWidth="0px"; +}else{ +if(_61a=="right"){ +_61b.borderRightWidth=_619; +_61b.borderLeftWidth="0px"; +}else{ +_61b.borderLeftWidth=_619; +_61b.borderRightWidth=_619; +} +} +},_marginSize:function(n){ +if(this.isTransparent){ +return 0; +} +var o=this.options; +if(o.compact&&o.blend){ +var _61e=[1,0]; +return _61e[n]; +}else{ +if(o.compact){ +var _61f=[2,1]; +return _61f[n]; +}else{ +if(o.blend){ +var _620=[3,2,1,0]; +return _620[n]; +}else{ +var _621=[5,3,2,1]; +return _621[n]; +} +} +} +},_borderSize:function(n){ +var o=this.options; +var _624; +if(o.compact&&(o.blend||this.isTransparent)){ +return 1; +}else{ +if(o.compact){ +_624=[1,0]; +}else{ +if(o.blend){ +_624=[2,1,1,1]; +}else{ +if(o.border){ +_624=[0,2,0,0]; +}else{ +if(this.isTransparent){ +_624=[5,3,2,1]; +}else{ +return 0; +} +} +} +} +} +return _624[n]; +},_hasString:function(str){ +for(var i=1;i=(_651||i)){ +_651=i; +} +},this.effects); +_64d=_651||_64d; +break; +case "break": +ma(function(e){ +e.finalize(); +},this.effects); +break; +} +_64c.startOn+=_64d; +_64c.finishOn+=_64d; +if(!_64c.options.queue.limit||this.effects.length<_64c.options.queue.limit){ +this.effects.push(_64c); +} +if(!this.interval){ +this.interval=this.startLoop(MochiKit.Base.bind(this.loop,this),40); +} +},startLoop:function(func,_656){ +return setInterval(func,_656); +},remove:function(_657){ +this.effects=MochiKit.Base.filter(function(e){ +return e!=_657; +},this.effects); +if(!this.effects.length){ +this.stopLoop(this.interval); +this.interval=null; +} +},stopLoop:function(_659){ +clearInterval(_659); +},loop:function(){ +var _65a=new Date().getTime(); +MochiKit.Base.map(function(_65b){ +_65b.loop(_65a); +},this.effects); +}}); +MochiKit.Visual.Queues={instances:{},get:function(_65c){ +if(typeof (_65c)!="string"){ +return _65c; +} +if(!this.instances[_65c]){ +this.instances[_65c]=new MochiKit.Visual.ScopedQueue(); +} +return this.instances[_65c]; +}}; +MochiKit.Visual.Queue=MochiKit.Visual.Queues.get("global"); +MochiKit.Visual.DefaultOptions={transition:MochiKit.Visual.Transitions.sinoidal,duration:1,fps:25,sync:false,from:0,to:1,delay:0,queue:"parallel"}; +MochiKit.Visual.Base=function(){ +}; +MochiKit.Visual.Base.prototype={__class__:MochiKit.Visual.Base,start:function(_65d){ +var v=MochiKit.Visual; +this.options=MochiKit.Base.setdefault(_65d,v.DefaultOptions); +this.currentFrame=0; +this.state="idle"; +this.startOn=this.options.delay*1000; +this.finishOn=this.startOn+(this.options.duration*1000); +this.event("beforeStart"); +if(!this.options.sync){ +v.Queues.get(typeof (this.options.queue)=="string"?"global":this.options.queue.scope).add(this); +} +},loop:function(_65f){ +if(_65f>=this.startOn){ +if(_65f>=this.finishOn){ +return this.finalize(); +} +var pos=(_65f-this.startOn)/(this.finishOn-this.startOn); +var _661=Math.round(pos*this.options.fps*this.options.duration); +if(_661>this.currentFrame){ +this.render(pos); +this.currentFrame=_661; +} +} +},render:function(pos){ +if(this.state=="idle"){ +this.state="running"; +this.event("beforeSetup"); +this.setup(); +this.event("afterSetup"); +} +if(this.state=="running"){ +if(this.options.transition){ +pos=this.options.transition(pos); +} +pos*=(this.options.to-this.options.from); +pos+=this.options.from; +this.event("beforeUpdate"); +this.update(pos); +this.event("afterUpdate"); +} +},cancel:function(){ +if(!this.options.sync){ +MochiKit.Visual.Queues.get(typeof (this.options.queue)=="string"?"global":this.options.queue.scope).remove(this); +} +this.state="finished"; +},finalize:function(){ +this.render(1); +this.cancel(); +this.event("beforeFinish"); +this.finish(); +this.event("afterFinish"); +},setup:function(){ +},finish:function(){ +},update:function(_663){ +},event:function(_664){ +if(this.options[_664+"Internal"]){ +this.options[_664+"Internal"](this); +} +if(this.options[_664]){ +this.options[_664](this); +} +},repr:function(){ +return "["+this.__class__.NAME+", options:"+MochiKit.Base.repr(this.options)+"]"; +}}; +MochiKit.Visual.Parallel=function(_665,_666){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_665,_666); +} +this.__init__(_665,_666); +}; +MochiKit.Visual.Parallel.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Parallel.prototype,{__class__:MochiKit.Visual.Parallel,__init__:function(_668,_669){ +this.effects=_668||[]; +this.start(_669); +},update:function(_66a){ +MochiKit.Base.map(function(_66b){ +_66b.render(_66a); +},this.effects); +},finish:function(){ +MochiKit.Base.map(function(_66c){ +_66c.finalize(); +},this.effects); +}}); +MochiKit.Visual.Sequence=function(_66d,_66e){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_66d,_66e); +} +this.__init__(_66d,_66e); +}; +MochiKit.Visual.Sequence.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Sequence.prototype,{__class__:MochiKit.Visual.Sequence,__init__:function(_670,_671){ +var defs={transition:MochiKit.Visual.Transitions.linear,duration:0}; +this.effects=_670||[]; +MochiKit.Base.map(function(_673){ +defs.duration+=_673.options.duration; +},this.effects); +MochiKit.Base.setdefault(_671,defs); +this.start(_671); +},update:function(_674){ +var time=_674*this.options.duration; +for(var i=0;i0){ +this.fontSize=parseFloat(_694); +this.fontSizeType=_695; +} +},this),["em","px","%"]); +this.factor=(this.options.scaleTo-this.options.scaleFrom)/100; +if(/^content/.test(this.options.scaleMode)){ +this.dims=[this.element.scrollHeight,this.element.scrollWidth]; +}else{ +if(this.options.scaleMode=="box"){ +this.dims=[this.element.offsetHeight,this.element.offsetWidth]; +}else{ +this.dims=[this.options.scaleMode.originalHeight,this.options.scaleMode.originalWidth]; +} +} +},update:function(_696){ +var _697=(this.options.scaleFrom/100)+(this.factor*_696); +if(this.options.scaleContent&&this.fontSize){ +MochiKit.Style.setStyle(this.element,{fontSize:this.fontSize*_697+this.fontSizeType}); +} +this.setDimensions(this.dims[0]*_697,this.dims[1]*_697); +},finish:function(){ +if(this.restoreAfterFinish){ +MochiKit.Style.setStyle(this.element,this.originalStyle); +} +},setDimensions:function(_698,_699){ +var d={}; +var r=Math.round; +if(/MSIE/.test(navigator.userAgent)){ +r=Math.ceil; +} +if(this.options.scaleX){ +d.width=r(_699)+"px"; +} +if(this.options.scaleY){ +d.height=r(_698)+"px"; +} +if(this.options.scaleFromCenter){ +var topd=(_698-this.dims[0])/2; +var _69d=(_699-this.dims[1])/2; +if(this.elementPositioning=="absolute"){ +if(this.options.scaleY){ +d.top=this.originalTop-topd+"px"; +} +if(this.options.scaleX){ +d.left=this.originalLeft-_69d+"px"; +} +}else{ +if(this.options.scaleY){ +d.top=-topd+"px"; +} +if(this.options.scaleX){ +d.left=-_69d+"px"; +} +} +} +MochiKit.Style.setStyle(this.element,d); +}}); +MochiKit.Visual.Highlight=function(_69e,_69f){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_69e,_69f); +} +this.__init__(_69e,_69f); +}; +MochiKit.Visual.Highlight.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Highlight.prototype,{__class__:MochiKit.Visual.Highlight,__init__:function(_6a1,_6a2){ +this.element=MochiKit.DOM.getElement(_6a1); +_6a2=MochiKit.Base.update({startcolor:"#ffff99"},_6a2); +this.start(_6a2); +},setup:function(){ +var b=MochiKit.Base; +var s=MochiKit.Style; +if(s.getStyle(this.element,"display")=="none"){ +this.cancel(); +return; +} +this.oldStyle={backgroundImage:s.getStyle(this.element,"background-image")}; +s.setStyle(this.element,{backgroundImage:"none"}); +if(!this.options.endcolor){ +this.options.endcolor=MochiKit.Color.Color.fromBackground(this.element).toHexString(); +} +if(b.isUndefinedOrNull(this.options.restorecolor)){ +this.options.restorecolor=s.getStyle(this.element,"background-color"); +} +this._base=b.map(b.bind(function(i){ +return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16); +},this),[0,1,2]); +this._delta=b.map(b.bind(function(i){ +return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i]; +},this),[0,1,2]); +},update:function(_6a7){ +var m="#"; +MochiKit.Base.map(MochiKit.Base.bind(function(i){ +m+=MochiKit.Color.toColorPart(Math.round(this._base[i]+this._delta[i]*_6a7)); +},this),[0,1,2]); +MochiKit.Style.setStyle(this.element,{backgroundColor:m}); +},finish:function(){ +MochiKit.Style.setStyle(this.element,MochiKit.Base.update(this.oldStyle,{backgroundColor:this.options.restorecolor})); +}}); +MochiKit.Visual.ScrollTo=function(_6aa,_6ab){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_6aa,_6ab); +} +this.__init__(_6aa,_6ab); +}; +MochiKit.Visual.ScrollTo.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.ScrollTo.prototype,{__class__:MochiKit.Visual.ScrollTo,__init__:function(_6ad,_6ae){ +this.element=MochiKit.DOM.getElement(_6ad); +this.start(_6ae); +},setup:function(){ +var p=MochiKit.Position; +p.prepare(); +var _6b0=p.cumulativeOffset(this.element); +if(this.options.offset){ +_6b0.y+=this.options.offset; +} +var max; +if(window.innerHeight){ +max=window.innerHeight-window.height; +}else{ +if(document.documentElement&&document.documentElement.clientHeight){ +max=document.documentElement.clientHeight-document.body.scrollHeight; +}else{ +if(document.body){ +max=document.body.clientHeight-document.body.scrollHeight; +} +} +} +this.scrollStart=p.windowOffset.y; +this.delta=(_6b0.y>max?max:_6b0.y)-this.scrollStart; +},update:function(_6b2){ +var p=MochiKit.Position; +p.prepare(); +window.scrollTo(p.windowOffset.x,this.scrollStart+(_6b2*this.delta)); +}}); +MochiKit.Visual.CSS_LENGTH=/^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; +MochiKit.Visual.Morph=function(_6b4,_6b5){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_6b4,_6b5); +} +this.__init__(_6b4,_6b5); +}; +MochiKit.Visual.Morph.prototype=new MochiKit.Visual.Base(); +MochiKit.Base.update(MochiKit.Visual.Morph.prototype,{__class__:MochiKit.Visual.Morph,__init__:function(_6b7,_6b8){ +this.element=MochiKit.DOM.getElement(_6b7); +this.start(_6b8); +},setup:function(){ +var b=MochiKit.Base; +var _6ba=this.options.style; +this.styleStart={}; +this.styleEnd={}; +this.units={}; +var _6bb,unit; +for(var s in _6ba){ +_6bb=_6ba[s]; +s=b.camelize(s); +if(MochiKit.Visual.CSS_LENGTH.test(_6bb)){ +var _6be=_6bb.match(/^([\+\-]?[0-9\.]+)(.*)$/); +_6bb=parseFloat(_6be[1]); +unit=(_6be.length==3)?_6be[2]:null; +this.styleEnd[s]=_6bb; +this.units[s]=unit; +_6bb=MochiKit.Style.getStyle(this.element,s); +_6be=_6bb.match(/^([\+\-]?[0-9\.]+)(.*)$/); +_6bb=parseFloat(_6be[1]); +this.styleStart[s]=_6bb; +}else{ +if(/[Cc]olor$/.test(s)){ +var c=MochiKit.Color.Color; +_6bb=c.fromString(_6bb); +if(_6bb){ +this.units[s]="color"; +this.styleEnd[s]=_6bb.toHexString(); +_6bb=MochiKit.Style.getStyle(this.element,s); +this.styleStart[s]=c.fromString(_6bb).toHexString(); +this.styleStart[s]=b.map(b.bind(function(i){ +return parseInt(this.styleStart[s].slice(i*2+1,i*2+3),16); +},this),[0,1,2]); +this.styleEnd[s]=b.map(b.bind(function(i){ +return parseInt(this.styleEnd[s].slice(i*2+1,i*2+3),16); +},this),[0,1,2]); +} +}else{ +this.element.style[s]=_6bb; +} +} +} +},update:function(_6c2){ +var _6c3; +for(var s in this.styleStart){ +if(this.units[s]=="color"){ +var m="#"; +var _6c6=this.styleStart[s]; +var end=this.styleEnd[s]; +MochiKit.Base.map(MochiKit.Base.bind(function(i){ +m+=MochiKit.Color.toColorPart(Math.round(_6c6[i]+(end[i]-_6c6[i])*_6c2)); +},this),[0,1,2]); +this.element.style[s]=m; +}else{ +_6c3=this.styleStart[s]+Math.round((this.styleEnd[s]-this.styleStart[s])*_6c2*1000)/1000+this.units[s]; +this.element.style[s]=_6c3; +} +} +}}); +MochiKit.Visual.fade=function(_6c9,_6ca){ +var s=MochiKit.Style; +var _6cc=s.getStyle(_6c9,"opacity"); +_6ca=MochiKit.Base.update({from:s.getStyle(_6c9,"opacity")||1,to:0,afterFinishInternal:function(_6cd){ +if(_6cd.options.to!==0){ +return; +} +s.hideElement(_6cd.element); +s.setStyle(_6cd.element,{"opacity":_6cc}); +}},_6ca); +return new MochiKit.Visual.Opacity(_6c9,_6ca); +}; +MochiKit.Visual.appear=function(_6ce,_6cf){ +var s=MochiKit.Style; +var v=MochiKit.Visual; +_6cf=MochiKit.Base.update({from:(s.getStyle(_6ce,"display")=="none"?0:s.getStyle(_6ce,"opacity")||0),to:1,afterFinishInternal:function(_6d2){ +v.forceRerendering(_6d2.element); +},beforeSetupInternal:function(_6d3){ +s.setStyle(_6d3.element,{"opacity":_6d3.options.from}); +s.showElement(_6d3.element); +}},_6cf); +return new v.Opacity(_6ce,_6cf); +}; +MochiKit.Visual.puff=function(_6d4,_6d5){ +var s=MochiKit.Style; +var v=MochiKit.Visual; +_6d4=MochiKit.DOM.getElement(_6d4); +var _6d8=MochiKit.Style.getElementDimensions(_6d4,true); +var _6d9={position:s.getStyle(_6d4,"position"),top:_6d4.style.top,left:_6d4.style.left,width:_6d4.style.width,height:_6d4.style.height,opacity:s.getStyle(_6d4,"opacity")}; +_6d5=MochiKit.Base.update({beforeSetupInternal:function(_6da){ +MochiKit.Position.absolutize(_6da.effects[0].element); +},afterFinishInternal:function(_6db){ +s.hideElement(_6db.effects[0].element); +s.setStyle(_6db.effects[0].element,_6d9); +},scaleContent:true,scaleFromCenter:true},_6d5); +return new v.Parallel([new v.Scale(_6d4,200,{sync:true,scaleFromCenter:_6d5.scaleFromCenter,scaleMode:{originalHeight:_6d8.h,originalWidth:_6d8.w},scaleContent:_6d5.scaleContent,restoreAfterFinish:true}),new v.Opacity(_6d4,{sync:true,to:0})],_6d5); +}; +MochiKit.Visual.blindUp=function(_6dc,_6dd){ +var d=MochiKit.DOM; +var s=MochiKit.Style; +_6dc=d.getElement(_6dc); +var _6e0=s.getElementDimensions(_6dc,true); +var _6e1=s.makeClipping(_6dc); +_6dd=MochiKit.Base.update({scaleContent:false,scaleX:false,scaleMode:{originalHeight:_6e0.h,originalWidth:_6e0.w},restoreAfterFinish:true,afterFinishInternal:function(_6e2){ +s.hideElement(_6e2.element); +s.undoClipping(_6e2.element,_6e1); +}},_6dd); +return new MochiKit.Visual.Scale(_6dc,0,_6dd); +}; +MochiKit.Visual.blindDown=function(_6e3,_6e4){ +var d=MochiKit.DOM; +var s=MochiKit.Style; +_6e3=d.getElement(_6e3); +var _6e7=s.getElementDimensions(_6e3,true); +var _6e8; +_6e4=MochiKit.Base.update({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:_6e7.h,originalWidth:_6e7.w},restoreAfterFinish:true,afterSetupInternal:function(_6e9){ +_6e8=s.makeClipping(_6e9.element); +s.setStyle(_6e9.element,{height:"0px"}); +s.showElement(_6e9.element); +},afterFinishInternal:function(_6ea){ +s.undoClipping(_6ea.element,_6e8); +}},_6e4); +return new MochiKit.Visual.Scale(_6e3,100,_6e4); +}; +MochiKit.Visual.switchOff=function(_6eb,_6ec){ +var d=MochiKit.DOM; +var s=MochiKit.Style; +_6eb=d.getElement(_6eb); +var _6ef=s.getElementDimensions(_6eb,true); +var _6f0=s.getStyle(_6eb,"opacity"); +var _6f1; +_6ec=MochiKit.Base.update({duration:0.7,restoreAfterFinish:true,beforeSetupInternal:function(_6f2){ +s.makePositioned(_6eb); +_6f1=s.makeClipping(_6eb); +},afterFinishInternal:function(_6f3){ +s.hideElement(_6eb); +s.undoClipping(_6eb,_6f1); +s.undoPositioned(_6eb); +s.setStyle(_6eb,{"opacity":_6f0}); +}},_6ec); +var v=MochiKit.Visual; +return new v.Sequence([new v.appear(_6eb,{sync:true,duration:0.57*_6ec.duration,from:0,transition:v.Transitions.flicker}),new v.Scale(_6eb,1,{sync:true,duration:0.43*_6ec.duration,scaleFromCenter:true,scaleX:false,scaleMode:{originalHeight:_6ef.h,originalWidth:_6ef.w},scaleContent:false,restoreAfterFinish:true})],_6ec); +}; +MochiKit.Visual.dropOut=function(_6f5,_6f6){ +var d=MochiKit.DOM; +var s=MochiKit.Style; +_6f5=d.getElement(_6f5); +var _6f9={top:s.getStyle(_6f5,"top"),left:s.getStyle(_6f5,"left"),opacity:s.getStyle(_6f5,"opacity")}; +_6f6=MochiKit.Base.update({duration:0.5,distance:100,beforeSetupInternal:function(_6fa){ +s.makePositioned(_6fa.effects[0].element); +},afterFinishInternal:function(_6fb){ +s.hideElement(_6fb.effects[0].element); +s.undoPositioned(_6fb.effects[0].element); +s.setStyle(_6fb.effects[0].element,_6f9); +}},_6f6); +var v=MochiKit.Visual; +return new v.Parallel([new v.Move(_6f5,{x:0,y:_6f6.distance,sync:true}),new v.Opacity(_6f5,{sync:true,to:0})],_6f6); +}; +MochiKit.Visual.shake=function(_6fd,_6fe){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_6fd=d.getElement(_6fd); +var _702={top:s.getStyle(_6fd,"top"),left:s.getStyle(_6fd,"left")}; +_6fe=MochiKit.Base.update({duration:0.5,afterFinishInternal:function(_703){ +s.undoPositioned(_6fd); +s.setStyle(_6fd,_702); +}},_6fe); +return new v.Sequence([new v.Move(_6fd,{sync:true,duration:0.1*_6fe.duration,x:20,y:0}),new v.Move(_6fd,{sync:true,duration:0.2*_6fe.duration,x:-40,y:0}),new v.Move(_6fd,{sync:true,duration:0.2*_6fe.duration,x:40,y:0}),new v.Move(_6fd,{sync:true,duration:0.2*_6fe.duration,x:-40,y:0}),new v.Move(_6fd,{sync:true,duration:0.2*_6fe.duration,x:40,y:0}),new v.Move(_6fd,{sync:true,duration:0.1*_6fe.duration,x:-20,y:0})],_6fe); +}; +MochiKit.Visual.slideDown=function(_704,_705){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +var s=MochiKit.Style; +_704=d.getElement(_704); +if(!_704.firstChild){ +throw new Error("MochiKit.Visual.slideDown must be used on a element with a child"); +} +d.removeEmptyTextNodes(_704); +var _709=s.getStyle(_704.firstChild,"bottom")||0; +var _70a=s.getElementDimensions(_704,true); +var _70b; +_705=b.update({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:_70a.h,originalWidth:_70a.w},restoreAfterFinish:true,afterSetupInternal:function(_70c){ +s.makePositioned(_70c.element); +s.makePositioned(_70c.element.firstChild); +if(/Opera/.test(navigator.userAgent)){ +s.setStyle(_70c.element,{top:""}); +} +_70b=s.makeClipping(_70c.element); +s.setStyle(_70c.element,{height:"0px"}); +s.showElement(_70c.element); +},afterUpdateInternal:function(_70d){ +var _70e=s.getElementDimensions(_70d.element,true); +s.setStyle(_70d.element.firstChild,{bottom:(_70d.dims[0]-_70e.h)+"px"}); +},afterFinishInternal:function(_70f){ +s.undoClipping(_70f.element,_70b); +if(/MSIE/.test(navigator.userAgent)){ +s.undoPositioned(_70f.element); +s.undoPositioned(_70f.element.firstChild); +}else{ +s.undoPositioned(_70f.element.firstChild); +s.undoPositioned(_70f.element); +} +s.setStyle(_70f.element.firstChild,{bottom:_709}); +}},_705); +return new MochiKit.Visual.Scale(_704,100,_705); +}; +MochiKit.Visual.slideUp=function(_710,_711){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +var s=MochiKit.Style; +_710=d.getElement(_710); +if(!_710.firstChild){ +throw new Error("MochiKit.Visual.slideUp must be used on a element with a child"); +} +d.removeEmptyTextNodes(_710); +var _715=s.getStyle(_710.firstChild,"bottom"); +var _716=s.getElementDimensions(_710,true); +var _717; +_711=b.update({scaleContent:false,scaleX:false,scaleMode:{originalHeight:_716.h,originalWidth:_716.w},scaleFrom:100,restoreAfterFinish:true,beforeStartInternal:function(_718){ +s.makePositioned(_718.element); +s.makePositioned(_718.element.firstChild); +if(/Opera/.test(navigator.userAgent)){ +s.setStyle(_718.element,{top:""}); +} +_717=s.makeClipping(_718.element); +s.showElement(_718.element); +},afterUpdateInternal:function(_719){ +var _71a=s.getElementDimensions(_719.element,true); +s.setStyle(_719.element.firstChild,{bottom:(_719.dims[0]-_71a.h)+"px"}); +},afterFinishInternal:function(_71b){ +s.hideElement(_71b.element); +s.undoClipping(_71b.element,_717); +s.undoPositioned(_71b.element.firstChild); +s.undoPositioned(_71b.element); +s.setStyle(_71b.element.firstChild,{bottom:_715}); +}},_711); +return new MochiKit.Visual.Scale(_710,0,_711); +}; +MochiKit.Visual.squish=function(_71c,_71d){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +var s=MochiKit.Style; +var _721=s.getElementDimensions(_71c,true); +var _722; +_71d=b.update({restoreAfterFinish:true,scaleMode:{originalHeight:_721.w,originalWidth:_721.h},beforeSetupInternal:function(_723){ +_722=s.makeClipping(_723.element); +},afterFinishInternal:function(_724){ +s.hideElement(_724.element); +s.undoClipping(_724.element,_722); +}},_71d); +return new MochiKit.Visual.Scale(_71c,/Opera/.test(navigator.userAgent)?1:0,_71d); +}; +MochiKit.Visual.grow=function(_725,_726){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_725=d.getElement(_725); +_726=MochiKit.Base.update({direction:"center",moveTransition:v.Transitions.sinoidal,scaleTransition:v.Transitions.sinoidal,opacityTransition:v.Transitions.full,scaleContent:true,scaleFromCenter:false},_726); +var _72a={top:_725.style.top,left:_725.style.left,height:_725.style.height,width:_725.style.width,opacity:s.getStyle(_725,"opacity")}; +var dims=s.getElementDimensions(_725,true); +var _72c,_72d; +var _72e,_72f; +switch(_726.direction){ +case "top-left": +_72c=_72d=_72e=_72f=0; +break; +case "top-right": +_72c=dims.w; +_72d=_72f=0; +_72e=-dims.w; +break; +case "bottom-left": +_72c=_72e=0; +_72d=dims.h; +_72f=-dims.h; +break; +case "bottom-right": +_72c=dims.w; +_72d=dims.h; +_72e=-dims.w; +_72f=-dims.h; +break; +case "center": +_72c=dims.w/2; +_72d=dims.h/2; +_72e=-dims.w/2; +_72f=-dims.h/2; +break; +} +var _730=MochiKit.Base.update({beforeSetupInternal:function(_731){ +s.setStyle(_731.effects[0].element,{height:"0px"}); +s.showElement(_731.effects[0].element); +},afterFinishInternal:function(_732){ +s.undoClipping(_732.effects[0].element); +s.undoPositioned(_732.effects[0].element); +s.setStyle(_732.effects[0].element,_72a); +}},_726); +return new v.Move(_725,{x:_72c,y:_72d,duration:0.01,beforeSetupInternal:function(_733){ +s.hideElement(_733.element); +s.makeClipping(_733.element); +s.makePositioned(_733.element); +},afterFinishInternal:function(_734){ +new v.Parallel([new v.Opacity(_734.element,{sync:true,to:1,from:0,transition:_726.opacityTransition}),new v.Move(_734.element,{x:_72e,y:_72f,sync:true,transition:_726.moveTransition}),new v.Scale(_734.element,100,{scaleMode:{originalHeight:dims.h,originalWidth:dims.w},sync:true,scaleFrom:/Opera/.test(navigator.userAgent)?1:0,transition:_726.scaleTransition,scaleContent:_726.scaleContent,scaleFromCenter:_726.scaleFromCenter,restoreAfterFinish:true})],_730); +}}); +}; +MochiKit.Visual.shrink=function(_735,_736){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_735=d.getElement(_735); +_736=MochiKit.Base.update({direction:"center",moveTransition:v.Transitions.sinoidal,scaleTransition:v.Transitions.sinoidal,opacityTransition:v.Transitions.none,scaleContent:true,scaleFromCenter:false},_736); +var _73a={top:_735.style.top,left:_735.style.left,height:_735.style.height,width:_735.style.width,opacity:s.getStyle(_735,"opacity")}; +var dims=s.getElementDimensions(_735,true); +var _73c,_73d; +switch(_736.direction){ +case "top-left": +_73c=_73d=0; +break; +case "top-right": +_73c=dims.w; +_73d=0; +break; +case "bottom-left": +_73c=0; +_73d=dims.h; +break; +case "bottom-right": +_73c=dims.w; +_73d=dims.h; +break; +case "center": +_73c=dims.w/2; +_73d=dims.h/2; +break; +} +var _73e; +var _73f=MochiKit.Base.update({beforeStartInternal:function(_740){ +s.makePositioned(_740.effects[0].element); +_73e=s.makeClipping(_740.effects[0].element); +},afterFinishInternal:function(_741){ +s.hideElement(_741.effects[0].element); +s.undoClipping(_741.effects[0].element,_73e); +s.undoPositioned(_741.effects[0].element); +s.setStyle(_741.effects[0].element,_73a); +}},_736); +return new v.Parallel([new v.Opacity(_735,{sync:true,to:0,from:1,transition:_736.opacityTransition}),new v.Scale(_735,/Opera/.test(navigator.userAgent)?1:0,{scaleMode:{originalHeight:dims.h,originalWidth:dims.w},sync:true,transition:_736.scaleTransition,scaleContent:_736.scaleContent,scaleFromCenter:_736.scaleFromCenter,restoreAfterFinish:true}),new v.Move(_735,{x:_73c,y:_73d,sync:true,transition:_736.moveTransition})],_73f); +}; +MochiKit.Visual.pulsate=function(_742,_743){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var b=MochiKit.Base; +var _747=MochiKit.Style.getStyle(_742,"opacity"); +_743=b.update({duration:3,from:0,afterFinishInternal:function(_748){ +MochiKit.Style.setStyle(_748.element,{"opacity":_747}); +}},_743); +var _749=_743.transition||v.Transitions.sinoidal; +_743.transition=function(pos){ +return _749(1-v.Transitions.pulse(pos,_743.pulses)); +}; +return new v.Opacity(_742,_743); +}; +MochiKit.Visual.fold=function(_74b,_74c){ +var d=MochiKit.DOM; +var v=MochiKit.Visual; +var s=MochiKit.Style; +_74b=d.getElement(_74b); +var _750=s.getElementDimensions(_74b,true); +var _751={top:_74b.style.top,left:_74b.style.left,width:_74b.style.width,height:_74b.style.height}; +var _752=s.makeClipping(_74b); +_74c=MochiKit.Base.update({scaleContent:false,scaleX:false,scaleMode:{originalHeight:_750.h,originalWidth:_750.w},afterFinishInternal:function(_753){ +new v.Scale(_74b,1,{scaleContent:false,scaleY:false,scaleMode:{originalHeight:_750.h,originalWidth:_750.w},afterFinishInternal:function(_754){ +s.hideElement(_754.element); +s.undoClipping(_754.element,_752); +s.setStyle(_754.element,_751); +}}); +}},_74c); +return new v.Scale(_74b,5,_74c); +}; +MochiKit.Visual.Color=MochiKit.Color.Color; +MochiKit.Visual.getElementsComputedStyle=MochiKit.DOM.computedStyle; +MochiKit.Visual.__new__=function(){ +var m=MochiKit.Base; +m.nameFunctions(this); +this.EXPORT_TAGS={":common":this.EXPORT,":all":m.concat(this.EXPORT,this.EXPORT_OK)}; +}; +MochiKit.Visual.EXPORT=["roundElement","roundClass","tagifyText","multiple","toggle","Parallel","Sequence","Opacity","Move","Scale","Highlight","ScrollTo","Morph","fade","appear","puff","blindUp","blindDown","switchOff","dropOut","shake","slideDown","slideUp","squish","grow","shrink","pulsate","fold"]; +MochiKit.Visual.EXPORT_OK=["Base","PAIRS"]; +MochiKit.Visual.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.Visual); +MochiKit.Base._deps("DragAndDrop",["Base","Iter","DOM","Signal","Visual","Position"]); +MochiKit.DragAndDrop.NAME="MochiKit.DragAndDrop"; +MochiKit.DragAndDrop.VERSION="1.4.2"; +MochiKit.DragAndDrop.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.DragAndDrop.toString=function(){ +return this.__repr__(); +}; +MochiKit.DragAndDrop.EXPORT=["Droppable","Draggable"]; +MochiKit.DragAndDrop.EXPORT_OK=["Droppables","Draggables"]; +MochiKit.DragAndDrop.Droppables={drops:[],remove:function(_756){ +this.drops=MochiKit.Base.filter(function(d){ +return d.element!=MochiKit.DOM.getElement(_756); +},this.drops); +},register:function(drop){ +this.drops.push(drop); +},unregister:function(drop){ +this.drops=MochiKit.Base.filter(function(d){ +return d!=drop; +},this.drops); +},prepare:function(_75b){ +MochiKit.Base.map(function(drop){ +if(drop.isAccepted(_75b)){ +if(drop.options.activeclass){ +MochiKit.DOM.addElementClass(drop.element,drop.options.activeclass); +} +drop.options.onactive(drop.element,_75b); +} +},this.drops); +},findDeepestChild:function(_75d){ +deepest=_75d[0]; +for(i=1;i<_75d.length;++i){ +if(MochiKit.DOM.isChildNode(_75d[i].element,deepest.element)){ +deepest=_75d[i]; +} +} +return deepest; +},show:function(_75e,_75f){ +if(!this.drops.length){ +return; +} +var _760=[]; +if(this.last_active){ +this.last_active.deactivate(); +} +MochiKit.Iter.forEach(this.drops,function(drop){ +if(drop.isAffected(_75e,_75f)){ +_760.push(drop); +} +}); +if(_760.length>0){ +drop=this.findDeepestChild(_760); +MochiKit.Position.within(drop.element,_75e.page.x,_75e.page.y); +drop.options.onhover(_75f,drop.element,MochiKit.Position.overlap(drop.options.overlap,drop.element)); +drop.activate(); +} +},fire:function(_762,_763){ +if(!this.last_active){ +return; +} +MochiKit.Position.prepare(); +if(this.last_active.isAffected(_762.mouse(),_763)){ +this.last_active.options.ondrop(_763,this.last_active.element,_762); +} +},reset:function(_764){ +MochiKit.Base.map(function(drop){ +if(drop.options.activeclass){ +MochiKit.DOM.removeElementClass(drop.element,drop.options.activeclass); +} +drop.options.ondesactive(drop.element,_764); +},this.drops); +if(this.last_active){ +this.last_active.deactivate(); +} +}}; +MochiKit.DragAndDrop.Droppable=function(_766,_767){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_766,_767); +} +this.__init__(_766,_767); +}; +MochiKit.DragAndDrop.Droppable.prototype={__class__:MochiKit.DragAndDrop.Droppable,__init__:function(_769,_76a){ +var d=MochiKit.DOM; +var b=MochiKit.Base; +this.element=d.getElement(_769); +this.options=b.update({greedy:true,hoverclass:null,activeclass:null,hoverfunc:b.noop,accept:null,onactive:b.noop,ondesactive:b.noop,onhover:b.noop,ondrop:b.noop,containment:[],tree:false},_76a); +this.options._containers=[]; +b.map(MochiKit.Base.bind(function(c){ +this.options._containers.push(d.getElement(c)); +},this),this.options.containment); +MochiKit.Style.makePositioned(this.element); +MochiKit.DragAndDrop.Droppables.register(this); +},isContained:function(_76e){ +if(this.options._containers.length){ +var _76f; +if(this.options.tree){ +_76f=_76e.treeNode; +}else{ +_76f=_76e.parentNode; +} +return MochiKit.Iter.some(this.options._containers,function(c){ +return _76f==c; +}); +}else{ +return true; +} +},isAccepted:function(_771){ +return ((!this.options.accept)||MochiKit.Iter.some(this.options.accept,function(c){ +return MochiKit.DOM.hasElementClass(_771,c); +})); +},isAffected:function(_773,_774){ +return ((this.element!=_774)&&this.isContained(_774)&&this.isAccepted(_774)&&MochiKit.Position.within(this.element,_773.page.x,_773.page.y)); +},deactivate:function(){ +if(this.options.hoverclass){ +MochiKit.DOM.removeElementClass(this.element,this.options.hoverclass); +} +this.options.hoverfunc(this.element,false); +MochiKit.DragAndDrop.Droppables.last_active=null; +},activate:function(){ +if(this.options.hoverclass){ +MochiKit.DOM.addElementClass(this.element,this.options.hoverclass); +} +this.options.hoverfunc(this.element,true); +MochiKit.DragAndDrop.Droppables.last_active=this; +},destroy:function(){ +MochiKit.DragAndDrop.Droppables.unregister(this); +},repr:function(){ +return "["+this.__class__.NAME+", options:"+MochiKit.Base.repr(this.options)+"]"; +}}; +MochiKit.DragAndDrop.Draggables={drags:[],register:function(_775){ +if(this.drags.length===0){ +var conn=MochiKit.Signal.connect; +this.eventMouseUp=conn(document,"onmouseup",this,this.endDrag); +this.eventMouseMove=conn(document,"onmousemove",this,this.updateDrag); +this.eventKeypress=conn(document,"onkeypress",this,this.keyPress); +} +this.drags.push(_775); +},unregister:function(_777){ +this.drags=MochiKit.Base.filter(function(d){ +return d!=_777; +},this.drags); +if(this.drags.length===0){ +var disc=MochiKit.Signal.disconnect; +disc(this.eventMouseUp); +disc(this.eventMouseMove); +disc(this.eventKeypress); +} +},activate:function(_77a){ +window.focus(); +this.activeDraggable=_77a; +},deactivate:function(){ +this.activeDraggable=null; +},updateDrag:function(_77b){ +if(!this.activeDraggable){ +return; +} +var _77c=_77b.mouse(); +if(this._lastPointer&&(MochiKit.Base.repr(this._lastPointer.page)==MochiKit.Base.repr(_77c.page))){ +return; +} +this._lastPointer=_77c; +this.activeDraggable.updateDrag(_77b,_77c); +},endDrag:function(_77d){ +if(!this.activeDraggable){ +return; +} +this._lastPointer=null; +this.activeDraggable.endDrag(_77d); +this.activeDraggable=null; +},keyPress:function(_77e){ +if(this.activeDraggable){ +this.activeDraggable.keyPress(_77e); +} +},notify:function(_77f,_780,_781){ +MochiKit.Signal.signal(this,_77f,_780,_781); +}}; +MochiKit.DragAndDrop.Draggable=function(_782,_783){ +var cls=arguments.callee; +if(!(this instanceof cls)){ +return new cls(_782,_783); +} +this.__init__(_782,_783); +}; +MochiKit.DragAndDrop.Draggable.prototype={__class__:MochiKit.DragAndDrop.Draggable,__init__:function(_785,_786){ +var v=MochiKit.Visual; +var b=MochiKit.Base; +_786=b.update({handle:false,starteffect:function(_789){ +this._savedOpacity=MochiKit.Style.getStyle(_789,"opacity")||1; +new v.Opacity(_789,{duration:0.2,from:this._savedOpacity,to:0.7}); +},reverteffect:function(_78a,_78b,_78c){ +var dur=Math.sqrt(Math.abs(_78b^2)+Math.abs(_78c^2))*0.02; +return new v.Move(_78a,{x:-_78c,y:-_78b,duration:dur}); +},endeffect:function(_78e){ +new v.Opacity(_78e,{duration:0.2,from:0.7,to:this._savedOpacity}); +},onchange:b.noop,zindex:1000,revert:false,scroll:false,scrollSensitivity:20,scrollSpeed:15,snap:false},_786); +var d=MochiKit.DOM; +this.element=d.getElement(_785); +if(_786.handle&&(typeof (_786.handle)=="string")){ +this.handle=d.getFirstElementByTagAndClassName(null,_786.handle,this.element); +} +if(!this.handle){ +this.handle=d.getElement(_786.handle); +} +if(!this.handle){ +this.handle=this.element; +} +if(_786.scroll&&!_786.scroll.scrollTo&&!_786.scroll.outerHTML){ +_786.scroll=d.getElement(_786.scroll); +this._isScrollChild=MochiKit.DOM.isChildNode(this.element,_786.scroll); +} +MochiKit.Style.makePositioned(this.element); +this.delta=this.currentDelta(); +this.options=_786; +this.dragging=false; +this.eventMouseDown=MochiKit.Signal.connect(this.handle,"onmousedown",this,this.initDrag); +MochiKit.DragAndDrop.Draggables.register(this); +},destroy:function(){ +MochiKit.Signal.disconnect(this.eventMouseDown); +MochiKit.DragAndDrop.Draggables.unregister(this); +},currentDelta:function(){ +var s=MochiKit.Style.getStyle; +return [parseInt(s(this.element,"left")||"0"),parseInt(s(this.element,"top")||"0")]; +},initDrag:function(_791){ +if(!_791.mouse().button.left){ +return; +} +var src=_791.target(); +var _793=(src.tagName||"").toUpperCase(); +if(_793==="INPUT"||_793==="SELECT"||_793==="OPTION"||_793==="BUTTON"||_793==="TEXTAREA"){ +return; +} +if(this._revert){ +this._revert.cancel(); +this._revert=null; +} +var _794=_791.mouse(); +var pos=MochiKit.Position.cumulativeOffset(this.element); +this.offset=[_794.page.x-pos.x,_794.page.y-pos.y]; +MochiKit.DragAndDrop.Draggables.activate(this); +_791.stop(); +},startDrag:function(_796){ +this.dragging=true; +if(this.options.selectclass){ +MochiKit.DOM.addElementClass(this.element,this.options.selectclass); +} +if(this.options.zindex){ +this.originalZ=parseInt(MochiKit.Style.getStyle(this.element,"z-index")||"0"); +this.element.style.zIndex=this.options.zindex; +} +if(this.options.ghosting){ +this._clone=this.element.cloneNode(true); +this.ghostPosition=MochiKit.Position.absolutize(this.element); +this.element.parentNode.insertBefore(this._clone,this.element); +} +if(this.options.scroll){ +if(this.options.scroll==window){ +var _797=this._getWindowScroll(this.options.scroll); +this.originalScrollLeft=_797.left; +this.originalScrollTop=_797.top; +}else{ +this.originalScrollLeft=this.options.scroll.scrollLeft; +this.originalScrollTop=this.options.scroll.scrollTop; +} +} +MochiKit.DragAndDrop.Droppables.prepare(this.element); +MochiKit.DragAndDrop.Draggables.notify("start",this,_796); +if(this.options.starteffect){ +this.options.starteffect(this.element); +} +},updateDrag:function(_798,_799){ +if(!this.dragging){ +this.startDrag(_798); +} +MochiKit.Position.prepare(); +MochiKit.DragAndDrop.Droppables.show(_799,this.element); +MochiKit.DragAndDrop.Draggables.notify("drag",this,_798); +this.draw(_799); +this.options.onchange(this); +if(this.options.scroll){ +this.stopScrolling(); +var p,q; +if(this.options.scroll==window){ +var s=this._getWindowScroll(this.options.scroll); +p=new MochiKit.Style.Coordinates(s.left,s.top); +q=new MochiKit.Style.Coordinates(s.left+s.width,s.top+s.height); +}else{ +p=MochiKit.Position.page(this.options.scroll); +p.x+=this.options.scroll.scrollLeft; +p.y+=this.options.scroll.scrollTop; +p.x+=(window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0); +p.y+=(window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0); +q=new MochiKit.Style.Coordinates(p.x+this.options.scroll.offsetWidth,p.y+this.options.scroll.offsetHeight); +} +var _79d=[0,0]; +if(_799.page.x>(q.x-this.options.scrollSensitivity)){ +_79d[0]=_799.page.x-(q.x-this.options.scrollSensitivity); +}else{ +if(_799.page.x<(p.x+this.options.scrollSensitivity)){ +_79d[0]=_799.page.x-(p.x+this.options.scrollSensitivity); +} +} +if(_799.page.y>(q.y-this.options.scrollSensitivity)){ +_79d[1]=_799.page.y-(q.y-this.options.scrollSensitivity); +}else{ +if(_799.page.y<(p.y+this.options.scrollSensitivity)){ +_79d[1]=_799.page.y-(p.y+this.options.scrollSensitivity); +} +} +this.startScrolling(_79d); +} +if(/AppleWebKit/.test(navigator.appVersion)){ +window.scrollBy(0,0); +} +_798.stop(); +},finishDrag:function(_79e,_79f){ +var dr=MochiKit.DragAndDrop; +this.dragging=false; +if(this.options.selectclass){ +MochiKit.DOM.removeElementClass(this.element,this.options.selectclass); +} +if(this.options.ghosting){ +MochiKit.Position.relativize(this.element,this.ghostPosition); +MochiKit.DOM.removeElement(this._clone); +this._clone=null; +} +if(_79f){ +dr.Droppables.fire(_79e,this.element); +} +dr.Draggables.notify("end",this,_79e); +var _7a1=this.options.revert; +if(_7a1&&typeof (_7a1)=="function"){ +_7a1=_7a1(this.element); +} +var d=this.currentDelta(); +if(_7a1&&this.options.reverteffect){ +this._revert=this.options.reverteffect(this.element,d[1]-this.delta[1],d[0]-this.delta[0]); +}else{ +this.delta=d; +} +if(this.options.zindex){ +this.element.style.zIndex=this.originalZ; +} +if(this.options.endeffect){ +this.options.endeffect(this.element); +} +dr.Draggables.deactivate(); +dr.Droppables.reset(this.element); +},keyPress:function(_7a3){ +if(_7a3.key().string!="KEY_ESCAPE"){ +return; +} +this.finishDrag(_7a3,false); +_7a3.stop(); +},endDrag:function(_7a4){ +if(!this.dragging){ +return; +} +this.stopScrolling(); +this.finishDrag(_7a4,true); +_7a4.stop(); +},draw:function(_7a5){ +var pos=MochiKit.Position.cumulativeOffset(this.element); +var d=this.currentDelta(); +pos.x-=d[0]; +pos.y-=d[1]; +if(this.options.scroll&&(this.options.scroll!=window&&this._isScrollChild)){ +pos.x-=this.options.scroll.scrollLeft-this.originalScrollLeft; +pos.y-=this.options.scroll.scrollTop-this.originalScrollTop; +} +var p=[_7a5.page.x-pos.x-this.offset[0],_7a5.page.y-pos.y-this.offset[1]]; +if(this.options.snap){ +if(typeof (this.options.snap)=="function"){ +p=this.options.snap(p[0],p[1]); +}else{ +if(this.options.snap instanceof Array){ +var i=-1; +p=MochiKit.Base.map(MochiKit.Base.bind(function(v){ +i+=1; +return Math.round(v/this.options.snap[i])*this.options.snap[i]; +},this),p); +}else{ +p=MochiKit.Base.map(MochiKit.Base.bind(function(v){ +return Math.round(v/this.options.snap)*this.options.snap; +},this),p); +} +} +} +var _7ac=this.element.style; +if((!this.options.constraint)||(this.options.constraint=="horizontal")){ +_7ac.left=p[0]+"px"; +} +if((!this.options.constraint)||(this.options.constraint=="vertical")){ +_7ac.top=p[1]+"px"; +} +if(_7ac.visibility=="hidden"){ +_7ac.visibility=""; +} +},stopScrolling:function(){ +if(this.scrollInterval){ +clearInterval(this.scrollInterval); +this.scrollInterval=null; +MochiKit.DragAndDrop.Draggables._lastScrollPointer=null; +} +},startScrolling:function(_7ad){ +if(!_7ad[0]&&!_7ad[1]){ +return; +} +this.scrollSpeed=[_7ad[0]*this.options.scrollSpeed,_7ad[1]*this.options.scrollSpeed]; +this.lastScrolled=new Date(); +this.scrollInterval=setInterval(MochiKit.Base.bind(this.scroll,this),10); +},scroll:function(){ +var _7ae=new Date(); +var _7af=_7ae-this.lastScrolled; +this.lastScrolled=_7ae; +if(this.options.scroll==window){ +var s=this._getWindowScroll(this.options.scroll); +if(this.scrollSpeed[0]||this.scrollSpeed[1]){ +var dm=_7af/1000; +this.options.scroll.scrollTo(s.left+dm*this.scrollSpeed[0],s.top+dm*this.scrollSpeed[1]); +} +}else{ +this.options.scroll.scrollLeft+=this.scrollSpeed[0]*_7af/1000; +this.options.scroll.scrollTop+=this.scrollSpeed[1]*_7af/1000; +} +var d=MochiKit.DragAndDrop; +MochiKit.Position.prepare(); +d.Droppables.show(d.Draggables._lastPointer,this.element); +d.Draggables.notify("drag",this); +if(this._isScrollChild){ +d.Draggables._lastScrollPointer=d.Draggables._lastScrollPointer||d.Draggables._lastPointer; +d.Draggables._lastScrollPointer.x+=this.scrollSpeed[0]*_7af/1000; +d.Draggables._lastScrollPointer.y+=this.scrollSpeed[1]*_7af/1000; +if(d.Draggables._lastScrollPointer.x<0){ +d.Draggables._lastScrollPointer.x=0; +} +if(d.Draggables._lastScrollPointer.y<0){ +d.Draggables._lastScrollPointer.y=0; +} +this.draw(d.Draggables._lastScrollPointer); +} +this.options.onchange(this); +},_getWindowScroll:function(win){ +var vp,w,h; +MochiKit.DOM.withWindow(win,function(){ +vp=MochiKit.Style.getViewportPosition(win.document); +}); +if(win.innerWidth){ +w=win.innerWidth; +h=win.innerHeight; +}else{ +if(win.document.documentElement&&win.document.documentElement.clientWidth){ +w=win.document.documentElement.clientWidth; +h=win.document.documentElement.clientHeight; +}else{ +w=win.document.body.offsetWidth; +h=win.document.body.offsetHeight; +} +} +return {top:vp.y,left:vp.x,width:w,height:h}; +},repr:function(){ +return "["+this.__class__.NAME+", options:"+MochiKit.Base.repr(this.options)+"]"; +}}; +MochiKit.DragAndDrop.__new__=function(){ +MochiKit.Base.nameFunctions(this); +this.EXPORT_TAGS={":common":this.EXPORT,":all":MochiKit.Base.concat(this.EXPORT,this.EXPORT_OK)}; +}; +MochiKit.DragAndDrop.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.DragAndDrop); +MochiKit.Base._deps("Sortable",["Base","Iter","DOM","Position","DragAndDrop"]); +MochiKit.Sortable.NAME="MochiKit.Sortable"; +MochiKit.Sortable.VERSION="1.4.2"; +MochiKit.Sortable.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.Sortable.toString=function(){ +return this.__repr__(); +}; +MochiKit.Sortable.EXPORT=[]; +MochiKit.Sortable.EXPORT_OK=[]; +MochiKit.Base.update(MochiKit.Sortable,{sortables:{},_findRootElement:function(_7b7){ +while(_7b7.tagName.toUpperCase()!="BODY"){ +if(_7b7.id&&MochiKit.Sortable.sortables[_7b7.id]){ +return _7b7; +} +_7b7=_7b7.parentNode; +} +},_createElementId:function(_7b8){ +if(_7b8.id==null||_7b8.id==""){ +var d=MochiKit.DOM; +var id; +var _7bb=1; +while(d.getElement(id="sortable"+_7bb)!=null){ +_7bb+=1; +} +d.setNodeAttribute(_7b8,"id",id); +} +},options:function(_7bc){ +_7bc=MochiKit.Sortable._findRootElement(MochiKit.DOM.getElement(_7bc)); +if(!_7bc){ +return; +} +return MochiKit.Sortable.sortables[_7bc.id]; +},destroy:function(_7bd){ +var s=MochiKit.Sortable.options(_7bd); +var b=MochiKit.Base; +var d=MochiKit.DragAndDrop; +if(s){ +MochiKit.Signal.disconnect(s.startHandle); +MochiKit.Signal.disconnect(s.endHandle); +b.map(function(dr){ +d.Droppables.remove(dr); +},s.droppables); +b.map(function(dr){ +dr.destroy(); +},s.draggables); +delete MochiKit.Sortable.sortables[s.element.id]; +} +},create:function(_7c3,_7c4){ +_7c3=MochiKit.DOM.getElement(_7c3); +var self=MochiKit.Sortable; +self._createElementId(_7c3); +_7c4=MochiKit.Base.update({element:_7c3,tag:"li",dropOnEmpty:false,tree:false,treeTag:"ul",overlap:"vertical",constraint:"vertical",containment:[_7c3],handle:false,only:false,hoverclass:null,ghosting:false,scroll:false,scrollSensitivity:20,scrollSpeed:15,format:/^[^_]*_(.*)$/,onChange:MochiKit.Base.noop,onUpdate:MochiKit.Base.noop,accept:null},_7c4); +self.destroy(_7c3); +var _7c6={revert:true,ghosting:_7c4.ghosting,scroll:_7c4.scroll,scrollSensitivity:_7c4.scrollSensitivity,scrollSpeed:_7c4.scrollSpeed,constraint:_7c4.constraint,handle:_7c4.handle}; +if(_7c4.starteffect){ +_7c6.starteffect=_7c4.starteffect; +} +if(_7c4.reverteffect){ +_7c6.reverteffect=_7c4.reverteffect; +}else{ +if(_7c4.ghosting){ +_7c6.reverteffect=function(_7c7){ +_7c7.style.top=0; +_7c7.style.left=0; +}; +} +} +if(_7c4.endeffect){ +_7c6.endeffect=_7c4.endeffect; +} +if(_7c4.zindex){ +_7c6.zindex=_7c4.zindex; +} +var _7c8={overlap:_7c4.overlap,containment:_7c4.containment,hoverclass:_7c4.hoverclass,onhover:self.onHover,tree:_7c4.tree,accept:_7c4.accept}; +var _7c9={onhover:self.onEmptyHover,overlap:_7c4.overlap,containment:_7c4.containment,hoverclass:_7c4.hoverclass,accept:_7c4.accept}; +MochiKit.DOM.removeEmptyTextNodes(_7c3); +_7c4.draggables=[]; +_7c4.droppables=[]; +if(_7c4.dropOnEmpty||_7c4.tree){ +new MochiKit.DragAndDrop.Droppable(_7c3,_7c9); +_7c4.droppables.push(_7c3); +} +MochiKit.Base.map(function(e){ +var _7cb=_7c4.handle?MochiKit.DOM.getFirstElementByTagAndClassName(null,_7c4.handle,e):e; +_7c4.draggables.push(new MochiKit.DragAndDrop.Draggable(e,MochiKit.Base.update(_7c6,{handle:_7cb}))); +new MochiKit.DragAndDrop.Droppable(e,_7c8); +if(_7c4.tree){ +e.treeNode=_7c3; +} +_7c4.droppables.push(e); +},(self.findElements(_7c3,_7c4)||[])); +if(_7c4.tree){ +MochiKit.Base.map(function(e){ +new MochiKit.DragAndDrop.Droppable(e,_7c9); +e.treeNode=_7c3; +_7c4.droppables.push(e); +},(self.findTreeElements(_7c3,_7c4)||[])); +} +self.sortables[_7c3.id]=_7c4; +_7c4.lastValue=self.serialize(_7c3); +_7c4.startHandle=MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables,"start",MochiKit.Base.partial(self.onStart,_7c3)); +_7c4.endHandle=MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables,"end",MochiKit.Base.partial(self.onEnd,_7c3)); +},onStart:function(_7cd,_7ce){ +var self=MochiKit.Sortable; +var _7d0=self.options(_7cd); +_7d0.lastValue=self.serialize(_7d0.element); +},onEnd:function(_7d1,_7d2){ +var self=MochiKit.Sortable; +self.unmark(); +var _7d4=self.options(_7d1); +if(_7d4.lastValue!=self.serialize(_7d4.element)){ +_7d4.onUpdate(_7d4.element); +} +},findElements:function(_7d5,_7d6){ +return MochiKit.Sortable.findChildren(_7d5,_7d6.only,_7d6.tree,_7d6.tag); +},findTreeElements:function(_7d7,_7d8){ +return MochiKit.Sortable.findChildren(_7d7,_7d8.only,_7d8.tree?true:false,_7d8.treeTag); +},findChildren:function(_7d9,only,_7db,_7dc){ +if(!_7d9.hasChildNodes()){ +return null; +} +_7dc=_7dc.toUpperCase(); +if(only){ +only=MochiKit.Base.flattenArray([only]); +} +var _7dd=[]; +MochiKit.Base.map(function(e){ +if(e.tagName&&e.tagName.toUpperCase()==_7dc&&(!only||MochiKit.Iter.some(only,function(c){ +return MochiKit.DOM.hasElementClass(e,c); +}))){ +_7dd.push(e); +} +if(_7db){ +var _7e0=MochiKit.Sortable.findChildren(e,only,_7db,_7dc); +if(_7e0&&_7e0.length>0){ +_7dd=_7dd.concat(_7e0); +} +} +},_7d9.childNodes); +return _7dd; +},onHover:function(_7e1,_7e2,_7e3){ +if(MochiKit.DOM.isChildNode(_7e2,_7e1)){ +return; +} +var self=MochiKit.Sortable; +if(_7e3>0.33&&_7e3<0.66&&self.options(_7e2).tree){ +return; +}else{ +if(_7e3>0.5){ +self.mark(_7e2,"before"); +if(_7e2.previousSibling!=_7e1){ +var _7e5=_7e1.parentNode; +_7e1.style.visibility="hidden"; +_7e2.parentNode.insertBefore(_7e1,_7e2); +if(_7e2.parentNode!=_7e5){ +self.options(_7e5).onChange(_7e1); +} +self.options(_7e2.parentNode).onChange(_7e1); +} +}else{ +self.mark(_7e2,"after"); +var _7e6=_7e2.nextSibling||null; +if(_7e6!=_7e1){ +var _7e5=_7e1.parentNode; +_7e1.style.visibility="hidden"; +_7e2.parentNode.insertBefore(_7e1,_7e6); +if(_7e2.parentNode!=_7e5){ +self.options(_7e5).onChange(_7e1); +} +self.options(_7e2.parentNode).onChange(_7e1); +} +} +} +},_offsetSize:function(_7e7,type){ +if(type=="vertical"||type=="height"){ +return _7e7.offsetHeight; +}else{ +return _7e7.offsetWidth; +} +},onEmptyHover:function(_7e9,_7ea,_7eb){ +var _7ec=_7e9.parentNode; +var self=MochiKit.Sortable; +var _7ee=self.options(_7ea); +if(!MochiKit.DOM.isChildNode(_7ea,_7e9)){ +var _7ef; +var _7f0=self.findElements(_7ea,{tag:_7ee.tag,only:_7ee.only}); +var _7f1=null; +if(_7f0){ +var _7f2=self._offsetSize(_7ea,_7ee.overlap)*(1-_7eb); +for(_7ef=0;_7ef<_7f0.length;_7ef+=1){ +if(_7f2-self._offsetSize(_7f0[_7ef],_7ee.overlap)>=0){ +_7f2-=self._offsetSize(_7f0[_7ef],_7ee.overlap); +}else{ +if(_7f2-(self._offsetSize(_7f0[_7ef],_7ee.overlap)/2)>=0){ +_7f1=_7ef+1<_7f0.length?_7f0[_7ef+1]:null; +break; +}else{ +_7f1=_7f0[_7ef]; +break; +} +} +} +} +_7ea.insertBefore(_7e9,_7f1); +self.options(_7ec).onChange(_7e9); +_7ee.onChange(_7e9); +} +},unmark:function(){ +var m=MochiKit.Sortable._marker; +if(m){ +MochiKit.Style.hideElement(m); +} +},mark:function(_7f4,_7f5){ +var d=MochiKit.DOM; +var self=MochiKit.Sortable; +var _7f8=self.options(_7f4.parentNode); +if(_7f8&&!_7f8.ghosting){ +return; +} +if(!self._marker){ +self._marker=d.getElement("dropmarker")||document.createElement("DIV"); +MochiKit.Style.hideElement(self._marker); +d.addElementClass(self._marker,"dropmarker"); +self._marker.style.position="absolute"; +document.getElementsByTagName("body").item(0).appendChild(self._marker); +} +var _7f9=MochiKit.Position.cumulativeOffset(_7f4); +self._marker.style.left=_7f9.x+"px"; +self._marker.style.top=_7f9.y+"px"; +if(_7f5=="after"){ +if(_7f8.overlap=="horizontal"){ +self._marker.style.left=(_7f9.x+_7f4.clientWidth)+"px"; +}else{ +self._marker.style.top=(_7f9.y+_7f4.clientHeight)+"px"; +} +} +MochiKit.Style.showElement(self._marker); +},_tree:function(_7fa,_7fb,_7fc){ +var self=MochiKit.Sortable; +var _7fe=self.findElements(_7fa,_7fb)||[]; +for(var i=0;i<_7fe.length;++i){ +var _800=_7fe[i].id.match(_7fb.format); +if(!_800){ +continue; +} +var _801={id:encodeURIComponent(_800?_800[1]:null),element:_7fa,parent:_7fc,children:[],position:_7fc.children.length,container:self._findChildrenElement(_7fe[i],_7fb.treeTag.toUpperCase())}; +if(_801.container){ +self._tree(_801.container,_7fb,_801); +} +_7fc.children.push(_801); +} +return _7fc; +},_findChildrenElement:function(_802,_803){ +if(_802&&_802.hasChildNodes){ +_803=_803.toUpperCase(); +for(var i=0;i<_802.childNodes.length;++i){ +if(_802.childNodes[i].tagName.toUpperCase()==_803){ +return _802.childNodes[i]; +} +} +} +return null; +},tree:function(_805,_806){ +_805=MochiKit.DOM.getElement(_805); +var _807=MochiKit.Sortable.options(_805); +_806=MochiKit.Base.update({tag:_807.tag,treeTag:_807.treeTag,only:_807.only,name:_805.id,format:_807.format},_806||{}); +var root={id:null,parent:null,children:new Array,container:_805,position:0}; +return MochiKit.Sortable._tree(_805,_806,root); +},setSequence:function(_809,_80a,_80b){ +var self=MochiKit.Sortable; +var b=MochiKit.Base; +_809=MochiKit.DOM.getElement(_809); +_80b=b.update(self.options(_809),_80b||{}); +var _80e={}; +b.map(function(n){ +var m=n.id.match(_80b.format); +if(m){ +_80e[m[1]]=[n,n.parentNode]; +} +n.parentNode.removeChild(n); +},self.findElements(_809,_80b)); +b.map(function(_811){ +var n=_80e[_811]; +if(n){ +n[1].appendChild(n[0]); +delete _80e[_811]; +} +},_80a); +},_constructIndex:function(node){ +var _814=""; +do{ +if(node.id){ +_814="["+node.position+"]"+_814; +} +}while((node=node.parent)!=null); +return _814; +},sequence:function(_815,_816){ +_815=MochiKit.DOM.getElement(_815); +var self=MochiKit.Sortable; +var _816=MochiKit.Base.update(self.options(_815),_816||{}); +return MochiKit.Base.map(function(item){ +return item.id.match(_816.format)?item.id.match(_816.format)[1]:""; +},MochiKit.DOM.getElement(self.findElements(_815,_816)||[])); +},serialize:function(_819,_81a){ +_819=MochiKit.DOM.getElement(_819); +var self=MochiKit.Sortable; +_81a=MochiKit.Base.update(self.options(_819),_81a||{}); +var name=encodeURIComponent(_81a.name||_819.id); +if(_81a.tree){ +return MochiKit.Base.flattenArray(MochiKit.Base.map(function(item){ +return [name+self._constructIndex(item)+"[id]="+encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); +},self.tree(_819,_81a).children)).join("&"); +}else{ +return MochiKit.Base.map(function(item){ +return name+"[]="+encodeURIComponent(item); +},self.sequence(_819,_81a)).join("&"); +} +}}); +MochiKit.Sortable.Sortable=MochiKit.Sortable; +MochiKit.Sortable.__new__=function(){ +MochiKit.Base.nameFunctions(this); +this.EXPORT_TAGS={":common":this.EXPORT,":all":MochiKit.Base.concat(this.EXPORT,this.EXPORT_OK)}; +}; +MochiKit.Sortable.__new__(); +MochiKit.Base._exportSymbols(this,MochiKit.Sortable); +if(typeof (MochiKit)=="undefined"){ +MochiKit={}; +} +if(typeof (MochiKit.MochiKit)=="undefined"){ +MochiKit.MochiKit={}; +} +MochiKit.MochiKit.NAME="MochiKit.MochiKit"; +MochiKit.MochiKit.VERSION="1.4.2"; +MochiKit.MochiKit.__repr__=function(){ +return "["+this.NAME+" "+this.VERSION+"]"; +}; +MochiKit.MochiKit.toString=function(){ +return this.__repr__(); +}; +MochiKit.MochiKit.SUBMODULES=["Base","Iter","Logging","DateTime","Format","Async","DOM","Selector","Style","LoggingPane","Color","Signal","Position","Visual","DragAndDrop","Sortable"]; +if(typeof (JSAN)!="undefined"||typeof (dojo)!="undefined"){ +if(typeof (dojo)!="undefined"){ +dojo.provide("MochiKit.MochiKit"); +(function(lst){ +for(var i=0;i"); +} +} +})(); +} + + diff --git a/paste/evalexception/media/debug.js b/paste/evalexception/media/debug.js new file mode 100644 index 0000000..57f9df3 --- /dev/null +++ b/paste/evalexception/media/debug.js @@ -0,0 +1,161 @@ +function showFrame(anchor) { + var tbid = anchor.getAttribute('tbid'); + var expanded = anchor.expanded; + if (expanded) { + MochiKit.DOM.hideElement(anchor.expandedElement); + anchor.expanded = false; + _swapImage(anchor); + return false; + } + anchor.expanded = true; + if (anchor.expandedElement) { + MochiKit.DOM.showElement(anchor.expandedElement); + _swapImage(anchor); + $('debug_input_'+tbid).focus(); + return false; + } + var url = debug_base + + '/show_frame?tbid=' + tbid + + '&debugcount=' + debug_count; + var d = MochiKit.Async.doSimpleXMLHttpRequest(url); + d.addCallbacks(function (data) { + var el = MochiKit.DOM.DIV({}); + anchor.parentNode.insertBefore(el, anchor.nextSibling); + el.innerHTML = data.responseText; + anchor.expandedElement = el; + _swapImage(anchor); + $('debug_input_'+tbid).focus(); + }, function (error) { + showError(error.req.responseText); + }); + return false; +} + +function _swapImage(anchor) { + var el = anchor.getElementsByTagName('IMG')[0]; + if (anchor.expanded) { + var img = 'minus.jpg'; + } else { + var img = 'plus.jpg'; + } + el.src = debug_base + '/media/' + img; +} + +function submitInput(button, tbid) { + var input = $(button.getAttribute('input-from')); + var output = $(button.getAttribute('output-to')); + var url = debug_base + + '/exec_input'; + var history = input.form.history; + input.historyPosition = 0; + if (! history) { + history = input.form.history = []; + } + history.push(input.value); + var vars = { + tbid: tbid, + debugcount: debug_count, + input: input.value + }; + MochiKit.DOM.showElement(output); + var d = MochiKit.Async.doSimpleXMLHttpRequest(url, vars); + d.addCallbacks(function (data) { + var result = data.responseText; + output.innerHTML += result; + input.value = ''; + input.focus(); + }, function (error) { + showError(error.req.responseText); + }); + return false; +} + +function showError(msg) { + var el = $('error-container'); + if (el.innerHTML) { + el.innerHTML += '
\n' + msg; + } else { + el.innerHTML = msg; + } + MochiKit.DOM.showElement('error-area'); +} + +function clearError() { + var el = $('error-container'); + el.innerHTML = ''; + MochiKit.DOM.hideElement('error-area'); +} + +function expandInput(button) { + var input = button.form.elements.input; + stdops = { + name: 'input', + style: 'width: 100%', + autocomplete: 'off' + }; + if (input.tagName == 'INPUT') { + var newEl = MochiKit.DOM.TEXTAREA(stdops); + var text = 'Contract'; + } else { + stdops['type'] = 'text'; + stdops['onkeypress'] = 'upArrow(this)'; + var newEl = MochiKit.DOM.INPUT(stdops); + var text = 'Expand'; + } + newEl.value = input.value; + newEl.id = input.id; + MochiKit.DOM.swapDOM(input, newEl); + newEl.focus(); + button.value = text; + return false; +} + +function upArrow(input, event) { + if (window.event) { + event = window.event; + } + if (event.keyCode != 38 && event.keyCode != 40) { + // not an up- or down-arrow + return true; + } + var dir = event.keyCode == 38 ? 1 : -1; + var history = input.form.history; + if (! history) { + history = input.form.history = []; + } + var pos = input.historyPosition || 0; + if (! pos && dir == -1) { + return true; + } + if (! pos && input.value) { + history.push(input.value); + pos = 1; + } + pos += dir; + if (history.length-pos < 0) { + pos = 1; + } + if (history.length-pos > history.length-1) { + input.value = ''; + return true; + } + input.historyPosition = pos; + var line = history[history.length-pos]; + input.value = line; +} + +function expandLong(anchor) { + var span = anchor; + while (span) { + if (span.style && span.style.display == 'none') { + break; + } + span = span.nextSibling; + } + if (! span) { + return false; + } + MochiKit.DOM.showElement(span); + MochiKit.DOM.hideElement(anchor); + return false; +} diff --git a/paste/evalexception/media/minus.jpg b/paste/evalexception/media/minus.jpg new file mode 100644 index 0000000..05f3306 Binary files /dev/null and b/paste/evalexception/media/minus.jpg differ diff --git a/paste/evalexception/media/plus.jpg b/paste/evalexception/media/plus.jpg new file mode 100644 index 0000000..a17aa5e Binary files /dev/null and b/paste/evalexception/media/plus.jpg differ diff --git a/paste/evalexception/middleware.py b/paste/evalexception/middleware.py new file mode 100644 index 0000000..481d498 --- /dev/null +++ b/paste/evalexception/middleware.py @@ -0,0 +1,613 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Exception-catching middleware that allows interactive debugging. + +This middleware catches all unexpected exceptions. A normal +traceback, like produced by +``paste.exceptions.errormiddleware.ErrorMiddleware`` is given, plus +controls to see local variables and evaluate expressions in a local +context. + +This can only be used in single-process environments, because +subsequent requests must go back to the same process that the +exception originally occurred in. Threaded or non-concurrent +environments both work. + +This shouldn't be used in production in any way. That would just be +silly. + +If calling from an XMLHttpRequest call, if the GET variable ``_`` is +given then it will make the response more compact (and less +Javascripty), since if you use innerHTML it'll kill your browser. You +can look for the header X-Debug-URL in your 500 responses if you want +to see the full debuggable traceback. Also, this URL is printed to +``wsgi.errors``, so you can open it up in another browser window. +""" + +from __future__ import print_function + +import sys +import os +import cgi +import traceback +import six +from six.moves import cStringIO as StringIO +import pprint +import itertools +import time +import re +from paste.exceptions import errormiddleware, formatter, collector +from paste import wsgilib +from paste import urlparser +from paste import httpexceptions +from paste import registry +from paste import request +from paste import response +from paste.evalexception import evalcontext + +limit = 200 + +def html_quote(v): + """ + Escape HTML characters, plus translate None to '' + """ + if v is None: + return '' + return cgi.escape(str(v), 1) + +def preserve_whitespace(v, quote=True): + """ + Quote a value for HTML, preserving whitespace (translating + newlines to ``
`` and multiple spaces to use `` ``). + + If ``quote`` is true, then the value will be HTML quoted first. + """ + if quote: + v = html_quote(v) + v = v.replace('\n', '
\n') + v = re.sub(r'()( +)', _repl_nbsp, v) + v = re.sub(r'(\n)( +)', _repl_nbsp, v) + v = re.sub(r'^()( +)', _repl_nbsp, v) + return '%s' % v + +def _repl_nbsp(match): + if len(match.group(2)) == 1: + return ' ' + return match.group(1) + ' ' * (len(match.group(2))-1) + ' ' + +def simplecatcher(application): + """ + A simple middleware that catches errors and turns them into simple + tracebacks. + """ + def simplecatcher_app(environ, start_response): + try: + return application(environ, start_response) + except: + out = StringIO() + traceback.print_exc(file=out) + start_response('500 Server Error', + [('content-type', 'text/html')], + sys.exc_info()) + res = out.getvalue() + return ['

Error

%s
' + % html_quote(res)] + return simplecatcher_app + +def wsgiapp(): + """ + Turns a function or method into a WSGI application. + """ + def decorator(func): + def wsgiapp_wrapper(*args): + # we get 3 args when this is a method, two when it is + # a function :( + if len(args) == 3: + environ = args[1] + start_response = args[2] + args = [args[0]] + else: + environ, start_response = args + args = [] + def application(environ, start_response): + form = wsgilib.parse_formvars(environ, + include_get_vars=True) + headers = response.HeaderDict( + {'content-type': 'text/html', + 'status': '200 OK'}) + form['environ'] = environ + form['headers'] = headers + res = func(*args, **form.mixed()) + status = headers.pop('status') + start_response(status, headers.headeritems()) + return [res] + app = httpexceptions.make_middleware(application) + app = simplecatcher(app) + return app(environ, start_response) + wsgiapp_wrapper.exposed = True + return wsgiapp_wrapper + return decorator + +def get_debug_info(func): + """ + A decorator (meant to be used under ``wsgiapp()``) that resolves + the ``debugcount`` variable to a ``DebugInfo`` object (or gives an + error if it can't be found). + """ + def debug_info_replacement(self, **form): + try: + if 'debugcount' not in form: + raise ValueError('You must provide a debugcount parameter') + debugcount = form.pop('debugcount') + try: + debugcount = int(debugcount) + except ValueError: + raise ValueError('Bad value for debugcount') + if debugcount not in self.debug_infos: + raise ValueError( + 'Debug %s no longer found (maybe it has expired?)' + % debugcount) + debug_info = self.debug_infos[debugcount] + return func(self, debug_info=debug_info, **form) + except ValueError as e: + form['headers']['status'] = '500 Server Error' + return 'There was an error: %s' % html_quote(e) + return debug_info_replacement + +debug_counter = itertools.count(int(time.time())) +def get_debug_count(environ): + """ + Return the unique debug count for the current request + """ + if 'paste.evalexception.debug_count' in environ: + return environ['paste.evalexception.debug_count'] + else: + environ['paste.evalexception.debug_count'] = next = six.next(debug_counter) + return next + +class EvalException(object): + + def __init__(self, application, global_conf=None, + xmlhttp_key=None): + self.application = application + self.debug_infos = {} + if xmlhttp_key is None: + if global_conf is None: + xmlhttp_key = '_' + else: + xmlhttp_key = global_conf.get('xmlhttp_key', '_') + self.xmlhttp_key = xmlhttp_key + + def __call__(self, environ, start_response): + assert not environ['wsgi.multiprocess'], ( + "The EvalException middleware is not usable in a " + "multi-process environment") + environ['paste.evalexception'] = self + if environ.get('PATH_INFO', '').startswith('/_debug/'): + return self.debug(environ, start_response) + else: + return self.respond(environ, start_response) + + def debug(self, environ, start_response): + assert request.path_info_pop(environ) == '_debug' + next_part = request.path_info_pop(environ) + method = getattr(self, next_part, None) + if not method: + exc = httpexceptions.HTTPNotFound( + '%r not found when parsing %r' + % (next_part, wsgilib.construct_url(environ))) + return exc.wsgi_application(environ, start_response) + if not getattr(method, 'exposed', False): + exc = httpexceptions.HTTPForbidden( + '%r not allowed' % next_part) + return exc.wsgi_application(environ, start_response) + return method(environ, start_response) + + def media(self, environ, start_response): + """ + Static path where images and other files live + """ + app = urlparser.StaticURLParser( + os.path.join(os.path.dirname(__file__), 'media')) + return app(environ, start_response) + media.exposed = True + + def mochikit(self, environ, start_response): + """ + Static path where MochiKit lives + """ + app = urlparser.StaticURLParser( + os.path.join(os.path.dirname(__file__), 'mochikit')) + return app(environ, start_response) + mochikit.exposed = True + + def summary(self, environ, start_response): + """ + Returns a JSON-format summary of all the cached + exception reports + """ + start_response('200 OK', [('Content-type', 'text/x-json')]) + data = []; + items = self.debug_infos.values() + items.sort(lambda a, b: cmp(a.created, b.created)) + data = [item.json() for item in items] + return [repr(data)] + summary.exposed = True + + def view(self, environ, start_response): + """ + View old exception reports + """ + id = int(request.path_info_pop(environ)) + if id not in self.debug_infos: + start_response( + '500 Server Error', + [('Content-type', 'text/html')]) + return [ + "Traceback by id %s does not exist (maybe " + "the server has been restarted?)" + % id] + debug_info = self.debug_infos[id] + return debug_info.wsgi_application(environ, start_response) + view.exposed = True + + def make_view_url(self, environ, base_path, count): + return base_path + '/_debug/view/%s' % count + + #@wsgiapp() + #@get_debug_info + def show_frame(self, tbid, debug_info, **kw): + frame = debug_info.frame(int(tbid)) + vars = frame.tb_frame.f_locals + if vars: + registry.restorer.restoration_begin(debug_info.counter) + local_vars = make_table(vars) + registry.restorer.restoration_end() + else: + local_vars = 'No local vars' + return input_form(tbid, debug_info) + local_vars + + show_frame = wsgiapp()(get_debug_info(show_frame)) + + #@wsgiapp() + #@get_debug_info + def exec_input(self, tbid, debug_info, input, **kw): + if not input.strip(): + return '' + input = input.rstrip() + '\n' + frame = debug_info.frame(int(tbid)) + vars = frame.tb_frame.f_locals + glob_vars = frame.tb_frame.f_globals + context = evalcontext.EvalContext(vars, glob_vars) + registry.restorer.restoration_begin(debug_info.counter) + output = context.exec_expr(input) + registry.restorer.restoration_end() + input_html = formatter.str2html(input) + return ('>>> ' + '%s
\n%s' + % (preserve_whitespace(input_html, quote=False), + preserve_whitespace(output))) + + exec_input = wsgiapp()(get_debug_info(exec_input)) + + def respond(self, environ, start_response): + if environ.get('paste.throw_errors'): + return self.application(environ, start_response) + base_path = request.construct_url(environ, with_path_info=False, + with_query_string=False) + environ['paste.throw_errors'] = True + started = [] + def detect_start_response(status, headers, exc_info=None): + try: + return start_response(status, headers, exc_info) + except: + raise + else: + started.append(True) + try: + __traceback_supplement__ = errormiddleware.Supplement, self, environ + app_iter = self.application(environ, detect_start_response) + try: + return_iter = list(app_iter) + return return_iter + finally: + if hasattr(app_iter, 'close'): + app_iter.close() + except: + exc_info = sys.exc_info() + for expected in environ.get('paste.expected_exceptions', []): + if isinstance(exc_info[1], expected): + raise + + # Tell the Registry to save its StackedObjectProxies current state + # for later restoration + registry.restorer.save_registry_state(environ) + + count = get_debug_count(environ) + view_uri = self.make_view_url(environ, base_path, count) + if not started: + headers = [('content-type', 'text/html')] + headers.append(('X-Debug-URL', view_uri)) + start_response('500 Internal Server Error', + headers, + exc_info) + environ['wsgi.errors'].write('Debug at: %s\n' % view_uri) + + exc_data = collector.collect_exception(*exc_info) + debug_info = DebugInfo(count, exc_info, exc_data, base_path, + environ, view_uri) + assert count not in self.debug_infos + self.debug_infos[count] = debug_info + + if self.xmlhttp_key: + get_vars = wsgilib.parse_querystring(environ) + if dict(get_vars).get(self.xmlhttp_key): + exc_data = collector.collect_exception(*exc_info) + html = formatter.format_html( + exc_data, include_hidden_frames=False, + include_reusable=False, show_extra_data=False) + return [html] + + # @@: it would be nice to deal with bad content types here + return debug_info.content() + + def exception_handler(self, exc_info, environ): + simple_html_error = False + if self.xmlhttp_key: + get_vars = wsgilib.parse_querystring(environ) + if dict(get_vars).get(self.xmlhttp_key): + simple_html_error = True + return errormiddleware.handle_exception( + exc_info, environ['wsgi.errors'], + html=True, + debug_mode=True, + simple_html_error=simple_html_error) + +class DebugInfo(object): + + def __init__(self, counter, exc_info, exc_data, base_path, + environ, view_uri): + self.counter = counter + self.exc_data = exc_data + self.base_path = base_path + self.environ = environ + self.view_uri = view_uri + self.created = time.time() + self.exc_type, self.exc_value, self.tb = exc_info + __exception_formatter__ = 1 + self.frames = [] + n = 0 + tb = self.tb + while tb is not None and (limit is None or n < limit): + if tb.tb_frame.f_locals.get('__exception_formatter__'): + # Stop recursion. @@: should make a fake ExceptionFrame + break + self.frames.append(tb) + tb = tb.tb_next + n += 1 + + def json(self): + """Return the JSON-able representation of this object""" + return { + 'uri': self.view_uri, + 'created': time.strftime('%c', time.gmtime(self.created)), + 'created_timestamp': self.created, + 'exception_type': str(self.exc_type), + 'exception': str(self.exc_value), + } + + def frame(self, tbid): + for frame in self.frames: + if id(frame) == tbid: + return frame + else: + raise ValueError("No frame by id %s found from %r" % (tbid, self.frames)) + + def wsgi_application(self, environ, start_response): + start_response('200 OK', [('content-type', 'text/html')]) + return self.content() + + def content(self): + html = format_eval_html(self.exc_data, self.base_path, self.counter) + head_html = (formatter.error_css + formatter.hide_display_js) + head_html += self.eval_javascript() + repost_button = make_repost_button(self.environ) + page = error_template % { + 'repost_button': repost_button or '', + 'head_html': head_html, + 'body': html} + return [page] + + def eval_javascript(self): + base_path = self.base_path + '/_debug' + return ( + '\n' + '\n' + '\n' + % (base_path, base_path, base_path, self.counter)) + +class EvalHTMLFormatter(formatter.HTMLFormatter): + + def __init__(self, base_path, counter, **kw): + super(EvalHTMLFormatter, self).__init__(**kw) + self.base_path = base_path + self.counter = counter + + def format_source_line(self, filename, frame): + line = formatter.HTMLFormatter.format_source_line( + self, filename, frame) + return (line + + '     ' + '    ' + % (frame.tbid, self.base_path)) + +def make_table(items): + if isinstance(items, dict): + items = items.items() + items.sort() + rows = [] + i = 0 + for name, value in items: + i += 1 + out = StringIO() + try: + pprint.pprint(value, out) + except Exception as e: + print('Error: %s' % e, file=out) + value = html_quote(out.getvalue()) + if len(value) > 100: + # @@: This can actually break the HTML :( + # should I truncate before quoting? + orig_value = value + value = value[:100] + value += '...' + value += '%s' % orig_value[100:] + value = formatter.make_wrappable(value) + if i % 2: + attr = ' class="even"' + else: + attr = ' class="odd"' + rows.append('' + '%s%s' + % (attr, html_quote(name), + preserve_whitespace(value, quote=False))) + return '%s
' % ( + '\n'.join(rows)) + +def format_eval_html(exc_data, base_path, counter): + short_formatter = EvalHTMLFormatter( + base_path=base_path, + counter=counter, + include_reusable=False) + short_er = short_formatter.format_collected_data(exc_data) + long_formatter = EvalHTMLFormatter( + base_path=base_path, + counter=counter, + show_hidden_frames=True, + show_extra_data=False, + include_reusable=False) + long_er = long_formatter.format_collected_data(exc_data) + text_er = formatter.format_text(exc_data, show_hidden_frames=True) + if short_formatter.filter_frames(exc_data.frames) != \ + long_formatter.filter_frames(exc_data.frames): + # Only display the full traceback when it differs from the + # short version + full_traceback_html = """ +
+ +
+ %s +
+ """ % long_er + else: + full_traceback_html = '' + + return """ + %s + %s +
+ +
+ +
+ """ % (short_er, full_traceback_html, cgi.escape(text_er)) + +def make_repost_button(environ): + url = request.construct_url(environ) + if environ['REQUEST_METHOD'] == 'GET': + return ('
' % url) + else: + # @@: I'd like to reconstruct this, but I can't because + # the POST body is probably lost at this point, and + # I can't get it back :( + return None + # @@: Use or lose the following code block + """ + fields = [] + for name, value in wsgilib.parse_formvars( + environ, include_get_vars=False).items(): + if hasattr(value, 'filename'): + # @@: Arg, we'll just submit the body, and leave out + # the filename :( + value = value.value + fields.append( + '' + % (html_quote(name), html_quote(value))) + return ''' +
+%s + +
''' % (url, '\n'.join(fields)) +""" + + +def input_form(tbid, debug_info): + return ''' +
+
+
+ + +
+ ''' % {'tbid': tbid} + +error_template = ''' + + + Server Error + %(head_html)s + + + + + +%(repost_button)s + +%(body)s + + + +''' + +def make_eval_exception(app, global_conf, xmlhttp_key=None): + """ + Wraps the application in an interactive debugger. + + This debugger is a major security hole, and should only be + used during development. + + xmlhttp_key is a string that, if present in QUERY_STRING, + indicates that the request is an XMLHttp request, and the + Javascript/interactive debugger should not be returned. (If you + try to put the debugger somewhere with innerHTML, you will often + crash the browser) + """ + if xmlhttp_key is None: + xmlhttp_key = global_conf.get('xmlhttp_key', '_') + return EvalException(app, xmlhttp_key=xmlhttp_key) diff --git a/paste/exceptions/__init__.py b/paste/exceptions/__init__.py new file mode 100644 index 0000000..813f855 --- /dev/null +++ b/paste/exceptions/__init__.py @@ -0,0 +1,6 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Package for catching exceptions and displaying annotated exception +reports +""" diff --git a/paste/exceptions/collector.py b/paste/exceptions/collector.py new file mode 100644 index 0000000..d6a30db --- /dev/null +++ b/paste/exceptions/collector.py @@ -0,0 +1,523 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +## Originally zExceptions.ExceptionFormatter from Zope; +## Modified by Ian Bicking, Imaginary Landscape, 2005 +""" +An exception collector that finds traceback information plus +supplements +""" + +import sys +import traceback +import time +from six.moves import cStringIO as StringIO +import linecache +from paste.exceptions import serial_number_generator +import warnings + +DEBUG_EXCEPTION_FORMATTER = True +DEBUG_IDENT_PREFIX = 'E-' +FALLBACK_ENCODING = 'UTF-8' + +__all__ = ['collect_exception', 'ExceptionCollector'] + +class ExceptionCollector(object): + + """ + Produces a data structure that can be used by formatters to + display exception reports. + + Magic variables: + + If you define one of these variables in your local scope, you can + add information to tracebacks that happen in that context. This + allows applications to add all sorts of extra information about + the context of the error, including URLs, environmental variables, + users, hostnames, etc. These are the variables we look for: + + ``__traceback_supplement__``: + You can define this locally or globally (unlike all the other + variables, which must be defined locally). + + ``__traceback_supplement__`` is a tuple of ``(factory, arg1, + arg2...)``. When there is an exception, ``factory(arg1, arg2, + ...)`` is called, and the resulting object is inspected for + supplemental information. + + ``__traceback_info__``: + This information is added to the traceback, usually fairly + literally. + + ``__traceback_hide__``: + If set and true, this indicates that the frame should be + hidden from abbreviated tracebacks. This way you can hide + some of the complexity of the larger framework and let the + user focus on their own errors. + + By setting it to ``'before'``, all frames before this one will + be thrown away. By setting it to ``'after'`` then all frames + after this will be thrown away until ``'reset'`` is found. In + each case the frame where it is set is included, unless you + append ``'_and_this'`` to the value (e.g., + ``'before_and_this'``). + + Note that formatters will ignore this entirely if the frame + that contains the error wouldn't normally be shown according + to these rules. + + ``__traceback_reporter__``: + This should be a reporter object (see the reporter module), + or a list/tuple of reporter objects. All reporters found this + way will be given the exception, innermost first. + + ``__traceback_decorator__``: + This object (defined in a local or global scope) will get the + result of this function (the CollectedException defined + below). It may modify this object in place, or return an + entirely new object. This gives the object the ability to + manipulate the traceback arbitrarily. + + The actually interpretation of these values is largely up to the + reporters and formatters. + + ``collect_exception(*sys.exc_info())`` will return an object with + several attributes: + + ``frames``: + A list of frames + ``exception_formatted``: + The formatted exception, generally a full traceback + ``exception_type``: + The type of the exception, like ``ValueError`` + ``exception_value``: + The string value of the exception, like ``'x not in list'`` + ``identification_code``: + A hash of the exception data meant to identify the general + exception, so that it shares this code with other exceptions + that derive from the same problem. The code is a hash of + all the module names and function names in the traceback, + plus exception_type. This should be shown to users so they + can refer to the exception later. (@@: should it include a + portion that allows identification of the specific instance + of the exception as well?) + + The list of frames goes innermost first. Each frame has these + attributes; some values may be None if they could not be + determined. + + ``modname``: + the name of the module + ``filename``: + the filename of the module + ``lineno``: + the line of the error + ``revision``: + the contents of __version__ or __revision__ + ``name``: + the function name + ``supplement``: + an object created from ``__traceback_supplement__`` + ``supplement_exception``: + a simple traceback of any exception ``__traceback_supplement__`` + created + ``traceback_info``: + the str() of any ``__traceback_info__`` variable found in the local + scope (@@: should it str()-ify it or not?) + ``traceback_hide``: + the value of any ``__traceback_hide__`` variable + ``traceback_log``: + the value of any ``__traceback_log__`` variable + + + ``__traceback_supplement__`` is thrown away, but a fixed + set of attributes are captured; each of these attributes is + optional. + + ``object``: + the name of the object being visited + ``source_url``: + the original URL requested + ``line``: + the line of source being executed (for interpreters, like ZPT) + ``column``: + the column of source being executed + ``expression``: + the expression being evaluated (also for interpreters) + ``warnings``: + a list of (string) warnings to be displayed + ``getInfo``: + a function/method that takes no arguments, and returns a string + describing any extra information + ``extraData``: + a function/method that takes no arguments, and returns a + dictionary. The contents of this dictionary will not be + displayed in the context of the traceback, but globally for + the exception. Results will be grouped by the keys in the + dictionaries (which also serve as titles). The keys can also + be tuples of (importance, title); in this case the importance + should be ``important`` (shows up at top), ``normal`` (shows + up somewhere; unspecified), ``supplemental`` (shows up at + bottom), or ``extra`` (shows up hidden or not at all). + + These are used to create an object with attributes of the same + names (``getInfo`` becomes a string attribute, not a method). + ``__traceback_supplement__`` implementations should be careful to + produce values that are relatively static and unlikely to cause + further errors in the reporting system -- any complex + introspection should go in ``getInfo()`` and should ultimately + return a string. + + Note that all attributes are optional, and under certain + circumstances may be None or may not exist at all -- the collector + can only do a best effort, but must avoid creating any exceptions + itself. + + Formatters may want to use ``__traceback_hide__`` as a hint to + hide frames that are part of the 'framework' or underlying system. + There are a variety of rules about special values for this + variables that formatters should be aware of. + + TODO: + + More attributes in __traceback_supplement__? Maybe an attribute + that gives a list of local variables that should also be + collected? Also, attributes that would be explicitly meant for + the entire request, not just a single frame. Right now some of + the fixed set of attributes (e.g., source_url) are meant for this + use, but there's no explicit way for the supplement to indicate + new values, e.g., logged-in user, HTTP referrer, environment, etc. + Also, the attributes that do exist are Zope/Web oriented. + + More information on frames? cgitb, for instance, produces + extensive information on local variables. There exists the + possibility that getting this information may cause side effects, + which can make debugging more difficult; but it also provides + fodder for post-mortem debugging. However, the collector is not + meant to be configurable, but to capture everything it can and let + the formatters be configurable. Maybe this would have to be a + configuration value, or maybe it could be indicated by another + magical variable (which would probably mean 'show all local + variables below this frame') + """ + + show_revisions = 0 + + def __init__(self, limit=None): + self.limit = limit + + def getLimit(self): + limit = self.limit + if limit is None: + limit = getattr(sys, 'tracebacklimit', None) + return limit + + def getRevision(self, globals): + if not self.show_revisions: + return None + revision = globals.get('__revision__', None) + if revision is None: + # Incorrect but commonly used spelling + revision = globals.get('__version__', None) + + if revision is not None: + try: + revision = str(revision).strip() + except: + revision = '???' + return revision + + def collectSupplement(self, supplement, tb): + result = {} + + for name in ('object', 'source_url', 'line', 'column', + 'expression', 'warnings'): + result[name] = getattr(supplement, name, None) + + func = getattr(supplement, 'getInfo', None) + if func: + result['info'] = func() + else: + result['info'] = None + func = getattr(supplement, 'extraData', None) + if func: + result['extra'] = func() + else: + result['extra'] = None + return SupplementaryData(**result) + + def collectLine(self, tb, extra_data): + f = tb.tb_frame + lineno = tb.tb_lineno + co = f.f_code + filename = co.co_filename + name = co.co_name + globals = f.f_globals + locals = f.f_locals + if not hasattr(locals, 'has_key'): + # Something weird about this frame; it's not a real dict + warnings.warn( + "Frame %s has an invalid locals(): %r" % ( + globals.get('__name__', 'unknown'), locals)) + locals = {} + data = {} + data['modname'] = globals.get('__name__', None) + data['filename'] = filename + data['lineno'] = lineno + data['revision'] = self.getRevision(globals) + data['name'] = name + data['tbid'] = id(tb) + + # Output a traceback supplement, if any. + if '__traceback_supplement__' in locals: + # Use the supplement defined in the function. + tbs = locals['__traceback_supplement__'] + elif '__traceback_supplement__' in globals: + # Use the supplement defined in the module. + # This is used by Scripts (Python). + tbs = globals['__traceback_supplement__'] + else: + tbs = None + if tbs is not None: + factory = tbs[0] + args = tbs[1:] + try: + supp = factory(*args) + data['supplement'] = self.collectSupplement(supp, tb) + if data['supplement'].extra: + for key, value in data['supplement'].extra.items(): + extra_data.setdefault(key, []).append(value) + except: + if DEBUG_EXCEPTION_FORMATTER: + out = StringIO() + traceback.print_exc(file=out) + text = out.getvalue() + data['supplement_exception'] = text + # else just swallow the exception. + + try: + tbi = locals.get('__traceback_info__', None) + if tbi is not None: + data['traceback_info'] = str(tbi) + except: + pass + + marker = [] + for name in ('__traceback_hide__', '__traceback_log__', + '__traceback_decorator__'): + try: + tbh = locals.get(name, globals.get(name, marker)) + if tbh is not marker: + data[name[2:-2]] = tbh + except: + pass + + return data + + def collectExceptionOnly(self, etype, value): + return traceback.format_exception_only(etype, value) + + def collectException(self, etype, value, tb, limit=None): + # The next line provides a way to detect recursion. + __exception_formatter__ = 1 + frames = [] + ident_data = [] + traceback_decorators = [] + if limit is None: + limit = self.getLimit() + n = 0 + extra_data = {} + while tb is not None and (limit is None or n < limit): + if tb.tb_frame.f_locals.get('__exception_formatter__'): + # Stop recursion. @@: should make a fake ExceptionFrame + frames.append('(Recursive formatException() stopped)\n') + break + data = self.collectLine(tb, extra_data) + frame = ExceptionFrame(**data) + frames.append(frame) + if frame.traceback_decorator is not None: + traceback_decorators.append(frame.traceback_decorator) + ident_data.append(frame.modname or '?') + ident_data.append(frame.name or '?') + tb = tb.tb_next + n = n + 1 + ident_data.append(str(etype)) + ident = serial_number_generator.hash_identifier( + ' '.join(ident_data), length=5, upper=True, + prefix=DEBUG_IDENT_PREFIX) + + result = CollectedException( + frames=frames, + exception_formatted=self.collectExceptionOnly(etype, value), + exception_type=etype, + exception_value=self.safeStr(value), + identification_code=ident, + date=time.localtime(), + extra_data=extra_data) + if etype is ImportError: + extra_data[('important', 'sys.path')] = [sys.path] + for decorator in traceback_decorators: + try: + new_result = decorator(result) + if new_result is not None: + result = new_result + except: + pass + return result + + def safeStr(self, obj): + try: + return str(obj) + except UnicodeEncodeError: + try: + return unicode(obj).encode(FALLBACK_ENCODING, 'replace') + except UnicodeEncodeError: + # This is when something is really messed up, but this can + # happen when the __str__ of an object has to handle unicode + return repr(obj) + +limit = 200 + +class Bunch(object): + + """ + A generic container + """ + + def __init__(self, **attrs): + for name, value in attrs.items(): + setattr(self, name, value) + + def __repr__(self): + name = '<%s ' % self.__class__.__name__ + name += ' '.join(['%s=%r' % (name, str(value)[:30]) + for name, value in self.__dict__.items() + if not name.startswith('_')]) + return name + '>' + +class CollectedException(Bunch): + """ + This is the result of collection the exception; it contains copies + of data of interest. + """ + # A list of frames (ExceptionFrame instances), innermost last: + frames = [] + # The result of traceback.format_exception_only; this looks + # like a normal traceback you'd see in the interactive interpreter + exception_formatted = None + # The *string* representation of the type of the exception + # (@@: should we give the # actual class? -- we can't keep the + # actual exception around, but the class should be safe) + # Something like 'ValueError' + exception_type = None + # The string representation of the exception, from ``str(e)``. + exception_value = None + # An identifier which should more-or-less classify this particular + # exception, including where in the code it happened. + identification_code = None + # The date, as time.localtime() returns: + date = None + # A dictionary of supplemental data: + extra_data = {} + +class SupplementaryData(Bunch): + """ + The result of __traceback_supplement__. We don't keep the + supplement object around, for fear of GC problems and whatnot. + (@@: Maybe I'm being too superstitious about copying only specific + information over) + """ + + # These attributes are copied from the object, or left as None + # if the object doesn't have these attributes: + object = None + source_url = None + line = None + column = None + expression = None + warnings = None + # This is the *return value* of supplement.getInfo(): + info = None + +class ExceptionFrame(Bunch): + """ + This represents one frame of the exception. Each frame is a + context in the call stack, typically represented by a line + number and module name in the traceback. + """ + + # The name of the module; can be None, especially when the code + # isn't associated with a module. + modname = None + # The filename (@@: when no filename, is it None or '?'?) + filename = None + # Line number + lineno = None + # The value of __revision__ or __version__ -- but only if + # show_revision = True (by defaut it is false). (@@: Why not + # collect this?) + revision = None + # The name of the function with the error (@@: None or '?' when + # unknown?) + name = None + # A SupplementaryData object, if __traceback_supplement__ was found + # (and produced no errors) + supplement = None + # If accessing __traceback_supplement__ causes any error, the + # plain-text traceback is stored here + supplement_exception = None + # The str() of any __traceback_info__ value found + traceback_info = None + # The value of __traceback_hide__ + traceback_hide = False + # The value of __traceback_decorator__ + traceback_decorator = None + # The id() of the traceback scope, can be used to reference the + # scope for use elsewhere + tbid = None + + def get_source_line(self, context=0): + """ + Return the source of the current line of this frame. You + probably want to .strip() it as well, as it is likely to have + leading whitespace. + + If context is given, then that many lines on either side will + also be returned. E.g., context=1 will give 3 lines. + """ + if not self.filename or not self.lineno: + return None + lines = [] + for lineno in range(self.lineno-context, self.lineno+context+1): + lines.append(linecache.getline(self.filename, lineno)) + return ''.join(lines) + +if hasattr(sys, 'tracebacklimit'): + limit = min(limit, sys.tracebacklimit) + +col = ExceptionCollector() + +def collect_exception(t, v, tb, limit=None): + """ + Collection an exception from ``sys.exc_info()``. + + Use like:: + + try: + blah blah + except: + exc_data = collect_exception(*sys.exc_info()) + """ + return col.collectException(t, v, tb, limit=limit) diff --git a/paste/exceptions/errormiddleware.py b/paste/exceptions/errormiddleware.py new file mode 100644 index 0000000..7a0918a --- /dev/null +++ b/paste/exceptions/errormiddleware.py @@ -0,0 +1,458 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +Error handler middleware +""" +import sys +import traceback +import cgi +from six.moves import cStringIO as StringIO +from paste.exceptions import formatter, collector, reporter +from paste import wsgilib +from paste import request + +__all__ = ['ErrorMiddleware', 'handle_exception'] + +class _NoDefault(object): + def __repr__(self): + return '' +NoDefault = _NoDefault() + +class ErrorMiddleware(object): + + """ + Error handling middleware + + Usage:: + + error_catching_wsgi_app = ErrorMiddleware(wsgi_app) + + Settings: + + ``debug``: + If true, then tracebacks will be shown in the browser. + + ``error_email``: + an email address (or list of addresses) to send exception + reports to + + ``error_log``: + a filename to append tracebacks to + + ``show_exceptions_in_wsgi_errors``: + If true, then errors will be printed to ``wsgi.errors`` + (frequently a server error log, or stderr). + + ``from_address``, ``smtp_server``, ``error_subject_prefix``, ``smtp_username``, ``smtp_password``, ``smtp_use_tls``: + variables to control the emailed exception reports + + ``error_message``: + When debug mode is off, the error message to show to users. + + ``xmlhttp_key``: + When this key (default ``_``) is in the request GET variables + (not POST!), expect that this is an XMLHttpRequest, and the + response should be more minimal; it should not be a complete + HTML page. + + Environment Configuration: + + ``paste.throw_errors``: + If this setting in the request environment is true, then this + middleware is disabled. This can be useful in a testing situation + where you don't want errors to be caught and transformed. + + ``paste.expected_exceptions``: + When this middleware encounters an exception listed in this + environment variable and when the ``start_response`` has not + yet occurred, the exception will be re-raised instead of being + caught. This should generally be set by middleware that may + (but probably shouldn't be) installed above this middleware, + and wants to get certain exceptions. Exceptions raised after + ``start_response`` have been called are always caught since + by definition they are no longer expected. + + """ + + def __init__(self, application, global_conf=None, + debug=NoDefault, + error_email=None, + error_log=None, + show_exceptions_in_wsgi_errors=NoDefault, + from_address=None, + smtp_server=None, + smtp_username=None, + smtp_password=None, + smtp_use_tls=False, + error_subject_prefix=None, + error_message=None, + xmlhttp_key=None): + from paste.util import converters + self.application = application + # @@: global_conf should be handled elsewhere in a separate + # function for the entry point + if global_conf is None: + global_conf = {} + if debug is NoDefault: + debug = converters.asbool(global_conf.get('debug')) + if show_exceptions_in_wsgi_errors is NoDefault: + show_exceptions_in_wsgi_errors = converters.asbool(global_conf.get('show_exceptions_in_wsgi_errors')) + self.debug_mode = converters.asbool(debug) + if error_email is None: + error_email = (global_conf.get('error_email') + or global_conf.get('admin_email') + or global_conf.get('webmaster_email') + or global_conf.get('sysadmin_email')) + self.error_email = converters.aslist(error_email) + self.error_log = error_log + self.show_exceptions_in_wsgi_errors = show_exceptions_in_wsgi_errors + if from_address is None: + from_address = global_conf.get('error_from_address', 'errors@localhost') + self.from_address = from_address + if smtp_server is None: + smtp_server = global_conf.get('smtp_server', 'localhost') + self.smtp_server = smtp_server + self.smtp_username = smtp_username or global_conf.get('smtp_username') + self.smtp_password = smtp_password or global_conf.get('smtp_password') + self.smtp_use_tls = smtp_use_tls or converters.asbool(global_conf.get('smtp_use_tls')) + self.error_subject_prefix = error_subject_prefix or '' + if error_message is None: + error_message = global_conf.get('error_message') + self.error_message = error_message + if xmlhttp_key is None: + xmlhttp_key = global_conf.get('xmlhttp_key', '_') + self.xmlhttp_key = xmlhttp_key + + def __call__(self, environ, start_response): + """ + The WSGI application interface. + """ + # We want to be careful about not sending headers twice, + # and the content type that the app has committed to (if there + # is an exception in the iterator body of the response) + if environ.get('paste.throw_errors'): + return self.application(environ, start_response) + environ['paste.throw_errors'] = True + + try: + __traceback_supplement__ = Supplement, self, environ + sr_checker = ResponseStartChecker(start_response) + app_iter = self.application(environ, sr_checker) + return self.make_catching_iter(app_iter, environ, sr_checker) + except: + exc_info = sys.exc_info() + try: + for expect in environ.get('paste.expected_exceptions', []): + if isinstance(exc_info[1], expect): + raise + start_response('500 Internal Server Error', + [('content-type', 'text/html')], + exc_info) + # @@: it would be nice to deal with bad content types here + response = self.exception_handler(exc_info, environ) + return [response] + finally: + # clean up locals... + exc_info = None + + def make_catching_iter(self, app_iter, environ, sr_checker): + if isinstance(app_iter, (list, tuple)): + # These don't raise + return app_iter + return CatchingIter(app_iter, environ, sr_checker, self) + + def exception_handler(self, exc_info, environ): + simple_html_error = False + if self.xmlhttp_key: + get_vars = wsgilib.parse_querystring(environ) + if dict(get_vars).get(self.xmlhttp_key): + simple_html_error = True + return handle_exception( + exc_info, environ['wsgi.errors'], + html=True, + debug_mode=self.debug_mode, + error_email=self.error_email, + error_log=self.error_log, + show_exceptions_in_wsgi_errors=self.show_exceptions_in_wsgi_errors, + error_email_from=self.from_address, + smtp_server=self.smtp_server, + smtp_username=self.smtp_username, + smtp_password=self.smtp_password, + smtp_use_tls=self.smtp_use_tls, + error_subject_prefix=self.error_subject_prefix, + error_message=self.error_message, + simple_html_error=simple_html_error) + +class ResponseStartChecker(object): + def __init__(self, start_response): + self.start_response = start_response + self.response_started = False + + def __call__(self, *args): + self.response_started = True + self.start_response(*args) + +class CatchingIter(object): + + """ + A wrapper around the application iterator that will catch + exceptions raised by the a generator, or by the close method, and + display or report as necessary. + """ + + def __init__(self, app_iter, environ, start_checker, error_middleware): + self.app_iterable = app_iter + self.app_iterator = iter(app_iter) + self.environ = environ + self.start_checker = start_checker + self.error_middleware = error_middleware + self.closed = False + + def __iter__(self): + return self + + def next(self): + __traceback_supplement__ = ( + Supplement, self.error_middleware, self.environ) + if self.closed: + raise StopIteration + try: + return self.app_iterator.next() + except StopIteration: + self.closed = True + close_response = self._close() + if close_response is not None: + return close_response + else: + raise StopIteration + except: + self.closed = True + close_response = self._close() + exc_info = sys.exc_info() + response = self.error_middleware.exception_handler( + exc_info, self.environ) + if close_response is not None: + response += ( + '
Error in .close():
%s' + % close_response) + + if not self.start_checker.response_started: + self.start_checker('500 Internal Server Error', + [('content-type', 'text/html')], + exc_info) + + return response + __next__ = next + + def close(self): + # This should at least print something to stderr if the + # close method fails at this point + if not self.closed: + self._close() + + def _close(self): + """Close and return any error message""" + if not hasattr(self.app_iterable, 'close'): + return None + try: + self.app_iterable.close() + return None + except: + close_response = self.error_middleware.exception_handler( + sys.exc_info(), self.environ) + return close_response + + +class Supplement(object): + + """ + This is a supplement used to display standard WSGI information in + the traceback. + """ + + def __init__(self, middleware, environ): + self.middleware = middleware + self.environ = environ + self.source_url = request.construct_url(environ) + + def extraData(self): + data = {} + cgi_vars = data[('extra', 'CGI Variables')] = {} + wsgi_vars = data[('extra', 'WSGI Variables')] = {} + hide_vars = ['paste.config', 'wsgi.errors', 'wsgi.input', + 'wsgi.multithread', 'wsgi.multiprocess', + 'wsgi.run_once', 'wsgi.version', + 'wsgi.url_scheme'] + for name, value in self.environ.items(): + if name.upper() == name: + if value: + cgi_vars[name] = value + elif name not in hide_vars: + wsgi_vars[name] = value + if self.environ['wsgi.version'] != (1, 0): + wsgi_vars['wsgi.version'] = self.environ['wsgi.version'] + proc_desc = tuple([int(bool(self.environ[key])) + for key in ('wsgi.multiprocess', + 'wsgi.multithread', + 'wsgi.run_once')]) + wsgi_vars['wsgi process'] = self.process_combos[proc_desc] + wsgi_vars['application'] = self.middleware.application + if 'paste.config' in self.environ: + data[('extra', 'Configuration')] = dict(self.environ['paste.config']) + return data + + process_combos = { + # multiprocess, multithread, run_once + (0, 0, 0): 'Non-concurrent server', + (0, 1, 0): 'Multithreaded', + (1, 0, 0): 'Multiprocess', + (1, 1, 0): 'Multi process AND threads (?)', + (0, 0, 1): 'Non-concurrent CGI', + (0, 1, 1): 'Multithread CGI (?)', + (1, 0, 1): 'CGI', + (1, 1, 1): 'Multi thread/process CGI (?)', + } + +def handle_exception(exc_info, error_stream, html=True, + debug_mode=False, + error_email=None, + error_log=None, + show_exceptions_in_wsgi_errors=False, + error_email_from='errors@localhost', + smtp_server='localhost', + smtp_username=None, + smtp_password=None, + smtp_use_tls=False, + error_subject_prefix='', + error_message=None, + simple_html_error=False, + ): + """ + For exception handling outside of a web context + + Use like:: + + import sys + from paste.exceptions.errormiddleware import handle_exception + try: + do stuff + except: + handle_exception( + sys.exc_info(), sys.stderr, html=False, ...other config...) + + If you want to report, but not fully catch the exception, call + ``raise`` after ``handle_exception``, which (when given no argument) + will reraise the exception. + """ + reported = False + exc_data = collector.collect_exception(*exc_info) + extra_data = '' + if error_email: + rep = reporter.EmailReporter( + to_addresses=error_email, + from_address=error_email_from, + smtp_server=smtp_server, + smtp_username=smtp_username, + smtp_password=smtp_password, + smtp_use_tls=smtp_use_tls, + subject_prefix=error_subject_prefix) + rep_err = send_report(rep, exc_data, html=html) + if rep_err: + extra_data += rep_err + else: + reported = True + if error_log: + rep = reporter.LogReporter( + filename=error_log) + rep_err = send_report(rep, exc_data, html=html) + if rep_err: + extra_data += rep_err + else: + reported = True + if show_exceptions_in_wsgi_errors: + rep = reporter.FileReporter( + file=error_stream) + rep_err = send_report(rep, exc_data, html=html) + if rep_err: + extra_data += rep_err + else: + reported = True + else: + error_stream.write('Error - %s: %s\n' % ( + exc_data.exception_type, exc_data.exception_value)) + if html: + if debug_mode and simple_html_error: + return_error = formatter.format_html( + exc_data, include_hidden_frames=False, + include_reusable=False, show_extra_data=False) + reported = True + elif debug_mode and not simple_html_error: + error_html = formatter.format_html( + exc_data, + include_hidden_frames=True, + include_reusable=False) + head_html = formatter.error_css + formatter.hide_display_js + return_error = error_template( + head_html, error_html, extra_data) + extra_data = '' + reported = True + else: + msg = error_message or ''' + An error occurred. See the error logs for more information. + (Turn debug on to display exception reports here) + ''' + return_error = error_template('', msg, '') + else: + return_error = None + if not reported and error_stream: + err_report = formatter.format_text(exc_data, show_hidden_frames=True) + err_report += '\n' + '-'*60 + '\n' + error_stream.write(err_report) + if extra_data: + error_stream.write(extra_data) + return return_error + +def send_report(rep, exc_data, html=True): + try: + rep.report(exc_data) + except: + output = StringIO() + traceback.print_exc(file=output) + if html: + return """ +

Additionally an error occurred while sending the %s report: + +

%s
+

""" % ( + cgi.escape(str(rep)), output.getvalue()) + else: + return ( + "Additionally an error occurred while sending the " + "%s report:\n%s" % (str(rep), output.getvalue())) + else: + return '' + +def error_template(head_html, exception, extra): + return ''' + + + Server Error + %s + + +

Server Error

+ %s + %s + + ''' % (head_html, exception, extra) + +def make_error_middleware(app, global_conf, **kw): + return ErrorMiddleware(app, global_conf=global_conf, **kw) + +doc_lines = ErrorMiddleware.__doc__.splitlines(True) +for i in range(len(doc_lines)): + if doc_lines[i].strip().startswith('Settings'): + make_error_middleware.__doc__ = ''.join(doc_lines[i:]) + break +del i, doc_lines diff --git a/paste/exceptions/formatter.py b/paste/exceptions/formatter.py new file mode 100644 index 0000000..7fa5e7d --- /dev/null +++ b/paste/exceptions/formatter.py @@ -0,0 +1,565 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +Formatters for the exception data that comes from ExceptionCollector. +""" +# @@: TODO: +# Use this: http://www.zope.org/Members/tino/VisualTraceback/VisualTracebackNews + +import cgi +import six +import re +from paste.util import PySourceColor + +def html_quote(s): + return cgi.escape(str(s), True) + +class AbstractFormatter(object): + + general_data_order = ['object', 'source_url'] + + def __init__(self, show_hidden_frames=False, + include_reusable=True, + show_extra_data=True, + trim_source_paths=()): + self.show_hidden_frames = show_hidden_frames + self.trim_source_paths = trim_source_paths + self.include_reusable = include_reusable + self.show_extra_data = show_extra_data + + def format_collected_data(self, exc_data): + general_data = {} + if self.show_extra_data: + for name, value_list in exc_data.extra_data.items(): + if isinstance(name, tuple): + importance, title = name + else: + importance, title = 'normal', name + for value in value_list: + general_data[(importance, name)] = self.format_extra_data( + importance, title, value) + lines = [] + frames = self.filter_frames(exc_data.frames) + for frame in frames: + sup = frame.supplement + if sup: + if sup.object: + general_data[('important', 'object')] = self.format_sup_object( + sup.object) + if sup.source_url: + general_data[('important', 'source_url')] = self.format_sup_url( + sup.source_url) + if sup.line: + lines.append(self.format_sup_line_pos(sup.line, sup.column)) + if sup.expression: + lines.append(self.format_sup_expression(sup.expression)) + if sup.warnings: + for warning in sup.warnings: + lines.append(self.format_sup_warning(warning)) + if sup.info: + lines.extend(self.format_sup_info(sup.info)) + if frame.supplement_exception: + lines.append('Exception in supplement:') + lines.append(self.quote_long(frame.supplement_exception)) + if frame.traceback_info: + lines.append(self.format_traceback_info(frame.traceback_info)) + filename = frame.filename + if filename and self.trim_source_paths: + for path, repl in self.trim_source_paths: + if filename.startswith(path): + filename = repl + filename[len(path):] + break + lines.append(self.format_source_line(filename or '?', frame)) + source = frame.get_source_line() + long_source = frame.get_source_line(2) + if source: + lines.append(self.format_long_source( + source, long_source)) + etype = exc_data.exception_type + if not isinstance(etype, six.string_types): + etype = etype.__name__ + exc_info = self.format_exception_info( + etype, + exc_data.exception_value) + data_by_importance = {'important': [], 'normal': [], + 'supplemental': [], 'extra': []} + for (importance, name), value in general_data.items(): + data_by_importance[importance].append( + (name, value)) + for value in data_by_importance.values(): + value.sort() + return self.format_combine(data_by_importance, lines, exc_info) + + def filter_frames(self, frames): + """ + Removes any frames that should be hidden, according to the + values of traceback_hide, self.show_hidden_frames, and the + hidden status of the final frame. + """ + if self.show_hidden_frames: + return frames + new_frames = [] + hidden = False + for frame in frames: + hide = frame.traceback_hide + # @@: It would be nice to signal a warning if an unknown + # hide string was used, but I'm not sure where to put + # that warning. + if hide == 'before': + new_frames = [] + hidden = False + elif hide == 'before_and_this': + new_frames = [] + hidden = False + continue + elif hide == 'reset': + hidden = False + elif hide == 'reset_and_this': + hidden = False + continue + elif hide == 'after': + hidden = True + elif hide == 'after_and_this': + hidden = True + continue + elif hide: + continue + elif hidden: + continue + new_frames.append(frame) + if frames[-1] not in new_frames: + # We must include the last frame; that we don't indicates + # that the error happened where something was "hidden", + # so we just have to show everything + return frames + return new_frames + + def pretty_string_repr(self, s): + """ + Formats the string as a triple-quoted string when it contains + newlines. + """ + if '\n' in s: + s = repr(s) + s = s[0]*3 + s[1:-1] + s[-1]*3 + s = s.replace('\\n', '\n') + return s + else: + return repr(s) + + def long_item_list(self, lst): + """ + Returns true if the list contains items that are long, and should + be more nicely formatted. + """ + how_many = 0 + for item in lst: + if len(repr(item)) > 40: + how_many += 1 + if how_many >= 3: + return True + return False + +class TextFormatter(AbstractFormatter): + + def quote(self, s): + return s + def quote_long(self, s): + return s + def emphasize(self, s): + return s + def format_sup_object(self, obj): + return 'In object: %s' % self.emphasize(self.quote(repr(obj))) + def format_sup_url(self, url): + return 'URL: %s' % self.quote(url) + def format_sup_line_pos(self, line, column): + if column: + return self.emphasize('Line %i, Column %i' % (line, column)) + else: + return self.emphasize('Line %i' % line) + def format_sup_expression(self, expr): + return self.emphasize('In expression: %s' % self.quote(expr)) + def format_sup_warning(self, warning): + return 'Warning: %s' % self.quote(warning) + def format_sup_info(self, info): + return [self.quote_long(info)] + def format_source_line(self, filename, frame): + return 'File %r, line %s in %s' % ( + filename, frame.lineno or '?', frame.name or '?') + def format_long_source(self, source, long_source): + return self.format_source(source) + def format_source(self, source_line): + return ' ' + self.quote(source_line.strip()) + def format_exception_info(self, etype, evalue): + return self.emphasize( + '%s: %s' % (self.quote(etype), self.quote(evalue))) + def format_traceback_info(self, info): + return info + + def format_combine(self, data_by_importance, lines, exc_info): + lines[:0] = [value for n, value in data_by_importance['important']] + lines.append(exc_info) + for name in 'normal', 'supplemental', 'extra': + lines.extend([value for n, value in data_by_importance[name]]) + return self.format_combine_lines(lines) + + def format_combine_lines(self, lines): + return '\n'.join(lines) + + def format_extra_data(self, importance, title, value): + if isinstance(value, str): + s = self.pretty_string_repr(value) + if '\n' in s: + return '%s:\n%s' % (title, s) + else: + return '%s: %s' % (title, s) + elif isinstance(value, dict): + lines = ['\n', title, '-'*len(title)] + items = value.items() + items.sort() + for n, v in items: + try: + v = repr(v) + except Exception as e: + v = 'Cannot display: %s' % e + v = truncate(v) + lines.append(' %s: %s' % (n, v)) + return '\n'.join(lines) + elif (isinstance(value, (list, tuple)) + and self.long_item_list(value)): + parts = [truncate(repr(v)) for v in value] + return '%s: [\n %s]' % ( + title, ',\n '.join(parts)) + else: + return '%s: %s' % (title, truncate(repr(value))) + +class HTMLFormatter(TextFormatter): + + def quote(self, s): + return html_quote(s) + def quote_long(self, s): + return '
%s
' % self.quote(s) + def emphasize(self, s): + return '%s' % s + def format_sup_url(self, url): + return 'URL: %s' % (url, url) + def format_combine_lines(self, lines): + return '
\n'.join(lines) + def format_source_line(self, filename, frame): + name = self.quote(frame.name or '?') + return 'Module %s:%s in %s' % ( + filename, frame.modname or '?', frame.lineno or '?', + name) + return 'File %r, line %s in %s' % ( + filename, frame.lineno, name) + def format_long_source(self, source, long_source): + q_long_source = str2html(long_source, False, 4, True) + q_source = str2html(source, True, 0, False) + return ('' + '>>  %s' + % (q_long_source, + q_source)) + def format_source(self, source_line): + return '  %s' % self.quote(source_line.strip()) + def format_traceback_info(self, info): + return '
%s
' % self.quote(info) + + def format_extra_data(self, importance, title, value): + if isinstance(value, str): + s = self.pretty_string_repr(value) + if '\n' in s: + return '%s:
%s
' % (title, self.quote(s)) + else: + return '%s: %s' % (title, self.quote(s)) + elif isinstance(value, dict): + return self.zebra_table(title, value) + elif (isinstance(value, (list, tuple)) + and self.long_item_list(value)): + return '%s: [
\n    %s]
' % ( + title, ',
    '.join(map(self.quote, map(repr, value)))) + else: + return '%s: %s' % (title, self.quote(repr(value))) + + def format_combine(self, data_by_importance, lines, exc_info): + lines[:0] = [value for n, value in data_by_importance['important']] + lines.append(exc_info) + for name in 'normal', 'supplemental': + lines.extend([value for n, value in data_by_importance[name]]) + if data_by_importance['extra']: + lines.append( + '\n' + + '
\n') + lines.extend([value for n, value in data_by_importance['extra']]) + lines.append('
') + text = self.format_combine_lines(lines) + if self.include_reusable: + return error_css + hide_display_js + text + else: + # Usually because another error is already on this page, + # and so the js & CSS are unneeded + return text + + def zebra_table(self, title, rows, table_class="variables"): + if isinstance(rows, dict): + rows = rows.items() + rows.sort() + table = ['' % table_class, + '' + % self.quote(title)] + odd = False + for name, value in rows: + try: + value = repr(value) + except Exception as e: + value = 'Cannot print: %s' % e + odd = not odd + table.append( + '' + % (odd and 'odd' or 'even', self.quote(name))) + table.append( + '' + % make_wrappable(self.quote(truncate(value)))) + table.append('
%s
%s%s
') + return '\n'.join(table) + +hide_display_js = r''' +''' + + +error_css = """ + +""" + +def format_html(exc_data, include_hidden_frames=False, **ops): + if not include_hidden_frames: + return HTMLFormatter(**ops).format_collected_data(exc_data) + short_er = format_html(exc_data, show_hidden_frames=False, **ops) + # @@: This should have a way of seeing if the previous traceback + # was actually trimmed at all + ops['include_reusable'] = False + ops['show_extra_data'] = False + long_er = format_html(exc_data, show_hidden_frames=True, **ops) + text_er = format_text(exc_data, show_hidden_frames=True, **ops) + return """ + %s +
+ +
+ %s +
+
+ +
+ +
+ """ % (short_er, long_er, cgi.escape(text_er)) + +def format_text(exc_data, **ops): + return TextFormatter(**ops).format_collected_data(exc_data) + +whitespace_re = re.compile(r' +') +pre_re = re.compile(r'') +error_re = re.compile(r'

ERROR: .*?

') + +def str2html(src, strip=False, indent_subsequent=0, + highlight_inner=False): + """ + Convert a string to HTML. Try to be really safe about it, + returning a quoted version of the string if nothing else works. + """ + try: + return _str2html(src, strip=strip, + indent_subsequent=indent_subsequent, + highlight_inner=highlight_inner) + except: + return html_quote(src) + +def _str2html(src, strip=False, indent_subsequent=0, + highlight_inner=False): + if strip: + src = src.strip() + orig_src = src + try: + src = PySourceColor.str2html(src, form='snip') + src = error_re.sub('', src) + src = pre_re.sub('', src) + src = re.sub(r'^[\n\r]{0,1}', '', src) + src = re.sub(r'[\n\r]{0,1}$', '', src) + except: + src = html_quote(orig_src) + lines = src.splitlines() + if len(lines) == 1: + return lines[0] + indent = ' '*indent_subsequent + for i in range(1, len(lines)): + lines[i] = indent+lines[i] + if highlight_inner and i == len(lines)/2: + lines[i] = '%s' % lines[i] + src = '
\n'.join(lines) + src = whitespace_re.sub( + lambda m: ' '*(len(m.group(0))-1) + ' ', src) + return src + +def truncate(string, limit=1000): + """ + Truncate the string to the limit number of + characters + """ + if len(string) > limit: + return string[:limit-20]+'...'+string[-17:] + else: + return string + +def make_wrappable(html, wrap_limit=60, + split_on=';?&@!$#-/\\"\''): + # Currently using , maybe should use ​ + # http://www.cs.tut.fi/~jkorpela/html/nobr.html + if len(html) <= wrap_limit: + return html + words = html.split() + new_words = [] + for word in words: + wrapped_word = '' + while len(word) > wrap_limit: + for char in split_on: + if char in word: + first, rest = word.split(char, 1) + wrapped_word += first+char+'' + word = rest + break + else: + for i in range(0, len(word), wrap_limit): + wrapped_word += word[i:i+wrap_limit]+'' + word = '' + wrapped_word += word + new_words.append(wrapped_word) + return ' '.join(new_words) + +def make_pre_wrappable(html, wrap_limit=60, + split_on=';?&@!$#-/\\"\''): + """ + Like ``make_wrappable()`` but intended for text that will + go in a ``
`` block, so wrap on a line-by-line basis.
+    """
+    lines = html.splitlines()
+    new_lines = []
+    for line in lines:
+        if len(line) > wrap_limit:
+            for char in split_on:
+                if char in line:
+                    parts = line.split(char)
+                    line = ''.join(parts)
+                    break
+        new_lines.append(line)
+    return '\n'.join(lines)
diff --git a/paste/exceptions/reporter.py b/paste/exceptions/reporter.py
new file mode 100644
index 0000000..7c0c266
--- /dev/null
+++ b/paste/exceptions/reporter.py
@@ -0,0 +1,141 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+import smtplib
+import time
+try:
+    from socket import sslerror
+except ImportError:
+    sslerror = None
+from paste.exceptions import formatter
+
+class Reporter(object):
+
+    def __init__(self, **conf):
+        for name, value in conf.items():
+            if not hasattr(self, name):
+                raise TypeError(
+                    "The keyword argument %s was not expected"
+                    % name)
+            setattr(self, name, value)
+        self.check_params()
+
+    def check_params(self):
+        pass
+
+    def format_date(self, exc_data):
+        return time.strftime('%c', exc_data.date)
+
+    def format_html(self, exc_data, **kw):
+        return formatter.format_html(exc_data, **kw)
+
+    def format_text(self, exc_data, **kw):
+        return formatter.format_text(exc_data, **kw)
+
+class EmailReporter(Reporter):
+
+    to_addresses = None
+    from_address = None
+    smtp_server = 'localhost'
+    smtp_username = None
+    smtp_password = None
+    smtp_use_tls = False
+    subject_prefix = ''
+
+    def report(self, exc_data):
+        msg = self.assemble_email(exc_data)
+        server = smtplib.SMTP(self.smtp_server)
+        if self.smtp_use_tls:
+            server.ehlo()
+            server.starttls()
+            server.ehlo()
+        if self.smtp_username and self.smtp_password:
+            server.login(self.smtp_username, self.smtp_password)
+        server.sendmail(self.from_address,
+                        self.to_addresses, msg.as_string())
+        try:
+            server.quit()
+        except sslerror:
+            # sslerror is raised in tls connections on closing sometimes
+            pass
+
+    def check_params(self):
+        if not self.to_addresses:
+            raise ValueError("You must set to_addresses")
+        if not self.from_address:
+            raise ValueError("You must set from_address")
+        if isinstance(self.to_addresses, (str, unicode)):
+            self.to_addresses = [self.to_addresses]
+
+    def assemble_email(self, exc_data):
+        short_html_version = self.format_html(
+            exc_data, show_hidden_frames=False)
+        long_html_version = self.format_html(
+            exc_data, show_hidden_frames=True)
+        text_version = self.format_text(
+            exc_data, show_hidden_frames=False)
+        msg = MIMEMultipart()
+        msg.set_type('multipart/alternative')
+        msg.preamble = msg.epilogue = ''
+        text_msg = MIMEText(text_version)
+        text_msg.set_type('text/plain')
+        text_msg.set_param('charset', 'ASCII')
+        msg.attach(text_msg)
+        html_msg = MIMEText(short_html_version)
+        html_msg.set_type('text/html')
+        # @@: Correct character set?
+        html_msg.set_param('charset', 'UTF-8')
+        html_long = MIMEText(long_html_version)
+        html_long.set_type('text/html')
+        html_long.set_param('charset', 'UTF-8')
+        msg.attach(html_msg)
+        msg.attach(html_long)
+        subject = '%s: %s' % (exc_data.exception_type,
+                              formatter.truncate(str(exc_data.exception_value)))
+        msg['Subject'] = self.subject_prefix + subject
+        msg['From'] = self.from_address
+        msg['To'] = ', '.join(self.to_addresses)
+        return msg
+
+class LogReporter(Reporter):
+
+    filename = None
+    show_hidden_frames = True
+
+    def check_params(self):
+        assert self.filename is not None, (
+            "You must give a filename")
+
+    def report(self, exc_data):
+        text = self.format_text(
+            exc_data, show_hidden_frames=self.show_hidden_frames)
+        f = open(self.filename, 'a')
+        try:
+            f.write(text + '\n' + '-'*60 + '\n')
+        finally:
+            f.close()
+
+class FileReporter(Reporter):
+
+    file = None
+    show_hidden_frames = True
+
+    def check_params(self):
+        assert self.file is not None, (
+            "You must give a file object")
+
+    def report(self, exc_data):
+        text = self.format_text(
+            exc_data, show_hidden_frames=self.show_hidden_frames)
+        self.file.write(text + '\n' + '-'*60 + '\n')
+
+class WSGIAppReporter(Reporter):
+
+    def __init__(self, exc_data):
+        self.exc_data = exc_data
+
+    def __call__(self, environ, start_response):
+        start_response('500 Server Error', [('Content-type', 'text/html')])
+        return [formatter.format_html(self.exc_data)]
diff --git a/paste/exceptions/serial_number_generator.py b/paste/exceptions/serial_number_generator.py
new file mode 100644
index 0000000..d4f6235
--- /dev/null
+++ b/paste/exceptions/serial_number_generator.py
@@ -0,0 +1,125 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+"""
+Creates a human-readable identifier, using numbers and digits,
+avoiding ambiguous numbers and letters.  hash_identifier can be used
+to create compact representations that are unique for a certain string
+(or concatenation of strings)
+"""
+
+try:
+    from hashlib import md5
+except ImportError:
+    from md5 import md5
+
+import six
+
+good_characters = "23456789abcdefghjkmnpqrtuvwxyz"
+
+base = len(good_characters)
+
+def make_identifier(number):
+    """
+    Encodes a number as an identifier.
+    """
+    if not isinstance(number, six.integer_types):
+        raise ValueError(
+            "You can only make identifiers out of integers (not %r)"
+            % number)
+    if number < 0:
+        raise ValueError(
+            "You cannot make identifiers out of negative numbers: %r"
+            % number)
+    result = []
+    while number:
+        next = number % base
+        result.append(good_characters[next])
+        # Note, this depends on integer rounding of results:
+        number = number // base
+    return ''.join(result)
+
+def hash_identifier(s, length, pad=True, hasher=md5, prefix='',
+                    group=None, upper=False):
+    """
+    Hashes the string (with the given hashing module), then turns that
+    hash into an identifier of the given length (using modulo to
+    reduce the length of the identifier).  If ``pad`` is False, then
+    the minimum-length identifier will be used; otherwise the
+    identifier will be padded with 0's as necessary.
+
+    ``prefix`` will be added last, and does not count towards the
+    target length.  ``group`` will group the characters with ``-`` in
+    the given lengths, and also does not count towards the target
+    length.  E.g., ``group=4`` will cause a identifier like
+    ``a5f3-hgk3-asdf``.  Grouping occurs before the prefix.
+    """
+    if not callable(hasher):
+        # Accept sha/md5 modules as well as callables
+        hasher = hasher.new
+    if length > 26 and hasher is md5:
+        raise ValueError(
+            "md5 cannot create hashes longer than 26 characters in "
+            "length (you gave %s)" % length)
+    if isinstance(s, six.text_type):
+        s = s.encode('utf-8')
+    h = hasher(six.binary_type(s))
+    bin_hash = h.digest()
+    modulo = base ** length
+    number = 0
+    for c in list(bin_hash):
+        number = (number * 256 + six.byte2int([c])) % modulo
+    ident = make_identifier(number)
+    if pad:
+        ident = good_characters[0]*(length-len(ident)) + ident
+    if group:
+        parts = []
+        while ident:
+            parts.insert(0, ident[-group:])
+            ident = ident[:-group]
+        ident = '-'.join(parts)
+    if upper:
+        ident = ident.upper()
+    return prefix + ident
+
+# doctest tests:
+__test__ = {
+    'make_identifier': """
+    >>> make_identifier(0)
+    ''
+    >>> make_identifier(1000)
+    'c53'
+    >>> make_identifier(-100)
+    Traceback (most recent call last):
+        ...
+    ValueError: You cannot make identifiers out of negative numbers: -100
+    >>> make_identifier('test')
+    Traceback (most recent call last):
+        ...
+    ValueError: You can only make identifiers out of integers (not 'test')
+    >>> make_identifier(1000000000000)
+    'c53x9rqh3'
+    """,
+    'hash_identifier': """
+    >>> hash_identifier(0, 5)
+    'cy2dr'
+    >>> hash_identifier(0, 10)
+    'cy2dr6rg46'
+    >>> hash_identifier('this is a test of a long string', 5)
+    'awatu'
+    >>> hash_identifier(0, 26)
+    'cy2dr6rg46cx8t4w2f3nfexzk4'
+    >>> hash_identifier(0, 30)
+    Traceback (most recent call last):
+        ...
+    ValueError: md5 cannot create hashes longer than 26 characters in length (you gave 30)
+    >>> hash_identifier(0, 10, group=4)
+    'cy-2dr6-rg46'
+    >>> hash_identifier(0, 10, group=4, upper=True, prefix='M-')
+    'M-CY-2DR6-RG46'
+    """}
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
+    
diff --git a/paste/fileapp.py b/paste/fileapp.py
new file mode 100644
index 0000000..3825386
--- /dev/null
+++ b/paste/fileapp.py
@@ -0,0 +1,354 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+# (c) 2005 Ian Bicking, Clark C. Evans and contributors
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+"""
+This module handles sending static content such as in-memory data or
+files.  At this time it has cache helpers and understands the
+if-modified-since request header.
+"""
+
+import os, time, mimetypes, zipfile, tarfile
+from paste.httpexceptions import *
+from paste.httpheaders import *
+
+CACHE_SIZE = 4096
+BLOCK_SIZE = 4096 * 16
+
+__all__ = ['DataApp', 'FileApp', 'DirectoryApp', 'ArchiveStore']
+
+class DataApp(object):
+    """
+    Returns an application that will send content in a single chunk,
+    this application has support for setting cache-control and for
+    responding to conditional (or HEAD) requests.
+
+    Constructor Arguments:
+
+        ``content``     the content being sent to the client
+
+        ``headers``     the headers to send /w the response
+
+        The remaining ``kwargs`` correspond to headers, where the
+        underscore is replaced with a dash.  These values are only
+        added to the headers if they are not already provided; thus,
+        they can be used for default values.  Examples include, but
+        are not limited to:
+
+            ``content_type``
+            ``content_encoding``
+            ``content_location``
+
+    ``cache_control()``
+
+        This method provides validated construction of the ``Cache-Control``
+        header as well as providing for automated filling out of the
+        ``EXPIRES`` header for HTTP/1.0 clients.
+
+    ``set_content()``
+
+        This method provides a mechanism to set the content after the
+        application has been constructed.  This method does things
+        like changing ``Last-Modified`` and ``Content-Length`` headers.
+
+    """
+
+    allowed_methods = ('GET', 'HEAD')
+
+    def __init__(self, content, headers=None, allowed_methods=None,
+                 **kwargs):
+        assert isinstance(headers, (type(None), list))
+        self.expires = None
+        self.content = None
+        self.content_length = None
+        self.last_modified = 0
+        if allowed_methods is not None:
+            self.allowed_methods = allowed_methods
+        self.headers = headers or []
+        for (k, v) in kwargs.items():
+            header = get_header(k)
+            header.update(self.headers, v)
+        ACCEPT_RANGES.update(self.headers, bytes=True)
+        if not CONTENT_TYPE(self.headers):
+            CONTENT_TYPE.update(self.headers)
+        if content is not None:
+            self.set_content(content)
+
+    def cache_control(self, **kwargs):
+        self.expires = CACHE_CONTROL.apply(self.headers, **kwargs) or None
+        return self
+
+    def set_content(self, content, last_modified=None):
+        assert content is not None
+        if last_modified is None:
+            self.last_modified = time.time()
+        else:
+            self.last_modified = last_modified
+        self.content = content
+        self.content_length = len(content)
+        LAST_MODIFIED.update(self.headers, time=self.last_modified)
+        return self
+
+    def content_disposition(self, **kwargs):
+        CONTENT_DISPOSITION.apply(self.headers, **kwargs)
+        return self
+
+    def __call__(self, environ, start_response):
+        method = environ['REQUEST_METHOD'].upper()
+        if method not in self.allowed_methods:
+            exc = HTTPMethodNotAllowed(
+                'You cannot %s a file' % method,
+                headers=[('Allow', ','.join(self.allowed_methods))])
+            return exc(environ, start_response)
+        return self.get(environ, start_response)
+
+    def calculate_etag(self):
+        return '"%s-%s"' % (self.last_modified, self.content_length)
+
+    def get(self, environ, start_response):
+        headers = self.headers[:]
+        current_etag = self.calculate_etag()
+        ETAG.update(headers, current_etag)
+        if self.expires is not None:
+            EXPIRES.update(headers, delta=self.expires)
+
+        try:
+            client_etags = IF_NONE_MATCH.parse(environ)
+            if client_etags:
+                for etag in client_etags:
+                    if etag == current_etag or etag == '*':
+                        # horribly inefficient, n^2 performance, yuck!
+                        for head in list_headers(entity=True):
+                            head.delete(headers)
+                        start_response('304 Not Modified', headers)
+                        return ['']
+        except HTTPBadRequest as exce:
+            return exce.wsgi_application(environ, start_response)
+
+        # If we get If-None-Match and If-Modified-Since, and
+        # If-None-Match doesn't match, then we should not try to
+        # figure out If-Modified-Since (which has 1-second granularity
+        # and just isn't as accurate)
+        if not client_etags:
+            try:
+                client_clock = IF_MODIFIED_SINCE.parse(environ)
+                if client_clock >= int(self.last_modified):
+                    # horribly inefficient, n^2 performance, yuck!
+                    for head in list_headers(entity=True):
+                        head.delete(headers)
+                    start_response('304 Not Modified', headers)
+                    return [''] # empty body
+            except HTTPBadRequest as exce:
+                return exce.wsgi_application(environ, start_response)
+
+        (lower, upper) = (0, self.content_length - 1)
+        range = RANGE.parse(environ)
+        if range and 'bytes' == range[0] and 1 == len(range[1]):
+            (lower, upper) = range[1][0]
+            upper = upper or (self.content_length - 1)
+            if upper >= self.content_length or lower > upper:
+                return HTTPRequestRangeNotSatisfiable((
+                  "Range request was made beyond the end of the content,\r\n"
+                  "which is %s long.\r\n  Range: %s\r\n") % (
+                     self.content_length, RANGE(environ))
+                ).wsgi_application(environ, start_response)
+
+        content_length = upper - lower + 1
+        CONTENT_RANGE.update(headers, first_byte=lower, last_byte=upper,
+                            total_length = self.content_length)
+        CONTENT_LENGTH.update(headers, content_length)
+        if range or content_length != self.content_length:
+            start_response('206 Partial Content', headers)
+        else:
+            start_response('200 OK', headers)
+        if self.content is not None:
+            return [self.content[lower:upper+1]]
+        return (lower, content_length)
+
+class FileApp(DataApp):
+    """
+    Returns an application that will send the file at the given
+    filename.  Adds a mime type based on ``mimetypes.guess_type()``.
+    See DataApp for the arguments beyond ``filename``.
+    """
+
+    def __init__(self, filename, headers=None, **kwargs):
+        self.filename = filename
+        content_type, content_encoding = self.guess_type()
+        if content_type and 'content_type' not in kwargs:
+            kwargs['content_type'] = content_type
+        if content_encoding and 'content_encoding' not in kwargs:
+            kwargs['content_encoding'] = content_encoding
+        DataApp.__init__(self, None, headers, **kwargs)
+
+    def guess_type(self):
+        return mimetypes.guess_type(self.filename)
+
+    def update(self, force=False):
+        stat = os.stat(self.filename)
+        if not force and stat.st_mtime == self.last_modified:
+            return
+        self.last_modified = stat.st_mtime
+        if stat.st_size < CACHE_SIZE:
+            fh = open(self.filename,"rb")
+            self.set_content(fh.read(), stat.st_mtime)
+            fh.close()
+        else:
+            self.content = None
+            self.content_length = stat.st_size
+            # This is updated automatically if self.set_content() is
+            # called
+            LAST_MODIFIED.update(self.headers, time=self.last_modified)
+
+    def get(self, environ, start_response):
+        is_head = environ['REQUEST_METHOD'].upper() == 'HEAD'
+        if 'max-age=0' in CACHE_CONTROL(environ).lower():
+            self.update(force=True) # RFC 2616 13.2.6
+        else:
+            self.update()
+        if not self.content:
+            if not os.path.exists(self.filename):
+                exc = HTTPNotFound(
+                    'The resource does not exist',
+                    comment="No file at %r" % self.filename)
+                return exc(environ, start_response)
+            try:
+                file = open(self.filename, 'rb')
+            except (IOError, OSError) as e:
+                exc = HTTPForbidden(
+                    'You are not permitted to view this file (%s)' % e)
+                return exc.wsgi_application(
+                    environ, start_response)
+        retval = DataApp.get(self, environ, start_response)
+        if isinstance(retval, list):
+            # cached content, exception, or not-modified
+            if is_head:
+                return ['']
+            return retval
+        (lower, content_length) = retval
+        if is_head:
+            return ['']
+        file.seek(lower)
+        file_wrapper = environ.get('wsgi.file_wrapper', None)
+        if file_wrapper:
+            return file_wrapper(file, BLOCK_SIZE)
+        else:
+            return _FileIter(file, size=content_length)
+
+class _FileIter(object):
+
+    def __init__(self, file, block_size=None, size=None):
+        self.file = file
+        self.size = size
+        self.block_size = block_size or BLOCK_SIZE
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        chunk_size = self.block_size
+        if self.size is not None:
+            if chunk_size > self.size:
+                chunk_size = self.size
+            self.size -= chunk_size
+        data = self.file.read(chunk_size)
+        if not data:
+            raise StopIteration
+        return data
+
+    def close(self):
+        self.file.close()
+
+
+class DirectoryApp(object):
+    """
+    Returns an application that dispatches requests to corresponding FileApps based on PATH_INFO.
+    FileApp instances are cached. This app makes sure not to serve any files that are not in a subdirectory.
+    To customize FileApp creation override ``DirectoryApp.make_fileapp``
+    """
+
+    def __init__(self, path):
+        self.path = os.path.abspath(path)
+        if not self.path.endswith(os.path.sep):
+            self.path += os.path.sep
+        assert os.path.isdir(self.path)
+        self.cached_apps = {}
+
+    make_fileapp = FileApp
+
+    def __call__(self, environ, start_response):
+        path_info = environ['PATH_INFO']
+        app = self.cached_apps.get(path_info)
+        if app is None:
+            path = os.path.join(self.path, path_info.lstrip('/'))
+            if not os.path.normpath(path).startswith(self.path):
+                app = HTTPForbidden()
+            elif os.path.isfile(path):
+                app = self.make_fileapp(path)
+                self.cached_apps[path_info] = app
+            else:
+                app = HTTPNotFound(comment=path)
+        return app(environ, start_response)
+
+
+class ArchiveStore(object):
+    """
+    Returns an application that serves up a DataApp for items requested
+    in a given zip or tar archive.
+
+    Constructor Arguments:
+
+        ``filepath``    the path to the archive being served
+
+    ``cache_control()``
+
+        This method provides validated construction of the ``Cache-Control``
+        header as well as providing for automated filling out of the
+        ``EXPIRES`` header for HTTP/1.0 clients.
+    """
+
+    def __init__(self, filepath):
+        if zipfile.is_zipfile(filepath):
+            self.archive = zipfile.ZipFile(filepath,"r")
+        elif tarfile.is_tarfile(filepath):
+            self.archive = tarfile.TarFileCompat(filepath,"r")
+        else:
+            raise AssertionError("filepath '%s' is not a zip or tar " % filepath)
+        self.expires = None
+        self.last_modified = time.time()
+        self.cache = {}
+
+    def cache_control(self, **kwargs):
+        self.expires = CACHE_CONTROL.apply(self.headers, **kwargs) or None
+        return self
+
+    def __call__(self, environ, start_response):
+        path = environ.get("PATH_INFO","")
+        if path.startswith("/"):
+            path = path[1:]
+        application = self.cache.get(path)
+        if application:
+            return application(environ, start_response)
+        try:
+            info = self.archive.getinfo(path)
+        except KeyError:
+            exc = HTTPNotFound("The file requested, '%s', was not found." % path)
+            return exc.wsgi_application(environ, start_response)
+        if info.filename.endswith("/"):
+            exc = HTTPNotFound("Path requested, '%s', is not a file." % path)
+            return exc.wsgi_application(environ, start_response)
+        content_type, content_encoding = mimetypes.guess_type(info.filename)
+        # 'None' is not a valid content-encoding, so don't set the header if
+        # mimetypes.guess_type returns None
+        if content_encoding is not None:
+            app = DataApp(None, content_type = content_type,
+                                content_encoding = content_encoding)
+        else:
+            app = DataApp(None, content_type = content_type)
+        app.set_content(self.archive.read(path),
+                time.mktime(info.date_time + (0,0,0)))
+        self.cache[path] = app
+        app.expires = self.expires
+        return app(environ, start_response)
+
diff --git a/paste/fixture.py b/paste/fixture.py
new file mode 100644
index 0000000..1b97c35
--- /dev/null
+++ b/paste/fixture.py
@@ -0,0 +1,1730 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+"""
+Routines for testing WSGI applications.
+
+Most interesting is the `TestApp `_
+for testing WSGI applications, and the `TestFileEnvironment
+`_ class for testing the
+effects of command-line scripts.
+"""
+
+from __future__ import print_function
+
+import sys
+import random
+import mimetypes
+import time
+import cgi
+import os
+import shutil
+import smtplib
+import shlex
+import re
+import six
+import subprocess
+from six.moves import cStringIO as StringIO
+from six.moves.urllib.parse import urlencode
+from six.moves.urllib import parse as urlparse
+try:
+    # Python 3
+    from http.cookies import BaseCookie
+    from urllib.parse import splittype, splithost
+except ImportError:
+    # Python 2
+    from Cookie import BaseCookie
+    from urllib import splittype, splithost
+
+from paste import wsgilib
+from paste import lint
+from paste.response import HeaderDict
+
+def tempnam_no_warning(*args):
+    """
+    An os.tempnam with the warning turned off, because sometimes
+    you just need to use this and don't care about the stupid
+    security warning.
+    """
+    return os.tempnam(*args)
+
+class NoDefault(object):
+    pass
+
+def sorted(l):
+    l = list(l)
+    l.sort()
+    return l
+
+class Dummy_smtplib(object):
+
+    existing = None
+
+    def __init__(self, server):
+        import warnings
+        warnings.warn(
+            'Dummy_smtplib is not maintained and is deprecated',
+            DeprecationWarning, 2)
+        assert not self.existing, (
+            "smtplib.SMTP() called again before Dummy_smtplib.existing.reset() "
+            "called.")
+        self.server = server
+        self.open = True
+        self.__class__.existing = self
+
+    def quit(self):
+        assert self.open, (
+            "Called %s.quit() twice" % self)
+        self.open = False
+
+    def sendmail(self, from_address, to_addresses, msg):
+        self.from_address = from_address
+        self.to_addresses = to_addresses
+        self.message = msg
+
+    def install(cls):
+        smtplib.SMTP = cls
+
+    install = classmethod(install)
+
+    def reset(self):
+        assert not self.open, (
+            "SMTP connection not quit")
+        self.__class__.existing = None
+
+class AppError(Exception):
+    pass
+
+class TestApp(object):
+
+    # for py.test
+    disabled = True
+
+    def __init__(self, app, namespace=None, relative_to=None,
+                 extra_environ=None, pre_request_hook=None,
+                 post_request_hook=None):
+        """
+        Wraps a WSGI application in a more convenient interface for
+        testing.
+
+        ``app`` may be an application, or a Paste Deploy app
+        URI, like ``'config:filename.ini#test'``.
+
+        ``namespace`` is a dictionary that will be written to (if
+        provided).  This can be used with doctest or some other
+        system, and the variable ``res`` will be assigned everytime
+        you make a request (instead of returning the request).
+
+        ``relative_to`` is a directory, and filenames used for file
+        uploads are calculated relative to this.  Also ``config:``
+        URIs that aren't absolute.
+
+        ``extra_environ`` is a dictionary of values that should go
+        into the environment for each request.  These can provide a
+        communication channel with the application.
+
+        ``pre_request_hook`` is a function to be called prior to
+        making requests (such as ``post`` or ``get``). This function
+        must take one argument (the instance of the TestApp).
+
+        ``post_request_hook`` is a function, similar to
+        ``pre_request_hook``, to be called after requests are made.
+        """
+        if isinstance(app, (six.binary_type, six.text_type)):
+            from paste.deploy import loadapp
+            # @@: Should pick up relative_to from calling module's
+            # __file__
+            app = loadapp(app, relative_to=relative_to)
+        self.app = app
+        self.namespace = namespace
+        self.relative_to = relative_to
+        if extra_environ is None:
+            extra_environ = {}
+        self.extra_environ = extra_environ
+        self.pre_request_hook = pre_request_hook
+        self.post_request_hook = post_request_hook
+        self.reset()
+
+    def reset(self):
+        """
+        Resets the state of the application; currently just clears
+        saved cookies.
+        """
+        self.cookies = {}
+
+    def _make_environ(self):
+        environ = self.extra_environ.copy()
+        environ['paste.throw_errors'] = True
+        return environ
+
+    def get(self, url, params=None, headers=None, extra_environ=None,
+            status=None, expect_errors=False):
+        """
+        Get the given url (well, actually a path like
+        ``'/page.html'``).
+
+        ``params``:
+            A query string, or a dictionary that will be encoded
+            into a query string.  You may also include a query
+            string on the ``url``.
+
+        ``headers``:
+            A dictionary of extra headers to send.
+
+        ``extra_environ``:
+            A dictionary of environmental variables that should
+            be added to the request.
+
+        ``status``:
+            The integer status code you expect (if not 200 or 3xx).
+            If you expect a 404 response, for instance, you must give
+            ``status=404`` or it will be an error.  You can also give
+            a wildcard, like ``'3*'`` or ``'*'``.
+
+        ``expect_errors``:
+            If this is not true, then if anything is written to
+            ``wsgi.errors`` it will be an error.  If it is true, then
+            non-200/3xx responses are also okay.
+
+        Returns a `response object
+        `_
+        """
+        if extra_environ is None:
+            extra_environ = {}
+        # Hide from py.test:
+        __tracebackhide__ = True
+        if params:
+            if not isinstance(params, (six.binary_type, six.text_type)):
+                params = urlencode(params, doseq=True)
+            if '?' in url:
+                url += '&'
+            else:
+                url += '?'
+            url += params
+        environ = self._make_environ()
+        url = str(url)
+        if '?' in url:
+            url, environ['QUERY_STRING'] = url.split('?', 1)
+        else:
+            environ['QUERY_STRING'] = ''
+        self._set_headers(headers, environ)
+        environ.update(extra_environ)
+        req = TestRequest(url, environ, expect_errors)
+        return self.do_request(req, status=status)
+
+    def _gen_request(self, method, url, params='', headers=None, extra_environ=None,
+             status=None, upload_files=None, expect_errors=False):
+        """
+        Do a generic request.
+        """
+        if headers is None:
+            headers = {}
+        if extra_environ is None:
+            extra_environ = {}
+        environ = self._make_environ()
+        # @@: Should this be all non-strings?
+        if isinstance(params, (list, tuple, dict)):
+            params = urlencode(params)
+        if hasattr(params, 'items'):
+            # Some other multi-dict like format
+            params = urlencode(params.items())
+        if upload_files:
+            params = cgi.parse_qsl(params, keep_blank_values=True)
+            content_type, params = self.encode_multipart(
+                params, upload_files)
+            environ['CONTENT_TYPE'] = content_type
+        elif params:
+            environ.setdefault('CONTENT_TYPE', 'application/x-www-form-urlencoded')
+        if '?' in url:
+            url, environ['QUERY_STRING'] = url.split('?', 1)
+        else:
+            environ['QUERY_STRING'] = ''
+        environ['CONTENT_LENGTH'] = str(len(params))
+        environ['REQUEST_METHOD'] = method
+        environ['wsgi.input'] = StringIO(params)
+        self._set_headers(headers, environ)
+        environ.update(extra_environ)
+        req = TestRequest(url, environ, expect_errors)
+        return self.do_request(req, status=status)
+
+    def post(self, url, params='', headers=None, extra_environ=None,
+             status=None, upload_files=None, expect_errors=False):
+        """
+        Do a POST request.  Very like the ``.get()`` method.
+        ``params`` are put in the body of the request.
+
+        ``upload_files`` is for file uploads.  It should be a list of
+        ``[(fieldname, filename, file_content)]``.  You can also use
+        just ``[(fieldname, filename)]`` and the file content will be
+        read from disk.
+
+        Returns a `response object
+        `_
+        """
+        return self._gen_request('POST', url, params=params, headers=headers,
+                                 extra_environ=extra_environ,status=status,
+                                 upload_files=upload_files,
+                                 expect_errors=expect_errors)
+
+    def put(self, url, params='', headers=None, extra_environ=None,
+             status=None, upload_files=None, expect_errors=False):
+        """
+        Do a PUT request.  Very like the ``.get()`` method.
+        ``params`` are put in the body of the request.
+
+        ``upload_files`` is for file uploads.  It should be a list of
+        ``[(fieldname, filename, file_content)]``.  You can also use
+        just ``[(fieldname, filename)]`` and the file content will be
+        read from disk.
+
+        Returns a `response object
+        `_
+        """
+        return self._gen_request('PUT', url, params=params, headers=headers,
+                                 extra_environ=extra_environ,status=status,
+                                 upload_files=upload_files,
+                                 expect_errors=expect_errors)
+
+    def delete(self, url, params='', headers=None, extra_environ=None,
+               status=None, expect_errors=False):
+        """
+        Do a DELETE request.  Very like the ``.get()`` method.
+        ``params`` are put in the body of the request.
+
+        Returns a `response object
+        `_
+        """
+        return self._gen_request('DELETE', url, params=params, headers=headers,
+                                 extra_environ=extra_environ,status=status,
+                                 upload_files=None, expect_errors=expect_errors)
+
+
+
+
+    def _set_headers(self, headers, environ):
+        """
+        Turn any headers into environ variables
+        """
+        if not headers:
+            return
+        for header, value in headers.items():
+            if header.lower() == 'content-type':
+                var = 'CONTENT_TYPE'
+            elif header.lower() == 'content-length':
+                var = 'CONTENT_LENGTH'
+            else:
+                var = 'HTTP_%s' % header.replace('-', '_').upper()
+            environ[var] = value
+
+    def encode_multipart(self, params, files):
+        """
+        Encodes a set of parameters (typically a name/value list) and
+        a set of files (a list of (name, filename, file_body)) into a
+        typical POST body, returning the (content_type, body).
+        """
+        boundary = '----------a_BoUnDaRy%s$' % random.random()
+        lines = []
+        for key, value in params:
+            lines.append('--'+boundary)
+            lines.append('Content-Disposition: form-data; name="%s"' % key)
+            lines.append('')
+            lines.append(value)
+        for file_info in files:
+            key, filename, value = self._get_file_info(file_info)
+            lines.append('--'+boundary)
+            lines.append('Content-Disposition: form-data; name="%s"; filename="%s"'
+                         % (key, filename))
+            fcontent = mimetypes.guess_type(filename)[0]
+            lines.append('Content-Type: %s' %
+                         fcontent or 'application/octet-stream')
+            lines.append('')
+            lines.append(value)
+        lines.append('--' + boundary + '--')
+        lines.append('')
+        body = '\r\n'.join(lines)
+        content_type = 'multipart/form-data; boundary=%s' % boundary
+        return content_type, body
+
+    def _get_file_info(self, file_info):
+        if len(file_info) == 2:
+            # It only has a filename
+            filename = file_info[1]
+            if self.relative_to:
+                filename = os.path.join(self.relative_to, filename)
+            f = open(filename, 'rb')
+            content = f.read()
+            f.close()
+            return (file_info[0], filename, content)
+        elif len(file_info) == 3:
+            return file_info
+        else:
+            raise ValueError(
+                "upload_files need to be a list of tuples of (fieldname, "
+                "filename, filecontent) or (fieldname, filename); "
+                "you gave: %r"
+                % repr(file_info)[:100])
+
+    def do_request(self, req, status):
+        """
+        Executes the given request (``req``), with the expected
+        ``status``.  Generally ``.get()`` and ``.post()`` are used
+        instead.
+        """
+        if self.pre_request_hook:
+            self.pre_request_hook(self)
+        __tracebackhide__ = True
+        if self.cookies:
+            c = BaseCookie()
+            for name, value in self.cookies.items():
+                c[name] = value
+            hc = '; '.join(['='.join([m.key, m.value]) for m in c.values()])
+            req.environ['HTTP_COOKIE'] = hc
+        req.environ['paste.testing'] = True
+        req.environ['paste.testing_variables'] = {}
+        app = lint.middleware(self.app)
+        old_stdout = sys.stdout
+        out = CaptureStdout(old_stdout)
+        try:
+            sys.stdout = out
+            start_time = time.time()
+            raise_on_wsgi_error = not req.expect_errors
+            raw_res = wsgilib.raw_interactive(
+                app, req.url,
+                raise_on_wsgi_error=raise_on_wsgi_error,
+                **req.environ)
+            end_time = time.time()
+        finally:
+            sys.stdout = old_stdout
+            sys.stderr.write(out.getvalue())
+        res = self._make_response(raw_res, end_time - start_time)
+        res.request = req
+        for name, value in req.environ['paste.testing_variables'].items():
+            if hasattr(res, name):
+                raise ValueError(
+                    "paste.testing_variables contains the variable %r, but "
+                    "the response object already has an attribute by that "
+                    "name" % name)
+            setattr(res, name, value)
+        if self.namespace is not None:
+            self.namespace['res'] = res
+        if not req.expect_errors:
+            self._check_status(status, res)
+            self._check_errors(res)
+        res.cookies_set = {}
+        for header in res.all_headers('set-cookie'):
+            c = BaseCookie(header)
+            for key, morsel in c.items():
+                self.cookies[key] = morsel.value
+                res.cookies_set[key] = morsel.value
+        if self.post_request_hook:
+            self.post_request_hook(self)
+        if self.namespace is None:
+            # It's annoying to return the response in doctests, as it'll
+            # be printed, so we only return it is we couldn't assign
+            # it anywhere
+            return res
+
+    def _check_status(self, status, res):
+        __tracebackhide__ = True
+        if status == '*':
+            return
+        if isinstance(status, (list, tuple)):
+            if res.status not in status:
+                raise AppError(
+                    "Bad response: %s (not one of %s for %s)\n%s"
+                    % (res.full_status, ', '.join(map(str, status)),
+                       res.request.url, res.body))
+            return
+        if status is None:
+            if res.status >= 200 and res.status < 400:
+                return
+            raise AppError(
+                "Bad response: %s (not 200 OK or 3xx redirect for %s)\n%s"
+                % (res.full_status, res.request.url,
+                   res.body))
+        if status != res.status:
+            raise AppError(
+                "Bad response: %s (not %s)" % (res.full_status, status))
+
+    def _check_errors(self, res):
+        if res.errors:
+            raise AppError(
+                "Application had errors logged:\n%s" % res.errors)
+
+    def _make_response(self, resp, total_time):
+        status, headers, body, errors = resp
+        return TestResponse(self, status, headers, body, errors,
+                            total_time)
+
+class CaptureStdout(object):
+
+    def __init__(self, actual):
+        self.captured = StringIO()
+        self.actual = actual
+
+    def write(self, s):
+        self.captured.write(s)
+        self.actual.write(s)
+
+    def flush(self):
+        self.actual.flush()
+
+    def writelines(self, lines):
+        for item in lines:
+            self.write(item)
+
+    def getvalue(self):
+        return self.captured.getvalue()
+
+class TestResponse(object):
+
+    # for py.test
+    disabled = True
+
+    """
+    Instances of this class are return by `TestApp
+    `_
+    """
+
+    def __init__(self, test_app, status, headers, body, errors,
+                 total_time):
+        self.test_app = test_app
+        self.status = int(status.split()[0])
+        self.full_status = status
+        self.headers = headers
+        self.header_dict = HeaderDict.fromlist(self.headers)
+        self.body = body
+        self.errors = errors
+        self._normal_body = None
+        self.time = total_time
+        self._forms_indexed = None
+
+    def forms__get(self):
+        """
+        Returns a dictionary of ``Form`` objects.  Indexes are both in
+        order (from zero) and by form id (if the form is given an id).
+        """
+        if self._forms_indexed is None:
+            self._parse_forms()
+        return self._forms_indexed
+
+    forms = property(forms__get,
+                     doc="""
+                     A list of 
s found on the page (instances of + `Form `_) + """) + + def form__get(self): + forms = self.forms + if not forms: + raise TypeError( + "You used response.form, but no forms exist") + if 1 in forms: + # There is more than one form + raise TypeError( + "You used response.form, but more than one form exists") + return forms[0] + + form = property(form__get, + doc=""" + Returns a single `Form + `_ instance; it + is an error if there are multiple forms on the + page. + """) + + _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)(.*?)>', re.S|re.I) + + def _parse_forms(self): + forms = self._forms_indexed = {} + form_texts = [] + started = None + for match in self._tag_re.finditer(self.body): + end = match.group(1) == '/' + tag = match.group(2).lower() + if tag != 'form': + continue + if end: + assert started, ( + " unexpected at %s" % match.start()) + form_texts.append(self.body[started:match.end()]) + started = None + else: + assert not started, ( + "Nested form tags at %s" % match.start()) + started = match.start() + assert not started, ( + "Danging form: %r" % self.body[started:]) + for i, text in enumerate(form_texts): + form = Form(self, text) + forms[i] = form + if form.id: + forms[form.id] = form + + def header(self, name, default=NoDefault): + """ + Returns the named header; an error if there is not exactly one + matching header (unless you give a default -- always an error + if there is more than one header) + """ + found = None + for cur_name, value in self.headers: + if cur_name.lower() == name.lower(): + assert not found, ( + "Ambiguous header: %s matches %r and %r" + % (name, found, value)) + found = value + if found is None: + if default is NoDefault: + raise KeyError( + "No header found: %r (from %s)" + % (name, ', '.join([n for n, v in self.headers]))) + else: + return default + return found + + def all_headers(self, name): + """ + Gets all headers by the ``name``, returns as a list + """ + found = [] + for cur_name, value in self.headers: + if cur_name.lower() == name.lower(): + found.append(value) + return found + + def follow(self, **kw): + """ + If this request is a redirect, follow that redirect. It + is an error if this is not a redirect response. Returns + another response object. + """ + assert self.status >= 300 and self.status < 400, ( + "You can only follow redirect responses (not %s)" + % self.full_status) + location = self.header('location') + type, rest = splittype(location) + host, path = splithost(rest) + # @@: We should test that it's not a remote redirect + return self.test_app.get(location, **kw) + + def click(self, description=None, linkid=None, href=None, + anchor=None, index=None, verbose=False): + """ + Click the link as described. Each of ``description``, + ``linkid``, and ``url`` are *patterns*, meaning that they are + either strings (regular expressions), compiled regular + expressions (objects with a ``search`` method), or callables + returning true or false. + + All the given patterns are ANDed together: + + * ``description`` is a pattern that matches the contents of the + anchor (HTML and all -- everything between ```` and + ````) + + * ``linkid`` is a pattern that matches the ``id`` attribute of + the anchor. It will receive the empty string if no id is + given. + + * ``href`` is a pattern that matches the ``href`` of the anchor; + the literal content of that attribute, not the fully qualified + attribute. + + * ``anchor`` is a pattern that matches the entire anchor, with + its contents. + + If more than one link matches, then the ``index`` link is + followed. If ``index`` is not given and more than one link + matches, or if no link matches, then ``IndexError`` will be + raised. + + If you give ``verbose`` then messages will be printed about + each link, and why it does or doesn't match. If you use + ``app.click(verbose=True)`` you'll see a list of all the + links. + + You can use multiple criteria to essentially assert multiple + aspects about the link, e.g., where the link's destination is. + """ + __tracebackhide__ = True + found_html, found_desc, found_attrs = self._find_element( + tag='a', href_attr='href', + href_extract=None, + content=description, + id=linkid, + href_pattern=href, + html_pattern=anchor, + index=index, verbose=verbose) + return self.goto(found_attrs['uri']) + + def clickbutton(self, description=None, buttonid=None, href=None, + button=None, index=None, verbose=False): + """ + Like ``.click()``, except looks for link-like buttons. + This kind of button should look like + ``