mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-08-14 03:13:18 +00:00
merge of 'a0d0b11a9d09b6e2a4c1105454c00cca7c853ac5'
and 'd35569adb5f95d077a2e359cae7af87c6ba720d1' Monotone-Parent: a0d0b11a9d09b6e2a4c1105454c00cca7c853ac5 Monotone-Parent: d35569adb5f95d077a2e359cae7af87c6ba720d1 Monotone-Revision: 2ae443d1e9e3ef66ebb42cbba16fc86deb005615 Monotone-Author: flachapelle@inverse.ca Monotone-Date: 2009-08-06T14:37:53 Monotone-Branch: ca.inverse.sogo
This commit is contained in:
@@ -1,3 +1,18 @@
|
||||
2009-08-05 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* SoObjects/SOGo/SOGoGCSFolder.m (-davSyncCollection): enhanced to
|
||||
return a DAV error with the "valid-sync-token" condition tag when
|
||||
a token is provided while the collection is empty.
|
||||
|
||||
* SoObjects/SOGo/WOResponse+SOGo.m: new class module.
|
||||
(-appendDAVError:): utility method that adds and formats the XML
|
||||
body of a DAV error, with the error condition passed as argument.
|
||||
|
||||
2009-08-05 Ludovic Marcotte <lmarcotte@inverse.ca>
|
||||
|
||||
* Added missing files for Hungarian translation
|
||||
* Fixed the reply / forward ops for many translations
|
||||
|
||||
2009-08-03 Ludovic Marcotte <lmarcotte@inverse.ca>
|
||||
|
||||
* Dropped the current caching subsystem based on
|
||||
|
||||
Binary file not shown.
@@ -7,6 +7,7 @@
|
||||
- added the possibility to disable alarms or tasks from Web calendars
|
||||
- added support for printing cards
|
||||
- the event/task summary dialog is now similar to Lightning
|
||||
- replaced GNUstep's DO subsystem with memcached
|
||||
|
||||
1.0-20090714 (1.0.3)
|
||||
--------------------
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface SOGoMailBrazilianPortugueseForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@interface SOGoMailCzechForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@interface SOGoMailDutchForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@@ -51,6 +57,9 @@
|
||||
@interface SOGoMailGermanForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@interface SOGoMailHungarianForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@interface SOGoMailItalianForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@@ -60,4 +69,7 @@
|
||||
@interface SOGoMailRussianForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
@interface SOGoMailWelshForward : SOGoMailForward
|
||||
@end
|
||||
|
||||
#endif /* SOGOMAILFORWARD_H */
|
||||
|
||||
@@ -225,6 +225,12 @@
|
||||
|
||||
@end
|
||||
|
||||
@implementation SOGoMailBrazilianPortugueseForward
|
||||
@end
|
||||
|
||||
@implementation SOGoMailCzechForward
|
||||
@end
|
||||
|
||||
@implementation SOGoMailDutchForward
|
||||
@end
|
||||
|
||||
@@ -237,6 +243,9 @@
|
||||
@implementation SOGoMailGermanForward
|
||||
@end
|
||||
|
||||
@implementation SOGoMailHungarianForward
|
||||
@end
|
||||
|
||||
@implementation SOGoMailItalianForward
|
||||
@end
|
||||
|
||||
@@ -245,3 +254,6 @@
|
||||
|
||||
@implementation SOGoMailRussianForward
|
||||
@end
|
||||
|
||||
@implementation SOGoMailWelshForward
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
subject: WOString {
|
||||
value = subject;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
date: WOString {
|
||||
value = date;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
from: WOString {
|
||||
value = from;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
newLine: WOString {
|
||||
value = newLine;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasReplyTo: WOConditional {
|
||||
condition = hasReplyTo;
|
||||
}
|
||||
|
||||
replyTo: WOString {
|
||||
value = replyTo;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasOrganization: WOConditional {
|
||||
condition = hasOrganization;
|
||||
}
|
||||
|
||||
organization: WOString {
|
||||
value = organization;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
to: WOString {
|
||||
value = to;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasCc: WOConditional {
|
||||
condition = hasCc;
|
||||
}
|
||||
|
||||
cc: WOString {
|
||||
value = cc;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasNewsGroups: WOConditional {
|
||||
condition = hasNewsGroups;
|
||||
}
|
||||
|
||||
newsgroups: WOString {
|
||||
value = newsgroups;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasReferences: WOConditional {
|
||||
condition = hasReferences;
|
||||
}
|
||||
|
||||
references: WOString {
|
||||
value = references;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
messageBody: WOString {
|
||||
value = messageBody;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
signature: WOString {
|
||||
value = signature;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
outlookMode: WOConditional {
|
||||
condition = outlookMode;
|
||||
}
|
||||
|
||||
standardMode: WOConditional {
|
||||
condition = outlookMode;
|
||||
negate = YES;
|
||||
}
|
||||
|
||||
subject: WOString {
|
||||
value = subject;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
date: WOString {
|
||||
value = date;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
from: WOString {
|
||||
value = from;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
newLine: WOString {
|
||||
value = newLine;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasReplyTo: WOConditional {
|
||||
condition = hasReplyTo;
|
||||
}
|
||||
|
||||
replyTo: WOString {
|
||||
value = replyTo;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasOrganization: WOConditional {
|
||||
condition = hasOrganization;
|
||||
}
|
||||
|
||||
organization: WOString {
|
||||
value = organization;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
to: WOString {
|
||||
value = to;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasCc: WOConditional {
|
||||
condition = hasCc;
|
||||
}
|
||||
|
||||
cc: WOString {
|
||||
value = cc;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasNewsGroups: WOConditional {
|
||||
condition = hasNewsGroups;
|
||||
}
|
||||
|
||||
newsgroups: WOString {
|
||||
value = newsgroups;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
hasReferences: WOConditional {
|
||||
condition = hasReferences;
|
||||
}
|
||||
|
||||
references: WOString {
|
||||
value = references;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
messageBody: WOString {
|
||||
value = messageBody;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
signature: WOString {
|
||||
value = signature;
|
||||
escapeHTML = NO;
|
||||
}
|
||||
|
||||
replyPlacementOnTop: WOConditional {
|
||||
condition = replyPlacementOnTop;
|
||||
}
|
||||
|
||||
replyPlacementOnBottom: WOConditional {
|
||||
condition = replyPlacementOnTop;
|
||||
negate = YES;
|
||||
}
|
||||
|
||||
signaturePlacementOnTop: WOConditional {
|
||||
condition = signaturePlacementOnTop;
|
||||
}
|
||||
|
||||
signaturePlacementOnBottom: WOConditional {
|
||||
condition = signaturePlacementOnTop;
|
||||
negate = YES;
|
||||
}
|
||||
@@ -44,6 +44,12 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface SOGoMailBrazilianPortugueseReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@interface SOGoMailCzechReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@interface SOGoMailDutchReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@@ -56,6 +62,9 @@
|
||||
@interface SOGoMailGermanReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@interface SOGoMailHungarianReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@interface SOGoMailItalianReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@@ -65,4 +74,7 @@
|
||||
@interface SOGoMailRussianReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
@interface SOGoMailWelshReply : SOGoMailReply
|
||||
@end
|
||||
|
||||
#endif /* SOGOMAILREPLY_H */
|
||||
|
||||
@@ -108,6 +108,12 @@
|
||||
|
||||
@end
|
||||
|
||||
@implementation SOGoMailBrazilianPortugueseReply
|
||||
@end
|
||||
|
||||
@implementation SOGoMailCzechReply
|
||||
@end
|
||||
|
||||
@implementation SOGoMailDutchReply
|
||||
@end
|
||||
|
||||
@@ -120,6 +126,9 @@
|
||||
@implementation SOGoMailGermanReply
|
||||
@end
|
||||
|
||||
@implementation SOGoMailHungarianReply
|
||||
@end
|
||||
|
||||
@implementation SOGoMailItalianReply
|
||||
@end
|
||||
|
||||
@@ -128,3 +137,6 @@
|
||||
|
||||
@implementation SOGoMailRussianReply
|
||||
@end
|
||||
|
||||
@implementation SOGoMailWelshReply
|
||||
@end
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Testun: <#subject/><#newLine/>
|
||||
Dyddiad: <#date/><#newLine/>
|
||||
Oddi wrth: <#from/><#newLine/>
|
||||
<#hasReplyTo>Ymateb i: <#replyTo/><#hasOrganization>Sefydliad: <#organization/></#hasOrganization>I: <#to/><#newLine/>
|
||||
<#hasReplyTo>Ymateb i: <#replyTo/></#hasReplyTo><#hasOrganization>Sefydliad: <#organization/></#hasOrganization>I: <#to/><#newLine/>
|
||||
<#hasCc>CC: <#cc/></#hasCc><#hasNewsGroups>Grwpiau newyddion: <#newsgroups/></#hasNewsGroups><#hasReferences>Cyfeirnodau: <#references/></#hasReferences><#newLine/>
|
||||
<#newLine/>
|
||||
<#messageBody/><#newLine/>
|
||||
|
||||
@@ -58,6 +58,7 @@ SOGo_HEADER_FILES = \
|
||||
DOMNode+SOGo.h \
|
||||
\
|
||||
WORequest+SOGo.h \
|
||||
WOResponse+SOGo.h \
|
||||
WOContext+SOGo.h
|
||||
|
||||
# SOGoCustomGroupFolder.m \
|
||||
@@ -106,6 +107,7 @@ SOGo_OBJC_FILES = \
|
||||
DOMNode+SOGo.m \
|
||||
\
|
||||
WORequest+SOGo.m \
|
||||
WOResponse+SOGo.m \
|
||||
WOContext+SOGo.m
|
||||
|
||||
SOGo_RESOURCE_FILES = \
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
#import "SOGoUser.h"
|
||||
#import "SOGoWebDAVAclManager.h"
|
||||
#import "WORequest+SOGo.h"
|
||||
#import "WOResponse+SOGo.h"
|
||||
|
||||
#import "SOGoGCSFolder.h"
|
||||
|
||||
@@ -896,7 +897,7 @@ static NSArray *childRecordFields = nil;
|
||||
}
|
||||
|
||||
- (NSArray *) _fetchSyncTokenFields: (NSDictionary *) properties
|
||||
matchingSyncToken: (int) syncToken
|
||||
matchingSyncToken: (NSString *) syncToken
|
||||
{
|
||||
/* TODO:
|
||||
- validation:
|
||||
@@ -908,6 +909,7 @@ static NSArray *childRecordFields = nil;
|
||||
EOQualifier *qualifier;
|
||||
NSEnumerator *addFields;
|
||||
NSString *currentField, *filter;
|
||||
int syncTokenInt;
|
||||
|
||||
fields = [NSMutableArray arrayWithObjects: @"c_name", @"c_component",
|
||||
@"c_creationdate", @"c_lastmodified", nil];
|
||||
@@ -916,16 +918,17 @@ static NSArray *childRecordFields = nil;
|
||||
if ([currentField length])
|
||||
[fields addObjectUniquely: currentField];
|
||||
|
||||
if (syncToken)
|
||||
if ([syncToken length])
|
||||
{
|
||||
syncTokenInt = [syncToken intValue];
|
||||
qualifier = [EOQualifier qualifierWithQualifierFormat:
|
||||
@"c_lastmodified > %d", syncToken];
|
||||
@"c_lastmodified > %d", syncTokenInt];
|
||||
mRecords = [NSMutableArray arrayWithArray: [self _fetchFields: fields
|
||||
withQualifier: qualifier
|
||||
ignoreDeleted: YES]];
|
||||
withQualifier: qualifier
|
||||
ignoreDeleted: YES]];
|
||||
qualifier = [EOQualifier qualifierWithQualifierFormat:
|
||||
@"c_lastmodified > %d and c_deleted == 1",
|
||||
syncToken];
|
||||
syncTokenInt];
|
||||
fields = [NSMutableArray arrayWithObjects: @"c_name", @"c_deleted", nil];
|
||||
[mRecords addObjectsFromArray: [self _fetchFields: fields
|
||||
withQualifier: qualifier
|
||||
@@ -1090,10 +1093,10 @@ static NSArray *childRecordFields = nil;
|
||||
if (newToken < currentLM)
|
||||
newToken = currentLM;
|
||||
[syncResponses addObject: [self _syncResponseWithProperties: properties
|
||||
andMethodSelectors: selectors
|
||||
fromRecord: record
|
||||
withToken: syncToken
|
||||
andBaseURL: baseURL]];
|
||||
andMethodSelectors: selectors
|
||||
fromRecord: record
|
||||
withToken: syncToken
|
||||
andBaseURL: baseURL]];
|
||||
}
|
||||
|
||||
NSZoneFree (NULL, selectors);
|
||||
@@ -1122,15 +1125,12 @@ static NSArray *childRecordFields = nil;
|
||||
NSString *syncToken;
|
||||
NSDictionary *properties;
|
||||
NSArray *records;
|
||||
int syncTokenInt;
|
||||
|
||||
r = [context response];
|
||||
[r setStatus: 207];
|
||||
[r setContentEncoding: NSUTF8StringEncoding];
|
||||
[r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"];
|
||||
[r setHeader: @"no-cache" forKey: @"pragma"];
|
||||
[r setHeader: @"no-cache" forKey: @"cache-control"];
|
||||
[r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
|
||||
|
||||
document = [[context request] contentAsDOMDocument];
|
||||
documentElement = (DOMElement *) [document documentElement];
|
||||
@@ -1139,15 +1139,21 @@ static NSArray *childRecordFields = nil;
|
||||
|
||||
propElement = [documentElement firstElementWithTag: @"prop"
|
||||
inNamespace: XMLNS_WEBDAV];
|
||||
|
||||
syncTokenInt = [syncToken intValue];
|
||||
properties = [self parseDAVRequestedProperties: propElement];
|
||||
records = [self _fetchSyncTokenFields: properties
|
||||
matchingSyncToken: syncTokenInt];
|
||||
[self _appendComponentProperties: [properties allKeys]
|
||||
fromRecords: records
|
||||
matchingSyncToken: syncTokenInt
|
||||
toResponse: r];
|
||||
matchingSyncToken: syncToken];
|
||||
if (![syncToken length] || [records count])
|
||||
{
|
||||
[r setStatus: 207];
|
||||
[r appendContentString: @"<?xml version=\"1.0\""
|
||||
@" encoding=\"utf-8\"?>\r\n"];
|
||||
[self _appendComponentProperties: [properties allKeys]
|
||||
fromRecords: records
|
||||
matchingSyncToken: [syncToken intValue]
|
||||
toResponse: r];
|
||||
}
|
||||
else
|
||||
[r appendDAVError: davElement (@"valid-sync-token", XMLNS_WEBDAV)];
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* WOResponse+SOGo.h - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2009 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
* This file 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, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file 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; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#ifndef WORESPONSE_SOGo_H
|
||||
#define WORESPONSE_SOGo_H
|
||||
|
||||
#import <NGObjWeb/WOResponse.h>
|
||||
|
||||
@class NSDictionary;
|
||||
|
||||
@interface WOResponse (SOGoSOPEUtilities)
|
||||
|
||||
- (void) appendDAVError: (NSDictionary *) errorCondition;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* WORESPONSE_SOGo_H */
|
||||
@@ -0,0 +1,46 @@
|
||||
/* WOResponse+SOGo.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2009 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
* This file 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, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file 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; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#import <Foundation/NSString.h>
|
||||
|
||||
#import <SaxObjC/XMLNamespaces.h>
|
||||
|
||||
#import "NSDictionary+DAV.h"
|
||||
#import "NSObject+DAV.h"
|
||||
|
||||
#import "WOResponse+SOGo.h"
|
||||
|
||||
@implementation WOResponse (SOGoSOPEUtilities)
|
||||
|
||||
- (void) appendDAVError: (NSDictionary *) errorCondition
|
||||
{
|
||||
NSDictionary *error;
|
||||
|
||||
error = davElementWithContent (@"error", XMLNS_WEBDAV, errorCondition);
|
||||
|
||||
[self setStatus: 403];
|
||||
[self appendContentString: @"<?xml version=\"1.0\""
|
||||
@" encoding=\"utf-8\"?>\r\n"];
|
||||
[self appendContentString: [error asWebDavStringWithNamespaces: nil]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
setup
|
||||
-----
|
||||
|
||||
(you need "python-xml" and "python-m2crypto" in order to run the scripts)
|
||||
|
||||
1) copy testconfig.py.in to testconfig.py (make sure to never EVER add it to monotone)
|
||||
2) edit testconfig.py to suit your environment
|
||||
3) run the test scripts
|
||||
|
||||
runnable scripts
|
||||
----------------
|
||||
|
||||
webdav-sync.py
|
||||
|
||||
other
|
||||
-----
|
||||
|
||||
propfind.py - a sample implementation of a PROPFIND request using webdavlib
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from testconfig import hostname, port, username, password
|
||||
|
||||
import webdavlib
|
||||
|
||||
import sys
|
||||
import getopt
|
||||
|
||||
def parseArguments():
|
||||
arguments = {}
|
||||
|
||||
depth = "0"
|
||||
quiet = False
|
||||
(opts, args) = getopt.getopt(sys.argv[1:], "d:q", ["depth=", "quiet"])
|
||||
|
||||
for pair in opts:
|
||||
print pair
|
||||
if (pair[0] == "-d" or pair[0] == "--depth"):
|
||||
depth = pair[1]
|
||||
elif (pair[0] == "-q" or pair[0] == "--quiet"):
|
||||
quiet = True
|
||||
|
||||
# print "depth: " + depth
|
||||
|
||||
nargs = len(args)
|
||||
if (nargs > 0):
|
||||
resource = args[0]
|
||||
if (nargs > 1):
|
||||
properties = args[1:]
|
||||
else:
|
||||
properties = [ "allprop" ]
|
||||
else:
|
||||
print "resource required"
|
||||
sys.exit(-1)
|
||||
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource, properties, depth)
|
||||
client.execute(propfind)
|
||||
@@ -0,0 +1,4 @@
|
||||
hostname = "localhost"
|
||||
port = "80"
|
||||
username = "myuser"
|
||||
password = "mypass"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from testconfig import hostname, port, username, password
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
import webdavlib
|
||||
import xml.xpath
|
||||
import time
|
||||
|
||||
resource = '/SOGo/dav/%s/Calendar/test-webdavsync/' % username
|
||||
|
||||
class WebdavSyncTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
|
||||
def tearDown(self):
|
||||
delete = webdavlib.WebDAVDELETE(resource)
|
||||
self.client.execute(delete)
|
||||
|
||||
def _xpath_query(self, query, top_node):
|
||||
xpath_context = xml.xpath.CreateContext(top_node)
|
||||
xpath_context.setNamespaces({ "D": "DAV:" })
|
||||
return xml.xpath.Evaluate(query, None, xpath_context)
|
||||
|
||||
def test(self):
|
||||
# missing tests:
|
||||
# invalid tokens: negative, non-numeric, > current timestamp
|
||||
# non-empty collections: token validity, status codes for added,
|
||||
# modified and removed elements
|
||||
|
||||
# preparation
|
||||
mkcol = webdavlib.WebDAVMKCOL(resource)
|
||||
self.client.execute(mkcol)
|
||||
self.assertEquals(mkcol.response["status"], 201,
|
||||
"preparation: failure creating collection (code != 201)")
|
||||
|
||||
# test queries:
|
||||
# empty collection:
|
||||
# without a token (query1)
|
||||
# with a token (query2)
|
||||
# non-empty collection:
|
||||
# without a token (query3)
|
||||
# with a token (query4)
|
||||
|
||||
query1 = webdavlib.WebDAVSyncQuery(resource, None, [ "getetag" ])
|
||||
self.client.execute(query1)
|
||||
self.assertEquals(query1.response["status"], 207,
|
||||
("query1: invalid status code: %d (!= 207)"
|
||||
% query1.response["status"]))
|
||||
token_node = self._xpath_query("/D:multistatus/D:sync-token",
|
||||
query1.response["document"])[0]
|
||||
# Implicit "assertion": we expect SOGo to return a token node, with a
|
||||
# non-empty numerical value. Anything else will trigger an exception
|
||||
token = int(token_node.childNodes[0].nodeValue)
|
||||
|
||||
self.assertTrue(token > 0)
|
||||
self.assertTrue(token < int(query1.start))
|
||||
|
||||
# we make sure that any token is invalid when the collection is empty
|
||||
query2 = webdavlib.WebDAVSyncQuery(resource, "1234", [ "getetag" ])
|
||||
self.client.execute(query2)
|
||||
self.assertEquals(query2.response["status"], 403)
|
||||
cond_nodes = self._xpath_query("/D:error/D:valid-sync-token",
|
||||
query2.response["document"])
|
||||
self.assertTrue(len(cond_nodes) > 0,
|
||||
"expected 'valid-sync-token' condition error")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,223 @@
|
||||
import httplib
|
||||
import M2Crypto.httpslib
|
||||
import time
|
||||
import xml.sax.saxutils
|
||||
import xml.dom.ext.reader.Sax2
|
||||
import sys
|
||||
|
||||
class WebDAVClient:
|
||||
def __init__(self, hostname, port, username, password, forcessl = False):
|
||||
if port == "443" or forcessl:
|
||||
self.conn = M2Crypto.httpslib.HTTPSConnection(hostname, int(port),
|
||||
True)
|
||||
else:
|
||||
self.conn = httplib.HTTPConnection(hostname, port, True)
|
||||
|
||||
self.simpleauth_hash = (("%s:%s" % (username, password))
|
||||
.encode('base64')[:-1])
|
||||
|
||||
def _prepare_headers(self, query, body):
|
||||
headers = { "User-Agent": "Mozilla/5.0",
|
||||
"content-length": len(body),
|
||||
"authorization": "Basic %s" % self.simpleauth_hash }
|
||||
if query.__dict__.has_key("query") and query.depth is not None:
|
||||
headers["depth"] = query.depth
|
||||
if query.__dict__.has_key("content_type"):
|
||||
headers["content-type"] = query.content_type
|
||||
|
||||
return headers
|
||||
|
||||
def execute(self, query):
|
||||
body = query.render()
|
||||
|
||||
query.start = time.time()
|
||||
self.conn.request(query.method, query.url,
|
||||
body, self._prepare_headers(query, body))
|
||||
query.set_response(self.conn.getresponse());
|
||||
query.duration = time.time() - query.start
|
||||
|
||||
class HTTPSimpleQuery:
|
||||
method = None
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
self.response = None
|
||||
self.start = -1
|
||||
self.duration = -1
|
||||
|
||||
def render():
|
||||
return None
|
||||
|
||||
class HTTPGET(HTTPSimpleQuery):
|
||||
method = "GET"
|
||||
|
||||
class HTTPQuery(HTTPSimpleQuery):
|
||||
def __init__(self, url, content_type):
|
||||
HTTPSimpleQuery.__init__(self, url)
|
||||
self.content_type = content_type
|
||||
|
||||
def set_response(self, http_response):
|
||||
headers = {}
|
||||
for rk, rv in http_response.getheaders():
|
||||
k = rk.lower()
|
||||
headers[k] = rv
|
||||
self.response = { "headers": headers,
|
||||
"status": http_response.status,
|
||||
"version": http_response.version,
|
||||
"body": http_response.read() }
|
||||
|
||||
class HTTPPUT(HTTPQuery):
|
||||
method = "PUT"
|
||||
|
||||
def __init__(self, url, content, content_type = "application/octet-stream"):
|
||||
HTTPQuery.__init__(self, url, content_type)
|
||||
self.content = content
|
||||
|
||||
def render(self):
|
||||
return self.content
|
||||
|
||||
class WebDAVQuery(HTTPQuery):
|
||||
method = None
|
||||
|
||||
def __init__(self, url, depth = None):
|
||||
HTTPQuery.__init__(self, url, "application/xml; charset=\"utf-8\"")
|
||||
self.depth = depth
|
||||
self.ns_mgr = _WD_XMLNS_MGR()
|
||||
self.top_node = None
|
||||
self.xml_response = None
|
||||
|
||||
def render(self):
|
||||
if self.top_node is not None:
|
||||
text = ("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n%s"
|
||||
% self.top_node.render(self.ns_mgr.render()))
|
||||
else:
|
||||
text = ""
|
||||
|
||||
return text
|
||||
|
||||
def render_tag(self, tag):
|
||||
cb = tag.find("}")
|
||||
if cb > -1:
|
||||
ns = tag[1:cb]
|
||||
real_tag = tag[cb+1:]
|
||||
new_tag = self.ns_mgr.register(real_tag, ns)
|
||||
else:
|
||||
new_tag = tag
|
||||
|
||||
return new_tag
|
||||
|
||||
def set_response(self, http_response):
|
||||
HTTPQuery.set_response(self, http_response)
|
||||
headers = self.response["headers"]
|
||||
if (headers.has_key("content-type")
|
||||
and headers.has_key("content-length")
|
||||
and (headers["content-type"].startswith("application/xml")
|
||||
or headers["content-type"].startswith("text/xml"))
|
||||
and int(headers["content-length"]) > 0):
|
||||
dom_response = xml.dom.ext.reader.Sax2.FromXml(self.response["body"])
|
||||
self.response["document"] = dom_response.documentElement
|
||||
|
||||
class WebDAVMKCOL(WebDAVQuery):
|
||||
method = "MKCOL"
|
||||
|
||||
class WebDAVDELETE(WebDAVQuery):
|
||||
method = "DELETE"
|
||||
|
||||
class WebDAVREPORT(WebDAVQuery):
|
||||
method = "REPORT"
|
||||
|
||||
class WebDAVPROPFIND(WebDAVQuery):
|
||||
method = "PROPFIND"
|
||||
|
||||
def __init__(self, url, properties, depth = None):
|
||||
WebDAVQuery.__init__(self, url, depth)
|
||||
self.top_node = _WD_XMLTreeElement("propfind")
|
||||
props = _WD_XMLTreeElement("prop")
|
||||
self.top_node.append(props)
|
||||
for prop in properties:
|
||||
prop_tag = self.render_tag(prop)
|
||||
props.append(_WD_XMLTreeElement(prop_tag))
|
||||
|
||||
class WebDAVSyncQuery(WebDAVREPORT):
|
||||
def __init__(self, url, token, properties):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
self.top_node = _WD_XMLTreeElement("sync-collection")
|
||||
|
||||
sync_token = _WD_XMLTreeElement("sync-token")
|
||||
self.top_node.append(sync_token)
|
||||
if token is not None:
|
||||
sync_token.append(_WD_XMLTreeTextNode(token))
|
||||
|
||||
props = _WD_XMLTreeElement("prop")
|
||||
self.top_node.append(props)
|
||||
for prop in properties:
|
||||
prop_tag = self.render_tag(prop)
|
||||
props.append(_WD_XMLTreeElement(prop_tag))
|
||||
|
||||
# private classes to handle XML stuff
|
||||
class _WD_XMLNS_MGR:
|
||||
def __init__(self):
|
||||
self.xmlns = {}
|
||||
self.counter = 0
|
||||
|
||||
def render(self):
|
||||
text = " xmlns=\"DAV:\""
|
||||
for k in self.xmlns:
|
||||
text = text + " xmlns:%s=\"%s\"" % (self.xmlns[k], k)
|
||||
|
||||
return text
|
||||
|
||||
def create_key(self, namespace):
|
||||
new_nssym = "n%d" % self.counter
|
||||
self.counter = self.counter + 1
|
||||
self.xmlns[namespace] = new_nssym
|
||||
|
||||
return new_nssym
|
||||
|
||||
def register(self, tag, namespace):
|
||||
if namespace != "DAV:":
|
||||
if self.xmlns.has_key(namespace):
|
||||
key = self.xmlns[namespace]
|
||||
else:
|
||||
key = self.create_key(namespace)
|
||||
else:
|
||||
key = None
|
||||
|
||||
if key is not None:
|
||||
newTag = "%s:%s" % (key, tag)
|
||||
else:
|
||||
newTag = tag
|
||||
|
||||
return newTag
|
||||
|
||||
class _WD_XMLTreeElement:
|
||||
def __init__(self, tag):
|
||||
self.tag = tag
|
||||
self.children = []
|
||||
|
||||
def append(self, child):
|
||||
self.children.append(child)
|
||||
|
||||
def render(self, ns_text = None):
|
||||
text = "<" + self.tag
|
||||
|
||||
if ns_text is not None:
|
||||
text = text + ns_text
|
||||
|
||||
count = len(self.children)
|
||||
if count > 0:
|
||||
text = text + ">"
|
||||
for x in range(0, count):
|
||||
text = text + self.children[x].render()
|
||||
text = text + "</" + self.tag + ">"
|
||||
else:
|
||||
text = text + "/>"
|
||||
|
||||
return text
|
||||
|
||||
class _WD_XMLTreeTextNode:
|
||||
def __init__(self, text):
|
||||
self.text = xml.sax.saxutils.escape(text)
|
||||
|
||||
def render(self):
|
||||
return self.text
|
||||
Reference in New Issue
Block a user