Commit 33e04236 authored by Jim Fulton's avatar Jim Fulton

Many changes, including:

  - Butter realm management
  - Automatic type conversion
  - Improved documentation
  - ...
parent 04e63f3f
...@@ -2,14 +2,38 @@ ...@@ -2,14 +2,38 @@
# $What$ # $What$
__doc__="""\ __doc__="""\
Publish python objects on web servers using CGI Python Object Publisher -- Publish Python objects on web servers
Introduction Introduction
The python object publisher provides a simple mechanism for publishing a The Python object publisher provides a simple mechanism for publishing a
collection of python objects as World-Wide-Web (Web) resources without any collection of Python objects as World-Wide-Web (Web) resources without any
plumbing (e.g. CGI) specific code. plumbing (e.g. CGI) specific code.
Benefits
- Applications do not have to include code for interfacing with the
web server.
- Applications can be moved from one publishing mechanism, such as
CGI, to another mechanism, such as Fast CGI or ILU Requestor, with
no change.
- Python objects are published as Python objects. The web server
"calls" the objects in much the same way that other Python objects
would.
- Automatic conversion of URL to object/sub-object traversal.
- Automatic marshaling of form data, cookie data, and request
meta-data to Python function arguments.
- Automated exception handling.
- Automatic generation of CGI headers.
- Automated authentication and authorization.
Published objects Published objects
Objects are published by including them in a published module. Objects are published by including them in a published module.
...@@ -17,16 +41,21 @@ Published objects ...@@ -17,16 +41,21 @@ Published objects
- can be found in the module's global name space, - can be found in the module's global name space,
- That do not have names starting with an underscore, - that do not have names starting with an underscore,
- that have non-empty doc strings, and - that have non-empty documentation strings, and
- that are not modules - that are not modules
are published Alternatively, a module variable, named are published.
'web_objects' can be defined. If this variable is defined, it should
be bound to a mapping object that maps published names to published Alternatively, a module variable, named 'web_objects' can be
objects. defined. If this variable is defined, it should be bound to a
mapping object that maps published names to published objects.
Objects that are published through a module's 'web_objects' are not
subject to the restrictions listed above. For example, modules or
objects without documentation strings may be published by including
them in a module's 'web_objects' attribute.
Sub-objects (or sub-sub objects, ...) of published objects are Sub-objects (or sub-sub objects, ...) of published objects are
also published, as long as the sub-objects: also published, as long as the sub-objects:
...@@ -37,8 +66,42 @@ Published objects ...@@ -37,8 +66,42 @@ Published objects
- are not modules. - are not modules.
A sub-object that cannot have a doc strings may be published by
including a special attribute in the containing object named:
subobject_name__doc__. For example, if foo.bar.spam doesn't have a
doc string, but foo.bar has a non-empty attribute
foo.bar.spam__doc__, then foo.bar.spam can be published.
Note that object methods are considered to be subobjects. Note that object methods are considered to be subobjects.
Object-to-subobject traversal is done by converting steps in the URI
path to get attribute or get item calls. For example, in traversing
from 'http://some.host/some_module/object' to
'http://some.host/some_module/object/subobject', the module
publisher will try to get 'some_module.object.subobject'. If the
access fails with other than an attribute error, then the object
publisher raises a "NotFound" exception. If the access fails with
an attribute error, then the object publisher will try to obtain the
subobject with: 'some_module.object["subobject"]'. If this access
fails, then the object publisher raises a '"Not Found"' exception. If
either of the accesses suceeds, then, of course, processing continues.
In some cases, a parent object may hold special attributed for a
subobject. This may be the case either when a sub-object cannot have
the special attribute or when it is convenience for the parent
object to manage attribute data (e.g. to share attribute data among
multiple children). When the object publisher looks for a special
attribute, it first trys to get the attribute from the published
object. If it fails to get the special attribute, it uses the same
access mechanism used to extract the subobject from the parent
object to get an attribute (or item) using a name obtained by
concatinating the sub-object name with the special attribute
name. For example, let 'foo.bar' be a dictionary, and foo.bar.spam
an item in the dictionary. When attempting to obtain the special
attribute '__realm__', the object publisher will first try to
evaluate 'foo.bar.spam.__realm__', and then try to evaluate:
'foo.bar["spam"+"__realm__"]'.
Access Control Access Control
Access to an object (and it's sub-objects) may be further Access to an object (and it's sub-objects) may be further
...@@ -53,10 +116,18 @@ Access Control ...@@ -53,10 +116,18 @@ Access Control
will be used and the object publisher will attempt to will be used and the object publisher will attempt to
authenticate the access to the object using one of the supplied authenticate the access to the object using one of the supplied
name and password pairs. The basic authentication realm name name and password pairs. The basic authentication realm name
used is "module_name.server_name", where "module_name" is the used is 'module_name.server_name', where 'module_name' is the
name of the module containing the published objects, and name of the module containing the published objects, and
server_name is the name of the web server. server_name is the name of the web server.
The module used to publish an object may contain it's own
'__allow_groups__' attribute, thereby limiting access to all of the
objects in a module.
If multiple objects in the URI path have '__allow_groups__'
attributes, then the effect will be that of intersecting all of the
groups.
Realms Realms
Realms provide a mechanism for separating authentication and Realms provide a mechanism for separating authentication and
...@@ -124,12 +195,20 @@ Function, method, and class objects ...@@ -124,12 +195,20 @@ Function, method, and class objects
Argument Types and File upload Argument Types and File upload
Normally, string arguments are passed to called objects. The Normally, string arguments are passed to called objects. The
called function must be prepared to convert string arguments to called object must be prepared to convert string arguments to
other data types, such as numbers. other data types, such as numbers.
If file upload is used, however, then file objects will be If file upload is used, however, then file objects will be
passed instead. passed instead.
If field names in form data are of the form: name:type, then an
attempt will be to convert data from from strings to the indicated
type. The data types currently supported are: float, int, and
long. For example, if the name of a field in an input form is
age:int, then the field value will be passed in argument, age, and
an attempt will be made to convert the argument value to an
integer.
Published objects that are not functions, methods, or classes Published objects that are not functions, methods, or classes
If a published object that is not a function, method, or class If a published object that is not a function, method, or class
...@@ -138,25 +217,76 @@ Published objects that are not functions, methods, or classes ...@@ -138,25 +217,76 @@ Published objects that are not functions, methods, or classes
Return types Return types
A published object, or the returned value of a called published A published object, or the returned value of a called published
object can be of any python type. The returned value will be object can be of any Python type. The returned value will be
converted to a string and examined to see if it appears to be an converted to a string and examined to see if it appears to be an
HTML document. If it appears to be an HTML document, then the HTML document. If it appears to be an HTML document, then the
response content-type will be set to text/html. Otherwise the response content-type will be set to 'text/html'. Otherwise the
content-type will be set to text/plain. content-type will be set to 'text/plain'.
A special case is when the returned object is a two-element tuple. A special case is when the returned object is a two-element tuple.
If the return value is a two-element tuple, then the first element If the return object is a two-element tuple, then the first element
will be converted to a string and treated as an HTML title, and will be converted to a string and treated as an HTML title, and
the second element will be converted to a string and treated as the second element will be converted to a string and treated as
the contents of an HTML body. An HTML document is created and the contents of an HTML body. An HTML document is created and
returned (with type text/html) by adding necessary html, head, returned (with type text/html) by adding necessary html, head,
title, and body tags. title, and body tags.
If the returned object is None or the string representation of the
returned object is an empty string, then HTTP the return status will
be set "No Content", and no body will be returned. On some
browsers, this will cause the displayed document to be unchanged.
Providing On-Line help
On-line help is provided for published objects, both explicitly and
implicitly. To provide on-line help for an object, simply provide a
'help' attribute for the object. If a 'help' attribute is not
provided, then the object's documentation string is used. When a URI
like: 'http:/some.server/cgi-bin/some_module/foo/bar/help' is
presented to the publisher, it will try to access the 'help'
attribute of 'some_module.foo.bar'. If the object does not have a
'help' attribute, then the object's documentation string will be
returned.
Exception handling Exception handling
Unhandled exceptions are caught by the object publisher Unhandled exceptions are caught by the object publisher
and are translated automatically to nicely formatted HTTP output. and are translated automatically to nicely formatted HTTP output.
Traceback information will be included in a comment in the output.
When an exception is raised, the exception type is mapped to an HTTP
code by matching the value of the exception type with a list of
standard HTTP status names. Any exception types that do not match
standard HTTP status names are mapped to "Internal Error" (500).
The standard HTTP status names are: '"OK"', '"Created"',
'"Accepted"', '"No Content"', '"Multiple Choices"', '"Redirect"',
'"Moved Permanently"', '"Moved Temporarily"', '"Not Modified"',
'"Bad Request"', '"Unauthorized"', '"Forbidden"', '"Not Found"',
'"Internal Error"', '"Not Implemented"', '"Bad Gateway"', and
'"Service Unavailable"', Variations on these names with different
cases and without spaces are also valid.
An attempt is made to use the exception value as the body of the
returned response. The object publisher will examine the exception
value. If the value is a string that contains some white space,
then it will be used as the body of the return error message. It it
appears to be HTML, the the error content type will be set to
'text/html', otherwise, it will be set to 'text/plain'. If the
exception value is not a string containing white space, then the
object publisher will generate it's own error message.
There are two exceptions to the above rule:
1. If the exception type is: '"Redirect"', '"Multiple Choices"'
'"Moved Permanently"', '"Moved Temporarily"', or
'"Not Modified"', and the exception value is an absolute URI,
then no body will be provided and a 'Location' header will be
included in the output with the given URI.
2. If the exception type is '"No Content"', then no body will be
returned.
When a body is returned, traceback information will be included in a
comment in the output.
Redirection Redirection
...@@ -166,8 +296,11 @@ Redirection ...@@ -166,8 +296,11 @@ Redirection
Examples Examples
Consider the following examples: Consider the following example:
"sample.py -- sample published module"
# URI: http://some.host/cgi-bin/sample/called
_called=0 _called=0
def called(): def called():
"Report how many times I've been called" "Report how many times I've been called"
...@@ -175,10 +308,12 @@ Examples ...@@ -175,10 +308,12 @@ Examples
_called=_called+1 _called=_called+1
return _called return _called
# URI: http://some.host/cgi-bin/sample/hi
def hi(): def hi():
"say hello" "say hello"
return '<html><head><base href=spam></head>hi' return "<html><head><base href=spam></head>hi"
# URI: http://some.host/cgi-bin/sample/add?x=1&y=2
def add(x,y): def add(x,y):
"add two numbers" "add two numbers"
from string import atof from string import atof
...@@ -187,46 +322,53 @@ Examples ...@@ -187,46 +322,53 @@ Examples
# Note that doc is not published # Note that doc is not published
def doc(m): def doc(m):
d={} d={}
exec 'import ' + m in d exec "import " + m in d
return d[m].__doc__ return d[m].__doc__
# URI: http://some.host/cgi-bin/sample/spam
class spam: class spam:
"spam is good" "spam is good"
__allow_groups__=[{'jim':1, 'super':1}] __allow_groups__=[{"jim":1, "super":1}]
# URI: http://some.host/cgi-bin/sample/aSpam/hi
def hi(self): def hi(self):
return self.__doc__ return self.__doc__
__super_group=[{'super':1}] __super_group=[{"super":1}]
# URI: http://some.host/cgi-bin/sample/aSpam/eat?module_name=foo
# Note that eat requires "super" access. # Note that eat requires "super" access.
eat__allow_groups__=__super_group eat__allow_groups__=__super_group
def eat(self,module_name): def eat(self,module_name):
"document a module" "document a module"
if not module_name: if not module_name:
raise 'BadRequest', 'The module name is blank' raise "BadRequest", "The module name is blank"
return doc(module_name) return doc(module_name)
# URI: http://some.host/cgi-bin/sample/aSpam/list
# Here we have a stream output example. # Here we have a stream output example.
# Note that only jim and super can use this. # Note that only jim and super can use this.
def list(self, RESPONSE): def list(self, RESPONSE):
"list some elements" "list some elements"
RESPONSE.setCookie('spam','eggs',path='/') RESPONSE.setCookie("spam","eggs",path="/")
RESPONSE.write('<html><head><title>A list</title></head>') RESPONSE.write("<html><head><title>A list</title></head>")
RESPONSE.write('list: \\n') RESPONSE.write("list: \\n")
for i in range(10): RESPONSE.write('\\telement %d' % i) for i in range(10): RESPONSE.write("\\telement %d" % i)
RESPONSE.write('\\n') RESPONSE.write("\\n")
# URI: http://some.host/cgi-bin/sample/aSpam
aSpam=spam() aSpam=spam()
# We create another spam that paul can use, # We create another spam that paul can use,
# but he still can't eat. # but he still can't eat.
# URI: http://some.host/cgi-bin/sample/moreSpam
moreSpam=spam() moreSpam=spam()
moreSpam.__dict__['__allow_groups__']=[{'paul':1}] moreSpam.__dict__['__allow_groups__']=[{'paul':1}]
# URI: http://some.host/cgi-bin/sample/taste
def taste(spam): def taste(spam):
"a favorable reviewer" "a favorable reviewer"
return spam,'yum yum, I like ' + spam return spam,'yum yum, I like ' + spam
...@@ -249,7 +391,7 @@ Publishing a module using CGI ...@@ -249,7 +391,7 @@ Publishing a module using CGI
o Copy the files: cgi_module_publisher.pyc and CGIResponse.pyc, o Copy the files: cgi_module_publisher.pyc and CGIResponse.pyc,
Realm.pyc, and newcgi.pyc, to the directory containing the Realm.pyc, and newcgi.pyc, to the directory containing the
module to be published, or to a directory in the standard module to be published, or to a directory in the standard
(compiled in) python search path. (compiled in) Python search path.
o Copy the file cgi-module-publisher to the directory containing the o Copy the file cgi-module-publisher to the directory containing the
module to be published. module to be published.
...@@ -263,7 +405,7 @@ Publishing a module using Fast CGI ...@@ -263,7 +405,7 @@ Publishing a module using Fast CGI
o Copy the files: cgi_module_publisher.pyc and CGIResponse.pyc, o Copy the files: cgi_module_publisher.pyc and CGIResponse.pyc,
Realm.pyc, and newcgi.pyc, to the directory containing the Realm.pyc, and newcgi.pyc, to the directory containing the
module to be published, or to a directory in the standard module to be published, or to a directory in the standard
(compiled in) python search path. (compiled in) Python search path.
o Copy the file fcgi-module-publisher to the directory containing the o Copy the file fcgi-module-publisher to the directory containing the
module to be published. module to be published.
...@@ -280,7 +422,7 @@ Publishing a module using the ILU Requestor (future) ...@@ -280,7 +422,7 @@ Publishing a module using the ILU Requestor (future)
o Copy the files: cgi_module_publisher.pyc and CGIResponse.pyc, o Copy the files: cgi_module_publisher.pyc and CGIResponse.pyc,
Realm.pyc, and newcgi.pyc, to the directory containing the Realm.pyc, and newcgi.pyc, to the directory containing the
module to be published, or to a directory in the standard module to be published, or to a directory in the standard
(compiled in) python search path. (compiled in) Python search path.
o Copy the file ilu-module-publisher to the directory containing the o Copy the file ilu-module-publisher to the directory containing the
module to be published. module to be published.
...@@ -292,10 +434,10 @@ Publishing a module using the ILU Requestor (future) ...@@ -292,10 +434,10 @@ Publishing a module using the ILU Requestor (future)
o Start the module server process by running the symbolically o Start the module server process by running the symbolically
linked file, giving the server name as an argument. linked file, giving the server name as an argument.
o Configure the web server to call module_name@server with o Configure the web server to call module_name@server_name with
the requestor. the requestor.
$Id: Publish.py,v 1.4 1996/07/04 22:57:20 jfulton Exp $""" $Id: Publish.py,v 1.5 1996/07/08 20:34:11 jfulton Exp $"""
#' #'
# Copyright # Copyright
# #
...@@ -348,6 +490,14 @@ $Id: Publish.py,v 1.4 1996/07/04 22:57:20 jfulton Exp $""" ...@@ -348,6 +490,14 @@ $Id: Publish.py,v 1.4 1996/07/04 22:57:20 jfulton Exp $"""
# (540) 371-6909 # (540) 371-6909
# #
# $Log: Publish.py,v $ # $Log: Publish.py,v $
# Revision 1.5 1996/07/08 20:34:11 jfulton
# Many changes, including:
#
# - Butter realm management
# - Automatic type conversion
# - Improved documentation
# - ...
#
# Revision 1.4 1996/07/04 22:57:20 jfulton # Revision 1.4 1996/07/04 22:57:20 jfulton
# Added lots of documentation. A few documented features have yet to be # Added lots of documentation. A few documented features have yet to be
# implemented. The module needs to be retested after adding some new # implemented. The module needs to be retested after adding some new
...@@ -355,7 +505,7 @@ $Id: Publish.py,v 1.4 1996/07/04 22:57:20 jfulton Exp $""" ...@@ -355,7 +505,7 @@ $Id: Publish.py,v 1.4 1996/07/04 22:57:20 jfulton Exp $"""
# #
# #
# #
__version__='$Revision: 1.4 $'[11:-2] __version__='$Revision: 1.5 $'[11:-2]
def main(): def main():
...@@ -369,6 +519,9 @@ import sys, os, string, types, newcgi, regex ...@@ -369,6 +519,9 @@ import sys, os, string, types, newcgi, regex
from CGIResponse import Response from CGIResponse import Response
from Realm import Realm from Realm import Realm
from newcgi import FieldStorage
class ModulePublisher: class ModulePublisher:
def html(self,title,body): def html(self,title,body):
...@@ -405,43 +558,15 @@ class ModulePublisher: ...@@ -405,43 +558,15 @@ class ModulePublisher:
except: return '' except: return ''
def document(self,o,response): def validate(self,groups,realm=None):
if type(o) is not types.StringType: o=o.__doc__ if not realm:
response.setBody(
self.html('Documentation for' +
((self.env('PATH_INFO') or
('/'+self.module_name))[1:]),
'<pre>\n%s\n</pre>' % o)
)
return response
def validate(self,object,parent=None,object_name='_'):
if type(object) is types.ModuleType: self.forbiddenError()
if (hasattr(object,'__allow_groups__') or
parent and hasattr(parent,object_name+'__allow_groups__')
):
if hasattr(object,'__allow_groups__'):
groups=object.__allow_groups__
else:
groups=getattr(parent,object_name+'__allow_groups__')
try: realm=self.realm try: realm=self.realm
except:
try:
realm=self.module.__realm__
if not hasattr(realm,'validate'):
# Hm. The realm must really be just a mapping
# object, so we will convert it to a proper
# realm using basic authentication
import Realm
realm=Realm("%s.%s" %
(self.module_name,self.request.SERVER_NAME),
realm)
self.module.__realm__=realm
except: except:
import Realm import Realm
realm=Realm("%s.%s" % realm=Realm("%s.%s" %
(self.module_name,self.request.SERVER_NAME)) (self.module_name,self.request.SERVER_NAME))
self.realm=realm self.realm=realm
try: try:
return realm.validate(self.env("HTTP_AUTHORIZATION"),groups) return realm.validate(self.env("HTTP_AUTHORIZATION"),groups)
except: except:
...@@ -464,18 +589,29 @@ class ModulePublisher: ...@@ -464,18 +589,29 @@ class ModulePublisher:
dict=imported_modules dict=imported_modules
try: try:
theModule, dict, published = module_dicts[module_name] theModule, object, published = module_dicts[module_name]
except: except:
exec 'import %s' % module_name in dict exec 'import %s' % module_name in dict
theModule=dict=dict[module_name] theModule=object=dict[module_name]
if hasattr(dict,published): if hasattr(theModule,published):
dict=getattr(dict,published) object=getattr(dict,published)
else: else:
dict=dict.__dict__ object=theModule
published=None published=None
module_dicts[module_name] = theModule, dict, published module_dicts[module_name] = theModule, object, published
self.module=theModule self.module=theModule
# Try to get realm from module
try: realm=theModule.__realm__
except: realm=None
# Do authorization check, if need be:
try:
groups=theModule.__allow_groups__
if groups: self.validate(groups,realm)
except: groups=None
# Get a nice clean path list: # Get a nice clean path list:
path=(string.strip(self.env('PATH_INFO')) or '/')[1:] path=(string.strip(self.env('PATH_INFO')) or '/')[1:]
path=string.splitfields(path,'/') path=string.splitfields(path,'/')
...@@ -484,72 +620,139 @@ class ModulePublisher: ...@@ -484,72 +620,139 @@ class ModulePublisher:
# Make help the default, if nothing is specified: # Make help the default, if nothing is specified:
if not path: path = ['help'] if not path: path = ['help']
# Try to look up the first one:
try: function, p, path = dict[path[0]], path[0], path[1:]
except KeyError: self.notFoundError()
# Do top-level object first-step authentication
if not (published or function.__doc__): self.forbiddenError()
self.validate(function)
p=''
while path: while path:
p,path=path[0], path[1:] entry_name,path,groups=path[0], path[1:], None
if p: if entry_name:
try: f=getattr(function,p) try:
subobject=getattr(object,entry_name)
try: groups=subobject.__allow_groups__
except:
try: groups=getattr(object,
entry_name+'__allow_groups__')
except: pass
try: doc=subobject.__doc__
except:
try: doc=getattr(object,entry_name+'__doc__')
except: doc=None
try: realm=subobject.__realm__
except:
try: realm=getattr(object,entry_name+'__realm__')
except: pass
except AttributeError: except AttributeError:
try: f=function[p] try:
subobject=object[entry_name]
try: groups=subobject.__allow_groups__
except:
try: groups=object[entry_name+'__allow_groups__']
except: pass
try: doc=subobject.__doc__
except:
try: doc=object[entry_name+'__doc__']
except: doc=None
try: realm=subobject.__realm__
except:
try: realm=object[entry_name+'__realm__']
except: pass
except TypeError: except TypeError:
if not path and p=='help': if not path and entry_name=='help' and doc:
p, f = '__doc__', self.document(function,response) object=doc
entry_name, subobject = (
'__doc__', self.html
('Documentation for' +
((self.env('PATH_INFO') or
('/'+self.module_name))[1:]),
'<pre>\n%s\n</pre>' % doc)
)
else: else:
self.notFoundError() self.notFoundError()
if not (p=='__doc__' or f.__doc__) or p[0]=='_': if published:
raise 'Forbidden',function # Bypass simple checks the first time
self.validate(f,function,p) published=None
function=f else:
# Perform simple checks
if type(f) is types.ClassType: if (type(subobject)==types.ModuleType or
if hasattr(f,'__init__'): entry_name != '__doc__' and
f=f.__init__ (not doc or entry_name[0]=='_')
):
raise 'Forbidden',object
# Do authorization checks
if groups: self.validate(groups, realm)
# Promote subobject to object
object=subobject
object_as_function=object
if type(object_as_function) is types.ClassType:
if hasattr(object_as_function,'__init__'):
object_as_function=object_as_function.__init__
else: else:
def ff(): pass def function_with_empty_signature(): pass
f=ff object_as_function=function_with_empty_signature
if type(f) is types.MethodType:
defaults=f.im_func.func_defaults if type(object_as_function) is types.MethodType:
names=(f.im_func. defaults=object_as_function.im_func.func_defaults
func_code.co_varnames[1:f.im_func.func_code.co_argcount]) argument_names=(
elif type(f) is types.FunctionType: object_as_function.im_func.
defaults=f.func_defaults func_code.co_varnames[
names=f.func_code.co_varnames[:f.func_code.co_argcount] 1:object_as_function.im_func.func_code.co_argcount])
elif type(object_as_function) is types.FunctionType:
defaults=object_as_function.func_defaults
argument_names=object_as_function.func_code.co_varnames[
:object_as_function.func_code.co_argcount]
else: else:
return response.setBody(function) return response.setBody(object)
query=self.request query=self.request
query['RESPONSE']=response query['RESPONSE']=response
args=[] args=[]
nrequired=len(names) - (len(defaults or [])) nrequired=len(argument_names) - (len(defaults or []))
for name_index in range(len(names)): for name_index in range(len(argument_names)):
name=names[name_index] argument_name=argument_names[name_index]
try: try:
v=query[name] v=query[argument_name]
args.append(v) args.append(v)
except: except:
if name_index < nrequired: if name_index < nrequired:
self.badRequestError(name) self.badRequestError(argument_name)
if args: result=apply(function,tuple(args)) if args: result=apply(object,tuple(args))
else: result=function() else: result=object()
if result and result is not response: response.setBody(result) if result and result is not response: response.setBody(result)
return response return response
def str_field(v): def str_field(v):
if type(v) is types.ListType:
return map(str_field,v)
if type(v) is types.InstanceType and v.__class__ is newcgi.MiniFieldStorage: if type(v) is types.InstanceType and v.__class__ is newcgi.MiniFieldStorage:
v=v.value v=v.value
elif type(v) is not types.StringType:
try:
if v.file:
v=v.file
else:
v=v.value
except: pass
return v
def flatten_field(v,converter):
if type(v) is types.ListType:
if len(v) > 1: return map(flatten_field,v)
v=v[0]
try:
if v.file:
v=v.file
else:
v=v.value
except: pass
if converter: v=converter(v)
return v return v
class Request: class Request:
...@@ -570,12 +773,13 @@ class Request: ...@@ -570,12 +773,13 @@ class Request:
These variables include input headers, server data, and other These variables include input headers, server data, and other
request-related data. The variable names are as request-related data. The variable names are as
<a href="http://hoohoo.ncsa.uiuc.edu/cgi/env.html">specified</a> <a href="http://hoohoo.ncsa.uiuc.edu/cgi/env.html">specified</a>
the <a href="http://hoohoo.ncsa.uiuc.edu/cgi/interface.html">CGI specification</a> in the
<a href="http://hoohoo.ncsa.uiuc.edu/cgi/interface.html">CGI specification</a>
- Form data - Form data
These are data extracted either a URL-encoded query string or These are data extracted from either a URL-encoded query
body, if present. string or body, if present.
- Cookies - Cookies
...@@ -588,6 +792,12 @@ class Request: ...@@ -588,6 +792,12 @@ class Request:
The request object has three attributes: "environ", "form", The request object has three attributes: "environ", "form",
"cookies", and "other" that are dictionaries containing this data. "cookies", and "other" that are dictionaries containing this data.
The form attribute of a request is actually a Field Storage
object. When file uploads are used, this provides a richer and
more complex interface than is provided by accessing form data as
items of the request. See the FieldStorage class documentation
for more details.
The request object may be used as a mapping object, in which case The request object may be used as a mapping object, in which case
values will be looked up in the order: environment variables, values will be looked up in the order: environment variables,
other variables, form data, and then cookies. Dot notation may be other variables, form data, and then cookies. Dot notation may be
...@@ -595,9 +805,10 @@ class Request: ...@@ -595,9 +805,10 @@ class Request:
other than "environ", "form", "cookies", and "other". other than "environ", "form", "cookies", and "other".
""" """
def __init__(self,environ,form): def __init__(self,environ,form,stdin):
self.environ=environ self.environ=environ
self.form=form self.form=form
self.stdin=stdin
self.other={} self.other={}
def __setitem__(self,key,value): def __setitem__(self,key,value):
...@@ -609,6 +820,11 @@ class Request: ...@@ -609,6 +820,11 @@ class Request:
self.other[key]=value self.other[key]=value
__type_converters = {'float':string.atof, 'int': string.atoi, 'long':string.atol}
__http_colon=regex.compile("\(:\|\(%3[aA]\)\)")
def __getitem__(self,key): def __getitem__(self,key):
"""Get a variable value """Get a variable value
...@@ -630,12 +846,29 @@ class Request: ...@@ -630,12 +846,29 @@ class Request:
if key=='REQUEST': return self if key=='REQUEST': return self
if key!='cookies': if key!='cookies':
try:
converter=None
try: try:
v=self.form[key] v=self.form[key]
if type(v) is types.ListType: except:
v=map(str_field, v) # Hm, maybe someone used a form with a name like: name:type
if len(v) == 1: v=v[0] try: tf=self.__dict__['___typed_form']
else: v=str_field(v) except:
tf=self.__dict__['___typed_form']={}
form=self.form
colon=self.__http_colon
search=colon.search
group=colon.group
for k in form.keys():
l = search(k)
if l > 0:
tf[k[:l]]=form[k],k[l+len(group(1)):]
v,t=tf[key]
try:
converter=self.__type_converters[t]
except: pass
v=flatten_field(v,converter)
return v return v
except: pass except: pass
...@@ -686,8 +919,9 @@ class CGIModulePublisher(ModulePublisher): ...@@ -686,8 +919,9 @@ class CGIModulePublisher(ModulePublisher):
try: try:
if environ['REQUEST_METHOD'] != 'GET': fp=stdin if environ['REQUEST_METHOD'] != 'GET': fp=stdin
except: pass except: pass
form=newcgi.FieldStorage(fp=fp,environ=environ,keep_blank_values=1) form=newcgi.FieldStorage(fp=fp,environ=environ,keep_blank_values=1)
self.request=Request(environ,form) self.request=Request(environ,form,stdin)
self.response=Response(stdout=stdout, stderr=stderr) self.response=Response(stdout=stdout, stderr=stderr)
self.stdin=stdin self.stdin=stdin
self.stdout=stdout self.stdout=stdout
...@@ -702,8 +936,10 @@ def publish_module(module_name, ...@@ -702,8 +936,10 @@ def publish_module(module_name,
stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr,
environ=os.environ): environ=os.environ):
try: try:
publisher=CGIModulePublisher(stdin=stdout, stdin=stdout, stderr=stderr, publisher = CGIModulePublisher(stdin=stdin, stdout=stdout,
stderr=stderr,
environ=environ) environ=environ)
response = publisher.response
response = publisher.publish(module_name) response = publisher.publish(module_name)
except: except:
response.exception() response.exception()
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
__doc__='''CGI Response Output formatter __doc__='''CGI Response Output formatter
$Id: Response.py,v 1.3 1996/07/03 18:25:50 jfulton Exp $''' $Id: Response.py,v 1.4 1996/07/08 20:34:09 jfulton Exp $'''
# Copyright # Copyright
# #
# Copyright 1996 Digital Creations, L.C., 910 Princess Anne # Copyright 1996 Digital Creations, L.C., 910 Princess Anne
...@@ -55,6 +55,14 @@ $Id: Response.py,v 1.3 1996/07/03 18:25:50 jfulton Exp $''' ...@@ -55,6 +55,14 @@ $Id: Response.py,v 1.3 1996/07/03 18:25:50 jfulton Exp $'''
# (540) 371-6909 # (540) 371-6909
# #
# $Log: Response.py,v $ # $Log: Response.py,v $
# Revision 1.4 1996/07/08 20:34:09 jfulton
# Many changes, including:
#
# - Butter realm management
# - Automatic type conversion
# - Improved documentation
# - ...
#
# Revision 1.3 1996/07/03 18:25:50 jfulton # Revision 1.3 1996/07/03 18:25:50 jfulton
# Added support for file upload via newcgi module. # Added support for file upload via newcgi module.
# #
...@@ -72,7 +80,7 @@ $Id: Response.py,v 1.3 1996/07/03 18:25:50 jfulton Exp $''' ...@@ -72,7 +80,7 @@ $Id: Response.py,v 1.3 1996/07/03 18:25:50 jfulton Exp $'''
# #
# #
# #
__version__='$Revision: 1.3 $'[11:-2] __version__='$Revision: 1.4 $'[11:-2]
import string, types, sys, regex import string, types, sys, regex
...@@ -81,6 +89,7 @@ status_reasons={ ...@@ -81,6 +89,7 @@ status_reasons={
201: 'Created', 201: 'Created',
202: 'Accepted', 202: 'Accepted',
204: 'No Content', 204: 'No Content',
300: 'Multiple Choices',
301: 'Moved Permanently', 301: 'Moved Permanently',
302: 'Moved Temporarily', 302: 'Moved Temporarily',
304: 'Not Modified', 304: 'Not Modified',
...@@ -99,6 +108,8 @@ status_codes={ ...@@ -99,6 +108,8 @@ status_codes={
'created':201, 'created':201,
'accepted':202, 'accepted':202,
'nocontent':204, 'nocontent':204,
'multiplechoices':300,
'redirect':300,
'movedpermanently':301, 'movedpermanently':301,
'movedtemporarily':302, 'movedtemporarily':302,
'notmodified':304, 'notmodified':304,
...@@ -110,6 +121,17 @@ status_codes={ ...@@ -110,6 +121,17 @@ status_codes={
'notimplemented':501, 'notimplemented':501,
'badgateway':502, 'badgateway':502,
'serviceunavailable':503, 'serviceunavailable':503,
'no content':204,
'multiple choices':300,
'moved permanently':301,
'moved temporarily':302,
'not modified':304,
'bad request':400,
'not found':404,
'internal error':500,
'not implemented':501,
'bad gateway':502,
'service unavailable':503,
200: 200, 200: 200,
201: 201, 201: 201,
202: 202, 202: 202,
...@@ -127,8 +149,8 @@ status_codes={ ...@@ -127,8 +149,8 @@ status_codes={
503: 503, 503: 503,
# Map standard python exceptions to status codes: # Map standard python exceptions to status codes:
'accesserror':403, 'accesserror':500,
'attributeerror':501, 'attributeerror':500,
'conflicterror':500, 'conflicterror':500,
'eoferror':500, 'eoferror':500,
'ioerror':500, 'ioerror':500,
...@@ -149,7 +171,29 @@ status_codes={ ...@@ -149,7 +171,29 @@ status_codes={
end_of_header_re=regex.compile('</head>',regex.casefold) end_of_header_re=regex.compile('</head>',regex.casefold)
base_re=regex.compile('<base',regex.casefold) base_re=regex.compile('<base',regex.casefold)
absuri_re=regex.compile("[a-zA-Z0-9+.-]+:[^\0- \"\#<>]+\(#[^\0- \"\#<>]*\)?")
class Response: class Response:
"""\
An object representation of an HTTP response.
The Response type encapsulates all possible responses to HTTP
requests. Responses are normally created by the object publisher.
A published object may recieve the response abject as an argument
named 'RESPONSE'. A published object may also create it's own
response object. Normally, published objects use response objects
to:
- Provide specific control over output headers,
- Set cookies, or
- Provide stream-oriented output.
If stream oriented output is used, then the response object
passed into the object must be used.
"""
def __init__(self,body='',status=200,headers=None, def __init__(self,body='',status=200,headers=None,
stdout=sys.stdout, stderr=sys.stderr,): stdout=sys.stdout, stderr=sys.stderr,):
...@@ -192,16 +236,21 @@ class Response: ...@@ -192,16 +236,21 @@ class Response:
the previous value set for the header, if one exists. ''' the previous value set for the header, if one exists. '''
self.headers[string.lower(name)]=value self.headers[string.lower(name)]=value
def __getitem__(self, name):
'Get the value of an output header'
return self.headers[name]
__setitem__=setHeader __setitem__=setHeader
def setBody(self, body, title=''): def setBody(self, body, title=''):
'''\ '''\
Set the body of the response
Sets the return body equal to the (string) argument "body". Also Sets the return body equal to the (string) argument "body". Also
updates the "content-length" return header. ''' updates the "content-length" return header.
You can also specify a title, in which case the title and body
will be wrapped up in html, head, title, and body tags.
If the body is a 2-element tuple, then it will be treated
as (title,body)
'''
if type(body)==types.TupleType: if type(body)==types.TupleType:
title,body=body title,body=body
if(title): if(title):
...@@ -244,7 +293,9 @@ class Response: ...@@ -244,7 +293,9 @@ class Response:
def expireCookie(self, name): def expireCookie(self, name):
'''\ '''\
Returns an HTTP header that will remove the cookie 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 corresponding to "name" on the client, if one exists. This is
accomplished by sending a new cookie with an expiration date accomplished by sending a new cookie with an expiration date
that has already passed. ''' that has already passed. '''
...@@ -253,7 +304,9 @@ class Response: ...@@ -253,7 +304,9 @@ class Response:
def setCookie(self,name, value=None, def setCookie(self,name, value=None,
expires=None, domain=None, path=None, secure=None): expires=None, domain=None, path=None, secure=None):
'''\ '''\
Returns an HTTP header that sets a cookie on cookie-enabled 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". This overwrites browsers with a key "name" and value "value". This overwrites
any previously set value for the cookie in the Response object. ''' any previously set value for the cookie in the Response object. '''
try: cookie=self.cookies[name] try: cookie=self.cookies[name]
...@@ -270,13 +323,20 @@ class Response: ...@@ -270,13 +323,20 @@ class Response:
def appendBody(self, body): def appendBody(self, body):
''
self.setBody(self.getBody() + body) self.setBody(self.getBody() + body)
def getHeader(self, name): def getHeader(self, name):
'''\ '''\
Get a header value
Returns the value associated with a HTTP return header, or Returns the value associated with a HTTP return header, or
"None" if no such header has been set in the response yet. ''' "None" if no such header has been set in the response
yet. '''
try: return self.headers[name]
except: return None
def __getitem__(self, name):
'Get the value of an output header'
return self.headers[name] return self.headers[name]
def getBody(self): def getBody(self):
...@@ -285,6 +345,8 @@ class Response: ...@@ -285,6 +345,8 @@ class Response:
def appendHeader(self, name, value, delimiter=","): def appendHeader(self, name, value, delimiter=","):
'''\ '''\
Append a value to a cookie
Sets an HTTP return header "name" with value "value", Sets an HTTP return header "name" with value "value",
appending it following a comma if there was a previous value appending it following a comma if there was a previous value
set for the header. ''' set for the header. '''
...@@ -295,7 +357,8 @@ class Response: ...@@ -295,7 +357,8 @@ class Response:
self.setHeader(name,h) self.setHeader(name,h)
def isHTML(self,str): def isHTML(self,str):
return string.lower(string.strip(str)[:6]) == '<html>' return (string.lower(string.strip(str)[:6]) == '<html>' or
string.find(str,'</') > 0)
def _traceback(self,t,v,tb): def _traceback(self,t,v,tb):
import traceback import traceback
...@@ -307,8 +370,21 @@ class Response: ...@@ -307,8 +370,21 @@ class Response:
t,v,tb=sys.exc_type, sys.exc_value,sys.exc_traceback t,v,tb=sys.exc_type, sys.exc_value,sys.exc_traceback
self.setStatus(t) self.setStatus(t)
if self.status >= 300 and self.status < 400:
if type(v) == types.StringType and absuri_re.match(v) >= 0:
self.setHeader('location', v)
return self
else:
try:
l,b=v
if type(l) == types.StringType and absuri_re.match(l) >= 0:
self.setHeader('location', l)
self.setBody(b)
return self
except: pass
b=v b=v
if type(b) is not type(''): if type(b) is not types.StringType:
return self.setBody( return self.setBody(
(str(t), (str(t),
'Sorry, an error occurred.<p>' 'Sorry, an error occurred.<p>'
...@@ -357,7 +433,23 @@ class Response: ...@@ -357,7 +433,23 @@ class Response:
return string.joinfields(headersl,'\n') return string.joinfields(headersl,'\n')
def flush(self): pass
def write(self,data): def write(self,data):
"""\
Return data as a stream
HTML data may be returned using a stream-oriented interface.
This allows the browser to display partial results while
computation of a response to proceed.
The published object should first set any output headers or
cookies on the response object.
Note that published objects must not generate any errors
after beginning stream-oriented output.
"""
self.body=self.body+data self.body=self.body+data
if end_of_header_re.search(self.body) >= 0: if end_of_header_re.search(self.body) >= 0:
try: del self.headers['content-length'] try: del self.headers['content-length']
...@@ -368,14 +460,13 @@ class Response: ...@@ -368,14 +460,13 @@ class Response:
body=self.body body=self.body
self.body='' self.body=''
self.write=write=self.stdout.write self.write=write=self.stdout.write
try: self.flush=self.stdout.flush
except: pass
write(str(self)) write(str(self))
self._wrote=1 self._wrote=1
write('\n\n') write('\n\n')
write(body) write(body)
def ExceptionResponse():
return Response().exception()
def main(): def main():
print Response('hello world') print Response('hello world')
print '-' * 70 print '-' * 70
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment