Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSantiago Collazo <scollazo@activitycentral.com>2012-10-17 18:08:47 (GMT)
committer Santiago Collazo <scollazo@activitycentral.com>2012-10-17 18:08:47 (GMT)
commit2727bbb9a2c91a97742d4757247a30e31742e33e (patch)
treec80174da7822d9a270ede4406b8cab9a00073fc4
parent624f9263dde350d8916290b5414718571ea8880a (diff)
parent2cc6b91918aa8fabeb68b143f12c71cbf017b613 (diff)
Merge remote-tracking branch 'ajay/0.97.7-as-base' into integration
-rw-r--r--bin/Makefile.am4
-rw-r--r--bin/journal-backup-volume57
-rw-r--r--bin/journal-restore-volume67
-rw-r--r--bin/sugar-launch10
-rwxr-xr-xbin/sugar-session33
-rw-r--r--bin/sugar.in2
-rw-r--r--configure.ac2
-rw-r--r--extensions/cpsection/aboutcomputer/model.py129
-rw-r--r--extensions/cpsection/aboutcomputer/view.py155
-rw-r--r--extensions/cpsection/datetime/model.py47
-rw-r--r--extensions/cpsection/datetime/view.py185
-rw-r--r--extensions/cpsection/modemconfiguration/Makefile.am1
-rw-r--r--extensions/cpsection/modemconfiguration/config.py25
-rwxr-xr-xextensions/cpsection/modemconfiguration/model.py187
-rw-r--r--extensions/cpsection/modemconfiguration/view.py198
-rw-r--r--extensions/cpsection/network/model.py17
-rw-r--r--extensions/cpsection/network/view.py55
-rw-r--r--extensions/deviceicon/Makefile.am1
-rw-r--r--extensions/deviceicon/network.py222
-rw-r--r--extensions/deviceicon/resources.py217
-rw-r--r--po/POTFILES.in1
-rw-r--r--src/Makefile.am2
-rw-r--r--src/jarabe/controlpanel/gui.py5
-rw-r--r--src/jarabe/controlpanel/sectionview.py4
-rw-r--r--src/jarabe/desktop/activitieslist.py17
-rw-r--r--src/jarabe/desktop/keydialog.py238
-rw-r--r--src/jarabe/desktop/networkviews.py248
-rw-r--r--src/jarabe/frame/__init__.py4
-rw-r--r--src/jarabe/frame/activitiestray.py18
-rw-r--r--src/jarabe/frame/frame.py133
-rw-r--r--src/jarabe/frame/notification.py217
-rw-r--r--src/jarabe/intro/window.py79
-rw-r--r--src/jarabe/journal/Makefile.am4
-rw-r--r--src/jarabe/journal/expandedentry.py39
-rw-r--r--src/jarabe/journal/journalactivity.py181
-rw-r--r--src/jarabe/journal/journaltoolbox.py336
-rw-r--r--src/jarabe/journal/journalwindow.py45
-rw-r--r--src/jarabe/journal/keepicon.py11
-rw-r--r--src/jarabe/journal/listmodel.py23
-rw-r--r--src/jarabe/journal/listview.py256
-rw-r--r--src/jarabe/journal/misc.py46
-rw-r--r--src/jarabe/journal/model.py482
-rw-r--r--src/jarabe/journal/objectchooser.py20
-rw-r--r--src/jarabe/journal/palettes.py1024
-rw-r--r--src/jarabe/journal/processdialog.py263
-rw-r--r--src/jarabe/journal/volumestoolbar.py186
-rw-r--r--src/jarabe/journal/webdavmanager.py312
-rw-r--r--src/jarabe/model/Makefile.am1
-rw-r--r--src/jarabe/model/neighborhood.py6
-rw-r--r--src/jarabe/model/network.py11
-rw-r--r--src/jarabe/model/processmanagement.py120
-rw-r--r--src/jarabe/view/buddymenu.py12
-rw-r--r--src/jarabe/view/palettes.py97
-rw-r--r--src/jarabe/view/pulsingicon.py24
-rw-r--r--src/webdav/Condition.py475
-rw-r--r--src/webdav/Connection.py324
-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.py848
-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.py339
-rw-r--r--src/webdav/logger.py51
-rw-r--r--src/webdav/qp_xml.py240
-rw-r--r--src/webdav/uuid_.py476
77 files changed, 11300 insertions, 428 deletions
diff --git a/bin/Makefile.am b/bin/Makefile.am
index 845816c..df53e04 100644
--- a/bin/Makefile.am
+++ b/bin/Makefile.am
@@ -3,7 +3,9 @@ python_scripts = \
sugar-emulator \
sugar-install-bundle \
sugar-launch \
- sugar-session
+ sugar-session \
+ journal-backup-volume \
+ journal-restore-volume
bin_SCRIPTS = \
sugar \
diff --git a/bin/journal-backup-volume b/bin/journal-backup-volume
new file mode 100644
index 0000000..9246760
--- /dev/null
+++ b/bin/journal-backup-volume
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import sys
+import subprocess
+import logging
+
+from sugar import env
+#from sugar.datastore import datastore
+
+backup_identifier = sys.argv[2]
+volume_path = sys.argv[1]
+
+if len(sys.argv) != 3:
+ print 'Usage: %s <volume_path> <backup_identifier>' % sys.argv[0]
+ exit(1)
+
+logging.debug('Backup started')
+
+backup_path = os.path.join(volume_path, 'backup', backup_identifier)
+
+if not os.path.exists(backup_path):
+ os.makedirs(backup_path)
+
+#datastore.freeze()
+#subprocess.call(['pkill', '-9', '-f', 'python.*datastore-service'])
+
+result = 0
+try:
+ cmd = ['tar', '-C', env.get_profile_path(), '-czf', \
+ os.path.join(backup_path, 'datastore.tar.gz'), 'datastore']
+
+ subprocess.check_call(cmd)
+
+except Exception, e:
+ logging.error('Backup failed: %s', str(e))
+ result = 1
+
+#datastore.thaw()
+
+logging.debug('Backup finished')
+exit(result)
diff --git a/bin/journal-restore-volume b/bin/journal-restore-volume
new file mode 100644
index 0000000..f3ad6d8
--- /dev/null
+++ b/bin/journal-restore-volume
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import sys
+import shutil
+import logging
+import subprocess
+
+from sugar import env
+#from sugar.datastore import datastore
+
+backup_identifier = sys.argv[2]
+volume_path = sys.argv[1]
+
+if len(sys.argv) != 3:
+ print 'Usage: %s <volume_path> <backup_identifier>' % sys.argv[0]
+ exit(1)
+
+logging.debug('Restore started')
+
+journal_path = os.path.join(env.get_profile_path(), 'datastore')
+backup_path = os.path.join(volume_path, 'backup', backup_identifier, 'datastore.tar.gz')
+
+if not os.path.exists(backup_path):
+ logging.error('Could not find backup file %s', backup_path)
+ exit(1)
+
+#datastore.freeze()
+subprocess.call(['pkill', '-9', '-f', 'python.*datastore-service'])
+
+result = 0
+try:
+ if os.path.exists(journal_path):
+ shutil.rmtree(journal_path)
+
+ subprocess.check_call(['tar', '-C', env.get_profile_path(), '-xzf', backup_path])
+
+except Exception, e:
+ logging.error('Restore failed: %s', str(e))
+ result = 1
+
+try:
+ shutil.rmtree(os.path.join(journal_path, 'index'))
+ os.remove(os.path.join(journal_path, 'index_updated'))
+ os.remove(os.path.join(journal_path, 'version'))
+except:
+ logging.debug('Restore has no index files')
+
+#datastore.thaw()
+
+logging.debug('Restore finished')
+exit(result)
diff --git a/bin/sugar-launch b/bin/sugar-launch
index 7297a8e..18c0bb7 100644
--- a/bin/sugar-launch
+++ b/bin/sugar-launch
@@ -28,6 +28,13 @@ usage = "usage: %prog [options] activity"
parser = OptionParser(usage)
parser.add_option("-d", "--debug", action="store_true", dest="debug",
help="launch activity inside gdb")
+parser.add_option("-a", "--activity_id", action="store", dest="activity_id",
+ help="unique id for the activity to be launched")
+parser.add_option("-o", "--object_id", action="store", dest="object_id",
+ help="identity of the journal object associated with" \
+ " the activity")
+parser.add_option("-u", "--uri", action="store", dest="uri",
+ help="URI associated with the activity")
(options, args) = parser.parse_args()
if len(args) == 0:
@@ -42,7 +49,8 @@ if not path:
sys.exit(1)
activity = ActivityBundle(path)
-cmd_args = activityfactory.get_command(activity)
+cmd_args = activityfactory.get_command(activity, options.activity_id,
+ options.object_id, options.uri)
def _which(exec_file):
if 'PATH' in os.environ:
diff --git a/bin/sugar-session b/bin/sugar-session
index 5cdc028..8475b9b 100755
--- a/bin/sugar-session
+++ b/bin/sugar-session
@@ -22,6 +22,7 @@ import time
import subprocess
import shutil
+
# Change the default encoding to avoid UnicodeDecodeError
# http://lists.sugarlabs.org/archive/sugar-devel/2012-August/038928.html
reload(sys)
@@ -43,6 +44,10 @@ from gi.repository import GObject
from gi.repository import Gst
import dbus.glib
from gi.repository import Wnck
+from gi.repository import Gio
+
+MONITORS = []
+MONITOR_ACTION_TAKEN = False
_USE_XKL = False
try:
@@ -143,6 +148,15 @@ def setup_notification_service_cb():
from jarabe.model import notifications
notifications.init()
+def show_notifications_cb():
+ client = GConf.Client.get_default()
+ if not client.get_bool('/desktop/sugar/frame/show_notifications'):
+ return
+
+ from ceibal.notifier import Notifier
+ n = Notifier()
+ n.show_messages_from_shell()
+
def setup_file_transfer_cb():
from jarabe.model import filetransfer
filetransfer.init()
@@ -218,14 +232,31 @@ def setup_window_manager():
shell=True):
logging.warning('Can not disable metacity mouse button modifiers')
+def file_monitor_changed_cb(monitor, one_file, other_file, event_type):
+ global MONITOR_ACTION_TAKEN
+ if (not MONITOR_ACTION_TAKEN) and \
+ (one_file.get_path() == os.path.expanduser('~/.sugar/journal_created')):
+ if event_type == Gio.FileMonitorEvent.CREATED:
+ GObject.idle_add(show_notifications_cb)
+ GObject.idle_add(setup_frame_cb)
+ MONITOR_ACTION_TAKEN = True
+
+def arrange_for_setup_frame_cb():
+ path = Gio.File.new_for_path(os.path.expanduser('~/.sugar/journal_created'))
+ monitor = path.monitor_file(Gio.FileMonitorFlags.NONE, None)
+ monitor.connect('changed', file_monitor_changed_cb)
+ MONITORS.append(monitor)
+
def bootstrap():
setup_window_manager()
from jarabe.view import launcher
launcher.setup()
- GObject.idle_add(setup_frame_cb)
GObject.idle_add(setup_keyhandler_cb)
+
+ arrange_for_setup_frame_cb()
+
GObject.idle_add(setup_gesturehandler_cb)
GObject.idle_add(setup_cursortracker_cb)
GObject.idle_add(setup_journal_cb)
diff --git a/bin/sugar.in b/bin/sugar.in
index 2df0ab8..428cbf2 100644
--- a/bin/sugar.in
+++ b/bin/sugar.in
@@ -42,7 +42,7 @@ if test -z "$SUGAR_PROFILE"; then
fi
if test -z "$SUGAR_SCALING"; then
- export SUGAR_SCALING=72
+ export SUGAR_SCALING=100
fi
export GTK2_RC_FILES="@prefix@/share/sugar/data/sugar-$SUGAR_SCALING.gtkrc"
diff --git a/configure.ac b/configure.ac
index 4d28e4a..2052586 100644
--- a/configure.ac
+++ b/configure.ac
@@ -77,6 +77,8 @@ src/jarabe/model/Makefile
src/jarabe/util/Makefile
src/jarabe/util/telepathy/Makefile
src/jarabe/view/Makefile
+src/webdav/Makefile
+src/webdav/acp/Makefile
src/Makefile
])
diff --git a/extensions/cpsection/aboutcomputer/model.py b/extensions/cpsection/aboutcomputer/model.py
index 86d2e3f..5d327ea 100644
--- a/extensions/cpsection/aboutcomputer/model.py
+++ b/extensions/cpsection/aboutcomputer/model.py
@@ -1,4 +1,5 @@
# Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2010 Plan Ceibal <comunidad@plan.ceibal.edu.uy>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -18,9 +19,12 @@
import os
import logging
import re
+import ConfigParser
+import time
import subprocess
from gettext import gettext as _
import errno
+from datetime import datetime
import dbus
@@ -39,6 +43,9 @@ _DMI_DIRECTORY = '/sys/class/dmi/id'
_SN = 'serial-number'
_MODEL = 'openprom/model'
+_XO_1_0_LEASE_PATH = '/security/lease.sig'
+_XO_1_5_LEASE_PATH = '/bootpart/boot/security/lease.sig'
+
_logger = logging.getLogger('ControlPanel - AboutComputer')
_not_available = _('Not available')
@@ -53,6 +60,31 @@ def print_aboutcomputer():
print get_aboutcomputer()
+def _get_lease_path():
+ if os.path.exists(_XO_1_0_LEASE_PATH):
+ return _XO_1_0_LEASE_PATH
+ elif os.path.exists(_XO_1_5_LEASE_PATH):
+ return _XO_1_5_LEASE_PATH
+ else:
+ return ''
+
+
+def get_lease_days():
+ lease_file = _read_file(_get_lease_path())
+ if lease_file is None:
+ return _not_available
+
+ encoded_date = str(str.split(lease_file)[3])
+ expiry_date = datetime.strptime(encoded_date
+ , '%Y%m%dT%H%M%SZ')
+ current_date = datetime.today()
+ days_remaining = (expiry_date - current_date).days
+
+ # TRANS: Do not translate %s
+ str_days_remaining = _('%s days remaining' % str(days_remaining))
+ return str_days_remaining
+
+
def get_serial_number():
serial_no = None
if os.path.exists(os.path.join(_OFW_TREE, _SN)):
@@ -72,7 +104,10 @@ def print_serial_number():
def get_build_number():
- build_no = _read_file('/boot/olpc_build')
+ if os.path.isfile('/boot/olpc_build'):
+ build_no = _read_file('/boot/olpc_build')
+ elif os.path.isfile('/bootpart/olpc_build'):
+ build_no = _read_file('/bootpart/olpc_build')
if build_no is None:
build_no = _read_file('/etc/redhat-release')
@@ -97,6 +132,15 @@ def print_build_number():
print get_build_number()
+def get_model_laptop():
+ from ceibal import laptop
+
+ model_laptop = laptops.get_model_laptop()
+ if model_laptop is None or not model_laptop:
+ model_laptop = _not_available
+ return model_laptop
+
+
def _parse_firmware_number(firmware_no):
if firmware_no is None:
firmware_no = _not_available
@@ -226,3 +270,86 @@ def get_license():
except IOError:
license_text = _not_available
return license_text
+
+
+def get_last_updated_on_field():
+
+ # Get the number of UNIX seconds of the last update date.
+ last_update_unix_seconds = {}
+ try:
+ last_update_unix_seconds = int(os.stat('/var/lib/rpm/Packages').st_mtime)
+ except:
+ msg_str = _('Information not available.')
+ _logger.exception(msg_str)
+ return msg_str
+
+
+ NO_UPDATE_MESSAGE = _('No update yet!')
+
+
+ # Check once again that 'last_update_unix_seconds' is not empty.
+ # You never know !
+ if not last_update_unix_seconds:
+ return NO_UPDATE_MESSAGE
+
+ if int(last_update_unix_seconds) == 1194004800:
+ return NO_UPDATE_MESSAGE
+
+
+ # If we reached here, we have the last-update-time, but it's in
+ # timestamp format.
+ # Using python-subprocess-module (no shell involved), to convert
+ # it into readable date-format; the hack being used (after
+ # removing '-u' option) is the first one mentioned at :
+ # http://www.commandlinefu.com/commands/view/3719/convert-unix-timestamp-to-date
+ environment = os.environ.copy()
+ environment['PATH'] = '%s:/usr/sbin' % (environment['PATH'], )
+
+ last_update_readable_format = {}
+ try:
+ last_update_readable_format = \
+ subprocess.Popen(['date', '-d',
+ '1970-01-01 + ' +
+ str(last_update_unix_seconds) +
+ ' seconds'],
+ stdout=subprocess.PIPE,
+ env=environment).stdout.readlines()[0]
+ except:
+ msg_str = _('Information not available.')
+ _logger.exception(msg_str)
+ return msg_str
+
+ if not last_update_readable_format:
+ return _('Information not available.')
+
+ # Everything should be fine (hopefully :-) )
+ return last_update_readable_format
+
+
+def get_sugar_version():
+ return config.version
+
+
+def get_plazo():
+ from ceibal import env
+ path_plazo = env.get_security_root()
+ try:
+ plazo = _read_file(os.path.join(path_plazo, "blacklist")).split("\n")[0].strip()
+ plazo = time.strftime( "%d-%m-%Y",time.strptime(plazo,'%Y%m%d'))
+ except:
+ plazo = _not_available
+
+ return plazo
+
+def get_act():
+ from ceibal import env
+ path_act = env.get_updates_root()
+ parser = ConfigParser.ConfigParser()
+ salida = parser.read(os.path.join(path_act, "mi_version"))
+ if salida == []:
+ version = _not_available
+ else:
+ version = ''
+ for seccion in parser.sections():
+ version = "%s%s: %s\n" %(version,seccion,parser.get(seccion,'version'))
+ return version
diff --git a/extensions/cpsection/aboutcomputer/view.py b/extensions/cpsection/aboutcomputer/view.py
index f44ca51..1e11301 100644
--- a/extensions/cpsection/aboutcomputer/view.py
+++ b/extensions/cpsection/aboutcomputer/view.py
@@ -23,7 +23,6 @@ from gi.repository import Gdk
from sugar3.graphics import style
-from jarabe import config
from jarabe.controlpanel.sectionview import SectionView
@@ -65,24 +64,51 @@ class AboutComputer(SectionView):
vbox_identity.set_border_width(style.DEFAULT_SPACING * 2)
vbox_identity.set_spacing(style.DEFAULT_SPACING)
- box_identity = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_serial = Gtk.Label(label=_('Serial Number:'))
- label_serial.set_alignment(1, 0)
- label_serial.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_identity.pack_start(label_serial, False, True, 0)
- self._group.add_widget(label_serial)
- label_serial.show()
- label_serial_no = Gtk.Label(label=self._model.get_serial_number())
- label_serial_no.set_alignment(0, 0)
- box_identity.pack_start(label_serial_no, False, True, 0)
- label_serial_no.show()
- vbox_identity.pack_start(box_identity, False, True, 0)
- box_identity.show()
+ self._setup_component_if_applicable(None,
+ _('Serial Number:'),
+ self._model.get_serial_number,
+ vbox_identity)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_lease',
+ _('Lease:'),
+ self._model.get_lease_days,
+ vbox_identity)
self._vbox.pack_start(vbox_identity, False, True, 0)
vbox_identity.show()
+ def _is_feature_to_be_shown(slf, gconf_key):
+ if gconf_key is None:
+ return True
+
+ from gi.repository import GConf
+ client = GConf.Client.get_default()
+
+ return client.get_bool(gconf_key) is True
+
+ def _setup_component_if_applicable(self, gconf_key, key, value_func, packer):
+ if not self._is_feature_to_be_shown(gconf_key):
+ return
+
+ # Now that we do need to show, fetch the value.
+ print value_func
+ value = value_func()
+
+ box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ key_label = Gtk.Label(label=key)
+ key_label.set_alignment(1, 0)
+ key_label.modify_fg(Gtk.StateType.NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box.pack_start(key_label, False, True, 0)
+ self._group.add_widget(key_label)
+ key_label.show()
+ value_label = Gtk.Label(label=value)
+ value_label.set_alignment(0, 0)
+ box.pack_start(value_label, False, True, 0)
+ value_label.show()
+ packer.pack_start(box, False, True, 0)
+ box.show()
+
def _setup_software(self):
separator_software = Gtk.HSeparator()
self._vbox.pack_start(separator_software, False, True, 0)
@@ -96,66 +122,45 @@ class AboutComputer(SectionView):
box_software.set_border_width(style.DEFAULT_SPACING * 2)
box_software.set_spacing(style.DEFAULT_SPACING)
- box_build = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_build = Gtk.Label(label=_('Build:'))
- label_build.set_alignment(1, 0)
- label_build.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_build.pack_start(label_build, False, True, 0)
- self._group.add_widget(label_build)
- label_build.show()
- label_build_no = Gtk.Label(label=self._model.get_build_number())
- label_build_no.set_alignment(0, 0)
- box_build.pack_start(label_build_no, False, True, 0)
- label_build_no.show()
- box_software.pack_start(box_build, False, True, 0)
- box_build.show()
-
- box_sugar = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_sugar = Gtk.Label(label=_('Sugar:'))
- label_sugar.set_alignment(1, 0)
- label_sugar.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_sugar.pack_start(label_sugar, False, True, 0)
- self._group.add_widget(label_sugar)
- label_sugar.show()
- label_sugar_ver = Gtk.Label(label=config.version)
- label_sugar_ver.set_alignment(0, 0)
- box_sugar.pack_start(label_sugar_ver, False, True, 0)
- label_sugar_ver.show()
- box_software.pack_start(box_sugar, False, True, 0)
- box_sugar.show()
-
- box_firmware = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_firmware = Gtk.Label(label=_('Firmware:'))
- label_firmware.set_alignment(1, 0)
- label_firmware.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_firmware.pack_start(label_firmware, False, True, 0)
- self._group.add_widget(label_firmware)
- label_firmware.show()
- label_firmware_no = Gtk.Label(label=self._model.get_firmware_number())
- label_firmware_no.set_alignment(0, 0)
- box_firmware.pack_start(label_firmware_no, False, True, 0)
- label_firmware_no.show()
- box_software.pack_start(box_firmware, False, True, 0)
- box_firmware.show()
-
- box_wireless_fw = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_wireless_fw = Gtk.Label(label=_('Wireless Firmware:'))
- label_wireless_fw.set_alignment(1, 0)
- label_wireless_fw.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_wireless_fw.pack_start(label_wireless_fw, False, True, 0)
- self._group.add_widget(label_wireless_fw)
- label_wireless_fw.show()
- wireless_fw_no = self._model.get_wireless_firmware()
- label_wireless_fw_no = Gtk.Label(label=wireless_fw_no)
- label_wireless_fw_no.set_alignment(0, 0)
- box_wireless_fw.pack_start(label_wireless_fw_no, False, True, 0)
- label_wireless_fw_no.show()
- box_software.pack_start(box_wireless_fw, False, True, 0)
- box_wireless_fw.show()
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_model',
+ _('Model:'),
+ self._model.get_model_laptop,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Build:'),
+ self._model.get_build_number,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Sugar:'),
+ self._model.get_sugar_version,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Firmware:'),
+ self._model.get_firmware_number,
+ box_software)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_wireless_firmware',
+ _('Wireless Firmware:'),
+ self._model.get_wireless_firmware,
+ box_software)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_plazo',
+ _('Plazo:'),
+ self._model.get_plazo,
+ box_software)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_version_de_actual',
+ _('Versión de Actualización:'),
+ self._model.get_act,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Last Updated On:'),
+ self._model.get_last_updated_on_field,
+ box_software)
self._vbox.pack_start(box_software, False, True, 0)
box_software.show()
diff --git a/extensions/cpsection/datetime/model.py b/extensions/cpsection/datetime/model.py
index c9b4586..f73bef7 100644
--- a/extensions/cpsection/datetime/model.py
+++ b/extensions/cpsection/datetime/model.py
@@ -21,10 +21,57 @@
#
import os
+import logging
+
from gettext import gettext as _
from gi.repository import GConf
_zone_tab = '/usr/share/zoneinfo/zone.tab'
+NTPDATE_PATH = '/usr/sbin/ntpdate'
+NTP_SERVER_CONFIG_FILENAME = '/etc/ntp/step-tickers'
+
+_logger = logging.getLogger('ControlPanel - TimeZone')
+
+
+def is_ntp_servers_config_feature_available():
+ return os.path.exists(NTPDATE_PATH)
+
+
+def get_ntp_servers():
+ servers = []
+
+ # If the file does not exist, return.
+ if not os.path.exists(NTP_SERVER_CONFIG_FILENAME):
+ return servers
+
+ f = open(NTP_SERVER_CONFIG_FILENAME, 'r')
+ for server in f.readlines():
+ servers.append(server.rstrip('\n'))
+ f.close()
+
+ return servers
+
+
+def set_ntp_servers(servers):
+
+ # First remove the old ssid-file, if it exists.
+ if os.path.exists(NTP_SERVER_CONFIG_FILENAME):
+ try:
+ os.remove(NTP_SERVER_CONFIG_FILENAME)
+ except:
+ _logger.exception('Error removing file.')
+ return
+
+ # Do nothing and return, if the values-list is empty
+ if len(servers) == 0:
+ return
+
+ # If we reach here, we have a non-empty ssid-values-list.
+ f = open(NTP_SERVER_CONFIG_FILENAME, 'w')
+ for server in servers:
+ if len(server) > 0:
+ f.write(server + '\n')
+ f.close()
def _initialize():
diff --git a/extensions/cpsection/datetime/view.py b/extensions/cpsection/datetime/view.py
index 64789b4..4ad94ca 100644
--- a/extensions/cpsection/datetime/view.py
+++ b/extensions/cpsection/datetime/view.py
@@ -20,11 +20,130 @@ from gettext import gettext as _
from sugar3.graphics import style
from sugar3.graphics import iconentry
+from sugar3.graphics.icon import Icon
from jarabe.controlpanel.sectionview import SectionView
from jarabe.controlpanel.inlinealert import InlineAlert
+class AddRemoveWidget(Gtk.HBox):
+
+ def __init__(self, label, add_button_clicked_cb,
+ remove_button_clicked_cb, index):
+ Gtk.Box.__init__(self)
+ self.set_homogeneous(False)
+ self.set_spacing(10)
+
+ self._index = index
+ self._add_button_added = False
+ self._remove_button_added = False
+
+ self._entry_box = Gtk.Entry()
+ self._entry_box.set_text(label)
+ self._entry_box.set_width_chars(40)
+ self.pack_start(self._entry_box, False, False, 0)
+ self._entry_box.show()
+
+ add_icon = Icon(icon_name='list-add')
+ self._add_button = Gtk.Button()
+ self._add_button.set_image(add_icon)
+ self._add_button.connect('clicked',
+ add_button_clicked_cb,
+ self)
+
+ remove_icon = Icon(icon_name='list-remove')
+ self._remove_button = Gtk.Button()
+ self._remove_button.set_image(remove_icon)
+ self._remove_button.connect('clicked',
+ remove_button_clicked_cb,
+ self)
+ self.__add_add_button()
+ self.__add_remove_button()
+
+ def _get_index(self):
+ return self._index
+
+ def _set_index(self, value):
+ self._index = value
+
+ def _get_entry(self):
+ return self._entry_box.get_text()
+
+ def __add_add_button(self):
+ self.pack_start(self._add_button, False, False, 0)
+ self._add_button.show()
+ self._add_button_added = True
+
+ def _remove_remove_button_if_not_already(self):
+ if self._remove_button_added:
+ self.__remove_remove_button()
+
+ def __remove_remove_button(self):
+ self.remove(self._remove_button)
+ self._remove_button_added = False
+
+ def _add_remove_button_if_not_already(self):
+ if not self._remove_button_added:
+ self.__add_remove_button()
+
+ def __add_remove_button(self):
+ self.pack_start(self._remove_button, False, False, 0)
+ self._remove_button.show()
+ self._remove_button_added = True
+
+
+class MultiWidget(Gtk.VBox):
+
+ def __init__(self):
+ Gtk.VBox.__init__(self)
+
+ def _add_widget(self, label):
+ new_widget = AddRemoveWidget(label,
+ self.__add_button_clicked_cb,
+ self.__remove_button_clicked_cb,
+ len(self.get_children()))
+ self.add(new_widget)
+ new_widget.show()
+ self.show()
+ self._update_remove_button_statuses()
+
+ def __add_button_clicked_cb(self, add_button,
+ add_button_container):
+ self._add_widget('')
+ self._update_remove_button_statuses()
+
+ def __remove_button_clicked_cb(self, remove_button,
+ remove_button_container):
+ for child in self.get_children():
+ if child._get_index() > remove_button_container._get_index():
+ child._set_index(child._get_index() - 1)
+
+ self.remove(remove_button_container)
+ self._update_remove_button_statuses()
+
+ def _update_remove_button_statuses(self):
+ children = self.get_children()
+
+ # Now, if there is only one entry, remove-button
+ # should not be shown.
+ if len(children) == 1:
+ children[0]._remove_remove_button_if_not_already()
+
+ # Alternatively, if there are more than 1 entries,
+ # remove-button should be shown for all.
+ if len(children) > 1:
+ for child in children:
+ child._add_remove_button_if_not_already()
+
+
+ def _get_entries(self):
+ entries = []
+ for child in self.get_children():
+ entries.append(child._get_entry())
+
+ return entries
+
+
class TimeZone(SectionView):
def __init__(self, model, alerts):
SectionView.__init__(self)
@@ -64,7 +183,10 @@ class TimeZone(SectionView):
self._treeview.set_search_entry(self._entry)
self._treeview.set_search_equal_func(self._search, None)
self._treeview.set_search_column(0)
- self._scrolled_window.add(self._treeview)
+ self._timezone_box = Gtk.VBox()
+ self._scrolled_window.add(self._timezone_box)
+ self._timezone_box.show_all()
+ self._timezone_box.add(self._treeview)
self._treeview.show()
self._timezone_column = Gtk.TreeViewColumn(_('Timezone'))
@@ -74,19 +196,29 @@ class TimeZone(SectionView):
self._timezone_column.set_sort_column_id(0)
self._treeview.append_column(self._timezone_column)
- self.pack_start(self._scrolled_window, True, True, 0)
- self._scrolled_window.show()
+ self._container = Gtk.VBox()
+ self._container.set_homogeneous(False)
+ self._container.pack_start(self._scrolled_window, True, True, 0)
+ self._container.set_spacing(style.DEFAULT_SPACING)
+ self._container.show_all()
+ self.pack_start(self._container, True, True, 0)
self._zone_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- self.pack_start(self._zone_alert_box, False, False, 0)
-
+ self._timezone_box.pack_start(self._zone_alert_box, False, False, 0)
self._zone_alert = InlineAlert()
self._zone_alert_box.pack_start(self._zone_alert, True, True, 0)
if 'zone' in self.restart_alerts:
self._zone_alert.props.msg = self.restart_msg
self._zone_alert.show()
+
+ # Not showing this, as this hides the selected timezone.
+ # Instead, the alert will anyways be shown when user clicks
+ # on "Ok".
+ #self._zone_alert.show()
self._zone_alert_box.show()
+ self._ntp_ui_setup = False
+
self.setup()
def setup(self):
@@ -102,6 +234,45 @@ class TimeZone(SectionView):
self.needs_restart = False
self._cursor_change_handler = self._treeview.connect( \
'cursor-changed', self.__zone_changed_cd)
+ if self._model.is_ntp_servers_config_feature_available():
+ self.setup_ui_for_ntp_server_config()
+
+ def setup_ui_for_ntp_server_config(self):
+ if self._ntp_ui_setup:
+ return
+ self._ntp_ui_setup = True
+
+ self._ntp_scrolled_window = Gtk.ScrolledWindow()
+ self._ntp_scrolled_window.set_policy(Gtk.PolicyType.NEVER,
+ Gtk.PolicyType.AUTOMATIC)
+ box_ntp_servers_config = Gtk.VBox()
+ box_ntp_servers_config.set_spacing(style.DEFAULT_SPACING)
+
+ separator_ntp_servers_config= Gtk.HSeparator()
+ self._container.pack_start(separator_ntp_servers_config,
+ False, False, 0)
+ separator_ntp_servers_config.show()
+
+ label_ntp_servers_config = Gtk.Label(_('NTP Servers Configuration'))
+ label_ntp_servers_config.set_alignment(0, 0)
+ self._container.pack_start(label_ntp_servers_config,
+ False, False, 0)
+ label_ntp_servers_config.show()
+
+ self._widget_table = MultiWidget()
+ box_ntp_servers_config.pack_start(self._widget_table, False, False, 0)
+ box_ntp_servers_config.show_all()
+
+ self._ntp_scrolled_window.add_with_viewport(box_ntp_servers_config)
+ self._container.pack_start(self._ntp_scrolled_window, True, True, 0)
+ self._ntp_scrolled_window.show_all()
+
+ ntp_servers = self._model.get_ntp_servers()
+ if len(ntp_servers) == 0:
+ self._widget_table._add_widget('')
+ else:
+ for server in ntp_servers:
+ self._widget_table._add_widget(server)
def undo(self):
self._treeview.disconnect(self._cursor_change_handler)
@@ -136,5 +307,7 @@ class TimeZone(SectionView):
self.restart_alerts.append('zone')
self.needs_restart = True
self._zone_alert.props.msg = self.restart_msg
- self._zone_alert.show()
return False
+
+ def perform_accept_actions(self):
+ self._model.set_ntp_servers(self._widget_table._get_entries())
diff --git a/extensions/cpsection/modemconfiguration/Makefile.am b/extensions/cpsection/modemconfiguration/Makefile.am
index 3e2613e..46f8e70 100644
--- a/extensions/cpsection/modemconfiguration/Makefile.am
+++ b/extensions/cpsection/modemconfiguration/Makefile.am
@@ -3,4 +3,5 @@ sugardir = $(pkgdatadir)/extensions/cpsection/modemconfiguration
sugar_PYTHON = \
__init__.py \
model.py \
+ config.py \
view.py
diff --git a/extensions/cpsection/modemconfiguration/config.py b/extensions/cpsection/modemconfiguration/config.py
new file mode 100644
index 0000000..9e27814
--- /dev/null
+++ b/extensions/cpsection/modemconfiguration/config.py
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+# Copyright (C) 2010 Andres Ambrois
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 US
+
+
+PROVIDERS_PATH = "/usr/share/mobile-broadband-provider-info/serviceproviders.xml"
+PROVIDERS_FORMAT_SUPPORTED = "2.0"
+COUNTRY_CODES_PATH = "/usr/share/zoneinfo/iso3166.tab"
+
+GSM_COUNTRY_PATH = '/desktop/sugar/network/gsm/country'
+GSM_PROVIDERS_PATH = '/desktop/sugar/network/gsm/providers'
+GSM_PLAN_PATH = '/desktop/sugar/network/gsm/plan'
diff --git a/extensions/cpsection/modemconfiguration/model.py b/extensions/cpsection/modemconfiguration/model.py
index f457293..e33d881 100755
--- a/extensions/cpsection/modemconfiguration/model.py
+++ b/extensions/cpsection/modemconfiguration/model.py
@@ -19,14 +19,27 @@ import logging
import dbus
from gi.repository import Gtk
+import os
+import locale
+import logging
+import gconf
+
+from xml.etree.cElementTree import ElementTree
+from gettext import gettext as _
+
from jarabe.model import network
+from cpsection.modemconfiguration.config import PROVIDERS_PATH, \
+ PROVIDERS_FORMAT_SUPPORTED, \
+ COUNTRY_CODES_PATH
+
+
def get_connection():
return network.find_gsm_connection()
-def get_modem_settings():
+def get_modem_settings(callback):
modem_settings = {}
connection = get_connection()
if not connection:
@@ -48,6 +61,10 @@ def get_modem_settings():
modem_settings['password'] = gsm_secrets.get('password', '')
modem_settings['pin'] = gsm_secrets.get('pin', '')
+ # sl#3800: We return the settings, via the "_secrets_cb()
+ # method", instead of busy-waiting.
+ callback(modem_settings)
+
def _secrets_err_cb(err):
secrets_call_done[0] = True
if isinstance(err, dbus.exceptions.DBusException) and \
@@ -57,14 +74,11 @@ def get_modem_settings():
logging.error('Error retrieving GSM secrets: %s', err)
# must be called asynchronously as this re-enters the GTK main loop
+ #
+ # sl#3800: We return the settings, via the "_secrets_cb()" method,
+ # instead of busy-waiting.
connection.get_secrets('gsm', _secrets_cb, _secrets_err_cb)
- # wait til asynchronous execution completes
- while not secrets_call_done[0]:
- Gtk.main_iteration()
-
- return modem_settings
-
def _set_or_clear(_dict, key, value):
"""Update a dictionary value for a specific key. If value is None or
@@ -98,3 +112,162 @@ def set_modem_settings(modem_settings):
_set_or_clear(gsm_settings, 'apn', apn)
_set_or_clear(gsm_settings, 'pin', pin)
connection.update_settings(settings)
+
+
+def has_providers_db():
+ if not os.path.isfile(COUNTRY_CODES_PATH):
+ logging.debug("Mobile broadband provider database: Country " \
+ "codes path %s not found.", COUNTRY_CODES_PATH)
+ return False
+ try:
+ tree = ElementTree(file=PROVIDERS_PATH)
+ except (IOError, SyntaxError), e:
+ logging.debug("Mobile broadband provider database: Could not read " \
+ "provider information %s error=%s", PROVIDERS_PATH)
+ return False
+ else:
+ elem = tree.getroot()
+ if elem is None or elem.get('format') != PROVIDERS_FORMAT_SUPPORTED:
+ logging.debug("Mobile broadband provider database: Could not " \
+ "read provider information. %s is wrong format.",
+ elem.get('format'))
+ return False
+ return True
+
+
+class CountryListStore(Gtk.ListStore):
+ COUNTRY_CODE = locale.getdefaultlocale()[0][3:5].lower()
+
+ def __init__(self):
+ Gtk.ListStore.__init__(self, str, object)
+ codes = {}
+ with open(COUNTRY_CODES_PATH) as codes_file:
+ for line in codes_file:
+ if line.startswith('#'):
+ continue
+ code, name = line.split('\t')[:2]
+ codes[code.lower()] = name.strip()
+ etree = ElementTree(file=PROVIDERS_PATH).getroot()
+ self._country_idx = None
+ i = 0
+
+ # This dictionary wil store the values, with "country-name" as
+ # the key, and "country-code" as the value.
+ temp_dict = {}
+
+ for elem in etree.findall('.//country'):
+ code = elem.attrib['code']
+ if code == self.COUNTRY_CODE:
+ self._country_idx = i
+ else:
+ i += 1
+ if code in codes:
+ temp_dict[codes[code]] = elem
+ else:
+ temp_dict[code] = elem
+
+ # Now, sort the list by country-names.
+ country_name_keys = temp_dict.keys()
+ country_name_keys.sort()
+
+ for country_name in country_name_keys:
+ self.append((country_name, temp_dict[country_name]))
+
+ def get_row_providers(self, row):
+ return self[row][1]
+
+ def guess_country_row(self):
+ if self._country_idx is not None:
+ return self._country_idx
+ else:
+ return 0
+
+ def search_index_by_code(self, code):
+ for index in range(0, len(self)):
+ if self[index][0] == code:
+ return index
+ return -1
+
+
+class ProviderListStore(Gtk.ListStore):
+ def __init__(self, elem):
+ Gtk.ListStore.__init__(self, str, object)
+ for provider_elem in elem.findall('.//provider'):
+ apns = provider_elem.findall('.//apn')
+ if not apns:
+ # Skip carriers with CDMA entries only
+ continue
+ self.append((provider_elem.find('.//name').text, apns))
+
+ def get_row_plans(self, row):
+ return self[row][1]
+
+ def guess_providers_row(self):
+ # Simply return the first entry as the default.
+ return 0
+
+ def search_index_by_code(self, code):
+ for index in range(0, len(self)):
+ if self[index][0] == code:
+ return index
+ return -1
+
+
+class PlanListStore(Gtk.ListStore):
+ LANG_NS_ATTR = '{http://www.w3.org/XML/1998/namespace}lang'
+ LANG = locale.getdefaultlocale()[0][:2]
+ DEFAULT_NUMBER = '*99#'
+
+ def __init__(self, elems):
+ Gtk.ListStore.__init__(self, str, object)
+ for apn_elem in elems:
+ plan = {}
+ names = apn_elem.findall('.//name')
+ if names:
+ for name in names:
+ if name.get(self.LANG_NS_ATTR) is None:
+ # serviceproviders.xml default value
+ plan['name'] = name.text
+ elif name.get(self.LANG_NS_ATTR) == self.LANG:
+ # Great! We found a name value for our locale!
+ plan['name'] = name.text
+ break
+ else:
+ plan['name'] = _('Default')
+ plan['apn'] = apn_elem.get('value')
+ user = apn_elem.find('.//username')
+ if user is not None:
+ plan['username'] = user.text
+ else:
+ plan['username'] = ''
+ passwd = apn_elem.find('.//password')
+ if passwd is not None:
+ plan['password'] = passwd.text
+ else:
+ plan['password'] = ''
+
+ plan['number'] = self.DEFAULT_NUMBER
+
+ self.append((plan['name'], plan))
+
+ def get_row_plan(self, row):
+ return self[row][1]
+
+ def guess_plan_row(self):
+ # Simply return the first entry as the default.
+ return 0
+
+ def search_index_by_code(self, code):
+ for index in range(0, len(self)):
+ if self[index][0] == code:
+ return index
+ return -1
+
+
+def get_gconf_setting_string(gconf_key):
+ client = gconf.client_get_default()
+ return client.get_string(gconf_key) or ''
+
+def set_gconf_setting_string(gconf_key, gconf_setting_string_value):
+ client = gconf.client_get_default()
+ client.set_string(gconf_key, gconf_setting_string_value)
diff --git a/extensions/cpsection/modemconfiguration/view.py b/extensions/cpsection/modemconfiguration/view.py
index d5aa399..d218330 100644
--- a/extensions/cpsection/modemconfiguration/view.py
+++ b/extensions/cpsection/modemconfiguration/view.py
@@ -24,6 +24,10 @@ from sugar3.graphics import style
from jarabe.controlpanel.sectionview import SectionView
+from cpsection.modemconfiguration.config import GSM_COUNTRY_PATH, \
+ GSM_PROVIDERS_PATH, \
+ GSM_PLAN_PATH
+
APPLY_TIMEOUT = 1000
@@ -64,6 +68,17 @@ class ModemConfiguration(SectionView):
self.set_border_width(style.DEFAULT_SPACING)
self.set_spacing(style.DEFAULT_SPACING)
self._group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
+ self._combo_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
+
+ scrolled_win = Gtk.ScrolledWindow()
+ scrolled_win.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
+ scrolled_win.show()
+ self.add(scrolled_win)
+
+ main_box = Gtk.VBox(spacing=style.DEFAULT_SPACING)
+ main_box.set_border_width(style.DEFAULT_SPACING)
+ main_box.show()
+ scrolled_win.add_with_viewport(main_box)
explanation = _('You will need to provide the following information'
' to set up a mobile broadband connection to a'
@@ -71,41 +86,85 @@ class ModemConfiguration(SectionView):
self._text = Gtk.Label(label=explanation)
self._text.set_line_wrap(True)
self._text.set_alignment(0, 0)
- self.pack_start(self._text, False, False, 0)
+ main_box.pack_start(self._text, False, False, 0)
self._text.show()
+ if model.has_providers_db():
+ self._upper_box = Gtk.VBox(spacing=style.DEFAULT_SPACING)
+ self._upper_box.set_border_width(style.DEFAULT_SPACING)
+ main_box.pack_start(self._upper_box, True, True, 0)
+ self._upper_box.show()
+
+ # Do not attach any 'change'-handlers for now.
+ # They will be attached (once per combobox), once the
+ # individual combobox is processed at startup.
+ self._country_store = model.CountryListStore()
+ self._country_combo = Gtk.ComboBox(model=self._country_store)
+ self._attach_combobox_widget(_('Country:'),
+ self._country_combo)
+
+ self._providers_combo = Gtk.ComboBox()
+ self._attach_combobox_widget(_('Provider:'),
+ self._providers_combo)
+
+ self._plan_combo = Gtk.ComboBox()
+ self._attach_combobox_widget(_('Plan:'),
+ self._plan_combo)
+
+ separator = Gtk.HSeparator()
+ main_box.pack_start(separator, True, True, 0)
+ separator.show()
+
+ self._lower_box = Gtk.VBox(spacing=style.DEFAULT_SPACING)
+ self._lower_box.set_border_width(style.DEFAULT_SPACING)
+ main_box.pack_start(self._lower_box, True, True, 0)
+ self._lower_box.show()
+
self._username_entry = EntryWithLabel(_('Username:'))
- self._username_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._username_entry.label)
- self.pack_start(self._username_entry, False, True, 0)
- self._username_entry.show()
+ self._attach_entry_widget(self._username_entry)
self._password_entry = EntryWithLabel(_('Password:'))
- self._password_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._password_entry.label)
- self.pack_start(self._password_entry, False, True, 0)
- self._password_entry.show()
+ self._attach_entry_widget(self._password_entry)
self._number_entry = EntryWithLabel(_('Number:'))
- self._number_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._number_entry.label)
- self.pack_start(self._number_entry, False, True, 0)
- self._number_entry.show()
+ self._attach_entry_widget(self._number_entry)
self._apn_entry = EntryWithLabel(_('Access Point Name (APN):'))
- self._apn_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._apn_entry.label)
- self.pack_start(self._apn_entry, False, True, 0)
- self._apn_entry.show()
+ self._attach_entry_widget(self._apn_entry)
self._pin_entry = EntryWithLabel(_('Personal Identity Number (PIN):'))
- self._pin_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._pin_entry.label)
- self.pack_start(self._pin_entry, False, True, 0)
- self._pin_entry.show()
+ self._attach_entry_widget(self._pin_entry)
self.setup()
+ def _attach_combobox_widget(self, label_text, combobox_obj):
+ box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label = Gtk.Label(label_text)
+ self._group.add_widget(label)
+ label.set_alignment(1, 0.5)
+ box.pack_start(label, False, False, 0)
+ label.show()
+
+ self._combo_group.add_widget(combobox_obj)
+ cell = Gtk.CellRendererText()
+ cell.props.xalign = 0.5
+
+ cell.set_property('width-chars', 30)
+
+ combobox_obj.pack_start(cell, True)
+ combobox_obj.add_attribute(cell, 'text', 0)
+
+ box.pack_start(combobox_obj, False, False, 0)
+ combobox_obj.show()
+ self._upper_box.pack_start(box, False, False, 0)
+ box.show()
+
+ def _attach_entry_widget(self, entry_with_label_obj):
+ entry_with_label_obj.entry.connect('changed', self.__entry_changed_cb)
+ self._group.add_widget(entry_with_label_obj.label)
+ self._lower_box.pack_start(entry_with_label_obj, True, True, 0)
+ entry_with_label_obj.show()
+
def undo(self):
self._model.undo()
@@ -113,12 +172,31 @@ class ModemConfiguration(SectionView):
"""Populate an entry with text, without triggering its 'changed'
handler."""
entry = entrywithlabel.entry
- entry.handler_block_by_func(self.__entry_changed_cb)
+
+ # Do not block/unblock the callback functions.
+ #
+ # Thus, the savings will be persisted to the NM settings,
+ # whenever any setting on the UI changes (by user-intervention,
+ # or otherwise).
+ #entry.handler_block_by_func(self.__entry_changed_cb)
entry.set_text(text)
- entry.handler_unblock_by_func(self.__entry_changed_cb)
+ #entry.handler_unblock_by_func(self.__entry_changed_cb)
def setup(self):
- settings = self._model.get_modem_settings()
+ if self._model.has_providers_db():
+ persisted_country = self._model.get_gconf_setting_string(GSM_COUNTRY_PATH)
+ if (self._model.has_providers_db()) and (persisted_country != ''):
+ self._country_combo.set_active(self._country_store.search_index_by_code(persisted_country))
+ else:
+ self._country_combo.set_active(self._country_store.guess_country_row())
+
+ # Call the selected callback anyway, so as to chain-set the
+ # default values for providers and the plans.
+ self.__country_selected_cb(self._country_combo, setup=True)
+
+ self._model.get_modem_settings(self.populate_entries)
+
+ def populate_entries(self, settings):
self._populate_entry(self._username_entry,
settings.get('username', ''))
self._populate_entry(self._number_entry, settings.get('number', ''))
@@ -133,6 +211,78 @@ class ModemConfiguration(SectionView):
self._timeout_sid = GObject.timeout_add(APPLY_TIMEOUT,
self.__timeout_cb)
+ def _get_selected_text(self, combo):
+ active_iter = combo.get_active_iter()
+ return combo.get_model().get(active_iter, 0)[0]
+
+ def __country_selected_cb(self, combo, setup=False):
+ country = self._get_selected_text(combo)
+ self._model.set_gconf_setting_string(GSM_COUNTRY_PATH, country)
+
+ model = combo.get_model()
+ providers = model.get_row_providers(combo.get_active())
+ self._providers_liststore = self._model.ProviderListStore(providers)
+ self._providers_combo.set_model(self._providers_liststore)
+
+ # Set the default provider as well.
+ if setup:
+ persisted_provider = self._model.get_gconf_setting_string(GSM_PROVIDERS_PATH)
+ if persisted_provider == '':
+ self._providers_combo.set_active(self._providers_liststore.guess_providers_row())
+ else:
+ self._providers_combo.set_active(self._providers_liststore.search_index_by_code(persisted_provider))
+ else:
+ self._providers_combo.set_active(self._providers_liststore.guess_providers_row())
+
+ # Country-combobox processed once at startip; now, attach the
+ # change-handler.
+ self._country_combo.connect('changed', self.__country_selected_cb, False)
+
+ # Call the callback, so that default provider may be set.
+ self.__provider_selected_cb(self._providers_combo, setup)
+
+ def __provider_selected_cb(self, combo, setup=False):
+ provider = self._get_selected_text(combo)
+ self._model.set_gconf_setting_string(GSM_PROVIDERS_PATH, provider)
+
+ model = combo.get_model()
+ plans = model.get_row_plans(combo.get_active())
+ self._plan_liststore = self._model.PlanListStore(plans)
+ self._plan_combo.set_model(self._plan_liststore)
+
+ # Set the default plan as well.
+ if setup:
+ persisted_plan = self._model.get_gconf_setting_string(GSM_PLAN_PATH)
+ if persisted_plan == '':
+ self._plan_combo.set_active(self._plan_liststore.guess_plan_row())
+ else:
+ self._plan_combo.set_active(self._plan_liststore.search_index_by_code(persisted_plan))
+ else:
+ self._plan_combo.set_active(self._plan_liststore.guess_plan_row())
+
+ # Providers-combobox processed once at startip; now, attach the
+ # change-handler.
+ self._providers_combo.connect('changed', self.__provider_selected_cb, False)
+
+ # Call the callback, so that the default plan is set.
+ self.__plan_selected_cb(self._plan_combo, setup)
+
+ def __plan_selected_cb(self, combo, setup=False):
+ plan = self._get_selected_text(combo)
+ self._model.set_gconf_setting_string(GSM_PLAN_PATH, plan)
+
+ # Plan-combobox processed once at startip; now, attach the
+ # change-handler.
+ self._plan_combo.connect('changed', self.__plan_selected_cb, False)
+
+ model = combo.get_model()
+ plan = model.get_row_plan(combo.get_active())
+
+ self._populate_entry(self._username_entry, plan['username'])
+ self._populate_entry(self._password_entry, plan['password'])
+ self._populate_entry(self._apn_entry, plan['apn'])
+ self._populate_entry(self._number_entry, plan['number'])
+
def __timeout_cb(self):
self._timeout_sid = 0
settings = {}
diff --git a/extensions/cpsection/network/model.py b/extensions/cpsection/network/model.py
index ae9e64d..83c3cf1 100644
--- a/extensions/cpsection/network/model.py
+++ b/extensions/cpsection/network/model.py
@@ -18,6 +18,10 @@
import logging
import dbus
+import os
+import subprocess
+import logging
+
from gettext import gettext as _
from gi.repository import GConf
@@ -30,6 +34,8 @@ _NM_IFACE = 'org.freedesktop.NetworkManager'
KEYWORDS = ['network', 'jabber', 'radio', 'server']
+_logger = logging.getLogger('ControlPanel - Network')
+
class ReadError(Exception):
def __init__(self, value):
@@ -154,3 +160,14 @@ def set_publish_information(value):
client = GConf.Client.get_default()
client.set_bool('/desktop/sugar/collaboration/publish_gadget', value)
return 0
+
+
+def launch_nm_connection_editor():
+ environment = os.environ.copy()
+ environment['PATH'] = '%s:/usr/sbin' % (environment['PATH'], )
+
+ try:
+ subprocess.Popen(['-c', 'sudo nm-connection-editor --type=802-11-wireless'],
+ shell=True)
+ except:
+ _logger.exception('Error running nm-connection-editor')
diff --git a/extensions/cpsection/network/view.py b/extensions/cpsection/network/view.py
index 9b89375..e4332e4 100644
--- a/extensions/cpsection/network/view.py
+++ b/extensions/cpsection/network/view.py
@@ -17,6 +17,7 @@
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
+from gi.repository import GConf
from gettext import gettext as _
from sugar3.graphics import style
@@ -30,6 +31,9 @@ ICON = 'module-network'
TITLE = _('Network')
_APPLY_TIMEOUT = 3000
+EXPLICIT_REBOOT_MESSAGE = _('Please restart your computer for changes to take effect.')
+
+gconf_client = GConf.Client.get_default()
class Network(SectionView):
@@ -51,6 +55,7 @@ class Network(SectionView):
self._radio_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
self._jabber_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._nm_connection_editor_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
@@ -178,8 +183,54 @@ class Network(SectionView):
workspace.pack_start(box_mesh, False, True, 0)
box_mesh.show()
+ if gconf_client.get_bool('/desktop/sugar/extensions/network/show_nm_connection_editor') is True:
+ box_nm_connection_editor = self.add_nm_connection_editor_launcher(workspace)
+
self.setup()
+ def add_nm_connection_editor_launcher(self, workspace):
+ separator_nm_connection_editor = Gtk.HSeparator()
+ workspace.pack_start(separator_nm_connection_editor, False, True, 0)
+ separator_nm_connection_editor.show()
+
+ label_nm_connection_editor = Gtk.Label(_('Advanced Network Settings'))
+ label_nm_connection_editor.set_alignment(0, 0)
+ workspace.pack_start(label_nm_connection_editor, False, True, 0)
+ label_nm_connection_editor.show()
+
+ box_nm_connection_editor = Gtk.VBox()
+ box_nm_connection_editor.set_border_width(style.DEFAULT_SPACING * 2)
+ box_nm_connection_editor.set_spacing(style.DEFAULT_SPACING)
+
+ info = Gtk.Label(_("For more specific network settings, use "
+ "the NetworkManager Connection Editor."))
+
+ info.set_alignment(0, 0)
+ info.set_line_wrap(True)
+ box_nm_connection_editor.pack_start(info, False, True, 0)
+
+ self._nm_connection_editor_alert = InlineAlert()
+ self._nm_connection_editor_alert.props.msg = EXPLICIT_REBOOT_MESSAGE
+ self._nm_connection_editor_alert_box.pack_start(self._nm_connection_editor_alert,
+ False, True, 0)
+ box_nm_connection_editor.pack_end(self._nm_connection_editor_alert_box,
+ False, True, 0)
+ self._nm_connection_editor_alert_box.show()
+ self._nm_connection_editor_alert.show()
+
+ launch_button = Gtk.Button()
+ launch_button.set_alignment(0, 0)
+ launch_button.set_label(_('Launch'))
+ launch_button.connect('clicked', self.__launch_button_clicked_cb)
+ box_launch_button = Gtk.HBox()
+ box_launch_button.set_homogeneous(False)
+ box_launch_button.pack_start(launch_button, False, True, 0)
+ box_launch_button.show_all()
+
+ box_nm_connection_editor.pack_start(box_launch_button, False, True, 0)
+ workspace.pack_start(box_nm_connection_editor, False, True, 0)
+ box_nm_connection_editor.show_all()
+
def setup(self):
self._entry.set_text(self._model.get_jabber())
try:
@@ -260,3 +311,7 @@ class Network(SectionView):
self._model.clear_networks()
if not self._model.have_networks():
self._clear_history_button.set_sensitive(False)
+
+ def __launch_button_clicked_cb(self, launch_button):
+ self._model.launch_nm_connection_editor()
+
diff --git a/extensions/deviceicon/Makefile.am b/extensions/deviceicon/Makefile.am
index 96a1753..42f3a4b 100644
--- a/extensions/deviceicon/Makefile.am
+++ b/extensions/deviceicon/Makefile.am
@@ -5,6 +5,7 @@ sugar_PYTHON = \
battery.py \
frame.py \
network.py \
+ resources.py \
speaker.py \
speech.py \
touchpad.py \
diff --git a/extensions/deviceicon/network.py b/extensions/deviceicon/network.py
index 79bc764..8ca9077 100644
--- a/extensions/deviceicon/network.py
+++ b/extensions/deviceicon/network.py
@@ -23,6 +23,8 @@ import logging
import hashlib
import socket
import struct
+import random
+import re
import datetime
import time
from gi.repository import Gtk
@@ -30,6 +32,7 @@ import glib
from gi.repository import GObject
from gi.repository import GConf
import dbus
+import uuid
from sugar3.graphics.icon import get_icon_state
from sugar3.graphics import style
@@ -55,6 +58,14 @@ _GSM_STATE_CONNECTING = 2
_GSM_STATE_CONNECTED = 3
_GSM_STATE_FAILED = 4
+_GSM_SHARING_PRIVATE = 0
+_GSM_SHARING_TRYING = 1
+_GSM_SHARING_NEIGHBORHOOD = 2
+
+_GSM_SHARING_CHANNELS = [2,3,4,5,7,8,9,10,12,13]
+
+_wifi_device = None
+
class WirelessPalette(Palette):
__gtype_name__ = 'SugarWirelessPalette'
@@ -200,6 +211,8 @@ class GsmPalette(Palette):
__gsignals__ = {
'gsm-connect': (GObject.SignalFlags.RUN_FIRST, None, ([])),
'gsm-disconnect': (GObject.SignalFlags.RUN_FIRST, None, ([])),
+ 'gsm-private': (GObject.SignalFlags.RUN_FIRST, None, ([])),
+ 'gsm-neighborhood': (GObject.SignalFlags.RUN_FIRST, None, ([])),
}
def __init__(self):
@@ -208,6 +221,7 @@ class GsmPalette(Palette):
self._current_state = None
self._failed_connection = False
+ self._sharing_state = _GSM_SHARING_PRIVATE
self.info_box = Gtk.VBox()
@@ -216,6 +230,11 @@ class GsmPalette(Palette):
self.info_box.pack_start(self._toggle_state_item, True, True, 0)
self._toggle_state_item.show()
+ self._sharing_box = Gtk.VBox()
+ self.info_box.pack_start(self._sharing_box, True, True, 0)
+ self.__update_sharing_toggle_widget(_('Private (Click to share)'), 'zoom-home')
+ self._sharing_box.hide()
+
self.error_title_label = Gtk.Label(label="")
self.error_title_label.set_alignment(0, 0.5)
self.error_title_label.set_line_wrap(True)
@@ -298,6 +317,9 @@ class GsmPalette(Palette):
icon = Icon(icon_name='media-eject', \
icon_size=Gtk.IconSize.MENU)
self._toggle_state_item.set_image(icon)
+ self.sharing_update_text()
+ self._sharing_toggle_item.show()
+ return
elif self._current_state == _GSM_STATE_FAILED:
message_error = self._get_error_by_nm_reason(reason)
@@ -306,6 +328,8 @@ class GsmPalette(Palette):
raise ValueError('Invalid GSM state while updating label and ' \
'text, %s' % str(self._current_state))
+ self._sharing_toggle_item.hide()
+
def __toggle_state_cb(self, menuitem):
if self._current_state == _GSM_STATE_NOT_READY:
pass
@@ -366,6 +390,50 @@ class GsmPalette(Palette):
message_tuple = (network.get_error_by_reason(reason), message)
return message_tuple
+ def sharing_update_text(self):
+ if self._sharing_state == _GSM_SHARING_PRIVATE:
+ self.__update_sharing_toggle_widget(_('Private (Click to share)'), 'zoom-home')
+
+ elif self._sharing_state == _GSM_SHARING_TRYING:
+ self.__update_sharing_toggle_widget(_('Please wait...'), 'zoom-home')
+
+ elif self._sharing_state == _GSM_SHARING_NEIGHBORHOOD:
+ self.__update_sharing_toggle_widget(_('Neighborhood (Click to unshare)'), 'zoom-neighborhood')
+
+ else:
+ raise ValueError('Invalid GSM sharing state while updating, %s' % \
+ str(self._sharing_state))
+
+ def __update_sharing_toggle_widget(self, label, icon_name):
+ for child_widget in self._sharing_box.get_children():
+ self._sharing_box.remove(child_widget)
+
+ self._sharing_toggle_item = PaletteMenuItem('')
+ self._sharing_toggle_item.connect('activate', self.__sharing_toggle_cb)
+ self._sharing_toggle_item.set_label(label)
+ icon = Icon(icon_name=icon_name, icon_size=Gtk.IconSize.MENU)
+ self._sharing_toggle_item.set_image(icon)
+ icon.show()
+ self._sharing_box.pack_start(self._sharing_toggle_item, True, True, 0)
+ separator = PaletteMenuItemSeparator()
+ self._sharing_box.pack_start(separator, True, True, 0)
+ separator.show()
+ self._sharing_box.show_all()
+
+ def __sharing_toggle_cb(self, menuitem):
+ if self._sharing_state == _GSM_SHARING_PRIVATE:
+ self.emit('gsm-neighborhood')
+
+ elif self._sharing_state == _GSM_SHARING_TRYING:
+ pass
+
+ elif self._sharing_state == _GSM_SHARING_NEIGHBORHOOD:
+ self.emit('gsm-private')
+
+ else:
+ raise ValueError('Invalid GSM sharing state, %s' % \
+ str(self._sharing_state))
+
class WirelessDeviceView(ToolButton):
@@ -527,17 +595,8 @@ class WirelessDeviceView(ToolButton):
else:
state = network.NM_DEVICE_STATE_UNKNOWN
- if self._mode != network.NM_802_11_MODE_ADHOC and \
- network.is_sugar_adhoc_network(self._ssid) == False:
- if state == network.NM_DEVICE_STATE_ACTIVATED:
- icon_name = '%s-connected' % 'network-wireless'
- else:
- icon_name = 'network-wireless'
-
- icon_name = get_icon_state(icon_name, self._strength)
- if icon_name:
- self._icon.props.icon_name = icon_name
- else:
+ if self._mode == network.NM_802_11_MODE_ADHOC and \
+ network.is_sugar_adhoc_network(self._ssid):
channel = network.frequency_to_channel(self._frequency)
if state == network.NM_DEVICE_STATE_ACTIVATED:
self._icon.props.icon_name = 'network-adhoc-%s-connected' \
@@ -729,8 +788,9 @@ class GsmDeviceView(TrayIcon):
def __init__(self, device):
self._connection_time_handler = None
-
self._connection_timestamp = 0
+ self._shared_connection_path = None
+ self._target_dev_path = None
client = GConf.Client.get_default()
color = xocolor.XoColor(client.get_string('/desktop/sugar/user/color'))
@@ -758,6 +818,8 @@ class GsmDeviceView(TrayIcon):
palette.set_group_id('frame')
palette.connect('gsm-connect', self.__gsm_connect_cb)
palette.connect('gsm-disconnect', self.__gsm_disconnect_cb)
+ palette.connect('gsm-neighborhood', self.__gsm_start_sharing_cb)
+ palette.connect('gsm-private', self.__gsm_stop_sharing_cb)
self._palette = palette
@@ -775,42 +837,32 @@ class GsmDeviceView(TrayIcon):
def __gsm_connect_cb(self, palette, data=None):
connection = network.find_gsm_connection()
if connection is not None:
- connection.activate(self._device.object_path,
- reply_handler=self.__connect_cb,
- error_handler=self.__connect_error_cb)
+ network.activate_connection_by_path(connection.get_path(),
+ self._device.object_path,
+ reply_handler=self._connect_cb,
+ error_handler=self._connect_error_cb)
else:
self._palette.add_alert(_('No GSM connection available.'), \
_('Create a connection in the ' \
'control panel.'))
- def __connect_cb(self, active_connection):
+ def _connect_cb(self, active_connection_path):
+ self._base_gsm_connection_path = active_connection_path
logging.debug('Connected successfully to gsm device, %s',
- active_connection)
+ active_connection_path)
- def __connect_error_cb(self, error):
+ def _connect_error_cb(self, error):
raise RuntimeError('Error when connecting to gsm device, %s' % error)
def __gsm_disconnect_cb(self, palette, data=None):
- obj = self._bus.get_object(network.NM_SERVICE, network.NM_PATH)
- netmgr = dbus.Interface(obj, network.NM_IFACE)
- netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE)
- active_connections_o = netmgr_props.Get(network.NM_IFACE, 'ActiveConnections')
+ network.get_manager().DeactivateConnection(self._base_gsm_connection_path,
+ reply_handler=self._disconnect_cb,
+ error_handler=self._disconnect_error_cb)
- for conn_o in active_connections_o:
- obj = self._bus.get_object(network.NM_IFACE, conn_o)
- props = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
- devices = props.Get(network.NM_ACTIVE_CONN_IFACE, 'Devices')
- if self._device.object_path in devices:
- netmgr.DeactivateConnection(
- conn_o,
- reply_handler=self.__disconnect_cb,
- error_handler=self.__disconnect_error_cb)
- break
-
- def __disconnect_cb(self):
+ def _disconnect_cb(self):
logging.debug('Disconnected successfully gsm device')
- def __disconnect_error_cb(self, error):
+ def _disconnect_error_cb(self, error):
raise RuntimeError('Error when disconnecting gsm device, %s' % error)
def __state_changed_cb(self, new_state, old_state, reason):
@@ -831,6 +883,10 @@ class GsmDeviceView(TrayIcon):
gsm_state = _GSM_STATE_CONNECTED
connection = network.find_gsm_connection()
if connection is not None:
+ # Introspect the settings's keys once; else sometimes
+ # the key 'timestamp' gets missed.
+ connection.get_settings('connection').keys()
+
self._connection_timestamp = time.time() - \
connection.get_settings('connection')['timestamp']
self._connection_time_handler = GObject.timeout_add_seconds( \
@@ -879,6 +935,95 @@ class GsmDeviceView(TrayIcon):
self._palette.update_connection_time(connection_time)
return True
+ def __gsm_start_sharing_cb(self, palette):
+ if self._palette._sharing_state == _GSM_SHARING_PRIVATE:
+ logging.debug('GSM will start sharing now')
+ self._palette._sharing_state = _GSM_SHARING_TRYING
+ self._palette.sharing_update_text()
+
+ self._target_device = _wifi_device
+ self._target_device_path = self._target_device.object_path
+
+ client = GConf.Client.get_default()
+ nick = client.get_string('/desktop/sugar/user/nick')
+ nick = re.sub('\W', '', nick)
+
+ name_format = '%s network'
+ format_length = len(name_format) - len('%s')
+ nick_length = 31 - format_length
+ name = name_format % nick[:nick_length]
+
+ connection = network.find_connection_by_ssid(name)
+ if connection == None:
+ settings = network.Settings()
+ settings.connection.id = name
+ settings.connection.uuid = str(uuid.uuid4())
+ settings.connection.type = '802-11-wireless'
+ settings.wireless.ssid = dbus.ByteArray(name)
+ settings.wireless.mode = 'adhoc'
+ settings.wireless.band = 'bg'
+ chosen_channel = random.randrange(len(_GSM_SHARING_CHANNELS))
+ settings.wireless.channel = _GSM_SHARING_CHANNELS[chosen_channel]
+ settings.ip4_config = network.IP4Config()
+ settings.ip4_config.method = 'shared'
+ network.add_and_activate_connection(self._target_device,
+ settings,
+ '/',
+ self._gsm_sharing_ok_cb_for_add_and_activate,
+ self._gsm_sharing_error_cb)
+ else:
+ network.activate_connection_by_path(connection.get_path(),
+ self._target_device,
+ self._gsm_sharing_ok_cb,
+ self._gsm_sharing_error_cb)
+
+ def _gsm_sharing_ok_cb_for_add_and_activate(self,
+ new_connection_path,
+ active_connection_path):
+ self._gsm_sharing_ok_cb(active_connection_path)
+
+ def _gsm_sharing_ok_cb(self, connection_path):
+ logging.debug('GSM sharing is enabled')
+ self._shared_connection_path = connection_path
+ self._bus.add_signal_receiver(self._gsm_sharing_changed_cb,
+ signal_name='StateChanged',
+ path=self._target_device_path,
+ dbus_interface=network.NM_DEVICE_IFACE)
+ self._palette._sharing_state = _GSM_SHARING_NEIGHBORHOOD
+ self._palette.sharing_update_text()
+
+ def _gsm_sharing_changed_cb(self, new_state, old_state, reason):
+ if new_state == network.NM_DEVICE_STATE_DISCONNECTED:
+ self._gsm_sharing_reset()
+
+ def _gsm_sharing_reset(self):
+ logging.debug('GSM sharing is disabled')
+ if self._target_dev_path != None:
+ self._bus.remove_signal_receiver(self._gsm_sharing_changed_cb,
+ signal_name='StateChanged',
+ path=self._target_dev_path,
+ dbus_interface=network.NM_DEVICE_IFACE)
+ self._shared_connection_path = None
+ self._target_dev_path = None
+ self._palette._sharing_state = _GSM_SHARING_PRIVATE
+ self._palette.sharing_update_text()
+
+ def _gsm_sharing_error_cb(self, error):
+ logging.debug('GSM sharing could not start: %s' % str(error))
+ self._gsm_sharing_reset()
+
+ def __gsm_stop_sharing_cb(self, palette):
+ logging.debug('GSM will stop sharing now')
+ network.get_manager().DeactivateConnection(self._shared_connection_path,
+ reply_handler=self._gsm_stop_sharing_ok_cb,
+ error_handler=self._gsm_stop_sharing_error_cb)
+
+ def _gsm_stop_sharing_ok_cb(self):
+ self._gsm_sharing_reset()
+
+ def _gsm_stop_sharing_error_cb(self):
+ logging.debug('GSM sharing could not stop')
+
class WirelessDeviceObserver(object):
def __init__(self, device, tray):
@@ -1056,6 +1201,8 @@ class NetworkManagerObserver(object):
device = WiredDeviceObserver(nm_device, self._tray)
self._devices[device_op] = device
elif device_type == network.NM_DEVICE_TYPE_WIFI:
+ global _wifi_device
+ _wifi_device = nm_device
device = WirelessDeviceObserver(nm_device, self._tray)
self._devices[device_op] = device
elif device_type == network.NM_DEVICE_TYPE_OLPC_MESH:
@@ -1075,5 +1222,10 @@ class NetworkManagerObserver(object):
del self._devices[device_op]
+def get_wifi_device():
+ global _wifi_device
+ return _wifi_device
+
+
def setup(tray):
device_observer = NetworkManagerObserver(tray)
diff --git a/extensions/deviceicon/resources.py b/extensions/deviceicon/resources.py
new file mode 100644
index 0000000..ef4adac
--- /dev/null
+++ b/extensions/deviceicon/resources.py
@@ -0,0 +1,217 @@
+# Copyright (C) Anish Mangal <anishmangal2002@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import logging
+import os
+
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import GConf
+
+from sugar3.graphics.tray import TrayIcon
+from sugar3.graphics.xocolor import XoColor
+from sugar3.graphics.palette import Palette
+from sugar3.graphics import style
+
+
+_SYSTEM_MOODS = ['-sad', '-normal', '-happy']
+_ICON_NAME = 'computer'
+_UPDATE_INTERVAL = 5 * 1000
+
+
+class DeviceView(TrayIcon):
+
+ FRAME_POSITION_RELATIVE = 500
+
+ def __init__(self):
+ client = GConf.Client.get_default()
+ self._color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ TrayIcon.__init__(self, icon_name=_ICON_NAME, xo_color=self._color)
+ self.create_palette()
+ self._icon_widget.connect('button-release-event', self._click_cb)
+
+ def create_palette(self):
+ self.palette = ResourcePalette(_('System resources'))
+ self.palette.set_group_id('frame')
+ self.palette.add_timer()
+ self.palette.connect('system-mood-changed',
+ self._system_mood_changed_cb)
+ return self.palette
+
+ def _system_mood_changed_cb(self, palette_, mood):
+ self.icon.props.icon_name = _ICON_NAME + mood
+
+ def _click_cb(self, widget, event):
+ self.palette_invoker.notify_right_click()
+
+
+class ResourcePalette(Palette):
+ __gsignals__ = {
+ 'system-mood-changed': (GObject.SignalFlags.RUN_FIRST, None, ([str])),
+ }
+
+ def __init__(self, primary_text):
+ Palette.__init__(self, label=primary_text)
+
+ self.vbox = Gtk.VBox()
+ self.set_content(self.vbox)
+
+ self._cpu_text = Gtk.Label()
+ self.vbox.pack_start(self._cpu_text, True, True, 0)
+ self._cpu_bar = Gtk.ProgressBar()
+ self._cpu_bar.set_size_request(
+ style.zoom(style.GRID_CELL_SIZE * 4), -1)
+ self.vbox.pack_start(self._cpu_bar, True, True, 0)
+
+ self._memory_text = Gtk.Label()
+ self.vbox.pack_start(self._memory_text, True, True, 0)
+ self._memory_bar = Gtk.ProgressBar()
+ self._memory_bar.set_size_request(
+ style.zoom(style.GRID_CELL_SIZE * 4), -1)
+ self.vbox.pack_start(self._memory_bar, True, True, 0)
+
+ self._system_mood = None
+ try:
+ self._cpu_times = self._get_cpu_times_list()
+ except IOError:
+ logging.exception('An error ocurred while attempting to '
+ 'read /proc/stat')
+ self._stop_computing_statistics()
+
+ self.vbox.show()
+ self._cpu_text.show()
+ self._cpu_bar.show()
+ self._memory_text.show()
+ self._memory_bar.show()
+
+ def add_timer(self):
+ GObject.timeout_add(_UPDATE_INTERVAL, self.__timer_cb)
+
+ def _get_cpu_times_list(self):
+ """Return various cpu times as read from /proc/stat
+
+ This method returns the following cpu times measured
+ in jiffies (1/100 of a second for x86 systems)
+ as an ordered list of numbers - [user, nice,
+ system, idle, iowait] where,
+
+ user: normal processes executing in user mode
+ nice: niced processes executing in user mode
+ system: processes executing in kernel mode
+ idle: twiddling thumbs
+ iowait: waiting for I/O to complete
+
+ Note: For systems having 2 or more CPU's, the above
+ numbers would be the cumulative sum of these times
+ for all CPU's present in the system.
+
+ """
+ return [int(count)
+ for count in file('/proc/stat').readline().split()[1:6]]
+
+ def _percentage_cpu_available(self):
+ """
+ Return free CPU resources as a percentage
+
+ """
+ _cpu_times_new = self._get_cpu_times_list()
+ _cpu_times_current = [(new - old)
+ for new, old in zip(_cpu_times_new, self._cpu_times)]
+ user_, nice_, system_, idle, iowait = _cpu_times_current
+ cpu_free = (idle + iowait) * 100.0 / sum(_cpu_times_current)
+ self._cpu_times = self._get_cpu_times_list()
+ return cpu_free
+
+ def _percentage_memory_available(self):
+ """
+ Return free memory as a percentage
+ """
+
+ for line in file('/proc/meminfo'):
+ name, value, unit_ = line.split()[:3]
+ if 'MemTotal:' == name:
+ total = int(value)
+ elif 'MemFree:' == name:
+ free = int(value)
+ elif 'Buffers:' == name:
+ buffers = int(value)
+ elif 'Cached:' == name:
+ cached = int(value)
+ elif 'Active:' == name:
+ break
+ return (free + buffers + cached) * 100.0 / total
+
+ def __timer_cb(self):
+ try:
+ cpu_in_use = 100 - self._percentage_cpu_available()
+ memory_in_use = 100 - self._percentage_memory_available()
+ except IOError:
+ logging.exception('An error ocurred while trying to '
+ 'retrieve resource usage statistics')
+ self._stop_and_show_error()
+ return False
+ else:
+ self._cpu_text.set_label(_('CPU in use: %d%%' % cpu_in_use))
+ self._cpu_bar.set_fraction(float(cpu_in_use) / 100)
+ self._memory_text.set_label(_('Memory in use: %d%%' %
+ memory_in_use))
+ self._memory_bar.set_fraction(float(memory_in_use) / 100)
+
+ # both cpu_free and memory_free lie between 0-100
+ system_mood = _SYSTEM_MOODS[
+ int(300 - (cpu_in_use + 2 * memory_in_use)) // 100]
+
+ # check if self._system_mood exists
+ try:
+ if self._system_mood != system_mood:
+ self.emit('system-mood-changed', system_mood)
+ self._system_mood = system_mood
+ except AttributeError:
+ self.emit('system-mood-changed', system_mood)
+ self._system_mood = system_mood
+
+ return True
+
+ def _stop_and_show_error(self):
+ """
+ Stop computing usage statistics and display an error message
+ since we've hit an exception.
+
+ """
+ # Use the existing _cpu_text label to display the error. Remove
+ # everything else.
+ self._cpu_text.set_size_request(
+ style.zoom(style.GRID_CELL_SIZE * 4), -1)
+ self._cpu_text.set_line_wrap(True)
+ self._cpu_text.set_text(_('Cannot compute CPU and memory usage '
+ 'statistics!'))
+ self.vbox.remove(self._cpu_bar)
+ self.vbox.remove(self._memory_text)
+ self.vbox.remove(self._memory_bar)
+ self.emit('system-mood-changed', '-error')
+
+
+def setup(tray):
+ client = GConf.Client.get_default()
+ if not client.get_bool('/desktop/sugar/frame/show_network_resources'):
+ return
+
+ if not (os.path.exists('/proc/stat') and os.path.exists('/proc/meminfo')):
+ logging.warning('Either /proc/stat or /proc/meminfo not present. Not '
+ 'adding the CPU and memory usage icon to the frame')
+ return
+ tray.add_device(DeviceView())
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 17c10c3..2408820 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -28,6 +28,7 @@ extensions/cpsection/updater/view.py
extensions/deviceicon/battery.py
extensions/deviceicon/frame.py
extensions/deviceicon/network.py
+extensions/deviceicon/resources.py
extensions/deviceicon/speech.py
extensions/deviceicon/speaker.py
extensions/deviceicon/touchpad.py
diff --git a/src/Makefile.am b/src/Makefile.am
index 83571a4..501c1a0 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1 +1 @@
-SUBDIRS = jarabe
+SUBDIRS = jarabe webdav
diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py
index f8afca3..daa58e4 100644
--- a/src/jarabe/controlpanel/gui.py
+++ b/src/jarabe/controlpanel/gui.py
@@ -208,6 +208,10 @@ class ControlPanel(Gtk.Window):
self.__accept_clicked_cb)
def show_section_view(self, option):
+ self.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
+ GObject.idle_add(self._finally_show_section_view, option)
+
+ def _finally_show_section_view(self, option):
self._set_toolbar(self._section_toolbar)
icon = self._section_toolbar.get_icon()
@@ -291,6 +295,7 @@ class ControlPanel(Gtk.Window):
self._show_main_view()
def __accept_clicked_cb(self, widget):
+ self._section_view.perform_accept_actions()
if self._section_view.needs_restart:
self._section_toolbar.accept_button.set_sensitive(False)
self._section_toolbar.cancel_button.set_sensitive(False)
diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py
index cbf4768..074a8ae 100644
--- a/src/jarabe/controlpanel/sectionview.py
+++ b/src/jarabe/controlpanel/sectionview.py
@@ -52,3 +52,7 @@ class SectionView(Gtk.VBox):
def undo(self):
"""Undo here the changes that have been made in this section."""
pass
+
+ def perform_accept_actions(self):
+ """Perform additional actions, when the "Ok" button is clicked."""
+ pass
diff --git a/src/jarabe/desktop/activitieslist.py b/src/jarabe/desktop/activitieslist.py
index 4d2eb1a..4adaf18 100644
--- a/src/jarabe/desktop/activitieslist.py
+++ b/src/jarabe/desktop/activitieslist.py
@@ -215,6 +215,15 @@ class ListModel(Gtk.TreeModelSort):
self._model.remove(row.iter)
return
+ def _is_activity_bundle_in_model_already(self, activity_info):
+ bundle_id = activity_info.get_bundle_id()
+ version = activity_info.get_activity_version()
+ for row in self._model:
+ if row[ListModel.COLUMN_BUNDLE_ID] == bundle_id and \
+ row[ListModel.COLUMN_VERSION] == version:
+ return True
+ return False
+
def _add_activity(self, activity_info):
if activity_info.get_bundle_id() == 'org.laptop.JournalActivity':
return
@@ -223,6 +232,12 @@ class ListModel(Gtk.TreeModelSort):
version = activity_info.get_activity_version()
registry = bundleregistry.get_registry()
+
+ # If the activity bundle is already a part of
+ # activities-list, do not re-add it.
+ if self._is_activity_bundle_in_model_already(activity_info):
+ return
+
favorite = registry.is_bundle_favorite(activity_info.get_bundle_id(),
version)
@@ -235,7 +250,7 @@ class ListModel(Gtk.TreeModelSort):
'<span style="italic" weight="light">%s</span>' % \
(activity_info.get_name(), tags)
- self._model.append([activity_info.get_bundle_id(),
+ self._model.prepend([activity_info.get_bundle_id(),
favorite,
activity_info.get_icon(),
title,
diff --git a/src/jarabe/desktop/keydialog.py b/src/jarabe/desktop/keydialog.py
index a4c8e36..a6cde8d 100644
--- a/src/jarabe/desktop/keydialog.py
+++ b/src/jarabe/desktop/keydialog.py
@@ -20,10 +20,14 @@ from gettext import gettext as _
from gi.repository import Gtk
import dbus
+import os
+import shutil
+from sugar3 import env
from sugar3.graphics.icon import Icon
from jarabe.model import network
+from jarabe.journal.objectchooser import ObjectChooser
IW_AUTH_ALG_OPEN_SYSTEM = 'open'
@@ -33,6 +37,10 @@ WEP_PASSPHRASE = 1
WEP_HEX = 2
WEP_ASCII = 3
+SETTING_TYPE_STRING = 1
+SETTING_TYPE_LIST = 2
+SETTING_TYPE_CHOOSER = 3
+
def string_is_hex(key):
is_hex = True
@@ -74,6 +82,216 @@ class CanceledKeyRequestError(dbus.DBusException):
self._dbus_error_name = network.NM_SETTINGS_IFACE + '.CanceledError'
+class NetworkParameters(Gtk.HBox):
+ def __init__(self, auth_param):
+ Gtk.HBox.__init__(self, homogeneous=True)
+ self._key = auth_param._key_name
+ self._label = Gtk.Label(_(auth_param._key_label))
+ self._key_type = auth_param._key_type
+ self._auth_param = auth_param
+
+ self.pack_start(self._label, True, True, 0)
+ self._label.show()
+
+ if self._is_entry():
+ self._entry = Gtk.Entry()
+ self.pack_start(self._entry, True, True, 0)
+ self._entry.show()
+ elif self._is_liststore():
+ self._option_store = Gtk.ListStore(str, str)
+ for option in auth_param._options:
+ self._option_store.append(option)
+
+ self._entry = auth_param._options[0][1]
+ self._option_combo = Gtk.ComboBox(model=self._option_store)
+ cell = Gtk.CellRendererText()
+ self._option_combo.pack_start(cell, True)
+ self._option_combo.add_attribute(cell, 'text', 0)
+ self._option_combo.set_active(0)
+ self._option_combo.connect('changed',
+ self._option_combo_changed_cb)
+ self.pack_start(self._option_combo, True, True, 0)
+ self.show()
+ self._option_combo.show()
+ elif self._is_chooser():
+ self._chooser_button = Gtk.Button(_('Choose..'))
+ self._chooser_button.connect('clicked',
+ self._object_chooser_cb)
+ self.pack_start(self._chooser_button, True, True, 0)
+ self._chooser_button.show()
+ self._entry = ''
+
+ def _is_entry(self):
+ return ( not self._is_chooser() ) and \
+ ( len(self._auth_param._options) == 0 )
+
+ def _is_liststore(self):
+ return ( not self._is_chooser() ) and \
+ ( len(self._auth_param._options) > 0 )
+
+ def _is_chooser(self):
+ return self._key_type == SETTING_TYPE_CHOOSER
+
+ def _object_chooser_cb(self, chooser_button):
+ self._want_document = True
+ self._show_picker_cb()
+
+ def _show_picker_cb(self):
+ if not self._want_document:
+ return
+ self._chooser = ObjectChooser()
+ self._chooser._set_callback(self.__process_selected_journal_object)
+
+ self._chooser.show()
+
+ def __process_selected_journal_object(self, object_id):
+ jobject = self._chooser.get_selected_object()
+ if jobject and jobject.file_path:
+ file_basename = \
+ os.path.basename(jobject._metadata._properties['title'])
+ self._chooser_button.set_label(file_basename)
+
+ profile_path = env.get_profile_path()
+ self._entry = os.path.join(profile_path, file_basename)
+
+ # Remove (older) file, if it exists.
+ if os.path.exists(self._entry):
+ os.remove(self._entry)
+
+ # Copy the file.
+ shutil.copy2(jobject.file_path, self._entry)
+
+ self._chooser.destroy()
+
+ def _option_combo_changed_cb(self, widget):
+ it = self._option_combo.get_active_iter()
+ (value, ) = self._option_store.get(it, 1)
+ self._entry = value
+
+ def _get_key(self):
+ return self._key
+
+ def _get_value(self):
+ if self._is_entry():
+ return self._entry.get_text()
+ elif self._is_liststore():
+ return self._entry
+ elif self._is_chooser():
+ if len(self._entry) > 0:
+ return dbus.ByteArray('file://' + self._entry + '\0')
+ else:
+ return self._entry
+
+
+class KeyValuesDialog(Gtk.Dialog):
+ def __init__(self, auth_lists, final_callback, settings):
+ # This must not be "modal", else the "chooser" widgets won't
+ # accept anything !!
+ Gtk.Dialog.__init__(self)
+ self.set_title(_('Wireless Parameters required'))
+
+ self._spacing_between_children_widgets = 5
+ self._auth_lists = auth_lists
+ self._final_callback = final_callback
+ self._settings = settings
+
+ label = Gtk.Label(_("Please enter parameters\n"))
+ self.vbox.set_spacing(self._spacing_between_children_widgets)
+ self.vbox.pack_start(label, True, True, 0)
+
+ self._auth_type_store = Gtk.ListStore(str, str)
+ for auth_list in self._auth_lists:
+ self._auth_type_store.append([auth_list._auth_label,
+ auth_list._auth_type])
+
+ self._auth_type_combo = Gtk.ComboBox(model=self._auth_type_store)
+ cell = Gtk.CellRendererText()
+ self._auth_type_combo.pack_start(cell, True)
+ self._auth_type_combo.add_attribute(cell, 'text', 0)
+ self._auth_type_combo.set_active(0)
+ self._auth_type_combo.connect('changed',
+ self._auth_type_combo_changed_cb)
+ self._auth_type_box = Gtk.HBox(homogeneous=True)
+ self._auth_label = Gtk.Label(_('Authentication'))
+ self._auth_type_box.pack_start(self._auth_label, True, True, 0)
+ self._auth_type_box.pack_start(self._auth_type_combo,
+ True, True, 0)
+ self.vbox.pack_start(self._auth_type_box, True, True, 0)
+ self._auth_label.show()
+ self._auth_type_combo.show()
+
+ button = Gtk.Button()
+ button.set_image(Icon(icon_name='dialog-cancel'))
+ button.set_label(_('Cancel'))
+ self.add_action_widget(button, Gtk.ResponseType.CANCEL)
+ button = Gtk.Button()
+ button.set_image(Icon(icon_name='dialog-ok'))
+ button.set_label(_('Ok'))
+ self.add_action_widget(button, Gtk.ResponseType.OK)
+ self.set_default_response(Gtk.ResponseType.OK)
+
+ self.connect('response', self._fetch_values)
+
+ auth_type = self._auth_lists[0]._auth_type
+ self._selected_auth_list = self._select_auth_list(auth_type)
+ self._add_key_value('eap', auth_type)
+ self._add_container_box()
+
+ def _auth_type_combo_changed_cb(self, widget):
+ it = self._auth_type_combo.get_active_iter()
+ (auth_type, ) = self._auth_type_store.get(it, 1)
+ self._selected_auth_list = self._select_auth_list(auth_type)
+ self._add_key_value('eap', auth_type)
+ self._reset()
+
+ def _select_auth_list(self, auth_type):
+ for auth_list in self._auth_lists:
+ if auth_list._params_list[0]._options[0][1] == auth_type:
+ return auth_list
+
+ def _populate_auth_params(self, auth_list):
+ for auth_param in auth_list._params_list[1:]:
+ obj = NetworkParameters(auth_param)
+ self._key_values_box.pack_start(obj, True, True, 0)
+ obj.show()
+
+ def _reset(self):
+ self.vbox.remove(self._key_values_box)
+ self._add_container_box()
+
+ def _add_container_box(self):
+ self._key_values_box = \
+ Gtk.VBox(spacing=self._spacing_between_children_widgets)
+ self.vbox.pack_start(self._key_values_box, True, True, 0)
+ self._key_values_box.show()
+ self._populate_auth_params(self._selected_auth_list)
+
+ def _remove_all_params(self):
+ self._key_values_box.remove_all()
+
+ def _fetch_values(self, key_dialog, response_id):
+ if response_id == Gtk.ResponseType.OK:
+ for child in self._key_values_box.get_children():
+ key = child._get_key()
+ value = child._get_value()
+ self._add_key_value(key, value)
+
+ key_dialog.destroy()
+ self._final_callback(self._settings,
+ self._selected_auth_list)
+
+ def _add_key_value(self, key, value):
+ for auth_param in self._selected_auth_list._params_list:
+ if auth_param._key_name == key:
+ if (auth_param._key_type == SETTING_TYPE_STRING) or \
+ (auth_param._key_type == SETTING_TYPE_CHOOSER):
+ auth_param._value = value
+ elif auth_param._key_type == SETTING_TYPE_LIST:
+ values = []
+ values.append(value)
+ auth_param._value = values
+
+
class KeyDialog(Gtk.Dialog):
def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response):
Gtk.Dialog.__init__(self, flags=Gtk.DialogFlags.MODAL)
@@ -219,7 +437,7 @@ class WEPKeyDialog(KeyDialog):
self.set_response_sensitive(Gtk.ResponseType.OK, valid)
-class WPAKeyDialog(KeyDialog):
+class WPAPersonalKeyDialog(KeyDialog):
def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response):
KeyDialog.__init__(self, ssid, flags, wpa_flags, rsn_flags,
dev_caps, response)
@@ -295,14 +513,26 @@ def create(ssid, flags, wpa_flags, rsn_flags, dev_caps, response):
rsn_flags == network.NM_802_11_AP_SEC_NONE:
key_dialog = WEPKeyDialog(ssid, flags, wpa_flags, rsn_flags,
dev_caps, response)
- else:
- key_dialog = WPAKeyDialog(ssid, flags, wpa_flags, rsn_flags,
- dev_caps, response)
+ elif (wpa_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK) or \
+ (rsn_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK):
+ key_dialog = WPAPersonalKeyDialog(ssid, flags, wpa_flags, rsn_flags,
+ dev_caps, settings, response)
+ elif (wpa_flags & network.NM_802_11_AP_SEC_KEY_MGMT_802_1X) or \
+ (rsn_flags & network.NM_802_11_AP_SEC_KEY_MGMT_802_1X):
+ # nothing. All details are asked for WPA/WPA2-Enterprise
+ # networks, before the conneection-activation is done.
+ return
key_dialog.connect('response', _key_dialog_response_cb)
key_dialog.show_all()
+def get_key_values(key_list, final_callback, settings):
+ key_dialog = KeyValuesDialog(key_list, final_callback,
+ settings)
+ key_dialog.show_all()
+
+
def _key_dialog_response_cb(key_dialog, response_id):
response = key_dialog.get_response_object()
secrets = None
diff --git a/src/jarabe/desktop/networkviews.py b/src/jarabe/desktop/networkviews.py
index 64b4be3..eccfd17 100644
--- a/src/jarabe/desktop/networkviews.py
+++ b/src/jarabe/desktop/networkviews.py
@@ -23,6 +23,7 @@ import uuid
import dbus
import glib
+import string
from sugar3.graphics.icon import Icon
from sugar3.graphics.xocolor import XoColor
@@ -48,6 +49,192 @@ _OLPC_MESH_ICON_NAME = 'network-mesh'
_FILTERED_ALPHA = 0.33
+SETTING_TYPE_STRING = 1
+SETTING_TYPE_LIST = 2
+SETTING_TYPE_CHOOSER = 3
+
+
+class AuthenticationType:
+ def __init__(self, auth_label, auth_type, params_list):
+ self._auth_label = auth_label
+ self._auth_type = auth_type
+ self._params_list = params_list
+
+
+class AuthenticationParameter:
+ def __init__(self, key_name, key_label, key_type,
+ options):
+ self._key_name = key_name
+ self._key_label = key_label
+ self._key_type = key_type
+ self._options = options
+ self._value = None
+
+
+
+AUTHENTICATION_LIST = \
+ [
+ AuthenticationType('TLS',
+ 'tls',
+ [
+ AuthenticationParameter(
+ 'eap',
+ 'Authentication',
+ SETTING_TYPE_LIST,
+ [['TLS', 'tls']]
+ ),
+ AuthenticationParameter(
+ 'identity',
+ 'Identity',
+ SETTING_TYPE_STRING,
+ []
+ ),
+ AuthenticationParameter(
+ 'client-cert',
+ 'User certificate',
+ SETTING_TYPE_CHOOSER,
+ []
+ ),
+ AuthenticationParameter(
+ 'ca-cert',
+ 'CA certificate',
+ SETTING_TYPE_CHOOSER,
+ []
+ ),
+ AuthenticationParameter(
+ 'private-key',
+ 'Private key',
+ SETTING_TYPE_CHOOSER,
+ []
+ ),
+ AuthenticationParameter(
+ 'private-key-password',
+ 'Private Key password',
+ SETTING_TYPE_STRING,
+ []
+ )
+ ]
+ ),
+ AuthenticationType('LEAP',
+ 'leap',
+ [
+ AuthenticationParameter(
+ 'eap',
+ 'Authentication',
+ SETTING_TYPE_LIST,
+ [['LEAP', 'leap']]
+ ),
+ AuthenticationParameter(
+ 'identity',
+ 'Username',
+ SETTING_TYPE_STRING,
+ []
+ ),
+ AuthenticationParameter(
+ 'password',
+ 'Password',
+ SETTING_TYPE_STRING,
+ []
+ )
+ ]
+ ),
+ AuthenticationType('Tunnelled TLS',
+ 'ttls',
+ [
+ AuthenticationParameter(
+ 'eap',
+ 'Authentication',
+ SETTING_TYPE_LIST,
+ [['Tunnelled TLS', 'ttls']]
+ ),
+ AuthenticationParameter(
+ 'anonymous-identity',
+ 'Anonymous identity',
+ SETTING_TYPE_STRING,
+ []
+ ),
+ AuthenticationParameter(
+ 'ca-cert',
+ 'CA certificate',
+ SETTING_TYPE_CHOOSER,
+ []
+ ),
+ AuthenticationParameter(
+ 'phase2-auth',
+ 'Inner Authentication',
+ SETTING_TYPE_STRING,
+ [['PAP', 'pap'],
+ ['MSCHAP', 'mschap'],
+ ['MSCHAPv2', 'mschapv2'],
+ ['CHAP', 'chap']]
+ ),
+ AuthenticationParameter(
+ 'identity',
+ 'Username',
+ SETTING_TYPE_STRING,
+ []
+ ),
+ AuthenticationParameter(
+ 'password',
+ 'Password',
+ SETTING_TYPE_STRING,
+ []
+ )
+ ]
+ ),
+ AuthenticationType('Protected EAP (PEAP)',
+ 'peap',
+ [
+ AuthenticationParameter(
+ 'eap',
+ 'Authentication',
+ SETTING_TYPE_LIST,
+ [['Protected EAP (PEAP)', 'peap']]
+ ),
+ AuthenticationParameter(
+ 'anonymous-identity',
+ 'Anonymous identity',
+ SETTING_TYPE_STRING,
+ []
+ ),
+ AuthenticationParameter(
+ 'ca-cert',
+ 'CA certificate',
+ SETTING_TYPE_CHOOSER,
+ []
+ ),
+ AuthenticationParameter(
+ 'phase1-peapver',
+ 'PEAP version',
+ SETTING_TYPE_STRING,
+ [['Automatic', ''],
+ ['Version 0', '0'],
+ ['Version 1', '1']]
+ ),
+ AuthenticationParameter(
+ 'phase2-auth',
+ 'Inner Authentication',
+ SETTING_TYPE_STRING,
+ [['MSCHAPv2', 'mschapv2'],
+ ['MD5', 'md5'],
+ ['GTC', 'gtc']]
+ ),
+ AuthenticationParameter(
+ 'identity',
+ 'Username',
+ SETTING_TYPE_STRING,
+ []
+ ),
+ AuthenticationParameter(
+ 'password',
+ 'Password',
+ SETTING_TYPE_STRING,
+ []
+ )
+ ]
+ )
+ ]
+
class WirelessNetworkView(EventPulsingIcon):
def __init__(self, initial_ap):
@@ -312,7 +499,7 @@ class WirelessNetworkView(EventPulsingIcon):
group = self._add_ciphers_from_flags(self._rsn_flags, False)
wireless_security = WirelessSecurity()
wireless_security.key_mgmt = 'wpa-psk'
- wireless_security.proto = 'rsn'
+ wireless_security.proto = ['rsn']
wireless_security.pairwise = pairwise
wireless_security.group = group
return wireless_security
@@ -324,7 +511,31 @@ class WirelessNetworkView(EventPulsingIcon):
group = self._add_ciphers_from_flags(self._wpa_flags, False)
wireless_security = WirelessSecurity()
wireless_security.key_mgmt = 'wpa-psk'
- wireless_security.proto = 'wpa'
+ wireless_security.proto = ['wpa']
+ wireless_security.pairwise = pairwise
+ wireless_security.group = group
+ return wireless_security
+
+ if (self._rsn_flags & network.NM_802_11_AP_SEC_KEY_MGMT_802_1X) and \
+ (self._device_caps & network.NM_WIFI_DEVICE_CAP_RSN):
+ # WPA2 Enterprise
+ pairwise = self._add_ciphers_from_flags(self._rsn_flags, True)
+ group = self._add_ciphers_from_flags(self._rsn_flags, False)
+ wireless_security = WirelessSecurity()
+ wireless_security.key_mgmt = 'wpa-eap'
+ wireless_security.proto = ['rsn']
+ wireless_security.pairwise = pairwise
+ wireless_security.group = group
+ return wireless_security
+
+ if (self._wpa_flags & network.NM_802_11_AP_SEC_KEY_MGMT_802_1X) and \
+ (self._device_caps & network.NM_WIFI_DEVICE_CAP_WPA):
+ # WPA Enterprise
+ pairwise = self._add_ciphers_from_flags(self._wpa_flags, True)
+ group = self._add_ciphers_from_flags(self._wpa_flags, False)
+ wireless_security = WirelessSecurity()
+ wireless_security.key_mgmt = 'wpa-eap'
+ wireless_security.proto = ['wpa']
wireless_security.pairwise = pairwise
wireless_security.group = group
return wireless_security
@@ -359,8 +570,9 @@ class WirelessNetworkView(EventPulsingIcon):
elif self._mode == network.NM_802_11_MODE_ADHOC:
settings.wireless.mode = 'adhoc'
settings.wireless.band = 'bg'
- settings.ip4_config = IP4Config()
- settings.ip4_config.method = 'link-local'
+ if network.is_sugar_adhoc_network(self._name):
+ settings.ip4_config = IP4Config()
+ settings.ip4_config.method = 'link-local'
wireless_security = self._get_security()
settings.wireless_security = wireless_security
@@ -368,6 +580,34 @@ class WirelessNetworkView(EventPulsingIcon):
if wireless_security is not None:
settings.wireless.security = '802-11-wireless-security'
+ # Take in the settings, if applicable.
+ if (wireless_security is not None) and \
+ ( (wireless_security.key_mgmt == 'ieee8021x') or \
+ (wireless_security.key_mgmt == 'wpa-eap') ):
+ keydialog.get_key_values(AUTHENTICATION_LIST,
+ self.__add_and_activate_connection,
+ settings)
+ else:
+ self.__add_and_activate_connection(settings)
+
+ def __add_and_activate_connection(self, settings, additional_settings=None):
+ if additional_settings is not None:
+ key_value_dict = {}
+ auth_params_list = additional_settings._params_list
+
+ for auth_param in auth_params_list:
+ key = auth_param._key_name
+ value = auth_param._value
+ logging.debug('key == %s', key)
+ logging.debug('value == %s', value)
+ if len(value) > 0:
+ key_value_dict[key] = value
+ else:
+ logging.debug('Not setting empty value for key :'
+ ' %s', key)
+
+ settings.wpa_eap_setting = key_value_dict
+
network.add_and_activate_connection(self._device, settings,
self.get_first_ap().model)
diff --git a/src/jarabe/frame/__init__.py b/src/jarabe/frame/__init__.py
index b3e4b80..8732b96 100644
--- a/src/jarabe/frame/__init__.py
+++ b/src/jarabe/frame/__init__.py
@@ -14,13 +14,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from jarabe.frame.frame import Frame
-
_view = None
def get_view():
+ from jarabe.frame.frame import Frame
+
global _view
if not _view:
_view = Frame()
diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py
index 38fde7b..ef8435f 100644
--- a/src/jarabe/frame/activitiestray.py
+++ b/src/jarabe/frame/activitiestray.py
@@ -246,6 +246,24 @@ class ActivitiesTray(HTray):
button.connect('clicked', self.__activity_clicked_cb, home_activity)
button.show()
+ # JournalActivity is always the first activity to be added.
+ # Broadcast the signal-of-its-creation.
+ if group is None:
+ self._signal_addition_of_journal_activity()
+
+ def _signal_addition_of_journal_activity(self):
+ monitor_file = os.path.expanduser('~/.sugar/journal_created')
+
+ # Remove the file, if it exists.
+ # This is important, since we are checking for the
+ # FILE_CREATED event in the monitor.
+ if os.path.exists(monitor_file):
+ os.remove(monitor_file)
+
+ # Now, create the file.
+ f = open(monitor_file, 'w')
+ f.close()
+
def __activity_removed_cb(self, home_model, home_activity):
logging.debug('__activity_removed_cb: %r', home_activity)
button = self._buttons[home_activity]
diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py
index 410e08b..659df19 100644
--- a/src/jarabe/frame/frame.py
+++ b/src/jarabe/frame/frame.py
@@ -15,6 +15,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import logging
+import os
from gi.repository import Gtk
from gi.repository import Gdk
@@ -33,6 +34,7 @@ from jarabe.frame.devicestray import DevicesTray
from jarabe.frame.framewindow import FrameWindow
from jarabe.frame.clipboardpanelwindow import ClipboardPanelWindow
from jarabe.frame.notification import NotificationIcon, NotificationWindow
+from jarabe.frame.notification import NotificationButton, HistoryPalette
from jarabe.model import notifications
@@ -43,6 +45,8 @@ BOTTOM_LEFT = 3
_NOTIFICATION_DURATION = 5000
+_DEFAULT_ICON = 'emblem-notification'
+
class _Animation(animator.Animation):
def __init__(self, frame, end):
@@ -83,6 +87,10 @@ class Frame(object):
self._event_area.connect('enter', self._enter_corner_cb)
self._event_area.show()
+ self._activities_tray = None
+ self._devices_tray = None
+ self._friends_tray = None
+
self._top_panel = self._create_top_panel()
self._bottom_panel = self._create_bottom_panel()
self._left_panel = self._create_left_panel()
@@ -94,6 +102,7 @@ class Frame(object):
self._key_listener = _KeyListener(self)
self._notif_by_icon = {}
+ self._notif_by_message = {}
notification_service = notifications.get_service()
notification_service.notification_received.connect(
@@ -143,6 +152,8 @@ class Frame(object):
panel.append(activities_tray)
activities_tray.show()
+ self._activities_tray = activities_tray
+
return panel
def _create_bottom_panel(self):
@@ -152,6 +163,8 @@ class Frame(object):
panel.append(devices_tray)
devices_tray.show()
+ self._devices_tray = devices_tray
+
return panel
def _create_right_panel(self):
@@ -161,6 +174,8 @@ class Frame(object):
panel.append(tray)
tray.show()
+ self._friends_tray = tray
+
return panel
def _create_left_panel(self):
@@ -211,15 +226,7 @@ class Frame(object):
else:
self.show()
- def notify_key_press(self):
- self._key_listener.key_press()
-
- def add_notification(self, icon, corner=Gtk.CornerType.TOP_LEFT,
- duration=_NOTIFICATION_DURATION):
-
- if not isinstance(icon, NotificationIcon):
- raise TypeError('icon must be a NotificationIcon.')
-
+ def _create_notification_window(self, corner):
window = NotificationWindow()
screen = Gdk.Screen.get_default()
@@ -235,6 +242,47 @@ class Frame(object):
else:
raise ValueError('Inalid corner: %r' % corner)
+ return window
+
+ def _add_message_button(self, button, corner):
+ if corner == Gtk.CornerType.BOTTOM_RIGHT:
+ self._devices_tray.add_item(button)
+ elif corner == Gtk.CornerType.TOP_RIGHT:
+ self._friends_tray.add_item(button)
+ else:
+ self._activities_tray.add_item(button)
+
+ def _remove_message_button(self, button, corner):
+ if corner == Gtk.CornerType.BOTTOM_RIGHT:
+ self._devices_tray.remove_item(button)
+ elif corner == Gtk.CornerType.TOP_RIGHT:
+ self._friends_tray.remove_item(button)
+ else:
+ self._activities_tray.remove_item(button)
+
+ def _launch_notification_icon(self, icon_name, xo_color,
+ position, duration):
+ icon = NotificationIcon()
+ icon.props.xo_color = xo_color
+
+ if icon_name.startswith(os.sep):
+ icon.props.icon_filename = icon_name
+ else:
+ icon.props.icon_name = icon_name
+
+ self.add_notification(icon, position, duration)
+
+ def notify_key_press(self):
+ self._key_listener.key_press()
+
+ def add_notification(self, icon, corner=Gtk.CornerType.TOP_LEFT,
+ duration=_NOTIFICATION_DURATION):
+
+ if not isinstance(icon, NotificationIcon):
+ raise TypeError('icon must be a NotificationIcon.')
+
+ window = self._create_notification_window(corner)
+
window.add(icon)
icon.show()
window.show()
@@ -253,28 +301,77 @@ class Frame(object):
window.destroy()
del self._notif_by_icon[icon]
+ def add_message(self, body, summary='', icon_name=_DEFAULT_ICON,
+ xo_color=None, corner=Gtk.CornerType.TOP_LEFT,
+ duration=_NOTIFICATION_DURATION):
+
+ if xo_color is None:
+ xo_color = profile.get_color()
+
+ button = self._notif_by_message.get(corner, None)
+ if button is None:
+ button = NotificationButton(_DEFAULT_ICON, xo_color)
+ button.show()
+ self._add_message_button(button, corner)
+ self._notif_by_message[corner] = button
+
+ palette = button.get_palette()
+ if palette is None:
+ palette = HistoryPalette()
+ palette.set_group_id('frame')
+ palette.connect('clear-messages', self.remove_message, corner)
+ palette.connect('notice-messages', button.stop_pulsing)
+ button.set_palette(palette)
+
+ button.start_pulsing()
+
+ palette.push_message(body, summary, icon_name, xo_color)
+ if not self.visible:
+ self._launch_notification_icon(_DEFAULT_ICON, xo_color, corner, duration)
+
+ def remove_message(self, palette, corner):
+ if corner not in self._notif_by_message:
+ logging.debug('Button %s is not active', str(corner))
+ return
+
+ button = self._notif_by_message[corner]
+ self._remove_message_button(button, corner)
+ del self._notif_by_message[corner]
+
def __notification_received_cb(self, **kwargs):
- logging.debug('__notification_received_cb')
- icon = NotificationIcon()
+ logging.debug('__notification_received_cb %r', kwargs)
hints = kwargs['hints']
- icon_file_name = hints.get('x-sugar-icon-file-name', '')
- if icon_file_name:
- icon.props.icon_filename = icon_file_name
- else:
- icon.props.icon_name = 'application-octet-stream'
+ icon_name = hints.get('x-sugar-icon-file-name', '')
+ if not icon_name:
+ icon_name = _DEFAULT_ICON
icon_colors = hints.get('x-sugar-icon-colors', '')
if not icon_colors:
icon_colors = profile.get_color()
- icon.props.xo_color = icon_colors
duration = kwargs.get('expire_timeout', -1)
if duration == -1:
duration = _NOTIFICATION_DURATION
- self.add_notification(icon, Gtk.CornerType.TOP_RIGHT, duration)
+ category = hints.get('category', '')
+ if category == 'device':
+ position = Gtk.CornerType.BOTTOM_RIGHT
+ elif category == 'presence':
+ position = Gtk.CornerType.TOP_RIGHT
+ else:
+ position = Gtk.CornerType.TOP_LEFT
+
+ summary = kwargs.get('summary', '')
+ body = kwargs.get('body', '')
+
+ if summary or body:
+ self.add_message(body, summary, icon_name,
+ icon_colors, position, duration)
+ else:
+ self._launch_notification_icon(icon_name, icon_colors,
+ position, duration)
def __notification_cancelled_cb(self, **kwargs):
# Do nothing for now. Our notification UI is so simple, there's no
diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py
index 184a779..3adaed1 100644
--- a/src/jarabe/frame/notification.py
+++ b/src/jarabe/frame/notification.py
@@ -18,11 +18,197 @@ from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
+import re
+import os
+
+from gettext import gettext as _
+
from sugar3.graphics import style
from sugar3.graphics.xocolor import XoColor
+from sugar3.graphics.palette import Palette
+from sugar3.graphics.palettemenuitem import PaletteMenuItem
+from sugar3.graphics.toolbutton import ToolButton
+from sugar3 import profile
+
+from jarabe.frame.frameinvoker import FrameWidgetInvoker
from jarabe.view.pulsingicon import PulsingIcon
+_PULSE_TIMEOUT = 3
+_PULSE_COLOR = XoColor('%s,%s' % \
+ (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg()))
+_BODY_FILTERS = "<img.*?/>"
+
+
+def _create_pulsing_icon(icon_name, xo_color, timeout=None):
+ icon = PulsingIcon(
+ pixel_size=style.STANDARD_ICON_SIZE,
+ pulse_color=_PULSE_COLOR,
+ base_color=xo_color
+ )
+
+ if timeout is not None:
+ icon.timeout = timeout
+
+ if icon_name.startswith(os.sep):
+ icon.props.file = icon_name
+ else:
+ icon.props.icon_name = icon_name
+
+ return icon
+
+
+class _HistoryIconWidget(Gtk.Alignment):
+ __gtype_name__ = 'SugarHistoryIconWidget'
+
+ def __init__(self, icon_name, xo_color):
+ icon = _create_pulsing_icon(icon_name, xo_color, _PULSE_TIMEOUT)
+ icon.props.pulsing = True
+
+ Gtk.Alignment.__init__(self, xalign=0.5, yalign=0.0)
+ self.props.top_padding = style.DEFAULT_PADDING
+ self.set_size_request(
+ style.GRID_CELL_SIZE - style.FOCUS_LINE_WIDTH * 2,
+ style.GRID_CELL_SIZE - style.DEFAULT_PADDING)
+ self.add(icon)
+
+
+class _HistorySummaryWidget(Gtk.Alignment):
+ __gtype_name__ = 'SugarHistorySummaryWidget'
+
+ def __init__(self, summary):
+ summary_label = Gtk.Label()
+ summary_label.props.wrap = True
+ summary_label.set_markup(
+ '<b>%s</b>' % GObject.markup_escape_text(summary))
+
+ Gtk.Alignment.__init__(self, xalign=0.0, yalign=1.0)
+ self.props.right_padding = style.DEFAULT_SPACING
+ self.add(summary_label)
+
+
+class _HistoryBodyWidget(Gtk.Alignment):
+ __gtype_name__ = 'SugarHistoryBodyWidget'
+ def __init__(self, body):
+ body_label = Gtk.Label()
+ body_label.props.wrap = True
+ body_label.set_markup(body)
+
+ Gtk.Alignment.__init__(self, xalign=0, yalign=0.0)
+ self.props.right_padding = style.DEFAULT_SPACING
+ self.add(body_label)
+
+
+class _MessagesHistoryBox(Gtk.VBox):
+ __gtype_name__ = 'SugarMessagesHistoryBox'
+
+ def __init__(self):
+ Gtk.VBox.__init__(self)
+ self._setup_links_style()
+
+ def _setup_links_style(self):
+ # XXX: find a better way to change style for upstream
+ link_color = profile.get_color().get_fill_color()
+ visited_link_color = profile.get_color().get_stroke_color()
+
+ links_style='''
+ style "label" {
+ GtkLabel::link-color="%s"
+ GtkLabel::visited-link-color="%s"
+ }
+ widget_class "*GtkLabel" style "label"
+ ''' % (link_color, visited_link_color)
+ Gtk.rc_parse_string(links_style)
+
+ def push_message(self, body, summary, icon_name, xo_color):
+ entry = Gtk.HBox()
+
+ icon_widget = _HistoryIconWidget(icon_name, xo_color)
+ entry.pack_start(icon_widget, False, False, 0)
+
+ message = Gtk.VBox()
+ message.props.border_width = style.DEFAULT_PADDING
+ entry.pack_start(message, True, True, 0)
+
+ if summary:
+ summary_widget = _HistorySummaryWidget(summary)
+ message.pack_start(summary_widget, False, False, 0)
+
+ body = re.sub(_BODY_FILTERS, '', body)
+
+ if body:
+ body_widget = _HistoryBodyWidget(body)
+ message.pack_start(body_widget, True, True, 0)
+
+ entry.show_all()
+ self.pack_start(entry, True, True, 0)
+ self.reorder_child(entry, 0)
+
+ self_width_ = self.props.width_request
+ self_height = self.props.height_request
+ if (self_height > Gdk.Screen.height() / 4 * 3) and \
+ (len(self.get_children()) > 1):
+ self.remove(self.get_children()[-1])
+
+class HistoryPalette(Palette):
+ __gtype_name__ = 'SugarHistoryPalette'
+
+ __gsignals__ = {
+ 'clear-messages': (GObject.SignalFlags.RUN_FIRST, None, ([])),
+ 'notice-messages': (GObject.SignalFlags.RUN_FIRST, None, ([]))
+ }
+
+ def __init__(self):
+ Palette.__init__(self)
+
+ self._update_accept_focus()
+
+ self._messages_box = _MessagesHistoryBox()
+ self._messages_box.show()
+
+ palette_box = self._palette_box
+ primary_box = self._primary_box
+ primary_box.hide()
+ palette_box.add(self._messages_box)
+ palette_box.reorder_child(self._messages_box, 0)
+
+ clear_option = PaletteMenuItem(_('Clear history'), 'dialog-cancel')
+ clear_option.connect('activate', self.__clear_messages_cb)
+ clear_option.show()
+
+ vbox = Gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ vbox.add(clear_option)
+
+ self.connect('popup', self.__notice_messages_cb)
+
+ def __clear_messages_cb(self, clear_option):
+ self.emit('clear-messages')
+
+ def __notice_messages_cb(self, palette):
+ self.emit('notice-messages')
+
+ def push_message(self, body, summary, icon_name, xo_color):
+ self._messages_box.push_message(body, summary, icon_name, xo_color)
+
+
+class NotificationButton(ToolButton):
+
+ def __init__(self, icon_name, xo_color):
+ ToolButton.__init__(self)
+ self._icon = _create_pulsing_icon(icon_name, xo_color)
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+ self.set_palette_invoker(FrameWidgetInvoker(self))
+
+ def start_pulsing(self):
+ self._icon.props.pulsing = True
+
+ def stop_pulsing(self, widget):
+ self._icon.props.pulsing = False
+
class NotificationIcon(Gtk.EventBox):
__gtype_name__ = 'SugarNotificationIcon'
@@ -33,28 +219,29 @@ class NotificationIcon(Gtk.EventBox):
'icon-filename': (str, None, None, None, GObject.PARAM_READWRITE),
}
- _PULSE_TIMEOUT = 3
-
def __init__(self, **kwargs):
self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE)
Gtk.EventBox.__init__(self, **kwargs)
self.props.visible_window = False
+ self.set_app_paintable(True)
- self._icon.props.pulse_color = \
- XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
- style.COLOR_TRANSPARENT.get_svg()))
- self._icon.props.pulsing = True
+ color = Gdk.color_parse(style.COLOR_BLACK.get_html())
+ self.modify_bg(Gtk.StateType.PRELIGHT, color)
+
+ color = Gdk.color_parse(style.COLOR_BUTTON_GREY.get_html())
+ self.modify_bg(Gtk.StateType.ACTIVE, color)
+
+ self._icon.props.pulse_color = _PULSE_COLOR
+ self._icon.props.timeout = _PULSE_TIMEOUT
self.add(self._icon)
self._icon.show()
- GObject.timeout_add_seconds(self._PULSE_TIMEOUT,
- self.__stop_pulsing_cb)
+ self.start_pulsing()
self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
- def __stop_pulsing_cb(self):
- self._icon.props.pulsing = False
- return False
+ def start_pulsing(self):
+ self._icon.props.pulsing = True
def do_set_property(self, pspec, value):
if pspec.name == 'xo-color':
@@ -87,17 +274,13 @@ class NotificationIcon(Gtk.EventBox):
class NotificationWindow(Gtk.Window):
__gtype_name__ = 'SugarNotificationWindow'
- def __init__(self, **kwargs):
-
- Gtk.Window.__init__(self, **kwargs)
+ def __init__(self):
+ Gtk.Window.__init__(self)
self.set_decorated(False)
self.set_resizable(False)
self.connect('realize', self._realize_cb)
def _realize_cb(self, widget):
- self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
- self.window.set_accept_focus(False)
-
color = Gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html())
self.modify_bg(Gtk.StateType.NORMAL, color)
diff --git a/src/jarabe/intro/window.py b/src/jarabe/intro/window.py
index 252870d..469514e 100644
--- a/src/jarabe/intro/window.py
+++ b/src/jarabe/intro/window.py
@@ -34,12 +34,30 @@ from sugar3.graphics.xocolor import XoColor
from jarabe.intro import colorpicker
-def create_profile(name, color=None):
+def create_profile(name, age, color=None):
if not color:
color = XoColor()
client = GConf.Client.get_default()
client.set_string('/desktop/sugar/user/nick', name)
+
+
+ # Algorithm to generate the timestamp of the birthday of the
+ # XO-user ::
+ #
+ # timestamp = current_timestamp - [age * (365 * 24 * 60 * 60)]
+ #
+ # Note that, this timestamp may actually (in worst-case) be
+ # off-target by 1 year, but that is ok, since we want an
+ # "approximate" age of the XO-user (for statistics-collection).
+ import time
+ current_timestamp = time.time()
+ xo_user_age_as_timestamp = int(age) * 365 * 24 * 60 * 60
+
+ approx_timestamp_at_user_birthday = current_timestamp - xo_user_age_as_timestamp
+ client.set_int('/desktop/sugar/user/birth_timestamp', int(approx_timestamp_at_user_birthday))
+ # Done.
+
client.set_string('/desktop/sugar/user/color', color.to_string())
client.suggest_sync()
@@ -125,6 +143,46 @@ class _NamePage(_Page):
self._entry.grab_focus()
+class _AgePage(_Page):
+ def __init__(self, intro):
+ _Page.__init__(self)
+ self._intro = intro
+ self._max_age = 1000
+
+ alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
+ self.pack_start(alignment, expand=True, fill=True, padding=0)
+
+ hbox = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ alignment.add(hbox)
+
+ label = Gtk.Label(_('Age:'))
+ hbox.pack_start(label, False, True, 0)
+
+ adjustment = Gtk.Adjustment(0, 0, self._max_age, 1, 0, 0)
+ self._entry = Gtk.SpinButton(adjustment=adjustment)
+ self._entry.props.editable = True
+ self._entry.connect('notify::text', self._text_changed_cb)
+ self._entry.set_max_length(15)
+ hbox.pack_start(self._entry, False, True, 0)
+
+ label = Gtk.Label(_('years'))
+ hbox.pack_start(label, False, True, 0)
+
+
+ def _text_changed_cb(self, entry, pspec):
+ valid = False
+ if entry.props.text.isdigit():
+ int_value = int(entry.props.text)
+ valid = ((int_value > 0) and (int_value <= self._max_age))
+ self.set_valid(valid)
+
+ def get_age(self):
+ return int(self._entry.props.text)
+
+ def activate(self):
+ self._entry.grab_focus()
+
+
class _ColorPage(_Page):
def __init__(self):
_Page.__init__(self)
@@ -148,11 +206,12 @@ class _ColorPage(_Page):
class _IntroBox(Gtk.VBox):
__gsignals__ = {
'done': (GObject.SignalFlags.RUN_FIRST, None,
- ([GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT])),
+ ([GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT])),
}
PAGE_NAME = 0
- PAGE_COLOR = 1
+ PAGE_AGE = 1
+ PAGE_COLOR = 2
PAGE_FIRST = PAGE_NAME
PAGE_LAST = PAGE_COLOR
@@ -163,6 +222,7 @@ class _IntroBox(Gtk.VBox):
self._page = self.PAGE_NAME
self._name_page = _NamePage(self)
+ self._age_page = _AgePage(self)
self._color_page = _ColorPage()
self._current_page = None
self._next_button = None
@@ -185,6 +245,8 @@ class _IntroBox(Gtk.VBox):
if self._page == self.PAGE_NAME:
self._current_page = self._name_page
+ if self._page == self.PAGE_AGE:
+ self._current_page = self._age_page
elif self._page == self.PAGE_COLOR:
self._current_page = self._color_page
@@ -253,9 +315,10 @@ class _IntroBox(Gtk.VBox):
def done(self):
name = self._name_page.get_name()
+ age = self._age_page.get_age()
color = self._color_page.get_color()
- self.emit('done', name, color)
+ self.emit('done', name, age, color)
class IntroWindow(Gtk.Window):
@@ -274,12 +337,12 @@ class IntroWindow(Gtk.Window):
self._intro_box.show()
self.connect('key-press-event', self.__key_press_cb)
- def _done_cb(self, box, name, color):
+ def _done_cb(self, box, name, age, color):
self.hide()
- GObject.idle_add(self._create_profile_cb, name, color)
+ GObject.idle_add(self._create_profile_cb, name, age, color)
- def _create_profile_cb(self, name, color):
- create_profile(name, color)
+ def _create_profile_cb(self, name, age, color):
+ create_profile(name, age, color)
Gtk.main_quit()
return False
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am
index ba29062..df8f961 100644
--- a/src/jarabe/journal/Makefile.am
+++ b/src/jarabe/journal/Makefile.am
@@ -15,4 +15,6 @@ sugar_PYTHON = \
model.py \
objectchooser.py \
palettes.py \
- volumestoolbar.py
+ processdialog.py \
+ volumestoolbar.py \
+ webdavmanager.py
diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py
index 21c0672..0170d0b 100644
--- a/src/jarabe/journal/expandedentry.py
+++ b/src/jarabe/journal/expandedentry.py
@@ -160,11 +160,30 @@ class ExpandedEntry(Gtk.EventBox):
self._buddy_list.pack_start(self._create_buddy_list(), False, False,
style.DEFAULT_SPACING)
- description = metadata.get('description', '')
+ # TRANS: Do not translate the """%s""".
+ uploader_nick_text = self.__create_text_description(
+ _('Source XO Nick :: \n%s'), metadata.get('uploader-nick', ''))
+
+ # TRANS: Do not translate the """%s""".
+ uploader_serial_text = self.__create_text_description(
+ _('Source XO Serial Number :: \n%s'), metadata.get('uploader-serial', ''))
+
+ # TRANS: Do not translate the """%s""".
+ misc_info_text = self.__create_text_description(
+ _('Misellaneous Information :: \n%s'), metadata.get('description', ''))
+
+ description = uploader_nick_text + uploader_serial_text + misc_info_text
self._description.get_buffer().set_text(description)
+
tags = metadata.get('tags', '')
self._tags.get_buffer().set_text(tags)
+ def __create_text_description(self, heading, value):
+ if (value == '') or (value is None):
+ return ''
+
+ return ((heading % value) + '\n\n')
+
def _create_keep_icon(self):
keep_icon = KeepIcon()
keep_icon.connect('toggled', self._keep_icon_toggled_cb)
@@ -396,18 +415,20 @@ class ExpandedEntry(Gtk.EventBox):
needs_update = True
if needs_update:
- if self._metadata.get('mountpoint', '/') == '/':
- model.write(self._metadata, update_mtime=False)
- else:
- old_file_path = os.path.join(self._metadata['mountpoint'],
- model.get_file_name(old_title,
- self._metadata['mime_type']))
- model.write(self._metadata, file_path=old_file_path,
- update_mtime=False)
+ from jarabe.journal.journalactivity import get_journal
+ self._metadata['mountpoint'] = \
+ get_journal().get_detail_toolbox().get_mount_point()
+
+ model.update_only_metadata_and_preview_files_and_return_file_paths(self._metadata)
self._update_title_sid = None
def _keep_icon_toggled_cb(self, keep_icon):
+ # If it is a locally-mounted remote-share, return without doing
+ # any processing.
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if keep_icon.get_active():
self._metadata['keep'] = 1
else:
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index 4bb68fd..b2ffae0 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -19,6 +19,7 @@ import logging
from gettext import gettext as _
import uuid
+from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkX11
@@ -27,7 +28,8 @@ import statvfs
import os
from sugar3.graphics.window import Window
-from sugar3.graphics.alert import ErrorAlert
+from sugar3.graphics.icon import Icon
+from sugar3.graphics.alert import Alert, ErrorAlert, ConfirmationAlert
from sugar3.bundle.bundle import ZipExtractException, RegistrationException
from sugar3 import env
@@ -37,7 +39,9 @@ from gi.repository import SugarExt
from jarabe.model import bundleregistry
from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.journaltoolbox import EditToolbox
from jarabe.journal.listview import ListView
+from jarabe.journal.listmodel import ListModel
from jarabe.journal.detailview import DetailView
from jarabe.journal.volumestoolbar import VolumesToolbar
from jarabe.journal import misc
@@ -46,6 +50,7 @@ from jarabe.journal.objectchooser import ObjectChooser
from jarabe.journal.modalalert import ModalAlert
from jarabe.journal import model
from jarabe.journal.journalwindow import JournalWindow
+from jarabe.journal.journalwindow import show_normal_cursor
J_DBUS_SERVICE = 'org.laptop.Journal'
@@ -56,6 +61,7 @@ _SPACE_TRESHOLD = 52428800
_BUNDLE_ID = 'org.laptop.JournalActivity'
_journal = None
+_mount_point = None
class JournalActivityDBusService(dbus.service.Object):
@@ -124,8 +130,33 @@ class JournalActivity(JournalWindow):
self._list_view = None
self._detail_view = None
self._main_toolbox = None
+ self._edit_toolbox = None
self._detail_toolbox = None
self._volumes_toolbar = None
+ self._editing_mode = False
+ self._alert = Alert()
+
+ self._error_alert = Alert()
+ icon = Icon(icon_name='dialog-ok')
+ self._error_alert.add_button(Gtk.ResponseType.OK, _('Ok'), icon)
+ icon.show()
+
+ self._confirmation_alert = Alert()
+ icon = Icon(icon_name='dialog-cancel')
+ self._confirmation_alert.add_button(Gtk.ResponseType.CANCEL, _('Stop'), icon)
+ icon.show()
+ icon = Icon(icon_name='dialog-ok')
+ self._confirmation_alert.add_button(Gtk.ResponseType.OK, _('Continue'), icon)
+ icon.show()
+
+ self._current_alert = None
+ self.setup_handlers_for_alert_actions()
+
+ self._info_alert = None
+ self._selected_entries = []
+ self._bundle_installation_allowed = True
+
+ set_mount_point('/')
self._setup_main_view()
self._setup_secondary_view()
@@ -151,10 +182,17 @@ class JournalActivity(JournalWindow):
self._check_available_space()
def __volume_error_cb(self, gobject, message, severity):
- alert = ErrorAlert(title=severity, msg=message)
- alert.connect('response', self.__alert_response_cb)
- self.add_alert(alert)
- alert.show()
+ self.update_title_and_message(self._error_alert, severity,
+ message)
+ self._callback = None
+ self._data = None
+ self.update_alert(self._error_alert)
+
+ def _show_alert(self, message, severity):
+ self.__volume_error_cb(None, message, severity)
+
+ def _volume_error_cb(self, gobject, message, severity):
+ self.update_error_alert(severity, message, None, None)
def __alert_response_cb(self, alert, response_id):
self.remove_alert(alert)
@@ -196,11 +234,14 @@ class JournalActivity(JournalWindow):
self._main_toolbox.search_entry.connect('icon-press',
self.__search_icon_pressed_cb)
self._main_toolbox.set_mount_point('/')
+ #search_toolbar.set_mount_point('/')
+ set_mount_point('/')
def _setup_secondary_view(self):
self._secondary_view = Gtk.VBox()
self._detail_toolbox = DetailToolbox()
+ self._detail_toolbox.set_mount_point('/')
self._detail_toolbox.connect('volume-error',
self.__volume_error_cb)
@@ -240,9 +281,16 @@ class JournalActivity(JournalWindow):
self.connect('key-press-event', self._key_press_event_cb)
def show_main_view(self):
- if self.toolbar_box != self._main_toolbox:
- self.set_toolbar_box(self._main_toolbox)
- self._main_toolbox.show()
+ if self._editing_mode:
+ self._toolbox = EditToolbox()
+
+ # TRANS: Do not translate the "%d"
+ self._toolbox.set_total_number_of_entries(self.get_total_number_of_entries())
+ else:
+ self._toolbox = self._main_toolbox
+
+ self.set_toolbar_box(self._toolbox)
+ self._toolbox.show()
if self.canvas != self._main_view:
self.set_canvas(self._main_view)
@@ -277,6 +325,10 @@ class JournalActivity(JournalWindow):
def __volume_changed_cb(self, volume_toolbar, mount_point):
logging.debug('Selected volume: %r.', mount_point)
self._main_toolbox.set_mount_point(mount_point)
+ set_mount_point(mount_point)
+
+ # Also, need to update the mount-point for Detail-View.
+ self._detail_toolbox.set_mount_point(mount_point)
def __model_created_cb(self, sender, **kwargs):
self._check_for_bundle(kwargs['object_id'])
@@ -301,6 +353,9 @@ class JournalActivity(JournalWindow):
self._list_view.update_dates()
def _check_for_bundle(self, object_id):
+ if not self._bundle_installation_allowed:
+ return
+
registry = bundleregistry.get_registry()
metadata = model.get(object_id)
@@ -336,6 +391,9 @@ class JournalActivity(JournalWindow):
metadata['bundle_id'] = bundle.get_bundle_id()
model.write(metadata)
+ def set_bundle_installation_allowed(self, allowed):
+ self._bundle_installation_allowed = allowed
+
def __window_state_event_cb(self, window, event):
logging.debug('window_state_event_cb %r', self)
if event.changed_mask & Gdk.WindowState.ICONIFIED:
@@ -378,6 +436,105 @@ class JournalActivity(JournalWindow):
self.reveal()
self.show_main_view()
+ def switch_to_editing_mode(self, switch):
+ # (re)-switch, only if not already.
+ if (switch) and (not self._editing_mode):
+ self._editing_mode = True
+ self.get_list_view().disable_drag_and_copy()
+ self.show_main_view()
+ elif (not switch) and (self._editing_mode):
+ self._editing_mode = False
+ self.get_list_view().enable_drag_and_copy()
+ self.show_main_view()
+
+ def get_list_view(self):
+ return self._list_view
+
+ def setup_handlers_for_alert_actions(self):
+ self._error_alert.connect('response',
+ self.__check_for_alert_action)
+ self._confirmation_alert.connect('response',
+ self.__check_for_alert_action)
+
+ def __check_for_alert_action(self, alert, response_id):
+ self.hide_alert()
+ if self._callback is not None:
+ GObject.idle_add(self._callback, self._data,
+ response_id)
+
+ def update_title_and_message(self, alert, title, message):
+ alert.props.title = title
+ alert.props.msg = message
+
+ def update_alert(self, alert):
+ if self._current_alert is None:
+ self.add_alert(alert)
+ elif self._current_alert != alert:
+ self.remove_alert(self._current_alert)
+ self.add_alert(alert)
+
+ self.remove_alert(self._current_alert)
+ self.add_alert(alert)
+ self._current_alert = alert
+ self._current_alert.show()
+ show_normal_cursor()
+
+ def hide_alert(self):
+ if self._current_alert is not None:
+ self._current_alert.hide()
+
+ def update_info_alert(self, title, message):
+ self.get_toolbar_box().display_running_status_in_multi_select(title, message)
+
+ def update_error_alert(self, title, message, callback, data):
+ self.update_title_and_message(self._error_alert, title,
+ message)
+ self._callback = callback
+ self._data = data
+ self.update_alert(self._error_alert)
+
+ def update_confirmation_alert(self, title, message, callback,
+ data):
+ self.update_title_and_message(self._confirmation_alert, title,
+ message)
+ self._callback = callback
+ self._data = data
+ self.update_alert(self._confirmation_alert)
+
+ def update_progress(self, fraction):
+ self.get_toolbar_box().update_progress(fraction)
+
+ def get_metadata_list(self, selected_state):
+ metadata_list = []
+
+ list_view_model = self.get_list_view().get_model()
+ for index in range(0, len(list_view_model)):
+ metadata = list_view_model.get_metadata(index)
+ metadata_selected = \
+ list_view_model.get_selected_value(metadata['uid'])
+
+ if ( (selected_state and metadata_selected) or \
+ ((not selected_state) and (not metadata_selected)) ):
+ metadata_list.append(metadata)
+
+ return metadata_list
+
+ def get_total_number_of_entries(self):
+ list_view_model = self.get_list_view().get_model()
+ return len(list_view_model)
+
+ def is_editing_mode_present(self):
+ return self._editing_mode
+
+ def get_volumes_toolbar(self):
+ return self._volumes_toolbar
+
+ def get_toolbar_box(self):
+ return self._toolbox
+
+ def get_detail_toolbox(self):
+ return self._detail_toolbox
+
def get_journal():
global _journal
@@ -389,3 +546,11 @@ def get_journal():
def start():
get_journal()
+
+
+def set_mount_point(mount_point):
+ global _mount_point
+ _mount_point = mount_point
+
+def get_mount_point():
+ return _mount_point
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index 09d8a31..cb19f65 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+from gettext import ngettext
import logging
from datetime import datetime, timedelta
import os
@@ -26,6 +27,7 @@ from gi.repository import GObject
from gi.repository import Gio
import glib
from gi.repository import Gtk
+from gi.repository import Gdk
from sugar3.graphics.palette import Palette
from sugar3.graphics.toolbarbox import ToolbarBox
@@ -45,8 +47,9 @@ from jarabe.journal import misc
from jarabe.journal import model
from jarabe.journal.palettes import ClipboardMenu
from jarabe.journal.palettes import VolumeMenu
-from jarabe.journal import journalwindow
+from jarabe.journal import journalwindow, palettes
+COPY_MENU_HELPER = palettes.get_copy_menu_helper()
_AUTOSEARCH_TIMEOUT = 1000
@@ -74,6 +77,10 @@ class MainToolbox(ToolbarBox):
def __init__(self):
ToolbarBox.__init__(self)
+ self._info_widget = MultiSelectEntriesInfoWidget()
+ self.add(self._info_widget)
+ self._info_widget.hide()
+
self._mount_point = None
self.search_entry = iconentry.IconEntry()
@@ -123,6 +130,12 @@ class MainToolbox(ToolbarBox):
self.refresh_filters()
+ def update_progress(self, fraction):
+ self._info_widget.update_progress(fraction)
+
+ def hide_info_widget(self):
+ self._info_widget.hide()
+
def _get_when_search_combo(self):
when_search = ComboBox()
when_search.append_item(_ACTION_ANYTIME, _('Anytime'))
@@ -390,11 +403,30 @@ class DetailToolbox(ToolbarBox):
separator.show()
erase_button = ToolButton('list-remove')
+ self._erase_button = erase_button
erase_button.set_tooltip(_('Erase'))
erase_button.connect('clicked', self._erase_button_clicked_cb)
self.toolbar.insert(erase_button, -1)
erase_button.show()
+ def set_mount_point(self, mount_point):
+ self._mount_point = mount_point
+ self.set_sensitivity_of_icons()
+
+ def get_mount_point(self):
+ return self._mount_point
+
+ def set_sensitivity_of_icons(self):
+ mount_point = self.get_mount_point()
+ if model.is_mount_point_for_locally_mounted_remote_share(mount_point):
+ sensitivity = False
+ else:
+ sensitivity = True
+
+ self._resume.set_sensitive(sensitivity)
+ self._duplicate.set_sensitive(sensitivity)
+ self._erase_button.set_sensitive(sensitivity)
+
def set_metadata(self, metadata):
self._metadata = metadata
self._refresh_copy_palette()
@@ -452,39 +484,11 @@ class DetailToolbox(ToolbarBox):
palette.menu.remove(menu_item)
menu_item.destroy()
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=Gtk.IconSize.MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(clipboard_menu)
- clipboard_menu.show()
-
- if self._metadata['mountpoint'] != '/':
- client = GConf.Client.get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=Gtk.IconSize.MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(journal_menu)
- journal_menu.show()
-
- volume_monitor = Gio.VolumeMonitor.get()
- icon_theme = Gtk.IconTheme.get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=Gtk.IconSize.MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(volume_menu)
- volume_menu.show()
+ COPY_MENU_HELPER.insert_copy_to_menu_items(palette.menu,
+ [self._metadata],
+ show_editing_alert=False,
+ show_progress_info_alert=False,
+ batch_mode=False)
def _refresh_duplicate_palette(self):
color = misc.get_icon_color(self._metadata)
@@ -524,6 +528,270 @@ class DetailToolbox(ToolbarBox):
menu_item.show()
+class EditToolbox(ToolbarBox):
+ def __init__(self):
+ ToolbarBox.__init__(self)
+
+ self.toolbar.add(SelectNoneButton())
+ self.toolbar.add(SelectAllButton())
+
+ self.toolbar.add(Gtk.SeparatorToolItem())
+
+ self.toolbar.add(BatchEraseButton())
+ self.toolbar.add(BatchCopyButton())
+
+ self.toolbar.add(Gtk.SeparatorToolItem())
+
+ self._multi_select_info_widget = MultiSelectEntriesInfoWidget()
+ self.toolbar.add(self._multi_select_info_widget)
+
+ self.show_all()
+ self.toolbar.show_all()
+
+ def process_new_selected_entry_in_multi_select(self):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ '', '', True, True)
+
+ def process_new_deselected_entry_in_multi_select(self):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ '', '', False, True)
+
+ def display_running_status_in_multi_select(self, primary_info,
+ secondary_info):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ primary_info, secondary_info,
+ None, None)
+
+ def display_already_selected_entries_status(self):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ '', '', True, False)
+
+ def set_total_number_of_entries(self, total):
+ self._multi_select_info_widget.set_total_number_of_entries(total)
+
+ def get_current_entry_number(self):
+ return self._multi_select_info_widget.get_current_entry_number()
+
+ def update_progress(self, fraction):
+ self._multi_select_info_widget.update_progress(fraction)
+
+
+class SelectNoneButton(ToolButton):
+ def __init__(self):
+ ToolButton.__init__(self, 'select-none')
+ self.props.tooltip = _('Deselect all')
+
+ self.connect('clicked', self.__do_deselect_all)
+
+ def __do_deselect_all(self, widget_clicked):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.get_list_view()._selected_entries = 0
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+ journal.get_list_view().refresh()
+
+
+class SelectAllButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'select-all')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=False,
+ show_progress_info_alert=False,
+ batch_mode=True,
+ auto_deselect_source_entries=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=True,
+ show_not_completed_ops_info=False)
+ self.props.tooltip = _('Select all')
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _get_editing_alert_operation(self):
+ return _('Select all')
+
+ def _get_info_alert_title(self):
+ return _('Selecting')
+
+ def _get_post_selection_alert_message_entries_len(self):
+ return self._model_len
+
+ def _get_post_selection_alert_message(self, entries_len):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return ngettext('You have selected %d entry.',
+ 'You have selected %d entries.',
+ entries_len) % (entries_len,)
+
+ def _operate(self, metadata):
+ # Nothing specific needs to be done.
+ # The checkboxes are unchecked as part of the toggling of any
+ # operation that operates on selected entries.
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchEraseButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-delete')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ auto_deselect_source_entries=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ 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'
+
+ def _get_editing_alert_title(self):
+ return _('Erase')
+
+ def _get_editing_alert_message(self, entries_len):
+ return ngettext('Do you want to erase %d entry?',
+ 'Do you want to erase %d entries?',
+ entries_len) % (entries_len)
+
+ def _get_editing_alert_operation(self):
+ return _('Erase')
+
+ def _get_info_alert_title(self):
+ return _('Erasing')
+
+ def _operate(self, metadata):
+ model.delete(metadata['uid'])
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchCopyButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-copy')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ auto_deselect_source_entries=False,
+ need_to_popup_options=True,
+ operate_on_deselected_entries=False,
+ show_not_completed_ops_info=False)
+
+ self.props.tooltip = _('Copy')
+
+ self._metadata_list = None
+ self._fill_and_pop_up_options(None)
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _fill_and_pop_up_options(self, widget_clicked):
+ for child in self.props.palette.menu.get_children():
+ self.props.palette.menu.remove(child)
+
+ COPY_MENU_HELPER.insert_copy_to_menu_items(self.props.palette.menu,
+ [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True)
+ if widget_clicked is not None:
+ self.props.palette.popup(immediate=True, state=1)
+
+
+class MultiSelectEntriesInfoWidget(Gtk.ToolItem):
+ def __init__(self):
+ Gtk.ToolItem.__init__(self)
+
+ self._box = Gtk.VBox()
+ self._selected_entries = 0
+
+ self._label = Gtk.Label()
+ self._box.pack_start(self._label, True, True, 0)
+
+ self._progress_label = Gtk.Label()
+ self._box.pack_start(self._progress_label, True, True, 0)
+
+ self.add(self._box)
+
+ self.show_all()
+ self._box.show_all()
+ self._progress_label.hide()
+
+ def set_total_number_of_entries(self, total):
+ self._total = total
+
+ def update_progress(self, fraction):
+ percent = '%.02f' % (fraction * 100)
+
+ # TRANS: Do not translate %.02f
+ text = '%.02f%% complete' % (fraction * 100)
+ if (str(percent) != '100.00') and (str(percent).endswith('00')):
+ self._progress_label.set_text(text)
+ self._progress_label.show()
+ self.show_all()
+ Gdk.Window.process_all_updates()
+ else:
+ self._progress_label.hide()
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ self.hide()
+
+ def update_text(self, primary_text, secondary_text, special_action,
+ update_selected_entries):
+ # If "special_action" is None,
+ # we need to display the info, conveyed by
+ # "primary_message" and "secondary_message"
+ #
+ # If "special_action" is True,
+ # a new entry has been selected.
+ #
+ # If "special_action" is False,
+ # an enrty has been deselected.
+ if special_action == None:
+ self._label.set_text(primary_text + secondary_text)
+ self._label.show()
+ else:
+ if update_selected_entries:
+ if special_action == True:
+ self._selected_entries = self._selected_entries + 1
+ elif special_action == False:
+ self._selected_entries = self._selected_entries - 1
+
+ # TRANS: Do not translate the two "%d".
+ message = _('Selected %d of %d') % (self._selected_entries,
+ self._total)
+
+ # Only show the "selected x of y" for "Select All", or
+ # "Deselect All", or if the user checked/unchecked a
+ # checkbox.
+ from jarabe.journal.palettes import get_current_action_item
+ current_action_item = get_current_action_item()
+ if current_action_item == None or \
+ isinstance(current_action_item, SelectAllButton) or \
+ isinstance(current_action_item, SelectNoneButton):
+ self._label.set_text(message)
+ self._label.show()
+
+ Gdk.Window.process_all_updates()
+
+ def get_current_entry_number(self):
+ return self._selected_entries
+
+
class SortingButton(ToolButton):
__gtype_name__ = 'JournalSortingButton'
diff --git a/src/jarabe/journal/journalwindow.py b/src/jarabe/journal/journalwindow.py
index 776a495..8fcecaf 100644
--- a/src/jarabe/journal/journalwindow.py
+++ b/src/jarabe/journal/journalwindow.py
@@ -15,6 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+from gi.repository import Gdk
+
from sugar3.graphics.window import Window
_journal_window = None
@@ -31,3 +33,46 @@ class JournalWindow(Window):
def get_journal_window():
return _journal_window
+
+
+def set_widgets_active_state(active_state):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.get_toolbar_box().set_sensitive(active_state)
+ journal.get_list_view().set_sensitive(active_state)
+ journal.get_volumes_toolbar().set_sensitive(active_state)
+
+
+def show_waiting_cursor():
+ # Only show waiting-cursor, if this is the batch-mode.
+
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ return
+
+ _journal_window.get_root_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
+
+
+def freeze_ui():
+ # Only freeze, if this is the batch-mode.
+
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ return
+
+ show_waiting_cursor()
+
+ set_widgets_active_state(False)
+
+
+def show_normal_cursor():
+ _journal_window.get_root_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR))
+
+
+def unfreeze_ui():
+ # Unfreeze, irrespective of whether this is the batch mode.
+
+ set_widgets_active_state(True)
+
+ show_normal_cursor()
diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py
index 16e3a57..9c7b7d5 100644
--- a/src/jarabe/journal/keepicon.py
+++ b/src/jarabe/journal/keepicon.py
@@ -22,6 +22,8 @@ from sugar3.graphics.icon import Icon
from sugar3.graphics import style
from sugar3.graphics.xocolor import XoColor
+from jarabe.journal import model
+
class KeepIcon(Gtk.ToggleButton):
def __init__(self):
@@ -37,6 +39,9 @@ class KeepIcon(Gtk.ToggleButton):
self.connect('enter-notify-event', self.__enter_notify_event_cb)
def __toggled_cb(self, widget):
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if self.get_active():
client = GConf.Client.get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
@@ -47,9 +52,15 @@ class KeepIcon(Gtk.ToggleButton):
self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
def __enter_notify_event_cb(self, icon, event):
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if not self.get_active():
self._icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
def __leave_notify_event_cb(self, icon, event):
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if not self.get_active():
self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
index b98d01c..a5bb7b0 100644
--- a/src/jarabe/journal/listmodel.py
+++ b/src/jarabe/journal/listmodel.py
@@ -54,6 +54,7 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
COLUMN_BUDDY_1 = 9
COLUMN_BUDDY_2 = 10
COLUMN_BUDDY_3 = 11
+ COLUMN_SELECT = 12
_COLUMN_TYPES = {
COLUMN_UID: str,
@@ -68,6 +69,7 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
COLUMN_BUDDY_1: object,
COLUMN_BUDDY_3: object,
COLUMN_BUDDY_2: object,
+ COLUMN_SELECT: bool,
}
_PAGE_SIZE = 10
@@ -79,6 +81,8 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
self._cached_row = None
self._result_set = model.find(query, ListModel._PAGE_SIZE)
self._temp_drag_file_path = None
+ self._selected = {}
+ self._uid_metadata_assoc = {}
# HACK: The view will tell us that it is resizing so the model can
# avoid hitting D-Bus and disk.
@@ -248,3 +252,22 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
return True
return False
+
+ def update_uid_metadata_assoc(self, uid, metadata):
+ self._uid_metadata_assoc[uid] = metadata
+
+ def set_selected_value(self, uid, value):
+ if value == False:
+ del self._selected[uid]
+ elif value == True:
+ self._selected[uid] = value
+
+ def get_selected_value(self, uid):
+ if self._selected.has_key(uid):
+ return True
+ else:
+ return False
+
+ def get_in_memory_metadata(self, path):
+ uid = self[path][ListModel.COLUMN_UID]
+ return self._uid_metadata_assoc[uid]
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index 5b2c5ab..35c8092 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -67,7 +67,8 @@ class BaseListView(Gtk.Bin):
'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])),
}
- def __init__(self):
+ def __init__(self, is_object_chooser):
+ self._is_object_chooser = is_object_chooser
self._query = {}
self._model = None
self._progress_bar = None
@@ -100,11 +101,11 @@ class BaseListView(Gtk.Bin):
self._title_column = None
self.sort_column = None
self._add_columns()
+ self._inhibit_refresh = False
+ self._selected_entries = 0
+
+ self.enable_drag_and_copy()
- self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
- [('text/uri-list', 0, 0),
- ('journal-object-id', 0, 0)],
- Gdk.DragAction.COPY)
# Auto-update stuff
self._fully_obscured = True
@@ -116,6 +117,15 @@ class BaseListView(Gtk.Bin):
model.updated.connect(self.__model_updated_cb)
model.deleted.connect(self.__model_deleted_cb)
+ def enable_drag_and_copy(self):
+ self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
+ [('text/uri-list', 0, 0),
+ ('journal-object-id', 0, 0)],
+ Gdk.DragAction.COPY)
+
+ def disable_drag_and_copy(self):
+ self.tree_view.unset_rows_drag_source()
+
def __model_created_cb(self, sender, signal, object_id):
if self._is_new_item_visible(object_id):
self._set_dirty()
@@ -136,6 +146,17 @@ class BaseListView(Gtk.Bin):
return object_id.startswith(self._query['mountpoints'][0])
def _add_columns(self):
+ if not self._is_object_chooser:
+ cell_select = CellRendererToggle(self.tree_view)
+ cell_select.connect('clicked', self.__cell_select_clicked_cb)
+
+ column = Gtk.TreeViewColumn()
+ column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
+ column.props.fixed_width = cell_select.props.width
+ column.pack_start(cell_select, True)
+ column.set_cell_data_func(cell_select, self.__select_set_data_cb)
+ self.tree_view.append_column(column)
+
cell_favorite = CellRendererFavorite(self.tree_view)
cell_favorite.connect('clicked', self.__favorite_clicked_cb)
@@ -248,8 +269,30 @@ class BaseListView(Gtk.Bin):
def __favorite_set_data_cb(self, column, cell, tree_model,
tree_iter, data):
- favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
- if favorite:
+ # Instead of querying the favorite-status from the "cached"
+ # entries in listmodel, hit the DS, and retrieve the persisted
+ # favorite-status.
+ # This solves the issue in "Multi-Select", wherein the
+ # listview is inhibited from refreshing. Now, if the user
+ # clicks favorite-star-icon(s), the change(s) is(are) written
+ # to the DS, but no refresh takes place. Thus, in order to have
+ # the change(s) reflected on the UI, we need to hit the DS for
+ # querying the favorite-status (instead of relying on the
+ # cached-listmodel.
+ uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+ if uid is None:
+ return
+
+ try:
+ metadata = model.get(uid)
+ except:
+ return
+
+ favorite = None
+ if 'keep' in metadata.keys():
+ favorite = str(metadata['keep'])
+
+ if favorite == '1':
client = GConf.Client.get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
cell.props.xo_color = color
@@ -257,6 +300,11 @@ class BaseListView(Gtk.Bin):
cell.props.xo_color = None
def __favorite_clicked_cb(self, cell, path):
+ # If this is a remote-share, return without doing any
+ # processing.
+ if model.is_current_mount_point_for_remote_share(model.LIST_VIEW):
+ return
+
row = self._model[path]
metadata = model.get(row[ListModel.COLUMN_UID])
if not model.is_editable(metadata):
@@ -265,7 +313,94 @@ class BaseListView(Gtk.Bin):
metadata['keep'] = '0'
else:
metadata['keep'] = '1'
- model.write(metadata, update_mtime=False)
+
+ from jarabe.journal.journalactivity import get_mount_point
+ metadata['mountpoint'] = get_mount_point()
+
+ model.update_only_metadata_and_preview_files_and_return_file_paths(metadata)
+ self.__redraw_view_if_necessary()
+
+ def __select_set_data_cb(self, column, cell, tree_model, tree_iter,
+ data):
+ uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+ if uid is None:
+ return
+
+ # Hack to associate the cell with the metadata, so that it (the
+ # cell) is available offline as well (example during
+ # batch-operations, when the processing has to be done, without
+ # actually clicking any cell.
+ try:
+ metadata = model.get(uid)
+ except:
+ # https://dev.laptop.org.au/issues/1119
+ # http://bugs.sugarlabs.org/ticket/3344
+ # Occurs, when copying entries from journal to pen-drive.
+ # Simply swallow the exception, and return, as this too,
+ # like the above case, does not have any impact on the
+ # functionality.
+ return
+
+ metadata['cell'] = cell
+ tree_model.update_uid_metadata_assoc(uid, metadata)
+
+ self.do_ui_select_change(metadata)
+
+ def __cell_select_clicked_cb(self, cell, path):
+ row = self._model[path]
+ treeiter = self._model.get_iter(path)
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ self.do_backend_select_change(metadata)
+
+ def do_ui_select_change(self, metadata):
+ tree_model = self.get_model()
+ selected = tree_model.get_selected_value(metadata['uid'])
+
+ if 'cell' in metadata.keys():
+ cell = metadata['cell']
+ if selected:
+ cell.props.icon_name = 'emblem-checked'
+ else:
+ cell.props.icon_name = 'emblem-unchecked'
+
+ def do_backend_select_change(self, metadata):
+ uid = metadata['uid']
+ selected = self._model.get_selected_value(uid)
+
+ self._model.set_selected_value(uid, not selected)
+ self._process_new_selected_status(not selected)
+
+ def _process_new_selected_status(self, new_status):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal_toolbar_box = journal.get_toolbar_box()
+
+ self.__redraw_view_if_necessary()
+
+ if new_status == False:
+ self._selected_entries = self._selected_entries - 1
+ journal_toolbar_box.process_new_deselected_entry_in_multi_select()
+ GObject.idle_add(self._post_backend_processing)
+ else:
+ self._selected_entries = self._selected_entries + 1
+ journal.get_list_view().inhibit_refresh(True)
+ journal.switch_to_editing_mode(True)
+
+ # For the case, when we are switching to editing-mode.
+ # The previous call won't actually redraw, as we are not in
+ # editing-mode that time.
+ self.__redraw_view_if_necessary()
+
+ journal.get_toolbar_box().process_new_selected_entry_in_multi_select()
+
+ def _post_backend_processing(self):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if self._selected_entries == 0:
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+ journal.get_list_view().refresh()
def update_with_query(self, query_dict):
logging.debug('ListView.update_with_query')
@@ -282,6 +417,11 @@ class BaseListView(Gtk.Bin):
self.refresh()
def refresh(self):
+ if not self._inhibit_refresh:
+ self.set_sensitive(True)
+ self.proceed_with_refresh()
+
+ def proceed_with_refresh(self):
logging.debug('ListView.refresh query %r', self._query)
self._stop_progress_bar()
@@ -482,6 +622,64 @@ class BaseListView(Gtk.Bin):
self.update_dates()
return True
+ def get_model(self):
+ return self._model
+
+ def inhibit_refresh(self, inhibit):
+ self._inhibit_refresh = inhibit
+
+ def __redraw_view_if_necessary(self):
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ return
+
+ # First, get the total number of entries, for which the
+ # batch-operation is under progress.
+ from jarabe.journal.palettes import get_current_action_item
+
+ current_action_item = get_current_action_item()
+ if current_action_item is None:
+ # A single checkbox has been clicked/unclicked.
+ self.__redraw()
+ return
+
+ total_items = current_action_item.get_number_of_entries_to_operate_upon()
+
+ # Then, get the current entry being processed.
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ current_entry_number = journal.get_toolbar_box().get_current_entry_number()
+
+ # Redraw, if "current_entry_number" is 10.
+ if current_entry_number == 10:
+ self.__log(current_entry_number, total_items)
+ self.__redraw()
+ return
+
+ # Redraw, if this is the last entry.
+ if current_entry_number == total_items:
+ self.__log(current_entry_number, total_items)
+ self.__redraw()
+ return
+
+ # Redraw, if this is the 20% interval.
+ twenty_percent_of_total_items = total_items / 5
+ if twenty_percent_of_total_items < 10:
+ return
+
+ if (current_entry_number % twenty_percent_of_total_items) == 0:
+ self.__log(current_entry_number, total_items)
+ self.__redraw()
+ return
+
+ def __log(self, current_entry_number, total_items):
+ pass
+
+ def __redraw(self):
+ tree_view_window = self.tree_view.get_bin_window()
+ tree_view_window.hide()
+ tree_view_window.show()
+
class ListView(BaseListView):
__gtype_name__ = 'JournalListView'
@@ -497,8 +695,8 @@ class ListView(BaseListView):
([])),
}
- def __init__(self):
- BaseListView.__init__(self)
+ def __init__(self, is_object_chooser=False):
+ BaseListView.__init__(self, is_object_chooser)
self._is_dragging = False
self.tree_view.connect('drag-begin', self.__drag_begin_cb)
@@ -559,11 +757,25 @@ 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)
def __cell_title_edited_cb(self, cell, path, new_text):
+ from jarabe.journal.journalactivity import get_journal, \
+ get_mount_point
+ if get_journal().is_editing_mode_present():
+ return
+
row = self._model[path]
metadata = model.get(row[ListModel.COLUMN_UID])
metadata['title'] = new_text
@@ -592,6 +804,25 @@ class CellRendererFavorite(CellRendererIcon):
self.props.prelit_stroke_color = prelit_color.get_stroke_color()
self.props.prelit_fill_color = prelit_color.get_fill_color()
+ def do_render(self, cr, widget, background_area, cell_area, flags):
+ # If this is a remote-share, mask the "PRELIT" flag.
+ if model.is_current_mount_point_for_remote_share(model.LIST_VIEW):
+ flags = flags & (~(Gtk.CellRendererState.PRELIT))
+
+ CellRendererIcon.do_render(self, cr, widget, background_area, cell_area, flags)
+
+class CellRendererToggle(CellRendererIcon):
+ __gtype_name__ = 'JournalCellRendererSelect'
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.SMALL_ICON_SIZE
+ self.props.icon_name = 'checkbox-unchecked'
+ self.props.mode = Gtk.CellRendererMode.ACTIVATABLE
+
class CellRendererDetail(CellRendererIcon):
__gtype_name__ = 'JournalCellRendererDetail'
@@ -636,6 +867,11 @@ class CellRendererActivityIcon(CellRendererIcon):
if not self._show_palette:
return None
+ # Also, if we are in batch-operations mode, return 'None'
+ from jarabe.journal.journalactivity import get_journal
+ if get_journal().is_editing_mode_present():
+ return None
+
tree_model = self.tree_view.get_model()
metadata = tree_model.get_metadata(self.props.palette_invoker.path)
diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py
index efd0dbe..f627c1b 100644
--- a/src/jarabe/journal/misc.py
+++ b/src/jarabe/journal/misc.py
@@ -40,6 +40,7 @@ from jarabe.journal.journalentrybundle import JournalEntryBundle
from jarabe.journal import model
from jarabe.journal import journalwindow
+_NOT_AVAILABLE = _('Not available')
def _get_icon_for_mime(mime_type):
generic_types = mime.get_all_generic_types()
@@ -312,3 +313,48 @@ def get_icon_color(metadata):
return XoColor(client.get_string('/desktop/sugar/user/color'))
else:
return XoColor(metadata['icon-color'])
+
+
+def get_xo_serial():
+ _OFW_TREE = '/ofw'
+ _PROC_TREE = '/proc/device-tree'
+ _SN = 'serial-number'
+
+ serial_no = None
+ if os.path.exists(os.path.join(_OFW_TREE, _SN)):
+ serial_no = read_file(os.path.join(_OFW_TREE, _SN))
+ elif os.path.exists(os.path.join(_PROC_TREE, _SN)):
+ serial_no = read_file(os.path.join(_PROC_TREE, _SN))
+
+ if serial_no is None:
+ serial_no = _NOT_AVAILABLE
+
+ # Remove the trailing binary character, else DBUS will crash.
+ return serial_no.rstrip('\x00')
+
+
+def read_file(path):
+ if os.access(path, os.R_OK) == 0:
+ return None
+
+ fd = open(path, 'r')
+ value = fd.read()
+ fd.close()
+ if value:
+ value = value.strip('\n')
+ return value
+ else:
+ logging.debug('No information in file or directory: %s', path)
+ return None
+
+
+def get_nick():
+ client = GConf.Client.get_default()
+ return client.get_string("/desktop/sugar/user/nick")
+
+
+def get_backup_identifier():
+ serial_number = get_xo_serial()
+ if serial_number is _NOT_AVAILABLE:
+ serial_number = get_nick()
+ return serial_number
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 0a5b354..c9e08ec 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -16,6 +16,7 @@
import logging
import os
+import stat
import errno
import subprocess
from datetime import datetime
@@ -37,6 +38,8 @@ from sugar3 import dispatch
from sugar3 import mime
from sugar3 import util
+from jarabe.journal import webdavmanager
+
DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
@@ -50,14 +53,99 @@ PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id',
MIN_PAGES_TO_CACHE = 3
MAX_PAGES_TO_CACHE = 5
+WEBDAV_MOUNT_POINT = '/tmp/'
+LOCAL_SHARES_MOUNT_POINT = '/var/www/web1/web/'
+
JOURNAL_METADATA_DIR = '.Sugar-Metadata'
+LIST_VIEW = 1
+DETAIL_VIEW = 2
+
_datastore = None
created = dispatch.Signal()
updated = dispatch.Signal()
deleted = dispatch.Signal()
+SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME_PATH = \
+ '/desktop/sugar/network/school_server_ip_address_or_dns_name'
+IS_PEER_TO_PEER_SHARING_AVAILABLE_PATH = \
+ '/desktop/sugar/network/is_peer_to_peer_sharing_available'
+
+client = GConf.Client.get_default()
+SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME = client.get_string(SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME_PATH) or ''
+IS_PEER_TO_PEER_SHARING_AVAILABLE = client.get_bool(IS_PEER_TO_PEER_SHARING_AVAILABLE_PATH)
+
+
+
+def is_school_server_present():
+ return not (SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME is '')
+
+
+def is_peer_to_peer_sharing_available():
+ return IS_PEER_TO_PEER_SHARING_AVAILABLE == True
+
+
+def _get_mount_point(path):
+ dir_path = os.path.dirname(path)
+ while dir_path:
+ if os.path.ismount(dir_path):
+ return dir_path
+ else:
+ dir_path = dir_path.rsplit(os.sep, 1)[0]
+ return None
+
+
+def _check_remote_sharing_mount_point(mount_point, share_type):
+ from jarabe.journal.journalactivity import get_journal
+
+ mount_point_button = get_journal().get_volumes_toolbar()._get_button_for_mount_point(mount_point)
+ if mount_point_button._share_type == share_type:
+ return True
+ return False
+
+
+def is_mount_point_for_school_server(mount_point):
+ from jarabe.journal.volumestoolbar import SHARE_TYPE_SCHOOL_SERVER
+ return _check_remote_sharing_mount_point(mount_point, SHARE_TYPE_SCHOOL_SERVER)
+
+
+def is_mount_point_for_peer_share(mount_point):
+ from jarabe.journal.volumestoolbar import SHARE_TYPE_PEER
+ return _check_remote_sharing_mount_point(mount_point, SHARE_TYPE_PEER)
+
+
+def is_current_mount_point_for_remote_share(view_type):
+ from jarabe.journal.journalactivity import get_journal, get_mount_point
+ if view_type == LIST_VIEW:
+ current_mount_point = get_mount_point()
+ elif view_type == DETAIL_VIEW:
+ current_mount_point = get_journal().get_detail_toolbox().get_mount_point()
+
+ if is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ return True
+ return False
+
+
+def extract_ip_address_or_dns_name_from_locally_mounted_remote_share_path(path):
+ """
+ Path is of type ::
+
+ /tmp/1.2.3.4/webdav/a.txt; OR
+ /tmp/this.is.dns.name/a.txt
+ """
+ return path.split('/')[2]
+
+
+def is_mount_point_for_locally_mounted_remote_share(mount_point):
+ """
+ The mount-point can be either of the ip-Address, or the DNS name.
+ More importantly, whatever the "name" be, it does NOT have a
+ forward-slash.
+ """
+ return mount_point.find(WEBDAV_MOUNT_POINT) == 0
+
+
class _Cache(object):
__gtype_name__ = 'model_Cache'
@@ -422,6 +510,127 @@ class InplaceResultSet(BaseResultSet):
return
+class RemoteShareResultSet(object):
+ def __init__(self, ip_address_or_dns_name, query):
+ self._ip_address_or_dns_name = ip_address_or_dns_name
+ 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):
+ try:
+ metadata_list_complete = webdavmanager.get_remote_webdav_share_metadata(self._ip_address_or_dns_name)
+ except Exception, e:
+ metadata_list_complete = []
+
+ 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.
@@ -434,9 +643,13 @@ def _get_file_metadata(path, stat, fetch_preview=True):
metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview)
if metadata:
if 'filesize' not in metadata:
- metadata['filesize'] = stat.st_size
+ if stat is not None:
+ metadata['filesize'] = stat.st_size
return metadata
+ if stat is None:
+ raise ValueError('File does not exist')
+
mime_type, uncertain_result_ = Gio.content_type_guess(filename=path,
data=None)
return {'uid': path,
@@ -457,10 +670,17 @@ def _get_file_metadata_from_json(dir_path, filename, fetch_preview):
If the metadata is corrupted we do remove it and the preview as well.
"""
+
+ # In case of nested mount-points, (eg. ~/Documents/in1/in2/in3.txt),
+ # "dir_path = ~/Documents/in1/in2"; while
+ # "metadata_dir_path = ~/Documents".
+ from jarabe.journal.journalactivity import get_mount_point
+ metadata_dir_path = get_mount_point()
+
metadata = None
- metadata_path = os.path.join(dir_path, JOURNAL_METADATA_DIR,
+ metadata_path = os.path.join(metadata_dir_path, JOURNAL_METADATA_DIR,
filename + '.metadata')
- preview_path = os.path.join(dir_path, JOURNAL_METADATA_DIR,
+ preview_path = os.path.join(metadata_dir_path, JOURNAL_METADATA_DIR,
filename + '.preview')
if not os.path.exists(metadata_path):
@@ -529,6 +749,9 @@ def find(query_, page_size):
if mount_points[0] == '/':
return DatastoreResultSet(query, page_size)
+ elif is_mount_point_for_locally_mounted_remote_share(mount_points[0]):
+ ip_address = extract_ip_address_or_dns_name_from_locally_mounted_remote_share_path(mount_points[0])
+ return RemoteShareResultSet(ip_address, query)
else:
return InplaceResultSet(query, page_size, mount_points[0])
@@ -546,8 +769,12 @@ def _get_mount_point(path):
def get(object_id):
"""Returns the metadata for an object
"""
- if os.path.exists(object_id):
- stat = os.stat(object_id)
+ if (object_id[0] == '/'):
+ if os.path.exists(object_id):
+ stat = os.stat(object_id)
+ else:
+ stat = None
+
metadata = _get_file_metadata(object_id, stat)
metadata['mountpoint'] = _get_mount_point(object_id)
else:
@@ -620,7 +847,21 @@ def delete(object_id):
def copy(metadata, mount_point):
"""Copies an object to another mount point
"""
+ # In all cases (except one), "copy" means the actual duplication of
+ # the content.
+ # Only in case of remote downloading, the content is first copied
+ # to "/tmp" folder. In those cases, copying would refer to a mere
+ # renaming.
+ transfer_ownership = False
+
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
+ if is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ transfer_ownership = True
+
metadata = get(metadata['uid'])
+
if mount_point == '/' and metadata['icon-color'] == '#000000,#ffffff':
client = GConf.Client.get_default()
metadata['icon-color'] = client.get_string('/desktop/sugar/user/color')
@@ -631,7 +872,7 @@ def copy(metadata, mount_point):
metadata['mountpoint'] = mount_point
del metadata['uid']
- return write(metadata, file_path, transfer_ownership=False)
+ return write(metadata, file_path, transfer_ownership=transfer_ownership)
def write(metadata, file_path='', update_mtime=True, transfer_ownership=True):
@@ -653,22 +894,113 @@ def write(metadata, file_path='', update_mtime=True, transfer_ownership=True):
object_id = _get_datastore().create(dbus.Dictionary(metadata),
file_path,
transfer_ownership)
+ elif metadata.get('mountpoint', '/') == (WEBDAV_MOUNT_POINT + SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME):
+ filename = metadata['title']
+
+ ip_address_or_dns_name = SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME
+ webdavmanager.get_remote_webdav_share_metadata(ip_address_or_dns_name)
+
+ data_webdav_manager = \
+ webdavmanager.get_data_webdav_manager(ip_address_or_dns_name)
+ metadata_webdav_manager = \
+ webdavmanager.get_metadata_webdav_manager(ip_address_or_dns_name)
+
+
+ # If we get a resource by this name, there is already an entry
+ # on the server with this name; we do not want to do any
+ # overwrites.
+ data_resource = webdavmanager.get_resource_by_resource_key(data_webdav_manager,
+ '/webdav/' + filename)
+ metadata_resource = webdavmanager.get_resource_by_resource_key(metadata_webdav_manager,
+ '/webdav/.Sugar-Metadata/' + filename + '.metadata')
+ if (data_resource is not None) or (metadata_resource is not None):
+ raise Exception(_('Entry already present on the server with '
+ 'this name. Try again after renaming.'))
+
+ # No entry for this name present.
+ # So, first write the metadata- and preview-file to temporary
+ # locations.
+ metadata_file_path, preview_file_path = \
+ _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ filename)
+
+ # Finally,
+ # Upload the data file.
+ webdavmanager.add_resource_by_resource_key(data_webdav_manager,
+ filename,
+ file_path)
+
+ # Upload the preview file.
+ if preview_file_path is not None:
+ webdavmanager.add_resource_by_resource_key(metadata_webdav_manager,
+ filename + '.preview',
+ preview_file_path)
+
+ # Upload the metadata file.
+ #
+ # Note that this needs to be the last step. If there was any
+ # error uploading the data- or the preview-file, control would
+ # not reach here.
+ #
+ # In other words, the control reaches here only if the data-
+ # and the preview- files have been uploaded. Finally, IF this
+ # file is successfully uploaded, we have the guarantee that all
+ # files for a particular journal entry are in place.
+ webdavmanager.add_resource_by_resource_key(metadata_webdav_manager,
+ filename + '.metadata',
+ metadata_file_path)
+
+
+ object_id = 'doesn\'t matter'
+
else:
- object_id = _write_entry_on_external_device(metadata, file_path)
+ object_id = _write_entry_on_external_device(metadata,
+ file_path,
+ transfer_ownership)
return object_id
-def _rename_entry_on_external_device(file_path, destination_path,
- metadata_dir_path):
+def make_file_fully_permissible(file_path):
+ fd = os.open(file_path, os.O_RDONLY)
+ os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG |stat.S_IRWXO)
+ os.close(fd)
+
+
+def _rename_entry_on_external_device(file_path, destination_path):
"""Rename an entry with the associated metadata on an external device."""
old_file_path = file_path
if old_file_path != destination_path:
- os.rename(file_path, destination_path)
+ # Strangely, "os.rename" works fine on sugar-jhbuild, but fails
+ # on XOs, wih the OSError 13 ("invalid cross-device link"). So,
+ # using the system call "mv".
+ os.system('mv "%s" "%s"' % (file_path, destination_path))
+ make_file_fully_permissible(destination_path)
+
+
+ # In renaming, we want to delete the metadata-, and preview-
+ # files of the current mount-point, and not the destination
+ # mount-point.
+ # But we also need to ensure that the directory of
+ # 'old_file_path' and 'destination_path' are not same.
+ if os.path.dirname(old_file_path) == os.path.dirname(destination_path):
+ return
+
+ from jarabe.journal.journalactivity import get_mount_point
+
+ # Also, as a special case, the metadata- and preview-files of
+ # the remote-shares must never be deleted. For them, only the
+ # data-file needs to be moved.
+ if is_mount_point_for_locally_mounted_remote_share(get_mount_point()):
+ return
+
+
+ source_metadata_dir_path = get_mount_point() + '/.Sugar-Metadata'
+
old_fname = os.path.basename(file_path)
- old_files = [os.path.join(metadata_dir_path,
+ old_files = [os.path.join(source_metadata_dir_path,
old_fname + '.metadata'),
- os.path.join(metadata_dir_path,
+ os.path.join(source_metadata_dir_path,
old_fname + '.preview')]
for ofile in old_files:
if os.path.exists(ofile):
@@ -679,41 +1011,32 @@ def _rename_entry_on_external_device(file_path, destination_path,
'for file=%s', ofile, old_fname)
-def _write_entry_on_external_device(metadata, file_path):
- """Create and update an entry copied from the
- DS to an external storage device.
-
- Besides copying the associated file a file for the preview
- and one for the metadata are stored in the hidden directory
- .Sugar-Metadata.
-
- This function handles renames of an entry on the
- external device and avoids name collisions. Renames are
- handled failsafe.
-
- """
- if 'uid' in metadata and os.path.exists(metadata['uid']):
- file_path = metadata['uid']
+def _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ file_name):
+ metadata_copy = metadata.copy()
+ metadata_copy.pop('mountpoint', None)
+ metadata_copy.pop('uid', None)
- if not file_path or not os.path.exists(file_path):
- raise ValueError('Entries without a file cannot be copied to '
- 'removable devices')
- if not metadata.get('title'):
- metadata['title'] = _('Untitled')
- file_name = get_file_name(metadata['title'], metadata['mime_type'])
+ # For copying to School-Server, we need to retain this property.
+ # Else wise, I have no idea why this property is being removed !!
+ if (is_mount_point_for_locally_mounted_remote_share(metadata.get('mountpoint', '/')) == False) and \
+ (metadata.get('mountpoint', '/') != LOCAL_SHARES_MOUNT_POINT):
+ metadata_copy.pop('filesize', None)
- destination_path = os.path.join(metadata['mountpoint'], file_name)
- if destination_path != file_path:
- file_name = get_unique_file_name(metadata['mountpoint'], file_name)
- destination_path = os.path.join(metadata['mountpoint'], file_name)
- clean_name, extension_ = os.path.splitext(file_name)
- metadata['title'] = clean_name
+ # For journal case, there is the special treatment.
+ if metadata.get('mountpoint', '/') == '/':
+ if metadata.get('uid', ''):
+ object_id = _get_datastore().update(metadata['uid'],
+ dbus.Dictionary(metadata),
+ '',
+ False)
+ else:
+ object_id = _get_datastore().create(dbus.Dictionary(metadata),
+ '',
+ False)
+ return
- metadata_copy = metadata.copy()
- metadata_copy.pop('mountpoint', None)
- metadata_copy.pop('uid', None)
- metadata_copy.pop('filesize', None)
metadata_dir_path = os.path.join(metadata['mountpoint'],
JOURNAL_METADATA_DIR)
@@ -742,11 +1065,64 @@ def _write_entry_on_external_device(metadata, file_path):
os.close(fh)
os.rename(fn, os.path.join(metadata_dir_path, preview_fname))
- if not os.path.dirname(destination_path) == os.path.dirname(file_path):
- shutil.copy(file_path, destination_path)
+ metadata_destination_path = os.path.join(metadata_dir_path, file_name + '.metadata')
+ make_file_fully_permissible(metadata_destination_path)
+ if preview:
+ preview_destination_path = os.path.join(metadata_dir_path, preview_fname)
+ make_file_fully_permissible(preview_destination_path)
+ else:
+ preview_destination_path = None
+
+ return (metadata_destination_path, preview_destination_path)
+
+
+def update_only_metadata_and_preview_files_and_return_file_paths(metadata):
+ file_name = get_file_name(metadata['title'], metadata['mime_type'])
+ _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ file_name)
+
+
+def _write_entry_on_external_device(metadata, file_path,
+ transfer_ownership):
+ """Create and update an entry copied from the
+ DS to an external storage device.
+
+ Besides copying the associated file a file for the preview
+ and one for the metadata are stored in the hidden directory
+ .Sugar-Metadata.
+
+ This function handles renames of an entry on the
+ external device and avoids name collisions. Renames are
+ handled failsafe.
+
+ """
+ if 'uid' in metadata and os.path.exists(metadata['uid']):
+ file_path = metadata['uid']
+
+ if not file_path or not os.path.exists(file_path):
+ raise ValueError('Entries without a file cannot be copied to '
+ 'removable devices')
+
+ if not metadata.get('title'):
+ metadata['title'] = _('Untitled')
+ file_name = get_file_name(metadata['title'], metadata['mime_type'])
+
+ destination_path = os.path.join(metadata['mountpoint'], file_name)
+ if destination_path != file_path:
+ file_name = get_unique_file_name(metadata['mountpoint'], file_name)
+ destination_path = os.path.join(metadata['mountpoint'], file_name)
+ clean_name, extension_ = os.path.splitext(file_name)
+ metadata['title'] = clean_name
+
+ _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ file_name)
+
+ if (os.path.dirname(destination_path) == os.path.dirname(file_path)) or \
+ (transfer_ownership == True):
+ _rename_entry_on_external_device(file_path, destination_path)
else:
- _rename_entry_on_external_device(file_path, destination_path,
- metadata_dir_path)
+ shutil.copy(file_path, destination_path)
+ make_file_fully_permissible(destination_path)
object_id = destination_path
created.send(None, object_id=object_id)
@@ -796,7 +1172,17 @@ def is_editable(metadata):
if metadata.get('mountpoint', '/') == '/':
return True
else:
- return os.access(metadata['mountpoint'], os.W_OK)
+ # sl#3605: Instead of relying on mountpoint property being
+ # present in the metadata, use journalactivity api.
+ # This would work seamlessly, as "Details View' is
+ # called, upon an entry in the context of a singular
+ # mount-point.
+ from jarabe.journal.journalactivity import get_mount_point
+ mount_point = get_mount_point()
+
+ if is_mount_point_for_locally_mounted_remote_share(mount_point):
+ return False
+ return os.access(mount_point, os.W_OK)
def get_documents_path():
diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py
index d860b0d..45e72af 100644
--- a/src/jarabe/journal/objectchooser.py
+++ b/src/jarabe/journal/objectchooser.py
@@ -16,15 +16,20 @@
from gettext import gettext as _
import logging
+import os
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Wnck
+from sugar3 import env
+
from sugar3.graphics import style
from sugar3.graphics.toolbutton import ToolButton
+from sugar3.datastore import datastore
+
from jarabe.journal.listview import BaseListView
from jarabe.journal.listmodel import ListModel
from jarabe.journal.journaltoolbox import MainToolbox
@@ -47,6 +52,7 @@ class ObjectChooser(Gtk.Window):
self.set_border_width(style.LINE_WIDTH)
self._selected_object_id = None
+ self._callback = None
self.add_events(Gdk.EventMask.VISIBILITY_NOTIFY_MASK)
self.connect('visibility-notify-event',
@@ -111,6 +117,15 @@ class ObjectChooser(Gtk.Window):
self._selected_object_id = uid
self.emit('response', Gtk.ResponseType.ACCEPT)
+ if self._callback is not None:
+ self._callback(self._selected_object_id)
+
+ def get_selected_object(self):
+ if self._selected_object_id is None:
+ return None
+ else:
+ return datastore.get(self._selected_object_id)
+
def __delete_event_cb(self, chooser, event):
self.emit('response', Gtk.ResponseType.DELETE_EVENT)
@@ -121,6 +136,8 @@ class ObjectChooser(Gtk.Window):
def __close_button_clicked_cb(self, button):
self.emit('response', Gtk.ResponseType.DELETE_EVENT)
+ if self._callback is not None:
+ self._callback(self._selected_object_id)
def get_selected_object_id(self):
return self._selected_object_id
@@ -140,6 +157,9 @@ class ObjectChooser(Gtk.Window):
def __clear_clicked_cb(self, list_view):
self._toolbar.clear_query()
+ def _set_callback(self, callback):
+ self._callback = callback
+
class TitleBox(VolumesToolbar):
__gtype_name__ = 'TitleBox'
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 43f9905..f770d55 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -15,6 +15,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+from gettext import ngettext
import logging
import os
@@ -38,6 +39,61 @@ from jarabe.model import mimeregistry
from jarabe.journal import misc
from jarabe.journal import model
from jarabe.journal import journalwindow
+from jarabe.journal import webdavmanager
+from jarabe.journal.journalwindow import freeze_ui, \
+ unfreeze_ui, \
+ show_normal_cursor, \
+ show_waiting_cursor
+
+from webdav.Connection import WebdavError
+
+
+friends_model = friends.get_model()
+
+_copy_menu_helper = None
+_current_action_item = None
+
+USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE = _('Cannot perform request. Connection failed.')
+
+
+class PassphraseDialog(Gtk.Dialog):
+ def __init__(self, callback, metadata):
+ Gtk.Dialog.__init__(self, flags=Gtk.DialogFlags.MODAL)
+ self._callback = callback
+ self._metadata = metadata
+ self.set_title(_('Passphrase required'))
+
+ # TRANS: Please do not translate the '%s'.
+ label_text = _('Please enter the passphrase for "%s"' % metadata['title'])
+ label = Gtk.Label(label_text)
+ self.vbox.pack_start(label, True, True, 0)
+
+ self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
+ self.set_default_response(Gtk.ResponseType.OK)
+ self.add_key_entry()
+
+ self.connect('response', self._key_dialog_response_cb)
+ self.show_all()
+
+ def add_key_entry(self):
+ self._entry = Gtk.Entry()
+ self._entry.connect('activate', self._entry_activate_cb)
+ self.vbox.pack_start(self._entry, True, True, 0)
+ self.vbox.set_spacing(6)
+ self.vbox.show_all()
+
+ self._entry.grab_focus()
+
+ def _entry_activate_cb(self, entry):
+ self.response(Gtk.ResponseType.OK)
+
+ def get_response_object(self):
+ return self._response
+
+ def _key_dialog_response_cb(self, widget, response_id):
+ self.hide()
+ GObject.idle_add(self._callback, self._metadata,
+ self._entry.get_text())
class ObjectPalette(Palette):
@@ -68,6 +124,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')
@@ -77,10 +136,14 @@ 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)
@@ -99,6 +162,15 @@ class ObjectPalette(Palette):
self.menu.append(menu_item)
menu_item.show()
copy_menu = CopyMenu(metadata)
+ copy_menu_helper = get_copy_menu_helper()
+
+ metadata_list = []
+ metadata_list.append(metadata)
+ copy_menu_helper.insert_copy_to_menu_items(copy_menu,
+ metadata_list,
+ False,
+ False,
+ False)
copy_menu.connect('volume-error', self.__volume_error_cb)
menu_item.set_submenu(copy_menu)
@@ -112,6 +184,8 @@ class ObjectPalette(Palette):
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()
@@ -127,6 +201,8 @@ class ObjectPalette(Palette):
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()
@@ -197,123 +273,798 @@ class CopyMenu(Gtk.Menu):
__gsignals__ = {
'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
- ([str, str])),
+ ([str, str])),
}
def __init__(self, metadata):
Gtk.Menu.__init__(self)
- self._metadata = metadata
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=Gtk.IconSize.MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- self.append(clipboard_menu)
- clipboard_menu.show()
+class ActionItem(GObject.GObject):
+ """
+ This class implements the course of actions that happens when clicking
+ upon an Action-Item (eg. Batch-Copy-Toolbar-button;
+ Actual-Batch-Copy-To-Journal-button;
+ Actual-Batch-Copy-To-Documents-button;
+ Actual-Batch-Copy-To-Mounted-Drive-button;
+ Actual-Batch-Copy-To-Clipboard-button;
+ Single-Copy-To-Journal-button;
+ Single-Copy-To-Documents-button;
+ Single-Copy-To-Mounted-Drive-button;
+ Single-Copy-To-Clipboard-button;
+ Batch-Erase-Button;
+ Select-None-Toolbar-button;
+ Select-All-Toolbar-button
+ """
+ __gtype_name__ = 'JournalActionItem'
+
+ def __init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ auto_deselect_source_entries,
+ need_to_popup_options,
+ operate_on_deselected_entries,
+ show_not_completed_ops_info):
+ GObject.GObject.__init__(self)
+
+ self._label = label
+
+ # Make a copy.
+ self._immutable_metadata_list = []
+ for metadata in metadata_list:
+ self._immutable_metadata_list.append(metadata)
+
+ self._metadata_list = metadata_list
+ self._show_progress_info_alert = show_progress_info_alert
+ self._batch_mode = batch_mode
+ self._auto_deselect_source_entries = \
+ auto_deselect_source_entries
+ self._need_to_popup_options = \
+ need_to_popup_options
+ self._operate_on_deselected_entries = \
+ operate_on_deselected_entries
+ self._show_not_completed_ops_info = \
+ show_not_completed_ops_info
+
+ actionable_signal = self._get_actionable_signal()
+
+ if need_to_popup_options:
+ self.connect(actionable_signal, self._pre_fill_and_pop_up_options)
+ else:
+ if show_editing_alert:
+ self.connect(actionable_signal, self._show_editing_alert)
+ else:
+ self.connect(actionable_signal,
+ self._pre_operate_per_action,
+ Gtk.ResponseType.OK)
+
+ def _get_actionable_signal(self):
+ """
+ Some widgets like 'buttons' have 'clicked' as actionable signal;
+ some like 'menuitems' have 'activate' as actionable signal.
+ """
+
+ raise NotImplementedError
+
+ def _pre_fill_and_pop_up_options(self, widget_clicked):
+ self._set_current_action_item_widget()
+ self._fill_and_pop_up_options(widget_clicked)
+
+ def _fill_and_pop_up_options(self, widget_clicked):
+ """
+ Eg. Batch-Copy-Toolbar-button does not do anything by itself
+ useful; but rather pops-up the actual 'copy-to' options.
+ """
+
+ raise NotImplementedError
+
+ def _show_editing_alert(self, widget_clicked):
+ """
+ Upon clicking the actual operation button (eg.
+ Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT
+ Batch-Copy-Toolbar-button, since it does not do anything
+ actually useful, but only pops-up the actual 'copy-to' options.
+ """
+
+ freeze_ui()
+ GObject.idle_add(self.__show_editing_alert_after_freezing_ui,
+ widget_clicked)
+
+ def __show_editing_alert_after_freezing_ui(self, widget_clicked):
+ self._set_current_action_item_widget()
+
+ alert_parameters = self._get_editing_alert_parameters()
+ title = alert_parameters[0]
+ message = alert_parameters[1]
+ operation = alert_parameters[2]
+
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_confirmation_alert(title, message,
+ self._pre_operate_per_action,
+ None)
+
+ def _get_editing_alert_parameters(self):
+ """
+ Get the alert parameters for widgets that can show editing
+ alert.
+ """
+
+ self._metadata_list = self._get_metadata_list()
+ entries_len = len(self._metadata_list)
+
+ title = self._get_editing_alert_title()
+ message = self._get_editing_alert_message(entries_len)
+ operation = self._get_editing_alert_operation()
+
+ return (title, message, operation)
+
+ def _get_list_model_len(self):
+ """
+ Get the total length of the model under view.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return len(journal.get_list_view().get_model())
+
+ def _get_metadata_list(self):
+ """
+ For batch-mode, get the metadata list, according to button-type.
+ For eg, Select-All-Toolbar-button operates on non-selected entries;
+ while othere operate on selected-entries.
+
+ For single-mode, simply copy from the
+ "immutable_metadata_list".
+ """
+
+ if self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if self._operate_on_deselected_entries:
+ metadata_list = journal.get_metadata_list(False)
+ else:
+ metadata_list = journal.get_metadata_list(True)
- if self._metadata['mountpoint'] != '/':
- client = GConf.Client.get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=Gtk.IconSize.MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- self.append(journal_menu)
- journal_menu.show()
+ # Make a backup copy, of this metadata_list.
+ self._immutable_metadata_list = []
+ for metadata in metadata_list:
+ self._immutable_metadata_list.append(metadata)
- volume_monitor = Gio.VolumeMonitor.get()
- icon_theme = Gtk.IconTheme.get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=Gtk.IconSize.MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- self.append(volume_menu)
- volume_menu.show()
+ return metadata_list
+ else:
+ metadata_list = []
+ for metadata in self._immutable_metadata_list:
+ metadata_list.append(metadata)
+ return metadata_list
+
+ def _get_editing_alert_title(self):
+ raise NotImplementedError
+
+ def _get_editing_alert_message(self, entries_len):
+ raise NotImplementedError
+
+ def _get_editing_alert_operation(self):
+ raise NotImplementedError
+
+ def _is_metadata_list_empty(self):
+ return (self._metadata_list is None) or \
+ (len(self._metadata_list) == 0)
+
+ def _set_current_action_item_widget(self):
+ """
+ Only set this, if this widget achieves some effective action.
+ """
+ if not self._need_to_popup_options:
+ global _current_action_item
+ _current_action_item = self
+
+ def _pre_operate_per_action(self, obj, response_id):
+ """
+ This is the stage, just before the FIRST metadata gets into its
+ processing cycle.
+ """
+ freeze_ui()
+ GObject.idle_add(self._pre_operate_per_action_after_done_ui_freezing,
+ obj, response_id)
+
+ def _pre_operate_per_action_after_done_ui_freezing(self, obj,
+ response_id):
+ self._set_current_action_item_widget()
+
+ self._continue_operation = True
+
+ # If the user chose to cancel the operation from the onset,
+ # simply proceeed to the last.
+ if response_id == Gtk.ResponseType.CANCEL:
+ unfreeze_ui()
+
+ self._cancel_further_batch_operation_items()
+ self._post_operate_per_action()
+ return
- def __volume_error_cb(self, menu_item, message, severity):
- self.emit('volume-error', message, severity)
+ self._skip_all = False
+ # Also, get the initial length of the model.
+ self._model_len = self._get_list_model_len()
-class VolumeMenu(MenuItem):
- __gtype_name__ = 'JournalVolumeMenu'
+ # Speed Optimisation:
+ # ===================
+ # If the metadata-list is empty, fetch it;
+ # else we have already fetched it, when we showed the
+ # "editing-alert".
+ if len(self._metadata_list) == 0:
+ self._metadata_list = self._get_metadata_list()
- __gsignals__ = {
- 'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
- ([str, str])),
- }
+ # Set the initial length of metadata-list.
+ self._metadata_list_initial_len = len(self._metadata_list)
- def __init__(self, metadata, label, mount_point):
- MenuItem.__init__(self, label)
- self._metadata = metadata
- self.connect('activate', self.__copy_to_volume_cb, mount_point)
+ self._metadata_processed = 0
- def __copy_to_volume_cb(self, menu_item, mount_point):
- file_path = model.get_file(self._metadata['uid'])
+ # Next, proceed with the metadata
+ self._pre_operate_per_metadata_per_action()
+
+ def _pre_operate_per_metadata_per_action(self):
+ """
+ This is the stage, just before EVERY metadata gets into doing
+ its actual work.
+ """
+
+ show_waiting_cursor()
+ GObject.idle_add(self.__pre_operate_per_metadata_per_action_after_freezing_ui)
+
+ def __pre_operate_per_metadata_per_action_after_freezing_ui(self):
+ from jarabe.journal.journalactivity import get_journal
+
+ # If there is still some metadata left, proceed with the
+ # metadata operation.
+ # Else, proceed to post-operations.
+ if len(self._metadata_list) > 0:
+ metadata = self._metadata_list.pop(0)
+
+ # If info-alert needs to be shown, show the alert, and
+ # arrange for actual operation.
+ # Else, proceed to actual operation directly.
+ if self._show_progress_info_alert:
+ current_len = len(self._metadata_list)
+
+ # TRANS: Do not translate the two %d, and the %s.
+ info_alert_message = _(' %d of %d : %s') % (
+ self._metadata_list_initial_len - current_len,
+ self._metadata_list_initial_len, metadata['title'])
+
+ get_journal().update_info_alert(self._get_info_alert_title(),
+ info_alert_message)
+
+ # Call the core-function !!
+ GObject.idle_add(self._operate_per_metadata_per_action, metadata)
+ else:
+ self._post_operate_per_action()
+
+ def _get_info_alert_title(self):
+ raise NotImplementedError
+
+ def _operate_per_metadata_per_action(self, metadata):
+ """
+ This is just a code-convenient-function, which allows
+ runtime-overriding. It just delegates to the actual
+ "self._operate" method, the actual which is determined at
+ runtime.
+ """
+
+ if self._continue_operation is False:
+ # Jump directly to the post-operation
+ self._post_operate_per_metadata_per_action(metadata)
+ else:
+ # Pass the callback for the post-operation-for-metadata. This
+ # will ensure that async-operations on the metadata are taken
+ # care of.
+ if self._operate(metadata) is False:
+ return
+ else:
+ self._metadata_processed = self._metadata_processed + 1
+
+ def _operate(self, metadata):
+ """
+ Actual, core, productive stage for EVERY metadata.
+ """
+ raise NotImplementedError
+
+ def _post_operate_per_metadata_per_action(self, metadata,
+ response_id=None):
+ """
+ This is the stage, just after EVERY metadata has been
+ processed.
+ """
+ self._hide_info_widget_for_single_mode()
+
+ # Toggle the corresponding checkbox - but only for batch-mode.
+ if self._batch_mode and self._auto_deselect_source_entries:
+ from jarabe.journal.journalactivity import get_journal
+ list_view = get_journal().get_list_view()
+
+ list_view.do_ui_select_change(metadata)
+ list_view.do_backend_select_change(metadata)
+
+ # Call the next ...
+ self._pre_operate_per_metadata_per_action()
+
+ def _post_operate_per_action(self):
+ """
+ This is the stage, just after the LAST metadata has been
+ processed.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal_toolbar_box = journal.get_toolbar_box()
+
+ if self._batch_mode and (not self._auto_deselect_source_entries):
+ journal_toolbar_box.display_already_selected_entries_status()
+
+ self._process_switching_mode(None, False)
+
+ unfreeze_ui()
+
+ # Set the "_current_action_item" to None.
+ global _current_action_item
+ _current_action_item = None
+
+ def _process_switching_mode(self, metadata, ok_clicked=False):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ # Necessary to do this, when the alert needs to be hidden,
+ # WITHOUT user-intervention.
+ journal.hide_alert()
+
+ def _refresh(self):
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().get_list_view().refresh()
+
+ def _handle_single_mode_notification(self, message, severity):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal._show_alert(message, severity)
+ self._hide_info_widget_for_single_mode()
+
+ def _hide_info_widget_for_single_mode(self):
+ if (not self._batch_mode):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.get_toolbar_box().hide_info_widget()
+
+ def _unhide_info_widget_for_single_mode(self):
+ if not self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_progress(0)
+
+ def _handle_error_alert(self, error_message, metadata):
+ """
+ This handles any error scenarios. Examples are of entries that
+ display the message "Entries without a file cannot be copied."
+ This is kind of controller-functionl the model-function is
+ "self._set_error_info_alert".
+ """
+
+ if self._skip_all:
+ self._post_operate_per_metadata_per_action(metadata)
+ else:
+ self._set_error_info_alert(error_message, metadata)
+
+ def _set_error_info_alert(self, error_message, metadata):
+ """
+ This method displays the error alert.
+ """
+
+ current_len = len(self._metadata_list)
+
+ # Only show the alert, if allowed to.
+ if self._show_not_completed_ops_info:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_confirmation_alert(_('Error'),
+ error_message,
+ self._process_error_skipping,
+ metadata)
+ else:
+ self._process_error_skipping(metadata, gtk.RESPONSE_OK)
+
+ def _process_error_skipping(self, metadata, response_id):
+ # This sets up the decision, as to whether continue operations
+ # with the rest of the metadata.
+ if response_id == Gtk.ResponseType.CANCEL:
+ self._cancel_further_batch_operation_items()
+
+ self._post_operate_per_metadata_per_action(metadata)
+
+ def _cancel_further_batch_operation_items(self):
+ self._continue_operation = False
+
+ # Optimization:
+ # Clear the metadata-list as well.
+ # This would prevent the unnecessary traversing of the
+ # remaining checkboxes-corresponding-to-remaining-metadata (of
+ # course without doing any effective action).
+ self._metadata_list = []
+
+ 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_or_dns_name = \
+ model.extract_ip_address_or_dns_name_from_locally_mounted_remote_share_path(file_path)
+
+ data_webdav_manager = \
+ webdavmanager.get_data_webdav_manager(ip_address_or_dns_name)
+ metadata_webdav_manager = \
+ webdavmanager.get_metadata_webdav_manager(ip_address_or_dns_name)
+
+ # Download the preview file, if it exists.
+ preview_resource = \
+ webdavmanager.get_resource_by_resource_key(metadata_webdav_manager,
+ '/webdav/.Sugar-Metadata/' + filename + '.preview')
+ preview_path = os.path.dirname(file_path) + '/.Sugar-Metadata/'+ filename + '.preview'
+
+ if preview_resource is not None:
+ try:
+ preview_resource.downloadFile(preview_path,
+ show_progress=False,
+ filesize=0)
+ except (WebdavError, socket.error), e:
+ error_message = USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ logging.warn(error_message)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message,
+ _('Error'))
+ return False
+
+ # If we manage to reach here, download the data file.
+ data_resource = \
+ webdavmanager.get_resource_by_resource_key(data_webdav_manager,
+ '/webdav/'+ filename)
+ try:
+ data_resource.downloadFile(metadata['uid'],
+ show_progress=True,
+ filesize=int(metadata['filesize']))
+ return True
+ except (WebdavError, socket.error), e:
+ # Delete the downloaded preview file, if it exists.
+ if os.path.exists(preview_path):
+ os.unlink(preview_path)
+
+ error_message = USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ logging.warn(error_message)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(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.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+ error_message = _('Entries without a file cannot be copied.')
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Warning'))
+ return False
+ else:
+ return True
+
+ def _metadata_copy_valid(self, metadata, mount_point):
+ self._set_bundle_installation_allowed(False)
try:
- model.copy(self._metadata, mount_point)
- except IOError, e:
- logging.exception('Error while copying the entry. %s', e.strerror)
- self.emit('volume-error',
- _('Error while copying the entry. %s') % e.strerror,
- _('Error'))
+ model.copy(metadata, mount_point)
+ return True
+ except Exception, e:
+ logging.exception(e)
+ error_message = _('Error while copying the entry. %s') % e
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Error'))
+ return False
+ finally:
+ self._set_bundle_installation_allowed(True)
+ def _metadata_write_valid(self, metadata):
+ operation = self._get_info_alert_title()
+ self._set_bundle_installation_allowed(False)
-class ClipboardMenu(MenuItem):
- __gtype_name__ = 'JournalClipboardMenu'
+ try:
+ model.update_only_metadata_and_preview_files_and_return_file_paths(metadata)
+ return True
+ except Exception, e:
+ logging.exception('Error while writing the metadata. %s', e)
+ error_message = _('Error occurred while %s : %s.') % \
+ (operation, e,)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Error'))
+ return False
+ finally:
+ self._set_bundle_installation_allowed(True)
- __gsignals__ = {
- 'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
- ([str, str])),
- }
+ def _set_bundle_installation_allowed(self, allowed):
+ """
+ This method serves only as a "delegating" method.
+ This has been done to aid easy configurability.
+ """
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
- def __init__(self, metadata):
- MenuItem.__init__(self, _('Clipboard'))
+ if self._batch_mode:
+ journal.set_bundle_installation_allowed(allowed)
- self._temp_file_path = None
- self._metadata = metadata
- self.connect('activate', self.__copy_to_clipboard_cb)
+ def get_number_of_entries_to_operate_upon(self):
+ return len(self._immutable_metadata_list)
- def __copy_to_clipboard_cb(self, menu_item):
- file_path = model.get_file(self._metadata['uid'])
- if not file_path or not os.path.exists(file_path):
- logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+
+class BaseCopyMenuItem(MenuItem, ActionItem):
+ __gtype_name__ = 'JournalBaseCopyMenuItem'
+
+ __gsignals__ = {
+ 'volume-error': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str, str])),
+ }
+
+ def __init__(self, metadata_list, label, show_editing_alert,
+ show_progress_info_alert, batch_mode, mount_point):
+ MenuItem.__init__(self, label)
+ ActionItem.__init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ auto_deselect_source_entries=False,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ show_not_completed_ops_info=True)
+ self._mount_point = mount_point
+
+ def get_mount_point(self):
+ return self._mount_point
+
+ def _get_actionable_signal(self):
+ return 'activate'
+
+ def _get_editing_alert_title(self):
+ return _('Copy')
+
+ def _get_editing_alert_message(self, entries_len):
+ return ngettext('Do you want to copy %d entry to %s?',
+ 'Do you want to copy %d entries to %s?',
+ entries_len) % (entries_len, self._label)
+
+ def _get_editing_alert_operation(self):
+ return _('Copy')
+
+ def _get_info_alert_title(self):
+ return _('Copying')
+
+ def _operate(self, metadata):
+ from jarabe.journal.journalactivity import get_mount_point
+ if(model.is_mount_point_for_locally_mounted_remote_share(get_mount_point())) \
+ and (model.is_mount_point_for_school_server(get_mount_point()) == True):
+ PassphraseDialog(self._proceed_after_receiving_passphrase, metadata)
+ else:
+ self._proceed_with_copy(metadata)
+
+ def _proceed_after_receiving_passphrase(self, metadata, passphrase):
+ if metadata['passphrase'] != passphrase:
+ error_message = _('Passphrase does not match.')
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Error'))
+ return False
+ else:
+ self._unhide_info_widget_for_single_mode()
+ GObject.idle_add(self._proceed_with_copy, metadata)
+
+ def _proceed_with_copy(self, metadata):
+ return NotImplementedError
+
+ def _post_successful_copy(self, metadata, response_id=None):
+ from jarabe.journal.journalactivity import get_journal, \
+ get_mount_point
+
+ if model.is_mount_point_for_locally_mounted_remote_share(get_mount_point()):
+ successful_downloading_message = None
+
+ if model.is_mount_point_for_school_server(get_mount_point()) == True:
+ # TRANS: Do not translate the %s.
+ successful_downloading_message = \
+ _('Your file "%s" was correctly downloaded from the School Server.' % metadata['title'])
+ else:
+ # TRANS: Do not translate the %s.
+ successful_downloading_message = \
+ _('Your file "%s" was correctly downloaded from the Peer.' % metadata['title'])
+
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_error_alert(self._get_editing_alert_title(),
+ successful_downloading_message,
+ self._post_operate_per_metadata_per_action,
+ metadata)
+ else:
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class VolumeMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, label, mount_point,
+ show_editing_alert, show_progress_info_alert,
+ batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, label,
+ show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ mount_point)
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata, self._mount_point):
+ return False
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_successful_copy(metadata)
+
+
+class ClipboardMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Clipboard'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode, None)
+ self._temp_file_path_list = []
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
clipboard = Gtk.Clipboard()
clipboard.set_with_data([('text/uri-list', 0, 0)],
self.__clipboard_get_func_cb,
- self.__clipboard_clear_func_cb)
+ self.__clipboard_clear_func_cb,
+ metadata)
- def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info,
+ metadata):
# Get hold of a reference so the temp file doesn't get deleted
- self._temp_file_path = model.get_file(self._metadata['uid'])
+ self._temp_file_path = model.get_file(metadata['uid'])
logging.debug('__clipboard_get_func_cb %r', self._temp_file_path)
selection_data.set_uris(['file://' + self._temp_file_path])
- def __clipboard_clear_func_cb(self, clipboard, data):
+ def __clipboard_clear_func_cb(self, clipboard, metadata):
# Release and delete the temp file
self._temp_file_path = None
+ # This is async-operation; and this is the ending point.
+ self._post_successful_copy(metadata)
+
+
+class DocumentsMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode,
+ model.get_documents_path())
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata,
+ model.get_documents_path()):
+ return False
+
+ # This is sync-operation. Call the post-operation now.
+ self._post_successful_copy(metadata)
+
+
+class LocalSharesMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Local Shares'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode,
+ model.LOCAL_SHARES_MOUNT_POINT)
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ # Attach the filesize.
+ file_path = model.get_file(metadata['uid'])
+ metadata['filesize'] = os.stat(file_path).st_size
+
+ # Attach the current mount-point.
+ from jarabe.journal.journalactivity import get_mount_point
+ metadata['mountpoint'] = get_mount_point()
+
+ if not self._metadata_write_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata,
+ model.LOCAL_SHARES_MOUNT_POINT):
+ return False
+
+ # This is sync-operation. Call the post-operation now.
+ self._post_successful_copy(metadata)
+
+
+class SchoolServerMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('School Server'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode,
+ model.WEBDAV_MOUNT_POINT + model.SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME)
+
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ # If the entry is copyable, proceed with asking the
+ # upload-passphrase.
+ PassphraseDialog(self._proceed_after_receiving_passphrase, metadata)
+
+ def _proceed_after_receiving_passphrase(self, metadata, passphrase):
+ self._unhide_info_widget_for_single_mode()
+ GObject.idle_add(self._proceed_with_uploading, metadata,
+ passphrase)
+
+ def _proceed_with_uploading(self, metadata, passphrase):
+ #
+ # Attach the passphrase.
+ metadata['passphrase'] = passphrase
+
+ # Attach the filesize.
+ file_path = model.get_file(metadata['uid'])
+ metadata['filesize'] = os.stat(file_path).st_size
+
+ # Attach the current mount-point.
+ from jarabe.journal.journalactivity import get_mount_point, \
+ get_journal
+ metadata['mountpoint'] = get_mount_point()
+
+ # Attach the info of the uploader.
+ from jarabe.model.buddy import get_owner_instance
+ metadata['uploader-nick'] = get_owner_instance().props.nick
+ metadata['uploader-serial'] = misc.get_xo_serial()
+
+ if not self._metadata_write_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata,
+ model.WEBDAV_MOUNT_POINT + model.SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME):
+ return False
+
+ # TRANS: Do not translate the %s.
+ successful_uploading_message = \
+ _('Your file "%s" was correctly uploaded to the School Server.' % metadata['title'])
+ get_journal().update_error_alert(self._get_editing_alert_title(),
+ successful_uploading_message,
+ self._post_successful_copy,
+ metadata)
+
class FriendsMenu(Gtk.Menu):
__gtype_name__ = 'JournalFriendsMenu'
@@ -401,3 +1152,116 @@ class BuddyPalette(Palette):
icon=buddy_icon)
# TODO: Support actions on buddies, like make friend, invite, etc.
+
+
+class CopyMenuHelper(Gtk.Menu):
+ __gtype_name__ = 'JournalCopyMenuHelper'
+
+ __gsignals__ = {
+ 'volume-error': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str, str])),
+ }
+
+ def insert_copy_to_menu_items(self, menu, metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode):
+ self._metadata_list = metadata_list
+
+ clipboard_menu = ClipboardMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
+ icon_size=Gtk.IconSize.MENU))
+ clipboard_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(clipboard_menu)
+ clipboard_menu.show()
+
+ from jarabe.journal.journalactivity import get_mount_point
+
+ if get_mount_point() != model.get_documents_path():
+ documents_menu = DocumentsMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ documents_menu.set_image(Icon(icon_name='user-documents',
+ icon_size=Gtk.IconSize.MENU))
+ documents_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(documents_menu)
+ documents_menu.show()
+
+ if (model.is_school_server_present()) and \
+ (not model.is_mount_point_for_locally_mounted_remote_share(get_mount_point())):
+ documents_menu = SchoolServerMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ documents_menu.set_image(Icon(icon_name='school-server',
+ icon_size=Gtk.IconSize.MENU))
+ documents_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(documents_menu)
+ documents_menu.show()
+
+ if (model.is_peer_to_peer_sharing_available()) and \
+ (get_mount_point() != model.LOCAL_SHARES_MOUNT_POINT):
+ local_shares_menu = LocalSharesMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ local_shares_menu.set_image(Icon(icon_name='emblem-neighborhood-shared',
+ icon_size=Gtk.IconSize.MENU))
+ local_shares_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(local_shares_menu)
+ local_shares_menu.show()
+
+ if get_mount_point() != '/':
+ client = GConf.Client.get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ journal_menu = VolumeMenu(metadata_list, _('Journal'), '/',
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ journal_menu.set_image(Icon(icon_name='activity-journal',
+ xo_color=color,
+ icon_size=Gtk.IconSize.MENU))
+ journal_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(journal_menu)
+ journal_menu.show()
+
+ volume_monitor = Gio.VolumeMonitor.get()
+ icon_theme = Gtk.IconTheme.get_default()
+ for mount in volume_monitor.get_mounts():
+ if get_mount_point() == mount.get_root().get_path():
+ continue
+
+ volume_menu = VolumeMenu(metadata_list, mount.get_name(),
+ mount.get_root().get_path(),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ for name in mount.get_icon().props.names:
+ if icon_theme.has_icon(name):
+ volume_menu.set_image(Icon(icon_name=name,
+ icon_size=Gtk.IconSize.MENU))
+ break
+
+ volume_menu.connect('volume-error', self.__volume_error_cb)
+ menu.insert(volume_menu, -1)
+ volume_menu.show()
+
+ def __volume_error_cb(self, menu_item, message, severity):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal._volume_error_cb(menu_item, message, severity)
+
+
+def get_copy_menu_helper():
+ global _copy_menu_helper
+ if _copy_menu_helper is None:
+ _copy_menu_helper = CopyMenuHelper()
+ return _copy_menu_helper
+
+
+def get_current_action_item():
+ return _current_action_item
diff --git a/src/jarabe/journal/processdialog.py b/src/jarabe/journal/processdialog.py
new file mode 100644
index 0000000..d738303
--- /dev/null
+++ b/src/jarabe/journal/processdialog.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python
+# Copyright (C) 2010, Plan Ceibal <comunidad@plan.ceibal.edu.uy>
+# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import GConf
+
+import logging
+
+from gettext import gettext as _
+from sugar3.graphics import style
+from sugar3.graphics.icon import Icon
+from sugar3.graphics.xocolor import XoColor
+
+from jarabe.journal import misc
+from jarabe.model import shell
+from jarabe.model import processmanagement
+from jarabe.model.session import get_session_manager
+
+class ProcessDialog(Gtk.Window):
+
+ __gtype_name__ = 'SugarProcessDialog'
+
+ def __init__(self, process_script='', process_params=[], restart_after=True):
+
+ #FIXME: Workaround limitations of Sugar core modal handling
+ shell_model = shell.get_model()
+ shell_model.set_zoom_level(shell_model.ZOOM_HOME)
+
+ Gtk.Window.__init__(self)
+
+ self._process_script = processmanagement.find_and_absolutize(process_script)
+ self._process_params = process_params
+ self._restart_after = restart_after
+ self._start_message = _('Running')
+ self._failed_message = _('Failed')
+ self._finished_message = _('Finished')
+ self._prerequisite_message = ('Prerequisites were not met')
+
+ self.set_border_width(style.LINE_WIDTH)
+ width = Gdk.Screen.width()
+ height = Gdk.Screen.height()
+ self.set_size_request(width, height)
+ self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ self._colored_box = Gtk.EventBox()
+ self._colored_box.modify_bg(Gtk.StateType.NORMAL, Gdk.Color.parse("white")[1])
+ self._colored_box.show()
+
+ self._vbox = Gtk.VBox()
+ self._vbox.set_spacing(style.DEFAULT_SPACING)
+ self._vbox.set_border_width(style.GRID_CELL_SIZE)
+
+ self._colored_box.add(self._vbox)
+ self.add(self._colored_box)
+
+ self._setup_information()
+ self._setup_progress_bar()
+ self._setup_options()
+
+ self._vbox.show()
+
+ self.connect("realize", self.__realize_cb)
+
+ self._process_management = processmanagement.ProcessManagement()
+ self._process_management.connect('process-management-running', self._set_status_updated)
+ self._process_management.connect('process-management-started', self._set_status_started)
+ self._process_management.connect('process-management-finished', self._set_status_finished)
+ self._process_management.connect('process-management-failed', self._set_status_failed)
+
+ def _setup_information(self):
+ client = GConf.Client.get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+
+ self._icon = Icon(icon_name='activity-journal', pixel_size=style.XLARGE_ICON_SIZE, xo_color=color)
+ self._icon.show()
+
+ self._vbox.pack_start(self._icon, False, False, 0)
+
+ self._title = Gtk.Label()
+ self._title.modify_fg(Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color())
+ self._title.set_use_markup(True)
+ self._title.set_justify(Gtk.Justification.CENTER)
+ self._title.show()
+
+ self._vbox.pack_start(self._title, False, False, 0)
+
+ self._message = Gtk.Label()
+ self._message.modify_fg(Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color())
+ self._message.set_use_markup(True)
+ self._message.set_line_wrap(True)
+ self._message.set_justify(Gtk.Justification.CENTER)
+ self._message.show()
+
+ self._vbox.pack_start(self._message, True, True, 0)
+
+ def _setup_options(self):
+ hbox = Gtk.HBox(True, 3)
+ hbox.show()
+
+ icon = Icon(icon_name='dialog-ok')
+
+ self._start_button = Gtk.Button()
+ self._start_button.set_image(icon)
+ self._start_button.set_label(_('Start'))
+ self._start_button.connect('clicked', self.__start_cb)
+ self._start_button.show()
+
+ icon = Icon(icon_name='dialog-cancel')
+
+ self._close_button = Gtk.Button()
+ self._close_button.set_image(icon)
+ self._close_button.set_label(_('Close'))
+ self._close_button.connect('clicked', self.__close_cb)
+ self._close_button.show()
+
+ icon = Icon(icon_name='system-restart')
+
+ self._restart_button = Gtk.Button()
+ self._restart_button.set_image(icon)
+ self._restart_button.set_label(_('Restart'))
+ self._restart_button.connect('clicked', self.__restart_cb)
+ self._restart_button.hide()
+
+ hbox.add(self._start_button)
+ hbox.add(self._close_button)
+ hbox.add(self._restart_button)
+
+ halign = Gtk.Alignment(xalign=1, yalign=0, xscale=0, yscale=0)
+ halign.show()
+ halign.add(hbox)
+
+ self._vbox.pack_start(halign, False, False, 3)
+
+ def _setup_progress_bar(self):
+ alignment = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5)
+ alignment.show()
+
+ self._progress_bar = Gtk.ProgressBar()
+ self._progress_bar.hide()
+
+ alignment.add(self._progress_bar)
+ self._vbox.pack_start(alignment, False, False, 0)
+
+ def __realize_cb(self, widget):
+ self.get_window().set_accept_focus(True)
+
+ def __close_cb(self, button):
+ self.destroy()
+
+ def __start_cb(self, button):
+ if self._check_prerequisites():
+ self._process_management.do_process([self._process_script] + self._process_params)
+ else:
+ self._set_status_failed(self, error_message=self._prerequisite_message)
+
+ def __restart_cb(self, button):
+ session_manager = get_session_manager()
+ session_manager.logout()
+
+ def _check_prerequisites(self):
+ return True
+
+ def _set_status_started(self, model, data=None):
+ self._message.set_markup(self._start_message)
+
+ self._start_button.hide()
+ self._close_button.hide()
+
+ self._progress_bar.set_fraction(0.05)
+ self._progress_bar_handler = GObject.timeout_add(1000, self.__progress_bar_handler_cb)
+ self._progress_bar.show()
+
+ def __progress_bar_handler_cb(self):
+ self._progress_bar.pulse()
+ return True
+
+ def _set_status_updated(self, model, data):
+ pass
+
+ def _set_status_finished(self, model, data=None):
+ self._message.set_markup(self._finished_message)
+
+ self._progress_bar.hide()
+ self._start_button.hide()
+
+ if self._restart_after:
+ self._restart_button.show()
+ else:
+ self._close_button.show()
+
+ def _set_status_failed(self, model=None, error_message=''):
+ self._message.set_markup('%s %s' % (self._failed_message, error_message))
+
+ self._progress_bar.hide()
+ self._start_button.show()
+ self._close_button.show()
+ self._restart_button.hide()
+
+ logging.error(error_message)
+
+
+class VolumeBackupDialog(ProcessDialog):
+
+ def __init__(self, volume_path):
+ ProcessDialog.__init__(self, 'journal-backup-volume', \
+ [volume_path, misc.get_backup_identifier()], restart_after=False)
+
+ self._resetup_information(volume_path)
+
+ def _resetup_information(self, volume_path):
+ self._start_message = '%s %s. \n\n' % (_('Please wait, saving Journal content to'), volume_path) + \
+ '<big><b>%s</b></big>' % _('Do not remove the storage device!')
+
+ self._finished_message = _('The Journal content has been saved.')
+
+ self._title.set_markup('<big><b>%s</b></big>' % _('Backup'))
+
+ self._message.set_markup('%s %s' % (_('Journal content will be saved to'), volume_path))
+
+
+class VolumeRestoreDialog(ProcessDialog):
+
+ def __init__(self, volume_path):
+ ProcessDialog.__init__(self, 'journal-restore-volume', \
+ [volume_path, misc.get_backup_identifier()])
+
+ self._resetup_information(volume_path)
+
+ def _resetup_information(self, volume_path):
+ self._start_message = '%s %s. \n\n' % (_('Please wait, restoring Journal content from'), volume_path) + \
+ '<big><b>%s</b></big>' % _('Do not remove the storage device!')
+
+ self._finished_message = _('The Journal content has been restored.')
+
+ self._title.set_markup('<big><b>%s</b></big>' % _('Restore'))
+
+ self._message.set_markup('%s %s.\n\n' % (_('Journal content will be restored from'), volume_path) + \
+ '<big><b>%s</b> %s</big>' % (_('Warning:'), _('Current Journal content will be deleted!')))
+
+ self._prerequisite_message = _(', please close all the running activities.')
+
+ def _check_prerequisites(self):
+ return len(shell.get_model()) <= 2
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 1fc368e..1bf81bb 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -37,11 +37,14 @@ from sugar3.graphics.xocolor import XoColor
from sugar3 import env
from jarabe.journal import model
-from jarabe.view.palettes import VolumePalette
+from jarabe.view.palettes import JournalVolumePalette, RemoteSharePalette
_JOURNAL_0_METADATA_DIR = '.olpc.store'
+SHARE_TYPE_PEER = 1
+SHARE_TYPE_SCHOOL_SERVER = 2
+
def _get_id(document):
"""Get the ID for the document in the xapian database."""
@@ -193,6 +196,17 @@ class VolumesToolbar(Gtk.Toolbar):
def _set_up_volumes(self):
self._set_up_documents_button()
+ if model.is_peer_to_peer_sharing_available():
+ self._set_up_local_shares_button()
+
+ client = GConf.Client.get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+
+ if model.is_school_server_present():
+ self._add_remote_share_button(_('School-Server Shares'),
+ model.SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME,
+ color, SHARE_TYPE_SCHOOL_SERVER)
+
volume_monitor = Gio.VolumeMonitor.get()
self._mount_added_hid = volume_monitor.connect('mount-added',
self.__mount_added_cb)
@@ -202,12 +216,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()
@@ -217,6 +230,44 @@ 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_local_shares_button(self):
+ local_shares_path = model.LOCAL_SHARES_MOUNT_POINT
+ self._set_up_directory_button(local_shares_path,
+ 'emblem-neighborhood-shared',
+ _('Local Shares'))
+
+ def _add_remote_share_button(self, primary_text,
+ ip_address_or_dns_name, color,
+ share_type):
+ button = RemoteSharesButton(primary_text, ip_address_or_dns_name,
+ color, share_type)
+ button._share_type = share_type
+ button.props.group = self._volume_buttons[0]
+
+ show_unmount_option = None
+ if share_type == SHARE_TYPE_PEER:
+ show_unmount_option = True
+ else:
+ show_unmount_option = False
+ button.set_palette(RemoteSharePalette(primary_text,
+ ip_address_or_dns_name, button,
+ show_unmount_option))
+ 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()
+
+ return button
+
def __mount_added_cb(self, volume_monitor, mount):
self._add_button(mount)
@@ -247,10 +298,26 @@ 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:
+ button.set_active(True)
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.hide_alert()
+ journal.get_list_view()._selected_entries = 0
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+
self.emit('volume-changed', button.mount_point)
+ def _unmount_activated_cb(self, menu_item, mount):
+ logging.debug('VolumesToolbar._unmount_activated_cb: %r', mount)
+ mount.unmount(self.__unmount_cb)
+
+ def __unmount_cb(self, source, result):
+ logging.debug('__unmount_cb %r %r', source, result)
+
def _get_button_for_mount(self, mount):
mount_point = mount.get_root().get_path()
for button in self.get_children():
@@ -259,6 +326,13 @@ class VolumesToolbar(Gtk.Toolbar):
logging.error('Couldnt find button with mount_point %r', mount_point)
return None
+ def _get_button_for_mount_point(self, mount_point):
+ for button in self.get_children():
+ if button.mount_point == mount_point:
+ return button
+ logging.error('Couldnt find button with mount_point %r', mount_point)
+ return None
+
def _remove_button(self, mount):
button = self._get_button_for_mount(mount)
self._volume_buttons.remove(button)
@@ -268,10 +342,33 @@ class VolumesToolbar(Gtk.Toolbar):
if len(self.get_children()) < 2:
self.hide()
+ def _remove_remote_share_button(self, ip_address_or_dns_name):
+ for button in self.get_children():
+ if type(button) == RemoteSharesButton and \
+ button.mount_point == (model.WEBDAV_MOUNT_POINT + ip_address_or_dns_name):
+ self._volume_buttons.remove(button)
+ self.remove(button)
+
+ from jarabe.journal.webdavmanager import \
+ unmount_share_from_backend
+ unmount_share_from_backend(ip_address_or_dns_name)
+
+ self.get_children()[0].props.active = True
+
+ if len(self.get_children()) < 2:
+ self.hide()
+ break;
+
def set_active_volume(self, mount):
button = self._get_button_for_mount(mount)
button.props.active = True
+ 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__ = {
@@ -291,23 +388,36 @@ class BaseButton(RadioToolButton):
def _drag_data_received_cb(self, widget, drag_context, x, y,
selection_data, info, timestamp):
+ # Disallow copying to mounted-shares for peers.
+ if (model.is_mount_point_for_locally_mounted_remote_share(self.mount_point)) and \
+ (model.is_mount_point_for_peer_share(self.mount_point)):
+ from jarabe.journal.journalactivity import get_journal
+
+ journal = get_journal()
+ journal._show_alert(_('Entries cannot be copied to Peer-Shares.'), _('Error'))
+ return
+
object_id = selection_data.data
metadata = model.get(object_id)
- 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.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
- try:
- model.copy(metadata, self.mount_point)
- except IOError, e:
- logging.exception('Error while copying the entry. %s', e.strerror)
- self.emit('volume-error',
- _('Error while copying the entry. %s') % e.strerror,
- _('Error'))
+ from jarabe.journal.palettes import CopyMenu, get_copy_menu_helper
+ copy_menu_helper = get_copy_menu_helper()
+
+ dummy_copy_menu = CopyMenu()
+ copy_menu_helper.insert_copy_to_menu_items(dummy_copy_menu,
+ [metadata],
+ False,
+ False,
+ False)
+
+ # Now, activate the menuitem, whose mount-point matches the
+ # mount-point of the button, upon whom the item has been
+ # dragged.
+ children_menu_items = dummy_copy_menu.get_children()
+ for child in children_menu_items:
+ if child.get_mount_point() == self.mount_point:
+ child.activate()
+ return
class VolumeButton(BaseButton):
@@ -335,7 +445,7 @@ class VolumeButton(BaseButton):
self.props.xo_color = color
def create_palette(self):
- palette = VolumePalette(self._mount)
+ palette = JournalVolumePalette(self._mount)
#palette.props.invoker = FrameWidgetInvoker(self)
#palette.set_group_id('frame')
return palette
@@ -386,13 +496,35 @@ 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._mount = dir_path
- self.props.icon_name = 'user-documents'
+ self.props.icon_name = 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, primary_text, ip_address_or_dns_name, color,
+ share_type):
+ BaseButton.__init__(self, mount_point=(model.WEBDAV_MOUNT_POINT + ip_address_or_dns_name))
+
+ self._primary_text = primary_text
+ self._ip_address_or_dns_name = ip_address_or_dns_name
+
+ if share_type == SHARE_TYPE_PEER:
+ self.props.icon_name = 'emblem-neighborhood-shared'
+ elif share_type == SHARE_TYPE_SCHOOL_SERVER:
+ self.props.icon_name = 'school-server'
+ self.props.xo_color = color
+
+ def create_palette(self):
+ palette = RemoteSharePalette(self._primary_text, self._ip_address_or_dns_name,
+ self, True)
+ return palette
diff --git a/src/jarabe/journal/webdavmanager.py b/src/jarabe/journal/webdavmanager.py
new file mode 100644
index 0000000..6936239
--- /dev/null
+++ b/src/jarabe/journal/webdavmanager.py
@@ -0,0 +1,312 @@
+from gettext import gettext as _
+
+from gi.repository import GObject
+
+import logging
+import os
+import sys
+
+import simplejson
+import shutil
+
+from webdav.Connection import AuthorizationError, WebdavError
+from webdav.WebdavClient import CollectionStorer
+
+def get_key_from_resource(resource):
+ return resource.path
+
+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_root(self):
+ return self._root
+
+ 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):
+ if key in self._remote_webdav_share_resources.keys():
+ return self._remote_webdav_share_resources[key]['resource']
+ return None
+
+ def _add_or_replace_resource_by_key(self, key, resource):
+ self._remote_webdav_share_resources[key] = {}
+ self._remote_webdav_share_resources[key]['resource'] = 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 _fetch_resources_and_collections(self):
+ webdavConnection = CollectionStorer(self._WebDavUrl, validateResourceNames=False)
+ self._root = webdavConnection
+
+ 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
+
+ from jarabe.journal.palettes import USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ error_message = USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ get_journal()._volume_error_cb(None, error_message,_('Error'))
+
+ # Re-raise this error.
+ # Note that since this is not an
+ # "AuthorizationError", this will not be caught
+ # by the outer except-block. Instead, it will
+ # navigate all the way back up, and will report
+ # the error in the enclosing except block.
+ raise e
+
+ 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_data_webdav_manager(ip_address_or_dns_name):
+ return webdav_manager[ip_address_or_dns_name]['data']
+
+
+def get_metadata_webdav_manager(ip_address_or_dns_name):
+ return webdav_manager[ip_address_or_dns_name]['metadata']
+
+
+def get_resource_by_resource_key(root_webdav, key):
+ resources_dict = root_webdav._get_resources_dict()
+ if key in resources_dict.keys():
+ resource_dict = resources_dict[key]
+ resource = resource_dict['resource']
+ return resource
+ return None
+
+
+def add_resource_by_resource_key(root_webdav, key,
+ content_file_path):
+ root = root_webdav._get_root()
+
+ resource = root.addResource(key)
+
+ # Procure the resource-lock.
+ lockToken = resource.lock('olpc')
+
+ input_stream = open(content_file_path)
+
+ # Now, upload the data; but it's necessary to enclose this in a
+ # try-except-finally block here, since we need to close the
+ # input-stream, whatever may happen.
+ try:
+ resource.uploadFile(input_stream, lockToken)
+ root_webdav._add_or_replace_resource_by_key(key, resource)
+ except Exception, e:
+ logging.exception(e)
+ resource.delete(lockToken)
+ raise e
+ else:
+ resource.unlock(lockToken)
+ finally:
+ input_stream.close()
+
+
+def get_remote_webdav_share_metadata(ip_address_or_dns_name):
+ protocol = 'davs://'
+ root_webdav_url = '/webdav'
+ complete_root_url = protocol + ip_address_or_dns_name + 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_or_dns_name] = {}
+ webdav_manager[ip_address_or_dns_name]['data'] = 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_or_dns_name + 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 []
+
+ webdav_manager[ip_address_or_dns_name]['metadata'] = root_webdav_sugar_metadata
+
+ # 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_or_dns_name
+ 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)
+
+ metadata_list = []
+
+ # Note that the presence of a resource in the metadata directory,
+ # is the only assurance of the entry (and its constituents) being
+ # present in entirety. Thus, always proceed taking the metadata as
+ # the "key".
+ for root_webdav_sugar_metadata_resource_name in root_webdav_sugar_metadata_resources.keys():
+ """
+ root_webdav_sugar_metadata_resource_name is of the type ::
+
+ /webdav/.Sugar-Metadata/a.txt.metadata, OR
+ /webdav/.Sugar-Metadata/a.txt.preview
+ """
+
+ # If this is a "preview" resource, continue forward, as we only
+ # want the metadata list. The "preview" resources are anyways
+ # already present in the manager DS.
+ if root_webdav_sugar_metadata_resource_name.endswith('.preview'):
+ continue
+
+ split_tokens_array = root_webdav_sugar_metadata_resource_name.split('/')
+
+ # This will provide us with "a.txt.metadata"
+ sugar_metadata_basename = split_tokens_array[len(split_tokens_array) - 1]
+
+ # This will provide us with "a.txt"
+ basename = sugar_metadata_basename[0:sugar_metadata_basename.index('.metadata')]
+
+ downloaded_metadata_file_path = downloaded_metadata_file_dir + '/' + sugar_metadata_basename
+ metadata_resource = \
+ root_webdav_sugar_metadata._get_resource_by_key(root_webdav_sugar_metadata_resource_name)
+ metadata_resource.downloadFile(downloaded_metadata_file_path)
+
+
+ # We need to download the preview-file as well at this stage,
+ # so that it can be shown in the expanded entry.
+ downloaded_preview_file_path = downloaded_metadata_file_dir + \
+ '/' + basename + '.preview'
+ root_webdav_sugar_preview_resource_name = \
+ root_webdav_sugar_metadata_resource_name[0:root_webdav_sugar_metadata_resource_name.index('.metadata')] + \
+ '.preview'
+ preview_resource = \
+ root_webdav_sugar_metadata._get_resource_by_key(root_webdav_sugar_preview_resource_name)
+ if preview_resource is not None:
+ preview_resource.downloadFile(downloaded_preview_file_path)
+
+
+ file_pointer = open(downloaded_metadata_file_path)
+ metadata = eval(file_pointer.read())
+ file_pointer.close()
+
+ # Fill in the missing metadata properties.
+ # Note that the file is not physically present.
+ metadata['uid'] = downloaded_data_root_dir + '/' + basename
+ metadata['creation_time'] = metadata['timestamp']
+
+ # 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()
+
+ metadata_list.append(metadata)
+
+ return metadata_list
+
+
+def is_remote_webdav_loaded(ip_address_or_dns_name):
+ return ip_address_or_dns_name in webdav_manager.keys()
+
+
+def unmount_share_from_backend(ip_address_or_dns_name):
+ del webdav_manager[ip_address_or_dns_name]
diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am
index 2fc6b1c..d40fb8d 100644
--- a/src/jarabe/model/Makefile.am
+++ b/src/jarabe/model/Makefile.am
@@ -12,6 +12,7 @@ sugar_PYTHON = \
neighborhood.py \
network.py \
notifications.py \
+ processmanagement.py \
shell.py \
screen.py \
session.py \
diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py
index 85c35c9..0712073 100644
--- a/src/jarabe/model/neighborhood.py
+++ b/src/jarabe/model/neighborhood.py
@@ -427,7 +427,8 @@ class _Account(GObject.GObject):
def __buddy_info_updated_cb(self, handle, properties):
logging.debug('_Account.__buddy_info_updated_cb %r', handle)
- self.emit('buddy-updated', self._buddy_handles[handle], properties)
+ if handle in self._buddy_handles:
+ self.emit('buddy-updated', self._buddy_handles[handle], properties)
def __current_activity_changed_cb(self, contact_handle, activity_id,
room_handle):
@@ -929,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/model/network.py b/src/jarabe/model/network.py
index 53e170a..c60f6af 100644
--- a/src/jarabe/model/network.py
+++ b/src/jarabe/model/network.py
@@ -492,6 +492,7 @@ class Settings(object):
self.connection = ConnectionSettings()
self.ip4_config = None
self.wireless_security = None
+ self.wpa_eap_setting = None
if wireless_cfg is not None:
self.wireless = wireless_cfg
@@ -507,6 +508,8 @@ class Settings(object):
self.wireless_security.get_dict()
if self.ip4_config is not None:
settings['ipv4'] = self.ip4_config.get_dict()
+ if self.wpa_eap_setting is not None:
+ settings['802-1x'] = self.wpa_eap_setting
return settings
@@ -907,12 +910,14 @@ def activate_connection_by_path(connection, device_o,
error_handler=error_handler)
-def add_and_activate_connection(device_o, settings, specific_object):
+def add_and_activate_connection(device_o, settings, specific_object,
+ reply_handler=_add_and_activate_reply_cb,
+ error_handler=_add_and_activate_error_cb):
manager = get_manager()
manager.AddAndActivateConnection(settings.get_dict(), device_o,
specific_object,
- reply_handler=_add_and_activate_reply_cb,
- error_handler=_add_and_activate_error_cb)
+ reply_handler=reply_handler,
+ error_handler=error_handler)
def _migrate_old_wifi_connections():
diff --git a/src/jarabe/model/processmanagement.py b/src/jarabe/model/processmanagement.py
new file mode 100644
index 0000000..cb429f6
--- /dev/null
+++ b/src/jarabe/model/processmanagement.py
@@ -0,0 +1,120 @@
+# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org>
+# Copyright (C) 2010, Plan Ceibal <comunidad@plan.ceibal.edu.uy>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gi.repository import GObject
+from gi.repository import Gio
+
+import os
+import glib
+
+
+from sugar import env
+from gettext import gettext as _
+
+BYTES_TO_READ = 100
+
+class ProcessManagement(GObject.GObject):
+
+ __gtype_name__ = 'ProcessManagement'
+
+ __gsignals__ = {
+ 'process-management-running' : (GObject.SignalFlags.RUN_FIRST, None, ([str])),
+ 'process-management-started' : (GObject.SignalFlags.RUN_FIRST, None, ([])),
+ 'process-management-finished' : (GObject.SignalFlags.RUN_FIRST, None, ([])),
+ 'process-management-failed' : (GObject.SignalFlags.RUN_FIRST, None, ([str]))
+ }
+
+ def __init__(self):
+ GObject.GObject.__init__(self)
+ self._running = False
+
+ def do_process(self, cmd):
+ self._run_cmd_async(cmd)
+
+ def _report_process_status(self, stream, result, user_data=None):
+ data = stream.read_finish(result)
+
+ if data != 0:
+ self.emit('process-management-running', data)
+ stream.read_async([],
+ BYTES_TO_READ,
+ GObject.PRIORITY_LOW,
+ None,
+ self._report_process_status,
+ None)
+
+ def _report_process_error(self, stream, result, concat_err=''):
+ data = stream.read_finish(result)
+ concat_err = concat_err + data
+
+ if data != 0:
+ self.emit('process-management-failed', concat_err)
+ else:
+ stream.read_async([],
+ BYTES_TO_READ,
+ GObject.PRIORITY_LOW,
+ None,
+ self._report_process_error,
+ concat_err)
+
+ def _notify_error(self, stderr):
+ stdin_stream = Gio.UnixInputStream(fd=stderr, close_fd=True)
+ stdin_stream.read_async([],
+ BYTES_TO_READ,
+ GObject.PRIORITY_LOW,
+ None,
+ self._report_process_error,
+ None)
+
+ def _notify_process_status(self, stdout):
+ stdin_stream = Gio.UnixInputStream(fd=stdout, close_fd=True)
+ stdin_stream.read_async([],
+ BYTES_TO_READ,
+ GObject.PRIORITY_LOW,
+ None,
+ self._report_process_status,
+ None)
+
+ def _run_cmd_async(self, cmd):
+ if self._running == False:
+ try:
+ pid, stdin, stdout, stderr = glib.spawn_async(cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True, standard_error=True)
+ GObject.child_watch_add(pid, _handle_process_end, (self, stderr))
+ except Exception:
+ self.emit('process-management-failed', _("Error - Call process: ") + str(cmd))
+ else:
+ self._notify_process_status(stdout)
+ self._running = True
+ self.emit('process-management-started')
+
+def _handle_process_end(pid, condition, (myself, stderr)):
+ myself._running = False
+
+ if os.WIFEXITED(condition) and\
+ os.WEXITSTATUS(condition) == 0:
+ myself.emit('process-management-finished')
+ else:
+ myself._notify_error(stderr)
+
+def find_and_absolutize(script_name):
+ paths = env.os.environ['PATH'].split(':')
+ for path in paths:
+ looking_path = path + '/' + script_name
+ if env.os.path.isfile(looking_path):
+ return looking_path
+
+ return None
diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
index d17f4ff..93790fd 100644
--- a/src/jarabe/view/buddymenu.py
+++ b/src/jarabe/view/buddymenu.py
@@ -73,6 +73,18 @@ class BuddyMenu(Palette):
self.menu.append(menu_item)
menu_item.show()
+ remote_share_menu_item = None
+ from jarabe.journal import webdavmanager
+ if not webdavmanager.is_remote_webdav_loaded(self._buddy.props.ip_address):
+ remote_share_menu_item = MenuItem(_('Access Share'), 'list-add')
+ remote_share_menu_item.connect('activate', self._access_share_cb)
+ else:
+ remote_share_menu_item = MenuItem(_('Unmount Share'), 'list-remove')
+ remote_share_menu_item.connect('activate', self.__unmount_cb)
+
+ self.menu.append(remote_share_menu_item)
+ remote_share_menu_item.show()
+
self._invite_menu = MenuItem('')
self._invite_menu.connect('activate', self._invite_friend_cb)
self.menu.append(self._invite_menu)
diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py
index 10844ea..bbbf822 100644
--- a/src/jarabe/view/palettes.py
+++ b/src/jarabe/view/palettes.py
@@ -1,4 +1,6 @@
# Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2010, Plan Ceibal <comunidad@plan.ceibal.edu.uy>
+# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -33,6 +35,7 @@ from sugar3.graphics import style
from sugar3.graphics.xocolor import XoColor
from sugar3.activity.i18n import pgettext
+from jarabe.journal.processdialog import VolumeBackupDialog, VolumeRestoreDialog
from jarabe.model import shell
from jarabe.view.viewsource import setup_view_source
from jarabe.journal import misc
@@ -268,3 +271,97 @@ class VolumePalette(Palette):
self._progress_bar.props.fraction = fraction
self._free_space_label.props.label = _('%(free_space)d MB Free') % \
{'free_space': free_space / (1024 * 1024)}
+
+
+class RemoteSharePalette(Palette):
+ def __init__(self, primary_text, ip_address_or_dns_name, button,
+ show_unmount_option):
+ Palette.__init__(self, label=primary_text)
+ self._button = button
+ self._ip_address_or_dns_name = ip_address_or_dns_name
+
+ self.props.secondary_text = \
+ glib.markup_escape_text(self._ip_address_or_dns_name)
+
+ vbox = Gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self.connect('popup', self.__popup_cb)
+
+ menu_item = PaletteMenuItem(pgettext('Share', _('Reload')))
+ icon = Icon(icon_name='system-restart', icon_size=Gtk.IconSize.MENU)
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__reload_remote_share)
+ vbox.add(menu_item)
+ menu_item.show()
+
+
+ if show_unmount_option == True:
+ menu_item = PaletteMenuItem(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)
+ vbox.add(menu_item)
+ menu_item.show()
+
+ def __reload_remote_share(self, menu_item):
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().hide_alert()
+ get_journal().get_list_view().refresh()
+
+ 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._ip_address_or_dns_name)
+
+ def __popup_cb(self, palette):
+ pass
+
+
+
+
+class JournalVolumePalette(VolumePalette):
+
+ __gtype_name__ = 'JournalVolumePalette'
+
+ def __init__(self, mount):
+ VolumePalette.__init__(self, mount)
+
+ journal_separator = gtk.SeparatorMenuItem()
+ journal_separator.show()
+
+ self.menu.prepend(journal_separator)
+
+ icon = Icon(icon_name='transfer-from', icon_size=gtk.ICON_SIZE_MENU)
+ icon.show()
+
+ menu_item_journal_restore = MenuItem(_('Restore Journal'))
+ menu_item_journal_restore.set_image(icon)
+ menu_item_journal_restore.connect('activate', self.__journal_restore_activate_cb, mount.get_root().get_path())
+ menu_item_journal_restore.show()
+
+ self.menu.prepend(menu_item_journal_restore)
+
+ icon = Icon(icon_name='transfer-to', icon_size=gtk.ICON_SIZE_MENU)
+ icon.show()
+
+ menu_item_journal_backup = MenuItem(_('Backup Journal'))
+ menu_item_journal_backup.set_image(icon)
+ menu_item_journal_backup.connect('activate', self.__journal_backup_activate_cb, mount.get_root().get_path())
+ menu_item_journal_backup.show()
+
+ self.menu.prepend(menu_item_journal_backup)
+
+ def __journal_backup_activate_cb(self, menu_item, mount_path):
+ dialog = VolumeBackupDialog(mount_path)
+ dialog.show()
+
+ def __journal_restore_activate_cb(self, menu_item, mount_path):
+ dialog = VolumeRestoreDialog(mount_path)
+ dialog.show()
diff --git a/src/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py
index 81e2e03..652e22e 100644
--- a/src/jarabe/view/pulsingicon.py
+++ b/src/jarabe/view/pulsingicon.py
@@ -90,12 +90,23 @@ class PulsingIcon(Icon):
self._pulse_color = None
self._paused = False
self._pulsing = False
+ self._timeout = 0
+ self._pulsing_sid = None
Icon.__init__(self, **kwargs)
self._palette = None
self.connect('destroy', self.__destroy_cb)
+ def set_timeout(self, timeout):
+ self._timeout = timeout
+
+ def get_timeout(self):
+ return self._timeout
+
+ timeout = GObject.property(
+ type=int, getter=get_timeout, setter=set_timeout)
+
def set_pulse_color(self, pulse_color):
self._pulse_color = pulse_color
self._pulser.update()
@@ -142,10 +153,20 @@ class PulsingIcon(Icon):
type=bool, default=False, getter=get_paused, setter=set_paused)
def set_pulsing(self, pulsing):
+ if self._pulsing == pulsing:
+ return
+
+ if self._pulsing_sid is not None:
+ GObject.source_remove(self._pulsing_sid)
+ self._pulsing_sid = None
+
self._pulsing = pulsing
if self._pulsing:
self._pulser.start(restart=True)
+ if self.props.timeout > 0:
+ self._pulsing_sid = GObject.timeout_add_seconds(
+ self.props.timeout, self.__timeout_cb)
else:
self._pulser.stop()
@@ -165,6 +186,9 @@ class PulsingIcon(Icon):
palette = property(_get_palette, _set_palette)
+ def __timeout_cb(self):
+ self.props.pulsing = False
+
def __destroy_cb(self, icon):
self._pulser.stop()
if self._palette is not None:
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..33719f9
--- /dev/null
+++ b/src/webdav/Connection.py
@@ -0,0 +1,324 @@
+# 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)
+ filesize = os.path.getsize(srcfile.name)
+ self._blockCopySocket(srcfile, self,
+ Connection.blockSize,filesize)
+ 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, filesize):
+ transferedBytes = 0
+ block = source.read(blockSize)
+ while len(block):
+ self.logger.debug("Wrote %d bytes." % len(block))
+ transferedBytes += len(block)
+ toSocket.send(block)
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_progress(transferedBytes/(filesize*1.0))
+ 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..8ce5c77
--- /dev/null
+++ b/src/webdav/WebdavClient.py
@@ -0,0 +1,848 @@
+# 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
+
+ # We need to change the header["Content-length"] to a string.
+ header["Content-length"] = str(header["Content-length"])
+
+ 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, show_progress=False,
+ filesize=0):
+ """
+ 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,
+ show_progress, filesize)
+ 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, show_progress, filesize):
+ """
+ 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):
+ transferedBytes += len(block);
+ dest.write(block)
+ if show_progress:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_progress(transferedBytes/(filesize * 1.0))
+ 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..a611e51
--- /dev/null
+++ b/src/webdav/davlib.py
@@ -0,0 +1,339 @@
+# 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.__is_secure_protocol():
+ self.default_port = 443
+ else:
+ self.default_port = 80
+
+ apply(httplib.HTTPSConnection.__init__, (self,) + args, kw)
+
+ def connect(self):
+ if self.__is_secure_protocol():
+ httplib.HTTPSConnection.connect(self)
+ else:
+ httplib.HTTPConnection.connect(self)
+
+ def __is_secure_protocol(self):
+ return (self.protocol == 'https') or (self.protocol == 'davs')
+
+
+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
+
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')