Monotone-Parent: 14fb6c252ef1563d917c59f289ac3d06479a7eab
Monotone-Revision: d085b8d79d3eca94ece4f311bf3d652d7f20bb00 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2009-08-10T20:59:49 Monotone-Branch: ca.inverse.sogomaint-2.0.2
parent
89b67f8434
commit
ecfd8f026e
|
@ -6,6 +6,13 @@
|
|||
|
||||
2009-08-10 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* SoObjects/Appointments/SOGoAppointmentObject.m (-PUTAction):
|
||||
modified to handle the case where the active user has the "respond
|
||||
to" permission, in which case we grab the copy of the iCalendar
|
||||
which is already in the DB and modify the partstats thereof based
|
||||
on the entries found in the uploaded iCalendar. The latter is
|
||||
discarded silently to avoid any further change.
|
||||
|
||||
* SoObjects/Appointments/iCalEntityObject+SOGo.m
|
||||
(-userAsParticipant): new method that returns the "PARTICIPANT"
|
||||
entity of the SOGoUser passed as parameter, if found.
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
#import <NGCards/iCalEvent.h>
|
||||
#import <NGCards/iCalEventChanges.h>
|
||||
#import <NGCards/iCalPerson.h>
|
||||
#import <NGCards/NSCalendarDate+NGCards.h>
|
||||
#import <SaxObjC/XMLNamespaces.h>
|
||||
|
||||
#import <SoObjects/SOGo/iCalEntityObject+Utilities.h>
|
||||
|
@ -1245,26 +1246,84 @@
|
|||
return @"IPM.Appointment";
|
||||
}
|
||||
|
||||
//
|
||||
// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
|
||||
// simply invoke super's PUTAction.
|
||||
//
|
||||
- (id) PUTAction: (WOContext *) _ctx
|
||||
- (NSDictionary *) _partStatsFromCalendar: (iCalCalendar *) calendar
|
||||
{
|
||||
NSMutableDictionary *partStats;
|
||||
NSArray *allEvents;
|
||||
int count, max;
|
||||
iCalEvent *currentEvent;
|
||||
iCalPerson *ownerParticipant;
|
||||
NSString *key;
|
||||
SOGoUser *ownerUser;
|
||||
|
||||
ownerUser = [SOGoUser userWithLogin: owner roles: nil];
|
||||
|
||||
allEvents = [calendar events];
|
||||
max = [allEvents count];
|
||||
partStats = [NSMutableDictionary dictionaryWithCapacity: max];
|
||||
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
currentEvent = [allEvents objectAtIndex: count];
|
||||
ownerParticipant = [currentEvent userAsParticipant: ownerUser];
|
||||
if (ownerParticipant)
|
||||
{
|
||||
if (count == 0)
|
||||
key = @"master";
|
||||
else
|
||||
key = [[currentEvent recurrenceId] iCalFormattedDateTimeString];
|
||||
[partStats setObject: ownerParticipant forKey: key];
|
||||
}
|
||||
}
|
||||
|
||||
return partStats;
|
||||
}
|
||||
|
||||
- (void) _setupResponseCalendarInRequest: (WORequest *) rq
|
||||
{
|
||||
iCalCalendar *calendar, *putCalendar;
|
||||
NSData *newContent;
|
||||
NSArray *keys;
|
||||
NSDictionary *partStats, *newPartStats;
|
||||
NSString *partStat, *key;
|
||||
int count, max;
|
||||
|
||||
calendar = [self calendar: NO secure: NO];
|
||||
partStats = [self _partStatsFromCalendar: calendar];
|
||||
keys = [partStats allKeys];
|
||||
max = [keys count];
|
||||
if (max > 0)
|
||||
{
|
||||
putCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
||||
newPartStats = [self _partStatsFromCalendar: putCalendar];
|
||||
if ([keys isEqualToArray: [newPartStats allKeys]])
|
||||
{
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
key = [keys objectAtIndex: count];
|
||||
partStat = [[newPartStats objectForKey: key] partStat];
|
||||
[[partStats objectForKey: key] setPartStat: partStat];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newContent = [[calendar versitString]
|
||||
dataUsingEncoding: [rq contentEncoding]];
|
||||
[rq setContent: newContent];
|
||||
}
|
||||
|
||||
- (void) _decomposeGroupsInRequest: (WORequest *) rq
|
||||
{
|
||||
iCalCalendar *calendar;
|
||||
NSArray *allEvents;
|
||||
iCalEvent *event;
|
||||
WORequest *rq;
|
||||
|
||||
BOOL b;
|
||||
int i;
|
||||
BOOL modified;
|
||||
|
||||
rq = [_ctx request];
|
||||
|
||||
if ([[rq headersForKey: @"X-SOGo"] containsObject: @"NoGroupsDecomposition"])
|
||||
return [super PUTAction: _ctx];
|
||||
|
||||
//NSLog(@"Content from request: %@", [rq contentAsString]);
|
||||
// If we decomposed at least one group, let's rewrite the content
|
||||
// of the request. Otherwise, leave it as is in case this rewrite
|
||||
// isn't totaly lossless.
|
||||
calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
||||
|
||||
// The algorithm is pretty straightforward:
|
||||
//
|
||||
|
@ -1273,24 +1332,43 @@
|
|||
// If some are groups, we decompose them
|
||||
// We regenerate the iCalendar string
|
||||
//
|
||||
calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
||||
allEvents = [calendar events];
|
||||
b = NO;
|
||||
modified = NO;
|
||||
|
||||
for (i = 0; i < [allEvents count]; i++)
|
||||
{
|
||||
event = [allEvents objectAtIndex: i];
|
||||
if ([self expandGroupsInEvent: event])
|
||||
b = YES;
|
||||
modified |= [self expandGroupsInEvent: event];
|
||||
}
|
||||
|
||||
//NSLog(@"Content from calendar:secure: %@", [calendar versitString]);
|
||||
|
||||
// If we decomposed at least one group, let's rewrite the content
|
||||
// of the request. Otherwise, leave it as is in case this rewrite
|
||||
// isn't totaly lossless.
|
||||
if (b)
|
||||
if (modified)
|
||||
[rq setContent: [[calendar versitString] dataUsingEncoding: [rq contentEncoding]]];
|
||||
}
|
||||
|
||||
//
|
||||
// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
|
||||
// simply invoke super's PUTAction.
|
||||
//
|
||||
- (id) PUTAction: (WOContext *) _ctx
|
||||
{
|
||||
WORequest *rq;
|
||||
NSArray *roles;
|
||||
|
||||
rq = [_ctx request];
|
||||
|
||||
roles = [[context activeUser] rolesForObject: self inContext: context];
|
||||
if ([roles containsObject: @"ComponentResponder"]
|
||||
&& ![roles containsObject: @"ComponentModifier"])
|
||||
[self _setupResponseCalendarInRequest: rq];
|
||||
else
|
||||
{
|
||||
if (![[rq headersForKey: @"X-SOGo"]
|
||||
containsObject: @"NoGroupsDecomposition"])
|
||||
[self _decomposeGroupsInRequest: rq];
|
||||
}
|
||||
|
||||
return [super PUTAction: _ctx];
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"ModifyComponent" = ( "Owner", "ComponentModifier", "ObjectEditor" );
|
||||
"RespondToComponent" = ( "Owner", "ComponentModifier", "ComponentResponder" );
|
||||
"Access Object" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ObjectEditor" );
|
||||
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ComponentResponder", "ObjectEditor" );
|
||||
"Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||
"WebDAV Access" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||
};
|
||||
|
@ -61,7 +61,7 @@
|
|||
"ModifyComponent" = ( "Owner", "ComponentModifier" );
|
||||
"RespondToComponent" = ( "Participant", "ComponentModifier", "ComponentResponder" );
|
||||
"Access Object" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier" );
|
||||
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ComponentResponder" );
|
||||
"Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||
"WebDAV Access" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||
};
|
||||
|
|
|
@ -9,14 +9,31 @@ import xml.xpath
|
|||
import time
|
||||
|
||||
# TODO:
|
||||
# - cal: we don't test "respond" yet
|
||||
# - cal: complete test for "modify": "respond to" causes a 204 but no actual
|
||||
# modification should occur
|
||||
# - ab: testcase for addressbook-query, webdav-sync (no "calendar-data"
|
||||
# equivalent)
|
||||
# ? cal: testcase for "calendar-query"
|
||||
# - cal: testcase for "calendar-query"
|
||||
# - test rights validity:
|
||||
# - send invalid rights to SOGo and expect failures
|
||||
# - refetch the set of rights and make sure it matches what was set
|
||||
# originally
|
||||
# - test "current-user-acl-set"
|
||||
|
||||
def fetchUserEmail(login):
|
||||
client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
resource = '/SOGo/dav/%s/' % login
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"],
|
||||
0)
|
||||
client.execute(propfind)
|
||||
xpath_context = xml.xpath.CreateContext(propfind.response["document"])
|
||||
xpath_context.setNamespaces({ "D": "DAV:",
|
||||
"C": "urn:ietf:params:xml:ns:caldav" })
|
||||
nodes = xml.xpath.Evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', None, xpath_context)
|
||||
|
||||
return nodes[0].childNodes[0].nodeValue
|
||||
|
||||
class DAVAclTest(unittest.TestCase):
|
||||
resource = None
|
||||
|
@ -102,15 +119,18 @@ DTEND:20090805T140000Z
|
|||
CLASS:%(class)s
|
||||
DESCRIPTION:%(class)s description
|
||||
LOCATION:location
|
||||
CREATED:20090805T100000Z
|
||||
%(organizer_line)s%(attendee_line)sCREATED:20090805T100000Z
|
||||
DTSTAMP:20090805T100000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
|
||||
class DAVCalendarAclTest(DAVAclTest):
|
||||
resource = '/SOGo/dav/%s/Calendar/test-dav-acl/' % username
|
||||
user_email = None
|
||||
|
||||
def setUp(self):
|
||||
if self.user_email is None:
|
||||
self.user_email = fetchUserEmail(username)
|
||||
DAVAclTest.setUp(self)
|
||||
self.classToICSClass = { "pu": "PUBLIC",
|
||||
"pr": "PRIVATE",
|
||||
|
@ -139,15 +159,32 @@ class DAVCalendarAclTest(DAVAclTest):
|
|||
"""'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE"""
|
||||
self._testRights({ "c": True, "d": True, "pu": "d", "pr": "m" })
|
||||
|
||||
def testCreateRespondToPublic(self):
|
||||
"""'create', 'respond to' PUBLIC"""
|
||||
self._testRights({ "c": True, "pu": "r" })
|
||||
|
||||
def testNothing(self):
|
||||
"""no right given"""
|
||||
self._testRights({})
|
||||
|
||||
def _putEvent(self, client, filename,
|
||||
event_class = "PUBLIC", exp_status = 201):
|
||||
event_class = "PUBLIC",
|
||||
exp_status = 201,
|
||||
organizer = None, attendee = None,
|
||||
partstat = "NEEDS-ACTION"):
|
||||
url = "%s%s" % (self.resource, filename)
|
||||
if organizer is not None:
|
||||
organizer_line = "ORGANIZER:%s\n" % organizer
|
||||
else:
|
||||
organizer_line = ""
|
||||
if attendee is not None:
|
||||
attendee_line = "ATTENDEE;PARTSTAT=%s:%s\n" % (partstat, attendee)
|
||||
else:
|
||||
attendee_line = ""
|
||||
event = event_template % { "class": event_class,
|
||||
"filename": filename }
|
||||
"filename": filename,
|
||||
"organizer_line": organizer_line,
|
||||
"attendee_line": attendee_line }
|
||||
put = webdavlib.HTTPPUT(url, event, "text/calendar; charset=utf-8")
|
||||
client.execute(put)
|
||||
self.assertEquals(put.response["status"], exp_status,
|
||||
|
@ -219,19 +256,24 @@ class DAVCalendarAclTest(DAVAclTest):
|
|||
right = None
|
||||
|
||||
event = self._getEvent(event_class)
|
||||
self._checkEventRight("GET", event, event_class, right)
|
||||
self._checkViewEventRight("GET", event, event_class, right)
|
||||
event = self._propfindEvent(event_class)
|
||||
self._checkEventRight("PROPFIND", event, event_class, right)
|
||||
self._checkViewEventRight("PROPFIND", event, event_class, right)
|
||||
event = self._multigetEvent(event_class)
|
||||
self._checkEventRight("multiget", event, event_class, right)
|
||||
self._checkViewEventRight("multiget", event, event_class, right)
|
||||
event = self._webdavSyncEvent(event_class)
|
||||
self._checkEventRight("webdav-sync", event, event_class, right)
|
||||
self._checkViewEventRight("webdav-sync", event, event_class, right)
|
||||
|
||||
self._testModify(event_class, right)
|
||||
self._testRespondTo(event_class, right)
|
||||
|
||||
def _getEvent(self, event_class):
|
||||
def _getEvent(self, event_class, is_invitation = False):
|
||||
icsClass = self.classToICSClass[event_class]
|
||||
url = "%s%s.ics" % (self.resource, icsClass.lower())
|
||||
if is_invitation:
|
||||
filename = "invitation-%s" % icsClass.lower()
|
||||
else:
|
||||
filename = "%s" % icsClass.lower()
|
||||
url = "%s%s.ics" % (self.resource, filename)
|
||||
get = webdavlib.HTTPGET(url)
|
||||
self.subscriber_client.execute(get)
|
||||
|
||||
|
@ -315,7 +357,7 @@ class DAVCalendarAclTest(DAVAclTest):
|
|||
|
||||
return event
|
||||
|
||||
def _checkEventRight(self, operation, event, event_class, right):
|
||||
def _checkViewEventRight(self, operation, event, event_class, right):
|
||||
if right is None:
|
||||
self.assertEquals(event, None,
|
||||
"None right expecting event invisibility for"
|
||||
|
@ -327,8 +369,10 @@ class DAVCalendarAclTest(DAVAclTest):
|
|||
if right == "v" or right == "r" or right == "m":
|
||||
icsClass = self.classToICSClass[event_class]
|
||||
complete_event = (event_template % { "class": icsClass,
|
||||
"filename": "%s.ics" % icsClass.lower() })
|
||||
self.assertTrue(event == complete_event,
|
||||
"filename": "%s.ics" % icsClass.lower(),
|
||||
"organizer_line": "",
|
||||
"attendee_line": ""})
|
||||
self.assertTrue(event.strip() == complete_event.strip(),
|
||||
"Right '%s' should return complete event"
|
||||
" during operation '%s'"
|
||||
% (right, operation))
|
||||
|
@ -367,7 +411,7 @@ class DAVCalendarAclTest(DAVAclTest):
|
|||
% key)
|
||||
|
||||
def _testModify(self, event_class, right):
|
||||
if right == "m":
|
||||
if right == "m" or right == "r":
|
||||
exp_code = 204
|
||||
else:
|
||||
exp_code = 403
|
||||
|
@ -376,6 +420,42 @@ class DAVCalendarAclTest(DAVAclTest):
|
|||
self._putEvent(self.subscriber_client, filename, icsClass,
|
||||
exp_code)
|
||||
|
||||
def _testRespondTo(self, event_class, right):
|
||||
icsClass = self.classToICSClass[event_class]
|
||||
filename = "invitation-%s.ics" % icsClass.lower()
|
||||
self._putEvent(self.client, filename, icsClass,
|
||||
201,
|
||||
"mailto:nobody@somewhere.com", self.user_email,
|
||||
"NEEDS-ACTION")
|
||||
|
||||
if right == "m" or right == "r":
|
||||
exp_code = 204
|
||||
else:
|
||||
exp_code = 403
|
||||
|
||||
# here we only do 'passive' validation: if a user has a "respond to"
|
||||
# right, only the attendee entry will me modified. The change of
|
||||
# organizer must thus be silently ignored below.
|
||||
self._putEvent(self.subscriber_client, filename, icsClass,
|
||||
exp_code, "mailto:someone@nowhere.com", self.user_email,
|
||||
"ACCEPTED")
|
||||
if exp_code == 204:
|
||||
att_line = "ATTENDEE;PARTSTAT=ACCEPTED:%s\n" % self.user_email
|
||||
if right == "r":
|
||||
exp_event = event_template % {"class": icsClass,
|
||||
"filename": filename,
|
||||
"organizer_line": "ORGANIZER:mailto:nobody@somewhere.com\n",
|
||||
"attendee_line": att_line}
|
||||
else:
|
||||
exp_event = event_template % {"class": icsClass,
|
||||
"filename": filename,
|
||||
"organizer_line": "ORGANIZER:mailto:someone@nowhere.com\n",
|
||||
"attendee_line": att_line}
|
||||
event = self._getEvent(event_class, True).replace("\r", "")
|
||||
self.assertEquals(exp_event.strip(), event.strip(),
|
||||
"'respond to' event does not match:\nreceived:\n"
|
||||
"%s\nexpected:\n%s" % (event, exp_event))
|
||||
|
||||
# Addressbook:
|
||||
# short rights notation: { "c": create,
|
||||
# "d": delete,
|
||||
|
|
Loading…
Reference in New Issue