[CalendarServer-changes] [8092] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue Sep 13 12:14:09 PDT 2011
Revision: 8092
http://trac.macosforge.org/projects/calendarserver/changeset/8092
Author: glyph at apple.com
Date: 2011-09-13 12:14:09 -0700 (Tue, 13 Sep 2011)
Log Message:
-----------
Use twisted.web.template rather than string concatenation for remaining uses of HTML.
Modified Paths:
--------------
CalendarServer/trunk/setup.py
CalendarServer/trunk/twext/web2/dav/static.py
CalendarServer/trunk/twext/web2/dav/test/test_static.py
CalendarServer/trunk/twext/web2/error.py
CalendarServer/trunk/twext/web2/http.py
CalendarServer/trunk/twext/web2/static.py
CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
CalendarServer/trunk/twistedcaldav/directory/principal.py
CalendarServer/trunk/twistedcaldav/extensions.py
CalendarServer/trunk/twistedcaldav/test/test_extensions.py
Added Paths:
-----------
CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html
CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html
CalendarServer/trunk/twistedcaldav/directory-listing.html
Removed Paths:
-------------
CalendarServer/trunk/twext/web2/dirlist.py
Property Changed:
----------------
CalendarServer/trunk/
Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
- /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
+ /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
Modified: CalendarServer/trunk/setup.py
===================================================================
--- CalendarServer/trunk/setup.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/setup.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -108,6 +108,7 @@
packages = find_modules(),
package_data = {
"twistedcaldav": [
+ "*.html",
"zoneinfo/*.ics",
"zoneinfo/*/*.ics",
"zoneinfo/*/*/*.ics",
@@ -116,7 +117,7 @@
"calendarserver.webadmin": [
"*.html"
],
- "twistedcaldav": [
+ "twistedcaldav.directory": [
"*.html"
],
"txdav.common.datastore": [
Modified: CalendarServer/trunk/twext/web2/dav/static.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/static.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/dav/static.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twext.web2.dav.test.test_static -*-
##
# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
#
Modified: CalendarServer/trunk/twext/web2/dav/test/test_static.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/test_static.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/dav/test/test_static.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -49,7 +49,7 @@
data = []
d = readStream(response.stream, lambda s: data.append(str(s)))
d.addCallback(lambda _: self.failIf(
- 'href="dir2/"' not in "".join(data),
+ 'dir2/' not in "".join(data),
"'dir2' expected in listing: %r" % (data,)
))
return d
Deleted: CalendarServer/trunk/twext/web2/dirlist.py
===================================================================
--- CalendarServer/trunk/twext/web2/dirlist.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/dirlist.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,140 +0,0 @@
-##
-# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
-# Copyright (c) 2010 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.
-#
-##
-
-"""Directory listing."""
-
-# system imports
-import os
-import urllib
-import stat
-import time
-
-# twisted imports
-from twext.web2 import resource, http, http_headers
-
-def formatFileSize(size):
- if size < 1024:
- return '%i' % size
- elif size < (1024**2):
- return '%iK' % (size / 1024)
- elif size < (1024**3):
- return '%iM' % (size / (1024**2))
- else:
- return '%iG' % (size / (1024**3))
-
-class DirectoryLister(resource.Resource):
- def __init__(self, pathname, dirs=None,
- contentTypes={},
- contentEncodings={},
- defaultType='text/html'):
- self.contentTypes = contentTypes
- self.contentEncodings = contentEncodings
- self.defaultType = defaultType
- # dirs allows usage of the File to specify what gets listed
- self.dirs = dirs
- self.path = pathname
- resource.Resource.__init__(self)
-
- def data_listing(self, request, data):
- if self.dirs is None:
- directory = os.listdir(self.path)
- directory.sort()
- else:
- directory = self.dirs
-
- files = []
-
- for path in directory:
- url = urllib.quote(path, '/')
- fullpath = os.path.join(self.path, path)
- try:
- st = os.stat(fullpath)
- except OSError:
- continue
- if stat.S_ISDIR(st.st_mode):
- url = url + '/'
- files.append({
- 'link': url,
- 'linktext': path + "/",
- 'size': '',
- 'type': '-',
- 'lastmod': time.strftime("%Y-%b-%d %H:%M", time.localtime(st.st_mtime))
- })
- else:
- from twext.web2.static import getTypeAndEncoding
- mimetype, encoding = getTypeAndEncoding(
- path,
- self.contentTypes, self.contentEncodings, self.defaultType)
-
- filesize = st.st_size
- files.append({
- 'link': url,
- 'linktext': path,
- 'size': formatFileSize(filesize),
- 'type': mimetype,
- 'lastmod': time.strftime("%Y-%b-%d %H:%M", time.localtime(st.st_mtime))
- })
-
- return files
-
- def __repr__(self):
- return '<DirectoryLister of %r>' % self.path
-
- __str__ = __repr__
-
-
- def render(self, request):
- title = "Directory listing for %s" % urllib.unquote(request.path)
-
- s= """<html><head><title>%s</title><style>
- th, .even td, .odd td { padding-right: 0.5em; font-family: monospace}
- .even-dir { background-color: #efe0ef }
- .even { background-color: #eee }
- .odd-dir {background-color: #f0d0ef }
- .odd { background-color: #dedede }
- .icon { text-align: center }
- .listing {
- margin-left: auto;
- margin-right: auto;
- width: 50%%;
- padding: 0.1em;
- }
-
- body { border: 0; padding: 0; margin: 0; background-color: #efefef;}
- h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}
-</style></head><body><div class="directory-listing"><h1>%s</h1>""" % (title,title)
- s+="<table>"
- s+="<tr><th>Filename</th><th>Size</th><th>Last Modified</th><th>File Type</th></tr>"
- even = False
- for row in self.data_listing(request, None):
- s+='<tr class="%s">' % (even and 'even' or 'odd',)
- s+='<td><a href="%(link)s">%(linktext)s</a></td><td align="right">%(size)s</td><td>%(lastmod)s</td><td>%(type)s</td></tr>' % row
- even = not even
-
- s+="</table></div></body></html>"
- response = http.Response(200, {}, s)
- response.headers.setHeader("content-type", http_headers.MimeType('text', 'html'))
- return response
-
-__all__ = ['DirectoryLister']
Modified: CalendarServer/trunk/twext/web2/error.py
===================================================================
--- CalendarServer/trunk/twext/web2/error.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/error.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twext.web2.test.test_log -*-
##
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
# Copyright (c) 2010 Apple Computer, Inc. All rights reserved.
@@ -27,12 +28,24 @@
"""
from twext.web2 import stream, http_headers
-from twext.web2.responsecode import *
+from twext.web2.responsecode import (
+ MOVED_PERMANENTLY, FOUND, SEE_OTHER, USE_PROXY, TEMPORARY_REDIRECT,
+ BAD_REQUEST, UNAUTHORIZED, PAYMENT_REQUIRED, FORBIDDEN, NOT_FOUND,
+ NOT_ALLOWED, NOT_ACCEPTABLE, PROXY_AUTH_REQUIRED, REQUEST_TIMEOUT, CONFLICT,
+ GONE, LENGTH_REQUIRED, PRECONDITION_FAILED, REQUEST_ENTITY_TOO_LARGE,
+ REQUEST_URI_TOO_LONG, UNSUPPORTED_MEDIA_TYPE,
+ REQUESTED_RANGE_NOT_SATISFIABLE, EXPECTATION_FAILED, INTERNAL_SERVER_ERROR,
+ NOT_IMPLEMENTED, BAD_GATEWAY, SERVICE_UNAVAILABLE, GATEWAY_TIMEOUT,
+ HTTP_VERSION_NOT_SUPPORTED, INSUFFICIENT_STORAGE_SPACE, NOT_EXTENDED,
+ RESPONSES,
+)
+from twisted.web.template import Element, flattenString, XMLString, renderer
+
# 300 - Should include entity with choices
# 301 -
# 304 - Must include Date, ETag, Content-Location, Expires, Cache-Control, Vary.
-#
+
# 401 - Must include WWW-Authenticate.
# 405 - Must include Allow.
# 406 - Should include entity describing allowable characteristics
@@ -44,80 +57,190 @@
ERROR_MESSAGES = {
# 300
# no MULTIPLE_CHOICES
- MOVED_PERMANENTLY: 'The document has permanently moved <a href="%(location)s">here</a>.',
- FOUND: 'The document has temporarily moved <a href="%(location)s">here</a>.',
- SEE_OTHER: 'The results are available <a href="%(location)s">here</a>.',
+ MOVED_PERMANENTLY:
+ 'The document has permanently moved <a>here<t:attr name="href">'
+ '<t:slot name="location" /></t:attr></a>.',
+ FOUND:
+ 'The document has temporarily moved <a>here<t:attr name="href">'
+ '<t:slot name="location" /></t:attr></a>.',
+ SEE_OTHER:
+ 'The results are available <a>here<t:attr name="href">'
+ '<t:slot name="location" /></t:attr></a>.',
# no NOT_MODIFIED
- USE_PROXY: "Access to this resource must be through the proxy %(location)s.",
+ USE_PROXY:
+ 'Access to this resource must be through the proxy '
+ '<t:slot name="location" />.',
# 306 unused
- TEMPORARY_REDIRECT: 'The document has temporarily moved <a href="%(location)s">here</a>.',
+ TEMPORARY_REDIRECT:
+ 'The document has temporarily moved <a><t:attr name="href">'
+ '<t:slot name="location" /></t:attr>here</a>.',
# 400
- BAD_REQUEST: "Your browser sent an invalid request.",
- UNAUTHORIZED: "You are not authorized to view the resource at %(uri)s. Perhaps you entered a wrong password, or perhaps your browser doesn't support authentication.",
- PAYMENT_REQUIRED: "Payment Required (useful result code, this...).",
- FORBIDDEN: "You don't have permission to access %(uri)s.",
- NOT_FOUND: "The resource %(uri)s cannot be found.",
- NOT_ALLOWED: "The requested method %(method)s is not supported by %(uri)s.",
- NOT_ACCEPTABLE: "No representation of %(uri)s that is acceptable to your client could be found.",
- PROXY_AUTH_REQUIRED: "You are not authorized to view the resource at %(uri)s. Perhaps you entered a wrong password, or perhaps your browser doesn't support authentication.",
- REQUEST_TIMEOUT: "Server timed out waiting for your client to finish sending the HTTP request.",
- CONFLICT: "Conflict (?)",
- GONE: "The resource %(uri)s has been permanently removed.",
- LENGTH_REQUIRED: "The resource %(uri)s requires a Content-Length header.",
- PRECONDITION_FAILED: "A precondition evaluated to false.",
- REQUEST_ENTITY_TOO_LARGE: "The provided request entity data is too longer than the maximum for the method %(method)s at %(uri)s.",
- REQUEST_URI_TOO_LONG: "The request URL is longer than the maximum on this server.",
- UNSUPPORTED_MEDIA_TYPE: "The provided request data has a format not understood by the resource at %(uri)s.",
- REQUESTED_RANGE_NOT_SATISFIABLE: "None of the ranges given in the Range request header are satisfiable by the resource %(uri)s.",
- EXPECTATION_FAILED: "The server does support one of the expectations given in the Expect header.",
+ BAD_REQUEST:
+ 'Your browser sent an invalid request.',
+ UNAUTHORIZED:
+ 'You are not authorized to view the resource at <t:slot name="uri" />. '
+ "Perhaps you entered a wrong password, or perhaps your browser doesn't "
+ 'support authentication.',
+ PAYMENT_REQUIRED:
+ 'Payment Required (useful result code, this...).',
+ FORBIDDEN:
+ 'You don\'t have permission to access <t:slot name="uri" />.',
+ NOT_FOUND:
+ 'The resource <t:slot name="uri" /> cannot be found.',
+ NOT_ALLOWED:
+ 'The requested method <t:slot name="method" /> is not supported by '
+ '<t:slot name="uri" />.',
+ NOT_ACCEPTABLE:
+ 'No representation of <t:slot name="uri" /> that is acceptable to your '
+ 'client could be found.',
+ PROXY_AUTH_REQUIRED:
+ 'You are not authorized to view the resource at <t:slot name="uri" />. '
+ 'Perhaps you entered a wrong password, or perhaps your browser doesn\'t '
+ 'support authentication.',
+ REQUEST_TIMEOUT:
+ 'Server timed out waiting for your client to finish sending the request.',
+ CONFLICT:
+ 'Conflict (?)',
+ GONE:
+ 'The resource <t:slot name="uri" /> has been permanently removed.',
+ LENGTH_REQUIRED:
+ 'The resource <t:slot name="uri" /> requires a Content-Length header.',
+ PRECONDITION_FAILED:
+ 'A precondition evaluated to false.',
+ REQUEST_ENTITY_TOO_LARGE:
+ 'The provided request entity data is too longer than the maximum for '
+ 'the method <t:slot name="method" /> at <t:slot name="uri" />.',
+ REQUEST_URI_TOO_LONG:
+ 'The request URL is longer than the maximum on this server.',
+ UNSUPPORTED_MEDIA_TYPE:
+ 'The provided request data has a format not understood by the resource '
+ 'at <t:slot name="uri" />.',
+ REQUESTED_RANGE_NOT_SATISFIABLE:
+ 'None of the ranges given in the Range request header are satisfiable by '
+ 'the resource <t:slot name="uri" />.',
+ EXPECTATION_FAILED:
+ 'The server does support one of the expectations given in the Expect '
+ 'header.',
# 500
- INTERNAL_SERVER_ERROR: "An internal error occurred trying to process your request. Sorry.",
- NOT_IMPLEMENTED: "Some functionality requested is not implemented on this server.",
- BAD_GATEWAY: "An upstream server returned an invalid response.",
- SERVICE_UNAVAILABLE: "This server cannot service your request becaues it is overloaded.",
- GATEWAY_TIMEOUT: "An upstream server is not responding.",
- HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported.",
- INSUFFICIENT_STORAGE_SPACE: "There is insufficient storage space available to perform that request.",
- NOT_EXTENDED: "This server does not support the a mandatory extension requested."
+ INTERNAL_SERVER_ERROR:
+ 'An internal error occurred trying to process your request. Sorry.',
+ NOT_IMPLEMENTED:
+ 'Some functionality requested is not implemented on this server.',
+ BAD_GATEWAY:
+ 'An upstream server returned an invalid response.',
+ SERVICE_UNAVAILABLE:
+ 'This server cannot service your request becaues it is overloaded.',
+ GATEWAY_TIMEOUT:
+ 'An upstream server is not responding.',
+ HTTP_VERSION_NOT_SUPPORTED:
+ 'HTTP Version not supported.',
+ INSUFFICIENT_STORAGE_SPACE:
+ 'There is insufficient storage space available to perform that request.',
+ NOT_EXTENDED:
+ 'This server does not support the a mandatory extension requested.'
}
-# Is there a good place to keep this function?
-def _escape(original):
- if original is None:
- return None
- return original.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
+
+class DefaultErrorElement(Element):
+ """
+ An L{ErrorElement} is an L{Element} that renders some HTML for the default
+ rendering of an error page.
+ """
+
+ loader = XMLString("""
+ <html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="error">
+ <head>
+ <title><t:slot name="code"/> <t:slot name="title"/></title>
+ </head>
+ <body>
+ <h1><t:slot name="title" /></h1>
+ <t:slot name="message" />
+ </body>
+ </html>
+ """)
+
+ def __init__(self, request, response):
+ super(DefaultErrorElement, self).__init__()
+ self.request = request
+ self.response = response
+
+
+ @renderer
+ def error(self, request, tag):
+ """
+ Top-level renderer for page.
+ """
+ return tag.fillSlots(
+ code=str(self.response.code),
+ title=RESPONSES.get(self.response.code),
+ message=self.loadMessage(self.response.code).fillSlots(
+ uri=self.request.uri,
+ location=self.response.headers.getHeader('location'),
+ method=self.request.method,
+ )
+ )
+
+
+ def loadMessage(self, code):
+ tag = XMLString(('<t:transparent xmlns:t="http://twistedmatrix.com/'
+ 'ns/twisted.web.template/0.1">') +
+ ERROR_MESSAGES.get(code, "") +
+ '</t:transparent>').load()[0]
+ return tag
+
+
+
def defaultErrorHandler(request, response):
+ """
+ Handle errors which do not have any stream (i.e. output) associated with
+ them, so that users will see a nice message in their browser.
+
+ This is used as a response filter in L{twext.web2.server.Request}.
+ """
if response.stream is not None:
# Already got an error message
return response
+
if response.code < 300:
# We only do error messages
return response
-
+
message = ERROR_MESSAGES.get(response.code, None)
if message is None:
# No message specified for that code
return response
-
+
message = message % {
- 'uri':_escape(request.uri),
- 'location':_escape(response.headers.getHeader('location')),
- 'method':_escape(request.method)
- }
+ 'uri': request.uri,
+ 'location': response.headers.getHeader('location'),
+ 'method': request.method,
+ }
- title = RESPONSES.get(response.code, "")
- body = ("<html><head><title>%d %s</title></head>"
- "<body><h1>%s</h1>%s</body></html>") % (
- response.code, title, title, message)
-
- response.headers.setHeader("content-type", http_headers.MimeType('text', 'html', {'charset':'utf-8'}))
+ data = []
+ error = []
+
+ (flattenString(request, DefaultErrorElement(request, response))
+ .addCallbacks(data.append, error.append))
+
+ # No deferreds from our renderers above, so this has always already fired.
+ if data:
+ subtype = 'html'
+ body = data[0]
+ else:
+ subtype = 'error'
+ body = 'Error in default error handler:\n' + error[0].getTraceback()
+
+ ctype = http_headers.MimeType('text', subtype, {'charset':'utf-8'})
+ response.headers.setHeader("content-type", ctype)
response.stream = stream.MemoryStream(body)
-
return response
+
defaultErrorHandler.handleErrors = True
__all__ = ['defaultErrorHandler',]
+
Modified: CalendarServer/trunk/twext/web2/http.py
===================================================================
--- CalendarServer/trunk/twext/web2/http.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/http.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -33,10 +33,11 @@
# import traceback; log.msg(''.join(traceback.format_stack()))
import time
-import cgi
from twisted.internet import interfaces, error
from twisted.python import components
+from twisted.web.template import Element, XMLString, renderer, flattenString
+
from zope.interface import implements
from twext.python.log import Logger
@@ -138,11 +139,39 @@
return "<%s.%s code=%d, streamlen=%s>" % (self.__module__, self.__class__.__name__, self.code, streamlen)
+class StatusResponseElement(Element):
+ """
+ Render the HTML for a L{StatusResponse}
+ """
+
+ loader = XMLString("""<html
+ xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="response"><head><title><t:slot name="title"
+ /></title></head><body><h1><t:slot name="title"
+ /></h1><p><t:slot name="description"
+ /></p></body></html>""")
+
+ def __init__(self, title, description):
+ super(StatusResponseElement, self).__init__()
+ self.title = title
+ self.description = description
+
+
+ @renderer
+ def response(self, request, tag):
+ """
+ Top-level renderer.
+ """
+ return tag.fillSlots(title=self.title, description=self.description)
+
+
+
class StatusResponse (Response):
"""
- A L{Response} object which simply contains a status code and a description of
- what happened.
+ A L{Response} object which simply contains a status code and a description
+ of what happened.
"""
+
def __init__(self, code, description, title=None):
"""
@param code: a response code in L{responsecode.RESPONSES}.
@@ -151,36 +180,27 @@
to C{responsecode.RESPONSES[code]}.
"""
if title is None:
- title = cgi.escape(responsecode.RESPONSES[code])
+ title = responsecode.RESPONSES[code]
- output = "".join((
- "<html>",
- "<head>",
- "<title>%s</title>" % (title,),
- "</head>",
- "<body>",
- "<h1>%s</h1>" % (title,),
- "<p>%s</p>" % (cgi.escape(description),),
- "</body>",
- "</html>",
- ))
+ element = StatusResponseElement(title, description)
+ out = []
+ flattenString(None, element).addCallback(out.append)
- if type(output) == unicode:
- output = output.encode("utf-8")
- mime_params = {"charset": "utf-8"}
- else:
- mime_params = {}
+ mime_params = {"charset": "utf-8"}
+ super(StatusResponse, self).__init__(code=code, stream=out[0])
- super(StatusResponse, self).__init__(code=code, stream=output)
+ self.headers.setHeader(
+ "content-type", http_headers.MimeType("text", "html", mime_params)
+ )
- self.headers.setHeader("content-type", http_headers.MimeType("text", "html", mime_params))
-
self.description = description
+
def __repr__(self):
return "<%s %s %s>" % (self.__class__.__name__, self.code, self.description)
+
class RedirectResponse (StatusResponse):
"""
A L{Response} object that contains a redirect to another network location.
Modified: CalendarServer/trunk/twext/web2/static.py
===================================================================
--- CalendarServer/trunk/twext/web2/static.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/static.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -34,7 +34,7 @@
# Sibling Imports
from twext.web2 import http_headers, resource
-from twext.web2 import http, iweb, stream, responsecode, server, dirlist
+from twext.web2 import http, iweb, stream, responsecode, server
from twext.web2.http import HTTPError
# Twisted Imports
@@ -189,10 +189,9 @@
be files underneath that directory. This provides access to an entire
filesystem tree with a single Resource.
- If you map the URL 'http://server/FILE' to a resource created as
- File('/tmp'), then http://server/FILE/ will return an HTML-formatted
- listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
- return the contents of /tmp/foo/bar.html .
+ If you map the URL C{http://server/FILE} to a resource created as
+ File('/tmp'), C{http://server/FILE/foo/bar.html} will return the contents of
+ C{/tmp/foo/bar.html} .
"""
implements(iweb.IResource)
@@ -418,14 +417,12 @@
# Render from the index file
standin = self.createSimilarFile(ifp.path)
else:
- # Render from a DirectoryLister
- standin = dirlist.DirectoryLister(
- self.fp.path,
- self.listChildren(),
- self.contentTypes,
- self.contentEncodings,
- self.defaultType
- )
+ # Directory listing is in twistedcaldav.extensions
+ standin = Data(
+ "\n".join(["Directory: " + str(req.path), "---"] +
+ [x.basename() + ("/" if x.isdir() else "")
+ for x in self.fp.children()]),
+ "text/plain")
return standin.render(req)
try:
Copied: CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html (from rev 8091, CalendarServer/branches/users/glyph/other-html/twistedcaldav/directory/calendar-user-proxy-principal-resource.html)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html 2011-09-13 19:14:09 UTC (rev 8092)
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<t:transparent xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="principal">
+ <div class="directory-listing">
+ <h1>Proxy Principal Details</h1>
+<pre><blockquote>Directory Information
+---------------------
+Directory GUID: <t:slot name="directoryGUID"></t:slot>
+Realm: <t:slot name="realm"></t:slot>
+
+Parent Principal Information
+---------------------
+GUID: <t:slot name="guid"></t:slot>
+Record type: <t:slot name="recordType"></t:slot>
+Short names: <t:slot name="shortNames"></t:slot>
+Full name: <t:slot name="fullName"></t:slot>
+Principal UID: <t:slot name="principalUID"></t:slot>
+Principal URL: <t:slot name="principalURL"></t:slot>
+
+Proxy Principal Information
+---------------------
+Principal UID: <t:slot name="proxyPrincipalUID"></t:slot>
+Principal URL: <t:slot name="proxyPrincipalURL"></t:slot>
+
+Alternate URIs:
+<t:slot name="alternateURIs"></t:slot>
+Group members:
+<t:slot name="groupMembers"></t:slot>
+Group memberships:
+<t:slot name="groupMemberships"></t:slot></blockquote></pre></div>
+ </t:transparent>
Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twistedcaldav.directory.test.test_proxyprincipalmembers -*-
##
# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
#
@@ -39,6 +40,12 @@
from twext.python.log import Logger, LoggingMixIn
+from twisted.web.template import XMLFile, Element, renderer
+from twisted.python.modules import getModule
+from twistedcaldav.extensions import DirectoryElement
+from twistedcaldav.directory.principal import formatLink
+from twistedcaldav.directory.principal import formatLinks
+from twistedcaldav.directory.principal import formatPrincipals
from twistedcaldav.config import config, fullServerPath
from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
@@ -49,6 +56,7 @@
from twistedcaldav.memcacher import Memcacher
from twistedcaldav.resource import CalDAVComplianceMixIn
+thisModule = getModule(__name__)
log = Logger()
class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
@@ -79,14 +87,79 @@
return davxml.ACL(*aces)
- def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+
+ def accessControlList(self, request, inheritance=True, expanding=False,
+ inherited_aces=None):
# Permissions here are fixed, and are not subject to inheritance rules, etc.
return succeed(self.defaultAccessControlList())
-class CalendarUserProxyPrincipalResource (CalDAVComplianceMixIn, PermissionsMixIn, DAVResourceWithChildrenMixin, DAVPrincipalResource):
+
+
+class ProxyPrincipalDetailElement(Element):
"""
+ A L{ProxyPrincipalDetailElement} is an L{Element} that can render the
+ details of a L{CalendarUserProxyPrincipalResource}.
+ """
+
+ loader = XMLFile(thisModule.filePath.sibling(
+ "calendar-user-proxy-principal-resource.html").open()
+ )
+
+ def __init__(self, resource):
+ super(ProxyPrincipalDetailElement, self).__init__()
+ self.resource = resource
+
+
+ @renderer
+ def principal(self, request, tag):
+ """
+ Top-level renderer in the template.
+ """
+ record = self.resource.parent.record
+ resource = self.resource
+ parent = self.resource.parent
+ return tag.fillSlots(
+ directoryGUID=record.service.guid,
+ realm=record.service.realmName,
+ guid=record.guid,
+ recordType=record.recordType,
+ shortNames=record.shortNames,
+ fullName=record.fullName,
+ principalUID=parent.principalUID(),
+ principalURL=formatLink(parent.principalURL()),
+ proxyPrincipalUID=resource.principalUID(),
+ proxyPrincipalURL=formatLink(resource.principalURL()),
+ alternateURIs=formatLinks(resource.alternateURIs()),
+ groupMembers=resource.groupMembers().addCallback(formatPrincipals),
+ groupMemberships=resource.groupMemberships().addCallback(
+ formatPrincipals
+ ),
+ )
+
+
+
+class ProxyPrincipalElement(DirectoryElement):
+ """
+ L{ProxyPrincipalElement} is a renderer for a
+ L{CalendarUserProxyPrincipalResource}.
+ """
+
+ @renderer
+ def resourceDetail(self, request, tag):
+ """
+ Render the proxy principal's details.
+ """
+ return ProxyPrincipalDetailElement(self.resource)
+
+
+
+class CalendarUserProxyPrincipalResource (
+ CalDAVComplianceMixIn, PermissionsMixIn, DAVResourceWithChildrenMixin,
+ DAVPrincipalResource):
+ """
Calendar user proxy principal resource.
"""
+
def __init__(self, parent, proxyType):
"""
@param parent: the parent of this resource.
@@ -102,28 +175,27 @@
super(CalendarUserProxyPrincipalResource, self).__init__()
DAVResourceWithChildrenMixin.__init__(self)
- self.parent = parent
- self.proxyType = proxyType
- self.pcollection = self.parent.parent.parent # FIXME: if this is supposed to be public, it needs a better name
- self._url = url
+ self.parent = parent
+ self.proxyType = proxyType
+ self._url = url
- # Not terribly useful at present because we don't have a way
- # to map a GUID back to the correct principal.
- #self.guid = uuidFromName(self.parent.principalUID(), proxyType)
+ # FIXME: if this is supposed to be public, it needs a better name:
+ self.pcollection = self.parent.parent.parent
- # Principal UID is parent's GUID plus the proxy type; this we
- # can easily map back to a principal.
- self.uid = "%s#%s" % (self.parent.principalUID(), proxyType)
-
+ # Principal UID is parent's GUID plus the proxy type; this we can easily
+ # map back to a principal.
+ self.uid = "%s#%s" % (self.parent.principalUID(), proxyType)
self._alternate_urls = tuple(
joinURL(url, proxyType) + slash
for url in parent.alternateURIs()
if url.startswith("/")
)
+
def __str__(self):
return "%s [%s]" % (self.parent, self.proxyType)
+
def _index(self):
"""
Return the SQL database for this group principal.
@@ -160,24 +232,29 @@
self._dead_properties = NonePropertyStore(self)
return self._dead_properties
+
def writeProperty(self, property, request):
assert isinstance(property, davxml.WebDAVElement)
if property.qname() == (dav_namespace, "group-member-set"):
return self.setGroupMemberSet(property, request)
- return super(CalendarUserProxyPrincipalResource, self).writeProperty(property, request)
+ return super(CalendarUserProxyPrincipalResource, self).writeProperty(
+ property, request)
+
@inlineCallbacks
def setGroupMemberSet(self, new_members, request):
- # FIXME: as defined right now it is not possible to specify a calendar-user-proxy group as
- # a member of any other group since the directory service does not know how to lookup
- # these special resource UIDs.
+ # FIXME: as defined right now it is not possible to specify a
+ # calendar-user-proxy group as a member of any other group since the
+ # directory service does not know how to lookup these special resource
+ # UIDs.
#
- # Really, c-u-p principals should be treated the same way as any other principal, so
- # they should be allowed as members of groups.
+ # Really, c-u-p principals should be treated the same way as any other
+ # principal, so they should be allowed as members of groups.
#
- # This implementation now raises an exception for any principal it cannot find.
+ # This implementation now raises an exception for any principal it
+ # cannot find.
# Break out the list into a set of URIs.
members = [str(h) for h in new_members.children]
@@ -191,29 +268,31 @@
if principal is None or principal.principalURL() != uri:
raise HTTPError(StatusResponse(
responsecode.BAD_REQUEST,
- "Attempt to use a non-existent principal %s as a group member of %s." % (uri, self.principalURL(),)
+ "Attempt to use a non-existent principal %s "
+ "as a group member of %s." % (uri, self.principalURL(),)
))
principals.append(principal)
newUIDs.add(principal.principalUID())
# Get the old set of UIDs
oldUIDs = (yield self._index().getMembers(self.uid))
-
+
# Change membership
yield self.setGroupMemberSetPrincipals(principals)
-
+
# Invalidate the primary principal's cache, and any principal's whose
# membership status changed
yield self.parent.cacheNotifier.changed()
-
+
changedUIDs = newUIDs.symmetric_difference(oldUIDs)
for uid in changedUIDs:
principal = self.pcollection.principalForUID(uid)
if principal:
yield principal.cacheNotifier.changed()
-
+
returnValue(True)
+
def setGroupMemberSetPrincipals(self, principals):
# Map the principals to UIDs.
return self._index().setGroupMembers(
@@ -225,55 +304,13 @@
# HTTP
##
- def renderDirectoryBody(self, request):
- # FIXME: Too much code duplication here from principal.py
- from twistedcaldav.directory.principal import format_list, format_principals, format_link
+ def htmlElement(self):
+ """
+ Customize HTML display of proxy groups.
+ """
+ return ProxyPrincipalElement(self)
- closure = {}
- d = super(CalendarUserProxyPrincipalResource, self).renderDirectoryBody(request)
- d.addCallback(lambda output: closure.setdefault("output", output))
-
- d.addCallback(lambda _: self.groupMembers())
- d.addCallback(lambda members: closure.setdefault("members", members))
-
- d.addCallback(lambda _: self.groupMemberships())
- d.addCallback(lambda memberships: closure.setdefault("memberships", memberships))
-
- d.addCallback(
- lambda _: "".join((
- """<div class="directory-listing">"""
- """<h1>Principal Details</h1>"""
- """<pre><blockquote>"""
- """Directory Information\n"""
- """---------------------\n"""
- """Directory GUID: %s\n""" % (self.parent.record.service.guid,),
- """Realm: %s\n""" % (self.parent.record.service.realmName,),
- """\n"""
- """Parent Principal Information\n"""
- """---------------------\n"""
- """GUID: %s\n""" % (self.parent.record.guid,),
- """Record type: %s\n""" % (self.parent.record.recordType,),
- """Short names: %s\n""" % (",".join(self.parent.record.shortNames,)),
- """Full name: %s\n""" % (self.parent.record.fullName,),
- """Principal UID: %s\n""" % (self.parent.principalUID(),),
- """Principal URL: %s\n""" % (format_link(self.parent.principalURL()),),
- """\n"""
- """Proxy Principal Information\n"""
- """---------------------\n"""
- #"""GUID: %s\n""" % (self.guid,),
- """Principal UID: %s\n""" % (self.principalUID(),),
- """Principal URL: %s\n""" % (format_link(self.principalURL()),),
- """\nAlternate URIs:\n""" , format_list(format_link(u) for u in self.alternateURIs()),
- """\nGroup members:\n""" , format_principals(closure["members"]),
- """\nGroup memberships:\n""" , format_principals(closure["memberships"]),
- """</pre></blockquote></div>""",
- closure["output"]
- ))
- )
-
- return d
-
##
# DAV
##
@@ -281,6 +318,7 @@
def displayName(self):
return self.proxyType
+
##
# ACL
##
Copied: CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html (from rev 8091, CalendarServer/branches/users/glyph/other-html/twistedcaldav/directory/directory-principal-resource.html)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html 2011-09-13 19:14:09 UTC (rev 8092)
@@ -0,0 +1,41 @@
+<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="principal" class="directory-listing">
+ <h1>Principal Details</h1>
+ <pre><blockquote>Directory Information
+---------------------
+Directory GUID: <t:slot name="directoryGUID"/>
+Realm: <t:slot name="realm"/>
+<t:transparent t:render="serversEnabled">Hosted-At: <t:slot name="hostedAt"/>
+Partition: <t:slot name="partition"/></t:transparent>
+Principal Information
+---------------------
+GUID: <t:slot name="principalGUID"/>
+Record type: <t:slot name="recordType"/>
+Short names: <t:slot name="shortNames"/>
+Security Identities: <t:slot name="securityIDs"/>
+Full name: <t:slot name="fullName"/>
+First name: <t:slot name="firstName"/>
+Last name: <t:slot name="lastName"/>
+Email addresses:
+<t:slot name="emailAddresses" />Principal UID: <t:slot name="principalUID"/>
+Principal URL: <t:slot name="principalURL"/>
+
+Alternate URIs:
+<t:slot name="alternateURIs"/>
+Group members:
+<t:slot name="groupMembers"/>
+Group memberships:
+<t:slot name="groupMemberships"/>
+Read-write Proxy For:
+<t:slot name="readWriteProxyFor"/>
+Read-only Proxy For:
+<t:slot name="readOnlyProxyFor"/><t:transparent
+t:render="extra"><t:transparent t:render="enabledForCalendaring">
+Calendar Homes:
+<t:slot name="calendarHomes" />
+Calendar user addresses:
+<t:slot name="calendarUserAddresses" /></t:transparent><t:transparent
+t:render="enabledForAddressBooks">
+Address Book homes:
+<t:slot name="addressBookHomes"
+/></t:transparent></t:transparent></blockquote></pre></div>
Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -26,12 +26,8 @@
"DirectoryPrincipalUIDProvisioningResource",
"DirectoryPrincipalResource",
"DirectoryCalendarPrincipalResource",
- "format_list",
- "format_principals",
- "format_link",
]
-from cgi import escape
from urllib import unquote
from urlparse import urlparse
@@ -39,6 +35,8 @@
from twisted.python.failure import Failure
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.defer import succeed
+from twisted.web.template import XMLFile, Element, renderer, tags
+
from twext.web2.auth.digest import DigestedCredentials
from twext.web2 import responsecode
from twext.web2.http import HTTPError
@@ -50,29 +48,37 @@
try:
from twistedcaldav.authkerb import NegotiateCredentials
+ NegotiateCredentials # sigh, pyflakes
except ImportError:
NegotiateCredentials = None
+from twisted.python.modules import getModule
from twistedcaldav.config import config
from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
-from twistedcaldav.directory import calendaruserproxy
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
+
+from twistedcaldav.extensions import DirectoryElement
+
from twistedcaldav.directory.common import uidsResourceName
from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVPrincipalResource,\
DAVResourceWithChildrenMixin
-from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
+from twistedcaldav.resource import (
+ CalendarPrincipalCollectionResource, CalendarPrincipalResource
+)
from twistedcaldav.directory.idirectory import IDirectoryService
from twistedcaldav import caldavxml, customxml
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
+thisModule = getModule(__name__)
log = Logger()
+
class PermissionsMixIn (ReadOnlyResourceMixIn):
def defaultAccessControlList(self):
return authReadACL
- def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+ def accessControlList(self, request, inheritance=True, expanding=False,
+ inherited_aces=None):
return succeed(self.defaultAccessControlList())
@@ -487,8 +493,172 @@
def principalCollections(self):
return self.parent.principalCollections()
-class DirectoryPrincipalResource (PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource):
+
+
+class DirectoryPrincipalDetailElement(Element):
"""
+ Element that can render the details of a
+ L{CalendarUserDirectoryPrincipalResource}.
+ """
+
+ loader = XMLFile(thisModule.filePath.sibling(
+ "directory-principal-resource.html").open()
+ )
+
+ def __init__(self, resource):
+ super(DirectoryPrincipalDetailElement, self).__init__()
+ self.resource = resource
+
+
+ @renderer
+ def serversEnabled(self, request, tag):
+ """
+ Renderer for when servers are enabled.
+ """
+ if not config.Servers.Enabled:
+ return ""
+ record = self.resource.record
+ return tag.fillSlots(
+ hostedAt=str(record.serverURI()),
+ partition=str(record.effectivePartitionID()),
+ )
+
+
+ @renderer
+ def principal(self, request, tag):
+ """
+ Top-level renderer in the template.
+ """
+ record = self.resource.record
+ return tag.fillSlots(
+ directoryGUID=str(record.service.guid),
+ realm=str(record.service.realmName),
+ principalGUID=str(record.guid),
+ recordType=str(record.recordType),
+ shortNames=",".join(record.shortNames),
+ securityIDs=",".join(record.authIDs),
+ fullName=str(record.fullName),
+ firstName=str(record.firstName),
+ lastName=str(record.lastName),
+ emailAddresses=formatList(record.emailAddresses),
+ principalUID=str(self.resource.principalUID()),
+ principalURL=formatLink(self.resource.principalURL()),
+ alternateURIs=formatLinks(self.resource.alternateURIs()),
+ groupMembers=self.resource.groupMembers().addCallback(
+ formatPrincipals
+ ),
+ groupMemberships=self.resource.groupMemberships().addCallback(
+ formatPrincipals
+ ),
+ readWriteProxyFor=self.resource.proxyFor(True).addCallback(
+ formatPrincipals
+ ),
+ readOnlyProxyFor=self.resource.proxyFor(False).addCallback(
+ formatPrincipals
+ ),
+ )
+
+
+ @renderer
+ def extra(self, request, tag):
+ """
+ No-op; implemented in subclass.
+ """
+ return ''
+
+
+ @renderer
+ def enabledForCalendaring(self, request, tag):
+ """
+ No-op; implemented in subclass.
+ """
+ return ''
+
+
+ @renderer
+ def enabledForAddressBooks(self, request, tag):
+ """
+ No-op; implemented in subclass.
+ """
+ return ''
+
+
+
+class DirectoryPrincipalElement(DirectoryElement):
+ """
+ L{DirectoryPrincipalElement} is a renderer for directory details.
+ """
+
+ @renderer
+ def resourceDetail(self, request, tag):
+ """
+ Render the directory principal's details.
+ """
+ return DirectoryPrincipalDetailElement(self.resource)
+
+
+class DirectoryCalendarPrincipalDetailElement(DirectoryPrincipalDetailElement):
+
+ @renderer
+ def extra(self, request, tag):
+ """
+ Renderer for extra directory body items for calendar/addressbook
+ principals.
+ """
+ return tag
+
+
+ @renderer
+ def enabledForCalendaring(self, request, tag):
+ """
+ Renderer which returns its tag when the wrapped record is enabled for
+ calendaring.
+ """
+ resource = self.resource
+ record = resource.record
+ if record.enabledForCalendaring:
+ return tag.fillSlots(
+ calendarUserAddresses=formatLinks(
+ resource.calendarUserAddresses()
+ ),
+ calendarHomes=formatLinks(resource.calendarHomeURLs())
+ )
+ return ''
+
+
+ @renderer
+ def enabledForAddressBooks(self, request, tag):
+ """
+ Renderer which returnst its tag when the wrapped record is enabled for
+ addressbooks.
+ """
+ resource = self.resource
+ record = resource.record
+ if record.enabledForAddressBooks:
+ return tag.fillSlots(
+ addressBookHomes=formatLinks(resource.addressBookHomeURLs())
+ )
+ return ''
+
+
+
+class DirectoryCalendarPrincipalElement(DirectoryPrincipalElement):
+ """
+ L{DirectoryPrincipalElement} is a renderer for directory details, with
+ calendaring additions.
+ """
+
+ @renderer
+ def resourceDetail(self, request, tag):
+ """
+ Render the directory calendar principal's details.
+ """
+ return DirectoryCalendarPrincipalDetailElement(self.resource)
+
+
+class DirectoryPrincipalResource (
+ PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource):
+ """
Directory principal resource.
"""
@@ -579,54 +749,12 @@
# HTTP
##
- @inlineCallbacks
- def renderDirectoryBody(self, request):
+ def htmlElement(self):
+ """
+ Customize HTML rendering for directory principals.
+ """
+ return DirectoryPrincipalElement(self)
- extras = self.extraDirectoryBodyItems(request)
- output = (yield super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
-
- members = (yield self.groupMembers())
-
- memberships = (yield self.groupMemberships())
-
- proxyFor = (yield self.proxyFor(True))
- readOnlyProxyFor = (yield self.proxyFor(False))
-
- returnValue("".join((
- """<div class="directory-listing">"""
- """<h1>Principal Details</h1>"""
- """<pre><blockquote>"""
- """Directory Information\n"""
- """---------------------\n"""
- """Directory GUID: %s\n""" % (self.record.service.guid,),
- """Realm: %s\n""" % (self.record.service.realmName,),
- """Hosted-At: %s\n""" % (self.record.serverURI(),) if config.Servers.Enabled else "",
- """Partition: %s\n""" % (self.record.effectivePartitionID(),) if config.Servers.Enabled else "",
- """\n"""
- """Principal Information\n"""
- """---------------------\n"""
- """GUID: %s\n""" % (self.record.guid,),
- """Record type: %s\n""" % (self.record.recordType,),
- """Short names: %s\n""" % (",".join(self.record.shortNames),),
- """Security Identities: %s\n""" % (",".join(self.record.authIDs),),
- """Full name: %s\n""" % (self.record.fullName,),
- """First name: %s\n""" % (self.record.firstName,),
- """Last name: %s\n""" % (self.record.lastName,),
- """Email addresses:\n""" , format_list(self.record.emailAddresses),
- """Principal UID: %s\n""" % (self.principalUID(),),
- """Principal URL: %s\n""" % (format_link(self.principalURL()),),
- """\nAlternate URIs:\n""" , format_list(format_link(u) for u in self.alternateURIs()),
- """\nGroup members:\n""" , format_principals(members),
- """\nGroup memberships:\n""" , format_principals(memberships),
- """\nRead-write Proxy For:\n""" , format_principals(proxyFor),
- """\nRead-only Proxy For:\n""" , format_principals(readOnlyProxyFor),
- """%s</pre></blockquote></div>""" % extras,
- output
- )))
-
- def extraDirectoryBodyItems(self, request):
- return ""
-
##
# DAV
##
@@ -652,8 +780,10 @@
"""
# The db is located in the principal collection root
- return calendaruserproxy.ProxyDBService
+ from twistedcaldav.directory.calendaruserproxy import ProxyDBService
+ return ProxyDBService
+
def alternateURIs(self):
# FIXME: Add API to IDirectoryRecord for getting a record URI?
return self._alternate_urls
@@ -839,7 +969,8 @@
return ()
-class DirectoryCalendarPrincipalResource (DirectoryPrincipalResource, CalendarPrincipalResource):
+class DirectoryCalendarPrincipalResource(DirectoryPrincipalResource,
+ CalendarPrincipalResource):
"""
Directory calendar principal resource.
"""
@@ -861,18 +992,6 @@
result = (yield CalendarPrincipalResource.readProperty(self, property, request))
returnValue(result)
- def extraDirectoryBodyItems(self, request):
- extra = ""
- if self.record.enabledForCalendaring:
- extra += "".join((
- """\nCalendar homes:\n""" , format_list(format_link(u) for u in self.calendarHomeURLs()),
- """\nCalendar user addresses:\n""" , format_list(format_link(a) for a in self.calendarUserAddresses()),
- ))
- if self.record.enabledForAddressBooks:
- extra += "".join((
- """\nAddress Book homes:\n""" , format_list(format_link(u) for u in self.addressBookHomeURLs()),
- ))
- return extra
##
# CalDAV
@@ -897,6 +1016,14 @@
return addresses
+
+ def htmlElement(self):
+ """
+ Customize HTML generation for calendar principals.
+ """
+ return DirectoryCalendarPrincipalElement(self)
+
+
def canonicalCalendarUserAddress(self):
"""
Return a CUA for this principal, preferring in this order:
@@ -1052,8 +1179,12 @@
if name == "":
return self
- if config.EnableProxyPrincipals and name in ("calendar-proxy-read", "calendar-proxy-write"):
+ if config.EnableProxyPrincipals and name in ("calendar-proxy-read",
+ "calendar-proxy-write"):
# name is required to be str
+ from twistedcaldav.directory.calendaruserproxy import (
+ CalendarUserProxyPrincipalResource
+ )
return CalendarUserProxyPrincipalResource(self, str(name))
else:
return None
@@ -1077,21 +1208,11 @@
),
)
-def format_list(items, *args):
- def genlist():
- try:
- item = None
- for item in items:
- yield " -> %s\n" % (item,)
- if item is None:
- yield " '()\n"
- except Exception, e:
- log.err("Exception while rendering: %s" % (e,))
- Failure().printTraceback()
- yield " ** %s **: %s\n" % (e.__class__.__name__, e)
- return "".join(genlist())
-def format_principals(principals):
+def formatPrincipals(principals):
+ """
+ Format a list of principals into some twisted.web.template DOM objects.
+ """
def recordKey(principal):
try:
record = principal.record
@@ -1100,7 +1221,6 @@
record = principal.parent.record
except:
return None
-
return (record.recordType, record.shortNames[0])
def describe(principal):
@@ -1109,11 +1229,49 @@
else:
return ""
- return format_list(
- """<a href="%s">%s%s</a>"""
- % (principal.principalURL(), escape(str(principal)), describe(principal))
+ return formatList(
+ tags.a(href=principal.principalURL())(
+ str(principal), describe(principal)
+ )
for principal in sorted(principals, key=recordKey)
)
-def format_link(url):
- return """<a href="%s">%s</a>""" % (url, url)
+
+def formatList(iterable):
+ """
+ Format a list of stuff as an interable.
+ """
+ thereAreAny = False
+ try:
+ item = None
+ for item in iterable:
+ thereAreAny = True
+ yield " -> "
+ if item is None:
+ yield "None"
+ else:
+ yield item
+ yield "\n"
+ except Exception, e:
+ log.err("Exception while rendering: %s" % (e,))
+ Failure().printTraceback()
+ yield " ** %s **: %s\n" % (e.__class__.__name__, e)
+ if not thereAreAny:
+ yield " '()\n"
+
+
+
+def formatLink(url):
+ """
+ Convert a URL string into some twisted.web.template DOM objects for
+ rendering as a link to itself.
+ """
+ return tags.a(href=url)(url)
+
+
+def formatLinks(urls):
+ """
+ Format a list of URL strings as a list of twisted.web.template DOM links.
+ """
+ return formatList(formatLink(link) for link in urls)
+
Copied: CalendarServer/trunk/twistedcaldav/directory-listing.html (from rev 8091, CalendarServer/branches/users/glyph/other-html/twistedcaldav/directory-listing.html)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory-listing.html (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory-listing.html 2011-09-13 19:14:09 UTC (rev 8092)
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="main">
+ <head>
+ <title>Collection listing for <t:slot name="name" /></title>
+ </head>
+ <style>
+ th, .even td, .odd td { padding-right: 0.5em; font-family: monospace}
+ .even-dir { background-color: #efe0ef }
+ .even { background-color: #eee }
+ .odd-dir {background-color: #f0d0ef }
+ .odd { background-color: #dedede }
+ .icon { text-align: center }
+ .listing {
+ margin-left: auto;
+ margin-right: auto;
+ width: 50%;
+ padding: 0.1em;
+ }
+ body {
+ border: 0; padding: 0; margin: 0;
+ background-color: #efefef;
+ }
+ h1 {
+ padding: 0.1em;
+ background-color: #777;
+ color: white;
+ border-bottom: thin white dashed;
+ }
+ </style>
+ <body>
+ <t:transparent t:render="resourceDetail" />
+ <div class="directory-listing">
+ <h1>Collection Listing</h1>
+ <table>
+ <tr>
+ <th>Name</th><th>Size</th><th>Last Modified</th>
+ <th>MIME Type</th>
+ </tr>
+ <tr t:render="children">
+ <t:attr name="class"><t:slot name="even"/></t:attr>
+ <td><a><t:slot name="name"/><t:attr
+ name="href"><t:slot name="url"/></t:attr></a></td>
+ <td align="right"><t:slot name="size"/></td>
+ <td><t:slot name="lastModified"/></td>
+ <td><t:slot name="type"/></td>
+ </tr>
+ </table>
+ </div>
+ <div class="directory-listing">
+ <h1>Properties</h1>
+ <table>
+ <tr><th>Name</th><th>Value</th></tr>
+ <tr t:render="properties">
+ <t:attr name="class"><t:slot name="even"/></t:attr>
+ <td valign="top"><t:slot name="name"/></td>
+ <td><pre><t:slot name="value"/></pre></td>
+ </tr>
+ </table>
+ </div>
+ </body>
+</html>
Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/extensions.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -30,13 +30,16 @@
]
import urllib
-import cgi
import time
+from itertools import cycle
-from twisted.internet.defer import succeed, DeferredList, maybeDeferred
+from twisted.internet.defer import succeed, maybeDeferred
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.cred.error import LoginFailed, UnauthorizedLogin
+from twisted.web.template import Element, XMLFile, renderer, tags, flattenString
+from twisted.python.modules import getModule
+
from twext.web2 import responsecode, server
from twext.web2.auth.wrapper import UnauthorizedResponse
from twext.web2.http import HTTPError, Response, RedirectResponse
@@ -51,7 +54,10 @@
from twext.web2.dav.idav import IDAVPrincipalResource
from twext.web2.dav.static import DAVFile as SuperDAVFile
from twext.web2.dav.resource import DAVResource as SuperDAVResource
-from twext.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
+from twext.web2.dav.resource import (
+ DAVPrincipalResource as SuperDAVPrincipalResource
+)
+from twisted.internet.defer import gatherResults
from twext.web2.dav.method import prop_common
from twext.web2.dav.method.report import max_number_of_matches
@@ -59,11 +65,14 @@
from twistedcaldav import customxml
from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.util import Alternator
+
from twistedcaldav.directory.sudo import SudoDirectoryService
from twistedcaldav.directory.directory import DirectoryService
from twistedcaldav.method.report import http_REPORT
+
+thisModule = getModule(__name__)
+
log = Logger()
@@ -429,197 +438,156 @@
if resultsWereLimited is not None:
if resultsWereLimited[0] == "server":
- log.err("Too many matching resources in principal-property-search report")
+ log.err("Too many matching resources in "
+ "principal-property-search report")
responses.append(davxml.StatusResponse(
davxml.HRef.fromString(request.uri),
- davxml.Status.fromResponseCode(responsecode.INSUFFICIENT_STORAGE_SPACE),
+ davxml.Status.fromResponseCode(
+ responsecode.INSUFFICIENT_STORAGE_SPACE
+ ),
davxml.Error(davxml.NumberOfMatchesWithinLimits()),
- davxml.ResponseDescription("Results limited by %s at %d" % resultsWereLimited),
+ davxml.ResponseDescription("Results limited by %s at %d"
+ % resultsWereLimited),
))
returnValue(MultiStatusResponse(responses))
-class DirectoryRenderingMixIn(object):
- def directoryStyleSheet(self):
- return (
- "th, .even td, .odd td { padding-right: 0.5em; font-family: monospace}"
- ".even-dir { background-color: #efe0ef }"
- ".even { background-color: #eee }"
- ".odd-dir {background-color: #f0d0ef }"
- ".odd { background-color: #dedede }"
- ".icon { text-align: center }"
- ".listing {"
- "margin-left: auto;"
- "margin-right: auto;"
- "width: 50%;"
- "padding: 0.1em;"
- "}"
- "body { border: 0; padding: 0; margin: 0; background-color: #efefef;}"
- "h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}"
- )
+class DirectoryElement(Element):
+ """
+ A L{DirectoryElement} is an L{Element} for rendering the contents of a
+ L{DirectoryRenderingMixIn} resource as HTML.
+ """
- def renderDirectory(self, request):
+ loader = XMLFile(
+ thisModule.filePath.sibling("directory-listing.html").open()
+ )
+
+ def __init__(self, resource):
"""
- Render a directory listing.
+ @param resource: the L{DirectoryRenderingMixIn} resource being
+ listed.
"""
- output = [
- """<html>"""
- """<head>"""
- """<title>Collection listing for %(path)s</title>"""
- """<style>%(style)s</style>"""
- """</head>"""
- """<body>"""
- % {
- "path": "%s" % cgi.escape(urllib.unquote(request.path)),
- "style": self.directoryStyleSheet(),
- }
- ]
+ super(DirectoryElement, self).__init__()
+ self.resource = resource
- def gotBody(body, output=output):
- output.append(body)
- output.append("</body></html>")
- output = "".join(output)
+ @renderer
+ def resourceDetail(self, request, tag):
+ """
+ Renderer which returns a distinct element for this resource's data.
+ Subclasses should override.
+ """
+ return ''
- if isinstance(output, unicode):
- output = output.encode("utf-8")
- mime_params = {"charset": "utf-8"}
+ @renderer
+ def children(self, request, tag):
+ """
+ Renderer which yields all child object tags as table rows.
+ """
+ whenChildren = (
+ maybeDeferred(self.resource.listChildren)
+ .addCallback(sorted)
+ .addCallback(
+ lambda names: gatherResults(
+ [maybeDeferred(self.resource.getChild, x) for x in names]
+ )
+ .addCallback(lambda children: zip(children, names))
+ )
+ )
+ @whenChildren.addCallback
+ def gotChildren(children):
+ for even, [child, name] in zip(cycle(["odd", "even"]), children):
+ [url, name, size, lastModified, contentType] = map(
+ str, self.resource.getChildDirectoryEntry(
+ child, name, request)
+ )
+ yield tag.clone().fillSlots(
+ url=url, name=name, size=str(size),
+ lastModified=lastModified, even=even, type=contentType,
+ )
+ return whenChildren
- response = Response(200, {}, output)
- response.headers.setHeader("content-type", MimeType("text", "html", mime_params))
- return response
- d = self.renderDirectoryBody(request)
- d.addCallback(gotBody)
- return d
-
- @inlineCallbacks
- def renderDirectoryBody(self, request):
+ @renderer
+ def main(self, request, tag):
"""
- Generate a directory listing table in HTML.
+ Main renderer; fills slots for title, etc.
"""
- output = [
- """<div class="directory-listing">"""
- """<h1>Collection Listing</h1>"""
- """<table>"""
- """<tr><th>Name</th> <th>Size</th> <th>Last Modified</th> <th>MIME Type</th></tr>"""
- ]
+ return tag.fillSlots(name=request.path)
- even = Alternator()
- for name in sorted((yield self.listChildren())):
- child = (yield maybeDeferred(self.getChild, name))
- url, name, size, lastModified, contentType = self.getChildDirectoryEntry(child, name, request)
-
- # FIXME: gray out resources that are not readable
- output.append(
- """<tr class="%(even)s">"""
- """<td><a href="%(url)s">%(name)s</a></td>"""
- """<td align="right">%(size)s</td>"""
- """<td>%(lastModified)s</td>"""
- """<td>%(type)s</td>"""
- """</tr>"""
- % {
- "even": even.state() and "even" or "odd",
- "url": url,
- "name": cgi.escape(name),
- "size": size,
- "lastModified": lastModified,
- "type": contentType,
- }
- )
-
- output.append(
- """</table></div>"""
- """<div class="directory-listing">"""
- """<h1>Properties</h1>"""
- """<table>"""
- """<tr><th>Name</th> <th>Value</th></tr>"""
- )
-
+ @renderer
+ def properties(self, request, tag):
+ """
+ Renderer which yields all properties as table row tags.
+ """
+ whenPropertiesListed = self.resource.listProperties(request)
+ @whenPropertiesListed.addCallback
def gotProperties(qnames):
- ds = []
-
- noneValue = object()
accessDeniedValue = object()
- def gotProperty(property):
- if property is None:
- name = "{%s}%s" % qname
- value = noneValue
- else:
- name = property.sname()
- value = property.toxml()
-
- return (name, value)
-
- def gotError(f, qname):
+ def gotError(f, name):
f.trap(HTTPError)
-
- name = "{%s}%s" % qname
code = f.value.response.code
-
if code == responsecode.NOT_FOUND:
- log.err("Property {%s}%s was returned by listProperties() but does not exist for resource %s."
- % (qname[0], qname[1], self))
+ log.err("Property %s was returned by listProperties() "
+ "but does not exist for resource %s."
+ % (name, self.resource))
return (name, None)
-
if code == responsecode.UNAUTHORIZED:
return (name, accessDeniedValue)
-
return f
- for qname in sorted(qnames):
- d = self.readProperty(qname, request)
- d.addCallback(gotProperty)
- d.addErrback(gotError, qname)
- ds.append(d)
+ whenAllProperties = gatherResults([
+ maybeDeferred(self.resource.readProperty, qn, request)
+ .addCallback(lambda p, iqn=qn: (p.sname(), p.toxml())
+ if p is not None else ("{%s}%s" % iqn, None) )
+ .addErrback(gotError, "{%s}%s" % qn)
+ for qn in sorted(qnames)
+ ])
- even = Alternator()
-
+ @whenAllProperties.addCallback
def gotValues(items):
- for result, (name, value) in items:
- if not result:
- continue
-
+ for even, [name, value] in zip(cycle(["odd", "even"]), items):
if value is None:
- # An AssertionError might be appropriate, but
- # we may as well continue rendering.
- log.err("Unexpected None value for property: %s" % (name,))
- continue
- elif value is noneValue:
- value = "<i>(no value)</i>"
+ value = tags.i("(no value)")
elif value is accessDeniedValue:
- value = "<i>(access forbidden)</i>"
- else:
- value = cgi.escape(value)
-
- output.append(
- str("""<tr class="%(even)s">"""
- """<td valign="top">%(name)s</td>"""
- """<td><pre>%(value)s</pre></td>"""
- """</tr>"""
- % {
- "even": even.state() and "even" or "odd",
- "name": name,
- "value": value,
- }
- )
+ value = tags.i("(access forbidden)")
+ yield tag.clone().fillSlots(
+ even=even, name=name, value=value,
)
+ return whenAllProperties
+ return whenPropertiesListed
- output.append("</div>")
- return "".join(output)
- d = DeferredList(ds)
- d.addCallback(gotValues)
- return d
- qnames = (yield self.listProperties(request))
- result = (yield gotProperties(qnames))
- returnValue(result)
+class DirectoryRenderingMixIn(object):
+ def renderDirectory(self, request):
+ """
+ Render a directory listing.
+ """
+ def gotBody(output):
+ mime_params = {"charset": "utf-8"}
+ response = Response(200, {}, output)
+ response.headers.setHeader(
+ "content-type",
+ MimeType("text", "html", mime_params)
+ )
+ return response
+ return flattenString(request, self.htmlElement()).addCallback(gotBody)
+
+
+ def htmlElement(self):
+ """
+ Create a L{DirectoryElement} or appropriate subclass for rendering this
+ resource.
+ """
+ return DirectoryElement(self)
+
+
def getChildDirectoryEntry(self, child, name, request):
def orNone(value, default="?", f=None):
if value is None:
@@ -628,7 +596,7 @@
return f(value)
else:
return value
-
+
url = urllib.quote(name, '/')
if isinstance(child, DAVResource) and child.isCollection():
url += "/"
@@ -666,9 +634,8 @@
rtypes.append(rtype.name)
if rtypes:
contentType = "(%s)" % (", ".join(rtypes),)
-
- return ((
+ return (
url,
name,
orNone(size),
@@ -676,11 +643,12 @@
lastModified,
default="",
f=lambda t: time.strftime("%Y-%b-%d %H:%M", time.localtime(t))
- ),
- contentType,
- ))
+ ),
+ contentType,
+ )
+
def updateCacheTokenOnCallback(f):
def wrapper(self, *args, **kwargs):
if hasattr(self, "cacheNotifier"):
Modified: CalendarServer/trunk/twistedcaldav/test/test_extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_extensions.py 2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/test/test_extensions.py 2011-09-13 19:14:09 UTC (rev 8092)
@@ -105,7 +105,8 @@
"""
@inlineCallbacks
- def doDirectoryTest(self, addedNames, modify=lambda x: None, expectedNames=None):
+ def doDirectoryTest(self, addedNames, modify=lambda x: None,
+ expectedNames=None):
"""
Do a test of a L{DAVFile} pointed at a directory, verifying that files
existing with the given names will be faithfully 'played back' via HTML
@@ -119,9 +120,8 @@
fp.child(sampleName).touch()
df = DAVFile(fp)
modify(df)
- responseXML = browserHTML2ETree(
- (yield df.render(SimpleFakeRequest('/'))).stream.read()
- )
+ responseText = (yield df.render(SimpleFakeRequest('/'))).stream.read()
+ responseXML = browserHTML2ETree(responseText)
names = set([element.text.encode("utf-8")
for element in responseXML.findall(".//a")])
self.assertEquals(set(expectedNames), names)
@@ -185,7 +185,16 @@
[nonASCIIFilename.encode("utf-8")])
+ def test_quotedCharacters(self):
+ """
+ Filenames might contain < or > characters, which need to be quoted in
+ HTML.
+ """
+ return self.doDirectoryTest([u'<a>.txt', u'<script>.html',
+ u'<style>.xml'])
+
+
class ChildTraversalTests(TestCase):
def test_makeChildDeferred(self):
"""
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110913/9ca95ee3/attachment-0001.html>
More information about the calendarserver-changes
mailing list