Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAjay Garg <ajay@activitycentral.com>2012-05-03 19:33:13 (GMT)
committer Anish Mangal <anish@activitycentral.com>2012-05-17 18:01:58 (GMT)
commite27a8a7b0bf78bb2f3bc7032d5d663b94facf18a (patch)
treee7a05ca5158a1ee04d279747e768554f6d00054e
parent8b040a7dea9e30a6b3f8fe0e062fb99dcfbc0dd1 (diff)
uy#1769: 1-to-N Feature
-rw-r--r--configure.ac4
-rw-r--r--src/Makefile.am4
-rw-r--r--src/jarabe/journal/Makefile.am5
-rw-r--r--src/jarabe/journal/journalactivity.py3
-rw-r--r--src/jarabe/journal/journaltoolbox.py7
-rw-r--r--src/jarabe/journal/listview.py9
-rw-r--r--src/jarabe/journal/model.py205
-rw-r--r--src/jarabe/journal/palettes.py79
-rw-r--r--src/jarabe/journal/volumestoolbar.py94
-rw-r--r--src/jarabe/journal/webdavmanager.py256
-rw-r--r--src/jarabe/model/buddy.py11
-rw-r--r--src/jarabe/model/neighborhood.py3
-rw-r--r--src/jarabe/view/buddymenu.py11
-rw-r--r--src/jarabe/view/palettes.py33
-rw-r--r--src/webdav/Condition.py475
-rw-r--r--src/webdav/Connection.py321
-rw-r--r--src/webdav/Constants.py199
-rw-r--r--src/webdav/Makefile.am20
-rw-r--r--src/webdav/NameCheck.py193
-rw-r--r--src/webdav/Utils.py154
-rw-r--r--src/webdav/VersionHandler.py198
-rw-r--r--src/webdav/WebdavClient.py840
-rw-r--r--src/webdav/WebdavRequests.py205
-rw-r--r--src/webdav/WebdavResponse.py525
-rw-r--r--src/webdav/__init__.py16
-rw-r--r--src/webdav/acp/Ace.py293
-rw-r--r--src/webdav/acp/AceHandler.py182
-rw-r--r--src/webdav/acp/Acl.py311
-rw-r--r--src/webdav/acp/GrantDeny.py241
-rw-r--r--src/webdav/acp/Makefile.am12
-rw-r--r--src/webdav/acp/Principal.py189
-rw-r--r--src/webdav/acp/Privilege.py125
-rw-r--r--src/webdav/acp/__init__.py33
-rw-r--r--src/webdav/davlib.py336
-rw-r--r--src/webdav/logger.py51
-rw-r--r--src/webdav/qp_xml.py240
-rw-r--r--src/webdav/uuid_.py476
37 files changed, 6340 insertions, 19 deletions
diff --git a/configure.ac b/configure.ac
index 8e6d871..e04bdec 100644
--- a/configure.ac
+++ b/configure.ac
@@ -61,8 +61,6 @@ extensions/cpsection/modemconfiguration/config.py
extensions/cpsection/Makefile
extensions/cpsection/network/Makefile
extensions/cpsection/power/Makefile
-extensions/cpsection/updater/backends/Makefile
-extensions/cpsection/updater/Makefile
extensions/deviceicon/Makefile
extensions/globalkey/Makefile
extensions/Makefile
@@ -79,6 +77,8 @@ src/jarabe/model/Makefile
src/jarabe/util/Makefile
src/jarabe/util/telepathy/Makefile
src/jarabe/view/Makefile
+src/webdav/acp/Makefile
+src/webdav/Makefile
src/Makefile
])
diff --git a/src/Makefile.am b/src/Makefile.am
index 83571a4..765da8b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1 +1,3 @@
-SUBDIRS = jarabe
+SUBDIRS = \
+ jarabe \
+ webdav
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am
index f24dcfe..8efca6d 100644
--- a/src/jarabe/journal/Makefile.am
+++ b/src/jarabe/journal/Makefile.am
@@ -15,5 +15,6 @@ sugar_PYTHON = \
model.py \
objectchooser.py \
palettes.py \
- volumestoolbar.py \
- processdialog.py
+ volumestoolbar.py \
+ processdialog.py \
+ webdavmanager.py
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index fa308cd..1b841e9 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -485,6 +485,9 @@ class JournalActivity(JournalWindow):
def is_editing_mode_present(self):
return self._editing_mode
+ def get_volumes_toolbar(self):
+ return self._volumes_toolbar
+
def get_journal():
global _journal
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index fd14826..c3614d7 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -624,6 +624,13 @@ class BatchEraseButton(ToolButton, palettes.ActionItem):
show_not_completed_ops_info=True)
self.props.tooltip = _('Erase')
+ # De-sensitize Batch-Erase button, for locally-mounted-remote-shares.
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ self.set_sensitive(False)
+
def _get_actionable_signal(self):
return 'clicked'
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index 8522dca..10f468f 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -634,6 +634,15 @@ class ListView(BaseListView):
self.emit('volume-error', message, severity)
def __icon_clicked_cb(self, cell, path):
+ # For locally-mounted remote shares, we do not want to launch
+ # by clicking on the icons.
+ # So, check if this is a part of locally-mounted-remote share,
+ # and if yes, return, without doing anything.
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ return
+
row = self.tree_view.get_model()[path]
metadata = model.get(row[ListModel.COLUMN_UID])
misc.resume(metadata)
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 83e216f..527f78d 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -20,6 +20,7 @@
import logging
import os
+import stat
import errno
import subprocess
from datetime import datetime
@@ -36,11 +37,14 @@ import gobject
import dbus
import gio
import gconf
+import string
from sugar import dispatch
from sugar import mime
from sugar import util
+from jarabe.journal.webdavmanager import get_remote_webdav_share_metadata
+
DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
@@ -425,6 +429,123 @@ class InplaceResultSet(BaseResultSet):
return
+class RemoteShareResultSet(object):
+ def __init__(self, ip_address, query):
+ self._ip_address = ip_address
+ self._file_list = []
+
+ self.ready = dispatch.Signal()
+ self.progress = dispatch.Signal()
+
+ # First time, query is none.
+ if query is None:
+ return
+
+ query_text = query.get('query', '')
+ if query_text.startswith('"') and query_text.endswith('"'):
+ self._regex = re.compile('*%s*' % query_text.strip(['"']))
+ elif query_text:
+ expression = ''
+ for word in query_text.split(' '):
+ expression += '(?=.*%s.*)' % word
+ self._regex = re.compile(expression, re.IGNORECASE)
+ else:
+ self._regex = None
+
+ if query.get('timestamp', ''):
+ self._date_start = int(query['timestamp']['start'])
+ self._date_end = int(query['timestamp']['end'])
+ else:
+ self._date_start = None
+ self._date_end = None
+
+ self._mime_types = query.get('mime_type', [])
+
+ self._sort = query.get('order_by', ['+timestamp'])[0]
+
+ def setup(self):
+ metadata_list_complete = get_remote_webdav_share_metadata(self._ip_address)
+ for metadata in metadata_list_complete:
+
+ add_to_list = False
+ if self._regex is not None:
+ for f in ['fulltext', 'title',
+ 'description', 'tags']:
+ if f in metadata and \
+ self._regex.match(metadata[f]):
+ add_to_list = True
+ break
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ add_to_list = False
+ if self._date_start is not None:
+ if metadata['timestamp'] > self._date_start:
+ add_to_list = True
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ add_to_list = False
+ if self._date_end is not None:
+ if metadata['timestamp'] < self._date_end:
+ add_to_list = True
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ add_to_list = False
+ if self._mime_types:
+ mime_type = metadata['mime_type']
+ if mime_type in self._mime_types:
+ add_to_list = True
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ # If control reaches here, the current metadata has passed
+ # out all filter-tests.
+ file_info = (metadata['timestamp'],
+ metadata['creation_time'],
+ metadata['filesize'],
+ metadata)
+ self._file_list.append(file_info)
+
+ if self._sort[1:] == 'filesize':
+ keygetter = itemgetter(2)
+ elif self._sort[1:] == 'creation_time':
+ keygetter = itemgetter(1)
+ else:
+ # timestamp
+ keygetter = itemgetter(0)
+
+ self._file_list.sort(lambda a, b: cmp(b, a),
+ key=keygetter,
+ reverse=(self._sort[0] == '-'))
+
+ self.ready.send(self)
+
+ def get_length(self):
+ return len(self._file_list)
+
+ length = property(get_length)
+
+ def seek(self, position):
+ self._position = position
+
+ def read(self):
+ modified_timestamp, creation_timestamp, filesize, metadata = self._file_list[self._position]
+ return metadata
+
+ def stop(self):
+ self._stopped = True
+
+
def _get_file_metadata(path, stat, fetch_preview=True):
"""Return the metadata from the corresponding file.
@@ -436,10 +557,16 @@ def _get_file_metadata(path, stat, fetch_preview=True):
dir_path = os.path.dirname(path)
metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview)
if metadata:
+ # For Documents/Shares/Mounted-Drives.
+ # Special case: for locally-mounted-remote-files, ensure that
+ # "metadata['filesize' is already present before-hand. This
+ # will have to be done at the time of fetching
+ # webdav-properties per resource.
if 'filesize' not in metadata:
metadata['filesize'] = stat.st_size
return metadata
+ # For Journal.
return {'uid': path,
'title': os.path.basename(path),
'timestamp': stat.st_mtime,
@@ -529,11 +656,33 @@ def find(query_, page_size):
raise ValueError('Exactly one mount point must be specified')
if mount_points[0] == '/':
+ """
+ For Journal.
+ """
return DatastoreResultSet(query, page_size)
+ elif is_mount_point_for_locally_mounted_remote_share(mount_points[0]):
+ """
+ For Locally-Mounted-Remote-Shares.
+ Regex Matching is used, to ensure that the mount-point is an
+ IP-Address.
+ """
+ return RemoteShareResultSet(mount_points[0], query)
else:
+ """
+ For Documents/Shares/Mounted-Drives.
+ """
return InplaceResultSet(query, page_size, mount_points[0])
+def is_mount_point_for_locally_mounted_remote_share(mount_point):
+ import re
+
+ pattern = '[1-9][0-9]{0,2}\.[0-9]{0,3}\.[0-9]{0,3}\.[0-9]{0,3}'
+ if re.match(pattern, mount_point) is None:
+ return False
+ return True
+
+
def _get_mount_point(path):
dir_path = os.path.dirname(path)
while dir_path:
@@ -544,14 +693,45 @@ def _get_mount_point(path):
return None
+def is_locally_mounted_remote_share(path):
+ return string.find(path, '/tmp/') == 0
+
+
+def extract_ip_address_from_locally_mounted_remote_share_path(path):
+ """
+ Path is of type ::
+
+ /tmp/127.0.0.1/webdav/a.txt
+ """
+ return path.split('/')[2]
+
+
def get(object_id):
"""Returns the metadata for an object
"""
- if os.path.exists(object_id):
- stat = os.stat(object_id)
+ if (os.path.exists(object_id) or (is_locally_mounted_remote_share(object_id))):
+ """
+ For Documents/Shares/Mounted-Drives/Locally-Mounted-Remote-Shares,
+ where ".Sugar-Metadata" folder exists.
+
+ The only thing is that, for locally-mounted-remote-shares, the
+ "file" is not physically present.
+ """
+ if os.path.exists(object_id):
+ # if the file is physically present, derive file-metadata
+ # by physical examination of the file.
+ stat = os.stat(object_id)
+ else:
+ # if the file is remote, derive file-metadata by fetching
+ # properties remotely (webdav properties).
+ stat = None
+
metadata = _get_file_metadata(object_id, stat)
metadata['mountpoint'] = _get_mount_point(object_id)
else:
+ """
+ For journal, where ".Sugar-Metadata" folder does not exists.
+ """
metadata = _get_datastore().get_properties(object_id, byte_arrays=True)
metadata['mountpoint'] = '/'
return metadata
@@ -561,9 +741,16 @@ def get_file(object_id):
"""Returns the file for an object
"""
if os.path.exists(object_id):
+ """
+ For Documents/Shares/Mounted-Drives/
+ Locally-Mounted-Remote-Shares-in-case-when-it-is-present-already.
+ """
logging.debug('get_file asked for file with path %r', object_id)
return object_id
else:
+ """
+ For Journal.
+ """
logging.debug('get_file asked for entry with id %r', object_id)
file_path = _get_datastore().get_filename(object_id)
if file_path:
@@ -754,6 +941,20 @@ def _write_entry_on_external_device(metadata, file_path):
_rename_entry_on_external_device(file_path, destination_path,
metadata_dir_path)
+ # For "Shares" folder, we need to set the permissions of the newly
+ # copied file to 0777, else it will not be accessible by "httpd"
+ # service.
+ if metadata['mountpoint'] == '/var/www/web1/web':
+ fd = os.open(destination_path, os.O_RDONLY)
+ os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+ os.close(fd)
+
+ metadata_file_path = os.path.join(metadata_dir_path, file_name + '.metadata')
+ fd = os.open(metadata_file_path, os.O_RDONLY)
+ os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+ os.close(fd)
+
+
object_id = destination_path
created.send(None, object_id=object_id)
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 740b65a..46ade98 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -29,6 +29,7 @@ import gconf
import gio
import glib
import time
+import socket
from sugar import _sugarext
@@ -45,6 +46,10 @@ from jarabe.model import mimeregistry
from jarabe.journal import misc
from jarabe.journal import model
+from webdav.Connection import WebdavError
+from jarabe.journal.webdavmanager import get_resource_by_ip_address_and_resource_key
+
+
friends_model = friends.get_model()
_copy_menu_helper = None
@@ -77,6 +82,9 @@ class ObjectPalette(Palette):
Palette.__init__(self, primary_text=title,
icon=activity_icon)
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
if misc.get_activities(metadata) or misc.is_bundle(metadata):
if metadata.get('activity_id', ''):
resume_label = _('Resume')
@@ -86,10 +94,15 @@ class ObjectPalette(Palette):
resume_with_label = _('Start with')
menu_item = MenuItem(resume_label, 'activity-start')
menu_item.connect('activate', self.__start_activate_cb)
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
menu_item = MenuItem(resume_with_label, 'activity-start')
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
+
self.menu.append(menu_item)
menu_item.show()
start_with_menu = StartWithMenu(self._metadata)
@@ -101,6 +114,7 @@ class ObjectPalette(Palette):
self.menu.append(menu_item)
menu_item.show()
+
menu_item = MenuItem(_('Copy to'))
icon = Icon(icon_name='edit-copy', xo_color=color,
icon_size=gtk.ICON_SIZE_MENU)
@@ -120,16 +134,21 @@ class ObjectPalette(Palette):
copy_menu.connect('volume-error', self.__volume_error_cb)
menu_item.set_submenu(copy_menu)
+
if self._metadata['mountpoint'] == '/':
menu_item = MenuItem(_('Duplicate'))
icon = Icon(icon_name='edit-duplicate', xo_color=color,
icon_size=gtk.ICON_SIZE_MENU)
menu_item.set_image(icon)
menu_item.connect('activate', self.__duplicate_activate_cb)
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
menu_item = MenuItem(_('Send to'), 'document-send')
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
@@ -140,14 +159,19 @@ class ObjectPalette(Palette):
if detail == True:
menu_item = MenuItem(_('View Details'), 'go-right')
menu_item.connect('activate', self.__detail_activate_cb)
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
menu_item = MenuItem(_('Erase'), 'list-remove')
menu_item.connect('activate', self.__erase_activate_cb)
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
+
def __start_activate_cb(self, menu_item):
misc.resume(self._metadata)
@@ -541,6 +565,31 @@ class ActionItem(gobject.GObject):
self._post_operate_per_metadata_per_action(metadata)
def _file_path_valid(self, metadata):
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
+ # Now, for locally mounted remote-shares, download the file.
+ # Note that, always download the file, to avoid the problems
+ # of stale-cache.
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ file_path = metadata['uid']
+ filename = os.path.basename(file_path)
+ ip_address = model.extract_ip_address_from_locally_mounted_remote_share_path(file_path)
+ resource = get_resource_by_ip_address_and_resource_key(ip_address, '/webdav/' + filename)
+ download_file_path = '/tmp/' + ip_address + '/' + filename
+ try:
+ resource.downloadFile(download_file_path)
+ return True
+ except (WebdavError, socket.error), e:
+ error_message = e
+ logging.warn(error_message)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self.emit('volume-error', error_message,
+ _('Error'))
+ return False
+
file_path = model.get_file(metadata['uid'])
if not file_path or not os.path.exists(file_path):
logging.warn('Entries without a file cannot be copied.')
@@ -708,6 +757,25 @@ class DocumentsMenu(BaseCopyMenuItem):
self._post_operate_per_metadata_per_action(metadata)
+class SharesMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Shares'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+ if not self._metadata_copy_valid(metadata,
+ '/var/www/web1/web'):
+ return False
+
+ # This is sync-operation. Call the post-operation now.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
class FriendsMenu(gtk.Menu):
__gtype_name__ = 'JournalFriendsMenu'
@@ -835,6 +903,17 @@ class CopyMenuHelper(gtk.Menu):
menu.append(documents_menu)
documents_menu.show()
+ if get_mount_point() != '/var/www/web1/web':
+ documents_menu = SharesMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ documents_menu.set_image(Icon(icon_name='emblem-neighborhood-shared',
+ icon_size=gtk.ICON_SIZE_MENU))
+ documents_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(documents_menu)
+ documents_menu.show()
+
if get_mount_point() != '/':
client = gconf.client_get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 94914e6..1f6e3ec 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -37,8 +37,10 @@ from sugar.graphics.palette import Palette
from sugar.graphics.xocolor import XoColor
from sugar import env
+from jarabe.frame.notification import NotificationIcon
from jarabe.journal import model
-from jarabe.view.palettes import JournalVolumePalette, JournalXSPalette
+from jarabe.view.palettes import JournalVolumePalette, JournalXSPalette, RemoteSharePalette
+import jarabe.frame
_JOURNAL_0_METADATA_DIR = '.olpc.store'
@@ -209,6 +211,7 @@ class VolumesToolbar(gtk.Toolbar):
def _set_up_volumes(self):
self._set_up_documents_button()
+ self._set_up_shares_button()
volume_monitor = gio.volume_monitor_get()
self._mount_added_hid = volume_monitor.connect('mount-added',
@@ -219,12 +222,11 @@ class VolumesToolbar(gtk.Toolbar):
for mount in volume_monitor.get_mounts():
self._add_button(mount)
- def _set_up_documents_button(self):
- documents_path = model.get_documents_path()
- if documents_path is not None:
- button = DocumentsButton(documents_path)
+ def _set_up_directory_button(self, dir_path, icon_name, label_text):
+ if dir_path is not None:
+ button = DirectoryButton(dir_path, icon_name)
button.props.group = self._volume_buttons[0]
- label = glib.markup_escape_text(_('Documents'))
+ label = glib.markup_escape_text(label_text)
button.set_palette(Palette(label))
button.connect('toggled', self._button_toggled_cb)
button.show()
@@ -234,6 +236,39 @@ class VolumesToolbar(gtk.Toolbar):
self._volume_buttons.append(button)
self.show()
+ def _set_up_documents_button(self):
+ documents_path = model.get_documents_path()
+ self._set_up_directory_button(documents_path,
+ 'user-documents',
+ _('Documents'))
+
+ def _set_up_shares_button(self):
+ shares_dir_path = '/var/www/web1/web'
+ self._set_up_directory_button(shares_dir_path,
+ 'emblem-neighborhood-shared',
+ _('Shares'))
+
+ def _add_remote_share_button(self, buddy):
+ button = RemoteSharesButton(buddy)
+ button.props.group = self._volume_buttons[0]
+ label = glib.markup_escape_text(_('%s\'s share') % \
+ buddy.props.nick)
+ button.set_palette(RemoteSharePalette(buddy, button))
+ button.connect('toggled', self._button_toggled_cb)
+ button.show()
+
+ position = self.get_item_index(self._volume_buttons[-1]) + 1
+ self.insert(button, position)
+ self._volume_buttons.append(button)
+ self.show()
+
+ frame = jarabe.frame.get_view()
+ notif_icon = NotificationIcon()
+ notif_icon.props.icon_name = 'emblem-neighborhood-shared'
+ notif_icon.props.xo_color = buddy.props.color
+ frame.add_notification(notif_icon,
+ gtk.CORNER_BOTTOM_RIGHT)
+
def __mount_added_cb(self, volume_monitor, mount):
self._add_button(mount)
@@ -271,8 +306,8 @@ class VolumesToolbar(gtk.Toolbar):
def __volume_error_cb(self, button, strerror, severity):
self.emit('volume-error', strerror, severity)
- def _button_toggled_cb(self, button):
- if button.props.active:
+ def _button_toggled_cb(self, button, force_toggle=False):
+ if button.props.active or force_toggle:
self.emit('volume-changed', button.mount_point)
def _unmount_activated_cb(self, menu_item, mount):
@@ -300,6 +335,19 @@ class VolumesToolbar(gtk.Toolbar):
if len(self.get_children()) < 2:
self.hide()
+ def _remove_remote_share_button(self, mount_point):
+ # Here, IP_Address is the mount_point.
+ for button in self.get_children():
+ if type(button) == RemoteSharesButton and \
+ button.mount_point == mount_point:
+ self._volume_buttons.remove(button)
+ self.remove(button)
+ self.get_children()[0].props.active = True
+
+ if len(sel.get_children()) < 2:
+ self.hide()
+ break;
+
def set_active_volume(self, mount):
button = self._get_button_for_mount(mount)
button.props.active = True
@@ -313,6 +361,12 @@ class VolumesToolbar(gtk.Toolbar):
if button.mount_point != mount_point:
button.set_sensitive(sensitive)
+ def get_journal_button(self):
+ return self._volume_buttons[0]
+
+ def get_button_toggled_cb(self):
+ return self._button_toggled_cb
+
class BaseButton(RadioToolButton):
__gsignals__ = {
@@ -427,18 +481,34 @@ class JournalButtonPalette(Palette):
{'free_space': free_space / (1024 * 1024)}
-class DocumentsButton(BaseButton):
+class DirectoryButton(BaseButton):
- def __init__(self, documents_path):
- BaseButton.__init__(self, mount_point=documents_path)
+ def __init__(self, dir_path, icon_name):
+ BaseButton.__init__(self, mount_point=dir_path)
- self.props.named_icon = 'user-documents'
+ self.props.named_icon = icon_name
client = gconf.client_get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
self.props.xo_color = color
+class RemoteSharesButton(BaseButton):
+
+ def __init__(self, buddy):
+ BaseButton.__init__(self, mount_point=buddy.props.ip_address)
+
+ self._buddy = buddy
+ self.props.named_icon = 'emblem-neighborhood-shared'
+ self.props.xo_color = buddy.props.color
+ self._buddy_ip_address = buddy.props.ip_address
+
+ def create_palette(self):
+ palette = RemoteSharePalette(self._buddy)
+ return palette
+
+
+
class XSButton(ToolButton):
def __init__(self):
ToolButton.__init__(self)
diff --git a/src/jarabe/journal/webdavmanager.py b/src/jarabe/journal/webdavmanager.py
new file mode 100644
index 0000000..6cd0713
--- /dev/null
+++ b/src/jarabe/journal/webdavmanager.py
@@ -0,0 +1,256 @@
+from gettext import gettext as _
+
+import os
+import sys
+
+import gobject
+import simplejson
+import shutil
+
+from webdav.Connection import AuthorizationError, WebdavError
+from webdav.WebdavClient import CollectionStorer
+
+def get_key_from_resource(resource):
+ return resource.path
+
+def ensure_correct_remote_webdav_hierarchy(remote_webdav_share_resources,
+ remote_webdav_share_collections):
+ pass
+ #assert len(remote_webdav_share_collections.keys()) == 1
+
+class WebDavUrlManager(gobject.GObject):
+ """
+ This class holds all data, relevant to a WebDavUrl.
+
+ One thing must be noted, that a valid WebDavUrl is the one which
+ may contain zero or more resources (files), or zero or more
+ collections (directories).
+
+ Thus, following are valid WebDavUrls ::
+
+ dav://1.2.3.4/webdav
+ dav://1.2.3.4/webdav/dir_1
+ dav://1.2.3.4/webdav/dir_1/dir_2
+
+ but following are not ::
+
+ dav://1.2.3.4/webdav/a.txt
+ dav://1.2.3.4/webdav/dir_1/b.jpg
+ dav://1.2.3.4/webdab/dir_1/dir_2/c.avi
+ """
+
+ def __init__(self, WebDavUrl, username, password):
+ self._WebDavUrl = WebDavUrl
+ self._username = username
+ self._password = password
+
+ def _get_key_from_resource(self, resource):
+ return resource.path.encode(sys.getfilesystemencoding())
+
+ def _get_number_of_collections(self):
+ return len(self._remote_webdav_share_collections)
+
+ def _get_resources_dict(self):
+ return self._remote_webdav_share_resources
+
+ def _get_collections_dict(self):
+ return self._remote_webdav_share_collections
+
+ def _get_resource_by_key(self, key):
+ return self._remote_webdav_share_resources[key]['resource']
+
+ def _get_metadata_list(self):
+ metadata_list = []
+ for key in self._remote_webdav_share_resources.keys():
+ metadata_list.append(self._remote_webdav_share_resources[key]['metadata'])
+ return metadata_list
+
+ def _get_live_properties(self, resource_key):
+ resource_container = self._remote_webdav_share_resources[resource_key]
+ return resource_container['webdav-properties']
+
+ def _set_metadata_for_resource(self, key, metadata):
+ self._remote_webdav_share_resources[key]['metadata'] = metadata
+
+ def _fetch_resources_and_collections(self):
+ webdavConnection = CollectionStorer(self._WebDavUrl, validateResourceNames=False)
+
+ authFailures = 0
+ while authFailures < 2:
+ try:
+ self._remote_webdav_share_resources = {}
+ self._remote_webdav_share_collections = {}
+
+ try:
+ self._collection_contents = webdavConnection.getCollectionContents()
+ for resource, properties in self._collection_contents:
+ try:
+ key = self._get_key_from_resource(resource)
+ selected_dict = None
+
+ if properties.getResourceType() == 'resource':
+ selected_dict = self._remote_webdav_share_resources
+ else:
+ selected_dict = self._remote_webdav_share_collections
+
+ selected_dict[key] = {}
+ selected_dict[key]['resource'] = resource
+ selected_dict[key]['webdav-properties'] = properties
+ except UnicodeEncodeError:
+ print("Cannot encode resource path or properties.")
+
+ return True
+
+ except WebdavError, e:
+ # Note that, we need to deal with all errors,
+ # except "AuthorizationError", as that is not
+ # really an error from our perspective.
+ if not type(e) == AuthorizationError:
+ from jarabe.journal.journalactivity import get_journal
+
+ error_message = e
+ get_journal()._volume_error_cb(None, error_message,_('Error'))
+
+ # Simply return, in case of connection-not-available.
+ return False
+
+ else:
+ # If this indeed is an Authorization Error,
+ # re-raise it, so that it is caught by the outer
+ # "except" block.
+ raise e
+
+
+ except AuthorizationError, e:
+ if self._username is None or self._password is None:
+ raise Exception("WebDav username or password is None. Please specify appropriate values.")
+
+ if e.authType == "Basic":
+ webdavConnection.connection.addBasicAuthorization(self._username, self._password)
+ elif e.authType == "Digest":
+ info = parseDigestAuthInfo(e.authInfo)
+ webdavConnection.connection.addDigestAuthorization(self._username, self._password, realm=info["realm"], qop=info["qop"], nonce=info["nonce"])
+ else:
+ raise
+ authFailures += 1
+
+ return False
+
+webdav_manager = {}
+
+
+def get_resource_by_ip_address_and_resource_key(ip_address, key):
+ global webdav_manager
+
+ if ip_address in webdav_manager.keys():
+ root_webdav = webdav_manager[ip_address]
+ resources_dict = root_webdav._get_resources_dict()
+ resource_dict = resources_dict[key]
+ resource = resource_dict['resource']
+
+ return resource
+
+
+def get_remote_webdav_share_metadata(ip_address):
+ protocol = 'dav://'
+
+ root_webdav_url = '/webdav'
+
+ complete_root_url = protocol + ip_address + root_webdav_url
+
+ root_webdav = WebDavUrlManager(complete_root_url, 'test', 'olpc')
+ if root_webdav._fetch_resources_and_collections() is False:
+ # Return empty metadata list.
+ return []
+
+ # Keep reference to the "WebDavUrlManager", keyed by IP-Address.
+ global webdav_manager
+ webdav_manager[ip_address] = root_webdav
+
+
+ # Assert that the number of collections is only one at this url
+ # (i.e. only ".Sugar-Metadata" is present).
+ assert root_webdav._get_number_of_collections() == 1
+
+ root_sugar_metadata_url = root_webdav_url + '/.Sugar-Metadata'
+
+ complete_root_sugar_metadata_url = protocol + ip_address + root_sugar_metadata_url
+ root_webdav_sugar_metadata = WebDavUrlManager(complete_root_sugar_metadata_url, 'test', 'olpc')
+ if root_webdav_sugar_metadata._fetch_resources_and_collections() is False:
+ # Return empty metadata list.
+ return []
+
+ # assert that the number of collections is zero at this url.
+ assert root_webdav_sugar_metadata._get_number_of_collections() == 0
+
+ # Now. associate sugar-metadata with each of the "root-webdav"
+ # resource.
+ root_webdav_resources = root_webdav._get_resources_dict()
+ root_webdav_sugar_metadata_resources = root_webdav_sugar_metadata._get_resources_dict()
+
+ # Prepare the metadata-download folder.
+ downloaded_data_root_dir = '/tmp/' + ip_address
+ downloaded_metadata_file_dir = downloaded_data_root_dir + '/.Sugar-Metadata'
+ if os.path.isdir(downloaded_data_root_dir):
+ shutil.rmtree(downloaded_data_root_dir)
+ os.makedirs(downloaded_metadata_file_dir)
+
+ for root_webdav_resource_name in root_webdav_resources.keys():
+ """
+ root_webdav_resource_name is of the type ::
+
+ /webdav/a.txt
+ """
+ split_tokens_array = root_webdav_resource_name.split('/')
+
+ # This will provide us with "a.txt"
+ basename = split_tokens_array[len(split_tokens_array) - 1]
+
+ # This will provide us with "a.txt.metadata"
+ sugar_metadata_basename = basename + '.metadata'
+
+ # Thus will provide us with "/webdav/.Sugar-Metadata/a.txt.metadata"
+ sugar_metadata_url = root_sugar_metadata_url + '/' + sugar_metadata_basename
+
+ # Ensure that "sugar_metadata_url" is present as one of the
+ # keys in "root_webdav_sugar_metadata_resources"
+ assert sugar_metadata_url in root_webdav_sugar_metadata_resources.keys()
+
+ # Now download the metadata file, read its contents, and store
+ # the metadata in memory.
+ # It is assumed that the metadata-file is small enough to be
+ # read in one call to "read".
+
+ downloaded_metadata_file_path = downloaded_metadata_file_dir + '/' + sugar_metadata_basename
+ metadata_resource = root_webdav_sugar_metadata._get_resource_by_key(sugar_metadata_url)
+ metadata_resource.downloadFile(downloaded_metadata_file_path)
+
+ file_pointer = open(downloaded_metadata_file_path)
+ metadata = eval(file_pointer.read())
+ file_pointer.close()
+
+ # Very critical.
+ # 1. CRITICAL ONE:
+ # Fill in the uid.
+ # Note that the file is not physically present.
+ metadata['uid'] = downloaded_data_root_dir + '/' + basename
+
+ # 2. CRITICAL TWO:
+ # Fill in the properties, that can only be done by reading
+ # in the webdav-properties.
+ live_properties = root_webdav._get_live_properties(root_webdav_resource_name)
+ metadata['filesize'] = live_properties.getContentLength()
+ metadata['timestamp'] = live_properties.getLastModified()
+ metadata['creation_time'] = live_properties.getCreationDate()
+
+ # Now, write this to the metadata-file, so that
+ # webdav-properties get gelled into sugar-metadata.
+
+ file_pointer = open(downloaded_metadata_file_path, 'w')
+ file_pointer.write(simplejson.dumps(metadata))
+ file_pointer.close()
+
+ root_webdav._set_metadata_for_resource(root_webdav_resource_name,
+ metadata)
+
+ return root_webdav._get_metadata_list()
diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py
index 8f17d7e..c088aa9 100644
--- a/src/jarabe/model/buddy.py
+++ b/src/jarabe/model/buddy.py
@@ -43,6 +43,7 @@ class BaseBuddyModel(gobject.GObject):
self._color = None
self._tags = None
self._current_activity = None
+ self._ip_address = None
gobject.GObject.__init__(self, **kwargs)
@@ -87,6 +88,16 @@ class BaseBuddyModel(gobject.GObject):
getter=get_current_activity,
setter=set_current_activity)
+ def get_ip_address(self):
+ return self._ip_address
+
+ def set_ip_address(self, ip_address):
+ self._ip_address = ip_address
+
+ ip_address = gobject.property(type=object,
+ getter=get_ip_address,
+ setter=set_ip_address)
+
def is_owner(self):
raise NotImplementedError
diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py
index 4528a15..39f648e 100644
--- a/src/jarabe/model/neighborhood.py
+++ b/src/jarabe/model/neighborhood.py
@@ -930,6 +930,9 @@ class Neighborhood(gobject.GObject):
if 'key' in properties:
buddy.props.key = properties['key']
+ if 'ip4-address' in properties:
+ buddy.props.ip_address = properties['ip4-address']
+
nick_key = CONNECTION_INTERFACE_ALIASING + '/alias'
if nick_key in properties:
buddy.props.nick = properties[nick_key]
diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
index de5a772..dfbcfa3 100644
--- a/src/jarabe/view/buddymenu.py
+++ b/src/jarabe/view/buddymenu.py
@@ -73,6 +73,12 @@ class BuddyMenu(Palette):
self.menu.append(menu_item)
menu_item.show()
+ access_buddy_remote_share_menu_item = MenuItem(_('Access Share'), 'list-add')
+ access_buddy_remote_share_menu_item.connect('activate',
+ self._access_share_cb)
+ self.menu.append(access_buddy_remote_share_menu_item)
+ access_buddy_remote_share_menu_item.show()
+
self._invite_menu = MenuItem('')
self._invite_menu.connect('activate', self._invite_friend_cb)
self.menu.append(self._invite_menu)
@@ -83,6 +89,11 @@ class BuddyMenu(Palette):
activity = home_model.get_active_activity()
self._update_invite_menu(activity)
+ def _access_share_cb(self, menuitem):
+ from jarabe.journal.journalactivity import get_journal
+ volumes_toolbar = get_journal().get_volumes_toolbar()
+ volumes_toolbar._add_remote_share_button(self._buddy)
+
def _add_my_items(self):
item = MenuItem(_('Shutdown'), 'system-shutdown')
item.connect('activate', self.__shutdown_activate_cb)
diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py
index 7a17f32..3b26faf 100644
--- a/src/jarabe/view/palettes.py
+++ b/src/jarabe/view/palettes.py
@@ -336,3 +336,36 @@ class JournalXSPalette(Palette):
def __journal_restore_activate_cb(self, menu_item, xs_hostname):
dialog = XSRestoreDialog(xs_hostname)
dialog.show()
+
+
+class RemoteSharePalette(Palette):
+ def __init__(self, buddy, button):
+ Palette.__init__(self, label=('%s\'s share' % buddy.props.nick))
+ self._buddy = buddy
+ self._button = button
+
+ self.props.secondary_text = glib.markup_escape_text(buddy.props.ip_address)
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self.connect('popup', self.__popup_cb)
+
+ menu_item = MenuItem(pgettext('Share', 'Unmount'))
+
+ icon = Icon(icon_name='media-eject', icon_size=gtk.ICON_SIZE_MENU)
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__unmount_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __unmount_activate_cb(self, menu_item):
+ from jarabe.journal.journalactivity import get_journal
+ singleton_volumes_toolbar = get_journal().get_volumes_toolbar()
+ singleton_volumes_toolbar._remove_remote_share_button(self._buddy.props.ip_address)
+
+ def __popup_cb(self, palette):
+ pass
diff --git a/src/webdav/Condition.py b/src/webdav/Condition.py
new file mode 100644
index 0000000..76acf94
--- /dev/null
+++ b/src/webdav/Condition.py
@@ -0,0 +1,475 @@
+# pylint: disable-msg=R0921,W0704,R0901,W0511,R0201
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+This module contains classes for creating a search condition according to the DASL draft.
+The classes will output the WHERE part of a search request to a WebDAV server.
+
+Instances of the classes defined in this module form a tree data structure which represents
+a search condition. This tree is made up of AND-nodes, OR-nodes, Operator- and comparison-
+nodes and from property (i.e. variable) and constant leaf nodes.
+"""
+
+
+import types
+from time import strftime
+from calendar import timegm
+from rfc822 import formatdate
+
+from webdav.Constants import NS_DAV, PROP_LAST_MODIFIED, DATE_FORMAT_ISO8601
+
+
+__version__ = "$Revision$"[11:-2]
+
+
+class ConditionTerm(object):
+ """
+ This is the abstact base class for all condition terms.
+ """
+ def __init__(self):
+ pass
+
+ def toXML(self):
+ """
+ Abstact method which return a XML string which can be passed to a WebDAV server
+ for a search condition.
+ """
+ raise NotImplementedError
+
+ # start Tamino workaround for missing like-op:
+ def postFilter(self, resultSet):
+ """
+ Abstact method for temporary workaround for Tamino's absense of the like-operator.
+ This method shall filter the given result set for those resources which match
+ all Contains-trems.
+ """
+ return resultSet
+ # end of workaround
+
+
+class IsCollectionTerm(ConditionTerm):
+ """ Leaf condition. Checks if the matching resources are collections. """
+
+ def __init__(self):
+ """ Constructor. """
+
+ ConditionTerm.__init__(self)
+
+ def toXML(self):
+ """
+ Returns XML encoding.
+ """
+
+ return "<D:is-collection/>"
+
+
+class Literal(ConditionTerm):
+ """
+ A leaf class for condition expressions representing a constant value.
+ """
+ def __init__(self, literal):
+ ConditionTerm.__init__(self)
+ self.literal = literal
+
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:literal>" + self.literal + "</D:literal>"
+
+
+class UnaryTerm(ConditionTerm):
+ """
+ Base class of all nodes with a single child node.
+ """
+ def __init__(self, child):
+ ConditionTerm.__init__(self)
+ self.child = child
+
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return self.child.toXML()
+
+
+class BinaryTerm(ConditionTerm):
+ """
+ Base class of all nodes with two child nodes
+ """
+ def __init__(self, left, right):
+ ConditionTerm.__init__(self)
+ self.left = left
+ self.right = right
+
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return self.left.toXML() + self.right.toXML()
+
+class TupleTerm(ConditionTerm):
+ """
+ Base class of all nodes with multiple single child nodes.
+ """
+ def __init__(self, terms):
+ ConditionTerm.__init__(self)
+ self.terms = terms
+
+ def addTerm(self, term):
+ '''
+ Removes a term.
+
+ @param term: term to add
+ '''
+ self.terms.append(term)
+
+ def removeTerm(self, term):
+ '''
+ Adds a term.
+
+ @param term: term to remove
+ '''
+ try:
+ self.terms.remove(term)
+ except ValueError:
+ pass
+
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ result = ""
+ for term in self.terms:
+ result += term.toXML()
+ return result
+
+
+class AndTerm(TupleTerm):
+ """
+ This class represents and logical AND-condition with multiple sub terms.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:and>" + TupleTerm.toXML(self) + "</D:and>"
+
+ # start Tamino workaround for missing like-op:
+ def postFilter(self, resultSet):
+ '''
+ Filters the given result set. This is a TAMINO WebDav server workaround
+ for the missing 'like' tag.
+
+ @param resultSet: the result set that needs to be filtered.
+ '''
+ for term in self.terms:
+ filtered = term.postFilter(resultSet)
+ resultSet = filtered
+ return resultSet
+ # end of workaround
+
+class OrTerm(TupleTerm):
+ """
+ This class represents and logical OR-condition with multiple sub terms.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:or>" + TupleTerm.toXML(self) + "</D:or>"
+
+ # start Tamino workaround for missing like-op:
+ def postFilter(self, resultSet):
+ '''
+ Filters the given result set. This is a TAMINO WebDav server workaround
+ for the missing 'like' tag.
+
+ @param resultSet: the result set that needs to be filtered.
+ '''
+ raise NotImplementedError
+
+
+class NotTerm(UnaryTerm):
+ """
+ This class represents a negation term for the contained sub term.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ # start Tamino workaround for missing like-op:
+ if isinstance(self.child, ContainsTerm):
+ return ""
+ # end of workaround
+ return "<D:not>" + UnaryTerm.toXML(self) + "</D:not>"
+
+ # start Tamino workaround for missing like-op:
+ def postFilter(self, resultSet):
+ '''
+ Filters the given result set. This is a TAMINO WebDav server workaround
+ for the missing 'like' tag.
+
+ @param resultSet: the result set that needs to be filtered.
+ '''
+ if isinstance(self.child, ContainsTerm):
+ self.child.negate = 1
+ # TODO: pass on filter
+ return self.child.postFilter(resultSet)
+
+
+class ExistsTerm(UnaryTerm):
+ """
+ Nodes of this class must have a single child with tuple type (of len 2) representing a
+ WebDAV property.
+ This leaf term evaluates to true if the (child) property exists.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return '<D:is-defined><D:prop xmlns="%s"><%s' % self.child + ' /></D:prop></D:is-defined>'
+
+class ContentContainsTerm(UnaryTerm):
+ """
+ This class can be used to search for a given phrase in resources' contents.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:contains>" + self.child + "</D:contains>"
+
+
+
+class BinaryRelationTerm(BinaryTerm):
+ """
+ This is the abstact base class for the following relation operands.
+ """
+ def __init__(self, left, right):
+ BinaryTerm.__init__(self, left, right)
+ if isinstance(self.left, types.StringType): # Must be namespace + name pair
+ self.left = ('DAV:', self.left)
+ if not isinstance(self.right, Literal):
+ self.right = Literal(self.right) # Must be Literal instance
+
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ ## TODO: extract name space and create shortcut for left element
+ return '<D:prop xmlns="%s"><%s /></D:prop>' % self.left + self.right.toXML()
+
+
+class StringRelationTerm(BinaryRelationTerm):
+ """
+ This is the abstact base class for the following string relation classes.
+ """
+ def __init__(self, left, right, caseless=None):
+ """
+ @param left: webdav property (namespace, name)
+ @param right: string/unicode literal
+ qparam caseless: 1 for case sensitive comparison
+ """
+ BinaryRelationTerm.__init__(self, left, Literal(right))
+ self.caseless = caseless
+ if self.caseless:
+ self.attrCaseless = "yes"
+ else:
+ self.attrCaseless = "no"
+
+class NumberRelationTerm(BinaryRelationTerm):
+ """
+ This is the abstact base class for the following number comparison classes.
+ """
+ def __init__(self, left, right):
+ """
+ @param left: webdav property (namespace, name)
+ @param right: constant number
+ """
+ ## TODO: implemet typed literal
+ BinaryRelationTerm.__init__(self, left, Literal(str(right)))
+
+class DateRelationTerm(BinaryRelationTerm):
+ """
+ This is the abstact base class for the following date comparison classes.
+ """
+ def __init__(self, left, right):
+ """
+ @param left: webdav property (namespace, name)
+ @param right: string literal containing a date in ISO8601 format
+ """
+ ## TODO: implemet typed literal
+ assert len(right) == 9, "No time is specified for literal: " + str(right)
+ BinaryRelationTerm.__init__(self, left, right)
+ if self.left == (NS_DAV, PROP_LAST_MODIFIED):
+ rfc822Time = formatdate(timegm(right)) # must not use locale setting
+ self.right = Literal(rfc822Time)
+ else:
+ self.right = Literal(strftime(DATE_FORMAT_ISO8601, right))
+
+
+class MatchesTerm(StringRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (child) property's value matches the (child) string.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return '<D:eq caseless="%s">' % self.attrCaseless + StringRelationTerm.toXML(self) + "</D:eq>"
+
+class ContainsTerm(StringRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) property's value contains the
+ (right child) string.
+ """
+ def __init__(self, left, right, isTaminoWorkaround=False):
+ right = unicode(right)
+ StringRelationTerm.__init__(self, left, "%" + right + "%")
+ # Tamino workaround: operator like is not yet implemented:
+ self.negate = 0
+ self.isTaminoWorkaround = isTaminoWorkaround
+
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ # Tamino workaround: operator like is not yet implemented:
+ # Produce a is-defined-condition instead
+ if self.isTaminoWorkaround:
+ return "<D:isdefined><D:prop xmlns='%s'><%s" % self.left + " /></D:prop></D:isdefined>"
+ else:
+ return '<D:like caseless="%s">' % self.attrCaseless + StringRelationTerm.toXML(self) + "</D:like>"
+
+ # start Tamino workaround for missing like-op:
+ def postFilter(self, resultSet):
+ '''
+ Filters the given result set. This is a TAMINO WebDav server workaround
+ for the missing 'like' tag.
+
+ @param resultSet: the result set that needs to be filtered.
+ '''
+ newResult = {}
+ word = self.right.literal[1:-1] # remove leading and trailing '%' characters (see __init__())
+ for url, properties in resultSet.items():
+ value = properties.get(self.left)
+ if self.negate:
+ if not value or value.textof().find(word) < 0:
+ newResult[url] = properties
+ else:
+ if value and value.textof().find(word) >= 0:
+ newResult[url] = properties
+ return newResult
+ # end of workaround
+
+class IsEqualTerm(NumberRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) numerical property's value is equal
+ to the (right child) number.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:eq>" + NumberRelationTerm.toXML(self) + "</D:eq>"
+
+class IsGreaterTerm(NumberRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) numerical property's value is greater
+ than the (right child) number.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:gt>" + NumberRelationTerm.toXML(self) + "</D:gt>"
+
+class IsGreaterOrEqualTerm(NumberRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) numerical property's value is greater
+ than or equal to the (right child) number.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:gte>" + NumberRelationTerm.toXML(self) + "</D:gte>"
+
+class IsSmallerTerm(NumberRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) numerical property's value is less
+ than the (right child) number.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:lt>" + NumberRelationTerm.toXML(self) + "</D:lt>"
+
+class IsSmallerOrEqualTerm(NumberRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) numerical property's value is less
+ than or equal to the (right child) number.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:lte>" + NumberRelationTerm.toXML(self) + "</D:lte>"
+
+
+class OnTerm(DateRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) property's value is a date
+ equal to the (right child) date.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:eq>" + DateRelationTerm.toXML(self) + "</D:eq>"
+
+class AfterTerm(DateRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) property's value is a date
+ succeeding the (right child) date.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:gt>" + DateRelationTerm.toXML(self) + "</D:gt>"
+
+class BeforeTerm(DateRelationTerm):
+ """
+ Nodes of this class evaluate to true if the (left child) property's value is a date
+ preceeding the (right child) date.
+ """
+ def toXML(self):
+ '''
+ Returns XML encoding.
+ '''
+ return "<D:lt>" + DateRelationTerm.toXML(self) + "</D:lt>"
+
+
+
+# Simple module test
+if __name__ == '__main__':
+ # use the example from the webdav specification
+ condition = AndTerm( (MatchesTerm('getcontenttype', 'image/gif'), \
+ IsGreaterTerm('getcontentlength', 4096)) )
+ print "Where: " + condition.toXML()
diff --git a/src/webdav/Connection.py b/src/webdav/Connection.py
new file mode 100644
index 0000000..66f7833
--- /dev/null
+++ b/src/webdav/Connection.py
@@ -0,0 +1,321 @@
+# pylint: disable-msg=W0142,W0102,R0901,R0904,E0203,E1101,C0103
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+The contained class extends the HTTPConnection class for WebDAV support.
+"""
+
+
+from httplib import HTTPConnection, CannotSendRequest, BadStatusLine, ResponseNotReady
+from copy import copy
+import base64 # for basic authentication
+try:
+ import hashlib
+except ImportError: # for Python 2.4 compatibility
+ import md5
+ hashlib = md5
+import mimetypes
+import os # file handling
+import urllib
+import types
+import socket # to "catch" socket.error
+from threading import RLock
+try:
+ from uuid import uuid4
+except ImportError: # for Python 2.4 compatibility
+ from uuid_ import uuid4
+from xml.parsers.expat import ExpatError
+
+from davlib import DAV
+from qp_xml import Parser
+
+from webdav.WebdavResponse import MultiStatusResponse, ResponseFormatError
+from webdav import Constants
+from webdav.logger import getDefaultLogger
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class Connection(DAV):
+ """
+ This class handles a connection to a WebDAV server.
+ This class is used internally. Client code should prefer classes
+ L{WebdavClient.ResourceStorer} and L{WebdavClient.CollectionStorer}.
+
+ @author: Roland Betz
+ """
+
+ # Constants
+ # The following switch activates a workaround for the Tamino webdav server:
+ # Tamino expects URLs which are passed in a HTTP header to be Latin-1 encoded
+ # instead of Utf-8 encoded.
+ # Set this switch to zero in order to communicate with conformant servers.
+ blockSize = 30000
+ MaxRetries = 10
+
+ def __init__(self, *args, **kwArgs):
+ DAV.__init__(self, *args, **kwArgs)
+ self.__authorizationInfo = None
+ self.logger = getDefaultLogger()
+ self.isConnectedToCatacomb = True
+ self.serverTypeChecked = False
+ self._lock = RLock()
+
+ def _request(self, method, url, body=None, extra_hdrs={}):
+
+ self._lock.acquire()
+ try:
+ # add the authorization header
+ extraHeaders = copy(extra_hdrs)
+ if self.__authorizationInfo:
+
+ # update (digest) authorization data
+ if hasattr(self.__authorizationInfo, "update"):
+ self.__authorizationInfo.update(method=method, uri=url)
+
+ extraHeaders["AUTHORIZATION"] = self.__authorizationInfo.authorization
+
+ # encode message parts
+ body = _toUtf8(body)
+ url = _urlEncode(url)
+ for key, value in extraHeaders.items():
+ extraHeaders[key] = _toUtf8(value)
+ if key == "Destination": # copy/move header
+ if self.isConnectedToCatacomb:
+ extraHeaders[key] = _toUtf8(value.replace(Constants.SHARP, Constants.QUOTED_SHARP))
+
+ else: # in case of TAMINO 4.4
+ extraHeaders[key] = _urlEncode(value)
+ # pass message to httplib class
+ for retry in range(0, Connection.MaxRetries): # retry loop
+ try:
+ self.logger.debug("REQUEST Send %s for %s" % (method, url))
+ self.logger.debug("REQUEST Body: " + repr(body))
+ for hdr in extraHeaders.items():
+ self.logger.debug("REQUEST Header: " + repr(hdr))
+ self.request(method, url, body, extraHeaders)
+ response = self.getresponse()
+ break # no retry needed
+ except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc:
+ # Workaround, start: reconnect and retry...
+ self.logger.debug("Exception: " + str(exc) + " Retry ... ")
+ self.close()
+ try:
+ self.connect()
+ except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc:
+ raise WebdavError("Cannot perform request. Connection failed.")
+ if retry == Connection.MaxRetries - 1:
+ raise WebdavError("Cannot perform request.")
+ return self.__evaluateResponse(method, response)
+ finally:
+ self._lock.release()
+
+ def __evaluateResponse(self, method, response):
+ """ Evaluates the response of the WebDAV server. """
+
+ status, reason = response.status, response.reason
+ self.logger.debug("Method: " + method + " Status %d: " % status + reason)
+
+ if status >= Constants.CODE_LOWEST_ERROR: # error has occured ?
+ self.logger.debug("ERROR Response: " + response.read())
+
+ # identify authentication CODE_UNAUTHORIZED, throw appropriate exception
+ if status == Constants.CODE_UNAUTHORIZED:
+ raise AuthorizationError(reason, status, response.msg["www-authenticate"])
+
+ response.close()
+ raise WebdavError(reason, status)
+
+ if status == Constants.CODE_MULTISTATUS:
+ content = response.read()
+ ## check for UTF-8 encoding
+ try:
+ response.root = Parser().parse(content)
+ except ExpatError, error:
+ errorMessage = "Invalid XML document has been returned.\nReason: '%s'" % str(error.args)
+ raise WebdavError(errorMessage)
+ try:
+ response.msr = MultiStatusResponse(response.root)
+ except ResponseFormatError:
+ raise WebdavError("Invalid WebDAV response.")
+ response.close()
+ self.logger.debug("RESPONSE (Multi-Status): " + unicode(response.msr))
+ elif method == 'LOCK' and status == Constants.CODE_SUCCEEDED:
+ response.parse_lock_response()
+ response.close()
+ elif method != 'GET' and method != 'PUT':
+ self.logger.debug("RESPONSE Body: " + response.read())
+ response.close()
+ return response
+
+ def addBasicAuthorization(self, user, password, realm=None):
+ if user and len(user) > 0:
+ self.__authorizationInfo = _BasicAuthenticationInfo(realm=realm, user=user, password=password)
+
+ def addDigestAuthorization(self, user, password, realm, qop, nonce, uri = None, method = None):
+ if user and len(user) > 0:
+ # username, realm, password, uri, method, qop are required
+ self.__authorizationInfo = _DigestAuthenticationInfo(realm=realm, user=user, password=password, uri=uri, method=method, qop=qop, nonce=nonce)
+
+ def putFile(self, path, srcfile, header={}):
+ self._lock.acquire()
+ try:
+ # Assemble header
+ try:
+ size = os.path.getsize(srcfile.name)
+ except os.error, error:
+ raise WebdavError("Cannot determine file size.\nReason: ''" % str(error.args))
+ header["Content-length"] = str(size)
+
+ contentType, contentEnc = mimetypes.guess_type(path)
+ if contentType:
+ header['Content-Type'] = contentType
+ if contentEnc:
+ header['Content-Encoding'] = contentEnc
+ if self.__authorizationInfo:
+ # update (digest) authorization data
+ if hasattr(self.__authorizationInfo, "update"):
+ self.__authorizationInfo.update(method="PUT", uri=path)
+ header["AUTHORIZATION"] = self.__authorizationInfo.authorization
+
+ # send first request
+ path = _urlEncode(path)
+ try:
+ HTTPConnection.request(self, 'PUT', path, "", header)
+ self._blockCopySocket(srcfile, self, Connection.blockSize)
+ srcfile.close()
+ response = self.getresponse()
+ except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc:
+ self.logger.debug("Exception: " + str(exc) + " Retry ... ")
+ raise WebdavError("Cannot perform request.")
+ status, reason = (response.status, response.reason)
+ self.logger.debug("Status %d: %s" % (status, reason))
+ try:
+ if status >= Constants.CODE_LOWEST_ERROR: # error has occured ?
+ raise WebdavError(reason, status)
+ finally:
+ self.logger.debug("RESPONSE Body: " + response.read())
+ response.close()
+ return response
+ finally:
+ self._lock.release()
+
+ def _blockCopySocket(self, source, toSocket, blockSize):
+ transferedBytes = 0
+ block = source.read(blockSize)
+ #while source.readinto(block, blockSize):
+ while len(block):
+ toSocket.send(block)
+ self.logger.debug("Wrote %d bytes." % len(block))
+ transferedBytes += len(block)
+ block = source.read(blockSize)
+ self.logger.info("Transfered %d bytes." % transferedBytes)
+
+ def __str__(self):
+ return self.protocol + "://" + self.host + ':' + str(self.port)
+
+
+class _BasicAuthenticationInfo(object):
+ def __init__(self, **kwArgs):
+ self.__dict__.update(kwArgs)
+ self.cookie = base64.encodestring("%s:%s" % (self.user, self.password) ).strip()
+ self.authorization = "Basic " + self.cookie
+ self.password = None # protect password security
+
+class _DigestAuthenticationInfo(object):
+
+ __nc = "0000000" # in hexadecimal without leading 0x
+
+ def __init__(self, **kwArgs):
+
+ self.__dict__.update(kwArgs)
+
+ if self.qop is None:
+ raise WebdavError("Digest without qop is not implemented.")
+ if self.qop == "auth-int":
+ raise WebdavError("Digest with qop-int is not implemented.")
+
+ def update(self, **kwArgs):
+ """ Update input data between requests"""
+
+ self.__dict__.update(kwArgs)
+
+ def _makeDigest(self):
+ """ Creates the digest information. """
+
+ # increment nonce count
+ self._incrementNc()
+
+ # username, realm, password, uri, method, qop are required
+
+ a1 = "%s:%s:%s" % (self.user, self.realm, self.password)
+ ha1 = hashlib.md5(a1).hexdigest()
+
+ #qop == auth
+ a2 = "%s:%s" % (self.method, self.uri)
+ ha2 = hashlib.md5(a2).hexdigest()
+
+ cnonce = str(uuid4())
+
+ responseData = "%s:%s:%s:%s:%s:%s" % (ha1, self.nonce, _DigestAuthenticationInfo.__nc, cnonce, self.qop, ha2)
+ digestResponse = hashlib.md5(responseData).hexdigest()
+
+ authorization = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", algorithm=MD5, response=\"%s\", qop=auth, nc=%s, cnonce=\"%s\"" \
+ % (self.user, self.realm, self.nonce, self.uri, digestResponse, _DigestAuthenticationInfo.__nc, cnonce)
+ return authorization
+
+ authorization = property(_makeDigest)
+
+ def _incrementNc(self):
+ _DigestAuthenticationInfo.__nc = self._dec2nc(self._nc2dec() + 1)
+
+ def _nc2dec(self):
+ return int(_DigestAuthenticationInfo.__nc, 16)
+
+ def _dec2nc(self, decimal):
+ return hex(decimal)[2:].zfill(8)
+
+
+class WebdavError(IOError):
+ def __init__(self, reason, code=0):
+ IOError.__init__(self, code)
+ self.code = code
+ self.reason = reason
+ def __str__(self):
+ return self.reason
+
+
+class AuthorizationError(WebdavError):
+ def __init__(self, reason, code, authHeader):
+ WebdavError.__init__(self, reason, code)
+
+ self.authType = authHeader.split(" ")[0]
+ self.authInfo = authHeader
+
+
+def _toUtf8(body):
+ if not body is None:
+ if type(body) == types.UnicodeType:
+ body = body.encode('utf-8')
+ return body
+
+
+def _urlEncode(url):
+ if type(url) == types.UnicodeType:
+ url = url.encode('utf-8')
+ return urllib.quote(url)
diff --git a/src/webdav/Constants.py b/src/webdav/Constants.py
new file mode 100644
index 0000000..56dfd77
--- /dev/null
+++ b/src/webdav/Constants.py
@@ -0,0 +1,199 @@
+# pylint: disable-msg=C0103
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Contains XML tag names for the WebDAV protocol (RFC 2815)
+and further WebDAV related constants.
+"""
+
+
+__version__ = "$Revision$"[11:-2]
+
+
+QUOTED_SHARP = "%23"
+SHARP = "#"
+
+# Date formats
+DATE_FORMAT_ISO8601 = r"%Y-%m-%dT%H:%M:%SZ"
+DATE_FORMAT_HTTP = r"%a, %d %b %Y %H:%M:%S GMT" # not used, substituted by rfc822 function
+
+NS_DAV = 'DAV:'
+NS_TAMINO = 'http://namespaces.softwareag.com/tamino/response2'
+
+TAG_PROPERTY_FIND = 'propfind'
+TAG_PROPERTY_NAME = 'propname'
+TAG_PROPERTY_UPDATE = 'propertyupdate'
+TAG_PROPERTY_SET = 'set'
+TAG_PROPERTY_REMOVE = 'remove'
+TAG_ALL_PROPERTY = 'allprop'
+TAG_PROP = 'prop'
+
+TAG_MULTISTATUS = 'multistatus'
+TAG_RESPONSE = 'response'
+TAG_HREF = 'href'
+TAG_PROPERTY_STATUS = 'propstat'
+TAG_STATUS = 'status'
+TAG_RESPONSEDESCRIPTION = 'responsdescription'
+
+PROP_CREATION_DATE = 'creationdate'
+PROP_DISPLAY_NAME = 'displayname'
+PROP_CONTENT_LANGUAGE = 'getcontentlanguage'
+PROP_CONTENT_LENGTH = 'getcontentlength'
+PROP_CONTENT_TYPE = 'getcontenttype'
+PROP_ETAG = 'getetag'
+PROP_MODIFICATION_DATE = 'modificationdate' # this property is supported by
+# Tamino 4.4 but not by Catacomb; the date format is ISO8601
+PROP_LAST_MODIFIED = 'getlastmodified'
+PROP_LOCK_DISCOVERY = 'lockdiscovery'
+PROP_RESOURCE_TYPE = 'resourcetype'
+PROP_SOURCE = 'source'
+PROP_SUPPORTED_LOCK = 'supportedlock'
+PROP_OWNER = 'owner'
+
+PROP_RESOURCE_TYPE_RESOURCE = 'resource'
+PROP_RESOURCE_TYPE_COLLECTION = 'collection'
+
+TAG_LINK = 'link'
+TAG_LINK_SOURCE = 'src'
+TAG_LINK_DESTINATION = 'dst'
+
+TAG_LOCK_ENTRY = 'lockentry'
+TAG_LOCK_SCOPE = 'lockscope'
+TAG_LOCK_TYPE = 'locktype'
+TAG_LOCK_INFO = 'lockinfo'
+TAG_ACTIVE_LOCK = 'activelock'
+TAG_LOCK_DEPTH = 'depth'
+TAG_LOCK_TOKEN = 'locktoken'
+TAG_LOCK_TIMEOUT = 'timeout'
+TAG_LOCK_EXCLUSIVE = 'exclusive'
+TAG_LOCK_SHARED = 'shared'
+TAG_LOCK_OWNER = 'owner'
+
+# HTTP error code constants
+CODE_MULTISTATUS = 207
+CODE_SUCCEEDED = 200
+CODE_CREATED = 201
+CODE_NOCONTENT = 204
+
+CODE_LOWEST_ERROR = 300
+
+CODE_UNAUTHORIZED = 401
+CODE_FORBIDDEN = 403
+CODE_NOT_FOUND = 404
+CODE_CONFLICT = 409
+CODE_PRECONDITION_FAILED = 412
+CODE_LOCKED = 423 # no permission
+CODE_FAILED_DEPENDENCY = 424
+
+CODE_OUTOFMEM = 507
+
+# ?
+CONFIG_UNICODE_URL = 1
+
+# constants for WebDAV DASL according to draft
+
+TAG_SEARCH_REQUEST = 'searchrequest'
+TAG_SEARCH_BASIC = 'basicsearch'
+TAG_SEARCH_SELECT = 'select'
+TAG_SEARCH_FROM = 'from'
+TAG_SEARCH_SCOPE = 'scope'
+TAG_SEARCH_WHERE = 'where'
+
+# constants for WebDAV ACP (according to draft-ietf-webdav-acl-09) below ...
+
+TAG_ACL = 'acl'
+TAG_ACE = 'ace'
+TAG_GRANT = 'grant'
+TAG_DENY = 'deny'
+TAG_PRIVILEGE = 'privilege'
+TAG_PRINCIPAL = 'principal'
+TAG_ALL = 'all'
+TAG_AUTHENTICATED = 'authenticated'
+TAG_UNAUTHENTICATED = 'unauthenticated'
+TAG_OWNER = 'owner'
+TAG_PROPERTY = 'property'
+TAG_SELF = 'self'
+TAG_INHERITED = 'inherited'
+TAG_PROTECTED = 'protected'
+TAG_SUPPORTED_PRIVILEGE = 'supported-privilege'
+TAG_DESCRIPTION = 'description'
+
+# privileges for WebDAV ACP:
+TAG_READ = 'read'
+TAG_WRITE = 'write'
+TAG_WRITE_PROPERTIES = 'write-properties'
+TAG_WRITE_CONTENT = 'write-content'
+TAG_UNLOCK = 'unlock'
+TAG_READ_ACL = 'read-acl'
+TAG_READ_CURRENT_USER_PRIVILEGE_SET = 'read-current-user-privilege-set'
+TAG_WRITE_ACL = 'write-acl'
+TAG_ALL = 'all'
+TAG_BIND = 'bind'
+TAG_UNBIND = 'unbind'
+# Tamino-specific privileges
+TAG_TAMINO_SECURITY = 'security'
+# Limestone-specific privileges
+TAG_BIND_COLLECTION = 'bind-collection'
+TAG_UNBIND_COLLECTION = 'unbind-collection'
+TAG_READ_PRIVATE_PROPERTIES = 'read-private-properties'
+TAG_WRITE_PRIVATE_PROPERTIES = 'write-private-properties'
+
+# properties for WebDAV ACP:
+PROP_CURRENT_USER_PRIVILEGE_SET = 'current-user-privilege-set'
+PROP_SUPPORTED_PRIVILEGE_SET = 'supported-privilege-set'
+PROP_PRINCIPAL_COLLECTION_SET = 'principal-collection-set'
+
+# reports for WebDAV ACP
+REPORT_ACL_PRINCIPAL_PROP_SET = 'acl-principal-prop-set'
+
+
+
+# constants for WebDAV Delta-V
+
+# WebDAV Delta-V method names
+METHOD_REPORT = 'REPORT'
+METHOD_VERSION_CONTROL = 'VERSION-CONTROL'
+METHOD_UNCHECKOUT = 'UNCHECKOUT'
+METHOD_CHECKOUT = 'CHECKOUT'
+METHOD_CHECKIN = 'CHECKIN'
+METHOD_UPDATE = 'UPDATE'
+
+# Special properties
+PROP_SUCCESSOR_SET = (NS_DAV, 'successor-set')
+PROP_PREDECESSOR_SET = (NS_DAV, 'predecessor-set')
+PROP_VERSION_HISTORY = (NS_DAV, 'version-history')
+PROP_CREATOR = (NS_DAV, 'creator-displayname')
+PROP_VERSION_NAME = (NS_DAV, 'version-name')
+PROP_CHECKEDIN = (NS_DAV, 'checked-in')
+PROP_CHECKEDOUT = (NS_DAV, 'checked-out')
+PROP_COMMENT = (NS_DAV, 'comment')
+
+# XML tags for request body
+TAG_VERSION_TREE = 'version-tree'
+TAG_LOCATE_BY_HISTORY = 'locate-by-history'
+TAG_UPDATE = 'update'
+TAG_VERSION = 'version'
+
+# HTTP header constants
+HTTP_HEADER_DEPTH_INFINITY = 'infinity'
+HTTP_HEADER_IF = 'if'
+HTTP_HEADER_DAV = 'dav'
+HTTP_HEADER_DASL = 'dasl'
+HTTP_HEADER_OPTION_ACL = 'access-control'
+HTTP_HEADER_OPTION_DAV_BASIC_SEARCH = 'DAV:basicsearch'
+HTTP_HEADER_SERVER = 'server'
+HTTP_HEADER_SERVER_TAMINO = 'Apache/2.0.54 (Win32)'
diff --git a/src/webdav/Makefile.am b/src/webdav/Makefile.am
new file mode 100644
index 0000000..3356daf
--- /dev/null
+++ b/src/webdav/Makefile.am
@@ -0,0 +1,20 @@
+SUBDIRS = acp
+
+sugardir = $(pythondir)/webdav
+sugar_PYTHON = \
+ Connection.py \
+ davlib.py \
+ logger.py \
+ NameCheck.py \
+ Utils.py \
+ VersionHandler.py \
+ WebdavRequests.py \
+ Condition.py \
+ Constants.py \
+ __init__.py \
+ qp_xml.py \
+ uuid_.py \
+ WebdavClient.py \
+ WebdavResponse.py
+
+
diff --git a/src/webdav/NameCheck.py b/src/webdav/NameCheck.py
new file mode 100644
index 0000000..7976973
--- /dev/null
+++ b/src/webdav/NameCheck.py
@@ -0,0 +1,193 @@
+# pylint: disable-msg=R0904,W0142,W0511,W0104,C0321,E1103,W0212
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Check name of new collections/resources for "illegal" characters.
+"""
+
+
+import re
+import unicodedata
+
+
+__version__ = "$LastChangedRevision$"
+
+
+_unicodeUmlaut = [unicodedata.lookup("LATIN CAPITAL LETTER A WITH DIAERESIS"),
+ unicodedata.lookup("LATIN SMALL LETTER A WITH DIAERESIS"),
+ unicodedata.lookup("LATIN CAPITAL LETTER O WITH DIAERESIS"),
+ unicodedata.lookup("LATIN SMALL LETTER O WITH DIAERESIS"),
+ unicodedata.lookup("LATIN CAPITAL LETTER U WITH DIAERESIS"),
+ unicodedata.lookup("LATIN SMALL LETTER U WITH DIAERESIS"),
+ unicodedata.lookup("LATIN SMALL LETTER SHARP S")]
+
+# Define characters and character base sets
+_german = u"".join(_unicodeUmlaut)
+_alpha = "A-Za-z"
+_num = "0-9"
+_alphaNum = _alpha + _num
+_space = " "
+_under = "_"
+_dash = "\-"
+_dot = "\."
+_exclam = "\!"
+_tilde = "\~"
+_dollar = "\$"
+_plus = "+"
+_equal = "="
+_sharp = "#"
+
+# Define character groups
+_letterNum = _alphaNum + _german
+_letter = _alpha + _german
+
+# Define character sets for names
+firstPropertyChar = _letter + _under
+propertyChar = firstPropertyChar + _num + _dash + _dot
+firstResourceChar = firstPropertyChar + _num + _tilde + _exclam + _dollar + \
+ _dot + _dash + _plus + _equal + _sharp
+resourceChar = firstResourceChar + _space
+
+# Define regular expressions for name validation
+_propertyFirstRe = re.compile(u"^["+ firstPropertyChar +"]")
+
+_propertyRe = re.compile(u"[^"+ propertyChar +"]")
+_resourceFirstRe = re.compile(u"^["+ firstResourceChar +"]")
+_resourceRe = re.compile(u"[^"+ resourceChar +"]")
+
+
+def isValidPropertyName(name):
+ """
+ Check if the given property name is valid.
+
+ @param name: Property name.
+ @type name: C{unicode}
+
+ @return: Boolean indicating whether the given property name is valid or not.
+ @rtype: C{bool}
+ """
+
+ illegalChar = _propertyRe.search(name)
+ return illegalChar == None and _propertyFirstRe.match(name) != None
+
+
+def isValidResourceName(name):
+ """
+ Check if the given resource name is valid.
+
+ @param name: Resource name.
+ @type name: C{unicode}
+
+ @return: Boolean indicating whether the given resource name is valid or not.
+ @rtype: C{bool}
+ """
+
+ illegalChar = _resourceRe.search(name)
+ return illegalChar == None and _resourceFirstRe.match(name) != None
+
+
+def validatePropertyName(name):
+ """
+ Check if the given property name is valid.
+
+ @param name: Property name.
+ @type name: C{unicode}
+ @raise WrongNameError: if validation fails (see L{datafinder.common.NameCheck.WrongNameError})
+ """
+
+ illegalChar = _propertyRe.search(name)
+ if illegalChar:
+ raise WrongNameError(illegalChar.start(), name[illegalChar.start()])
+ if not _propertyFirstRe.match(name):
+ if len(name) > 0:
+ raise WrongNameError(0, name[0])
+ else:
+ raise WrongNameError(0, 0)
+
+
+def validateResourceName(name):
+ """
+ Check if the given resource name is valid.
+
+ @param name: name of resource/collection
+ @type name: C{unicode}
+ @raise WrongNameError: if validation fails (@see L{datafinder.common.NameCheck.WrongNameError})
+ """
+
+ illegalChar = _resourceRe.search(name)
+ if illegalChar:
+ raise WrongNameError(illegalChar.start(), name[illegalChar.start()])
+ if not _resourceFirstRe.match(name):
+ if len(name) > 0:
+ raise WrongNameError(0, name[0])
+ else:
+ raise WrongNameError(0, 0)
+
+
+def getResourceNameErrorPosition(name):
+ """
+ Get position of illegal character (and the error-message).
+ This method can be used to get this information if L{isValidPropertyName}
+ or L{isValidResourceName} failed.
+
+ @param name: Resource name.
+ @type name: C{unicode}
+
+ @return: Tuple of error position and message.
+ @rtype: C{tuple} of C{int} and C{unicode}
+ """
+
+ result = (-1, None)
+ illegalChar = _resourceRe.search(name)
+ if illegalChar:
+ result = (illegalChar.start(), \
+ u"Illegal character '%s' at index %d." % \
+ (name[illegalChar.start()], illegalChar.start()))
+ elif not _resourceFirstRe.match(name):
+ result = (0, u"Illegal character '%s' at index %d." % (name[0], 0))
+ return result
+
+
+class WrongNameError(ValueError):
+ """
+ Exception raised if an "illegal" character was found.
+
+ @ivar character: character that caused the exception
+ @type character: C{unicode}
+ @ivar position: position of C{character}
+ @type position: C{int}
+ """
+
+ def __init__(self, position, character):
+ """
+ Constructor.
+
+ @param character: Character that caused the exception.
+ @type character: C{unicode}
+ @param position: Position of C{character}
+ @type position: C{int}
+ """
+
+ ValueError.__init__(self)
+ self.character = character
+ self.position = position
+
+ def __str__(self):
+ """ Returns string representation. """
+
+ return ValueError.__str__(self) + \
+ "Character '%s' at index %d." % (self.character, self.position)
diff --git a/src/webdav/Utils.py b/src/webdav/Utils.py
new file mode 100644
index 0000000..ec05755
--- /dev/null
+++ b/src/webdav/Utils.py
@@ -0,0 +1,154 @@
+# pylint: disable-msg=W0141,R0912
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+The module contains functions to support use of the WebDav functionalities.
+"""
+
+
+import os
+import sys
+
+from webdav.WebdavClient import CollectionStorer, ResourceStorer
+from webdav.Constants import NS_DAV, PROP_RESOURCE_TYPE, CODE_NOT_FOUND, PROP_RESOURCE_TYPE_RESOURCE
+from webdav.Connection import WebdavError
+
+
+__version__ = "$Revision$"[11:-2]
+
+
+def resourceExists(node, name = None, resourceType = PROP_RESOURCE_TYPE_RESOURCE):
+ """
+ Check if resource exists.
+
+ Usage:
+ - resourceExists(ResourceStorer-object):
+ check if resource exists
+ - resourceExists(CollectionStorer-object, name):
+ check if resource name exists in collection
+
+ @param node: node that has to be checked or node of collection
+ @type node: L{ResourceStorer<webdav.WebdavClient.ResourceStorer>}
+ @param name: name of resource (in collection node) that has to be checked
+ @type name: string
+
+ @return: boolean
+
+ @raise WebdavError: all WebDAV errors except WebDAV error 404 (not found)
+ """
+
+ exists = False
+ if not node:
+ return exists
+ try:
+ myResourceType = ""
+ if name:
+ # make sure it's unicode:
+ if not isinstance(name, unicode):
+ name = name.decode(sys.getfilesystemencoding())
+ url = node.url
+ if url.endswith("/"):
+ url = url + name
+ else:
+ url = url + "/" + name
+ newNode = ResourceStorer(url, node.connection)
+ element = newNode.readProperty(NS_DAV, PROP_RESOURCE_TYPE)
+ else: # name is "None":
+ element = node.readProperty(NS_DAV, PROP_RESOURCE_TYPE)
+
+ if len(element.children) > 0:
+ myResourceType = element.children[0].name
+ if resourceType == myResourceType or resourceType == PROP_RESOURCE_TYPE_RESOURCE:
+ exists = True
+ else:
+ exists = False
+ except WebdavError, wderr:
+ if wderr.code == CODE_NOT_FOUND:
+ # node doesn't exist -> exists = False:
+ exists = False
+ else:
+ # another exception occured -> "re-raise" it:
+ raise
+ return exists
+
+
+def downloadCollectionContent(destinationPath, collectionToDownload):
+ """
+ Downloads the resources contained to the given directory.
+
+ @param destinationPath: Path to download the files to, will be created if it not exists.
+ @type destinationPath: C{String}
+ @param collectionToDownload: Collection to download the content from.
+ @type collectionToDownload: instance of L{CollectionStorer<webdav.WebdavClient.CollectionStorer>}
+
+ @raise WebdavError: If something goes wrong.
+ """
+
+ from time import mktime, gmtime
+
+ downloadCount = 0
+
+ listOfItems = collectionToDownload.getCollectionContents()
+
+ if not os.path.exists(destinationPath):
+ try:
+ os.makedirs(destinationPath)
+ except OSError:
+ errorMessage = "Cannot create download destination directory '%s'." % destinationPath
+ raise WebdavError(errorMessage)
+
+ try:
+ itemsInPath = os.listdir(destinationPath)
+ except OSError:
+ errorMessage = "Cannot read the content of download destination directory '%s'." % destinationPath
+ raise WebdavError(errorMessage)
+
+ for item in listOfItems:
+ # skip collections
+ if not isinstance(item[0], CollectionStorer):
+ itemSavePath = os.path.join(destinationPath, item[0].name)
+ existsItemSavePath = os.path.exists(itemSavePath)
+
+ # update?
+ if existsItemSavePath:
+ try:
+ isUpdateNecessary = mktime(item[1].getLastModified()) > mktime(gmtime(os.path.getmtime(itemSavePath)))
+ except (ValueError, OverflowError):
+ isUpdateNecessary = True
+ # windows is not case sensitive
+ for realItem in itemsInPath:
+ if realItem.lower() == item[0].name.lower():
+ itemsInPath.remove(realItem)
+ else:
+ isUpdateNecessary = True
+
+ # download
+ if not existsItemSavePath or (existsItemSavePath and isUpdateNecessary):
+ item[0].downloadFile(itemSavePath)
+ downloadCount = downloadCount + 1
+
+ # delete old items
+ try:
+ for item in itemsInPath:
+ os.remove(os.path.join(destinationPath, item))
+ except OSError, e:
+ if e.errno == 13: # permission error
+ sys.stderr.write("permission problem on '%s' in %s\n" % (e.filename, e.strerror))
+ else:
+ raise
+
+ return downloadCount
diff --git a/src/webdav/VersionHandler.py b/src/webdav/VersionHandler.py
new file mode 100644
index 0000000..a1962c6
--- /dev/null
+++ b/src/webdav/VersionHandler.py
@@ -0,0 +1,198 @@
+# pylint: disable-msg=W0612,W0142
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+The WebDAV client module forwards Delta-V related method invocations to
+the following VersionHandler class.
+"""
+
+__version__ = '$Revision$'[11:-2]
+
+
+import types
+
+from webdav import Constants
+from davlib import XML_CONTENT_TYPE, XML_DOC_HEADER
+
+
+class VersionHandler(object):
+ """
+ Implements a client interface for WebDAV Delta-V methods
+ For the Delta-V see RFC 3253 at http://www.ietf.org/rfc/rfc3253.txt
+ """
+
+ # restrict instance variables
+ __slots__ = ('path', 'connection')
+
+
+ def __init__(self, connection, path):
+ """
+ Construct a VersionHandler with a URL path and a WebDAV connection.
+ This constructor must not be called outside class ResourceStorer.
+
+ @param connection: L{webdav.Connection} instance
+ @param path: resource's path part of URL
+ """
+ #assert isinstance(connection, Connection), \
+ # "Class of connection is %s." % connection.__class__.__name__
+ self.connection = connection
+ self.path = path
+
+
+ def activateVersionControl(self):
+ """
+ Turns version control on for this resource.
+ The resource becomes a version controlled resource (VCR)
+ """
+ response = self.connection._request(Constants.METHOD_VERSION_CONTROL, self.path, None, {})
+ # set auto-versioning to DAV:locked-checkout
+ ## parse response body in case of an error
+
+ def uncheckout(self, lockToken=None):
+ """
+ Undos a previous check-out operation on this VCR.
+ The VCR is reverted to the state before the checkout/lock operation.
+ Beware: Property or content changes will be lost !
+ A (optional) lock has to be removed seperatedly.
+
+ @param lockToken: returned by a preceeding lock() method invocation or None
+ """
+ headers = {}
+ if lockToken:
+ headers = lockToken.toHeader()
+ response = self.connection._request(Constants.METHOD_UNCHECKOUT, self.path, None, headers)
+ ## parse response body in case of an error
+
+ def listAllVersions(self):
+ """
+ List version history.
+
+ @return: List of versions for this VCR. Each version entry is a tuple adhering
+ to the format (URL-path, name, creator, tuple of successor URL-paths).
+ If there are no branches then there is at most one successor within the tuple.
+ """
+ # implementation is similar to the propfind method
+ headers = {}
+ headers['Content-Type'] = XML_CONTENT_TYPE
+ body = _createReportVersionTreeBody()
+ response = self.connection._request(Constants.METHOD_REPORT, self.path, body, headers)
+ # response is multi-status
+ result = []
+ for path, properties in response.msr.items():
+ # parse the successor-set value from XML into alist
+ result.append( (path, str(properties[Constants.PROP_VERSION_NAME]), \
+ str(properties[Constants.PROP_CREATOR]), \
+ _extractSuccessorList(properties[Constants.PROP_SUCCESSOR_SET])) )
+ ## TODO: sort for path and produce list
+ result.sort()
+ return result
+
+ # warning: not tested yet
+ def readVersionProperties(self):
+ """
+ Provide version related information on this VCR.
+ This include a reference to the latest version resource,
+ check-out state information and a comment.
+
+ @return: map of version properties with values.
+ """
+ versionProperties = (Constants.PROP_CHECKEDIN, Constants.PROP_CHECKEDOUT, Constants.PROP_COMMENT)
+ return self.connection.readProperties(*versionProperties)
+
+
+ def revertToVersion(self, oldVersion):
+ """
+ Revert this VCR to the given version.
+ Beware: All versions succeeding the given version are made unavailable.
+
+ @param oldVersion: URL-path of a previous version of this VCR.
+ """
+ ## send an update request
+ assert isinstance(oldVersion, types.StringType) or isinstance(oldVersion, types.UnicodeType)
+ response = self.connection._request(Constants.METHOD_UPDATE, self.path,
+ _createUpdateBody(oldVersion), {})
+ return response
+
+
+ # the following is not needed when using auto-versioning
+
+ # warning: not tested yet
+ def checkout(self):
+ """
+ Marks resource as checked-out
+ This is usually followed by a GET (download) operation.
+ """
+ response = self.connection._request(Constants.METHOD_CHECKOUT, self.path, None, {})
+ ## parse response body in case of an error
+
+ # warning: not tested yet
+ def checkin(self):
+ """
+ Creates a new version from the VCR's content.
+ This opeartion is usually preceeded by one or more write operations.
+ """
+ response = self.connection._request(Constants.METHOD_CHECKIN, self.path, None, {})
+ versionUrl = response.getheader('Location')
+ return versionUrl
+ ## parse response body in case of an error
+
+
+
+
+# Helper functions
+def _createReportVersionTreeBody():
+ """
+ TBD
+
+ @return: ...
+ @rtype: string
+ """
+ versions = 'D:' + Constants.TAG_VERSION_TREE
+ prop = 'D:' + Constants.TAG_PROP
+ nameList = [Constants.PROP_SUCCESSOR_SET, Constants.PROP_VERSION_NAME, Constants.PROP_CREATOR]
+ return XML_DOC_HEADER + \
+ '<%s xmlns:D="DAV:"><%s>' % (versions, prop) + \
+ reduce(lambda xml, name: xml + "<D:%s/>" % name[1], [''] + nameList) + \
+ '</%s></%s>' % (prop, versions)
+
+def _createUpdateBody(path):
+ """
+ TBD
+
+ @return: ...
+ @rtype: string
+ """
+ update = 'D:' + Constants.TAG_UPDATE
+ version = 'D:' + Constants.TAG_VERSION
+ href = 'D:' + Constants.TAG_HREF
+ #PROP = 'D:' + TAG_PROP
+ return XML_DOC_HEADER + \
+ '<%s xmlns:D="DAV:"><%s><%s>' % (update, version, href) + \
+ path + \
+ '</%s></%s></%s>' % (href, version, update)
+
+def _extractSuccessorList(element):
+ """
+ TBD
+
+ @return: ...
+ @rtype: tuple of strings
+ """
+ result = []
+ for href in element.children:
+ result.append(href.textof())
+ return tuple(result)
diff --git a/src/webdav/WebdavClient.py b/src/webdav/WebdavClient.py
new file mode 100644
index 0000000..ab5cec3
--- /dev/null
+++ b/src/webdav/WebdavClient.py
@@ -0,0 +1,840 @@
+# pylint: disable-msg=R0904,W0142,W0511,W0104,C0321,E1103,W0212
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+This module contains the classes ResourceStorer and CollectionStorer for accessing WebDAV resources.
+"""
+
+
+from davlib import XML_CONTENT_TYPE
+
+from urlparse import urlsplit
+import re
+import types
+import sys
+import os
+import shutil
+import socket
+
+from webdav import Constants
+from webdav.WebdavResponse import LiveProperties
+from webdav.WebdavRequests import createFindBody, createUpdateBody, createDeleteBody, createSearchBody
+from webdav.Condition import ConditionTerm
+from webdav.Connection import Connection, WebdavError, AuthorizationError
+from webdav.VersionHandler import VersionHandler
+
+from webdav.acp.Privilege import Privilege
+from webdav.acp.Acl import ACL
+from webdav.NameCheck import validateResourceName, WrongNameError
+
+
+__version__ = '$Revision$'[11:-2]
+
+SOCKET_DEFAULT_TIMEOUT = 10
+
+
+def switchUnicodeUrlOn(switch):
+ """
+ Configure whether to use unicode (UTF-8) encoded URLs (default) or
+ Latin-1 encoded URLs.
+
+ @param switch: 1 if unicode URLs shall be used
+ """
+
+ assert switch == 0 or switch == 1, "Pass boolean argument, please."
+ Constants.CONFIG_UNICODE_URL = switch
+
+
+def parseDigestAuthInfo(authInfo):
+ """
+ Parses the authentication information returned from a server and returns
+ a dictionary containing realm, qop, and nonce.
+
+ @see: L{AuthorizationError<webdav.Connection.AuthorizationError>}
+ or the main function of this module.
+ """
+
+ info = dict()
+ info["realm"] = re.search('realm="([^"]+)"', authInfo).group(1)
+ info["qop"] = re.search('qop="([^"]+)"', authInfo).group(1)
+ info["nonce"] = re.search('nonce="([^"]+)"', authInfo).group(1)
+ return info
+
+
+class ResourceStorer(object):
+ """
+ This class provides client access to a WebDAV resource
+ identified by an URI. It provides all WebDAV class 2 features which include
+ uploading data, getting and setting properties qualified by a XML name space,
+ locking and unlocking the resource.
+ This class does not cache resource data. This has to be performed by its clients.
+
+ @author: Roland Betz
+ """
+
+ # Instance properties
+ url = property(lambda self: str(self.connection) + self.path, None, None, "Resource's URL")
+
+ def __init__(self, url, connection=None, validateResourceNames=True):
+ """
+ Creates an instance for the given URL
+ User must invoke validate() after construction to check the resource on the server.
+
+ @param url: Unique resource location for this storer.
+ @type url: C{string}
+ @param connection: this optional parameter contains a Connection object
+ for the host part of the given URL. Passing a connection saves
+ memory by sharing this connection. (defaults to None)
+ @type connection: L{webdav.Connection}
+ @raise WebdavError: If validation of resource name path parts fails.
+ """
+
+ assert connection == None or isinstance(connection, Connection)
+ parts = urlsplit(url, allow_fragments=False)
+ self.path = parts[2]
+ self.validateResourceNames = validateResourceNames
+
+ # validate URL path
+ for part in self.path.split('/'):
+ if part != '' and not "ino:" in part: # explicitly allowing this character sequence as a part of a path (Tamino 4.4)
+ if self.validateResourceNames:
+ try:
+ validateResourceName(part)
+ except WrongNameError:
+ raise WebdavError("Found invalid resource name part.")
+ self.name = part
+ # was: filter(lambda part: part and validateResourceName(part), self.path.split('/'))
+ # but filter is deprecated
+
+ self.defaultNamespace = None # default XML name space of properties
+ if connection:
+ self.connection = connection
+ else:
+ conn = parts[1].split(":")
+ if len(conn) == 1:
+ self.connection = Connection(conn[0], protocol = parts[0]) # host and protocol
+ else:
+ self.connection = Connection(conn[0], int(conn[1]), protocol = parts[0]) # host and port and protocol
+ self.versionHandler = VersionHandler(self.connection, self.path)
+
+
+ def validate(self):
+ """
+ Check whether URL contains a WebDAV resource
+ Uses the WebDAV OPTIONS method.
+
+ @raise WebdavError: L{WebdavError} if URL does not contain a WebDAV resource
+ """
+ #davHeader = response.getheader(HTTP_HEADER_DAV)
+ davHeader = self.getSpecificOption(Constants.HTTP_HEADER_DAV)
+ self.connection.logger.debug("HEADER DAV: %s" % davHeader)
+ if not(davHeader) or davHeader.find("2") < 0: # DAV class 2 supported ?
+ raise WebdavError("URL does not support WebDAV", 0)
+
+ def options(self):
+ """
+ Send an OPTIONS request to server and return all HTTP headers.
+
+ @return: map of all HTTP headers returned by the OPTIONS method.
+ """
+ response = self.connection.options(self.path)
+ result = {}
+ result.update(response.msg)
+ self.connection.logger.debug("OPTION returns: " + str(result.keys()))
+ return result
+
+ def _getAclSupportAvailable(self):
+ """
+ Returns True if the current connection has got ACL support.
+
+ @return: ACL support (True / False)
+ @rtype: C{bool}
+ """
+ options = self.getSpecificOption(Constants.HTTP_HEADER_DAV)
+ if options.find(Constants.HTTP_HEADER_OPTION_ACL) >= 0:
+ return True
+ else:
+ return False
+
+ aclSupportAvailable = property(_getAclSupportAvailable)
+
+ def _getDaslBasicsearchSupportAvailable(self):
+ """
+ Returns True if the current connection supports DASL basic search.
+
+ @return: DASL basic search support (True / False)
+ @rtype: C{bool}
+ """
+ options = self.getSpecificOption(Constants.HTTP_HEADER_DASL)
+ if not options or \
+ not options.find(Constants.HTTP_HEADER_OPTION_DAV_BASIC_SEARCH) >= 0:
+ return False
+ else:
+ return True
+
+ daslBasicsearchSupportAvailable = property(_getDaslBasicsearchSupportAvailable)
+
+ def isConnectedToCatacombServer(self):
+ """
+ Returns True if connected to a Catacomb WebDav server.
+
+ @return: if connected to Catacomb Webdav server (True / False)
+ @rtype: C{bool}
+ """
+ if not self.connection.serverTypeChecked:
+ options = self.getSpecificOption(Constants.HTTP_HEADER_SERVER)
+ if options.find(Constants.HTTP_HEADER_SERVER_TAMINO) >= 0:
+ self.connection.isConnectedToCatacomb = False
+ else:
+ self.connection.isConnectedToCatacomb = True
+ self.connection.serverTypeChecked = True
+ return self.connection.isConnectedToCatacomb
+
+ def getSpecificOption(self, option):
+ """
+ Returns specified WebDav options.
+ @param option: name of the option
+
+ @return: String containing the value of the option.
+ @rtype: C{string}
+ """
+ options = ''
+ try:
+ options = self.options().get(option)
+ except KeyError:
+ return options
+ return options
+
+ ### delegate some method invocations
+ def __getattr__(self, name):
+ """
+ Build-in method:
+ Forwards unknow lookups (methods) to delegate object 'versionHandler'.
+
+ @param name: name of unknown attribute
+ """
+ # delegate Delta-V methods
+ return getattr(self.versionHandler, name)
+
+ def copy(self, toUrl, infinity=True):
+ """
+ Copies this resource.
+
+ @param toUrl: target URI path
+ @param infinity: Flag that indicates that the complete content of collection is copied. (default)
+ @type depth: C{boolean}
+ """
+ self.connection.logger.debug("Copy to " + repr(toUrl));
+ _checkUrl(toUrl)
+ if infinity:
+ response = self.connection.copy(self.path, toUrl)
+ else:
+ response = self.connection.copy(self.path, toUrl, 0)
+ if response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+ raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+ def delete(self, lockToken=None):
+ """
+ Deletes this resource.
+
+ @param lockToken: String returned by last lock operation or null.
+ @type lockToken: L{LockToken}
+ """
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ response = self.connection.delete(self.path, header)
+ if response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+ raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+ def move(self, toUrl):
+ """
+ Moves this resource to the given path or renames it.
+
+ @param toUrl: new (URI) path
+ """
+ self.connection.logger.debug("Move to " + repr(toUrl));
+ _checkUrl(toUrl)
+ response = self.connection.move(self.path, toUrl)
+ if response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+ raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+
+ def lock(self, owner):
+ """
+ Locks this resource for exclusive write access. This means that for succeeding
+ write operations the returned lock token has to be passed.
+ If the methode does not throw an exception the lock has been granted.
+
+ @param owner: describes the lock holder
+ @return: lock token string (automatically generated)
+ @rtype: L{LockToken}
+ """
+ response = self.connection.lock(self.path, owner)
+ if response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+ raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+ return LockToken(self.url, response.locktoken)
+
+ def unlock(self, lockToken):
+ """
+ Removes the lock from this resource.
+
+ @param lockToken: which has been return by the lock() methode
+ @type lockToken: L{LockToken}
+ """
+ self.connection.unlock(self.path, lockToken.token)
+
+
+ def deleteContent(self, lockToken=None):
+ """
+ Delete binary data at permanent storage.
+
+ @param lockToken: None or lock token from last lock request
+ @type lockToken: L{LockToken}
+ """
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ self.connection.put(self.path, "", extra_hdrs=header)
+
+ def uploadContent(self, content, lockToken=None):
+ """
+ Write binary data to permanent storage.
+
+ @param content: containing binary data
+ @param lockToken: None or lock token from last lock request
+ @type lockToken: L{LockToken}
+ """
+ assert not content or isinstance(content, types.UnicodeType) or\
+ isinstance(content, types.StringType), "Content is not a string: " + content.__class__.__name__
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ response = None
+ if not content is None:
+ header["Content-length"] = len(content)
+ else:
+ header["Content-length"] = 0
+
+ try:
+ response = self.connection.put(self.path, content, extra_hdrs=header)
+ finally:
+ if response:
+ self.connection.logger.debug(response.read())
+ response.close()
+
+ def uploadFile(self, newFile, lockToken=None):
+ """
+ Write binary data to permanent storage.
+
+ @param newFile: File containing binary data.
+ @param lockToken: None or lock token from last lock request
+ @type lockToken: L{LockToken}
+ """
+ assert isinstance(newFile, types.FileType), "Argument is no file: " + file.__class__.__name__
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ self.connection.putFile(self.path, newFile, header=header)
+
+ def downloadContent(self):
+ """
+ Read binary data from permanent storage.
+ """
+ response = self.connection.get(self.path)
+ # TODO: Other interface ? return self.connection.getfile()
+ return response
+
+ def downloadFile(self, localFileName):
+ """
+ Copy binary data from permanent storage to a local file.
+
+ @param localFileName: file to write binary data to
+ """
+ localFile = open(localFileName, 'wb')
+ remoteFile = self.downloadContent()
+ try:
+ socket.setdefaulttimeout(SOCKET_DEFAULT_TIMEOUT)
+ _blockCopyFile(remoteFile, localFile, Connection.blockSize)
+ except socket.error, e:
+ raise e
+ remoteFile.close()
+ localFile.close()
+
+ def readProperties(self, *names):
+ """
+ Reads the given properties.
+
+ @param names: a list of property names.
+ A property name is a (XmlNameSpace, propertyName) tuple.
+ @return: a map from property names to DOM Element or String values.
+ """
+ assert names, "Property names are missing."
+ body = createFindBody(names, self.defaultNamespace)
+ response = self.connection.propfind(self.path, body, depth=0)
+ properties = response.msr.values()[0]
+ if properties.errorCount > 0:
+ raise WebdavError("Property is missing on '%s': %s" % (self.path, properties.reason), properties.code)
+ return properties
+
+ def readProperty(self, nameSpace, name):
+ """
+ Reads the given property.
+
+ @param nameSpace: XML-namespace
+ @type nameSpace: string
+ @param name: A property name.
+ @type name: string
+
+ @return: a map from property names to DOM Element or String values.
+ """
+ results = self.readProperties((nameSpace, name))
+ if len(results) == 0:
+ raise WebdavError("Property is missing: " + results.reason)
+ return results.values()[0]
+
+ def readAllProperties(self):
+ """
+ Reads all properties of this resource.
+
+ @return: a map from property names to DOM Element or String values.
+ """
+ response = self.connection.allprops(self.path, depth=0)
+ return response.msr.values()[0]
+
+ def readAllPropertyNames(self):
+ """
+ Returns the names of all properties attached to this resource.
+
+ @return: List of property names
+ """
+ response = self.connection.propnames(self.path, depth=0)
+ return response.msr.values()[0]
+
+ def readStandardProperties(self):
+ """
+ Read all WebDAV live properties.
+
+ @return: A L{LiveProperties} instance which contains a getter method for each live property.
+ """
+ body = createFindBody(LiveProperties.NAMES, Constants.NS_DAV)
+ response = self.connection.propfind(self.path, body, depth=0)
+ properties = response.msr.values()[0]
+ return LiveProperties(properties)
+
+ def writeProperties(self, properties, lockToken=None):
+ """
+ Sets or updates the given properties.
+
+ @param lockToken: if the resource has been locked this is the lock token.
+ @type lockToken: L{LockToken}
+ @param properties: a map from property names to a String or
+ DOM element value for each property to add or update.
+ """
+ assert isinstance(properties, types.DictType)
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ body = createUpdateBody(properties, self.defaultNamespace)
+ response = self.connection.proppatch(self.path, body, header)
+ if response.msr.errorCount > 0:
+ raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+ def deleteProperties(self, lockToken=None, *names):
+ """
+ Removes the given properties from this resource.
+
+ @param lockToken: if the resource has been locked this is the lock token.
+ @type lockToken: L{LockToken}
+ @param names: a collection of property names.
+ A property name is a (XmlNameSpace, propertyName) tuple.
+ """
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ body = createDeleteBody(names, self.defaultNamespace)
+ response = self.connection.proppatch(self.path, body, header)
+ if response.msr.errorCount > 0:
+ raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+ # ACP extension
+ def setAcl(self, acl, lockToken=None):
+ """
+ Sets ACEs in the non-inherited and non-protected ACL or the resource.
+ This is the implementation of the ACL method of the WebDAV ACP.
+
+ @param acl: ACL to be set on resource as ACL object.
+ @param lockToken: If the resource has been locked this is the lock token (defaults to None).
+ @type lockToken: L{LockToken}
+ """
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ headers = {}
+ if lockToken:
+ headers = lockToken.toHeader()
+ headers['Content-Type'] = XML_CONTENT_TYPE
+ body = acl.toXML()
+ response = self.connection._request('ACL', self.path, body, headers)
+ return response
+ ## TODO: parse DAV:error response
+
+ def getAcl(self):
+ """
+ Returns this resource's ACL in an ACL instance.
+
+ @return: Access Control List.
+ @rtype: L{ACL<webdav.acp.Acl.ACL>}
+ """
+ xmlAcl = self.readProperty(Constants.NS_DAV, Constants.TAG_ACL)
+ return ACL(xmlAcl)
+
+ def getCurrentUserPrivileges(self):
+ """
+ Returns a tuple of the current user privileges.
+
+ @return: list of Privilege instances
+ @rtype: list of L{Privilege<webdav.acp.Privilege.Privilege>}
+ """
+ privileges = self.readProperty(Constants.NS_DAV, Constants.PROP_CURRENT_USER_PRIVILEGE_SET)
+ result = []
+ for child in privileges.children:
+ result.append(Privilege(domroot=child))
+ return result
+
+ def getPrincipalCollections(self):
+ """
+ Returns a list principal collection URLs.
+
+ @return: list of principal collection URLs
+ @rtype: C{list} of C{unicode} elements
+ """
+ webdavQueryResult = self.readProperty(Constants.NS_DAV, Constants.PROP_PRINCIPAL_COLLECTION_SET)
+ principalCollectionList = []
+ for child in webdavQueryResult.children:
+ principalCollectionList.append(child.first_cdata)
+ return principalCollectionList
+
+ def getOwnerUrl(self):
+ """ Explicitly retireve the Url of the owner. """
+
+ result = self.readProperty(Constants.NS_DAV, Constants.PROP_OWNER)
+ if result and len(result.children):
+ return result.children[0].textof()
+ return None
+
+class CollectionStorer(ResourceStorer):
+ """
+ This class provides client access to a WebDAV collection resource identified by an URI.
+ This class does not cache resource data. This has to be performed by its clients.
+
+ @author: Roland Betz
+ """
+
+ def __init__(self, url, connection=None, validateResourceNames=True):
+ """
+ Creates a CollectionStorer instance for a URL and an optional Connection object.
+ User must invoke validate() after constuction to check the resource on the server.
+
+ @see: L{webdav.WebdavClient.ResourceStorer.__init__}
+ @param url: unique resource location for this storer
+ @param connection: this optional parameter contains a Connection object for the host part
+ of the given URL. Passing a connection saves memory by sharing this connection.
+ """
+ if url[-1] != '/': # Collection URL must end with slash
+ url += '/'
+ ResourceStorer.__init__(self, url, connection, validateResourceNames)
+
+ def getResourceStorer(self, name):
+ """
+ Return a ResourceStorer instance for a child resource (member) of this Collection.
+
+ @param name: leaf name of child resource
+ @return: L{ResourceStorer} instance
+ """
+ assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+ return ResourceStorer(self.url + name, self.connection, self.validateResourceNames)
+
+ def validate(self):
+ """
+ Check whether this URL contains a WebDAV collection.
+ Uses the WebDAV OPTION method.
+
+ @raise WebdavError: L{WebdavError} if URL does not contain a WebDAV collection resource.
+ """
+ super(CollectionStorer, self).validate()
+ isCollection = self.readProperty(Constants.NS_DAV, Constants.PROP_RESOURCE_TYPE)
+ if not (isCollection and isCollection.children):
+ raise WebdavError("Not a collection URL.", 0)
+
+ def addCollection(self, name, lockToken=None):
+ """
+ Make a new WebDAV collection resource within this collection.
+
+ @param name: of the new collection
+ @param lockToken: None or token returned by last lock operation
+ @type lockToken: L{LockToken}
+ """
+ assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ if self.validateResourceNames:
+ validateResourceName(name)
+ if name[-1] != '/': # Collection URL must end with slash
+ name += '/'
+ self.connection.mkcol(self.path + name, header)
+ return CollectionStorer(self.url + name, self.connection, self.validateResourceNames)
+
+ def addResource(self, name, content=None, properties=None, lockToken=None):
+ """
+ Create a new empty WebDAV resource contained in this collection with the given
+ properties.
+
+ @param name: leaf name of the new resource
+ @param content: None or initial binary content of resource
+ @param properties: name/value-map containing properties
+ @param lockToken: None or token returned by last lock operation
+ @type lockToken: L{LockToken}
+ """
+ assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ if self.validateResourceNames:
+ validateResourceName(name) # check for invalid characters
+ resource_ = ResourceStorer(self.url + name, self.connection, self.validateResourceNames)
+ resource_.uploadContent(content, lockToken)
+ if properties:
+ resource_.writeProperties(properties, lockToken)
+ return resource_
+
+ def deleteResource(self, name, lockToken=None):
+ """
+ Delete a collection which is contained within this collection
+
+ @param name: leaf name of a contained collection resource
+ @param lockToken: None or token returned by last lock operation
+ @type lockToken: L{LockToken}
+ """
+ assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+ assert lockToken == None or isinstance(lockToken, LockToken), \
+ "Invalid lockToken argument %s" % type(lockToken)
+ header = {}
+ if lockToken:
+ header = lockToken.toHeader()
+ if self.validateResourceNames:
+ validateResourceName(name)
+ response = self.connection.delete(self.path + name, header)
+ if response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+ raise WebdavError("Request failed: %s" % response.msr.reason, response.msr.code)
+
+ def lockAll(self, owner):
+ """
+ Locks this collection resource for exclusive write access. This means that for
+ succeeding write operations the returned lock token has to be passed.
+ The operation is applied recursively to all contained resources.
+ If the methode does not throw an exception then the lock has been granted.
+
+ @param owner: describes the lock holder
+ @return: Lock token string (automatically generated).
+ @rtype: L{LockToken}
+ """
+ assert isinstance(owner, types.StringType) or isinstance(owner, types.UnicodeType)
+ response = self.connection.lock(self.path, owner, depth=Constants.HTTP_HEADER_DEPTH_INFINITY)
+ return LockToken(self.url, response.locktoken)
+
+ def listResources(self):
+ """
+ Describe all members within this collection.
+
+ @return: map from URI to a L{LiveProperties} instance containing the WebDAV
+ live attributes of the contained resource
+ """
+ # *LiveProperties.NAMES denotes the list of all live properties as an
+ # argument to the method call.
+ response = self.connection.getprops(self.path,
+ depth=1,
+ ns=Constants.NS_DAV,
+ *LiveProperties.NAMES)
+ result = {}
+ for path, properties in response.msr.items():
+ if path == self.path: # omit this collection resource
+ continue
+ ## some servers do not append a trailing slash to collection paths
+ if self.path.endswith('/') and self.path[0:-1] == path:
+ continue
+ result[path] = LiveProperties(properties=properties)
+ return result
+
+ def getCollectionContents(self):
+ """
+ Return a list of the tuple (resources or collection) / properties)
+
+ @return: a list of the tuple (resources or collection) / properties)
+ @rtype: C{list}
+ """
+ self.validate()
+ collectionContents = []
+ result = self.listResources()
+ for url, properties_ in result.items():
+ if not self.path == url:
+ if properties_.getResourceType() == 'resource':
+ myWebDavStorer = ResourceStorer(url, self.connection, self.validateResourceNames)
+ else:
+ myWebDavStorer = CollectionStorer(url, self.connection, self.validateResourceNames)
+ collectionContents.append((myWebDavStorer, properties_))
+ return collectionContents
+
+ def findProperties(self, *names):
+ """
+ Retrieve given properties for this collection and all directly contained resources.
+
+ @param names: a list of property names
+ @return: a map from resource URI to a map from property name to value.
+ """
+ assert isinstance(names, types.ListType) or isinstance(names, types.TupleType), \
+ "Argument name has type %s" % str(type(names))
+ body = createFindBody(names, self.defaultNamespace)
+ response = self.connection.propfind(self.path, body, depth=1)
+ return response.msr
+
+ def deepFindProperties(self, *names):
+ """
+ Retrieve given properties for this collection and all contained (nested) resources.
+
+ Note:
+ =====
+ This operation can take a long time if used with recursive=true and is therefore
+ disabled on some WebDAV servers.
+
+ @param names: a list of property names
+ @return: a map from resource URI to a map from property name to value.
+ """
+ assert isinstance(names, types.ListType.__class__) or isinstance(names, types.TupleType), \
+ "Argument name has type %s" % str(type(names))
+ body = createFindBody(names, self.defaultNamespace)
+ response = self.connection.propfind(self.path, body, depth=Constants.HTTP_HEADER_DEPTH_INFINITY)
+ return response.msr
+
+ def findAllProperties(self):
+ """
+ Retrieve all properties for this collection and all directly contained resources.
+
+ @return: a map from resource URI to a map from property name to value.
+ """
+ response = self.connection.allprops(self.path, depth=1)
+ return response.msr
+
+
+ # DASL extension
+ def search(self, conditions, selects):
+ """
+ Search for contained resources which match the given search condition.
+
+ @param conditions: tree of ConditionTerm instances representing a logical search term
+ @param selects: list of property names to retrieve for the found resources
+ """
+ assert isinstance(conditions, ConditionTerm)
+ headers = { 'Content-Type' : XML_CONTENT_TYPE, "depth": Constants.HTTP_HEADER_DEPTH_INFINITY}
+ body = createSearchBody(selects, self.path, conditions)
+ response = self.connection._request('SEARCH', self.path, body, headers)
+ return response.msr
+
+
+class LockToken(object):
+ """
+ This class provides help on handling WebDAV lock tokens.
+
+ @author: Roland Betz
+ """
+ # restrict instance variables
+ __slots__ = ('url', 'token')
+
+ def __init__(self, url, token):
+ assert isinstance(url, types.StringType) or isinstance(url, types.UnicodeType), \
+ "Invalid url argument %s" % type(url)
+ assert isinstance(token, types.StringType) or isinstance(token, types.UnicodeType), \
+ "Invalid lockToken argument %s" % type(token)
+ self.url = url
+ self.token = token
+
+ def value(self):
+ """
+ Descriptive string containing the lock token's URL and the token itself.
+
+ @return: Descriptive lock token with URL.
+ @rtype: C{string}
+ """
+ return "<" + self.url + "> (<" + self.token + ">)"
+
+ def toHeader(self):
+ """
+ Header fragment for WebDAV request.
+
+ @return: Dictionary containing an entry for the lock token query.
+ @rtype: C{dictionary}
+ """
+ return {Constants.HTTP_HEADER_IF: self.value()}
+
+ def __str__(self):
+ return self.value()
+
+
+def _blockCopyFile(source, dest, blockSize):
+ """
+ Copies a file in chunks of C{blockSize}.
+
+ @param source: Source file.
+ @type source: FileIO buffer.
+ @param dest: Destination file.
+ @type dest: FileIO buffer.
+ @param blockSize: Size of block in bytes.
+ @type blockSize: C{int}
+ """
+ transferedBytes = 0
+ block = source.read(blockSize)
+ while len(block):
+ dest.write(block)
+ transferedBytes += len(block);
+ block = source.read(blockSize)
+
+def _checkUrl(url):
+ """
+ Checks the given URL for validity.
+
+ @param url: URL to check.
+ @type url: C{string}
+
+ @raise ValueError: If the URL does not contain valid/usable content.
+ """
+
+ parts = urlsplit(url, allow_fragments=False)
+ if len(parts[0]) == 0 or len(parts[1]) == 0 or len(parts[2]) == 0:
+ raise ValueError("Invalid URL: " + repr(url))
diff --git a/src/webdav/WebdavRequests.py b/src/webdav/WebdavRequests.py
new file mode 100644
index 0000000..79e586a
--- /dev/null
+++ b/src/webdav/WebdavRequests.py
@@ -0,0 +1,205 @@
+# pylint: disable-msg=W0511,W0212,E1111
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+This module handles WebDav server requests.
+"""
+
+
+import types
+from webdav import Constants
+import qp_xml
+from tempfile import TemporaryFile
+
+from davlib import XML_DOC_HEADER
+
+from webdav.NameCheck import validatePropertyName
+
+
+__version__ = "$LastChangedRevision$"
+
+
+## TODO: create a property list class
+
+class XmlNameSpaceMangler(object):
+ '''
+ Handles WebDav requests.
+ '''
+
+ # restrict instance variables
+ __slots__ = ('shortcuts', 'defaultNameSpace')
+
+ def __init__(self, nameList, defaultNameSpace = None):
+ '''
+
+ @param nameList:
+ @param defaultNameSpace:
+ '''
+
+ assert isinstance(nameList, types.ListType) or isinstance(nameList, types.TupleType), \
+ "1. argument has wrong type %s" % type(nameList)
+ self.shortcuts = {}
+ self.defaultNameSpace = defaultNameSpace
+ for name in nameList:
+ if not isinstance(name, types.TupleType):
+ name = (defaultNameSpace, name)
+ assert isinstance(name, types.TupleType) and len(name) == 2, \
+ "Name is not a namespace, name tuple: %s" % type(name)
+ validatePropertyName(name[1])
+ if name[0] and not self.shortcuts.has_key(name[0]):
+ self.shortcuts[name[0]] = 'ns%d' % len(self.shortcuts)
+
+ def getNameSpaces(self):
+ '''
+ Returns the namespace.
+ '''
+
+ result = ""
+ for namespace, short in self.shortcuts.items():
+ result += ' xmlns:%s="%s"' % (short, namespace)
+ return result
+
+ def getUpdateElements(self, valueMap):
+ '''
+
+ @param valueMap:
+ '''
+
+ elements = ""
+ for name in valueMap.keys():
+ fullname = name
+ if isinstance(name, types.StringType):
+ fullname = (self.defaultNameSpace, name)
+ if not fullname[0]:
+ tag = fullname[1]
+ else:
+ tag = self.shortcuts[fullname[0]] + ':' + fullname[1]
+ value = valueMap[name]
+ if value:
+ if isinstance(value, qp_xml._element):
+ tmpFile = TemporaryFile('w+')
+ value = qp_xml.dump(tmpFile, value)
+ tmpFile.flush()
+ tmpFile.seek(0)
+ tmpFile.readline()
+ value = tmpFile.read()
+ else:
+ value = "<![CDATA[%s]]>" % value
+ else:
+ value = ""
+ elements += "<%s>%s</%s>" % (tag, value, tag)
+ return elements
+
+ def getNameElements(self, nameList):
+ '''
+
+ @param nameList:
+ '''
+
+ elements = ""
+ for name in nameList:
+ if isinstance(name, types.StringType):
+ name = (self.defaultNameSpace, name)
+ if not name[0]:
+ tag = name[1]
+ else:
+ tag = self.shortcuts[name[0]] + ':' + name[1]
+ elements += "<%s />" % tag
+ return elements
+
+
+
+def createUpdateBody(propertyDict, defaultNameSpace = None):
+ '''
+
+ @param propertyDict:
+ @param defaultNameSpace:
+ '''
+
+ updateTag = 'D:' + Constants.TAG_PROPERTY_UPDATE
+ setTag = 'D:' + Constants.TAG_PROPERTY_SET
+ propTag = 'D:' + Constants.TAG_PROP
+ mangler = XmlNameSpaceMangler(propertyDict.keys(), defaultNameSpace)
+ return XML_DOC_HEADER + \
+ '<%s xmlns:D="DAV:"><%s><%s %s>' % (updateTag, setTag, propTag, mangler.getNameSpaces()) + \
+ mangler.getUpdateElements(propertyDict) + \
+ '</%s></%s></%s>' % (propTag, setTag, updateTag)
+
+
+def createDeleteBody(nameList, defaultNameSpace = None):
+ '''
+
+ @param nameList:
+ @param defaultNameSpace:
+ '''
+
+ updateTag = 'D:' + Constants.TAG_PROPERTY_UPDATE
+ removeTag = 'D:' + Constants.TAG_PROPERTY_REMOVE
+ propTag = 'D:' + Constants.TAG_PROP
+ mangler = XmlNameSpaceMangler(nameList, defaultNameSpace)
+ return XML_DOC_HEADER + \
+ '<%s xmlns:D="DAV:"><%s><%s %s>' % (updateTag, removeTag, propTag, mangler.getNameSpaces()) + \
+ mangler.getNameElements(nameList) + \
+ '</%s></%s></%s>' % (propTag, removeTag, updateTag)
+
+
+def createFindBody(nameList, defaultNameSpace = None):
+ '''
+
+ @param nameList:
+ @param defaultNameSpace:
+ '''
+
+ findTag = 'D:' + Constants.TAG_PROPERTY_FIND
+ propTag = 'D:' + Constants.TAG_PROP
+ mangler = XmlNameSpaceMangler(nameList, defaultNameSpace)
+ return XML_DOC_HEADER + \
+ '<%s xmlns:D="DAV:"><%s %s>' % (findTag, propTag, mangler.getNameSpaces()) + \
+ mangler.getNameElements(nameList) + \
+ '</%s></%s>' % (propTag, findTag)
+
+
+def createSearchBody(selects, path, conditions, defaultNameSpace = None):
+ '''
+ Creates DASL XML body.
+
+ @param selects: list of property names to retrieve for the found resources
+ @param path: list of conditions
+ @param conditions: tree of ConditionTerm instances representing a logical search term
+ @param defaultNameSpace: default namespace
+ '''
+
+ searchTag = 'D:' + Constants.TAG_SEARCH_REQUEST
+ basicTag = 'D:' + Constants.TAG_SEARCH_BASIC
+ selectTag = 'D:' + Constants.TAG_SEARCH_SELECT
+ fromTag = 'D:' + Constants.TAG_SEARCH_FROM
+ scopeTag = 'D:' + Constants.TAG_SEARCH_SCOPE
+ whereTag = 'D:' + Constants.TAG_SEARCH_WHERE
+ propTag = 'D:' + Constants.TAG_PROP
+ hrefTag = 'D:' + Constants.TAG_HREF
+ depthTag = 'D:' + Constants.TAG_LOCK_DEPTH
+ depthValue = Constants.HTTP_HEADER_DEPTH_INFINITY
+ mangler = XmlNameSpaceMangler(selects, defaultNameSpace)
+ return XML_DOC_HEADER + \
+ '<%s xmlns:D="DAV:"><%s>' % (searchTag, basicTag) + \
+ '<%s><%s %s>%s</%s></%s>' % (selectTag, propTag, mangler.getNameSpaces(),
+ mangler.getNameElements(selects), propTag, selectTag) + \
+ '<%s><%s><%s>%s</%s><%s>%s</%s></%s></%s>' % (fromTag, scopeTag, hrefTag, path, hrefTag,
+ depthTag, depthValue, depthTag, scopeTag, fromTag) + \
+ '<%s>%s</%s>' % (whereTag, conditions.toXML(),whereTag) + \
+ '</%s></%s>' % (basicTag, searchTag)
+ \ No newline at end of file
diff --git a/src/webdav/WebdavResponse.py b/src/webdav/WebdavResponse.py
new file mode 100644
index 0000000..c84943d
--- /dev/null
+++ b/src/webdav/WebdavResponse.py
@@ -0,0 +1,525 @@
+# pylint: disable-msg=R0903,W0142,W0221,W0212,W0104,W0511,C0103,R0901
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handles WebDAV responses.
+"""
+
+
+from davlib import _parse_status
+import qp_xml
+from webdav import Constants
+import time
+import rfc822
+import urllib
+# Handling Jython 2.5 bug concerning the date pattern
+# conversion in time.strptime
+try:
+ from java.lang import IllegalArgumentException
+except ImportError:
+ class IllegalArgumentException(object):
+ pass
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class HttpStatus(object):
+ """
+ TBD
+
+ @ivar code:
+ @type code:
+ @ivar reason:
+ @type reason:
+ @ivar errorCount:
+ @type errorCount: int
+ """
+
+ def __init__(self, elem):
+ """
+ TBD
+
+ @param elem: ...
+ @type elem: instance of L{Element}
+ """
+ self.code, self.reason = _parse_status(elem)
+ self.errorCount = (self.code >= Constants.CODE_LOWEST_ERROR)
+ def __str__(self):
+ return "HTTP status %d: %s" % (self.code, self.reason)
+
+
+class MultiStatusResponse(dict):
+ """
+ TBD
+
+ @ivar status:
+ @type status:
+ @ivar reason:
+ @type reason:
+ @ivar errorCount:
+ @type errorCount:
+ """
+
+ # restrict instance variables
+ __slots__ = ('errorCount', 'reason', 'status')
+
+ def __init__(self, domroot):
+ dict.__init__(self)
+ self.errorCount = 0
+ self.reason = None
+ self.status = Constants.CODE_MULTISTATUS
+ if (domroot.ns != Constants.NS_DAV) or (domroot.name != Constants.TAG_MULTISTATUS):
+ raise ResponseFormatError(domroot, 'Invalid response: <DAV:multistatus> expected.')
+ self._scan(domroot)
+
+ def getCode(self):
+ if self.errorCount == 0:
+ return Constants.CODE_SUCCEEDED
+ if len(self) > self.errorCount:
+ return Constants.CODE_MULTISTATUS
+ return self.values()[0].code
+
+ def getReason(self):
+ result = ""
+ for response in self.values():
+ if response.code > Constants.CODE_LOWEST_ERROR:
+ result += response.reason
+ return result
+
+ def __str__(self):
+ result = ""
+ for key, value in self.items():
+ if isinstance(value, PropertyResponse):
+ result += "Resource at %s has %d properties and %d errors.\n" % (key, len(value), value.errorCount)
+ else:
+ result += "Resource at %s returned " % key + str(value)
+ return result
+
+ def _scan(self, root):
+ for child in root.children:
+ if child.ns != Constants.NS_DAV:
+ continue
+ if child.name == Constants.TAG_RESPONSEDESCRIPTION:
+ self.reason = child.textof()
+ elif child.name == Constants.TAG_RESPONSE:
+ self._scanResponse(child)
+ ### unknown child element
+
+ def _scanResponse(self, elem):
+ hrefs = []
+ response = None
+ for child in elem.children:
+ if child.ns != Constants.NS_DAV:
+ continue
+ if child.name == Constants.TAG_HREF:
+ try:
+ href = _unquoteHref(child.textof())
+ except UnicodeDecodeError:
+ raise ResponseFormatError(child, "Invalid 'href' data encoding.")
+ hrefs.append(href)
+ elif child.name == Constants.TAG_STATUS:
+ self._scanStatus(child, *hrefs)
+ elif child.name == Constants.TAG_PROPERTY_STATUS:
+ if not response:
+ if len(hrefs) != 1:
+ raise ResponseFormatError(child, 'Invalid response: One <DAV:href> expected.')
+ response = PropertyResponse()
+ self[hrefs[0]] = response
+ response._scan(child)
+ elif child.name == Constants.TAG_RESPONSEDESCRIPTION:
+ for href in hrefs:
+ self[href].reasons.append(child.textOf())
+ ### unknown child element
+ if response and response.errorCount > 0:
+ self.errorCount += 1
+
+ def _scanStatus(self, elem, *hrefs):
+ if len(hrefs) == 0:
+ raise ResponseFormatError(elem, 'Invalid response: <DAV:href> expected.')
+ status = HttpStatus(elem)
+ for href in hrefs:
+ self[href] = status
+ if status.errorCount:
+ self.errorCount += 1
+
+ # Instance properties
+ code = property(getCode, None, None, "HTTP response code")
+
+
+
+class PropertyResponse(dict):
+ """
+ TBD
+
+ @ivar errors:
+ @type errors: list of ...
+ @ivar reasons:
+ @type reasons: list of ...
+ @ivar failedProperties:
+ @type failedProperties: dict of ...
+ """
+
+ # restrict instance variables
+ __slots__ = ('errors', 'reasons', 'failedProperties')
+
+ def __init__(self):
+ dict.__init__(self)
+ self.errors = []
+ self.reasons = []
+ self.failedProperties = {}
+
+ def __str__(self):
+ result = ""
+ for value in self.values():
+ result += value.name + '= ' + value.textof() + '\n'
+ result += self.getReason()
+ return result
+
+ def getCode(self):
+ if len(self.errors) == 0:
+ return Constants.CODE_SUCCEEDED
+ if len(self) > 0:
+ return Constants.CODE_MULTISTATUS
+ return self.errors[-1].code
+
+ def getReason(self):
+ result = ""
+ if len(self.errors) > 0:
+ result = "Failed for: " + repr(self.failedProperties.keys()) + "\n"
+ for error in self.errors:
+ result += "%s (%d). " % (error.reason, error.code)
+ for reason in self.reasons:
+ result += "%s. " % reason
+ return result
+
+ def _scan(self, element):
+ status = None
+ statusElement = element.find(Constants.TAG_STATUS, Constants.NS_DAV)
+ if statusElement:
+ status = HttpStatus(statusElement)
+ if status.errorCount:
+ self.errors.append(status)
+
+ propElement = element.find(Constants.TAG_PROP, Constants.NS_DAV)
+ if propElement:
+ for prop in propElement.children:
+ if status.errorCount:
+ self.failedProperties[(prop.ns, prop.name)]= status
+ else:
+ prop.__class__ = Element # bad, bad trick
+ self[prop.fullname] = prop
+ reasonElement = element.find(Constants.TAG_RESPONSEDESCRIPTION, Constants.NS_DAV)
+ if reasonElement:
+ self.reasons.append(reasonElement.textOf())
+
+ # Instance properties
+ code = property(getCode, None, None, "HTTP response code")
+ errorCount = property(lambda self: len(self.errors), None, None, "HTTP response code")
+ reason = property(getReason, None, None, "HTTP response code")
+
+
+
+
+class LiveProperties(object):
+ """
+ This class provides convenient access to the WebDAV 'live' properties of a resource.
+ WebDav 'live' properties are defined in RFC 2518, Section 13.
+ Each property is converted from string to its natural data type.
+
+ @version: $Revision$
+ @author: Roland Betz
+ """
+
+ # restrict instance variables
+ __slots__ = ('properties')
+
+ NAMES = (Constants.PROP_CREATION_DATE, Constants.PROP_DISPLAY_NAME,
+ Constants.PROP_CONTENT_LENGTH, Constants.PROP_CONTENT_TYPE, Constants.PROP_ETAG,
+ Constants.PROP_LAST_MODIFIED, Constants.PROP_OWNER,
+ Constants.PROP_LOCK_DISCOVERY, Constants.PROP_RESOURCE_TYPE, Constants.PROP_SUPPORTED_LOCK )
+
+ def __init__(self, properties=None, propElement=None):
+ """
+ Construct <code>StandardProperties</code> from a map of properties containing
+ live properties or from a XML 'prop' element containing live properties
+
+ @param properties: map as implemented by class L{PropertyResponse}
+ @param propElement: an C{Element} value
+ """
+ assert isinstance(properties, PropertyResponse) or \
+ isinstance(propElement, qp_xml._element), \
+ "Argument properties has type %s" % str(type(properties))
+ self.properties = {}
+ for name, value in properties.items():
+ if name[0] == Constants.NS_DAV and name[1] in self.NAMES:
+ self.properties[name[1]] = value
+
+ def getContentLanguage(self):
+ """
+ Return the language of a resource's textual content or null
+
+ @return: string
+ """
+
+ result = ""
+ if not self.properties.get(Constants.PROP_CONTENT_LANGUAGE, None) is None:
+ result = self.properties.get(Constants.PROP_CONTENT_LANGUAGE).textof()
+ return result
+
+ def getContentLength(self):
+ """
+ Returns the length of the resource's content in bytes.
+
+ @return: number of bytes
+ """
+
+ result = 0
+ if not self.properties.get(Constants.PROP_CONTENT_LENGTH, None) is None:
+ result = int(self.properties.get(Constants.PROP_CONTENT_LENGTH).textof())
+ return result
+
+ def getContentType(self):
+ """
+ Return the resource's content MIME type.
+
+ @return: MIME type string
+ """
+
+ result = ""
+ if not self.properties.get(Constants.PROP_CONTENT_TYPE, None) is None:
+ result = self.properties.get(Constants.PROP_CONTENT_TYPE).textof()
+ return result
+
+ def getCreationDate(self):
+ """
+ Return date of creation as time tuple.
+
+ @return: time tuple
+ @rtype: C{time.struct_time}
+
+ @raise ValueError: If string is not in the expected format (ISO 8601).
+ """
+
+ datetimeString = ""
+ if not self.properties.get(Constants.PROP_CREATION_DATE, None) is None:
+ datetimeString = self.properties.get(Constants.PROP_CREATION_DATE).textof()
+
+ result = rfc822.parsedate(datetimeString)
+ if result is None:
+ result = _parseIso8601String(datetimeString)
+
+ return time.mktime(result)
+
+ def getEntityTag(self):
+ """
+ Return a entity tag which is unique for a particular version of a resource.
+ Different resources or one resource before and after modification have different etags.
+
+ @return: entity tag string
+ """
+
+ result = ""
+ if not self.properties.get(Constants.PROP_ETAG, None) is None:
+ result = self.properties.get(Constants.PROP_ETAG).textof()
+ return result
+
+ def getDisplayName(self):
+ """
+ Returns a resource's display name.
+
+ @return: string
+ """
+
+ result = ""
+ if not self.properties.get(Constants.PROP_DISPLAY_NAME, None) is None:
+ result = self.properties.get(Constants.PROP_DISPLAY_NAME).textof()
+ return result
+
+ def getLastModified(self):
+ """
+ Return last modification of resource as time tuple.
+
+ @return: Modification date time.
+ @rtype: C{time.struct_time}
+
+ @raise ValueError: If the date time string is not in the expected format (RFC 822 / ISO 8601).
+ """
+
+ datetimeString = None
+ if not self.properties.get(Constants.PROP_LAST_MODIFIED, None) is None:
+ datetimeString = self.properties.get(Constants.PROP_LAST_MODIFIED).textof()
+ result = rfc822.parsedate(datetimeString)
+ if result is None:
+ result = _parseIso8601String(datetimeString)
+ return time.mktime(result)
+
+ def getLockDiscovery(self):
+ """
+ Return all current lock's applied to a resource or null if it is not locked.
+
+ @return: a lockdiscovery DOM element according to RFC 2815
+ """
+
+ xml = self.properties.get(Constants.PROP_LOCK_DISCOVERY)
+ return _scanLockDiscovery(xml)
+
+ def getResourceType(self):
+ """
+ Return a resource's WebDAV type.
+
+ @return: 'collection' or 'resource'
+ """
+
+ xml = self.properties.get(Constants.PROP_RESOURCE_TYPE)
+ if xml and xml.children:
+ return xml.children[0].name
+ return "resource"
+
+ def getSupportedLock(self):
+ """
+ Return a DOM element describing all supported lock options for a resource.
+ Usually this is shared and exclusive write lock.
+
+ @return: supportedlock DOM element according to RFC 2815
+ """
+
+ xml = self.properties.get(Constants.PROP_SUPPORTED_LOCK)
+ return xml
+
+ def getOwnerAsUrl(self):
+ """
+ Return a resource's owner in form of a URL.
+
+ @return: string
+ """
+
+ xml = self.properties.get(Constants.PROP_OWNER)
+ if xml and len(xml.children):
+ return xml.children[0].textof()
+ return None
+
+ def __str__(self):
+ result = ""
+ result += " Name=" + self.getDisplayName()
+ result += "\n Type=" + self.getResourceType()
+ result += "\n Length=" + str(self.getContentLength())
+ result += "\n Content Type="+ self.getContentType()
+ result += "\n ETag=" + self.getEntityTag()
+ result += "\n Created=" + time.strftime("%c GMT", self.getCreationDate())
+ result += "\n Modified=" + time.strftime("%c GMT", self.getLastModified())
+ return result
+
+
+def _parseIso8601String(date):
+ """
+ Parses the given ISO 8601 string and returns a time tuple.
+ The strings should be formatted according to RFC 3339 (see section 5.6).
+ But currently there are two exceptions:
+ 1. Time offset is limited to "Z".
+ 2. Fragments of seconds are ignored.
+ """
+
+ if "." in date and "Z" in date: # Contains fragments of second?
+ secondFragmentPos = date.rfind(".")
+ timeOffsetPos = date.rfind("Z")
+ date = date[:secondFragmentPos] + date[timeOffsetPos:]
+ try:
+ timeTuple = time.strptime(date, Constants.DATE_FORMAT_ISO8601)
+ except IllegalArgumentException: # Handling Jython 2.5 bug concerning the date pattern accordingly
+ import _strptime # Using the Jython fall back solution directly
+ timeTuple = _strptime.strptime(date, Constants.DATE_FORMAT_ISO8601)
+ return timeTuple
+
+
+class ResponseFormatError(IOError):
+ """
+ An instance of this class is raised when the web server returned a webdav
+ reply which does not adhere to the standard and cannot be recognized.
+ """
+ def __init__(self, element, message= None):
+ IOError.__init__(self, "ResponseFormatError at element %s: %s" % (element.name, message))
+ self.element = element
+ self.message = message
+
+
+class Element(qp_xml._element):
+ """
+ This class improves the DOM interface (i.e. element interface) provided by the qp_xml module
+ TODO: substitute qp_xml by 'real' implementation. e.g. domlette
+ """
+ def __init__(self, namespace, name, cdata=''):
+ qp_xml._element.__init__(self, ns=namespace, name=name, lang=None, parent=None,
+ children=[], ns_scope={}, attrs={},
+ first_cdata=cdata, following_cdata='')
+
+ def __str__(self):
+ return self.textof()
+
+ def __getattr__(self, name):
+ if (name == 'fullname'):
+ return (self.__dict__['ns'], self.__dict__['name'])
+ raise AttributeError, name
+
+ def add(self, child):
+ self.children.append(child)
+ return child
+
+def _scanLockDiscovery(root):
+ assert root.name == Constants.PROP_LOCK_DISCOVERY, "Invalid lock discovery XML element"
+ active = root.find(Constants.TAG_ACTIVE_LOCK, Constants.NS_DAV)
+ if active:
+ return _scanActivelock(active)
+ return None
+
+def _scanActivelock(root):
+ assert root.name == Constants.TAG_ACTIVE_LOCK, "Invalid active lock XML element"
+ token = _scanOrError(root, Constants.TAG_LOCK_TOKEN)
+ value = _scanOrError(token, Constants.TAG_HREF)
+ owner = _scanOwner(root)
+ depth = _scanOrError(root, Constants.TAG_LOCK_DEPTH)
+ return (value.textof(), owner, depth.textof())
+
+def _scanOwner(root):
+ owner = root.find(Constants.TAG_LOCK_OWNER, Constants.NS_DAV)
+ if owner:
+ href = owner.find(Constants.TAG_HREF, Constants.NS_DAV)
+ if href:
+ return href.textof()
+ return owner.textof()
+ return None
+
+def _scanOrError(elem, childName):
+ child = elem.find(childName, Constants.NS_DAV)
+ if not child:
+ raise ResponseFormatError(elem, "Invalid response: <"+childName+"> expected")
+ return child
+
+
+def _unquoteHref(href):
+ #print "*** Response HREF=", repr(href)
+ if type(href) == type(u""):
+ try:
+ href = href.encode('ascii')
+ except UnicodeError: # URL contains unescaped non-ascii character
+ # handle bug in Tamino webdav server
+ return urllib.unquote(href)
+ href = urllib.unquote(href)
+ if Constants.CONFIG_UNICODE_URL:
+ return unicode(href, 'utf-8')
+ else:
+ return unicode(href, 'latin-1')
diff --git a/src/webdav/__init__.py b/src/webdav/__init__.py
new file mode 100644
index 0000000..3e46609
--- /dev/null
+++ b/src/webdav/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+__version__ = "$LastChangedRevision$"
diff --git a/src/webdav/acp/Ace.py b/src/webdav/acp/Ace.py
new file mode 100644
index 0000000..8321d41
--- /dev/null
+++ b/src/webdav/acp/Ace.py
@@ -0,0 +1,293 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+ACE object handling according to WebDAV ACP specification.
+"""
+
+
+from webdav.acp.Principal import Principal
+from webdav.acp.GrantDeny import GrantDeny
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class ACE(object):
+ """
+ This class provides functionality for handling ACEs
+
+ @ivar principal: A principal (user or group)
+ @type principal: L{Principal} object
+ @ivar invert: Flag indicating whether ACE should invert the principal.
+ @type invert: C{bool}
+ @ivar grantDenies: Grant or deny clauses for privileges
+ @type grantDenies: C{list} of L{GrantDeny} objects
+ @ivar protected: Flag indicating whether ACE is protected.
+ @type protected: C{bool}
+ @ivar inherited: URL indicating the source from where the ACE is inherited.
+ @type inherited: C{string}
+ """
+
+ # restrict instance variables
+ __slots__ = ('principal', 'invert', 'grantDenies', 'protected', 'inherited')
+
+ def __init__(self, domroot=None, principal=None, grantDenies=None):
+ """
+ Constructor should be called with either no parameters (create blank ACE),
+ one parameter (a DOM tree or principal), or two parameters (principal and
+ sequence of GrantDenies).
+
+ @param domroot: A DOM tree (default: None).
+ @type domroot: L{webdav.WebdavResponse.Element} object
+ @param principal: A principal (user or group), (default: None).
+ @type principal: L{Principal} object
+ @param grantDenies: Grant and deny clauses for privileges (default: None).
+ @type grantDenies: sequence of L{GrantDeny} objects
+
+ @raise WebdavError: When non-valid parameters are passed a L{WebdavError} is raised.
+ """
+ self.principal = Principal()
+ self.protected = None
+ self.inherited = None
+ self.invert = None
+ self.grantDenies = []
+
+ if domroot:
+ self.principal = Principal(domroot=domroot.find(Constants.TAG_PRINCIPAL, Constants.NS_DAV))
+ self.inherited = domroot.find(Constants.TAG_INHERITED, Constants.NS_DAV)
+ if self.inherited:
+ self.inherited = self.inherited.children[0].textof()
+ if domroot.find(Constants.TAG_PROTECTED, Constants.NS_DAV):
+ self.protected = 1
+ for child in domroot.children:
+ if child.ns == Constants.NS_DAV \
+ and (child.name == Constants.TAG_GRANT or child.name == Constants.TAG_DENY):
+ self.grantDenies.append(GrantDeny(domroot=child))
+ elif isinstance(principal, Principal):
+ newPrincipal = Principal()
+ newPrincipal.copy(principal)
+ self.principal = newPrincipal
+ if (isinstance(grantDenies, list) or isinstance(grantDenies, tuple)):
+ self.addGrantDenies(grantDenies)
+ elif domroot == None and grantDenies == None:
+ # no param ==> blank ACE
+ pass
+ else:
+ # This shouldn't happen, someone screwed up with the params ...
+ raise WebdavError('non-valid parameters handed to ACE constructor')
+
+ def __cmp__(self, other):
+ if not isinstance(other, ACE):
+ return 1
+ if self.principal == other.principal \
+ and self.invert == other.invert \
+ and self.protected == other.protected \
+ and self.inherited == other.inherited:
+ equal = 1
+ for grantDeny in self.grantDenies:
+ inList = 0
+ for otherGrantDeny in other.grantDenies:
+ if grantDeny == otherGrantDeny:
+ inList = 1
+ if inList == 0:
+ equal = 0
+ return not equal
+ else:
+ return 1
+
+ def __repr__(self):
+ repr = '<class ACE: '
+ if self.invert:
+ repr += 'inverted principal, ' % (self.invert)
+ if self.principal:
+ repr += 'principal: %s, ' % (self.principal)
+ if self.protected:
+ repr += 'protected, '
+ if self.inherited:
+ repr += 'inherited href: %s, ' % (self.inherited)
+ first = 1
+ repr += 'grantDenies: ['
+ for grantDeny in self.grantDenies:
+ if first:
+ repr += '%s' % grantDeny
+ first = 0
+ else:
+ repr += ', %s' % grantDeny
+ return '%s]>' % (repr)
+
+ def copy(self, other):
+ '''Copy an ACE object.
+
+ @param other: Another ACE to copy.
+ @type other: L{ACE} object
+
+ @raise WebdavError: When an object that is not an L{ACE} is passed
+ a L{WebdavError} is raised.
+ '''
+ if not isinstance(other, ACE):
+ raise WebdavError('Non-ACE object passed to copy method: %s.' % other.__class__)
+ self.invert = other.invert
+ self.protected = other.protected
+ self.inherited = other.inherited
+ self.principal = Principal()
+ if other.principal:
+ self.principal.copy(other.principal)
+ if other.grantDenies:
+ self.addGrantDenies(other.grantDenies)
+
+ def isValid(self):
+ """
+ Returns true/false (1/0) whether necessarry props
+ principal and grantDenies are set and whether the ACE contains one
+ grant or deny clauses.
+
+ @return: Validity of ACE.
+ @rtype: C{bool}
+ """
+ return self.principal and len(self.grantDenies) == 1
+
+ def isGrant(self):
+ '''
+ Returns true/false (1/0) if ACE contains only grant clauses.
+
+ @return: Value whether the ACE is of grant type.
+ @rtype: C{bool}
+ '''
+ if self.isMixed() or len(self.grantDenies) < 1:
+ return 0
+ else:
+ return self.grantDenies[0].isGrant()
+
+ def isDeny(self):
+ '''
+ Returns true/false (1/0) if ACE contains only deny clauses.
+
+ @return: Value whether the ACE is of deny type.
+ @rtype: C{bool}
+ '''
+ if self.isMixed() or len(self.grantDenies) < 1:
+ return 0
+ else:
+ return self.grantDenies[0].isDeny()
+
+ def isMixed(self):
+ '''
+ Returns true/false (1/0) if ACE contains both types (grant and deny) of clauses.
+
+ @return: Value whether the ACE is of mixed (grant and deny) type.
+ @rtype: C{bool}
+ '''
+ mixed = 0
+ if len(self.grantDenies):
+ first = self.grantDenies[0].grantDeny
+ for grantDeny in self.grantDenies:
+ if grantDeny.grantDeny != first:
+ mixed = 1
+ return mixed
+
+ def toXML(self, defaultNameSpace=None):
+ """
+ Returns ACE content as a string of valid XML as described in WebDAV ACP.
+
+ @param defaultNameSpace: Name space (default: None).
+ @type defaultNameSpace: C(string)
+ """
+ assert self.isValid(), "ACE is not initialized or does not contain valid content!"
+
+ ACE = 'D:' + Constants.TAG_ACE
+ res = self.principal.toXML(self.invert)
+ for grantDeny in self.grantDenies:
+ res += grantDeny.toXML()
+ if self.protected:
+ res += '<D:protected/>'
+ if self.inherited:
+ res += '<D:inherited><D:href>%s</D:href></D:inherited>' % (self.inherited)
+ return '<%s>%s</%s>' % (ACE, res, ACE)
+
+ def setPrincipal(self, principal):
+ '''
+ Sets the passed principal on the ACE.
+
+ @param principal: A principal.
+ @type principal: L{Principal} object
+ '''
+ self.principal = Principal()
+ self.principal.copy(principal)
+
+ def setInherited(self, href):
+ '''
+ Sets the passed URL on the ACE to denote from where it is inherited.
+
+ @param href: A URL.
+ @type href: C{string}
+ '''
+ self.inherited = href
+
+ def addGrantDeny(self, grantDeny):
+ '''
+ Adds the passed GrantDeny object to list if it's not in it, yet.
+
+ @param grantDeny: A grant or deny clause.
+ @type grantDeny: L{GrantDeny} object
+ '''
+ # only add it if it's not in the list, yet ...
+ inList = 0
+ for element in self.grantDenies:
+ if element == grantDeny:
+ inList = 1
+ if not inList:
+ newGrantDeny = GrantDeny()
+ newGrantDeny.copy(grantDeny)
+ self.grantDenies.append(newGrantDeny)
+
+ def addGrantDenies(self, grantDenies):
+ '''Adds the list of passed grant/deny objects to list.
+
+ @param grantDenies: Grant or deny clauses.
+ @type grantDenies: sequence of L{GrantDeny} objects
+ '''
+ map(lambda grantDeny: self.addGrantDeny(grantDeny), grantDenies)
+
+ def delGrantDeny(self, grantDeny):
+ '''Deletes the passed GrantDeny object from list.
+
+ @param grantDeny: A grant or deny clause.
+ @type grantDeny: L{GrantDeny} object
+
+ @raise WebdavError: A L{WebdavError} is raised if the clause to be
+ deleted is not present.
+ '''
+ # only add it if it's not in the list, yet ...
+ count = 0
+ index = 0
+ for element in self.grantDenies:
+ count += 1
+ if element == grantDeny:
+ index = count
+ if index:
+ self.grantDenies.pop(index - 1)
+ else:
+ raise WebdavError('GrantDeny to be deleted not in list: %s.' % grantDeny)
+
+ def delGrantDenies(self, grantDenies):
+ '''Deletes the list of passed grant/deny objects from list.
+
+ @param grantDenies: Grant or deny clauses.
+ @type grantDenies: sequence of L{GrantDeny} objects
+ '''
+ map(lambda grantDeny: self.delGrantDeny(grantDeny), grantDenies)
diff --git a/src/webdav/acp/AceHandler.py b/src/webdav/acp/AceHandler.py
new file mode 100644
index 0000000..e07b74d
--- /dev/null
+++ b/src/webdav/acp/AceHandler.py
@@ -0,0 +1,182 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling of WebDAV Access Protocol Extensions and ACL preparation for UI.
+"""
+
+
+from webdav import Constants
+from webdav.WebdavClient import ResourceStorer
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+def extractSupportedPrivilegeSet(userPrivileges):
+ """
+ Returns a dictionary of supported privileges.
+
+ @param userPrivileges: A DOM tree.
+ @type userPrivileges: L{webdav.WebdavResponse.Element} object
+
+ @raise WebdavError: When unknown elements appear in the
+ C{DAV:supported-privilege} appear a L{WebdavError} is raised.
+
+ @return: A dictionary with privilege names as keys and privilege descriptions as values.
+ @rtype: C{dictionary}
+ """
+ result = {}
+ for element in userPrivileges.children:
+ if element.name == Constants.TAG_SUPPORTED_PRIVILEGE:
+ privName = ''
+ privDescription = ''
+ for privilege in element.children:
+ if privilege.name == Constants.TAG_PRIVILEGE:
+ privName = privilege.children[0].name
+ elif privilege.name == Constants.TAG_DESCRIPTION:
+ privDescription = privilege.textof()
+ else:
+ raise WebdavError('Unknown element in DAV:supported-privilege: ' + privilege.name)
+
+ if privName and privDescription:
+ result[privName] = privDescription
+ privName = ''
+ privDescription = ''
+ else:
+ raise WebdavError('Invalid element tag in DAV:supported-privilege-set: ' + element.name)
+ return result
+
+
+def _insertAclDisplaynames(acl):
+ """
+ Modifies the ACL by adding the human readable names
+ (DAV:displayname property) of each principal found in an ACL.
+
+ This should be done with the REPORT method, but it is not supported by
+ Jacarta Slide, yet. (As of Aug. 1, 2003 in CVS repository)
+
+ So we are going to do it differently by foot the harder way ...
+
+ @param acl: An ACL object for which the displaynames should be retrieved.
+ @type acl: L{ACL} object
+ """
+ ## This is redundant code to be still kept for the REPORT method way of doing it ...
+ ## property = '''<D:prop><D:displayname/></D:prop>'''
+ ## return self.getReport(REPORT_ACL_PRINCIPAL_PROP_SET, property)
+ for ace in acl.aces:
+ if not ace.principal.property:
+ principalConnection = \
+ ResourceStorer(ace.principal.principalURL)
+ ace.principal.displayname = \
+ principalConnection.readProperty(Constants.NS_DAV, Constants.PROP_DISPLAY_NAME)
+
+
+def prepareAcls(acls):
+ """
+ Returns all ACLs describing the behaviour of the resource. The information
+ in the ACL is modified to contain all information needed to display in the UI.
+
+ @param acls: ACL objects.
+ @type acls: C{list} of L{ACL} objects
+
+ @return: (non-valid) ACLs that contain both grant and deny clauses in an ACE.
+ Displaynames are added to the Principals where needed.
+ @rtype: C{list} of L{ACL} objects
+ """
+ for acl in acls.keys():
+ acls[acl] = acls[acl].joinGrantDeny()
+ _insertAclDisplaynames(acls[acl])
+ return acls
+
+
+def prepareAcl(acl):
+ """
+ Returns an ACL describing the behaviour of the resource. The information
+ in the ACL is modified to contain all information needed to display in the UI.
+
+ @param acl: An ACL object.
+ @type acl: L{ACL} object
+
+ @return: A (non-valid) ACL that contains both grant and deny clauses in an ACE.
+ Displaynames are added to the Principals where needed.
+ @rtype: L{ACL} object
+ """
+ acl = acl.joinGrantDeny()
+ _insertAclDisplaynames(acl)
+ return acl
+
+
+def refineAclForSet(acl):
+ """
+ Sets the ACL composed from the UI on the WebDAV server. For that purpose the
+ ACL object gets refined first to form a well accepted ACL to be set by the
+ ACL WebDAV method.
+
+ @param acl: An ACL object to be refined.
+ @type acl: L{ACL} object
+
+ @return: A valid ACL that contains only grant or deny clauses in an ACE.
+ Inherited and protected ACEs are stripped out.
+ @rtype: L{ACL} object
+ """
+ acl = acl.splitGrantDeny()
+ acl = acl.stripAces()
+ return acl
+
+
+##~ unsupported or unfinished methods:
+##~
+##~ def report(self, report, request=None, lockToken=None):
+##~ """
+##~ This method implements the WebDAV ACP method: REPORT for given report
+##~ types.
+##~
+##~ Parameters:
+##~
+##~ 'report' -- Report type as a string.
+##~
+##~ 'request' -- XML content of the request for the report (defaults to None).
+##~
+##~ 'lockToken' -- Lock token to be set (defaults to None).
+##~ """
+##~ raise WebdavError('Reports are not supported by our Jacarta Slide, yet (as of Aug. 1, 2003 in CVS).')
+##~
+##~ headers = createCondition(lockToken)
+##~ headers['Content-Type'] = XML_CONTENT_TYPE
+##~ body = '<D:%s xmlns:D="DAV:">%s</D:%s>' % (report, request, report)
+##~ #print "Body: ", body
+##~ response = self.connection._request('REPORT', self.path, body, headers)
+##~ return response
+##~ ## TODO: parse DAV:error response
+##~
+##~
+##~ def getAllAcls(self):
+##~ """
+##~ Returns a dictionary of ACL resources with respective ACL objects
+##~ that apply to the given resource.
+##~
+##~ ### This method needs to be extended for inherited ACLs when Tamino
+##~ support tells me (Guy) how to get to them.
+##~ """
+##~ acls = {self.path: self.getAcl()}
+##~ for ace in acls[self.path].aces:
+##~ if ace.inherited:
+##~ if not ace.inherited in acls:
+##~ acls[ace.inherited] = self.getAcl()
+##~
+##~ # append some more stuff here to acls for possible inherited ACLs
+##~ return acls
diff --git a/src/webdav/acp/Acl.py b/src/webdav/acp/Acl.py
new file mode 100644
index 0000000..8f2b36f
--- /dev/null
+++ b/src/webdav/acp/Acl.py
@@ -0,0 +1,311 @@
+# pylint: disable-msg=W0622
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+
+"""
+ACL object handling according to WebDAV ACP specification.
+"""
+
+
+from webdav.acp.Ace import ACE
+from webdav import Constants
+from webdav.Connection import WebdavError
+from webdav.davlib import XML_DOC_HEADER
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class ACL(object):
+ """
+ This class provides access to Access Control List funcionality
+ as specified in the WebDAV ACP.
+
+ @ivar aces: ACEs in ACL
+ @type aces: C{list} of L{ACE} objects
+ @ivar withInherited: Flag indicating whether ACL contains inherited ACEs.
+ @type withInherited: C{bool}
+ """
+
+ # restrict instance variables
+ __slots__ = ('aces', 'withInherited')
+
+ def __init__(self, domroot=None, aces=None):
+ """
+ Constructor should be called with either no parameters (create blank ACE),
+ or one parameter (a DOM tree or ACE list).
+
+ @param domroot: A DOM tree (default: None).
+ @type domroot: L{webdav.WebdavResponse.Element} object
+ @param aces: ACE objects (default: None)
+ @type aces: C{list} of L{ACE} objects
+
+ @raise WebdavError: When non-valid parameters are passed a L{WebdavError} is raised.
+ """
+ self.withInherited = None
+ self.aces = []
+
+ if domroot:
+ for child in domroot.children:
+ if child.name == Constants.TAG_ACE and child.ns == Constants.NS_DAV:
+ self.addAce(ACE(child))
+ else:
+ # This shouldn't happen, someone screwed up with the params ...
+ raise WebdavError('Non-ACE tag handed to ACL constructor: ' + child.ns + child.name)
+ elif isinstance(aces, list) or isinstance(aces, tuple):
+ self.addAces(aces)
+ elif domroot == None and aces == None:
+ # no param ==> blank object
+ pass
+ else:
+ # This shouldn't happen, someone screwed up with the params ...
+ raise WebdavError('non-valid parameters handed to ACL constructor')
+
+ def __cmp__(self, other):
+ if not isinstance(other, ACL):
+ return 1
+ if self.withInherited == other.withInherited:
+ equal = 1
+ for ace in self.aces:
+ inList = 0
+ for otherAce in other.aces:
+ if ace == otherAce:
+ inList = 1
+ if inList == 0:
+ equal = 0
+ return not equal
+ else:
+ return 1
+
+ def __repr__(self):
+ repr = '<class ACL: '
+ if self.withInherited:
+ repr += 'with inherited, '
+ first = 1
+ repr += 'aces: ['
+ for ace in self.aces:
+ if first:
+ repr += '%s' % ace
+ first = 0
+ else:
+ repr += ', %s' % ace
+ return '%s]>' % (repr)
+
+ def copy(self, other):
+ '''Copy an ACL object.
+
+ @param other: Another ACL to copy.
+ @type other: L{ACL} object
+
+ @raise WebdavError: When an object that is not an L{ACL} is passed
+ a L{WebdavError} is raised.
+ '''
+ if not isinstance(other, ACL):
+ raise WebdavError('Non-ACL object passed to copy method: %s' % other.__class__)
+ self.withInherited = other.withInherited
+ if other.aces:
+ self.addAces(other.aces)
+
+ def toXML(self):
+ """
+ Returns ACL content as a string of valid XML as described in WebDAV ACP.
+ """
+ aclTag = 'D:' + Constants.TAG_ACL
+ return XML_DOC_HEADER +\
+ '<' + aclTag + ' xmlns:D="DAV:">' + reduce(lambda xml, ace: xml + ace.toXML() + '\n', [''] + self.aces) +\
+ '</' + aclTag + '>'
+
+ def addAce(self, ace):
+ '''
+ Adds the passed ACE object to list if it's not in it, yet.
+
+ @param ace: An ACE.
+ @type ace: L{ACE} object
+ '''
+ newAce = ACE()
+ newAce.copy(ace)
+ # only add it if it's not in the list, yet ...
+ inList = 0
+ for element in self.aces:
+ if element == ace:
+ inList = 1
+ if not inList:
+ self.aces.append(newAce)
+
+ def addAces(self, aces):
+ '''Adds the list of passed ACE objects to list.
+
+ @param aces: ACEs
+ @type aces: sequence of L{ACE} objects
+ '''
+ for ace in aces:
+ self.addAce(ace)
+
+ def delAce(self, ace):
+ '''Deletes the passed ACE object from list.
+
+ @param ace: An ACE.
+ @type ace: L{ACE} object
+
+ @raise WebdavError: When the ACE to be deleted is not within the ACL
+ a L{WebdavError} is raised.
+ '''
+ # find where it is and delete it ...
+ count = 0
+ index = 0
+ for element in self.aces:
+ count += 1
+ if element == ace:
+ index = count
+ if index:
+ self.aces.pop(index - 1)
+ else:
+ raise WebdavError('ACE to be deleted not in list: %s.' % ace)
+
+ def delAces(self, aces):
+ '''Deletes the list of passed ACE objects from list.
+
+ @param aces: ACEs
+ @type aces: sequence of L{ACE} objects
+ '''
+ for ace in aces:
+ self.delAce(ace)
+
+ def delPrincipalsAces(self, principal):
+ """
+ Deletes all ACEs in ACL by given principal.
+
+ @param principal: A principal.
+ @type principal: L{Principal} object
+ """
+ # find where it is and delete it ...
+ index = 0
+ while index < len(self.aces):
+ if self.aces[index].principal.principalURL == principal.principalURL:
+ self.aces.pop(index)
+ else:
+ index += 1
+
+ def joinGrantDeny(self):
+ """
+ Returns a "refined" ACL of the ACL for ease of use in the UI.
+ The purpose is to post the user an ACE that can contain both, granted
+ and denied, privileges. So possible pairs of grant and deny ACEs are joined
+ to return them in one ACE. This resulting ACE then of course IS NOT valid
+ for setting ACLs anymore. They will have to be reconverted to yield valid
+ ACLs for the ACL method.
+
+ @return: A (non-valid) ACL that contains both grant and deny clauses in an ACE.
+ @rtype: L{ACL} object
+ """
+ joinedAces = {}
+ for ace in self.aces:
+ if not ace.principal.principalURL is None:
+ principalKey = ace.principal.principalURL
+ elif not ace.principal.property is None:
+ principalKey = ace.principal.property
+ else:
+ principalKey = None
+ if ace.inherited:
+ principalKey = ace.inherited + ":" + principalKey
+ if principalKey in joinedAces:
+ joinedAces[principalKey].addGrantDenies(ace.grantDenies)
+ else:
+ joinedAces[principalKey] = ACE()
+ joinedAces[principalKey].copy(ace)
+ newAcl = ACL()
+ newAcl.addAces(joinedAces.values())
+ return newAcl
+
+ def splitGrantDeny(self):
+ """
+ Returns a "refined" ACL of the ACL for ease of use in the UI.
+ The purpose is to post the user an ACE that can contain both, granted
+ and denied, privileges. So possible joined grant and deny clauses in ACEs
+ splitted to return them in separate ACEs. This resulting ACE then is valid
+ for setting ACLs again. This method is to be seen in conjunction with the
+ method joinGrantDeny as it reverts its effect.
+
+ @return: A valid ACL that contains only ACEs with either grant or deny clauses.
+ @rtype: L{ACL} object
+ """
+ acesGrant = {}
+ acesDeny = {}
+ for ace in self.aces:
+ for grantDeny in ace.grantDenies:
+ if grantDeny.isGrant():
+ if ace.principal.principalURL in acesGrant:
+ ace.addGrantDeny(grantDeny)
+ else:
+ acesGrant[ace.principal.principalURL] = ACE()
+ acesGrant[ace.principal.principalURL].copy(ace)
+ acesGrant[ace.principal.principalURL].grantDenies = []
+ acesGrant[ace.principal.principalURL].addGrantDeny(grantDeny)
+ else:
+ if ace.principal.principalURL in acesDeny:
+ ace.addGrantDeny(grantDeny)
+ else:
+ acesDeny[ace.principal.principalURL] = ACE()
+ acesDeny[ace.principal.principalURL].copy(ace)
+ acesDeny[ace.principal.principalURL].grantDenies = []
+ acesDeny[ace.principal.principalURL].addGrantDeny(grantDeny)
+ newAcl = ACL()
+ newAcl.addAces(acesGrant.values())
+ newAcl.addAces(acesDeny.values())
+ return newAcl
+
+ def isValid(self):
+ """
+ Returns true (1) if all contained ACE objects are valid,
+ otherwise false (0) is returned.
+
+ @return: Validity of ACL.
+ @rtype: C{bool}
+ """
+ valid = 1
+ if len(self.aces):
+ for ace in self.aces:
+ if not ace.isValid():
+ valid = 0
+ return valid
+
+ def stripAces(self, inherited=True, protected=True):
+ """
+ Returns an ACL object with all ACEs stripped that are inherited
+ and/or protected.
+
+ @param inherited: Flag to indicate whether inherited ACEs should
+ be stripped (default: True).
+ @type inherited: C{bool}
+ @param protected: Flag to indicate whether protected ACEs should
+ be stripped (default: True).
+ @type protected: C{bool}
+
+ @return: An ACL without the stripped ACEs.
+ @rtype: L{ACL} object
+ """
+ newAcl = ACL()
+ if len(self.aces):
+ for ace in self.aces:
+ keep = 1
+ if inherited and ace.inherited:
+ keep = 0
+ elif protected and ace.protected:
+ keep = 0
+ if keep:
+ newAcl.addAce(ace)
+ return newAcl
diff --git a/src/webdav/acp/GrantDeny.py b/src/webdav/acp/GrantDeny.py
new file mode 100644
index 0000000..52c9b93
--- /dev/null
+++ b/src/webdav/acp/GrantDeny.py
@@ -0,0 +1,241 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling of grant and deny clauses in ACEs according to WebDAV ACP specification.
+"""
+
+
+from webdav.acp.Privilege import Privilege
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class GrantDeny(object):
+ """
+ This class provides functionality for handling
+ grant and deny clauses in ACEs.
+
+ @ivar grantDeny: Flag indicating whether clause grants or denies.
+ @type grantDeny: C{bool}
+ @ivar privileges: Privileges to be granted or denied.
+ @type privileges: C{list} of L{Privilege} objects
+ """
+
+ def __init__(self, domroot=None):
+ """
+ Constructor should be called with either no parameters
+ (create blank GrantDeny), or one parameter (a DOM tree).
+
+ @param domroot: A DOM tree (default: None).
+ @type domroot: L{webdav.WebdavResponse.Element} object
+
+ @raise WebdavError: When non-valid parameters are passed a L{WebdavError} is raised.
+ """
+ self.grantDeny = 0 # 0: deny, 1: grant
+ self.privileges = []
+
+ if domroot:
+ self.grantDeny = (domroot.name == Constants.TAG_GRANT)
+ for child in domroot.children:
+ if child.name == Constants.TAG_PRIVILEGE and child.ns == Constants.NS_DAV:
+ self.privileges.append(Privilege(domroot=child))
+ else:
+ # This shouldn't happen, someone screwed up with the params ...
+ raise WebdavError('Non-privilege tag handed to GrantDeny constructor: %s' \
+ % child.name)
+ elif domroot == None:
+ # no param ==> blank object
+ pass
+ else:
+ # This shouldn't happen, someone screwed up with the params ...
+ raise WebdavError('Non-valid parameters handed to GrantDeny constructor.')
+
+ def __cmp__(self, other):
+ """ Compares two GrantDeny instances. """
+ if not isinstance(other, GrantDeny):
+ return 1
+ if self.grantDeny == other.grantDeny:
+ equal = 1
+ for priv in self.privileges:
+ inList = 0
+ for otherPriv in other.privileges:
+ if priv == otherPriv:
+ inList = 1
+ if inList == 0:
+ equal = 0
+ return not equal
+ else:
+ return 1
+
+ def __repr__(self):
+ """ Returns the representation of an instance. """
+ representation = '<class GrantDeny: '
+ if self.grantDeny:
+ representation += 'grant privileges: ['
+ else:
+ representation += 'deny privileges: ['
+ first = 1
+ for priv in self.privileges:
+ if first:
+ representation += '%s' % priv
+ first = 0
+ else:
+ representation += ', %s' % priv
+ return '%s]>' % (representation)
+
+ def copy(self, other):
+ """
+ Copy a GrantDeny object.
+
+ @param other: Another grant or deny clause to copy.
+ @type other: L{GrantDeny} object
+
+ @raise WebdavError: When an object that is not an L{GrantDeny} is passed
+ a L{WebdavError} is raised.
+ """
+ if not isinstance(other, GrantDeny):
+ raise WebdavError('Non-GrantDeny object passed to copy method: %s' \
+ % other)
+ self.grantDeny = other.grantDeny
+ if other.privileges:
+ self.addPrivileges(other.privileges)
+
+ def isGrant(self):
+ """
+ Returns whether the set of privileges is of type "grant"
+ indicating true or false.
+
+ @return: Value whether the clause is of grant type.
+ @rtype: C{bool}
+ """
+ return self.grantDeny
+
+ def isDeny(self):
+ """
+ Returns whether the set of privileges is of type "deny"
+ indicating true or false.
+
+ @return: Value whether the clause is of deny type.
+ @rtype: C{bool}
+ """
+ return not self.grantDeny
+
+ def setGrantDeny(self, grantDeny):
+ """
+ Sets the set of privileges to given value for grantDeny.
+
+ @param grantDeny: Grant/deny value for clause (grant: True/1, deny: False/0).
+ @type grantDeny: C{bool}
+ """
+ if grantDeny == 0 or grantDeny == 1:
+ self.grantDeny = grantDeny
+
+ def setGrant(self):
+ """ Sets the set of privileges to type "grant". """
+ self.grantDeny = 1
+
+ def setDeny(self):
+ """ Sets the set of privileges to type "deny". """
+ self.grantDeny = 0
+
+ def isAll(self):
+ """
+ Checks whether the privileges contained are equal
+ to aggregate DAV:all privilege.
+
+ @return: Value whether all un-aggregated privileges are present.
+ @rtype: C{bool}
+ """
+
+ if len(self.privileges) == 1 and self.privileges[0].name == Constants.TAG_ALL:
+ return 1
+ return 0
+
+ def addPrivilege(self, privilege):
+ """
+ Adds the passed privilege to list if it's not in it, yet.
+
+ @param privilege: A privilege.
+ @type privilege: L{Privilege} object
+ """
+ inList = False
+ for priv in self.privileges:
+ if priv == privilege:
+ inList = True
+ if not inList:
+ newPrivilege = Privilege()
+ newPrivilege.copy(privilege)
+ self.privileges.append(newPrivilege)
+
+ def addPrivileges(self, privileges):
+ """
+ Adds the list of passed privileges to list.
+
+ @param privileges: Several privileges.
+ @type privileges: sequence of L{Privilege} objects
+ """
+ for priv in privileges:
+ self.addPrivilege(priv)
+
+ def delPrivilege(self, privilege):
+ """
+ Deletes the passed privilege from list if it's in it.
+
+ @param privilege: A privilege.
+ @type privilege: L{Privilege} object
+
+ @raise WebdavError: A L{WebdavError} is raised if the privilege to be
+ deleted is not present.
+ """
+ count = 0
+ index = 0
+ for priv in self.privileges:
+ count += 1
+ if priv == privilege:
+ index = count
+ if index:
+ self.privileges.pop(index - 1)
+ else:
+ raise WebdavError('Privilege to be deleted not in list: %s' % privilege)
+
+ def delPrivileges(self, privileges):
+ """
+ Deletes the list of passed privileges from list.
+
+ @param privileges: Several privileges.
+ @type privileges: sequence of L{Privilege} objects
+ """
+ for priv in privileges:
+ self.delPrivilege(priv)
+
+ def toXML(self):
+ """
+ Returns string of GrantDeny content to valid XML as described in WebDAV ACP.
+ """
+ assert self.privileges, "GrantDeny object is not initialized or does not contain content!"
+
+ if self.isGrant():
+ tag = 'D:' + Constants.TAG_GRANT
+ else:
+ tag = 'D:' + Constants.TAG_DENY
+
+ res = ''
+ for privilege in self.privileges:
+ res += privilege.toXML()
+ return '<%s>%s</%s>' % (tag, res, tag)
diff --git a/src/webdav/acp/Makefile.am b/src/webdav/acp/Makefile.am
new file mode 100644
index 0000000..506eb92
--- /dev/null
+++ b/src/webdav/acp/Makefile.am
@@ -0,0 +1,12 @@
+sugardir = $(pythondir)/webdav/acp
+sugar_PYTHON = \
+ AceHandler.py \
+ Ace.py \
+ Acl.py \
+ GrantDeny.py \
+ __init__.py \
+ Principal.py \
+ Privilege.py
+
+
+
diff --git a/src/webdav/acp/Principal.py b/src/webdav/acp/Principal.py
new file mode 100644
index 0000000..a0d5ec9
--- /dev/null
+++ b/src/webdav/acp/Principal.py
@@ -0,0 +1,189 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling of principals for ACEs according to WebDAV ACP specification.
+"""
+
+
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class Principal(object):
+ """
+ This class provides functionality for handling
+ principals according to the WebDAV ACP.
+
+ @ivar displayname: Name of the principal for output
+ @type displayname: C{string}
+ @ivar principalURL: URL under which the principal can be referenced on the server.
+ @type principalURL: C{string}
+ @ivar property: Information on type of a pseudo/jproperty principal, e. g.
+ DAV:owner, DAV:authenticated, etc.
+ @type property: C{string}
+
+ @cvar _TAG_LIST_PRINCIPALS: List of allowed XML tags within a principal declaration.
+ @type _TAG_LIST_PRINCIPALS: C{tuple} of C{string}s
+ @cvar _TAG_LIST_STATUS: List of XML tags for the status of a pseudo principal.
+ @type _TAG_LIST_STATUS: C{tuple} of C{string}s
+ """
+
+ # some local constants for this class to make things easier/more readable:
+ _TAG_LIST_PRINCIPALS = (Constants.TAG_HREF, # directly by URL
+ Constants.TAG_ALL, Constants.TAG_AUTHENTICATED, Constants.TAG_UNAUTHENTICATED,
+ # by log-in status
+ Constants.TAG_PROPERTY, # for property info, e. g. 'owner'
+ Constants.TAG_SELF, # only if the resource is the principal itself
+ Constants.TAG_PROP) # contains property info like 'displayname'
+ _TAG_LIST_STATUS = (Constants.TAG_ALL, Constants.TAG_AUTHENTICATED, Constants.TAG_UNAUTHENTICATED)
+
+ # restrict instance variables
+ __slots__ = ('displayname', 'principalURL', 'property')
+
+ def __init__(self, domroot=None, displayname=None, principalURL=None):
+ """
+ Constructor should be called with either no parameters (create blank Principal),
+ one parameter (a DOM tree), or two parameters (displayname and URL or property tag).
+
+ @param domroot: A DOM tree (default: None).
+ @type domroot: L{webdav.WebdavResponse.Element} object
+ @param displayname: The display name of a principal (default: None).
+ @type displayname: C{string}
+ @param principalURL: The URL representing a principal (default: None).
+ @type principalURL: C{string}
+
+ @raise WebdavError: When non-valid parameters or sets of parameters are
+ passed a L{WebdavError} is raised.
+ """
+ self.displayname = None
+ self.principalURL = None
+ self.property = None
+
+ if domroot:
+ for child in domroot.children:
+ if child.ns == Constants.NS_DAV and (child.name in self._TAG_LIST_PRINCIPALS):
+ if child.name == Constants.TAG_PROP:
+ self.displayname = \
+ child.find(Constants.PROP_DISPLAY_NAME, Constants.NS_DAV)
+ elif child.name == Constants.TAG_HREF:
+ self.principalURL = child.textof()
+ if self.principalURL and self.property in self._TAG_LIST_STATUS:
+ raise WebdavError('Principal cannot contain a URL and "%s"' % (self.property))
+ elif child.name == Constants.TAG_PROPERTY:
+ if child.count() == 1:
+ if self.property:
+ raise WebdavError('Property for principal has already been set: old "%s", new "%s"' \
+ % (self.property, child.pop().name))
+ elif self.principalURL:
+ raise WebdavError('Principal cannot contain a URL and "%s"' % (self.property))
+ else:
+ self.property = child.pop().name
+ else:
+ raise WebdavError("There should be only one value in the property for a principal, we have: %s" \
+ % child.name)
+ else:
+ if self.property:
+ raise WebdavError('Property for principal has already been set: old "%s", new "%s"' \
+ % (self.property, child.name))
+ else:
+ self.property = child.name
+ if self.principalURL and self.property in self._TAG_LIST_STATUS:
+ raise WebdavError('Principal cannot contain a URL and "%s"' % (self.property))
+ else: # This shouldn't happen, something's wrong with the DOM tree
+ raise WebdavError('Non-valid tag in principal DOM tree for constructor: %s' % child.name)
+ elif displayname == None or principalURL == None:
+ if displayname:
+ self.displayname = displayname
+ if principalURL:
+ self.principalURL = principalURL
+ else:
+ # This shouldn't happen, someone screwed up with the params ...
+ raise WebdavError('Non-valid parameters handed to Principal constructor.')
+
+ def __cmp__(self, other):
+ if not isinstance(other, Principal):
+ return 1
+ if self.displayname == other.displayname \
+ and self.principalURL == other.principalURL \
+ and self.property == other.property:
+ return 0
+ else:
+ return 1
+
+ def __repr__(self):
+ return '<class Principal: displayname: "%s", principalURL: "%s", property: "%s">' \
+ % (self.displayname, self.principalURL, self.property)
+
+ def copy(self, other):
+ """Copy Principal object.
+
+ @param other: Another principal to copy.
+ @type other: L{Principal} object
+
+ @raise WebdavError: When an object that is not a L{Principal} is passed
+ a L{WebdavError} is raised.
+ """
+ if not isinstance(other, Principal):
+ raise WebdavError('Non-Principal object passed to copy method: ' % other.__class__)
+ self.displayname = other.displayname
+ self.principalURL = other.principalURL
+ self.property = other.property
+
+ def isValid(self):
+ """
+ Checks whether necessarry props for principal are set.
+
+ @return: Validity of principal.
+ @rtype: C{bool}
+ """
+ return (self.displayname and
+ (self.principalURL or self.property) and
+ not (self.principalURL and self.property))
+
+ def toXML(self, invert=False, displayname=False, defaultNameSpace=None):
+ """Returns string of Principal content in valid XML as described in WebDAV ACP.
+
+ @param defaultNameSpace: Name space (default: None).
+ @type defaultNameSpace: C(string)
+ @param invert: True if principal should be inverted (default: False).
+ @type invert: C{bool}
+ @param displayname: True if displayname should be in output (default: False).
+ @type displayname: C{bool}
+ """
+ # this check is needed for setting principals only:
+ # assert self.isValid(), "principal is not initialized or does not contain valid content!"
+
+ PRINCIPAL = 'D:' + Constants.TAG_PRINCIPAL
+ res = ''
+ if self.principalURL:
+ res += '<D:%s>%s</D:%s>' % (Constants.TAG_HREF, self.principalURL, Constants.TAG_HREF)
+ elif self.property in self._TAG_LIST_STATUS \
+ or self.property == Constants.TAG_SELF:
+ res += '<D:%s/>' % (self.property)
+ elif self.property:
+ res += '<D:%s><D:%s/></D:%s>' \
+ % (Constants.TAG_PROPERTY, self.property, Constants.TAG_PROPERTY)
+ if self.displayname and displayname:
+ res += '<D:%s><D:%s>%s</D:%s></D:%s>' \
+ % (Constants.TAG_PROP, Constants.PROP_DISPLAY_NAME,
+ self.displayname,
+ Constants.PROP_DISPLAY_NAME, Constants.TAG_PROP)
+ if invert:
+ res = '<D:invert>%s</D:invert>' % (res)
+ return '<%s>%s</%s>' % (PRINCIPAL, res, PRINCIPAL)
diff --git a/src/webdav/acp/Privilege.py b/src/webdav/acp/Privilege.py
new file mode 100644
index 0000000..abfdcf9
--- /dev/null
+++ b/src/webdav/acp/Privilege.py
@@ -0,0 +1,125 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling for privileges for grant and deny clauses in ACEs
+according to WebDAV ACP specification.
+"""
+
+
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class Privilege(object):
+ """This class provides functionality for handling privileges for ACEs.
+
+ @ivar name: Name of the privilege.
+ @type name: C{string}
+
+ @cvar __privileges: List of allowed XML tags for privileges.
+ @type __privileges: C{tuple} of C{string}s
+ """
+
+
+ __privileges = list()
+
+
+ def __init__(self, privilege=None, domroot=None):
+ """
+ Constructor should be called with either no parameters (create blank Privilege),
+ one parameter (a DOM tree or privilege name to initialize it directly).
+
+ @param domroot: A DOM tree (default: None).
+ @type domroot: L{webdav.WebdavResponse.Element} object
+ @param privilege: The valid name of a privilege (default: None).
+ @type privilege: C{string}
+
+ @raise WebdavError: When non-valid parameters or sets of parameters are
+ passed a L{WebdavError} is raised.
+ """
+
+ self.name = None
+
+ if domroot:
+ if len(domroot.children) != 1:
+ raise WebdavError('Wrong number of elements for Privilege constructor, we have: %i' \
+ % (len(domroot.children)))
+ else:
+ child = domroot.children[0]
+ if child.ns == Constants.NS_DAV and child.name in self.__privileges:
+ self.name = child.name
+ else:
+ raise WebdavError('Not a valid privilege tag, we have: %s%s' \
+ % (child.ns, child.name))
+ elif privilege:
+ if privilege in self.__privileges:
+ self.name = privilege
+ else:
+ raise WebdavError('Not a valid privilege tag, we have: %s.' % str(privilege))
+
+ @classmethod
+ def registerPrivileges(cls, privileges):
+ """
+ Registers supported privilege tags.
+
+ @param privileges: List of privilege tags.
+ @type privileges: C{list} of C{unicode}
+ """
+
+ for privilege in privileges:
+ cls.__privileges.append(privilege)
+
+ def __cmp__(self, other):
+ """ Compares two Privilege instances. """
+ if not isinstance(other, Privilege):
+ return 1
+ if self.name != other.name:
+ return 1
+ else:
+ return 0
+
+ def __repr__(self):
+ """ Returns the string representation of an instance. """
+ return '<class Privilege: name: "%s">' % (self.name)
+
+ def copy(self, other):
+ """
+ Copy Privilege object.
+
+ @param other: Another privilege to copy.
+ @type other: L{Privilege} object
+
+ @raise WebdavError: When an object that is not a L{Privilege} is passed
+ a L{WebdavError} is raised.
+ """
+ if not isinstance(other, Privilege):
+ raise WebdavError('Non-Privilege object passed to copy method: %s' % other.__class__)
+ self.name = other.name
+
+ def toXML(self):
+ """
+ Returns privilege content as string in valid XML as described in WebDAV ACP.
+
+ @param defaultNameSpace: Name space (default: None).
+ @type defaultNameSpace: C(string)
+ """
+ assert self.name != None, "privilege is not initialized or does not contain valid content!"
+
+ privilege = 'D:' + Constants.TAG_PRIVILEGE
+ return '<%s><D:%s/></%s>' % (privilege, self.name, privilege)
diff --git a/src/webdav/acp/__init__.py b/src/webdav/acp/__init__.py
new file mode 100644
index 0000000..b5af299
--- /dev/null
+++ b/src/webdav/acp/__init__.py
@@ -0,0 +1,33 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from webdav import Constants
+from webdav.acp.Acl import ACL
+from webdav.acp.Ace import ACE
+from webdav.acp.GrantDeny import GrantDeny
+from webdav.acp.Privilege import Privilege
+from webdav.acp.Principal import Principal
+
+
+__version__ = "$LastChangedRevision$"
+
+
+privileges = [Constants.TAG_READ, Constants.TAG_WRITE, Constants.TAG_WRITE_PROPERTIES,
+ Constants.TAG_WRITE_CONTENT, Constants.TAG_UNLOCK, Constants.TAG_READ_ACL,
+ Constants.TAG_READ_CURRENT_USER_PRIVILEGE_SET, Constants.TAG_WRITE_ACL, Constants.TAG_ALL,
+ Constants.TAG_BIND, Constants.TAG_UNBIND, Constants.TAG_TAMINO_SECURITY,
+ Constants.TAG_BIND_COLLECTION, Constants.TAG_UNBIND_COLLECTION, Constants.TAG_READ_PRIVATE_PROPERTIES,
+ Constants.TAG_WRITE_PRIVATE_PROPERTIES]
+Privilege.registerPrivileges(privileges)
diff --git a/src/webdav/davlib.py b/src/webdav/davlib.py
new file mode 100644
index 0000000..f4dac91
--- /dev/null
+++ b/src/webdav/davlib.py
@@ -0,0 +1,336 @@
+# pylint: disable-msg=W0402,W0231,W0141,R0903,C0321,W0701,R0904,C0103,W0201,W0102,R0913,W0622,E1101,C0111,C0121,R0901
+# DAV client library
+#
+# Copyright (C) 1998-2000 Guido van Rossum. All Rights Reserved.
+# Written by Greg Stein. Given to Guido. Licensed using the Python license.
+#
+# This module is maintained by Greg and is available at:
+# http://www.lyra.org/greg/python/davlib.py
+#
+# Since this isn't in the Python distribution yet, we'll use the CVS ID
+# for tracking:
+# $Id: davlib.py 3182 2008-02-22 15:57:55 +0000 (Fr, 22 Feb 2008) schlauch $
+#
+
+import httplib
+import urllib
+import string
+import types
+import mimetypes
+import qp_xml
+
+
+INFINITY = 'infinity'
+XML_DOC_HEADER = '<?xml version="1.0" encoding="utf-8"?>'
+XML_CONTENT_TYPE = 'text/xml; charset="utf-8"'
+
+# block size for copying files up to the server
+BLOCKSIZE = 16384
+
+
+class HTTPProtocolChooser(httplib.HTTPSConnection):
+ def __init__(self, *args, **kw):
+ self.protocol = kw.pop('protocol')
+ if self.protocol == "https":
+ self.default_port = 443
+ else:
+ self.default_port = 80
+
+ apply(httplib.HTTPSConnection.__init__, (self,) + args, kw)
+
+ def connect(self):
+ if self.protocol == "https":
+ httplib.HTTPSConnection.connect(self)
+ else:
+ httplib.HTTPConnection.connect(self)
+
+
+class HTTPConnectionAuth(HTTPProtocolChooser):
+ def __init__(self, *args, **kw):
+ apply(HTTPProtocolChooser.__init__, (self,) + args, kw)
+
+ self.__username = None
+ self.__password = None
+ self.__nonce = None
+ self.__opaque = None
+
+ def setauth(self, username, password):
+ self.__username = username
+ self.__password = password
+
+
+def _parse_status(elem):
+ text = elem.textof()
+ idx1 = string.find(text, ' ')
+ idx2 = string.find(text, ' ', idx1+1)
+ return int(text[idx1:idx2]), text[idx2+1:]
+
+class _blank:
+ def __init__(self, **kw):
+ self.__dict__.update(kw)
+class _propstat(_blank): pass
+class _response(_blank): pass
+class _multistatus(_blank): pass
+
+def _extract_propstat(elem):
+ ps = _propstat(prop={}, status=None, responsedescription=None)
+ for child in elem.children:
+ if child.ns != 'DAV:':
+ continue
+ if child.name == 'prop':
+ for prop in child.children:
+ ps.prop[(prop.ns, prop.name)] = prop
+ elif child.name == 'status':
+ ps.status = _parse_status(child)
+ elif child.name == 'responsedescription':
+ ps.responsedescription = child.textof()
+ ### unknown element name
+
+ return ps
+
+def _extract_response(elem):
+ resp = _response(href=[], status=None, responsedescription=None, propstat=[])
+ for child in elem.children:
+ if child.ns != 'DAV:':
+ continue
+ if child.name == 'href':
+ resp.href.append(child.textof())
+ elif child.name == 'status':
+ resp.status = _parse_status(child)
+ elif child.name == 'responsedescription':
+ resp.responsedescription = child.textof()
+ elif child.name == 'propstat':
+ resp.propstat.append(_extract_propstat(child))
+ ### unknown child element
+
+ return resp
+
+def _extract_msr(root):
+ if root.ns != 'DAV:' or root.name != 'multistatus':
+ raise 'invalid response: <DAV:multistatus> expected'
+
+ msr = _multistatus(responses=[ ], responsedescription=None)
+
+ for child in root.children:
+ if child.ns != 'DAV:':
+ continue
+ if child.name == 'responsedescription':
+ msr.responsedescription = child.textof()
+ elif child.name == 'response':
+ msr.responses.append(_extract_response(child))
+ ### unknown child element
+
+ return msr
+
+def _extract_locktoken(root):
+ if root.ns != 'DAV:' or root.name != 'prop':
+ raise 'invalid response: <DAV:prop> expected'
+ elem = root.find('lockdiscovery', 'DAV:')
+ if not elem:
+ raise 'invalid response: <DAV:lockdiscovery> expected'
+ elem = elem.find('activelock', 'DAV:')
+ if not elem:
+ raise 'invalid response: <DAV:activelock> expected'
+ elem = elem.find('locktoken', 'DAV:')
+ if not elem:
+ raise 'invalid response: <DAV:locktoken> expected'
+ elem = elem.find('href', 'DAV:')
+ if not elem:
+ raise 'invalid response: <DAV:href> expected'
+ return elem.textof()
+
+
+class DAVResponse(httplib.HTTPResponse):
+ def parse_multistatus(self):
+ self.root = qp_xml.Parser().parse(self)
+ self.msr = _extract_msr(self.root)
+
+ def parse_lock_response(self):
+ self.root = qp_xml.Parser().parse(self)
+ self.locktoken = _extract_locktoken(self.root)
+
+
+class DAV(HTTPConnectionAuth):
+
+ response_class = DAVResponse
+
+ def get(self, url, extra_hdrs={ }):
+ return self._request('GET', url, extra_hdrs=extra_hdrs)
+
+ def head(self, url, extra_hdrs={ }):
+ return self._request('HEAD', url, extra_hdrs=extra_hdrs)
+
+ def post(self, url, data={ }, body=None, extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+
+ assert body or data, "body or data must be supplied"
+ assert not (body and data), "cannot supply both body and data"
+ if data:
+ body = ''
+ for key, value in data.items():
+ if isinstance(value, types.ListType):
+ for item in value:
+ body = body + '&' + key + '=' + urllib.quote(str(item))
+ else:
+ body = body + '&' + key + '=' + urllib.quote(str(value))
+ body = body[1:]
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
+
+ return self._request('POST', url, body, headers)
+
+ def options(self, url='*', extra_hdrs={ }):
+ return self._request('OPTIONS', url, extra_hdrs=extra_hdrs)
+
+ def trace(self, url, extra_hdrs={ }):
+ return self._request('TRACE', url, extra_hdrs=extra_hdrs)
+
+ def put(self, url, contents,
+ content_type=None, content_enc=None, extra_hdrs={ }):
+
+ if not content_type:
+ content_type, content_enc = mimetypes.guess_type(url)
+
+ headers = extra_hdrs.copy()
+ if content_type:
+ headers['Content-Type'] = content_type
+ if content_enc:
+ headers['Content-Encoding'] = content_enc
+ return self._request('PUT', url, contents, headers)
+
+ def delete(self, url, extra_hdrs={ }):
+ return self._request('DELETE', url, extra_hdrs=extra_hdrs)
+
+ def propfind(self, url, body=None, depth=None, extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+ headers['Content-Type'] = XML_CONTENT_TYPE
+ if depth is not None:
+ headers['Depth'] = str(depth)
+ return self._request('PROPFIND', url, body, headers)
+
+ def proppatch(self, url, body, extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+ headers['Content-Type'] = XML_CONTENT_TYPE
+ return self._request('PROPPATCH', url, body, headers)
+
+ def mkcol(self, url, extra_hdrs={ }):
+ return self._request('MKCOL', url, extra_hdrs=extra_hdrs)
+
+ def move(self, src, dst, extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+ headers['Destination'] = dst
+ return self._request('MOVE', src, extra_hdrs=headers)
+
+ def copy(self, src, dst, depth=None, extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+ headers['Destination'] = dst
+ if depth is not None:
+ headers['Depth'] = str(depth)
+ return self._request('COPY', src, extra_hdrs=headers)
+
+ def lock(self, url, owner='', timeout=None, depth=None,
+ scope='exclusive', type='write', extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+ headers['Content-Type'] = XML_CONTENT_TYPE
+ if depth is not None:
+ headers['Depth'] = str(depth)
+ if timeout is not None:
+ headers['Timeout'] = timeout
+ body = XML_DOC_HEADER + \
+ '<DAV:lockinfo xmlns:DAV="DAV:">' + \
+ '<DAV:lockscope><DAV:%s/></DAV:lockscope>' % scope + \
+ '<DAV:locktype><DAV:%s/></DAV:locktype>' % type + \
+ '<DAV:owner>' + owner + '</DAV:owner>' + \
+ '</DAV:lockinfo>'
+ return self._request('LOCK', url, body, extra_hdrs=headers)
+
+ def unlock(self, url, locktoken, extra_hdrs={ }):
+ headers = extra_hdrs.copy()
+ if locktoken[0] != '<':
+ locktoken = '<' + locktoken + '>'
+ headers['Lock-Token'] = locktoken
+ return self._request('UNLOCK', url, extra_hdrs=headers)
+
+ def _request(self, method, url, body=None, extra_hdrs={}):
+ "Internal method for sending a request."
+
+ self.request(method, url, body, extra_hdrs)
+ return self.getresponse()
+
+
+ #
+ # Higher-level methods for typical client use
+ #
+
+ def allprops(self, url, depth=None):
+ body = XML_DOC_HEADER + \
+ '<DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
+ return self.propfind(url, body, depth=depth)
+
+ def propnames(self, url, depth=None):
+ body = XML_DOC_HEADER + \
+ '<DAV:propfind xmlns:DAV="DAV:"><DAV:propname/></DAV:propfind>'
+ return self.propfind(url, body, depth)
+
+ def getprops(self, url, *names, **kw):
+ assert names, 'at least one property name must be provided'
+ if kw.has_key('ns'):
+ xmlns = ' xmlns:NS="' + kw['ns'] + '"'
+ ns = 'NS:'
+ del kw['ns']
+ else:
+ xmlns = ns = ''
+ if kw.has_key('depth'):
+ depth = kw['depth']
+ del kw['depth']
+ else:
+ depth = 0
+ assert not kw, 'unknown arguments'
+ body = XML_DOC_HEADER + \
+ '<DAV:propfind xmlns:DAV="DAV:"' + xmlns + '><DAV:prop><' + ns + \
+ string.joinfields(names, '/><' + ns) + \
+ '/></DAV:prop></DAV:propfind>'
+ return self.propfind(url, body, depth)
+
+ def delprops(self, url, *names, **kw):
+ assert names, 'at least one property name must be provided'
+ if kw.has_key('ns'):
+ xmlns = ' xmlns:NS="' + kw['ns'] + '"'
+ ns = 'NS:'
+ del kw['ns']
+ else:
+ xmlns = ns = ''
+ assert not kw, 'unknown arguments'
+ body = XML_DOC_HEADER + \
+ '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns + \
+ '><DAV:remove><DAV:prop><' + ns + \
+ string.joinfields(names, '/><' + ns) + \
+ '/></DAV:prop></DAV:remove></DAV:propertyupdate>'
+ return self.proppatch(url, body)
+
+ def setprops(self, url, *xmlprops, **props):
+ assert xmlprops or props, 'at least one property must be provided'
+ xmlprops = list(xmlprops)
+ if props.has_key('ns'):
+ xmlns = ' xmlns:NS="' + props['ns'] + '"'
+ ns = 'NS:'
+ del props['ns']
+ else:
+ xmlns = ns = ''
+ for key, value in props.items():
+ if value:
+ xmlprops.append('<%s%s>%s</%s%s>' % (ns, key, value, ns, key))
+ else:
+ xmlprops.append('<%s%s/>' % (ns, key))
+ elems = string.joinfields(xmlprops, '')
+ body = XML_DOC_HEADER + \
+ '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns + \
+ '><DAV:set><DAV:prop>' + \
+ elems + \
+ '</DAV:prop></DAV:set></DAV:propertyupdate>'
+ return self.proppatch(url, body)
+
+ def get_lock(self, url, owner='', timeout=None, depth=None):
+ response = self.lock(url, owner, timeout, depth)
+ response.parse_lock_response()
+ return response.locktoken
+ \ No newline at end of file
diff --git a/src/webdav/logger.py b/src/webdav/logger.py
new file mode 100644
index 0000000..d2538ef
--- /dev/null
+++ b/src/webdav/logger.py
@@ -0,0 +1,51 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+""""
+Module provides access to a configured logger instance.
+The logger writes C{sys.stdout}.
+"""
+
+
+import logging
+import sys
+
+
+__version__ = "$LastChangedRevision$"[11:-2]
+
+
+_defaultLoggerName = "webdavLogger"
+_fileLogFormat = "%(asctime)s: %(levelname)s: %(message)s"
+
+
+def getDefaultLogger(handler=None):
+ """
+ Returns a configured logger object.
+
+ @return: Logger instance.
+ @rtype: C{logging.Logger}
+ """
+
+ myLogger = logging.getLogger(_defaultLoggerName)
+ if len(myLogger.handlers) == 0:
+ myLogger.level = logging.DEBUG
+ formatter = logging.Formatter(_fileLogFormat)
+ if handler is None:
+ stdoutHandler = logging.StreamHandler(sys.stdout)
+ stdoutHandler.setFormatter(formatter)
+ myLogger.addHandler(stdoutHandler)
+ else:
+ myLogger.addHandler(handler)
+ return myLogger
diff --git a/src/webdav/qp_xml.py b/src/webdav/qp_xml.py
new file mode 100644
index 0000000..f167e1b
--- /dev/null
+++ b/src/webdav/qp_xml.py
@@ -0,0 +1,240 @@
+# pylint: disable-msg=W0311,E1101,E1103,W0201,C0103,W0622,W0402,W0706,R0911,W0613,W0612,R0912,W0141,C0111,C0121
+
+# qp_xml: Quick Parsing for XML
+#
+# Written by Greg Stein. Public Domain.
+# No Copyright, no Rights Reserved, and no Warranties.
+#
+# This module is maintained by Greg and is available as part of the XML-SIG
+# distribution. This module and its changelog can be fetched at:
+# http://www.lyra.org/cgi-bin/viewcvs.cgi/xml/xml/utils/qp_xml.py
+#
+# Additional information can be found on Greg's Python page at:
+# http://www.lyra.org/greg/python/
+#
+# This module was added to the XML-SIG distribution on February 14, 2000.
+# As part of that distribution, it falls under the XML distribution license.
+#
+
+import string
+from xml.parsers import expat
+
+
+error = __name__ + '.error'
+
+
+#
+# The parsing class. Instantiate and pass a string/file to .parse()
+#
+class Parser:
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.root = None
+ self.cur_elem = None
+
+ def find_prefix(self, prefix):
+ elem = self.cur_elem
+ while elem:
+ if elem.ns_scope.has_key(prefix):
+ return elem.ns_scope[prefix]
+ elem = elem.parent
+
+ if prefix == '':
+ return '' # empty URL for "no namespace"
+
+ return None
+
+ def process_prefix(self, name, use_default):
+ idx = string.find(name, ':')
+ if idx == -1:
+ if use_default:
+ return self.find_prefix(''), name
+ return '', name # no namespace
+
+ if string.lower(name[:3]) == 'xml':
+ return '', name # name is reserved by XML. don't break out a NS.
+
+ ns = self.find_prefix(name[:idx])
+ if ns is None:
+ raise error, 'namespace prefix ("%s") not found' % name[:idx]
+
+ return ns, name[idx+1:]
+
+ def start(self, name, attrs):
+ elem = _element(name=name, lang=None, parent=None,
+ children=[], ns_scope={}, attrs={},
+ first_cdata='', following_cdata='')
+
+ if self.cur_elem:
+ elem.parent = self.cur_elem
+ elem.parent.children.append(elem)
+ self.cur_elem = elem
+ else:
+ self.cur_elem = self.root = elem
+
+ work_attrs = [ ]
+
+ # scan for namespace declarations (and xml:lang while we're at it)
+ for name, value in attrs.items():
+ if name == 'xmlns':
+ elem.ns_scope[''] = value
+ elif name[:6] == 'xmlns:':
+ elem.ns_scope[name[6:]] = value
+ elif name == 'xml:lang':
+ elem.lang = value
+ else:
+ work_attrs.append((name, value))
+
+ # inherit xml:lang from parent
+ if elem.lang is None and elem.parent:
+ elem.lang = elem.parent.lang
+
+ # process prefix of the element name
+ elem.ns, elem.name = self.process_prefix(elem.name, 1)
+
+ # process attributes' namespace prefixes
+ for name, value in work_attrs:
+ elem.attrs[self.process_prefix(name, 0)] = value
+
+ def end(self, name):
+ parent = self.cur_elem.parent
+
+ del self.cur_elem.ns_scope
+ del self.cur_elem.parent
+
+ self.cur_elem = parent
+
+ def cdata(self, data):
+ elem = self.cur_elem
+ if elem.children:
+ last = elem.children[-1]
+ last.following_cdata = last.following_cdata + data
+ else:
+ elem.first_cdata = elem.first_cdata + data
+
+ def parse(self, input):
+ self.reset()
+
+ p = expat.ParserCreate()
+ p.StartElementHandler = self.start
+ p.EndElementHandler = self.end
+ p.CharacterDataHandler = self.cdata
+
+ try:
+ if type(input) == type(''):
+ p.Parse(input, 1)
+ else:
+ while 1:
+ s = input.read(_BLOCKSIZE)
+ if not s:
+ p.Parse('', 1)
+ break
+
+ p.Parse(s, 0)
+
+ finally:
+ if self.root:
+ _clean_tree(self.root)
+
+ return self.root
+
+
+#
+# handy function for dumping a tree that is returned by Parser
+#
+def dump(f, root):
+ f.write('<?xml version="1.0"?>\n')
+ namespaces = _collect_ns(root)
+ _dump_recurse(f, root, namespaces, dump_ns=1)
+ f.write('\n')
+
+
+#
+# This function returns the element's CDATA. Note: this is not recursive --
+# it only returns the CDATA immediately within the element, excluding the
+# CDATA in child elements.
+#
+def textof(elem):
+ return elem.textof()
+
+
+#########################################################################
+#
+# private stuff for qp_xml
+#
+
+_BLOCKSIZE = 16384 # chunk size for parsing input
+
+class _element:
+ def __init__(self, **kw):
+ self.__dict__.update(kw)
+
+ def textof(self):
+ '''Return the CDATA of this element.
+
+ Note: this is not recursive -- it only returns the CDATA immediately
+ within the element, excluding the CDATA in child elements.
+ '''
+ s = self.first_cdata
+ for child in self.children:
+ s = s + child.following_cdata
+ return s
+
+ def find(self, name, ns=''):
+ for elem in self.children:
+ if elem.name == name and elem.ns == ns:
+ return elem
+ return None
+
+
+def _clean_tree(elem):
+ elem.parent = None
+ del elem.parent
+ map(_clean_tree, elem.children)
+
+
+def _collect_recurse(elem, dict):
+ dict[elem.ns] = None
+ for ns, name in elem.attrs.keys():
+ dict[ns] = None
+ for child in elem.children:
+ _collect_recurse(child, dict)
+
+def _collect_ns(elem):
+ "Collect all namespaces into a NAMESPACE -> PREFIX mapping."
+ d = { '' : None }
+ _collect_recurse(elem, d)
+ del d[''] # make sure we don't pick up no-namespace entries
+ keys = d.keys()
+ for i in range(len(keys)):
+ d[keys[i]] = i
+ return d
+
+def _dump_recurse(f, elem, namespaces, lang=None, dump_ns=0):
+ if elem.ns:
+ f.write('<ns%d:%s' % (namespaces[elem.ns], elem.name))
+ else:
+ f.write('<' + elem.name)
+ for (ns, name), value in elem.attrs.items():
+ if ns:
+ f.write(' ns%d:%s="%s"' % (namespaces[ns], name, value))
+ else:
+ f.write(' %s="%s"' % (name, value))
+ if dump_ns:
+ for ns, id in namespaces.items():
+ f.write(' xmlns:ns%d="%s"' % (id, ns))
+ if elem.lang != lang:
+ f.write(' xml:lang="%s"' % elem.lang)
+ if elem.children or elem.first_cdata:
+ f.write('>' + elem.first_cdata)
+ for child in elem.children:
+ _dump_recurse(f, child, namespaces, elem.lang)
+ f.write(child.following_cdata)
+ if elem.ns:
+ f.write('</ns%d:%s>' % (namespaces[elem.ns], elem.name))
+ else:
+ f.write('</%s>' % elem.name)
+ else:
+ f.write('/>')
diff --git a/src/webdav/uuid_.py b/src/webdav/uuid_.py
new file mode 100644
index 0000000..3b590e8
--- /dev/null
+++ b/src/webdav/uuid_.py
@@ -0,0 +1,476 @@
+r"""UUID objects (universally unique identifiers) according to RFC 4122.
+
+This module provides immutable UUID objects (class UUID) and the functions
+uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5
+UUIDs as specified in RFC 4122.
+
+If all you want is a unique ID, you should probably call uuid1() or uuid4().
+Note that uuid1() may compromise privacy since it creates a UUID containing
+the computer's network address. uuid4() creates a random UUID.
+
+Typical usage:
+
+ >>> import uuid
+
+ # make a UUID based on the host ID and current time
+ >>> uuid.uuid1()
+ UUID('a8098c1a-f86e-11da-bd1a-00112444be1e')
+
+ # make a UUID using an MD5 hash of a namespace UUID and a name
+ >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org')
+ UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e')
+
+ # make a random UUID
+ >>> uuid.uuid4()
+ UUID('16fd2706-8baf-433b-82eb-8c7fada847da')
+
+ # make a UUID using a SHA-1 hash of a namespace UUID and a name
+ >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
+ UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d')
+
+ # make a UUID from a string of hex digits (braces and hyphens ignored)
+ >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}')
+
+ # convert a UUID to a string of hex digits in standard form
+ >>> str(x)
+ '00010203-0405-0607-0809-0a0b0c0d0e0f'
+
+ # get the raw 16 bytes of the UUID
+ >>> x.bytes
+ '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f'
+
+ # make a UUID from a 16-byte string
+ >>> uuid.UUID(bytes=x.bytes)
+ UUID('00010203-0405-0607-0809-0a0b0c0d0e0f')
+"""
+
+__author__ = 'Ka-Ping Yee <ping@zesty.ca>'
+__date__ = '$Date: 2006/06/12 23:15:40 $'.split()[1].replace('/', '-')
+__version__ = '$Revision: 1.30 $'.split()[1]
+
+RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
+ 'reserved for NCS compatibility', 'specified in RFC 4122',
+ 'reserved for Microsoft compatibility', 'reserved for future definition']
+
+class UUID(object):
+ """Instances of the UUID class represent UUIDs as specified in RFC 4122.
+ UUID objects are immutable, hashable, and usable as dictionary keys.
+ Converting a UUID to a string with str() yields something in the form
+ '12345678-1234-1234-1234-123456789abc'. The UUID constructor accepts
+ four possible forms: a similar string of hexadecimal digits, or a
+ string of 16 raw bytes as an argument named 'bytes', or a tuple of
+ six integer fields (with 32-bit, 16-bit, 16-bit, 8-bit, 8-bit, and
+ 48-bit values respectively) as an argument named 'fields', or a single
+ 128-bit integer as an argument named 'int'.
+
+ UUIDs have these read-only attributes:
+
+ bytes the UUID as a 16-byte string
+
+ fields a tuple of the six integer fields of the UUID,
+ which are also available as six individual attributes
+ and two derived attributes:
+
+ time_low the first 32 bits of the UUID
+ time_mid the next 16 bits of the UUID
+ time_hi_version the next 16 bits of the UUID
+ clock_seq_hi_variant the next 8 bits of the UUID
+ clock_seq_low the next 8 bits of the UUID
+ node the last 48 bits of the UUID
+
+ time the 60-bit timestamp
+ clock_seq the 14-bit sequence number
+
+ hex the UUID as a 32-character hexadecimal string
+
+ int the UUID as a 128-bit integer
+
+ urn the UUID as a URN as specified in RFC 4122
+
+ variant the UUID variant (one of the constants RESERVED_NCS,
+ RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE)
+
+ version the UUID version number (1 through 5, meaningful only
+ when the variant is RFC_4122)
+ """
+
+ def __init__(self, hex=None, bytes=None, fields=None, int=None,
+ version=None):
+ r"""Create a UUID from either a string of 32 hexadecimal digits,
+ a string of 16 bytes as the 'bytes' argument, a tuple of six
+ integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version,
+ 8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as
+ the 'fields' argument, or a single 128-bit integer as the 'int'
+ argument. When a string of hex digits is given, curly braces,
+ hyphens, and a URN prefix are all optional. For example, these
+ expressions all yield the same UUID:
+
+ UUID('{12345678-1234-5678-1234-567812345678}')
+ UUID('12345678123456781234567812345678')
+ UUID('urn:uuid:12345678-1234-5678-1234-567812345678')
+ UUID(bytes='\x12\x34\x56\x78'*4)
+ UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678))
+ UUID(int=0x12345678123456781234567812345678)
+
+ Exactly one of 'hex', 'bytes', 'fields', or 'int' must be given.
+ The 'version' argument is optional; if given, the resulting UUID
+ will have its variant and version number set according to RFC 4122,
+ overriding bits in the given 'hex', 'bytes', 'fields', or 'int'.
+ """
+
+ if [hex, bytes, fields, int].count(None) != 3:
+ raise TypeError('need just one of hex, bytes, fields, or int')
+ if hex is not None:
+ hex = hex.replace('urn:', '').replace('uuid:', '')
+ hex = hex.strip('{}').replace('-', '')
+ if len(hex) != 32:
+ raise ValueError('badly formed hexadecimal UUID string')
+ int = long(hex, 16)
+ if bytes is not None:
+ if len(bytes) != 16:
+ raise ValueError('bytes is not a 16-char string')
+ int = long(('%02x'*16) % tuple(map(ord, bytes)), 16)
+ if fields is not None:
+ if len(fields) != 6:
+ raise ValueError('fields is not a 6-tuple')
+ (time_low, time_mid, time_hi_version,
+ clock_seq_hi_variant, clock_seq_low, node) = fields
+ if not 0 <= time_low < 1<<32L:
+ raise ValueError('field 1 out of range (need a 32-bit value)')
+ if not 0 <= time_mid < 1<<16L:
+ raise ValueError('field 2 out of range (need a 16-bit value)')
+ if not 0 <= time_hi_version < 1<<16L:
+ raise ValueError('field 3 out of range (need a 16-bit value)')
+ if not 0 <= clock_seq_hi_variant < 1<<8L:
+ raise ValueError('field 4 out of range (need an 8-bit value)')
+ if not 0 <= clock_seq_low < 1<<8L:
+ raise ValueError('field 5 out of range (need an 8-bit value)')
+ if not 0 <= node < 1<<48L:
+ raise ValueError('field 6 out of range (need a 48-bit value)')
+ clock_seq = (clock_seq_hi_variant << 8L) | clock_seq_low
+ int = ((time_low << 96L) | (time_mid << 80L) |
+ (time_hi_version << 64L) | (clock_seq << 48L) | node)
+ if int is not None:
+ if not 0 <= int < 1<<128L:
+ raise ValueError('int is out of range (need a 128-bit value)')
+ if version is not None:
+ if not 1 <= version <= 5:
+ raise ValueError('illegal version number')
+ # Set the variant to RFC 4122.
+ int &= ~(0xc000 << 48L)
+ int |= 0x8000 << 48L
+ # Set the version number.
+ int &= ~(0xf000 << 64L)
+ int |= version << 76L
+ self.__dict__['int'] = int
+
+ def __cmp__(self, other):
+ if isinstance(other, UUID):
+ return cmp(self.int, other.int)
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self.int)
+
+ def __int__(self):
+ return self.int
+
+ def __repr__(self):
+ return 'UUID(%r)' % str(self)
+
+ def __setattr__(self, name, value):
+ raise TypeError('UUID objects are immutable')
+
+ def __str__(self):
+ hex = '%032x' % self.int
+ return '%s-%s-%s-%s-%s' % (
+ hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:])
+
+ def get_bytes(self):
+ bytes = ''
+ for shift in range(0, 128, 8):
+ bytes = chr((self.int >> shift) & 0xff) + bytes
+ return bytes
+
+ bytes = property(get_bytes)
+
+ def get_fields(self):
+ return (self.time_low, self.time_mid, self.time_hi_version,
+ self.clock_seq_hi_variant, self.clock_seq_low, self.node)
+
+ fields = property(get_fields)
+
+ def get_time_low(self):
+ return self.int >> 96L
+
+ time_low = property(get_time_low)
+
+ def get_time_mid(self):
+ return (self.int >> 80L) & 0xffff
+
+ time_mid = property(get_time_mid)
+
+ def get_time_hi_version(self):
+ return (self.int >> 64L) & 0xffff
+
+ time_hi_version = property(get_time_hi_version)
+
+ def get_clock_seq_hi_variant(self):
+ return (self.int >> 56L) & 0xff
+
+ clock_seq_hi_variant = property(get_clock_seq_hi_variant)
+
+ def get_clock_seq_low(self):
+ return (self.int >> 48L) & 0xff
+
+ clock_seq_low = property(get_clock_seq_low)
+
+ def get_time(self):
+ return (((self.time_hi_version & 0x0fffL) << 48L) |
+ (self.time_mid << 32L) | self.time_low)
+
+ time = property(get_time)
+
+ def get_clock_seq(self):
+ return (((self.clock_seq_hi_variant & 0x3fL) << 8L) |
+ self.clock_seq_low)
+
+ clock_seq = property(get_clock_seq)
+
+ def get_node(self):
+ return self.int & 0xffffffffffff
+
+ node = property(get_node)
+
+ def get_hex(self):
+ return '%032x' % self.int
+
+ hex = property(get_hex)
+
+ def get_urn(self):
+ return 'urn:uuid:' + str(self)
+
+ urn = property(get_urn)
+
+ def get_variant(self):
+ if not self.int & (0x8000 << 48L):
+ return RESERVED_NCS
+ elif not self.int & (0x4000 << 48L):
+ return RFC_4122
+ elif not self.int & (0x2000 << 48L):
+ return RESERVED_MICROSOFT
+ else:
+ return RESERVED_FUTURE
+
+ variant = property(get_variant)
+
+ def get_version(self):
+ # The version bits are only meaningful for RFC 4122 UUIDs.
+ if self.variant == RFC_4122:
+ return int((self.int >> 76L) & 0xf)
+
+ version = property(get_version)
+
+def _ifconfig_getnode():
+ """Get the hardware address on Unix by running ifconfig."""
+ import os
+ for dir in ['', '/sbin/', '/usr/sbin']:
+ try:
+ pipe = os.popen(os.path.join(dir, 'ifconfig'))
+ except IOError:
+ continue
+ for line in pipe:
+ words = line.lower().split()
+ for i in range(len(words)):
+ if words[i] in ['hwaddr', 'ether']:
+ return int(words[i + 1].replace(':', ''), 16)
+
+def _ipconfig_getnode():
+ """Get the hardware address on Windows by running ipconfig.exe."""
+ import os, re
+ dirs = ['', r'c:\windows\system32', r'c:\winnt\system32']
+ try:
+ import ctypes
+ buffer = ctypes.create_string_buffer(300)
+ ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300)
+ dirs.insert(0, buffer.value.decode('mbcs'))
+ except:
+ pass
+ for dir in dirs:
+ try:
+ pipe = os.popen(os.path.join(dir, 'ipconfig') + ' /all')
+ except IOError:
+ continue
+ for line in pipe:
+ value = line.split(':')[-1].strip().lower()
+ if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value):
+ return int(value.replace('-', ''), 16)
+
+def _netbios_getnode():
+ """Get the hardware address on Windows using NetBIOS calls.
+ See http://support.microsoft.com/kb/118623 for details."""
+ import win32wnet, netbios
+ ncb = netbios.NCB()
+ ncb.Command = netbios.NCBENUM
+ ncb.Buffer = adapters = netbios.LANA_ENUM()
+ adapters._pack()
+ if win32wnet.Netbios(ncb) != 0:
+ return
+ adapters._unpack()
+ for i in range(adapters.length):
+ ncb.Reset()
+ ncb.Command = netbios.NCBRESET
+ ncb.Lana_num = ord(adapters.lana[i])
+ if win32wnet.Netbios(ncb) != 0:
+ continue
+ ncb.Reset()
+ ncb.Command = netbios.NCBASTAT
+ ncb.Lana_num = ord(adapters.lana[i])
+ ncb.Callname = '*'.ljust(16)
+ ncb.Buffer = status = netbios.ADAPTER_STATUS()
+ if win32wnet.Netbios(ncb) != 0:
+ continue
+ status._unpack()
+ bytes = map(ord, status.adapter_address)
+ return ((bytes[0]<<40L) + (bytes[1]<<32L) + (bytes[2]<<24L) +
+ (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5])
+
+# Thanks to Thomas Heller for ctypes and for his help with its use here.
+
+# If ctypes is available, use it to find system routines for UUID generation.
+_uuid_generate_random = _uuid_generate_time = _UuidCreate = None
+try:
+ import ctypes, ctypes.util
+ _buffer = ctypes.create_string_buffer(16)
+
+ # The uuid_generate_* routines are provided by libuuid on at least
+ # Linux and FreeBSD, and provided by libc on Mac OS X.
+ for libname in ['uuid', 'c']:
+ try:
+ lib = ctypes.CDLL(ctypes.util.find_library(libname))
+ except:
+ continue
+ if hasattr(lib, 'uuid_generate_random'):
+ _uuid_generate_random = lib.uuid_generate_random
+ if hasattr(lib, 'uuid_generate_time'):
+ _uuid_generate_time = lib.uuid_generate_time
+
+ # On Windows prior to 2000, UuidCreate gives a UUID containing the
+ # hardware address. On Windows 2000 and later, UuidCreate makes a
+ # random UUID and UuidCreateSequential gives a UUID containing the
+ # hardware address. These routines are provided by the RPC runtime.
+ try:
+ lib = ctypes.windll.rpcrt4
+ except:
+ lib = None
+ _UuidCreate = getattr(lib, 'UuidCreateSequential',
+ getattr(lib, 'UuidCreate', None))
+except:
+ pass
+
+def _unixdll_getnode():
+ """Get the hardware address on Unix using ctypes."""
+ _uuid_generate_time(_buffer)
+ return UUID(bytes=_buffer.raw).node
+
+def _windll_getnode():
+ """Get the hardware address on Windows using ctypes."""
+ if _UuidCreate(_buffer) == 0:
+ return UUID(bytes=_buffer.raw).node
+
+def _random_getnode():
+ """Get a random node ID, with eighth bit set as suggested by RFC 4122."""
+ import random
+ return random.randrange(0, 1<<48L) | 0x010000000000L
+
+_node = None
+
+def getnode():
+ """Get the hardware address as a 48-bit integer. The first time this
+ runs, it may launch a separate program, which could be quite slow. If
+ all attempts to obtain the hardware address fail, we choose a random
+ 48-bit number with its eighth bit set to 1 as recommended in RFC 4122."""
+
+ global _node
+ if _node is not None:
+ return _node
+
+ import sys
+ if sys.platform == 'win32':
+ getters = [_windll_getnode, _netbios_getnode, _ipconfig_getnode]
+ else:
+ getters = [_unixdll_getnode, _ifconfig_getnode]
+
+ for getter in getters + [_random_getnode]:
+ try:
+ _node = getter()
+ except:
+ continue
+ if _node is not None:
+ return _node
+
+def uuid1(node=None, clock_seq=None):
+ """Generate a UUID from a host ID, sequence number, and the current time.
+ If 'node' is not given, getnode() is used to obtain the hardware
+ address. If 'clock_seq' is given, it is used as the sequence number;
+ otherwise a random 14-bit sequence number is chosen."""
+
+ # When the system provides a version-1 UUID generator, use it (but don't
+ # use UuidCreate here because its UUIDs don't conform to RFC 4122).
+ if _uuid_generate_time and node is clock_seq is None:
+ _uuid_generate_time(_buffer)
+ return UUID(bytes=_buffer.raw)
+
+ import time
+ nanoseconds = int(time.time() * 1e9)
+ # 0x01b21dd213814000 is the number of 100-ns intervals between the
+ # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
+ timestamp = int(nanoseconds/100) + 0x01b21dd213814000L
+ if clock_seq is None:
+ import random
+ clock_seq = random.randrange(1<<14L) # instead of stable storage
+ time_low = timestamp & 0xffffffffL
+ time_mid = (timestamp >> 32L) & 0xffffL
+ time_hi_version = (timestamp >> 48L) & 0x0fffL
+ clock_seq_low = clock_seq & 0xffL
+ clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL
+ if node is None:
+ node = getnode()
+ return UUID(fields=(time_low, time_mid, time_hi_version,
+ clock_seq_hi_variant, clock_seq_low, node), version=1)
+
+def uuid3(namespace, name):
+ """Generate a UUID from the MD5 hash of a namespace UUID and a name."""
+ import md5
+ hash = md5.md5(namespace.bytes + name).digest()
+ return UUID(bytes=hash[:16], version=3)
+
+def uuid4():
+ """Generate a random UUID."""
+
+ # When the system provides a version-4 UUID generator, use it.
+ if _uuid_generate_random:
+ _uuid_generate_random(_buffer)
+ return UUID(bytes=_buffer.raw)
+
+ # Otherwise, get randomness from urandom or the 'random' module.
+ try:
+ import os
+ return UUID(bytes=os.urandom(16), version=4)
+ except:
+ import random
+ bytes = [chr(random.randrange(256)) for i in range(16)]
+ return UUID(bytes=bytes, version=4)
+
+def uuid5(namespace, name):
+ """Generate a UUID from the SHA-1 hash of a namespace UUID and a name."""
+ import sha
+ hash = sha.sha(namespace.bytes + name).digest()
+ return UUID(bytes=hash[:16], version=5)
+
+# The following standard UUIDs are for use with uuid3() or uuid5().
+
+NAMESPACE_DNS = UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_URL = UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_OID = UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_X500 = UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8')