Code Repositories xandikos / a6e2ed5
Move methods out of WebDAVApp. Jelmer Vernooń≥ 2 years ago
1 changed file(s) with 373 addition(s) and 348 deletion(s). Raw diff Collapse all Expand all
12491249 return et
12501250
12511251
1252 def do_DELETE(self, environ, start_response):
1253 unused_href, path, r = self._get_resource_from_environ(environ)
1254 if r is None:
1255 return _send_not_found(environ, start_response)
1256 container_path, item_name = posixpath.split(path)
1257 pr = self.backend.get_resource(container_path)
1258 if pr is None:
1259 return _send_not_found(environ, start_response)
1260 current_etag = r.get_etag()
1261 if_match = environ.get('HTTP_IF_MATCH', None)
1262 if if_match is not None and not etag_matches(if_match, current_etag):
1263 start_response('412 Precondition Failed', [])
1264 return []
1265 pr.delete_member(item_name, current_etag)
1266 start_response('204 No Content', [])
1267 return []
1268
1269
1270 def do_POST(self, environ, start_response):
1271 # see RFC5995
1272 new_contents = _readBody(environ)
1273 unused_href, path, r = self._get_resource_from_environ(environ)
1274 if r is None:
1275 return _send_not_found(environ, start_response)
1276 if COLLECTION_RESOURCE_TYPE not in r.resource_types:
1277 start_response('405 Method Not Allowed', [])
1278 return []
1279 content_type = environ['CONTENT_TYPE'].split(';')[0]
1280 try:
1281 (name, etag) = r.create_member(None, new_contents, content_type)
1282 except PreconditionFailure as e:
1283 return _send_simple_dav_error(
1284 environ, start_response, '412 Precondition Failed',
1285 error=ET.Element(e.precondition),
1286 description=e.description)
1287 href = (
1288 environ['SCRIPT_NAME'] +
1289 urllib.parse.urljoin(ensure_trailing_slash(path), name)
1290 )
1291 start_response('200 OK', [('Location', href)])
1292 return []
1293
1294
1295 def do_PUT(self, environ, start_response):
1296 new_contents = _readBody(environ)
1297 unused_href, path, r = self._get_resource_from_environ(environ)
1298 if r is not None:
1299 current_etag = r.get_etag()
1300 else:
1301 current_etag = None
1302 if_match = environ.get('HTTP_IF_MATCH', None)
1303 if if_match is not None and not etag_matches(if_match, current_etag):
1304 start_response('412 Precondition Failed', [])
1305 return []
1306 if_none_match = environ.get('HTTP_IF_NONE_MATCH', None)
1307 if if_none_match and etag_matches(if_none_match, current_etag):
1308 start_response('412 Precondition Failed', [])
1309 return []
1310 if r is not None:
1311 try:
1312 new_etag = r.set_body(new_contents, current_etag)
1313 except PreconditionFailure as e:
1314 return _send_simple_dav_error(
1315 environ, start_response, '412 Precondition Failed',
1316 error=ET.Element(e.precondition),
1317 description=e.description)
1318 except NotImplementedError:
1319 return _send_method_not_allowed(
1320 environ, start_response,
1321 self._get_allowed_methods(environ))
1322 else:
1323 start_response('204 No Content', [
1324 ('ETag', new_etag)])
1325 return []
1326 content_type = environ.get('CONTENT_TYPE')
1327 container_path, name = posixpath.split(path)
1328 r = self.backend.get_resource(container_path)
1329 if r is not None:
1330 if COLLECTION_RESOURCE_TYPE not in r.resource_types:
1331 start_response('405 Method Not Allowed', [])
1332 return []
1333 try:
1334 (new_name, new_etag) = r.create_member(
1335 name, new_contents, content_type)
1336 except PreconditionFailure as e:
1337 return _send_simple_dav_error(
1338 environ, start_response, '412 Precondition Failed',
1339 error=ET.Element(e.precondition),
1340 description=e.description)
1341 start_response('201 Created', [
1342 ('ETag', new_etag)])
1343 return []
1344 return _send_not_found(environ, start_response)
1345
1346
1347 def do_REPORT(self, environ, start_response):
1348 # See https://tools.ietf.org/html/rfc3253, section 3.6
1349 base_href, unused_path, r = self._get_resource_from_environ(environ)
1350 if r is None:
1351 return _send_not_found(environ, start_response)
1352 depth = environ.get("HTTP_DEPTH", "0")
1353 et = _readXmlBody(environ, None)
1354 try:
1355 reporter = self.reporters[et.tag]
1356 except KeyError:
1357 logging.warning('Client requested unknown REPORT %s', et.tag)
1358 return _send_simple_dav_error(
1359 environ, start_response,
1360 '403 Forbidden', error=ET.Element('{DAV:}supported-report'),
1361 description=('Unknown report %s.' % et.tag)
1362 )
1363 if not reporter.supported_on(r):
1364 return _send_simple_dav_error(
1365 environ, start_response,
1366 '403 Forbidden', error=ET.Element('{DAV:}supported-report'),
1367 description=('Report %s not supported on resource.' % et.tag)
1368 )
1369 return reporter.report(
1370 environ, start_response, et,
1371 functools.partial(
1372 _get_resources_by_hrefs, self.backend, environ),
1373 self.properties, base_href, r, depth)
1374
1375
1376 @multistatus
1377 def do_PROPFIND(self, environ):
1378 base_href, unused_path, base_resource = (
1379 self._get_resource_from_environ(environ))
1380 if base_resource is None:
1381 return Status(request_uri(environ), '404 Not Found')
1382 # Default depth is infinity, per RFC2518
1383 depth = environ.get("HTTP_DEPTH", "infinity")
1384 if (
1385 'CONTENT_TYPE' not in environ and
1386 environ.get('CONTENT_LENGTH') == '0'
1387 ):
1388 requested = ET.Element('{DAV:}allprop')
1389 else:
1390 et = _readXmlBody(environ, '{DAV:}propfind')
1391 try:
1392 [requested] = et
1393 except ValueError:
1394 raise BadRequestError(
1395 'Received more than one element in propfind.')
1396 if requested.tag == '{DAV:}prop':
1397 ret = []
1398 for href, resource in traverse_resource(
1399 base_resource, base_href, depth):
1400 propstat = get_properties(
1401 href, resource, self.properties, requested)
1402 ret.append(Status(href, '200 OK', propstat=list(propstat)))
1403 # By my reading of the WebDAV RFC, it should be legal to return
1404 # '200 OK' here if Depth=0, but the RFC is not super clear and
1405 # some clients don't seem to like it .
1406 return ret
1407 elif requested.tag == '{DAV:}allprop':
1408 ret = []
1409 for href, resource in traverse_resource(
1410 base_resource, base_href, depth):
1411 propstat = []
1412 for name in self.properties:
1413 ps = get_property(href, resource, self.properties, name)
1414 if ps.statuscode == '200 OK':
1415 propstat.append(ps)
1416 ret.append(Status(href, '200 OK', propstat=propstat))
1417 return ret
1418 elif requested.tag == '{DAV:}propname':
1419 ret = []
1420 for href, resource in traverse_resource(
1421 base_resource, base_href, depth):
1422 propstat = []
1423 for name, prop in self.properties.items():
1424 if prop.is_set(href, resource):
1425 propstat.append(
1426 PropStatus('200 OK', None, ET.Element(name)))
1427 ret.append(Status(href, '200 OK', propstat=propstat))
1428 return ret
1429 else:
1430 raise BadRequestError('Expected prop/allprop/propname tag, got ' +
1431 requested.tag)
1432
1433
1434 @multistatus
1435 def do_PROPPATCH(self, environ):
1436 href, unused_path, resource = self._get_resource_from_environ(environ)
1437 if resource is None:
1438 return Status(request_uri(environ), '404 Not Found')
1439 et = _readXmlBody(environ, '{DAV:}propertyupdate')
1440 propstat = []
1441 for el in et:
1442 if el.tag not in ('{DAV:}set', '{DAV:}remove'):
1443 raise BadRequestError('Unknown tag %s in propertyupdate'
1444 % el.tag)
1445 propstat.extend(apply_modify_prop(el, href, resource,
1446 self.properties))
1447 return [Status(request_uri(environ), propstat=propstat)]
1448
1449
1450 # TODO(jelmer): This should really live in xandikos.caldav
1451 def do_MKCALENDAR(self, environ, start_response):
1452 try:
1453 content_type = environ['CONTENT_TYPE']
1454 except KeyError:
1455 base_content_type = None
1456 else:
1457 base_content_type, params = parse_type(content_type)
1458 if base_content_type not in (
1459 'text/xml', 'application/xml', None, 'text/plain'
1460 ):
1461 raise UnsupportedMediaType(content_type)
1462 href, path, resource = self._get_resource_from_environ(environ)
1463 if resource is not None:
1464 start_response('405 Method Not Allowed', [])
1465 return []
1466 try:
1467 resource = self.backend.create_collection(path)
1468 except FileNotFoundError:
1469 start_response('409 Conflict', [])
1470 return []
1471 el = ET.Element('{DAV:}resourcetype')
1472 self.properties['{DAV:}resourcetype'].get_value(href, resource, el)
1473 ET.SubElement(el, '{urn:ietf:params:xml:ns:caldav}calendar')
1474 self.properties['{DAV:}resourcetype'].set_value(href, resource, el)
1475 if base_content_type in ('text/xml', 'application/xml'):
1476 et = _readXmlBody(environ, '{DAV:}mkcalendar')
1477 propstat = []
1478 for el in et:
1479 if el.tag != '{DAV:}set':
1480 raise BadRequestError('Unknown tag %s in mkcalendar'
1481 % el.tag)
1482 propstat.extend(apply_modify_prop(el, href, resource,
1483 self.properties))
1484 ret = ET.Element('{DAV:}mkcalendar-response')
1485 for propstat_el in propstat_as_xml(propstat):
1486 ret.append(propstat_el)
1487 return _send_xml_response(start_response, '201 Created',
1488 ret, DEFAULT_ENCODING)
1489 else:
1490 start_response('201 Created', [])
1491 return []
1492
1493
1494 def do_MKCOL(self, environ, start_response):
1495 try:
1496 content_type = environ['CONTENT_TYPE']
1497 except KeyError:
1498 base_content_type = None
1499 else:
1500 base_content_type, params = parse_type(content_type)
1501 if base_content_type not in (
1502 'text/plain', 'text/xml', 'application/xml', None
1503 ):
1504 raise UnsupportedMediaType(base_content_type)
1505 href, path, resource = self._get_resource_from_environ(environ)
1506 if resource is not None:
1507 start_response('405 Method Not Allowed', [])
1508 return []
1509 try:
1510 resource = self.backend.create_collection(path)
1511 except FileNotFoundError:
1512 start_response('409 Conflict', [])
1513 return []
1514 if base_content_type in ('text/xml', 'application/xml'):
1515 # Extended MKCOL (RFC5689)
1516 et = _readXmlBody(environ, '{DAV:}mkcol')
1517 propstat = []
1518 for el in et:
1519 if el.tag != '{DAV:}set':
1520 raise BadRequestError('Unknown tag %s in mkcol' % el.tag)
1521 propstat.extend(apply_modify_prop(el, href, resource,
1522 self.properties))
1523 ret = ET.Element('{DAV:}mkcol-response')
1524 for propstat_el in propstat_as_xml(propstat):
1525 ret.append(propstat_el)
1526 return _send_xml_response(start_response, '201 Created', ret,
1527 DEFAULT_ENCODING)
1528 else:
1529 start_response('201 Created', [])
1530 return []
1531
1532
1533 def do_OPTIONS(self, environ, start_response):
1534 headers = []
1535 if environ['PATH_INFO'] != '*':
1536 unused_href, unused_path, r = (
1537 self._get_resource_from_environ(environ))
1538 if r is None:
1539 return _send_not_found(environ, start_response)
1540 dav_features = self._get_dav_features(r)
1541 headers.append(('DAV', ', '.join(dav_features)))
1542 allowed_methods = self._get_allowed_methods(environ)
1543 headers.append(('Allow', ', '.join(allowed_methods)))
1544
1545 # RFC7231 requires that if there is no response body,
1546 # Content-Length: 0 must be sent. This implies that there is
1547 # content (albeit empty), and thus a 204 is not a valid reply.
1548 # Thunderbird also fails if a 204 is sent rather than a 200.
1549 start_response('200 OK', headers + [
1550 ('Content-Length', '0')])
1551 return []
1552
1553
1554 def do_HEAD(self, environ, start_response):
1555 return _do_get(self, environ, start_response, send_body=False)
1556
1557
1558 def do_GET(self, environ, start_response):
1559 return _do_get(self, environ, start_response, send_body=True)
1560
1561
1562 def _do_get(self, environ, start_response, send_body):
1563 unused_href, unused_path, r = self._get_resource_from_environ(environ)
1564 if r is None:
1565 return _send_not_found(environ, start_response)
1566 accept_content_types = parse_accept_header(
1567 environ.get('HTTP_ACCEPT', '*/*'))
1568 accept_content_languages = parse_accept_header(
1569 environ.get('HTTP_ACCEPT_LANGUAGES', '*'))
1570
1571 (
1572 body,
1573 content_length,
1574 current_etag,
1575 content_type,
1576 content_languages
1577 ) = r.render(accept_content_types, accept_content_languages)
1578
1579 if_none_match = environ.get('HTTP_IF_NONE_MATCH', None)
1580 if (
1581 if_none_match and current_etag is not None and
1582 etag_matches(if_none_match, current_etag)
1583 ):
1584 start_response('304 Not Modified', [])
1585 return []
1586 headers = [
1587 ('Content-Length', str(content_length)),
1588 ]
1589 if current_etag is not None:
1590 headers.append(('ETag', current_etag))
1591 if content_type is not None:
1592 headers.append(('Content-Type', content_type))
1593 try:
1594 last_modified = r.get_last_modified()
1595 except KeyError:
1596 pass
1597 else:
1598 headers.append(('Last-Modified', last_modified))
1599 if content_languages is not None:
1600 headers.append(('Content-Language', ', '.join(content_languages)))
1601 start_response('200 OK', headers)
1602 if send_body:
1603 return body
1604 else:
1605 return []
1606
1607
12521608 class WebDAVApp(object):
12531609 """A wsgi App that provides a WebDAV server.
12541610
12611617 self.backend = backend
12621618 self.properties = {}
12631619 self.reporters = {}
1620 self.methods = {
1621 'DELETE': do_DELETE,
1622 'POST': do_POST,
1623 'PUT': do_PUT,
1624 'REPORT': do_REPORT,
1625 'PROPFIND': do_PROPFIND,
1626 'PROPPATCH': do_PROPPATCH,
1627 'MKCALENDAR': do_MKCALENDAR,
1628 'MKCOL': do_MKCOL,
1629 'OPTIONS': do_OPTIONS,
1630 'GET': do_GET,
1631 'HEAD': do_HEAD,
1632 }
12641633
12651634 def _get_resource_from_environ(self, environ):
12661635 path = path_from_environ(environ, 'PATH_INFO')
12831652 def _get_allowed_methods(self, environ):
12841653 """List of supported methods on this endpoint."""
12851654 # TODO(jelmer): Look up resource to determine supported methods.
1286 return sorted([n[3:] for n in dir(self) if n.startswith('do_')])
1287
1288 def do_HEAD(self, environ, start_response):
1289 return self._do_get(environ, start_response, send_body=False)
1290
1291 def do_GET(self, environ, start_response):
1292 return self._do_get(environ, start_response, send_body=True)
1293
1294 def _do_get(self, environ, start_response, send_body):
1295 unused_href, unused_path, r = self._get_resource_from_environ(environ)
1296 if r is None:
1297 return _send_not_found(environ, start_response)
1298 accept_content_types = parse_accept_header(
1299 environ.get('HTTP_ACCEPT', '*/*'))
1300 accept_content_languages = parse_accept_header(
1301 environ.get('HTTP_ACCEPT_LANGUAGES', '*'))
1302
1303 (
1304 body,
1305 content_length,
1306 current_etag,
1307 content_type,
1308 content_languages
1309 ) = r.render(accept_content_types, accept_content_languages)
1310
1311 if_none_match = environ.get('HTTP_IF_NONE_MATCH', None)
1312 if (
1313 if_none_match and current_etag is not None and
1314 etag_matches(if_none_match, current_etag)
1315 ):
1316 start_response('304 Not Modified', [])
1317 return []
1318 headers = [
1319 ('Content-Length', str(content_length)),
1320 ]
1321 if current_etag is not None:
1322 headers.append(('ETag', current_etag))
1323 if content_type is not None:
1324 headers.append(('Content-Type', content_type))
1325 try:
1326 last_modified = r.get_last_modified()
1327 except KeyError:
1328 pass
1329 else:
1330 headers.append(('Last-Modified', last_modified))
1331 if content_languages is not None:
1332 headers.append(('Content-Language', ', '.join(content_languages)))
1333 start_response('200 OK', headers)
1334 if send_body:
1335 return body
1336 else:
1337 return []
1338
1339 def do_DELETE(self, environ, start_response):
1340 unused_href, path, r = self._get_resource_from_environ(environ)
1341 if r is None:
1342 return _send_not_found(environ, start_response)
1343 container_path, item_name = posixpath.split(path)
1344 pr = self.backend.get_resource(container_path)
1345 if pr is None:
1346 return _send_not_found(environ, start_response)
1347 current_etag = r.get_etag()
1348 if_match = environ.get('HTTP_IF_MATCH', None)
1349 if if_match is not None and not etag_matches(if_match, current_etag):
1350 start_response('412 Precondition Failed', [])
1351 return []
1352 pr.delete_member(item_name, current_etag)
1353 start_response('204 No Content', [])
1354 return []
1355
1356 def do_POST(self, environ, start_response):
1357 # see RFC5995
1358 new_contents = _readBody(environ)
1359 unused_href, path, r = self._get_resource_from_environ(environ)
1360 if r is None:
1361 return _send_not_found(environ, start_response)
1362 if COLLECTION_RESOURCE_TYPE not in r.resource_types:
1363 start_response('405 Method Not Allowed', [])
1364 return []
1365 content_type = environ['CONTENT_TYPE'].split(';')[0]
1366 try:
1367 (name, etag) = r.create_member(None, new_contents, content_type)
1368 except PreconditionFailure as e:
1369 return _send_simple_dav_error(
1370 environ, start_response, '412 Precondition Failed',
1371 error=ET.Element(e.precondition),
1372 description=e.description)
1373 href = (
1374 environ['SCRIPT_NAME'] +
1375 urllib.parse.urljoin(ensure_trailing_slash(path), name)
1376 )
1377 start_response('200 OK', [('Location', href)])
1378 return []
1379
1380 def do_PUT(self, environ, start_response):
1381 new_contents = _readBody(environ)
1382 unused_href, path, r = self._get_resource_from_environ(environ)
1383 if r is not None:
1384 current_etag = r.get_etag()
1385 else:
1386 current_etag = None
1387 if_match = environ.get('HTTP_IF_MATCH', None)
1388 if if_match is not None and not etag_matches(if_match, current_etag):
1389 start_response('412 Precondition Failed', [])
1390 return []
1391 if_none_match = environ.get('HTTP_IF_NONE_MATCH', None)
1392 if if_none_match and etag_matches(if_none_match, current_etag):
1393 start_response('412 Precondition Failed', [])
1394 return []
1395 if r is not None:
1396 try:
1397 new_etag = r.set_body(new_contents, current_etag)
1398 except PreconditionFailure as e:
1399 return _send_simple_dav_error(
1400 environ, start_response, '412 Precondition Failed',
1401 error=ET.Element(e.precondition),
1402 description=e.description)
1403 except NotImplementedError:
1404 return _send_method_not_allowed(
1405 environ, start_response,
1406 self._get_allowed_methods(environ))
1407 else:
1408 start_response('204 No Content', [
1409 ('ETag', new_etag)])
1410 return []
1411 content_type = environ.get('CONTENT_TYPE')
1412 container_path, name = posixpath.split(path)
1413 r = self.backend.get_resource(container_path)
1414 if r is not None:
1415 if COLLECTION_RESOURCE_TYPE not in r.resource_types:
1416 start_response('405 Method Not Allowed', [])
1417 return []
1418 try:
1419 (new_name, new_etag) = r.create_member(
1420 name, new_contents, content_type)
1421 except PreconditionFailure as e:
1422 return _send_simple_dav_error(
1423 environ, start_response, '412 Precondition Failed',
1424 error=ET.Element(e.precondition),
1425 description=e.description)
1426 start_response('201 Created', [
1427 ('ETag', new_etag)])
1428 return []
1429 return _send_not_found(environ, start_response)
1430
1431 def do_REPORT(self, environ, start_response):
1432 # See https://tools.ietf.org/html/rfc3253, section 3.6
1433 base_href, unused_path, r = self._get_resource_from_environ(environ)
1434 if r is None:
1435 return _send_not_found(environ, start_response)
1436 depth = environ.get("HTTP_DEPTH", "0")
1437 et = _readXmlBody(environ, None)
1438 try:
1439 reporter = self.reporters[et.tag]
1440 except KeyError:
1441 logging.warning('Client requested unknown REPORT %s', et.tag)
1442 return _send_simple_dav_error(
1443 environ, start_response,
1444 '403 Forbidden', error=ET.Element('{DAV:}supported-report'),
1445 description=('Unknown report %s.' % et.tag)
1446 )
1447 if not reporter.supported_on(r):
1448 return _send_simple_dav_error(
1449 environ, start_response,
1450 '403 Forbidden', error=ET.Element('{DAV:}supported-report'),
1451 description=('Report %s not supported on resource.' % et.tag)
1452 )
1453 return reporter.report(
1454 environ, start_response, et,
1455 functools.partial(
1456 _get_resources_by_hrefs, self.backend, environ),
1457 self.properties, base_href, r, depth)
1458
1459 @multistatus
1460 def do_PROPFIND(self, environ):
1461 base_href, unused_path, base_resource = (
1462 self._get_resource_from_environ(environ))
1463 if base_resource is None:
1464 return Status(request_uri(environ), '404 Not Found')
1465 # Default depth is infinity, per RFC2518
1466 depth = environ.get("HTTP_DEPTH", "infinity")
1467 if (
1468 'CONTENT_TYPE' not in environ and
1469 environ.get('CONTENT_LENGTH') == '0'
1470 ):
1471 requested = ET.Element('{DAV:}allprop')
1472 else:
1473 et = _readXmlBody(environ, '{DAV:}propfind')
1474 try:
1475 [requested] = et
1476 except ValueError:
1477 raise BadRequestError(
1478 'Received more than one element in propfind.')
1479 if requested.tag == '{DAV:}prop':
1480 ret = []
1481 for href, resource in traverse_resource(
1482 base_resource, base_href, depth):
1483 propstat = get_properties(
1484 href, resource, self.properties, requested)
1485 ret.append(Status(href, '200 OK', propstat=list(propstat)))
1486 # By my reading of the WebDAV RFC, it should be legal to return
1487 # '200 OK' here if Depth=0, but the RFC is not super clear and
1488 # some clients don't seem to like it .
1489 return ret
1490 elif requested.tag == '{DAV:}allprop':
1491 ret = []
1492 for href, resource in traverse_resource(
1493 base_resource, base_href, depth):
1494 propstat = []
1495 for name in self.properties:
1496 ps = get_property(href, resource, self.properties, name)
1497 if ps.statuscode == '200 OK':
1498 propstat.append(ps)
1499 ret.append(Status(href, '200 OK', propstat=propstat))
1500 return ret
1501 elif requested.tag == '{DAV:}propname':
1502 ret = []
1503 for href, resource in traverse_resource(
1504 base_resource, base_href, depth):
1505 propstat = []
1506 for name, prop in self.properties.items():
1507 if prop.is_set(href, resource):
1508 propstat.append(
1509 PropStatus('200 OK', None, ET.Element(name)))
1510 ret.append(Status(href, '200 OK', propstat=propstat))
1511 return ret
1512 else:
1513 raise BadRequestError('Expected prop/allprop/propname tag, got ' +
1514 requested.tag)
1515
1516 @multistatus
1517 def do_PROPPATCH(self, environ):
1518 href, unused_path, resource = self._get_resource_from_environ(environ)
1519 if resource is None:
1520 return Status(request_uri(environ), '404 Not Found')
1521 et = _readXmlBody(environ, '{DAV:}propertyupdate')
1522 propstat = []
1523 for el in et:
1524 if el.tag not in ('{DAV:}set', '{DAV:}remove'):
1525 raise BadRequestError('Unknown tag %s in propertyupdate'
1526 % el.tag)
1527 propstat.extend(apply_modify_prop(el, href, resource,
1528 self.properties))
1529 return [Status(request_uri(environ), propstat=propstat)]
1530
1531 # TODO(jelmer): This should really live in xandikos.caldav
1532 def do_MKCALENDAR(self, environ, start_response):
1533 try:
1534 content_type = environ['CONTENT_TYPE']
1535 except KeyError:
1536 base_content_type = None
1537 else:
1538 base_content_type, params = parse_type(content_type)
1539 if base_content_type not in (
1540 'text/xml', 'application/xml', None, 'text/plain'
1541 ):
1542 raise UnsupportedMediaType(content_type)
1543 href, path, resource = self._get_resource_from_environ(environ)
1544 if resource is not None:
1545 start_response('405 Method Not Allowed', [])
1546 return []
1547 try:
1548 resource = self.backend.create_collection(path)
1549 except FileNotFoundError:
1550 start_response('409 Conflict', [])
1551 return []
1552 el = ET.Element('{DAV:}resourcetype')
1553 self.properties['{DAV:}resourcetype'].get_value(href, resource, el)
1554 ET.SubElement(el, '{urn:ietf:params:xml:ns:caldav}calendar')
1555 self.properties['{DAV:}resourcetype'].set_value(href, resource, el)
1556 if base_content_type in ('text/xml', 'application/xml'):
1557 et = _readXmlBody(environ, '{DAV:}mkcalendar')
1558 propstat = []
1559 for el in et:
1560 if el.tag != '{DAV:}set':
1561 raise BadRequestError('Unknown tag %s in mkcalendar'
1562 % el.tag)
1563 propstat.extend(apply_modify_prop(el, href, resource,
1564 self.properties))
1565 ret = ET.Element('{DAV:}mkcalendar-response')
1566 for propstat_el in propstat_as_xml(propstat):
1567 ret.append(propstat_el)
1568 return _send_xml_response(start_response, '201 Created',
1569 ret, DEFAULT_ENCODING)
1570 else:
1571 start_response('201 Created', [])
1572 return []
1573
1574 def do_MKCOL(self, environ, start_response):
1575 try:
1576 content_type = environ['CONTENT_TYPE']
1577 except KeyError:
1578 base_content_type = None
1579 else:
1580 base_content_type, params = parse_type(content_type)
1581 if base_content_type not in (
1582 'text/plain', 'text/xml', 'application/xml', None
1583 ):
1584 raise UnsupportedMediaType(base_content_type)
1585 href, path, resource = self._get_resource_from_environ(environ)
1586 if resource is not None:
1587 start_response('405 Method Not Allowed', [])
1588 return []
1589 try:
1590 resource = self.backend.create_collection(path)
1591 except FileNotFoundError:
1592 start_response('409 Conflict', [])
1593 return []
1594 if base_content_type in ('text/xml', 'application/xml'):
1595 # Extended MKCOL (RFC5689)
1596 et = _readXmlBody(environ, '{DAV:}mkcol')
1597 propstat = []
1598 for el in et:
1599 if el.tag != '{DAV:}set':
1600 raise BadRequestError('Unknown tag %s in mkcol' % el.tag)
1601 propstat.extend(apply_modify_prop(el, href, resource,
1602 self.properties))
1603 ret = ET.Element('{DAV:}mkcol-response')
1604 for propstat_el in propstat_as_xml(propstat):
1605 ret.append(propstat_el)
1606 return _send_xml_response(start_response, '201 Created', ret,
1607 DEFAULT_ENCODING)
1608 else:
1609 start_response('201 Created', [])
1610 return []
1611
1612 def do_OPTIONS(self, environ, start_response):
1613 headers = []
1614 if environ['PATH_INFO'] != '*':
1615 unused_href, unused_path, r = (
1616 self._get_resource_from_environ(environ))
1617 if r is None:
1618 return _send_not_found(environ, start_response)
1619 dav_features = self._get_dav_features(r)
1620 headers.append(('DAV', ', '.join(dav_features)))
1621 allowed_methods = self._get_allowed_methods(environ)
1622 headers.append(('Allow', ', '.join(allowed_methods)))
1623
1624 # RFC7231 requires that if there is no response body,
1625 # Content-Length: 0 must be sent. This implies that there is
1626 # content (albeit empty), and thus a 204 is not a valid reply.
1627 # Thunderbird also fails if a 204 is sent rather than a 200.
1628 start_response('200 OK', headers + [
1629 ('Content-Length', '0')])
1630 return []
1655 return sorted(self.methods.keys())
16311656
16321657 def __call__(self, environ, start_response):
16331658 if environ.get('HTTP_EXPECT', '') != '':
16351660 return []
16361661 method = environ['REQUEST_METHOD']
16371662 try:
1638 do = getattr(self, 'do_' + method)
1639 except AttributeError as e:
1663 do = self.methods[method]
1664 except KeyError as e:
16401665 return _send_method_not_allowed(environ, start_response,
16411666 self._get_allowed_methods(environ))
16421667 try:
1643 return do(environ, start_response)
1668 return do(self, environ, start_response)
16441669 except BadRequestError as e:
16451670 start_response('400 Bad Request', [])
16461671 return [e.message.encode(DEFAULT_ENCODING)]