Commit 1a36e9fb authored by Tres Seaver's avatar Tres Seaver

Merge r112823-112876 from the tseaver-fix_wsgi branch.

- Full test coverage for ZPublisher.WSGIPublisher.

- Add 'add_user' script and finder help, borrowed from 'repoze.zope2'.

- Add call to 'setDefaultSkin(request)' to fix view lookups.

- Override the 'write' method in 'WSGIHTTPReponse' to avoid inappropriate
  stringification, allowing things like the Plone resource registry to work
  properly.

- Defer closing the request until the transaction commits, if and only if
  we know that middleware is handling the transaction.

- Make the WSGI publish function deal with three special cases:
  
  - App returns a file-like object as the response body (keep the app from
    stringifying it).
  
  - App raises an Unauthorized exception (just set the response status, to
    let middleware handle issuing a challenge).
  
  - App raises a Redirect exception (just call redirect on the response).

- Adapt test code to the new signature of 'HTTPResponse._cookie_list',
  which now returns a list of two-tuples, rather than rendered strings.

- Get quickstart page rendering under plain paste config.

- Make WSGIResponse.__str__ raise and exception, preventing an
  'attractive nuisance.'

  The real logic is in finalize and listHeaders now, and the publish*
  functions call those directly.

- Move finalization logic out of HTTPResponse.listHeaders.

- Refactor WSGIHTTPResponse to avoid the need to use str() and parse.
  
  o Instead, compute status and headers directly.
  
- Chop out error and transaction handling from the 'publish*' functions:
  the point of doing WSGI is to move that stuff out of the application,
  and out into middleware.
  
- One backward incompatibility:  the special "shutdown" behavior is gone
  here.  It should be replaced by something in view code.

- Factor out computation of the list of response headers from stringifying
  them, allowing WSGIHTTPResponse do reuse them as tuples.

- Chop out copy-and-paste fossils irrelevant to WSGI publication.

- Replace contorted logic for case-normalizing response headers with
  idiomatic Python.

- More PEP8 conformance.
parent 0e977040
......@@ -116,11 +116,15 @@ setup(name='Zope2',
include_package_data=True,
zip_safe=False,
entry_points={
'paste.app_factory': [
'main=Zope2.Startup.run:make_wsgi_app',
],
'console_scripts': [
'mkzopeinstance=Zope2.utilities.mkzopeinstance:main',
'runzope=Zope2.Startup.run:run',
'zopectl=Zope2.Startup.zopectl:run',
'zpasswd=Zope2.utilities.zpasswd:main',
'addzope2user=Zope2.utilities.adduser:main'
],
},
)
......@@ -184,7 +184,7 @@ def http(request_string, handle_errors=True):
)
header_output.setResponseStatus(response.getStatus(), response.errmsg)
header_output.setResponseHeaders(response.headers)
header_output.appendResponseHeaders(response._cookie_list())
header_output.headersl.extend(response._cookie_list())
header_output.appendResponseHeaders(response.accumulated_headers)
sync()
......
......@@ -59,7 +59,7 @@ class PublisherConnection(testing.PublisherConnection):
l = key.find('-', start)
headers.append((key, val))
# get the cookies, breaking them into tuples for sorting
cookies = [(c[:10], c[12:]) for c in real_response._cookie_list()]
cookies = real_response._cookie_list()
headers.extend(cookies)
headers.sort()
headers.insert(0, ('Status', "%s %s" % (status, reason)))
......
......@@ -883,9 +883,9 @@ class HTTPResponse(BaseResponse):
# of name=value pairs may be quoted.
if attrs.get('quoted', True):
cookie = 'Set-Cookie: %s="%s"' % (name, quote(attrs['value']))
cookie = '%s="%s"' % (name, quote(attrs['value']))
else:
cookie = 'Set-Cookie: %s=%s' % (name, quote(attrs['value']))
cookie = '%s=%s' % (name, quote(attrs['value']))
for name, v in attrs.items():
name = name.lower()
if name == 'expires':
......@@ -904,41 +904,60 @@ class HTTPResponse(BaseResponse):
# and block read/write access via JavaScript
elif name == 'http_only' and v:
cookie = '%s; HTTPOnly' % cookie
cookie_list.append(cookie)
cookie_list.append(('Set-Cookie', cookie))
# Should really check size of cookies here!
return cookie_list
def finalize(self):
""" Set headers required by various parts of protocol.
"""
body = self.body
if (not 'content-length' in self.headers and
not 'transfer-encoding' in self.headers):
self.setHeader('content-length', len(body))
return "%d %s" % (self.status, self.errmsg), self.listHeaders()
def listHeaders(self):
""" Return a list of (key, value) pairs for our headers.
o Do appropriate case normalization.
"""
result = [
('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)')
]
for key, value in self.headers.items():
if key.lower() == key:
# only change non-literal header names
key = '-'.join([x.capitalize() for x in key.split('-')])
result.append((key, value))
result.extend(self._cookie_list())
result.extend(self.accumulated_headers)
return result
def __str__(self,
html_search=re.compile('<html>',re.I).search,
):
if self._wrote:
return '' # Streaming output was used.
headers = self.headers
status, headers = self.finalize()
body = self.body
if not headers.has_key('content-length') and \
not headers.has_key('transfer-encoding'):
self.setHeader('content-length',len(body))
chunks = []
append = chunks.append
# status header must come first.
append("Status: %d %s" % (self.status, self.errmsg))
append("X-Powered-By: Zope (www.zope.org), Python (www.python.org)")
for key, value in headers.items():
if key.lower() == key:
# only change non-literal header names
key = '-'.join([x.capitalize() for x in key.split('-')])
append("%s: %s" % (key, value))
chunks.extend(self._cookie_list())
for key, value in self.accumulated_headers:
append("%s: %s" % (key, value))
append('') # RFC 2616 mandates empty line between headers and payload
append(body)
chunks.append("Status: %s" % status)
for key, value in headers:
chunks.append("%s: %s" % (key, value))
# RFC 2616 mandates empty line between headers and payload
chunks.append('')
chunks.append(body)
return '\r\n'.join(chunks)
def write(self,data):
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -140,7 +140,7 @@ class ZServerHTTPResponse(HTTPResponse):
val = val.replace('\n\t', '\r\n\t')
append("%s: %s" % (key, val))
if self.cookies:
chunks.extend(self._cookie_list())
chunks.extend(['%s: %s' % x for x in self._cookie_list()])
append('')
append(body)
......
......@@ -52,6 +52,22 @@ def _setconfig(configfile=None):
App.config.setConfiguration(opts.configroot)
return opts
def make_wsgi_app(global_config, zope_conf):
from App.config import setConfiguration
from Zope2.Startup import get_starter
from Zope2.Startup.handlers import handleConfig
from Zope2.Startup.options import ZopeOptions
from ZPublisher.WSGIPublisher import publish_module
starter = get_starter()
opts = ZopeOptions()
opts.configfile = zope_conf
opts.realize(args=(), progname='Zope2WSGI', raise_getopt_errs=False)
handleConfig(opts.configroot, opts.confighandlers)
setConfiguration(opts.configroot)
starter.setConfiguration(opts.configroot)
starter.prepare()
return publish_module
if __name__ == '__main__':
run()
##############################################################################
#
# This was yanked out of repoze.zope2
#
##############################################################################
""" Add a Zope management user to the root Zope user folder """
import sys
from Zope2.utilities.finder import ZopeFinder
def adduser(app, user, pwd):
import transaction
result = app.acl_users._doAddUser(user, pwd, ['Manager'], [])
transaction.commit()
return result
def main(argv=sys.argv):
import sys
try:
user, pwd = argv[1], argv[2]
except IndexError:
print "%s <username> <password>" % argv[0]
sys.exit(255)
finder = ZopeFinder(argv)
finder.filter_warnings()
app = finder.get_app()
adduser(app, user, pwd)
if __name__ == '__main__':
main()
##############################################################################
#
# yanked from repoze.zope2
#
##############################################################################
import os
class ZopeFinder:
def __init__(self, argv):
self.cmd = argv[0]
def filter_warnings(self):
import warnings
warnings.simplefilter('ignore', Warning, append=True)
def get_app(self, config_file=None):
# given a config file, return a Zope application object
if config_file is None:
config_file = self.get_zope_conf()
from Zope2.Startup import options, handlers
import App.config
import Zope2
opts = options.ZopeOptions()
opts.configfile = config_file
opts.realize(args=[], doc="", raise_getopt_errs=0)
handlers.handleConfig(opts.configroot, opts.confighandlers)
App.config.setConfiguration(opts.configroot)
app = Zope2.app()
return app
def get_zope_conf(self):
# the default config file path is assumed to live in
# $instance_home/etc/zope.conf, and the console scripts that use this
# are assumed to live in $instance_home/bin; override if the
# environ contains "ZOPE_CONF".
ihome = os.path.dirname(os.path.abspath(os.path.dirname(self.cmd)))
default_config_file = os.path.join(ihome, 'etc', 'zope.conf')
zope_conf = os.environ.get('ZOPE_CONF', default_config_file)
return zope_conf
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