diff options
Diffstat (limited to 'pypers/europython05/Quixote-2.0')
63 files changed, 13803 insertions, 0 deletions
diff --git a/pypers/europython05/Quixote-2.0/__init__.py b/pypers/europython05/Quixote-2.0/__init__.py new file mode 100755 index 0000000..44bbf4a --- /dev/null +++ b/pypers/europython05/Quixote-2.0/__init__.py @@ -0,0 +1,26 @@ +"""quixote +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/__init__.py $ +$Id: __init__.py 26539 2005-04-11 15:47:33Z dbinger $ + +A highly Pythonic web application framework. +""" + +__version__ = '2.0' + +# These are frequently needed by Quixote applications. +from quixote.publish import \ + get_publisher, get_request, get_response, get_path, redirect, \ + get_session, get_session_manager, get_user, get_field, get_cookie + + +def enable_ptl(): + """ + Installs the import hooks needed to import PTL modules. This must + be done explicitly because not all Quixote applications need to use + PTL, and import hooks are deep magic that can cause all sorts of + mischief and deeply confuse innocent bystanders. Thus, we avoid + invoking them behind the programmer's back. One known problem is + that, if you use ZODB, you must import ZODB before calling this + function. + """ + import quixote.ptl.install diff --git a/pypers/europython05/Quixote-2.0/config.py b/pypers/europython05/Quixote-2.0/config.py new file mode 100755 index 0000000..0a8a651 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/config.py @@ -0,0 +1,175 @@ +""" +$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/config.py $ +$Id: config.py 26378 2005-03-17 14:04:45Z dbinger $ + +Quixote configuration information. This module provides both the +default configuration values, and some code that Quixote uses for +dealing with configuration info. You should not edit the configuration +values in this file, since your edits will be lost if you upgrade to a +newer Quixote version in the future. However, this is the canonical +source of information about Quixote configuration variables, and editing +the defaults here is harmless if you're just playing around and don't +care what happens in the future. +""" + + +# Note that the default values here are geared towards a production +# environment, preferring security and performance over verbosity and +# debug-ability. If you just want to get a Quixote application +# up-and-running in a production environment, these settings are mostly +# right; all you really need to customize are ERROR_EMAIL, and ERROR_LOG. +# If you need to test/debug/develop a Quixote application, though, you'll +# probably want to also change DISPLAY_EXCEPTIONS. +# Again, you shouldn't edit this file unless you don't care what happens +# in the future (in particular, an upgrade to Quixote would clobber your +# edits). + + +# E-mail address to send application errors to; None to send no mail at +# all. This should probably be the email address of your web +# administrator. +ERROR_EMAIL = None +#ERROR_EMAIL = 'webmaster@example.com' + +# Filename for writing the Quixote access log; None for no access log. +ACCESS_LOG = None +#ACCESS_LOG = "/www/log/quixote-access.log" + +# Filename for logging error messages and debugging output; if None, +# everything will be sent to standard error (normally ending up in the +# Web server's error log file. +ERROR_LOG = None + +# Controls what's done when uncaught exceptions occur. If set to +# 'plain', the traceback will be returned to the browser in addition +# to being logged, If set to 'html' and the cgitb module is installed, +# a more elaborate display will be returned to the browser, showing +# the local variables and a few lines of context for each level of the +# traceback. If set to None, a generic error display, containing no +# information about the traceback, will be used. +DISPLAY_EXCEPTIONS = None + +# Compress large pages using gzip if the client accepts that encoding. +COMPRESS_PAGES = False + +# If true, then a cryptographically secure token will be inserted into forms +# as a hidden field. The token will be checked when the form is submitted. +# This prevents cross-site request forgeries (CSRF). It is off by default +# since it doesn't work if sessions are not persistent across requests. +FORM_TOKENS = False + +# Session-related variables +# ========================= + +# Name of the cookie that will hold the session ID string. +SESSION_COOKIE_NAME = "QX_session" + +# Domain and path to which the session cookie is restricted. Leaving +# these undefined is fine. Quixote does not have a default "domain" +# option, meaning the session cookie will only be sent to the +# originating server. If you don't set the cookie path, Quixote will +# use your application's root URL (ie. SCRIPT_NAME in a CGI-like +# environment), meaning the session cookie will be sent to all URLs +# controlled by your application, but no other. +SESSION_COOKIE_DOMAIN = None # eg. ".example.com" +SESSION_COOKIE_PATH = None # eg. "/" + + +# Mail-related variables +# ====================== +# These are only used by the quixote.sendmail module, which is +# provided for use by Quixote applications that need to send +# e-mail. This is a common task for web apps, but by no means +# universal. +# +# E-mail addresses can be specified either as a lone string +# containing a bare e-mail address ("addr-spec" in the RFC 822 +# grammar), or as an (address, real_name) tuple. + +# MAIL_FROM is used as the default for the "From" header and the SMTP +# sender for all outgoing e-mail. If you don't set it, your application +# will crash the first time it tries to send e-mail without an explicit +# "From" address. +MAIL_FROM = None # eg. "webmaster@example.com" + # or ("webmaster@example.com", "Example Webmaster") + +# E-mail is sent by connecting to an SMTP server on MAIL_SERVER. This +# server must be configured to relay outgoing e-mail from the current +# host (ie., the host where your Quixote application runs, most likely +# your web server) to anywhere on the Internet. If you don't know what +# this means, talk to your system administrator. +MAIL_SERVER = "localhost" + +# If MAIL_DEBUG_ADDR is set, then all e-mail will actually be sent to +# this address rather than the intended recipients. This should be a +# single, bare e-mail address. +MAIL_DEBUG_ADDR = None # eg. "developers@example.com" + + +# -- End config variables ---------------------------------------------- +# (no user serviceable parts after this point) + +class Config: + """Holds all Quixote configuration variables -- see above for + documentation of them. The naming convention is simple: + downcase the above variables to get the names of instance + attributes of this class. + """ + + config_vars = [ + 'error_email', + 'access_log', + 'display_exceptions', + 'error_log', + 'compress_pages', + 'form_tokens', + 'session_cookie_domain', + 'session_cookie_name', + 'session_cookie_path', + 'check_session_addr', + 'mail_from', + 'mail_server', + 'mail_debug_addr', + ] + + def __init__(self, **kwargs): + self.set_from_dict(globals()) # set defaults + for name, value in kwargs.items(): + if name not in self.config_vars: + raise ValueError('unknown config variable %r' % name) + setattr(self, name, value) + + def dump(self, file=None): + import sys + if file is None: + file = sys.stdout + file.write("<%s.%s instance at %x>:\n" % + (self.__class__.__module__, + self.__class__.__name__, + id(self))) + for var in self.config_vars: + file.write(" %s = %s\n" % (var, `getattr(self, var)`)) + + def set_from_dict(self, config_vars): + for name, value in config_vars.items(): + if name.isupper(): + name = name.lower() + if name not in self.config_vars: + raise ValueError('unknown config variable %r' % name) + setattr(self, name, value) + + def read_file(self, filename): + """Read configuration from a file. Any variables already + defined in this Config instance, but not in the file, are + unchanged, so you can use this to build up a configuration + by accumulating data from several config files. + """ + # The config file is Python code -- makes life easy. + config_vars = {} + try: + execfile(filename, config_vars) + except IOError, exc: + if exc.filename is None: # arg! execfile() loses filename + exc.filename = filename + raise exc + self.set_from_dict(config_vars) diff --git a/pypers/europython05/Quixote-2.0/demo/__init__.py b/pypers/europython05/Quixote-2.0/demo/__init__.py new file mode 100755 index 0000000..4efe0a7 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/demo/__init__.py @@ -0,0 +1,10 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/__init__.py $ +$Id: __init__.py 25575 2004-11-11 16:56:44Z nascheme $ +""" +from quixote import enable_ptl +from quixote.publish import Publisher +enable_ptl() + +def create_publisher(): + from quixote.demo.root import RootDirectory + return Publisher(RootDirectory(), display_exceptions='plain') diff --git a/pypers/europython05/Quixote-2.0/demo/altdemo.py b/pypers/europython05/Quixote-2.0/demo/altdemo.py new file mode 100755 index 0000000..fc32ca5 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/demo/altdemo.py @@ -0,0 +1,205 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/demo/altdemo.py $ +$Id: altdemo.py 26377 2005-03-16 23:32:15Z dbinger $ + +An alternative Quixote demo. This version is contained in a single module +and does not use PTL. The easiest way to run this demo is to use the +simple HTTP server included with Quixote. For example: + + $ server/simple_server.py --factory quixote.demo.altdemo.create_publisher + +The server listens on localhost:8080 by default. Debug and error output +will be sent to the terminal. + +If you have installed durus, you can run the same demo, except with +persistent sessions stored in a durus database, by running: + + $ server/simple_server.py --factory quixote.demo.altdemo.create_durus_publisher + +""" + +from quixote import get_user, get_session, get_session_manager, get_field +from quixote.directory import Directory +from quixote.html import href, htmltext +from quixote.publish import Publisher +from quixote.session import Session, SessionManager +from quixote.util import dump_request + +def format_page(title, content): + request = htmltext( + '<div style="font-size: smaller;background:#eee">' + '<h1>Request:</h1>%s</div>') % dump_request() + return htmltext( + '<html><head><title>%(title)s</title>' + '<style type="text/css">\n' + 'body { border: thick solid green; padding: 2em; }\n' + 'h1 { font-size: larger; }\n' + 'th { background: #aaa; text-align:left; font-size: smaller; }\n' + 'td { background: #ccc; font-size: smaller; }\n' + '</style>' + '</head><body>%(content)s%(request)s</body></html>') % locals() + +def format_request(): + return format_page('Request', dump_request()) + +def format_link_list(targets): + return htmltext('<ul>%s</ul>') % htmltext('').join([ + htmltext('<li>%s</li>') % href(target, target) for target in targets]) + +class RootDirectory(Directory): + + _q_exports = ['', 'login', 'logout'] + + def _q_index(self): + content = htmltext('') + if not get_user(): + content += htmltext('<p>%s</p>' % href('login', 'login')) + else: + content += htmltext( + '<p>Hello, %s.</p>') % get_user() + content += htmltext('<p>%s</p>' % href('logout', 'logout')) + sessions = get_session_manager().items() + if sessions: + sessions.sort() + content += htmltext('<table><tr>' + '<th></th>' + '<th>Session</th>' + '<th>User</th>' + '<th>Number of Requests</th>' + '</tr>') + this_session = get_session() + for index, (id, session) in enumerate(sessions): + if session is this_session: + formatted_id = htmltext( + '<span style="font-weight:bold">%s</span>' % id) + else: + formatted_id = id + content += htmltext( + '<tr><td>%s</td><td>%s</td><td>%s</td><td>%d</td>' % ( + index, + formatted_id, + session.user or htmltext("<em>None</em>"), + session.num_requests)) + content += htmltext('</table>') + return format_page("Quixote Session Management Demo", content) + + def login(self): + content = htmltext('') + if get_field("name"): + session = get_session() + session.set_user(get_field("name")) # This is the important part. + content += htmltext( + '<p>Welcome, %s! Thank you for logging in.</p>') % get_user() + content += href("..", "go back") + else: + content += htmltext( + '<p>Please enter your name here:</p>\n' + '<form method="POST" action="login">' + '<input name="name" />' + '<input type="submit" />' + '</form>') + return format_page("Quixote Session Demo: Login", content) + + def logout(self): + if get_user(): + content = htmltext('<p>Goodbye, %s.</p>') % get_user() + else: + content = htmltext('<p>That would be redundant.</p>') + content += href("..", "start over") + get_session_manager().expire_session() # This is the important part. + return format_page("Quixote Session Demo: Logout", content) + + +class DemoSession(Session): + + def __init__(self, id): + Session.__init__(self, id) + self.num_requests = 0 + + def start_request(self): + """ + This is called from the main object publishing loop whenever + we start processing a new request. Obviously, this is a good + place to track the number of requests made. (If we were + interested in the number of *successful* requests made, then + we could override finish_request(), which is called by + the publisher at the end of each successful request.) + """ + Session.start_request(self) + self.num_requests += 1 + + def has_info(self): + """ + Overriding has_info() is essential but non-obvious. The + session manager uses has_info() to know if it should hang on + to a session object or not: if a session is "dirty", then it + must be saved. This prevents saving sessions that don't need + to be saved, which is especially important as a defensive + measure against clients that don't handle cookies: without it, + we might create and store a new session object for every + request made by such clients. With has_info(), we create the + new session object every time, but throw it away unsaved as + soon as the request is complete. + + (Of course, if you write your session class such that + has_info() always returns true after a request has been + processed, you're back to the original problem -- and in fact, + this class *has* been written that way, because num_requests + is incremented on every request, which makes has_info() return + true, which makes SessionManager always store the session + object. In a real application, think carefully before putting + data in a session object that causes has_info() to return + true.) + """ + return (self.num_requests > 0) or Session.has_info(self) + + is_dirty = has_info + + +def create_publisher(): + return Publisher(RootDirectory(), + session_manager=SessionManager(session_class=DemoSession), + display_exceptions='plain') + +try: + # If durus is installed, define a create_durus_publisher() that + # uses a durus database to store persistent sessions. + import os, tempfile + from durus.persistent import Persistent + from durus.persistent_dict import PersistentDict + from durus.file_storage import FileStorage + from durus.connection import Connection + connection = None # set in create_durus_publisher() + + class PersistentSession(DemoSession, Persistent): + pass + + class PersistentSessionManager(SessionManager, Persistent): + def __init__(self): + sessions = PersistentDict() + SessionManager.__init__(self, + session_class=PersistentSession, + session_mapping=sessions) + def forget_changes(self, session): + print 'abort changes', get_session() + connection.abort() + + def commit_changes(self, session): + print 'commit changes', get_session() + connection.commit() + + def create_durus_publisher(): + global connection + filename = os.path.join(tempfile.gettempdir(), 'quixote-demo.durus') + print 'Opening %r as a Durus database.' % filename + connection = Connection(FileStorage(filename)) + root = connection.get_root() + session_manager = root.get('session_manager', None) + if session_manager is None: + session_manager = PersistentSessionManager() + connection.get_root()['session_manager'] = session_manager + connection.commit() + return Publisher(RootDirectory(), + session_manager=session_manager, + display_exceptions='plain') +except ImportError: + pass # durus not installed. diff --git a/pypers/europython05/Quixote-2.0/demo/mini_demo.py b/pypers/europython05/Quixote-2.0/demo/mini_demo.py new file mode 100755 index 0000000..f422211 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/demo/mini_demo.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +""" +A minimal Quixote demo. If you have the 'quixote' package in your Python +path, you can run it like this: + + $ python demo/mini_demo.py + +The server listens on localhost:8080 by default. Debug and error output +will be sent to the terminal. +""" + +from quixote.publish import Publisher +from quixote.directory import Directory + +class RootDirectory(Directory): + + _q_exports = ['', 'hello'] + + def _q_index(self): + return '''<html> + <body>Welcome to the Quixote demo. Here is a + <a href="hello">link</a>. + </body> + </html> + ''' + + def hello(self): + return '<html><body>Hello world!</body></html>' + + +def create_publisher(): + return Publisher(RootDirectory(), + display_exceptions='plain') + + +if __name__ == '__main__': + from quixote.server.simple_server import run + print 'creating demo listening on http://localhost:8080/' + run(create_publisher, host='localhost', port=8080) diff --git a/pypers/europython05/Quixote-2.0/directory.py b/pypers/europython05/Quixote-2.0/directory.py new file mode 100755 index 0000000..e3a8816 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/directory.py @@ -0,0 +1,109 @@ +"""$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/directory.py $ +$Id: directory.py 26327 2005-03-10 11:30:45Z dbinger $ + +Logic for traversing directory objects and generating output. +""" +import quixote +from quixote.errors import TraversalError + +class Directory(object): + """ + Instance attributes: none + """ + + # A list containing strings or 2-tuples of strings that map external + # names to internal names. Note that the empty string will be + # implicitly mapped to '_q_index'. + _q_exports = [] + + def _q_translate(self, component): + """(component : string) -> string | None + + Translate a path component into a Python identifier. Returning + None signifies that the component does not exist. + """ + if component in self._q_exports: + if component == '': + return '_q_index' # implicit mapping + else: + return component + else: + # check for an explicit external to internal mapping + for value in self._q_exports: + if isinstance(value, tuple): + if value[0] == component: + return value[1] + else: + return None + + def _q_lookup(self, component): + """(component : string) -> object + + Lookup a path component and return the corresponding object (usually + a Directory, a method or a string). Returning None signals that the + component does not exist. + """ + return None + + def _q_traverse(self, path): + """(path: [string]) -> object + + Traverse a path and return the result. + """ + assert len(path) > 0 + component = path[0] + path = path[1:] + name = self._q_translate(component) + if name is not None: + obj = getattr(self, name) + else: + obj = self._q_lookup(component) + if obj is None: + raise TraversalError(private_msg=('directory %r has no component ' + '%r' % (self, component))) + if path: + return obj._q_traverse(path) + elif callable(obj): + return obj() + else: + return obj + + def __call__(self): + if "" in self._q_exports and not quixote.get_request().form: + # Fix missing trailing slash. + path = quixote.get_path() + print "Adding slash to: %r " % path + return quixote.redirect(path + "/", permanent=True) + else: + raise TraversalError(private_msg=('directory %r is not ' + 'callable' % self)) + +class AccessControlled(object): + """ + A mix-in class that calls the _q_access() method before traversing + into the directory. + """ + def _q_access(self): + pass + + def _q_traverse(self, path): + self._q_access() + return super(AccessControlled, self)._q_traverse(path) + + +class Resolving(object): + """ + A mix-in class that provides the _q_resolve() method. _q_resolve() + is called if a component name appears in the _q_exports list but is + not an instance attribute. _q_resolve is expected to return the + component object. + """ + def _q_resolve(self, name): + return None + + def _q_translate(self, component): + name = super(Resolving, self)._q_translate(component) + if name is not None and not hasattr(self, name): + obj = self._q_resolve(name) + setattr(self, name, obj) + return name diff --git a/pypers/europython05/Quixote-2.0/doc/INSTALL.txt b/pypers/europython05/Quixote-2.0/doc/INSTALL.txt new file mode 100755 index 0000000..ccc18c8 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/INSTALL.txt @@ -0,0 +1,32 @@ +Installing Quixote +================== + +Quixote requires Python 2.3 or later. + +If you have a previously installed quixote, we strongly recommend that +you remove it before installing a new one. +First, find out where your old Quixote installation is: + + python -c "import os, quixote; print os.path.dirname(quixote.__file__)" + +and then remove away the reported directory. (If the import fails, then +you don't have an existing Quixote installation.) + +Now install the new version by running (in the distribution directory), + + python setup.py install + +and you're done. + +Quick start +=========== + +In a terminal window, run server/simple_server.py. +In a browser, open http://localhost:8080 + + +Upgrading a Quixote 1 application to Quixote 2. +=============================================== + +See upgrading.txt for details. + diff --git a/pypers/europython05/Quixote-2.0/doc/Makefile b/pypers/europython05/Quixote-2.0/doc/Makefile new file mode 100755 index 0000000..b09d2f3 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/Makefile @@ -0,0 +1,31 @@ +# +# Makefile to convert Quixote docs to HTML +# +# $Id: Makefile 20217 2003-01-16 20:51:53Z akuchlin $ +# + +TXT_FILES = $(wildcard *.txt) +HTML_FILES = $(filter-out ZPL%,$(TXT_FILES:%.txt=%.html)) + +RST2HTML = /www/python/bin/rst2html +RST2HTML_OPTS = -o us-ascii + +DEST_HOST = staging.mems-exchange.org +DEST_DIR = /www/www-docroot/software/quixote/doc + +SS = default.css + +%.html: %.txt + $(RST2HTML) $(RST2HTML_OPTS) $< $@ + +all: $(HTML_FILES) + +clean: + rm -f $(HTML_FILES) + +install: + rsync -vptgo *.html $(SS) $(DEST_HOST):$(DEST_DIR) + +local-install: + dir=`pwd` ; \ + cd $(DEST_DIR) && ln -sf $$dir/*.html $$dir/$(SS) . diff --git a/pypers/europython05/Quixote-2.0/doc/PTL.txt b/pypers/europython05/Quixote-2.0/doc/PTL.txt new file mode 100755 index 0000000..c0e4b0f --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/PTL.txt @@ -0,0 +1,264 @@ +PTL: Python Template Language +============================= + +Introduction +------------ + +PTL is the templating language used by Quixote. Most web templating +languages embed a real programming language in HTML, but PTL inverts +this model by merely tweaking Python to make it easier to generate +HTML pages (or other forms of text). In other words, PTL is basically +Python with a novel way to specify function return values. + +Specifically, a PTL template is designated by inserting a ``[plain]`` +or ``[html]`` modifier after the function name. The value of +expressions inside templates are kept, not discarded. If the type is +``[html]`` then non-literal strings are passed through a function that +escapes HTML special characters. + + +Plain text templates +-------------------- + +Here's a sample plain text template:: + + def foo [plain] (x, y = 5): + "This is a chunk of static text." + greeting = "hello world" # statement, no PTL output + print 'Input values:', x, y + z = x + y + """You can plug in variables like x (%s) + in a variety of ways.""" % x + + "\n\n" + "Whitespace is important in generated text.\n" + "z = "; z + ", but y is " + y + "." + +Obviously, templates can't have docstrings, but otherwise they follow +Python's syntactic rules: indentation indicates scoping, single-quoted +and triple-quoted strings can be used, the same rules for continuing +lines apply, and so forth. PTL also follows all the expected semantics +of normal Python code: so templates can have parameters, and the +parameters can have default values, be treated as keyword arguments, +etc. + +The difference between a template and a regular Python function is that +inside a template the result of expressions are saved as the return +value of that template. Look at the first part of the example again:: + + def foo [plain] (x, y = 5): + "This is a chunk of static text." + greeting = "hello world" # statement, no PTL output + print 'Input values:', x, y + z = x + y + """You can plug in variables like x (%s) + in a variety of ways.""" % x + +Calling this template with ``foo(1, 2)`` results in the following +string:: + + This is a chunk of static text.You can plug in variables like x (1) + in a variety of ways. + +Normally when Python evaluates expressions inside functions, it just +discards their values, but in a ``[plain]`` PTL template the value is +converted to a string using ``str()`` and appended to the template's +return value. There's a single exception to this rule: ``None`` is the +only value that's ever ignored, adding nothing to the output. (If this +weren't the case, calling methods or functions that return ``None`` +would require assigning their value to a variable. You'd have to write +``dummy = list.sort()`` in PTL code, which would be strange and +confusing.) + +The initial string in a template isn't treated as a docstring, but is +just incorporated in the generated output; therefore, templates can't +have docstrings. No whitespace is ever automatically added to the +output, resulting in ``...text.You can ...`` from the example. You'd +have to add an extra space to one of the string literals to correct +this. + +The assignment to the ``greeting`` local variable is a statement, not an +expression, so it doesn't return a value and produces no output. The +output from the ``print`` statement will be printed as usual, but won't +go into the string generated by the template. Quixote directs standard +output into Quixote's debugging log; if you're using PTL on its own, you +should consider doing something similar. ``print`` should never be used +to generate output returned to the browser, only for adding debugging +traces to a template. + +Inside templates, you can use all of Python's control-flow statements:: + + def numbers [plain] (n): + for i in range(n): + i + " " # PTL does not add any whitespace + +Calling ``numbers(5)`` will return the string ``"1 2 3 4 5 "``. You can +also have conditional logic or exception blocks:: + + def international_hello [plain] (language): + if language == "english": + "hello" + elif language == "french": + "bonjour" + else: + raise ValueError, "I don't speak %s" % language + + +HTML templates +-------------- + +Since PTL is usually used to generate HTML documents, an ``[html]`` +template type has been provided to make generating HTML easier. + +A common error when generating HTML is to grab data from the browser +or from a database and incorporate the contents without escaping +special characters such as '<' and '&'. This leads to a class of +security bugs called "cross-site scripting" bugs, where a hostile user +can insert arbitrary HTML in your site's output that can link to other +sites or contain JavaScript code that does something nasty (say, +popping up 10,000 browser windows). + +Such bugs occur because it's easy to forget to HTML-escape a string, +and forgetting it in just one location is enough to open a hole. PTL +offers a solution to this problem by being able to escape strings +automatically when generating HTML output, at the cost of slightly +diminished performance (a few percent). + +Here's how this feature works. PTL defines a class called +``htmltext`` that represents a string that's already been HTML-escaped +and can be safely sent to the client. The function ``htmlescape(string)`` +is used to escape data, and it always returns an ``htmltext`` +instance. It does nothing if the argument is already ``htmltext``. + +If a template function is declared ``[html]`` instead of ``[text]`` +then two things happen. First, all literal strings in the function +become instances of ``htmltext`` instead of Python's ``str``. Second, +the values of expressions are passed through ``htmlescape()`` instead +of ``str()``. + +``htmltext`` type is like the ``str`` type except that operations +combining strings and ``htmltext`` instances will result in the string +being passed through ``htmlescape()``. For example:: + + >>> from quixote.html import htmltext + >>> htmltext('a') + 'b' + <htmltext 'ab'> + >>> 'a' + htmltext('b') + <htmltext 'ab'> + >>> htmltext('a%s') % 'b' + <htmltext 'ab'> + >>> response = 'green eggs & ham' + >>> htmltext('The response was: %s') % response + <htmltext 'The response was: green eggs & ham'> + +Note that calling ``str()`` strips the ``htmltext`` type and should be +avoided since it usually results in characters being escaped more than +once. While ``htmltext`` behaves much like a regular string, it is +sometimes necessary to insert a ``str()`` inside a template in order +to obtain a genuine string. For example, the ``re`` module requires +genuine strings. We have found that explicit calls to ``str()`` can +often be avoided by splitting some code out of the template into a +helper function written in regular Python. + +It is also recommended that the ``htmltext`` constructor be used as +sparingly as possible. The reason is that when using the htmltext +feature of PTL, explicit calls to ``htmltext`` become the most likely +source of cross-site scripting holes. Calling ``htmltext`` is like +saying "I am absolutely sure this piece of data cannot contain malicious +HTML code injected by a user. Don't escape HTML special characters +because I want them." + +Note that literal strings in template functions declared with +``[html]`` are htmltext instances, and therefore won't be escaped. +You'll only need to use ``htmltext`` when HTML markup comes from +outside the template. For example, if you want to include a file +containing HTML:: + + def output_file [html] (): + '<html><body>' # does not get escaped + htmltext(open("myfile.html").read()) + '</body></html>' + +In the common case, templates won't be dealing with HTML markup from +external sources, so you can write straightforward code. Consider +this function to generate the contents of the ``HEAD`` element:: + + def meta_tags [html] (title, description): + '<title>%s</title>' % title + '<meta name="description" content="%s">\n' % description + +There are no calls to ``htmlescape()`` at all, but string literals +such as ``<title>%s</title>`` have all be turned into ``htmltext`` +instances, so the string variables will be automatically escaped:: + + >>> t.meta_tags('Catalog', 'A catalog of our cool products') + <htmltext '<title>Catalog</title> + <meta name="description" content="A catalog of our cool products">\n'> + >>> t.meta_tags('Dissertation on <HEAD>', + ... 'Discusses the "LINK" and "META" tags') + <htmltext '<title>Dissertation on <HEAD></title> + <meta name="description" + content="Discusses the "LINK" and "META" tags">\n'> + >>> + +Note how the title and description have had HTML-escaping applied to them. +(The output has been manually pretty-printed to be more readable.) + +Once you start using ``htmltext`` in one of your templates, mixing +plain and HTML templates is tricky because of ``htmltext``'s automatic +escaping; plain templates that generate HTML tags will be +double-escaped. One approach is to just use HTML templates throughout +your application. Alternatively you can use ``str()`` to convert +``htmltext`` instances to regular Python strings; just be sure the +resulting string isn't HTML-escaped again. + +Two implementations of ``htmltext`` are provided, one written in pure +Python and a second one implemented as a C extension. Both versions +have seen production use. + + +PTL modules +----------- + +PTL templates are kept in files with the extension .ptl. Like Python +files, they are byte-compiled on import, and the byte-code is written to +a compiled file with the extension ``.pyc``. Since vanilla Python +doesn't know anything about PTL, Quixote provides an import hook to let +you import PTL files just like regular Python modules. The standard way +to install this import hook is by calling the ``enable_ptl()`` function:: + + from quixote import enable_ptl + enable_ptl() + +(Note: if you're using ZODB, always import ZODB *before* installing the +PTL import hook. There's some interaction which causes importing the +TimeStamp module to fail when the PTL import hook is installed; we +haven't debugged the problem. A similar problem has been reported for +BioPython and win32com.client imports.) + +Once the import hook is installed, PTL files can be imported as if they +were Python modules. If all the example templates shown here were put +into a file named ``foo.ptl``, you could then write Python code that did +this:: + + from foo import numbers + def f(): + return numbers(10) + +You may want to keep this little function in your ``PYTHONSTARTUP`` +file:: + + def ptl(): + try: + import ZODB + except ImportError: + pass + from quixote import enable_ptl + enable_ptl() + +This is useful if you want to interactively play with a PTL module. + diff --git a/pypers/europython05/Quixote-2.0/doc/demo.txt b/pypers/europython05/Quixote-2.0/doc/demo.txt new file mode 100755 index 0000000..4272223 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/demo.txt @@ -0,0 +1,221 @@ +Running the Quixote Demos +========================= + +Quixote comes with some demonstration applications in the demo directory. +After quixote is installed (see INSTALL.txt for instructions), +you can run the demos using the scripts located in the server directory. + +Each server script is written for a specific method of connecting a +quixote publisher to a web server, and you will ultimately want to +choose the one that matches your needs. More information about the +different server scripts may be found in the scripts themselves and in +web-server.txt. To start, though, the easiest way to view the demos +is as follows: in a terminal window, run server/simple_server.py, and +in a browser, open http://localhost:8080. + +The simple_server.py script prints a usage message if you run it with +a '--help' command line argument. You can run different demos by +using the '--factory' option to identify a callable that creates the +publisher you want to use. In particular, you might try these demos: + + simple_server.py --factory quixote.demo.mini_demo.create_publisher + +or + + simple_server.py --factory quixote.demo.altdemo.create_publisher + + + +Understanding the mini_demo +--------------------------- + +Start the mini demo by running the command: + simple_server.py --factory quixote.demo.mini_demo.create_publisher + +In a browser, load http://localhost:8080. In your browser, you should +see "Welcome ..." page. In your terminal window, you will see a +"localhost - - ..." line for each request. These are access log +messages from the web server. + +Look at the source code in demo/mini_demo.py. Near the bottom you +will find the create_publisher() function. The create_publisher() +function creates a Publisher instance whose root directory is an +instance of the RootDirectory class defined just above. When a +request arrives, the Publisher calls the _q_traverse() method on the +root directory. In this case, the RootDirectory is using the standard +_q_traverse() implementation, inherited from Directory. + +Look, preferably in another window, at the source code for +_q_traverse() in directory.py. The path argument provided to +_q_traverse() is a list of string components of the path part of the +URL, obtained by splitting the request location at each '/' and +dropping the first element (which is always '') For example, if the +path part of the URL is '/', the path argument to _q_traverse() is +['']. If the path part of the URL is '/a', the path argument to +_q_traverse() is ['a']. If the path part of the URL is '/a/', the +path argument to _q_traverse() is ['a', '']. + +Looking at the code of _q_traverse(), observe that it starts by +splitting off the first component of the path and calling +_q_translate() to see if there is a designated attribute name +corresponding to this component. For the '/' page, the component is +'', and _q_translate() returns the attribute name '_q_index'. The +_q_traverse() function goes on to lookup the _q_index method and +return the result of calling it. + +Looking back at mini_demo.py, you can see that the RootDirectory class +includes a _q_index() method, and this method does return the HTML for +http://localhost:8080/ + +As mentioned above, the _q_translate() identifies a "designated" +attribute name for a given component. The default implementation uses +self._q_exports to define this designation. In particular, if the +component is in self._q_exports, then it is returned as the attribute +name, except in the special case of '', which is translated to the +special attribute name '_q_index'. + +When you click on the link on the top page, you get +http://localhost:8080/hello. In this case, the path argument to the +_q_traverse() call is ['hello'], and the return value is the result of +calling the hello() method. + +Feeling bold? (Just kidding, this won't hurt at all.) Try opening +http://localhost:8080/bogus. This is what happens when _q_traverse() +raises a TraversalError. A TraversalError is no big deal, but how +does quixote handle more exceptional exceptions? To see, you can +introduce one by editing mini_demo.py. Try inserting the line "raise +'ouch'" into the hello() method. Kill the demo server (Control-c) and +start a new one with the same command as before. Now load the +http://localhost:8080/hello page. You should see a plain text python +traceback followed by some information extracted from the HTTP +request. This information is always printed to the error log on an +exception. Here, it is also displayed in the browser because the +create_publisher() function made a publisher using the 'plain' value +for the display_exceptions keyword argument. If you omit that keyword +argument from the Publisher constructor, the browser will get an +"Internal Server Error" message instead of the full traceback. If you +provide the value 'html', the browser displays a prettier version of +the traceback. + +One more thing to try here. Replace your 'raise "ouch"' line in the hello() method with 'print "ouch"'. If you restart the server and load the /hello page, +you will see that print statements go the the error log (in this case, your +terminal window). This can be useful. + + +Understanding the root demo +--------------------------- + +Start the root demo by running the command: + simple_server.py --factory quixote.demo.create_publisher + +In a browser, open http://localhost:8080 as before. +Click around at will. + +This is the default demo, but it is more complicated than the +mini_demo described above. The create_publisher() function in +quixote.demo.__init__.py creates a publisher whose root directory is +an instance of quixote.demo.root.RootDirectory. Note that the source +code is a file named "root.ptl". The suffix of "ptl" indicates that +it is a PTL file, and the import must follow a call to +quixote.enable_ptl() or else the source file will not be found or +compiled. The quixote.demo.__init__.py file takes care of that. + +Take a look at the source code in root.ptl. You will see code that +looks like regular python, except that some function definitions have +"[html]" between the function name and the parameter list. These +functions are ptl templates. For details about PTL, see the PTL.txt +file. + +This RootDirectory class is similar to the one in mini_demo.py, in +that it has a _q_index() method and '' appears in the _q_exports list. +One new feature here is the presence of a tuple in the _q_exports +list. Most of the time, the elements of the _q_exports lists are just +strings that name attributes that should be available as URL +components. This pattern does not work, however, when the particular +URL component you want to use includes characters (like '.') that +can't appear in Python attribute names. To work around these cases, +the _q_exports list may contain tuples such as ("favicon.ico", +"favicon_ico") to designate "favicon_ico" as the attribute name +corresponding the the "favicon.ico" URL component. + +Looking at the RootDirectoryMethods, including plain(), css() and +favon_ico(), you will see examples where, in addition to returning a +string containing the body of the HTTP response, the function also +makes side-effect modifications to the response object itself, to set +the content type and the expiration time for the response. +Most of the time, these direct modifications to the response are +not needed. When they are, though, the get_response() function +gives you direct access to the response instance. + +The RootDirectory here also sets an 'extras' attribute to be an +instance of ExtraDirectory, imported from the quixote.demo.extras +module. Note that 'extras' also appears in the _q_exports list. This +is the ordinary way to extend your URL space through another '/'. +For example, the URL path '/extras/' will result in a call to +the ExtraDirectory instance's _q_index() method. + +The _q_lookup() method +---------------------- + +Now take a look at the ExtraDirectory class in extras.ptl. This class +exhibits some more advanced publishing features. If you look back at +the default _q_traverse() implementation (in directory.py), you will +see that the _q_traverse does not give up if _q_translate() returns +None, indicating that the path component has no designated +corresponding attribute name. In this case, _q_traverse() tries +calling self._q_lookup() to see if the object of interest can be found +in a different way. Note that _q_lookup() takes the component as an +argument and must return either (if there is more path to traverse) a +Directory instance, or else (if the component is the last in the path) +a callable or a string. + +In this particular case, the ExtrasDirectory._q_lookup() call returns +an instance of IntegerUI (a subclass of Directory). The interest +here, unlike the ExtrasDirectory() instance itself, is created +on-the-fly during the traversal, especially for this particular +component. Try loading http://localhost:8080/extras/12/ to see how +this behaves. + +Note that the correct URL to get to the IntegerUI(12)._q_index() call +ends with a '/'. This can sometimes be confusing to people who expect +http://localhost:8080/extras/12 to yield the same page as +http://localhost:8080/extras/12/. If given the path ['extras', '12'], +the default _q_traverse() ends up *calling* the instance of IntegerUI. +The Directory.__call__() (see directory.py) determines the result: if +no form values were submitted and adding a slash would produce a page, +the call returns the result of calling quixote.redirect(). The +redirect() call here causes the server to issue a permanent redirect +response to the path with the slash added. When this automatic +redirect is used, a message is printed to the error log. If the +conditions for a redirect are not met, the call falls back to raising +a TraversalError. [Note, if you don't like this redirect behavior, +override, replace, or delete Directory.__call__] + +The _q_lookup() pattern is useful when you want to allow URL +components that you either don't know or don't want to list in +_q_exports ahead of time. + +The _q_resolve() method +----------------------- + +Note that the ExtraDirectory class inherits from Resolving (in +addition to Directory). The Resolving mixin modifies the +_q_traverse() so that, when a component has an attribute name +designated by _q_translate(), but the Directory instance does not +actually *have* that attribute, the _q_resolve() method is called to +"resolve" the trouble. Typically, the _q_resolve() imports or +constructs what *should* be the value of the designated attribute. +The modified _q_translate() sets the attribute value so that the +_q_resolve() won't be called again for the same attribute. The +_q_resolve() pattern is useful when you want to delay the work of +constructing the values for exported attributes. + +Forms +----- + +You can't get very far writing web applications without writing forms. +The root demo includes, at http://localhost:8080/extras/form, a page +that demonstrates basic usage of the Form class and widgets defined in +the quixote.form package. + +$Id: demo.txt 25695 2004-11-30 20:53:44Z dbinger $ diff --git a/pypers/europython05/Quixote-2.0/doc/form2conversion.txt b/pypers/europython05/Quixote-2.0/doc/form2conversion.txt new file mode 100755 index 0000000..290db38 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/form2conversion.txt @@ -0,0 +1,358 @@ +Converting form1 forms to use the form2 library +=============================================== + +Note: +----- +The packages names have changed in Quixote 2. + +Quixote form1 forms are now in the package quixote.form1. +(In Quixote 1, they were in quixote.form.) + +Quixote form2 forms are now in the package quixote.form. +(In Quixote 1, they were in quixote.form2.) + + +Introduction +------------ + +These are some notes and examples for converting Quixote form1 forms, +that is forms derived from ``quixote.form1.Form``, to the newer form2 +forms. + +Form2 forms are more flexible than their form1 counterparts in that they +do not require you to use the ``Form`` class as a base to get form +functionality as form1 forms did. Form2 forms can be instantiated +directly and then manipulated as instances. You may also continue to +use inheritance for your form2 classes to get form functionality, +particularly if the structured separation of ``process``, ``render``, +and ``action`` is desirable. + +There are many ways to get from form1 code ported to form2. At one +end of the spectrum is to rewrite the form class using a functional +programing style. This method is arguably best since the functional +style makes the flow of control clearer. + +The other end of the spectrum and normally the easiest way to port +form1 forms to form2 is to use the ``compatibility`` module provided +in the form2 package. The compatibility module's Form class provides +much of the same highly structured machinery (via a ``handle`` master +method) that the form1 framework uses. + +Converting form1 forms using using the compatibility module +----------------------------------------------------------- + +Here's the short list of things to do to convert form1 forms to +form2 using compatibility. + + 1. Import the Form base class from ``quixote.form.compatibility`` + rather than from quixote.form1. + + 2. Getting and setting errors is slightly different. In your form's + process method, where errors are typically set, form2 + has a new interface for marking a widget as having an error. + + Form1 API:: + + self.error['widget_name'] = 'the error message' + + Form2 API:: + + self.set_error('widget_name', 'the error message') + + If you want to find out if the form already has errors, change + the form1 style of direct references to the ``self.errors`` + dictionary to a call to the ``has_errors`` method. + + Form1 API:: + + if not self.error: + do some more error checking... + + Form2 API:: + + if not self.has_errors(): + do some more error checking... + + 3. Form2 select widgets no longer take ``allowed_values`` or + ``descriptions`` arguments. If you are adding type of form2 select + widget, you must provide the ``options`` argument instead. Options + are the way you define the list of things that are selectable and + what is returned when they are selected. the options list can be + specified in in one of three ways:: + + options: [objects:any] + or + options: [(object:any, description:any)] + or + options: [(object:any, description:any, key:any)] + + An easy way to construct options if you already have + allowed_values and descriptions is to use the built-in function + ``zip`` to define options:: + + options=zip(allowed_values, descriptions) + + Note, however, that often it is simpler to to construct the + ``options`` list directly. + + 4. You almost certainly want to include some kind of cascading style + sheet (since form2 forms render with minimal markup). There is a + basic set of CSS rules in ``quixote.form.css``. + + +Here's the longer list of things you may need to tweak in order for +form2 compatibility forms to work with your form1 code. + + * ``widget_type`` widget class attribute is gone. This means when + adding widgets other than widgets defined in ``quixote.form.widget``, + you must import the widget class into your module and pass the + widget class as the first argument to the ``add_widget`` method + rather than using the ``widget_type`` string. + + * The ``action_url`` argument to the form's render method is now + a keyword argument. + + * If you use ``OptionSelectWidget``, there is no longer a + ``get_current_option`` method. You can get the current value + in the normal way. + + * ``ListWidget`` has been renamed to ``WidgetList``. + + * There is no longer a ``CollapsibleListWidget`` class. If you need + this functionality, consider writing a 'deletable composite widget' + to wrap your ``WidgetList`` widgets in it:: + + class DeletableWidget(CompositeWidget): + + def __init__(self, name, value=None, + element_type=StringWidget, + element_kwargs={}, **kwargs): + CompositeWidget.__init__(self, name, value=value, **kwargs) + self.add(HiddenWidget, 'deleted', value='0') + if self.get('deleted') != '1': + self.add(element_type, 'element', value=value, + **element_kwargs) + self.add(SubmitWidget, 'delete', value='Delete') + if self.get('delete'): + self.get_widget('deleted').set_value('1') + + def _parse(self, request): + if self.get('deleted') == '1': + self.value = None + else: + self.value = self.get('element') + + def render(self): + if self.get('deleted') == '1': + return self.get_widget('deleted').render() + else: + return CompositeWidget.render(self) + + +Congratulations, now that you've gotten your form1 forms working in form2, +you may wish to simplify this code using some of the new features available +in form2 forms. Here's a list of things you may wish to consider: + + * In your process method, you don't really need to get a ``form_data`` + dictionary by calling ``Form.process`` to ensure your widgets are + parsed. Instead, the parsed value of any widget is easy to obtain + using the widget's ``get_value`` method or the form's + ``__getitem__`` method. So, instead of:: + + form_data = Form.process(self, request) + val = form_data['my_widget'] + + You can use:: + + val = self['my_widget'] + + If the widget may or may not be in the form, you can use ``get``:: + + val = self.get('my_widget') + + + * It's normally not necessary to provide the ``action_url`` argument + to the form's ``render`` method. + + * You don't need to save references to your widgets in your form + class. You may have a particular reason for wanting to do that, + but any widget added to the form using ``add`` (or ``add_widget`` in + the compatibility module) can be retrieved using the form's + ``get_widget`` method. + + +Converting form1 forms to form2 by functional rewrite +----------------------------------------------------- + +The best way to get started on a functional version of a form2 rewrite +is to look at a trivial example form first written using the form1 +inheritance model followed by it's form2 functional equivalent. + +First the form1 form:: + + class MyForm1Form(Form): + def __init__(self, request, obj): + Form.__init__(self) + + if obj is None: + self.obj = Obj() + self.add_submit_button('add', 'Add') + else: + self.obj = obj + self.add_submit_button('update', 'Update') + + self.add_cancel_button('Cancel', request.get_path(1) + '/') + + self.add_widget('single_select', 'obj_type', + title='Object Type', + value=self.obj.get_type(), + allowed_values=list(obj.VALID_TYPES), + descriptions=['type1', 'type2', 'type3']) + self.add_widget('float', 'cost', + title='Cost', + value=obj.get_cost()) + + def render [html] (self, request, action_url): + title = 'Obj %s: Edit Object' % self.obj + header(title) + Form.render(self, request, action_url) + footer(title) + + def process(self, request): + form_data = Form.process(self, request) + + if not self.error: + if form_data['cost'] is None: + self.error['cost'] = 'A cost is required.' + elif form_data['cost'] < 0: + self.error['cost'] = 'The amount must be positive' + return form_data + + def action(self, request, submit, form_data): + self.obj.set_type(form_data['obj_type']) + self.obj.set_cost(form_data['cost']) + if submit == 'add': + db = get_database() + db.add(self.obj) + else: + assert submit == 'update' + return request.redirect(request.get_path(1) + '/') + +Here's the same form using form2 where the function operates on a Form +instance it keeps a reference to it as a local variable:: + + def obj_form(request, obj): + form = Form() # quixote.form.Form + if obj is None: + obj = Obj() + form.add_submit('add', 'Add') + else: + form.add_submit('update', 'Update') + form.add_submit('cancel', 'Cancel') + + form.add_single_select('obj_type', + title='Object Type', + value=obj.get_type(), + options=zip(obj.VALID_TYPES, + ['type1', 'type2', 'type3'])) + form.add_float('cost', + title='Cost', + value=obj.get_cost(), + required=1) + + def render [html] (): + title = 'Obj %s: Edit Object' % obj + header(title) + form.render() + footer(title) + + def process(): + if form['cost'] < 0: + self.set_error('cost', 'The amount must be positive') + + def action(submit): + obj.set_type(form['obj_type']) + obj.set_cost(form['cost']) + if submit == 'add': + db = get_database() + db.add(self.obj) + else: + assert submit == 'update' + + exit_path = request.get_path(1) + '/' + submit = form.get_submit() + if submit == 'cancel': + return request.redirect(exit_path) + + if not form.is_submitted() or form.has_errors(): + return render() + process() + if form.has_errors(): + return render() + + action(submit) + return request.redirect(exit_path) + + +As you can see in the example, the function still has all of the same +parts of it's form1 equivalent. + + 1. It determines if it's to create a new object or edit an existing one + 2. It adds submit buttons and widgets + 3. It has a function that knows how to render the form + 4. It has a function that knows how to do error processing on the form + 5. It has a function that knows how to register permanent changes to + objects when the form is submitted successfully. + +In the form2 example, we have used inner functions to separate out these +parts. This, of course, is optional, but it does help readability once +the form gets more complicated and has the additional advantage of +mapping directly with it's form1 counterparts. + +Form2 functional forms do not have the ``handle`` master-method that +is called after the form is initialized. Instead, we deal with this +functionality manually. Here are some things that the ``handle`` +portion of your form might need to implement illustrated in the +order that often makes sense. + + 1. Get the value of any submit buttons using ``form.get_submit`` + 2. If the form has not been submitted yet, return ``render()``. + 3. See if the cancel button was pressed, if so return a redirect. + 4. Call your ``process`` inner function to do any widget-level error + checks. The form may already have set some errors, so you + may wish to check for that before trying additional error checks. + 5. See if the form was submitted by an unknown submit button. + This will be the case if the form was submitted via a JavaScript + action, which is the case when an option select widget is selected. + The value of ``get_submit`` is ``True`` in this case and if it is, + you want to clear any errors and re-render the form. + 6. If the form has not been submitted or if the form has errors, + you simply want to render the form. + 7. Check for your named submit buttons which you expect for + successful form posting e.g. ``add`` or ``update``. If one of + these is pressed, call you action inner function. + 8. Finally, return a redirect to the expected page following a + form submission. + +These steps are illustrated by the following snippet of code and to a +large degree in the above functional form2 code example. Often this +``handle`` block of code can be simplified. For example, if you do not +expect form submissions from unregistered submit buttons, you can +eliminate the test for that. Similarly, if your form does not do any +widget-specific error checking, there's no reason to have an error +checking ``process`` function or the call to it:: + + exit_path = request.get_path(1) + '/' + submit = form.get_submit() + if not submit: + return render() + if submit == 'cancel': + return request.redirect(exit_path) + if submit == True: + form.clear_errors() + return render() + process() + if form.has_errors(): + return render() + action(submit) + return request.redirect(exit_path) diff --git a/pypers/europython05/Quixote-2.0/doc/multi-threaded.txt b/pypers/europython05/Quixote-2.0/doc/multi-threaded.txt new file mode 100755 index 0000000..f3be1a5 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/multi-threaded.txt @@ -0,0 +1,39 @@ +Multi-Threaded Quixote Applications +=================================== + +Starting with Quixote 0.6, it's possible to write multi-threaded Quixote +applications. In previous versions, Quixote stored the current +HTTPRequest object in a global variable, meaning that processing +multiple requests in the same process simultaneously was impossible. + +However, the Publisher class as shipped still can't handle multiple +simultaneous requests; you'll need to subclass Publisher to make it +re-entrant. Here's a starting point:: + + import thread + from quixote.publish import Publisher + + [...] + + class ThreadedPublisher (Publisher): + def __init__ (self, root_namespace, config=None): + Publisher.__init__(self, root_namespace, config) + self._request_dict = {} + + def _set_request(self, request): + self._request_dict[thread.get_ident()] = request + + def _clear_request(self): + try: + del self._request_dict[thread.get_ident()] + except KeyError: + pass + + def get_request(self): + return self._request_dict.get(thread.get_ident()) + +Using ThreadedPublisher, you now have one current request per thread, +rather than one for the entire process. + + +$Id: multi-threaded.txt 20217 2003-01-16 20:51:53Z akuchlin $ diff --git a/pypers/europython05/Quixote-2.0/doc/programming.txt b/pypers/europython05/Quixote-2.0/doc/programming.txt new file mode 100755 index 0000000..d94adf7 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/programming.txt @@ -0,0 +1,157 @@ +Quixote Programming Overview +============================ + +This document explains how a Quixote application is structured. +The demo.txt file should probably be read before you read this file. +There are three components to a Quixote application: + +1) A driver script, usually a CGI or FastCGI script. This is the + interface between your web server (eg., Apache) and the bulk of your + application code. The driver script is responsible for creating a + Quixote publisher customized for your application and invoking its + publishing loop. + +2) A configuration file. This file specifies various features of the + Publisher class, such as how errors are handled, the paths of + various log files, and various other things. Read through + quixote/config.py for the full list of configuration settings. + + The most important configuration parameters are: + + ``ERROR_EMAIL`` + e-mail address to which errors will be mailed + ``ERROR_LOG`` + file to which errors will be logged + + For development/debugging, you should also set ``DISPLAY_EXCEPTIONS`` + true; the default value is false, to favor security over convenience. + +3) Finally, the bulk of the code will be called through a call (by the + Publisher) to the _q_traverse() method of an instance designated as + the ``root_directory``. Normally, the root_directory will be an + instance of the Directory class. + + +Driver script +------------- + +The driver script is the interface between your web server and Quixote's +"publishing loop", which in turn is the gateway to your application +code. Thus, there are two things that your Quixote driver script must +do: + +* create a Quixote publisher -- that is, an instance of the Publisher + class provided by the quixote.publish module -- and customize it for + your application + +* invoke the publisher's process_request() method as needed to get + responses for one or more requests, writing the responses back + to the client(s). + +The publisher is responsible for translating URLs to Python objects and +calling the appropriate function, method, or PTL template to retrieve +the information and/or carry out the action requested by the URL. + +The most important application-specific customization done by the driver +script is to set the root directory of your application. + +The quixote.servers package includes driver modules for cgi, fastcgi, +scgi, medusa, twisted, and the simple_server. Each of these modules +includes a ``run()`` function that you can use in a driver script that +provides a function to create the publisher that you want. For an example +of this pattern, see the __main__ part of demo/mini_demo.py. You could +run the mini_demo.py with scgi by using the ``run()`` function imported +from quixote.server.scgi_server instead of the one from +quixote.server.simple_server. (You would also need your http server +set up to use the scgi server.) + +That's almost the simplest possible case -- there's no +application-specific configuration info apart from the root directory. + +Getting the driver script to actually run is between you and your web +server. See the web-server.txt document for help. + + +Configuration file +------------------ + +By default, the Publisher uses the configuration information from +quixote/config.py. You should never edit the default values in +quixote/config.py, because your edits will be lost if you upgrade to a +newer Quixote version. You should certainly read it, though, to +understand what all the configuration variables are. If you want to +customize any of the configuration variables, your driver script +should provide your customized Config instance as an argument to the +Publisher constructor. + +Logging +------- + +The publisher also accepts an optional ``logger`` keyword argument, +that should, if provided, support the same methods as the +default value, an instance of ``DefaultLogger``. Even if you +use the default logger, you can still customize the behavior +by setting configuration values for ``access_log``, ``error_log``, and/or +``error_email``. These configuration variables are described +more fully in config.py. + +Quixote writes one (rather long) line to the access log for each request +it handles; we have split that line up here to make it easier to read:: + + 127.0.0.1 - 2001-10-15 09:48:43 + 2504 "GET /catalog/ HTTP/1.1" + 200 'Opera/6.0 (Linux; U)' 0.10sec + +This line consists of: + +* client IP address +* current user (according to Quixote session management mechanism, + so this will be "-" unless you're using a session manager that + does authentication) +* date and time of request in local timezone, as YYYY-MM-DD hh:mm:ss +* process ID of the process serving the request (eg. your CGI/FastCGI + driver script) +* the HTTP request line (request method, URI, and protocol) +* response status code +* HTTP user agent string (specifically, this is + ``repr(os.environ.get('HTTP_USER_AGENT', ''))``) +* time to complete the request + +If no access log is configured (ie., ``ACCESS_LOG`` is ``None``), then +Quixote will not do any access logging. + +The error log is used for three purposes: + +* application output to ``sys.stdout`` and ``sys.stderr`` goes to + Quixote's error log +* application tracebacks will be written to Quixote's error log + +If no error log is configured (with ``ERROR_LOG``), then all output is +redirected to the stderr supplied to Quixote for this request by your +web server. At least for CGI/FastCGI scripts under Apache, this winds +up in Apache's error log. + +Having stdout redirected to the error log is useful for debugging. You +can just sprinkle ``print`` statements into your application and the +output will wind up in the error log. + + +Application code +---------------- + +Finally, we reach the most complicated part of a Quixote application. +However, thanks to Quixote's design, everything you've ever learned +about designing and writing Python code is applicable, so there are no +new hoops to jump through. You may, optionally, wish to use PTL, +which is simply Python with a novel way of generating function return +values -- see PTL.txt for details. + +Quixote's Publisher constructs a request, splits the path into a list +of components, and calls the root directory's _q_traverse() method, +giving the component list as an argument. The _q_traverse() will either +return a value that will become the content of the HTTPResponse, or +else it may raise an Exception. Exceptions are caught by the Publisher +and handled as needed, depending on configuration variables and +whether or not the Exception is an instance of PublisherError. + + diff --git a/pypers/europython05/Quixote-2.0/doc/session-mgmt.txt b/pypers/europython05/Quixote-2.0/doc/session-mgmt.txt new file mode 100755 index 0000000..19df072 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/session-mgmt.txt @@ -0,0 +1,323 @@ +Quixote Session Management +========================== + +HTTP was originally designed as a stateless protocol, meaning that every +request for a document or image was conducted in a separate TCP +connection, and that there was no way for a web server to tell if two +separate requests actually come from the same user. It's no longer +necessarily true that every request is conducted in a separate TCP +connection, but HTTP is still fundamentally stateless. However, there +are many applications where it is desirable or even essential to +establish a "session" for each user, ie. where all requests performed by +that user are somehow tied together on the server. + +HTTP cookies were invented to address this requirement, and they are +still the best solution for establishing sessions on top of HTTP. Thus, +the session management mechanism that comes with Quixote is +cookie-based. (The most common alternative is to embed the session +identifier in the URL. Since Quixote views the URL as a fundamental +part of the web user interface, a URL-based session management scheme is +considered un-Quixotic.) + +For further reading: the standard for cookies that is approximately +implemented by most current browsers is RFC 2109; the latest version of +the standard is RFC 2965. + +In a nutshell, session management with Quixote works like this: + +* when a user-agent first requests a page from a Quixote application + that implements session management, Quixote creates a Session object + and generates a session ID (a random 64-bit number). The Session + object is attached to the current HTTPRequest object, so that + application code involved in processing this request has access to + the Session object. The get_session() function provides uniform + access to the current Session object. + +* if, at the end of processing that request, the application code has + stored any information in the Session object, Quixote saves the + session in its SessionManager object for use by future requests and + sends a session cookie, called ``QX_session`` by default, to the user. + The session cookie contains the session ID encoded as a hexadecimal + string, and is included in the response headers, eg. :: + + Set-Cookie: QX_session="928F82A9B8FA92FD" + + (You can instruct Quixote to specify the domain and path for + URLs to which this cookie should be sent.) + +* the user agent stores this cookie for future requests + +* the next time the user agent requests a resource that matches the + cookie's domain and path, it includes the ``QX_session`` cookie + previously generated by Quixote in the request headers, eg.:: + + Cookie: QX_session="928F82A9B8FA92FD" + +* while processing the request, Quixote decodes the session ID and + looks up the corresponding Session object in its SessionManager. If + there is no such session, the session cookie is bogus or + out-of-date, so Quixote raises SessionError; ultimately the user + gets an error page. Otherwise, the Session object is made + available, through the get_session() function, as the application + code processes the request. + +There are two caveats to keep in mind before proceeding, one major and +one minor: + +* Quixote's standard Session and SessionManager class do not + implement any sort of persistence, meaning that all sessions + disappear when the process handling web requests terminates. + Thus, session management is completely useless with a plain + CGI driver script unless you add some persistence to the mix; + see "Session persistence" below for information. + +* Quixote never expires sessions; if you want user sessions to + be cleaned up after a period of inactivity, you will have to + write code to do it yourself. + + +Session management demo +----------------------- + +There's a simple demo of Quixote's session management in demo/altdemo.py. +If the durus (http://www.mems-exchange.org/software/durus/) package is +installed, the demo uses a durus database to store sessions, so sessions +will be preserved, even if your are running it with plain cgi. + +This particular application uses sessions to keep track of just two +things: the user's identity and the number of requests made in this +session. The first is addressed by Quixote's standard Session class -- +every Session object has a ``user`` attribute, which you can use for +anything you like. In the session demo, we simply store a string, the +user's name, which is entered by the user. + +Tracking the number of requests is a bit more interesting: from the +DemoSession class in altdemo.py:: + + def __init__ (self, id): + Session.__init__(self, id) + self.num_requests = 0 + + def start_request (self): + Session.start_request(self) + self.num_requests += 1 + +When the session is created, we initialize the request counter; and +when we start processing each request, we increment it. Using the +session information in the application code is simple. If you want the +value of the user attribute of the current session, just call +get_user(). If you want some other attribute or method Use +get_session() to get the current Session if you need access to other +attributes (such as ``num_requests`` in the demo) or methods of the +current Session instance. + +Note that the Session class initializes the user attribute to None, +so get_user() will return None if no user has been identified for +this session. Application code can use this to change behavior, +as in the following:: + + if not get_user(): + content += htmltext('<p>%s</p>' % href('login', 'login')) + else: + content += htmltext( + '<p>Hello, %s.</p>') % get_user() + content += htmltext('<p>%s</p>' % href('logout', 'logout')) + + +Note that we must quote the user's name, because they are free to enter +anything they please, including special HTML characters like ``&`` or +``<``. + +Of course, ``session.user`` will never be set if we don't set it +ourselves. The code that processes the login form is just this (from +``login()`` in ``demo/altdemo.py``) :: + + if get_field("name"): + session = get_session() + session.set_user(get_field("name")) # This is the important part. + +This is obviously a very simple application -- we're not doing any +verification of the user's input. We have no user database, no +passwords, and no limitations on what constitutes a "user name". A real +application would have all of these, as well as a way for users to add +themselves to the user database -- ie. register with your web site. + + +Configuring the session cookie +------------------------------ + +Quixote allows you to configure several aspects of the session cookie +that it exchanges with clients. First, you can set the name of the +cookie; this is important if you have multiple independent Quixote +applications running on the same server. For example, the config file +for the first application might have :: + + SESSION_COOKIE_NAME = "foo_session" + +and the second application might have :: + + SESSION_COOKIE_NAME = "bar_session" + +Next, you can use ``SESSION_COOKIE_DOMAIN`` and ``SESSION_COOKIE_PATH`` +to set the cookie attributes that control which requests the cookie is +included with. By default, these are both ``None``, which instructs +Quixote to send the cookie without ``Domain`` or ``Path`` qualifiers. +For example, if the client requests ``/foo/bar/`` from +www.example.com, and Quixote decides that it must set the session +cookie in the response to that request, then the server would send :: + + Set-Cookie: QX_session="928F82A9B8FA92FD" + +in the response headers. Since no domain or path were specified with +that cookie, the browser will only include the cookie with requests to +www.example.com for URIs that start with ``/foo/bar/``. + +If you want to ensure that your session cookie is included with all +requests to www.example.com, you should set ``SESSION_COOKIE_PATH`` in your +config file:: + + SESSION_COOKIE_PATH = "/" + +which will cause Quixote to set the cookie like this:: + + Set-Cookie: QX_session="928F82A9B8FA92FD"; Path="/" + +which will instruct the browser to include that cookie with *all* +requests to www.example.com. + +However, think carefully about what you set ``SESSION_COOKIE_PATH`` to +-- eg. if you set it to "/", but all of your Quixote code is under "/q/" +in your server's URL-space, then your user's session cookies could be +unnecessarily exposed. On shared servers where you don't control all of +the code, this is especially dangerous; be sure to use (eg.) :: + + SESSION_COOKIE_PATH = "/q/" + +on such servers. The trailing slash is important; without it, your +session cookies will be sent to URIs like ``/qux`` and ``/qix``, even if +you don't control those URIs. + +If you want to share the cookie across servers in your domain, +eg. www1.example.com and www2.example.com, you'll also need to set +``SESSION_COOKIE_DOMAIN``: + + SESSION_COOKIE_DOMAIN = ".example.com" + +Finally, note that the ``SESSION_COOKIE_*`` configuration variables +*only* affect Quixote's session cookie; if you set your own cookies +using the ``HTTPResponse.set_cookie()`` method, then the cookie sent to +the client is completely determined by that ``set_cookie()`` call. + +See RFCs 2109 and 2965 for more information on the rules browsers are +supposed to follow for including cookies with HTTP requests. + + +Writing the session class +------------------------- + +You will almost certainly have to write a custom session class for your +application by subclassing Quixote's standard Session class. Every +custom session class has two essential responsibilities: + +* initialize the attributes that will be used by your application + +* override the ``has_info()`` method, so the session manager knows when + it must save your session object + +The first one is fairly obvious and just good practice. The second is +essential, and not at all obvious. The has_info() method exists because +SessionManager does not automatically hang on to all session objects; +this is a defense against clients that ignore cookies, making your +session manager create lots of session objects that are just used once. +As long as those session objects are not saved, the burden imposed by +these clients is not too bad -- at least they aren't sucking up your +memory, or bogging down the database that you save session data to. +Thus, the session manager uses has_info() to know if it should hang on +to a session object or not: if a session has information that must be +saved, the session manager saves it and sends a session cookie to the +client. + +For development/testing work, it's fine to say that your session objects +should always be saved:: + + def has_info (self): + return 1 + +The opposite extreme is to forget to override ``has_info()`` altogether, +in which case session management most likely won't work: unless you +tickle the Session object such that the base ``has_info()`` method +returns true, the session manager won't save the sessions that it +creates, and Quixote will never drop a session cookie on the client. + +In a real application, you need to think carefully about what data to +store in your sessions, and how ``has_info()`` should react to the +presence of that data. If you try and track something about every +single visitor to your site, sooner or later one of those a +broken/malicious client that ignores cookies and ``robots.txt`` will +come along and crawl your entire site, wreaking havoc on your Quixote +application (or the database underlying it). + + +Session persistence +------------------- + +Keeping session data across requests is all very nice, but in the real +world you want that data to survive across process termination. With +CGI, this is essential, since each process serves exactly one request +and then terminates. With other execution mechanisms, though, it's +still important -- you don't want to lose all your session data just +because your long-lived server process was restarted, or your server +machine was rebooted. + +However, every application is different, so Quixote doesn't provide any +built-in mechanism for session persistence. Instead, it provides a +number of hooks, most in the SessionManager class, that let you plug in +your preferred persistence mechanism. + +The first and most important hook is in the SessionManager +constructor: you can provide an alternate mapping object that +SessionManager will use to store session objects in. By default, +SessionManager uses an ordinary dictionary; if you provide a mapping +object that implements persistence, then your session data will +automatically persist across processes. + +The second hook (two hooks, really) apply if you use a transactional +persistence mechanism to provide your SessionManager's mapping. The +``altdemo.py`` script does this with Durus, if the durus package is +installed, but you could also use ZODB or a relational database for +this purpose. The hooks make sure that session (and other) changes +get committed or aborted at the appropriate times. SessionManager +provides two methods for you to override: ``forget_changes()`` and +``commit_changes()``. ``forget_changes()`` is called by +SessionPublisher whenever a request crashes, ie. whenever your +application raises an exception other than PublishError. +``commit_changes()`` is called for requests that complete +successfully, or that raise a PublishError exception. You'll have to +use your own SessionManager subclass if you need to take advantage of +these hooks for transactional session persistence. + +The third available hook is the Session's is_dirty() method. This is +used when your mapping class uses a more primitive storage mechanism, +as, for example, the standard 'shelve' module, which provides a +mapping object on top of a DBM or Berkeley DB file:: + + import shelve + sessions = shelve.open("/tmp/quixote-sessions") + session_manager = SessionManager(session_mapping=sessions) + +If you use one of these relatively simple persistent mapping types, +you'll also need to override ``is_dirty()`` in your Session class. +That's in addition to overriding ``has_info()``, which determines if a +session object is *ever* saved; ``is_dirty()`` is only called on +sessions that have already been added to the session mapping, to see +if they need to be "re-added". The default implementation always +returns false, because once an object has been added to a normal +dictionary, there's no need to add it again. However, with simple +persistent mapping types like shelve, you need to store the object +again each time it changes. Thus, ``is_dirty()`` should return true +if the session object needs to be re-written. For a simple, naive, +but inefficient implementation, making is_dirty an alias for +``has_info()`` will work -- that just means that once the session has +been written once, it will be re-written on every request. + + diff --git a/pypers/europython05/Quixote-2.0/doc/static-files.txt b/pypers/europython05/Quixote-2.0/doc/static-files.txt new file mode 100755 index 0000000..64254f4 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/static-files.txt @@ -0,0 +1,51 @@ +Examples of serving static files +================================ + +The ``quixote.util`` module includes classes for making files and +directories available as Quixote resources. Here are some examples. + + +Publishing a Single File +------------------------ + +The ``StaticFile`` class makes an individual filesystem file (possibly +a symbolic link) available. You can also specify the MIME type and +encoding of the file; if you don't specify this, the MIME type will be +guessed using the standard Python ``mimetypes.guess_type()`` function. +The default action is to not follow symbolic links, but this behaviour +can be changed using the ``follow_symlinks`` parameter. + +The following example publishes a file with the URL ``.../stylesheet_css``:: + + # 'stylesheet_css' must be in the _q_exports list + _q_exports = [ ..., 'stylesheet_css', ...] + + stylesheet_css = StaticFile( + "/htdocs/legacy_app/stylesheet.css", + follow_symlinks=1, mime_type="text/css") + + +If you want the URL of the file to have a ``.css`` extension, you use +the external to internal name mapping feature of ``_q_exports``. For +example:: + + _q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...] + + + +Publishing a Directory +---------------------- + +Publishing a directory is similar. The ``StaticDirectory`` class +makes a complete filesystem directory available. Again, the default +behaviour is to not follow symlinks. You can also request that the +``StaticDirectory`` object cache information about the files in +memory so that it doesn't try to guess the MIME type on every hit. + +This example publishes the ``notes/`` directory:: + + _q_exports = [ ..., 'notes', ...] + + notes = StaticDirectory("/htdocs/legacy_app/notes") + + diff --git a/pypers/europython05/Quixote-2.0/doc/upgrading.txt b/pypers/europython05/Quixote-2.0/doc/upgrading.txt new file mode 100755 index 0000000..4d002cb --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/upgrading.txt @@ -0,0 +1,324 @@ +Upgrading code from older versions of Quixote +============================================= + +This document lists backward-incompatible changes in Quixote, and +explains how to update application code to work with the newer +version. + +Changes from 1.0 to 2.0 +------------------------- + +Change any imports you have from quixote.form to be from quixote.form1. + +Change any imports you have from quixote.form2 to be from quixote.form. + +Replace calls to HTTPRequest.get_form_var() with calls to get_field(). + +Define a create_publisher() function to get the publisher you need +and figure out how you want to connect it to web server. +See files in demo and server for examples. Note that publish1.py +contains a publisher that works more like the Quixote1 Publisher, +and does not require the changes listed below. + +Make every namespace be an instance of quixote.directory.Directory. +Update namespaces that are modules (or in the init.py of a package) by +defining a new class in the module that inherits from Directory and +moving your _q_exports and _q_* functions onto the class. Replace +"request" parameters with "self" parameters on the new methods. If +you have a _q_resolve method, include Resolving in the bases of your +new class. + +Remove request from calls to _q_ functions. If request, session, +user, path, or redirect is used in these new methods, replace as +needed with calls to get_request(), get_session(), get_user(), +get_path(), and/or redirect(), imported from quixote. + +In every namespace that formerly traversed into a module, import the +new Directory class from the module and create an instance of the +Directory in a variable whose name is the name of the module. + +In every namespace with a _q_exports and a _q_index, either add "" to +_q_exports or make sure that _q_lookup handles "" by returning the result +of a call to _q_index. + +If your code depends on the Publisher's namespace_stack attribute, +try using quixote.util.get_directory_path() instead. If you need the +namespace stack after the traversal, override Directory._q_traverse() +to call get_directory_path() when the end of the path is reached, and +record the result somewhere for later reference. + +If your code depends on _q_exception_handler, override the _q_traverse +on your root namespace or on your own Directory class to catch exceptions +and handle them the way you want. If you just want a general customization +for exception responses, you can change or override +Publisher.format_publish_error(). + +If your code depended on _q_access, include the AccessControlled with +the bases of your Directory classes as needed. + +Provide imports as needed to htmltext, TemplateIO, get_field, +get_request, get_session, get_user, get_path, redirect, ?. You may +find dulcinea/bin/unknown.py useful for identifying missing imports. + +Quixote 1's secure_errors configuration variable is not present in Quixote 2. + +Form.__init__ no longer has name or attrs keywords. If your existing +code calls Form.__init__ with 'attrs=foo', you'll need to change it to +'**foo'. Form instances no longer have a name attribute. If your code +looks for form.name, you can find it with form.attrs.get('name'). +The Form.__init__ keyword parameter (and attribute) 'action_url' is now +named 'action'. + +The SessionPublisher class is gone. Use the Publisher class instead. +Also, the 'session_mgr' keyword has been renamed to 'session_manager'. + + +Changes from 0.6.1 to 1.0 +------------------------- + +Sessions +******** + +A leading underscore was removed from the ``Session`` attributes +``__remote_address``, ``__creation_time``, and ``__access_time``. If +you have pickled ``Session`` objects you will need to upgrade them +somehow. Our preferred method is to write a script that unpickles each +object, renames the attributes and then re-pickles it. + + + +Changes from 0.6 to 0.6.1 +------------------------- + +``_q_exception_handler`` now called if exception while traversing +***************************************************************** + +``_q_exception_handler`` hooks will now be called if an exception is +raised during the traversal process. Quixote 0.6 had a bug that caused +``_q_exception_handler`` hooks to only be called if an exception was +raised after the traversal completed. + + + +Changes from 0.5 to 0.6 +----------------------- + +``_q_getname`` renamed to ``_q_lookup`` +*************************************** + +The ``_q_getname`` special function was renamed to ``_q_lookup``, +because that name gives a clearer impression of the function's +purpose. In 0.6, ``_q_getname`` still works but will trigger a +warning. + + +Form Framework Changes +********************** + +The ``quixote.form.form`` module was changed from a .ptl file to a .py +file. You should delete or move the existing ``quixote/`` directory +in ``site-packages`` before running ``setup.py``, or at least delete +the old ``form.ptl`` and ``form.ptlc`` files. + +The widget and form classes in the ``quixote.form`` package now return +``htmltext`` instances. Applications that use forms and widgets will +likely have to be changed to use the ``[html]`` template type to avoid +over-escaping of HTML special characters. + +Also, the constructor arguments to ``SelectWidget`` and its subclasses have +changed. This only affects applications that use the form framework +located in the ``quixote.form`` package. + +In Quixote 0.5, the ``SelectWidget`` constructor had this signature:: + + def __init__ (self, name, value=None, + allowed_values=None, + descriptions=None, + size=None, + sort=0): + +``allowed_values`` was the list of objects that the user could choose, +and ``descriptions`` was a list of strings that would actually be +shown to the user in the generated HTML. + +In Quixote 0.6, the signature has changed slightly:: + + def __init__ (self, name, value=None, + allowed_values=None, + descriptions=None, + options=None, + size=None, + sort=0): + +The ``quote`` argument is gone, and the ``options`` argument has been +added. If an ``options`` argument is provided, ``allowed_values`` +and ``descriptions`` must not be supplied. + +The ``options`` argument, if present, must be a list of tuples with +1,2, or 3 elements, of the form ``(value:any, description:any, +key:string)``. + + * ``value`` is the object that will be returned if the user chooses + this item, and must always be supplied. + + * ``description`` is a string or htmltext instance which will be + shown to the user in the generated HTML. It will be passed + through the htmlescape() functions, so for an ordinary string + special characters such as '&' will be converted to '&'. + htmltext instances will be left as they are. + + * If supplied, ``key`` will be used in the value attribute + of the option element (``<option value="...">``). + If not supplied, keys will be generated; ``value`` is checked for a + ``_p_oid`` attribute and if present, that string is used; + otherwise the description is used. + +In the common case, most applications won't have to change anything, +though the ordering of selection items may change due to the +difference in how keys are generated. + + +File Upload Changes +******************* + +Quixote 0.6 introduces new support for HTTP upload requests. Any HTTP +request with a Content-Type of "multipart/form-data" -- which is +generally only used for uploads -- is now represented by +HTTPUploadRequest, a subclass of HTTPRequest, and the uploaded files +themselves are represented by Upload objects. + +Whenever an HTTP request has a Content-Type of "multipart/form-data", +an instance of HTTPUploadRequest is created instead of HTTPRequest. +Some of the fields in the request are presumably uploaded files and +might be quite large, so HTTPUploadRequest will read all of the fields +supplied in the request body and write them out to temporary files; +the temporary files are written in the directory specified by the +UPLOAD_DIR configuration variable. + +Once the temporary files have been written, the HTTPUploadRequest +object is passed to a function or PTL template, just like an ordinary +request. The difference between HTTPRequest and HTTPUploadRequest +is that all of the form variables are represented as Upload objects. +Upload objects have three attributes: + +``orig_filename`` + the filename supplied by the browser. +``base_filename`` + a stripped-down version of orig_filename with unsafe characters removed. + This could be used when writing uploaded data to a permanent location. +``tmp_filename`` + the path of the temporary file containing the uploaded data for this field. + +Consult upload.txt for more information about handling file uploads. + + +Refactored `Publisher` Class +**************************** + +Various methods in the `Publisher` class were rearranged. If your +application subclasses Publisher, you may need to change your code +accordingly. + + * ``parse_request()`` no longer creates the HTTPRequest object; + instead a new method, ``create_request()``, handles this, + and can be overridden as required. + + As a result, the method signature has changed from + ``parse_request(stdin, env)`` to ``parse_request(request)``. + + * The ``Publisher.publish()`` method now catches exceptions raised + by ``parse_request()``. + + +Changes from 0.4 to 0.5 +----------------------- + +Session Management Changes +************************** + +The Quixote session management interface underwent lots of change and +cleanup with Quixote 0.5. It was previously undocumented (apart from +docstrings in the code), so we thought that this was a good opportunity +to clean up the interface. Nevertheless, those brave souls who got +session management working just by reading the code are in for a bit of +suffering; this brief note should help clarify things. The definitive +documentation for session management is session-mgmt.txt -- you should +start there. + + +Attribute renamings and pickled objects ++++++++++++++++++++++++++++++++++++++++ + +Most attributes of the standard Session class were made private in order +to reduce collisions with subclasses. The downside is that pickled +Session objects will break. You might want to (temporarily) modify +session.py and add this method to Session:: + + def __setstate__ (self, dict): + # Update for attribute renamings made in rev. 1.51.2.3 + # (between Quixote 0.4.7 and 0.5). + self.__dict__.update(dict) + if hasattr(self, 'remote_address'): + self.__remote_address = self.remote_address + del self.remote_address + if hasattr(self, 'creation_time'): + self.__creation_time = self.creation_time + del self.creation_time + if hasattr(self, 'access_time'): + self.__access_time = self.access_time + del self.access_time + if hasattr(self, 'form_tokens'): + self._form_tokens = self.form_tokens + del self.form_tokens + +However, if your sessions were pickled via ZODB, this may not work. (It +didn't work for us.) In that case, you'll have to add something like +this to your class that inherits from both ZODB's Persistent and +Quixote's Session:: + + def __setstate__ (self, dict): + # Blechhh! This doesn't work if I put it in Quixote's + # session.py, so I have to second-guess how Python + # treats "__" attribute names. + self.__dict__.update(dict) + if hasattr(self, 'remote_address'): + self._Session__remote_address = self.remote_address + del self.remote_address + if hasattr(self, 'creation_time'): + self._Session__creation_time = self.creation_time + del self.creation_time + if hasattr(self, 'access_time'): + self._Session__access_time = self.access_time + del self.access_time + if hasattr(self, 'form_tokens'): + self._form_tokens = self.form_tokens + del self.form_tokens + +It's not pretty, but it worked for us. + + +Cookie domains and paths +++++++++++++++++++++++++ + +The session cookie config variables -- ``COOKIE_NAME``, +``COOKIE_DOMAIN``, and ``COOKIE_PATH`` -- have been renamed to +``SESSION_COOKIE_*`` for clarity. + +If you previously set the config variable ``COOKIE_DOMAIN`` to the name +of your server, this is most likely no longer necessary -- it's now fine +to leave ``SESSION_COOKIE_DOMAIN`` unset (ie. ``None``), which +ultimately means browsers will only include the session cookie in +requests to the same server that sent it to them in the first place. + +If you previously set ``COOKIE_PATH``, then you should probably preserve +your setting as ``SESSION_COOKIE_PATH``. The default of ``None`` means +that browsers will only send session cookies with requests for URIs +under the URI that originally resulted in the session cookie being sent. +See session-mgmt.txt and RFCs 2109 and 2965. + +If you previously set ``COOKIE_NAME``, change it to +``SESSION_COOKIE_NAME``. + + + + diff --git a/pypers/europython05/Quixote-2.0/doc/web-server.txt b/pypers/europython05/Quixote-2.0/doc/web-server.txt new file mode 100755 index 0000000..2abfe21 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/web-server.txt @@ -0,0 +1,258 @@ +Web Server Configuration for Quixote +==================================== + +For a simple Quixote installation, there are two things you have to get +right: + +* installation of the Quixote modules to Python's library (the + trick here is that the ``quixote`` package must be visible to the user + that CGI scripts run as, not necessarily to you as an interactive + command-line user) + +* configuration of your web server to run Quixote driver scripts + +This document is concerned with the second of these. + + +Which web servers? +------------------ + +We are only familiar with Apache, and we develop Quixote for use under +Apache. However, Quixote doesn't rely on any Apache-specific tricks; +if you can execute CGI scripts, then you can run Quixote applications +(although they'll run a lot faster with mod_scgi or FastCGI). If you +can redirect arbitrary URLs to a CGI script and preserve parts of the +URL as an add-on to the script name (with ``PATH_INFO``), then you can +run Quixote applications in the ideal manner, ie. with superfluous +implementation details hidden from the user. + + +Which operating systems? +------------------------ + +We are mainly familiar with Unix, and develop and deploy Quixote under +Linux. However, we've had several reports of people using Quixote under +Windows, more-or-less successfully. There are still a few Unix-isms in +the code, but they are being rooted out in favor of portability. + +Remember that your system is only as secure as its weakest link. +Quixote can't help you write secure web applications on an inherently +insecure operating system. + + +Basic CGI configuration +----------------------- + +Throughout this document, I'm going to assume that: + +* CGI scripts live in the ``/www/cgi-bin`` directory of your web server, + and have the extension ``.cgi`` + +* HTTP requests for ``/cgi-bin/foo.cgi`` will result in the execution + of ``/www/cgi-bin/foo.cgi`` (for various values of ``foo``) + +* if the web server is instructed to serve an executable file + ``bar.cgi``, the file is treated as a CGI script + +With Apache, these configuration directives will do the trick:: + + AddHandler cgi-script .cgi + ScriptAlias /cgi-bin/ /www/cgi-bin/ + +Consult the Apache documentation for other ways of configuring CGI +script execution. + +For other web servers, consult your server's documentation. + + +Installing driver scripts +------------------------- + +Given the above configuration, installing a Quixote driver script is the +same as installing any other CGI script: copy it to ``/www/cgi-bin`` (or +whatever). To install the Quixote demo's cgi driver script:: + + cp -p server/cgi_server.py /www/cgi-bin/demo.cgi + +(The ``-p`` option ensures that ``cp`` preserves the file mode, so that +it remains executable.) + + +URL rewriting +------------- + +With the above configuration, users need to use URLs like :: + + http://www.example.com/cgi-bin/demo.cgi + +to access the Quixote demo (or other Quixote applications installed in +the same way). This works, but it's ugly and unnecessarily exposes +implementation details. + +In our view, it's preferable to give each Quixote application its own +chunk of URL-space -- a "virtual directory" if you like. For example, +you might want :: + + http://www.example.com/qdemo + +to handle the Quixote demo. + +With Apache, this is quite easy, as long as mod_rewrite is compiled, +loaded, and enabled. (Building and loading Apache modules is beyond the +scope of this document; consult the Apache documentation.) + +To enable the rewrite engine, use the :: + + RewriteEngine on + +directive. If you have virtual hosts, make sure to repeat this for each +``<VirtualHost>`` section of your config file. + +The rewrite rule to use in this case is :: + + RewriteRule ^/qdemo(/.*) /www/cgi-bin/demo.cgi$1 [last] + +This is *not* a redirect; this is all handled with one HTTP +request/response cycle, and the user never sees ``/cgi-bin/demo.cgi`` in +a URL. + +Note that requests for ``/qdemo/`` and ``/qdemo`` are *not* the same; in +particular, with the above rewrite rule, the former will succeed and the +latter will not. (Look at the regex again if you don't believe me: +``/qdemo`` doesn't match the regex, so ``demo.cgi`` is never invoked.) + +The solution for ``/qdemo`` is the same as if it corresponded to a +directory in your document tree: redirect it to ``/qdemo/``. Apache +(and, presumably, other web servers) does this automatically for "real" +directories; however, ``/qdemo/`` is just a directory-like chunk of +URL-space, so either you or Quixote have to take care of the redirect. + +It's almost certainly faster for you to take care of it in the web +server's configuration. With Apache, simply insert this directive +*before* the above rewrite rule:: + + RewriteRule ^/qdemo$ /qdemo/ [redirect=permanent] + +If, for some reason, you are unwilling or unable to instruct your web +server to perform this redirection, Quixote will do it for you. +However, you have to make sure that the ``/qdemo`` URL is handled by +Quixote. Change the rewrite rule to:: + + RewriteRule ^/qdemo(/.*)?$ /www/cgi-bin/demo.cgi$1 [last] + +Now a request for ``/qdemo`` will be handled by Quixote, and it will +generate a redirect to ``/qdemo/``. If you're using a CGI driver +script, this will be painfully slow, but it will work. + +For redirecting and rewriting URLs with other web servers, consult your +server's documentation. + + +Long-running processes +---------------------- + +For serious web applications, CGI is unacceptably slow. For a CGI-based +Quixote application, you have to start a Python interpreter, load the +Quixote modules, and load your application's modules before you can +start working. For sophisticated, database-backed applications, you'll +probably have to open a new database connection as well for every hit. + +Small wonder so many high-performance alternatives to CGI exist. (The +main advantages of CGI are that it is widely supported and easy to +develop with. Even for large Quixote applications, running in CGI mode +is nice in development because you don't have to kill a long-running +driver script every time the code changes.) Quixote includes support +for mod_scgi and FastCGI. + + +mod_scgi configuration +---------------------- + +SCGI is a CGI replacement written by Neil Schemenauer, one of +Quixote's developers, and is similar to FastCGI but is designed to be +easier to implement. mod_scgi simply forwards requests to an +already-running SCGI server on a different TCP port, and doesn't try +to start or stop processes, leaving that up to the SCGI server. + +The SCGI code is available from http://www.mems-exchange.org/software/scgi/ . + +The quixote.server.scgi_server module is a script that +publishes the demo quixote application via SCGI. You can use +it for your application by importing it and calling the ``run()`` +function with arguments to run your application, on the port +you choose. Here is an example:: + + #!/usr/bin/python + from quixote.server.scgi_server import run + from quixote.publish import Publisher + from mymodule import MyRootDirectory + + def create_my_publisher(): + return Publisher(MyRootDirectory()) + + run(create_my_publisher, port=3001) + +The following Apache directive will direct requests to an SCGI server +running on port 3001:: + + <Location /> + SCGIServer 127.0.0.1 3001 + SCGIHandler On + </Location> + +[Note: the mod_scgi module for Apache 2 requires a colon, instead of a +space, between the host and port on the SCGIServer line.] + + +SCGI through CGI +---------------- + +Recent releases of the scgi package include cgi2scgi.c, a small program +that offers an extremely convenient way to take advantage of SCGI using +Apache or any web server that supports CGI. To use it, compile the +cgi2scgi.c and install the compiled program as usual for your +webserver. The default SCGI port is 3000, but you can change that +by adding ``-DPORT=3001`` (for example) to your compile command. + +Although this method requires a new process to be launched for each +request, the process is small and fast, so the performance is +acceptable for many applications. + + +FastCGI configuration +--------------------- + +If your web server supports FastCGI, you can significantly speed up your +Quixote applications with a simple change to your configuration. You +don't have to change your code at all (unless it makes assumptions about +how many requests are handled by each process). (See +http://www.fastcgi.com/ for more information on FastCGI.) + +To use FastCGI with Apache, you'll need to download mod_fastcgi from +http://www.fastcgi.com/ and add it to your Apache installation. + +Configuring a FastCGI driver script is best done after reading the fine +documentation for mod_fastcgi at +http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html + +However, if you just want to try it with the Quixote demo to see if it +works, add this directive to your Apache configuration:: + + AddHandler fastcgi-script .fcgi + +and copy server/fastcgi_server.py to demo.fcgi. If you're using a URL +rewrite to map requests for (eg.) ``/qdemo`` to +``/www/cgi-bin/demo.cgi``, be sure to change the rewrite -- it should +now point to ``/www/cgi-bin/demo.fcgi``. + +After the first access to ``demo.fcgi`` (or ``/qdemo/`` with the +modified rewrite rule), the demo should be noticeably faster. You +should also see a ``demo.fcgi`` process running if you do ``ps -le`` +(``ps -aux`` on BSD-ish systems, or maybe ``ps aux``). (On my 800 MHz +Athlon machine, there are slight but perceptible delays navigating the +Quixote demo in CGI mode. In FastCGI mode, the delay between pages is +no longer perceptible -- navigation is instantaneous.) The larger your +application is, the more code it loads, and the more work it does at +startup, the bigger a win FastCGI will be for you (in comparison to CGI). + + diff --git a/pypers/europython05/Quixote-2.0/doc/web-services.txt b/pypers/europython05/Quixote-2.0/doc/web-services.txt new file mode 100755 index 0000000..c89125c --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/web-services.txt @@ -0,0 +1,169 @@ +Implementing Web Services with Quixote +====================================== + +This document will show you how to implement Web services using +Quixote. + + +An XML-RPC Service +------------------ + +XML-RPC is the simplest protocol commonly used to expose a Web +service. In XML-RPC, there are a few basic data types such as +integers, floats, strings, and dates, and a few aggregate types such +as arrays and structs. The xmlrpclib module, part of the Python 2.2 +standard library and available separately from +http://www.pythonware.com/products/xmlrpc/, converts between Python's +standard data types and the XML-RPC data types. + +============== ===================== +XML-RPC Type Python Type or Class +-------------- --------------------- +<int> int +<double> float +<string> string +<array> list +<struct> dict +<boolean> xmlrpclib.Boolean +<base64> xmlrpclib.Binary +<dateTime> xmlrpclib.DateTime +============== ===================== + + +Making XML-RPC Calls +-------------------- + +Making an XML-RPC call using xmlrpclib is easy. An XML-RPC server +lives at a particular URL, so the first step is to create an +xmlrpclib.ServerProxy object pointing at that URL. :: + + >>> import xmlrpclib + >>> s = xmlrpclib.ServerProxy( + 'http://www.stuffeddog.com/speller/speller-rpc.cgi') + +Now you can simply make a call to the spell-checking service offered +by this server:: + + >>> s.speller.spellCheck('my speling isnt gud', {}) + [{'word': 'speling', 'suggestions': ['apeling', 'spelding', + 'spelling', 'sperling', 'spewing', 'spiling'], 'location': 4}, + {'word': 'isnt', 'suggestions': [``isn't'', 'ist'], 'location': 12}] + >>> + +This call results in the following XML being sent:: + + <?xml version='1.0'?> + <methodCall> + <methodName>speller.spellCheck</methodName> + <params> + <param> + <value><string>my speling isnt gud</string></value> + </param> + <param> + <value><struct></struct></value> + </param> + </params> + </methodCall> + + +Writing a Quixote Service +------------------------- + +In the quixote.util module, Quixote provides a function, +``xmlrpc(request, func)``, that processes the body of an XML-RPC +request. ``request`` is the HTTPRequest object that Quixote passes to +every function it invokes. ``func`` is a user-supplied function that +receives the name of the XML-RPC method being called and a tuple +containing the method's parameters. If there's a bug in the function +you supply and it raises an exception, the ``xmlrpc()`` function will +catch the exception and return a ``Fault`` to the remote caller. + +Here's an example of implementing a simple XML-RPC handler with a +single method, ``get_time()``, that simply returns the current +time. The first task is to expose a URL for accessing the service. :: + + from quixote.directory import Directory + from quixote.util import xmlrpc + from quixote import get_request + + class RPCDirectory(Directory): + + _q_exports = ['rpc'] + + def rpc (self): + return xmlrpc(get_request(), rpc_process) + + def rpc_process (meth, params): + ... + +When the above code is placed in the __init__.py file for the Python +package corresponding to your Quixote application, it exposes the URL +``http://<hostname>/rpc`` as the access point for the XML-RPC service. + +Next, we need to fill in the contents of the ``rpc_process()`` +function:: + + import time + + def rpc_process (meth, params): + if meth == 'get_time': + # params is ignored + now = time.gmtime(time.time()) + return xmlrpclib.DateTime(now) + else: + raise RuntimeError, "Unknown XML-RPC method: %r" % meth + +``rpc_process()`` receives the method name and the parameters, and its +job is to run the right code for the method, returning a result that +will be marshalled into XML-RPC. The body of ``rpc_process()`` will +therefore usually be an ``if`` statement that checks the name of the +method, and calls another function to do the actual work. In this case, +``get_time()`` is very simple so the two lines of code it requires are +simply included in the body of ``rpc_process()``. + +If the method name doesn't belong to a supported method, execution +will fall through to the ``else`` clause, which will raise a +RuntimeError exception. Quixote's ``xmlrpc()`` will catch this +exception and report it to the caller as an XML-RPC fault, with the +error code set to 1. + +As you add additional XML-RPC services, the ``if`` statement in +``rpc_process()`` will grow more branches. You might be tempted to pass +the method name to ``getattr()`` to select a method from a module or +class. That would work, too, and avoids having a continually growing +set of branches, but you should be careful with this and be sure that +there are no private methods that a remote caller could access. I +generally prefer to have the ``if... elif... elif... else`` blocks, for +three reasons: 1) adding another branch isn't much work, 2) it's +explicit about the supported method names, and 3) there won't be any +security holes in doing so. + +An alternative approach is to have a dictionary mapping method names +to the corresponding functions and restrict the legal method names +to the keys of this dictionary:: + + def echo (*params): + # Just returns the parameters it's passed + return params + + def get_time (): + now = time.gmtime(time.time()) + return xmlrpclib.DateTime(now) + + methods = {'echo' : echo, + 'get_time' : get_time} + + def rpc_process (meth, params): + func = methods.get[meth] + if methods.has_key(meth): + # params is ignored + now = time.gmtime(time.time()) + return xmlrpclib.DateTime(now) + else: + raise RuntimeError, "Unknown XML-RPC method: %r" % meth + +This approach works nicely when there are many methods and the +``if...elif...else`` statement would be unworkably long. + + +$Id: web-services.txt 25695 2004-11-30 20:53:44Z dbinger $ diff --git a/pypers/europython05/Quixote-2.0/doc/widgets.txt b/pypers/europython05/Quixote-2.0/doc/widgets.txt new file mode 100755 index 0000000..0dc3597 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/widgets.txt @@ -0,0 +1,524 @@ +Quixote Widget Classes +====================== + +[This is reference documentation. If you haven't yet read "Lesson 5: +widgets" of demo.txt, you should go and do so now. This document also +assumes you have a good understanding of HTML forms and form elements. +If not, you could do worse than pick up a copy of *HTML: The Definitive +Guide* by Chuck Musciano & Bill Kennedy (O'Reilly). I usually keep it +within arm's reach.] + +Web forms are built out of form elements: string input, select lists, +checkboxes, submit buttons, and so forth. Quixote provides a family of +classes for handling these form elements, or widgets, in the +quixote.form.widget module. The class hierarchy is:: + + Widget [A] + | + +--StringWidget + | | + | +--PasswordWidget + | | + | +--NumberWidget [*] [A] + | | + | +-FloatWidget [*] + | +-IntWidget [*] + | + +--TextWidget + | + +--CheckboxWidget + | + +--SelectWidget [A] + | | + | +--SingleSelectWidget + | | | + | | +-RadiobuttonsWidget + | | | + | | +-OptionSelectWidget [*] + | | + | +--MultipleSelectWidget + | + +--SubmitButtonWidget + | + +--HiddenWidget + | + +--ListWidget [*] + + [*] Widget classes that do not correspond exactly with a particular + HTML form element + [A] Abstract classes + + +Widget: the base class +---------------------- + +Widget is the abstract base class for the widget hierarchy. It provides +the following facilities: + +* widget name (``name`` attribute, ``set_name()`` method) +* widget value (``value`` attribute, ``set_value()`` and ``clear()`` methods) +* ``__str__()`` and ``__repr__()`` methods +* some facilities for writing composite widget classes + +The Widget constructor signature is:: + + Widget(name : string, value : any = None) + +``name`` + the name of the widget. For non-compound widgets (ie. everything in + the above class hierarchy), this will be used as the "name" + attribute for the main HTML tag that defines the form element. + +``value`` + the current value of the form element. The type of 'value' depends + on the widget class. Most widget classes support only a single + type, eg. StringWidget always deals with strings and IntWidget with + integers. The SelectWidget classes are different; see the + descriptions below for details. + + +Common widget methods +--------------------- + +The Widget base class also provides a couple of useful +methods: + +``set_name(name:string)`` + use this to change the widget name supplied to the constructor. + Unless you know what you're doing, you should do this before + rendering or parsing the widget. + +``set_value(value:any)`` + use this to set the widget value; this is the same as supplying + a value to the constructor (and the same type rules apply, ie. + the type of 'value' depends on the widget class). + +``clear()`` + clear the widget's current value. Equivalent to + ``widget.set_value(None)``. + +The following two methods will be used on every widget object you +create; if you write your own widget classes, you will almost certainly +have to define both of these: + +``render(request:HTTPRequest)`` : ``string`` + return a chunk of HTML that implements the form element + corresponding to this widget. + +``parse(request:HTTPRequest)`` : ``any`` + extract the form value for this widget from ``request.form``, parse it + according to the rules for this widget class, and return the + resulting value. The return value depends on the widget class, and + will be of the same type as the value passed to the constructor + and/or ``set_value()``. + + +StringWidget +------------ + +Used for short, single-line string input with no validation (ie. any +string will be accepted.) Generates an ``<input type="text">`` form +element. + +Constructor +~~~~~~~~~~~ +:: + + StringWidget(name : string, + value : string = None, + size : int = None, + maxlength : int = None) + +``size`` + used as the ``size`` attribute of the generated ``<input>`` tag; + controls the physical size of the input field. + +``maxlength`` + used as the ``maxlength`` attribute; controls the maximum amount + of input. + +Examples +~~~~~~~~ +:: + + >>> StringWidget("foo", value="hello").render(request) + '<input name="foo" type="text" value="hello">' + + >>> StringWidget("foo", size=10, maxlength=20).render(request) + '<input name="foo" type="text" size="10" maxlength="20">' + + +PasswordWidget +-------------- + +PasswordWidget is identical to StringWidget except for the type of the +HTML form element: ``password`` instead of ``text``. + + +TextWidget +---------- + +Used for multi-line text input. The value is a single string with +newlines right where the browser supplied them. (``\r\n``, if present, +is converted to ``\n``.) Generates a ``<textarea>`` form element. + +Constructor +~~~~~~~~~~~ +:: + + TextWidget(name : string, + value : string = None, + cols : int = None, + rows : int = None, + wrap : string = "physical") + +``cols``, ``rows`` + number of columns/rows in the textarea +``wrap`` + controls how the browser wraps text and includes newlines in the + submitted form value; consult an HTML book for details. + + +CheckboxWidget +-------------- + +Used for single boolean (on/off) value. The value you supply can be +anything, since Python has a boolean interpretation for all values; the +value returned by ``parse()`` will always be 0 or 1 (but you shouldn't +rely on that!). Generates an ``<input type="checkbox">`` form element. + +Constructor +~~~~~~~~~~~ +:: + + CheckboxWidget(name : string, + value : boolean = false) + +Examples +~~~~~~~~ +:: + + >>> CheckboxWidget("foo", value=0).render(request) + '<input name="foo" type="checkbox" value="yes">' + + >>> CheckboxWidget("foo", value="you bet").render(request) + '<input name="foo" type="checkbox" value="yes" checked>' + + +RadiobuttonsWidget +------------------ + +Used for a *set* of related radiobuttons, ie. several ``<input +type="radio">`` tags with the same name and different values. The set +of values are supplied to the constructor as ``allowed_values``, which +may be a list of any Python objects (not just strings). The current +value must be either ``None`` (the default) or one of the values in +``allowed_values``; if you supply a ``value`` not in ``allowed_values``, +it will be ignored. ``parse()`` will return either ``None`` or one of +the values in ``allowed_values``. + +Constructor +~~~~~~~~~~~ +:: + + RadiobuttonsWidget(name : string, + value : any = None, + allowed_values : [any] = None, + descriptions : [string] = map(str, allowed_values), + quote : boolean = true, + delim : string = "\n") + +``allowed_values`` + specifies how many ``<input type="radio">`` tags to generate and the + values for each. Eg. ``allowed_values=["foo", "bar"]`` will result in + (roughly):: + + <input type="radio" value="foo"> + <input type="radio" value="bar"> + +``descriptions`` + the text that will actually be shown to the user in the web page + that includes this widget. Handy when the elements of + ``allowed_values`` are too terse, or don't have a meaningful + ``str()``, or you want to add some additional cues for the user. If + not supplied, ``map(str, allowed_values)`` is used, with the + exception that ``None`` in ``allowed_values`` becomes ``""`` (the + empty string) in ``descriptions``. If supplied, ``descriptions`` + must be the same length as ``allowed_values``. + +``quote`` + if true (the default), the elements of 'descriptions' will be + HTML-quoted (using ``quixote.html.html_quote()``) when the widget is + rendered. This is essential if you might have characters like + ``&`` or ``<`` in your descriptions. However, you'll want to set + ``quote`` to false if you are deliberately including HTML markup + in your descriptions. + +``delim`` + the delimiter to separate the radiobuttons with when rendering + the whole widget. The default ensures that your HTML is readable + (by putting each ``<input>`` tag on a separate line), and that there + is horizontal whitespace between each radiobutton. + +Examples +~~~~~~~~ +:: + + >>> colours = ["red", "green", "blue", "pink"] + >>> widget = RadiobuttonsWidget("foo", allowed_values=colours) + >>> print widget.render(request) + <input name="foo" type="radio" value="0">red</input> + <input name="foo" type="radio" value="1">green</input> + <input name="foo" type="radio" value="2">blue</input> + <input name="foo" type="radio" value="3">pink</input> + +(Note that the actual form values, ie. what the browser returns to the +server, are always stringified indices into the 'allowed_values' list. +This is irrelevant to you, since SingleSelectWidget takes care of +converting ``"1"`` to ``1`` and looking up ``allowed_values[1]``.) + +:: + + >>> values = [val1, val2, val3] + >>> descs = ["thing <b>1</b>", + "thing <b>2</b>", + "thing <b>3</b>"] + >>> widget = RadiobuttonsWidget("bar", + allowed_values=values, + descriptions=descs, + value=val3, + delim="<br>\n", + quote=0) + >>> print widget.render(request) + <input name="bar" type="radio" value="0">thing <b>1</b></input><br> + <input name="bar" type="radio" value="1">thing <b>2</b></input><br> + <input name="bar" type="radio" value="2" checked="checked">thing <b>3</b></input> + + +SingleSelectWidget +------------------ + +Used to select a single value from a list that's too long or ungainly +for a set of radiobuttons. (Most browsers implement this as a scrolling +list; UNIX versions of Netscape 4.x and earlier used a pop-up menu.) +The value can be any Python object; ``parse()`` will return either +``None`` or one of the values you supply to the constructor as +``allowed_values``. Generates a ``<select>...</select>`` tag, with one +``<option>`` tag for each element of ``allowed_values``. + +Constructor +~~~~~~~~~~~ +:: + + SingleSelectWidget(name : string, + value : any = None, + allowed_values : [any] = None, + descriptions : [string] = map(str, allowed_values), + quote : boolean = true, + size : int = None) + +``allowed_values`` + determines the set of ``<option>`` tags that will go inside the + ``<select>`` tag; these can be any Python values (not just strings). + ``parse()`` will return either one of the ``allowed_values`` or ``None``. + If you supply a ``value`` that is not in ``allowed_values``, it + will be ignored. + +``descriptions`` + (same as RadiobuttonsWidget above) + +``quote`` + (same as RadiobuttonsWidget above) + +``size`` + corresponds to the ``size`` attribute of the ``<select>`` tag: ask + the browser to show a select list with ``size`` items visible. + Not always respected by the browser; consult an HTML book. + +Examples +~~~~~~~~ +:: + + >>> widget = SingleSelectWidget("foo", + allowed_values=["abc", 123, 5.5]) + >>> print widget.render(request) + <select name="foo"> + <option value="0">abc + <option value="1">123 + <option value="2">5.5 + </select> + + >>> widget = SingleSelectWidget("bar", + value=val2, + allowed_values=[val1, val2, val3], + descriptions=["foo", "bar", "foo & bar"], + size=3) + >>> print widget.render(request) + <select name="bar" size="3"> + <option value="0">foo + <option selected value="1">bar + <option value="2">foo & bar + </select> + + +MultipleSelectWidget +-------------------- + +Used to select multiple values from a list. Everything is just like +SingleSelectWidget, except that ``value`` can be a list of objects +selected from ``allowed_values`` (in which case every object in ``value`` +will initially be selected). Generates a ``<select multiple>...</select>`` +tag, with one ``<option>`` tag for each element of ``allowed_values``. + +Constructor +~~~~~~~~~~~ +:: + + MultipleSelectWidget(name : string, + value : any | [any] = None, + allowed_values : [any] = None, + descriptions : [string] = map(str, allowed_values), + quote : boolean = true, + size : int = None) + + +SubmitButtonWidget +------------------ + +Used for generating submit buttons. Note that HTML submit buttons are +rather weird, and Quixote preserves this weirdness -- the Widget classes +are meant to be a fairly thin wrapper around HTML form elements, after +all. + +In particular, the widget value for a submit button controls two things: +what the user sees in their browser (the text in the button) and what +the browser returns as the value for that form element. You can't +control the two separately, as you can with radiobuttons or selection +widgets. + +Also, SubmitButtonWidget is the only widget with an optional ``name``. +In many simple forms, all you care about is the fact that the form was +submitted -- which submit button the user used doesn't matter. + +Constructor +~~~~~~~~~~~ +:: + + SubmitButtonWidget(name : string = None, + value : string = None) + +``value`` + the text that will be shown in the user's browser, *and* the + value that will be returned for this form element (widget) + if the user selects this submit button. + +Examples +~~~~~~~~ + + >>> SubmitButtonWidget(value="Submit Form").render(request) + '<input type="submit" value="Submit Form">' + + +HiddenWidget +------------ + +Used to generate HTML hidden widgets, which can be useful for carrying +around non-sensitive application state. (The Quixote form framework +uses hidden widgets for form tokens as a measure against cross-site +request forgery [CSRF] attacks. So by "sensitive" I mean "information +which should not be revealed", rather than "security-related". If you +wouldn't put it in a cookie or in email, don't put it in a hidden form +element.) + +Constructor +~~~~~~~~~~~ +:: + + HiddenWidget(name : string, + value : string) + +Examples +~~~~~~~~ +:: + + >>> HiddenWidget("form_id", "2452345135").render(request) + '<input type="hidden" name="form_id" value="2452345135">' + + +IntWidget +--------- + +The first derived widget class: this is a subclass of StringWidget +specifically for entering integer values. As such, this is the first +widget class we've covered that can reject certain user input. (The +selection widgets all have to validate their input in case of broken or +malicious clients, but they just drop bogus values.) If the user enters +a string that Python's built-in ``int()`` can't convert to an integer, +IntWidget's ``parse()`` method raises FormValueError (also defined in +the quixote.form.widget module). This exception is handled by Quixote's +form framework, but if you're using widget objects on their own, you'll +have to handle it yourself. + +``IntWidget.parse()`` always returns an integer or ``None``. + +Constructor +~~~~~~~~~~~ +:: + + IntWidget(name : string, + value : int = None, + size : int = None, + maxlength : int = None) + +Constructor arguments are as for StringWidget, except that ``value`` +must be an integer (or ``None``). Note that ``size`` and +``maxlength`` have exactly the same meaning: they control the size of +the input widget and the maximum number of characters of input. + +[Examples] + + >>> IntWidget("num", value=37, size=5).render(request) + '<input type="string" name="num" value="37" size="5">' + + +FloatWidget +----------- + +FloatWidget is identical to IntWidget, except: + +* ``value`` must be a float +* ``parse()`` returns a float or ``None`` +* ``parse()`` raises FormValueError if the string entered by the + user cannot be converted by Python's built-in ``float()`` function + + +OptionSelectWidget +------------------ + +OptionSelectWidget is simply a SingleSelectWidget that uses a bit of +Javascript to automatically submit the current form as soon as the user +selects a value. This is useful for very simple one-element forms where +you don't want to bother with a submit button, or for very complex forms +where you need to revamp the user interface based on a user's selection. +Your form-processing code could then detect that style of form +submission, and regenerate a slightly different form for the user. (Or +you could treat it as a full-blown form submission, if the only widget +of interest is the OptionSelectWidget.) + +For example, if you're asking a user for their address, some of the +details will vary depending on which country they're in. You might make +the country widget an OptionSelectWidget: if the user selects "Canada", +you'll ask them for a province and a postal code; if they select "United +States", you ask for a state and a zip code; and so forth. (I don't +really recommend a user interface that works this way: you'll spend way +too much time getting the details right ["How many states does Australia +have again?"], and you're bound to get something wrong -- there are over +200 countries in the world, after all.) + +Be warned that since OptionSelectWidget relies on Javascript to work, +using it makes immediately makes your application less portable and more +fragile. One thing to avoid: form elements with a name of ``submit``, +since that masks the Javascript function called by OptionSelectWidget. + + +$Id: widgets.txt 20217 2003-01-16 20:51:53Z akuchlin $ diff --git a/pypers/europython05/Quixote-2.0/doc/win32.txt b/pypers/europython05/Quixote-2.0/doc/win32.txt new file mode 100755 index 0000000..9204f10 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/doc/win32.txt @@ -0,0 +1,14 @@ +Compiling Quixote c extensions for Windows using the mingw compiler: + +The variable build_extensions in setup.py must be set to True (instead +of "sys.platform != win32"). Then, Quixote can be installed through + + python setup.py build_ext --compiler=mingw32 install + +The mingw compiler can be downloaded from +http://www.bloodshed.net/dev/devcpp.html. Probably you need to list +the bin directory (C:\Program files\Dev-Cpp\bin) in the PATH. + +To permit gcc to link against Python library, you also need a +libpython2x.a file. Instructions for its creation are provided at +http://sebsauvage.net/python/mingw.html. diff --git a/pypers/europython05/Quixote-2.0/errors.py b/pypers/europython05/Quixote-2.0/errors.py new file mode 100755 index 0000000..f33dc41 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/errors.py @@ -0,0 +1,141 @@ +"""quixote.errors +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/errors.py $ +$Id: errors.py 26378 2005-03-17 14:04:45Z dbinger $ + +Exception classes used by Quixote +""" +from quixote.html import htmltext, htmlescape + +__revision__ = "$Id: errors.py 26378 2005-03-17 14:04:45Z dbinger $" + + +class PublishError(Exception): + """PublishError exceptions are raised due to some problem with the + data provided by the client and are raised during the publishing + process. Quixote will abort the current request and return an error + page to the client. + + public_msg should be a user-readable message that reveals no + inner workings of your application; it will always be shown. + + private_msg will only be shown if the config option DISPLAY_EXCEPTIONS is + true; Quixote uses this to give you more detail about why the error + occurred. You might want to use it for similar, application-specific + information. (DISPLAY_EXCEPTIONS should always be false in a production + environment, since these details about the inner workings of your + application could conceivably be useful to attackers.) + + The formatting done by the Quixote versions of these exceptions is + very simple. Applications will probably wish to raise application + specific subclasses which do more sophisticated formatting or provide + a _q_except handler to format the exception. + + """ + + status_code = 400 # bad request + title = "Publishing error" + description = "no description" + + def __init__(self, public_msg=None, private_msg=None): + self.public_msg = public_msg + self.private_msg = private_msg # cleared if DISPLAY_EXCEPTIONS is false + + def __str__(self): + return self.private_msg or self.public_msg or "???" + + def format(self): + msg = htmlescape(self.title) + if self.public_msg: + msg = msg + ": " + self.public_msg + if self.private_msg: + msg = msg + ": " + self.private_msg + return msg + + +class TraversalError(PublishError): + """ + Raised when a client attempts to access a resource that does not + exist or is otherwise unavailable to them (eg. a Python function + not listed in its module's _q_exports list). + + path should be the path to the requested resource; if not + supplied, the current request object will be fetched and its + get_path() method called. + """ + + status_code = 404 # not found + title = "Page not found" + description = ("The requested link does not exist on this site. If " + "you arrived here by following a link from an external " + "page, please inform that page's maintainer.") + + def __init__(self, public_msg=None, private_msg=None, path=None): + PublishError.__init__(self, public_msg, private_msg) + if path is None: + import quixote + path = quixote.get_request().get_path() + self.path = path + + def format(self): + msg = htmlescape(self.title) + ": " + self.path + if self.public_msg: + msg = msg + ": " + self.public_msg + if self.private_msg: + msg = msg + ": " + self.private_msg + return msg + +class RequestError(PublishError): + """ + Raised when Quixote is unable to parse an HTTP request (or its CGI + representation). This is a lower-level error than QueryError -- it + either means that Quixote is not smart enough to handle the request + being passed to it, or the user-agent is broken and/or malicious. + """ + status_code = 400 + title = "Invalid request" + description = "Unable to parse HTTP request." + + +class QueryError(PublishError): + """Should be raised if bad data was provided in the query part of a + URL or in the content of a POST request. What constitutes bad data is + solely application dependent (eg: letters in a form field when the + application expects a number). + """ + + status_code = 400 + title = "Invalid query" + description = ("An error occurred while handling your request. The " + "query data provided as part of the request is invalid.") + + + +class AccessError(PublishError): + """Should be raised if the client does not have access to the + requested resource. Usually applications will raise this error from + an _q_access method. + """ + + status_code = 403 + title = "Access denied" + description = ("An error occurred while handling your request. " + "Access to the requested resource was not permitted.") + + + +def format_publish_error(exc): + """(exc : PublishError) -> string + + Format a PublishError exception as a web page. + """ + return htmltext("""\ + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" + "http://www.w3.org/TR/REC-html40/strict.dtd"> + <html> + <head><title>Error: %s</title></head> + <body> + <p>%s</p> + <p>%s</p> + </body> + </html> + """) % (exc.title, exc.description, exc.format()) diff --git a/pypers/europython05/Quixote-2.0/form/__init__.py b/pypers/europython05/Quixote-2.0/form/__init__.py new file mode 100755 index 0000000..5685c8a --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form/__init__.py @@ -0,0 +1,18 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/__init__.py $ +$Id: __init__.py 26469 2005-04-05 10:26:03Z dbinger $ + +The web interface framework, consisting of Form and Widget base classes +(and a bunch of standard widget classes recognized by Form). +Application developers will typically create a Form instance each +form in their application; each form object will contain a number +of widget objects. Custom widgets can be created by inheriting +and/or composing the standard widget classes. +""" + +from quixote.form.form import Form, FormTokenWidget +from quixote.form.widget import Widget, StringWidget, FileWidget, \ + PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ + SingleSelectWidget, SelectWidget, OptionSelectWidget, \ + MultipleSelectWidget, SubmitWidget, HiddenWidget, \ + FloatWidget, IntWidget, subname, WidgetValueError, CompositeWidget, \ + WidgetList, WidgetDict diff --git a/pypers/europython05/Quixote-2.0/form/compatibility.py b/pypers/europython05/Quixote-2.0/form/compatibility.py new file mode 100755 index 0000000..4ad0389 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form/compatibility.py @@ -0,0 +1,101 @@ +'''$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/compatibility.py $ +$Id: compatibility.py 26061 2005-02-11 02:48:16Z dbinger $ + +A Form subclass that provides close to the same API as the old form +class (useful for transitioning existing forms). +''' + +from quixote import get_request, get_path, redirect +from quixote.form import Form as _Form, Widget, StringWidget, FileWidget, \ + PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ + SingleSelectWidget, SelectWidget, OptionSelectWidget, \ + MultipleSelectWidget, SubmitWidget, HiddenWidget, \ + FloatWidget, IntWidget +from quixote.html import url_quote + +_widget_names = { + "string" : StringWidget, + "file" : FileWidget, + "password" : PasswordWidget, + "text" : TextWidget, + "checkbox" : CheckboxWidget, + "single_select" : SingleSelectWidget, + "radiobuttons" : RadiobuttonsWidget, + "multiple_select" : MultipleSelectWidget, + "submit_button" : SubmitWidget, + "hidden" : HiddenWidget, + "float" : FloatWidget, + "int" : IntWidget, + "option_select" : OptionSelectWidget, +} + + +class Form(_Form): + def __init__(self, action_url=None, *args, **kwargs): + _Form.__init__(self, action=action_url, *args, **kwargs) + self.cancel_url = None + self.action_url = self.action + + def add_widget(self, widget_class, name, value=None, + title=None, hint=None, required=False, **kwargs): + try: + widget_class = _widget_names[widget_class] + except KeyError: + pass + self.add(widget_class, name, value=value, title=title, hint=hint, + required=required, **kwargs) + + def add_submit_button(self, name, value): + self.add_submit(name, value) + + def add_cancel_button(self, caption, url): + self.add_submit("cancel", caption) + self.cancel_url = url + + def get_action_url(self): + action_url = url_quote(get_path()) + query = get_request().get_query() + if query: + action_url += "?" + query + return action_url + + def render(self, action_url=None): + if action_url: + self.action_url = action_url + return _Form.render(self) + + def process(self): + values = {} + request = get_request() + for name, widget in self._names.items(): + values[name] = widget.parse() + return values + + def action(self, submit, values): + raise NotImplementedError, "sub-classes must implement 'action()'" + + def handle(self): + """handle() -> string + + Master method for handling forms. It should be called after + initializing a form. Controls form action based on a request. You + probably should override 'process' and 'action' instead of + overriding this method. + """ + request = get_request() + if not self.is_submitted(): + return self.render(self.action_url) + submit = self.get_submit() + if submit == "cancel": + return redirect(self.cancel_url) + values = self.process() + if submit == True: + # The form was submitted by an unregistered submit button, assume + # that the submission was required to update the layout of the form. + self.clear_errors() + return self.render(self.action_url) + + if self.has_errors(): + return self.render(self.action_url) + else: + return self.action(submit, values) diff --git a/pypers/europython05/Quixote-2.0/form/css.py b/pypers/europython05/Quixote-2.0/form/css.py new file mode 100755 index 0000000..c2849f8 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form/css.py @@ -0,0 +1,76 @@ + +BASIC_FORM_CSS = """\ +form.quixote div.title { + font-weight: bold; +} + +form.quixote br.submit, +form.quixote br.widget, +br.quixoteform { + clear: left; +} + +form.quixote div.submit br.widget { + display: none; +} + +form.quixote div.widget { + float: left; + padding: 4px; + padding-right: 1em; + margin-bottom: 6px; +} + +/* pretty forms (attribute selector hides from broken browsers (e.g. IE) */ +form.quixote[action] { + float: left; +} + +form.quixote[action] > div.widget { + float: none; +} + +form.quixote[action] > br.widget { + display: none; +} + +form.quixote div.widget div.widget { + padding: 0; + margin-bottom: 0; +} + +form.quixote div.SubmitWidget { + float: left +} + +form.quixote div.content { + margin-left: 0.6em; /* indent content */ +} + +form.quixote div.content div.content { + margin-left: 0; /* indent content only for top-level widgets */ +} + +form.quixote div.error { + color: #c00; + font-size: small; + margin-top: .1em; +} + +form.quixote div.hint { + font-size: small; + font-style: italic; + margin-top: .1em; +} + +form.quixote div.errornotice { + color: #c00; + padding: 0.5em; + margin: 0.5em; +} + +form.quixote div.FormTokenWidget, +form.quixote.div.HiddenWidget { + display: none; +} +""" diff --git a/pypers/europython05/Quixote-2.0/form/form.py b/pypers/europython05/Quixote-2.0/form/form.py new file mode 100755 index 0000000..fd91fd4 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form/form.py @@ -0,0 +1,365 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/form.py $ +$Id: form.py 26200 2005-02-18 22:58:30Z dbinger $ + +Provides the Form class and related classes. Forms are a convenient +way of building HTML forms that are composed of Widget objects. +""" + +from quixote import get_request, get_session, get_publisher +from quixote.html import htmltag, htmltext, TemplateIO +from quixote.form.widget import HiddenWidget, StringWidget, TextWidget, \ + CheckboxWidget, SingleSelectWidget, RadiobuttonsWidget, \ + MultipleSelectWidget, ResetWidget, SubmitWidget, FloatWidget, \ + IntWidget, PasswordWidget + + +class FormTokenWidget(HiddenWidget): + + def _parse(self, request): + token = request.form.get(self.name) + session = get_session() + if not session.has_form_token(token): + self.error = 'invalid' # this error does not get displayed + else: + session.remove_form_token(token) + + def render_error(self, error): + return '' + + def render(self): + self.value = get_session().create_form_token() + return HiddenWidget.render(self) + + +class Form(object): + """ + Provides a high-level mechanism for collecting and processing user + input that is based on HTML forms. + + Instance attributes: + widgets : [Widget] + widgets that are not subclasses of SubmitWidget or HiddenWidget + submit_widgets : [SubmitWidget] + subclasses of SubmitWidget, normally rendered at the end of the + form + hidden_widgets : [HiddenWidget] + subclasses of HiddenWidget, normally rendered at the beginning + of the form + _names : { name:string : Widget } + names used in the form and the widgets associated with them + """ + + TOKEN_NAME = "_form_id" # name of hidden token widget + + JAVASCRIPT_MARKUP = htmltext('<script type="text/javascript">\n' + '<!--\n' + '%s\n' + '// -->\n' + '</script>\n') + + TOKEN_NOTICE = htmltext('<div class="errornotice">' + 'The form you have submitted is invalid. Most ' + 'likely it has been successfully submitted once ' + 'already. Please review the the form data ' + 'and submit the form again.' + '</div>') + + ERROR_NOTICE = htmltext('<div class="errornotice">' + 'There were errors processing your form. ' + 'See below for details.' + '</div>') + + def __init__(self, + method="post", + action=None, + enctype=None, + use_tokens=True, + **attrs): + + if method not in ("post", "get"): + raise ValueError("Form method must be 'post' or 'get', " + "not %r" % method) + self.method = method + self.action = action or self._get_default_action() + if 'class' not in attrs: + attrs['class'] = 'quixote' + self.attrs = attrs + self.widgets = [] + self.submit_widgets = [] + self.hidden_widgets = [] + self._names = {} + + if enctype is not None and enctype not in ( + "application/x-www-form-urlencoded", "multipart/form-data"): + raise ValueError, ("Form enctype must be " + "'application/x-www-form-urlencoded' or " + "'multipart/form-data', not %r" % enctype) + self.enctype = enctype + + if use_tokens and self.method == "post": + config = get_publisher().config + if config.form_tokens: + # unique token for each form, this prevents many cross-site + # attacks and prevents a form from being submitted twice + self.add(FormTokenWidget, self.TOKEN_NAME, value=None) + + def _get_default_action(self): + query = get_request().get_query() + if query: + return "?" + query + else: + return "" + + # -- Form data access methods -------------------------------------- + + def __getitem__(self, name): + """(name:string) -> any + Return a widget's value. Raises KeyError if widget named 'name' + does not exist. + """ + try: + return self._names[name].parse() + except KeyError: + raise KeyError, 'no widget named %r' % name + + def has_key(self, name): + """Return true if the widget named 'name' is in the form.""" + return self._names.has_key(name) + + def get(self, name, default=None): + """(name:string, default=None) -> any + Return a widget's value. Returns 'default' if widget named 'name' + does not exist. + """ + widget = self._names.get(name) + if widget is not None: + return widget.parse() + else: + return default + + def get_widget(self, name): + """(name:string) -> Widget | None + Return the widget named 'name'. Returns None if the widget does + not exist. + """ + return self._names.get(name) + + def get_submit_widgets(self): + """() -> [SubmitWidget] + """ + return self.submit_widgets + + def get_all_widgets(self): + """() -> [Widget] + Return all the widgets that have been added to the form. Note that + this while this list includes submit widgets and hidden widgets, it + does not include sub-widgets (e.g. widgets that are part of + CompositeWidgets) + """ + return self._names.values() + + # -- Form processing and error checking ---------------------------- + + def is_submitted(self): + """() -> bool + + Return true if a form was submitted. If the form method is 'POST' + and the page was not requested using 'POST', then the form is not + considered to be submitted. If the form method is 'GET' then the + form is considered submitted if there is any form data in the + request. + """ + request = get_request() + if self.method == 'post': + if request.get_method() == 'POST': + return True + else: + return False + else: + return bool(request.form) + + def has_errors(self): + """() -> bool + + Ensure that all components of the form have parsed themselves. Return + true if any of them have errors. + """ + request = get_request() + has_errors = False + if self.is_submitted(): + for widget in self.get_all_widgets(): + if widget.has_error(request=request): + has_errors = True + return has_errors + + def clear_errors(self): + """Ensure that all components of the form have parsed themselves. + Clear any errors that might have occured during parsing. + """ + request = get_request() + for widget in self.get_all_widgets(): + widget.clear_error(request) + + def get_submit(self): + """() -> string | bool + + Get the name of the submit button that was used to submit the + current form. If the form is submitted but not by any known + SubmitWidget then return True. Otherwise, return False. + """ + request = get_request() + for button in self.submit_widgets: + if button.parse(request): + return button.name + else: + if self.is_submitted(): + return True + else: + return False + + def set_error(self, name, error): + """(name : string, error : string) + Set the error attribute of the widget named 'name'. + """ + widget = self._names.get(name) + if not widget: + raise KeyError, "unknown name %r" % name + widget.set_error(error) + + # -- Form population methods --------------------------------------- + + def add(self, widget_class, name, *args, **kwargs): + if self._names.has_key(name): + raise ValueError, "form already has '%s' widget" % name + widget = widget_class(name, *args, **kwargs) + self._names[name] = widget + if isinstance(widget, SubmitWidget): + self.submit_widgets.append(widget) # will be rendered at end + elif isinstance(widget, HiddenWidget): + self.hidden_widgets.append(widget) # will be render at beginning + else: + self.widgets.append(widget) + + # convenience methods + + def add_submit(self, name, value=None, **kwargs): + self.add(SubmitWidget, name, value, **kwargs) + + def add_reset(self, name, value=None, **kwargs): + self.add(ResetWidget, name, value, **kwargs) + + def add_hidden(self, name, value=None, **kwargs): + self.add(HiddenWidget, name, value, **kwargs) + + def add_string(self, name, value=None, **kwargs): + self.add(StringWidget, name, value, **kwargs) + + def add_text(self, name, value=None, **kwargs): + self.add(TextWidget, name, value, **kwargs) + + def add_password(self, name, value=None, **kwargs): + self.add(PasswordWidget, name, value, **kwargs) + + def add_checkbox(self, name, value=None, **kwargs): + self.add(CheckboxWidget, name, value, **kwargs) + + def add_single_select(self, name, value=None, **kwargs): + self.add(SingleSelectWidget, name, value, **kwargs) + + def add_multiple_select(self, name, value=None, **kwargs): + self.add(MultipleSelectWidget, name, value, **kwargs) + + def add_radiobuttons(self, name, value=None, **kwargs): + self.add(RadiobuttonsWidget, name, value, **kwargs) + + def add_float(self, name, value=None, **kwargs): + self.add(FloatWidget, name, value, **kwargs) + + def add_int(self, name, value=None, **kwargs): + self.add(IntWidget, name, value, **kwargs) + + + # -- Layout (rendering) methods ------------------------------------ + + def render(self): + """() -> HTML text + Render a form as HTML. + """ + r = TemplateIO(html=True) + r += self._render_start() + r += self._render_body() + r += self._render_finish() + return r.getvalue() + + def _render_start(self): + r = TemplateIO(html=True) + r += htmltag('form', method=self.method, + enctype=self.enctype, action=self.action, + **self.attrs) + r += self._render_hidden_widgets() + return r.getvalue() + + def _render_finish(self): + r = TemplateIO(html=True) + r += htmltext('</form><br class="quixoteform" />') + code = get_request().response.javascript_code + if code: + r += self._render_javascript(code) + return r.getvalue() + + def _render_widgets(self): + r = TemplateIO(html=True) + for widget in self.widgets: + r += widget.render() + return r.getvalue() + + def _render_hidden_widgets(self): + r = TemplateIO(html=True) + for widget in self.hidden_widgets: + r += widget.render() + return r.getvalue() + + def _render_submit_widgets(self): + r = TemplateIO(html=True) + if self.submit_widgets: + r += htmltext('<div class="submit">') + for widget in self.submit_widgets: + r += widget.render() + r += htmltext('</div><br class="submit" />') + return r.getvalue() + + def _render_error_notice(self): + token_widget = self.get_widget(self.TOKEN_NAME) + if token_widget is not None and token_widget.has_error(): + # form tokens are enabled but the token data in the request + # does not match anything in the session. It could be an + # a cross-site attack but most likely the back button has + # be used + return self.TOKEN_NOTICE + else: + return self.ERROR_NOTICE + + def _render_javascript(self, javascript_code): + """Render javacript code for the form. Insert code lexically + sorted by code_id. + """ + form_code = [] + code_ids = javascript_code.keys() + code_ids.sort() + for code_id in code_ids: + code = javascript_code[code_id] + if code: + form_code.append(code) + javascript_code[code_id] = '' + if form_code: + return self.JAVASCRIPT_MARKUP % htmltext(''.join(form_code)) + else: + return '' + + def _render_body(self): + r = TemplateIO(html=True) + if self.has_errors(): + r += self._render_error_notice() + r += self._render_widgets() + r += self._render_submit_widgets() + return r.getvalue() diff --git a/pypers/europython05/Quixote-2.0/form/widget.py b/pypers/europython05/Quixote-2.0/form/widget.py new file mode 100755 index 0000000..b989766 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form/widget.py @@ -0,0 +1,951 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form/widget.py $ +$Id: widget.py 26514 2005-04-07 13:29:50Z dbinger $ + +Provides the basic web widget classes: Widget itself, plus StringWidget, +TextWidget, CheckboxWidget, etc. +""" + +import struct +from quixote import get_request +from quixote.html import htmltext, htmlescape, htmltag, TemplateIO, stringify +from quixote.http_request import Upload + +def subname(prefix, name): + """Create a unique name for a sub-widget or sub-component.""" + # $ is nice because it's valid as part of a Javascript identifier + return "%s$%s" % (prefix, name) + + +def merge_attrs(base, overrides): + """({string: any}, {string: any}) -> {string: any} + """ + items = [] + if base: + items.extend(base.items()) + if overrides: + items.extend(overrides.items()) + attrs = {} + for name, val in items: + if name.endswith('_'): + name = name[:-1] + attrs[name] = val + return attrs + + +class WidgetValueError(Exception): + """May be raised a widget has problems parsing its value.""" + + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return stringify(self.msg) + + + +class Widget(object): + """Abstract base class for web widgets. + + Instance attributes: + name : string + value : any + error : string + title : string + hint : string + required : bool + attrs : {string: any} + _parsed : bool + + Feel free to access these directly; to set them, use the 'set_*()' + modifier methods. + """ + + REQUIRED_ERROR = 'required' + + def __init__(self, name, value=None, title="", hint="", required=False, + render_br=True, attrs=None, **kwattrs): + assert self.__class__ is not Widget, "abstract class" + self.name = name + self.value = value + self.error = None + self.title = title + self.hint = hint + self.required = required + self.render_br = render_br + self.attrs = merge_attrs(attrs, kwattrs) + self._parsed = False + + def __repr__(self): + return "<%s at %x: %s>" % (self.__class__.__name__, + id(self), + self.name) + + def __str__(self): + return "%s: %s" % (self.__class__.__name__, self.name) + + def get_name(self): + return self.name + + def set_value(self, value): + self.value = value + + def set_error(self, error): + self.error = error + + def get_error(self, request=None): + self.parse(request=request) + return self.error + + def has_error(self, request=None): + return bool(self.get_error(request=request)) + + def clear_error(self, request=None): + self.parse(request=request) + self.error = None + + def set_title(self, title): + self.title = title + + def get_title(self): + return self.title + + def set_hint(self, hint): + self.hint = hint + + def get_hint(self): + return self.hint + + def is_required(self): + return self.required + + def parse(self, request=None): + if not self._parsed: + self._parsed = True + if request is None: + request = get_request() + if request.form or request.get_method() == 'POST': + try: + self._parse(request) + except WidgetValueError, exc: + self.set_error(stringify(exc)) + if (self.required and self.value is None and + not self.has_error()): + self.set_error(self.REQUIRED_ERROR) + return self.value + + def _parse(self, request): + # subclasses may override but this is not part of the public API + value = request.form.get(self.name) + if isinstance(value, basestring) and value.strip(): + self.value = value + else: + self.value = None + + def render_title(self, title): + if title: + if self.required: + title += htmltext('<span class="required">*</span>') + return htmltext('<div class="title">%s</div>') % title + else: + return '' + + def render_hint(self, hint): + if hint: + return htmltext('<div class="hint">%s</div>') % hint + else: + return '' + + def render_error(self, error): + if error: + return htmltext('<div class="error">%s</div>') % error + else: + return '' + + def render(self): + r = TemplateIO(html=True) + classnames = '%s widget' % self.__class__.__name__ + r += htmltext('<div class="%s">') % classnames + r += self.render_title(self.get_title()) + r += htmltext('<div class="content">') + r += self.render_content() + r += self.render_hint(self.get_hint()) + r += self.render_error(self.get_error()) + r += htmltext('</div>') + r += htmltext('</div>') + if self.render_br: + r += htmltext('<br class="%s" />') % classnames + r += htmltext('\n') + return r.getvalue() + + def render_content(self): + raise NotImplementedError + +# class Widget + +# -- Fundamental widget types ------------------------------------------ +# These correspond to the standard types of input tag in HTML: +# text StringWidget +# password PasswordWidget +# radio RadiobuttonsWidget +# checkbox CheckboxWidget +# +# and also to the other basic form elements: +# <textarea> TextWidget +# <select> SingleSelectWidget +# <select multiple> +# MultipleSelectWidget + +class StringWidget(Widget): + """Widget for entering a single string: corresponds to + '<input type="text">' in HTML. + + Instance attributes: + value : string + """ + + # This lets PasswordWidget be a trivial subclass + HTML_TYPE = "text" + + def render_content(self): + return htmltag("input", xml_end=True, + type=self.HTML_TYPE, + name=self.name, + value=self.value, + **self.attrs) + + +class FileWidget(StringWidget): + """Subclass of StringWidget for uploading files. + + Instance attributes: none + """ + + HTML_TYPE = "file" + + def _parse(self, request): + parsed_value = request.form.get(self.name) + if isinstance(parsed_value, Upload): + self.value = parsed_value + else: + self.value = None + + +class PasswordWidget(StringWidget): + """Trivial subclass of StringWidget for entering passwords (different + widget type because HTML does it that way). + + Instance attributes: none + """ + + HTML_TYPE = "password" + + +class TextWidget(Widget): + """Widget for entering a long, multi-line string; corresponds to + the HTML "<textarea>" tag. + + Instance attributes: + value : string + """ + + def _parse(self, request): + Widget._parse(self, request) + if self.value and self.value.find("\r\n") >= 0: + self.value = self.value.replace("\r\n", "\n") + + def render_content(self): + return (htmltag("textarea", name=self.name, **self.attrs) + + htmlescape(self.value or "") + + htmltext("</textarea>")) + + +class CheckboxWidget(Widget): + """Widget for a single checkbox: corresponds to "<input + type=checkbox>". Do not put multiple CheckboxWidgets with the same + name in the same form. + + Instance attributes: + value : boolean + """ + + def _parse(self, request): + self.value = request.form.has_key(self.name) + + def render_content(self): + return htmltag("input", xml_end=True, + type="checkbox", + name=self.name, + value="yes", + checked=self.value and "checked" or None, + **self.attrs) + + + +class SelectWidget(Widget): + """Widget for single or multiple selection; corresponds to + <select name=...> + <option value="Foo">Foo</option> + ... + </select> + + Instance attributes: + options : [ (value:any, description:any, key:string) ] + value : any + The value is None or an element of dict(options.values()). + """ + + SELECTION_ERROR = "invalid value selected" + + def __init__(self, name, value=None, options=None, sort=False, + verify_selection=True, **kwargs): + assert self.__class__ is not SelectWidget, "abstract class" + Widget.__init__(self, name, value, **kwargs) + self.options = [] + if not options: + raise ValueError, "a non-empty list of 'options' is required" + else: + self.set_options(options, sort) + self.verify_selection = verify_selection + + def get_allowed_values(self): + return [item[0] for item in self.options] + + def get_descriptions(self): + return [item[1] for item in self.options] + + def set_value(self, value): + self.value = None + for object, description, key in self.options: + if value == object: + self.value = value + break + + def _generate_keys(self, values, descriptions): + """Called if no keys were provided. Try to generate a set of keys + that will be consistent between rendering and parsing. + """ + # try to use ZODB object IDs + keys = [] + for value in values: + if value is None: + oid = "" + else: + oid = getattr(value, "_p_oid", None) + if not oid: + break + hi, lo = struct.unpack(">LL", oid) + oid = "%x" % ((hi << 32) | lo) + keys.append(oid) + else: + # found OID for every value + return keys + # can't use OIDs, try using descriptions + used_keys = {} + keys = map(str, descriptions) + for key in keys: + if used_keys.has_key(key): + raise ValueError, "duplicated descriptions (provide keys)" + used_keys[key] = 1 + return keys + + def set_options(self, options, sort=False): + """(options: [objects:any], sort=False) + or + (options: [(object:any, description:any)], sort=False) + or + (options: [(object:any, description:any, key:any)], sort=False) + """ + + """ + Set the options list. The list of options can be a list of objects, in + which case the descriptions default to map(htmlescape, objects) + applying htmlescape() to each description and + key. + If keys are provided they must be distinct. If the sort keyword + argument is true, sort the options by case-insensitive lexicographic + order of descriptions, except that options with value None appear + before others. + """ + if options: + first = options[0] + values = [] + descriptions = [] + keys = [] + if isinstance(first, tuple): + if len(first) == 2: + for value, description in options: + values.append(value) + descriptions.append(description) + elif len(first) == 3: + for value, description, key in options: + values.append(value) + descriptions.append(description) + keys.append(stringify(key)) + else: + raise ValueError, 'invalid options %r' % options + else: + values = descriptions = options + + if not keys: + keys = self._generate_keys(values, descriptions) + + options = zip(values, descriptions, keys) + + if sort: + def make_sort_key(option): + value, description, key = option + if value is None: + return ('', option) + else: + return (stringify(description).lower(), option) + doptions = map(make_sort_key, options) + doptions.sort() + options = [item[1] for item in doptions] + self.options = options + + def _parse_single_selection(self, parsed_key, default=None): + for value, description, key in self.options: + if key == parsed_key: + return value + else: + if self.verify_selection: + self.error = self.SELECTION_ERROR + return default + elif self.options: + return self.options[0][0] + else: + return default + + def set_allowed_values(self, allowed_values, descriptions=None, + sort=False): + """(allowed_values:[any], descriptions:[any], sort:boolean=False) + + Set the options for this widget. The allowed_values and descriptions + parameters must be sequences of the same length. The sort option + causes the options to be sorted using case-insensitive lexicographic + order of descriptions, except that options with value None appear + before others. + """ + if descriptions is None: + self.set_options(allowed_values, sort) + else: + assert len(descriptions) == len(allowed_values) + self.set_options(zip(allowed_values, descriptions), sort) + + def is_selected(self, value): + return value == self.value + + def render_content(self): + tags = [htmltag("select", name=self.name, **self.attrs)] + for object, description, key in self.options: + if self.is_selected(object): + selected = 'selected' + else: + selected = None + if description is None: + description = "" + r = htmltag("option", value=key, selected=selected) + tags.append(r + htmlescape(description) + htmltext('</option>')) + tags.append(htmltext("</select>")) + return htmltext("\n").join(tags) + + +class SingleSelectWidget(SelectWidget): + """Widget for single selection. + """ + + SELECT_TYPE = "single_select" + MULTIPLE_SELECTION_ERROR = "cannot select multiple values" + + def _parse(self, request): + parsed_key = request.form.get(self.name) + if parsed_key: + if isinstance(parsed_key, list): + self.error = self.MULTIPLE_SELECTION_ERROR + else: + self.value = self._parse_single_selection(parsed_key) + else: + self.value = None + + +class RadiobuttonsWidget(SingleSelectWidget): + """Widget for a *set* of related radiobuttons -- all have the + same name, but different values (and only one of those values + is returned by the whole group). + + Instance attributes: + delim : string = None + string to emit between each radiobutton in the group. If + None, a single newline is emitted. + """ + + SELECT_TYPE = "radiobuttons" + + def __init__(self, name, value=None, options=None, delim=None, **kwargs): + SingleSelectWidget.__init__(self, name, value, options=options, + **kwargs) + if delim is None: + self.delim = "\n" + else: + self.delim = delim + + def render_content(self): + tags = [] + for object, description, key in self.options: + if self.is_selected(object): + checked = 'checked' + else: + checked = None + r = htmltag("input", xml_end=True, + type="radio", + name=self.name, + value=key, + checked=checked, + **self.attrs) + tags.append(r + htmlescape(description)) + return htmlescape(self.delim).join(tags) + + +class MultipleSelectWidget(SelectWidget): + """Widget for multiple selection. + + Instance attributes: + value : [any] + for multipe selects, the value is None or a list of + elements from dict(self.options).values() + """ + + SELECT_TYPE = "multiple_select" + + def __init__(self, name, value=None, options=None, **kwargs): + SelectWidget.__init__(self, name, value, options=options, + multiple='multiple', **kwargs) + + def set_value(self, value): + allowed_values = self.get_allowed_values() + if value in allowed_values: + self.value = [ value ] + elif isinstance(value, (list, tuple)): + self.value = [ element + for element in value + if element in allowed_values ] or None + else: + self.value = None + + def is_selected(self, value): + if self.value is None: + return value is None + else: + return value in self.value + + def _parse(self, request): + parsed_keys = request.form.get(self.name) + if parsed_keys: + if isinstance(parsed_keys, list): + self.value = [value + for value, description, key in self.options + if key in parsed_keys] or None + else: + _marker = [] + value = self._parse_single_selection(parsed_keys, _marker) + if value is _marker: + self.value = None + else: + self.value = [value] + else: + self.value = None + + +class ButtonWidget(Widget): + """ + Instance attributes: + label : string + value : boolean + """ + + HTML_TYPE = "button" + + def __init__(self, name, value=None, **kwargs): + Widget.__init__(self, name, value=None, **kwargs) + self.set_label(value) + + def set_label(self, label): + self.label = label + + def get_label(self): + return self.label + + def render_content(self): + # slightly different behavior here, we always render the + # tag using the 'value' passed in as a parameter. 'self.value' + # is a boolean that is true if the button's name appears + # in the request. + value = (self.label and htmlescape(self.label) or None) + return htmltag("input", xml_end=True, type=self.HTML_TYPE, + name=self.name, value=value, **self.attrs) + + def _parse(self, request): + self.value = request.form.has_key(self.name) + + +class SubmitWidget(ButtonWidget): + HTML_TYPE = "submit" + +class ResetWidget(SubmitWidget): + HTML_TYPE = "reset" + + +class HiddenWidget(Widget): + """ + Instance attributes: + value : string + """ + + def set_error(self, error): + if error is not None: + raise TypeError, 'error not allowed on hidden widgets' + + def render_content(self): + if self.value is None: + value = None + else: + value = htmlescape(self.value) + return htmltag("input", xml_end=True, + type="hidden", + name=self.name, + value=value, + **self.attrs) + + def render(self): + return self.render_content() # Input elements of type hidden have no decoration. + +# -- Derived widget types ---------------------------------------------- +# (these don't correspond to fundamental widget types in HTML, +# so they're separated) + +class NumberWidget(StringWidget): + """ + Instance attributes: none + """ + + # Parameterize the number type (either float or int) through + # these class attributes: + TYPE_OBJECT = None # eg. int, float + TYPE_ERROR = None # human-readable error message + + def __init__(self, name, value=None, **kwargs): + assert self.__class__ is not NumberWidget, "abstract class" + assert value is None or type(value) is self.TYPE_OBJECT, ( + "form value '%s' not a %s: got %r" % (name, + self.TYPE_OBJECT, + value)) + StringWidget.__init__(self, name, value, **kwargs) + + def _parse(self, request): + StringWidget._parse(self, request) + if self.value is not None: + try: + self.value = self.TYPE_OBJECT(self.value) + except ValueError: + self.error = self.TYPE_ERROR + + +class FloatWidget(NumberWidget): + """ + Instance attributes: + value : float + """ + TYPE_OBJECT = float + TYPE_ERROR = "must be a number" + + +class IntWidget(NumberWidget): + """ + Instance attributes: + value : int + """ + TYPE_OBJECT = int + TYPE_ERROR = "must be an integer" + + +class OptionSelectWidget(SingleSelectWidget): + """Widget for single selection with automatic submission. Parse + will always return a value from it's options, even if the form is + not submitted. This allows its value to be used to decide what + other widgets need to be created in a form. It's a powerful + feature but it can be hard to understand what's going on. + + Instance attributes: + value : any + """ + + SELECT_TYPE = "option_select" + + def __init__(self, name, value=None, options=None, **kwargs): + SingleSelectWidget.__init__(self, name, value, options=options, + onchange='submit()', **kwargs) + + def parse(self, request=None): + if not self._parsed: + if request is None: + request = get_request() + self._parse(request) + self._parsed = True + return self.value + + def _parse(self, request): + parsed_key = request.form.get(self.name) + if parsed_key: + if isinstance(parsed_key, list): + self.error = self.MULTIPLE_SELECTION_ERROR + else: + self.value = self._parse_single_selection(parsed_key) + elif self.value is None: + self.value = self.options[0][0] + + def render_content(self): + return (SingleSelectWidget.render_content(self) + + htmltext('<noscript>' + '<input type="submit" name="" value="apply" />' + '</noscript>')) + + +class CompositeWidget(Widget): + """ + Instance attributes: + widgets : [Widget] + _names : {name:string : Widget} + """ + def __init__(self, name, value=None, **kwargs): + Widget.__init__(self, name, value, **kwargs) + self.widgets = [] + self._names = {} + + def _parse(self, request): + for widget in self.widgets: + widget.parse(request) + + def __getitem__(self, name): + return self._names[name].parse() + + def get(self, name): + widget = self._names.get(name) + if widget: + return widget.parse() + return None + + def get_widget(self, name): + return self._names.get(name) + + def get_widgets(self): + return self.widgets + + def clear_error(self, request=None): + Widget.clear_error(self, request) + for widget in self.widgets: + widget.clear_error(request) + + def set_widget_error(self, name, error): + self._names[name].set_error(error) + + def has_error(self, request=None): + has_error = False + if Widget.has_error(self, request=request): + has_error = True + for widget in self.widgets: + if widget.has_error(request=request): + has_error = True + return has_error + + def add(self, widget_class, name, *args, **kwargs): + if self._names.has_key(name): + raise ValueError, 'the name %r is already used' % name + if self.attrs.get('disabled') and 'disabled' not in kwargs: + kwargs['disabled'] = True + widget = widget_class(subname(self.name, name), *args, **kwargs) + self._names[name] = widget + self.widgets.append(widget) + + def render_content(self): + r = TemplateIO(html=True) + for widget in self.get_widgets(): + r += widget.render() + return r.getvalue() + + +class WidgetList(CompositeWidget): + """A variable length list of widgets. There is only one + title and hint but each element of the list can have its own + error. You can also set an error on the WidgetList itself (e.g. as a + result of higher-level processing). + + Instance attributes: + element_names : [string] + """ + + def __init__(self, name, value=None, + element_type=StringWidget, + element_kwargs={}, + add_element_label="Add row", **kwargs): + assert value is None or type(value) is list, ( + "value '%s' not a list: got %r" % (name, value)) + assert issubclass(element_type, Widget), ( + "value '%s' element_type not a Widget: " + "got %r" % (name, element_type)) + assert type(element_kwargs) is dict, ( + "value '%s' element_kwargs not a dict: " + "got %r" % (name, element_kwargs)) + assert type(add_element_label) in (str, htmltext), ( + "value '%s'add_element_label not a string: " + "got %r" % (name, add_element_label)) + + CompositeWidget.__init__(self, name, value, **kwargs) + self.element_names = [] + + self.add(HiddenWidget, 'added_elements') + added_elements_widget = self.get_widget('added_elements') + + + def add_element(value=None): + name = "element%d" % len(self.element_names) + self.add(element_type, name, value=value, **element_kwargs) + self.element_names.append(name) + + # Add element widgets for initial value + if value is not None: + for element_value in value: + add_element(value=element_value) + + # Add at least one additional element widget + num_added = int(added_elements_widget.parse() or 1) + for i in range(num_added): + add_element() + + # Add submit to add more element widgets + self.add(SubmitWidget, 'add_element', value=add_element_label) + if self.get('add_element'): + add_element() + num_added += 1 + added_elements_widget.set_value(num_added) + + def _parse(self, request): + values = [] + for name in self.element_names: + value = self.get(name) + if value is not None: + values.append(value) + self.value = values or None + + def render_content(self): + r = TemplateIO(html=True) + add_element_widget = self.get_widget('add_element') + for widget in self.get_widgets(): + if widget is add_element_widget: + continue + r += widget.render() + r += add_element_widget.render() + return r.getvalue() + + def render(self): + r = TemplateIO(html=True) + r += self.render_title(self.get_title()) + add_element_widget = self.get_widget('add_element') + for widget in self.get_widgets(): + if widget is add_element_widget: + continue + r += widget.render() + r += add_element_widget.render() + r += self.render_hint(self.get_hint()) + return r.getvalue() + + +class WidgetDict(CompositeWidget): + """A variable length dict of widgets. There is only one + title and hint but each element of the dict can have its own + error. You can also set an error on the WidgetDict itself (e.g. as a + result of higher-level processing). + + Instance attributes: + element_names : [string] + """ + + def __init__(self, name, value=None, title='', hint='', + element_key_type=StringWidget, + element_value_type=StringWidget, + element_key_kwargs={}, + element_value_kwargs={}, + add_element_label='Add row', **kwargs): + assert value is None or type(value) is dict, ( + 'value %r not a dict: got %r' % (name, value)) + assert issubclass(element_key_type, Widget), ( + "value '%s' element_key_type not a Widget: " + "got %r" % (name, element_key_type)) + assert issubclass(element_value_type, Widget), ( + "value '%s' element_value_type not a Widget: " + "got %r" % (name, element_value_type)) + assert type(element_key_kwargs) is dict, ( + "value '%s' element_key_kwargs not a dict: " + "got %r" % (name, element_key_kwargs)) + assert type(element_value_kwargs) is dict, ( + "value '%s' element_value_kwargs not a dict: " + "got %r" % (name, element_value_kwargs)) + assert type(add_element_label) in (str, htmltext), ( + 'value %r element_name not a string: ' + 'got %r' % (name, add_element_label)) + + CompositeWidget.__init__(self, name, value, **kwargs) + self.element_names = [] + + self.add(HiddenWidget, 'added_elements') + added_elements_widget = self.get_widget('added_elements') + + def add_element(key=None, value=None): + name = 'element%d' % len(self.element_names) + self.add(element_key_type, name + 'key', + value=key, render_br=False, **element_key_kwargs) + self.add(element_value_type, name + 'value', + value=value, **element_value_kwargs) + self.element_names.append(name) + + # Add element widgets for initial value + if value is not None: + for key, element_value in value.items(): + add_element(key=key, value=element_value) + + # Add at least one additional element widget + num_added = int(added_elements_widget.parse() or 1) + for i in range(num_added): + add_element() + + # Add submit to add more element widgets + self.add(SubmitWidget, 'add_element', value=add_element_label) + if self.get('add_element'): + add_element() + num_added += 1 + added_elements_widget.set_value(num_added) + + def _parse(self, request): + values = {} + for name in self.element_names: + key = self.get(name + 'key') + value = self.get(name + 'value') + if key and value: + values[key] = value + self.value = values or None + + def render_content(self): + r = TemplateIO(html=True) + for name in self.element_names: + if name in ('add_element', 'added_elements'): + continue + key_widget = self.get_widget(name + 'key') + value_widget = self.get_widget(name + 'value') + r += htmltext('%s<div class="widget">: </div>%s') % ( + key_widget.render(), + value_widget.render()) + if self.render_br: + r += htmltext('<br clear="left" class="widget" />') + r += htmltext('\n') + r += self.get_widget('add_element').render() + r += self.get_widget('added_elements').render() + return r.getvalue() diff --git a/pypers/europython05/Quixote-2.0/form1/__init__.py b/pypers/europython05/Quixote-2.0/form1/__init__.py new file mode 100755 index 0000000..2989d6d --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form1/__init__.py @@ -0,0 +1,34 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form1/__init__.py $ +$Id: __init__.py 25664 2004-11-22 20:35:07Z nascheme $ + +The web interface framework, consisting of Form and Widget base classes +(and a bunch of standard widget classes recognized by Form). +Application developers will typically create a Form subclass for each +form in their application; each form object will contain a number +of widget objects. Custom widgets can be created by inheriting +and/or composing the standard widget classes. +""" + +from quixote.form1.form import Form, register_widget_class, FormTokenWidget +from quixote.form1.widget import Widget, StringWidget, FileWidget, \ + PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ + SingleSelectWidget, SelectWidget, OptionSelectWidget, \ + MultipleSelectWidget, ListWidget, SubmitButtonWidget, HiddenWidget, \ + FloatWidget, IntWidget, CollapsibleListWidget, FormValueError + +# Register the standard widget classes +register_widget_class(StringWidget) +register_widget_class(FileWidget) +register_widget_class(PasswordWidget) +register_widget_class(TextWidget) +register_widget_class(CheckboxWidget) +register_widget_class(RadiobuttonsWidget) +register_widget_class(SingleSelectWidget) +register_widget_class(OptionSelectWidget) +register_widget_class(MultipleSelectWidget) +register_widget_class(ListWidget) +register_widget_class(SubmitButtonWidget) +register_widget_class(HiddenWidget) +register_widget_class(FloatWidget) +register_widget_class(IntWidget) +register_widget_class(CollapsibleListWidget) diff --git a/pypers/europython05/Quixote-2.0/form1/form.py b/pypers/europython05/Quixote-2.0/form1/form.py new file mode 100755 index 0000000..c67598c --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form1/form.py @@ -0,0 +1,534 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form1/form.py $ +$Id: form.py 25664 2004-11-22 20:35:07Z nascheme $ + +Provides the Form class and bureaucracy for registering widget classes. +(The standard widget classes are registered automatically.) +""" + +from types import StringType +from quixote import get_session, get_publisher, redirect +from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO +from quixote.form1.widget import FormValueError, HiddenWidget + + +class FormTokenWidget (HiddenWidget): + def render(self, request): + self.value = get_session().create_form_token() + return HiddenWidget.render(self, request) + + +JAVASCRIPT_MARKUP = htmltext('''\ +<script type="text/javascript"> +<!-- +%s +// --> +</script> +''') + +class Form: + """ + A form is the major element of an interactive web page. A form + consists of the following: + * widgets (input/interaction elements) + * text + * layout + * code to process the form + + All four of these are the responsibility of Form classes. + Typically, you will create one Form subclass for each form in your + application. Thanks to the separation of responsibilities here, + it's not too hard to structure things so that a given form is + rendered and/or processed somewhat differently depending on context. + That separation is as follows: + * the constructor declares what widgets are in the form, and + any static text that is always associated with those widgets + (in particular, a widget title and "hint" text) + * the 'render()' method combines the widgets and their associated + text to create a (1-D) stream of HTML that represents the + (2-D) web page that will be presented to the user + * the 'process()' method parses the user input values from the form + and validates them + * the 'action()' method takes care of finishing whatever action + was requested by the user submitting the form -- commit + a database transaction, update session flags, redirect the + user to a new page, etc. + + This class provides a default 'process()' method that just parses + each widget, storing any error messages for display on the next + 'render()', and returns the results (if the form parses + successfully) in a dictionary. + + This class also provides a default 'render()' method that lays out + widgets and text in a 3-column table: the first column is the widget + title, the second column is the widget itself, and the third column is + any hint and/or error text associated with the widget. Also provided + are methods that can be used to construct this table a row at a time, + so you can use this layout for most widgets, but escape from it for + oddities. + + Instance attributes: + widgets : { widget_name:string : widget:Widget } + dictionary of all widgets in the form + widget_order : [Widget] + same widgets as 'widgets', but ordered (because order matters) + submit_buttons : [SubmitButtonWidget] + the submit button widgets in the form + + error : { widget_name:string : error_message:string } + hint : { widget_name:string : hint_text:string } + title : { widget_name:string : widget_title:string } + required : { widget_name:string : boolean } + + """ + + TOKEN_NAME = "_form_id" # name of hidden token widget + + def __init__(self, method="post", enctype=None, use_tokens=1): + + if method not in ("post", "get"): + raise ValueError("Form method must be 'post' or 'get', " + "not %r" % method) + self.method = method + + if enctype is not None and enctype not in ( + "application/x-www-form-urlencoded", "multipart/form-data"): + raise ValueError, ("Form enctype must be " + "'application/x-www-form-urlencoded' or " + "'multipart/form-data', not %r" % enctype) + self.enctype = enctype + + # The first major component of a form: its widgets. We want + # both easy access and order, so we have a dictionary and a list + # of the same objects. The dictionary is keyed on widget name. + # These are populated by the 'add_*_widget()' methods. + self.widgets = {} + self.widget_order = [] + self.submit_buttons = [] + self.cancel_url = None + + # The second major component: text. It's up to the 'render()' + # method to figure out how to lay these out; the standard + # 'render()' does so in a fairly sensible way that should work + # for most of our forms. These are also populated by the + # 'add_*_widget()' methods. + self.error = {} + self.hint = {} + self.title = {} + self.required = {} + + config = get_publisher().config + if self.method == "post" and use_tokens and config.form_tokens: + # unique token for each form, this prevents many cross-site + # attacks and prevents a form from being submitted twice + self.add_widget(FormTokenWidget, self.TOKEN_NAME) + self.use_form_tokens = 1 + else: + self.use_form_tokens = 0 + + # Subclasses should override this method to specify the actual + # widgets in this form -- typically this consists of a series of + # calls to 'add_widget()', which updates the data structures we + # just defined. + + + # -- Layout (rendering) methods ------------------------------------ + + # The third major component of a web form is layout. These methods + # combine text and widgets in a 1-D stream of HTML, or in a 2-D web + # page (depending on your level of abstraction). + + def render(self, request, action_url): + # render(request : HTTPRequest, + # action_url : string) + # -> HTML text + # + # Render a form as HTML. + assert type(action_url) in (StringType, htmltext) + r = TemplateIO(html=1) + r += self._render_start(request, action_url, + enctype=self.enctype, method=self.method) + r += self._render_body(request) + r += self._render_finish(request) + return r.getvalue() + + def _render_start(self, request, action, + enctype=None, method='post', name=None): + r = TemplateIO(html=1) + r += htmltag('form', enctype=enctype, method=method, + action=action, name=name) + r += self._render_hidden_widgets(request) + return r.getvalue() + + def _render_finish(self, request): + r = TemplateIO(html=1) + r += htmltext('</form>') + r += self._render_javascript(request) + return r.getvalue() + + def _render_sep(self, text, line=1): + return htmltext('<tr><td colspan="3">%s<strong><big>%s' + '</big></strong></td></tr>') % \ + (line and htmltext('<hr>') or '', text) + + def _render_error(self, error): + if error: + return htmltext('<font color="red">%s</font><br />') % nl2br(error) + else: + return '' + + def _render_hint(self, hint): + if hint: + return htmltext('<em>%s</em>') % hint + else: + return '' + + def _render_widget_row(self, request, widget): + if widget.widget_type == 'hidden': + return '' + title = self.title[widget.name] or '' + if self.required.get(widget.name): + title = title + htmltext(' *') + r = TemplateIO(html=1) + r += htmltext('<tr><th colspan="3" align="left">') + r += title + r += htmltext('</th></tr>' + '<tr><td> </td><td>') + r += widget.render(request) + r += htmltext('</td><td>') + r += self._render_error(self.error.get(widget.name)) + r += self._render_hint(self.hint.get(widget.name)) + r += htmltext('</td></tr>') + return r.getvalue() + + def _render_hidden_widgets(self, request): + r = TemplateIO(html=1) + for widget in self.widget_order: + if widget.widget_type == 'hidden': + r += widget.render(request) + r += self._render_error(self.error.get(widget.name)) + return r.getvalue() + + def _render_submit_buttons(self, request, ncols=3): + r = TemplateIO(html=1) + r += htmltext('<tr><td colspan="%d">\n') % ncols + for button in self.submit_buttons: + r += button.render(request) + r += htmltext('</td></tr>') + return r.getvalue() + + def _render_visible_widgets(self, request): + r = TemplateIO(html=1) + for widget in self.widget_order: + r += self._render_widget_row(request, widget) + return r.getvalue() + + def _render_error_notice(self, request): + if self.error: + r = htmltext('<tr><td colspan="3">' + '<font color="red"><strong>Warning:</strong></font> ' + 'there were errors processing your form. ' + 'See below for details.' + '</td></tr>') + else: + r = '' + return r + + def _render_required_notice(self, request): + if filter(None, self.required.values()): + r = htmltext('<tr><td colspan="3">' + '<b>*</b> = <em>required field</em>' + '</td></tr>') + else: + r = '' + return r + + def _render_body(self, request): + r = TemplateIO(html=1) + r += htmltext('<table>') + r += self._render_error_notice(request) + r += self._render_required_notice(request) + r += self._render_visible_widgets(request) + r += self._render_submit_buttons(request) + r += htmltext('</table>') + return r.getvalue() + + def _render_javascript(self, request): + """Render javacript code for the form, if any. + Insert code lexically sorted by code_id + """ + javascript_code = request.response.javascript_code + if javascript_code: + form_code = [] + code_ids = javascript_code.keys() + code_ids.sort() + for code_id in code_ids: + code = javascript_code[code_id] + if code: + form_code.append(code) + javascript_code[code_id] = '' + if form_code: + return JAVASCRIPT_MARKUP % htmltext(''.join(form_code)) + return '' + + + # -- Processing methods -------------------------------------------- + + # The fourth and final major component: code to process the form. + # The standard 'process()' method just parses every widget and + # returns a { field_name : field_value } dictionary as 'values'. + + def process(self, request): + """process(request : HTTPRequest) -> values : { string : any } + + Process the form data, validating all input fields (widgets). + If any errors in input fields, adds error messages to the + 'error' attribute (so that future renderings of the form will + include the errors). Returns a dictionary mapping widget names to + parsed values. + """ + self.error.clear() + + values = {} + for widget in self.widget_order: + try: + val = widget.parse(request) + except FormValueError, exc: + self.error[widget.name] = exc.msg + else: + values[widget.name] = val + + return values + + def action(self, request, submit, values): + """action(request : HTTPRequest, submit : string, + values : { string : any }) -> string + + Carry out the action required by a form submission. 'submit' is the + name of submit button used to submit the form. 'values' is the + dictionary of parsed values from 'process()'. Note that error + checking cannot be done here -- it must done in the 'process()' + method. + """ + raise NotImplementedError, "sub-classes must implement 'action()'" + + def handle(self, request): + """handle(request : HTTPRequest) -> string + + Master method for handling forms. It should be called after + initializing a form. Controls form action based on a request. You + probably should override 'process' and 'action' instead of + overriding this method. + """ + action_url = self.get_action_url(request) + if not self.form_submitted(request): + return self.render(request, action_url) + submit = self.get_submit_button(request) + if submit == "cancel": + return redirect(self.cancel_url) + values = self.process(request) + if submit == "": + # The form was submitted by unknown submit button, assume that + # the submission was required to update the layout of the form. + # Clear the errors and re-render the form. + self.error.clear() + return self.render(request, action_url) + + if self.use_form_tokens: + # before calling action() ensure that there is a valid token + # present + token = values.get(self.TOKEN_NAME) + if not request.session.has_form_token(token): + if not self.error: + # if there are other errors then don't show the token + # error, the form needs to be resubmitted anyhow + self.error[self.TOKEN_NAME] = ( + "The form you have submitted is invalid. It has " + "already been submitted or has expired. Please " + "review and resubmit the form.") + else: + request.session.remove_form_token(token) + + if self.error: + return self.render(request, action_url) + else: + return self.action(request, submit, values) + + + # -- Convenience methods ------------------------------------------- + + def form_submitted(self, request): + """form_submitted(request : HTTPRequest) -> boolean + + Return true if a form was submitted in the current request. + """ + return len(request.form) > 0 + + def get_action_url(self, request): + action_url = url_quote(request.get_path()) + query = request.get_environ("QUERY_STRING") + if query: + action_url += "?" + query + return action_url + + def get_submit_button(self, request): + """get_submit_button(request : HTTPRequest) -> string | None + + Get the name of the submit button that was used to submit the + current form. If the browser didn't include this information in + the request, use the first submit button registered. + """ + for button in self.submit_buttons: + if request.form.has_key(button.name): + return button.name + else: + if request.form and self.submit_buttons: + return "" + else: + return None + + def get_widget(self, widget_name): + return self.widgets.get(widget_name) + + def parse_widget(self, name, request): + """parse_widget(name : string, request : HTTPRequest) -> any + + Parse the value of named widget. If any parse errors, store the + error message (in self.error) for use in the next rendering of + the form and return None; otherwise, return the value parsed + from the widget (whose type depends on the widget type). + """ + try: + return self.widgets[name].parse(request) + except FormValueError, exc: + self.error[name] = str(exc) + return None + + def store_value(self, widget_name, request, target, + mode="modifier", + key=None, + missing_error=None): + """store_value(widget_name : string, + request : HTTPRequest, + target : instance | dict, + mode : string = "modifier", + key : string = widget_name, + missing_error : string = None) + + Parse a widget and, if it parsed successfully, store its value + in 'target'. The value is stored in 'target' by name 'key'; + if 'key' is not supplied, it defaults to 'widget_name'. + How the value is stored depends on 'mode': + * modifier: call a modifier method, eg. if 'key' is "foo", + call 'target.set_foo(value)' + * direct: direct attribute update, eg. if 'key' is + "foo" do "target.foo = value" + * dict: dictionary update, eg. if 'key' is "foo" do + "target['foo'] = value" + + If 'missing_error' is supplied, use it as an error message if + the field doesn't have a value -- ie. supplying 'missing_error' + means this field is required. + """ + value = self.parse_widget(widget_name, request) + if (value is None or value == "") and missing_error: + self.error[widget_name] = missing_error + return None + + if key is None: + key = widget_name + if mode == "modifier": + # eg. turn "name" into "target.set_name", and + # call it like "target.set_name(value)" + mod = getattr(target, "set_" + key) + mod(value) + elif mode == "direct": + if not hasattr(target, key): + raise AttributeError, \ + ("target object %s doesn't have attribute %s" % + (`target`, key)) + setattr(target, key, value) + elif mode == "dict": + target[key] = value + else: + raise ValueError, "unknown update mode %s" % `mode` + + def clear_widget(self, widget_name): + self.widgets[widget_name].clear() + + def get_widget_value(self, widget_name): + return self.widgets[widget_name].value + + def set_widget_value(self, widget_name, value): + self.widgets[widget_name].set_value(value) + + + # -- Form population methods --------------------------------------- + + def add_widget(self, widget_type, name, value=None, + title=None, hint=None, required=0, **args): + """add_widget(widget_type : string | Widget, + name : string, + value : any = None, + title : string = None, + hint : string = None, + required : boolean = 0, + ...) -> Widget + + Create a new Widget object and add it to the form. The widget + class used depends on 'widget_type', and the expected type of + 'value' also depends on the widget class. Any extra keyword + args are passed to the widget constructor. + + Returns the new Widget. + """ + if self.widgets.has_key(name): + raise ValueError, "form already has '%s' variable" % name + klass = get_widget_class(widget_type) + new_widget = apply(klass, (name, value), args) + + self.widgets[name] = new_widget + self.widget_order.append(new_widget) + self.title[name] = title + self.hint[name] = hint + self.required[name] = required + return new_widget + + def add_submit_button(self, name, value): + global _widget_class + if self.widgets.has_key(name): + raise ValueError, "form already has '%s' variable" % name + new_widget = _widget_class['submit_button'](name, value) + + self.widgets[name] = new_widget + self.submit_buttons.append(new_widget) + + def add_cancel_button(self, caption, url): + if not isinstance(url, (StringType, htmltext)): + raise TypeError, "url must be a string (got %r)" % url + self.add_submit_button("cancel", caption) + self.cancel_url = url + +# class Form + + +_widget_class = {} + +def register_widget_class(klass, widget_type=None): + global _widget_class + if widget_type is None: + widget_type = klass.widget_type + assert widget_type is not None, "widget_type must be defined" + _widget_class[widget_type] = klass + +def get_widget_class(widget_type): + global _widget_class + if callable(widget_type): + # Presumably someone passed a widget class object to + # Widget.create_subwidget() or Form.add_widget() -- + # don't bother with the widget class registry at all. + return widget_type + else: + try: + return _widget_class[widget_type] + except KeyError: + raise ValueError("unknown widget type %r" % widget_type) diff --git a/pypers/europython05/Quixote-2.0/form1/widget.py b/pypers/europython05/Quixote-2.0/form1/widget.py new file mode 100755 index 0000000..1ddc229 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/form1/widget.py @@ -0,0 +1,842 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/form1/widget.py $ +$Id: widget.py 25664 2004-11-22 20:35:07Z nascheme $ + +Provides the basic web widget classes: Widget itself, plus StringWidget, +TextWidget, CheckboxWidget, etc. +""" + +import struct +from types import FloatType, IntType, ListType, StringType, TupleType +from quixote import get_request +from quixote.html import htmltext, htmlescape, htmltag +from quixote.http_request import Upload + + +class FormValueError (Exception): + """Raised whenever a widget has problems parsing its value.""" + + def __init__(self, msg): + self.msg = msg + + + def __str__(self): + return str(self.msg) + + +class Widget: + """Abstract base class for web widgets. The key elements + of a web widget are: + - name + - widget type (how the widget looks/works in the browser) + - value + + The name and value are instance attributes (because they're specific to + a particular widget in a particular context); widget type is a + class attributes. + + Instance attributes: + name : string + value : any + + Feel free to access these directly; to set them, use the 'set_*()' + modifier methods. + """ + + # Subclasses must define. 'widget_type' is just a string, e.g. + # "string", "text", "checkbox". + widget_type = None + + def __init__(self, name, value=None): + assert self.__class__ is not Widget, "abstract class" + self.set_name(name) + self.set_value(value) + + + def __repr__(self): + return "<%s at %x: %s>" % (self.__class__.__name__, + id(self), + self.name) + + + def __str__(self): + return "%s: %s" % (self.widget_type, self.name) + + + def set_name(self, name): + self.name = name + + + def set_value(self, value): + self.value = value + + + def clear(self): + self.value = None + + # -- Subclasses must implement these ------------------------------- + + def render(self, request): + """render(request) -> HTML text""" + raise NotImplementedError + + + def parse(self, request): + """parse(request) -> any""" + value = request.form.get(self.name) + if type(value) is StringType and value.strip(): + self.value = value + else: + self.value = None + + return self.value + + # -- Convenience methods for subclasses ---------------------------- + + # This one's really only for composite widgets; lives here until + # we have a demonstrated need for a CompositeWidget class. + def get_subwidget_name(self, name): + return "%s$%s" % (self.name, name) + + + def create_subwidget(self, widget_type, widget_name, value=None, **args): + from quixote.form.form import get_widget_class + klass = get_widget_class(widget_type) + name = self.get_subwidget_name(widget_name) + return apply(klass, (name, value), args) + +# class Widget + +# -- Fundamental widget types ------------------------------------------ +# These correspond to the standard types of input tag in HTML: +# text StringWidget +# password PasswordWidget +# radio RadiobuttonWidget +# checkbox CheckboxWidget +# +# and also to the other basic form elements: +# <textarea> TextWidget +# <select> SingleSelectWidget +# <select multiple> +# MultipleSelectWidget + +class StringWidget (Widget): + """Widget for entering a single string: corresponds to + '<input type="text">' in HTML. + + Instance attributes: + value : string + size : int + maxlength : int + """ + + widget_type = "string" + + # This lets PasswordWidget be a trivial subclass + html_type = "text" + + def __init__(self, name, value=None, + size=None, maxlength=None): + Widget.__init__(self, name, value) + self.size = size + self.maxlength = maxlength + + + def render(self, request, **attributes): + return htmltag("input", xml_end=1, + type=self.html_type, + name=self.name, + size=self.size, + maxlength=self.maxlength, + value=self.value, + **attributes) + + +class FileWidget (StringWidget): + """Trivial subclass of StringWidget for uploading files. + + Instance attributes: none + """ + widget_type = "file" + html_type = "file" + + def parse(self, request): + """parse(request) -> any""" + value = request.form.get(self.name) + if isinstance(value, Upload): + self.value = value + else: + self.value = None + return self.value + + +class PasswordWidget (StringWidget): + """Trivial subclass of StringWidget for entering passwords (different + widget type because HTML does it that way). + + Instance attributes: none + """ + + widget_type = "password" + html_type = "password" + + +class TextWidget (Widget): + """Widget for entering a long, multi-line string; corresponds to + the HTML "<textarea>" tag. + + Instance attributes: + value : string + cols : int + rows : int + wrap : string + (see an HTML book for details on text widget wrap options) + css_class : string + """ + + widget_type = "text" + + def __init__(self, name, value=None, cols=None, rows=None, wrap=None, + css_class=None): + Widget.__init__(self, name, value) + self.cols = cols + self.rows = rows + self.wrap = wrap + self.css_class = css_class + + def render(self, request): + return (htmltag("textarea", name=self.name, + cols=self.cols, + rows=self.rows, + wrap=self.wrap, + css_class=self.css_class) + + htmlescape(self.value or "") + + htmltext("</textarea>")) + + + def parse(self, request): + value = Widget.parse(self, request) + if value: + value = value.replace("\r\n", "\n") + self.value = value + return self.value + + +class CheckboxWidget (Widget): + """Widget for a single checkbox: corresponds to "<input + type=checkbox>". Do not put multiple CheckboxWidgets with the same + name in the same form. + + Instance attributes: + value : boolean + """ + + widget_type = "checkbox" + + def render(self, request): + return htmltag("input", xml_end=1, + type="checkbox", + name=self.name, + value="yes", + checked=self.value and "checked" or None) + + + def parse(self, request): + self.value = request.form.has_key(self.name) + return self.value + + +class SelectWidget (Widget): + """Widget for single or multiple selection; corresponds to + <select name=...> + <option value="Foo">Foo</option> + ... + </select> + + Instance attributes: + options : [ (value:any, description:any, key:string) ] + value : any + The value is None or an element of dict(options.values()). + size : int + The number of options that should be presented without scrolling. + """ + + # NB. 'widget_type' not set here because this is an abstract class: it's + # set by subclasses SingleSelectWidget and MultipleSelectWidget. + + def __init__(self, name, value=None, + allowed_values=None, + descriptions=None, + options=None, + size=None, + sort=0, + verify_selection=1): + assert self.__class__ is not SelectWidget, "abstract class" + self.options = [] + # if options passed, cannot pass allowed_values or descriptions + if allowed_values is not None: + assert options is None, ( + 'cannot pass both allowed_values and options') + assert allowed_values, ( + 'cannot pass empty allowed_values list') + self.set_allowed_values(allowed_values, descriptions, sort) + elif options is not None: + assert descriptions is None, ( + 'cannot pass both options and descriptions') + assert options, ( + 'cannot pass empty options list') + self.set_options(options, sort) + self.set_name(name) + self.set_value(value) + self.size = size + self.verify_selection = verify_selection + + + def get_allowed_values(self): + return [item[0] for item in self.options] + + + def get_descriptions(self): + return [item[1] for item in self.options] + + + def set_value(self, value): + self.value = None + for object, description, key in self.options: + if value == object: + self.value = value + break + + + def _generate_keys(self, values, descriptions): + """Called if no keys were provided. Try to generate a set of keys + that will be consistent between rendering and parsing. + """ + # try to use ZODB object IDs + keys = [] + for value in values: + if value is None: + oid = "" + else: + oid = getattr(value, "_p_oid", None) + if not oid: + break + hi, lo = struct.unpack(">LL", oid) + oid = "%x" % ((hi << 32) | lo) + keys.append(oid) + else: + # found OID for every value + return keys + # can't use OIDs, try using descriptions + used_keys = {} + keys = map(str, descriptions) + for key in keys: + if used_keys.has_key(key): + raise ValueError, "duplicated descriptions (provide keys)" + used_keys[key] = 1 + return keys + + + def set_options(self, options, sort=0): + """(options: [objects:any], sort=0) + or + (options: [(object:any, description:any)], sort=0) + or + (options: [(object:any, description:any, key:any)], sort=0) + """ + + """ + Set the options list. The list of options can be a list of objects, in + which case the descriptions default to map(htmlescape, objects) + applying htmlescape() to each description and + key. + If keys are provided they must be distinct. If the sort keyword + argument is true, sort the options by case-insensitive lexicographic + order of descriptions, except that options with value None appear + before others. + """ + if options: + first = options[0] + values = [] + descriptions = [] + keys = [] + if type(first) is TupleType: + if len(first) == 2: + for value, description in options: + values.append(value) + descriptions.append(description) + elif len(first) == 3: + for value, description, key in options: + values.append(value) + descriptions.append(description) + keys.append(str(key)) + else: + raise ValueError, 'invalid options %r' % options + else: + values = descriptions = options + + if not keys: + keys = self._generate_keys(values, descriptions) + + options = zip(values, descriptions, keys) + + if sort: + def make_sort_key(option): + value, description, key = option + if value is None: + return ('', option) + else: + return (str(description).lower(), option) + doptions = map(make_sort_key, options) + doptions.sort() + options = [item[1] for item in doptions] + self.options = options + + + def parse_single_selection(self, parsed_key): + for value, description, key in self.options: + if key == parsed_key: + return value + else: + if self.verify_selection: + raise FormValueError, "invalid value selected" + else: + return self.options[0][0] + + + def set_allowed_values(self, allowed_values, descriptions=None, sort=0): + """(allowed_values:[any], descriptions:[any], sort:boolean=0) + + Set the options for this widget. The allowed_values and descriptions + parameters must be sequences of the same length. The sort option + causes the options to be sorted using case-insensitive lexicographic + order of descriptions, except that options with value None appear + before others. + """ + if descriptions is None: + self.set_options(allowed_values, sort) + else: + assert len(descriptions) == len(allowed_values) + self.set_options(zip(allowed_values, descriptions), sort) + + + def is_selected(self, value): + return value == self.value + + + def render(self, request): + if self.widget_type == "multiple_select": + multiple = "multiple" + else: + multiple = None + if self.widget_type == "option_select": + onchange = "submit()" + else: + onchange = None + tags = [htmltag("select", name=self.name, + multiple=multiple, onchange=onchange, + size=self.size)] + for object, description, key in self.options: + if self.is_selected(object): + selected = "selected" + else: + selected = None + if description is None: + description = "" + r = htmltag("option", value=key, selected=selected) + tags.append(r + htmlescape(description) + htmltext('</option>')) + tags.append(htmltext("</select>")) + return htmltext("\n").join(tags) + + +class SingleSelectWidget (SelectWidget): + """Widget for single selection. + """ + + widget_type = "single_select" + + def parse(self, request): + parsed_key = request.form.get(self.name) + self.value = None + if parsed_key: + if type(parsed_key) is ListType: + raise FormValueError, "cannot select multiple values" + self.value = self.parse_single_selection(parsed_key) + return self.value + + +class RadiobuttonsWidget (SingleSelectWidget): + """Widget for a *set* of related radiobuttons -- all have the + same name, but different values (and only one of those values + is returned by the whole group). + + Instance attributes: + delim : string = None + string to emit between each radiobutton in the group. If + None, a single newline is emitted. + """ + + widget_type = "radiobuttons" + + def __init__(self, name, value=None, + allowed_values=None, + descriptions=None, + options=None, + delim=None): + SingleSelectWidget.__init__(self, name, value, allowed_values, + descriptions, options) + if delim is None: + self.delim = "\n" + else: + self.delim = delim + + + def render(self, request): + tags = [] + for object, description, key in self.options: + if self.is_selected(object): + checked = "checked" + else: + checked = None + r = htmltag("input", xml_end=True, + type="radio", + name=self.name, + value=key, + checked=checked) + tags.append(r + htmlescape(description)) + return htmlescape(self.delim).join(tags) + + +class MultipleSelectWidget (SelectWidget): + """Widget for multiple selection. + + Instance attributes: + value : [any] + for multipe selects, the value is None or a list of + elements from dict(self.options).values() + """ + + widget_type = "multiple_select" + + def set_value(self, value): + allowed_values = self.get_allowed_values() + if value in allowed_values: + self.value = [ value ] + elif type(value) in (ListType, TupleType): + self.value = [ element + for element in value + if element in allowed_values ] or None + else: + self.value = None + + + def is_selected(self, value): + if self.value is None: + return value is None + else: + return value in self.value + + + def parse(self, request): + parsed_keys = request.form.get(self.name) + self.value = None + if parsed_keys: + if type(parsed_keys) is ListType: + self.value = [value + for value, description, key in self.options + if key in parsed_keys] or None + else: + self.value = [self.parse_single_selection(parsed_keys)] + return self.value + + +class SubmitButtonWidget (Widget): + """ + Instance attributes: + value : boolean + """ + + widget_type = "submit_button" + + def __init__(self, name=None, value=None): + Widget.__init__(self, name, value) + + + def render(self, request): + value = (self.value and htmlescape(self.value) or None) + return htmltag("input", xml_end=1, type="submit", + name=self.name, value=value) + + + def parse(self, request): + return request.form.get(self.name) + + + def is_submitted(self): + return self.parse(get_request()) + + +class HiddenWidget (Widget): + """ + Instance attributes: + value : string + """ + + widget_type = "hidden" + + def render(self, request): + if self.value is None: + value = None + else: + value = htmlescape(self.value) + return htmltag("input", xml_end=1, + type="hidden", + name=self.name, + value=value) + + + def set_current_value(self, value): + self.value = value + request = get_request() + if request.form: + request.form[self.name] = value + + + def get_current_value(self): + request = get_request() + if request.form: + return self.parse(request) + else: + return self.value + +# -- Derived widget types ---------------------------------------------- +# (these don't correspond to fundamental widget types in HTML, +# so they're separated) + +class NumberWidget (StringWidget): + """ + Instance attributes: none + """ + + # Parameterize the number type (either float or int) through + # these class attributes: + type_object = None # eg. int, float + type_error = None # human-readable error message + type_converter = None # eg. int(), float() + + def __init__(self, name, + value=None, + size=None, maxlength=None): + assert self.__class__ is not NumberWidget, "abstract class" + assert value is None or type(value) is self.type_object, ( + "form value '%s' not a %s: got %r" % (name, + self.type_object, + value)) + StringWidget.__init__(self, name, value, size, maxlength) + + + def parse(self, request): + value = StringWidget.parse(self, request) + if value: + try: + self.value = self.type_converter(value) + except ValueError: + raise FormValueError, self.type_error + return self.value + + +class FloatWidget (NumberWidget): + """ + Instance attributes: + value : float + """ + + widget_type = "float" + type_object = FloatType + type_converter = float + type_error = "must be a number" + + +class IntWidget (NumberWidget): + """ + Instance attributes: + value : int + """ + + widget_type = "int" + type_object = IntType + type_converter = int + type_error = "must be an integer" + + +class OptionSelectWidget (SingleSelectWidget): + """Widget for single selection with automatic submission and early + parsing. This widget parses the request when it is created. This + allows its value to be used to decide what other widgets need to be + created in a form. It's a powerful feature but it can be hard to + understand what's going on. + + Instance attributes: + value : any + """ + + widget_type = "option_select" + + def __init__(self, *args, **kwargs): + SingleSelectWidget.__init__(self, *args, **kwargs) + + request = get_request() + if request.form: + SingleSelectWidget.parse(self, request) + if self.value is None: + self.value = self.options[0][0] + + + def render(self, request): + return (SingleSelectWidget.render(self, request) + + htmltext('<noscript>' + '<input type="submit" name="" value="apply" />' + '</noscript>')) + + + def parse(self, request): + return self.value + + + def get_current_option(self): + return self.value + + +class ListWidget (Widget): + """Widget for lists of objects. + + Instance attributes: + value : [any] + """ + + widget_type = "list" + + def __init__(self, name, value=None, + element_type=None, + element_name="row", + **args): + assert value is None or type(value) is ListType, ( + "form value '%s' not a list: got %r" % (name, value)) + assert type(element_name) in (StringType, htmltext), ( + "form value '%s' element_name not a string: " + "got %r" % (name, element_name)) + + Widget.__init__(self, name, value) + + if element_type is None: + self.element_type = "string" + else: + self.element_type = element_type + self.args = args + + self.added_elements_widget = self.create_subwidget( + "hidden", "added_elements") + + added_elements = int(self.added_elements_widget.get_current_value() or + '1') + + self.add_button = self.create_subwidget( + "submit_button", "add_element", + value="Add %s" % element_name) + + if self.add_button.is_submitted(): + added_elements += 1 + self.added_elements_widget.set_current_value(str(added_elements)) + + self.element_widgets = [] + self.element_count = 0 + + if self.value is not None: + for element in self.value: + self.add_element(element) + + for index in range(added_elements): + self.add_element() + + def add_element(self, value=None): + self.element_widgets.append( + self.create_subwidget(self.element_type, + "element_%d" % self.element_count, + value=value, + **self.args)) + self.element_count += 1 + + def render(self, request): + tags = [] + for element_widget in self.element_widgets: + tags.append(element_widget.render(request)) + tags.append(self.add_button.render(request)) + tags.append(self.added_elements_widget.render(request)) + return htmltext('<br />\n').join(tags) + + def parse(self, request): + self.value = [] + for element_widget in self.element_widgets: + value = element_widget.parse(request) + if value is not None: + self.value.append(value) + self.value = self.value or None + return self.value + + + +class CollapsibleListWidget (ListWidget): + """Widget for lists of objects with associated delete buttons. + + CollapsibleListWidget behaves like ListWidget except that each element + is rendered with an associated delete button. Pressing the delete + button will cause the associated element name to be added to a hidden + widget that remembers all deletions until the form is submitted. + Only elements that are not marked as deleted will be rendered and + ultimately added to the value of the widget. + + Instance attributes: + value : [any] + """ + + widget_type = "collapsible_list" + + def __init__(self, name, value=None, element_name="row", **args): + self.name = name + self.element_name = element_name + self.deleted_elements_widget = self.create_subwidget( + "hidden", "deleted_elements") + self.element_delete_buttons = [] + self.deleted_elements = ( + self.deleted_elements_widget.get_current_value() or '') + ListWidget.__init__(self, name, value=value, + element_name=element_name, + **args) + + def add_element(self, value=None): + element_widget_name = "element_%d" % self.element_count + if self.deleted_elements.find(element_widget_name) == -1: + delete_button = self.create_subwidget( + "submit_button", "delete_" + element_widget_name, + value="Delete %s" % self.element_name) + if delete_button.is_submitted(): + self.element_count += 1 + self.deleted_elements += element_widget_name + self.deleted_elements_widget.set_current_value( + self.deleted_elements) + else: + self.element_delete_buttons.append(delete_button) + ListWidget.add_element(self, value=value) + else: + self.element_count += 1 + + def render(self, request): + tags = [] + for element_widget, element_delete_button in zip( + self.element_widgets, self.element_delete_buttons): + if self.deleted_elements.find(element_widget.name) == -1: + tags.append(element_widget.render(request) + + element_delete_button.render(request)) + tags.append(self.add_button.render(request)) + tags.append(self.added_elements_widget.render(request)) + tags.append(self.deleted_elements_widget.render(request)) + return htmltext('<br />\n').join(tags) diff --git a/pypers/europython05/Quixote-2.0/html/__init__.py b/pypers/europython05/Quixote-2.0/html/__init__.py new file mode 100755 index 0000000..563e78b --- /dev/null +++ b/pypers/europython05/Quixote-2.0/html/__init__.py @@ -0,0 +1,106 @@ +"""Various functions for dealing with HTML. +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/html/__init__.py $ +$Id: __init__.py 26357 2005-03-16 14:56:23Z dbinger $ + +These functions are fairly simple but it is critical that they be +used correctly. Many security problems are caused by escaping errors +(cross site scripting is one example). The HTML and XML standards on +www.w3c.org and www.xml.com should be studied, especially the sections +on character sets, entities, attribute and values. + +htmltext and htmlescape +----------------------- + +This type and function are meant to be used with [html] PTL template type. +The htmltext type designates data that does not need to be escaped and the +htmlescape() function calls str() on the argment, escapes the resulting +string and returns a htmltext instance. htmlescape() does nothing to +htmltext instances. + +url_quote +--------- + +Use for quoting data to be included as part of a URL, for example: + + input = "foo bar" + ... + '<a href="/search?keyword=%s">' % url_quote(input) + +Note that URLs are usually used as attribute values and might need to have +HTML special characters escaped. As an example of incorrect usage: + + url = 'http://example.com/?a=1©=0' # INCORRECT + url = 'http://example.com/?a=1&copy=0' # CORRECT + ... + '<a href="%s">do something</a>' % url + +Old browsers would treat "©" as an entity reference and replace it with +the copyright character. XML processors should treat it as an invalid entity +reference. +""" + +import urllib + +try: + # faster C implementation + from quixote.html._c_htmltext import htmltext, htmlescape, \ + stringify, TemplateIO +except ImportError: + from quixote.html._py_htmltext import htmltext, htmlescape, \ + stringify, TemplateIO + +ValuelessAttr = object() # magic singleton object + +def htmltag(tag, xml_end=False, css_class=None, **attrs): + """Create a HTML tag. + """ + r = ["<%s" % tag] + if css_class is not None: + attrs['class'] = css_class + for (attr, val) in attrs.items(): + if val is ValuelessAttr: + val = attr + if val is not None: + r.append(' %s="%s"' % (attr, htmlescape(val))) + if xml_end: + r.append(" />") + else: + r.append(">") + return htmltext("".join(r)) + + +def href(url, text, title=None, **attrs): + return (htmltag("a", href=url, title=title, **attrs) + + htmlescape(text) + + htmltext("</a>")) + +def url_with_query(path, **attrs): + result = htmltext(url_quote(path)) + if attrs: + result += "?" + "&".join([url_quote(key) + "=" + url_quote(value) + for key, value in attrs.items()]) + return result + +def nl2br(value): + """nl2br(value : any) -> htmltext + + Insert <br /> tags before newline characters. + """ + text = htmlescape(value) + return htmltext(text.s.replace('\n', '<br />\n')) + + +def url_quote(value, fallback=None): + """url_quote(value : any [, fallback : string]) -> string + + Quotes 'value' for use in a URL; see urllib.quote(). If value is None, + then the behavior depends on the fallback argument. If it is not + supplied then an error is raised. Otherwise, the fallback value is + returned unquoted. + """ + if value is None: + if fallback is None: + raise ValueError, "value is None and no fallback supplied" + else: + return fallback + return urllib.quote(stringify(value)) diff --git a/pypers/europython05/Quixote-2.0/html/_c_htmltext.c b/pypers/europython05/Quixote-2.0/html/_c_htmltext.c new file mode 100755 index 0000000..1c6581a --- /dev/null +++ b/pypers/europython05/Quixote-2.0/html/_c_htmltext.c @@ -0,0 +1,1019 @@ +/* htmltext type and the htmlescape function */ + +#include "Python.h" +#include "structmember.h" + +typedef struct { + PyObject_HEAD + PyObject *s; +} htmltextObject; + +static PyTypeObject htmltext_Type; + +#define htmltextObject_Check(v) ((v)->ob_type == &htmltext_Type) + +#define htmltext_STR(v) ((PyObject *)(((htmltextObject *)v)->s)) + +typedef struct { + PyObject_HEAD + PyObject *obj; +} QuoteWrapperObject; + +static PyTypeObject QuoteWrapper_Type; + +#define QuoteWrapper_Check(v) ((v)->ob_type == &QuoteWrapper_Type) + + +typedef struct { + PyObject_HEAD + PyObject *data; /* PyList_Object */ + int html; +} TemplateIO_Object; + +static PyTypeObject TemplateIO_Type; + +#define TemplateIO_Check(v) ((v)->ob_type == &TemplateIO_Type) + + +static PyObject * +type_error(const char *msg) +{ + PyErr_SetString(PyExc_TypeError, msg); + return NULL; +} + +static int +string_check(PyObject *v) +{ + return PyUnicode_Check(v) || PyString_Check(v); +} + +static PyObject * +stringify(PyObject *obj) +{ + static PyObject *unicodestr = NULL; + PyObject *res, *func; + if (string_check(obj)) { + Py_INCREF(obj); + return obj; + } + if (unicodestr == NULL) { + unicodestr = PyString_InternFromString("__unicode__"); + if (unicodestr == NULL) + return NULL; + } + func = PyObject_GetAttr(obj, unicodestr); + if (func != NULL) { + res = PyEval_CallObject(func, (PyObject *)NULL); + Py_DECREF(func); + } + else { + PyErr_Clear(); + if (obj->ob_type->tp_str != NULL) + res = (*obj->ob_type->tp_str)(obj); + else + res = PyObject_Repr(obj); + } + if (res == NULL) + return NULL; + if (!string_check(res)) { + Py_DECREF(res); + return type_error("string object required"); + } + return res; +} + +static PyObject * +escape_string(PyObject *obj) +{ + char *s; + PyObject *newobj; + size_t i, j, extra_space, size, new_size; + assert (PyString_Check(obj)); + size = PyString_GET_SIZE(obj); + extra_space = 0; + for (i=0; i < size; i++) { + switch (PyString_AS_STRING(obj)[i]) { + case '&': + extra_space += 4; + break; + case '<': + case '>': + extra_space += 3; + break; + case '"': + extra_space += 5; + break; + } + } + if (extra_space == 0) { + Py_INCREF(obj); + return (PyObject *)obj; + } + new_size = size + extra_space; + newobj = PyString_FromStringAndSize(NULL, new_size); + if (newobj == NULL) + return NULL; + s = PyString_AS_STRING(newobj); + for (i=0, j=0; i < size; i++) { + switch (PyString_AS_STRING(obj)[i]) { + case '&': + s[j++] = '&'; + s[j++] = 'a'; + s[j++] = 'm'; + s[j++] = 'p'; + s[j++] = ';'; + break; + case '<': + s[j++] = '&'; + s[j++] = 'l'; + s[j++] = 't'; + s[j++] = ';'; + break; + case '>': + s[j++] = '&'; + s[j++] = 'g'; + s[j++] = 't'; + s[j++] = ';'; + break; + case '"': + s[j++] = '&'; + s[j++] = 'q'; + s[j++] = 'u'; + s[j++] = 'o'; + s[j++] = 't'; + s[j++] = ';'; + break; + default: + s[j++] = PyString_AS_STRING(obj)[i]; + break; + } + } + assert (j == new_size); + return (PyObject *)newobj; +} + +static PyObject * +escape_unicode(PyObject *obj) +{ + Py_UNICODE *u; + PyObject *newobj; + size_t i, j, extra_space, size, new_size; + assert (PyUnicode_Check(obj)); + size = PyUnicode_GET_SIZE(obj); + extra_space = 0; + for (i=0; i < size; i++) { + switch (PyUnicode_AS_UNICODE(obj)[i]) { + case '&': + extra_space += 4; + break; + case '<': + case '>': + extra_space += 3; + break; + case '"': + extra_space += 5; + break; + } + } + if (extra_space == 0) { + Py_INCREF(obj); + return (PyObject *)obj; + } + new_size = size + extra_space; + newobj = PyUnicode_FromUnicode(NULL, new_size); + if (newobj == NULL) { + return NULL; + } + u = PyUnicode_AS_UNICODE(newobj); + for (i=0, j=0; i < size; i++) { + switch (PyUnicode_AS_UNICODE(obj)[i]) { + case '&': + u[j++] = '&'; + u[j++] = 'a'; + u[j++] = 'm'; + u[j++] = 'p'; + u[j++] = ';'; + break; + case '<': + u[j++] = '&'; + u[j++] = 'l'; + u[j++] = 't'; + u[j++] = ';'; + break; + case '>': + u[j++] = '&'; + u[j++] = 'g'; + u[j++] = 't'; + u[j++] = ';'; + break; + case '"': + u[j++] = '&'; + u[j++] = 'q'; + u[j++] = 'u'; + u[j++] = 'o'; + u[j++] = 't'; + u[j++] = ';'; + break; + default: + u[j++] = PyUnicode_AS_UNICODE(obj)[i]; + break; + } + } + assert (j == new_size); + return (PyObject *)newobj; +} + +static PyObject * +escape(PyObject *obj) +{ + if (PyString_Check(obj)) { + return escape_string(obj); + } + else if (PyUnicode_Check(obj)) { + return escape_unicode(obj); + } + else { + return type_error("string object required"); + } +} + +static PyObject * +quote_wrapper_new(PyObject *o) +{ + QuoteWrapperObject *self; + if (htmltextObject_Check(o) || + PyInt_Check(o) || + PyFloat_Check(o) || + PyLong_Check(o)) { + /* no need for wrapper */ + Py_INCREF(o); + return o; + } + self = PyObject_New(QuoteWrapperObject, &QuoteWrapper_Type); + if (self == NULL) + return NULL; + Py_INCREF(o); + self->obj = o; + return (PyObject *)self; +} + +static void +quote_wrapper_dealloc(QuoteWrapperObject *self) +{ + Py_DECREF(self->obj); + PyObject_Del(self); +} + +static PyObject * +quote_wrapper_repr(QuoteWrapperObject *self) +{ + PyObject *qs; + PyObject *s = PyObject_Repr(self->obj); + if (s == NULL) + return NULL; + qs = escape(s); + Py_DECREF(s); + return qs; +} + +static PyObject * +quote_wrapper_str(QuoteWrapperObject *self) +{ + PyObject *qs; + PyObject *s = stringify(self->obj); + if (s == NULL) + return NULL; + qs = escape(s); + Py_DECREF(s); + return qs; +} + +static PyObject * +quote_wrapper_subscript(QuoteWrapperObject *self, PyObject *key) +{ + PyObject *v, *w;; + v = PyObject_GetItem(self->obj, key); + if (v == NULL) { + return NULL; + } + w = quote_wrapper_new(v); + Py_DECREF(v); + return w; +} + +static PyObject * +htmltext_from_string(PyObject *s) +{ + /* note, this steals a reference */ + PyObject *self; + if (s == NULL) + return NULL; + assert (string_check(s)); + self = PyType_GenericAlloc(&htmltext_Type, 0); + if (self == NULL) { + Py_DECREF(s); + return NULL; + } + ((htmltextObject *)self)->s = s; + return self; +} + +static PyObject * +htmltext_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + htmltextObject *self; + PyObject *s; + static char *kwlist[] = {"s", 0}; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:htmltext", kwlist, + &s)) + return NULL; + s = stringify(s); + if (s == NULL) + return NULL; + self = (htmltextObject *)type->tp_alloc(type, 0); + if (self == NULL) { + Py_DECREF(s); + return NULL; + } + self->s = s; + return (PyObject *)self; +} + +/* htmltext methods */ + +static void +htmltext_dealloc(htmltextObject *self) +{ + Py_DECREF(self->s); + self->ob_type->tp_free((PyObject *)self); +} + +static long +htmltext_hash(PyObject *self) +{ + return PyObject_Hash(htmltext_STR(self)); +} + +static PyObject * +htmltext_str(htmltextObject *self) +{ + Py_INCREF(self->s); + return (PyObject *)self->s; +} + +static PyObject * +htmltext_repr(htmltextObject *self) +{ + PyObject *sr, *rv; + sr = PyObject_Repr((PyObject *)self->s); + if (sr == NULL) + return NULL; + rv = PyString_FromFormat("<htmltext %s>", PyString_AsString(sr)); + Py_DECREF(sr); + return rv; +} + +static PyObject * +htmltext_richcompare(PyObject *a, PyObject *b, int op) +{ + if (htmltextObject_Check(a)) { + a = htmltext_STR(a); + } + if (htmltextObject_Check(b)) { + b = htmltext_STR(b); + } + return PyObject_RichCompare(a, b, op); +} + +static long +htmltext_length(htmltextObject *self) +{ + return PyObject_Size(htmltext_STR(self)); +} + + +static PyObject * +wrap_arg(PyObject *arg) +{ + PyObject *warg; + if (htmltextObject_Check(arg)) { + /* don't bother with wrapper object */ + warg = arg; + Py_INCREF(arg); + } + else { + warg = quote_wrapper_new(arg); + } + return warg; +} + + +static PyObject * +htmltext_format(htmltextObject *self, PyObject *args) +{ + /* wrap the format arguments with QuoteWrapperObject */ + int is_unicode; + PyObject *rv, *wargs; + if (PyUnicode_Check(self->s)) { + is_unicode = 1; + } + else { + is_unicode = 0; + assert (PyString_Check(self->s)); + } + if (PyTuple_Check(args)) { + long i, n = PyTuple_GET_SIZE(args); + wargs = PyTuple_New(n); + for (i=0; i < n; i++) { + PyObject *wvalue = wrap_arg(PyTuple_GET_ITEM(args, i)); + if (wvalue == NULL) { + Py_DECREF(wargs); + return NULL; + } + PyTuple_SetItem(wargs, i, wvalue); + } + } + else { + wargs = wrap_arg(args); + if (wargs == NULL) + return NULL; + } + if (is_unicode) + rv = PyUnicode_Format(self->s, wargs); + else + rv = PyString_Format(self->s, wargs); + Py_DECREF(wargs); + return htmltext_from_string(rv); +} + +static PyObject * +htmltext_add(PyObject *v, PyObject *w) +{ + PyObject *qv, *qw, *rv; + if (htmltextObject_Check(v) && htmltextObject_Check(w)) { + qv = htmltext_STR(v); + qw = htmltext_STR(w); + Py_INCREF(qv); + Py_INCREF(qw); + } + else if (string_check(w)) { + assert (htmltextObject_Check(v)); + qv = htmltext_STR(v); + qw = escape(w); + if (qw == NULL) + return NULL; + Py_INCREF(qv); + } + else if (string_check(v)) { + assert (htmltextObject_Check(w)); + qv = escape(v); + if (qv == NULL) + return NULL; + qw = htmltext_STR(w); + Py_INCREF(qw); + } + else { + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } + if (PyString_Check(qv)) { + PyString_ConcatAndDel(&qv, qw); + rv = qv; + } + else { + assert (PyUnicode_Check(qv)); + rv = PyUnicode_Concat(qv, qw); + Py_DECREF(qv); + Py_DECREF(qw); + } + return htmltext_from_string(rv); +} + +static PyObject * +htmltext_repeat(htmltextObject *self, int n) +{ + PyObject *s = PySequence_Repeat(htmltext_STR(self), n); + if (s == NULL) + return NULL; + return htmltext_from_string(s); +} + +static PyObject * +htmltext_join(PyObject *self, PyObject *args) +{ + int i; + PyObject *quoted_args, *rv; + + quoted_args = PySequence_List(args); + if (quoted_args == NULL) + return NULL; + for (i=0; i < PyList_Size(quoted_args); i++) { + PyObject *value, *qvalue; + value = PyList_GET_ITEM(quoted_args, i); + if (value == NULL) { + goto error; + } + if (htmltextObject_Check(value)) { + qvalue = htmltext_STR(value); + Py_INCREF(qvalue); + } + else { + if (!string_check(value)) { + type_error("join requires a list of strings"); + goto error; + } + qvalue = escape(value); + if (qvalue == NULL) + goto error; + } + if (PyList_SetItem(quoted_args, i, qvalue) < 0) { + goto error; + } + } + if (PyUnicode_Check(htmltext_STR(self))) { + rv = PyUnicode_Join(htmltext_STR(self), quoted_args); + } + else { + rv = _PyString_Join(htmltext_STR(self), quoted_args); + } + Py_DECREF(quoted_args); + return htmltext_from_string(rv); + +error: + Py_DECREF(quoted_args); + return NULL; +} + +static PyObject * +quote_arg(PyObject *s) +{ + PyObject *ss; + if (string_check(s)) { + ss = escape(s); + if (ss == NULL) + return NULL; + } + else if (htmltextObject_Check(s)) { + ss = htmltext_STR(s); + Py_INCREF(ss); + } + else { + return type_error("string object required"); + } + return ss; +} + +static PyObject * +htmltext_call_method1(PyObject *self, PyObject *s, char *method) +{ + PyObject *ss, *rv; + ss = quote_arg(s); + if (ss == NULL) + return NULL; + rv = PyObject_CallMethod(htmltext_STR(self), method, "O", ss); + Py_DECREF(ss); + return rv; +} + +static PyObject * +htmltext_startswith(PyObject *self, PyObject *s) +{ + return htmltext_call_method1(self, s, "startswith"); +} + +static PyObject * +htmltext_endswith(PyObject *self, PyObject *s) +{ + return htmltext_call_method1(self, s, "endswith"); +} + +static PyObject * +htmltext_replace(PyObject *self, PyObject *args) +{ + PyObject *old, *new, *q_old, *q_new, *rv; + int maxsplit = -1; + if (!PyArg_ParseTuple(args,"OO|i:replace", &old, &new, &maxsplit)) + return NULL; + q_old = quote_arg(old); + if (q_old == NULL) + return NULL; + q_new = quote_arg(new); + if (q_new == NULL) { + Py_DECREF(q_old); + return NULL; + } + rv = PyObject_CallMethod(htmltext_STR(self), "replace", "OOi", + q_old, q_new, maxsplit); + Py_DECREF(q_old); + Py_DECREF(q_new); + return htmltext_from_string(rv); +} + + +static PyObject * +htmltext_lower(PyObject *self) +{ + return htmltext_from_string(PyObject_CallMethod(htmltext_STR(self), + "lower", "")); +} + +static PyObject * +htmltext_upper(PyObject *self) +{ + return htmltext_from_string(PyObject_CallMethod(htmltext_STR(self), + "upper", "")); +} + +static PyObject * +htmltext_capitalize(PyObject *self) +{ + return htmltext_from_string(PyObject_CallMethod(htmltext_STR(self), + "capitalize", "")); +} + +static PyObject * +template_io_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + TemplateIO_Object *self; + int html = 0; + static char *kwlist[] = {"html", 0}; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i:TemplateIO", + kwlist, &html)) + return NULL; + self = (TemplateIO_Object *)type->tp_alloc(type, 0); + if (self == NULL) { + return NULL; + } + self->data = PyList_New(0); + if (self->data == NULL) { + Py_DECREF(self); + return NULL; + } + self->html = html != 0; + return (PyObject *)self; +} + +static void +template_io_dealloc(TemplateIO_Object *self) +{ + Py_DECREF(self->data); + self->ob_type->tp_free((PyObject *)self); +} + +static PyObject * +template_io_str(TemplateIO_Object *self) +{ + static PyObject *empty = NULL; + if (empty == NULL) { + empty = PyString_FromStringAndSize(NULL, 0); + if (empty == NULL) + return NULL; + } + return _PyString_Join(empty, self->data); +} + +static PyObject * +template_io_getvalue(TemplateIO_Object *self) +{ + if (self->html) { + return htmltext_from_string(template_io_str(self)); + } + else { + return template_io_str(self); + } +} + +static PyObject * +template_io_iadd(TemplateIO_Object *self, PyObject *other) +{ + PyObject *s = NULL; + if (!TemplateIO_Check(self)) + return type_error("TemplateIO object required"); + if (other == Py_None) { + Py_INCREF(self); + return (PyObject *)self; + } + else if (htmltextObject_Check(other)) { + s = htmltext_STR(other); + Py_INCREF(s); + } + else { + if (self->html) { + PyObject *ss = stringify(other); + if (ss == NULL) + return NULL; + s = escape(ss); + Py_DECREF(ss); + } + else { + s = stringify(other); + } + if (s == NULL) + return NULL; + } + if (PyList_Append(self->data, s) != 0) + return NULL; + Py_DECREF(s); + Py_INCREF(self); + return (PyObject *)self; +} + +static PyMethodDef htmltext_methods[] = { + {"join", (PyCFunction)htmltext_join, METH_O, ""}, + {"startswith", (PyCFunction)htmltext_startswith, METH_O, ""}, + {"endswith", (PyCFunction)htmltext_endswith, METH_O, ""}, + {"replace", (PyCFunction)htmltext_replace, METH_VARARGS, ""}, + {"lower", (PyCFunction)htmltext_lower, METH_NOARGS, ""}, + {"upper", (PyCFunction)htmltext_upper, METH_NOARGS, ""}, + {"capitalize", (PyCFunction)htmltext_capitalize, METH_NOARGS, ""}, + {NULL, NULL} +}; + +static PyMemberDef htmltext_members[] = { + {"s", T_OBJECT, offsetof(htmltextObject, s), READONLY, "the string"}, + {NULL}, +}; + +static PySequenceMethods htmltext_as_sequence = { + (inquiry)htmltext_length, /*sq_length*/ + 0, /*sq_concat*/ + (intargfunc)htmltext_repeat, /*sq_repeat*/ + 0, /*sq_item*/ + 0, /*sq_slice*/ + 0, /*sq_ass_item*/ + 0, /*sq_ass_slice*/ + 0, /*sq_contains*/ +}; + +static PyNumberMethods htmltext_as_number = { + (binaryfunc)htmltext_add, /*nb_add*/ + 0, /*nb_subtract*/ + 0, /*nb_multiply*/ + 0, /*nb_divide*/ + (binaryfunc)htmltext_format, /*nb_remainder*/ + 0, /*nb_divmod*/ + 0, /*nb_power*/ + 0, /*nb_negative*/ + 0, /*nb_positive*/ + 0, /*nb_absolute*/ + 0, /*nb_nonzero*/ + 0, /*nb_invert*/ + 0, /*nb_lshift*/ + 0, /*nb_rshift*/ + 0, /*nb_and*/ + 0, /*nb_xor*/ + 0, /*nb_or*/ + 0, /*nb_coerce*/ + 0, /*nb_int*/ + 0, /*nb_long*/ + 0, /*nb_float*/ +}; + +static PyTypeObject htmltext_Type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "htmltext", /*tp_name*/ + sizeof(htmltextObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)htmltext_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + (unaryfunc)htmltext_repr,/*tp_repr*/ + &htmltext_as_number, /*tp_as_number*/ + &htmltext_as_sequence, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + htmltext_hash, /*tp_hash*/ + 0, /*tp_call*/ + (unaryfunc)htmltext_str,/*tp_str*/ + 0, /*tp_getattro set to PyObject_GenericGetAttr by module init*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE \ + | Py_TPFLAGS_CHECKTYPES, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + htmltext_richcompare, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + htmltext_methods, /*tp_methods*/ + htmltext_members, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc set to PyType_GenericAlloc by module init*/ + htmltext_new, /*tp_new*/ + 0, /*tp_free set to _PyObject_Del by module init*/ + 0, /*tp_is_gc*/ +}; + +static PyNumberMethods quote_wrapper_as_number = { + 0, /*nb_add*/ + 0, /*nb_subtract*/ + 0, /*nb_multiply*/ + 0, /*nb_divide*/ + 0, /*nb_remainder*/ + 0, /*nb_divmod*/ + 0, /*nb_power*/ + 0, /*nb_negative*/ + 0, /*nb_positive*/ + 0, /*nb_absolute*/ + 0, /*nb_nonzero*/ + 0, /*nb_invert*/ + 0, /*nb_lshift*/ + 0, /*nb_rshift*/ + 0, /*nb_and*/ + 0, /*nb_xor*/ + 0, /*nb_or*/ + 0, /*nb_coerce*/ + 0, /*nb_int*/ + 0, /*nb_long*/ + 0, /*nb_float*/ +}; + +static PyMappingMethods quote_wrapper_as_mapping = { + 0, /*mp_length*/ + (binaryfunc)quote_wrapper_subscript, /*mp_subscript*/ + 0, /*mp_ass_subscript*/ +}; + + +static PyTypeObject QuoteWrapper_Type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "QuoteWrapper", /*tp_name*/ + sizeof(QuoteWrapperObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)quote_wrapper_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + (unaryfunc)quote_wrapper_repr,/*tp_repr*/ + "e_wrapper_as_number,/*tp_as_number*/ + 0, /*tp_as_sequence*/ + "e_wrapper_as_mapping,/*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + (unaryfunc)quote_wrapper_str, /*tp_str*/ +}; + +static PyNumberMethods template_io_as_number = { + 0, /*nb_add*/ + 0, /*nb_subtract*/ + 0, /*nb_multiply*/ + 0, /*nb_divide*/ + 0, /*nb_remainder*/ + 0, /*nb_divmod*/ + 0, /*nb_power*/ + 0, /*nb_negative*/ + 0, /*nb_positive*/ + 0, /*nb_absolute*/ + 0, /*nb_nonzero*/ + 0, /*nb_invert*/ + 0, /*nb_lshift*/ + 0, /*nb_rshift*/ + 0, /*nb_and*/ + 0, /*nb_xor*/ + 0, /*nb_or*/ + 0, /*nb_coerce*/ + 0, /*nb_int*/ + 0, /*nb_long*/ + 0, /*nb_float*/ + 0, /*nb_oct*/ + 0, /*nb_hex*/ + (binaryfunc)template_io_iadd, /*nb_inplace_add*/ +}; + +static PyMethodDef template_io_methods[] = { + {"getvalue", (PyCFunction)template_io_getvalue, METH_NOARGS, ""}, + {NULL, NULL} +}; + +static PyTypeObject TemplateIO_Type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "TemplateIO", /*tp_name*/ + sizeof(TemplateIO_Object),/*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)template_io_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + &template_io_as_number, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + (unaryfunc)template_io_str,/*tp_str*/ + 0, /*tp_getattro set to PyObject_GenericGetAttr by module init*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + template_io_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc set to PyType_GenericAlloc by module init*/ + template_io_new, /*tp_new*/ + 0, /*tp_free set to _PyObject_Del by module init*/ + 0, /*tp_is_gc*/ +}; + +/* --------------------------------------------------------------------- */ + +static PyObject * +html_escape(PyObject *self, PyObject *o) +{ + if (htmltextObject_Check(o)) { + Py_INCREF(o); + return o; + } + else { + PyObject *rv; + PyObject *s = stringify(o); + if (s == NULL) + return NULL; + rv = escape(s); + Py_DECREF(s); + return htmltext_from_string(rv); + } +} + +static PyObject * +py_escape_string(PyObject *self, PyObject *o) +{ + return escape(o); +} + +static PyObject * +py_stringify(PyObject *self, PyObject *o) +{ + return stringify(o); +} + +/* List of functions defined in the module */ + +static PyMethodDef htmltext_module_methods[] = { + {"htmlescape", (PyCFunction)html_escape, METH_O}, + {"_escape_string", (PyCFunction)py_escape_string, METH_O}, + {"stringify", (PyCFunction)py_stringify, METH_O}, + {NULL, NULL} +}; + +static char module_doc[] = "htmltext string type"; + +void +init_c_htmltext(void) +{ + PyObject *m; + + /* Initialize the type of the new type object here; doing it here + * is required for portability to Windows without requiring C++. */ + htmltext_Type.ob_type = &PyType_Type; + QuoteWrapper_Type.ob_type = &PyType_Type; + TemplateIO_Type.ob_type = &PyType_Type; + + /* Fix not constant element initialization */ + htmltext_Type.tp_getattro = PyObject_GenericGetAttr; + htmltext_Type.tp_alloc = PyType_GenericAlloc; + htmltext_Type.tp_free = _PyObject_Del; + TemplateIO_Type.tp_getattro = PyObject_GenericGetAttr; + TemplateIO_Type.tp_alloc = PyType_GenericAlloc; + TemplateIO_Type.tp_free = _PyObject_Del; + + /* Create the module and add the functions */ + m = Py_InitModule4("_c_htmltext", htmltext_module_methods, module_doc, + NULL, PYTHON_API_VERSION); + + Py_INCREF((PyObject *)&htmltext_Type); + Py_INCREF((PyObject *)&QuoteWrapper_Type); + Py_INCREF((PyObject *)&TemplateIO_Type); + PyModule_AddObject(m, "htmltext", (PyObject *)&htmltext_Type); + PyModule_AddObject(m, "TemplateIO", (PyObject *)&TemplateIO_Type); +} diff --git a/pypers/europython05/Quixote-2.0/html/_py_htmltext.py b/pypers/europython05/Quixote-2.0/html/_py_htmltext.py new file mode 100755 index 0000000..798944d --- /dev/null +++ b/pypers/europython05/Quixote-2.0/html/_py_htmltext.py @@ -0,0 +1,213 @@ +"""Python implementation of the htmltext type, the htmlescape function and +TemplateIO. +""" + +#$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/html/_py_htmltext.py $ +#$Id: _py_htmltext.py 26357 2005-03-16 14:56:23Z dbinger $ + +def _escape_string(s): + if not isinstance(s, basestring): + raise TypeError, 'string object required' + s = s.replace("&", "&") + s = s.replace("<", "<") + s = s.replace(">", ">") + s = s.replace('"', """) + return s + +def stringify(obj): + """Return 'obj' as a string or unicode object. Tries to prevent + turning strings into unicode objects. + """ + tp = type(obj) + if issubclass(tp, basestring): + return obj + elif hasattr(tp, '__unicode__'): + s = tp.__unicode__(obj) + if not isinstance(s, basestring): + raise TypeError, '__unicode__ did not return a string' + return s + elif hasattr(tp, '__str__'): + s = tp.__str__(obj) + if not isinstance(s, basestring): + raise TypeError, '__str__ did not return a string' + return s + else: + return str(obj) + +class htmltext(object): + """The htmltext string-like type. This type serves as a tag + signifying that HTML special characters do not need to be escaped + using entities. + """ + + __slots__ = ['s'] + + def __init__(self, s): + self.s = stringify(s) + + # XXX make read-only + #def __setattr__(self, name, value): + # raise AttributeError, 'immutable object' + + def __getstate__(self): + raise ValueError, 'htmltext objects should not be pickled' + + def __repr__(self): + return '<htmltext %r>' % self.s + + def __str__(self): + return self.s + + def __len__(self): + return len(self.s) + + def __cmp__(self, other): + return cmp(self.s, other) + + def __hash__(self): + return hash(self.s) + + def __mod__(self, args): + if isinstance(args, tuple): + return htmltext(self.s % tuple(map(_wraparg, args))) + else: + return htmltext(self.s % _wraparg(args)) + + def __add__(self, other): + if isinstance(other, basestring): + return htmltext(self.s + _escape_string(other)) + elif isinstance(other, htmltext): + return htmltext(self.s + other.s) + else: + return NotImplemented + + def __radd__(self, other): + if isinstance(other, basestring): + return htmltext(_escape_string(other) + self.s) + else: + return NotImplemented + + def __mul__(self, n): + return htmltext(self.s * n) + + def join(self, items): + quoted_items = [] + for item in items: + if isinstance(item, htmltext): + quoted_items.append(stringify(item)) + elif isinstance(item, basestring): + quoted_items.append(_escape_string(item)) + else: + raise TypeError( + 'join() requires string arguments (got %r)' % item) + return htmltext(self.s.join(quoted_items)) + + def startswith(self, s): + if isinstance(s, htmltext): + s = s.s + else: + s = _escape_string(s) + return self.s.startswith(s) + + def endswith(self, s): + if isinstance(s, htmltext): + s = s.s + else: + s = _escape_string(s) + return self.s.endswith(s) + + def replace(self, old, new, maxsplit=-1): + if isinstance(old, htmltext): + old = old.s + else: + old = _escape_string(old) + if isinstance(new, htmltext): + new = new.s + else: + new = _escape_string(new) + return htmltext(self.s.replace(old, new)) + + def lower(self): + return htmltext(self.s.lower()) + + def upper(self): + return htmltext(self.s.upper()) + + def capitalize(self): + return htmltext(self.s.capitalize()) + +class _QuoteWrapper(object): + # helper for htmltext class __mod__ + + __slots__ = ['value'] + + def __init__(self, value): + self.value = value + + def __str__(self): + return _escape_string(stringify(self.value)) + + def __repr__(self): + return _escape_string(`self.value`) + + def __getitem__(self, key): + return _wraparg(self.value[key]) + + +def _wraparg(arg): + if (isinstance(arg, htmltext) or + isinstance(arg, int) or + isinstance(arg, long) or + isinstance(arg, float)): + # ints, longs, floats, and htmltext are okay + return arg + else: + # everything is gets wrapped + return _QuoteWrapper(arg) + +def htmlescape(s): + """htmlescape(s) -> htmltext + + Return an 'htmltext' object using the argument. If the argument is not + already a 'htmltext' object then the HTML markup characters \", <, >, + and & are first escaped. + """ + if isinstance(s, htmltext): + return s + else: + s = stringify(s) + # inline _escape_string for speed + s = s.replace("&", "&") # must be done first + s = s.replace("<", "<") + s = s.replace(">", ">") + s = s.replace('"', """) + return htmltext(s) + + +class TemplateIO(object): + """Collect output for PTL scripts. + """ + + __slots__ = ['html', 'data'] + + def __init__(self, html=False): + self.html = html + self.data = [] + + def __iadd__(self, other): + if other is not None: + self.data.append(other) + return self + + def __repr__(self): + return ("<%s at %x: %d chunks>" % + (self.__class__.__name__, id(self), len(self.data))) + + def __str__(self): + return stringify(self.getvalue()) + + def getvalue(self): + if self.html: + return htmltext('').join(map(htmlescape, self.data)) + else: + return ''.join(map(stringify, self.data)) diff --git a/pypers/europython05/Quixote-2.0/html/test/utest_html.py b/pypers/europython05/Quixote-2.0/html/test/utest_html.py new file mode 100755 index 0000000..fa5450f --- /dev/null +++ b/pypers/europython05/Quixote-2.0/html/test/utest_html.py @@ -0,0 +1,368 @@ +import sys +from sancho.utest import UTest +from quixote.html import _py_htmltext +from quixote.html import href, url_with_query, url_quote, nl2br, htmltag + +markupchars = '<>&"' +quotedchars = '<>&"' +if sys.hexversion >= 0x20400a2: + unicodechars = u'\u1234' +else: + unicodechars = 'x' # lie, Python <= 2.3 is broken + +class Wrapper: + def __init__(self, s): + self.s = s + + def __repr__(self): + return self.s + + def __str__(self): + return self.s + +class BrokenError(Exception): + pass + +class Broken: + def __str__(self): + raise BrokenError, 'eieee' + + def __repr__(self): + raise BrokenError, 'eieee' + +htmltext = escape = htmlescape = TemplateIO = stringify = None + +class HTMLTest (UTest): + + def check_href(self): + assert str(href('/foo/bar', 'bar')) == '<a href="/foo/bar">bar</a>' + + def check_url_with_query(self): + assert str(url_with_query('/f/b', a='1')) == '/f/b?a=1' + assert str(url_with_query( + '/f/b', a='1', b='3 4')) == '/f/b?a=1&b=3%204' + + def check_nl2br(self): + assert str(nl2br('a\nb\nc')) == 'a<br />\nb<br />\nc' + + def check_url_quote(self): + assert url_quote('abc') == 'abc' + assert url_quote('a b c') == 'a%20b%20c' + assert url_quote(None, fallback='abc') == 'abc' + + +class HTMLTextTest (UTest): + + def _pre(self): + global htmltext, escape, htmlescape, TemplateIO, stringify + htmltext = _py_htmltext.htmltext + escape = _py_htmltext._escape_string + stringify = _py_htmltext.stringify + htmlescape = _py_htmltext.htmlescape + TemplateIO = _py_htmltext.TemplateIO + + def _post(self): + global htmltext, escape, htmlescape, TemplateIO, stringify + htmltext = escape = htmlescape = TemplateIO = stringify = None + + def _check_init(self): + assert str(htmltext('foo')) == 'foo' + assert str(htmltext(markupchars)) == markupchars + assert unicode(htmltext(unicodechars)) == unicodechars + assert str(htmltext(unicode(markupchars))) == markupchars + assert str(htmltext(None)) == 'None' + assert str(htmltext(1)) == '1' + try: + htmltext(Broken()) + assert 0 + except BrokenError: pass + + def check_stringify(self): + assert stringify(markupchars) is markupchars + assert stringify(unicodechars) is unicodechars + assert stringify(Wrapper(unicodechars)) is unicodechars + assert stringify(Wrapper(markupchars)) is markupchars + assert stringify(Wrapper) == str(Wrapper) + assert stringify(None) == str(None) + + def check_escape(self): + assert htmlescape(markupchars) == quotedchars + assert isinstance(htmlescape(markupchars), htmltext) + assert escape(markupchars) == quotedchars + assert escape(unicodechars) == unicodechars + assert escape(unicode(markupchars)) == quotedchars + assert isinstance(escape(markupchars), basestring) + assert htmlescape(htmlescape(markupchars)) == quotedchars + try: + escape(1) + assert 0 + except TypeError: pass + + def check_cmp(self): + s = htmltext("foo") + assert s == 'foo' + assert s != 'bar' + assert s == htmltext('foo') + assert s != htmltext('bar') + assert htmltext(u'\u1234') == u'\u1234' + assert htmltext('1') != 1 + assert 1 != s + + def check_len(self): + assert len(htmltext('foo')) == 3 + assert len(htmltext(markupchars)) == len(markupchars) + assert len(htmlescape(markupchars)) == len(quotedchars) + + def check_hash(self): + assert hash(htmltext('foo')) == hash('foo') + assert hash(htmltext(markupchars)) == hash(markupchars) + assert hash(htmlescape(markupchars)) == hash(quotedchars) + + def check_concat(self): + s = htmltext("foo") + assert s + 'bar' == "foobar" + assert 'bar' + s == "barfoo" + assert s + htmltext('bar') == "foobar" + assert s + markupchars == "foo" + quotedchars + assert isinstance(s + markupchars, htmltext) + assert markupchars + s == quotedchars + "foo" + assert isinstance(markupchars + s, htmltext) + assert markupchars + htmltext(u'') == quotedchars + try: + s + 1 + assert 0 + except TypeError: pass + try: + 1 + s + assert 0 + except TypeError: pass + # mixing unicode and str + assert repr(htmltext('a') + htmltext('b')) == "<htmltext 'ab'>" + assert repr(htmltext(u'a') + htmltext('b')) == "<htmltext u'ab'>" + assert repr(htmltext('a') + htmltext(u'b')) == "<htmltext u'ab'>" + + def check_repeat(self): + s = htmltext('a') + assert s * 3 == "aaa" + assert isinstance(s * 3, htmltext) + assert htmlescape(markupchars) * 3 == quotedchars * 3 + try: + s * 'a' + assert 0 + except TypeError: pass + try: + 'a' * s + assert 0 + except TypeError: pass + try: + s * s + assert 0 + except TypeError: pass + + def check_format(self): + s_fmt = htmltext('%s') + u_fmt = htmltext(u'%s') + assert s_fmt % 'foo' == "foo" + assert u_fmt % 'foo' == u"foo" + assert isinstance(s_fmt % 'foo', htmltext) + assert isinstance(u_fmt % 'foo', htmltext) + assert s_fmt % markupchars == quotedchars + assert u_fmt % markupchars == quotedchars + assert s_fmt % None == "None" + assert u_fmt % None == "None" + assert u_fmt % unicodechars == unicodechars + assert htmltext('%r') % Wrapper(markupchars) == quotedchars + assert htmltext('%s%s') % ('foo', htmltext(markupchars)) \ + == ("foo" + markupchars) + assert htmltext('%d') % 10 == "10" + assert htmltext('%.1f') % 10 == "10.0" + try: + s_fmt % Broken() + assert 0 + except BrokenError: pass + try: + htmltext('%r') % Broken() + assert 0 + except BrokenError: pass + try: + s_fmt % (1, 2) + assert 0 + except TypeError: pass + assert htmltext('%d') % 12300000000000000000L == "12300000000000000000" + + def check_dict_format(self): + args = {'a': 'foo&', 'b': htmltext('bar&')} + result = "foo& 'foo&' bar&" + assert htmltext('%(a)s %(a)r %(b)s') % args == result + assert htmltext('%(a)s') % {'a': 'foo&'} == "foo&" + assert isinstance(htmltext('%(a)s') % {'a': 'a'}, htmltext) + assert htmltext('%s') % {'a': 'foo&'} == "{'a': 'foo&'}" + try: + htmltext('%(a)s') % 1 + assert 0 + except TypeError: pass + try: + htmltext('%(a)s') % {} + assert 0 + except KeyError: pass + assert htmltext('') % {} == '' + assert htmltext('%%') % {} == '%' + + def check_join(self): + assert htmltext(' ').join(['foo', 'bar']) == "foo bar" + assert htmltext(' ').join(['foo', markupchars]) == \ + "foo " + quotedchars + assert htmlescape(markupchars).join(['foo', 'bar']) == \ + "foo" + quotedchars + "bar" + assert htmltext(' ').join([htmltext(markupchars), 'bar']) == \ + markupchars + " bar" + assert isinstance(htmltext('').join([]), htmltext) + assert htmltext(u' ').join([unicodechars]) == unicodechars + assert htmltext(u' ').join(['']) == u'' + try: + htmltext('').join(1) + assert 0 + except TypeError: pass + try: + htmltext('').join([1]) + assert 0 + except TypeError: pass + + def check_startswith(self): + assert htmltext('foo').startswith('fo') + assert htmlescape(markupchars).startswith(markupchars[:3]) + assert htmltext(markupchars).startswith(htmltext(markupchars[:3])) + try: + htmltext('').startswith(1) + assert 0 + except TypeError: pass + + def check_endswith(self): + assert htmltext('foo').endswith('oo') + assert htmlescape(markupchars).endswith(markupchars[-3:]) + assert htmltext(markupchars).endswith(htmltext(markupchars[-3:])) + try: + htmltext('').endswith(1) + assert 0 + except TypeError: pass + + def check_replace(self): + assert htmlescape('&').replace('&', 'foo') == "foo" + assert htmltext('&').replace(htmltext('&'), 'foo') == "foo" + assert htmltext('foo').replace('foo', htmltext('&')) == "&" + assert isinstance(htmltext('a').replace('a', 'b'), htmltext) + try: + htmltext('').replace(1, 'a') + assert 0 + except TypeError: pass + + def check_lower(self): + assert htmltext('aB').lower() == "ab" + assert isinstance(htmltext('a').lower(), htmltext) + + def check_upper(self): + assert htmltext('aB').upper() == "AB" + assert isinstance(htmltext('a').upper(), htmltext) + + def check_capitalize(self): + assert htmltext('aB').capitalize() == "Ab" + assert isinstance(htmltext('a').capitalize(), htmltext) + +class TemplateTest (UTest): + + def _pre(self): + global TemplateIO + TemplateIO = _py_htmltext.TemplateIO + + def _post(self): + global TemplateIO + TemplateIO = None + + def check_init(self): + TemplateIO() + TemplateIO(html=True) + TemplateIO(html=False) + + def check_text_iadd(self): + t = TemplateIO() + assert t.getvalue() == '' + t += "abcd" + assert t.getvalue() == 'abcd' + t += None + assert t.getvalue() == 'abcd' + t += 123 + assert t.getvalue() == 'abcd123' + t += u'\u1234' + assert t.getvalue() == u'abcd123\u1234' + try: + t += Broken(); t.getvalue() + assert 0 + except BrokenError: pass + + def check_html_iadd(self): + t = TemplateIO(html=1) + assert t.getvalue() == '' + t += "abcd" + assert t.getvalue() == 'abcd' + t += None + assert t.getvalue() == 'abcd' + t += 123 + assert t.getvalue() == 'abcd123' + try: + t += Broken(); t.getvalue() + assert 0 + except BrokenError: pass + t = TemplateIO(html=1) + t += markupchars + assert t.getvalue() == quotedchars + + def check_repr(self): + t = TemplateIO() + t += "abcd" + assert "TemplateIO" in repr(t) + + def check_str(self): + t = TemplateIO() + t += "abcd" + assert str(t) == "abcd" + + + +try: + from quixote.html import _c_htmltext +except ImportError: + _c_htmltext = None + +if _c_htmltext: + class CHTMLTest(HTMLTest): + def _pre(self): + # using globals like this is a bit of a hack since it assumes + # Sancho tests each class individually, oh well + global htmltext, escape, htmlescape, stringify + htmltext = _c_htmltext.htmltext + escape = _c_htmltext._escape_string + stringify = _py_htmltext.stringify + htmlescape = _c_htmltext.htmlescape + + class CHTMLTextTest(HTMLTextTest): + def _pre(self): + global htmltext, escape, htmlescape, stringify + htmltext = _c_htmltext.htmltext + escape = _c_htmltext._escape_string + stringify = _py_htmltext.stringify + htmlescape = _c_htmltext.htmlescape + + class CTemplateTest(TemplateTest): + def _pre(self): + global TemplateIO + TemplateIO = _c_htmltext.TemplateIO + + +if __name__ == "__main__": + HTMLTest() + HTMLTextTest() + TemplateTest() + if _c_htmltext: + CHTMLTest() + CHTMLTextTest() + CTemplateTest() diff --git a/pypers/europython05/Quixote-2.0/http_request.py b/pypers/europython05/Quixote-2.0/http_request.py new file mode 100755 index 0000000..6a9602d --- /dev/null +++ b/pypers/europython05/Quixote-2.0/http_request.py @@ -0,0 +1,759 @@ +"""quixote.http_request +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/http_request.py $ +$Id: http_request.py 26337 2005-03-11 17:06:05Z dbinger $ + +Provides the HTTPRequest class and related code for parsing HTTP +requests, such as the Upload class. +""" + +import re +import string +import tempfile +import urllib +import rfc822 +from cStringIO import StringIO + +from quixote.http_response import HTTPResponse +from quixote.errors import RequestError + + +# Various regexes for parsing specific bits of HTTP, all from RFC 2616. + +# These are needed by 'get_encoding()', to parse the "Accept-Encoding" +# header. LWS is linear whitespace; the latter two assume that LWS +# has been removed. +_http_lws_re = re.compile(r"(\r\n)?[ \t]+") +_http_list_re = re.compile(r",+") +_http_encoding_re = re.compile(r"([^;]+)(;q=([\d.]+))?$") + +# These are needed by 'guess_browser_version()', for parsing the +# "User-Agent" header. +# token = 1*<any CHAR except CTLs or separators> +# CHAR = any 7-bit US ASCII character (0-127) +# separators are ( ) < > @ , ; : \ " / [ ] ? = { } +# +# The user_agent RE is a simplification; it only looks for one "product", +# possibly followed by a comment. +_http_token_pat = r"[\w!#$%&'*+.^`|~-]+" +_http_product_pat = r'(%s)(?:/(%s))?' % (_http_token_pat, _http_token_pat) +_http_product_re = re.compile(_http_product_pat) +_comment_delim_re = re.compile(r';\s*') + + +def get_content_type(environ): + ctype = environ.get("CONTENT_TYPE") + if ctype: + return ctype.split(";")[0] + else: + return None + +def _decode_string(s, charset): + if charset == 'iso-8859-1': + return s + try: + return s.decode(charset) + except LookupError: + raise RequestError('unknown charset %r' % charset) + except UnicodeDecodeError: + raise RequestError('invalid %r encoded string' % charset) + +def parse_header(line): + """Parse a Content-type like header. + + Return the main content-type and a dictionary of options. + + """ + plist = map(lambda x: x.strip(), line.split(';')) + key = plist.pop(0).lower() + pdict = {} + for p in plist: + i = p.find('=') + if i >= 0: + name = p[:i].strip().lower() + value = p[i+1:].strip() + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + pdict[name] = value + return key, pdict + +def parse_content_disposition(full_cdisp): + (cdisp, cdisp_params) = parse_header(full_cdisp) + name = cdisp_params.get('name') + if not (cdisp == 'form-data' and name): + raise RequestError('expected Content-Disposition: form-data ' + 'with a "name" parameter: got %r' % full_cdisp) + return (name, cdisp_params.get('filename')) + +def parse_query(qs, charset): + """(qs: string) -> {key:string, string|[string]} + + Parse a query given as a string argument and return a dictionary. + """ + fields = {} + for chunk in filter(None, qs.split('&')): + if '=' not in chunk: + name = chunk + value = '' + else: + name, value = chunk.split('=', 1) + name = urllib.unquote(name.replace('+', ' ')) + value = urllib.unquote(value.replace('+', ' ')) + name = _decode_string(name, charset) + value = _decode_string(value, charset) + _add_field_value(fields, name, value) + return fields + +def _add_field_value(fields, name, value): + if name in fields: + values = fields[name] + if not isinstance(values, list): + fields[name] = values = [values] + values.append(value) + else: + fields[name] = value + + +class HTTPRequest: + """ + Model a single HTTP request and all associated data: environment + variables, form variables, cookies, etc. + + To access environment variables associated with the request, use + get_environ(): eg. request.get_environ('SERVER_PORT', 80). + + To access form variables, use get_field(), eg. + request.get_field("name"). + + To access cookies, use get_cookie(). + + Various bits and pieces of the requested URL can be accessed with + get_url(), get_path(), get_server() + + The HTTPResponse object corresponding to this request is available + in the 'response' attribute. This is rarely needed: eg. to send an + error response, you should raise one of the exceptions in errors.py; + to send a redirect, you should use the request's redirect() method, + which lets you specify relative URLs. However, if you need to tweak + the response object in other ways, you can do so via 'response'. + Just keep in mind that Quixote discards the original response object + when handling an exception. + """ + + DEFAULT_CHARSET = 'iso-8859-1' + + def __init__(self, stdin, environ): + self.stdin = stdin + self.environ = environ + self.form = {} + self.session = None + self.response = HTTPResponse() + + # The strange treatment of SERVER_PORT_SECURE is because IIS + # sets this environment variable to "0" for non-SSL requests + # (most web servers -- well, Apache at least -- simply don't set + # it in that case). + if (environ.get('HTTPS', 'off').lower() == 'on' or + environ.get('SERVER_PORT_SECURE', '0') != '0'): + self.scheme = "https" + else: + self.scheme = "http" + + k = self.environ.get('HTTP_COOKIE', '') + if k: + self.cookies = parse_cookies(k) + else: + self.cookies = {} + + # IIS breaks PATH_INFO because it leaves in the path to + # the script, so SCRIPT_NAME is "/cgi-bin/q.py" and PATH_INFO + # is "/cgi-bin/q.py/foo/bar". The following code fixes + # PATH_INFO to the expected value "/foo/bar". + web_server = environ.get('SERVER_SOFTWARE', 'unknown') + if web_server.find('Microsoft-IIS') != -1: + script = environ['SCRIPT_NAME'] + path = environ['PATH_INFO'] + if path.startswith(script): + path = path[len(script):] + self.environ['PATH_INFO'] = path + + def process_inputs(self): + query = self.get_query() + if query: + self.form.update(parse_query(query, self.DEFAULT_CHARSET)) + length = self.environ.get('CONTENT_LENGTH') or "0" + try: + length = int(length) + except ValueError: + raise RequestError('invalid content-length header') + ctype = self.environ.get("CONTENT_TYPE") + if ctype: + ctype, ctype_params = parse_header(ctype) + if ctype == 'application/x-www-form-urlencoded': + self._process_urlencoded(length, ctype_params) + elif ctype == 'multipart/form-data': + self._process_multipart(length, ctype_params) + + def _process_urlencoded(self, length, params): + query = self.stdin.read(length) + if len(query) != length: + raise RequestError('unexpected end of request body') + charset = params.get('charset', self.DEFAULT_CHARSET) + self.form.update(parse_query(query, charset)) + + def _process_multipart(self, length, params): + boundary = params.get('boundary') + if not boundary: + raise RequestError('multipart/form-data missing boundary') + charset = params.get('charset') + mimeinput = MIMEInput(self.stdin, boundary, length) + try: + for line in mimeinput.readpart(): + pass # discard lines up to first boundary + while mimeinput.moreparts(): + self._process_multipart_body(mimeinput, charset) + except EOFError: + raise RequestError('unexpected end of multipart/form-data') + + def _process_multipart_body(self, mimeinput, charset): + headers = StringIO() + lines = mimeinput.readpart() + for line in lines: + headers.write(line) + if line == '\r\n': + break + headers.seek(0) + headers = rfc822.Message(headers) + ctype, ctype_params = parse_header(headers.get('content-type', '')) + if ctype and 'charset' in ctype_params: + charset = ctype_params['charset'] + cdisp, cdisp_params = parse_header(headers.get('content-disposition', + '')) + if not cdisp: + raise RequestError('expected Content-Disposition header') + name = cdisp_params.get('name') + filename = cdisp_params.get('filename') + if not (cdisp == 'form-data' and name): + raise RequestError('expected Content-Disposition: form-data' + 'with a "name" parameter: got %r' % + headers.get('content-disposition', '')) + # FIXME: should really to handle Content-Transfer-Encoding and other + # MIME complexity here. See RFC2048 for the full horror story. + if filename: + # it might be large file upload so use a temporary file + upload = Upload(filename, ctype, charset) + upload.receive(lines) + _add_field_value(self.form, name, upload) + else: + value = _decode_string(''.join(lines), + charset or self.DEFAULT_CHARSET) + _add_field_value(self.form, name, value) + + def get_header(self, name, default=None): + """get_header(name : string, default : string = None) -> string + + Return the named HTTP header, or an optional default argument + (or None) if the header is not found. Note that both original + and CGI-ified header names are recognized, e.g. 'Content-Type', + 'CONTENT_TYPE' and 'HTTP_CONTENT_TYPE' should all return the + Content-Type header, if available. + """ + environ = self.environ + name = name.replace("-", "_").upper() + val = environ.get(name) + if val is not None: + return val + if name[:5] != 'HTTP_': + name = 'HTTP_' + name + return environ.get(name, default) + + def get_cookie(self, cookie_name, default=None): + return self.cookies.get(cookie_name, default) + + def get_cookies(self): + return self.cookies + + def get_field(self, name, default=None): + return self.form.get(name, default) + + def get_fields(self): + return self.form + + def get_method(self): + """Returns the HTTP method for this request + """ + return self.environ.get('REQUEST_METHOD', 'GET') + + def formiter(self): + return self.form.iteritems() + + def get_scheme(self): + return self.scheme + + # The following environment variables are useful for reconstructing + # the original URL, all of which are specified by CGI 1.1: + # + # SERVER_NAME "www.example.com" + # SCRIPT_NAME "/q" + # PATH_INFO "/debug/dump_sessions" + # QUERY_STRING "session_id=10.27.8.40...." + + def get_server(self): + """get_server() -> string + + Return the server name with an optional port number, eg. + "www.example.com" or "foo.bar.com:8000". + """ + http_host = self.environ.get("HTTP_HOST") + if http_host: + return http_host + server_name = self.environ["SERVER_NAME"].strip() + server_port = self.environ.get("SERVER_PORT") + if (not server_port or + (self.get_scheme() == "http" and server_port == "80") or + (self.get_scheme() == "https" and server_port == "443")): + return server_name + else: + return server_name + ":" + server_port + + def get_path(self, n=0): + """get_path(n : int = 0) -> string + + Return the path of the current request, chopping off 'n' path + components from the right. Eg. if the path is "/bar/baz/qux", + n=0 would return "/bar/baz/qux" and n=2 would return "/bar". + Note that the query string, if any, is not included. + + A path with a trailing slash should just be considered as having + an empty last component. Eg. if the path is "/bar/baz/", then: + get_path(0) == "/bar/baz/" + get_path(1) == "/bar/baz" + get_path(2) == "/bar" + + If 'n' is negative, then components from the left of the path + are returned. Continuing the above example, + get_path(-1) = "/bar" + get_path(-2) = "/bar/baz" + get_path(-3) = "/bar/baz/" + + Raises ValueError if absolute value of n is larger than the number of + path components.""" + + path_info = self.environ.get('PATH_INFO', '') + path = self.environ['SCRIPT_NAME'] + path_info + if n == 0: + return path + else: + path_comps = path.split('/') + if abs(n) > len(path_comps)-1: + raise ValueError, "n=%d too big for path '%s'" % (n, path) + if n > 0: + return '/'.join(path_comps[:-n]) + elif n < 0: + return '/'.join(path_comps[:-n+1]) + else: + assert 0, "Unexpected value for n (%s)" % n + + def get_query(self): + """() -> string + + Return the query component of the URL. + """ + return self.environ.get('QUERY_STRING', '') + + def get_url(self, n=0): + """get_url(n : int = 0) -> string + + Return the URL of the current request, chopping off 'n' path + components from the right. Eg. if the URL is + "http://foo.com/bar/baz/qux", n=2 would return + "http://foo.com/bar". Does not include the query string (if + any). + """ + return "%s://%s%s" % (self.get_scheme(), self.get_server(), + urllib.quote(self.get_path(n))) + + def get_environ(self, key, default=None): + """get_environ(key : string) -> string + + Fetch a CGI environment variable from the request environment. + See http://hoohoo.ncsa.uiuc.edu/cgi/env.html + for the variables specified by the CGI standard. + """ + return self.environ.get(key, default) + + def get_encoding(self, encodings): + """get_encoding(encodings : [string]) -> string + + Parse the "Accept-encoding" header. 'encodings' is a list of + encodings supported by the server sorted in order of preference. + The return value is one of 'encodings' or None if the client + does not accept any of the encodings. + """ + accept_encoding = self.get_header("accept-encoding") or "" + found_encodings = self._parse_pref_header(accept_encoding) + if found_encodings: + for encoding in encodings: + if found_encodings.has_key(encoding): + return encoding + return None + + def get_accepted_types(self): + """get_accepted_types() : {string:float} + Return a dictionary mapping MIME types the client will accept + to the corresponding quality value (1.0 if no value was specified). + """ + accept_types = self.environ.get('HTTP_ACCEPT', "") + return self._parse_pref_header(accept_types) + + + def _parse_pref_header(self, S): + """_parse_pref_header(S:string) : {string:float} + Parse a list of HTTP preferences (content types, encodings) and + return a dictionary mapping strings to the quality value. + """ + + found = {} + # remove all linear whitespace + S = _http_lws_re.sub("", S) + for coding in _http_list_re.split(S): + m = _http_encoding_re.match(coding) + if m: + encoding = m.group(1).lower() + q = m.group(3) or 1.0 + try: + q = float(q) + except ValueError: + continue + if encoding == "*": + continue # stupid, ignore it + if q > 0: + found[encoding] = q + return found + + def dump(self): + result=[] + row='%-15s %s' + + result.append("Form:") + L = self.form.items() ; L.sort() + for k,v in L: + result.append(row % (k,v)) + + result.append("") + result.append("Cookies:") + L = self.cookies.items() ; L.sort() + for k,v in L: + result.append(row % (k,v)) + + + result.append("") + result.append("Environment:") + L = self.environ.items() ; L.sort() + for k,v in L: + result.append(row % (k,v)) + return "\n".join(result) + + def guess_browser_version(self): + """guess_browser_version() -> (name : string, version : string) + + Examine the User-agent request header to try to figure out what + the current browser is. Returns either (name, version) where + each element is a string, (None, None) if we couldn't parse the + User-agent header at all, or (name, None) if we got the name but + couldn't figure out the version. + + Handles Microsoft's little joke of pretending to be Mozilla, + eg. if the "User-Agent" header is + Mozilla/5.0 (compatible; MSIE 5.5) + returns ("MSIE", "5.5"). Konqueror does the same thing, and + it's handled the same way. + """ + ua = self.get_header('user-agent') + if ua is None: + return (None, None) + + # The syntax for "User-Agent" in RFC 2616 is fairly simple: + # + # User-Agent = "User-Agent" ":" 1*( product | comment ) + # product = token ["/" product-version ] + # product-version = token + # comment = "(" *( ctext | comment ) ")" + # ctext = <any TEXT excluding "(" and ")"> + # token = 1*<any CHAR except CTLs or tspecials> + # tspecials = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | + # "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" | + # "}" | SP | HT + # + # This function handles the most-commonly-used subset of this syntax, + # namely + # User-Agent = "User-Agent" ":" product 1*SP [comment] + # ie. one product string followed by an optional comment; + # anything after that first comment is ignored. This should be + # enough to distinguish Mozilla/Netscape, MSIE, Opera, and + # Konqueror. + + m = _http_product_re.match(ua) + if not m: + import sys + sys.stderr.write("couldn't parse User-Agent header: %r\n" % ua) + return (None, None) + + name, version = m.groups() + ua = ua[m.end():].lstrip() + + if ua.startswith('('): + # we need to handle nested comments since MSIE uses them + depth = 1 + chars = [] + for c in ua[1:]: + if c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth == 0: + break + elif depth == 1: + # nested comments are discarded + chars.append(c) + comment = ''.join(chars) + else: + comment = '' + if comment: + comment_chunks = _comment_delim_re.split(comment) + else: + comment_chunks = [] + + if ("compatible" in comment_chunks and + len(comment_chunks) > 1 and comment_chunks[1]): + # A-ha! Someone is kidding around, pretending to be what + # they are not. Most likely MSIE masquerading as Mozilla, + # but lots of other clients (eg. Konqueror) do the same. + real_ua = comment_chunks[1] + if "/" in real_ua: + (name, version) = real_ua.split("/", 1) + else: + if real_ua.startswith("MSIE") and ' ' in real_ua: + (name, version) = real_ua.split(" ", 1) + else: + name = real_ua + version = None + return (name, version) + + # Either nobody is pulling our leg, or we didn't find anything + # that looks vaguely like a user agent in the comment. So use + # what we found outside the comment, ie. what the spec says we + # should use (sigh). + return (name, version) + + # guess_browser_version () + + +# See RFC 2109 for details. Note that this parser is more liberal. +_COOKIE_RE = re.compile(r""" + \s* + (?P<name>[^=;,\s]+) + \s* + ( + = + \s* + ( + (?P<qvalue> "(\\[\x00-\x7f] | [^"])*") + | + (?P<value> [^";,\s]*) + ) + )? + \s* + [;,]? + """, re.VERBOSE) + +def parse_cookies(text): + result = {} + for m in _COOKIE_RE.finditer(text): + name = m.group('name') + if name[0] == '$': + # discard, we don't handle per cookie attributes (e.g. $Path) + continue + qvalue = m.group('qvalue') + if qvalue: + value = re.sub(r'\\(.)', r'\1', qvalue)[1:-1] + else: + value = m.group('value') or '' + result[name] = value + return result + +SAFE_CHARS = string.letters + string.digits + "-@&+=_., " +_safe_trans = None + +def make_safe_filename(s): + global _safe_trans + if _safe_trans is None: + _safe_trans = ["_"] * 256 + for c in SAFE_CHARS: + _safe_trans[ord(c)] = c + _safe_trans = "".join(_safe_trans) + + return s.translate(_safe_trans) + + +class Upload: + r""" + Represents a single uploaded file. Uploaded files live in the + filesystem, *not* in memory. + + fp + an open file containing the content of the upload. The file pointer + points to the beginning of the file + orig_filename + the complete filename supplied by the user-agent in the + request that uploaded this file. Depending on the browser, + this might have the complete path of the original file + on the client system, in the client system's syntax -- eg. + "C:\foo\bar\upload_this" or "/foo/bar/upload_this" or + "foo:bar:upload_this". + base_filename + the base component of orig_filename, shorn of MS-DOS, + Mac OS, and Unix path components and with "unsafe" + characters neutralized (see make_safe_filename()) + content_type + the content type provided by the user-agent in the request + that uploaded this file. + charset + the charset provide by the user-agent + """ + + def __init__(self, orig_filename, content_type=None, charset=None): + if orig_filename: + self.orig_filename = orig_filename + bspos = orig_filename.rfind("\\") + cpos = orig_filename.rfind(":") + spos = orig_filename.rfind("/") + if bspos != -1: # eg. "\foo\bar" or "D:\ding\dong" + filename = orig_filename[bspos+1:] + elif cpos != -1: # eg. "C:foo" or ":ding:dong:foo" + filename = orig_filename[cpos+1:] + elif spos != -1: # eg. "foo/bar/baz" or "/tmp/blah" + filename = orig_filename[spos+1:] + else: + filename = orig_filename + + self.base_filename = make_safe_filename(filename) + else: + self.orig_filename = None + self.base_filename = None + self.content_type = content_type + self.charset = charset + self.fp = None + + def receive(self, lines): + self.fp = tempfile.TemporaryFile("w+b") + for line in lines: + self.fp.write(line) + self.fp.seek(0) + + def __str__(self): + return str(self.orig_filename) + + def __repr__(self): + return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self) + + def read(self, n): + return self.fp.read(n) + + def readline(self): + return self.fp.readlines() + + def __iter__(self): + return iter(self.fp) + + def close(self): + self.fp.close() + + +class LineInput: + r""" + A wrapper for an input stream that has the following properties: + + * lines are terminated by \r\n + + * lines shorter than 'maxlength' are always returned unbroken + + * lines longer than 'maxlength' are broken but the pair of + characters \r\n are never split + + * no more than 'length' characters are read from the underlying + stream + + * if the underlying stream does not produce at least 'length' + characters then EOFError is raised + + """ + def __init__(self, fp, length): + self.fp = fp + self.length = length + self.buf = '' + + def readline(self, maxlength=4096): + # fill buffer + n = min(self.length, maxlength - len(self.buf)) + if n > 0: + self.length -= n + assert self.length >= 0 + chunk = self.fp.read(n) + if len(chunk) != n: + raise EOFError('unexpected end of input') + self.buf += chunk + # split into lines + buf = self.buf + i = buf.find('\r\n') + if i >= 0: + i += 2 + self.buf = buf[i:] + return buf[:i] + elif buf.endswith('\r'): + # avoid splitting CR LF pairs + self.buf = '\r' + return buf[:-1] + else: + self.buf = '' + return buf + +class MIMEInput: + """ + Split a MIME input stream into parts. Note that this class does not + handle headers, transfer encoding, etc. + """ + + def __init__(self, fp, boundary, length): + self.lineinput = LineInput(fp, length) + self.pat = re.compile(r'--%s(--)?[ \t]*\r\n' % re.escape(boundary)) + self.done = False + + def moreparts(self): + """Return true if there are more parts to be read.""" + return not self.done + + def readpart(self): + """Generate all the lines up to a MIME boundary. Note that you + must exhaust the generator before calling this function again.""" + assert not self.done + last_line = '' + while 1: + line = self.lineinput.readline() + if not line: + # Hit EOF -- nothing more to read. This should *not* happen + # in a well-formed MIME message. + raise EOFError('MIME boundary not found (end of input)') + if last_line.endswith('\r\n') or last_line == '': + m = self.pat.match(line) + if m: + # If we hit the boundary line, return now. Forget + # the current line *and* the CRLF ending of the + # previous line. + if m.group(1): + # hit final boundary + self.done = True + yield last_line[:-2] + return + if last_line: + yield last_line + last_line = line diff --git a/pypers/europython05/Quixote-2.0/http_response.py b/pypers/europython05/Quixote-2.0/http_response.py new file mode 100755 index 0000000..435b9e8 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/http_response.py @@ -0,0 +1,475 @@ +"""quixote.http_response +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/http_response.py $ +$Id: http_response.py 26251 2005-02-25 16:17:06Z dbinger $ + +Provides the HTTPResponse class. +""" + +import time +from sets import Set +try: + import zlib +except ImportError: + pass +import struct +from rfc822 import formatdate +from quixote.html import stringify + +status_reasons = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Moved Temporarily', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Time-out', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Large', + 415: 'Unsupported Media Type', + 416: 'Requested range not satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Time-out', + 505: 'HTTP Version not supported', + 507: 'Insufficient Storage', +} + +_GZIP_HEADER = ("\037\213" # magic + "\010" # compression method + "\000" # flags + "\000\000\000\000" # time, who cares? + "\002" + "\377") + +_GZIP_EXCLUDE = Set(["application/pdf", + "application/zip", + "audio/mpeg", + "image/gif", + "image/jpeg", + "image/png", + "video/mpeg", + "video/quicktime", + "video/x-msvideo", + ]) + +class HTTPResponse: + """ + An object representation of an HTTP response. + + The Response type encapsulates all possible responses to HTTP + requests. Responses are normally created by the Quixote publisher + or by the HTTPRequest class (every request must have a response, + after all). + + Instance attributes: + content_type : string + the MIME content type of the response (does not include extra params + like charset) + charset : string + the character encoding of the the response + status_code : int + HTTP response status code (integer between 100 and 599) + reason_phrase : string + the reason phrase that accompanies status_code (usually + set automatically by the set_status() method) + headers : { string : string } + most of the headers included with the response; every header set + by 'set_header()' goes here. Does not include "Status" or + "Set-Cookie" headers (unless someone uses set_header() to set + them, but that would be foolish). + body : str | Stream + the response body, None by default. Note that if the body is not a + stream then it is already encoded using 'charset'. + buffered : bool + if false, response data will be flushed as soon as it is + written (the default is true). This is most useful for + responses that use the Stream() protocol. Note that whether the + client actually receives the partial response data is highly + dependent on the web server + cookies : { name:string : { attrname : value } } + collection of cookies to set in this response; it is expected + that the user-agent will remember the cookies and send them on + future requests. The cookie value is stored as the "value" + attribute. The other attributes are as specified by RFC 2109. + cache : int | None + the number of seconds the response may be cached. The default is 0, + meaning don't cache at all. This variable is used to set the HTTP + expires header. If set to None then the expires header will not be + added. + javascript_code : { string : string } + a collection of snippets of JavaScript code to be included in + the response. The collection is built by calling add_javascript(), + but actually including the code in the HTML document is somebody + else's problem. + """ + + DEFAULT_CONTENT_TYPE = 'text/html' + DEFAULT_CHARSET = 'iso-8859-1' + + def __init__(self, status=200, body=None, content_type=None, charset=None): + """ + Creates a new HTTP response. + """ + self.content_type = content_type or self.DEFAULT_CONTENT_TYPE + self.charset = charset or self.DEFAULT_CHARSET + self.set_status(status) + self.headers = {} + + if body is not None: + self.set_body(body) + else: + self.body = None + + self.cookies = {} + self.cache = 0 + self.buffered = True + self.javascript_code = None + + def set_content_type(self, content_type, charset='iso-8859-1'): + """(content_type : string, charset : string = 'iso-8859-1') + + Set the content type of the response to the MIME type specified by + 'content_type'. Also sets the charset, defaulting to 'iso-8859-1'. + """ + self.charset = charset + self.content_type = content_type + + def set_charset(self, charset): + self.charset = str(charset).lower() + + def set_status(self, status, reason=None): + """set_status(status : int, reason : string = None) + + Sets the HTTP status code of the response. 'status' must be an + integer in the range 100 .. 599. 'reason' must be a string; if + not supplied, the default reason phrase for 'status' will be + used. If 'status' is a non-standard status code, the generic + reason phrase for its group of status codes will be used; eg. + if status == 493, the reason for status 400 will be used. + """ + if not isinstance(status, int): + raise TypeError, "status must be an integer" + if not (100 <= status <= 599): + raise ValueError, "status must be between 100 and 599" + + self.status_code = status + if reason is None: + if status_reasons.has_key(status): + reason = status_reasons[status] + else: + # Eg. for generic 4xx failures, use the reason + # associated with status 400. + reason = status_reasons[status - (status % 100)] + else: + reason = str(reason) + + self.reason_phrase = reason + + def set_header(self, name, value): + """set_header(name : string, value : string) + + Sets an HTTP return header "name" with value "value", clearing + the previous value set for the header, if one exists. + """ + self.headers[name.lower()] = value + + def get_header(self, name, default=None): + """get_header(name : string, default=None) -> value : string + + Gets an HTTP return header "name". If none exists then 'default' is + returned. + """ + return self.headers.get(name.lower(), default) + + def set_expires(self, seconds=0, minutes=0, hours=0, days=0): + if seconds is None: + self.cache = None # don't generate 'Expires' header + else: + self.cache = seconds + 60*(minutes + 60*(hours + 24*days)) + + def _encode_chunk(self, chunk): + """(chunk : str | unicode) -> str + """ + if self.charset == 'iso-8859-1' and isinstance(chunk, str): + return chunk # non-ASCII chars are okay + else: + return chunk.encode(self.charset) + + def _compress_body(self, body): + """(body: str) -> str + """ + n = len(body) + co = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS, + zlib.DEF_MEM_LEVEL, 0) + chunks = [_GZIP_HEADER, + co.compress(body), + co.flush(), + struct.pack("<ll", zlib.crc32(body), n)] + compressed_body = "".join(chunks) + ratio = float(n) / len(compressed_body) + #print "gzip original size %d, ratio %.1f" % (n, ratio) + if ratio > 1.0: + self.set_header("Content-Encoding", "gzip") + return compressed_body + else: + return body + + def set_body(self, body, compress=False): + """(body : any, compress : bool = False) + + Sets the response body equal to the argument 'body'. If 'compress' + is true then the body may be compressed using 'gzip'. + """ + if not isinstance(body, Stream): + body = self._encode_chunk(stringify(body)) + if compress and self.content_type not in _GZIP_EXCLUDE: + body = self._compress_body(body) + self.body = body + + def expire_cookie(self, name, **attrs): + """ + Cause an HTTP cookie to be removed from the browser + + The response will include an HTTP header that will remove the cookie + corresponding to "name" on the client, if one exists. This is + accomplished by sending a new cookie with an expiration date + that has already passed. Note that some clients require a path + to be specified - this path must exactly match the path given + when creating the cookie. The path can be specified as a keyword + argument. + """ + dict = {'max_age': 0, 'expires': 'Thu, 01-Jan-1970 00:00:00 GMT'} + dict.update(attrs) + self.set_cookie(name, "deleted", **dict) + + def set_cookie(self, name, value, **attrs): + """set_cookie(name : string, value : string, **attrs) + + Set an HTTP cookie on the browser. + + The response will include an HTTP header that sets a cookie on + cookie-enabled browsers with a key "name" and value "value". + Cookie attributes such as "expires" and "domains" may be + supplied as keyword arguments; see RFC 2109 for a full list. + (For the "secure" attribute, use any true value.) + + This overrides any previous value for this cookie. Any + previously-set attributes for the cookie are preserved, unless + they are explicitly overridden with keyword arguments to this + call. + """ + cookies = self.cookies + if cookies.has_key(name): + cookie = cookies[name] + else: + cookie = cookies[name] = {} + cookie.update(attrs) + cookie['value'] = value + + def add_javascript(self, code_id, code): + """Add javascript code to be included in the response. + + code_id is used to ensure that the same piece of code is not + included twice. The caller must be careful to avoid + unintentional code_id and javascript identifier collisions. + Note that the response object only provides a mechanism for + collecting code -- actually including it in the HTML document + that is the response body is somebody else's problem. (For + an example, see Form._render_javascript().) + """ + if self.javascript_code is None: + self.javascript_code = {code_id: code} + elif not self.javascript_code.has_key(code_id): + self.javascript_code[code_id] = code + + def redirect(self, location, permanent=False): + """Cause a redirection without raising an error""" + if not isinstance(location, str): + raise TypeError, "location must be a string (got %s)" % `location` + # Ensure that location is a full URL + if location.find('://') == -1: + raise ValueError, "URL must include the server name" + if permanent: + status = 301 + else: + status = 302 + self.set_status(status) + self.headers['location'] = location + self.set_content_type('text/plain') + return "Your browser should have redirected you to %s" % location + + def get_status_code(self): + return self.status_code + + def get_reason_phrase(self): + return self.reason_phrase + + def get_content_type(self): + return self.content_type + + def get_content_length(self): + if self.body is None: + return None + elif isinstance(self.body, Stream): + return self.body.length + else: + return len(self.body) + + def _gen_cookie_headers(self): + """_gen_cookie_headers() -> [string] + + Build a list of "Set-Cookie" headers based on all cookies + set with 'set_cookie()', and return that list. + """ + cookie_headers = [] + for name, attrs in self.cookies.items(): + value = str(attrs['value']) + if '"' in value: + value = value.replace('"', '\\"') + chunks = ['%s="%s"' % (name, value)] + for name, val in attrs.items(): + name = name.lower() + if val is None: + continue + if name in ('expires', 'domain', 'path', 'max_age', 'comment'): + name = name.replace('_', '-') + chunks.append('%s=%s' % (name, val)) + elif name == 'secure' and val: + chunks.append("secure") + cookie_headers.append(("Set-Cookie", '; '.join(chunks))) + return cookie_headers + + def generate_headers(self): + """generate_headers() -> [(name:string, value:string)] + + Generate a list of headers to be returned as part of the response. + """ + headers = [] + + for name, value in self.headers.items(): + headers.append((name.title(), value)) + + # All the "Set-Cookie" headers. + if self.cookies: + headers.extend(self._gen_cookie_headers()) + + # Date header + now = time.time() + if "date" not in self.headers: + headers.append(("Date", formatdate(now))) + + # Cache directives + if self.cache is None: + pass # don't mess with the expires header + elif "expires" not in self.headers: + if self.cache > 0: + expire_date = formatdate(now + self.cache) + else: + expire_date = "-1" # allowed by HTTP spec and may work better + # with some clients + headers.append(("Expires", expire_date)) + + # Content-type + if "content-type" not in self.headers: + headers.append(('Content-Type', + '%s; charset=%s' % (self.content_type, + self.charset))) + + # Content-Length + if "content-length" not in self.headers: + length = self.get_content_length() + if length is not None: + headers.append(('Content-Length', length)) + + return headers + + def generate_body_chunks(self): + """Return a sequence of body chunks, encoded using 'charset'. + """ + if self.body is None: + pass + elif isinstance(self.body, Stream): + for chunk in self.body: + yield self._encode_chunk(chunk) + else: + yield self.body # already encoded + + def write(self, output, include_status=True): + """(output : file, include_status : bool = True) + + Write the HTTP response headers and body to 'output'. This is not + a complete HTTP response, as it doesn't start with a response + status line as specified by RFC 2616. By default, it does start + with a "Status" header as described by the CGI spec. It is expected + that this response is parsed by the web server and turned into a + complete HTTP response. + """ + flush_output = not self.buffered and hasattr(output, 'flush') + if include_status: + # "Status" header must come first. + output.write("Status: %03d %s\r\n" % (self.status_code, + self.reason_phrase)) + for name, value in self.generate_headers(): + output.write("%s: %s\r\n" % (name, value)) + output.write("\r\n") + if flush_output: + output.flush() + for chunk in self.generate_body_chunks(): + output.write(chunk) + if flush_output: + output.flush() + if flush_output: + output.flush() + + +class Stream: + """ + A wrapper around response data that can be streamed. The 'iterable' + argument must support the iteration protocol. Items returned by 'next()' + must be strings. Beware that exceptions raised while writing the stream + will not be handled gracefully. + + Instance attributes: + iterable : any + an object that supports the iteration protocol. The items produced + by the stream must be strings. + length: int | None + the number of bytes that will be produced by the stream, None + if it is not known. Used to set the Content-Length header. + """ + def __init__(self, iterable, length=None): + self.iterable = iterable + self.length = length + + def __iter__(self): + return iter(self.iterable) diff --git a/pypers/europython05/Quixote-2.0/logger.py b/pypers/europython05/Quixote-2.0/logger.py new file mode 100755 index 0000000..f631c57 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/logger.py @@ -0,0 +1,92 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/logger.py $ +$Id: logger.py 25521 2004-11-04 18:16:18Z nascheme $ +""" +import sys +import os +import time +import socket +from quixote.sendmail import sendmail + +class DefaultLogger: + """ + This is the default logger object used by the Quixote publisher. It + controls access log and error log behavior. You may provide your own + object if you wish to have different behavior. + + Instance attributes: + + access_log : file | None + file to which every access will be logged. If None then access + is not logged. + error_log : file + file to which application errors (exceptions caught by Quixote, + as well as anything printed to stderr by application code) will + be logged. Set to sys.stderr by default. + error_email : string | None + if set then internal server errors will cause messages to be sent to + this address + """ + def __init__(self, access_log=None, error_log=None, error_email=None): + if access_log: + self.access_log = open(access_log, 'a', 1) + else: + self.access_log = None + if error_log is None: + self.error_log = sys.stderr + else: + self.error_log = open(error_log, 'a', 1) + self.error_email = error_email + sys.stdout = self.error_log # print is handy for debugging + + def log(self, msg): + """ + Write an message to the error log with a time stamp. + """ + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(time.time())) + self.error_log.write("[%s] %s\n" % (timestamp, msg)) + + def log_internal_error(self, error_summary, error_msg): + """(error_summary: str, error_msg: str) + + error_summary is a single line summary of the internal error, suitable + for an email subject. error_msg is a multi-line plaintext message + describing the error in detail. + """ + self.log("exception caught") + self.error_log.write(error_msg) + if self.error_email: + sendmail('Quixote Traceback (%s)' % error_summary, + error_msg, [self.error_email], + from_addr=(self.error_email, socket.gethostname())) + + def log_request(self, request, start_time): + """Log a request in the access_log file. + """ + if self.access_log is None: + return + if request.session: + user = request.session.user or "-" + else: + user = "-" + now = time.time() + seconds = now - start_time + timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)) + + request_uri = request.get_path() + query = request.get_query() + if query: + request_uri += "?" + query + proto = request.get_environ('SERVER_PROTOCOL') + self.access_log.write('%s %s %s %d "%s %s %s" %s %r %0.2fsec\n' % + (request.get_environ('REMOTE_ADDR'), + user, + timestamp, + os.getpid(), + request.get_method(), + request_uri, + proto, + request.response.status_code, + request.get_environ('HTTP_USER_AGENT', ''), + seconds + )) diff --git a/pypers/europython05/Quixote-2.0/ptl/__init__.py b/pypers/europython05/Quixote-2.0/ptl/__init__.py new file mode 100755 index 0000000..3409414 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/__init__.py @@ -0,0 +1,245 @@ +''' +$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/__init__.py $ +$Id: __init__.py 26357 2005-03-16 14:56:23Z dbinger $ + +PTL: Python Template Language +============================= + +Introduction +------------ + +PTL is the templating language used by Quixote. Most web templating +languages embed a real programming language in HTML, but PTL inverts +this model by merely tweaking Python to make it easier to generate +HTML pages (or other forms of text). In other words, PTL is basically +Python with a novel way to specify function return values. + +Specifically, a PTL template is designated by inserting a ``[plain]`` +or ``[html]`` modifier after the function name. The value of +expressions inside templates are kept, not discarded. If the type is +``[html]`` then non-literal strings are passed through a function that +escapes HTML special characters. + + +Plain text templates +-------------------- + +Here's a sample plain text template:: + + def foo [plain] (x, y = 5): + "This is a chunk of static text." + greeting = "hello world" # statement, no PTL output + print 'Input values:', x, y + z = x + y + """You can plug in variables like x (%s) + in a variety of ways.""" % x + + "\n\n" + "Whitespace is important in generated text.\n" + "z = "; z + ", but y is " + y + "." + +Obviously, templates can't have docstrings, but otherwise they follow +Python's syntactic rules: indentation indicates scoping, single-quoted +and triple-quoted strings can be used, the same rules for continuing +lines apply, and so forth. PTL also follows all the expected semantics +of normal Python code: so templates can have parameters, and the +parameters can have default values, be treated as keyword arguments, +etc. + +The difference between a template and a regular Python function is that +inside a template the result of expressions are saved as the return +value of that template. Look at the first part of the example again:: + + def foo [plain] (x, y = 5): + "This is a chunk of static text." + greeting = "hello world" # statement, no PTL output + print 'Input values:', x, y + z = x + y + """You can plug in variables like x (%s) + in a variety of ways.""" % x + +Calling this template with ``foo(1, 2)`` results in the following +string:: + + This is a chunk of static text.You can plug in variables like x (1) + in a variety of ways. + +Normally when Python evaluates expressions inside functions, it just +discards their values, but in a ``[plain]`` PTL template the value is +converted to a string using ``str()`` and appended to the template's +return value. There's a single exception to this rule: ``None`` is the +only value that's ever ignored, adding nothing to the output. (If this +weren't the case, calling methods or functions that return ``None`` +would require assigning their value to a variable. You'd have to write +``dummy = list.sort()`` in PTL code, which would be strange and +confusing.) + +The initial string in a template isn't treated as a docstring, but is +just incorporated in the generated output; therefore, templates can't +have docstrings. No whitespace is ever automatically added to the +output, resulting in ``...text.You can ...`` from the example. You'd +have to add an extra space to one of the string literals to correct +this. + +The assignment to the ``greeting`` local variable is a statement, not an +expression, so it doesn't return a value and produces no output. The +output from the ``print`` statement will be printed as usual, but won't +go into the string generated by the template. Quixote directs standard +output into Quixote's debugging log; if you're using PTL on its own, you +should consider doing something similar. ``print`` should never be used +to generate output returned to the browser, only for adding debugging +traces to a template. + +Inside templates, you can use all of Python's control-flow statements:: + + def numbers [plain] (n): + for i in range(n): + i + " " # PTL does not add any whitespace + +Calling ``numbers(5)`` will return the string ``"1 2 3 4 5 "``. You can +also have conditional logic or exception blocks:: + + def international_hello [plain] (language): + if language == "english": + "hello" + elif language == "french": + "bonjour" + else: + raise ValueError, "I don't speak %s" % language + + +HTML templates +-------------- + +Since PTL is usually used to generate HTML documents, an ``[html]`` +template type has been provided to make generating HTML easier. + +A common error when generating HTML is to grab data from the browser +or from a database and incorporate the contents without escaping +special characters such as '<' and '&'. This leads to a class of +security bugs called "cross-site scripting" bugs, where a hostile user +can insert arbitrary HTML in your site's output that can link to other +sites or contain JavaScript code that does something nasty (say, +popping up 10,000 browser windows). + +Such bugs occur because it's easy to forget to HTML-escape a string, +and forgetting it in just one location is enough to open a hole. PTL +offers a solution to this problem by being able to escape strings +automatically when generating HTML output, at the cost of slightly +diminished performance (a few percent). + +Here's how this feature works. PTL defines a class called +``htmltext`` that represents a string that's already been HTML-escaped +and can be safely sent to the client. The function ``htmlescape(string)`` +is used to escape data, and it always returns an ``htmltext`` +instance. It does nothing if the argument is already ``htmltext``. + +If a template function is declared ``[html]`` instead of ``[text]`` +then two things happen. First, all literal strings in the function +become instances of ``htmltext`` instead of Python's ``str``. Second, +the values of expressions are passed through ``htmlescape()`` instead +of ``str()``. + +``htmltext`` type is like the ``str`` type except that operations +combining strings and ``htmltext`` instances will result in the string +being passed through ``htmlescape()``. For example:: + + >>> from quixote.html import htmltext + >>> htmltext('a') + 'b' + <htmltext 'ab'> + >>> 'a' + htmltext('b') + <htmltext 'ab'> + >>> htmltext('a%s') % 'b' + <htmltext 'ab'> + >>> response = 'green eggs & ham' + >>> htmltext('The response was: %s') % response + <htmltext 'The response was: green eggs & ham'> + +Note that calling ``str()`` strips the ``htmltext`` type and should be +avoided since it usually results in characters being escaped more than +once. While ``htmltext`` behaves much like a regular string, it is +sometimes necessary to insert a ``str()`` inside a template in order +to obtain a genuine string. For example, the ``re`` module requires +genuine strings. We have found that explicit calls to ``str()`` can +often be avoided by splitting some code out of the template into a +helper function written in regular Python. + +It is also recommended that the ``htmltext`` constructor be used as +sparingly as possible. The reason is that when using the htmltext +feature of PTL, explicit calls to ``htmltext`` become the most likely +source of cross-site scripting holes. Calling ``htmltext`` is like +saying "I am absolutely sure this piece of data cannot contain malicious +HTML code injected by a user. Don't escape HTML special characters +because I want them." + +Note that literal strings in template functions declared with +``[html]`` are htmltext instances, and therefore won't be escaped. +You'll only need to use ``htmltext`` when HTML markup comes from +outside the template. For example, if you want to include a file +containing HTML:: + + def output_file [html] (): + '<html><body>' # does not get escaped + htmltext(open("myfile.html").read()) + '</body></html>' + +In the common case, templates won't be dealing with HTML markup from +external sources, so you can write straightforward code. Consider +this function to generate the contents of the ``HEAD`` element:: + + def meta_tags [html] (title, description): + '<title>%s</title>' % title + '<meta name="description" content="%s">\n' % description + +There are no calls to ``htmlescape()`` at all, but string literals +such as ``<title>%s</title>`` have all be turned into ``htmltext`` +instances, so the string variables will be automatically escaped:: + + >>> t.meta_tags('Catalog', 'A catalog of our cool products') + <htmltext '<title>Catalog</title> + <meta name="description" content="A catalog of our cool products">\n'> + >>> t.meta_tags('Dissertation on <HEAD>', + ... 'Discusses the "LINK" and "META" tags') + <htmltext '<title>Dissertation on <HEAD></title> + <meta name="description" + content="Discusses the "LINK" and "META" tags">\n'> + >>> + +Note how the title and description have had HTML-escaping applied to them. +(The output has been manually pretty-printed to be more readable.) + +Once you start using ``htmltext`` in one of your templates, mixing +plain and HTML templates is tricky because of ``htmltext``'s automatic +escaping; plain templates that generate HTML tags will be +double-escaped. One approach is to just use HTML templates throughout +your application. Alternatively you can use ``str()`` to convert +``htmltext`` instances to regular Python strings; just be sure the +resulting string isn't HTML-escaped again. + +Two implementations of ``htmltext`` are provided, one written in pure +Python and a second one implemented as a C extension. Both versions +have seen production use. + + +PTL modules +----------- + +PTL templates are kept in files with the extension .ptl. Like Python +files, they are byte-compiled on import, and the byte-code is written to +a compiled file with the extension ``.pyc``. Since vanilla Python +doesn't know anything about PTL, this package provides an import hook to let +you import PTL files just like regular Python modules. The import +hook is installed when you import *this* package. + +(Note: if you're using ZODB, always import ZODB *before* installing the +PTL import hook. There's some interaction which causes importing the +TimeStamp module to fail when the PTL import hook is installed; we +haven't debugged the problem. A similar problem has been reported for +BioPython and win32com.client imports.) +''' + + diff --git a/pypers/europython05/Quixote-2.0/ptl/cimport.c b/pypers/europython05/Quixote-2.0/ptl/cimport.c new file mode 100755 index 0000000..6e37ca5 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/cimport.c @@ -0,0 +1,483 @@ +/* Mostly stolen from Python/import.c. PSF license applies. */ + + +#include "Python.h" +#include "osdefs.h" + +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif + +/* Python function to find and load a module. */ +static PyObject *loader_hook; + + +PyObject * +call_find_load(char *fullname, char *subname, PyObject *path) +{ + PyObject *args, *m; + + if (!(args = Py_BuildValue("(ssO)", fullname, subname, + path != NULL ? path : Py_None))) + return NULL; + + m = PyEval_CallObject(loader_hook, args); + + Py_DECREF(args); + return m; +} + + +/* Forward declarations for helper routines */ +static PyObject *get_parent(PyObject *globals, char *buf, int *p_buflen); +static PyObject *load_next(PyObject *mod, PyObject *altmod, + char **p_name, char *buf, int *p_buflen); +static int mark_miss(char *name); +static int ensure_fromlist(PyObject *mod, PyObject *fromlist, + char *buf, int buflen, int recursive); +static PyObject * import_submodule(PyObject *mod, char *name, char *fullname); + + +static PyObject * +import_module(char *name, PyObject *globals, PyObject *locals, + PyObject *fromlist) +{ + char buf[MAXPATHLEN+1]; + int buflen = 0; + PyObject *parent, *head, *next, *tail; + + parent = get_parent(globals, buf, &buflen); + if (parent == NULL) + return NULL; + + head = load_next(parent, Py_None, &name, buf, &buflen); + if (head == NULL) + return NULL; + + tail = head; + Py_INCREF(tail); + while (name) { + next = load_next(tail, tail, &name, buf, &buflen); + Py_DECREF(tail); + if (next == NULL) { + Py_DECREF(head); + return NULL; + } + tail = next; + } + + if (fromlist != NULL) { + if (fromlist == Py_None || !PyObject_IsTrue(fromlist)) + fromlist = NULL; + } + + if (fromlist == NULL) { + Py_DECREF(tail); + return head; + } + + Py_DECREF(head); + if (!ensure_fromlist(tail, fromlist, buf, buflen, 0)) { + Py_DECREF(tail); + return NULL; + } + + return tail; +} + +static PyObject * +get_parent(PyObject *globals, char *buf, int *p_buflen) +{ + static PyObject *namestr = NULL; + static PyObject *pathstr = NULL; + PyObject *modname, *modpath, *modules, *parent; + + if (globals == NULL || !PyDict_Check(globals)) + return Py_None; + + if (namestr == NULL) { + namestr = PyString_InternFromString("__name__"); + if (namestr == NULL) + return NULL; + } + if (pathstr == NULL) { + pathstr = PyString_InternFromString("__path__"); + if (pathstr == NULL) + return NULL; + } + + *buf = '\0'; + *p_buflen = 0; + modname = PyDict_GetItem(globals, namestr); + if (modname == NULL || !PyString_Check(modname)) + return Py_None; + + modpath = PyDict_GetItem(globals, pathstr); + if (modpath != NULL) { + int len = PyString_GET_SIZE(modname); + if (len > MAXPATHLEN) { + PyErr_SetString(PyExc_ValueError, + "Module name too long"); + return NULL; + } + strcpy(buf, PyString_AS_STRING(modname)); + *p_buflen = len; + } + else { + char *start = PyString_AS_STRING(modname); + char *lastdot = strrchr(start, '.'); + size_t len; + if (lastdot == NULL) + return Py_None; + len = lastdot - start; + if (len >= MAXPATHLEN) { + PyErr_SetString(PyExc_ValueError, + "Module name too long"); + return NULL; + } + strncpy(buf, start, len); + buf[len] = '\0'; + *p_buflen = len; + } + + modules = PyImport_GetModuleDict(); + parent = PyDict_GetItemString(modules, buf); + if (parent == NULL) + parent = Py_None; + return parent; + /* We expect, but can't guarantee, if parent != None, that: + - parent.__name__ == buf + - parent.__dict__ is globals + If this is violated... Who cares? */ +} + +/* altmod is either None or same as mod */ +static PyObject * +load_next(PyObject *mod, PyObject *altmod, char **p_name, char *buf, + int *p_buflen) +{ + char *name = *p_name; + char *dot = strchr(name, '.'); + size_t len; + char *p; + PyObject *result; + + if (dot == NULL) { + *p_name = NULL; + len = strlen(name); + } + else { + *p_name = dot+1; + len = dot-name; + } + if (len == 0) { + PyErr_SetString(PyExc_ValueError, + "Empty module name"); + return NULL; + } + + p = buf + *p_buflen; + if (p != buf) + *p++ = '.'; + if (p+len-buf >= MAXPATHLEN) { + PyErr_SetString(PyExc_ValueError, + "Module name too long"); + return NULL; + } + strncpy(p, name, len); + p[len] = '\0'; + *p_buflen = p+len-buf; + + result = import_submodule(mod, p, buf); + if (result == Py_None && altmod != mod) { + Py_DECREF(result); + /* Here, altmod must be None and mod must not be None */ + result = import_submodule(altmod, p, p); + if (result != NULL && result != Py_None) { + if (mark_miss(buf) != 0) { + Py_DECREF(result); + return NULL; + } + strncpy(buf, name, len); + buf[len] = '\0'; + *p_buflen = len; + } + } + if (result == NULL) + return NULL; + + if (result == Py_None) { + Py_DECREF(result); + PyErr_Format(PyExc_ImportError, + "No module named %.200s", name); + return NULL; + } + + return result; +} + +static int +mark_miss(char *name) +{ + PyObject *modules = PyImport_GetModuleDict(); + return PyDict_SetItemString(modules, name, Py_None); +} + +static int +ensure_fromlist(PyObject *mod, PyObject *fromlist, char *buf, int buflen, + int recursive) +{ + int i; + + if (!PyObject_HasAttrString(mod, "__path__")) + return 1; + + for (i = 0; ; i++) { + PyObject *item = PySequence_GetItem(fromlist, i); + int hasit; + if (item == NULL) { + if (PyErr_ExceptionMatches(PyExc_IndexError)) { + PyErr_Clear(); + return 1; + } + return 0; + } + if (!PyString_Check(item)) { + PyErr_SetString(PyExc_TypeError, + "Item in ``from list'' not a string"); + Py_DECREF(item); + return 0; + } + if (PyString_AS_STRING(item)[0] == '*') { + PyObject *all; + Py_DECREF(item); + /* See if the package defines __all__ */ + if (recursive) + continue; /* Avoid endless recursion */ + all = PyObject_GetAttrString(mod, "__all__"); + if (all == NULL) + PyErr_Clear(); + else { + if (!ensure_fromlist(mod, all, buf, buflen, 1)) + return 0; + Py_DECREF(all); + } + continue; + } + hasit = PyObject_HasAttr(mod, item); + if (!hasit) { + char *subname = PyString_AS_STRING(item); + PyObject *submod; + char *p; + if (buflen + strlen(subname) >= MAXPATHLEN) { + PyErr_SetString(PyExc_ValueError, + "Module name too long"); + Py_DECREF(item); + return 0; + } + p = buf + buflen; + *p++ = '.'; + strcpy(p, subname); + submod = import_submodule(mod, subname, buf); + Py_XDECREF(submod); + if (submod == NULL) { + Py_DECREF(item); + return 0; + } + } + Py_DECREF(item); + } + + /* NOTREACHED */ +} + +static PyObject * +import_submodule(PyObject *mod, char *subname, char *fullname) +{ + PyObject *modules = PyImport_GetModuleDict(); + PyObject *m; + + /* Require: + if mod == None: subname == fullname + else: mod.__name__ + "." + subname == fullname + */ + + if ((m = PyDict_GetItemString(modules, fullname)) != NULL) { + Py_INCREF(m); + } + else { + PyObject *path; + + if (mod == Py_None) + path = NULL; + else { + path = PyObject_GetAttrString(mod, "__path__"); + if (path == NULL) { + PyErr_Clear(); + Py_INCREF(Py_None); + return Py_None; + } + } + + m = call_find_load(fullname, subname, path); + + if (m != NULL && mod != Py_None) { + if (PyObject_SetAttrString(mod, subname, m) < 0) { + Py_DECREF(m); + m = NULL; + } + } + } + + return m; +} + + +PyObject * +reload_module(PyObject *m) +{ + PyObject *modules = PyImport_GetModuleDict(); + PyObject *path = NULL; + char *name, *subname; + + if (m == NULL || !PyModule_Check(m)) { + PyErr_SetString(PyExc_TypeError, + "reload_module() argument must be module"); + return NULL; + } + name = PyModule_GetName(m); + if (name == NULL) + return NULL; + if (m != PyDict_GetItemString(modules, name)) { + PyErr_Format(PyExc_ImportError, + "reload(): module %.200s not in sys.modules", + name); + return NULL; + } + subname = strrchr(name, '.'); + if (subname == NULL) + subname = name; + else { + PyObject *parentname, *parent; + parentname = PyString_FromStringAndSize(name, (subname-name)); + if (parentname == NULL) + return NULL; + parent = PyDict_GetItem(modules, parentname); + Py_DECREF(parentname); + if (parent == NULL) { + PyErr_Format(PyExc_ImportError, + "reload(): parent %.200s not in sys.modules", + name); + return NULL; + } + subname++; + path = PyObject_GetAttrString(parent, "__path__"); + if (path == NULL) + PyErr_Clear(); + } + m = call_find_load(name, subname, path); + Py_XDECREF(path); + return m; +} + + +static PyObject * +cimport_import_module(PyObject *self, PyObject *args) +{ + char *name; + PyObject *globals = NULL; + PyObject *locals = NULL; + PyObject *fromlist = NULL; + + if (!PyArg_ParseTuple(args, "s|OOO:import_module", &name, &globals, + &locals, &fromlist)) + return NULL; + return import_module(name, globals, locals, fromlist); +} + +static PyObject * +cimport_reload_module(PyObject *self, PyObject *args) +{ + PyObject *m; + if (!PyArg_ParseTuple(args, "O:reload_module", &m)) + return NULL; + return reload_module(m); +} + +static char doc_reload_module[] = +"reload(module) -> module\n\ +\n\ +Reload the module. The module must have been successfully imported before."; + +static PyObject * +cimport_set_loader(PyObject *self, PyObject *args) +{ + PyObject *l = NULL; + if (!PyArg_ParseTuple(args, "O:set_loader", &l)) + return NULL; + if (!PyCallable_Check(l)) { + PyErr_SetString(PyExc_TypeError, "callable object needed"); + return NULL; + } + Py_XDECREF(loader_hook); + loader_hook = l; + Py_INCREF(loader_hook); + Py_INCREF(Py_None); + return Py_None; +} +static char doc_set_loader[] = "\ +Set the function that will be used to import modules.\n\ +\n\ +The function should should have the signature:\n\ +\n\ + loader(fullname : str, subname : str, path : [str] | None) -> module | None\n\ +\n\ +It should return the initialized module or None if it is not found.\n\ +"; + + +static PyObject * +cimport_get_loader(PyObject *self, PyObject *args) +{ + if (!PyArg_ParseTuple(args, ":get_loader")) + return NULL; + Py_INCREF(loader_hook); + return loader_hook; +} + +static char doc_get_loader[] = "\ +Get the function that will be used to import modules.\n\ +"; + +static char doc_import_module[] = "\ +import_module(name, globals, locals, fromlist) -> module\n\ +\n\ +Import a module. The globals are only used to determine the context;\n\ +they are not modified. The locals are currently unused. The fromlist\n\ +should be a list of names to emulate ``from name import ...'', or an\n\ +empty list to emulate ``import name''.\n\ +\n\ +When importing a module from a package, note that import_module('A.B', ...)\n\ +returns package A when fromlist is empty, but its submodule B when\n\ +fromlist is not empty.\n\ +"; + + +static PyMethodDef cimport_methods[] = { + {"import_module", cimport_import_module, 1, doc_import_module}, + {"reload_module", cimport_reload_module, 1, doc_reload_module}, + {"get_loader", cimport_get_loader, 1, doc_get_loader}, + {"set_loader", cimport_set_loader, 1, doc_set_loader}, + {NULL, NULL} /* sentinel */ +}; + +void +initcimport(void) +{ + PyObject *m, *d; + + m = Py_InitModule4("cimport", cimport_methods, "", + NULL, PYTHON_API_VERSION); + d = PyModule_GetDict(m); + +} diff --git a/pypers/europython05/Quixote-2.0/ptl/install.py b/pypers/europython05/Quixote-2.0/ptl/install.py new file mode 100755 index 0000000..642c69b --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/install.py @@ -0,0 +1,2 @@ +import quixote.ptl.ptl_import +quixote.ptl.ptl_import.install() diff --git a/pypers/europython05/Quixote-2.0/ptl/ptl_compile.py b/pypers/europython05/Quixote-2.0/ptl/ptl_compile.py new file mode 100755 index 0000000..47c0e32 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/ptl_compile.py @@ -0,0 +1,314 @@ +#!/www/python/bin/python +""" +$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/ptl_compile.py $ +$Id: ptl_compile.py 26357 2005-03-16 14:56:23Z dbinger $ + +Compile a PTL template. + +First template function names are mangled, noting the template type. +Next, the file is parsed into a parse tree. This tree is converted into +a modified AST. It is during this state that the semantics are modified +by adding extra nodes to the tree. Finally bytecode is generated using +the compiler package. +""" + +import sys +import os +import stat +import symbol +import token +import re +import imp +import stat +import marshal +import struct + +assert sys.hexversion >= 0x20300b1, 'PTL requires Python 2.3 or newer' + +from compiler import pycodegen, transformer +from compiler import ast +from compiler.consts import OP_ASSIGN +from compiler import misc, syntax + +HTML_TEMPLATE_PREFIX = "_q_html_template_" +PLAIN_TEMPLATE_PREFIX = "_q_plain_template_" + +class TemplateTransformer(transformer.Transformer): + + def __init__(self, *args, **kwargs): + transformer.Transformer.__init__(self, *args, **kwargs) + # __template_type is a stack whose values are + # "html", "plain", or None + self.__template_type = [] + + def _get_template_type(self): + """Return the type of the function being compiled ( + "html", "plain", or None) + """ + if self.__template_type: + return self.__template_type[-1] + else: + return None + + def file_input(self, nodelist): + doc = None # self.get_docstring(nodelist, symbol.file_input) + + html_imp = ast.From('quixote.html', [('TemplateIO', '_q_TemplateIO'), + ('htmltext', '_q_htmltext')]) + vars_imp = ast.From("__builtin__", [("vars", "_q_vars")]) + stmts = [ vars_imp, html_imp ] + + for node in nodelist: + if node[0] != token.ENDMARKER and node[0] != token.NEWLINE: + self.com_append_stmt(stmts, node) + + return ast.Module(doc, ast.Stmt(stmts)) + + def funcdef(self, nodelist): + if len(nodelist) == 6: + assert nodelist[0][0] == symbol.decorators + decorators = self.decorators(nodelist[0][1:]) + else: + assert len(nodelist) == 5 + decorators = None + + lineno = nodelist[-4][2] + name = nodelist[-4][1] + args = nodelist[-3][2] + + if not re.match('_q_(html|plain)_(dollar_)?template_', name): + # just a normal function, let base class handle it + self.__template_type.append(None) + n = transformer.Transformer.funcdef(self, nodelist) + else: + if name.startswith(PLAIN_TEMPLATE_PREFIX): + name = name[len(PLAIN_TEMPLATE_PREFIX):] + template_type = "plain" + elif name.startswith(HTML_TEMPLATE_PREFIX): + name = name[len(HTML_TEMPLATE_PREFIX):] + template_type = "html" + else: + raise RuntimeError, 'unknown prefix on %s' % name + + self.__template_type.append(template_type) + + if args[0] == symbol.varargslist: + names, defaults, flags = self.com_arglist(args[1:]) + else: + names = defaults = () + flags = 0 + doc = None # self.get_docstring(nodelist[-1]) + + # code for function + code = self.com_node(nodelist[-1]) + + # _q_output = _q_TemplateIO() + klass = ast.Name('_q_TemplateIO') + args = [ast.Const(template_type == "html")] + instance = ast.CallFunc(klass, args) + assign_name = ast.AssName('_q_output', OP_ASSIGN) + assign = ast.Assign([assign_name], instance) + + # return _q_output.getvalue() + func = ast.Getattr(ast.Name('_q_output'), "getvalue") + ret = ast.Return(ast.CallFunc(func, [])) + + # wrap original function code + code = ast.Stmt([assign, code, ret]) + + if sys.hexversion >= 0x20400a2: + n = ast.Function(decorators, name, names, defaults, flags, doc, + code) + else: + n = ast.Function(name, names, defaults, flags, doc, code) + n.lineno = lineno + + self.__template_type.pop() + return n + + def expr_stmt(self, nodelist): + if self._get_template_type() is None: + return transformer.Transformer.expr_stmt(self, nodelist) + + # Instead of discarding objects on the stack, call + # "_q_output += obj". + exprNode = self.com_node(nodelist[-1]) + if len(nodelist) == 1: + lval = ast.Name('_q_output') + n = ast.AugAssign(lval, '+=', exprNode) + if hasattr(exprNode, 'lineno'): + n.lineno = exprNode.lineno + elif nodelist[1][0] == token.EQUAL: + nodes = [ ] + for i in range(0, len(nodelist) - 2, 2): + nodes.append(self.com_assign(nodelist[i], OP_ASSIGN)) + n = ast.Assign(nodes, exprNode) + n.lineno = nodelist[1][2] + else: + lval = self.com_augassign(nodelist[0]) + op = self.com_augassign_op(nodelist[1]) + n = ast.AugAssign(lval, op[1], exprNode) + n.lineno = op[2] + return n + + def atom_string(self, nodelist): + k = '' + for node in nodelist: + k = k + eval(node[1]) + lineno = node[2] + return self._get_text_node(k) + + def _get_text_node(self, k): + if self._get_template_type() == "html": + return ast.CallFunc(ast.Name('_q_htmltext'), [ast.Const(k)]) + else: + return ast.Const(k) + +_template_re = re.compile( + r"^(?P<indent>[ \t]*) def (?:[ \t]+)" + r" (?P<name>[a-zA-Z_][a-zA-Z_0-9]*)" + r" (?:[ \t]*) \[(?P<type>plain|html)\] (?:[ \t]*)" + r" (?:[ \t]*[\(\\])", + re.MULTILINE|re.VERBOSE) + +def translate_tokens(buf): + """ + Since we can't modify the parser in the builtin parser module we + must do token translation here. Luckily it does not affect line + numbers. + + def foo [plain] (...): -> def _q_plain_template__foo(...): + + def foo [html] (...): -> def _q_html_template__foo(...): + + XXX This parser is too stupid. For example, it doesn't understand + triple quoted strings. + """ + def replacement(match): + template_type = match.group('type') + return '%sdef _q_%s_template_%s(' % (match.group('indent'), + template_type, + match.group('name')) + return _template_re.sub(replacement, buf) + +def parse(buf, filename='<string>'): + buf = translate_tokens(buf) + try: + return TemplateTransformer().parsesuite(buf) + except SyntaxError, e: + # set the filename attribute + raise SyntaxError(str(e), (filename, e.lineno, e.offset, e.text)) + + +PTL_EXT = ".ptl" + +class Template(pycodegen.Module): + + def _get_tree(self): + tree = parse(self.source, self.filename) + misc.set_filename(self.filename, tree) + syntax.check(tree) + return tree + + def dump(self, fp): + mtime = os.stat(self.filename)[stat.ST_MTIME] + fp.write('\0\0\0\0') + fp.write(struct.pack('<I', mtime)) + marshal.dump(self.code, fp) + fp.flush() + fp.seek(0) + fp.write(imp.get_magic()) + + +def compile_template(input, filename, output=None): + """(input, filename, output=None) -> code + + Compile an open file. + If output is not None then the code is written to output. + The code object is returned. + """ + buf = input.read() + template = Template(buf, filename) + template.compile() + if output is not None: + template.dump(output) + return template.code + +def compile(inputname, outputname): + """(inputname, outputname) + + Compile a template file. The new template is written to outputname. + """ + input = open(inputname) + output = open(outputname, "wb") + try: + compile_template(input, inputname, output) + except: + # don't leave a corrupt .pyc file around + output.close() + os.unlink(outputname) + raise + +def compile_dir(dir, maxlevels=10, force=0): + """Byte-compile all PTL modules in the given directory tree. + (Adapted from compile_dir in Python module: compileall.py) + + Arguments (only dir is required): + + dir: the directory to byte-compile + maxlevels: maximum recursion level (default 10) + force: if true, force compilation, even if timestamps are up-to-date + """ + print 'Listing', dir, '...' + try: + names = os.listdir(dir) + except os.error: + print "Can't list", dir + names = [] + names.sort() + success = 1 + for name in names: + fullname = os.path.join(dir, name) + if os.path.isfile(fullname): + head, tail = name[:-4], name[-4:] + if tail == PTL_EXT: + cfile = fullname[:-4] + '.pyc' + ftime = os.stat(fullname)[stat.ST_MTIME] + try: + ctime = os.stat(cfile)[stat.ST_MTIME] + except os.error: ctime = 0 + if (ctime > ftime) and not force: + continue + print 'Compiling', fullname, '...' + try: + ok = compile(fullname, cfile) + except KeyboardInterrupt: + raise KeyboardInterrupt + except: + # XXX compile catches SyntaxErrors + if type(sys.exc_type) == type(''): + exc_type_name = sys.exc_type + else: exc_type_name = sys.exc_type.__name__ + print 'Sorry:', exc_type_name + ':', + print sys.exc_value + success = 0 + else: + if ok == 0: + success = 0 + elif (maxlevels > 0 and name != os.curdir and name != os.pardir and + os.path.isdir(fullname) and not os.path.islink(fullname)): + if not compile_dir(fullname, maxlevels - 1, force): + success = 0 + return success + +def main(): + args = sys.argv[1:] + if not args: + print "no files to compile" + else: + for filename in args: + path, ext = os.path.splitext(filename) + compile(filename, path + ".pyc") + +if __name__ == "__main__": + main() diff --git a/pypers/europython05/Quixote-2.0/ptl/ptl_import.py b/pypers/europython05/Quixote-2.0/ptl/ptl_import.py new file mode 100755 index 0000000..d6ac2a0 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/ptl_import.py @@ -0,0 +1,148 @@ +""" +$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/ptl_import.py $ +$Id: ptl_import.py 26357 2005-03-16 14:56:23Z dbinger $ + +Import hooks; when installed, these hooks allow importing .ptl files +as if they were Python modules. + +Note: there's some unpleasant incompatibility between ZODB's import +trickery and the import hooks here. Bottom line: if you're using ZODB, +import it *before* installing the PTL import hooks. +""" + +import sys +import os.path +import imp, ihooks, new +import struct +import marshal +import __builtin__ + +from ptl_compile import compile_template, PTL_EXT + +assert sys.hexversion >= 0x20000b1, "need Python 2.0b1 or later" + +def _exec_module_code(code, name, filename): + if sys.modules.has_key(name): + mod = sys.modules[name] # necessary for reload() + else: + mod = new.module(name) + sys.modules[name] = mod + mod.__name__ = name + mod.__file__ = filename + exec code in mod.__dict__ + return mod + +def _timestamp(filename): + try: + s = os.stat(filename) + except OSError: + return None + return s.st_mtime + +def _load_pyc(name, filename, pyc_filename): + try: + fp = open(pyc_filename, "rb") + except IOError: + return None + if fp.read(4) == imp.get_magic(): + mtime = struct.unpack('<I', fp.read(4))[0] + ptl_mtime = _timestamp(filename) + if ptl_mtime is not None and mtime >= ptl_mtime: + code = marshal.load(fp) + return _exec_module_code(code, name, filename) + return None + +def _load_ptl(name, filename, file=None): + if not file: + try: + file = open(filename, "rb") + except IOError: + return None + path, ext = os.path.splitext(filename) + pyc_filename = path + ".pyc" + module = _load_pyc(name, filename, pyc_filename) + if module is not None: + return module + try: + output = open(pyc_filename, "wb") + except IOError: + output = None + try: + code = compile_template(file, filename, output) + except: + if output: + output.close() + os.unlink(pyc_filename) + raise + else: + if output: + output.close() + return _exec_module_code(code, name, filename) + + +# Constant used to signal a PTL files +PTL_FILE = object() + +class PTLHooks(ihooks.Hooks): + + def get_suffixes(self): + # add our suffixes + return [(PTL_EXT, 'r', PTL_FILE)] + imp.get_suffixes() + +class PTLLoader(ihooks.ModuleLoader): + + def load_module(self, name, stuff): + file, filename, info = stuff + (suff, mode, type) = info + + # If it's a PTL file, load it specially. + if type is PTL_FILE: + return _load_ptl(name, filename, file) + + else: + # Otherwise, use the default handler for loading + return ihooks.ModuleLoader.load_module(self, name, stuff) + +try: + import cimport +except ImportError: + cimport = None + +class cModuleImporter(ihooks.ModuleImporter): + def __init__(self, loader=None): + self.loader = loader or ihooks.ModuleLoader() + cimport.set_loader(self.find_import_module) + + def find_import_module(self, fullname, subname, path): + stuff = self.loader.find_module(subname, path) + if not stuff: + return None + return self.loader.load_module(fullname, stuff) + + def install(self): + self.save_import_module = __builtin__.__import__ + self.save_reload = __builtin__.reload + if not hasattr(__builtin__, 'unload'): + __builtin__.unload = None + self.save_unload = __builtin__.unload + __builtin__.__import__ = cimport.import_module + __builtin__.reload = cimport.reload_module + __builtin__.unload = self.unload + +_installed = False + +def install(): + global _installed + if not _installed: + hooks = PTLHooks() + loader = PTLLoader(hooks) + if cimport is not None: + importer = cModuleImporter(loader) + else: + importer = ihooks.ModuleImporter(loader) + ihooks.install(importer) + _installed = True + + +if __name__ == '__main__': + install() diff --git a/pypers/europython05/Quixote-2.0/ptl/ptlrun.py b/pypers/europython05/Quixote-2.0/ptl/ptlrun.py new file mode 100755 index 0000000..490188a --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/ptlrun.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +import sys +from quixote.ptl.ptl_compile import compile_template +exec compile_template(open(sys.argv[1]), sys.argv[1]) + diff --git a/pypers/europython05/Quixote-2.0/ptl/qx_distutils.py b/pypers/europython05/Quixote-2.0/ptl/qx_distutils.py new file mode 100755 index 0000000..163545a --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/qx_distutils.py @@ -0,0 +1,47 @@ +""" +$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/ptl/qx_distutils.py $ +$Id: qx_distutils.py 26357 2005-03-16 14:56:23Z dbinger $ + +Provides a version of the Distutils "build_py" command that knows about +PTL files. +""" + +import os, string +from glob import glob +from types import StringType, ListType, TupleType +from distutils.command.build_py import build_py + +class qx_build_py(build_py): + + def find_package_modules(self, package, package_dir): + self.check_package(package, package_dir) + module_files = (glob(os.path.join(package_dir, "*.py")) + + glob(os.path.join(package_dir, "*.ptl"))) + modules = [] + setup_script = os.path.abspath(self.distribution.script_name) + + for f in module_files: + abs_f = os.path.abspath(f) + if abs_f != setup_script: + module = os.path.splitext(os.path.basename(f))[0] + modules.append((package, module, f)) + else: + self.debug_print("excluding %s" % setup_script) + return modules + + def build_module(self, module, module_file, package): + if type(package) is StringType: + package = string.split(package, '.') + elif type(package) not in (ListType, TupleType): + raise TypeError, \ + "'package' must be a string (dot-separated), list, or tuple" + + # Now put the module source file into the "build" area -- this is + # easy, we just copy it somewhere under self.build_lib (the build + # directory for Python source). + outfile = self.get_module_outfile(self.build_lib, package, module) + if module_file.endswith(".ptl"): # XXX hack for PTL + outfile = outfile[0:outfile.rfind('.')] + ".ptl" + dir = os.path.dirname(outfile) + self.mkpath(dir) + return self.copy_file(module_file, outfile, preserve_mode=0) diff --git a/pypers/europython05/Quixote-2.0/ptl/test/utest_ptl.py b/pypers/europython05/Quixote-2.0/ptl/test/utest_ptl.py new file mode 100755 index 0000000..91b96ba --- /dev/null +++ b/pypers/europython05/Quixote-2.0/ptl/test/utest_ptl.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +from sancho.utest import UTest +from quixote.ptl.ptl_compile import compile_template +from cStringIO import StringIO +from quixote.html import TemplateIO, htmltext + +def run_ptl(*source): + """ + Compile the given lines of source code using the ptl compiler + and run the resulting compiled code. + """ + # When the ptl compiler compiles a module, it places _q_TemplateIO + # and _q_htmltext into the globals of the module. Here, we don't + # have a module, but we provide these same globals for eval. + eval(compile_template(StringIO('\n'.join(source)), 'test'), + dict(_q_TemplateIO=TemplateIO, _q_htmltext=htmltext)) + +class Test (UTest): + + def check_html(self): + run_ptl( + 'from quixote.html import htmltext', + 'def f [html] (a):', + ' "&"', + ' a', + 'assert type(f(1)) == htmltext', + 'assert f("") == "&"', + 'assert f("&") == "&&"', + 'assert f(htmltext("&")) == "&&"') + + def check_plain(self): + run_ptl( + 'from quixote.html import htmltext', + 'def f [plain] (a):', + ' "&"', + ' a', + 'assert type(f(1)) == str', + 'assert f("") == "&"', + 'assert f("&") == "&&"', + 'assert f(htmltext("&")) == "&&"', + 'assert type(f(htmltext("&"))) == str') + + def check_syntax(self): + run_ptl('def f(a):\n a') + try: + run_ptl('def f [] (a):\n a') + assert 0 + except SyntaxError, e: + assert e.lineno == 1 + try: + run_ptl('def f [HTML] (a):\n a') + assert 0 + except SyntaxError, e: + assert e.lineno == 1 + +if __name__ == "__main__": + Test() + diff --git a/pypers/europython05/Quixote-2.0/publish.py b/pypers/europython05/Quixote-2.0/publish.py new file mode 100755 index 0000000..058875b --- /dev/null +++ b/pypers/europython05/Quixote-2.0/publish.py @@ -0,0 +1,336 @@ +"""$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/publish.py $ +$Id: publish.py 26333 2005-03-11 01:15:40Z dbinger $ + +Logic for publishing modules and objects on the Web. +""" + +import sys, traceback, StringIO +import time +import urlparse +import cgitb + +from quixote.errors import PublishError, format_publish_error +from quixote import util +from quixote.config import Config +from quixote.http_response import HTTPResponse +from quixote.logger import DefaultLogger + +# Error message to dispay when DISPLAY_EXCEPTIONS in config file is not +# true. Note that SERVER_ADMIN must be fetched from the environment and +# plugged in here -- we can't do it now because the environment isn't +# really setup for us yet if running as a FastCGI script. +INTERNAL_ERROR_MESSAGE = """\ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" + "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html> +<head><title>Internal Server Error</title></head> +<body> +<h1>Internal Server Error</h1> +<p>An internal error occurred while handling your request.</p> + +<p>The server administrator should have been notified of the problem. +You may wish to contact the server administrator (%s) and inform them of +the time the error occurred, and anything you might have done to trigger +the error.</p> + +<p>If you are the server administrator, more information may be +available in either the server's error log or Quixote's error log.</p> +</body> +</html> +""" + +class Publisher: + """ + The core of Quixote and of any Quixote application. This class is + responsible for converting each HTTP request into a traversal of the + application's directory tree and, ultimately, a call of a Python + function/method/callable object. + + Each invocation of a driver script should have one Publisher + instance that lives for as long as the driver script itself. Eg. if + your driver script is plain CGI, each Publisher instance will handle + exactly one HTTP request; if you have a FastCGI driver, then each + Publisher will handle every HTTP request handed to that driver + script process. + + Instance attributes: + root_directory : Directory + the root directory that will be searched for objects to fulfill + each request. This can be any object with a _q_traverse method + that acts like Directory._q_traverse. + logger : DefaultLogger + controls access log and error log behavior + session_manager : NullSessionManager + keeps track of sessions + config : Config + holds all configuration info for this application. If the + application doesn't provide values then default values + from the quixote.config module are used. + _request : HTTPRequest + the HTTP request currently being processed. + """ + + def __init__(self, root_directory, logger=None, session_manager=None, + config=None, **kwargs): + global _publisher + if config is None: + self.config = Config(**kwargs) + else: + if kwargs: + raise ValueError("cannot provide both 'config' object and" + " config arguments") + self.config = config + if logger is None: + self.logger = DefaultLogger(error_log=self.config.error_log, + access_log=self.config.access_log, + error_email=self.config.error_email) + else: + self.logger = logger + if session_manager is not None: + self.session_manager = session_manager + else: + from quixote.session import NullSessionManager + self.session_manager = NullSessionManager() + + if _publisher is not None: + raise RuntimeError, "only one instance of Publisher allowed" + _publisher = self + + if not callable(getattr(root_directory, '_q_traverse')): + raise TypeError( + 'Expected something with a _q_traverse method, got %r' % + root_directory) + self.root_directory = root_directory + self._request = None + + def set_session_manager(self, session_manager): + self.session_manager = session_manager + + def log(self, msg): + self.logger.log(msg) + + def parse_request(self, request): + """Parse the request information waiting in 'request'. + """ + request.process_inputs() + + def start_request(self): + """Called at the start of each request. + """ + self.session_manager.start_request() + + def _set_request(self, request): + """Set the current request object. + """ + self._request = request + + def _clear_request(self): + """Unset the current request object. + """ + self._request = None + + def get_request(self): + """Return the current request object. + """ + return self._request + + def finish_successful_request(self): + """Called at the end of a successful request. + """ + self.session_manager.finish_successful_request() + + def format_publish_error(self, exc): + return format_publish_error(exc) + + def finish_interrupted_request(self, exc): + """ + Called at the end of an interrupted request. Requests are + interrupted by raising a PublishError exception. This method + should return a string object which will be used as the result of + the request. + """ + if not self.config.display_exceptions and exc.private_msg: + exc.private_msg = None # hide it + request = get_request() + request.response = HTTPResponse(status=exc.status_code) + output = self.format_publish_error(exc) + self.session_manager.finish_successful_request() + return output + + def finish_failed_request(self): + """ + Called at the end of an failed request. Any exception (other + than PublishError) causes a request to fail. This method should + return a string object which will be used as the result of the + request. + """ + # build new response to be safe + request = get_request() + original_response = request.response + request.response = HTTPResponse() + #self.log("caught an error (%s), reporting it." % + # sys.exc_info()[1]) + + (exc_type, exc_value, tb) = sys.exc_info() + error_summary = traceback.format_exception_only(exc_type, exc_value) + error_summary = error_summary[0][0:-1] # de-listify and strip newline + + plain_error_msg = self._generate_plaintext_error(request, + original_response, + exc_type, exc_value, + tb) + + if not self.config.display_exceptions: + # DISPLAY_EXCEPTIONS is false, so return the most + # secure (and cryptic) page. + request.response.set_header("Content-Type", "text/html") + user_error_msg = self._generate_internal_error(request) + elif self.config.display_exceptions == 'html': + # Generate a spiffy HTML display using cgitb + request.response.set_header("Content-Type", "text/html") + user_error_msg = self._generate_cgitb_error(request, + original_response, + exc_type, exc_value, + tb) + else: + # Generate a plaintext page containing the traceback + request.response.set_header("Content-Type", "text/plain") + user_error_msg = plain_error_msg + + self.logger.log_internal_error(error_summary, plain_error_msg) + request.response.set_status(500) + self.session_manager.finish_failed_request() + return user_error_msg + + + def _generate_internal_error(self, request): + admin = request.get_environ('SERVER_ADMIN', + "<i>email address unknown</i>") + return INTERNAL_ERROR_MESSAGE % admin + + + def _generate_plaintext_error(self, request, original_response, + exc_type, exc_value, tb): + error_file = StringIO.StringIO() + + # format the traceback + traceback.print_exception(exc_type, exc_value, tb, file=error_file) + + # include request and response dumps + error_file.write('\n') + error_file.write(request.dump()) + error_file.write('\n') + + return error_file.getvalue() + + + def _generate_cgitb_error(self, request, original_response, + exc_type, exc_value, tb): + error_file = StringIO.StringIO() + hook = cgitb.Hook(file=error_file) + hook(exc_type, exc_value, tb) + error_file.write('<h2>Original Request</h2>') + error_file.write(str(util.dump_request(request))) + error_file.write('<h2>Original Response</h2><pre>') + original_response.write(error_file) + error_file.write('</pre>') + return error_file.getvalue() + + + def try_publish(self, request): + """(request : HTTPRequest) -> object + + The master method that does all the work for a single request. + Exceptions are handled by the caller. + """ + self.start_request() + path = request.get_environ('PATH_INFO', '') + assert path[:1] == '/' + # split path into components + path = path[1:].split('/') + output = self.root_directory._q_traverse(path) + # The callable ran OK, commit any changes to the session + self.finish_successful_request() + return output + + def filter_output(self, request, output): + """Hook for post processing the output. Subclasses may wish to + override (e.g. check HTML syntax). + """ + return output + + def process_request(self, request): + """(request : HTTPRequest) -> HTTPResponse + + Process a single request, given an HTTPRequest object. The + try_publish() method will be called to do the work and + exceptions will be handled here. + """ + self._set_request(request) + start_time = time.time() + try: + self.parse_request(request) + output = self.try_publish(request) + except PublishError, exc: + # Exit the publishing loop and return a result right away. + output = self.finish_interrupted_request(exc) + except: + # Some other exception, generate error messages to the logs, etc. + output = self.finish_failed_request() + output = self.filter_output(request, output) + self.logger.log_request(request, start_time) + if output: + if self.config.compress_pages and request.get_encoding(["gzip"]): + compress = True + else: + compress = False + request.response.set_body(output, compress) + self._clear_request() + return request.response + + +# Publisher singleton, only one of these per process. +_publisher = None + +def get_publisher(): + return _publisher + +def get_request(): + return _publisher.get_request() + +def get_response(): + return _publisher.get_request().response + +def get_field(name, default=None): + return _publisher.get_request().get_field(name, default) + +def get_cookie(name, default=None): + return _publisher.get_request().get_cookie(name, default) + +def get_path(n=0): + return _publisher.get_request().get_path(n) + +def redirect(location, permanent=False): + """(location : string, permanent : boolean = false) -> string + + Create a redirection response. If the location is relative, then it + will automatically be made absolute. The return value is an HTML + document indicating the new URL (useful if the client browser does + not honor the redirect). + """ + request = _publisher.get_request() + location = urlparse.urljoin(request.get_url(), str(location)) + return request.response.redirect(location, permanent) + +def get_session(): + return _publisher.get_request().session + +def get_session_manager(): + return _publisher.session_manager + +def get_user(): + session = _publisher.get_request().session + if session is None: + return None + else: + return session.user diff --git a/pypers/europython05/Quixote-2.0/publish1.py b/pypers/europython05/Quixote-2.0/publish1.py new file mode 100755 index 0000000..93bfaf3 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/publish1.py @@ -0,0 +1,270 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/publish1.py $ +$Id: publish1.py 25664 2004-11-22 20:35:07Z nascheme $ + +Provides a publisher object that behaves like the Quixote 1 Publisher. +Specifically, arbitrary namespaces may be exported and the HTTPRequest +object is passed as the first argument to exported functions. Also, +the _q_lookup(), _q_resolve(), and _q_access() methods work as they did +in Quixote 1. +""" + +import sys +import re +import types +import warnings +from quixote import errors, get_request, redirect +from quixote.publish import Publisher as _Publisher +from quixote.directory import Directory +from quixote.html import htmltext + + +class Publisher(_Publisher): + """ + Instance attributes: + namespace_stack : [ module | instance | class ] + """ + + def __init__(self, root_namespace, config=None): + from quixote.config import Config + if type(root_namespace) is types.StringType: + root_namespace = _get_module(root_namespace) + self.namespace_stack = [root_namespace] + if config is None: + config = Config() + directory = RootDirectory(root_namespace, self.namespace_stack) + _Publisher.__init__(self, directory, config=config) + + def debug(self, msg): + self.log(msg) + + def get_namespace_stack(self): + """get_namespace_stack() -> [ module | instance | class ] + """ + return self.namespace_stack + + +class RootDirectory(Directory): + def __init__(self, root_namespace, namespace_stack): + self.root_namespace = root_namespace + self.namespace_stack = namespace_stack + + def _q_traverse(self, path): + # Initialize the publisher's namespace_stack + del self.namespace_stack[:] + + request = get_request() + + # Traverse package to a (hopefully-) callable object + object = _traverse_url(self.root_namespace, path, request, + self.namespace_stack) + + # None means no output -- traverse_url() just issued a redirect. + if object is None: + return None + + # Anything else must be either a string... + if isstring(object): + output = object + + # ...or a callable. + elif callable(object): + output = object(request) + if output is None: + raise RuntimeError, 'callable %s returned None' % repr(object) + + # Uh-oh: 'object' is neither a string nor a callable. + else: + raise RuntimeError( + "object is neither callable nor a string: %s" % repr(object)) + + return output + + +def _get_module(name): + """Get a module object by name.""" + __import__(name) + module = sys.modules[name] + return module + + +_slash_pat = re.compile("//*") + +def _traverse_url(root_namespace, path_components, request, namespace_stack): + """(root_namespace : any, path_components : [string], + request : HTTPRequest, namespace_stack : list) -> (object : any) + + Perform traversal based on the provided path, starting at the root + object. It returns the script name and path info values for + the arrived-at object, along with the object itself and + a list of the namespaces traversed to get there. + + It's expected that the final object is something callable like a + function or a method; intermediate objects along the way will + usually be packages or modules. + + To prevent crackers from writing URLs that traverse private + objects, every package, module, or object along the way must have + a _q_exports attribute containing a list of publicly visible + names. Not having a _q_exports attribute is an error, though + having _q_exports be an empty list is OK. If a component of the path + isn't in _q_exports, that also produces an error. + + Modifies the namespace_stack as it traverses the url, so that + any exceptions encountered along the way can be handled by the + nearest handler. + """ + + path = '/' + '/'.join(path_components) + + # If someone accesses a Quixote driver script without a trailing + # slash, we'll wind up here with an empty path. This won't + # work; relative references in the page generated by the root + # namespace's _q_index() will be off. Fix it by redirecting the + # user to the right URL; when the client follows the redirect, + # we'll wind up here again with path == '/'. + if not path: + return redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=1) + + # Traverse starting at the root + object = root_namespace + namespace_stack.append(object) + + # Loop over the components of the path + for component in path_components: + if component == "": + # "/q/foo/" == "/q/foo/_q_index" + component = "_q_index" + object = _get_component(object, component, request, namespace_stack) + + if not (isstring(object) or callable(object)): + # We went through all the components of the path and ended up at + # something which isn't callable, like a module or an instance + # without a __call__ method. + if path[-1] != '/': + if not request.form: + # This is for the convenience of users who type in paths. + # Repair the path and redirect. This should not happen for + # URLs within the site. + return redirect(request.get_path() + "/", permanent=1) + + else: + # Automatic redirects disabled or there is form data. If + # there is form data then the programmer is using the + # wrong path. A redirect won't work if the form data came + # from a POST anyhow. + raise errors.TraversalError( + "object is neither callable nor string " + "(missing trailing slash?)", + private_msg=repr(object), + path=path) + else: + raise errors.TraversalError( + "object is neither callable nor string", + private_msg=repr(object), + path=path) + + return object + + +def _get_component(container, component, request, namespace_stack): + """Get one component of a path from a namespace. + """ + # First security check: if the container doesn't even have an + # _q_exports list, fail now: all Quixote-traversable namespaces + # (modules, packages, instances) must have an export list! + if not hasattr(container, '_q_exports'): + raise errors.TraversalError( + private_msg="%r has no _q_exports list" % container) + + # Second security check: call _q_access function if it's present. + if hasattr(container, '_q_access'): + # will raise AccessError if access failed + container._q_access(request) + + # Third security check: make sure the current name component + # is in the export list or is '_q_index'. If neither + # condition is true, check for a _q_lookup() and call it. + # '_q_lookup()' translates an arbitrary string into an object + # that we continue traversing. (This is very handy; it lets + # you put user-space objects into your URL-space, eliminating + # the need for digging ID strings out of a query, or checking + # PATHINFO after Quixote's done with it. But it is a + # compromise to security: it opens up the traversal algorithm + # to arbitrary names not listed in _q_exports!) If + # _q_lookup() doesn't exist or is None, a TraversalError is + # raised. + + # Check if component is in _q_exports. The elements in + # _q_exports can be strings or 2-tuples mapping external names + # to internal names. + if component in container._q_exports or component == '_q_index': + internal_name = component + else: + # check for an explicit external to internal mapping + for value in container._q_exports: + if type(value) is types.TupleType: + if value[0] == component: + internal_name = value[1] + break + else: + internal_name = None + + if internal_name is None: + # Component is not in exports list. + object = None + if hasattr(container, "_q_lookup"): + object = container._q_lookup(request, component) + elif hasattr(container, "_q_getname"): + warnings.warn("_q_getname() on %s used; should " + "be replaced by _q_lookup()" % type(container)) + object = container._q_getname(request, component) + if object is None: + raise errors.TraversalError( + private_msg="object %r has no attribute %r" % ( + container, + component)) + + # From here on, you can assume that the internal_name is not None + elif hasattr(container, internal_name): + # attribute is in _q_exports and exists + object = getattr(container, internal_name) + + elif internal_name == '_q_index': + if hasattr(container, "_q_lookup"): + object = container._q_lookup(request, "") + else: + raise errors.AccessError( + private_msg=("_q_index not found in %r" % container)) + + elif hasattr(container, "_q_resolve"): + object = container._q_resolve(internal_name) + if object is None: + raise RuntimeError, ("component listed in _q_exports, " + "but not returned by _q_resolve(%r)" + % internal_name) + else: + # Set the object, so _q_resolve won't need to be called again. + setattr(container, internal_name, object) + + elif type(container) is types.ModuleType: + # try importing it as a sub-module. If we get an ImportError + # here we don't catch it. It means that something that + # doesn't exist was exported or an exception was raised from + # deeper in the code. + mod_name = container.__name__ + '.' + internal_name + object = _get_module(mod_name) + + else: + # a non-existent attribute is in _q_exports, + # and the container is not a module. Give up. + raise errors.TraversalError( + private_msg=("%r in _q_exports list, " + "but not found in %r" % (component, + container))) + + namespace_stack.append(object) + return object + + +def isstring(x): + return isinstance(x, (str, unicode, htmltext)) diff --git a/pypers/europython05/Quixote-2.0/sendmail.py b/pypers/europython05/Quixote-2.0/sendmail.py new file mode 100755 index 0000000..0a13884 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/sendmail.py @@ -0,0 +1,273 @@ +"""quixote.sendmail +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/sendmail.py $ +$Id: sendmail.py 25234 2004-09-30 17:36:19Z nascheme $ + +Tools for sending mail from Quixote applications. +""" + +# created 2001/08/27, Greg Ward (with a long and complicated back-story) + +__revision__ = "$Id: sendmail.py 25234 2004-09-30 17:36:19Z nascheme $" + +import re +from types import ListType, TupleType, StringType +from smtplib import SMTP + +rfc822_specials_re = re.compile(r'[\(\)\<\>\@\,\;\:\\\"\.\[\]]') + +class RFC822Mailbox: + """ + In RFC 822, a "mailbox" is either a bare e-mail address or a bare + e-mail address coupled with a chunk of text, most often someone's + name. Eg. the following are all "mailboxes" in the RFC 822 grammar: + luser@example.com + Joe Luser <luser@example.com> + Paddy O'Reilly <paddy@example.ie> + "Smith, John" <smith@example.com> + Dick & Jane <dickjane@example.net> + "Tom, Dick, & Harry" <tdh@example.org> + + This class represents an (addr_spec, real_name) pair and takes care + of quoting the real_name according to RFC 822's rules for you. + Just use the format() method and it will spit out a properly- + quoted RFC 822 "mailbox". + """ + + def __init__(self, *args): + """RFC822Mailbox(addr_spec : string, name : string) + RFC822Mailbox(addr_spec : string) + RFC822Mailbox((addr_spec : string, name : string)) + RFC822Mailbox((addr_spec : string)) + + Create a new RFC822Mailbox instance. The variety of call + signatures is purely for your convenience. + """ + if (len(args) == 1 and type(args[0]) is TupleType): + args = args[0] + + if len(args) == 1: + addr_spec = args[0] + real_name = None + elif len(args) == 2: + (addr_spec, real_name) = args + else: + raise TypeError( + "invalid number of arguments: " + "expected 1 or 2 strings or " + "a tuple of 1 or 2 strings") + + self.addr_spec = addr_spec + self.real_name = real_name + + def __str__(self): + return self.addr_spec + + def __repr__(self): + return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self) + + def format(self): + if self.real_name and rfc822_specials_re.search(self.real_name): + return '"%s" <%s>' % (self.real_name.replace('"', '\\"'), + self.addr_spec) + elif self.real_name: + return '%s <%s>' % (self.real_name, self.addr_spec) + + else: + return self.addr_spec + + +def _ensure_mailbox(s): + """_ensure_mailbox(s : string | + (string,) | + (string, string) | + RFC822Mailbox | + None) + -> RFC822Mailbox | None + + If s is a string, or a tuple of 1 or 2 strings, returns an + RFC822Mailbox encapsulating them as an addr_spec and real_name. If + s is already an RFC822Mailbox, returns s. If s is None, returns + None. + """ + if s is None or isinstance(s, RFC822Mailbox): + return s + else: + return RFC822Mailbox(s) + + +# Maximum number of recipients that will be explicitly listed in +# any single message header. Eg. if MAX_HEADER_RECIPIENTS is 10, +# there could be up to 10 "To" recipients and 10 "CC" recipients +# explicitly listed in the message headers. +MAX_HEADER_RECIPIENTS = 10 + +def _add_recip_headers(headers, field_name, addrs): + if not addrs: + return + addrs = [addr.format() for addr in addrs] + + if len(addrs) == 1: + headers.append("%s: %s" % (field_name, addrs[0])) + elif len(addrs) <= MAX_HEADER_RECIPIENTS: + headers.append("%s: %s," % (field_name, addrs[0])) + for addr in addrs[1:-1]: + headers.append(" %s," % addr) + headers.append(" %s" % addrs[-1]) + else: + headers.append("%s: (long recipient list suppressed) : ;" % field_name) + + +def sendmail(subject, msg_body, to_addrs, + from_addr=None, cc_addrs=None, + extra_headers=None, + smtp_sender=None, smtp_recipients=None, + config=None): + """sendmail(subject : string, + msg_body : string, + to_addrs : [email_address], + from_addr : email_address = config.MAIL_SENDER, + cc_addrs : [email_address] = None, + extra_headers : [string] = None, + smtp_sender : email_address = (derived from from_addr) + smtp_recipients : [email_address] = (derived from to_addrs), + config : quixote.config.Config = (current publisher's config)): + + Send an email message to a list of recipients via a local SMTP + server. In normal use, you supply a list of primary recipient + e-mail addresses in 'to_addrs', an optional list of secondary + recipient addresses in 'cc_addrs', and a sender address in + 'from_addr'. sendmail() then constructs a message using those + addresses, 'subject', and 'msg_body', and mails the message to every + recipient address. (Specifically, it connects to the mail server + named in the MAIL_SERVER config variable -- default "localhost" -- + and instructs the server to send the message to every recipient + address in 'to_addrs' and 'cc_addrs'.) + + 'from_addr' is optional because web applications often have a common + e-mail sender address, such as "webmaster@example.com". Just set + the Quixote config variable MAIL_FROM, and it will be used as the + default sender (both header and envelope) for all e-mail sent by + sendmail(). + + E-mail addresses can be specified a number of ways. The most + efficient is to supply instances of RFC822Mailbox, which bundles a + bare e-mail address (aka "addr_spec" from the RFC 822 grammar) and + real name together in a readily-formattable object. You can also + supply an (addr_spec, real_name) tuple, or an addr_spec on its own. + The latter two are converted into RFC822Mailbox objects for + formatting, which is why it may be more efficient to construct + RFC822Mailbox objects yourself. + + Thus, the following are all equivalent in terms of who gets the + message: + sendmail(to_addrs=["joe@example.com"], ...) + sendmail(to_addrs=[("joe@example.com", "Joe User")], ...) + sendmail(to_addrs=[RFC822Mailbox("joe@example.com", "Joe User")], ...) + ...although the "To" header will be slightly different. In the + first case, it will be + To: joe@example.com + while in the other two, it will be: + To: Joe User <joe@example.com> + which is a little more user-friendly. + + In more advanced usage, you might wish to specify the SMTP sender + and recipient addresses separately. For example, if you want your + application to send mail to users that looks like it comes from a + real human being, but you don't want that human being to get the + bounce messages from the mailing, you might do this: + sendmail(to_addrs=user_list, + ..., + from_addr=("realuser@example.com", "A Real User"), + smtp_sender="postmaster@example.com") + + End users will see mail from "A Real User <realuser@example.com>" in + their inbox, but bounces will go to postmaster@example.com. + + One use of different header and envelope recipients is for + testing/debugging. If you want to test that your application is + sending the right mail to bigboss@example.com without filling + bigboss' inbox with dross, you might do this: + sendmail(to_addrs=["bigboss@example.com"], + ..., + smtp_recipients=["developers@example.com"]) + + This is so useful that it's a Quixote configuration option: just set + MAIL_DEBUG_ADDR to (eg.) "developers@example.com", and every message + that sendmail() would send out is diverted to the debug address. + + Generally raises an exception on any SMTP errors; see smtplib (in + the standard library documentation) for details. + """ + if config is None: + from quixote import get_publisher + config = get_publisher().config + + if not isinstance(to_addrs, ListType): + raise TypeError("'to_addrs' must be a list") + if not (cc_addrs is None or isinstance(cc_addrs, ListType)): + raise TypeError("'cc_addrs' must be a list or None") + + # Make sure we have a "From" address + if from_addr is None: + from_addr = config.mail_from + if from_addr is None: + raise RuntimeError( + "no from_addr supplied, and MAIL_FROM not set in config file") + + # Ensure all of our addresses are really RFC822Mailbox objects. + from_addr = _ensure_mailbox(from_addr) + to_addrs = map(_ensure_mailbox, to_addrs) + if cc_addrs: + cc_addrs = map(_ensure_mailbox, cc_addrs) + + # Start building the message headers. + headers = ["From: %s" % from_addr.format(), + "Subject: %s" % subject] + _add_recip_headers(headers, "To", to_addrs) + + if cc_addrs: + _add_recip_headers(headers, "Cc", cc_addrs) + + if extra_headers: + headers.extend(extra_headers) + + if config.mail_debug_addr: + debug1 = ("[debug mode, message actually sent to %s]\n" + % config.mail_debug_addr) + if smtp_recipients: + debug2 = ("[original SMTP recipients: %s]\n" + % ", ".join(smtp_recipients)) + else: + debug2 = "" + + sep = ("-"*72) + "\n" + msg_body = debug1 + debug2 + sep + msg_body + + smtp_recipients = [config.mail_debug_addr] + + if smtp_sender is None: + smtp_sender = from_addr.addr_spec + else: + smtp_sender = _ensure_mailbox(smtp_sender).addr_spec + + if smtp_recipients is None: + smtp_recipients = [addr.addr_spec for addr in to_addrs] + if cc_addrs: + smtp_recipients.extend([addr.addr_spec for addr in cc_addrs]) + else: + smtp_recipients = [_ensure_mailbox(recip).addr_spec + for recip in smtp_recipients] + + message = "\n".join(headers) + "\n\n" + msg_body + + # Sanity checks + assert type(smtp_sender) is StringType, \ + "smtp_sender not a string: %r" % (smtp_sender,) + assert (type(smtp_recipients) is ListType and + map(type, smtp_recipients) == [StringType]*len(smtp_recipients)), \ + "smtp_recipients not a list of strings: %r" % (smtp_recipients,) + smtp = SMTP(config.mail_server) + smtp.sendmail(smtp_sender, smtp_recipients, message) + smtp.quit() + +# sendmail () diff --git a/pypers/europython05/Quixote-2.0/server/__init__.py b/pypers/europython05/Quixote-2.0/server/__init__.py new file mode 100755 index 0000000..6947382 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/__init__.py @@ -0,0 +1,5 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/__init__.py $ +$Id: __init__.py 25579 2004-11-11 20:56:32Z nascheme $ + +This package is for Quixote to server glue. +""" diff --git a/pypers/europython05/Quixote-2.0/server/_fcgi.py b/pypers/europython05/Quixote-2.0/server/_fcgi.py new file mode 100755 index 0000000..7ac41d2 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/_fcgi.py @@ -0,0 +1,466 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/_fcgi.py $ +$Id: _fcgi.py 25688 2004-11-30 20:05:33Z dbinger $ +Derived from Robin Dunn's FastCGI module, +available at http://alldunn.com/python/#fcgi. +""" +#------------------------------------------------------------------------ +# Copyright (c) 1998 by Total Control Software +# All Rights Reserved +#------------------------------------------------------------------------ +# +# Module Name: fcgi.py +# +# Description: Handles communication with the FastCGI module of the +# web server without using the FastCGI developers kit, but +# will also work in a non-FastCGI environment, (straight CGI.) +# This module was originally fetched from someplace on the +# Net (I don't remember where and I can't find it now...) and +# has been significantly modified to fix several bugs, be more +# readable, more robust at handling large CGI data and return +# document sizes, and also to fit the model that we had previously +# used for FastCGI. +# +# WARNING: If you don't know what you are doing, don't tinker with this +# module! +# +# Creation Date: 1/30/98 2:59:04PM +# +# License: This is free software. You may use this software for any +# purpose including modification/redistribution, so long as +# this header remains intact and that you do not claim any +# rights of ownership or authorship of this software. This +# software has been tested, but no warranty is expressed or +# implied. +# +#------------------------------------------------------------------------ + +__revision__ = "$Id: _fcgi.py 25688 2004-11-30 20:05:33Z dbinger $" + + +import os, sys, string, socket, errno, struct +from cStringIO import StringIO +import cgi + +#--------------------------------------------------------------------------- + +# Set various FastCGI constants +# Maximum number of requests that can be handled +FCGI_MAX_REQS=1 +FCGI_MAX_CONNS = 1 + +# Supported version of the FastCGI protocol +FCGI_VERSION_1 = 1 + +# Boolean: can this application multiplex connections? +FCGI_MPXS_CONNS=0 + +# Record types +FCGI_BEGIN_REQUEST = 1 ; FCGI_ABORT_REQUEST = 2 ; FCGI_END_REQUEST = 3 +FCGI_PARAMS = 4 ; FCGI_STDIN = 5 ; FCGI_STDOUT = 6 +FCGI_STDERR = 7 ; FCGI_DATA = 8 ; FCGI_GET_VALUES = 9 +FCGI_GET_VALUES_RESULT = 10 +FCGI_UNKNOWN_TYPE = 11 +FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE + +# Types of management records +ManagementTypes = [FCGI_GET_VALUES] + +FCGI_NULL_REQUEST_ID = 0 + +# Masks for flags component of FCGI_BEGIN_REQUEST +FCGI_KEEP_CONN = 1 + +# Values for role component of FCGI_BEGIN_REQUEST +FCGI_RESPONDER = 1 ; FCGI_AUTHORIZER = 2 ; FCGI_FILTER = 3 + +# Values for protocolStatus component of FCGI_END_REQUEST +FCGI_REQUEST_COMPLETE = 0 # Request completed nicely +FCGI_CANT_MPX_CONN = 1 # This app can't multiplex +FCGI_OVERLOADED = 2 # New request rejected; too busy +FCGI_UNKNOWN_ROLE = 3 # Role value not known + + +error = 'fcgi.error' + + +#--------------------------------------------------------------------------- + +# The following function is used during debugging; it isn't called +# anywhere at the moment + +def error(msg): + "Append a string to /tmp/err" + errf = open('/tmp/err', 'a+') + errf.write(msg+'\n') + errf.close() + +#--------------------------------------------------------------------------- + +class record: + "Class representing FastCGI records" + def __init__(self): + self.version = FCGI_VERSION_1 + self.recType = FCGI_UNKNOWN_TYPE + self.reqId = FCGI_NULL_REQUEST_ID + self.content = "" + + #---------------------------------------- + def readRecord(self, sock, unpack=struct.unpack): + (self.version, self.recType, self.reqId, contentLength, + paddingLength) = unpack(">BBHHBx", sock.recv(8)) + + content = "" + while len(content) < contentLength: + content = content + sock.recv(contentLength - len(content)) + self.content = content + + if paddingLength != 0: + padding = sock.recv(paddingLength) + + # Parse the content information + if self.recType == FCGI_BEGIN_REQUEST: + (self.role, self.flags) = unpack(">HB", content[:3]) + + elif self.recType == FCGI_UNKNOWN_TYPE: + self.unknownType = ord(content[0]) + + elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS: + self.values = {} + pos = 0 + while pos < len(content): + name, value, pos = readPair(content, pos) + self.values[name] = value + + elif self.recType == FCGI_END_REQUEST: + (self.appStatus, self.protocolStatus) = unpack(">IB", content[0:5]) + + #---------------------------------------- + def writeRecord(self, sock, pack=struct.pack): + content = self.content + if self.recType == FCGI_BEGIN_REQUEST: + content = pack(">HBxxxxx", self.role, self.flags) + + elif self.recType == FCGI_UNKNOWN_TYPE: + content = pack(">Bxxxxxx", self.unknownType) + + elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS: + content = "" + for i in self.values.keys(): + content = content + writePair(i, self.values[i]) + + elif self.recType == FCGI_END_REQUEST: + content = pack(">IBxxx", self.appStatus, self.protocolStatus) + + cLen = len(content) + eLen = (cLen + 7) & (0xFFFF - 7) # align to an 8-byte boundary + padLen = eLen - cLen + + hdr = pack(">BBHHBx", self.version, self.recType, self.reqId, cLen, + padLen) + + ##debug.write('Sending fcgi record: %s\n' % repr(content[:50]) ) + sock.send(hdr + content + padLen*'\000') + +#--------------------------------------------------------------------------- + +_lowbits = ~(1L << 31) # everything but the 31st bit + +def readPair(s, pos): + nameLen = ord(s[pos]) ; pos = pos+1 + if nameLen & 128: + pos = pos + 3 + nameLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits) + valueLen = ord(s[pos]) ; pos = pos+1 + if valueLen & 128: + pos = pos + 3 + valueLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits) + return ( s[pos:pos+nameLen], s[pos+nameLen:pos+nameLen+valueLen], + pos+nameLen+valueLen ) + +#--------------------------------------------------------------------------- + +_highbit = (1L << 31) + +def writePair(name, value): + l = len(name) + if l < 128: + s = chr(l) + else: + s = struct.pack(">I", l | _highbit) + l = len(value) + if l < 128: + s = s + chr(l) + else: + s = s + struct.pack(">I", l | _highbit) + return s + name + value + +#--------------------------------------------------------------------------- + +def HandleManTypes(r, conn): + if r.recType == FCGI_GET_VALUES: + r.recType = FCGI_GET_VALUES_RESULT + v = {} + vars = {'FCGI_MAX_CONNS' : FCGI_MAX_CONNS, + 'FCGI_MAX_REQS' : FCGI_MAX_REQS, + 'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS} + for i in r.values.keys(): + if vars.has_key(i): v[i] = vars[i] + r.values = vars + r.writeRecord(conn) + +#--------------------------------------------------------------------------- +#--------------------------------------------------------------------------- + + +_isFCGI = 1 # assume it is until we find out for sure + +def isFCGI(): + return _isFCGI + + + +#--------------------------------------------------------------------------- + + +_init = None +_sock = None + +class FCGI: + def __init__(self): + self.haveFinished = 0 + if _init == None: + _startup() + if not _isFCGI: + self.haveFinished = 1 + self.inp = sys.__stdin__ + self.out = sys.__stdout__ + self.err = sys.__stderr__ + self.env = os.environ + return + + if os.environ.has_key('FCGI_WEB_SERVER_ADDRS'): + good_addrs = string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',') + good_addrs = map(string.strip, good_addrs) # Remove whitespace + else: + good_addrs = None + + self.conn, addr = _sock.accept() + stdin, data = "", "" + self.env = {} + self.requestId = 0 + remaining = 1 + + # Check if the connection is from a legal address + if good_addrs != None and addr not in good_addrs: + raise error, 'Connection from invalid server!' + + while remaining: + r = record() + r.readRecord(self.conn) + + if r.recType in ManagementTypes: + HandleManTypes(r, self.conn) + + elif r.reqId == 0: + # Oh, poopy. It's a management record of an unknown + # type. Signal the error. + r2 = record() + r2.recType = FCGI_UNKNOWN_TYPE + r2.unknownType = r.recType + r2.writeRecord(self.conn) + continue # Charge onwards + + # Ignore requests that aren't active + elif r.reqId != self.requestId and r.recType != FCGI_BEGIN_REQUEST: + continue + + # If we're already doing a request, ignore further BEGIN_REQUESTs + elif r.recType == FCGI_BEGIN_REQUEST and self.requestId != 0: + continue + + # Begin a new request + if r.recType == FCGI_BEGIN_REQUEST: + self.requestId = r.reqId + if r.role == FCGI_AUTHORIZER: remaining = 1 + elif r.role == FCGI_RESPONDER: remaining = 2 + elif r.role == FCGI_FILTER: remaining = 3 + + elif r.recType == FCGI_PARAMS: + if r.content == "": + remaining = remaining-1 + else: + for i in r.values.keys(): + self.env[i] = r.values[i] + + elif r.recType == FCGI_STDIN: + if r.content == "": + remaining = remaining-1 + else: + stdin = stdin+r.content + + elif r.recType == FCGI_DATA: + if r.content == "": + remaining = remaining-1 + else: + data = data+r.content + # end of while remaining: + + self.inp = StringIO(stdin) + self.err = StringIO() + self.out = StringIO() + self.data = StringIO(data) + + def __del__(self): + self.Finish() + + def Finish(self, status=0): + if not self.haveFinished: + self.haveFinished = 1 + + self.err.seek(0,0) + self.out.seek(0,0) + + ##global debug + ##debug = open("/tmp/quixote-debug.log", "a+") + ##debug.write("fcgi.FCGI.Finish():\n") + + r = record() + r.recType = FCGI_STDERR + r.reqId = self.requestId + data = self.err.read() + ##debug.write(" sending stderr (%s)\n" % `self.err`) + ##debug.write(" data = %s\n" % `data`) + while data: + chunk, data = self.getNextChunk(data) + ##debug.write(" chunk, data = %s, %s\n" % (`chunk`, `data`)) + r.content = chunk + r.writeRecord(self.conn) + r.content = "" + r.writeRecord(self.conn) # Terminate stream + + r.recType = FCGI_STDOUT + data = self.out.read() + ##debug.write(" sending stdout (%s)\n" % `self.out`) + ##debug.write(" data = %s\n" % `data`) + while data: + chunk, data = self.getNextChunk(data) + r.content = chunk + r.writeRecord(self.conn) + r.content = "" + r.writeRecord(self.conn) # Terminate stream + + r = record() + r.recType = FCGI_END_REQUEST + r.reqId = self.requestId + r.appStatus = status + r.protocolStatus = FCGI_REQUEST_COMPLETE + r.writeRecord(self.conn) + self.conn.close() + + #debug.close() + + + def getFieldStorage(self): + method = 'GET' + if self.env.has_key('REQUEST_METHOD'): + method = string.upper(self.env['REQUEST_METHOD']) + if method == 'GET': + return cgi.FieldStorage(environ=self.env, keep_blank_values=1) + else: + return cgi.FieldStorage(fp=self.inp, + environ=self.env, + keep_blank_values=1) + + def getNextChunk(self, data): + chunk = data[:8192] + data = data[8192:] + return chunk, data + + +Accept = FCGI # alias for backwards compatibility +#--------------------------------------------------------------------------- + +def _startup(): + global _isFCGI, _init, _sock + # This function won't work on Windows at all. + if sys.platform[:3] == 'win': + _isFCGI = 0 + return + + _init = 1 + try: + s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET, + socket.SOCK_STREAM) + s.getpeername() + except socket.error, (err, errmsg): + if err != errno.ENOTCONN: # must be a non-fastCGI environment + _isFCGI = 0 + return + + _sock = s + + +#--------------------------------------------------------------------------- + +def _test(): + counter = 0 + try: + while isFCGI(): + req = Accept() + counter = counter+1 + + try: + fs = req.getFieldStorage() + size = string.atoi(fs['size'].value) + doc = ['*' * size] + except: + doc = ['<HTML><HEAD>' + '<TITLE>FCGI TestApp</TITLE>' + '</HEAD>\n<BODY>\n'] + doc.append('<H2>FCGI TestApp</H2><P>') + doc.append('<b>request count</b> = %d<br>' % counter) + doc.append('<b>pid</b> = %s<br>' % os.getpid()) + if req.env.has_key('CONTENT_LENGTH'): + cl = string.atoi(req.env['CONTENT_LENGTH']) + doc.append('<br><b>POST data (%s):</b><br><pre>' % cl) + keys = fs.keys() + keys.sort() + for k in keys: + val = fs[k] + if type(val) == type([]): + doc.append(' <b>%-15s :</b> %s\n' + % (k, val)) + else: + doc.append(' <b>%-15s :</b> %s\n' + % (k, val.value)) + doc.append('</pre>') + + + doc.append('<P><HR><P><pre>') + keys = req.env.keys() + keys.sort() + for k in keys: + doc.append('<b>%-20s :</b> %s\n' % (k, req.env[k])) + doc.append('\n</pre><P><HR>\n') + doc.append('</BODY></HTML>\n') + + + doc = string.join(doc, '') + req.out.write('Content-length: %s\r\n' + 'Content-type: text/html\r\n' + 'Cache-Control: no-cache\r\n' + '\r\n' + % len(doc)) + req.out.write(doc) + + req.Finish() + except: + import traceback + f = open('traceback', 'w') + traceback.print_exc( file = f ) +# f.write('%s' % doc) + +if __name__ == '__main__': + #import pdb + #pdb.run('_test()') + _test() diff --git a/pypers/europython05/Quixote-2.0/server/cgi_server.py b/pypers/europython05/Quixote-2.0/server/cgi_server.py new file mode 100755 index 0000000..c4e8ea2 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/cgi_server.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/cgi_server.py $ +$Id: cgi_server.py 25476 2004-10-27 21:54:59Z nascheme $ +""" + +import sys +import os +from quixote.http_request import HTTPRequest + +def run(create_publisher): + if sys.platform == "win32": + # on Windows, stdin and stdout are in text mode by default + import msvcrt + msvcrt.setmode(sys.__stdin__.fileno(), os.O_BINARY) + msvcrt.setmode(sys.__stdout__.fileno(), os.O_BINARY) + publisher = create_publisher() + request = HTTPRequest(sys.__stdin__, os.environ) + response = publisher.process_request(request) + try: + response.write(sys.__stdout__) + except IOError, err: + publisher.log("IOError while sending response ignored: %s" % err) + + +if __name__ == '__main__': + from quixote.demo import create_publisher + run(create_publisher) diff --git a/pypers/europython05/Quixote-2.0/server/fastcgi_server.py b/pypers/europython05/Quixote-2.0/server/fastcgi_server.py new file mode 100755 index 0000000..4ba7530 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/fastcgi_server.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/fastcgi_server.py $ +$Id: fastcgi_server.py 25476 2004-10-27 21:54:59Z nascheme $ + +Server for Quixote applications that use FastCGI. It should work +for CGI too but the cgi_server module is preferred as it is more +portable. +""" + +from quixote.server import _fcgi +from quixote.http_request import HTTPRequest + +def run(create_publisher): + publisher = create_publisher() + while _fcgi.isFCGI(): + f = _fcgi.FCGI() + request = HTTPRequest(f.inp, f.env) + response = publisher.process_request(request) + try: + response.write(f.out) + except IOError, err: + publisher.log("IOError while sending response ignored: %s" % err) + f.Finish() + + +if __name__ == '__main__': + from quixote.demo import create_publisher + run(create_publisher) diff --git a/pypers/europython05/Quixote-2.0/server/medusa_server.py b/pypers/europython05/Quixote-2.0/server/medusa_server.py new file mode 100755 index 0000000..239d95e --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/medusa_server.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/medusa_server.py $ +$Id: medusa_server.py 25579 2004-11-11 20:56:32Z nascheme $ + +An HTTP handler for Medusa that publishes a Quixote application. +""" + +import asyncore, rfc822, socket, urllib +from StringIO import StringIO +from medusa import http_server, xmlrpc_handler +import quixote +from quixote.http_request import HTTPRequest + + +class StreamProducer: + def __init__(self, chunks): + self.chunks = chunks # a generator + + def more(self): + try: + return self.chunks.next() + except StopIteration: + return '' + + +class QuixoteHandler: + def __init__(self, publisher, server): + self.publisher = publisher + self.server = server + + def match(self, request): + # Always match, since this is the only handler there is. + return True + + def handle_request(self, request): + msg = rfc822.Message(StringIO('\n'.join(request.header))) + length = int(msg.get('Content-Length', '0')) + if length: + request.collector = xmlrpc_handler.collector(self, request) + else: + self.continue_request('', request) + + def continue_request(self, data, request): + msg = rfc822.Message(StringIO('\n'.join(request.header))) + remote_addr, remote_port = request.channel.addr + if '#' in request.uri: + # MSIE is buggy and sometimes includes fragments in URLs + [request.uri, fragment] = request.uri.split('#', 1) + if '?' in request.uri: + [path, query_string] = request.uri.split('?', 1) + else: + path = request.uri + query_string = '' + + path = urllib.unquote(path) + server_port = str(self.server.port) + http_host = msg.get("Host") + if http_host: + if ":" in http_host: + server_name, server_port = http_host.split(":", 1) + else: + server_name = http_host + else: + server_name = (self.server.ip or + socket.gethostbyaddr(socket.gethostname())[0]) + + environ = {'REQUEST_METHOD': request.command, + 'ACCEPT_ENCODING': msg.get('Accept-encoding', ''), + 'CONTENT_TYPE': msg.get('Content-type', ''), + 'CONTENT_LENGTH': len(data), + "GATEWAY_INTERFACE": "CGI/1.1", + 'PATH_INFO': path, + 'QUERY_STRING': query_string, + 'REMOTE_ADDR': remote_addr, + 'REMOTE_PORT': str(remote_port), + 'REQUEST_URI': request.uri, + 'SCRIPT_NAME': '', + "SCRIPT_FILENAME": '', + 'SERVER_NAME': server_name, + 'SERVER_PORT': server_port, + 'SERVER_PROTOCOL': 'HTTP/1.1', + 'SERVER_SOFTWARE': 'Quixote/%s' % quixote.__version__, + } + for title, header in msg.items(): + envname = 'HTTP_' + title.replace('-', '_').upper() + environ[envname] = header + + stdin = StringIO(data) + qrequest = HTTPRequest(stdin, environ) + qresponse = self.publisher.process_request(qrequest) + + # Copy headers from Quixote's HTTP response + for name, value in qresponse.generate_headers(): + # XXX Medusa's HTTP request is buggy, and only allows unique + # headers. + request[name] = value + + request.response(qresponse.status_code) + request.push(StreamProducer(qresponse.generate_body_chunks())) + request.done() + + +def run(create_publisher, host='', port=80): + """Runs a Medusa HTTP server that publishes a Quixote + application. + """ + server = http_server.http_server(host, port) + publisher = create_publisher() + handler = QuixoteHandler(publisher, server) + server.install_handler(handler) + asyncore.loop() + + +if __name__ == '__main__': + from quixote.server.util import main + main(run) diff --git a/pypers/europython05/Quixote-2.0/server/mod_python_handler.py b/pypers/europython05/Quixote-2.0/server/mod_python_handler.py new file mode 100755 index 0000000..17f32ec --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/mod_python_handler.py @@ -0,0 +1,106 @@ +""" +$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/mod_python_handler.py $ +$Id: mod_python_handler.py 25893 2005-01-19 14:26:25Z dbinger $ + +This needs testing. + +mod_python configuration +------------------------ + +mod_python is an Apache module for embedding a Python interpreter into +the Apache server. To use mod_python as the interface layer between +Apache and Quixote, add something like this to your httpd.conf:: + + LoadModule python_module /usr/lib/apache/1.3/mod_python.so + <LocationMatch "^/qdemo(/|$)"> + SetHandler python-program + PythonHandler quixote.server.mod_python_handler + PythonOption quixote-publisher-factory quixote.demo.create_publisher + PythonInterpreter quixote.demo + PythonDebug On + </LocationMatch> + +This will attach URLs starting with ``/qdemo`` to the Quixote demo. +When you use mod_python, there's no need for rewrite rules (because of +the pattern in the ``LocationMatch`` directive), and no need for a +driver script. + +mod_python support was contributed to Quixote (1) by Erno Kuusela +<erno@iki.fi> and the Quixote 2 port comes from Clint. +""" + +import sys +from mod_python import apache +from quixote import enable_ptl +from quixote.publish import Publisher +from quixote.config import Config +from quixote.util import import_object + +class ErrorLog: + def __init__(self, publisher): + self.publisher = publisher + + def write(self, msg): + self.publisher.log(msg) + + def close(self): + pass + +class ModPythonPublisher(Publisher): + def __init__(self, package, **kwargs): + Publisher.__init__(self, package, **kwargs) + # may be overwritten + self.logger.error_log = self.__error_log = ErrorLog(self) + self.__apache_request = None + + def log(self, msg): + if self.logger.error_log is self.__error_log: + try: + self.__apache_request.log_error(msg) + except AttributeError: + apache.log_error(msg) + else: + Publisher.log(self, msg) + + def publish_modpython(self, req): + """publish_modpython() -> None + + Entry point from mod_python. + """ + self.__apache_request = req + try: + self.publish(apache.CGIStdin(req), + apache.CGIStdout(req), + sys.stderr, + apache.build_cgi_env(req)) + + return apache.OK + finally: + self.__apache_request = None + +name2publisher = {} + +def run(publisher, req): + from quixote.http_request import HTTPRequest + request = HTTPRequest(apache.CGIStdin(req), apache.build_cgi_env(req)) + response = publisher.process_request(request) + try: + response.write(apache.CGIStdout(req)) + except IOError, err: + publisher.log("IOError while sending response ignored: %s" % err) + return apache.OK + +def handler(req): + opts = req.get_options() + try: + factory = opts['quixote-publisher-factory'] + except KeyError: + apache.log_error('quixote-publisher-factory setting required') + return apache.HTTP_INTERNAL_SERVER_ERROR + pub = name2publisher.get(factory) + if pub is None: + factory_fcn = import_object(factory) + pub = factory_fcn() + name2publisher[factory] = pub + return run(pub, req) + diff --git a/pypers/europython05/Quixote-2.0/server/scgi_server.py b/pypers/europython05/Quixote-2.0/server/scgi_server.py new file mode 100755 index 0000000..1f99ede --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/scgi_server.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/scgi_server.py $ +$Id: scgi_server.py 25579 2004-11-11 20:56:32Z nascheme $ + +A SCGI server that uses Quixote to publish dynamic content. +""" + +from scgi import scgi_server +from quixote.http_request import HTTPRequest + +class QuixoteHandler(scgi_server.SCGIHandler): + def __init__(self, parent_fd, create_publisher, script_name=None): + scgi_server.SCGIHandler.__init__(self, parent_fd) + self.publisher = create_publisher() + self.script_name = script_name + + def handle_connection(self, conn): + input = conn.makefile("r") + output = conn.makefile("w") + env = self.read_env(input) + + if self.script_name is not None: + # mod_scgi doesn't know SCRIPT_NAME :-( + prefix = self.script_name + path = env['SCRIPT_NAME'] + assert path[:len(prefix)] == prefix, ( + "path %r doesn't start with script_name %r" % (path, prefix)) + env['SCRIPT_NAME'] = prefix + env['PATH_INFO'] = path[len(prefix):] + env.get('PATH_INFO', '') + + request = HTTPRequest(input, env) + response = self.publisher.process_request(request) + try: + response.write(output) + input.close() + output.close() + conn.close() + except IOError, err: + self.publisher.log("IOError while sending response " + "ignored: %s" % err) + + +def run(create_publisher, host='', port=3000, script_name=None, max_children=5): + def create_handler(parent_fd): + return QuixoteHandler(parent_fd, create_publisher, script_name) + s = scgi_server.SCGIServer(create_handler, host=host, port=port, + max_children=max_children) + s.serve() + + +def main(): + from optparse import OptionParser + from quixote.util import import_object + parser = OptionParser() + parser.set_description(run.__doc__) + default_host = 'localhost' + parser.add_option( + '--host', dest="host", default=default_host, type="string", + help="Host interface to listen on. (default=%s)" % default_host) + default_port = 3000 + parser.add_option( + '--port', dest="port", default=default_port, type="int", + help="Port to listen on. (default=%s)" % default_port) + default_maxchild = 5 + parser.add_option( + '--max-children', dest="maxchild", default=default_maxchild, + type="string", + help="Maximum number of children to spawn. (default=%s)" % + default_maxchild) + parser.add_option( + '--script-name', dest="script_name", default=None, type="string", + help="Value of SCRIPT_NAME (only needed if using mod_scgi)") + default_factory = 'quixote.demo.create_publisher' + parser.add_option( + '--factory', dest="factory", + default=default_factory, + help="Path to factory function to create the site Publisher. " + "(default=%s)" % default_factory) + (options, args) = parser.parse_args() + run(import_object(options.factory), host=options.host, port=options.port, + script_name=options.script_name, max_children=options.maxchild) + +if __name__ == '__main__': + main() diff --git a/pypers/europython05/Quixote-2.0/server/simple_server.py b/pypers/europython05/Quixote-2.0/server/simple_server.py new file mode 100755 index 0000000..08d5149 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/simple_server.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/simple_server.py $ +$Id: simple_server.py 26472 2005-04-05 12:40:24Z dbinger $ + +A simple, single threaded, synchronous HTTP server. +""" +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +import urllib +import quixote +from quixote import get_publisher +from quixote.http_request import HTTPRequest +from quixote.util import import_object + +class HTTPRequestHandler(BaseHTTPRequestHandler): + + required_cgi_environment = {} + + def get_cgi_env(self, method): + env = dict( + SERVER_SOFTWARE="Quixote/%s" % quixote.__version__, + SERVER_NAME=self.server.server_name, + GATEWAY_INTERFACE='CGI/1.1', + SERVER_PROTOCOL=self.protocol_version, + SERVER_PORT=str(self.server.server_port), + REQUEST_METHOD=method, + REMOTE_ADDR=self.client_address[0], + SCRIPT_NAME='') + if '?' in self.path: + env['PATH_INFO'], env['QUERY_STRING'] = self.path.split('?') + else: + env['PATH_INFO'] = self.path + env['PATH_INFO'] = urllib.unquote(env['PATH_INFO']) + if self.headers.typeheader is None: + env['CONTENT_TYPE'] = self.headers.type + else: + env['CONTENT_TYPE'] = self.headers.typeheader + env['CONTENT_LENGTH'] = self.headers.getheader('content-length') or "0" + for name, value in self.headers.items(): + header_name = 'HTTP_' + name.upper().replace('-', '_') + env[header_name] = value + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in "\t\n\r ": + accept.append(line.strip()) + else: + accept = accept + line[7:].split(',') + env['HTTP_ACCEPT'] = ','.join(accept) + co = filter(None, self.headers.getheaders('cookie')) + if co: + env['HTTP_COOKIE'] = ', '.join(co) + env.update(self.required_cgi_environment) + return env + + def process(self, env): + request = HTTPRequest(self.rfile, env) + response = get_publisher().process_request(request) + try: + self.send_response(response.get_status_code(), + response.get_reason_phrase()) + response.write(self.wfile, include_status=False) + except IOError, err: + print "IOError while sending response ignored: %s" % err + + def do_POST(self): + return self.process(self.get_cgi_env('POST')) + + def do_GET(self): + return self.process(self.get_cgi_env('GET')) + + +def run(create_publisher, host='', port=80, https=False): + """Runs a simple, single threaded, synchronous HTTP server that + publishes a Quixote application. + """ + if https: + HTTPRequestHandler.required_cgi_environment['HTTPS'] = 'on' + httpd = HTTPServer((host, port), HTTPRequestHandler) + publisher = create_publisher() + httpd.serve_forever() + + +if __name__ == '__main__': + from quixote.server.util import get_server_parser + parser = get_server_parser(run.__doc__) + parser.add_option( + '--https', dest="https", default=False, action="store_true", + help=("Force the scheme for all requests to be https. " + "Not that this is for running the simple server " + "through a proxy or tunnel that provides real SSL " + "support. The simple server itself does not. ")) + (options, args) = parser.parse_args() + run(import_object(options.factory), host=options.host, port=options.port, + https=options.https) diff --git a/pypers/europython05/Quixote-2.0/server/twisted_server.py b/pypers/europython05/Quixote-2.0/server/twisted_server.py new file mode 100755 index 0000000..911ec50 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/twisted_server.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/twisted_server.py $ +$Id: twisted_server.py 25711 2004-12-07 22:52:41Z nascheme $ + +An HTTP server for Twisted that publishes a Quixote application. +""" + +import urllib +from twisted.protocols import http +from twisted.web import server +from twisted.python import threadable +from twisted.internet import reactor +from quixote.http_request import HTTPRequest + + +class QuixoteFactory(http.HTTPFactory): + def __init__(self, publisher): + self.publisher = publisher + http.HTTPFactory.__init__(self, None) + + def buildProtocol(self, addr): + protocol = http.HTTPFactory.buildProtocol(self, addr) + protocol.requestFactory = QuixoteRequest + return protocol + + +class QuixoteRequest(server.Request): + def process(self): + environ = self.create_environment() + # this seek is important, it doesn't work without it (it doesn't + # matter for GETs, but POSTs will not work properly without it.) + self.content.seek(0, 0) + qxrequest = HTTPRequest(self.content, environ) + qxresponse = self.channel.factory.publisher.process_request(qxrequest) + self.setResponseCode(qxresponse.status_code) + for name, value in qxresponse.generate_headers(): + if name != 'Set-Cookie': + self.setHeader(name, value) + # Cookies get special treatment since it seems Twisted cannot handle + # multiple Set-Cookie headers. + for name, attrs in qxresponse.cookies.items(): + attrs = attrs.copy() + value = attrs.pop('value') + self.addCookie(name, value, **attrs) + QuixoteProducer(qxresponse, self) + + def create_environment(self): + """ + Borrowed heavily from twisted.web.twcgi + """ + # Twisted doesn't decode the path for us, so let's do it here. + if '%' in self.path: + self.path = urllib.unquote(self.path) + + serverName = self.getRequestHostname().split(':')[0] + env = {"SERVER_SOFTWARE": server.version, + "SERVER_NAME": serverName, + "GATEWAY_INTERFACE": "CGI/1.1", + "SERVER_PROTOCOL": self.clientproto, + "SERVER_PORT": str(self.getHost()[2]), + "REQUEST_METHOD": self.method, + "SCRIPT_NAME": '', + "SCRIPT_FILENAME": '', + "REQUEST_URI": self.uri, + "HTTPS": (self.isSecure() and 'on') or 'off', + 'SERVER_PROTOCOL': 'HTTP/1.1', + } + + for env_var, header in [('ACCEPT_ENCODING', 'Accept-encoding'), + ('CONTENT_TYPE', 'Content-type'), + ('HTTP_COOKIE', 'Cookie'), + ('HTTP_REFERER', 'Referer'), + ('HTTP_USER_AGENT', 'User-agent')]: + value = self.getHeader(header) + if value is not None: + env[env_var] = value + + client = self.getClient() + if client is not None: + env['REMOTE_HOST'] = client + ip = self.getClientIP() + if ip is not None: + env['REMOTE_ADDR'] = ip + _, _, remote_port = self.transport.getPeer() + env['REMOTE_PORT'] = remote_port + env["PATH_INFO"] = self.path + + qindex = self.uri.find('?') + if qindex != -1: + env['QUERY_STRING'] = self.uri[qindex+1:] + else: + env['QUERY_STRING'] = '' + + # Propogate HTTP headers + for title, header in self.getAllHeaders().items(): + envname = title.replace('-', '_').upper() + if title not in ('content-type', 'content-length'): + envname = "HTTP_" + envname + env[envname] = header + + return env + + +class QuixoteProducer: + """ + Produce the Quixote response for twisted. + """ + def __init__(self, qxresponse, request): + self.request = request + self.size = qxresponse.get_content_length() + self.stream = qxresponse.generate_body_chunks() + request.registerProducer(self, 0) + + def resumeProducing(self): + if self.request: + try: + chunk = self.stream.next() + except StopIteration: + self.request.unregisterProducer() + self.request.finish() + self.request = None + else: + self.request.write(chunk) + + def pauseProducing(self): + pass + + def stopProducing(self): + self.request = None + + synchronized = ['resumeProducing', 'stopProducing'] + +threadable.synchronize(QuixoteProducer) + + +def run(create_publisher, host='', port=80): + """Runs a Twisted HTTP server server that publishes a Quixote + application.""" + publisher = create_publisher() + factory = QuixoteFactory(publisher) + reactor.listenTCP(port, factory, interface=host) + reactor.run() + + +if __name__ == '__main__': + from quixote.server.util import main + main(run) diff --git a/pypers/europython05/Quixote-2.0/server/util.py b/pypers/europython05/Quixote-2.0/server/util.py new file mode 100755 index 0000000..69ed675 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/server/util.py @@ -0,0 +1,32 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/server/util.py $ +$Id: util.py 26427 2005-03-30 18:03:32Z dbinger $ + +Miscellaneous utility functions shared by servers. +""" + +from optparse import OptionParser +from quixote.util import import_object + +def get_server_parser(doc): + parser = OptionParser() + parser.set_description(doc) + default_host = 'localhost' + parser.add_option( + '--host', dest="host", default=default_host, type="string", + help="Host interface to listen on. (default=%s)" % default_host) + default_port = 8080 + parser.add_option( + '--port', dest="port", default=default_port, type="int", + help="Port to listen on. (default=%s)" % default_port) + default_factory = 'quixote.demo.create_publisher' + parser.add_option( + '--factory', dest="factory", + default=default_factory, + help="Path to factory function to create the site Publisher. " + "(default=%s)" % default_factory) + return parser + +def main(run): + parser = get_server_parser(run.__doc__) + (options, args) = parser.parse_args() + run(import_object(options.factory), host=options.host, port=options.port) diff --git a/pypers/europython05/Quixote-2.0/session.py b/pypers/europython05/Quixote-2.0/session.py new file mode 100755 index 0000000..0241b7f --- /dev/null +++ b/pypers/europython05/Quixote-2.0/session.py @@ -0,0 +1,567 @@ +"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/session.py $ +$Id: session.py 26524 2005-04-08 10:22:34Z dbinger $ + +Quixote session management. There are two levels to Quixote's +session management system: + - SessionManager + - Session + +A SessionManager is responsible for creating sessions, setting and reading +session cookies, maintaining the collection of all sessions, and so forth. +There is one SessionManager instance per Quixote process. + +A Session is the umbrella object for a single session (notionally, a (user, +host, browser_process) triple). Simple applications can probably get away +with putting all session data into a Session object (or, better, into an +application-specific subclass of Session). + +The default implementation provided here is not persistent: when the +Quixote process shuts down, all session data is lost. See +doc/session-mgmt.txt for information on session persistence. +""" + +from time import time, localtime, strftime + +from quixote import get_publisher, get_cookie, get_response, get_request, \ + get_session +from quixote.util import randbytes + +class NullSessionManager: + """A session manager that does nothing. It is the default session manager. + """ + + def start_request(self): + """ + Called near the beginning of each request: after the HTTPRequest + object has been built, but before we traverse the URL or call the + callable object found by URL traversal. + """ + + def finish_successful_request(self): + """Called near the end of each successful request. Not called if + there were any errors processing the request. + """ + + def finish_failed_request(self): + """Called near the end of a failed request (i.e. a exception that was + not a PublisherError was raised. + """ + + +class SessionManager: + """ + SessionManager acts as a dictionary of all sessions, mapping session + ID strings to individual session objects. Session objects are + instances of Session (or a custom subclass for your application). + SessionManager is also responsible for creating and destroying + sessions, for generating and interpreting session cookies, and for + session persistence (if any -- this implementation is not + persistent). + + Most applications can just use this class directly; sessions will + be kept in memory-based dictionaries, and will be lost when the + Quixote process dies. Alternatively an application can subclass + SessionManager to implement specific behaviour, such as persistence. + + Instance attributes: + session_class : class + the class that is instantiated to create new session objects + (in new_session()) + sessions : mapping { session_id:string : Session } + the collection of sessions managed by this SessionManager + """ + + ACCESS_TIME_RESOLUTION = 1 # in seconds + + + def __init__(self, session_class=None, session_mapping=None): + """(session_class : class = Session, session_mapping : mapping = None) + + Create a new session manager. There should be one session + manager per publisher, ie. one per process + + session_class is used by the new_session() method -- it returns + an instance of session_class. + """ + self.sessions = {} + if session_class is None: + self.session_class = Session + else: + self.session_class = session_class + if session_mapping is None: + self.sessions = {} + else: + self.sessions = session_mapping + + def __repr__(self): + return "<%s at %x>" % (self.__class__.__name__, id(self)) + + + # -- Mapping interface --------------------------------------------- + # (subclasses shouldn't need to override any of this, unless + # your application passes in a session_mapping object that + # doesn't provide all of the mapping methods needed here) + + def keys(self): + """() -> [string] + + Return the list of session IDs of sessions in this session manager. + """ + return self.sessions.keys() + + def sorted_keys(self): + """() -> [string] + + Return the same list as keys(), but sorted. + """ + keys = self.keys() + keys.sort() + return keys + + def values(self): + """() -> [Session] + + Return the list of sessions in this session manager. + """ + return self.sessions.values() + + def items(self): + """() -> [(string, Session)] + + Return the list of (session_id, session) pairs in this session + manager. + """ + return self.sessions.items() + + def get(self, session_id, default=None): + """(session_id : string, default : any = None) -> Session + + Return the session object identified by 'session_id', or None if + no such session. + """ + return self.sessions.get(session_id, default) + + def __getitem__(self, session_id): + """(session_id : string) -> Session + + Return the session object identified by 'session_id'. Raise KeyError + if no such session. + """ + return self.sessions[session_id] + + def has_key(self, session_id): + """(session_id : string) -> boolean + + Return true if a session identified by 'session_id' exists in + the session manager. + """ + return self.sessions.has_key(session_id) + + # has_session() is a synonym for has_key() -- if you override + # has_key(), be sure to repeat this alias! + has_session = has_key + + def __setitem__(self, session_id, session): + """(session_id : string, session : Session) + + Store 'session' in the session manager under 'session_id'. + """ + if not isinstance(session, self.session_class): + raise TypeError("session not an instance of %r: %r" + % (self.session_class, session)) + assert session.id is not None, "session ID not set" + assert session_id == session.id, "session ID mismatch" + self.sessions[session_id] = session + + def __delitem__(self, session_id): + """(session_id : string) -> Session + + Remove the session object identified by 'session_id' from the session + manager. Raise KeyError if no such session. + """ + del self.sessions[session_id] + + # -- Transactional interface --------------------------------------- + # Useful for applications that provide a transaction-oriented + # persistence mechanism. You'll still need to provide a mapping + # object that works with your persistence mechanism; these two + # methods let you hook into your transaction machinery after a + # request is finished processing. + + def abort_changes(self, session): + """(session : Session) + + Placeholder for subclasses that implement transactional + persistence: forget about saving changes to the current + session. Called by the publisher when a request fails, + ie. when it catches an exception other than PublishError. + """ + pass + + def commit_changes(self, session): + """(session : Session) + + Placeholder for subclasses that implement transactional + persistence: commit changes to the current session. Called by + the publisher when a request completes successfully, or is + interrupted by a PublishError exception. + """ + pass + + + # -- Session management -------------------------------------------- + # these build on the storage mechanism implemented by the + # above mapping methods, and are concerned with all the high- + # level details of managing web sessions + + def new_session(self, id): + """(id : string) -> Session + + Return a new session object, ie. an instance of the session_class + class passed to the constructor (defaults to Session). + """ + return self.session_class(id) + + def _get_session_id(self, config): + """() -> string + + Find the ID of the current session by looking for the session + cookie in the request. Return None if no such cookie or the + cookie has been expired, otherwise return the cookie's value. + """ + id = get_cookie(config.session_cookie_name) + if id == "" or id == "*del*": + return None + else: + return id + + def _make_session_id(self): + # Generate a session ID, which is just the value of the session + # cookie we are about to drop on the user. (It's also the key + # used with the session manager mapping interface.) + id = None + while id is None or self.has_session(id): + id = randbytes(8) # 64-bit random number + return id + + def _create_session(self): + # Create a new session object, with no ID for now - one will + # be assigned later if we save the session. + return self.new_session(None) + + def get_session(self): + """() -> Session + + Fetch or create a session object for the current session, and + return it. If a session cookie is found in the HTTP request + object, use it to look up and return an existing session object. + If no session cookie is found, create a new session. + + Note that this method does *not* cause the new session to be + stored in the session manager, nor does it drop a session cookie + on the user. Those are both the responsibility of + maintain_session(), called at the end of a request. + """ + config = get_publisher().config + id = self._get_session_id(config) + session = self.get(id) or self._create_session() + session._set_access_time(self.ACCESS_TIME_RESOLUTION) + return session + + def maintain_session(self, session): + """(session : Session) + + Maintain session information. This method is called after servicing + an HTTP request, just before the response is returned. If a session + contains information it is saved and a cookie dropped on the client. + If not, the session is discarded and the client will be instructed + to delete the session cookie (if any). + """ + if not session.has_info(): + # Session has no useful info -- forget it. If it previously + # had useful information and no longer does, we have to + # explicitly forget it. + if session.id and self.has_session(session.id): + del self[session.id] + self.revoke_session_cookie() + return + + if session.id is None: + # This is the first time this session has had useful + # info -- store it and set the session cookie. + session.id = self._make_session_id() + self[session.id] = session + self.set_session_cookie(session.id) + + elif session.is_dirty(): + # We have already stored this session, but it's dirty + # and needs to be stored again. This will never happen + # with the default Session class, but it's there for + # applications using a persistence mechanism that requires + # repeatedly storing the same object in the same mapping. + self[session.id] = session + + def _set_cookie(self, value, **attrs): + config = get_publisher().config + name = config.session_cookie_name + if config.session_cookie_path: + path = config.session_cookie_path + else: + path = get_request().get_environ('SCRIPT_NAME') + if not path.endswith("/"): + path += "/" + domain = config.session_cookie_domain + get_response().set_cookie(name, value, domain=domain, + path=path, **attrs) + return name + + def set_session_cookie(self, session_id): + """(session_id : string) + + Ensure that a session cookie with value 'session_id' will be + returned to the client via the response object. + """ + self._set_cookie(session_id) + + def revoke_session_cookie(self): + """ + Remove the session cookie from the remote user's session by + resetting the value and maximum age in the response object. Also + remove the cookie from the request so that further processing of + this request does not see the cookie's revoked value. + """ + cookie_name = self._set_cookie("", max_age=0) + if get_cookie(cookie_name) is not None: + del get_request().cookies[cookie_name] + + def expire_session(self): + """ + Expire the current session, ie. revoke the session cookie from + the client and remove the session object from the session + manager and from the current request. + """ + self.revoke_session_cookie() + request = get_request() + try: + del self[request.session.id] + except KeyError: + # This can happen if the current session hasn't been saved + # yet, eg. if someone tries to leave a session with no + # interesting data. That's not a big deal, so ignore it. + pass + request.session = None + + def has_session_cookie(self, must_exist=False): + """(must_exist : boolean = false) -> bool + + Return true if the request already has a cookie identifying a + session object. If 'must_exist' is true, the cookie must + correspond to a currently existing session; otherwise (the + default), we just check for the existence of the session cookie + and don't inspect its content at all. + """ + config = get_publisher().config + id = get_cookie(config.session_cookie_name) + if id is None: + return False + if must_exist: + return self.has_session(id) + else: + return True + + # -- Hooks into the Quixote main loop ------------------------------ + + def start_request(self): + """ + Called near the beginning of each request: after the HTTPRequest + object has been built, but before we traverse the URL or call the + callable object found by URL traversal. + """ + session = self.get_session() + get_request().session = session + session.start_request() + + def finish_successful_request(self): + """Called near the end of each successful request. Not called if + there were any errors processing the request. + """ + session = get_session() + if session is not None: + self.maintain_session(session) + self.commit_changes(session) + + def finish_failed_request(self): + """Called near the end of a failed request (i.e. a exception that was + not a PublisherError was raised. + """ + self.abort_changes(get_session()) + + +class Session: + """ + Holds information about the current session. The only information + that is likely to be useful to applications is the 'user' attribute, + which applications can use as they please. + + Instance attributes: + id : string + the session ID (generated by SessionManager and used as the + value of the session cookie) + user : any + an object to identify the human being on the other end of the + line. It's up to you whether to store just a string in 'user', + or some more complex data structure or object. + _remote_address : string + IP address of user owning this session (only set when the + session is created) + _creation_time : float + _access_time : float + two ways of keeping track of the "age" of the session. + Note that '__access_time' is maintained by the SessionManager that + owns this session, using _set_access_time(). + _form_tokens : [string] + outstanding form tokens. This is used as a queue that can grow + up to MAX_FORM_TOKENS. Tokens are removed when forms are submitted. + + Feel free to access 'id' and 'user' directly, but do not modify + 'id'. The preferred way to set 'user' is with the set_user() method + (which you might want to override for type-checking). + """ + + MAX_FORM_TOKENS = 16 # maximum number of outstanding form tokens + + def __init__(self, id): + self.id = id + self.user = None + self._remote_address = get_request().get_environ("REMOTE_ADDR") + self._creation_time = self._access_time = time() + self._form_tokens = [] # queue + + def __repr__(self): + return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self.id) + + def __str__(self): + if self.user: + return "session %s (user %s)" % (self.id, self.user) + else: + return "session %s (no user)" % self.id + + def has_info(self): + """() -> boolean + + Return true if this session contains any information that must + be saved. + """ + return self.user or self._form_tokens + + def is_dirty(self): + """() -> boolean + + Return true if this session has changed since it was last saved + such that it needs to be saved again. + + Default implementation always returns false since the default + storage mechanism is an in-memory dictionary, and you don't have + to put the same object into the same slot of a dictionary twice. + If sessions are stored to, eg., files in a directory or slots in + a hash file, is_dirty() should probably be an alias or wrapper + for has_info(). See doc/session-mgmt.txt. + """ + return False + + def dump(self, file=None, header=True, deep=True): + time_fmt = "%Y-%m-%d %H:%M:%S" + ctime = strftime(time_fmt, localtime(self._creation_time)) + atime = strftime(time_fmt, localtime(self._access_time)) + + if header: + file.write('session %s:' % self.id) + file.write(' user %s' % self.user) + file.write(' _remote_address: %s' % self._remote_address) + file.write(' created %s, last accessed %s' % (ctime, atime)) + file.write(' _form_tokens: %s\n' % self._form_tokens) + + def start_request(self): + """ + Called near the beginning of each request: after the HTTPRequest + object has been built, but before we traverse the URL or call the + callable object found by URL traversal. + """ + if self.user is not None: + get_request().environ['REMOTE_USER'] = str(self.user) + + # -- Simple accessors and modifiers -------------------------------- + + def set_user(self, user): + self.user = user + + def get_user(self): + return self.user + + def get_remote_address(self): + """Return the IP address (dotted-quad string) that made the + initial request in this session. + """ + return self._remote_address + + def get_creation_time(self): + """Return the time that this session was created (seconds + since epoch). + """ + return self._creation_time + + def get_access_time(self): + """Return the time that this session was last accessed (seconds + since epoch). + """ + return self._access_time + + def get_creation_age(self, _now=None): + """Return the number of seconds since session was created.""" + # _now arg is not strictly necessary, but there for consistency + # with get_access_age() + return (_now or time()) - self._creation_time + + def get_access_age(self, _now=None): + """Return the number of seconds since session was last accessed.""" + # _now arg is for SessionManager's use + return (_now or time()) - self._access_time + + + # -- Methods for SessionManager only ------------------------------- + + def _set_access_time(self, resolution): + now = time() + if now - self._access_time > resolution: + self._access_time = now + + + # -- Form token methods -------------------------------------------- + + def create_form_token(self): + """() -> string + + Create a new form token and add it to a queue of outstanding form + tokens for this session. A maximum of MAX_FORM_TOKENS are saved. + The new token is returned. + """ + token = randbytes(8) + self._form_tokens.append(token) + extra = len(self._form_tokens) - self.MAX_FORM_TOKENS + if extra > 0: + del self._form_tokens[:extra] + return token + + def has_form_token(self, token): + """(token : string) -> boolean + + Return true if 'token' is in the queue of outstanding tokens. + """ + return token in self._form_tokens + + def remove_form_token(self, token): + """(token : string) + + Remove 'token' from the queue of outstanding tokens. + """ + self._form_tokens.remove(token) diff --git a/pypers/europython05/Quixote-2.0/setup.py b/pypers/europython05/Quixote-2.0/setup.py new file mode 100755 index 0000000..7498982 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/setup.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +#$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/setup.py $ +#$Id: setup.py 26539 2005-04-11 15:47:33Z dbinger $ + +# Setup script for Quixote + +__revision__ = "$Id: setup.py 26539 2005-04-11 15:47:33Z dbinger $" + +import sys, os +from distutils import core +from distutils.extension import Extension +from ptl.qx_distutils import qx_build_py + +# a fast htmltext type +htmltext = Extension(name="quixote.html._c_htmltext", + sources=["html/_c_htmltext.c"]) + +# faster import hook for PTL modules +cimport = Extension(name="quixote.ptl.cimport", + sources=["ptl/cimport.c"]) + +kw = {'name': "Quixote", + 'version': "2.0", + 'description': "A highly Pythonic Web application framework", + 'author': "MEMS Exchange", + 'author_email': "quixote@mems-exchange.org", + 'url': "http://www.mems-exchange.org/software/quixote/", + 'license': "CNRI Open Source License (see LICENSE.txt)", + + 'package_dir': {'quixote':os.curdir}, + 'packages': ['quixote', 'quixote.demo', 'quixote.form', + 'quixote.html', 'quixote.ptl', + 'quixote.server'], + + 'ext_modules': [], + + 'cmdclass': {'build_py': qx_build_py}, + } + + +build_extensions = sys.platform != 'win32' + +if build_extensions: + # The _c_htmltext module requires Python 2.2 features. + if sys.hexversion >= 0x20200a1: + kw['ext_modules'].append(htmltext) + kw['ext_modules'].append(cimport) + +# If we're running Python 2.3, add extra information +if hasattr(core, 'setup_keywords'): + if 'classifiers' in core.setup_keywords: + kw['classifiers'] = ['Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'License :: OSI Approved :: Python License (CNRI Python License)', + 'Intended Audience :: Developers', + 'Operating System :: Unix', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + ] + if 'download_url' in core.setup_keywords: + kw['download_url'] = ('http://www.mems-exchange.org/software/files' + '/quixote/Quixote-%s.tar.gz' % kw['version']) + +core.setup(**kw) diff --git a/pypers/europython05/Quixote-2.0/test/__init__.py b/pypers/europython05/Quixote-2.0/test/__init__.py new file mode 100755 index 0000000..bcc196b --- /dev/null +++ b/pypers/europython05/Quixote-2.0/test/__init__.py @@ -0,0 +1,2 @@ + +# Empty file to make this directory a package diff --git a/pypers/europython05/Quixote-2.0/test/ua_test.py b/pypers/europython05/Quixote-2.0/test/ua_test.py new file mode 100755 index 0000000..d1de207 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/test/ua_test.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# Test Quixote's ability to parse the "User-Agent" header, ie. +# the 'guess_browser_version()' method of HTTPRequest. +# +# Reads User-Agent strings on stdin, and writes Quixote's interpretation +# of each on stdout. This is *not* an automated test! + +import sys, os +from copy import copy +from quixote.http_request import HTTPRequest + +env = copy(os.environ) +file = sys.stdin +while 1: + line = file.readline() + if not line: + break + if line[-1] == "\n": + line = line[:-1] + + env["HTTP_USER_AGENT"] = line + req = HTTPRequest(None, env) + (name, version) = req.guess_browser_version() + if name is None: + print "%s -> ???" % line + else: + print "%s -> (%s, %s)" % (line, name, version) diff --git a/pypers/europython05/Quixote-2.0/test/utest_request.py b/pypers/europython05/Quixote-2.0/test/utest_request.py new file mode 100755 index 0000000..ba3f053 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/test/utest_request.py @@ -0,0 +1,43 @@ +from sancho.utest import UTest +from quixote.http_request import parse_cookies + + +class ParseCookiesTest (UTest): + + def check_basic(self): + assert parse_cookies('a') == {'a': ''} + assert parse_cookies('a = ') == {'a': ''} + assert parse_cookies('a = ""') == {'a': ''} + assert parse_cookies(r'a = "\""') == {'a': '"'} + assert parse_cookies('a, b; c') == {'a': '', 'b': '', 'c': ''} + assert parse_cookies('a, b=1') == {'a': '', 'b': '1'} + assert parse_cookies('a = ";, \t";') == {'a': ';, \t'} + + def check_rfc2109_example(self): + s = ('$Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme"; ' + 'Part_Number="Rocket_Launcher_0001"; $Path="/acme"') + result = {'Customer': 'WILE_E_COYOTE', + 'Part_Number': 'Rocket_Launcher_0001', + } + assert parse_cookies(s) == result + + def check_other(self): + s = 'PREF=ID=0a06b1:TM=108:LM=1069:C2COFF=1:S=ETXrcU' + result = {'PREF': 'ID=0a06b1:TM=108:LM=1069:C2COFF=1:S=ETXrcU'} + assert parse_cookies(s) == result + s = 'pageColor=White; pageWidth=990; fontSize=12; fontFace=1; E=E' + assert parse_cookies(s) == {'pageColor': 'White', + 'pageWidth': '990', + 'fontSize': '12', + 'fontFace': '1', + 'E': 'E'} + s = 'userid="joe"; QX_session="58a3ced39dcd0d"' + assert parse_cookies(s) == {'userid': 'joe', + 'QX_session': '58a3ced39dcd0d'} + + def check_invalid(self): + parse_cookies('a="123') + parse_cookies('a=123"') + +if __name__ == "__main__": + ParseCookiesTest() diff --git a/pypers/europython05/Quixote-2.0/util.py b/pypers/europython05/Quixote-2.0/util.py new file mode 100755 index 0000000..1835be3 --- /dev/null +++ b/pypers/europython05/Quixote-2.0/util.py @@ -0,0 +1,390 @@ +"""quixote.util +$HeadURL: svn+ssh://svn.mems-exchange.org/repos/trunk/quixote/util.py $ +$Id: util.py 26523 2005-04-08 10:20:19Z dbinger $ + +Contains various useful functions and classes: + + xmlrpc(request, func) : Processes the body of an XML-RPC request, and calls + 'func' with the method name and parameters. + StaticFile : Wraps a file from a filesystem as a + Quixote resource. + StaticDirectory : Wraps a directory containing static files as + a Quixote directory. + +StaticFile and StaticDirectory were contributed by Hamish Lawson. +See doc/static-files.txt for examples of their use. +""" + +import sys +import os +import time +import binascii +import mimetypes +import urllib +import xmlrpclib +from rfc822 import formatdate +import quixote +from quixote import errors +from quixote.directory import Directory +from quixote.html import htmltext, TemplateIO +from quixote.http_response import Stream + +if hasattr(os, 'urandom'): + # available in Python 2.4 and also works on win32 + def randbytes(bytes): + """Return bits of random data as a hex string.""" + return binascii.hexlify(os.urandom(bytes)) + +elif os.path.exists('/dev/urandom'): + # /dev/urandom is just as good as /dev/random for cookies (assuming + # SHA-1 is secure) and it never blocks. + def randbytes(bytes): + """Return bits of random data as a hex string.""" + return binascii.hexlify(open("/dev/urandom").read(bytes)) + +else: + # this is much less secure than the above function + import sha + class _PRNG: + def __init__(self): + self.state = sha.new(str(time.time() + time.clock())) + self.count = 0 + + def _get_bytes(self): + self.state.update('%s %d' % (time.time() + time.clock(), + self.count)) + self.count += 1 + return self.state.hexdigest() + + def randbytes(self, bytes): + """Return bits of random data as a hex string.""" + s = "" + chars = 2*bytes + while len(s) < chars: + s += self._get_bytes() + return s[:chars] + + randbytes = _PRNG().randbytes + + +def import_object(name): + i = name.rfind('.') + if i != -1: + module_name = name[:i] + object_name = name[i+1:] + __import__(module_name) + return getattr(sys.modules[module_name], object_name) + else: + __import__(name) + return sys.modules[name] + +def xmlrpc(request, func): + """xmlrpc(request:Request, func:callable) : string + + Processes the body of an XML-RPC request, and calls 'func' with + two arguments, a string containing the method name and a tuple of + parameters. + """ + + # Get contents of POST body + if request.get_method() != 'POST': + request.response.set_status(405, "Only the POST method is accepted") + return "XML-RPC handlers only accept the POST method." + + length = int(request.environ['CONTENT_LENGTH']) + data = request.stdin.read(length) + + # Parse arguments + params, method = xmlrpclib.loads(data) + + try: + result = func(method, params) + except xmlrpclib.Fault, exc: + result = exc + except: + # report exception back to client + result = xmlrpclib.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) + ) + else: + result = (result,) + result = xmlrpclib.dumps(result, methodresponse=1) + + request.response.set_content_type('text/xml') + return result + + +class FileStream(Stream): + + CHUNK_SIZE = 20000 + + def __init__(self, fp, size=None): + self.fp = fp + self.length = size + + def __iter__(self): + return self + + def next(self): + chunk = self.fp.read(self.CHUNK_SIZE) + if not chunk: + raise StopIteration + return chunk + + +class StaticFile: + + """ + Wrapper for a static file on the filesystem. + """ + + def __init__(self, path, follow_symlinks=False, + mime_type=None, encoding=None, cache_time=None): + """StaticFile(path:string, follow_symlinks:bool) + + Initialize instance with the absolute path to the file. If + 'follow_symlinks' is true, symbolic links will be followed. + 'mime_type' specifies the MIME type, and 'encoding' the + encoding; if omitted, the MIME type will be guessed, + defaulting to text/plain. + + Optional cache_time parameter indicates the number of + seconds a response is considered to be valid, and will + be used to set the Expires header in the response when + quixote gets to that part. If the value is None then + the Expires header will not be set. + """ + + # Check that the supplied path is absolute and (if a symbolic link) may + # be followed + self.path = path + if not os.path.isabs(path): + raise ValueError, "Path %r is not absolute" % path + # Decide the Content-Type of the file + guess_mime, guess_enc = mimetypes.guess_type(os.path.basename(path), + strict=False) + self.mime_type = mime_type or guess_mime or 'text/plain' + self.encoding = encoding or guess_enc or None + self.cache_time = cache_time + self.follow_symlinks = follow_symlinks + + def __call__(self): + if not self.follow_symlinks and os.path.islink(self.path): + raise errors.TraversalError(private_msg="Path %r is a symlink" + % self.path) + request = quixote.get_request() + response = quixote.get_response() + + if self.cache_time is None: + response.set_expires(None) # don't set the Expires header + else: + # explicitly allow client to cache page by setting the Expires + # header, this is even more efficient than the using + # Last-Modified/If-Modified-Since since the browser does not need + # to contact the server + response.set_expires(seconds=self.cache_time) + + stat = os.stat(self.path) + last_modified = formatdate(stat.st_mtime) + if last_modified == request.get_header('If-Modified-Since'): + # handle exact match of If-Modified-Since header + response.set_status(304) + return '' + + # Set the Content-Type for the response and return the file's contents. + response.set_content_type(self.mime_type) + if self.encoding: + response.set_header("Content-Encoding", self.encoding) + + response.set_header('Last-Modified', last_modified) + + return FileStream(open(self.path, 'rb'), stat.st_size) + + +class StaticDirectory(Directory): + + """ + Wrap a filesystem directory containing static files as a Quixote directory. + """ + + _q_exports = [''] + + FILE_CLASS = StaticFile + + def __init__(self, path, use_cache=False, list_directory=False, + follow_symlinks=False, cache_time=None, file_class=None, + index_filenames=None): + """(path:string, use_cache:bool, list_directory:bool, + follow_symlinks:bool, cache_time:int, + file_class=None, index_filenames:[string]) + + Initialize instance with the absolute path to the file. + If 'use_cache' is true, StaticFile instances will be cached in memory. + If 'list_directory' is true, users can request a directory listing. + If 'follow_symlinks' is true, symbolic links will be followed. + + Optional parameter cache_time allows setting of Expires header in + response object (see note for StaticFile for more detail). + + Optional parameter 'index_filenames' specifies a list of + filenames to be used as index files in the directory. First + file found searching left to right is returned. + """ + + # Check that the supplied path is absolute + self.path = path + if not os.path.isabs(path): + raise ValueError, "Path %r is not absolute" % path + + self.use_cache = use_cache + self.cache = {} + self.list_directory = list_directory + self.follow_symlinks = follow_symlinks + self.cache_time = cache_time + if file_class is not None: + self.file_class = file_class + else: + self.file_class = self.FILE_CLASS + self.index_filenames = index_filenames + + def _render_header(self, title): + r = TemplateIO(html=True) + r += htmltext('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 ' + 'Transitional//EN" ' + '"http://www.w3.org/TR/REC-html40/loose.dtd">') + r += htmltext('<html>') + r += htmltext('<head><title>%s</title></head>') % title + r += htmltext('<body>') + r += htmltext("<h1>%s</h1>") % title + return r.getvalue() + + def _render_footer(self): + return htmltext('</body></html>') + + def _q_index(self): + """ + If directory listings are allowed, generate a simple HTML + listing of the directory's contents with each item hyperlinked; + if the item is a subdirectory, place a '/' after it. If not allowed, + return a page to that effect. + """ + if self.index_filenames: + for name in self.index_filenames: + try: + obj = self._q_lookup(name) + except errors.TraversalError: + continue + if not isinstance(obj, StaticDirectory) and callable(obj): + return obj() + r = TemplateIO(html=True) + if self.list_directory: + r += self._render_header('Index of %s' % quixote.get_path()) + template = htmltext('<a href="%s">%s</a>%s\n') + r += htmltext('<pre>') + r += template % ('..', '..', '') + files = os.listdir(self.path) + files.sort() + for filename in files: + filepath = os.path.join(self.path, filename) + marker = os.path.isdir(filepath) and "/" or "" + r += template % (urllib.quote(filename), filename, marker) + r += htmltext('</pre>') + r += self._render_footer() + else: + r += self._render_header('Directory listing denied') + r += htmltext('<p>This directory does not allow its contents ' + 'to be listed.</p>') + r += self._render_footer() + return r.getvalue() + + def _q_lookup(self, name): + """ + Get a file from the filesystem directory and return the StaticFile + or StaticDirectory wrapper of it; use caching if that is in use. + """ + if name in ('.', '..'): + raise errors.TraversalError(private_msg="Attempt to use '.', '..'") + if self.cache.has_key(name): + # Get item from cache + item = self.cache[name] + else: + # Get item from filesystem; cache it if caching is in use. + item_filepath = os.path.join(self.path, name) + while os.path.islink(item_filepath): + if not self.follow_symlinks: + raise errors.TraversalError + else: + dest = os.readlink(item_filepath) + item_filepath = os.path.join(self.path, dest) + + if os.path.isdir(item_filepath): + item = self.__class__(item_filepath, self.use_cache, + self.list_directory, + self.follow_symlinks, self.cache_time, + self.file_class, self.index_filenames) + + elif os.path.isfile(item_filepath): + item = self.file_class(item_filepath, self.follow_symlinks, + cache_time=self.cache_time) + else: + raise errors.TraversalError + if self.use_cache: + self.cache[name] = item + return item + + +class Redirector: + """ + A simple class that can be used from inside _q_lookup() to redirect + requests. + """ + + _q_exports = [] + + def __init__(self, location, permanent=False): + self.location = location + self.permanent = permanent + + def _q_lookup(self, component): + return self + + def __call__(self): + return quixote.redirect(self.location, self.permanent) + + +def dump_request(request=None): + if request is None: + request = quixote.get_request() + """Dump an HTTPRequest object as HTML.""" + row_fmt = htmltext('<tr><th>%s</th><td>%s</td></tr>') + r = TemplateIO(html=True) + r += htmltext('<h3>form</h3>' + '<table>') + for k, v in request.form.items(): + r += row_fmt % (k, v) + r += htmltext('</table>' + '<h3>cookies</h3>' + '<table>') + for k, v in request.cookies.items(): + r += row_fmt % (k, v) + r += htmltext('</table>' + '<h3>environ</h3>' + '<table>') + for k, v in request.environ.items(): + r += row_fmt % (k, v) + r += htmltext('</table>') + return r.getvalue() + +def get_directory_path(): + """() -> [object] + Return the list of traversed instances. + """ + path = [] + frame = sys._getframe() + while frame: + if frame.f_code.co_name == '_q_traverse': + self = frame.f_locals.get('self', None) + if path[:1] != [self]: + path.insert(0, self) + frame = frame.f_back + return path |