[Checkins] SVN: z3c.conditionalviews/ Create a new project that implements validating conditional HTTP requests.

Michael Kerrin michael.kerrin at openapp.ie
Thu May 31 14:51:08 EDT 2007


Log message for revision 76052:
  Create a new project that implements validating conditional HTTP requests.
  
  This is an other piece of the WebDAV puzzle.
  

Changed:
  A   z3c.conditionalviews/
  A   z3c.conditionalviews/trunk/
  A   z3c.conditionalviews/trunk/README.txt
  A   z3c.conditionalviews/trunk/buildout.cfg
  A   z3c.conditionalviews/trunk/setup.py
  A   z3c.conditionalviews/trunk/src/
  A   z3c.conditionalviews/trunk/src/z3c/
  A   z3c.conditionalviews/trunk/src/z3c/__init__.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/README.txt
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/__init__.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/adapters.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/configure.zcml
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/etag.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/ftesting.zcml
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/interfaces.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/lastmodification.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/tests.py
  A   z3c.conditionalviews/trunk/src/z3c/conditionalviews/validation.txt

-=-
Added: z3c.conditionalviews/trunk/README.txt
===================================================================
--- z3c.conditionalviews/trunk/README.txt	                        (rev 0)
+++ z3c.conditionalviews/trunk/README.txt	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,3 @@
+This package intergrates with the zope publisher to validate conditional
+requests based on the `If-None-Match`, `If-Match`, and `If-Modified-Since`,
+`If-UnModifiedSince` protocols.


Property changes on: z3c.conditionalviews/trunk/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: z3c.conditionalviews/trunk/buildout.cfg
===================================================================
--- z3c.conditionalviews/trunk/buildout.cfg	                        (rev 0)
+++ z3c.conditionalviews/trunk/buildout.cfg	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,9 @@
+[buildout]
+develop = .
+parts = test
+
+[test]
+recipe = zc.recipe.testrunner
+working-directory = .
+defaults = ["--tests-pattern", "^f?tests$"]
+eggs = z3c.conditionalviews [test]

Added: z3c.conditionalviews/trunk/setup.py
===================================================================
--- z3c.conditionalviews/trunk/setup.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/setup.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,24 @@
+from setuptools import setup, find_packages
+
+setup(
+    name = "z3c.conditionalviews",
+    version = "0.8",
+    author = "Michael Kerrin",
+    author_email = "michael.kerrin at openapp.ie",
+    url = "http://svn.zope.org/z3c.conditionalviews/",
+    description = "Validation mechanism for conditional HTTP requests.",
+    long_description = open("README.txt").read(),
+    license = "ZPL 2.1",
+
+    packages = find_packages("src"),
+    package_dir = {"": "src"},
+    namespace_packages = ["z3c"],
+    install_requires = ["setuptools",
+                        "zope.component",
+                        "zope.schema"],
+    extras_require = dict(test = ["zope.app.testing",
+                                  "zope.app.zcmlfiles",
+                                  "zope.app.securitypolicy",
+                                  ]),
+    zip_safe = False,
+    )

Added: z3c.conditionalviews/trunk/src/z3c/__init__.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/__init__.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/__init__.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/README.txt
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/README.txt	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/README.txt	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,384 @@
+===================
+z3c.condtionalviews
+===================
+
+z3c.conditionalviews is a mechanism to validate a HTTP request based on some
+conditional protocol like entity tags, or last modification date. It is also
+extensible so that protocols like WebDAV can define there own conditional
+protocol like the IF header.
+
+It works by implementing each conditional protocol as a `IHTTPValidator`
+utility, see etag and lastmodification modules for the most common use cases.
+Then when certain views are called by the publisher we lookup these utilities
+and ask them to validate the request object according to whatever protocol
+the utility implements.
+
+At the time of the view is called, and when we validate the request, we
+generally have access to the context, request and view itself. So the
+`IHTTPValidator` utilities generally adapt these 3 objects to an object
+implementing an interface specific to the protocol in question. For example
+the entity tag validator looks up an adapter implementing `IEtag`.
+
+Integration with Zope
+=====================
+
+  >>> import zope.component
+  >>> import zope.interface
+  >>> import z3c.conditionalviews.interfaces
+  >>> import z3c.conditionalviews.tests
+
+Decorator
+---------
+
+In order to integrate common browser views that can be cached, we can decorate
+the views call method with the `z3c.conditionalviews.ConditionalView` object.
+Note that all the views used in this test are defined in the ftesting.zcml
+file.
+
+  >>> response = http(r"""
+  ... GET /@@simpleview.html HTTP/1.1
+  ... Host: localhost
+  ... """, handle_errors = False)
+  >>> response.getStatus()
+  200
+  >>> response.getHeader('content-length')
+  '82'
+  >>> response.getHeader('content-type')
+  'text/plain'
+  >>> print response.getBody()
+  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+Since we haven't yet defined an adapter implementing IETag, the response
+contains no ETag header.
+
+  >>> response.getHeader('ETag') is None
+  True
+
+Define our IETag implementation.
+
+  >>> class SimpleEtag(object):
+  ...    zope.interface.implements(z3c.conditionalviews.interfaces.IETag)
+  ...    def __init__(self, context, request, view):
+  ...        pass
+  ...    weak = False
+  ...    etag = "3d32b-211-bab57a40"
+
+  >>> zope.component.getGlobalSiteManager().registerAdapter(
+  ...    SimpleEtag,
+  ...    (zope.interface.Interface,
+  ...     zope.publisher.interfaces.browser.IBrowserRequest,
+  ...     zope.interface.Interface))
+
+  >>> response = http(r"""
+  ... GET /@@simpleview.html HTTP/1.1
+  ... Host: localhost
+  ... """, handle_errors = False)
+  >>> response.getStatus()
+  200
+  >>> response.getHeader('content-length')
+  '82'
+  >>> response.getHeader('content-type')
+  'text/plain'
+  >>> response.getHeader('ETag')
+  '"3d32b-211-bab57a40"'
+  >>> print response.getBody()
+  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+Now by setting the request header If-None-Match: "3d32b-211-bab57a40", our
+view fails the validation and a 304 response is returned.
+
+  >>> response = http(r"""
+  ... GET /@@simpleview.html HTTP/1.1
+  ... Host: localhost
+  ... If-None-Match: "3d32b-211-bab57a40"
+  ... """, handle_errors = False)
+  >>> response.getStatus()
+  304
+  >>> response.getHeader('ETag')
+  '"3d32b-211-bab57a40"'
+  >>> response.getBody()
+  ''
+
+XXX - this seems wrong the content-length and content-type should not be set
+for this response.
+
+  >>> response.getHeader('content-length')
+  '0'
+  >>> response.getHeader('content-type')
+  'text/plain'
+
+Now make sure that we haven't broken the publisher, by making sure that we
+can still pass arguments to the different views.
+
+  >>> response = http(r"""
+  ... GET /@@simpleview.html?letter=y HTTP/1.1
+  ... Host: localhost
+  ... """, handle_errors = False)
+  >>> response.getStatus()
+  200
+  >>> response.getHeader('content-length')
+  '82'
+  >>> print response.getBody()
+  yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+  yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+
+We are now getting a charset value for this request because the default
+value for the SimpleView is not a unicode string, while the data received
+from the request is automatically converted to unicode by default.
+
+  >>> response.getHeader('content-type')
+  'text/plain;charset=utf-8'
+
+Since there is a query string present in the request, we don't set the ETag
+header.
+
+  >>> response.getHeader('ETag') is None
+  True
+
+The query string present in the following request causes the request to
+be valid, otherwise it would be invalid.
+
+  >>> response = http(r"""
+  ... GET /@@simpleview.html?letter=y HTTP/1.1
+  ... If-None-Match: "3d32b-211-bab57a40"
+  ... Host: localhost
+  ... """, handle_errors = False)
+  >>> response.getStatus()
+  200
+
+Generic HTTP conditional publication
+====================================
+
+We can integrate the validation method with the publication call method. This
+as the effect of trying to validate every request that passes through the
+publications `callObject` method. This is useful to validate requests that
+modify objects so that the client can say modify this resource if it hasn't
+changed since it last downloaded the resource, or if there is no existing
+resource at a location.
+
+This has the added benifit in that we don't have to specify how some one
+implements the PUT method.
+
+  >>> resp = http(r"""
+  ... PUT /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-type: text/plain
+  ... Content-length: 55
+  ... aaaaaaaaaa
+  ... aaaaaaaaaa
+  ... aaaaaaaaaa
+  ... aaaaaaaaaa
+  ... aaaaaaaaaa""", handle_errors = False)
+  >>> resp.getStatus()
+  201
+  >>> resp.getHeader('Content-length')
+  '0'
+  >>> resp.getHeader('Location')
+  'http://localhost/testfile'
+  >>> resp.getHeader('ETag', None) is None
+  True
+ 
+We can now get the resource and the entity tag.
+
+  >>> resp = http(r"""
+  ... GET /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  200
+  >>> resp.getHeader('ETag')
+  '"testfile:1"'
+  >>> print resp.getBody()
+  aaaaaaaaaa
+  aaaaaaaaaa
+  aaaaaaaaaa
+  aaaaaaaaaa
+  aaaaaaaaaa
+
+We could have used the HEAD method to get the entity tag.
+
+  >>> resp = http(r"""
+  ... HEAD /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  200
+  >>> resp.getHeader('ETag')
+  '"testfile:1"'
+
+With no 'If-None-Match' header we override the data.
+
+  >>> resp = http(r"""
+  ... PUT /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-type: text/plain
+  ... Content-length: 55
+  ... bbbbbbbbbb
+  ... bbbbbbbbbb
+  ... bbbbbbbbbb
+  ... bbbbbbbbbb
+  ... bbbbbbbbbb""", handle_errors = False)
+  >>> resp.getStatus()
+  200
+  >>> resp.getHeader('Content-length')
+  '0'
+  >>> resp.getHeader('Location', None) is None
+  True
+  >>> resp.getHeader('ETag')
+  '"testfile:2"'
+
+  >>> resp = http(r"""
+  ... GET /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  200
+  >>> print resp.getBody()
+  bbbbbbbbbb
+  bbbbbbbbbb
+  bbbbbbbbbb
+  bbbbbbbbbb
+  bbbbbbbbbb
+
+Specifying a `If-None-Match: "*"` header, says to upload the data only if there
+is no resource at the location specified in the request URI. If there is a
+resource at the location then a `412 Precondition Failed` response is
+returned and the resource is not modified'
+
+  >>> resp = http(r"""
+  ... PUT /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... If-None-Match: "*"
+  ... Content-type: text/plain
+  ... Content-length: 55
+  ... cccccccccc
+  ... cccccccccc
+  ... cccccccccc
+  ... cccccccccc
+  ... cccccccccc""")
+  >>> resp.getStatus()
+  412
+  >>> resp.getHeader('Content-length')
+  '0'
+  >>> resp.getHeader('Location', None) is None
+  True
+  >>> resp.getHeader('ETag')
+  '"testfile:2"'
+
+The file does not change.
+
+  >>> resp = http(r"""
+  ... GET /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  200
+  >>> print resp.getBody()
+  bbbbbbbbbb
+  bbbbbbbbbb
+  bbbbbbbbbb
+  bbbbbbbbbb
+  bbbbbbbbbb
+
+And now since testfile2 does exist yet we content the content.
+
+  >>> resp = http(r"""
+  ... PUT /testfile2 HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... If-None-Match: "*"
+  ... Content-type: text/plain
+  ... Content-length: 55
+  ... yyyyyyyyyy
+  ... yyyyyyyyyy
+  ... yyyyyyyyyy
+  ... yyyyyyyyyy
+  ... yyyyyyyyyy""")
+  >>> resp.getStatus()
+  201
+  >>> resp.getHeader('Content-length')
+  '0'
+  >>> resp.getHeader('Location')
+  'http://localhost/testfile2'
+  >>> resp.getHeader('ETag', None) is None # No etag adapter is configured
+  True
+
+  >>> resp = http(r"""
+  ... GET /testfile2 HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  200
+  >>> print resp.getBody()
+  yyyyyyyyyy
+  yyyyyyyyyy
+  yyyyyyyyyy
+  yyyyyyyyyy
+  yyyyyyyyyy
+
+We can now delete the resource, only if it hasn't changed. So for the
+'/testfile' resource we can use its first entity tag to confirm this.
+
+  >>> resp = http(r"""
+  ... DELETE /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... If-Match: "testfile:1"
+  ... """)
+  >>> resp.getStatus()
+  412
+
+And the file still exists.
+
+  >>> resp = http(r"""
+  ... GET /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  200
+
+But using a valid entity tag we can delete the resource.
+
+  >>> resp = http(r"""
+  ... DELETE /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... If-Match: "testfile:2"
+  ... """)
+  >>> resp.getStatus()
+  200
+  >>> resp.getBody()
+  ''
+
+  >>> resp = http(r"""
+  ... GET /testfile HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  404
+
+Method not allowed
+==================
+
+We should still get a `405 Method Not Allowed` status for methods that aren't
+registered yet.
+
+We need to be logged in order to traverse to the file.
+
+  >>> resp = http(r"""
+  ... FROG /testfile2 HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... """)
+  >>> resp.getStatus()
+  405
+  >>> resp.getHeader('ETag', None) is None
+  True
+
+Cleanup
+=======
+
+  >>> zope.component.getGlobalSiteManager().unregisterAdapter(
+  ...    SimpleEtag,
+  ...    (zope.interface.Interface,
+  ...     zope.publisher.interfaces.browser.IBrowserRequest,
+  ...     zope.interface.Interface))
+  True


Property changes on: z3c.conditionalviews/trunk/src/z3c/conditionalviews/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/__init__.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/__init__.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/__init__.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,114 @@
+##############################################################################
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
+import zope.component
+import zope.publisher.http
+import zope.publisher.publish
+import zope.publisher.interfaces
+import zope.app.http.interfaces
+import zope.app.publication.http
+import zope.app.publication.interfaces
+
+import interfaces
+
+def validate(context, request, func, viewobj, *args, **kw):
+    # count the number of invalid and evaulated validators, if evaluated is
+    # greater then zero and equal to hte invalid count then the request is
+    # invalid.
+    evaluated = invalid = 0
+
+    validators = [validator
+                  for validator_name, validator in
+                  zope.component.getUtilitiesFor(interfaces.IHTTPValidator)]
+
+    invalidStatus = 999
+    for validator in validators:
+        if validator.evaluate(context, request, viewobj):
+            evaluated += 1
+            invalid += not validator.valid(context, request, viewobj)
+            invalidStatus = validator.invalidStatus(context, request, viewobj)
+
+    if evaluated > 0 and evaluated == invalid:
+        # The request is invalid so we do not process it.
+        request.response.setStatus(invalidStatus)
+        result = ""
+    else:
+        # The request is valid so we do process it.
+        result = func(viewobj, *args, **kw)
+
+    for validator in validators:
+        validator.updateResponse(context, request, viewobj)
+
+    return result
+
+
+class BoundConditionalView(object):
+    def __init__(self, pt, ob):
+        object.__setattr__(self, "im_func", pt)
+        object.__setattr__(self, "im_self", ob)
+
+    def __call__(self, *args, **kw):
+        args = (self.im_self,) + args
+        return validate(self.im_self.context, self.im_self.request,
+                        self.im_func, *args, **kw)
+
+###############################################################################
+#
+# Decorators to turn a specific view into a conditonal view. For example
+# file downloads / upload views.
+#
+###############################################################################
+
+class ConditionalView(object):
+    def __init__(self, viewmethod):
+        self.viewmethod = viewmethod
+
+    def __get__(self, instance, class_):
+        return BoundConditionalView(self.viewmethod, instance)
+
+###############################################################################
+#
+# Generic publication object to turn a whole set of request handlers into
+# conditional views.
+#
+###############################################################################
+
+class ConditionalHTTPRequest(zope.publisher.http.HTTPRequest):
+    zope.interface.classProvides(
+        zope.app.publication.interfaces.IHTTPRequestFactory)
+
+    def setPublication(self, publication):
+        super(ConditionalHTTPRequest, self).setPublication(
+            ConditionalPublication(publication))
+
+
+class ConditionalPublication(object):
+
+    def __init__(self, publication):
+        self._publication = publication
+        for name in zope.publisher.interfaces.IPublication:
+            if name not in ("callObject",):
+                setattr(self, name, getattr(publication, name))
+
+    def callObject(self, request, ob):
+        # Exception handling, dont try to call request.method
+        if not zope.app.http.interfaces.IHTTPException.providedBy(ob):
+            view = zope.component.queryMultiAdapter(
+                (ob, request), name = request.method)
+            method = getattr(view, request.method, None)
+            if method is None:
+                raise zope.app.publication.http.MethodNotAllowed(ob, request)
+
+            ob = BoundConditionalView(method.im_func, method.im_self)
+
+        return zope.publisher.publish.mapply(
+            ob, request.getPositionalArguments(), request)

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/adapters.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/adapters.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/adapters.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,69 @@
+##############################################################################
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
+import zope.interface
+import zope.dublincore.interfaces
+
+import interfaces
+
+class LastModificationDate(object):
+    """
+      >>> import datetime
+      >>> import zope.component
+      >>> import zope.publisher.browser
+      >>> from zope.interface.verify import verifyObject
+
+      >>> lmt = datetime.datetime(2007, 3, 2, 13, 34, 23)
+      >>> class SimpleContent(object):
+      ...    def __init__(self):
+      ...        self.lastmodified = lmt
+
+      >>> class DublinCore(object):
+      ...    zope.interface.implements(
+      ...        zope.dublincore.interfaces.IZopeDublinCore)
+      ...    zope.component.adapts(SimpleContent)
+      ...    def __init__(self, context):
+      ...        self.context = context
+      ...    @property
+      ...    def modified(self):
+      ...        return self.context.lastmodified
+
+      >>> zope.component.getGlobalSiteManager().registerAdapter(DublinCore)
+
+      >>> class SimpleView(zope.publisher.browser.BrowserView):
+      ...    pass
+
+      >>> content = SimpleContent()
+
+    Adapting our simple view goes us our desired result.
+
+      >>> validatordata = LastModificationDate(
+      ...    content, None, SimpleView(content, None))
+      >>> verifyObject(interfaces.ILastModificationDate, validatordata)
+      True
+      >>> validatordata.lastmodified == lmt
+      True
+
+    Cleanup
+
+      >>> zope.component.getGlobalSiteManager().unregisterAdapter(DublinCore)
+      True
+
+    """
+    zope.interface.implements(interfaces.ILastModificationDate)
+
+    def __init__(self, context, request, view):
+        self.dcadapter = zope.dublincore.interfaces.IZopeDublinCore(context)
+
+    @property
+    def lastmodified(self):
+        return self.dcadapter.modified

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/configure.zcml
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/configure.zcml	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/configure.zcml	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,20 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+  <class class="z3c.conditionalviews.BoundConditionalView">
+    <require
+        attributes="__call__"
+        permission="zope.Public"
+        />
+  </class>
+
+  <utility
+      factory=".lastmodification.ModifiedSinceValidator"
+      name="http.modifiedsince"
+      />
+
+  <utility
+      factory=".etag.ETagValidator"
+      name="http.etag"
+      />
+
+</configure>

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/etag.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/etag.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/etag.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,487 @@
+##############################################################################
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
+import zope.interface
+import zope.component
+from zope.app.http.interfaces import INullResource
+
+import interfaces
+
+class ETagValidator(object):
+    """
+
+      >>> from zope.interface.verify import verifyObject
+      >>> from zope.publisher.interfaces.browser import IBrowserRequest
+      >>> from zope.publisher.browser import TestRequest
+      >>> from zope.publisher.browser import BrowserView
+
+      >>> class SimpleView(BrowserView):
+      ...    def __call__(self):
+      ...        self.request.response.setStatus(200)
+      ...        self.request.response.setHeader('ETag', '"xxxetag"')
+      ...        return 'Rendered view representation'
+
+    The ETagValidator is a HTTP utility validator that implements the entity
+    tag protocol.
+
+      >>> validator = ETagValidator()
+      >>> verifyObject(interfaces.IHTTPValidator, validator)
+      True
+
+    We need to make sure that we can corrctly parse any `If-Match`, or
+    `If-None-Match` HTTP header.
+
+      >>> request = TestRequest(environ = {
+      ...    'NOQUOTE': 'aa',
+      ...    'ONE': '"aa"',
+      ...    'TWO': '"aa", "bb"',
+      ...    'ALL': '"aa", *, "bb"',
+      ...    'WEAK': 'W/"w1"',
+      ...    'WEAK_TWO': 'W/"w1", W/"w2"',
+      ...    })
+      >>> view = SimpleView(None, request)
+
+      >>> validator.parseMatchList(request, 'missing')
+      []
+      >>> validator.parseMatchList(request, 'noquote')
+      []
+      >>> validator.parseMatchList(request, 'one')
+      ['aa']
+      >>> validator.parseMatchList(request, 'two')
+      ['aa', 'bb']
+      >>> validator.parseMatchList(request, 'all')
+      ['aa', '*', 'bb']
+      >>> validator.parseMatchList(request, 'weak')
+      ['w1']
+      >>> validator.parseMatchList(request, 'weak_two')
+      ['w1', 'w2']
+
+    When neither a `If-None-Match` or a `If-Match` header is present in the
+    request then we cannot evaluate this request as a conditional request,
+    according to the `If-None-Match` or `If-Match` protocol.
+
+      >>> validator.evaluate(None, request, view)
+      False
+
+    But if someone does try and validate the request then it is just True.
+
+      >>> validator.valid(None, request, view)
+      True
+
+    If-None-Match
+    =============
+
+    Define a simple ETag adapter for getting the current entity tag of a
+    view. We can change the value of the current etag by setting the
+    etag class attribute.
+
+      >>> class CurrentETag(object):
+      ...    zope.interface.implements(interfaces.IETag)
+      ...    def __init__(self, context, request, view):
+      ...        pass
+      ...    weak = False
+      ...    etag = 'xyzzy'
+
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"'})
+      >>> view = SimpleView(None, request)
+
+    Since we have a conditional header present the validator can evaluate
+    this request.
+
+      >>> validator.evaluate(None, request, view)
+      True
+
+    No current IETag adapter available so the request is valid, and the
+    update response method works but doesn't set any headers.
+
+      >>> validator.valid(None, request, view)
+      True
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getHeader('ETag', None) is None
+      True
+
+    But if the `If-None-Match` value is '*' then the request is invalid
+    even tough the resource has no entity tag.
+
+      >>> request._environ['IF_NONE_MATCH'] = '"*"'
+      >>> validator.valid(None, request, view)
+      False
+      >>> request._environ['IF_NONE_MATCH'] = '"xyzzy"'
+
+    The default value for the current entity tab is the same as in the request,
+    so the request is invalid, that is none of the entity tags match.
+
+      >>> zope.component.getGlobalSiteManager().registerAdapter(
+      ...    CurrentETag, (None, IBrowserRequest, None))
+
+      >>> validator.valid(None, request, view)
+      False
+
+    Etags don't match so the request is valid.
+
+      >>> CurrentETag.etag = 'xxx'
+      >>> validator.valid(None, request, view)
+      True
+
+    Test '*' which matches all values.
+
+      >>> request._environ['IF_NONE_MATCH'] = '"*"'
+      >>> validator.valid(None, request, view)
+      False
+
+    Test multiple `If-None-Match` values.
+
+      >>> request._environ['IF_NONE_MATCH'] = '"xxx", "yyy"'
+      >>> validator.valid(None, request, view)
+      False
+      >>> CurrentETag.etag = 'xxxzz'
+      >>> validator.valid(None, request, view)
+      True
+
+    Test multiple `If-None-Match` values with a '*'
+
+      >>> request._environ['IF_NONE_MATCH'] = '"*", "xxx", "yyy"'
+      >>> validator.valid(None, request, view)
+      False
+      >>> CurrentETag.etag = None
+      >>> validator.valid(None, request, view)
+      False
+
+    If-Match
+    ========
+
+      >>> request = TestRequest(environ = {'IF_MATCH': '"xyzzy"'})
+      >>> view = SimpleView(None, request)
+      >>> CurrentETag.etag = None
+
+    Since we have a conditional header present the validator can
+    evaluate this request.
+
+      >>> validator.evaluate(None, request, view)
+      True
+
+    Since there is no entity tag for the view, we don't match 'xyzzy', or
+    equivalently it is set to None.
+
+      >>> validator.valid(None, request, view)
+      False
+
+    The entity tags differ.
+
+      >>> CurrentETag.etag = 'xxx'
+      >>> validator.valid(None, request, view)
+      False
+
+    The entity tags are the same.
+
+      >>> CurrentETag.etag = 'xyzzy'
+      >>> validator.valid(None, request, view)
+      True
+
+    A `If-Match` header value of '*' matches everything.
+
+      >>> request._environ['IF_MATCH'] = '"*"'
+      >>> validator.valid(None, request, view)
+      True
+
+    Try multiple values.
+
+      >>> request._environ['IF_MATCH'] = '"xxx", "yyy"'
+      >>> validator.valid(None, request, view)
+      False
+
+      >>> request._environ['IF_MATCH'] = '"xyzzy", "yyy"'
+      >>> validator.valid(None, request, view)
+      True
+
+    Try multiple values with an '*' value.
+
+      >>> request._environ['IF_MATCH'] = '"xxx", "*", "yyy"'
+      >>> validator.valid(None, request, view)
+      True
+
+    Common responses
+    ================
+
+    Whether or not a request is valid or invalid the updateResponse method
+    is called after the status of the request is set by either the view / or
+    the `invalidStatus` method. Note that the updateResponse method does not
+    return any value. If it did then this value is not used for anything.
+
+      >>> CurrentETag.etag = 'xxx'
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"'})
+      >>> view = SimpleView(None, request)
+      >>> validator.valid(None, request, view)
+      True
+
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getStatus() # status is unset
+      599
+      >>> request.response.getHeader('ETag')
+      '"xxx"'
+
+    Since the `ETag` response header is set, we don't override it. Changing
+    the current entity tag and recalling the updateResponse method confirms
+    this. Note that this feature is neccessary to avoid situations where
+    a developer manages the entity tag of a view independently of this package.
+
+      >>> CurrentETag.etag = 'yyy'
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getStatus()
+      599
+      >>> request.response.getHeader('ETag')
+      '"xxx"'
+
+    Marking the current entity tag as weak.
+
+      >>> CurrentETag.weak = True
+
+      >>> validator.updateResponse(None, request, view)
+      >>> request.response.getHeader('ETag')
+      '"xxx"'
+
+    Since the 'ETag' header is already set we need to recreate the response
+    object to test the condition when the weak attribute is True.
+
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"'})
+      >>> view = SimpleView(None, request)
+      >>> validator.valid(None, request, view)
+      True
+
+      >>> validator.updateResponse(None, request, view)
+      >>> request.response.getHeader('ETag')
+      'W/"yyy"'
+
+      >>> CurrentETag.weak = False
+
+    Invalid header
+    ==============
+
+    If the header doesn't parse then the requets is True.
+
+      >>> request._environ['IF_NONE_MATCH'] = '"yyy"'
+      >>> validator.valid(None, request, view)
+      False
+
+    Now if we are missing the double quotes around the entity tag.
+
+      >>> request._environ['IF_NONE_MATCH'] = 'yyy'
+      >>> validator.valid(None, request, view)
+      True
+
+    Invalid status
+    ==============
+
+    In the case of entity tags and invalid responses we should return a 304
+    for the GET, and HEAD method requests and 412 otherwise.
+
+      >>> CurrentETag.etag = 'xyzzy'
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"'})
+      >>> request.method
+      'GET'
+      >>> view = SimpleView(None, request)
+      >>> validator.valid(None, request, view)
+      False
+
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getStatus()
+      599
+      >>> request.response.getHeader('ETag')
+      '"xyzzy"'
+
+      >>> validator.invalidStatus(None, request, view)
+      304
+
+    And the same for `HEAD` methods.
+
+      >>> CurrentETag.etag = 'xyzzy'
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"',
+      ...                                  'REQUEST_METHOD': 'HEAD'})
+      >>> request.method
+      'HEAD'
+      >>> view = SimpleView(None, request)
+      >>> validator.valid(None, request, view)
+      False
+
+      >>> validator.invalidStatus(None, request, view)
+      304
+
+    For anyother request method we get a `412 Precondition failed` response.
+
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"',
+      ...                                  'REQUEST_METHOD': 'FROG'})
+      >>> request.method
+      'FROG'
+      >>> view = SimpleView(None, request)
+      >>> validator.valid(None, request, view)
+      False
+
+      >>> validator.invalidStatus(None, request, view)
+      412
+
+    Query strings
+    =============
+
+    If a query is present in the request then either the client is filling
+    in a form with a GET method (oh why..), or else they are trying to do
+    some content negotiation, and hence the data supplied by the IETag adapter
+    does not apply to the view. But there are still cases where a request with
+    a query string can still fail.
+
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"xyzzy"',
+      ...                                  'REQUEST_METHOD': 'GET',
+      ...                                  'QUERY_STRING': 'argument=value'})
+      >>> view = SimpleView(None, request)
+      >>> validator.evaluate(None, request, view)
+      True
+      >>> validator.valid(None, request, view)
+      True
+      >>> request._environ['IF_NONE_MATCH'] = '"*"'
+      >>> validator.valid(None, request, view)
+      False
+
+      >>> request = TestRequest(environ = {'IF_MATCH': '"xyzzy"',
+      ...                                  'REQUEST_METHOD': 'GET',
+      ...                                  'QUERY_STRING': 'argument=value'})
+      >>> view = SimpleView(None, request)
+      >>> validator.evaluate(None, request, view)
+      True
+      >>> validator.valid(None, request, view)
+      False
+      >>> request._environ['IF_MATCH'] = '"*"'
+      >>> validator.valid(None, request, view)
+      True
+
+    Finally if we have a query string then when we try and update the
+    response we should not set the entity tag.
+
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getHeader('ETag', None) is None
+      True
+
+    Null resources
+    ==============
+
+    Additional functionality is needed to deal with null resources.
+
+      >>> import zope.app.http.put
+      >>> nullresource = zope.app.http.put.NullResource(None, 'test')
+
+    The `If-None-Match: "*"' says that a request is valid if the resource
+    corresponding to the resource does not exsist.
+
+      >>> request = TestRequest(environ = {'IF_NONE_MATCH': '"*"'})
+      >>> view = SimpleView(nullresource, request)
+      >>> validator.valid(nullresource, request, view)
+      True
+
+    The `If-Match: "*"` header says that a request is valid if the resource
+    corresponding the the request exsists. A null resource does not exist - it
+    is a temporary non-presistent content object used as an location in the
+    database that might exist, if a user PUT's data to it.
+
+      >>> request = TestRequest(environ = {'IF_MATCH': '"*"'})
+      >>> view = SimpleView(nullresource, request)
+      >>> validator.valid(nullresource, request, view)
+      False
+
+    Cleanup
+    -------
+
+      >>> zope.component.getGlobalSiteManager().unregisterAdapter(
+      ...    CurrentETag, (None, IBrowserRequest, None))
+      True
+
+    """
+    zope.interface.implements(interfaces.IHTTPValidator)
+
+    def parseMatchList(self, request, header):
+        ret = []
+        matches = request.getHeader(header, None)
+        if matches is not None:
+            for val in matches.split(","):
+                val = val.strip()
+                if val == "*":
+                    ret.append(val)
+                else:
+                    if val[:2] == "W/":
+                        val = val[2:]
+                    if val[0] + val[-1] == '""' and len(val) > 2:
+                        ret.append(val[1:-1])
+        return ret
+
+    def evaluate(self, context, request, view):
+        return request.getHeader("If-None-Match", None) is not None or \
+               request.getHeader("If-Match") is not None
+
+    def getDataStorage(self, context, request, view):
+        return zope.component.queryMultiAdapter(
+            (context, request, view), interfaces.IETag)
+
+    def _matches(self, context, request, etag, matchset):
+        if "*" in matchset:
+            if INullResource.providedBy(context):
+                return False
+            return True
+
+        if request.get("QUERY_STRING", "") == "" and etag in matchset:
+            return True
+
+        return False
+
+    def valid(self, context, request, view):
+        etag = self.getDataStorage(context, request, view)
+        if etag is not None:
+            # A request can still be invalid without knowing entity tag.
+            # If-Match: "*" matches everything
+            etag = etag.etag
+
+        # Test the most common validator first.
+        matchset = self.parseMatchList(request, "If-None-Match")
+        if matchset:
+            return not self._matches(context, request, etag, matchset)
+
+        matchset = self.parseMatchList(request, "If-Match")
+        if matchset:
+            return self._matches(context, request, etag, matchset)
+
+        # Always default to True, this can happen if the requests contains
+        # invalid data.
+        return True
+
+    def invalidStatus(self, context, request, view):
+        if request.method in ("GET", "HEAD"):
+            return 304
+        else:
+            # RFC2616 Section 14.26:
+            # Instead, if the request method was GET or HEAD, the server
+            # SHOULD respond with a 304 (Not Modified) response, including
+            # the cache-related header fields (particularly ETag) of one of
+            # the entities that matched.
+            return 412
+
+    def updateResponse(self, context, request, view):
+        if request.response.getHeader("ETag", None) is None and \
+               request.get("QUERY_STRING", "") == "":
+            etag = self.getDataStorage(context, request, view)
+            weak = etag and etag.weak
+            etag = etag and etag.etag
+            if etag:
+                if weak:
+                    request.response.setHeader("ETag", 'W/"%s"' % etag)
+                else:
+                    request.response.setHeader("ETag", '"%s"' % etag)

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/ftesting.zcml
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/ftesting.zcml	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/ftesting.zcml	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,118 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:zcml="http://namespaces.zope.org/zcml"
+           xmlns:browser="http://namespaces.zope.org/browser"
+           i18n_domain="zope">
+
+  <include zcml:condition="installed zope.app.zcmlfiles"
+           package="zope.app.zcmlfiles" />
+  <include zcml:condition="not-installed zope.app.zcmlfiles"
+           package="zope.app.zcmlfiles" />
+
+  <include package="zope.app.securitypolicy" file="meta.zcml" />
+  <include package="zope.app.securitypolicy" />
+  <include package="zope.app.authentication" />
+  <securityPolicy
+      component="zope.app.securitypolicy.zopepolicy.ZopeSecurityPolicy"
+      />
+  <unauthenticatedPrincipal
+      id="zope.anybody"
+      title="Unauthenticated User"
+      />
+
+  <role id="zope.Manager" title="Site Manager" />
+  <grantAll role="zope.Manager" />
+
+  <principal
+      id="zope.mgr"
+      title="Manager"
+      login="mgr"
+      password="mgrpw"
+      />
+  <grant
+      role="zope.Manager"
+      principal="zope.mgr"
+      />
+
+  <include package="z3c.conditionalviews" />
+
+  <!--
+      This view is used only for the tests to demonstrate how to use the
+      ConditionalView decorator. It is easier define the view here and to
+      let the browser:page directive setup all the neccessary security to
+      get this view to work.
+  -->
+
+  <browser:page
+      name="simpleview.html"
+      for="zope.interface.Interface"
+      class="z3c.conditionalviews.tests.Simpleview"
+      permission="zope.Public"
+      />
+
+  <!--
+      Test against the PUT method from zope.app.http. First we need to
+      configure some contents types and some adapters for this work
+      with the PUT method.
+  -->
+  <class class="z3c.conditionalviews.tests.File">
+    <require
+        permission="zope.View"
+        interface="z3c.conditionalviews.tests.IFile"
+        />
+
+    <require
+        permission="zope.ManageContent"
+        set_schema="z3c.conditionalviews.tests.IFile"
+        />
+
+    <implements
+        interface="zope.annotation.interfaces.IAttributeAnnotatable"
+        />
+  </class>
+
+  <adapter
+      for="zope.app.folder.interfaces.IFolder"
+      provides="zope.filerepresentation.interfaces.IFileFactory"
+      factory="z3c.conditionalviews.tests.FileFactory"
+      permission="zope.ManageContent"
+      />
+
+  <adapter 
+      for="z3c.conditionalviews.tests.IFile"
+      provides="zope.filerepresentation.interfaces.IWriteFile"
+      factory="z3c.conditionalviews.tests.WriteFile"
+      permission="zope.ManageContent"
+      />
+
+  <browser:page
+      for="z3c.conditionalviews.tests.IFile"
+      class="z3c.conditionalviews.tests.ViewFile"
+      permission="zope.View"
+      name="index.html"
+      />
+
+  <!--
+      Setup up a conditional publication, so that every HTTP request
+      is validated with this package.
+  -->
+  <utility
+      component="z3c.conditionalviews.ConditionalHTTPRequest"
+      provides="zope.app.publication.interfaces.IHTTPRequestFactory"
+      />
+
+  <class class="z3c.conditionalviews.tests.FileETag">
+    <allow
+        interface="z3c.conditionalviews.interfaces.IETag"
+        />
+  </class>
+
+  <adapter
+      for="z3c.conditionalviews.tests.IFile
+           zope.publisher.interfaces.http.IHTTPRequest
+           zope.interface.Interface"
+      factory="z3c.conditionalviews.tests.FileETag"
+      provides="z3c.conditionalviews.interfaces.IETag"
+      trusted="1"
+      />
+
+</configure>

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/interfaces.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/interfaces.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/interfaces.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,104 @@
+##############################################################################
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
+from zope import interface
+from zope import schema
+
+class IETag(interface.Interface):
+    """
+    Entity tags are used for comparing two or more entities from the same
+    requested view.
+
+    Used by the .etag.ETagValidator to validate a request against `If-Match`
+    and `If-None-Match` conditional HTTP headers.
+    """
+
+    weak = interface.Attribute("""
+    Boolean value indicated that the entity tag is weak.
+    """)
+
+    etag = interface.Attribute("""
+    The current entity tag of this view.
+    """)
+
+
+class ILastModificationDate(interface.Interface):
+    """
+    Used by the ModificationSinceValidator to adapt a view in order to
+    validate the `If-Modified-Since` and `If-UnModified-Since` conditional
+    HTTP headers.
+    """
+
+    lastmodified = schema.Datetime(
+        title = u"Last modification date",
+        description = u"Indicates the last time this view last changed.",
+        required = False)
+
+
+class IHTTPValidator(interface.Interface):
+    """
+    This adapter is responsible for validating a HTTP request against one
+    or more conditional headers.
+
+    When a view is called then for each validator that is registered with
+    the system, all validators that could possible return False, that is
+    their `evaluate` method returns True are counted. And if the number
+    of validators whose evaluate method returns True equals the number
+    of validators who say that the request is invalid (i.e. the valid
+    method returns False) then the request is considered invalid by this 
+    package.
+
+    When a request is invalid then the `invalidStatus` method is called to
+    set the status code of the response and the view we are adapter is not
+    called.
+
+    If a request is valid then the view we adapted is called.
+
+    In both situations, after the view is called or the `invalidStatus`
+    then the `updateResponse` method is called for each registered validators.
+    This method should (if not present) add a validator HTTP header to
+    the response, so clients know how to make a request conditional.
+    """
+
+    def evaluate(context, request, view):
+        """
+        Return `True` if this request is a conditional request and this
+        validator knows can validate the request.
+        """
+
+    def valid(context, request, view):
+        """
+        Return `True` request is valid and should be executed.
+
+        If the request is not valid then the `invalidStatus` method will
+        be called.
+
+        By default this method should always return `True`.
+        """
+
+    def invalidStatus(context, request, view):
+        """
+        Return the integer status of that the response should be considering
+        that this validator evaluated the request as invalid.
+
+        If more then more validator are involved in failing a request then
+        the first validator used will in the validation algorithm will be
+        used. This isn't ideal but in practice, this problem is unlikely to
+        show up.
+        """
+
+    def updateResponse(context, request, view):
+        """
+	This method is always called, and should set a HTTP header informing
+        the current data needed to invalid a request the next time they
+        request the adapted view.
+        """

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/lastmodification.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/lastmodification.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/lastmodification.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,328 @@
+##############################################################################
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
+import time
+import calendar
+import zope.component
+import zope.datetime
+import zope.interface
+
+import interfaces
+
+class ModifiedSinceValidator(object):
+    """
+
+      >>> import datetime
+      >>> from zope.interface.verify import verifyObject
+      >>> from zope.publisher.interfaces.browser import IBrowserRequest
+      >>> from zope.publisher.browser import TestRequest
+      >>> from zope.publisher.browser import BrowserView
+
+      >>> def format(dt):
+      ...    return zope.datetime.rfc1123_date(time.mktime(dt.utctimetuple()))
+
+      >>> lmt = datetime.datetime(2007, 1, 6, 13, 42, 12,
+      ...    tzinfo = zope.datetime.tzinfo(60))
+
+    ModifiedSinceValidator is a HTTP validator utility that will evaluate
+    a HTTP request to see if it passes or fails the If-Modifed* protocol.
+
+      >>> validator = ModifiedSinceValidator()
+      >>> verifyObject(interfaces.IHTTPValidator, validator)
+      True
+
+    If-Modified-Since
+    =================
+
+    The If-Modified-Since request-header field is used with a method to
+    make it conditional: if the requested variant has not been modified
+    since the time specified in this field, an entity will not be
+    returned from the server; instead, a 304 (not modified) response will
+    be returned without any message-body.
+
+      >>> class SimpleView(BrowserView):
+      ...    def __call__(self):
+      ...        self.request.response.setStatus(200)
+      ...        self.request.response.setHeader('Last-Modified', format(lmt))
+      ...        return 'Rendered view representation.'
+
+    Create a context adapter to find the last modified date of the context
+    object. We store the lastmodifed datatime object as a class attribute so
+    that we can easily change its value during these tests.
+
+      >>> class LastModification(object):
+      ...    zope.interface.implements(interfaces.ILastModificationDate)
+      ...    lastmodified = None
+      ...    def __init__(self, context, request, view):
+      ...        pass
+
+    A ILastModificationDate adapter must be registered for this validator
+    to know when a view was last modified.
+
+      >>> request = TestRequest(
+      ...    environ = {'IF_MODIFIED_SINCE': format(lmt)})
+      >>> request['IF_MODIFIED_SINCE']
+      'Sat, 06 Jan 2007 12:42:12 GMT'
+      >>> view = SimpleView(None, request)
+
+      >>> validator.valid(None, request, view) #doctest:+ELLIPSIS
+      True
+
+    Now register the last modified data adapter, and set up 
+
+      >>> gsm = zope.component.getGlobalSiteManager()
+      >>> gsm.registerAdapter(LastModification, (None, IBrowserRequest, None))
+
+    Since there is no last modification date by default the request is valid.
+
+      >>> validator.valid(None, request, view)
+      True
+
+    Set the last modification date to be equal.
+
+      >>> LastModification.lastmodified = lmt
+      >>> validator.valid(None, request, view)
+      False
+
+    Increase the current last modification time of the view by 1 second.
+
+      >>> LastModification.lastmodified = lmt + datetime.timedelta(seconds = 1)
+      >>> validator.valid(None, request, view)
+      True
+
+    Decrease the current last modification time of the view by 1 second.
+
+      >>> LastModification.lastmodified = lmt - datetime.timedelta(seconds = 1)
+      >>> validator.valid(None, request, view)
+      False
+
+    Test invalid request data.
+
+      >>> invalidrequest = TestRequest(environ = {'IF_MODIFIED_SINCE': 'XXX'})
+      >>> validator.valid(None, invalidrequest, view)
+      True
+
+      >>> LastModification.lastmodified = lmt
+
+    If-UnModified-Since
+    ===================
+
+      >>> request = TestRequest(
+      ...    environ = {'IF_UNMODIFIED_SINCE': format(lmt)})
+      >>> request['IF_UNMODIFIED_SINCE']
+      'Sat, 06 Jan 2007 12:42:12 GMT'
+
+      >>> view = SimpleView(None, request)
+
+    The If-Unmodified-Since request-header field is used with a method to
+    make it conditional. If the requested resource has not been modified
+    since the time specified in this field, the server SHOULD perform the
+    requested operation as if the If-Unmodified-Since header were not
+    present.
+
+      >>> validator.valid(None, request, view)
+      True
+
+    Increase the current last modified time of the view by 1 second.
+
+      >>> LastModification.lastmodified = lmt + datetime.timedelta(seconds = 1)
+      >>> validator.valid(None, request, view)
+      False
+
+    Decrease the current last modified time of the view by 1 second.
+
+      >>> LastModification.lastmodified = lmt - datetime.timedelta(seconds = 1)
+      >>> validator.valid(None, request, view)
+      True
+
+    Invalid date header.
+
+      >>> request = TestRequest(
+      ...    environ = {'IF_UNMODIFIED_SINCE': 'xxx'})
+      >>> view = SimpleView(None, request)
+
+      >>> validator.valid(None, request, view)
+      False
+
+    If no `If-Modified-Since` or `If-UnModified-Since` conditional HTTP
+    headers are set then the request is valid.
+
+      >>> request = TestRequest()
+      >>> view = SimpleView(None, request)
+      >>> validator.valid(None, request, view)
+      Traceback (most recent call last):
+      ...
+      ValueError: Protocol implementation is broken - evaluate should be False
+
+    But then the validator should not evaluate the request.
+
+      >>> validator.evaluate(None, request, view)
+      False
+
+    Valid responses
+    ===============
+
+      >>> LastModification.lastmodified = lmt
+      >>> request = TestRequest(
+      ...    environ = {'IF_UNMODIFIED_SINCE': format(lmt)})
+      >>> view = SimpleView(None, request)
+
+    Since we have a conditional header present the validator can evaluate
+    the data.
+
+      >>> validator.evaluate(None, request, view)
+      True
+
+      >>> validator.valid(None, request, view)
+      True
+
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getStatus()
+      599
+      >>> request.response.getHeader('Last-Modified')
+      'Sat, 06 Jan 2007 12:42:12 GMT'
+
+    Since the `Last-Modified` header is already set, it doesn't get overriden.
+
+      >>> LastModification.lastmodified = lmt + datetime.timedelta(seconds = 1)
+      >>> validator.updateResponse(None, request, view)
+      >>> request.response.getStatus()
+      599
+      >>> request.response.getHeader('Last-Modified')
+      'Sat, 06 Jan 2007 12:42:12 GMT'
+
+    Invalid responses
+    =================
+
+      >>> LastModification.lastmodified = lmt + datetime.timedelta(seconds = 1)
+      >>> request = TestRequest(environ = {'IF_UNMODIFIED_SINCE': format(lmt)})
+      >>> view = SimpleView(None, request)
+
+    Since we have a conditional header present the validator can evaluate
+    the data.
+
+      >>> validator.evaluate(None, request, view)
+      True
+      >>> validator.valid(None, request, view)
+      False
+
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getStatus()
+      599
+      >>> request.response.getHeader('Last-Modified')
+      'Sat, 06 Jan 2007 12:42:13 GMT'
+      >>> validator.invalidStatus(None, request, view)
+      304
+
+    Since the `Last-Modified` header is already set, it doesn't get overriden.
+
+      >>> LastModification.lastmodified = lmt
+      >>> validator.updateResponse(None, request, view)
+      >>> request.response.getHeader('Last-Modified')
+      'Sat, 06 Jan 2007 12:42:13 GMT'
+
+    Query strings
+    =============
+
+    If a query string is present in the request, then the client requested
+    a content negotiated view, or else they tried to fill out a form. Either
+    way the request must evaluate to True, and be executed.
+
+      >>> LastModification.lastmodified = lmt + datetime.timedelta(seconds = 1)
+      >>> request = TestRequest(environ = {'IF_UNMODIFIED_SINCE': format(lmt),
+      ...                                  'QUERY_STRING': 'argument=true'})
+      >>> view = SimpleView(None, request)
+      >>> validator.evaluate(None, request, view)
+      True
+      >>> validator.valid(None, request, view)
+      True
+
+    We should not update the response when there is a query string present.
+
+      >>> validator.updateResponse(None, request, view) is None
+      True
+      >>> request.response.getHeader('Last-Modified') is None
+      True
+
+    Cleanup
+    -------
+
+      >>> gsm.unregisterAdapter(LastModification,
+      ...    (None, IBrowserRequest, None))
+      True
+
+    """
+    zope.interface.implements(interfaces.IHTTPValidator)
+
+    def ifModifiedSince(self, request, mtime, header):
+        headervalue = request.getHeader(header, None)
+        if headervalue is not None:
+            # current last modification time for this view
+            last_modification_time = long(time.mktime(mtime.utctimetuple()))
+            try:
+                headervalue = long(zope.datetime.time(
+                    headervalue.split(";", 1)[0]))
+            except:
+                # error processing the HTTP-date value - return the
+                # default value.
+                return True
+            else:
+                return last_modification_time > headervalue
+        # By default all HTTP Cache validators should return True so that the
+        # request proceeds as normal.
+        return True
+
+    def evaluate(self, context, request, view):
+        return \
+             request.getHeader("If-Modified-Since", None) is not None or \
+             request.getHeader("If-UnModified-Since", None) is not None
+
+    def getDataStorage(self, context, request, view):
+        return zope.component.queryMultiAdapter(
+            (context, request, view), interfaces.ILastModificationDate)
+
+    def valid(self, context, request, view):
+        if request.get("QUERY_STRING", "") != "":
+            # a query string was supplied in the URL, so the data supplied
+            # by the ILastModificationDate does not apply to this view.
+            return True
+
+        lmd = self.getDataStorage(context, request, view)
+        if lmd is None:
+            return True
+
+        lmd = lmd.lastmodified
+        if lmd is None:
+            return True
+
+        if request.getHeader("If-Modified-Since", None) is not None:
+            return self.ifModifiedSince(request, lmd, "If-Modified-Since")
+        if request.getHeader("If-UnModified-Since", None) is not None:
+            return not self.ifModifiedSince(
+                request, lmd, "If-UnModified-Since")
+
+        raise ValueError(
+            "Protocol implementation is broken - evaluate should be False")
+
+    def invalidStatus(self, context, request, view):
+        return 304
+
+    def updateResponse(self, context, request, view):
+        if request.response.getHeader("Last-Modified", None) is None and \
+               request.get("QUERY_STRING", "") == "":
+            storage = self.getDataStorage(context, request, view)
+            if storage is not None and storage.lastmodified is not None:
+                lmd = zope.datetime.rfc1123_date(
+                    calendar.timegm(storage.lastmodified.utctimetuple()))
+                request.response.setHeader("Last-Modified", lmd)

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/tests.py
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/tests.py	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/tests.py	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,138 @@
+##############################################################################
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
+import os
+import datetime
+import unittest
+
+import persistent
+import zope.interface
+import zope.schema
+import zope.filerepresentation.interfaces
+import zope.annotation.interfaces
+import zope.publisher.browser
+from zope.security.proxy import removeSecurityProxy
+from zope.testing import doctest
+import zope.app.testing.functional
+
+import z3c.conditionalviews
+import z3c.conditionalviews.interfaces
+
+here = os.path.dirname(os.path.realpath(__file__))
+ConditionalViewLayer = zope.app.testing.functional.ZCMLLayer(
+    os.path.join(here, "ftesting.zcml"), __name__, "ConditionalViewLayer")
+
+
+class Simpleview(zope.publisher.browser.BrowserView):
+
+    @z3c.conditionalviews.ConditionalView
+    def __call__(self, letter = "x"):
+        return "%s\n%s\n" %(letter * 40, letter * 40)
+
+
+class IFile(zope.interface.Interface):
+
+    data = zope.schema.Bytes(
+        title = u"Data")
+
+
+class File(persistent.Persistent):
+    zope.interface.implements(IFile)
+
+    def __init__(self, data):
+        self.data = data
+
+
+class ViewFile(zope.publisher.browser.BrowserView):
+
+    @z3c.conditionalviews.ConditionalView
+    def __call__(self):
+        return self.context.data
+
+
+class FileFactory(object):
+    zope.interface.implements(zope.filerepresentation.interfaces.IFileFactory)
+
+    def __init__(self, container):
+        self.container = container
+
+    def __call__(self, name, content_type, data):
+        fileobj = File(data)
+        setETag(fileobj, None)
+        return fileobj
+
+
+class WriteFile(object):
+    zope.interface.implements(zope.filerepresentation.interfaces.IWriteFile)
+
+    def __init__(self, context):
+        self.context = context
+
+    def write(self, data):
+        self.context.data = data
+        setETag(self.context, None)
+
+
+class FileETag(object):
+    zope.interface.implements(z3c.conditionalviews.interfaces.IETag)
+        
+    def __init__(self, context, request, view):
+        self.context = self.__parent__ = context
+
+    weak = False
+
+    @property
+    def etag(self):
+        annots = zope.annotation.interfaces.IAnnotations(self.context)
+        return "%s:%d" %(self.context.__name__, annots["ETAG"])
+
+
+def setETag(fileobj, event):
+    annots = zope.annotation.interfaces.IAnnotations(
+        removeSecurityProxy(fileobj))
+    annots.setdefault("ETAG", 0)
+    annots["ETAG"] = annots["ETAG"] + 1
+
+
+class LastModification(object):
+    zope.interface.implements(
+        z3c.conditionalviews.interfaces.ILastModificationDate)
+
+    def __init__(self, view):
+        self.view = view
+
+    lastmodified = datetime.datetime(2007, 2, 5, 5, 43, 23)
+
+
+def integrationSetup(test):
+    test.globs["http"] = zope.app.testing.functional.HTTPCaller()
+
+
+def integrationTeardown(test):
+    del test.globs["http"]
+
+
+def test_suite():
+    readme = doctest.DocFileSuite(
+        "README.txt",
+        setUp = integrationSetup,
+        tearDown = integrationTeardown,
+        optionflags = doctest.NORMALIZE_WHITESPACE)
+    readme.layer = ConditionalViewLayer
+
+    return unittest.TestSuite((
+        doctest.DocFileSuite("validation.txt"),
+        doctest.DocTestSuite("z3c.conditionalviews.lastmodification"),
+        doctest.DocTestSuite("z3c.conditionalviews.etag"),
+        doctest.DocTestSuite("z3c.conditionalviews.adapters"),
+        readme,
+        ))

Added: z3c.conditionalviews/trunk/src/z3c/conditionalviews/validation.txt
===================================================================
--- z3c.conditionalviews/trunk/src/z3c/conditionalviews/validation.txt	                        (rev 0)
+++ z3c.conditionalviews/trunk/src/z3c/conditionalviews/validation.txt	2007-05-31 18:51:07 UTC (rev 76052)
@@ -0,0 +1,157 @@
+====================================
+Validating conditional HTTP requests
+====================================
+
+RFC2626::
+
+  The semantics of the GET method change to a 'conditional GET' if the
+  request message includes an If-Modified-Since, If-Unmodified-Since,
+  If-Match, If-None-Match, or If-Range header field. A conditional GET
+  method requests that the entity be transferred only under the
+  circumstances described by the conditional header field(s).
+
+  >>> import z3c.conditionalviews
+  >>> import z3c.conditionalviews.interfaces
+
+  >>> import zope.interface
+  >>> import zope.interface.verify
+  >>> from zope.publisher.browser import BrowserView
+  >>> from zope.publisher.browser import TestRequest
+  >>> from zope.publisher.interfaces.http import IHTTPRequest
+  >>> from zope.publisher.interfaces.browser import IBrowserView
+
+A really simple view to test the conditional views.
+
+  >>> class SimpleView(BrowserView):
+  ...    @z3c.conditionalviews.ConditionalView
+  ...    def __call__(self):
+  ...        self.request.response.setStatus(200)
+  ...        return "xxxxx"
+
+  >>> class SimpleValidator(object):
+  ...    zope.interface.implements(
+  ...        z3c.conditionalviews.interfaces.IHTTPValidator)
+  ...    def __init__(self):
+  ...       self._header = 'COND_HEADER'
+  ...       self._value = True # default value for the `COND_HEADER`
+  ...    def evaluate(self, context, request, view):
+  ...        return request.get(self._header, None) is not None
+  ...    def valid(self, context, request, view):
+  ...        return request[self._header]
+  ...    def updateResponse(self, context, request, view):
+  ...        request.response.setHeader(self._header, self._value)
+  ...    def invalidStatus(self, context, request, view):
+  ...        return 304
+
+  >>> request = TestRequest()
+  >>> view = SimpleView(None, request)
+
+No validators registered so the view runs as normal.
+
+  >>> view()
+  'xxxxx'
+  >>> request.response.getStatus()
+  200
+  >>> request.response.getHeader('COND_HEADER', None) is None
+  True
+
+Now register the HTTP validator.
+
+  >>> simplevalidator = SimpleValidator()
+  >>> zope.interface.verify.verifyObject(
+  ...    z3c.conditionalviews.interfaces.IHTTPValidator, simplevalidator)
+  True
+  >>> zope.component.getGlobalSiteManager().registerUtility(
+  ...    simplevalidator, name = 'simplevalidator')
+
+  >>> view()
+  'xxxxx'
+  >>> request.response.getStatus()
+  200
+  >>> request.response.getHeader('COND_HEADER')
+  'True'
+
+Set the `COND_HEADER` in the request which should mark the request as
+invalid.
+
+  >>> request._environ['COND_HEADER'] = True
+  >>> view()
+  'xxxxx'
+  >>> request.response.getStatus()
+  200
+  >>> request.response.getHeader('COND_HEADER')
+  'True'
+
+Setting the COND_HEADER to false, indicates that the request is `invalid`
+and as such should not be executed.
+
+  >>> request._environ['COND_HEADER'] = False
+  >>> view()
+  ''
+  >>> request.response.getStatus()
+  304
+  >>> request.response.getHeader('COND_HEADER')
+  'True'
+
+If we have two validators registered in our application then.
+
+  >>> request = TestRequest(environ = {'COND_HEADER': True,
+  ...                                  'SECOND_COND_HEADER': False})
+  >>> view = SimpleView(None, request)
+  >>> view()
+  'xxxxx'
+
+  >>> class SimpleValidator2(SimpleValidator):
+  ...    def __init__(self):
+  ...        super(SimpleValidator2, self).__init__()
+  ...        self._header = 'SECOND_COND_HEADER'
+
+  >>> simplevalidator2 = SimpleValidator2()
+  >>> zope.interface.verify.verifyObject(
+  ...    z3c.conditionalviews.interfaces.IHTTPValidator, simplevalidator2)
+  True
+  >>> zope.component.getGlobalSiteManager().registerUtility(
+  ...    simplevalidator2, name = 'simplevalidator2')
+
+In this example the second validator should return False and 
+
+  >>> request = TestRequest(environ = {'COND_HEADER': True,
+  ...                                  'SECOND_COND_HEADER': False})
+  >>> view = SimpleView(None, request)
+  >>> view()
+  'xxxxx'
+
+  >>> request = TestRequest(environ = {'COND_HEADER': False,
+  ...                                  'SECOND_COND_HEADER': False})
+  >>> view = SimpleView(None, request)
+  >>> view()
+  ''
+  >>> request.response.getStatus()
+  304
+  >>> request.response.getHeader('COND_HEADER')
+  'True'
+  >>> request.response.getHeader('SECOND_COND_HEADER')
+  'True'
+
+If the conditional HTTP header corresponding to the second protocol is missing
+and the first protocol is invalid then we should treat this as if the
+second validator was unregistered.
+
+  >>> request = TestRequest(environ = {'COND_HEADER': False})
+  >>> view = SimpleView(None, request)
+  >>> view()
+  ''
+  >>> request.response.getStatus()
+  304
+  >>> request.response.getHeader('COND_HEADER')
+  'True'
+
+Cleanup
+-------
+
+  >>> zope.component.getGlobalSiteManager().unregisterUtility(
+  ...    simplevalidator, name = 'simplevalidator')
+  True
+  >>> zope.component.getGlobalSiteManager().unregisterUtility(
+  ...    simplevalidator2, name = 'simplevalidator2')
+  True


Property changes on: z3c.conditionalviews/trunk/src/z3c/conditionalviews/validation.txt
___________________________________________________________________
Name: svn:eol-style
   + native



More information about the Checkins mailing list