Commit e7d1dcad authored by Jim Fulton's avatar Jim Fulton

Hold on to your butts!

This is a pretty significant restructuring of ZPublisher to
allow a new module hook for handling errors.  There are two goals
in this:

  - To allow Zope to retry a request when ZODB raises a ConflictError, and

  - To move reformatting of exception using standard_error_message
    out to the publisher level so that:

    - Errors don't get obfuscated by error handling done by
      sub-documents and external methods, and

    - Errors raised by the publisher (e.g. for input errors,
      not-found errors, system exit, etc) get prettified.
parent a9f2bb81
This diff is collapsed.
...@@ -84,8 +84,8 @@ ...@@ -84,8 +84,8 @@
############################################################################## ##############################################################################
'''CGI Response Output formatter '''CGI Response Output formatter
$Id: BaseResponse.py,v 1.3 1999/03/18 22:34:53 jim Exp $''' $Id: BaseResponse.py,v 1.4 1999/08/04 18:05:26 jim Exp $'''
__version__='$Revision: 1.3 $'[11:-2] __version__='$Revision: 1.4 $'[11:-2]
import string, types, sys, regex import string, types, sys, regex
from string import find, rfind, lower, upper, strip, split, join, translate from string import find, rfind, lower, upper, strip, split, join, translate
...@@ -98,6 +98,7 @@ class BaseResponse: ...@@ -98,6 +98,7 @@ class BaseResponse:
""" """
debug_mode=None debug_mode=None
_auth=None _auth=None
_error_format='text/plain'
def __init__(self, stdout, stderr, def __init__(self, stdout, stderr,
body='', headers=None, status=None, cookies=None): body='', headers=None, status=None, cookies=None):
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
# #
############################################################################## ##############################################################################
__version__='$Revision: 1.19 $'[11:-2] __version__='$Revision: 1.20 $'[11:-2]
import regex, sys, os, string import regex, sys, os, string
from string import lower, atoi, rfind, split, strip, join, upper, find from string import lower, atoi, rfind, split, strip, join, upper, find
...@@ -170,8 +170,19 @@ class HTTPRequest(BaseRequest): ...@@ -170,8 +170,19 @@ class HTTPRequest(BaseRequest):
_hacked_path=None _hacked_path=None
args=() args=()
retry_max_count=3
def supports_retry(self): return self.retry_count < self.retry_max_coun
def retry(self):
r=self.__class__(stdin=self.stdin,
environ=self._orig_env,
response=self.response.retry()
)
r._held=self._held
return r
def __init__(self, stdin, environ, response, clean=0): def __init__(self, stdin, environ, response, clean=0):
self._orig_env=environ
# Avoid the overhead of scrubbing the environment in the # Avoid the overhead of scrubbing the environment in the
# case of request cloning for traversal purposes. If the # case of request cloning for traversal purposes. If the
# clean flag is set, we know we can use the passed in # clean flag is set, we know we can use the passed in
...@@ -180,8 +191,64 @@ class HTTPRequest(BaseRequest): ...@@ -180,8 +191,64 @@ class HTTPRequest(BaseRequest):
self.stdin=stdin self.stdin=stdin
self.environ=environ self.environ=environ
have_env=environ.has_key
get_env=environ.get
self.response=response self.response=response
other=self.other={} other=self.other={'RESPONSE': response}
self.form={}
self.steps=[]
################################################################
# Get base info first. This isn't likely to cause
# errors and might be useful to error handlers.
b=script=strip(get_env('SCRIPT_NAME',''))
while b and b[-1]=='/': b=b[:-1]
p = rfind(b,'/')
if p >= 0: b=b[:p+1]
else: b=''
while b and b[0]=='/': b=b[1:]
server_url=get_env('SERVER_URL',None)
if server_url is not None:
server_url=strip(server_url)
else:
if have_env('HTTPS') and (
environ['HTTPS'] == "on" or environ['HTTPS'] == "ON"):
server_url='https://'
elif (have_env('SERVER_PORT_SECURE') and
environ['SERVER_PORT_SECURE'] == "1"):
server_url='https://'
else: server_url='http://'
if have_env('HTTP_HOST'):
server_url=server_url+strip(environ['HTTP_HOST'])
else:
server_url=server_url+strip(environ['SERVER_NAME'])
server_port=environ['SERVER_PORT']
if server_port!='80': server_url=server_url+':'+server_port
other['SERVER_URL']=server_url
if server_url[-1:]=='/': server_url=server_url[:-1]
if b: self.base="%s/%s" % (server_url,b)
else: self.base=server_url
while script[:1]=='/': script=script[1:]
if script: script="%s/%s" % (server_url,script)
else: script=server_url
other['URL']=self.script=script
################################################################
# Cookie values should *not* be appended to existing form
# vars with the same name - they are more like default values
# for names not otherwise specified in the form.
cookies={}
k=get_env('HTTP_COOKIE','')
if k:
parse_cookie(k, cookies)
for k,item in cookies.items():
if not other.has_key(k):
other[k]=item
self.cookies=cookies
def processInputs( def processInputs(
self, self,
...@@ -201,8 +268,8 @@ class HTTPRequest(BaseRequest): ...@@ -201,8 +268,8 @@ class HTTPRequest(BaseRequest):
): ):
"""Process request inputs """Process request inputs
We need to delay input parsing so that it is done under publisher control for We need to delay input parsing so that it is done under
error handling prposes. publisher control for error handling purposes.
""" """
response=self.response response=self.response
environ=self.environ environ=self.environ
...@@ -216,8 +283,9 @@ class HTTPRequest(BaseRequest): ...@@ -216,8 +283,9 @@ class HTTPRequest(BaseRequest):
response._auth=1 response._auth=1
del environ['HTTP_AUTHORIZATION'] del environ['HTTP_AUTHORIZATION']
form={} form=self.form
other=self.other other=self.other
meth=None meth=None
fs=FieldStorage(fp=fp,environ=environ,keep_blank_values=1) fs=FieldStorage(fp=fp,environ=environ,keep_blank_values=1)
if not hasattr(fs,'list') or fs.list is None: if not hasattr(fs,'list') or fs.list is None:
...@@ -229,7 +297,8 @@ class HTTPRequest(BaseRequest): ...@@ -229,7 +297,8 @@ class HTTPRequest(BaseRequest):
global xmlrpc global xmlrpc
if xmlrpc is None: import xmlrpc if xmlrpc is None: import xmlrpc
meth, self.args = xmlrpc.parse_input(fs.value) meth, self.args = xmlrpc.parse_input(fs.value)
response=self.response=xmlrpc.response(response) response=xmlrpc.response(response)
other['RESPONSE']=self.response=response
other['REQUEST_METHOD']='' # We don't want index_html! other['REQUEST_METHOD']='' # We don't want index_html!
else: else:
self._file=fs.file self._file=fs.file
...@@ -461,23 +530,31 @@ class HTTPRequest(BaseRequest): ...@@ -461,23 +530,31 @@ class HTTPRequest(BaseRequest):
if getattr(x, '__class__',0) is record: if getattr(x, '__class__',0) is record:
# if the x is a record # if the x is a record
for k, v in x.__dict__.items(): for k, v in x.__dict__.items():
# loop through each attribute and value in
# loop through each
# attribute and value in
# the record # the record
for y in l: for y in l:
# loop through each record in the form
# list if it doesn't have the attributes # loop through each
# in the default dictionary, set them # record in the form
# list if it doesn't
# have the attributes
# in the default
# dictionary, set them
if not hasattr(y, k): if not hasattr(y, k):
setattr(y, k, v) setattr(y, k, v)
else: else:
# x is not a record # x is not a record
if not a in l: if not a in l:
l.append(a) l.append(a)
form[keys] = l form[keys] = l
else: else:
# The form has the key, the key is not mapped # The form has the key, the key is not mapped
# to a record or sequence so do nothing # to a record or sequence so do nothing
pass pass
# Convert to tuples # Convert to tuples
if tuple_items: if tuple_items:
...@@ -529,57 +606,6 @@ class HTTPRequest(BaseRequest): ...@@ -529,57 +606,6 @@ class HTTPRequest(BaseRequest):
other['PATH_INFO']=path="%s/%s" % (path,meth) other['PATH_INFO']=path="%s/%s" % (path,meth)
self._hacked_path=1 self._hacked_path=1
# Cookie values should *not* be appended to existing form
# vars with the same name - they are more like default values
# for names not otherwise specified in the form.
cookies={}
if environ.has_key('HTTP_COOKIE'):
parse_cookie(environ['HTTP_COOKIE'],cookies)
for k,item in cookies.items():
if not other.has_key(k):
other[k]=item
self.form=form
self.cookies=cookies
other['RESPONSE']=response
have_env=environ.has_key
b=script=strip(environ['SCRIPT_NAME'])
while b and b[-1]=='/': b=b[:-1]
p = rfind(b,'/')
if p >= 0: b=b[:p+1]
else: b=''
while b and b[0]=='/': b=b[1:]
if have_env('SERVER_URL'):
server_url=strip(environ['SERVER_URL'])
else:
if have_env('HTTPS') and (
environ['HTTPS'] == "on" or environ['HTTPS'] == "ON"):
server_url='https://'
elif (have_env('SERVER_PORT_SECURE') and
environ['SERVER_PORT_SECURE'] == "1"):
server_url='https://'
else: server_url='http://'
if have_env('HTTP_HOST'):
server_url=server_url+strip(environ['HTTP_HOST'])
else:
server_url=server_url+strip(environ['SERVER_NAME'])
server_port=environ['SERVER_PORT']
if server_port!='80': server_url=server_url+':'+server_port
other['SERVER_URL']=server_url
if server_url[-1:]=='/': server_url=server_url[:-1]
if b: self.base="%s/%s" % (server_url,b)
else: self.base=server_url
while script[:1]=='/': script=script[1:]
if script: script="%s/%s" % (server_url,script)
else: script=server_url
other['URL']=self.script=script
def resolve_url(self, url): def resolve_url(self, url):
# Attempt to resolve a url into an object in the Zope # Attempt to resolve a url into an object in the Zope
# namespace. The url must be a fully-qualified url. The # namespace. The url must be a fully-qualified url. The
......
...@@ -84,8 +84,8 @@ ...@@ -84,8 +84,8 @@
############################################################################## ##############################################################################
'''CGI Response Output formatter '''CGI Response Output formatter
$Id: HTTPResponse.py,v 1.16 1999/06/30 14:02:15 jim Exp $''' $Id: HTTPResponse.py,v 1.17 1999/08/04 18:05:27 jim Exp $'''
__version__='$Revision: 1.16 $'[11:-2] __version__='$Revision: 1.17 $'[11:-2]
import string, types, sys, regex import string, types, sys, regex
from string import find, rfind, lower, upper, strip, split, join, translate from string import find, rfind, lower, upper, strip, split, join, translate
...@@ -186,6 +186,7 @@ class HTTPResponse(BaseResponse): ...@@ -186,6 +186,7 @@ class HTTPResponse(BaseResponse):
accumulated_headers='' accumulated_headers=''
body='' body=''
realm='Zope' realm='Zope'
_error_format='text/html'
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,):
...@@ -207,6 +208,16 @@ class HTTPResponse(BaseResponse): ...@@ -207,6 +208,16 @@ class HTTPResponse(BaseResponse):
self.cookies={} self.cookies={}
self.stdout=stdout self.stdout=stdout
self.stderr=stderr self.stderr=stderr
def retry(self):
"""Return a response object to be used in a retry attempt
"""
# This implementation is a bit lame, because it assumes that
# only stdout stderr were passed to the constructor. OTOH, I
# think that that's all that is ever passed.
return self.__class__(stdout=self.stdout, stderr=self.stderr)
def setStatus(self, status, reason=None): def setStatus(self, status, reason=None):
'''\ '''\
......
...@@ -84,8 +84,8 @@ ...@@ -84,8 +84,8 @@
############################################################################## ##############################################################################
__doc__="""Python Object Publisher -- Publish Python objects on web servers __doc__="""Python Object Publisher -- Publish Python objects on web servers
$Id: Publish.py,v 1.134 1999/08/04 12:01:20 jim Exp $""" $Id: Publish.py,v 1.135 1999/08/04 18:05:28 jim Exp $"""
__version__='$Revision: 1.134 $'[11:-2] __version__='$Revision: 1.135 $'[11:-2]
import sys, os import sys, os
from string import lower, atoi, rfind, strip from string import lower, atoi, rfind, strip
...@@ -94,6 +94,10 @@ from Request import Request ...@@ -94,6 +94,10 @@ from Request import Request
from maybe_lock import allocate_lock from maybe_lock import allocate_lock
from mapply import mapply from mapply import mapply
class Retry(Exception):
"""Raise this to retry a request
"""
def call_object(object, args, request): def call_object(object, args, request):
result=apply(object,args) # Type s<cr> to step into published object. result=apply(object,args) # Type s<cr> to step into published object.
return result return result
...@@ -113,56 +117,125 @@ def publish(request, module_name, after_list, debug=0, ...@@ -113,56 +117,125 @@ def publish(request, module_name, after_list, debug=0,
mapply=mapply, mapply=mapply,
): ):
request_get=request.get (bobo_before, bobo_after, object, realm, debug_mode, err_hook,
response=request.response have_transactions)= get_module_info(module_name)
# First check for "cancel" redirect: parents=None
cancel=''
if lower(strip(request_get('SUBMIT','')))=='cancel':
cancel=request_get('CANCEL_ACTION','')
if cancel: raise 'Redirect', cancel
bobo_before, bobo_after, object, realm, debug_mode = get_module_info( try:
module_name) request.processInputs()
after_list[0]=bobo_after request_get=request.get
if debug_mode: response.debug_mode=debug_mode response=request.response
if realm and not request.get('REMOTE_USER',None):
response.realm=realm # First check for "cancel" redirect:
cancel=''
if lower(strip(request_get('SUBMIT','')))=='cancel':
cancel=request_get('CANCEL_ACTION','')
if cancel: raise 'Redirect', cancel
after_list[0]=bobo_after
if debug_mode: response.debug_mode=debug_mode
if realm and not request.get('REMOTE_USER',None):
response.realm=realm
if bobo_before is not None: bobo_before();
# Get a nice clean path list:
path=strip(request_get('PATH_INFO'))
request['PARENTS']=parents=[object]
if have_transactions: get_transaction().begin()
object=request.traverse(path)
# Record transaction meta-data
if have_transactions:
get_transaction().note(request_get('PATH_INFO'))
auth_user=request_get('AUTHENTICATED_USER',None)
if auth_user is not None:
get_transaction().setUser(auth_user,
request_get('AUTHENTICATION_PATH'))
result=mapply(object, request.args, request,
call_object,1,
missing_name,
dont_publish_class,
request)
if result is not response: response.setBody(result)
if have_transactions: get_transaction().commit()
if bobo_before is not None: bobo_before(); return response
# Get a nice clean path list: except:
path=strip(request_get('PATH_INFO')) if err_hook is not None:
if parents: parents=parents[-1]
try:
return err_hook(parents, request,
sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2],
)
except Retry:
# We need to try again....
if not request.supports_retry():
return err_hook(parents, request,
sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2],
)
return publish(request.retry(), module_name, after_list, debug)
else: raise
request['PARENTS']=parents=[object] def publish_module(module_name,
stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr,
environ=os.environ, debug=0, request=None, response=None):
must_die=0
status=200
after_list=[None]
try:
try:
if response is None:
response=Response(stdout=stdout, stderr=stderr)
else:
stdout=response.stdout
# Attempt to start a transaction: if request is None:
try: transaction=get_transaction() request=Request(stdin, environ, response)
except: transaction=None
if transaction is not None: transaction.begin()
object=request.traverse(path) response = publish(request, module_name, after_list, debug=debug)
except SystemExit, v:
if hasattr(sys, 'exc_info'): must_die=sys.exc_info()
else: must_die = SystemExit, v, sys.exc_traceback
response.exception(must_die)
except ImportError, v:
if type(v) is type(()) and len(v)==3: must_die=v
elif hasattr(sys, 'exc_info'): must_die=sys.exc_info()
else: must_die = SystemExit, v, sys.exc_traceback
response.exception(1, v)
except:
response.exception()
status=response.getStatus()
# Record transaction meta-data if response:
if transaction is not None: response=str(response)
get_transaction().note(request_get('PATH_INFO')) if response:
auth_user=request_get('AUTHENTICATED_USER',None) stdout.write(response)
if auth_user is not None:
get_transaction().setUser(auth_user,
request_get('AUTHENTICATION_PATH'))
result=mapply(object, request.args, request, # The module defined a post-access function, call it
call_object,1, if after_list[0] is not None: after_list[0]()
missing_name,
dont_publish_class,
request)
if result is not response: response.setBody(result) finally:
if request is not None: request.other={}
if transaction: get_transaction().commit() if must_die: raise must_die[0], must_die[1], must_die[2]
sys.exc_type, sys.exc_value, sys.exc_traceback = None, None, None
return status
return response
_l=allocate_lock() _l=allocate_lock()
def get_module_info(module_name, modules={}, def get_module_info(module_name, modules={},
...@@ -219,8 +292,15 @@ def get_module_info(module_name, modules={}, ...@@ -219,8 +292,15 @@ def get_module_info(module_name, modules={},
elif hasattr(module,'web_objects'): elif hasattr(module,'web_objects'):
object=module.web_objects object=module.web_objects
else: object=module else: object=module
error_hook=getattr(module,'zpublisher_exception_hook', None)
try: get_transaction()
except: have_transactions=0
else: have_transactions=1
info= (bobo_before, bobo_after, object, realm, debug_mode) info= (bobo_before, bobo_after, object, realm, debug_mode,
error_hook, have_transactions)
modules[module_name]=modules[module_name+'.cgi']=info modules[module_name]=modules[module_name+'.cgi']=info
...@@ -232,47 +312,3 @@ def get_module_info(module_name, modules={}, ...@@ -232,47 +312,3 @@ def get_module_info(module_name, modules={},
finally: finally:
tb=None tb=None
release() release()
def publish_module(module_name,
stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr,
environ=os.environ, debug=0, request=None, response=None):
must_die=0
status=200
after_list=[None]
try:
try:
if response is None:
response=Response(stdout=stdout, stderr=stderr)
else:
stdout=response.stdout
if request is None:
request=Request(stdin, environ, response)
request.processInputs()
response=request.response # could have changed!
response = publish(request, module_name, after_list, debug=debug)
except SystemExit, v:
if hasattr(sys, 'exc_info'): must_die=sys.exc_info()
else: must_die = SystemExit, v, sys.exc_traceback
response.exception(must_die)
except ImportError, v:
if type(v) is type(()) and len(v)==3: must_die=v
elif hasattr(sys, 'exc_info'): must_die=sys.exc_info()
else: must_die = SystemExit, v, sys.exc_traceback
response.exception(1, v)
except:
response.exception()
status=response.getStatus()
if response:
response=str(response)
if response: stdout.write(response)
# The module defined a post-access function, call it
if after_list[0] is not None: after_list[0]()
finally:
if request is not None: request.other={}
if must_die: raise must_die[0], must_die[1], must_die[2]
sys.exc_type, sys.exc_value, sys.exc_traceback = None, None, None
return status
...@@ -162,9 +162,9 @@ Examples ...@@ -162,9 +162,9 @@ Examples
s s
$Id: Test.py,v 1.32 1999/08/04 12:01:20 jim Exp $ $Id: Test.py,v 1.33 1999/08/04 18:05:29 jim Exp $
''' '''
__version__='$Revision: 1.32 $'[11:-2] __version__='$Revision: 1.33 $'[11:-2]
import sys, traceback, profile, os, getopt, string import sys, traceback, profile, os, getopt, string
from time import clock from time import clock
...@@ -256,8 +256,6 @@ def publish_module(module_name, ...@@ -256,8 +256,6 @@ def publish_module(module_name,
stdout=response.stdout stdout=response.stdout
if request is None: if request is None:
request=Request(stdin, environ, response) request=Request(stdin, environ, response)
request.processInputs()
response=request.response # could have changed!
for k, v in extra.items(): request[k]=v for k, v in extra.items(): request[k]=v
response = publish(request, module_name, after_list, debug=debug) response = publish(request, module_name, after_list, debug=debug)
except SystemExit, v: except SystemExit, v:
......
...@@ -82,9 +82,9 @@ ...@@ -82,9 +82,9 @@
# attributions are listed in the accompanying credits file. # attributions are listed in the accompanying credits file.
# #
############################################################################## ##############################################################################
__version__='$Revision: 1.7 $'[11:-2] __version__='$Revision: 1.8 $'[11:-2]
from Publish import publish_module from Publish import publish_module, Retry
def test(*args, **kw): def test(*args, **kw):
global test global test
......
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