HTTPResponse.py
上传用户:gyjinxi
上传日期:2007-01-04
资源大小:159k
文件大小:26k
源码类别:

WEB邮件程序

开发平台:

Python

  1. ##############################################################################
  2. # Zope Public License (ZPL) Version 1.0
  3. # -------------------------------------
  4. # Copyright (c) Digital Creations.  All rights reserved.
  5. # This license has been certified as Open Source(tm).
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions are
  8. # met:
  9. # 1. Redistributions in source code must retain the above copyright
  10. #    notice, this list of conditions, and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. #    notice, this list of conditions, and the following disclaimer in
  13. #    the documentation and/or other materials provided with the
  14. #    distribution.
  15. # 3. Digital Creations requests that attribution be given to Zope
  16. #    in any manner possible. Zope includes a "Powered by Zope"
  17. #    button that is installed by default. While it is not a license
  18. #    violation to remove this button, it is requested that the
  19. #    attribution remain. A significant investment has been put
  20. #    into Zope, and this effort will continue if the Zope community
  21. #    continues to grow. This is one way to assure that growth.
  22. # 4. All advertising materials and documentation mentioning
  23. #    features derived from or use of this software must display
  24. #    the following acknowledgement:
  25. #      "This product includes software developed by Digital Creations
  26. #      for use in the Z Object Publishing Environment
  27. #      (http://www.zope.org/)."
  28. #    In the event that the product being advertised includes an
  29. #    intact Zope distribution (with copyright and license included)
  30. #    then this clause is waived.
  31. # 5. Names associated with Zope or Digital Creations must not be used to
  32. #    endorse or promote products derived from this software without
  33. #    prior written permission from Digital Creations.
  34. # 6. Modified redistributions of any form whatsoever must retain
  35. #    the following acknowledgment:
  36. #      "This product includes software developed by Digital Creations
  37. #      for use in the Z Object Publishing Environment
  38. #      (http://www.zope.org/)."
  39. #    Intact (re-)distributions of any official Zope release do not
  40. #    require an external acknowledgement.
  41. # 7. Modifications are encouraged but must be packaged separately as
  42. #    patches to official Zope releases.  Distributions that do not
  43. #    clearly separate the patches from the original work must be clearly
  44. #    labeled as unofficial distributions.  Modifications which do not
  45. #    carry the name Zope may be packaged in any form, as long as they
  46. #    conform to all of the clauses above.
  47. # Disclaimer
  48. #   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
  49. #   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  50. #   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  51. #   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
  52. #   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  53. #   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  54. #   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  55. #   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  56. #   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  57. #   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  58. #   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  59. #   SUCH DAMAGE.
  60. # This software consists of contributions made by Digital Creations and
  61. # many individuals on behalf of Digital Creations.  Specific
  62. # attributions are listed in the accompanying credits file.
  63. ##############################################################################
  64. '''CGI Response Output formatter
  65. $Id: HTTPResponse.py,v 1.24.4.1 1999/12/14 00:07:14 jim Exp $'''
  66. __version__='$Revision: 1.24.4.1 $'[11:-2]
  67. import string, types, sys, regex
  68. from string import find, rfind, lower, upper, strip, split, join, translate
  69. from types import StringType, InstanceType
  70. from BaseResponse import BaseResponse
  71. nl2sp=string.maketrans('n',' ')
  72. status_reasons={
  73. 100: 'Continue',
  74. 101: 'Switching Protocols',
  75. 102: 'Processing',
  76. 200: 'OK',
  77. 201: 'Created',
  78. 202: 'Accepted',
  79. 203: 'Non-Authoritative Information',
  80. 204: 'No Content',
  81. 205: 'Reset Content',
  82. 206: 'Partial Content',
  83. 207: 'Multi-Status',
  84. 300: 'Multiple Choices',
  85. 301: 'Moved Permanently',
  86. 302: 'Moved Temporarily',
  87. 303: 'See Other',
  88. 304: 'Not Modified',
  89. 305: 'Use Proxy',
  90. 307: 'Temporary Redirect',
  91. 400: 'Bad Request',
  92. 401: 'Unauthorized',
  93. 402: 'Payment Required',
  94. 403: 'Forbidden',
  95. 404: 'Not Found',
  96. 405: 'Method Not Allowed',
  97. 406: 'Not Acceptable',
  98. 407: 'Proxy Authentication Required',
  99. 408: 'Request Time-out',
  100. 409: 'Conflict',
  101. 410: 'Gone',
  102. 411: 'Length Required',
  103. 412: 'Precondition Failed',
  104. 413: 'Request Entity Too Large',
  105. 414: 'Request-URI Too Large',
  106. 415: 'Unsupported Media Type',
  107. 416: 'Requested range not satisfiable',
  108. 417: 'Expectation Failed',
  109. 422: 'Unprocessable Entity',
  110. 423: 'Locked',
  111. 424: 'Failed Dependency',
  112. 500: 'Internal Server Error',
  113. 501: 'Not Implemented',
  114. 502: 'Bad Gateway',
  115. 503: 'Service Unavailable',
  116. 504: 'Gateway Time-out',
  117. 505: 'HTTP Version not supported',
  118. 507: 'Insufficient Storage',
  119. }
  120. status_codes={}
  121. # Add mappings for builtin exceptions and
  122. # provide text -> error code lookups.
  123. for key, val in status_reasons.items():
  124.     status_codes[lower(join(split(val, ' '), ''))]=key
  125.     status_codes[lower(val)]=key
  126.     status_codes[key]=key
  127. en=filter(lambda n: n[-5:]=='Error', dir(__builtins__))
  128. for name in map(lower, en):
  129.     status_codes[name]=500
  130. status_codes['nameerror']=503
  131. status_codes['keyerror']=503
  132. status_codes['redirect']=300
  133. end_of_header_search=regex.compile('</head>',regex.casefold).search
  134. accumulate_header={'set-cookie': 1}.has_key
  135. class HTTPResponse(BaseResponse):
  136.     """
  137.     An object representation of an HTTP response.
  138.     
  139.     The Response type encapsulates all possible responses to HTTP
  140.     requests.  Responses are normally created by the object publisher.
  141.     A published object may recieve the response abject as an argument
  142.     named 'RESPONSE'.  A published object may also create it's own
  143.     response object.  Normally, published objects use response objects
  144.     to:
  145.     - Provide specific control over output headers,
  146.     - Set cookies, or
  147.     - Provide stream-oriented output.
  148.     If stream oriented output is used, then the response object
  149.     passed into the object must be used.
  150.     """ #'
  151.     accumulated_headers=''
  152.     body=''
  153.     realm='Zope'
  154.     _error_format='text/html'
  155.     def __init__(self,body='',status=200,headers=None,
  156.                  stdout=sys.stdout, stderr=sys.stderr,):
  157.         '''
  158.         Creates a new response. In effect, the constructor calls
  159.         "self.setBody(body); self.setStatus(status); for name in
  160.         headers.keys(): self.setHeader(name, headers[name])"
  161.         '''
  162.         if headers is None: headers={}
  163.         self.headers=headers
  164.         if status==200:
  165.             self.status=200
  166.             self.errmsg='OK'
  167.             headers['status']="200 OK"      
  168.         else: self.setStatus(status)
  169.         self.base=''
  170.         if body: self.setBody(body)
  171.         self.cookies={}
  172.         self.stdout=stdout
  173.         self.stderr=stderr
  174.     def retry(self):
  175.         """Return a response object to be used in a retry attempt
  176.         """
  177.         
  178.         # This implementation is a bit lame, because it assumes that
  179.         # only stdout stderr were passed to the constructor. OTOH, I
  180.         # think that that's all that is ever passed.
  181.         
  182.         return self.__class__(stdout=self.stdout, stderr=self.stderr)
  183.     
  184.     def setStatus(self, status, reason=None):
  185.         '''
  186.         Sets the HTTP status code of the response; the argument may
  187.         either be an integer or a string from { OK, Created, Accepted,
  188.         NoContent, MovedPermanently, MovedTemporarily,
  189.         NotModified, BadRequest, Unauthorized, Forbidden,
  190.         NotFound, InternalError, NotImplemented, BadGateway,
  191.         ServiceUnavailable } that will be converted to the correct
  192.         integer value. '''
  193.         if type(status) is types.StringType:
  194.             status=lower(status)
  195.         if status_codes.has_key(status): status=status_codes[status]
  196.         else: status=500
  197.         self.status=status
  198.         if reason is None:
  199.             if status_reasons.has_key(status): reason=status_reasons[status]
  200.             else: reason='Unknown'
  201.         self.setHeader('Status', "%d %s" % (status,str(reason)))
  202.         self.errmsg=reason
  203.     def setHeader(self, name, value, literal=0):
  204.         '''
  205.         Sets an HTTP return header "name" with value "value", clearing
  206.         the previous value set for the header, if one exists. If the
  207.         literal flag is true, the case of the header name is preserved,
  208.         otherwise word-capitalization will be performed on the header
  209.         name on output.'''
  210.         key=lower(name)
  211.         if accumulate_header(key):
  212.             self.accumulated_headers=(
  213.                 "%s%s: %sn" % (self.accumulated_headers, name, value))
  214.             return
  215.         name=literal and name or key
  216.         self.headers[name]=value
  217.     def addHeader(self, name, value):
  218.         '''
  219.         Set a new HTTP return header with the given value, while retaining
  220.         any previously set headers with the same name.'''
  221.         self.accumulated_headers=(
  222.             "%s%s: %sn" % (self.accumulated_headers, name, value))
  223.     __setitem__=setHeader
  224.     def setBody(self, body, title='', is_error=0,
  225.                 bogus_str_search=regex.compile(" [a-fA-F0-9]+>$").search,
  226.                 ):
  227.         '''
  228.         Set the body of the response
  229.         
  230.         Sets the return body equal to the (string) argument "body". Also
  231.         updates the "content-length" return header.
  232.         You can also specify a title, in which case the title and body
  233.         will be wrapped up in html, head, title, and body tags.
  234.         If the body is a 2-element tuple, then it will be treated
  235.         as (title,body)
  236.         
  237.         If is_error is true then the HTML will be formatted as a Zope error
  238.         message instead of a generic HTML page.
  239.         '''
  240.         if not body: return self
  241.         
  242.         if type(body) is types.TupleType and len(body) == 2:
  243.             title,body=body
  244.         if type(body) is not types.StringType:
  245.             if hasattr(body,'asHTML'):
  246.                 body=body.asHTML()
  247.         body=str(body)
  248.         l=len(body)
  249.             
  250.         if (find(body,'>')==l-1 and body[:1]=='<' and l < 200 and
  251.             bogus_str_search(body) > 0):
  252.             
  253.             self.notFoundError(body[1:-1])
  254.                 
  255.         else:
  256.             if(title):
  257.                 if not is_error:
  258.                     self.body=self._html(str(title), str(body))
  259.                 else:
  260.                     self.body=self._error_html(str(title), str(body))
  261.             else:
  262.                 self.body=str(body)
  263.         self.insertBase()
  264.         return self
  265.     def setBase(self,base):
  266.         'Set the base URL for the returned document.'
  267.         if base[-1:] != '/': base=base+'/'
  268.         self.base=base
  269.         self.insertBase()
  270.     def insertBase(self,
  271.                    base_re_search=regex.compile('(<base[- ]+[^>]+>)',
  272.                                                 regex.casefold).search
  273.                    ):
  274.         if (self.headers.has_key('content-type') and
  275.             self.headers['content-type'] != 'text/html'): return
  276.         if self.base:
  277.             body=self.body
  278.             if body:
  279.                 e=end_of_header_search(body)
  280.                 if e >= 0:
  281.                     b=base_re_search(body) 
  282.                     if b < 0:
  283.                         self.body=('%st<base href="%s">n%s' %
  284.                                    (body[:e],self.base,body[e:]))
  285.     def appendCookie(self, name, value):
  286.         '''
  287.         Returns an HTTP header that sets a cookie on cookie-enabled
  288.         browsers with a key "name" and value "value". If a value for the
  289.         cookie has previously been set in the response object, the new
  290.         value is appended to the old one separated by a colon. '''
  291.         cookies=self.cookies
  292.         if cookies.has_key(name): cookie=cookies[name]
  293.         else: cookie=cookies[name]={}
  294.         if cookie.has_key('value'):
  295.             cookie['value']='%s:%s' % (cookie['value'], value)
  296.         else: cookie['value']=value
  297.     def expireCookie(self, name, **kw):
  298.         '''
  299.         Cause an HTTP cookie to be removed from the browser
  300.         
  301.         The response will include an HTTP header that will remove the cookie
  302.         corresponding to "name" on the client, if one exists. This is
  303.         accomplished by sending a new cookie with an expiration date
  304.         that has already passed. Note that some clients require a path
  305.         to be specified - this path must exactly match the path given
  306.         when creating the cookie. The path can be specified as a keyword
  307.         argument.
  308.         '''
  309.         dict={'max_age':0, 'expires':'Wed, 31-Dec-97 23:59:59 GMT'}
  310.         for k, v in kw.items():
  311.             dict[k]=v
  312.         apply(HTTPResponse.setCookie, (self, name, 'deleted'), dict)
  313.     def setCookie(self,name,value,**kw):
  314.         '''
  315.         Set an HTTP cookie on the browser
  316.         The response will include an HTTP header that sets a cookie on
  317.         cookie-enabled browsers with a key "name" and value
  318.         "value". This overwrites any previously set value for the
  319.         cookie in the Response object.
  320.         '''
  321.         cookies=self.cookies
  322.         if cookies.has_key(name):
  323.             cookie=cookies[name]
  324.         else: cookie=cookies[name]={}
  325.         for k, v in kw.items():
  326.             cookie[k]=v
  327.         cookie['value']=value
  328.     def appendHeader(self, name, value, delimiter=","):
  329.         '''
  330.         Append a value to a cookie
  331.         
  332.         Sets an HTTP return header "name" with value "value",
  333.         appending it following a comma if there was a previous value
  334.         set for the header. '''
  335.         headers=self.headers
  336.         if headers.has_key(name):
  337.             h=self.header[name]
  338.             h="%s%snt%s" % (h,delimiter,value)
  339.         else: h=value
  340.         self.setHeader(name,h)
  341.     def isHTML(self,str):
  342.         return lower(strip(str)[:6]) == '<html>' or find(str,'</') > 0
  343.     def quoteHTML(self,text,
  344.                   subs={'&':'&amp;', "<":'&lt;', ">":'&gt;', '"':'&quot;'}
  345.                   ):
  346.         for ent in '&<>"':
  347.             if find(text, ent) >= 0:
  348.                 text=join(split(text,ent),subs[ent])
  349.         return text
  350.          
  351.     def format_exception(self,etype,value,tb,limit=None):
  352.         import traceback
  353.         result=['Traceback (innermost last):']
  354.         if limit is None:
  355.                 if hasattr(sys, 'tracebacklimit'):
  356.                         limit = sys.tracebacklimit
  357.         n = 0
  358.         while tb is not None and (limit is None or n < limit):
  359.                 f = tb.tb_frame
  360.                 lineno = tb.tb_lineno
  361.                 co = f.f_code
  362.                 filename = co.co_filename
  363.                 name = co.co_name
  364.                 locals=f.f_locals
  365.                 result.append('  File %s, line %d, in %s'
  366.                               % (filename,lineno,name))
  367.                 try: result.append('    (Object: %s)' %
  368.                                    locals[co.co_varnames[0]].__name__)
  369.                 except: pass
  370.                 try: result.append('    (Info: %s)' %
  371.                                    str(locals['__traceback_info__']))
  372.                 except: pass
  373.                 tb = tb.tb_next
  374.                 n = n+1
  375.         result.append(join(traceback.format_exception_only(etype, value),
  376.                            ' '))
  377.         return result
  378.     def _traceback(self,t,v,tb):
  379.         tb=self.format_exception(t,v,tb,200)
  380.         tb=join(tb,'n')
  381.         tb=self.quoteHTML(tb)
  382.         if self.debug_mode: _tbopen, _tbclose = '<PRE>', '</PRE>'
  383.         else:               _tbopen, _tbclose = '<!--',  '-->'
  384.         return "n%sn%sn%s" % (_tbopen, tb, _tbclose)
  385.     def redirect(self, location):
  386.         """Cause a redirection without raising an error"""
  387.         self.status=302
  388.         headers=self.headers
  389.         headers['status']='302 Moved Temporarily'
  390.         headers['location']=location
  391.         return location
  392.     def _html(self,title,body):
  393.         return ("<html>n"
  394.                 "<head>n<title>%s</title>n</head>n"
  395.                 "<body>n%sn</body>n"
  396.                 "</html>n" % (title,body))
  397.     def _error_html(self,title,body):
  398.         # XXX could this try to use standard_error_message somehow?
  399.         return ("""
  400. <HTML>
  401. <HEAD><TITLE>Zope Error</TITLE></HEAD>
  402. <BODY>
  403. <TABLE BORDER="0" WIDTH="100%">
  404. <TR VALIGN="TOP">
  405. <TD WIDTH="10%" ALIGN="CENTER">
  406. &nbsp;
  407. </TD>
  408. <TD WIDTH="90%">
  409.   <H2>Zope Error</H2>
  410.   <P>Zope has encountered an error while publishing this resource.
  411.   </P>""" + 
  412.   """
  413.   <P><STRONG>%s</STRONG></P>
  414.   
  415.   %s""" %(title,body) + 
  416.   """
  417.   <HR NOSHADE>
  418.   <P>Troubleshooting Suggestions</P>
  419.   <UL>
  420.   <LI>The URL may be incorrect.</LI>
  421.   <LI>The parameters passed to this resource may be incorrect.</LI>
  422.   <LI>A resource that this resource relies on may be encountering an error.</LI>
  423.   </UL>
  424.   <P>For more detailed information about the error, please
  425.   refer to the HTML source for this page.
  426.   </P>
  427.   <P>If the error persists please contact the site maintainer.
  428.   Thank you for your patience.
  429.   </P>
  430. </TD></TR>
  431. </TABLE>
  432. </BODY>
  433. </HTML>""")
  434.     def notFoundError(self,entry='who knows!'):
  435.         self.setStatus(404)
  436.         raise 'NotFound',self._error_html(
  437.             "Resource not found",
  438.             "Sorry, the requested Zope resource does not exist.<p>" +
  439.             "Check the URL and try again.<p>" +
  440.             "n<!--n%sn-->" % entry)
  441.     forbiddenError=notFoundError  # If a resource is forbidden,
  442.                                   # why reveal that it exists?
  443.     def debugError(self,entry):
  444.         raise 'NotFound',self._error_html(
  445.             "Debugging Notice",
  446.             "Zope has encountered a problem publishing your object.<p>"
  447.             "n%s" % entry)
  448.     def badRequestError(self,name):
  449.         self.setStatus(400)
  450.         if regex.match('^[A-Z_0-9]+$',name) >= 0:
  451.             raise 'InternalError', self._error_html(
  452.                 "Internal Error",
  453.                 "Sorry, an internal error occurred in this Zope resource.")
  454.         raise 'BadRequest',self._error_html(
  455.             "Invalid request",
  456.             "The parameter, <em>%s</em>, " % name +
  457.             "was omitted from the request.<p>" + 
  458.             "Make sure to specify all required parameters, " +
  459.             "and try the request again."
  460.             )
  461.     def _unauthorized(self):
  462.         realm=self.realm
  463.         if realm: self['WWW-authenticate']='basic realm="%s"' % realm
  464.     def unauthorized(self):
  465.         self._unauthorized()
  466.         m="<strong>You are not authorized to access this resource.</strong>"
  467.         if self.debug_mode:
  468.             if self._auth:
  469.                 m=m+'<p>nUsername and password are not correct.'
  470.             else:
  471.                 m=m+'<p>nNo Authorization header found.'
  472.         raise 'Unauthorized', m
  473.     def exception(self, fatal=0, info=None,
  474.                   absuri_match=regex.compile(
  475.                       "^"
  476.                       "(/|([a-zA-Z0-9+.-]+:))"
  477.                       "[^00- "\#<>]*"
  478.                       "\(#[^00- "\#<>]*\)?"
  479.                       "$"
  480.                       ).match,
  481.                   tag_search=regex.compile('[a-zA-Z]>').search,
  482.                   abort=1
  483.                   ):
  484.         if type(info) is type(()) and len(info)==3: t,v,tb = info
  485.         else: t,v,tb = sys.exc_info()
  486.         if str(t)=='Unauthorized': self._unauthorized()
  487.         stb=tb
  488.         # Abort running transaction, if any
  489.         if abort:
  490.             try: get_transaction().abort()
  491.             except: pass
  492.         try:
  493.             # Try to capture exception info for bci calls
  494.             et=translate(str(t),nl2sp)
  495.             self.setHeader('bobo-exception-type',et)
  496.             ev=translate(str(v),nl2sp)
  497.             if find(ev,'<html>') >= 0: ev='bobo exception'
  498.             self.setHeader('bobo-exception-value',ev[:255])
  499.             # Get the tb tail, which is the interesting part:
  500.             while tb.tb_next is not None: tb=tb.tb_next
  501.             el=str(tb.tb_lineno)
  502.             ef=str(tb.tb_frame.f_code.co_filename)
  503.             self.setHeader('bobo-exception-file',ef)
  504.             self.setHeader('bobo-exception-line',el)
  505.         except:
  506.             # Dont try so hard that we cause other problems ;)
  507.             pass
  508.         tb=stb
  509.         stb=None
  510.         self.setStatus(t)
  511.         if self.status >= 300 and self.status < 400:
  512.             if type(v) == types.StringType and absuri_match(v) >= 0:
  513.                 if self.status==300: self.setStatus(302)
  514.                 self.setHeader('location', v)
  515.                 tb=None
  516.                 return self
  517.             else:
  518.                 try:
  519.                     l,b=v
  520.                     if type(l) == types.StringType and absuri_match(l) >= 0:
  521.                         if self.status==300: self.setStatus(302)
  522.                         self.setHeader('location', l)
  523.                         self.setBody(b)
  524.                         tb=None
  525.                         return self
  526.                 except: pass
  527.         b=v
  528.         if isinstance(b,Exception): b=str(b)
  529.         
  530.         if fatal and t is SystemExit and v.code==0:
  531.                 tb=self.setBody(
  532.                     (str(t),
  533.                     'Zope has exited normally.<p>'
  534.                      + self._traceback(t,v,tb)),
  535.                      is_error=1)
  536.         #elif 1: self.setBody(v)
  537.         elif type(b) is not types.StringType or tag_search(b) < 0:
  538.             tb=self.setBody(
  539.                 (str(t),
  540.                 'Sorry, a Zope error occurred.<p>'+
  541.                  self._traceback(t,v,tb)),
  542.                  is_error=1)
  543.         elif lower(strip(b))[:6]=='<html>' or lower(strip(b))[:14]=='<!doctype html':
  544.             # error is an HTML document, not just a snippet of html
  545.             tb=self.setBody(b + self._traceback(t,'(see above)',tb),
  546.                 is_error=1)
  547.         else:
  548.             tb=self.setBody(
  549.                 (str(t), b + self._traceback(t,'(see above)',tb)),
  550.                  is_error=1)
  551.         return tb
  552.     _wrote=None
  553.     def _cookie_list(self):
  554.         cookie_list=[]
  555.         for name, attrs in self.cookies.items():
  556.             # Note that as of May 98, IE4 ignores cookies with
  557.             # quoted cookie attr values, so only the value part
  558.             # of name=value pairs may be quoted.
  559.             cookie='Set-Cookie: %s="%s"' % (name, attrs['value'])
  560.             for name, v in attrs.items():
  561.                 name=lower(name)
  562.                 if name=='expires': cookie = '%s; Expires=%s' % (cookie,v)
  563.                 elif name=='domain': cookie = '%s; Domain=%s' % (cookie,v)
  564.                 elif name=='path': cookie = '%s; Path=%s' % (cookie,v)
  565.                 elif name=='max_age': cookie = '%s; Max-Age=%s' % (cookie,v)
  566.                 elif name=='comment': cookie = '%s; Comment=%s' % (cookie,v)
  567.                 elif name=='secure' and v: cookie = '%s; Secure' % cookie
  568.             cookie_list.append(cookie)
  569.         # Should really check size of cookies here!
  570.         
  571.         return cookie_list
  572.     def __str__(self,
  573.                 html_search=regex.compile('<html>',regex.casefold).search,
  574.                 ):
  575.         if self._wrote: return ''       # Streaming output was used.
  576.         headers=self.headers
  577.         body=self.body
  578.         if body:
  579.             isHTML=self.isHTML(body)
  580.             if not headers.has_key('content-type'):
  581.                 if isHTML:
  582.                     c='text/html'
  583.                 else:
  584.                     c='text/plain'
  585.                 self.setHeader('content-type',c)
  586.             else:
  587.                 isHTML = headers['content-type']=='text/html'
  588.             if isHTML and end_of_header_search(self.body) < 0:
  589.                 lhtml=html_search(body)
  590.                 if lhtml >= 0:
  591.                     lhtml=lhtml+6
  592.                     body='%s<head></head>n%s' % (body[:lhtml],body[lhtml:])
  593.                 else:
  594.                     body='<html><head></head>n' + body
  595.                 self.setBody(body)
  596.                 body=self.body
  597. #        if not headers.has_key('content-type') and self.status == 200:
  598. #            self.setStatus('nocontent')
  599.         if not headers.has_key('content-length') and 
  600.                 not headers.has_key('transfer-encoding'):
  601.             self.setHeader('content-length',len(body))
  602.         headersl=[]
  603.         append=headersl.append
  604.         # status header must come first.
  605.         append("Status: %s" % headers.get('status', '200 OK'))
  606.         append("X-Powered-By: Zope (www.zope.org), Python (www.python.org)")
  607.         if headers.has_key('status'):
  608.             del headers['status']
  609.         for key, val in headers.items():
  610.             if lower(key)==key:
  611.                 # only change non-literal header names
  612.                 key="%s%s" % (upper(key[:1]), key[1:])
  613.                 start=0
  614.                 l=find(key,'-',start)
  615.                 while l >= start:
  616.                     key="%s-%s%s" % (key[:l],upper(key[l+1:l+2]),key[l+2:])
  617.                     start=l+1
  618.                     l=find(key,'-',start)
  619.             append("%s: %s" % (key, val))
  620.         if self.cookies:
  621.             headersl=headersl+self._cookie_list()
  622.         headersl[len(headersl):]=[self.accumulated_headers, body]
  623.         return join(headersl,'n')
  624.     def write(self,data):
  625.         """
  626.         Return data as a stream
  627.         HTML data may be returned using a stream-oriented interface.
  628.         This allows the browser to display partial results while
  629.         computation of a response to proceed.
  630.         The published object should first set any output headers or
  631.         cookies on the response object.
  632.         Note that published objects must not generate any errors
  633.         after beginning stream-oriented output. 
  634.         """
  635.         if not self._wrote:
  636.             self.outputBody()
  637.             self._wrote=1
  638.             self.stdout.flush()
  639.         self.stdout.write(data)