[CalendarServer-changes] [4136] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Fri May 1 14:12:23 PDT 2009
Revision: 4136
http://trac.macosforge.org/projects/calendarserver/changeset/4136
Author: wsanchez at apple.com
Date: 2009-05-01 14:12:23 -0700 (Fri, 01 May 2009)
Log Message:
-----------
Back out 4134 for now
Modified Paths:
--------------
CalendarServer/trunk/lib-patches/Twisted/twisted.python.util.patch
CalendarServer/trunk/run
CalendarServer/trunk/twistedcaldav/notify.py
CalendarServer/trunk/twistedcaldav/test/test_index.py
CalendarServer/trunk/twistedcaldav/test/test_upgrade.py
CalendarServer/trunk/twistedcaldav/test/util.py
Added Paths:
-----------
CalendarServer/trunk/lib-patches/Twisted/twisted.application.app.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.conch.test.test_keys.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.internet._sslverify.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.internet.defer.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.mail.imap4.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.mail.pop3client.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.persisted.sob.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.plugins.__init__.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.python.filepath.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.runner.procmon.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.spread.pb.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_plugin.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_tcp.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web.test.test_webclient.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.__init__.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.auth.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.base.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.extensions.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.parser.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc2518.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.fileop.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.http.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.prop_common.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_acl_principal_prop_set.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_match.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.stream.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.data.quota_100.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_copy.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_pipeline.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_quota.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_static.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_stream.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_xml.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.tworequest_client.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.util.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.util.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.http.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.static.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_http.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.words.protocols.jabber.sasl_mechanisms.patch
Removed Paths:
-------------
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.application.app.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.application.app.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.application.app.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.application.app.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,13 @@
+Index: twisted/application/app.py
+===================================================================
+--- twisted/application/app.py (revision 19773)
++++ twisted/application/app.py (working copy)
+@@ -18,7 +18,7 @@
+ def runWithProfiler(reactor, config):
+ """Run reactor under standard profiler."""
+ try:
+- import profile
++ import cProfile as profile
+ except ImportError, e:
+ s = "Failed to import module profile: %s" % e
+ s += """
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.conch.test.test_keys.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.conch.test.test_keys.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.conch.test.test_keys.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.conch.test.test_keys.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,16 @@
+Index: twisted/conch/test/test_keys.py
+===================================================================
+--- twisted/conch/test/test_keys.py (revision 19773)
++++ twisted/conch/test/test_keys.py (working copy)
+@@ -4,10 +4,10 @@
+
+ try:
+ import Crypto
++ from twisted.conch.ssh import keys
+ except ImportError:
+ Crypto = None
+
+-from twisted.conch.ssh import keys
+ from twisted.trial import unittest
+
+ publicRSA_openssh = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHRivcJSkbh/C+BR3utDS555mV comment"
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.internet._sslverify.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.internet._sslverify.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.internet._sslverify.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.internet._sslverify.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,35 @@
+Index: twisted/internet/_sslverify.py
+===================================================================
+--- twisted/internet/_sslverify.py (revision 19773)
++++ twisted/internet/_sslverify.py (working copy)
+@@ -1,7 +1,11 @@
+ # -*- test-case-name: twisted.test.test_sslverify -*-
+ # Copyright 2005 Divmod, Inc. See LICENSE file for details
+
+-import itertools, md5
++import itertools
++try:
++ from hashlib import md5
++except ImportError:
++ from md5 import new as md5
+ from OpenSSL import SSL, crypto
+
+ from twisted.python import reflect, util
+@@ -666,7 +670,7 @@
+ MD5 hex digest of signature on an empty certificate request with this
+ key.
+ """
+- return md5.md5(self._emptyReq).hexdigest()
++ return md5(self._emptyReq).hexdigest()
+
+
+ def inspect(self):
+@@ -942,7 +946,7 @@
+ ctx.set_options(self._OP_ALL)
+
+ if self.enableSessions:
+- sessionName = md5.md5("%s-%d" % (reflect.qual(self.__class__), _sessionCounter())).hexdigest()
++ sessionName = md5("%s-%d" % (reflect.qual(self.__class__), _sessionCounter())).hexdigest()
+ ctx.set_session_id(sessionName)
+
+ return ctx
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.internet.defer.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.internet.defer.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.internet.defer.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.internet.defer.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,12 @@
+Index: twisted/internet/defer.py
+===================================================================
+--- twisted/internet/defer.py (revision 19773)
++++ twisted/internet/defer.py (working copy)
+@@ -998,6 +998,6 @@
+ __all__ = ["Deferred", "DeferredList", "succeed", "fail", "FAILURE", "SUCCESS",
+ "AlreadyCalledError", "TimeoutError", "gatherResults",
+ "maybeDeferred",
+- "waitForDeferred", "deferredGenerator", "inlineCallbacks",
++ "waitForDeferred", "deferredGenerator", "returnValue", "inlineCallbacks",
+ "DeferredLock", "DeferredSemaphore", "DeferredQueue",
+ ]
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.mail.imap4.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.mail.imap4.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.mail.imap4.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.mail.imap4.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,65 @@
+Index: twisted/mail/imap4.py
+===================================================================
+--- twisted/mail/imap4.py (revision 19773)
++++ twisted/mail/imap4.py (working copy)
+@@ -363,16 +363,11 @@
+ for L in self.lines:
+ names = parseNestedParens(L)
+ N = len(names)
++ # This section is patched as described in http://twistedmatrix.com/trac/ticket/1105
+ if (N >= 1 and names[0] in self._1_RESPONSES or
++ N >= 2 and names[1] in self._2_RESPONSES or
+ N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
+ send.append(L)
+- elif N >= 3 and names[1] in self._2_RESPONSES:
+- if isinstance(names[2], list) and len(names[2]) >= 1 and names[2][0] == 'FLAGS' and 'FLAGS' not in self.args:
+- unuse.append(L)
+- else:
+- send.append(L)
+- elif N >= 2 and names[1] in self._2_RESPONSES:
+- send.append(L)
+ else:
+ unuse.append(L)
+ d, self.defer = self.defer, None
+@@ -2245,10 +2240,12 @@
+ for f in fetched.get('FLAGS', []):
+ sum.append(f)
+ flags.setdefault(mId, []).extend(sum)
++ elif L.find('BYE LOGOUT') != -1:
++ pass
+ else:
+ log.msg('Unhandled unsolicited response: ' + repr(L))
+- if flags:
+- self.flagsChanged(flags)
++ #if flags:
++ # self.flagsChanged(flags)
+ if recent is not None or exists is not None:
+ self.newMessages(exists, recent)
+
+@@ -3336,6 +3333,8 @@
+ if len(data) < 2:
+ raise IllegalServerResponse("Not enough arguments", data)
+ flags.setdefault(id, {})[data[0]] = data[1]
++ if data[0] == 'FLAGS':
++ self.flagsChanged({id: data[1]})
+ del data[:2]
+ else:
+ print '(2)Ignoring ', parts
+@@ -3431,7 +3430,16 @@
+ except ValueError:
+ raise IllegalServerResponse, line
+ else:
+- info[id] = parseNestedParens(parts[2])
++ data = parseNestedParens(parts[2])[0]
++ # This section is patched as described in http://twistedmatrix.com/trac/ticket/1105
++ # XXX this will fail if 'FLAGS' is a retrieved part
++ for i in range(len(data) -1):
++ if data[i] == 'FLAGS':
++ self.flagsChanged({id: data[i+1]})
++ del data[i:i+2]
++ break
++ if data:
++ info.setdefault(id, []).append(data)
+ return info
+
+ def _fetch(self, messages, useUID=0, **terms):
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.mail.pop3client.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.mail.pop3client.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.mail.pop3client.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.mail.pop3client.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,28 @@
+Index: twisted/mail/pop3client.py
+===================================================================
+--- twisted/mail/pop3client.py (revision 19773)
++++ twisted/mail/pop3client.py (working copy)
+@@ -11,8 +11,13 @@
+ API Stability: Unstable
+ """
+
+-import re, md5
++import re
+
++try:
++ from hashlib import md5
++except ImportError:
++ from md5 import new as md5
++
+ from twisted.python import log
+ from twisted.internet import defer
+ from twisted.protocols import basic
+@@ -486,7 +491,7 @@
+ def _apop(self, username, password, challenge):
+ # Internal helper. Computes and sends an APOP response. Returns
+ # a Deferred that fires when the server responds to the response.
+- digest = md5.new(challenge + password).hexdigest()
++ digest = md5(challenge + password).hexdigest()
+ return self.apop(username, digest)
+
+ def apop(self, username, digest):
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.persisted.sob.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.persisted.sob.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.persisted.sob.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.persisted.sob.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,32 @@
+Index: twisted/persisted/sob.py
+===================================================================
+--- twisted/persisted/sob.py (revision 19773)
++++ twisted/persisted/sob.py (working copy)
+@@ -10,8 +10,12 @@
+ Maintainer: U{Moshe Zadka<mailto:moshez at twistedmatrix.com>}
+ """
+
+-import os, md5, sys
++import os, sys
+ try:
++ from hashlib import md5
++except ImportError:
++ from md5 import new as md5
++try:
+ import cPickle as pickle
+ except ImportError:
+ import pickle
+@@ -32,11 +36,11 @@
+ leftover = len(data) % cipher.block_size
+ if leftover:
+ data += ' '*(cipher.block_size - leftover)
+- return cipher.new(md5.new(passphrase).digest()[:16]).encrypt(data)
++ return cipher.new(md5(passphrase).digest()[:16]).encrypt(data)
+
+ def _decrypt(passphrase, data):
+ from Crypto.Cipher import AES
+- return AES.new(md5.new(passphrase).digest()[:16]).decrypt(data)
++ return AES.new(md5(passphrase).digest()[:16]).decrypt(data)
+
+
+ class IPersistable(Interface):
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.plugins.__init__.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.plugins.__init__.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.plugins.__init__.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.plugins.__init__.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,12 @@
+Index: twisted/plugins/__init__.py
+===================================================================
+--- twisted/plugins/__init__.py (revision 19773)
++++ twisted/plugins/__init__.py (working copy)
+@@ -12,6 +12,6 @@
+ """
+
+ import os, sys
+-__path__ = [os.path.abspath(os.path.join(x, 'twisted', 'plugins')) for x in sys.path]
++__path__ = [os.path.abspath(os.path.join(x, 'twisted', 'plugins')) for x in sys.path if not x.startswith('/System')]
+
+ __all__ = [] # nothing to see here, move along, move along
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.python.filepath.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.python.filepath.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.python.filepath.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.python.filepath.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,28 @@
+Index: twisted/python/filepath.py
+===================================================================
+--- twisted/python/filepath.py (revision 19773)
++++ twisted/python/filepath.py (working copy)
+@@ -9,9 +9,13 @@
+ import os
+ import errno
+ import random
+-import sha
+ import base64
+
++try:
++ from hashlib import sha1
++except ImportError:
++ from sha import new as sha1
++
+ from os.path import isabs, exists, normpath, abspath, splitext
+ from os.path import basename, dirname
+ from os.path import join as joinpath
+@@ -109,7 +113,7 @@
+ """
+ Create a pseudorandom, 16-character string for use in secure filenames.
+ """
+- return armor(sha.new(randomBytes(64)).digest())[:16]
++ return armor(sha1(randomBytes(64)).digest())[:16]
+
+ class _PathHelper:
+ """
Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.python.util.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.python.util.patch 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.python.util.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -1,36 +1,180 @@
Index: twisted/python/util.py
===================================================================
---- twisted/python/util.py (revision 26741)
+--- twisted/python/util.py (revision 19773)
+++ twisted/python/util.py (working copy)
-@@ -569,9 +569,30 @@
+@@ -561,83 +561,107 @@
L2.sort()
return [e[2] for e in L2]
--if pwd is None or grp is None or setgroups is None or getgroups is None:
+
-+try:
-+ from ctypes import c_int, c_char_p
-+ from ctypes.cdll import LoadLibrary
-+ from ctypes.util import find_library
++# The python implementation of initgroups below, which iterates all groups,
++# doesn't scale, so use the libc version if available:
++
+ try:
+- import pwd, grp
+- from os import setgroups, getgroups
+-
+- def _setgroups_until_success(l):
+- while(1):
+- # NASTY NASTY HACK (but glibc does it so it must be okay):
+- # In case sysconfig didn't give the right answer, find the limit
+- # on max groups by just looping, trying to set fewer and fewer
+- # groups each time until it succeeds.
++ from ctypes import *
++ import ctypes.util
+ hasCtypes = True
+except ImportError:
+ hasCtypes = False
+
+if sys.platform == "darwin" and hasCtypes:
+ import pwd
-+ libc = LoadLibrary(find_library("libc"))
- def initgroups(uid, primaryGid):
- """
-+ Call initgroups with ctypes.
-+ """
++
++ libc = cdll.LoadLibrary(ctypes.util.find_library("libc"))
++
++ def initgroups(uid, primaryGid):
+ c_gid = c_int(primaryGid)
+ username = pwd.getpwuid(uid)[0]
+ c_username = c_char_p(username)
+ return libc.initgroups(c_username, c_gid)
+
-+elif pwd is None or grp is None or setgroups is None or getgroups is None:
-+ def initgroups(uid, primaryGid):
-+ """
- Do nothing.
++else:
++ # Original twisted implementation
++ try:
++ import pwd, grp
++ from os import setgroups, getgroups
++
++ def _setgroups_until_success(l):
++ while(1):
++ # NASTY NASTY HACK (but glibc does it so it must be okay):
++ # In case sysconfig didn't give the right answer, find the limit
++ # on max groups by just looping, trying to set fewer and fewer
++ # groups each time until it succeeds.
++ try:
++ setgroups(l)
++ except ValueError:
++ # This exception comes from python itself restricting
++ # number of groups allowed.
++ if len(l) > 1:
++ del l[-1]
++ else:
++ raise
++ except OSError, e:
++ if e.errno == errno.EINVAL and len(l) > 1:
++ # This comes from the OS saying too many groups
++ del l[-1]
++ else:
++ raise
++ else:
++ # Success, yay!
++ return
++
++ def initgroups(uid, primaryGid):
++ """Initializes the group access list.
++
++ This is done by reading the group database /etc/group and using all
++ groups of which C{uid} is a member. The additional group
++ C{primaryGid} is also added to the list.
++
++ If the given user is a member of more than C{NGROUPS}, arbitrary
++ groups will be silently discarded to bring the number below that
++ limit.
++ """
+ try:
+- setgroups(l)
+- except ValueError:
+- # This exception comes from python itself restricting
+- # number of groups allowed.
+- if len(l) > 1:
+- del l[-1]
+- else:
+- raise
++ # Try to get the maximum number of groups
++ max_groups = os.sysconf("SC_NGROUPS_MAX")
++ except:
++ # No predefined limit
++ max_groups = 0
++
++ username = pwd.getpwuid(uid)[0]
++ l = []
++ if primaryGid is not None:
++ l.append(primaryGid)
++ for groupname, password, gid, userlist in grp.getgrall():
++ if username in userlist:
++ l.append(gid)
++ if len(l) == max_groups:
++ break # No more groups, ignore any more
++ try:
++ _setgroups_until_success(l)
+ except OSError, e:
+- if e.errno == errno.EINVAL and len(l) > 1:
+- # This comes from the OS saying too many groups
+- del l[-1]
++ # We might be able to remove this code now that we
++ # don't try to setgid/setuid even when not asked to.
++ if e.errno == errno.EPERM:
++ for g in getgroups():
++ if g not in l:
++ raise
+ else:
+ raise
+- else:
+- # Success, yay!
+- return
+-
+- def initgroups(uid, primaryGid):
+- """Initializes the group access list.
++
- Underlying platform support require to manipulate groups is missing.
+- This is done by reading the group database /etc/group and using all
+- groups of which C{uid} is a member. The additional group
+- C{primaryGid} is also added to the list.
++ except:
++ def initgroups(uid, primaryGid):
++ """Do nothing.
+
+- If the given user is a member of more than C{NGROUPS}, arbitrary
+- groups will be silently discarded to bring the number below that
+- limit.
+- """
+- try:
+- # Try to get the maximum number of groups
+- max_groups = os.sysconf("SC_NGROUPS_MAX")
+- except:
+- # No predefined limit
+- max_groups = 0
+-
+- username = pwd.getpwuid(uid)[0]
+- l = []
+- if primaryGid is not None:
+- l.append(primaryGid)
+- for groupname, password, gid, userlist in grp.getgrall():
+- if username in userlist:
+- l.append(gid)
+- if len(l) == max_groups:
+- break # No more groups, ignore any more
+- try:
+- _setgroups_until_success(l)
+- except OSError, e:
+- # We might be able to remove this code now that we
+- # don't try to setgid/setuid even when not asked to.
+- if e.errno == errno.EPERM:
+- for g in getgroups():
+- if g not in l:
+- raise
+- else:
+- raise
+-
++ Underlying platform support require to manipulate groups is missing.
++ """
+
+-except:
+- def initgroups(uid, primaryGid):
+- """Do nothing.
+
+- Underlying platform support require to manipulate groups is missing.
+- """
+-
+-
+ def switchUID(uid, gid, euid=False):
+ if euid:
+ setuid = os.seteuid
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.runner.procmon.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.runner.procmon.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.runner.procmon.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.runner.procmon.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,39 @@
+Index: twisted/runner/procmon.py
+===================================================================
+--- twisted/runner/procmon.py (revision 19773)
++++ twisted/runner/procmon.py (working copy)
+@@ -59,6 +59,9 @@
+
+ disconnecting = 0
+
++ def loseConnection(self):
++ pass
++
+ transport = DummyTransport()
+
+ class LineLogger(basic.LineReceiver):
+@@ -130,10 +133,10 @@
+ self.consistency = reactor.callLater(self.consistencyDelay,
+ self._checkConsistency)
+
+- def addProcess(self, name, args, uid=None, gid=None):
++ def addProcess(self, name, args, uid=None, gid=None, env={}):
+ if self.processes.has_key(name):
+ raise KeyError("remove %s first" % name)
+- self.processes[name] = args, uid, gid
++ self.processes[name] = args, uid, gid, env
+ if self.active:
+ self.startProcess(name)
+
+@@ -175,9 +178,9 @@
+ p = self.protocols[name] = LoggingProtocol()
+ p.service = self
+ p.name = name
+- args, uid, gid = self.processes[name]
++ args, uid, gid, env = self.processes[name]
+ self.timeStarted[name] = time.time()
+- reactor.spawnProcess(p, args[0], args, uid=uid, gid=gid)
++ reactor.spawnProcess(p, args[0], args, uid=uid, gid=gid, env=env)
+
+ def _forceStopProcess(self, proc):
+ try:
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.spread.pb.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.spread.pb.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.spread.pb.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.spread.pb.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,53 @@
+Index: twisted/spread/pb.py
+===================================================================
+--- twisted/spread/pb.py (revision 19773)
++++ twisted/spread/pb.py (working copy)
+@@ -64,7 +64,11 @@
+ except ImportError:
+ import StringIO
+
+-import md5
++try:
++ from hashlib import md5
++except ImportError:
++ from md5 import new as md5
++
+ import random
+ import new
+ import types
+@@ -1003,10 +1007,10 @@
+
+ This is useful for challenge/response authentication.
+ """
+- m = md5.new()
++ m = md5()
+ m.update(password)
+ hashedPassword = m.digest()
+- m = md5.new()
++ m = md5()
+ m.update(hashedPassword)
+ m.update(challenge)
+ doubleHashedPassword = m.digest()
+@@ -1017,7 +1021,7 @@
+ crap = ''
+ for x in range(random.randrange(15,25)):
+ crap = crap + chr(random.randint(65,90))
+- crap = md5.new(crap).digest()
++ crap = md5(crap).digest()
+ return crap
+
+
+@@ -1226,11 +1230,11 @@
+
+ # IUsernameHashedPassword:
+ def checkPassword(self, password):
+- return self.checkMD5Password(md5.md5(password).digest())
++ return self.checkMD5Password(md5(password).digest())
+
+ # IUsernameMD5Password
+ def checkMD5Password(self, md5Password):
+- md = md5.new()
++ md = md5()
+ md.update(md5Password)
+ md.update(self.challenge)
+ correct = md.digest()
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_plugin.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_plugin.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_plugin.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_plugin.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,11 @@
+Index: twisted/test/test_plugin.py
+===================================================================
+--- twisted/test/test_plugin.py (revision 19773)
++++ twisted/test/test_plugin.py (working copy)
+@@ -518,3 +518,6 @@
+ self.assertEqual(len(self.flushLoggedErrors()), 0)
+ self.assertIn('one', self.getAllPlugins())
+ self.assertEqual(len(self.flushLoggedErrors()), 1)
++
++
++ test_newPluginsOnReadOnlyPath.skip = "Seems not to work on OS X 10.4 buildbot machine."
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_tcp.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_tcp.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_tcp.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.test.test_tcp.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,13 @@
+Index: twisted/test/test_tcp.py
+===================================================================
+--- twisted/test/test_tcp.py (revision 19773)
++++ twisted/test/test_tcp.py (working copy)
+@@ -1294,6 +1294,8 @@
+ self.client.transport.loseConnection()
+ log.flushErrors(RuntimeError)
+ return d.addCallback(check)
++
++ testReadNotificationRaises.todo = "self.f.protocol is None"
+
+ def testWriteNotificationRaises(self):
+ self.client.writeConnectionLost = self.aBug
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web.test.test_webclient.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web.test.test_webclient.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web.test.test_webclient.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web.test.test_webclient.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,13 @@
+Index: twisted/web/test/test_webclient.py
+===================================================================
+--- twisted/web/test/test_webclient.py (revision 19773)
++++ twisted/web/test/test_webclient.py (working copy)
+@@ -206,6 +206,8 @@
+ d.addBoth(self._cleanupDownloadPageError3)
+ return d
+
++ testDownloadPageError3.skip = "Seems not to work on OS X."
++
+ def _cleanupDownloadPageError3(self, ignored):
+ os.chmod("unwritable", 0700)
+ os.unlink("unwritable")
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,29 @@
+Index: twisted/web2/auth/basic.py
+===================================================================
+--- twisted/web2/auth/basic.py (revision 19773)
++++ twisted/web2/auth/basic.py (working copy)
+@@ -1,6 +1,7 @@
+ # -*- test-case-name: twisted.web2.test.test_httpauth -*-
+
+ from twisted.cred import credentials, error
++from twisted.internet.defer import succeed
+ from twisted.web2.auth.interfaces import ICredentialFactory
+
+ from zope.interface import implements
+@@ -18,7 +19,7 @@
+ self.realm = realm
+
+ def getChallenge(self, peer):
+- return {'realm': self.realm}
++ return succeed({'realm': self.realm})
+
+ def decode(self, response, request):
+ try:
+@@ -28,6 +29,6 @@
+
+ creds = creds.split(':', 1)
+ if len(creds) == 2:
+- return credentials.UsernamePassword(*creds)
++ return succeed(credentials.UsernamePassword(*creds))
+ else:
+ raise error.LoginFailed('Invalid credentials')
Deleted: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -1,22 +0,0 @@
-Index: twisted/web2/auth/digest.py
-===================================================================
---- twisted/web2/auth/digest.py (revision 26741)
-+++ twisted/web2/auth/digest.py (working copy)
-@@ -158,6 +158,17 @@
- algo, nonce, nc, cnonce, qop, self.method, uri, None
- )
-
-+ if expected == response:
-+ return True
-+
-+ # IE7 sends cnonce and nc values, but auth fails if they are used.
-+ # So try again without them...
-+ # They can be omitted for backwards compatibility [RFC 2069].
-+ expected = calcResponse(
-+ calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
-+ algo, nonce, None, None, qop, self.method, uri, None
-+ )
-+
- return expected == response
-
- def checkHash(self, digestHash):
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,135 @@
+Index: twisted/web2/auth/digest.py
+===================================================================
+--- twisted/web2/auth/digest.py (revision 19773)
++++ twisted/web2/auth/digest.py (working copy)
+@@ -8,19 +8,28 @@
+ import time
+
+ from twisted.cred import credentials, error
++from twisted.internet.defer import succeed
+ from zope.interface import implements, Interface
+
+ from twisted.web2.auth.interfaces import ICredentialFactory
++from twisted.web2.http_headers import tokenize
++from twisted.web2.http_headers import Token
++from twisted.web2.http_headers import split
++from twisted.web2.http_headers import parseKeyValue
+
+-import md5, sha
++try:
++ from hashlib import md5, sha1
++except ImportError:
++ from md5 import new as md5
++ from sha import new as sha1
+ import random, sys
+
+ # The digest math
+
+ algorithms = {
+- 'md5': md5.new,
+- 'md5-sess': md5.new,
+- 'sha': sha.new,
++ 'md5': md5,
++ 'md5-sess': md5,
++ 'sha': sha1,
+ }
+
+ # DigestCalcHA1
+@@ -153,7 +162,18 @@
+ calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
+ algo, nonce, nc, cnonce, qop, self.method, uri, None
+ )
++
++ if expected == response:
++ return True
+
++ # IE7 sends cnonce and nc values, but auth fails if they are used.
++ # So try again without them...
++ # They can be omitted for backwards compatibility [RFC 2069].
++ expected = calcResponse(
++ calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
++ algo, nonce, None, None, qop, self.method, uri, None
++ )
++
+ return expected == response
+
+ def checkHash(self, digestHash):
+@@ -228,9 +248,9 @@
+ # Now, what we do is encode the nonce, client ip and a timestamp
+ # in the opaque value with a suitable digest
+ key = "%s,%s,%s" % (nonce, clientip, str(int(self._getTime())))
+- digest = md5.new(key + self.privateKey).hexdigest()
++ digest = md5(key + self.privateKey).hexdigest()
+ ekey = key.encode('base64')
+- return "%s-%s" % (digest, ekey.strip('\n'))
++ return "%s-%s" % (digest, ekey.replace('\n', ''))
+
+ def verifyOpaque(self, opaque, nonce, clientip):
+ """
+@@ -274,7 +294,7 @@
+ 'Invalid response, incompatible opaque/nonce too old')
+
+ # Verify the digest
+- digest = md5.new(key + self.privateKey).hexdigest()
++ digest = md5(key + self.privateKey).hexdigest()
+ if digest != opaqueParts[0]:
+ raise error.LoginFailed('Invalid response, invalid opaque value')
+
+@@ -293,11 +313,12 @@
+ c = self.generateNonce()
+ o = self.generateOpaque(c, peer.host)
+
+- return {'nonce': c,
+- 'opaque': o,
+- 'qop': 'auth',
+- 'algorithm': self.algorithm,
+- 'realm': self.realm}
++ return succeed({'nonce': c,
++ 'opaque': o,
++ 'qop': 'auth',
++ 'algorithm': self.algorithm,
++ 'realm': self.realm,
++ })
+
+ def decode(self, response, request):
+ """
+@@ -315,18 +336,18 @@
+ @raise: L{error.LoginFailed} if the response does not contain a
+ username, a nonce, an opaque, or if the opaque is invalid.
+ """
+- def unq(s):
+- if s[0] == s[-1] == '"':
+- return s[1:-1]
+- return s
+ response = ' '.join(response.splitlines())
+- parts = response.split(',')
+-
+- auth = {}
+-
+- for (k, v) in [p.split('=', 1) for p in parts]:
+- auth[k.strip()] = unq(v.strip())
+-
++
++ try:
++ parts = split(tokenize((response,), foldCase=False), Token(","))
++
++ auth = {}
++
++ for (k, v) in [parseKeyValue(p) for p in parts]:
++ auth[k.strip()] = v.strip()
++ except ValueError:
++ raise error.LoginFailed('Invalid response.')
++
+ username = auth.get('username')
+ if not username:
+ raise error.LoginFailed('Invalid response, no username given.')
+@@ -342,7 +363,7 @@
+ auth.get('nonce'),
+ request.remoteAddr.host):
+
+- return DigestedCredentials(username,
++ return succeed(DigestedCredentials(username,
+ request.method,
+ self.realm,
+- auth)
++ auth))
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,22 @@
+Index: twisted/web2/auth/interfaces.py
+===================================================================
+--- twisted/web2/auth/interfaces.py (revision 19773)
++++ twisted/web2/auth/interfaces.py (working copy)
+@@ -18,7 +18,7 @@
+ @param peer: The client's address
+
+ @rtype: C{dict}
+- @return: dictionary of challenge arguments
++ @return: deferred returning dictionary of challenge arguments
+ """
+
+ def decode(response, request):
+@@ -32,7 +32,7 @@
+ @type request: L{twisted.web2.server.Request}
+ @param request: the request being processed
+
+- @return: ICredentials
++ @return: deferred returning ICredentials
+ """
+
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,205 @@
+Index: twisted/web2/auth/wrapper.py
+===================================================================
+--- twisted/web2/auth/wrapper.py (revision 19773)
++++ twisted/web2/auth/wrapper.py (working copy)
+@@ -5,32 +5,45 @@
+ """
+ from zope.interface import implements, directlyProvides
+ from twisted.cred import error, credentials
+-from twisted.python import failure
+ from twisted.web2 import responsecode
+ from twisted.web2 import http
+ from twisted.web2 import iweb
+ from twisted.web2.auth.interfaces import IAuthenticatedRequest
++from twisted.internet.defer import inlineCallbacks, returnValue
+
+ class UnauthorizedResponse(http.StatusResponse):
+ """A specialized response class for generating www-authenticate headers
+ from the given L{CredentialFactory} instances
+ """
+
+- def __init__(self, factories, remoteAddr=None):
++ @staticmethod
++ def makeResponse(factories, remoteAddr=None):
++
++ response = UnauthorizedResponse()
++ d = response.generateHeaders(factories, remoteAddr)
++ d.addCallback(lambda _:response)
++ return d
++
++ def __init__(self):
++
++ super(UnauthorizedResponse, self).__init__(
++ responsecode.UNAUTHORIZED,
++ "You are not authorized to access this resource."
++ )
++
++ @inlineCallbacks
++ def generateHeaders(self, factories, remoteAddr=None):
+ """
+ @param factories: A L{dict} of {'scheme': ICredentialFactory}
+
+ @param remoteAddr: An L{IAddress} for the connecting client.
+ """
+
+- super(UnauthorizedResponse, self).__init__(
+- responsecode.UNAUTHORIZED,
+- "You are not authorized to access this resource.")
+-
+ authHeaders = []
+ for factory in factories.itervalues():
+- authHeaders.append((factory.scheme,
+- factory.getChallenge(remoteAddr)))
++ scheme = factory.scheme
++ challenge = (yield factory.getChallenge(remoteAddr))
++ authHeaders.append((scheme, challenge,))
+
+ self.headers.setHeader('www-authenticate', authHeaders)
+
+@@ -71,8 +84,6 @@
+
+ def _loginSucceeded(self, avatar, request):
+ """
+- Callback for successful login.
+-
+ @param avatar: A tuple of the form (interface, avatar) as
+ returned by your realm.
+
+@@ -85,6 +96,7 @@
+
+ directlyProvides(request, IAuthenticatedRequest)
+
++ @inlineCallbacks
+ def _addAuthenticateHeaders(request, response):
+ """
+ A response filter that adds www-authenticate headers
+@@ -93,14 +105,16 @@
+ """
+ if response.code == responsecode.UNAUTHORIZED:
+ if not response.headers.hasHeader('www-authenticate'):
+- newResp = UnauthorizedResponse(self.credentialFactories,
+- request.remoteAddr)
++ newResp = (yield UnauthorizedResponse.makeResponse(
++ self.credentialFactories,
++ request.remoteAddr
++ ))
+
+ response.headers.setHeader(
+ 'www-authenticate',
+ newResp.headers.getHeader('www-authenticate'))
+
+- return response
++ returnValue(response)
+
+ _addAuthenticateHeaders.handleErrors = True
+
+@@ -108,27 +122,22 @@
+
+ return self.wrappedResource
+
+- def _loginFailed(self, result, request):
++ @inlineCallbacks
++ def _loginFailed(self, request):
+ """
+- Errback for failed login.
+-
+- @param result: L{Failure} returned by portal.login
+-
+ @param request: L{IRequest} that encapsulates this auth
+ attempt.
+
+- @return: A L{Failure} containing an L{HTTPError} containing the
+- L{UnauthorizedResponse} if C{result} is an L{UnauthorizedLogin}
+- or L{UnhandledCredentials} error
++ @raise: always rais HTTPError
+ """
+- result.trap(error.UnauthorizedLogin, error.UnhandledCredentials)
+
+- return failure.Failure(
+- http.HTTPError(
+- UnauthorizedResponse(
+- self.credentialFactories,
+- request.remoteAddr)))
++ response = (yield UnauthorizedResponse.makeResponse(
++ self.credentialFactories,
++ request.remoteAddr
++ ))
++ raise http.HTTPError(response)
+
++ @inlineCallbacks
+ def login(self, factory, response, request):
+ """
+ @param factory: An L{ICredentialFactory} that understands the given
+@@ -142,50 +151,48 @@
+ or a failure containing an L{UnauthorizedResponse}
+ """
+ try:
+- creds = factory.decode(response, request)
++ creds = (yield factory.decode(response, request))
+ except error.LoginFailed:
+- raise http.HTTPError(UnauthorizedResponse(
+- self.credentialFactories,
+- request.remoteAddr))
++ yield self._loginFailed(request)
+
++ try:
++ avatar = (yield self.portal.login(creds, None, *self.interfaces))
++ except (error.UnauthorizedLogin, error.UnhandledCredentials):
++ yield self._loginFailed(request)
++ resource = self._loginSucceeded(avatar, request)
++ returnValue(resource)
+
+- return self.portal.login(creds, None, *self.interfaces
+- ).addCallbacks(self._loginSucceeded,
+- self._loginFailed,
+- (request,), None,
+- (request,), None)
+-
++ @inlineCallbacks
+ def authenticate(self, request):
+ """
+- Attempt to authenticate the givin request
++ Attempt to authenticate the giving request
+
+ @param request: An L{IRequest} to be authenticated.
+ """
+ authHeader = request.headers.getHeader('authorization')
+
+ if authHeader is None:
+- return self.portal.login(credentials.Anonymous(),
+- None,
+- *self.interfaces
+- ).addCallbacks(self._loginSucceeded,
+- self._loginFailed,
+- (request,), None,
+- (request,), None)
++ try:
++ avatar = (yield self.portal.login(credentials.Anonymous(), None, *self.interfaces))
++ except:
++ yield self._loginFailed(request)
++ resource = self._loginSucceeded(avatar, request)
++ returnValue(resource)
+
+ elif authHeader[0] not in self.credentialFactories:
+- raise http.HTTPError(UnauthorizedResponse(
+- self.credentialFactories,
+- request.remoteAddr))
++ yield self._loginFailed(request)
+ else:
+- return self.login(self.credentialFactories[authHeader[0]],
+- authHeader[1], request)
++ result = (yield self.login(self.credentialFactories[authHeader[0]], authHeader[1], request))
++ returnValue(result)
+
+ def locateChild(self, request, seg):
+ """
+ Authenticate the request then return the C{self.wrappedResource}
+ and the unmodified segments.
+ """
+- return self.authenticate(request), seg
++ d = self.authenticate(request)
++ d.addCallback(lambda result:(result, seg,))
++ return d
+
+ def renderHTTP(self, request):
+ """
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.__init__.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.__init__.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.__init__.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.__init__.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,12 @@
+Index: twisted/web2/dav/__init__.py
+===================================================================
+--- twisted/web2/dav/__init__.py (revision 19773)
++++ twisted/web2/dav/__init__.py (working copy)
+@@ -45,6 +45,7 @@
+ "noneprops",
+ "resource",
+ "static",
++ "stream",
+ "util",
+ "xattrprops",
+ ]
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.auth.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.auth.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.auth.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.auth.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,83 @@
+Index: twisted/web2/dav/auth.py
+===================================================================
+--- twisted/web2/dav/auth.py (revision 19773)
++++ twisted/web2/dav/auth.py (working copy)
+@@ -5,7 +5,13 @@
+ from twisted.web2.dav import davxml
+ from twisted.web2.dav.davxml import twisted_private_namespace
+
+-__all__ = ["PrincipalCredentials", "AuthenticationWrapper"]
++__all__ = [
++ "IPrincipal",
++ "DavRealm",
++ "IPrincipalCredentials",
++ "PrincipalCredentials",
++ "AuthenticationWrapper",
++]
+
+ class AuthenticationWrapper(WrapperResource):
+ def __init__(self, resource, portal, credentialFactories, loginInterfaces):
+@@ -40,7 +46,7 @@
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IPrincipal in interfaces:
+- return IPrincipal, davxml.Principal(davxml.HRef(avatarId))
++ return IPrincipal, davxml.Principal(davxml.HRef(avatarId[0])), davxml.Principal(davxml.HRef(avatarId[1]))
+
+ raise NotImplementedError("Only IPrincipal interface is supported")
+
+@@ -52,33 +58,44 @@
+ class PrincipalCredentials(object):
+ implements(IPrincipalCredentials)
+
+- def __init__(self, principal, principalURI, credentials):
+- self.principal = principal
+- self.principalURI = principalURI
++ def __init__(self, authnPrincipal, authzPrincipal, credentials):
++ """
++ Initialize with both authentication and authorization values. Note that in most cases theses will be the same
++ since HTTP auth makes no distinction between the two - but we may be layering some addition auth on top of this
++ (.e.g.. proxy auth, cookies, forms etc) that make result in authentication and authorization being different.
++
++ @param authnPrincipal: L{IDAVPrincipalResource} for the authenticated principal.
++ @param authnURI: C{str} containing the URI of the authenticated principal.
++ @param authzPrincipal: L{IDAVPrincipalResource} for the authorized principal.
++ @param authzURI: C{str} containing the URI of the authorized principal.
++ @param credentials: L{ICredentials} for the authentication credentials.
++ """
++ self.authnPrincipal = authnPrincipal
++ self.authzPrincipal = authzPrincipal
+ self.credentials = credentials
+
+ def checkPassword(self, password):
+ return self.credentials.checkPassword(password)
+
+
+-class TwistedPropertyChecker:
++class TwistedPropertyChecker(object):
+ implements(checkers.ICredentialsChecker)
+
+ credentialInterfaces = (IPrincipalCredentials,)
+
+- def _cbPasswordMatch(self, matched, principalURI):
++ def _cbPasswordMatch(self, matched, principalURIs):
+ if matched:
+- return principalURI
++ # We return both URIs
++ return principalURIs
+ else:
+- raise error.UnauthorizedLogin(
+- "Bad credentials for: %s" % (principalURI,))
++ raise error.UnauthorizedLogin("Bad credentials for: %s" % (principalURIs[0],))
+
+ def requestAvatarId(self, credentials):
+ pcreds = IPrincipalCredentials(credentials)
+- pswd = str(pcreds.principal.readDeadProperty(TwistedPasswordProperty))
++ pswd = str(pcreds.authnPrincipal.readDeadProperty(TwistedPasswordProperty))
+
+ d = defer.maybeDeferred(credentials.checkPassword, pswd)
+- d.addCallback(self._cbPasswordMatch, pcreds.principalURI)
++ d.addCallback(self._cbPasswordMatch, (pcreds.authnPrincipal.principalURL(), pcreds.authzPrincipal.principalURL()))
+ return d
+
+ ##
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,30 @@
+Index: twisted/web2/dav/davxml.py
+===================================================================
+--- twisted/web2/dav/davxml.py (revision 19773)
++++ twisted/web2/dav/davxml.py (working copy)
+@@ -45,6 +45,8 @@
+ from twisted.web2.dav.element.rfc2518 import *
+ from twisted.web2.dav.element.rfc3253 import *
+ from twisted.web2.dav.element.rfc3744 import *
++from twisted.web2.dav.element.rfc4331 import *
++from twisted.web2.dav.element.extensions import *
+
+ #
+ # Register all XML elements with the parser
+@@ -56,11 +58,15 @@
+ import twisted.web2.dav.element.rfc2518
+ import twisted.web2.dav.element.rfc3253
+ import twisted.web2.dav.element.rfc3744
++import twisted.web2.dav.element.rfc4331
++import twisted.web2.dav.element.extensions
+
+ __all__ = (
+ registerElements(twisted.web2.dav.element.base ) +
+ registerElements(twisted.web2.dav.element.parser ) +
+ registerElements(twisted.web2.dav.element.rfc2518) +
+ registerElements(twisted.web2.dav.element.rfc3253) +
+- registerElements(twisted.web2.dav.element.rfc3744)
++ registerElements(twisted.web2.dav.element.rfc3744) +
++ registerElements(twisted.web2.dav.element.rfc4331) +
++ registerElements(twisted.web2.dav.element.extensions)
+ )
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,11 @@
+Index: twisted/web2/dav/element/__init__.py
+===================================================================
+--- twisted/web2/dav/element/__init__.py (revision 19773)
++++ twisted/web2/dav/element/__init__.py (working copy)
+@@ -35,4 +35,6 @@
+ "rfc2518",
+ "rfc3253",
+ "rfc3744",
++ "rfc4331",
++ "extensions",
+ ]
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.base.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.base.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.base.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.base.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,267 @@
+Index: twisted/web2/dav/element/base.py
+===================================================================
+--- twisted/web2/dav/element/base.py (revision 19773)
++++ twisted/web2/dav/element/base.py (working copy)
+@@ -45,7 +45,7 @@
+ ]
+
+ import string
+-import StringIO
++import cStringIO as StringIO
+ import xml.dom.minidom
+
+ import datetime
+@@ -90,6 +90,35 @@
+ raise NotImplementedError("WebDAVElement subclass %s is not implemented."
+ % (self.__class__.__name__,))
+
++ my_children = []
++
++ allowPCDATA = self.allowed_children.has_key(PCDATAElement)
++
++ for child in children:
++ if child is None:
++ continue
++
++ if isinstance(child, (str, unicode)):
++ child = PCDATAElement(child)
++
++ if isinstance(child, PCDATAElement) and not allowPCDATA:
++ continue
++
++ my_children.append(child)
++
++ self.children = tuple(my_children)
++
++ self.attributes = attributes
++
++ def validate(self):
++
++ children = self.children
++ attributes = self.attributes
++
++ if self.allowed_children is None:
++ raise NotImplementedError("WebDAVElement subclass %s is not implemented."
++ % (self.__class__.__name__,))
++
+ #
+ # Validate that children are of acceptable types
+ #
+@@ -102,13 +131,10 @@
+ my_children = []
+
+ for child in children:
+- if child is None:
+- continue
+
+- if isinstance(child, (str, unicode)):
+- child = PCDATAElement(child)
+-
+ assert isinstance(child, (WebDAVElement, PCDATAElement)), "Not an element: %r" % (child,)
++
++ child.validate()
+
+ for allowed, (min, max) in allowed_children.items():
+ if type(allowed) == type and isinstance(child, allowed):
+@@ -145,24 +171,26 @@
+
+ if self.allowed_attributes:
+ for name in attributes:
+- if name in self.allowed_attributes:
+- my_attributes[name] = attributes[name]
+- else:
+- log.msg("Attribute %s is unexpected and therefore ignored in %s element"
+- % (name, self.sname()))
++ if name not in self.allowed_attributes:
++ log.msg("Attribute %s is unexpected in %s element" % (name, self.sname()))
++ my_attributes[name] = attributes[name]
+
+ for name, required in self.allowed_attributes.items():
+ if required and name not in my_attributes:
+ raise ValueError("Attribute %s is required in %s element"
+ % (name, self.sname()))
+
+- elif not isinstance(self, WebDAVUnknownElement):
+- if attributes:
+- log.msg("Attributes %s are unexpected and therefore ignored in %s element"
++ else:
++ if not isinstance(self, WebDAVUnknownElement) and attributes:
++ log.msg("Attributes %s are unexpected in %s element"
+ % (attributes.keys(), self.sname()))
++ my_attributes.update(attributes)
+
+ self.attributes = my_attributes
+
++ def emptyCopy(self):
++ return self.__class__()
++
+ def __str__(self):
+ return self.sname()
+
+@@ -190,14 +218,93 @@
+ return child in self.children
+
+ def writeXML(self, output):
+- document = xml.dom.minidom.Document()
+- self.addToDOM(document, None)
+- PrintXML(document, stream=output)
++ # FIXME: Now have a 'fast' write implementation as well as previous PyXML-based one.
++ # For now the fast one is the default and we will test to see if its good enough.
++
++ usePyXML = False
++ if usePyXML:
++ document = xml.dom.minidom.Document()
++ self.addToDOM(document, None)
++ PrintXML(document, stream=output)
++ else:
++ output.write("<?xml version='1.0' encoding='UTF-8'?>\r\n")
++ self.writeToStream(output, "", 0, True)
++ output.write("\r\n")
++
++ def writeToStream(self, output, ns, level, pretty):
++ """
++ Fast XML output.
+
++ @param output: C{stream} to write to.
++ @param ns: C{str} containing the namespace of the enclosing element.
++ @param level: C{int} containing the element nesting level (starts at 0).
++ @param pretty: C{bool} whether to use 'pretty' formatted output or not.
++ """
++
++ # Do pretty indent
++ if pretty and level:
++ output.write(" " * level)
++
++ # Check for empty element (one with either no children or a single PCDATA that is itself empty)
++ if (len(self.children) == 0 or
++ (len(self.children) == 1 and isinstance(self.children[0], PCDATAElement) and len(str(self.children[0])) == 0)):
++
++ # Write out any attributes or the namespace if difference from enclosing element.
++ if self.attributes or (ns != self.namespace):
++ output.write("<%s" % (self.name,))
++ for name, value in self.attributes.iteritems():
++ self.writeAttributeToStream(output, name, value)
++ if ns != self.namespace:
++ output.write(" xmlns='%s'" % (self.namespace,))
++ output.write("/>")
++ else:
++ output.write("<%s/>" % (self.name,))
++ else:
++ # Write out any attributes or the namespace if difference from enclosing element.
++ if self.attributes or (ns != self.namespace):
++ output.write("<%s" % (self.name,))
++ for name, value in self.attributes.iteritems():
++ self.writeAttributeToStream(output, name, value)
++ if ns != self.namespace:
++ output.write(" xmlns='%s'" % (self.namespace,))
++ ns = self.namespace
++ output.write(">")
++ else:
++ output.write("<%s>" % (self.name,))
++
++ # Determine nature of children when doing pretty print: we do
++ # not want to insert CRLFs or any other whitespace in PCDATA.
++ hasPCDATA = False
++ for child in self.children:
++ if isinstance(child, PCDATAElement):
++ hasPCDATA = True
++ break
++
++ # Write out the children.
++ if pretty and not hasPCDATA:
++ output.write("\r\n")
++ for child in self.children:
++ child.writeToStream(output, ns, level+1, pretty)
++
++ # Close the element.
++ if pretty and not hasPCDATA and level:
++ output.write(" " * level)
++ output.write("</%s>" % (self.name,))
++
++ if pretty and level:
++ output.write("\r\n")
++
++ def writeAttributeToStream(self, output, name, value):
++
++ # Quote any single quotes. We do not need to be any smarter than this.
++ value = value.replace("'", "'")
++
++ output.write(" %s='%s'" % (name, value,))
++
+ def toxml(self):
+ output = StringIO.StringIO()
+ self.writeXML(output)
+- return output.getvalue()
++ return str(output.getvalue())
+
+ def element(self, document):
+ element = document.createElementNS(self.namespace, self.name)
+@@ -285,6 +392,9 @@
+
+ self.data = data
+
++ def validate(self):
++ pass
++
+ def __str__(self):
+ return str(self.data)
+
+@@ -324,6 +434,22 @@
+ log.err("Invalid PCDATA: %r" % (self.data,))
+ raise
+
++ def writeToStream(self, output, ns, level, pretty):
++ # Do escaping/CDATA behavior
++ if "\r" in self.data or "\n" in self.data:
++ # Do CDATA
++ cdata = "<![CDATA[%s]]>" % (self.data.replace("]]>", "]]>"),)
++ else:
++ cdata = self.data
++ if "&" in cdata:
++ cdata = cdata.replace("&", "&")
++ if "<" in cdata:
++ cdata = cdata.replace("<", "<")
++ if ">" in cdata:
++ cdata = cdata.replace(">", ">")
++
++ output.write(cdata)
++
+ class WebDAVOneShotElement (WebDAVElement):
+ """
+ Element with exactly one WebDAVEmptyElement child and no attributes.
+@@ -364,6 +490,18 @@
+ PCDATAElement: (0, None),
+ }
+
++ def qname(self):
++ return (self.namespace, self.name)
++
++ def sname(self):
++ return "{%s}%s" % (self.namespace, self.name)
++
++ def emptyCopy(self):
++ copied = self.__class__()
++ copied.name = self.name
++ copied.namespace = self.namespace
++ return copied
++
+ class WebDAVEmptyElement (WebDAVElement):
+ """
+ WebDAV element with no contents.
+@@ -388,6 +526,7 @@
+ """
+ WebDAV element containing PCDATA.
+ """
++ @classmethod
+ def fromString(clazz, string):
+ if string is None:
+ return clazz()
+@@ -396,8 +535,6 @@
+ else:
+ return clazz(PCDATAElement(str(string)))
+
+- fromString = classmethod(fromString)
+-
+ allowed_children = { PCDATAElement: (0, None) }
+
+ def __str__(self):
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.extensions.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.extensions.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.extensions.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.extensions.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,44 @@
+Index: twisted/web2/dav/element/extensions.py
+===================================================================
+--- twisted/web2/dav/element/extensions.py (revision 0)
++++ twisted/web2/dav/element/extensions.py (revision 0)
+@@ -0,0 +1,39 @@
++##
++# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++#
++##
++
++from twisted.web2.dav.element.base import *
++
++##
++# draft-sanchez-webdav-current-principal
++##
++
++class CurrentUserPrincipal(WebDAVElement):
++ """
++ Current principal information
++ """
++
++ name = "current-user-principal"
++ allowed_children = {
++ (dav_namespace, "href" ) : (0, 1),
++ (dav_namespace, "unauthenticated" ) : (0, 1),
++ }
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.parser.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.parser.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.parser.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.parser.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,81 @@
+Index: twisted/web2/dav/element/parser.py
+===================================================================
+--- twisted/web2/dav/element/parser.py (revision 19773)
++++ twisted/web2/dav/element/parser.py (working copy)
+@@ -37,7 +37,7 @@
+ "WebDAVDocument",
+ ]
+
+-import StringIO
++import cStringIO as StringIO
+ import xml.dom.minidom
+ import xml.sax
+
+@@ -106,6 +106,12 @@
+ "children" : [],
+ }]
+
++ # Keep a cache of the subclasses we create for unknown XML
++ # elements, so that we don't create multiple classes for the
++ # same element; it's fairly typical for elements to appear
++ # multiple times in a document.
++ self.unknownElementClasses = {}
++
+ def endDocument(self):
+ top = self.stack[-1]
+
+@@ -115,6 +121,7 @@
+ assert len(top["children"]) is 1, "Must have exactly one root element, got %d" % len(top["children"])
+
+ self.dom = WebDAVDocument(top["children"][0])
++ del(self.unknownElementClasses)
+
+ def startElementNS(self, name, qname, attributes):
+ attributes_dict = {}
+@@ -125,13 +132,17 @@
+
+ tag_namespace, tag_name = name
+
+- if (name not in elements_by_tag_name):
+- class UnknownElement (WebDAVUnknownElement):
+- namespace = tag_namespace
+- name = tag_name
+- element_class = UnknownElement
++ if name in elements_by_tag_name:
++ element_class = elements_by_tag_name[name]
++ elif name in self.unknownElementClasses:
++ element_class = self.unknownElementClasses[name]
+ else:
+- element_class = elements_by_tag_name[name]
++ def element_class(*args, **kwargs):
++ element = WebDAVUnknownElement(*args, **kwargs)
++ element.namespace = tag_namespace
++ element.name = tag_name
++ return element
++ self.unknownElementClasses[name] = element_class
+
+ self.stack.append({
+ "name" : name,
+@@ -158,7 +169,12 @@
+ self.stack[-1]["children"].append(element)
+
+ def characters(self, content):
+- self.stack[-1]["children"].append(PCDATAElement(content))
++ # Coalesce adjacent PCDATAElements
++ pcdata = PCDATAElement(content)
++ if len(self.stack[-1]["children"]) and isinstance(self.stack[-1]["children"][-1], PCDATAElement):
++ self.stack[-1]["children"][-1] = self.stack[-1]["children"][-1] + pcdata
++ else:
++ self.stack[-1]["children"].append(pcdata)
+
+ def ignorableWhitespace(self, whitespace):
+ self.characters(self, whitespace)
+@@ -194,6 +210,8 @@
+ except xml.sax.SAXParseException, e:
+ raise ValueError(e)
+
++ #handler.dom.root_element.validate()
++
+ return handler.dom
+
+ return parse
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc2518.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc2518.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc2518.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc2518.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,37 @@
+Index: twisted/web2/dav/element/rfc2518.py
+===================================================================
+--- twisted/web2/dav/element/rfc2518.py (revision 19773)
++++ twisted/web2/dav/element/rfc2518.py (working copy)
+@@ -59,8 +59,8 @@
+ """
+ name = "depth"
+
+- def __init__(self, *children, **attributes):
+- super(Depth, self).__init__(*children, **attributes)
++ def validate(self):
++ super(Depth, self).validate()
+
+ depth = str(self)
+ if depth not in ("0", "1", "infinity"):
+@@ -382,8 +382,8 @@
+ PCDATAElement: (0, 1),
+ }
+
+- def __init__(self, *children, **attributes):
+- super(KeepAlive, self).__init__(*children, **attributes)
++ def validate(self):
++ super(KeepAlive, self).validate()
+
+ type = None
+
+@@ -450,8 +450,8 @@
+ (dav_namespace, "prop" ): (0, 1),
+ }
+
+- def __init__(self, *children, **attributes):
+- super(PropertyFind, self).__init__(*children, **attributes)
++ def validate(self):
++ super(PropertyFind, self).validate()
+
+ if len(self.children) != 1:
+ raise ValueError(
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,102 @@
+Index: twisted/web2/dav/element/rfc3744.py
+===================================================================
+--- twisted/web2/dav/element/rfc3744.py (revision 19773)
++++ twisted/web2/dav/element/rfc3744.py (working copy)
+@@ -131,8 +131,8 @@
+ (dav_namespace, "self" ): (0, 1),
+ }
+
+- def __init__(self, *children, **attributes):
+- super(Principal, self).__init__(*children, **attributes)
++ def validate(self):
++ super(Principal, self).validate()
+
+ if len(self.children) > 1:
+ raise ValueError(
+@@ -385,9 +385,14 @@
+ self.inherited = None
+ self.protected = False
+
++ my_children = []
++
+ for child in self.children:
+ namespace, name = child.qname()
+
++ if isinstance(child, PCDATAElement):
++ continue
++
+ if (namespace == dav_namespace):
+ if name in ("principal", "invert"):
+ if self.principal is not None:
+@@ -417,6 +422,10 @@
+ elif name == "protected":
+ self.protected = True
+
++ my_children.append(child)
++
++ self.children = tuple(my_children)
++
+ if self.principal is None:
+ raise ValueError(
+ "One of DAV:principal or DAV:invert is required in %s, got: %s"
+@@ -456,7 +465,7 @@
+ """
+ name = "self"
+
+-class Invert (WebDAVEmptyElement):
++class Invert (WebDAVElement):
+ """
+ Principal which matches a user if the user does not match the principal
+ contained by this principal. (RFC 3744, section 5.5.1)
+@@ -551,8 +560,8 @@
+ (dav_namespace, "property" ): (0, None),
+ }
+
+- def __init__(self, *children, **attributes):
+- super(RequiredPrincipal, self).__init__(*children, **attributes)
++ def validate(self):
++ super(RequiredPrincipal, self).validate()
+
+ type = None
+
+@@ -628,8 +637,8 @@
+
+ allowed_children = { WebDAVElement: (0, None) }
+
+- def __init__(self, *children, **attributes):
+- super(ACLPrincipalPropSet, self).__init__(*children, **attributes)
++ def validate(self):
++ super(ACLPrincipalPropSet, self).validate()
+
+ prop = False
+
+@@ -656,8 +665,8 @@
+ (dav_namespace, "prop" ): (0, 1),
+ }
+
+- def __init__(self, *children, **attributes):
+- super(PrincipalMatch, self).__init__(*children, **attributes)
++ def validate(self):
++ super(PrincipalMatch, self).validate()
+
+ # This element can be empty when uses in supported-report-set
+ if not len(self.children):
+@@ -705,6 +714,7 @@
+ (dav_namespace, "prop" ): (0, 1),
+ (dav_namespace, "apply-to-principal-collection-set"): (0, 1),
+ }
++ allowed_attributes = { "test": False }
+
+ class PropertySearch (WebDAVElement):
+ """
+@@ -745,4 +755,10 @@
+ (dav_namespace, "description"): (1, 1),
+ }
+
++class NumberOfMatchesWithinLimits (WebDAVEmptyElement):
++ """
++ Error which indicates too many results
++ """
++ name = "number-of-matches-within-limits"
++
+ # For DAV:description element (RFC 3744, section 9.5) see Description class above.
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,60 @@
+Index: twisted/web2/dav/element/rfc4331.py
+===================================================================
+--- twisted/web2/dav/element/rfc4331.py (revision 0)
++++ twisted/web2/dav/element/rfc4331.py (revision 0)
+@@ -0,0 +1,55 @@
++##
++# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++#
++# DRI: Cyrus Daboo, cdaboo at apple.com
++##
++
++"""
++RFC 4331 (Quota and Size Properties for WebDAV Collections) XML Elements
++
++This module provides XML element definitions for use with WebDAV.
++
++See RFC 4331: http://www.ietf.org/rfc/rfc4331.txt
++"""
++
++from twisted.web2.dav.element.base import WebDAVTextElement
++
++##
++# Section 3 & 4 (Quota Properties)
++##
++
++class QuotaAvailableBytes (WebDAVTextElement):
++ """
++ Property which contains the the number of bytes available under the
++ current quota to store data in a collection (RFC 4331, section 3)
++ """
++ name = "quota-available-bytes"
++ hidden = True
++ protected = True
++
++class QuotaUsedBytes (WebDAVTextElement):
++ """
++ Property which contains the the number of bytes used under the
++ current quota to store data in a collection (RFC 4331, section 4)
++ """
++ name = "quota-used-bytes"
++ hidden = True
++ protected = True
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.fileop.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.fileop.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.fileop.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.fileop.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,31 @@
+Index: twisted/web2/dav/fileop.py
+===================================================================
+--- twisted/web2/dav/fileop.py (revision 19773)
++++ twisted/web2/dav/fileop.py (working copy)
+@@ -35,6 +35,7 @@
+ "move",
+ "put",
+ "mkcollection",
++ "rmdir",
+ ]
+
+ import os
+@@ -287,7 +288,7 @@
+ response = waitForDeferred(copy(FilePath(source_path), FilePath(destination_path), destination_uri, depth))
+ yield response
+ response = response.getResult()
+- checkResponse(response, "copy", responsecode.NO_CONTENT)
++ checkResponse(response, "copy", responsecode.CREATED, responsecode.NO_CONTENT)
+
+ for subdir in subdirs:
+ source_path, destination_path = paths(dir, subdir)
+@@ -509,7 +510,5 @@
+ os.rmdir(dirname)
+
+ def checkResponse(response, method, *codes):
+- assert (
+- response in codes,
+- "%s() should have raised, but returned one of %r instead" % (method, codes)
+- )
++ assert response in codes, \
++ "%s() returned %r, but should have returned one of %r instead" % (method, response, codes)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.http.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.http.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.http.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.http.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,41 @@
+Index: twisted/web2/dav/http.py
+===================================================================
+--- twisted/web2/dav/http.py (revision 19773)
++++ twisted/web2/dav/http.py (working copy)
+@@ -28,10 +28,13 @@
+
+ __all__ = [
+ "ErrorResponse",
++ "NeedPrivilegesResponse",
+ "MultiStatusResponse",
+ "ResponseQueue",
+ "PropertyStatusResponseQueue",
+ "statusForFailure",
++ "errorForFailure",
++ "messageForFailure",
+ ]
+
+ import errno
+@@ -69,10 +72,9 @@
+ """
+ if type(error) is tuple:
+ xml_namespace, xml_name = error
+- class EmptyError (davxml.WebDAVEmptyElement):
+- namespace = xml_namespace
+- name = xml_name
+- error = EmptyError()
++ error = davxml.WebDAVUnknownElement()
++ error.namespace = xml_namespace
++ error.name = xml_name
+
+ output = davxml.Error(error).toxml()
+
+@@ -227,7 +229,7 @@
+
+ if len(property.children) > 0:
+ # Re-instantiate as empty element.
+- property = property.__class__()
++ property = property.emptyCopy()
+
+ if code > 400: # Error codes only
+ log.err("Error during %s for %s: %s" % (self.method, property, message))
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,157 @@
+Index: twisted/web2/dav/idav.py
+===================================================================
+--- twisted/web2/dav/idav.py (revision 19773)
++++ twisted/web2/dav/idav.py (working copy)
+@@ -26,7 +26,7 @@
+ web2.dav interfaces.
+ """
+
+-__all__ = [ "IDAVResource", "IDAVPrincipalResource" ]
++__all__ = [ "IDAVResource", "IDAVPrincipalResource", "IDAVPrincipalCollectionResource", ]
+
+ from twisted.web2.iweb import IResource
+
+@@ -41,7 +41,7 @@
+ otherwise.
+ """
+
+- def findChildren(depth, request, callback, privileges):
++ def findChildren(depth, request, callback, privileges, inherited_aces):
+ """
+ Returns an iterable of child resources for the given depth.
+ Because resources do not know their request URIs, chidren are returned
+@@ -52,6 +52,8 @@
+ @param callback: C{callable} that will be called for each child found
+ @param privileges: the list of L{Privilege}s to test for. This should
+ default to None.
++ @param inherited_aces: a list of L{Privilege}s for aces being inherited from
++ the parent collection used to bypass inheritance lookup.
+ @return: An L{Deferred} that fires when all the children have been found
+ """
+
+@@ -125,15 +127,10 @@
+ L{responsecode.UNAUTHORIZED}) if not authorized.
+ """
+
+- def principalCollections(request):
++ def principalCollections():
+ """
+- Provides the DAV:HRef's of collection resources which contain principal
+- resources which may be used in access control entries on this resource.
+- (RFC 3744, section 5.8)
+- @param request: the request being processed.
+- @return: a deferred sequence of L{davxml.HRef}s referring to
+- collection resources which implement the
+- C{DAV:principal-property-search} C{REPORT}.
++ @return: an interable of L{IDAVPrincipalCollectionResource}s which
++ contain principals used in ACLs for this resource.
+ """
+
+ def setAccessControlList(acl):
+@@ -180,6 +177,80 @@
+ the specified principal.
+ """
+
++ ##
++ # Quota
++ ##
++
++ def quota(request):
++ """
++ Get current available & used quota values for this resource's quota root
++ collection.
++
++ @return: a C{tuple} containing two C{int}'s the first is
++ quota-available-bytes, the second is quota-used-bytes, or
++ C{None} if quota is not defined on the resource.
++ """
++
++ def hasQuota(request):
++ """
++ Check whether this resource is undre quota control by checking each parent to see if
++ it has a quota root.
++
++ @return: C{True} if under quota control, C{False} if not.
++ """
++
++ def hasQuotaRoot(request):
++ """
++ Determine whether the resource has a quota root.
++
++ @return: a C{True} if this resource has quota root, C{False} otherwise.
++ """
++
++
++ def quotaRoot(request):
++ """
++ Get the quota root (max. allowed bytes) value for this collection.
++
++ @return: a C{int} containing the maximum allowed bytes if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++
++ def setQuotaRoot(request, maxsize):
++ """
++ Set the quota root (max. allowed bytes) value for this collection.
++
++ @param maxsize: a C{int} containing the maximum allowed bytes for the contents
++ of this collection.
++ """
++
++ def quotaSize(request):
++ """
++ Get the size of this resource (if its a collection get total for all children as well).
++ TODO: Take into account size of dead-properties.
++
++ @return: a L{Deferred} with a C{int} result containing the size of the resource.
++ """
++
++ def currentQuotaUse(request):
++ """
++ Get the cached quota use value, or if not present (or invalid) determine
++ quota use by brute force.
++
++ @return: an L{Deferred} with a C{int} result containing the current used byte count if
++ this collection is quota-controlled, or C{None} if not quota controlled.
++ """
++
++ def updateQuotaUse(request, adjust):
++ """
++ Adjust current quota use on this all all parent collections that also
++ have quota roots.
++
++ @param adjust: a C{int} containing the number of bytes added (positive) or
++ removed (negative) that should be used to adjust the cached total.
++ @return: an L{Deferred} with a C{int} result containing the current used byte if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++
+ class IDAVPrincipalResource (IDAVResource):
+ """
+ WebDAV principal resource. (RFC 3744, section 2)
+@@ -203,12 +274,23 @@
+ """
+ Provides the principal URLs of principals that are direct members of
+ this (group) principal. (RFC 3744, section 4.3)
+- @return: a iterable of principal URLs.
++ @return: a deferred returning an iterable of principal URLs.
+ """
+
+ def groupMemberships():
+ """
+ Provides the URLs of the group principals in which the principal is
+ directly a member. (RFC 3744, section 4.4)
+- @return: a iterable of group principal URLs.
++ @return: a deferred containing an iterable of group principal URLs.
+ """
++
++class IDAVPrincipalCollectionResource (IDAVResource):
++ """
++ WebDAV principal collection resource. (RFC 3744, section 5.8)
++ """
++ def principalCollectionURL():
++ """
++ Provides a URL for this resource which may be used to identify this
++ resource in ACL requests. (RFC 3744, section 5.8)
++ @return: a URL.
++ """
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,12 @@
+Index: twisted/web2/dav/method/__init__.py
+===================================================================
+--- twisted/web2/dav/method/__init__.py (revision 19773)
++++ twisted/web2/dav/method/__init__.py (working copy)
+@@ -40,6 +40,7 @@
+ "proppatch",
+ "prop_common",
+ "put",
++ "put_common",
+ "report",
+ "report_acl_principal_prop_set",
+ "report_expand",
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,76 @@
+Index: twisted/web2/dav/method/copymove.py
+===================================================================
+--- twisted/web2/dav/method/copymove.py (revision 19773)
++++ twisted/web2/dav/method/copymove.py (working copy)
+@@ -34,11 +34,12 @@
+ from twisted.python import log
+ from twisted.internet.defer import waitForDeferred, deferredGenerator
+ from twisted.web2 import responsecode
++from twisted.web2.dav.fileop import move
+ from twisted.web2.http import HTTPError, StatusResponse
+ from twisted.web2.filter.location import addLocation
+ from twisted.web2.dav import davxml
+ from twisted.web2.dav.idav import IDAVResource
+-from twisted.web2.dav.fileop import copy, move
++from twisted.web2.dav.method import put_common
+ from twisted.web2.dav.util import parentForURL
+
+ # FIXME: This is circular
+@@ -81,7 +82,15 @@
+ # May need to add a location header
+ addLocation(request, destination_uri)
+
+- x = waitForDeferred(copy(self.fp, destination.fp, destination_uri, depth))
++ #x = waitForDeferred(copy(self.fp, destination.fp, destination_uri, depth))
++ x = waitForDeferred(put_common.storeResource(request,
++ source=self,
++ source_uri=request.uri,
++ destination=destination,
++ destination_uri=destination_uri,
++ deletesource=False,
++ depth=depth
++ ))
+ yield x
+ yield x.getResult()
+
+@@ -100,7 +109,8 @@
+ #
+ # Check authentication and access controls
+ #
+- parent = waitForDeferred(request.locateResource(parentForURL(request.uri)))
++ parentURL = parentForURL(request.uri)
++ parent = waitForDeferred(request.locateResource(parentURL))
+ yield parent
+ parent = parent.getResult()
+
+@@ -117,7 +127,8 @@
+ yield x
+ x.getResult()
+ else:
+- destparent = waitForDeferred(request.locateResource(parentForURL(destination_uri)))
++ destparentURL = parentForURL(destination_uri)
++ destparent = waitForDeferred(request.locateResource(destparentURL))
+ yield destparent
+ destparent = destparent.getResult()
+
+@@ -144,7 +155,19 @@
+ log.err(msg)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
+
+- x = waitForDeferred(move(self.fp, request.uri, destination.fp, destination_uri, depth))
++ # Lets optimise a move within the same directory to a new resource as a simple move
++ # rather than using the full transaction based storeResource api. This allows simple
++ # "rename" operations to work quickly.
++ if (not destination.exists()) and destparent == parent:
++ x = waitForDeferred(move(self.fp, request.uri, destination.fp, destination_uri, depth))
++ else:
++ x = waitForDeferred(put_common.storeResource(request,
++ source=self,
++ source_uri=request.uri,
++ destination=destination,
++ destination_uri=destination_uri,
++ deletesource=True,
++ depth=depth))
+ yield x
+ yield x.getResult()
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,34 @@
+Index: twisted/web2/dav/method/delete.py
+===================================================================
+--- twisted/web2/dav/method/delete.py (revision 19773)
++++ twisted/web2/dav/method/delete.py (working copy)
+@@ -58,8 +58,28 @@
+ yield x
+ x.getResult()
+
++ # Do quota checks before we start deleting things
++ myquota = waitForDeferred(self.quota(request))
++ yield myquota
++ myquota = myquota.getResult()
++ if myquota is not None:
++ old_size = waitForDeferred(self.quotaSize(request))
++ yield old_size
++ old_size = old_size.getResult()
++ else:
++ old_size = 0
++
++ # Do delete
+ x = waitForDeferred(delete(request.uri, self.fp, depth))
+ yield x
+- yield x.getResult()
++ result = x.getResult()
+
++ # Adjust quota
++ if myquota is not None:
++ d = waitForDeferred(self.quotaSizeAdjust(request, -old_size))
++ yield d
++ d.getResult()
++
++ yield result
++
+ http_DELETE = deferredGenerator(http_DELETE)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.prop_common.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.prop_common.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.prop_common.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.prop_common.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,36 @@
+Index: twisted/web2/dav/method/prop_common.py
+===================================================================
+--- twisted/web2/dav/method/prop_common.py (revision 19773)
++++ twisted/web2/dav/method/prop_common.py (working copy)
+@@ -23,19 +23,21 @@
+ properties_by_status = waitForDeferred(propertiesForResource(request, propertyreq, resource))
+ yield properties_by_status
+ properties_by_status = properties_by_status.getResult()
+-
++
++ propstats = []
++
+ for status in properties_by_status:
+ properties = properties_by_status[status]
+ if properties:
+- responses.append(
+- davxml.PropertyStatusResponse(
+- href,
+- davxml.PropertyStatus(
+- davxml.PropertyContainer(*properties),
+- davxml.Status.fromResponseCode(status)
+- )
+- )
+- )
++ xml_status = davxml.Status.fromResponseCode(status)
++ xml_container = davxml.PropertyContainer(*properties)
++ xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
++
++ propstats.append(xml_propstat)
++
++ if propstats:
++ responses.append(davxml.PropertyStatusResponse(href, *propstats))
++
+ else:
+ responses.append(
+ davxml.StatusResponse(
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,28 @@
+Index: twisted/web2/dav/method/propfind.py
+===================================================================
+--- twisted/web2/dav/method/propfind.py (revision 19773)
++++ twisted/web2/dav/method/propfind.py (working copy)
+@@ -27,7 +27,10 @@
+ WebDAV PROPFIND method
+ """
+
+-__all__ = ["http_PROPFIND"]
++__all__ = [
++ "http_PROPFIND",
++ "propertyName",
++]
+
+ from twisted.python import log
+ from twisted.python.failure import Failure
+@@ -200,7 +203,7 @@
+
+ def propertyName(name):
+ property_namespace, property_name = name
+- class PropertyName (davxml.WebDAVEmptyElement):
+- namespace = property_namespace
+- name = property_name
+- return PropertyName()
++ pname = davxml.WebDAVUnknownElement()
++ pname.namespace = property_namespace
++ pname.name = property_name
++ return pname
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,20 @@
+Index: twisted/web2/dav/method/put.py
+===================================================================
+--- twisted/web2/dav/method/put.py (revision 19773)
++++ twisted/web2/dav/method/put.py (working copy)
+@@ -34,7 +34,7 @@
+ from twisted.web2 import responsecode
+ from twisted.web2.http import HTTPError, StatusResponse
+ from twisted.web2.dav import davxml
+-from twisted.web2.dav.fileop import put
++from twisted.web2.dav.method import put_common
+ from twisted.web2.dav.util import parentForURL
+
+ def preconditions_PUT(self, request):
+@@ -107,4 +107,5 @@
+ # to return a MULTI_STATUS response, which is WebDAV-specific (and PUT is
+ # not).
+ #
+- return put(request.stream, self.fp)
++ #return put(request.stream, self.fp)
++ return put_common.storeResource(request, destination=self, destination_uri=request.uri)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,270 @@
+Index: twisted/web2/dav/method/put_common.py
+===================================================================
+--- twisted/web2/dav/method/put_common.py (revision 0)
++++ twisted/web2/dav/method/put_common.py (revision 0)
+@@ -0,0 +1,265 @@
++##
++# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
++#
++# Licensed under the Apache License, Version 2.0 (the "License");
++# you may not use this file except in compliance with the License.
++# You may obtain a copy of the License at
++#
++# http://www.apache.org/licenses/LICENSE-2.0
++#
++# Unless required by applicable law or agreed to in writing, software
++# distributed under the License is distributed on an "AS IS" BASIS,
++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++# See the License for the specific language governing permissions and
++# limitations under the License.
++#
++# DRI: Cyrus Daboo, cdaboo at apple.com
++##
++
++"""
++PUT/COPY/MOVE common behavior.
++"""
++
++__version__ = "0.0"
++
++__all__ = ["storeCalendarObjectResource"]
++
++from twisted.internet.defer import deferredGenerator, maybeDeferred, waitForDeferred
++from twisted.python import failure, log
++from twisted.python.filepath import FilePath
++from twisted.web2 import responsecode
++from twisted.web2.dav import davxml
++from twisted.web2.dav.element.base import dav_namespace
++from twisted.web2.dav.fileop import copy, delete, put
++from twisted.web2.dav.http import ErrorResponse
++from twisted.web2.dav.resource import TwistedGETContentMD5
++from twisted.web2.dav.stream import MD5StreamWrapper
++from twisted.web2.http import HTTPError
++from twisted.web2.http_headers import generateContentType
++from twisted.web2.iweb import IResponse
++from twisted.web2.stream import MemoryStream
++
++def storeResource(
++ request,
++ source=None, source_uri=None, data=None,
++ destination=None, destination_uri=None,
++ deletesource=False,
++ depth="0"
++):
++ """
++ Function that does common PUT/COPY/MOVE behaviour.
++
++ @param request: the L{twisted.web2.server.Request} for the current HTTP request.
++ @param source: the L{DAVFile} for the source resource to copy from, or None if source data
++ is to be read from the request.
++ @param source_uri: the URI for the source resource.
++ @param data: a C{str} to copy data from instead of the request stream.
++ @param destination: the L{DAVFile} for the destination resource to copy into.
++ @param destination_uri: the URI for the destination resource.
++ @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
++ @param depth: a C{str} containing the COPY/MOVE Depth header value.
++ @return: status response.
++ """
++
++ try:
++ assert request is not None and destination is not None and destination_uri is not None
++ assert (source is None) or (source is not None and source_uri is not None)
++ assert not deletesource or (deletesource and source is not None)
++ except AssertionError:
++ log.err("Invalid arguments to storeResource():")
++ log.err("request=%s\n" % (request,))
++ log.err("source=%s\n" % (source,))
++ log.err("source_uri=%s\n" % (source_uri,))
++ log.err("data=%s\n" % (data,))
++ log.err("destination=%s\n" % (destination,))
++ log.err("destination_uri=%s\n" % (destination_uri,))
++ log.err("deletesource=%s\n" % (deletesource,))
++ log.err("depth=%s\n" % (depth,))
++ raise
++
++ class RollbackState(object):
++ """
++ This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE
++ transaction, leaving the server state the same as it was before the request was
++ processed. The DoRollback method will actually execute the rollback operations.
++ """
++
++ def __init__(self):
++ self.active = True
++ self.source_copy = None
++ self.destination_copy = None
++ self.destination_created = False
++ self.source_deleted = False
++
++ def Rollback(self):
++ """
++ Rollback the server state. Do not allow this to raise another exception. If
++ rollback fails then we are going to be left in an awkward state that will need
++ to be cleaned up eventually.
++ """
++ if self.active:
++ self.active = False
++ log.err("Rollback: rollback")
++ try:
++ if self.source_copy and self.source_deleted:
++ self.source_copy.moveTo(source.fp)
++ log.err("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path))
++ self.source_copy = None
++ self.source_deleted = False
++ if self.destination_copy:
++ destination.fp.remove()
++ log.err("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path))
++ self.destination_copy.moveTo(destination.fp)
++ self.destination_copy = None
++ elif self.destination_created:
++ destination.fp.remove()
++ log.err("Rollback: destination removed %s" % (destination.fp.path,))
++ self.destination_created = False
++ except:
++ log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
++
++ def Commit(self):
++ """
++ Commit the resource changes by wiping the rollback state.
++ """
++ if self.active:
++ log.err("Rollback: commit")
++ self.active = False
++ if self.source_copy:
++ self.source_copy.remove()
++ log.err("Rollback: removed source backup %s" % (self.source_copy.path,))
++ self.source_copy = None
++ if self.destination_copy:
++ self.destination_copy.remove()
++ log.err("Rollback: removed destination backup %s" % (self.destination_copy.path,))
++ self.destination_copy = None
++ self.destination_created = False
++ self.source_deleted = False
++
++ rollback = RollbackState()
++
++ try:
++ """
++ Handle validation operations here.
++ """
++
++ """
++ Handle rollback setup here.
++ """
++
++ # Do quota checks on destination and source before we start messing with adding other files
++ destquota = waitForDeferred(destination.quota(request))
++ yield destquota
++ destquota = destquota.getResult()
++ if destquota is not None and destination.exists():
++ old_dest_size = waitForDeferred(destination.quotaSize(request))
++ yield old_dest_size
++ old_dest_size = old_dest_size.getResult()
++ else:
++ old_dest_size = 0
++
++ if source is not None:
++ sourcequota = waitForDeferred(source.quota(request))
++ yield sourcequota
++ sourcequota = sourcequota.getResult()
++ if sourcequota is not None and source.exists():
++ old_source_size = waitForDeferred(source.quotaSize(request))
++ yield old_source_size
++ old_source_size = old_source_size.getResult()
++ else:
++ old_source_size = 0
++ else:
++ sourcequota = None
++ old_source_size = 0
++
++ # We may need to restore the original resource data if the PUT/COPY/MOVE fails,
++ # so rename the original file in case we need to rollback.
++ overwrite = destination.exists()
++ if overwrite:
++ rollback.destination_copy = FilePath(destination.fp.path)
++ rollback.destination_copy.path += ".rollback"
++ destination.fp.copyTo(rollback.destination_copy)
++ else:
++ rollback.destination_created = True
++
++ if deletesource:
++ rollback.source_copy = FilePath(source.fp.path)
++ rollback.source_copy.path += ".rollback"
++ source.fp.copyTo(rollback.source_copy)
++
++ """
++ Handle actual store operations here.
++ """
++
++ # Do put or copy based on whether source exists
++ if source is not None:
++ response = maybeDeferred(copy, source.fp, destination.fp, destination_uri, depth)
++ else:
++ datastream = request.stream
++ if data is not None:
++ datastream = MemoryStream(data)
++ md5 = MD5StreamWrapper(datastream)
++ response = maybeDeferred(put, md5, destination.fp)
++
++ response = waitForDeferred(response)
++ yield response
++ response = response.getResult()
++
++ # Update the MD5 value on the resource
++ if source is not None:
++ # Copy MD5 value from source to destination
++ if source.hasDeadProperty(TwistedGETContentMD5):
++ md5 = source.readDeadProperty(TwistedGETContentMD5)
++ destination.writeDeadProperty(md5)
++ else:
++ # Finish MD5 calc and write dead property
++ md5.close()
++ md5 = md5.getMD5()
++ destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
++
++ # Update the content-type value on the resource if it is not been copied or moved
++ if source is None:
++ content_type = request.headers.getHeader("content-type")
++ if content_type is not None:
++ destination.writeDeadProperty(davxml.GETContentType.fromString(generateContentType(content_type)))
++
++ response = IResponse(response)
++
++ # Do quota check on destination
++ if destquota is not None:
++ # Get size of new/old resources
++ new_dest_size = waitForDeferred(destination.quotaSize(request))
++ yield new_dest_size
++ new_dest_size = new_dest_size.getResult()
++ diff_size = new_dest_size - old_dest_size
++ if diff_size >= destquota[0]:
++ log.err("Over quota: available %d, need %d" % (destquota[0], diff_size))
++ raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
++ d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size))
++ yield d
++ d.getResult()
++
++ if deletesource:
++ # Delete the source resource
++ if sourcequota is not None:
++ delete_size = 0 - old_source_size
++ d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
++ yield d
++ d.getResult()
++
++ delete(source_uri, source.fp, depth)
++ rollback.source_deleted = True
++
++ # Can now commit changes and forget the rollback details
++ rollback.Commit()
++
++ yield response
++ return
++
++ except:
++ # Roll back changes to original server state. Note this may do nothing
++ # if the rollback has already ocurred or changes already committed.
++ rollback.Rollback()
++ raise
++
++storeResource = deferredGenerator(storeResource)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,33 @@
+Index: twisted/web2/dav/method/report.py
+===================================================================
+--- twisted/web2/dav/method/report.py (revision 19773)
++++ twisted/web2/dav/method/report.py (working copy)
+@@ -27,7 +27,11 @@
+ WebDAV REPORT method
+ """
+
+-__all__ = ["http_REPORT"]
++__all__ = [
++ "max_number_of_matches",
++ "NumberOfMatchesWithinLimits",
++ "http_REPORT",
++]
+
+ import string
+
+@@ -43,7 +47,14 @@
+ max_number_of_matches = 500
+
+ class NumberOfMatchesWithinLimits(Exception):
+- pass
++
++ def __init__(self, limit):
++
++ super(NumberOfMatchesWithinLimits, self).__init__()
++ self.limit = limit
++
++ def maxLimit(self):
++ return self.limit
+
+ def http_REPORT(self, request):
+ """
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_acl_principal_prop_set.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_acl_principal_prop_set.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_acl_principal_prop_set.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_acl_principal_prop_set.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,22 @@
+Index: twisted/web2/dav/method/report_acl_principal_prop_set.py
+===================================================================
+--- twisted/web2/dav/method/report_acl_principal_prop_set.py (revision 19773)
++++ twisted/web2/dav/method/report_acl_principal_prop_set.py (working copy)
+@@ -103,7 +103,7 @@
+ # Check size of results is within limit
+ matchcount += 1
+ if matchcount > max_number_of_matches:
+- raise NumberOfMatchesWithinLimits
++ raise NumberOfMatchesWithinLimits(max_number_of_matches)
+
+ resource = waitForDeferred(request.locateResource(str(principal)))
+ yield resource
+@@ -144,7 +144,7 @@
+ log.err("Too many matching components")
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+- (dav_namespace, "number-of-matches-within-limits")
++ davxml.NumberOfMatchesWithinLimits()
+ ))
+
+ yield MultiStatusResponse(responses)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_expand.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,214 @@
+Index: twisted/web2/dav/method/report_expand.py
+===================================================================
+--- twisted/web2/dav/method/report_expand.py (revision 19773)
++++ twisted/web2/dav/method/report_expand.py (working copy)
+@@ -1,6 +1,6 @@
+ # -*- test-case-name: twisted.web2.dav.test.test_report_expand -*-
+ ##
+-# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++# Copyright (c) 2005-2008 Apple Computer, Inc. All rights reserved.
+ #
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
+ # of this software and associated documentation files (the "Software"), to deal
+@@ -19,8 +19,6 @@
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ # SOFTWARE.
+-#
+-# DRI: Wilfredo Sanchez, wsanchez at apple.com
+ ##
+
+ """
+@@ -29,86 +27,143 @@
+
+ __all__ = ["report_DAV__expand_property"]
+
++from twisted.internet.defer import inlineCallbacks, returnValue
+ from twisted.python import log
+ from twisted.python.failure import Failure
+-from twisted.internet.defer import deferredGenerator, waitForDeferred
+ from twisted.web2 import responsecode
+ from twisted.web2.dav import davxml
+-from twisted.web2.dav.http import statusForFailure
+ from twisted.web2.dav.davxml import dav_namespace
++from twisted.web2.dav.http import statusForFailure, MultiStatusResponse
++from twisted.web2.dav.method import prop_common
++from twisted.web2.dav.method.propfind import propertyName
++from twisted.web2.dav.resource import AccessDeniedError
++from twisted.web2.dav.util import parentForURL
++from twisted.web2.http import HTTPError, StatusResponse
+
++ at inlineCallbacks
+ def report_DAV__expand_property(self, request, expand_property):
+ """
+ Generate an expand-property REPORT. (RFC 3253, section 3.8)
++
++ TODO: for simplicity we will only support one level of expansion.
+ """
+- # FIXME: Handle depth header
+-
++ # Verify root element
+ if not isinstance(expand_property, davxml.ExpandProperty):
+ raise ValueError("%s expected as root element, not %s."
+ % (davxml.ExpandProperty.sname(), expand_property.sname()))
+
++ # Only handle Depth: 0
++ depth = request.headers.getHeader("depth", "0")
++ if depth != "0":
++ log.err("Non-zero depth is not allowed: %s" % (depth,))
++ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
++
+ #
+- # Expand DAV:allprop
++ # Get top level properties to expand and make sure we only have one level
+ #
+ properties = {}
+
+ for property in expand_property.children:
+- namespace = property.getAttribute("namespace")
+- name = property.getAttribute("name")
++ namespace = property.attributes.get("namespace", dav_namespace)
++ name = property.attributes.get("name", "")
++
++ # Make sure children have no children
++ props_to_find = []
++ for child in property.children:
++ if child.children:
++ log.err("expand-property REPORT only supports single level expansion")
++ raise HTTPError(StatusResponse(
++ responsecode.NOT_IMPLEMENTED,
++ "expand-property REPORT only supports single level expansion"
++ ))
++ child_namespace = child.attributes.get("namespace", dav_namespace)
++ child_name = child.attributes.get("name", "")
++ props_to_find.append((child_namespace, child_name))
+
+- if not namespace: namespace = dav_namespace
++ properties[(namespace, name)] = props_to_find
+
+- if (namespace, name) == (dav_namespace, "allprop"):
+- all_properties = waitForDeferred(self.listAllProp(request))
+- yield all_properties
+- all_properties = all_properties.getResult()
+-
+- for all_property in all_properties:
+- properties[all_property.qname()] = property
+- else:
+- properties[(namespace, name)] = property
+-
+ #
+- # Look up the requested properties
++ # Generate the expanded responses status for each top-level property
+ #
+ properties_by_status = {
+ responsecode.OK : [],
+ responsecode.NOT_FOUND : [],
+ }
++
++ filteredaces = None
++ lastParent = None
+
+- for property in properties:
+- my_properties = waitForDeferred(self.listProperties(request))
+- yield my_properties
+- my_properties = my_properties.getResult()
++ for qname in properties.iterkeys():
++ try:
++ prop = (yield self.readProperty(qname, request))
++
++ # Form the PROPFIND-style DAV:prop element we need later
++ props_to_return = davxml.PropertyContainer(*properties[qname])
+
+- if property in my_properties:
+- try:
+- value = waitForDeferred(self.readProperty(property, request))
+- yield value
+- value = value.getResult()
++ # Now dereference any HRefs
++ responses = []
++ for href in prop.children:
++ if isinstance(href, davxml.HRef):
++
++ # Locate the Href resource and its parent
++ resource_uri = str(href)
++ child = (yield request.locateResource(resource_uri))
++
++ if not child or not child.exists():
++ responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
++ continue
++ parent = (yield request.locateResource(parentForURL(resource_uri)))
++
++ # Check privileges on parent - must have at least DAV:read
++ try:
++ yield parent.checkPrivileges(request, (davxml.Read(),))
++ except AccessDeniedError:
++ responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
++ continue
++
++ # Cache the last parent's inherited aces for checkPrivileges optimization
++ if lastParent != parent:
++ lastParent = parent
++
++ # Do some optimisation of access control calculation by determining any inherited ACLs outside of
++ # the child resource loop and supply those to the checkPrivileges on each child.
++ filteredaces = (yield parent.inheritedACEsforChildren(request))
+
+- if isinstance(value, davxml.HRef):
+- raise NotImplementedError()
+- else:
+- raise NotImplementedError()
+- except:
+- f = Failure()
++ # Check privileges - must have at least DAV:read
++ try:
++ yield child.checkPrivileges(request, (davxml.Read(),), inherited_aces=filteredaces)
++ except AccessDeniedError:
++ responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
++ continue
++
++ # Now retrieve all the requested properties on the HRef resource
++ yield prop_common.responseForHref(
++ request,
++ responses,
++ href,
++ child,
++ prop_common.propertyListForResource,
++ props_to_return,
++ )
++
++ prop.children = responses
++ properties_by_status[responsecode.OK].append(prop)
++ except:
++ f = Failure()
+
+- log.err("Error reading property %r for resource %s: %s"
+- % (property, self, f.value))
++ log.err("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))
+
+- status = statusForFailure(f, "getting property: %s" % (property,))
+- if status not in properties_by_status:
+- properties_by_status[status] = []
++ status = statusForFailure(f, "getting property: %s" % (qname,))
++ if status not in properties_by_status: properties_by_status[status] = []
++ properties_by_status[status].append(propertyName(qname))
+
+- raise NotImplementedError()
++ # Build the overall response
++ propstats = [
++ davxml.PropertyStatus(
++ davxml.PropertyContainer(*properties_by_status[status]),
++ davxml.Status.fromResponseCode(status)
++ )
++ for status in properties_by_status if properties_by_status[status]
++ ]
+
+- #properties_by_status[status].append(
+- # ____propertyName(property)
+- #)
+- else:
+- properties_by_status[responsecode.NOT_FOUND].append(property)
+-
+- raise NotImplementedError()
+-
+-report_DAV__expand_property = deferredGenerator(report_DAV__expand_property)
++ returnValue(MultiStatusResponse((davxml.PropertyStatusResponse(davxml.HRef(request.uri), *propstats),)))
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_match.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_match.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_match.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_match.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,150 @@
+Index: twisted/web2/dav/method/report_principal_match.py
+===================================================================
+--- twisted/web2/dav/method/report_principal_match.py (revision 19773)
++++ twisted/web2/dav/method/report_principal_match.py (working copy)
+@@ -89,40 +89,64 @@
+ responses = []
+ matchcount = 0
+
+- selfPrincipal = self.currentPrincipal(request).children[0]
++ selfPrincipalURL = self.currentPrincipal(request).children[0]
+
+- # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+- # the child resource loop and supply those to the checkPrivileges on each child.
+- filteredaces = waitForDeferred(self.inheritedACEsforChildren(request))
+- yield filteredaces
+- filteredaces = filteredaces.getResult()
+-
+- children = []
+- d = waitForDeferred(self.findChildren("infinity", request, lambda x, y: children.append((x,y)),
+- privileges=(davxml.Read(),), inherited_aces=filteredaces))
+- yield d
+- d.getResult()
+-
+ if lookForPrincipals:
+
+- for child, uri in children:
+- if isPrincipalResource(child) and child.principalMatch(selfPrincipal):
+- # Check size of results is within limit
+- matchcount += 1
+- if matchcount > max_number_of_matches:
+- raise NumberOfMatchesWithinLimits
++ # Find the set of principals that represent "self".
++
++ # First add "self"
++ principal = waitForDeferred(request.locateResource(str(selfPrincipalURL)))
++ yield principal
++ principal = principal.getResult()
++ selfItems = [principal,]
++
++ # Get group memberships for "self" and add each of those
++ d = waitForDeferred(principal.groupMemberships())
++ yield d
++ memberships = d.getResult()
++ selfItems.extend(memberships)
++
++ # Now add each principal found to the response provided the principal resource is a child of
++ # the current resource.
++ for principal in selfItems:
++ # Get all the URIs that point to the principal resource
++ # FIXME: making the assumption that the principalURL() is the URL of the resource we found
++ principal_uris = [principal.principalURL()]
++ principal_uris.extend(principal.alternateURIs())
++
++ # Compare each one to the request URI and return at most one that matches
++ for uri in principal_uris:
++ if uri.startswith(request.uri):
++ # Check size of results is within limit
++ matchcount += 1
++ if matchcount > max_number_of_matches:
++ raise NumberOfMatchesWithinLimits(max_number_of_matches)
++
++ d = waitForDeferred(prop_common.responseForHref(
++ request,
++ responses,
++ davxml.HRef.fromString(uri),
++ principal,
++ propertiesForResource,
++ propElement
++ ))
++ yield d
++ d.getResult()
++ break
++ else:
++ # Do some optimisation of access control calculation by determining any inherited ACLs outside of
++ # the child resource loop and supply those to the checkPrivileges on each child.
++ filteredaces = waitForDeferred(self.inheritedACEsforChildren(request))
++ yield filteredaces
++ filteredaces = filteredaces.getResult()
++
++ children = []
++ d = waitForDeferred(self.findChildren("infinity", request, lambda x, y: children.append((x,y)),
++ privileges=(davxml.Read(),), inherited_aces=filteredaces))
++ yield d
++ d.getResult()
+
+- d = waitForDeferred(prop_common.responseForHref(
+- request,
+- responses,
+- davxml.HRef.fromString(uri),
+- child,
+- propertiesForResource,
+- propElement
+- ))
+- yield d
+- d.getResult()
+- else:
+ for child, uri in children:
+ # Try to read the requested property from this resource
+ try:
+@@ -137,22 +161,26 @@
+ yield principal
+ principal = principal.getResult()
+
+- if principal and isPrincipalResource(principal) and principal.principalMatch(selfPrincipal):
+- # Check size of results is within limit
+- matchcount += 1
+- if matchcount > max_number_of_matches:
+- raise NumberOfMatchesWithinLimits
+-
+- d = waitForDeferred(prop_common.responseForHref(
+- request,
+- responses,
+- davxml.HRef.fromString(uri),
+- child,
+- propertiesForResource,
+- propElement
+- ))
++ if principal and isPrincipalResource(principal):
++ d = waitForDeferred(principal.principalMatch(selfPrincipalURL))
+ yield d
+- d.getResult()
++ matched = d.getResult()
++ if matched:
++ # Check size of results is within limit
++ matchcount += 1
++ if matchcount > max_number_of_matches:
++ raise NumberOfMatchesWithinLimits(max_number_of_matches)
++
++ d = waitForDeferred(prop_common.responseForHref(
++ request,
++ responses,
++ davxml.HRef.fromString(uri),
++ child,
++ propertiesForResource,
++ propElement
++ ))
++ yield d
++ d.getResult()
+ except HTTPError:
+ # Just ignore a failure to access the property. We treat this like a property that does not exist
+ # or does not match the principal.
+@@ -162,7 +190,7 @@
+ log.err("Too many matching components in principal-match report")
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+- (dav_namespace, "number-of-matches-within-limits")
++ davxml.NumberOfMatchesWithinLimits()
+ ))
+
+ yield MultiStatusResponse(responses)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,22 @@
+Index: twisted/web2/dav/method/report_principal_property_search.py
+===================================================================
+--- twisted/web2/dav/method/report_principal_property_search.py (revision 19773)
++++ twisted/web2/dav/method/report_principal_property_search.py (working copy)
+@@ -166,7 +166,7 @@
+ # Check size of results is within limit
+ matchcount += 1
+ if matchcount > max_number_of_matches:
+- raise NumberOfMatchesWithinLimits
++ raise NumberOfMatchesWithinLimits(max_number_of_matches)
+
+ d = waitForDeferred(prop_common.responseForHref(
+ request,
+@@ -183,7 +183,7 @@
+ log.err("Too many matching components in prinicpal-property-search report")
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+- (dav_namespace, "number-of-matches-within-limits")
++ davxml.NumberOfMatchesWithinLimits()
+ ))
+
+ yield MultiStatusResponse(responses)
Deleted: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -1,13 +0,0 @@
-Index: twisted/web2/dav/resource.py
-===================================================================
---- twisted/web2/dav/resource.py (revision 26741)
-+++ twisted/web2/dav/resource.py (working copy)
-@@ -1888,7 +1888,7 @@
- # If this is a collection and the URI doesn't end in "/", redirect.
- #
- if self.isCollection() and request.path[-1:] != "/":
-- return RedirectResponse(request.unparseURL(path=request.path+'/'))
-+ return RedirectResponse(request.unparseURL(path=urllib.quote(request.path, safe=':/')+'/'))
-
- def setHeaders(response):
- response = IResponse(response)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,1238 @@
+Index: twisted/web2/dav/resource.py
+===================================================================
+--- twisted/web2/dav/resource.py (revision 19773)
++++ twisted/web2/dav/resource.py (working copy)
+@@ -31,20 +31,31 @@
+ "DAVResource",
+ "DAVLeafResource",
+ "DAVPrincipalResource",
++ "DAVPrincipalCollectionResource",
+ "AccessDeniedError",
+ "isPrincipalResource",
+ "TwistedACLInheritable",
++ "TwistedGETContentMD5",
++ "TwistedQuotaRootProperty",
+ "allACL",
+ "readonlyACL",
+ "davPrivilegeSet",
+ "unauthenticatedPrincipal",
+ ]
+
+ import urllib
++import __builtin__
++if not hasattr(__builtin__, "set"):
++ import sets.Set as set
++if not hasattr(__builtin__, "frozenset"):
++ import sets.ImmutableSet as frozenset
+
+ from zope.interface import implements
+ from twisted.python import log
+-from twisted.internet.defer import Deferred, maybeDeferred, succeed
++from twisted.python.failure import Failure
++from twisted.cred.error import LoginFailed, UnauthorizedLogin
++from twisted.internet.defer import Deferred, maybeDeferred, succeed,\
++ inlineCallbacks, returnValue
+ from twisted.internet.defer import waitForDeferred, deferredGenerator
+ from twisted.internet import reactor
+ from twisted.web2 import responsecode
+@@ -52,12 +62,13 @@
+ from twisted.web2.http_headers import generateContentType
+ from twisted.web2.iweb import IResponse
+ from twisted.web2.resource import LeafResource
++from twisted.web2.server import NoURLForResourceError
+ from twisted.web2.static import MetaDataMixin, StaticRenderMixin
+ from twisted.web2.auth.wrapper import UnauthorizedResponse
+ from twisted.web2.dav import davxml
+ from twisted.web2.dav.davxml import dav_namespace, lookupElement
+ from twisted.web2.dav.davxml import twisted_dav_namespace, twisted_private_namespace
+-from twisted.web2.dav.idav import IDAVResource, IDAVPrincipalResource
++from twisted.web2.dav.idav import IDAVResource, IDAVPrincipalResource, IDAVPrincipalCollectionResource
+ from twisted.web2.dav.http import NeedPrivilegesResponse
+ from twisted.web2.dav.noneprops import NonePropertyStore
+ from twisted.web2.dav.util import unimplemented, parentForURL, joinURL
+@@ -126,10 +137,13 @@
+ #(dav_namespace, "group" ), # RFC 3744, section 5.2
+ (dav_namespace, "supported-privilege-set" ), # RFC 3744, section 5.3
+ (dav_namespace, "current-user-privilege-set"), # RFC 3744, section 5.4
++ (dav_namespace, "current-user-principal" ), # draft-sanchez-webdav-current-principal
+ (dav_namespace, "acl" ), # RFC 3744, section 5.5
+ (dav_namespace, "acl-restrictions" ), # RFC 3744, section 5.6
+ (dav_namespace, "inherited-acl-set" ), # RFC 3744, section 5.7
+ (dav_namespace, "principal-collection-set" ), # RFC 3744, section 5.8
++ (dav_namespace, "quota-available-bytes" ), # RFC 4331, section 3
++ (dav_namespace, "quota-used-bytes" ), # RFC 4331, section 4
+
+ (twisted_dav_namespace, "resource-class"),
+ )
+@@ -166,6 +180,14 @@
+ if qname[0] == twisted_private_namespace:
+ return succeed(False)
+
++ # Need to special case the dynamic live properties
++ namespace, name = qname
++ if namespace == dav_namespace:
++ if name in ("quota-available-bytes", "quota-used-bytes"):
++ d = self.hasQuota(request)
++ d.addCallback(lambda result: result)
++ return d
++
+ return succeed(qname in self.liveProperties or self.deadProperties().contains(qname))
+
+ def readProperty(self, property, request):
+@@ -201,7 +223,6 @@
+ mimeType = self.contentType()
+ if mimeType is None:
+ return None
+- mimeType.params = None # WebDAV getcontenttype property does not include parameters
+ return davxml.GETContentType(generateContentType(mimeType))
+
+ if name == "getcontentlength":
+@@ -239,8 +260,10 @@
+ )
+
+ if name == "supported-report-set":
+- supported = [davxml.SupportedReport(report,) for report in self.supportedReports()]
+- return davxml.SupportedReportSet(*supported)
++ return davxml.SupportedReportSet(*[
++ davxml.SupportedReport(report,)
++ for report in self.supportedReports()
++ ])
+
+ if name == "supported-privilege-set":
+ return self.supportedPrivileges(request)
+@@ -252,9 +275,10 @@
+ return davxml.InheritedACLSet(*self.inheritedACLSet())
+
+ if name == "principal-collection-set":
+- d = self.principalCollections(request)
+- d.addCallback(lambda collections: davxml.PrincipalCollectionSet(*collections))
+- return d
++ return davxml.PrincipalCollectionSet(*[
++ davxml.HRef(principalCollection.principalCollectionURL())
++ for principalCollection in self.principalCollections()
++ ])
+
+ def ifAllowed(privileges, callback):
+ def onError(failure):
+@@ -286,7 +310,36 @@
+ d.addCallback(gotACL)
+ return d
+ return ifAllowed((davxml.ReadACL(),), callback)
++
++ if name == "current-user-principal":
++ return davxml.CurrentUserPrincipal(self.currentPrincipal(request).children[0])
+
++ if name == "quota-available-bytes":
++ def callback(qvalue):
++ if qvalue is None:
++ raise HTTPError(StatusResponse(
++ responsecode.NOT_FOUND,
++ "Property %s does not exist." % (sname,)
++ ))
++ else:
++ return davxml.QuotaAvailableBytes(str(qvalue[0]))
++ d = self.quota(request)
++ d.addCallback(callback)
++ return d
++
++ if name == "quota-used-bytes":
++ def callback(qvalue):
++ if qvalue is None:
++ raise HTTPError(StatusResponse(
++ responsecode.NOT_FOUND,
++ "Property %s does not exist." % (sname,)
++ ))
++ else:
++ return davxml.QuotaUsedBytes(str(qvalue[1]))
++ d = self.quota(request)
++ d.addCallback(callback)
++ return d
++
+ elif namespace == twisted_dav_namespace:
+ if name == "resource-class":
+ class ResourceClass (davxml.WebDAVTextElement):
+@@ -309,10 +362,7 @@
+ """
+ See L{IDAVResource.writeProperty}.
+ """
+- assert (
+- isinstance(property, davxml.WebDAVElement),
+- "Not a property: %r" % (property,)
+- )
++ assert isinstance(property, davxml.WebDAVElement), "Not a property: %r" % (property,)
+
+ def defer():
+ if property.protected:
+@@ -363,15 +413,28 @@
+ """
+ See L{IDAVResource.listProperties}.
+ """
+- # FIXME: A set would be better here, that that's a python 2.4+ feature.
+- qnames = list(self.liveProperties)
++ qnames = set(self.liveProperties)
+
++ # Add dynamic live properties that exist
++ dynamicLiveProperties = (
++ (dav_namespace, "quota-available-bytes" ),
++ (dav_namespace, "quota-used-bytes" ),
++ )
++ for dqname in dynamicLiveProperties:
++ has = waitForDeferred(self.hasProperty(dqname, request))
++ yield has
++ has = has.getResult()
++ if not has:
++ qnames.remove(dqname)
++
+ for qname in self.deadProperties().list():
+ if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+- qnames.append(qname)
++ qnames.add(qname)
+
+- return succeed(qnames)
++ yield qnames
+
++ listProperties = deferredGenerator(listProperties)
++
+ def listAllprop(self, request):
+ """
+ Some DAV properties should not be returned to a C{DAV:allprop} query.
+@@ -465,8 +528,22 @@
+ return super(DAVPropertyMixIn, self).displayName()
+
+ class DAVResource (DAVPropertyMixIn, StaticRenderMixin):
++ """
++ WebDAV resource.
++ """
+ implements(IDAVResource)
+
++ def __init__(self, principalCollections=None):
++ """
++ @param principalCollections: an iterable of L{IDAVPrincipalCollectionResource}s
++ which contain principals to be used in ACLs for this resource.
++ """
++ if principalCollections is not None:
++ self._principalCollections = frozenset([
++ IDAVPrincipalCollectionResource(principalCollection)
++ for principalCollection in principalCollections
++ ])
++
+ ##
+ # DAV
+ ##
+@@ -553,69 +630,65 @@
+ def supportedReports(self):
+ """
+ See L{IDAVResource.supportedReports}.
+- This implementation lists the three main ACL reports.
++ This implementation lists the three main ACL reports and expand-property.
+ """
+ result = []
+ result.append(davxml.Report(davxml.ACLPrincipalPropSet(),))
+ result.append(davxml.Report(davxml.PrincipalMatch(),))
+ result.append(davxml.Report(davxml.PrincipalPropertySearch(),))
++ result.append(davxml.Report(davxml.ExpandProperty(),))
+ return result
+
+ ##
+ # Authentication
+ ##
+
++ @inlineCallbacks
+ def authorize(self, request, privileges, recurse=False):
+ """
+ See L{IDAVResource.authorize}.
+ """
+- def onError(failure):
+- log.err("Invalid authentication details: %s" % (request,))
+- raise HTTPError(UnauthorizedResponse(
++
++ try:
++ yield self.authenticate(request)
++ except (UnauthorizedLogin, LoginFailed), e:
++ log.msg("Authentication failed: %s" % (e,))
++ response = (yield UnauthorizedResponse.makeResponse(
+ request.credentialFactories,
+ request.remoteAddr
+ ))
++ raise HTTPError(response)
+
+- def onAuth(result):
+- def onErrors(failure):
+- failure.trap(AccessDeniedError)
+-
+- # If we were unauthorized to start with (no Authorization header from client) then
+- # we should return an unauthorized response instead to force the client to login if it can
+- if request.user == davxml.Principal(davxml.Unauthenticated()):
+- response = UnauthorizedResponse(request.credentialFactories,
+- request.remoteAddr)
+- else:
+- response = NeedPrivilegesResponse(request.uri,
+- failure.value.errors)
+- #
+- # We're not adding the headers here because this response
+- # class is supposed to be a FORBIDDEN status code and
+- # "Authorization will not help" according to RFC2616
+- #
+- raise HTTPError(response)
++ try:
++ yield self.checkPrivileges(request, privileges, recurse)
++ except AccessDeniedError, e:
++ # If we were unauthenticated to start with (no Authorization header from client) then
++ # we should return an unauthorized response instead to force the client to login if it can
++ if request.authnUser == davxml.Principal(davxml.Unauthenticated()):
++ response = (yield UnauthorizedResponse.makeResponse(
++ request.credentialFactories,
++ request.remoteAddr
++ ))
++ else:
++ response = NeedPrivilegesResponse(request.uri, e.errors)
++ #
++ # We're not adding the headers here because this response
++ # class is supposed to be a FORBIDDEN status code and
++ # "Authorization will not help" according to RFC2616
++ #
++ raise HTTPError(response)
+
+- d = self.checkPrivileges(request, privileges, recurse)
+- d.addErrback(onErrors)
+- return d
+-
+- d = maybeDeferred(self.authenticate, request)
+- d.addCallbacks(onAuth, onError)
+-
+- return d
+-
++ @inlineCallbacks
+ def authenticate(self, request):
+- def loginSuccess(result):
+- request.user = result[1]
+- return request.user
+
+ if not (
+ hasattr(request, 'portal') and
+ hasattr(request, 'credentialFactories') and
+ hasattr(request, 'loginInterfaces')
+ ):
+- request.user = davxml.Principal(davxml.Unauthenticated())
+- return request.user
++ request.authnUser = davxml.Principal(davxml.Unauthenticated())
++ request.authzUser = davxml.Principal(davxml.Unauthenticated())
++ returnValue((request.authnUser, request.authzUser,))
+
+ authHeader = request.headers.getHeader('authorization')
+
+@@ -623,31 +696,32 @@
+ if authHeader[0] not in request.credentialFactories:
+ log.err("Client authentication scheme %s is not provided by server %s"
+ % (authHeader[0], request.credentialFactories.keys()))
+- raise HTTPError(responsecode.FORBIDDEN)
++
++ response = (yield UnauthorizedResponse.makeResponse(
++ request.credentialFactories,
++ request.remoteAddr
++ ))
++ raise HTTPError(response)
+ else:
+ factory = request.credentialFactories[authHeader[0]]
+
+- creds = factory.decode(authHeader[1], request)
++ creds = (yield factory.decode(authHeader[1], request))
+
+ # Try to match principals in each principal collection on the resource
+- def gotDetails(details):
+- principal = IDAVPrincipalResource(details[0])
+- principalURI = details[1]
+- return PrincipalCredentials(principal, principalURI, creds)
++ authnPrincipal, authzPrincipal = (yield self.principalsForAuthID(request, creds.username))
++ authnPrincipal = IDAVPrincipalResource(authnPrincipal)
++ authzPrincipal = IDAVPrincipalResource(authzPrincipal)
+
+- def login(pcreds):
+- d = request.portal.login(pcreds, None, *request.loginInterfaces)
+- d.addCallback(loginSuccess)
++ pcreds = PrincipalCredentials(authnPrincipal, authzPrincipal, creds)
+
+- return d
+-
+- d = self.findPrincipalForAuthID(request, creds.username)
+- d.addCallback(gotDetails).addCallback(login)
+-
+- return d
++ result = (yield request.portal.login(pcreds, None, *request.loginInterfaces))
++ request.authnUser = result[1]
++ request.authzUser = result[2]
++ returnValue((request.authnUser, request.authzUser,))
+ else:
+- request.user = davxml.Principal(davxml.Unauthenticated())
+- return request.user
++ request.authnUser = davxml.Principal(davxml.Unauthenticated())
++ request.authzUser = davxml.Principal(davxml.Unauthenticated())
++ returnValue((request.authnUser, request.authzUser,))
+
+ ##
+ # ACL
+@@ -656,49 +730,23 @@
+ def currentPrincipal(self, request):
+ """
+ @param request: the request being processed.
+- @return: the current principal, as derived from the given request.
++ @return: the current authorized principal, as derived from the given request.
+ """
+- if hasattr(request, "user"):
+- return request.user
++ if hasattr(request, "authzUser"):
++ return request.authzUser
+ else:
+ return unauthenticatedPrincipal
+
+- def principalCollections(self, request):
++ def principalCollections(self):
+ """
+ See L{IDAVResource.accessControlList}.
+-
+- This implementation tries to read the L{davxml.PrincipalCollectionSet}
+- from the dead property store of this resource and uses that. If not
+- present on this resource, it tries to get it from the parent, unless it
+- is the root or has no parent.
+ """
+- try:
+- principalCollections = self.readDeadProperty(davxml.PrincipalCollectionSet).childrenOfType(davxml.HRef)
+- except HTTPError, e:
+- if e.response.code != responsecode.NOT_FOUND:
+- raise
++ if hasattr(self, "_principalCollections"):
++ return self._principalCollections
++ else:
++ return ()
+
+- principalCollections = []
+-
+- # Try the parent
+- myURL = request.urlForResource(self)
+- if myURL != "/":
+- parentURL = parentForURL(myURL)
+-
+- parent = waitForDeferred(request.locateResource(parentURL))
+- yield parent
+- parent = parent.getResult()
+-
+- if parent:
+- principalCollections = waitForDeferred(parent.principalCollections(request))
+- yield principalCollections
+- principalCollections = principalCollections.getResult()
+-
+- yield principalCollections
+-
+- principalCollections = deferredGenerator(principalCollections)
+-
+- def defaultAccessControlList(self):
++ def defaultRootAccessControlList(self):
+ """
+ @return: the L{davxml.ACL} element containing the default access control
+ list for this resource.
+@@ -710,6 +758,17 @@
+ #
+ return readonlyACL
+
++ def defaultAccessControlList(self):
++ """
++ @return: the L{davxml.ACL} element containing the default access control
++ list for this resource.
++ """
++ #
++ # The default behaviour is no ACL; we should inherrit from the parent
++ # collection.
++ #
++ return davxml.ACL()
++
+ def setAccessControlList(self, acl):
+ """
+ See L{IDAVResource.setAccessControlList}.
+@@ -748,13 +807,16 @@
+ # 10. Verify that new acl is not in conflict with itself
+ # 11. Update acl on the resource
+
+- old_acl = waitForDeferred(self.accessControlList(request))
++ # Get the current access control list, preserving any private properties on the ACEs as
++ # we will need to keep those when we change the ACL.
++ old_acl = waitForDeferred(self.accessControlList(request, expanding=True))
+ yield old_acl
+ old_acl = old_acl.getResult()
+
+ # Check disabled
+ if old_acl is None:
+ yield None
++ return
+
+ # Need to get list of supported privileges
+ supported = []
+@@ -773,10 +835,7 @@
+ yield supportedPrivs
+ supportedPrivs = supportedPrivs.getResult()
+ for item in supportedPrivs.children:
+- assert (
+- isinstance(item, davxml.SupportedPrivilege),
+- "Not a SupportedPrivilege: %r" % (item,)
+- )
++ assert isinstance(item, davxml.SupportedPrivilege), "Not a SupportedPrivilege: %r" % (item,)
+ addSupportedPrivilege(item)
+
+ # Steps 1 - 6
+@@ -910,8 +969,7 @@
+ supportedPrivs = supportedPrivs.getResult()
+
+ # Other principals types don't make sense as actors.
+- assert (
+- principal.children[0].name in ("unauthenticated", "href"),
++ assert principal.children[0].name in ("unauthenticated", "href"), (
+ "Principal is not an actor: %r" % (principal,)
+ )
+
+@@ -1019,15 +1077,16 @@
+ def getMyURL():
+ url = request.urlForResource(self)
+
+- assert url is not None, "urlForResource(self) returned None for resource %s" % (self,)
++ assert url is not None, (
++ "urlForResource(self) returned None for resource %s" % (self,)
++ )
+
+ return url
+
+ try:
+ acl = self.readDeadProperty(davxml.ACL)
+ except HTTPError, e:
+- assert (
+- e.response.code == responsecode.NOT_FOUND,
++ assert e.response.code == responsecode.NOT_FOUND, (
+ "Expected %s response from readDeadProperty() exception, not %s"
+ % (responsecode.NOT_FOUND, e.response.code)
+ )
+@@ -1038,9 +1097,9 @@
+
+ if myURL == "/":
+ # If we get to the root without any ACLs, then use the default.
++ acl = self.defaultRootAccessControlList()
++ else:
+ acl = self.defaultAccessControlList()
+- else:
+- acl = davxml.ACL()
+
+ # Dynamically update privileges for those ace's that are inherited.
+ if inheritance:
+@@ -1076,7 +1135,7 @@
+ # Adjust ACE for inherit on this resource
+ children = list(ace.children)
+ children.remove(TwistedACLInheritable())
+- children.append(davxml.Inherited(davxml.HRef.fromString(parentURL)))
++ children.append(davxml.Inherited(davxml.HRef(parentURL)))
+ aces.append(davxml.ACE(*children))
+ else:
+ aces.extend(inherited_aces)
+@@ -1105,8 +1164,7 @@
+ the child resource loop and supply those to the checkPrivileges on each child.
+
+ @param request: the L{IRequest} for the request in progress.
+- @return: a C{list} of L{Ace}s that child resources of this one will
+- inherit and which will match the currently authenticated principal.
++ @return: a C{list} of L{Ace}s that child resources of this one will inherit.
+ """
+
+ # Get the parent ACLs with inheritance and preserve the <inheritable> element.
+@@ -1128,21 +1186,9 @@
+ # Adjust ACE for inherit on this resource
+ children = list(ace.children)
+ children.remove(TwistedACLInheritable())
+- children.append(davxml.Inherited(davxml.HRef.fromString(request.urlForResource(self))))
++ children.append(davxml.Inherited(davxml.HRef(request.urlForResource(self))))
+ aces.append(davxml.ACE(*children))
+-
+- # Filter out those that do not have a principal match with the current principal
+- principal = self.currentPrincipal(request)
+- filteredaces = []
+- for ace in aces:
+- if self.matchPrincipal(principal, ace.principal, request):
+- if ace.invert:
+- continue
+- else:
+- if not ace.invert:
+- continue
+- filteredaces.append(ace)
+- yield filteredaces
++ yield aces
+
+ inheritedACEsforChildren = deferredGenerator(inheritedACEsforChildren)
+
+@@ -1152,49 +1198,69 @@
+
+ This implementation returns an empty set.
+ """
+-
+ return []
+
+- def findPrincipalForAuthID(self, request, authid):
++ def principalsForAuthID(self, request, authid):
+ """
++ Return authentication and authorization prinicipal identifiers for the
++ authentication identifer passed in. In this implementation authn and authz
++ principals are the same.
++
+ @param request: the L{IRequest} for the request in progress.
+ @param authid: a string containing the
+ authentication/authorization identifier for the principal
+ to lookup.
+- @return: a deferred tuple of C{(principal, principalURI)}
+- where: C{principal} is the L{Principal} that is found;
+- C{principalURI} is the C{str} URI of the principal.
++ @return: a deferred tuple of two tuples. Each tuple is
++ C{(principal, principalURI)} where: C{principal} is the L{Principal}
++ that is found; {principalURI} is the C{str} URI of the principal.
++ The first tuple corresponds to authentication identifiers,
++ the second to authorization identifiers.
+ It will errback with an HTTPError(responsecode.FORBIDDEN) if
+ the principal isn't found.
+ """
+- # Try to match principals in each principal collection on the resource
+- collections = waitForDeferred(self.principalCollections(request))
+- yield collections
+- collections = collections.getResult()
++ authnPrincipal = self.findPrincipalForAuthID(authid)
+
+- for collection in collections:
+- principalURI = joinURL(str(collection), authid)
++ if authnPrincipal is None:
++ log.msg("Could not find the principal resource for user id: %s" % (authid,))
++ raise HTTPError(responsecode.FORBIDDEN)
+
+- principal = waitForDeferred(request.locateResource(principalURI))
+- yield principal
+- principal = principal.getResult()
++ d = self.authorizationPrincipal(request, authid, authnPrincipal)
++ d.addCallback(lambda authzPrincipal: (authnPrincipal, authzPrincipal))
++ return d
+
+- if isPrincipalResource(principal):
+- yield (principal, principalURI)
+- return
+- else:
+- principalCollections = waitForDeferred(self.principalCollections(request))
+- yield principalCollections
+- principalCollections = principalCollections.getResult()
++ def findPrincipalForAuthID(self, authid):
++ """
++ Return authentication and authoirization prinicipal identifiers for the
++ authentication identifer passed in. In this implementation authn and authz
++ principals are the same.
+
+- if len(principalCollections) == 0:
+- log.msg("DAV:principal-collection-set property cannot be found on the resource being authorized: %s" % self)
+- else:
+- log.msg("Could not find principal matching user id: %s" % authid)
+- raise HTTPError(responsecode.FORBIDDEN)
++ @param authid: a string containing the
++ authentication/authorization identifier for the principal
++ to lookup.
++ @return: a tuple of C{(principal, principalURI)} where: C{principal} is the L{Principal}
++ that is found; {principalURI} is the C{str} URI of the principal.
++ If not found return None.
++ """
++ for collection in self.principalCollections():
++ principal = collection.principalForUser(authid)
++ if principal is not None:
++ return principal
++ return None
+
+- findPrincipalForAuthID = deferredGenerator(findPrincipalForAuthID)
+-
++ def authorizationPrincipal(self, request, authid, authnPrincipal):
++ """
++ Determine the authorization principal for the given request and authentication principal.
++ This implementation simply uses aht authentication principalk as the authoization principal.
++
++ @param request: the L{IRequest} for the request in progress.
++ @param authid: a string containing the uthentication/authorization identifier
++ for the principal to lookup.
++ @param authnPrincipal: the L{IDAVPrincipal} for the authenticated principal
++ @return: a deferred result C{tuple} of (L{IDAVPrincipal}, C{str}) containing the authorization principal
++ resource and URI respectively.
++ """
++ return succeed(authnPrincipal)
++
+ def samePrincipal(self, principal1, principal2):
+ """
+ Check whether the two prinicpals are exactly the same in terms of
+@@ -1219,7 +1285,6 @@
+ return False
+
+ def matchPrincipal(self, principal1, principal2, request):
+-
+ """
+ Check whether the principal1 is a principal in the set defined by
+ principal2.
+@@ -1244,6 +1309,9 @@
+ if isinstance(principal1, davxml.Unauthenticated):
+ yield False
+ return
++ elif isinstance(principal1, davxml.All):
++ yield False
++ return
+ else:
+ yield True
+ return
+@@ -1260,10 +1328,7 @@
+ yield False
+ return
+
+- assert (
+- isinstance(principal1, davxml.HRef),
+- "Not an HRef: %r" % (principal1,)
+- )
++ assert isinstance(principal1, davxml.HRef), "Not an HRef: %r" % (principal1,)
+
+ principal2 = waitForDeferred(self.resolvePrincipal(principal2, request))
+ yield principal2
+@@ -1271,7 +1336,6 @@
+
+ assert principal2 is not None, "principal2 is None"
+
+-
+ # Compare two HRefs and do group membership test as well
+ if principal1 == principal2:
+ yield True
+@@ -1289,6 +1353,7 @@
+
+ matchPrincipal = deferredGenerator(matchPrincipal)
+
++ @deferredGenerator
+ def principalIsGroupMember(self, principal1, principal2, request):
+ """
+ Check whether one principal is a group member of another.
+@@ -1299,18 +1364,21 @@
+ @return: L{Deferred} with result C{True} if principal1 is a member of principal2, C{False} otherwise
+ """
+
+- def testGroup(group):
+- # Get principal resource for principal2
+- if group and isinstance(group, DAVPrincipalResource):
+- members = group.groupMembers()
+- if principal1 in members:
+- return True
+-
+- return False
++ d = waitForDeferred(request.locateResource(principal2))
++ yield d
++ group = d.getResult()
+
+- d = request.locateResource(principal2)
+- d.addCallback(testGroup)
+- return d
++ # Get principal resource for principal2
++ if group and isinstance(group, DAVPrincipalResource):
++ d = waitForDeferred(group.expandedGroupMembers())
++ yield d
++ members = d.getResult()
++ for member in members:
++ if member.principalURL() == principal1:
++ yield True
++ return
++
++ yield False
+
+ def validPrincipal(self, ace_principal, request):
+ """
+@@ -1351,11 +1419,16 @@
+ @return C{True} if C{href_principal} is valid, C{False} otherwise.
+
+ This implementation tests for a href element that corresponds to
+- a principal resource.
++ a principal resource and matches the principal-URL.
+ """
+- # Must have the principal resource type
++
++ # Must have the principal resource type and must match the principal-URL
++
++ def _matchPrincipalURL(resource):
++ return isPrincipalResource(resource) and resource.principalURL() == str(href_principal)
++
+ d = request.locateResource(str(href_principal))
+- d.addCallback(isPrincipalResource)
++ d.addCallback(_matchPrincipalURL)
+ return d
+
+ def resolvePrincipal(self, principal, request):
+@@ -1404,8 +1477,7 @@
+ try:
+ principal = principal.getResult()
+ except HTTPError, e:
+- assert (
+- e.response.code == responsecode.NOT_FOUND,
++ assert e.response.code == responsecode.NOT_FOUND, (
+ "Expected %s response from readProperty() exception, not %s"
+ % (responsecode.NOT_FOUND, e.response.code)
+ )
+@@ -1432,15 +1504,15 @@
+ log.err("DAV:self ACE is set on non-principal resource %r" % (self,))
+ yield None
+ return
+- principal = davxml.HRef.fromString(self.principalURL())
++ principal = davxml.HRef(self.principalURL())
+
+ if isinstance(principal, davxml.HRef):
+ yield principal
++ return
+ else:
+ yield None
+
+- assert (
+- isinstance(principal, (davxml.All, davxml.Authenticated, davxml.Unauthenticated)),
++ assert isinstance(principal, (davxml.All, davxml.Authenticated, davxml.Unauthenticated)), (
+ "Not a meta-principal: %r" % (principal,)
+ )
+
+@@ -1517,6 +1589,280 @@
+ return None
+
+ ##
++ # Quota
++ ##
++
++ """
++ The basic policy here is to define a private 'quota-root' property on a collection.
++ That property will contain the maximum allowed bytes for the collections and all
++ its contents.
++
++ In order to determine the quota property values on a resource, the server must look
++ for the private property on that resource and any of its parents. If found on a parent,
++ then that parent should be queried for quota information. If not found, no quota
++ exists for the resource.
++
++ To determine tha actual quota in use we will cache the used byte count on the quota-root
++ collection in another private property. It is the servers responsibility to
++ keep that property up to date by adjusting it after every PUT, DELETE, COPY,
++ MOVE, MKCOL, PROPPATCH, ACL, POST or any other method that may affect the size of
++ stored data. If the private property is not present, the server will fall back to
++ getting the size by iterating over all resources (this is done in static.py).
++
++ """
++
++ def quota(self, request):
++ """
++ Get current available & used quota values for this resource's quota root
++ collection.
++
++ @return: an L{Defered} with result C{tuple} containing two C{int}'s the first is
++ quota-available-bytes, the second is quota-used-bytes, or
++ C{None} if quota is not defined on the resource.
++ """
++
++ # See if already cached
++ if not hasattr(request, "quota"):
++ request.quota = {}
++ if request.quota.has_key(self):
++ yield request.quota[self]
++ return
++
++ # Check this resource first
++ if self.isCollection():
++ qroot = self.quotaRoot(request)
++ if qroot is not None:
++ used = waitForDeferred(self.currentQuotaUse(request))
++ yield used
++ used = used.getResult()
++ available = qroot - used
++ if available < 0:
++ available = 0
++ request.quota[self] = (available, used)
++ yield request.quota[self]
++ return
++
++ # Check the next parent
++ url = request.urlForResource(self)
++ if url != "/":
++ parent = waitForDeferred(request.locateResource(parentForURL(url)))
++ yield parent
++ parent = parent.getResult()
++ d = waitForDeferred(parent.quota(request))
++ yield d
++ request.quota[self] = d.getResult()
++ else:
++ request.quota[self] = None
++
++ yield request.quota[self]
++ return
++
++ quota = deferredGenerator(quota)
++
++ def hasQuota(self, request):
++ """
++ Check whether this resource is undre quota control by checking each parent to see if
++ it has a quota root.
++
++ @return: C{True} if under quota control, C{False} if not.
++ """
++
++ # Check this one first
++ if self.hasQuotaRoot(request):
++ yield True
++ return
++
++ # Look at each parent
++ try:
++ url = request.urlForResource(self)
++ if url != "/":
++ parent = waitForDeferred(request.locateResource(parentForURL(url)))
++ yield parent
++ parent = parent.getResult()
++ d = waitForDeferred(parent.hasQuota(request))
++ yield d
++ yield d.getResult()
++ else:
++ yield False
++ except NoURLForResourceError:
++ yield False
++
++ hasQuota = deferredGenerator(hasQuota)
++
++ def hasQuotaRoot(self, request):
++ """
++ @return: a C{True} if this resource has quota root, C{False} otherwise.
++ """
++ return self.hasDeadProperty(TwistedQuotaRootProperty)
++
++ def quotaRoot(self, request):
++ """
++ @return: a C{int} containing the maximum allowed bytes if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++ if self.hasDeadProperty(TwistedQuotaRootProperty):
++ return int(str(self.readDeadProperty(TwistedQuotaRootProperty)))
++ else:
++ return None
++
++ def quotaRootParent(self, request):
++ """
++ Return the next quota root above this resource.
++
++ @return: L{DAVResource} or C{None}
++ """
++
++ # Check the next parent
++ url = request.urlForResource(self)
++ while (url != "/"):
++ url = parentForURL(url)
++ parent = waitForDeferred(request.locateResource(url))
++ yield parent
++ parent = parent.getResult()
++ if parent.hasQuotaRoot(request):
++ yield parent
++ return
++
++ yield None
++
++ quotaRootParent = deferredGenerator(quotaRootParent)
++
++ def setQuotaRoot(self, request, maxsize):
++ """
++ @param maxsize: a C{int} containing the maximum allowed bytes for the contents
++ of this collection, or C{None} tp remove quota restriction.
++ """
++ assert self.isCollection(), "Only collections can have a quota root"
++ assert maxsize is None or isinstance(maxsize, int), "maxsize must be an int or None"
++
++ if maxsize is not None:
++ self.writeDeadProperty(TwistedQuotaRootProperty(str(maxsize)))
++ else:
++ # Remove both the root and the cached used value
++ self.removeDeadProperty(TwistedQuotaRootProperty)
++ self.removeDeadProperty(TwistedQuotaUsedProperty)
++
++ def quotaSize(self, request):
++ """
++ Get the size of this resource (if its a collection get total for all children as well).
++ TODO: Take into account size of dead-properties.
++
++ @return: a C{int} containing the size of the resource.
++ """
++ unimplemented(self)
++
++ def checkQuota(self, request, available):
++ """
++ Check to see whether all quota roots have sufficient available bytes.
++ We currently do not use hierarchical quota checks - i.e. only the most
++ immediate quota root parent is checked for quota.
++
++ @param available: a C{int} containing the additional quota required.
++ @return: C{True} if there is sufficient quota remaining on all quota roots,
++ C{False} otherwise.
++ """
++
++ quotaroot = self
++ while(quotaroot is not None):
++ # Check quota on this root (if it has one)
++ quota = quotaroot.quotaRoot(request)
++ if quota is not None:
++ if available > quota[0]:
++ yield False
++ return
++
++ # Check the next parent with a quota root
++ quotaroot = waitForDeferred(quotaroot.quotaRootParent(request))
++ yield quotaroot
++ quotaroot = quotaroot.getResult()
++
++ yield True
++
++ checkQuota = deferredGenerator(checkQuota)
++
++ def quotaSizeAdjust(self, request, adjust):
++ """
++ Update the quota used value on all quota root parents of this resource.
++
++ @param adjust: a C{int} containing the number of bytes added (positive) or
++ removed (negative) that should be used to adjust the cached total.
++ """
++
++ # Check this resource first
++ if self.isCollection():
++ if self.hasQuotaRoot(request):
++ d = waitForDeferred(self.updateQuotaUse(request, adjust))
++ yield d
++ d.getResult()
++ yield None
++ return
++
++ # Check the next parent
++ url = request.urlForResource(self)
++ if url != "/":
++ parent = waitForDeferred(request.locateResource(parentForURL(url)))
++ yield parent
++ parent = parent.getResult()
++ d = waitForDeferred(parent.quotaSizeAdjust(request, adjust))
++ yield d
++ d.getResult()
++
++ yield None
++
++ quotaSizeAdjust = deferredGenerator(quotaSizeAdjust)
++
++ def currentQuotaUse(self, request):
++ """
++ Get the cached quota use value, or if not present (or invalid) determine
++ quota use by brute force.
++
++ @return: an L{Deferred} with a C{int} result containing the current used byte if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++ assert self.isCollection(), "Only collections can have a quota root"
++ assert self.hasQuotaRoot(request), "Quota use only on quota root collection"
++
++ # Try to get the cached value property
++ if self.hasDeadProperty(TwistedQuotaUsedProperty):
++ return succeed(int(str(self.readDeadProperty(TwistedQuotaUsedProperty))))
++ else:
++ # Do brute force size determination and cache the result in the private property
++ def _defer(result):
++ self.writeDeadProperty(TwistedQuotaUsedProperty(str(result)))
++ return result
++ d = self.quotaSize(request)
++ d.addCallback(_defer)
++ return d
++
++ def updateQuotaUse(self, request, adjust):
++ """
++ Update the quota used value on this resource.
++
++ @param adjust: a C{int} containing the number of bytes added (positive) or
++ removed (negative) that should be used to adjust the cached total.
++ @return: an L{Deferred} with a C{int} result containing the current used byte if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++ assert self.isCollection(), "Only collections can have a quota root"
++
++ # Get current value
++ def _defer(size):
++ size += adjust
++
++ # Sanity check the resulting size
++ if size >= 0:
++ self.writeDeadProperty(TwistedQuotaUsedProperty(str(size)))
++ else:
++ # Remove the dead property and re-read to do brute force quota calc
++ log.msg("Attempt to set quota used to a negative value: %s (adjustment: %s)" % (size, adjust,))
++ self.removeDeadProperty(TwistedQuotaUsedProperty)
++ return self.currentQuotaUse(request)
++
++ d = self.currentQuotaUse(request)
++ d.addCallback(_defer)
++ return d
++
++ ##
+ # HTTP
+ ##
+
+@@ -1525,15 +1871,11 @@
+ #litmus = request.headers.getRawHeaders("x-litmus")
+ #if litmus: log.msg("*** Litmus test: %s ***" % (litmus,))
+
+- # FIXME: Learn how to use twisted logging facility, wsanchez
+- protocol = "HTTP/%s.%s" % request.clientproto
+- log.msg("%s %s %s" % (request.method, urllib.unquote(request.uri), protocol))
+-
+ #
+ # If this is a collection and the URI doesn't end in "/", redirect.
+ #
+- if self.isCollection() and request.uri[-1:] != "/":
+- return RedirectResponse(request.uri + "/")
++ if self.isCollection() and request.path[-1:] != "/":
++ return RedirectResponse(request.unparseURL(path=urllib.quote(request.path, safe=':/')+'/'))
+
+ def setHeaders(response):
+ response = IResponse(response)
+@@ -1567,7 +1909,7 @@
+ def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
+ return succeed(None)
+
+-class DAVPrincipalResource (DAVLeafResource):
++class DAVPrincipalResource (DAVResource):
+ """
+ Resource representing a WebDAV principal. (RFC 3744, section 2)
+ """
+@@ -1577,7 +1919,7 @@
+ # WebDAV
+ ##
+
+- liveProperties = DAVLeafResource.liveProperties + (
++ liveProperties = DAVResource.liveProperties + (
+ (dav_namespace, "alternate-URI-set"),
+ (dav_namespace, "principal-URL" ),
+ (dav_namespace, "group-member-set" ),
+@@ -1585,14 +1927,11 @@
+ )
+
+ def davComplianceClasses(self):
+- return ("1",)
++ return ("1", "access-control",)
+
+ def isCollection(self):
+ return False
+
+- def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
+- return succeed(None)
+-
+ def readProperty(self, property, request):
+ def defer():
+ if type(property) is tuple:
+@@ -1610,10 +1949,20 @@
+ return davxml.PrincipalURL(davxml.HRef(self.principalURL()))
+
+ if name == "group-member-set":
+- return davxml.GroupMemberSet(*[davxml.HRef(p) for p in self.groupMembers()])
++ def callback(members):
++ return davxml.GroupMemberSet(*[davxml.HRef(p.principalURL()) for p in members])
++
++ d = self.groupMembers()
++ d.addCallback(callback)
++ return d
+
+ if name == "group-membership":
+- return davxml.GroupMembership(*[davxml.HRef(g) for g in self.groupMemberships()])
++ def callback(memberships):
++ return davxml.GroupMembership(*[davxml.HRef(g.principalURL()) for g in memberships])
++
++ d = self.groupMemberships()
++ d.addCallback(callback)
++ return d
+
+ if name == "resourcetype":
+ if self.isCollection():
+@@ -1655,7 +2004,7 @@
+ principals. Subclasses should override this method to provide member
+ URLs for this resource if appropriate.
+ """
+- return ()
++ return succeed(())
+
+ def groupMemberships(self):
+ """
+@@ -1666,6 +2015,7 @@
+ """
+ unimplemented(self)
+
++ @deferredGenerator
+ def principalMatch(self, href):
+ """
+ Check whether the supplied principal matches this principal or is a
+@@ -1675,10 +2025,33 @@
+ """
+ uri = str(href)
+ if self.principalURL() == uri:
+- return True
++ yield True
++ return
+ else:
+- return uri in self.groupMembers()
++ d = waitForDeferred(self.expandedGroupMembers())
++ yield d
++ members = d.getResult()
++ member_uris = [member.principalURL() for member in members]
++ yield uri in member_uris
+
++class DAVPrincipalCollectionResource (DAVResource):
++ """
++ WebDAV principal collection resource. (RFC 3744, section 5.8)
++ """
++ implements(IDAVPrincipalCollectionResource)
++
++ def __init__(self, url, principalCollections=()):
++ """
++ @param url: This resource's URL.
++ """
++ DAVResource.__init__(self, principalCollections=principalCollections)
++
++ assert url.endswith("/"), "Collection URL must end in '/'"
++ self._url = url
++
++ def principalCollectionURL(self):
++ return self._url
++
+ class AccessDeniedError(Exception):
+ def __init__(self, errors):
+ """
+@@ -1718,6 +2091,37 @@
+ davxml.registerElement(TwistedACLInheritable)
+ davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
+
++class TwistedGETContentMD5 (davxml.WebDAVTextElement):
++ """
++ MD5 hash of the resource content.
++ """
++ namespace = twisted_dav_namespace
++ name = "getcontentmd5"
++
++davxml.registerElement(TwistedGETContentMD5)
++
++"""
++When set on a collection, this property indicates that the collection has a quota limit for
++the size of all resources stored in the collection (and any associate meta-data such as properties).
++The value is a number - the maximum size in bytes allowed.
++"""
++class TwistedQuotaRootProperty (davxml.WebDAVTextElement):
++ namespace = twisted_private_namespace
++ name = "quota-root"
++
++davxml.registerElement(TwistedQuotaRootProperty)
++
++"""
++When set on a collection, this property contains the cached running total of the size of all
++resources stored in the collection (and any associate meta-data such as properties).
++The value is a number - the size in bytes used.
++"""
++class TwistedQuotaUsedProperty (davxml.WebDAVTextElement):
++ namespace = twisted_private_namespace
++ name = "quota-used"
++
++davxml.registerElement(TwistedQuotaUsedProperty)
++
+ allACL = davxml.ACL(
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,161 @@
+Index: twisted/web2/dav/static.py
+===================================================================
+--- twisted/web2/dav/static.py (revision 19773)
++++ twisted/web2/dav/static.py (working copy)
+@@ -28,16 +28,17 @@
+
+ __all__ = ["DAVFile"]
+
+-import os
+-
++from twisted.internet.defer import succeed, deferredGenerator, waitForDeferred
++from twisted.python.filepath import InsecurePath
+ from twisted.python import log
+-from twisted.internet.defer import succeed, deferredGenerator, waitForDeferred
+-from twisted.web2.static import File
++from twisted.web2 import http_headers
+ from twisted.web2 import responsecode, dirlist
+-from twisted.web2.http import RedirectResponse
+ from twisted.web2.dav import davxml
+ from twisted.web2.dav.resource import DAVResource, davPrivilegeSet
++from twisted.web2.dav.resource import TwistedGETContentMD5
+ from twisted.web2.dav.util import bindMethods
++from twisted.web2.http import HTTPError, StatusResponse, RedirectResponse
++from twisted.web2.static import File
+
+ try:
+ from twisted.web2.dav.xattrprops import xattrPropertyStore as DeadPropertyStore
+@@ -52,9 +53,11 @@
+
+ Extends twisted.web2.static.File to handle WebDAV methods.
+ """
+- def __init__(self, path,
+- defaultType="text/plain",
+- indexNames=None):
++ def __init__(
++ self, path,
++ defaultType="text/plain", indexNames=None,
++ principalCollections=()
++ ):
+ """
+ @param path: the path of the file backing this resource.
+ @param defaultType: the default mime type (as a string) for this
+@@ -62,11 +65,14 @@
+ @param indexNames: a sequence of index file names.
+ @param acl: an L{IDAVAccessControlList} with the .
+ """
+- super(DAVFile, self).__init__(path,
+- defaultType = defaultType,
+- ignoredExts = (),
+- processors = None,
+- indexNames = indexNames)
++ File.__init__(
++ self, path,
++ defaultType = defaultType,
++ ignoredExts = (),
++ processors = None,
++ indexNames = indexNames,
++ )
++ DAVResource.__init__(self, principalCollections=principalCollections)
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self.fp.path)
+@@ -75,6 +81,13 @@
+ # WebDAV
+ ##
+
++ def etag(self):
++ if not self.fp.exists(): return None
++ if self.hasDeadProperty(TwistedGETContentMD5):
++ return http_headers.ETag(str(self.readDeadProperty(TwistedGETContentMD5)))
++ else:
++ return super(DAVFile, self).etag()
++
+ def davComplianceClasses(self):
+ return ("1", "access-control") # Add "2" when we have locking
+
+@@ -87,7 +100,6 @@
+ """
+ See L{IDAVResource.isCollection}.
+ """
+- for child in self.listChildren(): return True
+ return self.fp.isdir()
+
+ ##
+@@ -98,6 +110,50 @@
+ return succeed(davPrivilegeSet)
+
+ ##
++ # Quota
++ ##
++
++ def quotaSize(self, request):
++ """
++ Get the size of this resource.
++ TODO: Take into account size of dead-properties. Does stat
++ include xattrs size?
++
++ @return: an L{Deferred} with a C{int} result containing the size of the resource.
++ """
++ if self.isCollection():
++ def walktree(top):
++ """
++ Recursively descend the directory tree rooted at top,
++ calling the callback function for each regular file
++
++ @param top: L{FilePath} for the directory to walk.
++ """
++
++ total = 0
++ for f in top.listdir():
++ child = top.child(f)
++ if child.isdir():
++ # It's a directory, recurse into it
++ result = waitForDeferred(walktree(child))
++ yield result
++ total += result.getResult()
++ elif child.isfile():
++ # It's a file, call the callback function
++ total += child.getsize()
++ else:
++ # Unknown file type, print a message
++ pass
++
++ yield total
++
++ walktree = deferredGenerator(walktree)
++
++ return walktree(self.fp)
++ else:
++ return succeed(self.fp.getsize())
++
++ ##
+ # Workarounds for issues with File
+ ##
+
+@@ -112,8 +168,12 @@
+ See L{IResource}C{.locateChild}.
+ """
+ # If getChild() finds a child resource, return it
+- child = self.getChild(segments[0])
+- if child is not None: return (child, segments[1:])
++ try:
++ child = self.getChild(segments[0])
++ if child is not None:
++ return (child, segments[1:])
++ except InsecurePath:
++ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Invalid URL path"))
+
+ # If we're not backed by a directory, we have no children.
+ # But check for existance first; we might be a collection resource
+@@ -132,7 +192,9 @@
+ return (self.createSimilarFile(self.fp.child(path).path), segments[1:])
+
+ def createSimilarFile(self, path):
+- return self.__class__(path, defaultType=self.defaultType, indexNames=self.indexNames[:])
++ return self.__class__(
++ path, defaultType=self.defaultType, indexNames=self.indexNames[:],
++ principalCollections=self.principalCollections())
+
+ #
+ # Attach method handlers to DAVFile
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.stream.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.stream.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.stream.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.stream.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,84 @@
+Index: twisted/web2/dav/stream.py
+===================================================================
+--- twisted/web2/dav/stream.py (revision 0)
++++ twisted/web2/dav/stream.py (revision 0)
+@@ -0,0 +1,79 @@
++##
++# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++#
++# DRI: Cyrus Daboo, cdaboo at apple.com
++##
++
++"""
++Class that implements a stream that calculates the MD5 hash of the data
++as the data is read.
++"""
++
++__all__ = ["MD5StreamWrapper"]
++
++from twisted.internet.defer import Deferred
++from twisted.web2.stream import SimpleStream
++
++try:
++ from hashlib import md5
++except ImportError:
++ from md5 import new as md5
++
++class MD5StreamWrapper(SimpleStream):
++
++ def __init__(self, wrap):
++
++ assert wrap is not None, "Must have a stream to wrap."
++
++ self.stream = wrap
++
++ # Init MD5
++ self.md5 = md5()
++
++ def read(self):
++ assert self.md5 is not None, "Cannot call read after close."
++
++ # Read from wrapped stream first
++ b = self.stream.read()
++
++ if isinstance(b, Deferred):
++ def _gotData(data):
++ if data is not None:
++ self.md5.update(data)
++ return data
++ b.addCallback(_gotData)
++ else:
++ # Update current MD5 state
++ if b is not None:
++ self.md5.update(str(b))
++
++ return b
++
++ def close(self):
++ # Close out the MD5 hash
++ self.md5value = self.md5.hexdigest()
++ self.md5 = None
++
++ SimpleStream.close(self)
++
++ def getMD5(self):
++ assert hasattr(self, "md5value"), "Stream has to be closed first"
++ return self.md5value
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.data.quota_100.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.data.quota_100.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.data.quota_100.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.data.quota_100.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,15 @@
+Index: twisted/web2/dav/test/data/quota_100.txt
+===================================================================
+--- twisted/web2/dav/test/data/quota_100.txt (revision 0)
++++ twisted/web2/dav/test/data/quota_100.txt (revision 0)
+@@ -0,0 +1,10 @@
++123456789
++123456789
++123456789
++123456789
++123456789
++123456789
++123456789
++123456789
++123456789
++123456789
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_acl.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,108 @@
+Index: twisted/web2/dav/test/test_acl.py
+===================================================================
+--- twisted/web2/dav/test/test_acl.py (revision 19773)
++++ twisted/web2/dav/test/test_acl.py (working copy)
+@@ -30,6 +30,7 @@
+ from twisted.web2.auth import basic
+ from twisted.web2.stream import MemoryStream
+ from twisted.web2.dav import davxml
++from twisted.web2.dav.resource import DAVPrincipalCollectionResource
+ from twisted.web2.dav.util import davXMLFromStream
+ from twisted.web2.dav.auth import TwistedPasswordProperty, IPrincipal, DavRealm, TwistedPropertyChecker, AuthenticationWrapper
+
+@@ -38,6 +39,25 @@
+ from twisted.web2.dav.test.util import Site, serialize
+ from twisted.web2.dav.test.test_resource import TestResource, TestDAVPrincipalResource
+
++class TestPrincipalsCollection(DAVPrincipalCollectionResource, TestResource):
++ def __init__(self, url, children):
++ DAVPrincipalCollectionResource.__init__(self, url)
++ TestResource.__init__(self, url, children, principalCollections=(self,))
++
++ def principalForUser(self, user):
++ return self.principalForShortName('users', user)
++
++ def principalForAuthID(self, creds):
++ return self.principalForShortName('users', creds.username)
++
++ def principalForShortName(self, type, shortName):
++ typeResource = self.children.get(type, None)
++ user = None
++ if typeResource:
++ user = typeResource.children.get(shortName, None)
++
++ return user
++
+ class ACL(twisted.web2.dav.test.util.TestCase):
+ """
+ RFC 3744 (WebDAV ACL) tests.
+@@ -46,8 +66,18 @@
+ if not hasattr(self, "docroot"):
+ self.docroot = self.mktemp()
+ os.mkdir(self.docroot)
+- rootresource = self.resource_class(self.docroot)
+
++ userResource = TestDAVPrincipalResource("/principals/users/user01")
++ userResource.writeDeadProperty(TwistedPasswordProperty("user01"))
++
++ principalCollection = TestPrincipalsCollection(
++ "/principals/",
++ children={"users": TestPrincipalsCollection(
++ "/principals/users/",
++ children={"user01": userResource})})
++
++ rootResource = self.resource_class(self.docroot, principalCollections=(principalCollection,))
++
+ portal = Portal(DavRealm())
+ portal.registerChecker(TwistedPropertyChecker())
+
+@@ -56,26 +86,14 @@
+ loginInterfaces = (IPrincipal,)
+
+ self.site = Site(AuthenticationWrapper(
+- rootresource,
++ rootResource,
+ portal,
+ credentialFactories,
+ loginInterfaces
+ ))
+
+- rootresource.setAccessControlList(self.grant(davxml.All()))
++ rootResource.setAccessControlList(self.grant(davxml.All()))
+
+- userresource = TestDAVPrincipalResource("/principals/user01")
+- userresource.writeDeadProperty(TwistedPasswordProperty.fromString("user01"))
+-
+- rootresource.putChild(
+- "principals",
+- TestResource("/principals", {"user01": userresource})
+- )
+-
+- rootresource.writeDeadProperty(
+- davxml.PrincipalCollectionSet(davxml.HRef("/principals/"))
+- )
+-
+ for name, acl in (
+ ("none" , self.grant()),
+ ("read" , self.grant(davxml.Read())),
+@@ -361,9 +379,7 @@
+ if method == "GET":
+ ok = responsecode.OK
+ elif method == "REPORT":
+- # BAD_REQUEST in the allowed case, because we're not actually
+- # including the required XML in the request body.
+- ok = responsecode.BAD_REQUEST
++ ok = responsecode.MULTI_STATUS
+ else:
+ raise AssertionError("We shouldn't be here. (method = %r)" % (method,))
+
+@@ -377,6 +393,9 @@
+ path = os.path.join(self.docroot, name)
+
+ request = SimpleRequest(self.site, method, "/" + name)
++ if method == "REPORT":
++ request.stream = MemoryStream(davxml.PrincipalPropertySearch().toxml())
++
+ _add_auth_header(request)
+
+ def test(response, code=code, path=path):
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_copy.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_copy.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_copy.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_copy.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,24 @@
+Index: twisted/web2/dav/test/test_copy.py
+===================================================================
+--- twisted/web2/dav/test/test_copy.py (revision 19773)
++++ twisted/web2/dav/test/test_copy.py (working copy)
+@@ -22,9 +22,9 @@
+ # DRI: Wilfredo Sanchez, wsanchez at apple.com
+ ##
+
++from hashlib import md5
+ import os
+ import urllib
+-import md5
+
+ import twisted.web2.dav.test.util
+ from twisted.web2 import responsecode
+@@ -161,7 +161,7 @@
+ yield (request, do_test)
+
+ def sumFile(path):
+- m = md5.new()
++ m = md5()
+
+ if os.path.isfile(path):
+ f = file(path)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_pipeline.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_pipeline.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_pipeline.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_pipeline.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,74 @@
+Index: twisted/web2/dav/test/test_pipeline.py
+===================================================================
+--- twisted/web2/dav/test/test_pipeline.py (revision 0)
++++ twisted/web2/dav/test/test_pipeline.py (revision 0)
+@@ -0,0 +1,69 @@
++##
++# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++#
++# DRI: Wilfredo Sanchez, wsanchez at apple.com
++##
++from twisted.internet import utils
++from twisted.web2.test import test_server
++from twisted.web2 import resource
++from twisted.web2 import http
++from twisted.web2.test import test_http
++import sys
++
++from twisted.internet.defer import waitForDeferred, deferredGenerator
++
++from twisted.python import util
++
++class Pipeline(test_server.BaseCase):
++ """
++ Pipelined request
++ """
++ class TestResource(resource.LeafResource):
++ def render(self, req):
++ return http.Response(stream="Host:%s, Path:%s"%(req.host, req.path))
++
++ def setUp(self):
++ self.root = self.TestResource()
++
++ def chanrequest(self, root, uri, length, headers, method, version, prepath, content):
++ self.cr = super(Pipeline, self).chanrequest(root, uri, length, headers, method, version, prepath, content)
++ return self.cr
++
++ def test_root(self):
++
++ def _testStreamRead(x):
++ self.assertTrue(self.cr.request.stream.length == 0)
++
++ return self.assertResponse(
++ (self.root, 'http://host/path', {"content-type":"text/plain",}, "PUT", None, '', "This is some text."),
++ (405, {}, None)).addCallback(_testStreamRead)
++
++class SSLPipeline(test_http.SSLServerTest):
++
++ @deferredGenerator
++ def testAdvancedWorkingness(self):
++ args = ('-u', util.sibpath(__file__, "tworequest_client.py"), "basic",
++ str(self.port), self.type)
++ d = waitForDeferred(utils.getProcessOutputAndValue(sys.executable, args=args))
++ yield d; out,err,code = d.getResult()
++
++ self.assertEquals(code, 0, "Error output:\n%s" % (err,))
++ self.assertEquals(out, "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\nHTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_prop.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,69 @@
+Index: twisted/web2/dav/test/test_prop.py
+===================================================================
+--- twisted/web2/dav/test/test_prop.py (revision 19773)
++++ twisted/web2/dav/test/test_prop.py (working copy)
+@@ -21,6 +21,8 @@
+ #
+ # DRI: Wilfredo Sanchez, wsanchez at apple.com
+ ##
++from twisted.web2.dav.element.rfc4331 import QuotaUsedBytes
++from twisted.web2.dav.element.rfc4331 import QuotaAvailableBytes
+
+ import random
+
+@@ -37,8 +39,13 @@
+ from twisted.web2.dav.test.util import serialize
+ import twisted.web2.dav.test.util
+
+-live_properties = [lookupElement(qname)() for qname in DAVResource.liveProperties if qname[0] == dav_namespace]
++# Remove dynamic live properties that exist
++dynamicLiveProperties = (
++ (dav_namespace, "quota-available-bytes" ),
++ (dav_namespace, "quota-used-bytes" ),
++)
+
++
+ #
+ # See whether dead properties are available
+ #
+@@ -49,6 +56,10 @@
+ """
+ PROPFIND, PROPPATCH requests
+ """
++
++ def liveProperties(self):
++ return [lookupElement(qname)() for qname in self.resource_class.liveProperties if (qname[0] == dav_namespace) and qname not in dynamicLiveProperties]
++
+ def test_PROPFIND_basic(self):
+ """
+ PROPFIND request
+@@ -85,7 +96,7 @@
+ self.fail("PROPFIND failed (status %s) to locate live properties: %s"
+ % (status.code, properties))
+
+- properties_to_find = [p.qname() for p in live_properties]
++ properties_to_find = [p.qname() for p in self.liveProperties()]
+
+ for property in properties:
+ qname = property.qname()
+@@ -102,7 +113,7 @@
+ else:
+ self.fail("No response for URI /")
+
+- query = davxml.PropertyFind(davxml.PropertyContainer(*live_properties))
++ query = davxml.PropertyFind(davxml.PropertyContainer(*self.liveProperties()))
+
+ request = SimpleRequest(self.site, "PROPFIND", "/")
+
+@@ -146,9 +157,9 @@
+ % (status.code, properties))
+
+ if which.name == "allprop":
+- properties_to_find = [p.qname() for p in live_properties if not p.hidden]
++ properties_to_find = [p.qname() for p in self.liveProperties() if not p.hidden]
+ else:
+- properties_to_find = [p.qname() for p in live_properties]
++ properties_to_find = [p.qname() for p in self.liveProperties()]
+
+ for property in properties:
+ qname = property.qname()
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_quota.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_quota.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_quota.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_quota.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,206 @@
+Index: twisted/web2/dav/test/test_quota.py
+===================================================================
+--- twisted/web2/dav/test/test_quota.py (revision 0)
++++ twisted/web2/dav/test/test_quota.py (revision 0)
+@@ -0,0 +1,201 @@
++##
++# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++#
++# DRI: Wilfredo Sanchez, wsanchez at apple.com
++##
++
++from twisted.web2 import responsecode
++from twisted.web2.iweb import IResponse
++from twisted.web2.stream import FileStream
++
++import twisted.web2.dav.test.util
++from twisted.web2.test.test_server import SimpleRequest
++from twisted.web2.dav.test.util import Site
++from twisted.web2.dav import davxml
++import os
++
++class QuotaBase(twisted.web2.dav.test.util.TestCase):
++
++ def setUp(self):
++
++ #super(Quota, self).setUp()
++ self.docroot = self.mktemp()
++ os.mkdir(self.docroot)
++ rootresource = self.resource_class(self.docroot)
++ rootresource.setAccessControlList(self.grantInherit(davxml.All()))
++ self.site = Site(rootresource)
++ self.site.resource.setQuotaRoot(None, 100000)
++
++ def checkQuota(self, value):
++ def _defer(quota):
++ self.assertEqual(quota, value)
++
++ d = self.site.resource.currentQuotaUse(None)
++ d.addCallback(_defer)
++ return d
++
++class QuotaEmpty(QuotaBase):
++
++ def test_Empty_Quota(self):
++
++ return self.checkQuota(0)
++
++class QuotaPUT(QuotaBase):
++
++ def test_Quota_PUT(self):
++ """
++ Quota change on PUT
++ """
++ dst_uri = "/dst"
++
++ def checkResult(response):
++ response = IResponse(response)
++
++ if response.code != responsecode.CREATED:
++ self.fail("Incorrect response code for PUT (%s != %s)"
++ % (response.code, responsecode.CREATED))
++
++ return self.checkQuota(100)
++
++ request = SimpleRequest(self.site, "PUT", dst_uri)
++ request.stream = FileStream(file(os.path.join(os.path.dirname(__file__), "data", "quota_100.txt"), "rb"))
++ return self.send(request, checkResult)
++
++class QuotaDELETE(QuotaBase):
++
++ def test_Quota_DELETE(self):
++ """
++ Quota change on DELETE
++ """
++ dst_uri = "/dst"
++
++ def checkPUTResult(response):
++ response = IResponse(response)
++
++ if response.code != responsecode.CREATED:
++ self.fail("Incorrect response code for PUT (%s != %s)"
++ % (response.code, responsecode.CREATED))
++
++ def doDelete(_ignore):
++ def checkDELETEResult(response):
++ response = IResponse(response)
++
++ if response.code != responsecode.NO_CONTENT:
++ self.fail("Incorrect response code for PUT (%s != %s)"
++ % (response.code, responsecode.NO_CONTENT))
++
++ return self.checkQuota(0)
++
++ request = SimpleRequest(self.site, "DELETE", dst_uri)
++ return self.send(request, checkDELETEResult)
++
++ d = self.checkQuota(100)
++ d.addCallback(doDelete)
++ return d
++
++ request = SimpleRequest(self.site, "PUT", dst_uri)
++ request.stream = FileStream(file(os.path.join(os.path.dirname(__file__), "data", "quota_100.txt"), "rb"))
++ return self.send(request, checkPUTResult)
++
++class OverQuotaPUT(QuotaBase):
++
++ def test_Quota_PUT(self):
++ """
++ Quota change on PUT
++ """
++ dst_uri = "/dst"
++
++ self.site.resource.setQuotaRoot(None, 90)
++
++ def checkResult(response):
++ response = IResponse(response)
++
++ if response.code != responsecode.INSUFFICIENT_STORAGE_SPACE:
++ self.fail("Incorrect response code for PUT (%s != %s)"
++ % (response.code, responsecode.INSUFFICIENT_STORAGE_SPACE))
++
++ return self.checkQuota(0)
++
++ request = SimpleRequest(self.site, "PUT", dst_uri)
++ request.stream = FileStream(file(os.path.join(os.path.dirname(__file__), "data", "quota_100.txt"), "rb"))
++ return self.send(request, checkResult)
++
++class QuotaOKAdjustment(QuotaBase):
++
++ def test_Quota_OK_Adjustment(self):
++ """
++ Quota adjustment OK
++ """
++ dst_uri = "/dst"
++
++ def checkPUTResult(response):
++ response = IResponse(response)
++
++ if response.code != responsecode.CREATED:
++ self.fail("Incorrect response code for PUT (%s != %s)"
++ % (response.code, responsecode.CREATED))
++
++ def doOKAdjustment(_ignore):
++ def checkAdjustmentResult(_ignore):
++ return self.checkQuota(10)
++
++ d = self.site.resource.quotaSizeAdjust(None, -90)
++ d.addCallback(checkAdjustmentResult)
++ return d
++
++ d = self.checkQuota(100)
++ d.addCallback(doOKAdjustment)
++ return d
++
++ request = SimpleRequest(self.site, "PUT", dst_uri)
++ request.stream = FileStream(file(os.path.join(os.path.dirname(__file__), "data", "quota_100.txt"), "rb"))
++ return self.send(request, checkPUTResult)
++
++class QuotaBadAdjustment(QuotaBase):
++
++ def test_Quota_Bad_Adjustment(self):
++ """
++ Quota adjustment too much
++ """
++ dst_uri = "/dst"
++
++ def checkPUTResult(response):
++ response = IResponse(response)
++
++ if response.code != responsecode.CREATED:
++ self.fail("Incorrect response code for PUT (%s != %s)"
++ % (response.code, responsecode.CREATED))
++
++ def doBadAdjustment(_ignore):
++ def checkAdjustmentResult(_ignore):
++ return self.checkQuota(100)
++
++ d = self.site.resource.quotaSizeAdjust(None, -200)
++ d.addCallback(checkAdjustmentResult)
++ return d
++
++ d = self.checkQuota(100)
++ d.addCallback(doBadAdjustment)
++ return d
++
++ request = SimpleRequest(self.site, "PUT", dst_uri)
++ request.stream = FileStream(file(os.path.join(os.path.dirname(__file__), "data", "quota_100.txt"), "rb"))
++ return self.send(request, checkPUTResult)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,102 @@
+Index: twisted/web2/dav/test/test_resource.py
+===================================================================
+--- twisted/web2/dav/test/test_resource.py (revision 19773)
++++ twisted/web2/dav/test/test_resource.py (working copy)
+@@ -192,13 +192,10 @@
+ class AccessTests(TestCase):
+ def setUp(self):
+ gooduser = TestDAVPrincipalResource('/users/gooduser')
++ gooduser.writeDeadProperty(TwistedPasswordProperty('goodpass'))
+
+- gooduser.writeDeadProperty(
+- TwistedPasswordProperty.fromString('goodpass'))
+-
+ baduser = TestDAVPrincipalResource('/users/baduser')
+- baduser.writeDeadProperty(
+- TwistedPasswordProperty.fromString('badpass'))
++ baduser.writeDeadProperty(TwistedPasswordProperty('badpass'))
+
+ protected = TestResource('/protected')
+ protected.setAccessControlList(davxml.ACL(
+@@ -282,7 +279,8 @@
+ # Has auth; should allow
+
+ request = SimpleRequest(site, "GET", "/")
+- request.user = davxml.Principal(davxml.HRef("/users/d00d"))
++ request.authnUser = davxml.Principal(davxml.HRef("/users/d00d"))
++ request.authzUser = davxml.Principal(davxml.HRef("/users/d00d"))
+ d = request.locateResource('/')
+ d.addCallback(_checkPrivileges)
+ d.addCallback(expectOK)
+@@ -301,6 +299,8 @@
+
+ return self.checkSecurity(request)
+
++ test_authorize.todo = "Needs refactoring"
++
+ def test_badUsernameOrPassword(self):
+ request = SimpleRequest(self.site, 'GET', '/protected')
+
+@@ -316,6 +316,8 @@
+
+ return d
+
++ test_badUsernameOrPassword.todo = "Needs refactoring."
++
+ def test_lacksPrivileges(self):
+ request = SimpleRequest(self.site, 'GET', '/protected')
+
+@@ -348,12 +350,12 @@
+ davxml.Grant(davxml.Privilege(davxml.All())),
+ davxml.Protected()))
+
+- def __init__(self, uri=None, children=None):
++ def __init__(self, uri=None, children=None, principalCollections=()):
+ """
+ @param uri: A string respresenting the URI of the given resource
+ @param children: a dictionary of names to Resources
+ """
+-
++ DAVResource.__init__(self, principalCollections=principalCollections)
+ self.children = children
+ self.uri = uri
+
+@@ -380,8 +382,8 @@
+ return succeed(davPrivilegeSet)
+
+ def currentPrincipal(self, request):
+- if hasattr(request, "user"):
+- return request.user
++ if hasattr(request, "authzUser"):
++ return request.authzUser
+ else:
+ return davxml.Principal(davxml.Unauthenticated())
+
+@@ -399,18 +401,21 @@
+
+ def accessControlList(self, request, **kwargs):
+ return succeed(self.acl)
+-
+
+ class AuthAllResource (TestResource):
+- """Give Authenticated principals all privileges deny everything else
+ """
++ Give Authenticated principals all privileges and deny everyone else.
++ """
+ acl = davxml.ACL(
+ davxml.ACE(
+ davxml.Principal(davxml.Authenticated()),
+ davxml.Grant(davxml.Privilege(davxml.All())),
+- davxml.Protected()))
+-
++ davxml.Protected()
++ )
++ )
+
+ class TestDAVPrincipalResource(DAVPrincipalResource, TestResource):
+- """Get deadProperties from TestResource
+- """
++ # Get dead properties from TestResource
++
++ def principalURL(self):
++ return self.uri
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_static.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_static.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_static.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_static.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,10 @@
+Index: twisted/web2/dav/test/test_static.py
+===================================================================
+--- twisted/web2/dav/test/test_static.py (revision 19773)
++++ twisted/web2/dav/test/test_static.py (working copy)
+@@ -60,3 +60,5 @@
+ d.addCallback(assertListing)
+
+ return d
++
++ test_renderPrivileges.todo = "We changed the rules here; test needs an update"
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_stream.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_stream.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_stream.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_stream.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,56 @@
+Index: twisted/web2/dav/test/test_stream.py
+===================================================================
+--- twisted/web2/dav/test/test_stream.py (revision 0)
++++ twisted/web2/dav/test/test_stream.py (revision 0)
+@@ -0,0 +1,51 @@
++##
++# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++##
++
++from twisted.trial import unittest
++from twisted.web2.stream import MemoryStream
++from twisted.web2.dav.stream import MD5StreamWrapper
++import md5
++
++class Stream(unittest.TestCase):
++ """
++ MD5StreamWrapper tests.
++ """
++ def test_simple(self):
++ """
++ Simple test
++ """
++
++ data = """I am sorry Dave, I can't do that.
++--HAL 9000
++"""
++
++ datastream = MemoryStream(data)
++ stream = MD5StreamWrapper(datastream)
++
++ while stream.read() is not None:
++ pass
++ stream.close()
++
++ m = md5.new()
++ m.update(data)
++
++ self.assertEqual(m.hexdigest(), stream.getMD5())
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_xml.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_xml.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_xml.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_xml.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,88 @@
+Index: twisted/web2/dav/test/test_xml.py
+===================================================================
+--- twisted/web2/dav/test/test_xml.py (revision 0)
++++ twisted/web2/dav/test/test_xml.py (revision 0)
+@@ -0,0 +1,83 @@
++##
++# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++##
++
++from twisted.trial import unittest
++from twisted.web2.dav import davxml
++
++class XML(unittest.TestCase):
++ """
++ XML tests.
++ """
++ def test_parse(self):
++ """
++ Simple parsing
++ """
++ doc = davxml.WebDAVDocument.fromString(
++ """<?xml version="1.0" encoding="utf-8" ?>"""
++ """<D:multistatus xmlns:D="DAV:">"""
++ """ <D:response>"""
++ """ <D:href>http://webdav.sb.aol.com/webdav/secret</D:href>"""
++ """ <D:status>HTTP/1.1 403 Forbidden</D:status>"""
++ """ </D:response>"""
++ """</D:multistatus>"""
++ )
++ self.assertEquals(
++ doc,
++ davxml.WebDAVDocument(
++ davxml.MultiStatus(
++ davxml.Response(
++ davxml.HRef("http://webdav.sb.aol.com/webdav/secret"),
++ davxml.Status("HTTP/1.1 403 Forbidden"),
++ )
++ )
++ )
++ )
++
++ def test_serialize_unserialize(self):
++ """
++ Serialization and unserialization results in equivalent document.
++ """
++ doc = davxml.WebDAVDocument(
++ davxml.MultiStatus(
++ davxml.Response(
++ davxml.HRef("http://webdav.sb.aol.com/webdav/secret"),
++ davxml.Status("HTTP/1.1 403 Forbidden"),
++ )
++ )
++ )
++ self.assertEquals(doc, davxml.WebDAVDocument.fromString(doc.toxml()))
++
++ def test_unknownElement(self):
++ """
++ Serialization and unserialization of unknown element.
++ """
++ doc = davxml.WebDAVDocument.fromString(
++ """<?xml version="1.0" encoding="utf-8" ?>"""
++ """<T:foo xmlns:T="http://twistedmatrix.com/"/>"""
++ )
++
++ foo = davxml.WebDAVUnknownElement()
++ foo.namespace = "http://twistedmatrix.com/"
++ foo.name = "foo"
++
++ self.assertEquals(doc, davxml.WebDAVDocument(foo))
++ self.assertEquals(doc, davxml.WebDAVDocument.fromString(doc.toxml()))
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.tworequest_client.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.tworequest_client.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.tworequest_client.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.tworequest_client.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,52 @@
+Index: twisted/web2/dav/test/tworequest_client.py
+===================================================================
+--- twisted/web2/dav/test/tworequest_client.py (revision 0)
++++ twisted/web2/dav/test/tworequest_client.py (revision 0)
+@@ -0,0 +1,47 @@
++import socket, sys
++
++test_type = sys.argv[1]
++port = int(sys.argv[2])
++socket_type = sys.argv[3]
++
++s = socket.socket(socket.AF_INET)
++s.connect(("127.0.0.1", port))
++s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 40000)
++
++if socket_type == 'ssl':
++ s2 = socket.ssl(s)
++ send=s2.write
++ recv=s2.read
++else:
++ send=s.send
++ recv=s.recv
++
++print >> sys.stderr, ">> Making %s request to port %d" % (socket_type, port)
++
++send("PUT /forbidden HTTP/1.1\r\n")
++send("Host: localhost\r\n")
++
++print >> sys.stderr, ">> Sending lots of data"
++send("Content-Length: 100\r\n\r\n")
++send("X"*100)
++
++send("PUT /forbidden HTTP/1.1\r\n")
++send("Host: localhost\r\n")
++
++print >> sys.stderr, ">> Sending lots of data"
++send("Content-Length: 100\r\n\r\n")
++send("X"*100)
++
++#import time
++#time.sleep(5)
++print >> sys.stderr, ">> Getting data"
++data=''
++while len(data) < 299999:
++ try:
++ x=recv(10000)
++ except:
++ break
++ if x == '':
++ break
++ data+=x
++sys.stdout.write(data)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.util.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.util.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.util.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.util.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,20 @@
+Index: twisted/web2/dav/test/util.py
+===================================================================
+--- twisted/web2/dav/test/util.py (revision 19773)
++++ twisted/web2/dav/test/util.py (working copy)
+@@ -184,10 +184,11 @@
+ d.addCallback(lambda resource: resource.renderHTTP(request))
+ d.addCallback(request._cbFinishRender)
+
+- if type(callback) is tuple:
+- d.addCallbacks(*callback)
+- else:
+- d.addCallback(callback)
++ if callback:
++ if type(callback) is tuple:
++ d.addCallbacks(*callback)
++ else:
++ d.addCallback(callback)
+
+ return d
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.util.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.util.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.util.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.util.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,23 @@
+Index: twisted/web2/dav/util.py
+===================================================================
+--- twisted/web2/dav/util.py (revision 19773)
++++ twisted/web2/dav/util.py (working copy)
+@@ -37,6 +37,7 @@
+ "normalizeURL",
+ "joinURL",
+ "parentForURL",
++ "unimplemented",
+ "bindMethods",
+ ]
+
+@@ -76,7 +77,9 @@
+
+ def parse(xml):
+ try:
+- return davxml.WebDAVDocument.fromString(xml)
++ doc = davxml.WebDAVDocument.fromString(xml)
++ doc.root_element.validate()
++ return doc
+ except ValueError:
+ log.err("Bad XML:\n%s" % (xml,))
+ raise
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.xattrprops.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,169 @@
+Index: twisted/web2/dav/xattrprops.py
+===================================================================
+--- twisted/web2/dav/xattrprops.py (revision 19773)
++++ twisted/web2/dav/xattrprops.py (working copy)
+@@ -33,15 +33,24 @@
+
+ import urllib
+ import sys
++import zlib
++from time import sleep
++from random import random
++from errno import EAGAIN
++from zlib import compress, decompress
++from cPickle import loads as unpickle, PicklingError, UnpicklingError
+
+ import xattr
+
+ if getattr(xattr, 'xattr', None) is None:
+ raise ImportError("wrong xattr package imported")
+
++from twisted.python import log
++from twisted.python.failure import Failure
+ from twisted.web2 import responsecode
+ from twisted.web2.http import HTTPError, StatusResponse
+ from twisted.web2.dav import davxml
++from twisted.web2.dav.http import statusForFailure
+
+ class xattrPropertyStore (object):
+ """
+@@ -66,16 +75,8 @@
+ deadPropertyXattrPrefix = "user."
+
+ def _encode(clazz, name):
+- #
+- # FIXME: The xattr API in Mac OS 10.4.2 breaks if you have "/" in an
+- # attribute name (radar://4202440). We'll quote the strings to get rid
+- # of "/" characters for now.
+- #
+- result = list("{%s}%s" % name)
+- for i in range(len(result)):
+- c = result[i]
+- if c in "%/": result[i] = "%%%02X" % (ord(c),)
+- r = clazz.deadPropertyXattrPrefix + ''.join(result)
++ result = urllib.quote("{%s}%s" % name, safe='{}:')
++ r = clazz.deadPropertyXattrPrefix + result
+ return r
+
+ def _decode(clazz, name):
+@@ -97,19 +98,83 @@
+
+ def get(self, qname):
+ try:
+- value = self.attrs[self._encode(qname)]
++ data = self.attrs[self._encode(qname)]
+ except KeyError:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No such property: {%s}%s" % qname
+ ))
++ except Exception, e:
++ raise HTTPError(StatusResponse(
++ statusForFailure(Failure()),
++ "Unable to read property: {%s}%s" % qname
++ ))
+
+- doc = davxml.WebDAVDocument.fromString(value)
++ #
++ # Serialize XML data to an xattr. The storage format has
++ # changed over time:
++ #
++ # 1- Started with XML
++ # 2- Started compressing the XML due to limits on xattr size
++ # 3- Switched to pickle which is faster, still compressing
++ # 4- Back to compressed XML for interoperability, size
++ #
++ # We only write the current format, but we also read the old
++ # ones for compatibility.
++ #
++ legacy = False
+
+- return doc.root_element
++ try:
++ data = decompress(data)
++ except zlib.error:
++ legacy = True
+
++ try:
++ doc = davxml.WebDAVDocument.fromString(data)
++ result = doc.root_element
++ except ValueError:
++ legacy = True
++
++ try:
++ result = unpickle(data)
++ except UnpicklingError:
++ msg = ("Invalid property value stored on server: {%s}%s %s"
++ % (qname[0], qname[1], data))
++ log.err(msg)
++ raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, msg))
++
++ if legacy:
++ # Try to upgrade the property to the current format
++ try:
++ log.msg("Upgrading property %r on resource %r"
++ % (qname, self.resource.fp.path))
++ self.set(result)
++ except Exception, e:
++ #
++ # Hrm, that failed. No need to re-raise here, since
++ # we can do what we were asked to do, but let's
++ # complain about it.
++ #
++ log.err("Error while upgrading property %r on resource %r: %s"
++ % (qname, self.resource.fp.path, e))
++
++ return result
++
+ def set(self, property):
+- self.attrs[self._encode(property.qname())] = property.toxml()
++ for n in range(20):
++ data = compress(property.toxml())
++ try:
++ self.attrs[self._encode(property.qname())] = data
++ except Exception, e:
++ if e.errno == EAGAIN and n < 19:
++ sleep(random() / 10) # OMG Brutal Hax
++ else:
++ raise HTTPError(StatusResponse(
++ statusForFailure(Failure()),
++ "Unable to write property: %s" % (property.sname(),)
++ ))
++ else:
++ break
+
+ # Update the resource because we've modified it
+ self.resource.fp.restat()
+@@ -121,15 +186,31 @@
+ # RFC 2518 Section 12.13.1 says that removal of
+ # non-existing property is not an error.
+ pass
++ except Exception, e:
++ raise HTTPError(StatusResponse(
++ statusForFailure(Failure()),
++ "Unable to delete property: {%s}%s" % qname
++ ))
+
+ def contains(self, qname):
+ try:
+ return self._encode(qname) in self.attrs
+ except TypeError:
+ return False
++ except Exception, e:
++ raise HTTPError(StatusResponse(
++ statusForFailure(Failure()),
++ "Unable to read properties"
++ ))
+
+ def list(self):
+ prefix = self.deadPropertyXattrPrefix
+ prefix_len = len(prefix)
+
+- return [ self._decode(name) for name in self.attrs if name.startswith(prefix) ]
++ try:
++ return [ self._decode(name) for name in self.attrs if name.startswith(prefix) ]
++ except Exception, e:
++ raise HTTPError(StatusResponse(
++ statusForFailure(Failure()),
++ "Unable to list properties"
++ ))
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.http.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.http.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.http.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.http.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,57 @@
+Index: twisted/web2/http.py
+===================================================================
+--- twisted/web2/http.py (revision 19773)
++++ twisted/web2/http.py (working copy)
+@@ -26,7 +26,7 @@
+ from twisted.web2 import http_headers
+ from twisted.web2 import iweb
+ from twisted.web2 import stream
+-from twisted.web2.stream import IByteStream
++from twisted.web2.stream import IByteStream, readAndDiscard
+
+ defaultPortForScheme = {'http': 80, 'https':443, 'ftp':21}
+
+@@ -66,9 +66,9 @@
+ object.
+ @type codeOrResponse: C{int} or L{http.Response}
+ """
+- Exception.__init__(self)
+- self.response = iweb.IResponse(codeOrResponse)
+- self.args = str(self.response)
++ response = iweb.IResponse(codeOrResponse)
++ Exception.__init__(self, str(response))
++ self.response = response
+
+ def __repr__(self):
+ return "<%s %s>" % (self.__class__.__name__, self.response)
+@@ -408,9 +408,22 @@
+ def _sendContinue(self):
+ self.chanRequest.writeIntermediateResponse(responsecode.CONTINUE)
+
+- def _finished(self, x):
++ def _reallyFinished(self, x):
+ """We are finished writing data."""
+ self.chanRequest.finish()
++
++ def _finished(self, x):
++ """
++ We are finished writing data.
++ But we need to check that we have also finished reading all data as we
++ might have sent a, for example, 401 response before we read any data.
++ To make sure that the stream/producer sequencing works properly we need
++ to discard the remaining data in the request.
++ """
++ if self.stream.length != 0:
++ return readAndDiscard(self.stream).addCallback(self._reallyFinished).addErrback(self._error)
++ else:
++ self._reallyFinished(x)
+
+ def _error(self, reason):
+ if reason.check(error.ConnectionLost):
+@@ -471,5 +484,5 @@
+ else:
+ components.registerAdapter(compat.OldResourceAdapter, resource.IResource, iweb.IOldNevowResource)
+
+-__all__ = ['HTTPError', 'NotModifiedResponse', 'Request', 'Response', 'checkIfRange', 'checkPreconditions', 'defaultPortForScheme', 'parseVersion', 'splitHostPort']
++__all__ = ['HTTPError', 'NotModifiedResponse', 'Request', 'Response', 'StatusResponse', 'RedirectResponse', 'checkIfRange', 'checkPreconditions', 'defaultPortForScheme', 'parseVersion', 'splitHostPort']
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,13 @@
+Index: twisted/web2/log.py
+===================================================================
+--- twisted/web2/log.py (revision 19773)
++++ twisted/web2/log.py (working copy)
+@@ -88,7 +88,7 @@
+ class LogWrapperResource(resource.WrapperResource):
+ def hook(self, request):
+ # Insert logger
+- request.addResponseFilter(logFilter, atEnd=True)
++ request.addResponseFilter(logFilter, atEnd=True, onlyOnce=True)
+
+ monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,129 @@
+Index: twisted/web2/server.py
+===================================================================
+--- twisted/web2/server.py (revision 19773)
++++ twisted/web2/server.py (working copy)
+@@ -26,6 +26,7 @@
+ from twisted.web2 import http_headers
+ from twisted.web2.filter.range import rangefilter
+ from twisted.web2 import error
++from twisted.web2.dav.util import joinURL
+
+ from twisted.web2 import version as web2_version
+ from twisted import __version__ as twisted_version
+@@ -143,6 +144,9 @@
+ error.defaultErrorHandler, defaultHeadersFilter]
+
+ def __init__(self, *args, **kw):
++
++ self.initTime = time.time()
++
+ if kw.has_key('site'):
+ self.site = kw['site']
+ del kw['site']
+@@ -150,17 +154,36 @@
+ self._initialprepath = kw['prepathuri']
+ del kw['prepathuri']
+
++ self._resourcesByURL = {}
++ self._urlsByResource = {}
++
+ # Copy response filters from the class
+ self.responseFilters = self.responseFilters[:]
+ self.files = {}
+ self.resources = []
+ http.Request.__init__(self, *args, **kw)
++ try:
++ self.serverInstance = self.chanRequest.channel.transport.server.port
++ except AttributeError:
++ self.serverInstance = "Unknown"
+
+- def addResponseFilter(self, f, atEnd=False):
++ def addResponseFilter(self, filter, atEnd=False, onlyOnce=False):
++ """
++ Add a response filter to this request.
++ Response filters are applied to the response to this request in order.
++ @param filter: a callable which takes an response argument and returns
++ a response object.
++ @param atEnd: if C{True}, C{filter} is added at the end of the list of
++ response filters; if C{False}, it is added to the beginning.
++ @param onlyOnce: if C{True}, C{filter} is not added to the list of
++ response filters if it already in the list.
++ """
++ if onlyOnce and filter in self.responseFilters:
++ return
+ if atEnd:
+- self.responseFilters.append(f)
++ self.responseFilters.append(filter)
+ else:
+- self.responseFilters.insert(0, f)
++ self.responseFilters.insert(0, filter)
+
+ def unparseURL(self, scheme=None, host=None, port=None,
+ path=None, params=None, querystring=None, fragment=None):
+@@ -265,6 +288,7 @@
+
+ d = defer.Deferred()
+ d.addCallback(self._getChild, self.site.resource, self.postpath)
++ d.addCallback(self._rememberResource, "/" + "/".join(quote(s) for s in self.postpath))
+ d.addCallback(lambda res, req: res.renderHTTP(req), self)
+ d.addCallback(self._cbFinishRender)
+ d.addErrback(self._processingFailed)
+@@ -321,7 +345,6 @@
+ if newpath is StopTraversal:
+ # We need to rethink how to do this.
+ #if newres is res:
+- self._rememberResource(res, url)
+ return res
+ #else:
+ # raise ValueError("locateChild must not return StopTraversal with a resource other than self.")
+@@ -337,7 +360,6 @@
+ self.prepath.append(self.postpath.pop(0))
+
+ child = self._getChild(None, newres, newpath, updatepaths=updatepaths)
+- self._rememberResource(child, url)
+
+ return child
+
+@@ -347,6 +369,7 @@
+ """
+ Remember the URL of a visited resource.
+ """
++ self._resourcesByURL[url] = resource
+ self._urlsByResource[resource] = url
+ return resource
+
+@@ -386,7 +409,8 @@
+ The contained response will have a status code of
+ L{responsecode.BAD_REQUEST}.
+ """
+- if url is None: return None
++ if url is None:
++ return defer.succeed(None)
+
+ #
+ # Parse the URL
+@@ -407,9 +431,13 @@
+ "URL is not on this site (%s://%s/): %s" % (scheme, self.headers.getHeader("host"), url)
+ ))
+
+- segments = path.split("/")
++ # Look for cached value
++ cached = self._resourcesByURL.get(path, None)
++ if cached is not None:
++ return defer.succeed(cached)
++
++ segments = unquote(path).split("/")
+ assert segments[0] == "", "URL path didn't begin with '/': %s" % (path,)
+- segments = map(unquote, segments[1:])
+
+ def notFound(f):
+ f.trap(http.HTTPError)
+@@ -417,7 +445,7 @@
+ return f
+ return None
+
+- d = defer.maybeDeferred(self._getChild, None, self.site.resource, segments, updatepaths=False)
++ d = defer.maybeDeferred(self._getChild, None, self.site.resource, segments[1:], updatepaths=False)
+ d.addCallback(self._rememberResource, path)
+ d.addErrback(notFound)
+ return d
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.static.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.static.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.static.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.static.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,33 @@
+Index: twisted/web2/static.py
+===================================================================
+--- twisted/web2/static.py (revision 19773)
++++ twisted/web2/static.py (working copy)
+@@ -9,7 +9,6 @@
+ # System Imports
+ import os, time, stat
+ import tempfile
+-import md5
+
+ # Sibling Imports
+ from twisted.web2 import http_headers, resource
+@@ -200,7 +199,10 @@
+ super(File, self).__init__()
+
+ self.putChildren = {}
+- self.fp = filepath.FilePath(path)
++ if isinstance(path, filepath.FilePath):
++ self.fp = path
++ else:
++ self.fp = filepath.FilePath(path)
+ # Remove the dots from the path to split
+ self.defaultType = defaultType
+ self.ignoredExts = list(ignoredExts)
+@@ -383,7 +385,7 @@
+ return responsecode.NOT_FOUND
+
+ if self.fp.isdir():
+- if req.uri[-1] != "/":
++ if req.path[-1] != "/":
+ # Redirect to include trailing '/' in URI
+ return http.RedirectResponse(req.unparseURL(path=req.path+'/'))
+ else:
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_http.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_http.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_http.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_http.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,23 @@
+Index: twisted/web2/test/test_http.py
+===================================================================
+--- twisted/web2/test/test_http.py (revision 19773)
++++ twisted/web2/test/test_http.py (working copy)
+@@ -324,6 +324,9 @@
+ def connectionLost(self, reason):
+ self.cmds.append(('connectionLost', reason))
+
++ def _finished(self, x):
++ self._reallyFinished(x)
++
+ class TestResponse(object):
+ implements(iweb.IResponse)
+
+@@ -1017,6 +1020,8 @@
+ response = TestResponse()
+ if self.uri == "/error":
+ response.code=402
++ elif self.uri == "/forbidden":
++ response.code=403
+ else:
+ response.code=404
+ response.write("URI %s unrecognized." % self.uri)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,243 @@
+Index: twisted/web2/test/test_httpauth.py
+===================================================================
+--- twisted/web2/test/test_httpauth.py (revision 19773)
++++ twisted/web2/test/test_httpauth.py (working copy)
+@@ -1,3 +1,4 @@
++from twisted.internet.defer import inlineCallbacks
+ import md5
+ from twisted.internet import address
+ from twisted.trial import unittest
+@@ -41,22 +42,25 @@
+ self.username = 'dreid'
+ self.password = 'S3CuR1Ty'
+
++ @inlineCallbacks
+ def testUsernamePassword(self):
+ response = base64.encodestring('%s:%s' % (
+ self.username,
+ self.password))
+
+- creds = self.credentialFactory.decode(response, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(response, _trivial_GET))
+ self.failUnless(creds.checkPassword(self.password))
+
++ @inlineCallbacks
+ def testIncorrectPassword(self):
+ response = base64.encodestring('%s:%s' % (
+ self.username,
+ 'incorrectPassword'))
+
+- creds = self.credentialFactory.decode(response, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(response, _trivial_GET))
+ self.failIf(creds.checkPassword(self.password))
+
++ @inlineCallbacks
+ def testIncorrectPadding(self):
+ response = base64.encodestring('%s:%s' % (
+ self.username,
+@@ -64,7 +68,7 @@
+
+ response = response.strip('=')
+
+- creds = self.credentialFactory.decode(response, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(response, _trivial_GET))
+ self.failUnless(creds.checkPassword(self.password))
+
+ def testInvalidCredentials(self):
+@@ -135,6 +139,7 @@
+ )
+ return expected
+
++ @inlineCallbacks
+ def test_getChallenge(self):
+ """
+ Test that all the required fields exist in the challenge,
+@@ -142,42 +147,44 @@
+ DigestCredentialFactory
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+ self.assertEquals(challenge['qop'], 'auth')
+ self.assertEquals(challenge['realm'], 'test realm')
+ self.assertEquals(challenge['algorithm'], 'md5')
+ self.assertTrue(challenge.has_key("nonce"))
+ self.assertTrue(challenge.has_key("opaque"))
+
++ @inlineCallbacks
+ def test_response(self):
+ """
+ Test that we can decode a valid response to our challenge
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+ self.failUnless(creds.checkPassword('password'))
+
++ @inlineCallbacks
+ def test_multiResponse(self):
+ """
+ Test that multiple responses to to a single challenge are handled
+ successfully.
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+ self.failUnless(creds.checkPassword('password'))
+
+ clientResponse = authRequest2 % (
+@@ -185,24 +192,25 @@
+ self.getDigestResponse(challenge, "00000002"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+ self.failUnless(creds.checkPassword('password'))
+
++ @inlineCallbacks
+ def test_failsWithDifferentMethod(self):
+ """
+ Test that the response fails if made for a different request method
+ than it is being issued for.
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse,
+- SimpleRequest(None, 'POST', '/'))
++ creds = (yield self.credentialFactory.decode(clientResponse,
++ SimpleRequest(None, 'POST', '/')))
+ self.failIf(creds.checkPassword('password'))
+
+ def test_noUsername(self):
+@@ -247,20 +255,21 @@
+ _trivial_GET)
+ self.assertEquals(str(e), "Invalid response, no opaque given.")
+
++ @inlineCallbacks
+ def test_checkHash(self):
+ """
+ Check that given a hash of the form 'username:realm:password'
+ we can verify the digest challenge
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+
+ self.failUnless(creds.checkHash(
+ md5.md5('username:test realm:password').hexdigest()))
+@@ -268,6 +277,7 @@
+ self.failIf(creds.checkHash(
+ md5.md5('username:test realm:bogus').hexdigest()))
+
++ @inlineCallbacks
+ def test_invalidOpaque(self):
+ """
+ Test that login fails when the opaque does not contain all the required
+@@ -276,7 +286,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ self.assertRaises(
+ error.LoginFailed,
+@@ -302,6 +312,7 @@
+ challenge['nonce'],
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_incompatibleNonce(self):
+ """
+ Test that login fails when the given nonce from the response, does not
+@@ -310,7 +321,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ badNonceOpaque = credentialFactory.generateOpaque(
+ '1234567890',
+@@ -330,6 +341,7 @@
+ '',
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_incompatibleClientIp(self):
+ """
+ Test that the login fails when the request comes from a client ip
+@@ -338,7 +350,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ badNonceOpaque = credentialFactory.generateOpaque(
+ challenge['nonce'],
+@@ -351,6 +363,7 @@
+ challenge['nonce'],
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_oldNonce(self):
+ """
+ Test that the login fails when the given opaque is older than
+@@ -359,7 +372,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ key = '%s,%s,%s' % (challenge['nonce'],
+ clientAddress.host,
+@@ -376,6 +389,7 @@
+ challenge['nonce'],
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_mismatchedOpaqueChecksum(self):
+ """
+ Test that login fails when the opaque checksum fails verification
+@@ -383,7 +397,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+
+ key = '%s,%s,%s' % (challenge['nonce'],
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.words.protocols.jabber.sasl_mechanisms.patch (from rev 4133, CalendarServer/trunk/lib-patches/Twisted/twisted.words.protocols.jabber.sasl_mechanisms.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.words.protocols.jabber.sasl_mechanisms.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.words.protocols.jabber.sasl_mechanisms.patch 2009-05-01 21:12:23 UTC (rev 4136)
@@ -0,0 +1,86 @@
+Index: twisted/words/protocols/jabber/sasl_mechanisms.py
+===================================================================
+--- twisted/words/protocols/jabber/sasl_mechanisms.py (revision 19773)
++++ twisted/words/protocols/jabber/sasl_mechanisms.py (working copy)
+@@ -7,8 +7,13 @@
+ Protocol agnostic implementations of SASL authentication mechanisms.
+ """
+
+-import md5, binascii, random, time, os
++import binascii, random, time, os
+
++try:
++ from hashlib import md5
++except ImportError:
++ from md5 import new as md5
++
+ from zope.interface import Interface, Attribute, implements
+
+ class ISASLMechanism(Interface):
+@@ -108,16 +113,43 @@
+ @return: challenge directives and their values.
+ @rtype: L{dict} of L{str} to L{str}.
+ """
+- directive_list = challenge.split(',')
+- directives = {}
+- for directive in directive_list:
+- name, value = directive.split('=')
+- value = value.replace("'","")
+- value = value.replace('"','')
+- directives[name] = value
+- return directives
++ s = challenge
++ paramDict = {}
++ cur = 0
++ remainingParams = True
++ while remainingParams:
++ # Parse a param. We can't just split on commas, because there can
++ # be some commas inside (quoted) param values, e.g.:
++ # qop="auth,auth-int"
+
++ middle = s.index("=", cur)
++ name = s[cur:middle].lstrip()
++ middle += 1
++ if s[middle] == '"':
++ middle += 1
++ end = s.index('"', middle)
++ value = s[middle:end]
++ cur = s.find(',', end) + 1
++ if cur == 0:
++ remainingParams = False
++ else:
++ end = s.find(',', middle)
++ if end == -1:
++ value = s[middle:].rstrip()
++ remainingParams = False
++ else:
++ value = s[middle:end].rstrip()
++ cur = end + 1
++ paramDict[name] = value
+
++ for param in ('qop', 'cipher'):
++ if param in paramDict:
++ paramDict[param] = paramDict[param].split(',')
++
++ return paramDict
++
++
++
+ def _unparse(self, directives):
+ """
+ Create message string from directives.
+@@ -153,7 +185,7 @@
+ """
+
+ def H(s):
+- return md5.new(s).digest()
++ return md5(s).digest()
+
+ def HEX(n):
+ return binascii.b2a_hex(n)
+@@ -196,4 +228,4 @@
+
+
+ def _gen_nonce(self):
+- return md5.new("%s:%s:%s" % (str(random.random()) , str(time.gmtime()),str(os.getpid()))).hexdigest()
++ return md5("%s:%s:%s" % (str(random.random()) , str(time.gmtime()),str(os.getpid()))).hexdigest()
Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/run 2009-05-01 21:12:23 UTC (rev 4136)
@@ -104,7 +104,7 @@
'R') reactor="-R ${OPTARG}"; ;;
't') service_type="${OPTARG}"; ;;
'K') read_key="${OPTARG}"; ;;
- 'S') profile="--profiler cprofile -p ${OPTARG}"; ;;
+ 'S') profile="-p ${OPTARG}"; ;;
'g') do_get="true" ; do_setup="false"; do_run="false"; ;;
's') do_get="true" ; do_setup="true" ; do_run="false"; ;;
'p') do_get="false"; do_setup="false"; do_run="false"; print_path="true"; ;;
@@ -655,8 +655,8 @@
proto="svn";
;;
esac;
-svn_uri="${proto}://svn.twistedmatrix.com/svn/Twisted/branches/dav-take-two-3081-4";
-svn_get "Twisted" "${twisted}" "${svn_uri}" 26741;
+svn_uri="${proto}://svn.twistedmatrix.com/svn/Twisted/branches/dav-acl-1608-4";
+svn_get "Twisted" "${twisted}" "${svn_uri}" 19773;
# No py_build step, since we tend to do edit Twisted, we want the sources in
# PYTHONPATH, not a build directory.
Modified: CalendarServer/trunk/twistedcaldav/notify.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/notify.py 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/twistedcaldav/notify.py 2009-05-01 21:12:23 UTC (rev 4136)
@@ -720,8 +720,7 @@
return
try:
- # XXX This codepath is not unit tested
- iq = IQ(self.xmlStream, 'get')
+ iq = IQ(self.xmlStream, type='get')
child = iq.addElement('pubsub',
defaultUri=self.pubsubNS+"#owner")
child = child.addElement('configure')
@@ -756,7 +755,7 @@
formElement = configureElement.firstChildElement()
if formElement['type'] == 'form':
# We've found the form; start building a response
- filledIq = IQ(self.xmlStream, 'set')
+ filledIq = IQ(self.xmlStream, type='set')
filledPubSub = filledIq.addElement('pubsub',
defaultUri=self.pubsubNS+"#owner")
filledConfigure = filledPubSub.addElement('configure')
@@ -785,8 +784,7 @@
valueElement.addContent(value)
# filledForm.addChild(field)
if configMatches:
- # XXX This codepath is not unit tested
- cancelIq = IQ(self.xmlStream, 'set')
+ cancelIq = IQ(self.xmlStream, type='set')
cancelPubSub = cancelIq.addElement('pubsub',
defaultUri=self.pubsubNS+"#owner")
cancelConfig = cancelPubSub.addElement('configure')
@@ -894,7 +892,7 @@
def requestRoster(self):
if self.doRoster:
self.roster = {}
- rosterIq = IQ(self.xmlStream, 'get')
+ rosterIq = IQ(self.xmlStream, type='get')
rosterIq.addElement("query", "jabber:iq:roster")
d = rosterIq.send()
d.addCallback(self.handleRoster)
@@ -952,8 +950,7 @@
self.xmlStream.send(response)
# remove from roster as well
- # XXX This codepath is not unit tested
- removal = IQ(self.xmlStream, 'set')
+ removal = IQ(self.xmlStream, type='set')
query = removal.addElement("query", "jabber:iq:roster")
query.addElement("item")
query.item["jid"] = iq["from"]
Modified: CalendarServer/trunk/twistedcaldav/test/test_index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_index.py 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/twistedcaldav/test/test_index.py 2009-05-01 21:12:23 UTC (rev 4136)
@@ -19,8 +19,7 @@
import twistedcaldav.test.util
from twistedcaldav.test.util import InMemoryMemcacheProtocol
from twistedcaldav.index import ReservationError, MemcachedUIDReserver
-from twisted.internet import reactor
-from twisted.internet.task import deferLater
+from twisted.web2.test.test_http import deferLater
class SQLIndexTests (twistedcaldav.test.util.TestCase):
"""
@@ -81,7 +80,7 @@
d.addCallback(lambda _: self.db.reserveUID(uid))
d.addCallback(lambda _: self.db.isReservedUID(uid))
d.addCallback(self.assertTrue)
- d.addCallback(lambda _: deferLater(reactor, 2, lambda: None))
+ d.addCallback(lambda _: deferLater(2))
d.addCallback(lambda _: self.db.isReservedUID(uid))
d.addCallback(self.assertFalse)
d.addBoth(_finally)
Modified: CalendarServer/trunk/twistedcaldav/test/test_upgrade.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_upgrade.py 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/twistedcaldav/test/test_upgrade.py 2009-05-01 21:12:23 UTC (rev 4136)
@@ -188,7 +188,7 @@
# Verify these values do require updating:
#
- expected = "<?xml version='1.0' encoding='UTF-8'?><calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>"
+ expected = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>\r\n"
# Uncompressed XML
value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/users/wsanchez/calendar</href>\r\n</calendar-free-busy-set>\r\n"
@@ -314,7 +314,7 @@
{
"@xattrs" :
{
- freeBusyAttr : zlib.compress("<?xml version='1.0' encoding='UTF-8'?><calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>"),
+ freeBusyAttr : zlib.compress("<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>\r\n"),
},
},
},
@@ -430,7 +430,7 @@
{
"@xattrs" :
{
- freeBusyAttr : zlib.compress("<?xml version='1.0' encoding='UTF-8'?><calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>"),
+ freeBusyAttr : zlib.compress("<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>\r\n"),
},
},
},
@@ -550,7 +550,7 @@
{
"@xattrs" :
{
- freeBusyAttr : zlib.compress("<?xml version='1.0' encoding='UTF-8'?><calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>"),
+ freeBusyAttr : zlib.compress("<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n <href xmlns='DAV:'>/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/calendar/</href>\r\n</calendar-free-busy-set>\r\n"),
},
},
},
Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py 2009-05-01 20:55:12 UTC (rev 4135)
+++ CalendarServer/trunk/twistedcaldav/test/util.py 2009-05-01 21:12:23 UTC (rev 4136)
@@ -117,7 +117,6 @@
try:
if xattr.getxattr(childPath, attr) != value:
print "Xattr mismatch:", childPath, attr
- print (xattr.getxattr(childPath, attr), " != ", value)
return False
except:
return False
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090501/bb7f0e0a/attachment-0001.html>
More information about the calendarserver-changes
mailing list