diff --git a/ChangeLog b/ChangeLog index 229527c5e..9c444503e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,66 @@ +2010-07-22 Ludovic Marcotte + + * Added Migration/Horde/* - scripts used to migrate + the address books and email signatures from Horde to + the SOGo database. + +2010-07-21 Wolfgang Sourdeau + + * UI/WebServerResources/ContactsUI.js: (dropSelectedContacts): + reenabled copying contacts from system (LDAP) addressbooks. + +2010-07-20 Wolfgang Sourdeau + + * SoObjects/SOGo/SOGoUserFolder.m (_subFoldersFromFolder:): we + need to validate the current user's access to the listed folders + again because of the recent changes on SOGoParentFolder. + + * UI/Contacts/UIxContactFoldersView.m (_subFoldersFromFolder:): + removed unused method. + +2010-07-16 Wolfgang Sourdeau + + * UI/Contacts/UIxContactEditor.m (-photosURL): copy of the method + below. + + * UI/Contacts/UIxContactView.m (-photosURL): new access that + returns the URL to the photos contained in the VCARD, whether + inline or not. + (-tabSelection): removed unused method. + + * SoObjects/Contacts/SOGoContactGCSEntry.m + (-lookupName:inContext:acquire:): new overriden method to handle + "photoX" lookups and return the corrsponding SOGoContactEntryPhoto + instance, if exists. + + * SoObjects/Contacts/SOGoContactEntryPhoto.[hm]: new controller + class for VCARD PHOTO objects. + + * SoObjects/SOGo/SOGoContentObject.m (-davContentLength): fixed a + crash occurring with certain versions of GNUstep by using + NSISOLatin1StringEncoding instead of UTF8. This is actually the + correct way of doing things anyway since we want to return the + length in bytes and not in characters. + +2010-07-16 Ludovic Marcotte + + * UI/Contacts/UIxContactView.m + In secondaryEmail, we make sure that we loop + in all available email address and we pick the + first one that doesn't match the primaryEmail + * UI/Contacts/UIxContactEditor.m + UI/Contacts/UIxContactView.m - we make sure, + when excluding a type, that we reset the + local variable to nil since we might exclude + all values in our array. + +2010-07-16 Wolfgang Sourdeau + + * SoObjects/Appointments/SOGoAppointmentObject.m + (_handleUpdatedEvent:fromOldEvent:): added handling of delegation + chains when resetting the participation status of the attendees. + Fixes #688. + 2010-07-15 Wolfgang Sourdeau * SoObjects/SOGo/SOGoUser.m (-personalCalendarFolderInContext:): @@ -11,6 +74,7 @@ * UI/Scheduler/UIxCalListingActions.m (_aptFolder:withClientObject:): removed unused method. + (_fixDates:): apply fix also for monthly views. * UI/WebServerResources/UIxAppointmentEditor.js (onComposeToAllAttendees): take the status image DIV into account diff --git a/Documentation/SOGo Installation Guide.odt b/Documentation/SOGo Installation Guide.odt index 3372a8c70..ac2528749 100644 Binary files a/Documentation/SOGo Installation Guide.odt and b/Documentation/SOGo Installation Guide.odt differ diff --git a/Documentation/SOGo Mobile Devices Configuration.odt b/Documentation/SOGo Mobile Devices Configuration.odt index ee4496216..7ff650d16 100644 Binary files a/Documentation/SOGo Mobile Devices Configuration.odt and b/Documentation/SOGo Mobile Devices Configuration.odt differ diff --git a/Documentation/SOGo Mozilla Thunderbird Configuration.odt b/Documentation/SOGo Mozilla Thunderbird Configuration.odt index ea8fd6499..5b1f09394 100644 Binary files a/Documentation/SOGo Mozilla Thunderbird Configuration.odt and b/Documentation/SOGo Mozilla Thunderbird Configuration.odt differ diff --git a/Main/Version b/Main/Version index de0c80cf1..c0ad2d6e6 100644 --- a/Main/Version +++ b/Main/Version @@ -1,6 +1,6 @@ # Version file -SUBMINOR_VERSION:=2 +SUBMINOR_VERSION:=0 # v0.9.32 requires libSOGo v0.9.57 # v0.9.24 requires libWEExtensions v4.5.67 diff --git a/Migration/Horde/HordeSignatureConverter.py b/Migration/Horde/HordeSignatureConverter.py new file mode 100644 index 000000000..29040f37a --- /dev/null +++ b/Migration/Horde/HordeSignatureConverter.py @@ -0,0 +1,84 @@ +import PHPDeserializer +import sys + +class HordeSignatureConverter: + def __init__(self, user, domain): + self.user = user + self.domain = domain + self.domainLen = len(domain) + + def fetchSignatures(self, conn): + self.signatures = None + self.conn = conn + self.fetchIdentities() + + return self.signatures + + def fetchIdentities(self): + self.users = {} + + cursor = self.conn.cursor() + if self.user == "ALL": + userClause = "" + else: + userClause = "AND pref_uid = '%s'" % self.user + query = "SELECT pref_uid, pref_value" \ + " FROM horde_prefs" \ + " WHERE pref_scope = 'horde'" \ + " AND pref_name = 'identities'" \ + " %s" % userClause + cursor.execute(query) + + self.signatures = {} + records = cursor.fetchall() + max = len(records) + if max > 0: + for record in records: + user = record[0] + signature = self.decodeSignature(record[1], user) + if signature is None or len(signature.strip()) == 0: + print "No useful signature found for %s" % user + else: + self.signatures[user] = signature + + print "%d useful signature(s) found in %d record(s)" % (len(self.signatures), max) + else: + print "No record found" + + cursor.close() + + def decodeSignature(self, prefs, user): + des = PHPDeserializer.PHPDeserializer(prefs) + identities = des.deserialize() + nbrEntries = len(identities) + signatures = [] + for identity in identities: + fromAddr = identity["from_addr"] + if (len(fromAddr) > self.domainLen + and fromAddr[-self.domainLen:] == self.domain): + if identity.has_key("signature"): + signatures.append(identity["signature"]) + + if len(signatures) > 0: + signature = self.chooseSignature(signatures) + else: + signature = None + + return signature + + def chooseSignature(self, signatures): + biggest = -1 + length = -1 + count = 0 + for signature in signatures: + thisLength = len(signature) + if thisLength > 0 and thisLength > length: + biggest = count + count = count + 1 + + if biggest == -1: + signature = None + else: + signature = signatures[biggest] + + return signature diff --git a/Migration/Horde/PHPDeserializer.py b/Migration/Horde/PHPDeserializer.py new file mode 100644 index 000000000..675b8a7be --- /dev/null +++ b/Migration/Horde/PHPDeserializer.py @@ -0,0 +1,135 @@ +# we perform no validation +class PHPDeserializer: + def __init__(self, string): + self.string = string + if string is None: + self.length = 0 + else: + self.length = len(string) + self.cursor = 0 + + def deserializeInteger(self): + start = self.cursor + + done = False + while not done: + if self.cursor < self.length: + currentChar = self.string[self.cursor] + if currentChar.isdigit(): + self.cursor = self.cursor + 1 + else: + done = True + else: + done = True + + length = self.cursor - start + if length > 0: + dInteger = int(self.string[start:self.cursor]) + else: + dInteger = 0 + + return dInteger + + def deserializeBoolean(self): + start = self.cursor + + if self.cursor < self.length: + currentChar = self.string[self.cursor] + if currentChar == "0": + dBoolean = False + else: + dBoolean = True + self.cursor = self.cursor + 1 + else: + dBoolean = None + + return dBoolean + + def deserializeString(self): + length = self.deserializeInteger() + start = self.cursor + 2 + end = start+length + value = self.string[start:end] + self.cursor = end + 1 + + return value + + def deserializeArray(self): + isHash = False + max = self.deserializeInteger() + dArray = [ None ] * max + + self.cursor = self.cursor + 2 + count = 0 + while count < max: + elementIndex = self.deserialize() + self.cursor = self.cursor + 1 + element = self.deserialize() + if isHash: + if type(elementIndex) == int: + elementIndex = "%d" % elementIndex + else: + if type(elementIndex) != int: + dArray = self._arrayToHash(dArray) + isHash = True + dArray[elementIndex] = element + self.cursor = self.cursor + 1 + count = count + 1 + + if self.string[self.cursor] != "}": + raise Exception, \ + ("inconsistency detected in serialized string at character %d:\n%s" + % (self.cursor, self._exceptionSample())) + + return dArray + + def _exceptionSample(self): + if self.cursor > 30: + start = self.cursor - 30 + prefix = "..." + else: + start = 0 + prefix = "" + carret = self.cursor + len(prefix) - start + length = len(self.string) + if self.cursor + 30 < length: + end = self.cursor + 30 + suffix = "..." + else: + end = length + suffix = "" + + sample = self.string[start:end] + while sample[0] == " ": + carret = carret - 1 + sample = sample[1:] + + return "%s%s%s\n%s^" % (prefix, sample, suffix, carret * " ") + + def _arrayToHash(self, array): + dHash = {} + count = 0 + for element in array: + dHash["%d" % count] = element + count = count + 1 + + return dHash + + def deserialize(self): + dObject = None + + if self.string is not None and self.length > 0: + currentChar = self.string[self.cursor] + self.cursor = self.cursor + 2 + if currentChar == 'a': + dObject = self.deserializeArray() + elif currentChar == 's': + dObject = self.deserializeString() + elif currentChar == 'i': + dObject = self.deserializeInteger() + elif currentChar == 'b': + dObject = self.deserializeBoolean() + elif currentChar == 'N': + dObject = None + + return dObject diff --git a/Migration/Horde/README b/Migration/Horde/README new file mode 100644 index 000000000..4d04218a8 --- /dev/null +++ b/Migration/Horde/README @@ -0,0 +1,7 @@ +1) copy config.py.in to config.py and personalize the settings +2) personalize the domain setting in signature.py (look for DOMAIN.COM) +3) the scripts require "webdavlib.py", which is located in /Tests/Integration/. Therefore, in order to use the scripts, we must set the environment variable "PYTHONPATH" to that directory on our system. +4) invoke "signature " for importing signatures +5) invoke "turba " for importing signatures +6) "" can have the special "ALL" value + diff --git a/Migration/Horde/TurbaConverter.py b/Migration/Horde/TurbaConverter.py new file mode 100644 index 000000000..cfe32b503 --- /dev/null +++ b/Migration/Horde/TurbaConverter.py @@ -0,0 +1,351 @@ +import PHPDeserializer +import webdavlib +import sys + +commonMappings = { "owner_id": "owner", + "object_id": "filename", + "object_uid": "uid", + "object_name": "fn" } +cardMappings = { "object_alias": "nickname", + "object_email": "email", + "object_homeaddress": "homeaddress", + "object_homephone": "homephone", + "object_workaddress": "workaddress", + "object_workphone": "workphone", + "object_cellphone": "cellphone", + "object_fax": "fax", + "object_title": "title", + "object_company": "org", + "object_notes": "notes", + "object_freebusyurl": "fburl" } + +prodid = "-//Inverse inc.//SOGo Turba Importer 1.0//EN" + +# a managed type of template where each line is put only if at least one field +# has been filled +cardTemplate = u"""BEGIN:VCARD\r +VERSION:3.0\r +PRODID:%s\r +UID:${uid}\r +FN:${fn}\r +TITLE:${title}\r +ORG:${org};\r +NICKNAME:${nickname}\r +EMAIL:${email}\r +ADR;TYPE=work:;;${workaddress};;;;\r +ADR;TYPE=home:;;${homeaddress};;;;\r +TEL;TYPE=work:${workphone}\r +TEL;TYPE=home:${homephone}\r +TEL;TYPE=fax:${fax}\r +NOTE:${notes}\r +FBURL:${fburl}\r +END:VCARD""" % prodid + +class TurbaConverter: + def __init__(self, user, webdavConfig): + self.user = user + self.webdavConfig = webdavConfig + + def start(self, conn): + self.conn = conn + self.readUsers() + self.missing = [] + for user in self.users.keys(): + self.hasCards = False + self.hasLists = False + self.currentUser = user + self.readUserRecords() + if self.hasCards or self.hasLists: + print "Converting addressbook of '%s'" % user + self.prepareCards() + self.uploadCards() + self.prepareLists() + self.uploadLists() + else: + self.missing.append(user) + + if len(self.missing) > 0: + print "No information extracted for: %s" % ", ".join(self.missing) + + print "Done" + + def readUsers(self): + self.users = {} + + cursor = self.conn.cursor() + query = "SELECT user_uid, datatree_name FROM horde_datatree" + if self.user != "ALL": + query = query + " WHERE user_uid = '%s'" % self.user + cursor.execute(query) + + records = cursor.fetchall() + count = 0 + max = len(records) + for record in records: + record_user = record[0].lower() + if not self.users.has_key(record_user): + self.users[record_user] = [] + self.users[record_user].append(record[1]) + count = count + 1 + cursor.close() + + def readUserRecords(self): + self.cards = {} + self.lists = {} + + cursor = self.conn.cursor() + owner_ids = self.users[self.currentUser] + whereClause = "owner_id = '%s'" % "' or owner_id = '".join(owner_ids) + query = "SELECT * FROM turba_objects WHERE %s" % whereClause + cursor.execute(query) + self.prepareColumns(cursor) + + records = cursor.fetchall() + count = 0 + max = len(records) + while count < max: + self.parseRecord(records[count]) + count = count + 1 + + cursor.close() + + def prepareColumns(self, cursor): + self.columns = {} + count = 0 + for dbColumn in cursor.description: + columnId = dbColumn[0] + self.columns[columnId] = count + count = count + 1 + + def parseRecord(self, record): + typeCol = self.columns["object_type"] + meta = {} + self.applyRecordMappings(meta, record, commonMappings) + + if record[typeCol] == "Object": + meta["type"] = "card" + self.hasCards = True + self.applyRecordMappings(meta, record, cardMappings) + elif record[typeCol] == "Group": + meta["type"] = "list" + self.hasLists = True + self.fillListMembers(meta, record) + else: + raise Exception, "UNKNOWN TYPE: %s" % record[type] + + self.dispatchMeta(meta) + + def applyRecordMappings(self, meta, record, mappings): + for k in mappings.keys(): + metaKey = mappings[k] + meta[metaKey] = self.recordColumn(record, k) + + def recordColumn(self, record, columnName): + columnIndex = self.columns[columnName] + value = record[columnIndex] + if value is None: + value = u"" + else: + value = self.deUTF8Ize(value) + + return value + + def deUTF8Ize(self, value): + # unicode -> repeat(utf-8 str -> iso-8859-1 str) -> unicode + oldValue = value + + done = False + while not done: + try: + utf8Value = value.encode("iso-8859-1") + value = utf8Value.decode("utf-8") + except: + done = True + if value == oldValue: + done = True + + return value + + def fillListMembers(self, meta, record): + members = self.recordColumn(record, "object_members") + if members is not None and len(members) > 0: + deserializer = PHPDeserializer.PHPDeserializer(members) + dMembers = deserializer.deserialize() + else: + dMembers = [] + meta["members"] = dMembers + + def dispatchMeta(self, meta): + owner = meta["owner"] + if meta["type"] == "card": + repository = self.cards + else: + repository = self.lists + filename = meta["filename"] + repository[filename] = meta + + def prepareCards(self): + count = 0 + for filename in self.cards.keys(): + card = self.cards[filename] + card["data"] = self.buildVCard(card).encode("utf-8") + count = count + 1 + if count > 0: + print " prepared %d cards" % count + + def buildVCard(self, card): + vcardArray = [] + tmplArray = cardTemplate.split("\r\n") + for line in tmplArray: + keyPos = line.find("${") + if keyPos > -1: + keyEndPos = line.find("}") + key = line[keyPos+2:keyEndPos] + if card.has_key(key): + value = card[key] + if len(value) > 0: + newLine = "%s%s%s" % (line[0:keyPos], + value.replace(";", "\;"), + line[keyEndPos + 1:]) + vcardArray.append(self.foldLineIfNeeded(newLine)) + else: + vcardArray.append(self.foldLineIfNeeded(line)) + + return "\r\n".join(vcardArray) + + def foldLineIfNeeded(self, line): + wasFolded = False + newLine = line\ + .replace("\\", "\\\\") \ + .replace("\r", "\\r") \ + .replace("\n", "\\n") + lines = [] + while len(newLine) > 73: + wasFolded = True + lines.append(newLine[0:73]) + newLine = newLine[73:] + lines.append(newLine) + + newLine = "\r\n ".join(lines) + if wasFolded: + print "line was folded: '%s' ->\n\n%s\n\n" % (line, newLine) + + return newLine + + def uploadCards(self): + self.uploadEntries(self.cards, + "vcf", "text/x-vcard; charset=utf-8"); + + def prepareLists(self): + count = 0 + skipped = 0 + for filename in self.lists.keys(): + list = self.lists[filename] + vlist = self.buildVList(list) + if vlist is None: + skipped = skipped + 1 + else: + list["data"] = vlist.encode("utf-8") + count = count + 1 + + if (count + skipped) > 0: + print " prepared %d lists. %d were skipped." % (count, skipped) + + def buildVList(self, list): + vlist = None + + members = list["members"] + if len(members) > 0: + cardMembers = [] + for member in members: + card = self.getListCard(member) + if card is not None: + cardMembers.append(card) + if len(cardMembers) > 0: + vlist = self.assembleVList(list, cardMembers) + else: + print " list '%s' skipped because of lack of usable" \ + " members" % list["filename"] + + return vlist + + def getListCard(self, cardRef): + card = None + if len(cardRef) != 0 and not cardRef.startswith("localldap:"): + if cardRef.startswith("localsql:"): + cardRef = cardRef[9:] + if self.cards.has_key(cardRef): + card = self.cards[cardRef] + else: + print "card reference does not exist: '%s'" % cardRef + + return card + + def assembleVList(self, list, cardMembers): + entries = [] + for cardMember in cardMembers: + if cardMember.has_key("fn") and len(cardMember["fn"]) > 0: + fn = ";FN=%s" % cardMember["fn"] + else: + fn = "" + if cardMember.has_key("email") and len(cardMember["email"]) > 0: + email = ";EMAIL=%s" % cardMember["email"] + else: + email = "" + entries.append("CARD%s%s:%s.vcf" + % (fn, email, cardMember["filename"])) + if list.has_key("fn") and len(list["fn"]) > 0: + listfn = "FN:%s\r\n" % list["fn"] + else: + listfn = "" + vlist = """BEGIN:VLIST\r +PRODID:%s\r +VERSION:1.0\r +UID:%s\r +%s%s\r +END:VLIST""" % (prodid, list["uid"], listfn, "\r\n".join(entries)) + + return vlist + + def uploadLists(self): + self.uploadEntries(self.lists, + "vlf", "text/x-vcard; charset=utf-8"); + + def uploadEntries(self, entries, extension, mimeType): + isatty = sys.stdout.isatty() # enable progressive display of summary + success = 0 + failure = 0 + client = webdavlib.WebDAVClient(self.webdavConfig["hostname"], + self.webdavConfig["port"], + self.webdavConfig["username"], + self.webdavConfig["password"]) + collection = '/SOGo/dav/%s/Contacts/personal' % self.currentUser + + mkcol = webdavlib.WebDAVMKCOL(collection) + client.execute(mkcol) + + for entryName in entries.keys(): + entry = entries[entryName] + if entry.has_key("data"): + fullFilename = "%s.%s" % (entry["filename"], extension) + url = "%s/%s" % (collection, fullFilename) + put = webdavlib.HTTPPUT(url, entry["data"]) + put.content_type = mimeType + client.execute(put) + if (put.response["status"] < 200 + or put.response["status"] > 399): + failure = failure + 1 + print " error uploading '%s': %d" \ + % (fullFilename, put.response["status"]) + else: + success = success + 1 + if isatty: + print "\r successes: %d; failures: %d" % (success, failure), + if (success + failure) % 5 == 0: + sys.stdout.flush() + if isatty: + print "" + else: + if (success + failure) > 0: + print " successes: %d; failures: %d\n" % (success, failure) + sys.stdout.flush() diff --git a/Migration/Horde/config.py.in b/Migration/Horde/config.py.in new file mode 100644 index 000000000..05aa6b60c --- /dev/null +++ b/Migration/Horde/config.py.in @@ -0,0 +1,11 @@ +#!/usr/bin/python + +webdavConfig = { "hostname": "sogo.hostname.com", + "port": 80, + "username": "username", + "password": "password" } + +dbConfig = { "host": "localhost", + "username": "username", + "password": "password", + "db": "database" } diff --git a/Migration/Horde/signature.py b/Migration/Horde/signature.py new file mode 100755 index 000000000..a8ce024e1 --- /dev/null +++ b/Migration/Horde/signature.py @@ -0,0 +1,48 @@ +#!/usr/bin/python + +import sys +import MySQLdb +import webdavlib + +import HordeSignatureConverter + +from config import webdavConfig, dbConfig + +xmlns_inversedav = "urn:inverse:params:xml:ns:inverse-dav" + +def UploadSignature(client, user, signature): + collection = '/SOGo/dav/%s/' % user + proppatch \ + = webdavlib.WebDAVPROPPATCH(collection, + { "{%s}signature" % xmlns_inversedav: \ + signature.encode("utf-8") }) + client.execute(proppatch) + if (proppatch.response["status"] < 200 + or proppatch.response["status"] > 399): + print "Failure uploading signature for user '%s': %d" \ + % (user, proppatch.response["status"]) + +if __name__ == "__main__": + if len(sys.argv) > 1: + user = sys.argv[1] + else: + raise Exception, " argument must be specified" \ + " (use 'ALL' for everyone)" + + conn = MySQLdb.connect(host = dbConfig["hostname"], + user = dbConfig["username"], + passwd = dbConfig["password"], + db = dbConfig["database"], + use_unicode = True) + + cnv = HordeSignatureConverter.HordeSignatureConverter(user, "DOMAIN.COM") + signatures = cnv.fetchSignatures(conn) + conn.close() + + client = webdavlib.WebDAVClient(webdavConfig["hostname"], + webdavConfig["port"], + webdavConfig["username"], + webdavConfig["password"]) + for user in signatures: + signature = signatures[user] + UploadSignature(client, user, signature) diff --git a/Migration/Horde/turba.py b/Migration/Horde/turba.py new file mode 100755 index 000000000..22247a275 --- /dev/null +++ b/Migration/Horde/turba.py @@ -0,0 +1,24 @@ +#!/usr/bin/python + +import sys +import MySQLdb + +import TurbaConverter + +from config import webdavConfig, dbConfig + +if __name__ == "__main__": + if len(sys.argv) > 1: + user = sys.argv[1] + else: + raise Exception, " argument must be specified" \ + " (use 'ALL' for everyone)" + + conn = MySQLdb.connect(host = dbConfig["hostname"], + user = dbConfig["username"], + passwd = dbConfig["password"], + db = dbConfig["database"], + use_unicode = True) + cnv = TurbaConverter.TurbaConverter(user, webdavConfig) + cnv.start(conn) + conn.close() diff --git a/NEWS b/NEWS index dd6cbeb54..47c5c81f9 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -1.2-2010XXXX (1.2.3) +1.3-20100721 (1.3.0) -------------------- - added support for the "tentative" status in the invitation responses - inviting a group of contacts is now possible, where each contact will be @@ -12,16 +12,22 @@ via the "/SOGo/dav/public" url - we now provide ICS and XML version of a user's personal calendars when accessed from his own "Calendar" base collection -- events are now displayed with the coloured stripe representing their +- events are now displayed with the colored stripe representing their category, if one is defined in the preferences +- fixed display of all-day events in a monthly view where the timezone differs + from the current one - the event location is now displayed in the calendar view when defined properly - added a caching mechanism for freebusy requests, in order to accelerate the display - added the ability to specify a time range when requesting a time slot suggestion +- added live-loading support in the webmail interface with caching support +- updated CKEditor and improved its integration with the current user + language for automatic spell checking support +- added support for displaying photos from contacts - added a Ukrainian translation -- updated the Czeck translation +- updated the Czech translation 1.2-20100504 (1.2.2) -------------------- diff --git a/SOPE/NGCards/ChangeLog b/SOPE/NGCards/ChangeLog index a7eb1a4b5..e03b9a050 100644 --- a/SOPE/NGCards/ChangeLog +++ b/SOPE/NGCards/ChangeLog @@ -1,3 +1,13 @@ +2010-07-21 Wolfgang Sourdeau + + * iCalXMLRenderer.m (_appendPaddingValues:withTag:intoString:): + fixed a typo causing a crash. + +2010-07-16 Wolfgang Sourdeau + + * NGVCardPhoto.[hm]: new class module that implement facilities + for handling "PHOTO" tags in vcards. + 2010-06-08 Wolfgang Sourdeau * iCalXMLRenderer.m (-[CardGroup xmlRender]): don't append empty diff --git a/SOPE/NGCards/GNUmakefile b/SOPE/NGCards/GNUmakefile index 3691f5fff..90f75848a 100644 --- a/SOPE/NGCards/GNUmakefile +++ b/SOPE/NGCards/GNUmakefile @@ -57,6 +57,7 @@ libNGCards_HEADER_FILES = \ \ NGVCard.h \ NGVList.h \ + NGVCardPhoto.h \ NGVCardReference.h \ # NGVCardAddress.h \ # NGVCardStrArrayValue.h \ @@ -110,6 +111,7 @@ libNGCards_OBJC_FILES = \ \ NGVCard.m \ NGVList.m \ + NGVCardPhoto.m \ NGVCardReference.m \ NGCardsSaxHandler.m \ # IcalElements.m diff --git a/SOPE/NGCards/NGVCard.m b/SOPE/NGCards/NGVCard.m index 41cb7c2b9..861ef82a3 100644 --- a/SOPE/NGCards/NGVCard.m +++ b/SOPE/NGCards/NGVCard.m @@ -26,6 +26,8 @@ #import "NSArray+NGCards.h" +#import "NGVCardPhoto.h" + #import "NGVCard.h" @implementation NGVCard @@ -79,6 +81,8 @@ || [classTag isEqualToString: @"TITLE"] || [classTag isEqualToString: @"VERSION"]) tagClass = [CardElement class]; + else if ([classTag isEqualToString: @"PHOTO"]) + tagClass = [NGVCardPhoto class]; else tagClass = [super classForTag: classTag]; diff --git a/SOPE/NGCards/NGVCardPhoto.h b/SOPE/NGCards/NGVCardPhoto.h new file mode 100644 index 000000000..0b586cb52 --- /dev/null +++ b/SOPE/NGCards/NGVCardPhoto.h @@ -0,0 +1,41 @@ +/* NGVCardPhoto.h - this file is part of NGCards + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Wolfgang Sourdeau + * + * NGCards is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * NGCards 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with NGCards; see the file COPYING. If not, write to the + * Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef NGVCARDPHOTO_H +#define NGVCARDPHOTO_H + +#import "CardElement.h" + +@class NSData; +@class NSString; + +@interface NGVCardPhoto : CardElement + +- (BOOL) isInline; + +- (NSString *) type; + +- (NSData *) decodedContent; + +@end + +#endif /* NGVCARDPHOTO_H */ diff --git a/SOPE/NGCards/NGVCardPhoto.m b/SOPE/NGCards/NGVCardPhoto.m new file mode 100644 index 000000000..ce00238ec --- /dev/null +++ b/SOPE/NGCards/NGVCardPhoto.m @@ -0,0 +1,78 @@ +/* NGVCardPhoto.m - this file is part of NGCards + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Wolfgang Sourdeau + * + * NGCards is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * NGCards 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with NGCards; see the file COPYING. If not, write to the + * Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#import +#import + +#import +#import + +#import "NGVCardPhoto.h" + +@implementation NGVCardPhoto + +- (BOOL) isInline +{ + return ![[self value: 0 ofAttribute: @"value"] isEqualToString: @"uri"]; +} + +- (NSString *) type +{ + return [[self value: 0 ofAttribute: @"type"] uppercaseString]; +} + +- (NSData *) decodedContent +{ + NSString *encoding, *value; + NSData *decodedContent; + + decodedContent = nil; + + if ([self isInline]) + { + encoding = [[self value: 0 ofAttribute: @"encoding"] uppercaseString]; + if ([encoding isEqualToString: @"B"] + || [encoding isEqualToString: @"BASE64"]) + { + /* We bypass -[values:] because we want to obtain the undecoded + value first. */ + if ([values count] > 0) + { + value = [values objectAtIndex: 0]; + decodedContent = [value dataByDecodingBase64]; + } + else + [self errorWithFormat: @"attempt to decode empty value"]; + } + else + [self errorWithFormat: + @"decoded content requested with an unknown encoding: '%@'", + encoding]; + } + else + [self errorWithFormat: + @"decoded content requested on a PHOTO of type 'uri'"]; + + return decodedContent; +} + +@end diff --git a/SOPE/NGCards/NSArray+NGCards.m b/SOPE/NGCards/NSArray+NGCards.m index ffbb9b92d..edda2f7a7 100644 --- a/SOPE/NGCards/NSArray+NGCards.m +++ b/SOPE/NGCards/NSArray+NGCards.m @@ -67,17 +67,14 @@ cmpTag = [aTag uppercaseString]; - matchingElements = [NSMutableArray new]; - [matchingElements autorelease]; + matchingElements = [NSMutableArray arrayWithCapacity: 16]; allElements = [self objectEnumerator]; - currentElement = [allElements nextObject]; - while (currentElement) + while ((currentElement = [allElements nextObject])) { currentTag = [[currentElement tag] uppercaseString]; if ([currentTag isEqualToString: cmpTag]) [matchingElements addObject: currentElement]; - currentElement = [allElements nextObject]; } return matchingElements; @@ -92,16 +89,12 @@ allElements = [self objectEnumerator]; - matchingElements = [NSMutableArray new]; - [matchingElements autorelease]; - currentElement = [allElements nextObject]; - while (currentElement) - { - if ([currentElement hasAttribute: anAttribute - havingValue: aValue]) - [matchingElements addObject: currentElement]; - currentElement = [allElements nextObject]; - } + matchingElements = [NSMutableArray arrayWithCapacity: 16]; + + while ((currentElement = [allElements nextObject])) + if ([currentElement hasAttribute: anAttribute + havingValue: aValue]) + [matchingElements addObject: currentElement]; return matchingElements; } diff --git a/SOPE/NGCards/iCalXMLRenderer.m b/SOPE/NGCards/iCalXMLRenderer.m index 530e582c7..c51c9ec9a 100644 --- a/SOPE/NGCards/iCalXMLRenderer.m +++ b/SOPE/NGCards/iCalXMLRenderer.m @@ -96,7 +96,7 @@ int count; for (count = 0; count < max; count++) - [rendering appendFormat: @"<%@/>"]; + [rendering appendFormat: @"<%@/>", valueTag]; } - (NSString *) _xmlRenderParameter: (NSString *) paramName diff --git a/SOPE/sope-patchset-r1664.diff b/SOPE/sope-patchset-r1664.diff index dd6e4e7b9..ab76f055a 100644 --- a/SOPE/sope-patchset-r1664.diff +++ b/SOPE/sope-patchset-r1664.diff @@ -5638,7 +5638,15 @@ Index: sope-core/NGExtensions/ChangeLog =================================================================== --- sope-core/NGExtensions/ChangeLog (revision 1664) +++ sope-core/NGExtensions/ChangeLog (working copy) -@@ -1,3 +1,8 @@ +@@ -1,3 +1,16 @@ ++2010-07-16 Wolfgang Sourdeau ++ ++ * NGBase64Coding.m (-dataByDecodingBase64) ++ (-stringByDecodingBase64, -stringByEncodingBase64): make use of ++ -[NSString lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding] ++ rather than -[... cStringLength] to avoid a crash within GNUstep. ++ The latter is deprecated anyway... ++ +2010-01-30 Wolfgang Sourdeau + + * NGRuleEngine.subproj/NGRuleModel.m (-candidateRulesForKey:): @@ -5647,6 +5655,40 @@ Index: sope-core/NGExtensions/ChangeLog 2009-03-24 Wolfgang Sourdeau * NGQuotedPrintableCoding.m: encode '_' as '=5F', so that it is not +Index: sope-core/NGExtensions/NGBase64Coding.m +=================================================================== +--- sope-core/NGExtensions/NGBase64Coding.m (revision 1664) ++++ sope-core/NGExtensions/NGBase64Coding.m (working copy) +@@ -53,7 +53,7 @@ + size_t destLength = -1; + char *dest, *src; + +- if ((len = [self cStringLength]) == 0) ++ if ((len = [self lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]) == 0) + return @""; + + destSize = (len + 2) / 3 * 4; // 3:4 conversion ratio +@@ -91,7 +91,7 @@ + + if (StringClass == Nil) StringClass = [NSString class]; + +- if ((len = [self cStringLength]) == 0) ++ if ((len = [self lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]) == 0) + return @""; + + destSize = (len / 4 + 1) * 3 + 1; +@@ -135,9 +135,9 @@ + + if (StringClass == Nil) StringClass = [NSString class]; + +- if ((len = [self cStringLength]) == 0) ++ if ((len = [self lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]) == 0) + return [NSData data]; +- ++ + destSize = (len / 4 + 1) * 3 + 1; + dest = malloc(destSize + 1); + Index: sope-core/NGExtensions/NGRuleEngine.subproj/NGRuleModel.m =================================================================== --- sope-core/NGExtensions/NGRuleEngine.subproj/NGRuleModel.m (revision 1664) diff --git a/SoObjects/Appointments/SOGoAppointmentFolders.m b/SoObjects/Appointments/SOGoAppointmentFolders.m index 7e861f424..dcacacbf5 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolders.m +++ b/SoObjects/Appointments/SOGoAppointmentFolders.m @@ -412,32 +412,6 @@ static SoSecurityManager *sm = nil; return componentSet; } -- (void) reloadWebCalendars: (BOOL) forceReload -{ - NSArray *refs; - SOGoWebAppointmentFolder *folder; - SOGoUserSettings *us; - NSDictionary *calSettings; - NSString *ref; - int count, max; - - [self _migrateWebCalendarsSettings]; - us = [[SOGoUser userWithLogin: owner] userSettings]; - calSettings = [us objectForKey: @"Calendar"]; - refs = [[calSettings objectForKey: @"WebCalendars"] allKeys]; - max = [refs count]; - for (count = 0; count < max; count++) - { - ref = [refs objectAtIndex: count]; - folder = [SOGoWebAppointmentFolder - folderWithSubscriptionReference: ref - inContainer: self]; - if (folder - && (forceReload || [folder reloadOnLogin])) - [folder loadWebCalendar]; - } -} - - (void) _migrateWebCalendarsSettings { SOGoUserSettings *us; @@ -474,6 +448,32 @@ static SoSecurityManager *sm = nil; [us synchronize]; } +- (void) reloadWebCalendars: (BOOL) forceReload +{ + NSArray *refs; + SOGoWebAppointmentFolder *folder; + SOGoUserSettings *us; + NSDictionary *calSettings; + NSString *ref; + int count, max; + + [self _migrateWebCalendarsSettings]; + us = [[SOGoUser userWithLogin: owner] userSettings]; + calSettings = [us objectForKey: @"Calendar"]; + refs = [[calSettings objectForKey: @"WebCalendars"] allKeys]; + max = [refs count]; + for (count = 0; count < max; count++) + { + ref = [refs objectAtIndex: count]; + folder = [SOGoWebAppointmentFolder + folderWithSubscriptionReference: ref + inContainer: self]; + if (folder + && (forceReload || [folder reloadOnLogin])) + [folder loadWebCalendar]; + } +} + - (NSException *) _fetchPersonalFolders: (NSString *) sql withChannel: (EOAdaptorChannel *) fc { diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 5e43eafc7..2e775624a 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -328,17 +328,56 @@ } } -- (void) _requireResponseFromAttendees: (NSArray *) attendees +- (void) _removeDelegationChain: (iCalPerson *) delegate + inEvent: (iCalEvent *) event { - NSEnumerator *enumerator; - iCalPerson *currentAttendee; + NSString *delegatedTo, *mailTo; - enumerator = [attendees objectEnumerator]; - while ((currentAttendee = [enumerator nextObject])) + delegatedTo = [delegate delegatedTo]; + if ([delegatedTo length] > 0) { + mailTo = [delegatedTo rfc822Email]; + delegate = [event findAttendeeWithEmail: mailTo]; + if (delegate) + { + [self _removeDelegationChain: delegate + inEvent: event]; + [event removeFromAttendees: delegate]; + } + else + [self errorWithFormat: + @"broken chain: delegate with email '%@' was not found", + mailTo]; + } +} + +/* This method returns YES when any attendee has been removed and NO + otherwise. */ +- (BOOL) _requireResponseFromAttendees: (iCalEvent *) event +{ + NSArray *attendees; + iCalPerson *currentAttendee; + BOOL listHasChanged; + int count, max; + + attendees = [event attendees]; + max = [attendees count]; + + for (count = 0; count < max; count++) + { + currentAttendee = [attendees objectAtIndex: count]; + if ([[currentAttendee delegatedTo] length] > 0) + { + [self _removeDelegationChain: currentAttendee + inEvent: event]; + [currentAttendee setDelegatedTo: nil]; + listHasChanged = YES; + } [currentAttendee setRsvp: @"TRUE"]; [currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction]; } + + return listHasChanged; } - (void) _handleSequenceUpdateInEvent: (iCalEvent *) newEvent @@ -400,6 +439,13 @@ iCalEventChanges *changes; changes = [newEvent getChangesRelativeToEvent: oldEvent]; + if ([changes sequenceShouldBeIncreased]) + { + // Set new attendees status to "needs action" and recompute changes when + // the list of attendees has changed. + if ([self _requireResponseFromAttendees: newEvent]) + changes = [newEvent getChangesRelativeToEvent: oldEvent]; + } attendees = [changes deletedAttendees]; if ([attendees count]) { @@ -417,8 +463,6 @@ if ([changes sequenceShouldBeIncreased]) { [newEvent increaseSequence]; - // Set new attendees status to "needs action" - [self _requireResponseFromAttendees: [newEvent attendees]]; // Update attendees calendars and send them an update // notification by email [self _handleSequenceUpdateInEvent: newEvent @@ -455,10 +499,6 @@ if ([attendees count]) { - NSArray *originalAttendees; - - originalAttendees = [NSArray arrayWithArray: [newEvent attendees]]; - // Send an invitation to new attendees [self _handleAddedUsers: attendees fromEvent: newEvent]; [self sendEMailUsingTemplateNamed: @"Invitation" diff --git a/SoObjects/Contacts/GNUmakefile b/SoObjects/Contacts/GNUmakefile index d1d357e47..cbbfa2e0a 100644 --- a/SoObjects/Contacts/GNUmakefile +++ b/SoObjects/Contacts/GNUmakefile @@ -19,6 +19,7 @@ Contacts_OBJC_FILES = \ SOGoContactLDIFEntry.m \ SOGoContactSourceFolder.m \ SOGoUserFolder+Contacts.m \ + SOGoContactEntryPhoto.m \ Contacts_RESOURCE_FILES += \ Version \ diff --git a/SoObjects/Contacts/SOGoContactEntryPhoto.h b/SoObjects/Contacts/SOGoContactEntryPhoto.h new file mode 100644 index 000000000..e35eb2cd7 --- /dev/null +++ b/SoObjects/Contacts/SOGoContactEntryPhoto.h @@ -0,0 +1,42 @@ +/* SOGoContactEntryPhoto.h - this file is part of SOGo + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Wolfgang Sourdeau + * + * 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 SOGOCONTACTENTRYPHOTO_H +#define SOGOCONTACTENTRYPHOTO_H + +#import + +@interface SOGoContactEntryPhoto : SOGoObject +{ + int photoID; +} + ++ (id) entryPhotoWithID: (int) photoId + inContainer: (id) container; + +- (void) setPhotoID: (int) newPhotoID; + +- (NSString *) davContentType; + +@end + +#endif /* SOGOCONTACTENTRYPHOTO_H */ diff --git a/SoObjects/Contacts/SOGoContactEntryPhoto.m b/SoObjects/Contacts/SOGoContactEntryPhoto.m new file mode 100644 index 000000000..850ce59f5 --- /dev/null +++ b/SoObjects/Contacts/SOGoContactEntryPhoto.m @@ -0,0 +1,114 @@ +/* SOGoContactEntryPhoto.m - this file is part of SOGo + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Wolfgang Sourdeau + * + * 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 +#import + +#import +#import + +#import +#import + +#import "SOGoContactObject.h" + +#import "SOGoContactEntryPhoto.h" + +@implementation SOGoContactEntryPhoto + ++ (id) entryPhotoWithID: (int) photoID + inContainer: (id) container +{ + id photo; + + photo + = [super objectWithName: [NSString stringWithFormat: @"photo%d", photoID] + inContainer: container]; + [photo setPhotoID: photoID]; + + return photo; +} + +- (void) setPhotoID: (int) newPhotoID +{ + photoID = newPhotoID; +} + +- (NGVCardPhoto *) photo +{ + NGVCardPhoto *photo; + NSArray *photoElements; + + photoElements = [[container vCard] childrenWithTag: @"photo"]; + if ([photoElements count] > photoID) + photo = [photoElements objectAtIndex: photoID]; + else + photo = nil; + + return photo; +} + +- (id) GETAction: (WOContext *) localContext +{ + NGVCardPhoto *photo; + NSData *data; + id response; + + photo = [self photo]; + if ([photo isInline]) + data = [photo decodedContent]; + else + data = [[photo value: 0] dataUsingEncoding: NSISOLatin1StringEncoding]; + if (data) + { + response = [localContext response]; + + [response setHeader: [self davContentType] forKey: @"content-type"]; + [response setHeader: [NSString stringWithFormat:@" %d", + [data length]] + forKey: @"content-length"]; + [response setContent: data]; + } + else + response = nil; + + return response; +} + +- (NSString *) davContentType +{ + NGVCardPhoto *photo; + NSString *type, *contentType; + + photo = [self photo]; + if ([photo isInline]) + { + type = [[photo type] lowercaseString]; + contentType = [NSString stringWithFormat: @"image/%@", type]; + } + else + contentType = @"text/plain"; + + return contentType; +} + +@end diff --git a/SoObjects/Contacts/SOGoContactGCSEntry.m b/SoObjects/Contacts/SOGoContactGCSEntry.m index 73bca8969..22ee70bc0 100644 --- a/SoObjects/Contacts/SOGoContactGCSEntry.m +++ b/SoObjects/Contacts/SOGoContactGCSEntry.m @@ -24,6 +24,8 @@ #import +#import "SOGoContactEntryPhoto.h" + #import "SOGoContactGCSEntry.h" @implementation SOGoContactGCSEntry @@ -62,6 +64,31 @@ /* actions */ +- (id) lookupName: (NSString *) lookupName + inContext: (id) localContext + acquire: (BOOL) acquire +{ + id obj; + int photoIndex; + NSArray *photoElements; + + if ([lookupName hasPrefix: @"photo"]) + { + photoElements = [[self vCard] childrenWithTag: @"photo"]; + photoIndex = [[lookupName substringFromIndex: 5] intValue]; + if (photoIndex > -1 && photoIndex < [photoElements count]) + obj = [SOGoContactEntryPhoto entryPhotoWithID: photoIndex + inContainer: self]; + else + obj = nil; + } + else + obj = [super lookupName: lookupName inContext: localContext + acquire: acquire]; + + return obj; +} + - (NSException *) copyToFolder: (SOGoGCSFolder *) newFolder { NGVCard *newCard; diff --git a/SoObjects/SOGo/SOGoContentObject.m b/SoObjects/SOGo/SOGoContentObject.m index 7ce7cc61a..e60b23e85 100644 --- a/SoObjects/SOGo/SOGoContentObject.m +++ b/SoObjects/SOGo/SOGoContentObject.m @@ -372,7 +372,7 @@ - (NSString *) davContentLength { return [NSString stringWithFormat: @"%u", - [content lengthOfBytesUsingEncoding: NSUTF8StringEncoding]]; + [content lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]]; } // - (NSString *) davResourceType diff --git a/SoObjects/SOGo/SOGoUserFolder.m b/SoObjects/SOGo/SOGoUserFolder.m index e029e2c94..072ebb7b0 100644 --- a/SoObjects/SOGo/SOGoUserFolder.m +++ b/SoObjects/SOGo/SOGoUserFolder.m @@ -27,6 +27,7 @@ #import #import +#import #import #import #import @@ -147,18 +148,27 @@ NSMutableArray *folders; NSEnumerator *subfolders; SOGoFolder *currentFolder; - NSString *folderName; + NSString *folderName, *folderOwner; Class subfolderClass; NSMutableDictionary *currentDictionary; + SoSecurityManager *securityManager; folders = [NSMutableArray array]; + folderOwner = [parentFolder ownerInContext: context]; + securityManager = [SoSecurityManager sharedSecurityManager]; + subfolderClass = [[parentFolder class] subFolderClass]; subfolders = [[parentFolder subFolders] objectEnumerator]; while ((currentFolder = [subfolders nextObject])) { - if ([currentFolder isMemberOfClass: subfolderClass]) + if (![securityManager validatePermission: SOGoPerm_AccessObject + onObject: currentFolder + inContext: context] + && [[currentFolder ownerInContext: context] + isEqualToString: folderOwner] + && [currentFolder isMemberOfClass: subfolderClass]) { folderName = [NSString stringWithFormat: @"/%@/%@", [parentFolder nameInContainer], diff --git a/UI/Common/UIxFolderActions.m b/UI/Common/UIxFolderActions.m index 7878f19cf..e8e0fe5bc 100644 --- a/UI/Common/UIxFolderActions.m +++ b/UI/Common/UIxFolderActions.m @@ -1,6 +1,6 @@ /* UIxFolderActions.m - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/UI/Contacts/BrazilianPortuguese.lproj/Localizable.strings b/UI/Contacts/BrazilianPortuguese.lproj/Localizable.strings index a92fd2183..6391d8dbb 100644 --- a/UI/Contacts/BrazilianPortuguese.lproj/Localizable.strings +++ b/UI/Contacts/BrazilianPortuguese.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contact"; "Address" = "Address"; +"Photos" = "Photos"; "Other" = "Other"; "Address Books" = "Addressbooks"; diff --git a/UI/Contacts/Czech.lproj/Localizable.strings b/UI/Contacts/Czech.lproj/Localizable.strings index d255bcd6c..e48aded25 100644 --- a/UI/Contacts/Czech.lproj/Localizable.strings +++ b/UI/Contacts/Czech.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Kontakt"; "Address" = "Adresa"; +"Photos" = "Photos"; "Other" = "Ostatní"; "Address Books" = "Složky kontaktů"; diff --git a/UI/Contacts/Dutch.lproj/Localizable.strings b/UI/Contacts/Dutch.lproj/Localizable.strings index 443ba52e1..91c107c8f 100644 --- a/UI/Contacts/Dutch.lproj/Localizable.strings +++ b/UI/Contacts/Dutch.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contactpersoon"; "Address" = "Adres"; +"Photos" = "Fotos"; "Other" = "Overige"; "Address Books" = "Addressbooks"; diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 21f365e0d..a7eb56eff 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contact"; "Address" = "Address"; +"Photos" = "Photos"; "Other" = "Other"; "Address Books" = "Address Books"; diff --git a/UI/Contacts/French.lproj/Localizable.strings b/UI/Contacts/French.lproj/Localizable.strings index 54638c801..57127b264 100644 --- a/UI/Contacts/French.lproj/Localizable.strings +++ b/UI/Contacts/French.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contact"; "Address" = "Adresses"; +"Photos" = "Photos"; "Other" = "Informations complémentaires"; "Address Books" = "Carnet d'adresses"; diff --git a/UI/Contacts/German.lproj/Localizable.strings b/UI/Contacts/German.lproj/Localizable.strings index 577a689c8..206cf5a66 100644 --- a/UI/Contacts/German.lproj/Localizable.strings +++ b/UI/Contacts/German.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Kontakt"; "Address" = "Adresse"; +"Photos" = "Fotos"; "Other" = "Sonstiges"; "Address Books" = "Adressbücher"; diff --git a/UI/Contacts/Hungarian.lproj/Localizable.strings b/UI/Contacts/Hungarian.lproj/Localizable.strings index 9f50e784e..c5a87aaca 100644 --- a/UI/Contacts/Hungarian.lproj/Localizable.strings +++ b/UI/Contacts/Hungarian.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contact"; "Address" = "Address"; +"Photos" = "Photos"; "Other" = "Other"; "Address Books" = "Addressbooks"; diff --git a/UI/Contacts/Italian.lproj/Localizable.strings b/UI/Contacts/Italian.lproj/Localizable.strings index 9af75b49c..16e3d74bd 100644 --- a/UI/Contacts/Italian.lproj/Localizable.strings +++ b/UI/Contacts/Italian.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contatto"; "Address" = "Indirizzo"; +"Photos" = "Photos"; "Other" = "Altro"; "Address Books" = "Rubrica"; diff --git a/UI/Contacts/Russian.lproj/Localizable.strings b/UI/Contacts/Russian.lproj/Localizable.strings index d3a3de694..41e431a1c 100644 --- a/UI/Contacts/Russian.lproj/Localizable.strings +++ b/UI/Contacts/Russian.lproj/Localizable.strings @@ -1,5 +1,10 @@ /* this file is in UTF-8 format! */ +"Contact" = "Contact"; +"Address" = "Address"; +"Photos" = "Photos"; +"Other" = "Other"; + "Addressbook" = "Адресная книга"; "Addresses" = "Адреса"; "Update" = "Обновить"; diff --git a/UI/Contacts/Spanish.lproj/Localizable.strings b/UI/Contacts/Spanish.lproj/Localizable.strings index 63f440265..ec4787009 100644 --- a/UI/Contacts/Spanish.lproj/Localizable.strings +++ b/UI/Contacts/Spanish.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Contacto"; "Address" = "Dirección"; +"Photos" = "Photos"; "Other" = "Otros datos"; "Address Books" = "Libretas de direcciones"; diff --git a/UI/Contacts/Swedish.lproj/Localizable.strings b/UI/Contacts/Swedish.lproj/Localizable.strings index 2e229fd7d..355a7fc5d 100644 --- a/UI/Contacts/Swedish.lproj/Localizable.strings +++ b/UI/Contacts/Swedish.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Kontakt"; "Address" = "Adress"; +"Photos" = "Photos"; "Other" = "Annat"; "Address Books" = "Adressböcker"; diff --git a/UI/Contacts/UIxContactEditor.h b/UI/Contacts/UIxContactEditor.h index 0d6269818..c66acdc49 100644 --- a/UI/Contacts/UIxContactEditor.h +++ b/UI/Contacts/UIxContactEditor.h @@ -37,6 +37,7 @@ NSString *preferredEmail; NSString *item; NGVCard *card; + NSMutableArray *photosURL; NSMutableDictionary *snapshot; /* contains the values for editing */ SOGoContactFolder *componentAddressBook; } diff --git a/UI/Contacts/UIxContactEditor.m b/UI/Contacts/UIxContactEditor.m index 8702f268d..43dbd4734 100644 --- a/UI/Contacts/UIxContactEditor.m +++ b/UI/Contacts/UIxContactEditor.m @@ -22,6 +22,7 @@ #import #import +#import #import #import @@ -33,6 +34,7 @@ #import #import +#import #import #import @@ -51,6 +53,7 @@ { snapshot = [[NSMutableDictionary alloc] initWithCapacity: 16]; preferredEmail = nil; + photosURL = nil; } return self; @@ -60,6 +63,7 @@ { [snapshot release]; [preferredEmail release]; + [photosURL release]; [super dealloc]; } @@ -282,6 +286,8 @@ if (![ce hasAttribute: @"type" havingValue: aTypeToExclude]) break; + + value = nil; } } @@ -412,7 +418,7 @@ [self _setSnapshotValue: @"telephoneNumber" to: [self _simpleValueForType: @"work" inArray: elements excluding: @"fax"]]; [self _setSnapshotValue: @"homeTelephoneNumber" - to: [self _simpleValueForType: @"home" inArray: elements excluding: nil]]; + to: [self _simpleValueForType: @"home" inArray: elements excluding: @"fax"]]; [self _setSnapshotValue: @"mobile" to: [self _simpleValueForType: @"cell" inArray: elements excluding: nil]]; [self _setSnapshotValue: @"facsimileTelephoneNumber" @@ -571,6 +577,36 @@ && [super canCreateOrModify]); } +- (NSArray *) photosURL +{ + NSArray *photoElements; + NSURL *soURL; + NSString *baseInlineURL, *photoURL; + NGVCardPhoto *photo; + int count, max; + + if (!photosURL) + { + soURL = [[self clientObject] soURL]; + baseInlineURL = [soURL absoluteString]; + photoElements = [card childrenWithTag: @"photo"]; + max = [photoElements count]; + photosURL = [[NSMutableArray alloc] initWithCapacity: max]; + for (count = 0; count < max; count++) + { + photo = [photoElements objectAtIndex: count]; + if ([photo isInline]) + photoURL = [NSString stringWithFormat: @"%@/photo%d", + baseInlineURL, count]; + else + photoURL = [photo value: 0]; + [photosURL addObject: photoURL]; + } + } + + return photosURL; +} + - (CardElement *) _elementWithTag: (NSString *) tag ofType: (NSString *) type { diff --git a/UI/Contacts/UIxContactFoldersView.m b/UI/Contacts/UIxContactFoldersView.m index 525a7044a..040b6b207 100644 --- a/UI/Contacts/UIxContactFoldersView.m +++ b/UI/Contacts/UIxContactFoldersView.m @@ -1,6 +1,6 @@ /* UIxContactFoldersView.m - this file is part of SOGo * - * Copyright (C) 2006-2009 Inverse inc. + * Copyright (C) 2006-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -246,42 +246,6 @@ return result; } -- (NSArray *) _subFoldersFromFolder: (SOGoParentFolder *) parentFolder -{ - NSMutableArray *folders; - NSEnumerator *subfolders; - SOGoGCSFolder *subfolder; - NSString *folderName; - NSMutableDictionary *currentDictionary; - SoSecurityManager *securityManager; - - securityManager = [SoSecurityManager sharedSecurityManager]; - - folders = [NSMutableArray array]; - - subfolders = [[parentFolder subFolders] objectEnumerator]; - while ((subfolder = [subfolders nextObject])) - { - if (![securityManager validatePermission: SOGoPerm_AccessObject - onObject: subfolder inContext: context]) - { - folderName = [NSString stringWithFormat: @"/%@/%@", - [parentFolder nameInContainer], - [subfolder nameInContainer]]; - currentDictionary - = [NSMutableDictionary dictionaryWithCapacity: 3]; - [currentDictionary setObject: [subfolder displayName] - forKey: @"displayName"]; - [currentDictionary setObject: folderName forKey: @"name"]; - [currentDictionary setObject: [subfolder folderType] - forKey: @"type"]; - [folders addObject: currentDictionary]; - } - } - - return folders; -} - - (void) checkDefaultModulePreference { SOGoUserDefaults *ud; diff --git a/UI/Contacts/UIxContactView.h b/UI/Contacts/UIxContactView.h index e617b43ad..9f59c49b0 100644 --- a/UI/Contacts/UIxContactView.h +++ b/UI/Contacts/UIxContactView.h @@ -32,6 +32,7 @@ NSArray *phones; CardElement *homeAdr; CardElement *workAdr; + NSMutableArray *photosURL; } - (NSString *) fullName; diff --git a/UI/Contacts/UIxContactView.m b/UI/Contacts/UIxContactView.m index dde492ca3..4a93c16ab 100644 --- a/UI/Contacts/UIxContactView.m +++ b/UI/Contacts/UIxContactView.m @@ -20,30 +20,40 @@ 02111-1307, USA. */ +#import + #import #import #import +#import #import #import #import -#import +#import #import "UIxContactView.h" @implementation UIxContactView -/* accessors */ +- (id) init +{ + if ((self = [super init])) + { + photosURL = nil; + } -- (NSString *)tabSelection { - NSString *selection; - - selection = [self queryParameterForKey:@"tab"]; - if (selection == nil) - selection = @"attributes"; - return selection; + return self; } +- (void) dealloc +{ + [photosURL release]; + [super dealloc]; +} + +/* accessors */ + - (NSString *) _cardStringWithLabel: (NSString *) label value: (NSString *) value { @@ -139,15 +149,29 @@ // We might not have a preferred item but rather something like this: // EMAIL;TYPE=work:dd@ee.com // EMAIL;TYPE=home:ff@gg.com - // In this case, we always return the last entry. + // + // or: + // + // EMAIL;TYPE=INTERNET:a@a.com + // EMAIL;TYPE=INTERNET,HOME:b@b.com + // + // In this case, we always return the entry NOT matching the primaryEmail if ([emails count] > 0) { - email = [[emails objectAtIndex: [emails count]-1] value: 0]; + int i; - if ([email caseInsensitiveCompare: [card preferredEMail]] != NSOrderedSame) - mailTo = [NSString stringWithFormat: @"');\">" - @"%@", email, [[card fn] stringByReplacingString: @"\"" withString: @""], email, email]; + for (i = 0; i < [emails count]; i++) + { + email = [[emails objectAtIndex: i] value: 0]; + + if ([email caseInsensitiveCompare: [card preferredEMail]] != NSOrderedSame) + { + mailTo = [NSString stringWithFormat: @"');\">" + @"%@", email, [[card fn] stringByReplacingString: @"\"" withString: @""], email, email]; + break; + } + } } return [self _cardStringWithLabel: @"Additional Email:" @@ -214,6 +238,8 @@ if (![ce hasAttribute: @"type" havingValue: aTypeToExclude]) break; + + phone = nil; } } @@ -229,7 +255,7 @@ - (NSString *) homePhone { - return [self _phoneOfType: @"home" withLabel: @"Home:" excluding: nil]; + return [self _phoneOfType: @"home" withLabel: @"Home:" excluding: @"fax"]; } - (NSString *) fax @@ -614,4 +640,34 @@ return self; } +- (NSArray *) photosURL +{ + NSArray *photoElements; + NSURL *soURL; + NSString *baseInlineURL, *photoURL; + NGVCardPhoto *photo; + int count, max; + + if (!photosURL) + { + soURL = [[self clientObject] soURL]; + baseInlineURL = [soURL absoluteString]; + photoElements = [card childrenWithTag: @"photo"]; + max = [photoElements count]; + photosURL = [[NSMutableArray alloc] initWithCapacity: max]; + for (count = 0; count < max; count++) + { + photo = [photoElements objectAtIndex: count]; + if ([photo isInline]) + photoURL = [NSString stringWithFormat: @"%@/photo%d", + baseInlineURL, count]; + else + photoURL = [photo value: 0]; + [photosURL addObject: photoURL]; + } + } + + return photosURL; +} + @end /* UIxContactView */ diff --git a/UI/Contacts/UIxContactsUserFolders.m b/UI/Contacts/UIxContactsUserFolders.m index a7485b210..1ff941edf 100644 --- a/UI/Contacts/UIxContactsUserFolders.m +++ b/UI/Contacts/UIxContactsUserFolders.m @@ -1,6 +1,6 @@ /* UIxContactsUserFolders.m - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/UI/Contacts/Ukrainian.lproj/Localizable.strings b/UI/Contacts/Ukrainian.lproj/Localizable.strings index fd713fbb5..e6a20b400 100644 --- a/UI/Contacts/Ukrainian.lproj/Localizable.strings +++ b/UI/Contacts/Ukrainian.lproj/Localizable.strings @@ -2,6 +2,7 @@ "Contact" = "Контакт"; "Address" = "Адреса"; +"Photos" = "Photos"; "Other" = "Інше"; "Address Books" = "Адресні книги"; diff --git a/UI/Contacts/Welsh.lproj/Localizable.strings b/UI/Contacts/Welsh.lproj/Localizable.strings index 61cb476e6..c22616c2c 100644 --- a/UI/Contacts/Welsh.lproj/Localizable.strings +++ b/UI/Contacts/Welsh.lproj/Localizable.strings @@ -1,5 +1,10 @@ /* this file is in UTF-8 format! */ +"Contact" = "Contact"; +"Address" = "Address"; +"Photos" = "Photos"; +"Other" = "Other"; + "Addressbook" = "Llyfr cyfeiriadau"; "Addresses" = "Cyfeiriadau"; "Update" = "Diweddaru"; diff --git a/UI/MailPartViewers/French.lproj/Localizable.strings b/UI/MailPartViewers/French.lproj/Localizable.strings index bbd27b084..3baf88240 100644 --- a/UI/MailPartViewers/French.lproj/Localizable.strings +++ b/UI/MailPartViewers/French.lproj/Localizable.strings @@ -22,7 +22,7 @@ request_info = "vous invite à une réunion."; "Update status" = "Intégrer les modifications"; Accept = "Accepter"; Decline = "Decliner"; -Tentative = "Tentative"; +Tentative = "Tentatif"; "Delegate ..." = "Déléguer ..."; "Delegated to" = "Délégué à"; "Update status in calendar" = "Mettre l'agenda à jour"; diff --git a/UI/MainUI/Version b/UI/MainUI/Version index 73946baab..051efa3c5 100644 --- a/UI/MainUI/Version +++ b/UI/MainUI/Version @@ -1,5 +1,5 @@ # Version file -SUBMINOR_VERSION:=2 +SUBMINOR_VERSION:=0 # v0.9.1 requires Main v0.9.59 diff --git a/UI/Scheduler/French.lproj/Localizable.strings b/UI/Scheduler/French.lproj/Localizable.strings index 210a9ea2a..1b461afb4 100644 --- a/UI/Scheduler/French.lproj/Localizable.strings +++ b/UI/Scheduler/French.lproj/Localizable.strings @@ -318,7 +318,7 @@ /* status type */ "status_" = "Non-spécifié"; "status_NOT-SPECIFIED" = "Non spécifié"; -"status_TENTATIVE" = "Tentative"; +"status_TENTATIVE" = "Tentatif"; "status_CONFIRMED" = "Confirmé"; "status_CANCELLED" = "Annulé"; "status_NEEDS-ACTION" = "En attente"; @@ -470,7 +470,7 @@ validate_endbeforestart = "La date de fin est avant la date de début."; "Needs action" = "En attente"; "Accepted" = "Accepté"; "Declined" = "Décliné"; -"Tentative" = "Tentative"; +"Tentative" = "Tentatif"; "Free" = "Libre"; "Busy" = "Occupé"; diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index 3329bb799..8e4df8e5d 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -256,23 +256,22 @@ static NSArray *tasksFields = nil; static NSString *fields[] = { @"startDate", @"c_startdate", @"endDate", @"c_enddate" }; - if (dayBasedView) - for (count = 0; count < 2; count++) - { - aDateField = fields[count * 2]; - aDate = [aRecord objectForKey: aDateField]; - daylightOffset = (int) ([userTimeZone secondsFromGMTForDate: aDate] - - [userTimeZone secondsFromGMTForDate: startDate]); - if (daylightOffset) - { - aDate = [aDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 - minutes: 0 seconds: daylightOffset]; - [aRecord setObject: aDate forKey: aDateField]; - aDateValue = [NSNumber numberWithInt: [aDate timeIntervalSince1970]]; - [aRecord setObject: aDateValue forKey: fields[count * 2 + 1]]; - } - } - + for (count = 0; count < 2; count++) + { + aDateField = fields[count * 2]; + aDate = [aRecord objectForKey: aDateField]; + daylightOffset = (int) ([userTimeZone secondsFromGMTForDate: aDate] + - [userTimeZone secondsFromGMTForDate: startDate]); + if (daylightOffset) + { + aDate = [aDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 + minutes: 0 seconds: daylightOffset]; + [aRecord setObject: aDate forKey: aDateField]; + aDateValue = [NSNumber numberWithInt: [aDate timeIntervalSince1970]]; + [aRecord setObject: aDateValue forKey: fields[count * 2 + 1]]; + } + } + aDateValue = [aRecord objectForKey: @"c_recurrence_id"]; aDate = [aRecord objectForKey: @"cycleStartDate"]; if (aDateValue && aDate) diff --git a/UI/Templates/ContactsUI/UIxContactEditor.wox b/UI/Templates/ContactsUI/UIxContactEditor.wox index ff2b535f5..91360ae76 100644 --- a/UI/Templates/ContactsUI/UIxContactEditor.wox +++ b/UI/Templates/ContactsUI/UIxContactEditor.wox @@ -29,6 +29,8 @@
  • +
  • +
  • @@ -332,6 +334,12 @@ +
    + +
    +
    +
    diff --git a/UI/Templates/ContactsUI/UIxContactView.wox b/UI/Templates/ContactsUI/UIxContactView.wox index 9584b86de..3fe014d9c 100644 --- a/UI/Templates/ContactsUI/UIxContactView.wox +++ b/UI/Templates/ContactsUI/UIxContactView.wox @@ -18,8 +18,10 @@ />
    +